跳转到主要内容

Beancount 查询语言(BQL):查询你的账本

使用 BQL 这款类 SQL 语言查询你的 Beancount 账本:SELECT 语法、列、聚合与库存函数,以及通过 bea query 运行的报表。

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

beancount.io BQL 查询编辑器在浏览器中对 Beancount 账本运行类 SQL 查询

探索实时账本 →


查询结构和执行​

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 使用两级过滤流程。

  1. 交易级别(FROM) 此子句作用于整个交易。如果一笔交易匹配 FROM 条件,那么_整笔交易_(包括它的所有分录)都会传递到下一阶段。这是过滤数据的主要方式,因为它维护了复式记账系统的完整性。例如,FROM year = 2024 会筛选出所有发生在 2024 年的交易。

  2. 分录级别(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 交易或其分录的任意属性。

  1. 交易属性 这些列在单笔交易内的每个分录中都相同。

    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])
  2. 分录属性 这些列特定于每一单独的分录行。

    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 是为效率而设计的,但理解它的运行流程可以帮助你在大型账本上编写更快的查询。

  1. 数据加载:Beancount 首先解析你的整个账本文件,并按时间顺序对所有交易排序。整个数据集都保存在内存中。
  2. 查询优化:查询引擎按特定顺序应用过滤器以达到最高效率:FROM(交易)-> WHERE(分录)-> 聚合。在 FROM 级别过滤是最快的,因为它能及早缩减数据集。
  3. 内存使用:所有操作都在内存中进行。Position 对象和 Inventory 聚合都经过了优化,但非常大的结果集可能消耗大量 RAM。BQL 不使用基于磁盘的临时存储。

最佳实践​

遵循这些建议,编写干净、有效且易于维护的查询。

  1. 查询组织 为了可读性,尤其是复杂的查询,要格式化你的查询。使用换行和缩进分隔各个子句。

    -- 一个干净、可读的查询,列出所有 2024 年的费用
    SELECT
        date,
        account,
        position
    FROM
        year = 2024
    WHERE
        account ~ "Expenses"
    ORDER BY
        date DESC;
  2. 调试 如果某个查询没有按预期工作,先用 LIMIT 运行一个小样本。要测试过滤器,使用 SELECT DISTINCT 看看它匹配了哪些唯一值。

    -- 迭代过程中预览前几行
    SELECT date, account, position LIMIT 5;
     
    -- 测试哪些账户匹配某个正则表达式
    SELECT DISTINCT account
    WHERE account ~ "^Assets:.*";
  3. 余额断言 你可以使用 BQL 来复核账本中的 balance 断言。这个查询应当返回你在该账户最后一次余额检查中指定的确切金额。

    -- 验证你支票账户的最终余额
    SELECT account, sum(position)
    FROM close ON 2025-01-01 -- 使用你余额指令中的日期
    WHERE account = "Assets:Checking";

来源:https://beancount.io/zh/docs/Basics/beancount-query-language