Beancount 拥有一套强大的、类 SQL 的查询语言(BQL),让你能够精准地切分、组合和分析你的财务数据。无论你是想生成一份快速报告、调试一笔分录,还是执行复杂分析,掌握 BQL 都是释放纯文本记账账本全部潜力的关键。本指南将带你了解它的结构、函数和最佳实践。🔍

查询结构和执行
BQL 的核心是它熟悉的、受 SQL 启发的语法。使用 bea query 运行查询:bea --file <ledger> query "SELECT …" 会在终端打印出表格,而不带参数的 bea query 会打开交互式 shell。本指南中的每个查询都是在 Beancount 3.2.3 配合 beanquery 0.2.0 上执行的。
基本查询格式
一个 BQL 查询由三个主要子句组成:SELECT、FROM 和 WHERE。
SELECT <target1>, <target2>, ...
FROM <entry-filter-expression>
WHERE <posting-filter-expression>;SELECT:指定你想检索的数据列。FROM:在交易被处理_之前_过滤整个交易。WHERE:在交易被选中_之后_过滤单独的分录行。
两级过滤系统
理解 FROM 和 WHERE 子句之间的区别,对于编写准确的查询至关重要。BQL 使用两级过滤流程。
-
交易级别(
FROM) 此子句作用于整个交易。如果一笔交易匹配FROM条件,那么_整笔交易_(包括它的所有分录)都会传递到下一阶段。这是过滤数据的主要方式,因为它维护了复式记账系统的完整性。例如,FROM year = 2024会筛选出所有发生在 2024 年的交易。 -
分录级别(
WHERE) 此子句过滤FROM子句所选交易_内部_的单独分录。这对于呈现以及聚焦交易的特定部分很有用。然而,要注意在这个级别过滤可能会在输出中“破坏”交易的完整性,因为你可能只看到一笔分录的一侧。例如,你可以选择所有发往Expenses:Groceries账户的分录。
具体来说,PRINT FROM year = 2024 返回整笔交易(每笔分录的两侧),而 SELECT date, narration, account, position FROM year = 2024 WHERE account ~ "Assets:Broker" 则为每个匹配的分录返回一行。在一个包含两笔券商购买的账本上,第一个查询返回完整分录,第二个则精确返回两行券商记录。
数据模型
为了有效地查询数据,你需要理解 Beancount 如何组织数据。账本是一系列指令的列表,但 BQL 主要关注 Transaction 分录。
交易结构
每一笔 Transaction 都是一个容器,包含顶层属性和一个 Posting 对象列表。
Transaction
├── date
├── flag
├── payee
├── narration
├── tags
├── links
└── Postings[]
├── account
├── units
├── cost
├── price
└── metadata可用的列类型
你可以 SELECT 交易或其分录的任意属性。
-
交易属性 这些列在单笔交易内的每个分录中都相同。
SELECT date, -- 交易的日期 (datetime.date) year, -- 交易的年份 (int) month, -- 交易的月份 (int) day, -- 交易的日 (int) flag, -- 交易标志,例如 "*" 或 "!" (str) payee, -- 收款方 (str) narration, -- 描述或备注 (str) tags, -- 一组标签,例如 #trip-2024 (set[str]) links -- 一组链接,例如 ^expense-report (set[str]) -
分录属性 这些列特定于每一单独的分录行。
SELECT account, -- 账户名称 (str) position, -- 完整金额,包含数量和成本 (Position) units(position), -- 分录的数量和币种 (Amount) cost(position), -- 分录的成本基础 (Amount) price, -- 分录中使用的价格 (Amount) weight, -- 转换为其成本基础后的头寸 (Amount) balance -- 账户中数量的运行总计 (Inventory)units和cost是接收position的函数。price、weight和balance是普通列。
查询函数
BQL 包含一套用于聚合和数据转换的函数,与 SQL 非常相似。
聚合函数
聚合函数跨多行汇总数据。与 GROUP BY 一起使用时,它们提供分组汇总。
-- 统计分录数量
SELECT COUNT(*)
-- 将所有分录汇总为一个 Inventory;币种和批次会被保留,而不转换
SELECT SUM(position)
-- 一行,例如 (-2300.00 USD, 10 HOOL {150.00 USD, 2024-09-05}, 5 HOOL {160.00 USD, 2024-11-02})
-- 将某个账户显式地折算为单一币种(没有价格的头寸保持其原始币种)
SELECT SUM(CONVERT(position, 'USD')) WHERE account ~ "Assets:Checking"
-- 一行,例如 (2580.00 USD)
-- 找到第一笔和最后一笔交易的日期
SELECT FIRST(date), LAST(date)
-- 找到头寸值的最小值和最大值
SELECT MIN(position), MAX(position)
-- 按账户分组,为每个账户求合计
SELECT account, SUM(position) GROUP BY account头寸/库存函数
position 列是一个复合对象。这些函数让你能够提取它的特定部分,或计算其市值。
-- 仅提取头寸中的数量和币种
SELECT UNITS(position)
-- 显示头寸的总成本
SELECT COST(position)
-- 按成本价值显示每个分录(这是一个列;没有 WEIGHT() 函数)
SELECT account, weight WHERE account ~ "Assets:Investments"
-- 使用最新价格数据计算市值
-- (需要为持仓提供价格指令;否则头寸会原样返回)
SELECT VALUE(position)你可以组合这些函数生成强大的报告。例如,查看你投资组合的总成本和当前市值。市值需要为每个持仓提供 price 指令(例如 2024-12-01 price HOOL 175.00 USD)。这两个聚合都按账户返回一个 Inventory,因此每个币种仍然单独列出。
SELECT
account,
COST(SUM(position)) AS total_cost,
VALUE(SUM(position)) AS market_value
FROM
account ~ "Assets:Investments"
GROUP BY
account
-- 每个账户一行,例如 Assets:Broker:HOOL | (2300.00 USD) | (2625.00 USD)市值查询所使用的价格指令可以来自手动录入、本地行情抓取器,或在兼容加载器中的实时价格。托管数据源不会改变查询语法。本地上游工具需要本地价格文件,历史查询仍然需要请求日期当天或之前的价格。
高级功能
除了基本的 SELECT 语句之外,BQL 还为常见的财务报告提供了专门命令。
余额报告
BALANCES 语句为特定期间生成资产负债表或损益表。
-- 生成截至 2024 年初的简单资产负债表
BALANCES FROM close ON 2024-01-01
WHERE account ~ "^Assets|^Liabilities"
-- 生成 2024 财年的损益表
BALANCES FROM
OPEN ON 2024-01-01
CLOSE ON 2024-12-31
WHERE account ~ "^Income|^Expenses"日记账报告
JOURNAL 语句显示一个或多个账户的详细活动,类似于传统的分类账视图。
-- 按原始成本显示你支票账户中的所有活动
JOURNAL "Assets:Checking" AT COST
-- 显示所有 401k 交易,仅展示数量(份额)
JOURNAL "Assets:.*:401k" AT UNITS打印操作
PRINT 语句是一个调试工具,它以原始 Beancount 文件格式输出匹配的完整交易。它只接受条目过滤器。在这里使用 WHERE 子句是语法错误。若要将输出缩小到每笔分录的一侧,请改用带分录过滤器的 SELECT。它为每个匹配的分录返回一行。
-- 完整打印所有 2024 年交易(每笔匹配分录的每个分录)
PRINT FROM year = 2024
-- 仅显示 2024 年交易中的投资分录
SELECT date, narration, account, position
FROM year = 2024
WHERE account ~ "Assets:Investments"
-- 通过唯一 ID 查找一笔交易(由某些工具生成)
-- 返回匹配的条目,或者当没有任何条目带有该 ID 时返回空行
PRINT FROM id = "8e7c47250d040ae2b85de580dd4f5c2a"过滤表达式
你可以使用逻辑运算符(AND、OR)、正则表达式(~)和比较运算来构建复杂的过滤器。
字符串字面量使用单引号。双引号用于界定 ~ 之后的正则表达式。
-- 查找 2024 年下半年的所有差旅费用
-- SELECT * 为每个匹配的分录返回 date、flag、payee、narration 和 position
SELECT * FROM
year = 2024 AND month >= 6
WHERE account ~ "Expenses:Travel"
-- 查找与度假或商务相关的所有交易
SELECT * FROM
'vacation-2024' IN tags OR
'business-trip' IN links性能考量 ⚙️
bea query 是为效率而设计的,但理解它的运行流程可以帮助你在大型账本上编写更快的查询。
- 数据加载:Beancount 首先解析你的整个账本文件,并按时间顺序对所有交易排序。整个数据集都保存在内存中。
- 查询优化:查询引擎按特定顺序应用过滤器以达到最高效率:
FROM(交易)->WHERE(分录)-> 聚合。在FROM级别过滤是最快的,因为它能及早缩减数据集。 - 内存使用:所有操作都在内存中进行。
Position对象和Inventory聚合都经过了优化,但非常大的结果集可能消耗大量 RAM。BQL 不使用基于磁盘的临时存储。
最佳实践
遵循这些建议,编写干净、有效且易于维护的查询。
-
查询组织 为了可读性,尤其是复杂的查询,要格式化你的查询。使用换行和缩进分隔各个子句。
-- 一个干净、可读的查询,列出所有 2024 年的费用 SELECT date, account, position FROM year = 2024 WHERE account ~ "Expenses" ORDER BY date DESC; -
调试 如果某个查询没有按预期工作,先用
LIMIT运行一个小样本。要测试过滤器,使用SELECT DISTINCT看看它匹配了哪些唯一值。-- 迭代过程中预览前几行 SELECT date, account, position LIMIT 5; -- 测试哪些账户匹配某个正则表达式 SELECT DISTINCT account WHERE account ~ "^Assets:.*"; -
余额断言 你可以使用 BQL 来复核账本中的
balance断言。这个查询应当返回你在该账户最后一次余额检查中指定的确切金额。-- 验证你支票账户的最终余额 SELECT account, sum(position) FROM close ON 2025-01-01 -- 使用你余额指令中的日期 WHERE account = "Assets:Checking";