Beancount 拥有一种强大的、类似 SQL 的查询语言(BQL),让你能够精确地切分、筛选和分析财务数据。无论你是想生成快速报告、调试条目,还是执行复杂分析,掌握 BQL 都是充分发挥纯文本会计账本潜力的关键。本指南将带你了解其结构、函数和最佳实践。🔍

查询结构和执行
BQL 的核心是其熟悉的、受 SQL 启发的语法。使用 bea query 运行查询:bea --file <账本> 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, -- The date of the transaction (datetime.date) year, -- The year of the transaction (int) month, -- The month of the transaction (int) day, -- The day of the transaction (int) flag, -- The transaction flag, e.g., "*" or "!" (str) payee, -- The payee (str) narration, -- The description or memo (str) tags, -- A set of tags, e.g., #trip-2024 (set[str]) links -- A set of links, e.g., ^expense-report (set[str]) -
过账属性 这些列特定于每个单独的过账行。
SELECT account, -- The account name (str) position, -- The full amount, including units and cost (Position) units(position), -- The number and currency of the posting (Amount) cost(position), -- The cost basis of the posting (Amount) price, -- The price used in the posting (Amount) weight, -- The position converted to its cost basis (Amount) balance -- The running total of units in the account (Inventory)units和cost是接受position的函数。price、weight和balance是普通列。
查询函数
BQL 包含一套用于聚合和数据转换的函数,非常类似于 SQL。
聚合函数
聚合函数跨多行汇总数据。当与 GROUP BY 一起使用时,它们提供分组摘要。
-- Count the number of postings
SELECT COUNT(*)
-- Sum all postings into one Inventory; currencies and lots are kept, not converted
SELECT SUM(position)
-- one row, e.g. (-2300.00 USD, 10 HOOL {150.00 USD, 2024-09-05}, 5 HOOL {160.00 USD, 2024-11-02})
-- Total one account in a single currency explicitly (positions without a price keep their currency)
SELECT SUM(CONVERT(position, 'USD')) WHERE account ~ "Assets:Checking"
-- one row, e.g. (2580.00 USD)
-- Find the date of the first and last transaction
SELECT FIRST(date), LAST(date)
-- Find the minimum and maximum position values
SELECT MIN(position), MAX(position)
-- Group by account to get a sum for each
SELECT account, SUM(position) GROUP BY account头寸/库存函数
position 列是一个复合对象。这些函数让你能够提取其特定部分或计算其市场价值。
-- Extract just the number and currency from a position
SELECT UNITS(position)
-- Show the total cost of a position
SELECT COST(position)
-- Show each posting at its cost value (a column; there is no WEIGHT() function)
SELECT account, weight WHERE account ~ "Assets:Investments"
-- Calculate the market value using the latest price data
-- (needs a price directive for the holding; otherwise the position is returned unchanged)
SELECT VALUE(position)你可以组合这些函数来制作强大的报告。例如,查看投资组合的总成本及其当前市场价值。市场价值需要每个持仓的价格指令(例如 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
-- one row per account, e.g. Assets:Broker:HOOL | (2300.00 USD) | (2625.00 USD)高级功能
除了基本的 SELECT 语句外,BQL 还提供了用于常见财务报告的专业命令。
余额报告
BALANCES 语句为特定时期生成资产负债表或损益表。
-- Generate a simple balance sheet as of the start of 2024
BALANCES FROM close ON 2024-01-01
WHERE account ~ "^Assets|^Liabilities"
-- Generate an income statement for the 2024 fiscal year
BALANCES FROM
OPEN ON 2024-01-01
CLOSE ON 2024-12-31
WHERE account ~ "^Income|^Expenses"日记账报告
JOURNAL 语句显示一个或多个账户的详细活动,类似于传统的账本视图。
-- Show all activity in your checking account at its original cost
JOURNAL "Assets:Checking" AT COST
-- Show all 401k transactions, displaying only the units (shares)
JOURNAL "Assets:.*:401k" AT UNITS打印操作
PRINT 语句是一个调试工具,它以其原始 Beancount 文件格式输出完整的匹配交易。它只接受条目过滤器。在这里使用 WHERE 子句是语法错误。若要将输出缩小到每条分录的一条腿,请改用带过账过滤器的 SELECT。它为每个匹配的过账返回一行。
-- Print all 2024 transactions in full (every posting of each matching entry)
PRINT FROM year = 2024
-- Show only the investment postings of 2024 transactions
SELECT date, narration, account, position
FROM year = 2024
WHERE account ~ "Assets:Investments"
-- Find a transaction by its unique ID (generated by some tools)
-- Returns the matching entry, or no rows when nothing carries that ID
PRINT FROM id = "8e7c47250d040ae2b85de580dd4f5c2a"过滤表达式
你可以使用逻辑运算符(AND、OR)、正则表达式(~)和比较来构建复杂的过滤器。
字符串字面量使用单引号。双引号界定 ~ 后的正则表达式。
-- Find all travel expenses from the second half of 2024
-- SELECT * returns date, flag, payee, narration and position per matching posting
SELECT * FROM
year = 2024 AND month >= 6
WHERE account ~ "Expenses:Travel"
-- Find all transactions related to a vacation or business
SELECT * FROM
'vacation-2024' IN tags OR
'business-trip' IN links性能考量 ⚙️
bea query 设计用于高效运行,但了解其操作流程可以帮助你在大型账本上编写更快的查询。
- 数据加载:Beancount 首先解析你的整个账本文件,并将所有交易按时间顺序排序。整个数据集保存在内存中。
- 查询优化:查询引擎按特定顺序应用过滤器以实现最大效率:
FROM(交易) ->WHERE(过账) -> 聚合。在FROM级别进行过滤最快,因为它能及早减少数据集。 - 内存使用:所有操作都在内存中执行。
Position对象和Inventory聚合已优化,但非常大的结果集可能消耗大量 RAM。BQL 不使用基于磁盘的临时存储。
最佳实践
遵循这些提示,编写干净、高效且可维护的查询。
-
查询组织 格式化你的查询以提高可读性,尤其是复杂查询。使用换行和缩进来分隔子句。
-- A clean, readable query for all 2024 expenses SELECT date, account, position FROM year = 2024 WHERE account ~ "Expenses" ORDER BY date DESC; -
调试 如果查询未按预期工作,请先使用
LIMIT运行一个小样本。要测试过滤器,请使用SELECT DISTINCT查看它匹配哪些唯一值。-- Preview the first rows while iterating SELECT date, account, position LIMIT 5; -- Test which accounts match a regular expression SELECT DISTINCT account WHERE account ~ "^Assets:.*"; -
余额断言 你可以使用 BQL 来双重检查账本中的
balance断言。此查询应返回你上次对该账户进行余额检查时指定的确切金额。-- Verify the final balance of your checking account SELECT account, sum(position) FROM close ON 2025-01-01 -- Use the date from your balance directive WHERE account = "Assets:Checking";