查询语言
张记账提供了一门小型查询语言,用来临时回答关于账本的各种问题。它支持 Beancount 查询语言(BQL)的一个子集,为 Beancount 或 Fava 编写的大多数查询无需修改即可使用。当 BQL v2 与其后继者 beanquery 行为不一致时,张记账以 beanquery 为准,本页末尾列出的少数有意为之的差异除外。
查询直接在张记账已加载到内存中的账本上执行。查询是只读的,所有运算都使用精确的十进制数,金额永远不会经过浮点数转换。
在网页界面中
Section titled “在网页界面中”打开 /explore 的 查询 页面(英文界面中为 Query)。
- 在编辑器中输入查询。只有点击运行按钮或按下 Ctrl+Enter(macOS 上为 Cmd+Enter)时才会执行,输入过程中不会自动执行。
- 结果以表格展示,每个单元格按其类型渲染。库存(inventory)单元格每行显示一个持仓。
- 由一个标签和一个数值或金额组成的两列结果,还会在表格上方绘制成图表,见图表。
- 查询出错时,编辑器会高亮出错的行和列。
- 示例 菜单把现成的查询放进编辑器并执行,其中包括损益表、年末余额和账户流水。
- 已保存 菜单列出账本中保存的查询,见保存的查询。
- 参考 面板列出了所有列和函数。
- 导出 CSV 把结果下载为 CSV 文件,见导出为 CSV。
- 应用其他页面中数字旁边的 打开查询,会打开本页,把该数字背后的内置查询放进编辑器并执行。链接到
/explore?query=...对任意查询有同样的效果。
当结果正好有两列、至少有一行,并且第二列是数值或金额(int、decimal、amount、position 或 inventory)时,查询页面会把它绘制成图表。图表的种类由第一列决定:
| 第一列 | 图表 |
|---|---|
date |
折线图,每个日期一个点,按日期升序排列。 |
str,且每个标签看起来都是账户名 |
按账户层级绘制的矩形树图。 |
其他 str |
柱状图,每个标签一根柱子,按结果中的顺序排列。最多绘制 50 根柱子。 |
- 由不含空格、以
:分隔的若干段组成的标签,看起来就是账户名。至少要有一个标签包含:,空标签和NULL标签不参与判断。 - 持仓和库存按单位绘制,不计成本。图表每次只显示一种货币。结果中有多种货币时,可以用 货币 选择器选择要绘制的货币。默认选中账本的运营货币(
operating_currency选项,如果结果中有),否则选中出现在最多行中的货币。 - 标签相同的行会相加,值为
NULL的行会被跳过。在折线图中,在所选货币下没有值的日期按零绘制。 - 在矩形树图中,账户按其各段逐层嵌套,所有账户共有的顶层(例如
Expenses)会被省略。矩形的面积表示绝对值,颜色表示值的正负。 - 数值的相加是精确的,只在绘制时才转换为浮点数。
- 图表 开关可以隐藏或显示图表,浏览器会记住这个设置。
下面的查询把一年的支出绘制成矩形树图:
SELECT account, sum(position) WHERE account ~ '^Expenses' AND year = 2024 GROUP BY account下面的查询把每月支出绘制成折线图:
SELECT yearmonth(date) AS month, sum(position) WHERE account ~ '^Expenses' GROUP BY month ORDER BY month用 query 指令保存在账本中的查询,会出现在 已保存 菜单中:
2024-01-01 query "food by payee" "SELECT payee, sum(position) WHERE account ~ '^Expenses:Food' GROUP BY payee"- 每一项显示查询的名字、日期和查询文本的开头。查询按账本顺序列出;同名的查询都会列出,可以通过日期区分。
- 选择一项会把它的查询放进编辑器并执行。
- 无法被当前版本的查询引擎编译的查询依然会列出,标记为 (无效),并显示错误信息。加载账本时不会检查保存的查询,所以无效的查询不会让账本报告错误。
- 每次打开菜单都会重新获取列表,所以打开页面之后才加入账本的查询,无需刷新页面就会出现。
- 查询是账本文件中带引号的字符串。不构成已知转义的反斜杠会原样保留,所以写
'\d+'即可保存正则表达式\d+;写两次的'\\d+'同样可以使用,并且在 Beancount 中的读取结果也相同。见转义。
导出为 CSV
Section titled “导出为 CSV”导出 CSV 把编辑器中的查询发送到 POST /api/query/csv,并把结果下载为 query.csv。查询不需要先执行。金额、持仓和库存会按货币拆分成数值列,因此文件可以直接在电子表格中打开。如果查询有错误,错误的显示方式与执行失败时相同。在电子表格软件中打开导出的文件之前,请先阅读关于公式的注意事项。
通过 HTTP
Section titled “通过 HTTP”将查询发送到 POST /api/query;发送到 POST /api/query/csv 则得到 CSV 格式的结果。GET /api/query/saved 列出保存的查询,GET /api/query/builtins 列出应用中各项数字背后的内置查询。请求和响应格式见 HTTP API。
SELECT date, payee, account, positionWHERE account ~ '^Expenses:Food'ORDER BY date DESCLIMIT 10这个查询返回 Expenses:Food 及其子账户最近的十条分录。
- 每一行是一条分录(posting)。
date和payee来自交易,account和position来自分录本身。 ~在文本的任意位置匹配正则表达式,并且不区分大小写。^把模式锚定在账户名的开头。注意'^Expenses:Food'也会匹配Expenses:Foodstuff,如果只想匹配该账户及其子账户,请使用'^Expenses:Food(:|$)'。ORDER BY date DESC让最新的分录排在最前,LIMIT 10只保留前十行。
SELECT [DISTINCT] target [, target ...] | * [FROM from_clause] [WHERE expression] [GROUP BY group_key [, group_key ...] [HAVING expression]] [ORDER BY order_key [ASC | DESC] [, order_key [ASC | DESC] ...]] [PIVOT BY pivot_key, pivot_key] [LIMIT count [OFFSET count]] [;]
BALANCES [AT function] [FROM from_clause] [WHERE expression] [;]
JOURNAL ['pattern'] [AT function] [FROM from_clause] [;]
target = expression [AS name]group_key = expression | target name | target numberorder_key = expression | target name | target numberpivot_key = target name | target numberfrom_clause = #table | [expression] [OPEN ON date] [CLOSE [ON date]] [CLEAR]count = integer | parameter- 一个查询就是一条语句:
SELECT,或者简写形式BALANCES和JOURNAL之一。不支持PRINT。 - 各子句必须按上面的顺序出现。除开头的关键字外都是可选的,末尾可以有一个
;。 - 关键字、列名和函数名都不区分大小写:
SELECT account、select ACCOUNT和Select Account是同一个查询。表名与 beanquery 一样区分大小写。 - 结构化的列的字段用点号读取,中间不能有空格:例如
#accounts中的open.date。 - 名字由 ASCII 字母、数字和下划线组成,不能以数字开头。
SELECT、DISTINCT、FROM、WHERE、GROUP、BY、ORDER、ASC、DESC、LIMIT、AS、AND、OR、NOT、IN、IS、NULL、TRUE、FALSE、HAVING和PIVOT是保留字,不能用作列名。OFFSET只在LIMIT的行数之后才是关键字,在其他地方offset与 beanquery 一样是普通的名字。 - 词法单元之间的空格和换行没有意义,一个查询可以分成多行书写。
--开始一段注释,直到行尾。
查询的执行过程
Section titled “查询的执行过程”FROM #table选择要读取的表,没有时读取分录。如果FROM中有会计期间子句(OPEN ON、CLOSE和CLEAR),它们先改写整个账本的分录。FROM中的表达式和WHERE子句决定哪些行参与计算。- 如果查询读取了累计余额,就按账本顺序在被选中的分录上累加。
- 如果查询使用了聚合函数或带有
GROUP BY子句,被选中的分录会被分组,每组产生一行;否则每条分录产生一行。 HAVING丢弃不满足其条件的分组。ORDER BY对结果行排序。DISTINCT去除重复的行,每组重复只保留第一行。OFFSET跳过前面的若干行,LIMIT保留接下来的若干行,丢弃其余的行。PIVOT BY把剩下的行转换成一张表,某个目标的每个值占一列。
在读取任何分录之前,张记账会先检查并简化查询。只包含常量的部分(例如 '^Expenses:' + 'Food')在这时就计算一次。依赖账本或当前日期的函数(today、convert、value、getprice、指令函数和元数据函数)不会被提前计算。因此,常量模式中无效的正则表达式会立即报错并给出位置,即使没有任何分录会与它匹配。
张记账只在第一个读取分录的查询中对账本的分录做一次批次记账,并把结果连同账本的价格一起保留,直到账本重新加载。如果查询没有会计期间子句,且其 FROM 或 WHERE 只可能对某些账户成立,查询就只读取这些账户的分录:account = 'Assets:Bank'、account = :account、account IN ('Assets:Bank', 'Assets:Cash')、account IN :accounts 和 under(account, 'Assets:Bank')(该账户及其子账户,也可以用参数),可以单独使用、用 OR 组合,或作为用 AND 连接的条件之一。这只会让这类查询更快,结果(包括累计余额)完全相同。
SELECT
Section titled “SELECT”目标(target)是要为每一行计算的表达式,用逗号分隔。
SELECT *是SELECT date, flag, payee, narration, account, position的简写。用于其他表时,展开为该表的列。AS name为目标命名。这个名字可以在GROUP BY和ORDER BY中使用,但不能在WHERE中使用。- 结果列有别名时以别名命名;否则以目标的原文命名(去掉首尾空格),与书写完全一致,例如
account、ACCOUNT、sum(position)或sum( position )。SELECT *的各列使用上面列出的小写名字。 SELECT DISTINCT会去掉与之前某一行完全相同的行,只比较被选择的值。
FROM 后面跟的是一个表、一个表达式、会计期间子句,或表达式加会计期间子句。其中的表达式和 WHERE 一样过滤分录;当两者同时存在时,分录必须同时满足两个条件:
SELECT account, sum(position)FROM year = 2024WHERE account ~ '^Expenses'GROUP BY account等价于 ... WHERE (year = 2024) AND (account ~ '^Expenses') ...。这与 beanquery 的行为一致,在 FROM 中做过滤的 BQL 查询因此可以继续使用。
会计期间子句写在表达式之后。它们先改写分录,表达式再过滤改写后的分录:
SELECT account, sum(position)FROM OPEN ON 2024-01-01 CLOSE ON 2025-01-01WHERE account ~ '^(Income|Expenses)'GROUP BY accountFROM #name 读取其他表之一,FROM #postings 则显式指定默认的表。表在 FROM 中必须单独出现,请用 WHERE 过滤它的行。会计期间子句只作用于分录,因此不能跟在表的后面;BALANCES 和 JOURNAL 总是读取分录。
SELECT date, account, amount, discrepancyFROM #balancesWHERE discrepancy IS NOT NULL与 beanquery 一样,不是 postings 表的列名的裸名字也表示一个表:FROM prices 就是 FROM #prices。未知的表会报错,错误位置指向表名。
WHERE 保留条件为 TRUE 的分录。条件结果为 NULL 时视为不成立,该分录会被丢弃(见 NULL 与三值逻辑)。WHERE 和 FROM 的条件必须是布尔表达式,两者中都不能使用聚合函数。
GROUP BY
Section titled “GROUP BY”只要有一个目标使用了聚合函数,或者查询带有 GROUP BY 子句,查询就是聚合查询。此时分录会被分组,每组产生一行。
- 分组键可以是表达式、目标名(别名,或者
account这类目标的原文),或目标在SELECT列表中的序号(从 1 开始)。GROUP BY 1, 2表示按前两个目标分组。名字的匹配不区分大小写。 - 分组键不必出现在
SELECT中。SELECT sum(position) GROUP BY account为每个账户返回一个不带标签的合计。 - 没有
GROUP BY时,聚合查询按所有非聚合的目标分组。SELECT account, sum(position)等价于SELECT account, sum(position) GROUP BY account。如果所有目标都是聚合函数,所有匹配的分录组成一个分组。 - 有
GROUP BY时,每个在聚合函数之外读取了列或调用了元数据函数的目标都必须是分组键。SELECT account, payee, count(*) GROUP BY account会报错,因为payee没有被分组。两者都不使用的目标(例如常量)不需要分组。 - 在聚合查询中,
ORDER BY里的非聚合表达式也必须是分组键。 set类型(如tags)和inventory类型的值不能作为分组键。- 分组键本身不能包含聚合函数,聚合函数也不能嵌套(
sum(count(*))会报错)。 - 同一个目标不能把聚合函数和聚合函数之外的列混在一起,即使这个列已经被分组。
number - sum(number)和possign(sum(position), account)都会报错,后者应写成sum(possign(position, account))。只由聚合函数和常量构成的表达式,例如sum(number) / 12或units(sum(position)),本身就是聚合表达式,可以正常使用。 - 没有分组键的聚合查询,即使没有匹配的分录也返回一行:
count和数值sum返回0,持仓sum返回空持仓,first、last、min和max返回NULL。HAVING、LIMIT和OFFSET仍然生效。有显式或隐式分组键的查询在没有匹配时返回零行。
SELECT root(account, 2) AS category, sum(position) AS totalWHERE account ~ '^Expenses'GROUP BY categoryHAVING
Section titled “HAVING”HAVING 过滤聚合查询的分组,就像 WHERE 过滤分录一样。它紧跟在 GROUP BY 之后,保留条件为 TRUE 的分组;条件为 FALSE 或 NULL 的分组会被丢弃。
SELECT root(account, 2) AS category, sum(position) AS totalWHERE account ~ '^Expenses'GROUP BY categoryHAVING sum(number) > 1000HAVING需要GROUP BY子句。没有GROUP BY时是语法错误,即使查询已经隐式分组。- 条件必须是使用了聚合函数的布尔表达式。其中的聚合函数不必出现在
SELECT中:即使count(*)不是目标,HAVING count(*) > 10也可以使用。 HAVING中的名字和WHERE中一样,指的是postings表的列,而不是目标的别名。HAVING total > 1000会报错,应该重复写出表达式,例如HAVING sum(number) > 1000。- 在聚合函数之外,只能通过分组键读取列:与某个分组键相同的表达式取该分组的值。在
GROUP BY account HAVING account ~ 'Food' AND count(*) > 10中,account是每个分组的账户。其他列必须写在聚合函数之内。 LIMIT只计算HAVING保留下来的分组。
ORDER BY
Section titled “ORDER BY”- 排序键和分组键一样,可以是表达式、目标名或目标序号。没有被选择的表达式也可以用来排序,只是不会出现在结果中。
- 每个排序键有自己的方向:
ASC(升序,默认)或DESC(降序)。在ORDER BY 1 DESC, 2中,第一个键降序,第二个键升序。 - 先按第一个键比较,第一个键相同时才比较第二个键,依此类推。所有键都相同的行保持原来的顺序。
NULL比其他任何值都小:升序时排在最前,降序时排在最后。- 各类型的值如何排序见排序与比较。
- 没有
ORDER BY时,普通查询按账本顺序返回分录:先按日期和时间,再按它们在文件中出现的顺序。聚合查询按各分组第一条分录在账本中出现的顺序返回。
LIMIT 与 OFFSET
Section titled “LIMIT 与 OFFSET”LIMIT n 在排序和 DISTINCT 之后保留前 n 行。LIMIT n OFFSET m 先跳过前 m 行,再保留接下来的 n 行,用于对结果分页:ORDER BY date DESC LIMIT 50 OFFSET 100 是每页五十行时的第三页。
n和m是非负整数,可以写成字面量,也可以是参数,例如LIMIT :size OFFSET :offset。OFFSET是张记账的扩展,只能跟在LIMIT之后;beanquery 只有LIMIT。它只在那里是关键字,所以SELECT date AS offset ORDER BY offset LIMIT 5仍然可用。LIMIT 0不返回任何行,偏移超过结果末尾时也不返回任何行。- 参数必须绑定为整数。负数、
NULL,或者偏移与行数之和超出 64 位时,会在参数处报错,绝不会悄悄变成另一个窗口。 - 没有
ORDER BY时结果按账本顺序排列,所以只要账本不变,分页就是稳定的。 - 分页的开销很小:没有
ORDER BY时,查询对OFFSET之前的行只计数、不构建,取够要保留的行就停止,所以一页只占用它自己的行;有ORDER BY时,扫描过程中只保留前OFFSET + LIMIT行,而不是把所有行都排序。排序时只计算每行的ORDER BY键,其他目标只为该页的行计算;可能出错的目标(例如除法)和扫描时读取累计余额的目标除外。 - 因此,与
LIMIT之后的行一样,没有ORDER BY的查询在OFFSET之前的行不会计算目标,只有计算这些行的某个目标才会出现的错误(例如整数溢出)不会报告。这些行仍要经过WHERE判断,所以条件中的错误仍会报告。
查询还可以要求返回 LIMIT 和 OFFSET 之前的总行数,例如用来显示页数:Rust API 和 POST /api/query 都有 count_total 选项。窗口之外的行只计数,不构造。
PIVOT BY
Section titled “PIVOT BY”PIVOT BY a, b 把聚合查询的结果转换成一张表:目标 a 的每个值占一行,目标 b 的每个值占一列。
SELECT root(account, 2) AS category, year, sum(position) AS totalWHERE account ~ '^Expenses:(Food|Home)'GROUP BY category, yearPIVOT BY category, year| category/year | 2015 | 2016 | 2017 |
|---|---|---|---|
| Expenses:Food | 6614.67 USD | 6859.72 USD | 4686.42 USD |
| Expenses:Home | 31304.42 USD | 31280.55 USD | 20866.27 USD |
PIVOT BY写在ORDER BY之后、LIMIT之前,正好接受两个目标,每个目标用名字或它在SELECT列表中的序号表示,不能是表达式。- 查询必须是聚合查询,第二个目标必须是分组键,两个目标不能相同。
- 它最后执行,作用于经过
HAVING、ORDER BY、DISTINCT和LIMIT之后剩下的行,只有这些行会产生列。 - 无论
ORDER BY如何,结果行都按a排序,列按b的值排序。 - 第一列以两个目标命名为
a/b。其余每一列以b的一个值命名(例如2016),存放该值对应的剩余目标。剩余的目标不止一个时,每个值的每个目标各占一列,命名为<值>/<目标>,例如2016/total和2016/count。 - 值在列名中的写法:日期为
2016-01-31,小数为12.50,布尔值为True和False,NULL为NULL。 - 与 beanquery 一样,列名不一定唯一:
NULL值和字符串'NULL'都命名为NULL,含有/的值也可能让<值>/<目标>形式的列名与另一列相同。列按位置区分,所以不会丢失数据,但按列名查找列的电子表格或程序会看到重名的列。 - 某对
a和b没有对应的行时,单元格为NULL。如果有多行对应同一对值(查询除a和b外还按其他键分组时会出现),由结果顺序中的最后一行填充。 - 各列保持其目标的类型,所以 CSV 导出会按货币拆分透视后的金额和库存列,例如
2016 (USD)。
张记账在自己的代码中(通过 zhang-query crate 的 Rust API)执行查询时,查询中凡是可以写值的地方都可以写参数 $1、$2、… 或 :name,参数的值另行绑定。JOURNAL 的模式、OPEN ON 和 CLOSE ON 的日期,以及 LIMIT 和 OFFSET 的行数也可以是参数。HTTP API 不绑定参数,所以通过 HTTP 发送的查询中如果含有参数,会报错 parameter $1 is not bound。内置查询可以把参数值写进查询文本,得到一个能通过 HTTP 执行的查询。
参数是一次执行中的常量:读取任何行之前,每个参数都会被替换成它的值,查询再被简化一次,就像值直接写在查询里一样。作为参数给出的正则表达式(payee ~ :keyword)只编译一次;作为参数给出的集合(account IN :accounts)和值的列表(IN ('a', 'b', :c))用哈希表查找;icontains 的查找文本只转换一次小写。所以带参数的查询与把值写成字面量的同一查询一样快。作为参数给出的无效正则表达式仍与以前一样,在某一行与它匹配时于匹配处报错。
BALANCES 与 JOURNAL
Section titled “BALANCES 与 JOURNAL”与 beanquery 一样,BALANCES 和 JOURNAL 是两种常用查询的简写。张记账在执行之前会把它们改写为下面所示的 SELECT,所以本页关于 SELECT 的所有内容同样适用于它们。
BALANCES
Section titled “BALANCES”BALANCES [AT function] [FROM from_clause] [WHERE expression]等价于
SELECT account, sum(function(position))FROM from_clauseWHERE expressionGROUP BY account, account_sortkey(account)ORDER BY account_sortkey(account)- 每个有分录的账户一行,给出其持仓之和。分录合计为零的账户也会列出,值为空库存。
- 账户先按类型排序,顺序为
Assets、Liabilities、Equity、Income、Expenses,再按名字排序,见account_sortkey。 FROM和WHERE决定哪些分录参与求和。BALANCES FROM year = 2024给出的是各账户在 2024 年内的变化,而不是年末余额;年末余额请使用CLOSE ON。BALANCES没有GROUP BY、ORDER BY和LIMIT子句。
BALANCES WHERE account ~ '^Assets'JOURNAL
Section titled “JOURNAL”JOURNAL ['pattern'] [AT function] [FROM from_clause]等价于
SELECT date, flag, maxwidth(payee, 48), maxwidth(narration, 80), account, function(position), function(balance)FROM from_clauseWHERE account ~ 'pattern'- 账户与模式匹配的每条分录一行,按账本顺序排列,最后一列是累计余额。没有模式时列出所有分录。
- 模式是写在引号中的正则表达式,用
~匹配,因此不区分大小写,并且可以匹配账户名的任意一部分。JOURNAL 'checking'列出名字中含有Checking的所有账户。 - 累计余额把流水中的每一行都加进去,不论属于哪个账户。如果模式匹配了多个账户,余额就是它们的合计余额。
- 用
maxwidth把收款方缩短到 48 个字符,把描述缩短到 80 个字符。 JOURNAL没有WHERE、GROUP BY、ORDER BY和LIMIT子句。请用FROM过滤分录。
JOURNAL 'Assets:Bank:Checking' FROM year = 2024AT function 把一个函数应用到每个持仓上;在 JOURNAL 中,还应用到累计余额上:
| 语句 | 结果列 |
|---|---|
BALANCES |
account、sum(position) |
BALANCES AT cost |
account、sum(cost(position)) |
JOURNAL 'Cash' |
date、flag、maxwidth(payee, 48)、maxwidth(narration, 80)、account、position、balance |
JOURNAL 'Cash' AT units |
date、flag、maxwidth(payee, 48)、maxwidth(narration, 80)、account、units(position)、units(balance) |
- 函数名后面不加括号。该函数必须接受单个
position参数;用于JOURNAL时还必须接受单个inventory参数。常用的是估值函数units、cost和value,abs和neg也可以。 AT units去掉成本,所以同一货币的各个批次合并为一个持仓。AT cost给出账面价值,AT value给出按最新价格计算的市值。- 未知的函数,或者没有合适重载的函数,会报错,错误位置指向函数名。
- 如表中所示,结果列的命名与等价的
SELECT相同。
FROM 子句中的 OPEN ON、CLOSE 和 CLEAR 像 Beancount 的报表那样,把账本变成某一个会计期间的账簿。有了它们,损益表和资产负债表都只需要一个查询。
2024 年的损益表:
SELECT account, sum(position) AS totalFROM OPEN ON 2024-01-01 CLOSE ON 2025-01-01WHERE account ~ '^(Income|Expenses)'GROUP BY accountORDER BY account2025 年初的资产负债表:
BALANCES FROM CLOSE ON 2025-01-01 CLEARWHERE account ~ '^(Assets|Liabilities|Equity)'期间子句的语法
Section titled “期间子句的语法”FROM [expression] [OPEN ON date] [CLOSE [ON date]] [CLEAR]- 各子句必须按这个顺序出现,每个最多出现一次。
FROM CLEAR OPEN ON 2024-01-01会报错。 FROM后面至少要有表达式或一个子句,也可以两者都有。- 日期是不带引号的日期字面量(例如
2024-01-01),或者一个参数。 - 同时给出两个日期时,
CLOSE的日期不能早于OPEN的日期,两者可以相同。 - 这些子句可以用于
SELECT、BALANCES和JOURNAL。
各子句的作用
Section titled “各子句的作用”这些子句按 OPEN、CLOSE、CLEAR 的顺序改写账本中的所有分录。之后 FROM 中的表达式和 WHERE 才选择分录,所以过滤条件不会改变子句的计算结果。例如 FROM year = 2023 OPEN ON 2024-01-01 只保留日期为 2023-12-31 的期初余额。
OPEN ON d 让期间从 d 开始,并把 d 之前的所有内容替换为期初余额:
d之前的转换差额被转入Equity:Conversions:Previous。转换差额是所有分录按成本计价后合计不为零的部分。按价格(@)把一种货币兑换为另一种货币的交易会留下这样的差额,批次买入时的舍入也会。d之前收入和支出账户的余额被转入Equity:Earnings:Previous,因此这些账户在期间开始时为零。- 仍有余额的每个账户得到一笔汇总交易,标记为
S,日期为d − 1。余额中的每个批次对应一条分录,所以持仓保留其成本、成本日期和标签;每条分录在Equity:Opening-Balances上都有一条按该批次成本计的对应分录。
d 当天及之后的分录保持不变。
CLOSE ON d 让期间在 d 之前结束。日期为 d 或更晚的分录被丢弃,所以 d 当天不属于该期间。如果剩下的分录存在转换差额,会加入一笔标记为 C、日期为 d − 1 的转换交易,把差额记入 Equity:Conversions:Current。这些分录的价格为零,以转换货币(默认为 NOTHING)计。
不带日期的 CLOSE 不丢弃任何分录,只加入上述转换交易,其日期为期间内最后一笔条目的日期。
CLEAR 把每个收入和支出账户的余额转入 Equity:Earnings:Current,每个账户一笔转账交易,标记为 T,日期为期间内最后一笔条目的日期。之后收入和支出账户的合计为零,资产负债表因此平衡。
期间内的最后一笔条目,指期间内的分录,以及账本中从 OPEN 日期到 CLOSE 日期前一天的其他带日期指令(例如价格和余额断言)中,日期最晚的那一个。预算指令不计算在内。
这些子句加入的交易和其他交易一样,是 postings 表中的行:
| 标记 | 由谁加入 | 日期 | 描述 | 账户 |
|---|---|---|---|---|
S |
OPEN ON d |
d − 1 |
Opening balance for '<account>' (Summarization) |
该账户,以及 Equity:Opening-Balances |
C |
CLOSE |
CLOSE ON d 时为 d − 1,否则为期间内最后一笔条目的日期 |
Conversion for (<inventory>) |
Equity:Conversions:Current |
T |
CLEAR |
期间内最后一笔条目的日期 | Transfer balance for '<account>' (Transfer balance) |
该账户,以及 Equity:Earnings:Current |
- 它们的
payee为NULL,没有标签、链接和元数据。同一笔交易的所有分录共享一个id。 - 对应分录的描述就是它所属交易的描述,因此其中写的是它所平衡的账户。
OPEN产生的前期收益和前期转换差额,表现为Equity:Earnings:Previous和Equity:Conversions:Previous两个账户的S交易。- 它们计入累计余额。使用
OPEN ON之后,每个账户的第一行就是它的期初余额。 - 可以按
flag过滤它们。例如WHERE flag != 'S'会隐藏期初余额。
SELECT flag, count(*) FROM OPEN ON 2024-01-01 CLOSE ON 2025-01-01 CLEAR WHERE flag IN ('S', 'C', 'T') GROUP BY flag这些子句记账所用的账户不需要在账本中开立。表格最后一列的 Beancount 选项可以修改它们:
| 账户 | 使用者 | 选项 |
|---|---|---|
Equity:Opening-Balances |
OPEN ON |
account_previous_balances |
Equity:Earnings:Previous |
OPEN ON |
account_previous_earnings |
Equity:Conversions:Previous |
OPEN ON |
account_previous_conversions |
Equity:Earnings:Current |
CLEAR |
account_current_earnings |
Equity:Conversions:Current |
CLOSE |
account_current_conversions |
- 选项给出的是名字中
Equity:之后的部分。写了option "account_previous_balances" "Opening"之后,OPEN ON使用Equity:Opening。 - 不是有效账户名的值(例如空值或含有空格的值)会被忽略,此时使用默认账户。
option "conversion_currency" "..."设置转换分录零价格所用的货币,默认为NOTHING。
| 字面量 | 示例 | 类型 |
|---|---|---|
| 字符串 | 'Food'、"USD" |
str |
| 整数 | 0、42 |
int |
| 小数 | 3.14、0.5、.5 |
decimal |
| 日期 | 2024-01-31 |
date |
| 布尔值 | TRUE、FALSE |
bool |
| 空值 | NULL |
null |
- 字符串可以用单引号或双引号。与标准 SQL 不同,
"USD"是字符串而不是列名。 - 字符串在下一个同类引号处结束,没有转义序列。如果字符串中需要包含引号,请用另一种引号把它括起来:
"Joe's Diner"或'say "hi"'。 - 日期写作
YYYY-MM-DD,不加引号。2024-13-01这样无效的日期会报错。 - 带引号的字符串与日期比较时会被当作日期读取,所以
date >= '2024-01-01'也可以使用。此时如果字符串不是有效的YYYY-MM-DD日期,会报错。 - 整数是 64 位的,更大的整数字面量会成为
decimal。不支持1e3这样的指数写法。 TRUE、FALSE和NULL是关键字,不区分大小写。
按优先级从高到低:
| 优先级 | 运算符 | 含义 |
|---|---|---|
| 1 | -x +x |
一元负号、一元正号 |
| 2 | * / |
乘、除 |
| 3 | + - |
加、减 |
| 4 | = == != <> < <= > >= |
比较 |
| 4 | ~ !~ ?~ |
正则匹配 |
| 4 | IN NOT IN |
成员测试 |
| 4 | IS NULL IS NOT NULL |
空值测试 |
| 5 | NOT |
逻辑非 |
| 6 | AND |
逻辑与 |
| 7 | OR |
逻辑或 |
- 可以用括号显式分组:
(a OR b) AND c。 - 由于
NOT的优先级低于比较运算,NOT account ~ '^Assets'的含义是NOT (account ~ '^Assets')。 - 第 4 级的运算符不能连用:
a = b = c会报错。
| 表达式 | 结果 | 说明 |
|---|---|---|
int + - * int |
int |
溢出时报错。 |
int / int |
decimal |
7 / 2 等于 3.5。 |
int 或 decimal + - * / int 或 decimal |
decimal |
|
date + int、int + date、date - int |
date |
加上或减去天数:2024-01-31 + 1 等于 2024-02-01。结果超出 1 到 9999 年时为 NULL。 |
date - date |
int |
两个日期相差的天数。 |
date + - interval、interval + date |
date |
先按间隔的月数移动,保留日期中的日,除非那个月更短(这时取月末),再按其天数移动:2024-01-31 + interval('1 month') 等于 2024-02-29。结果超出 1 到 9999 年时为 NULL。 |
interval + - interval |
interval |
interval('1 year') + interval('-1 month') 等于 11 months。 |
str + str |
str |
拼接字符串。 |
amount * 数值、数值 * amount |
amount |
例如 units(position) * 2。 |
amount / 数值 |
amount |
|
amount + - amount |
amount |
两个金额的货币必须相同,否则查询失败。 |
- 加、减、乘都是精确的。乘积保留两个操作数的全部小数位:
1000.00 * 1等于1000.00。 - 结果不超过 28 位有效数字时,除法是精确的;否则像 Beancount 一样四舍六入五成双,保留 28 位有效数字:
1 / 3等于0.3333333333333333333333333333。 - 除以零得到
NULL,而不是报错。 - 一元负号可用于
int、decimal、amount、position和inventory。 - 不支持取余运算
%。
=(或==)和!=(或<>)比较两个同类型的值。int和decimal之间可以按数值比较:1 = 1.00为TRUE。<、<=、>和>=只能用于bool、int、decimal、str和date。比较金额时,请用number取出数值再比较。- 字符串比较区分大小写,按字符编码比较,所以
'B' < 'a'。只有~和!~忽略大小写。
| 运算符 | 含义 |
|---|---|
text ~ pattern |
当 pattern 匹配 text 的任意一部分(忽略大小写)时为 TRUE。 |
text !~ pattern |
与 ~ 相反。 |
pattern ?~ text |
区分大小写的匹配。注意模式写在前面,与 beanquery 一致。 |
| 表达式 | 结果 |
|---|---|
'Expenses:Food:Dining' ~ 'food' |
TRUE,部分匹配且忽略大小写 |
'Expenses:Food:Dining' ~ '^Food' |
FALSE,因为 ^ 锚定在开头 |
'Expenses:Food:Dining' ~ 'Dining$' |
TRUE |
'Expenses:Food:Dining' !~ '^Income' |
TRUE |
'Food' ?~ 'Expenses:Food:Dining' |
TRUE |
'food' ?~ 'Expenses:Food:Dining' |
FALSE,因为 ?~ 区分大小写 |
- 需要完整匹配时,请用
^和$锚定模式。 - 模式使用 Rust
regex库的语法。它与 Python 的语法很接近,但不支持环视((?=...)、(?!...))和反向引用。 - 无效的模式会报错,错误位置指向该模式。编译后超过 1 MiB 的模式(例如
'a{1000}{1000}')也会这样报错。 - 两边都必须是字符串。任一边为
NULL时结果为NULL,三个运算符都是如此。
IN 测试一个值是否属于某个集合或列表:
WHERE 'trip-new-york' IN tagsWHERE 'Assets:Cash' NOT IN other_accountsWHERE account IN ('Assets:Cash', 'Assets:Bank:Checking')WHERE year IN (2023, 2024)WHERE payee IN ('Amazon')- 右侧可以是
set类型的值(例如tags、links或other_accounts列),也可以是括号中的表达式列表。列表可以只有一个元素。 - 右侧是集合时,左侧必须是字符串。把集合放在括号里的
'x' IN (tags)同样是测试集合成员。 - 右侧是列表时,每个元素都必须能像
=那样与左侧比较。 NOT IN与IN相反。- 左侧为
NULL时结果为NULL。如果在含有NULL的列表中没有找到该值,结果也是NULL,与标准 SQL 一致。
CASE 按条件选择一个值:
SELECT date, payee, position, CASE WHEN number >= 100 THEN 'large' WHEN number > 0 THEN 'small' ELSE 'refund' END AS sizeWHERE account ~ '^Expenses'- 它的值是第一个为
TRUE的WHEN条件之后THEN的值;如果没有条件为TRUE,就是ELSE之后的值。没有ELSE时,这种情况下它是NULL。 - 与
WHERE一样,值为NULL的条件不算TRUE,会继续尝试下一个WHEN。 - 每个条件都必须是布尔值。各个值必须是同一种类型:
NULL可以匹配任何类型;当另一个值是decimal时,int会扩展为decimal。 - 只计算被选中的值,所以一个会出错的值(例如整数溢出)只会在选中它的行上出错。
- 在聚合查询中,条件和值都可以是聚合:
CASE WHEN count(*) > 1 THEN sum(number) ELSE 0 END。与 SQL 一样,无论分组选中哪个分支,查询中的每个聚合都会累加其分组的每一行,所以CASE中聚合的参数会在每一行上计算,如果出错也会在那里出错:只有分支中普通的表达式才只为被选中的分支计算。 CASE、WHEN、THEN、ELSE和END只在以CASE WHEN开头的CASE表达式中是关键字,在其他地方是普通名字。不支持CASE x WHEN value THEN ...的写法,请写成CASE WHEN x = value THEN ...。CASE是张记账扩展,beanquery 没有条件表达式。
NULL 与三值逻辑
Section titled “NULL 与三值逻辑”缺失的值为 NULL,例如没有收款方的交易的 payee,或者没有按成本持有的分录的成本。
- 算术运算、比较、
~、!~、?~、IN、NOT IN和函数调用只要有一个操作数为NULL,结果就是NULL。例外的是IS NULL、IS NOT NULL、AND、OR和聚合函数。 AND、OR和NOT遵循标准 SQL 的三值逻辑:
| 表达式 | 结果 |
|---|---|
TRUE AND NULL |
NULL |
FALSE AND NULL、NULL AND FALSE |
FALSE |
TRUE OR NULL、NULL OR TRUE |
TRUE |
FALSE OR NULL |
NULL |
NOT NULL |
NULL |
WHERE和FROM把NULL视为不成立,相应的分录会被丢弃。- 请用
IS NULL和IS NOT NULL判断缺失值。payee = NULL的结果永远是NULL,不会是TRUE。 payee != 'Shop'和NOT (payee = 'Shop')都会丢弃payee为NULL的分录。如需保留,请写成payee IS NULL OR payee != 'Shop'。
postings 表
Section titled “postings 表”默认的表是 postings。每笔交易的每条分录对应一行,交易的字段会重复出现在它的每条分录上。其他表存放各种指令,张记账特有的表存放预算和账本错误。
- 包含:所有交易(无论标记是什么),以及张记账为
pad和balance ... with pad ...指令生成的补齐交易。补齐交易的标记为P,收款方为Balance Pad,描述形如pad Assets:Bank to Equity:Opening。带有会计期间子句的查询还会看到这些子句加入的合成交易。 - 不包含:余额断言,以及所有非交易指令,例如
open、close、price、note、document和预算指令。 - 没有写金额的分录,使用张记账在平衡交易时推断出的金额。
- 按成本持有的分录会按批次记账中的规则与批次匹配。减少了多个批次的分录,每个批次产生一行。
- 各行按账本顺序排列:先按日期和时间,再按交易在文件中出现的顺序。
按成本持有的分录,其 position、cost_* 和 weight 列取决于它与哪个批次匹配。这些行就是张记账自己的记账结果:张记账在加载账本时按账户和货币、按账本顺序对每笔交易记账,规则与 Beancount 相同,查询引擎读取记账后的分录。因此查询、流水和商品页面显示的是同样的批次。
- 减仓。按成本持有、且符号与某个未平仓批次相反的分录(例如卖出之前买入的持仓)会减少批次。成本中写出的字段(成本数值和货币、日期、标签)必须与批次一致,没有写出的字段可以匹配任何批次。
-4 AAPL {100 USD}减少以 100 USD 买入的批次,不论日期;-4 AAPL {100 USD, 2024-01-02}只减少 2024-01-02 买入的那一个;-4 AAPL {100 USD, "a"}只减少标签为a的那一个;-4 AAPL {}可以减少任何批次。 - 在匹配的批次中选择。匹配的批次按先进先出(FIFO)使用;如果账户的
booking_method为LIFO,则后进先出。在STRICT下,匹配多个批次的减仓必须把它们全部卖出,否则张记账报告AmbiguousLotMatch错误并按先进先出记账。跨越多个批次的减仓会拆成多行,每个批次一行,每行带有从该批次取出的单位和该批次的成本。 - 加仓。其他按成本持有的分录会开立一个新批次,或者加到完全相同的批次上。没有日期的成本(例如
10 AAPL {100 USD})使用其交易的日期,所以按成本持有的分录的cost_date永远不会是NULL。写成{}的加仓会加入该账户按成本持有的第一个批次,它的行带有那个批次的成本。 - 剩余部分。如果未平仓的批次不足以覆盖整个减仓,张记账报告
NoEnoughCommodityLot错误,并把剩余部分按加仓处理。没有成本数值的成本(例如{})无法开立批次,所以这部分没有成本。 - 总成本。以总成本买入的批次(例如
3 AAPL {{1000 USD}})的cost_number是总成本除以单位数量,与 beanquery 一样保留 28 位有效数字。
记账方法见批次与成本。
| 列 | 类型 | 说明 |
|---|---|---|
date |
date |
交易日期。如果交易带有时间,时间部分会被舍去,可以从 time 读取。 |
year |
int |
date 的年份。 |
month |
int |
date 的月份,1 到 12。 |
day |
int |
date 在当月的日,1 到 31。 |
flag |
str |
交易的标记:*(没有写标记时也是它)、!、表示补齐的 P、表示会计期间子句合成交易的 S、C 或 T,或自定义标记。 |
payee |
str |
交易的收款方,没有则为 NULL。交易头只有一个字符串时,该字符串是描述,收款方为 NULL。 |
narration |
str |
交易的描述,没有则为 ''(空字符串),与 Beancount 一致。 |
description |
str |
用 " | " 连接收款方和描述。缺失或为空的部分会被省略,所以两者都缺失时为 ''。 |
tags |
set |
交易的标签,不含开头的 #。 |
links |
set |
交易的链接,不含开头的 ^。 |
id |
str |
张记账为交易生成的标识符,是一个 UUID。同一交易的所有分录共享这个值。 |
posting_flag |
str |
分录自己的标记,例如 ! Assets:Cash -10 CNY 中的 !;分录没有标记时为 NULL。 |
account |
str |
分录的账户。 |
number |
decimal |
分录的单位数量。 |
currency |
str |
单位的货币(商品)。 |
position |
position |
分录的单位及其成本批次(如果有)。 |
cost_number |
decimal |
单位成本,未按成本持有则为 NULL。用 {{...}} 写出的总成本会除以单位数量。 |
cost_currency |
str |
成本的货币,或 NULL。 |
cost_date |
date |
成本批次的日期,未按成本持有则为 NULL。没有明确写出日期的批次使用开立它的交易的日期。 |
cost_label |
str |
成本批次的标签。未按成本持有时为 ''(空字符串),批次没有标签时为 NULL。 |
price |
amount |
用 @ 写出的单价,没有则为 NULL。用 @@ 写出的总价会除以单位数量。 |
weight |
amount |
分录在交易平衡中所占的金额:按成本持有时为单位数量乘以单位成本;否则如果有价格,为单位数量乘以价格;否则为单位本身。 |
other_accounts |
set |
同一交易中其他分录的账户。 |
meta |
str |
分录的元数据文本:按键排序的 key: "value" 对,用 , 分隔;没有元数据时为 ''。交易自己的元数据用 entry_meta() 读取。 |
metas |
metas |
分录的元数据,以 (key, value) 对的列表给出:按键排序,重复键的每个值按书写顺序保留。见结构化元数据。张记账扩展。 |
entry_metas |
metas |
分录所属交易的元数据,形式相同。张记账扩展。 |
balance |
inventory |
累计余额:截至并包括本行的各行持仓之和。不能用在 FROM 或 WHERE 中。 |
time |
str |
交易在账本时区中的时刻,格式为 HH:MM:SS,即写下的时刻,没有写时为午夜;在夏令时跳过该时刻的那天,向后推迟跳过时段的长度,与张记账存储的一致(纽约 2024-03-10 的 02:30 为 03:30:00,圣保罗 2018-11-04 的午夜为 01:00:00)。张记账扩展。 |
timestamp |
int |
交易日期和时间的 Unix 时间,单位为秒。张记账扩展。 |
seq |
int |
交易在处理顺序中的位置,从 0 开始,与 #entries 中的一致。同一交易的所有分录共享这个值,所以 ORDER BY seq DESC 以稳定的顺序把最新的交易排在最前。会计期间子句的合成交易为 NULL。张记账扩展。 |
posting_index |
int |
分录在其交易中按书写顺序的位置,从 0 开始。批次记账把一条分录拆成每个批次一行时,这些行共享这个值。张记账扩展。 |
account_balance |
inventory |
账户余额:本条分录之后该分录所属账户的余额。张记账扩展。 |
balanced |
bool |
张记账发现交易不平衡(UnbalancedTransaction 错误)时为 FALSE,否则为 TRUE。张记账扩展。 |
errors |
set |
张记账为该交易记录的错误种类,名称与 #errors 的 kind 列相同,例如 UnbalancedTransaction 或 AccountDoesNotExist。没有错误时为空集合。张记账扩展。 |
automatic |
bool |
分录书写时没有金额、由张记账推算出数量来平衡交易时为 TRUE(beancount 称这样的分录为自动分录);写了金额时为 FALSE。补齐交易中补齐来源账户的分录也是自动分录。张记账扩展。 |
balance 列是 position 的累计合计,类型为库存。
- 它从空库存开始,按账本顺序累加通过
FROM和WHERE的各行。被过滤掉的分录不计入。使用WHERE account = 'Assets:Bank:Checking' AND year = 2024时,余额从 2024 年的第一条分录开始从零累计。如果要从账户的真实余额开始,请改用OPEN ON和CLOSE ON限定日期:FROM OPEN ON 2024-01-01 CLOSE ON 2025-01-01 WHERE account = 'Assets:Bank:Checking'。 - 它是所有行的一个总计,而不是每个账户各一个。如果只想跟踪一个账户,请只选择该账户的分录。
- 它在分组、
ORDER BY、DISTINCT和LIMIT之前计算,所以对行排序不会改变它们的余额。使用ORDER BY date DESC时,第一行带有最终余额。 - 它保留批次,所以以不同成本买入的持仓会显示为多个持仓。
units(balance)会把它们合并,cost(balance)给出账面价值。 - 在聚合查询中,它可以用在聚合函数内部。
last(balance)是每组最后一条分录之后的余额。 - 它不能用在
FROM或WHERE中,因为正是这两个子句决定了累加哪些行。这样使用会报错。
SELECT date, payee, position, balanceWHERE account = 'Assets:Bank:Checking'ORDER BY date DESCLIMIT 10这个查询返回该账户最近的十条分录,每条都带有记账之后的余额。第一行显示的就是当前余额。
account_balance 列是本条分录之后,分录所属账户的余额。它和 balance 一样是保留批次的库存,但与 balance 不同,它不受查询的影响:
- 它按账本顺序累加该账户的所有分录,不论
FROM、WHERE和LIMIT选择了哪些行。即使查询只显示账户的部分分录,每条分录显示的仍是该分录之后账户真实的余额。 - 每个账户各有一个余额,所以涉及多个账户的查询中,每条分录显示的是它自己账户的余额。
- 使用会计期间子句时,它累加这些子句产生的分录,从
OPEN ON加入的期初余额开始。 - 与
balance不同,它可以用在WHERE中。
SELECT date, payee, position, account_balanceWHERE account = 'Assets:Bank:Checking' AND year = 2024ORDER BY seq DESC这个查询按从新到旧列出该账户 2024 年的分录,每条都带有之后的账户余额,其中包括 2024 年之前记入的所有金额。
除了 postings,查询还可以用 FROM #name 读取下面的表。它们就是 beanquery 的表,列名、类型和行的顺序都与 beanquery 相同:
| 表 | 每一行是 | SELECT * |
|---|---|---|
#entries |
任意一条指令 | id, type, filename, date, year, month, day, flag, payee, narration, description, tags, links, meta, accounts |
#transactions |
一笔交易 | date, flag, payee, narration, tags, links, accounts |
#prices |
一条 price 指令 |
date, currency, amount |
#balances |
一条余额断言 | date, account, amount, tolerance, discrepancy |
#notes |
一条 note 指令 |
date, account, comment, tags, links |
#events |
一条 event 指令 |
date, type, description |
#documents |
一条 document 指令,其后是交易或分录的一个 document 元数据 |
date, account, filename, tags, links |
#accounts |
一个有 open 或 close 指令的账户 |
account, open, close |
#commodities |
一条 commodity 指令 |
meta, date, name |
张记账还有四张自己的表 #budgets、#budget_definitions、#budget_events 和 #errors,见张记账特有的表。
SELECT currency, last(amount) AS latestFROM #pricesWHERE date >= 2024-01-01GROUP BY currencyORDER BY currency- 每个表有自己的列。一个表只有为它列出的列,没有
postings的列。year、month和day只存在于#entries和postings中,其他表请使用year(date)等日期函数。所有函数、聚合函数,以及GROUP BY、HAVING、ORDER BY、PIVOT BY、DISTINCT和LIMIT都可以用于每个表。 - 行的顺序。没有
ORDER BY时,各行按账本顺序排列:先按日期,再按 beancount 对同一天指令的排序(open最先,然后是余额断言、其他指令,document和close最后),再按指令在文件中的顺序。 - 元数据。每个指令表都有一列
meta,以文本形式给出指令的元数据:按键排序的key: "value"对,用,分隔;没有元数据时为''。#entries和#transactions还有metas列,以结构化的键值对给出同样的元数据。meta(key)、entry_meta(key)和any_meta(key)读取该行指令的某个键(在#accounts中读取其open指令),meta_values(key)和entry_meta_values(key)读取该键的所有值。 - 余额断言不是交易。断言不记任何账;它在
#entries中是一条balance记录,在#balances中是一行。加载账本时被张记账拒绝的交易也不会出现。pad和balance ... with pad生成的补齐交易(标记为P)与 beancount 一样算作交易。 - 张记账扩展。有些表有 beanquery 没有的列,下文标为张记账扩展:
#entries的seq、time、timestamp和metas;#transactions的id、seq、time、timestamp、balanced、errors和metas;#prices的time和timestamp;#balances的actual、passed、pad、id、seq、time和timestamp;以及#documents的source、path、transaction_id、seq、time和timestamp。它们排在 beanquery 的列之后,不属于SELECT *,因此SELECT *得到的列与 beanquery 相同。postings 表有它自己的扩展列,#budgets、#budget_definitions、#budget_events和#errors是张记账自己的表。
#entries、#transactions、#balances、#documents 和 postings 表的 seq 列是记录在张记账处理账本的顺序中的位置,从 0 开始:
- 先按日期和写下的时刻;没有写时刻的指令在午夜;
- 同一时刻内,先是
open和commodity指令,然后是余额记录(余额断言,以及标记为P的交易:balance ... with pad的补齐交易和手写的P交易),再是其他所有指令; - 再按指令在文件中的顺序;
- 例外:
balance ... with pad在同一时刻的其他余额记录(包括它的补齐交易)之后才检查,它的seq就是检查它的位置; - 另外,
pad排在它当天所有余额记录之后,不论它们的时刻(当最后一条的时刻晚于它自己时,它取那个时刻),它的补齐交易紧跟在它之后。
余额就是按这个顺序变化的:分录的累计余额按这个顺序相加,一个断言紧跟在它的 actual 余额所包含的分录之后,所以按 seq 合并 #balances 和分录的行,每个断言都会在正确的位置。postings 表和 #transactions 的行就是按这个顺序排列的。没有 ORDER BY 时,#entries 和其他指令表的行保持 beancount 的顺序:先按日期,再按种类:open 最先(在同一天的 commodity 之前),然后是余额断言、其他指令,document 和 close 最后,不论它们的时刻。两种顺序只在同一天之内不同:张记账让同一天的 commodity 和 open 指令保持文件中的顺序,按时刻排列一天中的指令(写了时刻的 open 排在没有写时刻的交易之后),把余额断言排在该时刻之前的交易之后、写在它之前的补齐之后,并让 document 和 close 留在原位。ORDER BY seq 按张记账的顺序列出各行。
决定顺序的是写下的时刻。在夏令时跳过某段时间的那天,写在跳过时段中的指令存储时向后推迟跳过时段的长度,所以 ORDER BY seq 在那里列出的 time 和 timestamp 列可能不是递增的:在纽约 2024-03-10,写在 02:30、存储为 03:30:00 的记录排在写在 03:15 的记录之前。
SELECT seq, date, time, type FROM #entries WHERE date = 2024-01-05 ORDER BY seq#entries
Section titled “#entries”| 列 | 类型 | 说明 |
|---|---|---|
id |
str |
指令的唯一 ID。交易的 ID 就是交易本身的 ID,与其分录的 id 列相同;余额断言的 ID 是张记账存储其检查结果时使用的 ID。 |
type |
str |
指令的种类,小写:transaction、open、close、balance、price、note、document、event、commodity、custom、query 或 pad,以及张记账的 budget、budget-add、budget-transfer 和 budget-close。balance ... with pad 算作 balance。 |
filename |
str |
指令所在的账本文件。 |
date、year、month、day |
date、int |
指令的日期及其各部分。 |
flag、payee、narration、description |
str |
对交易而言与 postings 中的同名列相同;其他指令为 NULL。 |
tags、links |
set |
交易、note 或 document 的标签和链接;其他指令为 NULL。 |
meta |
str |
指令的元数据。 |
accounts |
set |
指令涉及的账户:交易的各分录账户,open、close、balance、note 或 document 的账户,以及 pad 或 balance ... with pad 的被填充账户和填充账户。其他指令为空集合。 |
seq |
int |
指令在处理顺序中的位置,从 0 开始。ORDER BY seq DESC 把最新的记录排在最前。没有 ORDER BY 时各行保持 beancount 的顺序,在同一天之内可能与之不同。张记账扩展。 |
time、timestamp |
str、int |
指令在账本时区中的时刻(HH:MM:SS),与 postings 中相同,即写下的时刻,没有写时为午夜;在夏令时跳过该时刻的那天,向后推迟跳过时段的长度,与张记账存储的一致(纽约 2024-03-10 的 02:30 为 03:30:00,圣保罗 2018-11-04 的午夜为 01:00:00);以及其日期和时间的 Unix 时间,单位为秒。张记账扩展。 |
metas |
metas |
指令的元数据,以 (key, value) 对给出,见结构化元数据。张记账扩展,不包含在 SELECT * 中。 |
#transactions
Section titled “#transactions”| 列 | 类型 | 说明 |
|---|---|---|
date |
date |
交易日期。 |
flag |
str |
*、!,补齐交易为 P。 |
payee |
str |
收款方,或 NULL。 |
narration |
str |
描述,没有时为 ''。 |
tags、links |
set |
标签和链接。 |
accounts |
set |
各分录的账户。 |
meta |
str |
交易的元数据。 |
id |
str |
张记账为交易生成的标识符:与其分录的 id 以及它在 #entries 中那一行的 id 相同。张记账扩展。 |
seq、time、timestamp |
int、str、int |
与 postings 中相同:交易在处理顺序中的位置、交易的时刻和 Unix 时间。张记账扩展。 |
balanced、errors |
bool、set |
与 postings 中相同:交易是否平衡,以及为它记录的错误种类。张记账扩展。 |
metas |
metas |
交易的元数据,以 (key, value) 对给出,见结构化元数据。张记账扩展,不包含在 SELECT * 中。 |
#prices、#balances、#notes、#events、#documents 和 #commodities
Section titled “#prices、#balances、#notes、#events、#documents 和 #commodities”| 表 | 列 | 类型 | 说明 |
|---|---|---|---|
#prices |
date |
date |
价格的日期。 |
currency |
str |
被定价的商品。 | |
amount |
amount |
一单位商品的价格。 | |
time、timestamp |
str、int |
价格在账本时区中的时刻(HH:MM:SS,没有写时间时为 00:00:00),以及其日期和时间的 Unix 时间(秒),与 #entries 相同。同一天的多个价格可以据此区分,它们按时间排序。张记账扩展。 |
|
#balances |
date |
date |
断言的日期。 |
account |
str |
被断言余额的账户。 | |
amount |
amount |
断言的余额。 | |
tolerance |
decimal |
显式给出的容差(~ 0.01),或 NULL。 |
|
discrepancy |
amount |
断言不成立时为 actual 减去断言金额;成立时为 NULL。balance ... with pad 也会检查:除非同一时间在它之后的填充改变了它的余额,或者它从被断言的账户本身或其子账户填充(这不会改变它的余额),它总是成立。 |
|
actual |
amount |
断言时账户在断言货币下的真实余额:此前记到这个账户及其子账户的所有分录的数量之和,与张记账检查余额的方式一致。断言从不改变它。balance ... with pad 在同一时间的填充都记账之后检查。张记账扩展。 |
|
passed |
bool |
断言是否成立,与张记账的余额检查一致:actual 与断言金额之差在容差之内;断言没有容差时两者必须相等。不成立的断言也是 #errors 中的一条 AccountBalanceCheckError。张记账扩展。 |
|
pad |
str |
balance ... with pad 用来补齐的账户;没有 pad 的断言为 NULL。张记账扩展。 |
|
id |
str |
张记账存储该断言检查结果时使用的 id,也就是 GET /api/journals 列出它时使用的 id:它在 #entries 中那一行的 id。张记账扩展。 |
|
seq |
int |
断言在处理顺序中的位置,即张记账检查它的位置,与 #entries 中的一致:紧跟在它的 actual 余额所包含的分录之后。张记账扩展。 |
|
time、timestamp |
str、int |
断言在账本时区中的时刻(HH:MM:SS,与 #entries 中相同)及其 Unix 时间(秒)。张记账扩展。 |
|
#notes |
date、account |
date、str |
备注的日期和账户。 |
comment |
str |
备注的内容。 | |
tags、links |
set |
标签和链接。 | |
#events |
date |
date |
事件的日期。 |
type |
str |
事件的种类,例如 location。 |
|
description |
str |
事件的值,例如一个城市。 | |
#documents |
date、account |
date、str |
文档的日期和账户。对于元数据中的文档,日期是交易的日期,账户是分录的账户;交易本身的文档账户为 NULL。 |
filename |
str |
文件的路径。与 beancount 一样,相对路径相对于声明它的账本文件所在的目录。 | |
tags、links |
set |
document 指令的标签和链接,或者引用该文档的交易的标签和链接。 |
|
source |
str |
文档的来源:document 指令为 'directive',交易或其分录的 document 元数据分别为 'transaction' 和 'posting'。张记账扩展。 |
|
path |
str |
文件在账本中的路径,网页界面用它列出和下载文件。在张记账文件中,即按原样书写、相对于账本目录的路径,位于账本目录内的绝对路径会转换为相对于该目录的路径。Beancount 文件中的 document 指令则是张记账加载账本时解析出的路径:相对于该指令所在的文件,或在只有相对于账本目录才能找到文件时相对于账本目录(见路径)。张记账扩展。 |
|
transaction_id |
str |
元数据中的文档所属交易的 id,与 postings 表中的一致。document 指令为 NULL。张记账扩展。 |
|
seq |
int |
document 指令,或在元数据中提到该文档的交易的 seq。张记账扩展。 |
|
time、timestamp |
str、int |
document 指令,或在元数据中提到该文档的交易的时刻(HH:MM:SS,与 #entries 中相同)及其 Unix 时间(秒)。张记账扩展。 |
|
#commodities |
date |
date |
commodity 指令的日期。 |
name |
str |
商品,例如 USD。 |
这些表都还有一列 meta。对于元数据中的文档,meta 和 meta(key) 读取引用它的交易或分录的元数据。
*张记账扩展。*除了 document 指令,#documents 还与网页界面的文档页面一样,列出交易在元数据中引用的每个文档:在指令之后,按账本顺序,交易的 document 元数据键的每个值一行,然后是其各分录的。重复的键每个值各一行。被拒绝的交易的文档不会列出。
SELECT date, account, path, transaction_idFROM #documentsWHERE source != 'directive'ORDER BY date DESC#accounts
Section titled “#accounts”open 和 close 是账户的 open 和 close 指令,是结构化的值,用点号读取它们的字段:
| 列 | 类型 | 说明 |
|---|---|---|
account |
str |
账户名。 |
open、open.date |
date |
open 指令的日期,没有时为 NULL。单独使用 open 时读作这个日期。 |
open.account |
str |
open 指令的账户。 |
open.currencies |
set |
账户限定的货币;不限定时为 NULL。 |
open.booking |
str |
记账方法,取自 booking_method 元数据,或 NULL。 |
open.meta |
str |
open 指令的元数据。 |
close、close.date |
date |
close 指令的日期,账户未关闭时为 NULL。单独使用 close 时读作这个日期。 |
close.account、close.meta |
str |
close 指令的账户和元数据。 |
SELECT account, open.date, open.currenciesFROM #accountsWHERE close IS NULLORDER BY account不存在的指令的字段为 NULL。未知的字段(例如 open.datum)会报错,错误位置指向该字段。
张记账特有的表
Section titled “张记账特有的表”张记账有四张自己的表,存放 Beancount 中没有的数据:#budgets 是预算的逐月数据,#budget_definitions 是每个预算的定义,#budget_events 是预算指令产生的变动,#errors 是张记账在账本中发现的问题。它们与其他表一样用 FROM #budgets、FROM #budget_definitions、FROM #budget_events 和 FROM #errors 读取,规则也相同:一个表只有自己的列,例如 account 不是 #budgets 的列,所有子句和函数都可以用于它。与指令表不同,它们没有 meta 列,行的顺序见下面各表的说明。meta(key)、entry_meta(key) 和 any_meta(key) 读取的都是该行自己的元数据。
#budgets 中每个预算每个月对应一行,数据与网页界面的预算页面在该月显示的一致。
- 每个预算从其
budget指令所在的月份起,每个月都有一行,直到以下三个月份中最晚的一个:该预算最后一次budget-add、budget-transfer或budget-close所在的月份,账本中最后一笔交易所在的月份,以及当前月份,即账本时区中today()所在的月份。因此,用budget-add为未来月份提前安排的预算会显示那个月,WHERE date = yearmonth(today())也会列出本月的每个预算,即使本月还没有发生任何事。价格、事件、备注、余额断言等其他指令不会延长这些月份。没有预算条目、也没有支出的月份同样有一行,可用金额顺延到这个月,与预算页面一致。最后一行之后的月份没有行:它就是该预算最后一行顺延过去、没有任何支出的样子。 - 这些月份是生成的,而不是从账本中读取的,所以每个月份都计入结果大小限制,即使它随后被
WHERE丢弃。例外是只保留某个日期之前月份的WHERE,即用AND连接的条件中有date <= 2024-06-01、date < :month、date = :month或yearmonth(date) = :month这样的条件:此后的月份不会生成。只查询到所看的月份为止,这样的查询就很快,而且无论账本中有什么日期笔误都能正常工作。如果某笔交易或某条预算指令的日期被误写成遥远的未来,或某条budget指令的日期被误写成遥远的过去,查询会以“结果过大”的错误结束,而不会耗尽内存。错误信息会指出月份最多的预算,以及决定其结束月份的指令,例如budget 'food' runs from 2024-01 until 2204-05 because of a transaction dated 2204-05-01 (main.zhang); check that date;如果它的月份一直延续到当前月份,则会指出它的budget指令。改正日期后即可再次查询该表。 assigned、activity和available即预算页面上的 Assigned、Activity 和 Available 列。assigned是这个月的起始金额(上个月月底仍可用的金额),加上本月budget-add和budget-transfer指令放入的金额(added)。activity是预算关联的账户在本月的支出,available即assigned - activity,会顺延到下个月。- 所有金额都以预算的商品计。
activity把预算关联账户的分录相加,每笔分录都按其日期折算为预算的商品,与convert(position, currency, date)用账本中的价格折算的结果相同:activity就是对这些分录求sum(convert(position, 'CNY', date))的结果。以其他商品计的budget-add或budget-transfer金额,按指令的日期以同样方式折算。没有价格可以折算的分录或金额不计入,而不会被当作另一种商品的数字加进去。 - 预算从其
budget指令起才存在。针对尚不存在的预算的budget-add、budget-transfer或budget-close不起作用,预算的budget指令之前的分录也不算它的支出;张记账会把两者都报告为错误。同名的第二条budget指令是重复定义,会被忽略。 - 由于
assigned包含顺延的金额,把多个月的assigned相加会把同一笔钱算多次。要统计一段时间内一共安排了多少预算,请对added求和。 - 预算关联的账户,是
open指令中带有指向它的budget元数据(例如budget: food)的账户。这些账户的分录就是该预算的支出。每一条元数据都算数,所以open中同时有budget: food和budget: fun的账户同属两个预算。一笔分录计入其日期当时生效的账户open所指的预算:账户关闭后以其他预算重新开启,从重新开启起计入新的预算,之前的分录仍属原来的预算。可以用account_budgets(account, date)查看。 meta(key)读取budget指令的元数据。- 各行先按预算名称、再按月份排列。
SELECT *是SELECT name, date, assigned, activity, available的简写。
| 列 | 类型 | 说明 |
|---|---|---|
name |
str |
预算的名称,即指令中写的名字。 |
alias |
str |
预算的显示名称,来自 alias 元数据,没有则为 NULL。 |
category |
str |
预算页面对预算分组所用的类别,来自 category 元数据,没有则为 NULL。 |
currency |
str |
预算使用的商品(货币)。 |
date |
date |
该月的第一天。 |
year |
int |
该月所在的年份。 |
month |
int |
月份,1 到 12。 |
assigned |
amount |
本月分配给该预算的金额:从上个月顺延的可用金额加上 added。 |
added |
amount |
本月 budget-add 和 budget-transfer 指令放入该预算的金额,按指令的日期折算为预算的商品。从该预算转出的金额计为负数。 |
activity |
amount |
预算关联的账户在本月的支出,每笔分录按其日期折算为预算的商品。退款计为负数。 |
available |
amount |
月底剩余的金额,即 assigned - activity。它会顺延到下个月,超支时为负数。 |
accounts |
set |
其分录计入该预算支出的账户:任何时候有 open 指向该预算的账户。 |
closed |
bool |
该预算是否已在本月或更早用 budget-close 关闭。在此之前的月份为 FALSE。 |
按预算页面的分组,查看每个预算还剩多少:
SELECT category, name, availableFROM #budgetsWHERE date = 2024-06-01ORDER BY category, name2024 年每个预算安排了多少、花了多少:
SELECT name, sum(added) AS budgeted, sum(activity) AS spentFROM #budgetsWHERE year = 2024GROUP BY nameORDER BY spent DESC未关闭的预算在哪些月份超支:
SELECT date, name, availableFROM #budgetsWHERE number(available) < 0 AND NOT closed每个预算的当前状态,即它最后一个月的数据:
SELECT name, last(available) AS available, last(closed) AS closedFROM #budgetsGROUP BY nameORDER BY name#budget_definitions 中每个预算对应一行,内容是其预算指令定义的信息。它没有月份,也不读取任何交易,所以无论账本中的日期如何,都能回答一个预算是什么。
- 各行按预算名称排序。
SELECT *给出所有列。 meta(key)读取budget指令的元数据。
| 列 | 类型 | 说明 |
|---|---|---|
name |
str |
预算的名称。 |
date |
date |
budget 指令的日期,预算从这一天起存在。 |
currency |
str |
预算使用的商品。 |
alias |
str |
预算的显示名称,来自其 alias 元数据;没有时为 NULL。 |
category |
str |
预算的分类,来自其 category 元数据;没有时为 NULL。 |
accounts |
set |
其分录计入该预算支出的账户,与 #budgets 相同。 |
close |
date |
预算第一条 budget-close 的日期,预算从此关闭;仍在使用时为 NULL。之后的 budget-close 不再改变什么。 |
仍在使用的预算及其账户:
SELECT name, currency, accountsFROM #budget_definitionsWHERE close IS NULL#budget_events 中每条预算指令产生的每个变动对应一行,按账本顺序排列:即预算页面列出的某个月的变动,另加关闭预算。
budget-add是一条assign,金额为其金额。budget-transfer是两行:先是转出预算的transfer_out,金额取负;然后是转入预算的transfer_in。budget-close是一条close,没有金额。- 金额按原样书写,以指令中的商品计;
#budgets会把它们折算为预算的商品。正数表示增加预算。 - 因为预算尚不存在而不起作用的指令没有对应的行。
meta(key)读取指令的元数据。SELECT *给出所有列。
| 列 | 类型 | 说明 |
|---|---|---|
name |
str |
预算的名称。 |
date |
date |
指令的日期。 |
time |
str |
指令在账本时区中的时刻,格式为 HH:MM:SS;没有时刻的指令为 00:00:00。 |
timestamp |
int |
指令在账本时区中的日期和时刻对应的 Unix 时间(秒)。 |
type |
str |
'assign'、'transfer_out'、'transfer_in' 或 'close'。 |
amount |
amount |
放入预算的金额:转出为负数,关闭预算为 NULL。 |
2024 年各预算放入了多少(按原样书写):
SELECT name, sum(amount) AS addedFROM #budget_eventsWHERE year(date) = 2024 AND type != 'close'GROUP BY name#errors 中每个账本错误对应一行,即网页界面的错误页面列出、GET /api/errors 返回的那些问题。
kind是错误码,例如UnbalancedTransaction。错误码解释了每个代码及其修复方法。message是错误页面为它显示的那句话(英文界面中的文字)。file是引发错误的指令所在的文件,路径相对于账本目录,与网页界面的文件列表一致。source是该指令的文本。line和column是该指令开始的行号和列号,从 1 起算,列号按字符计;指令不是从文件读取的(例如插件生成的)时,两者为NULL。date是该指令的日期;没有日期的指令(例如option)为NULL。account是错误涉及的账户,只有指明了账户的错误才有,例如AccountDoesNotExist、AccountClosed和AccountBalanceCheckError。meta(key)读取张记账为错误记录的其他信息,metas列出全部信息。交易中的错误,meta('txn_id')是该交易的id,与 postings 表中的一致。分录引用了未定义的预算时,有meta('budget_name')。id是错误在GET /api/errors中的 id,span_start和span_end是引发错误的指令在其文件中开始和结束的位置(字节偏移)。id 由指令的位置得出,因此同一条指令的错误共用一个 id,交易中的错误的 id 就是该交易的id。- 各行先按文件、再按在文件中的位置排列。
SELECT *是SELECT file, date, kind, account, message的简写。
| 列 | 类型 | 说明 |
|---|---|---|
kind |
str |
错误码,例如 UnbalancedTransaction,即 GET /api/errors 中的 error_type。 |
message |
str |
错误页面对该错误的说明。 |
file |
str |
引发错误的指令所在的文件,路径相对于账本目录;文件不在账本目录中时为完整路径。 |
line |
int |
引发错误的指令在其文件中开始的行号,从 1 起算;未知时为 NULL。 |
column |
int |
该指令在行中开始的列号,从 1 起算,按字符计;未知时为 NULL。 |
date |
date |
指令的日期,没有日期的指令为 NULL。 |
account |
str |
错误涉及的账户,错误没有指明账户时为 NULL。 |
source |
str |
引发错误的指令的文本。 |
id |
str |
错误的 id,即 GET /api/errors 中的 id。 |
span_start |
int |
引发错误的指令在其文件中开始的字节偏移;未知时为 NULL。 |
span_end |
int |
引发错误的指令在其文件中结束的字节偏移;未知时为 NULL。 |
metas |
metas |
张记账为该错误记录的信息,例如 txn_id 和 account_name,按键排序的结构化键值对:即 GET /api/errors 的 metas。 |
每种错误各有多少:
SELECT kind, count(*) AS errorsFROM #errorsGROUP BY kindORDER BY errors DESC按文件中的顺序列出某个文件的错误:
SELECT date, kind, account, sourceFROM #errorsWHERE file = 'data/2024.zhang'| 类型 | 说明 | 示例 |
|---|---|---|
null |
NULL 字面量的类型。其他所有类型也都可以为 NULL。 |
NULL |
bool |
TRUE 或 FALSE。 |
TRUE |
int |
64 位整数。 | 2024 |
decimal |
任意精度的精确十进制数。 | 12.50 |
str |
文本。 | 'Expenses:Food' |
date |
日历日期。 | 2024-01-31 |
set |
无序的字符串集合,用于 tags、links 和 other_accounts。 |
{'trip', 'food'} |
amount |
带货币的十进制数,即金额。 | 12.50 USD |
position |
持仓:单位(一个金额)加上可选的成本批次。成本批次包含单位成本的数值和货币,还可以有日期和标签。 | 10 VTI {120.00 USD, 2024-01-02, "lot-a"} |
inventory |
库存:由多个持仓组成,可以包含任意多种货币和成本批次。 | -30.00 USD, 10 VTI {120.00 USD} |
interval |
由月数和天数组成的日历间隔,由 interval() 构造,用于与日期相加或对日期分箱。 |
1 year 2 months |
metas |
元数据,即有序的 (key, value) 对列表,见结构化元数据。张记账扩展。 |
invoice: a.pdf; invoice: b.pdf |
持仓如何合并为库存:
sum(position)把所有持仓加到同一个库存中。- 货币相同且成本批次(数值、货币、日期和标签)相同的持仓会合并,数量相加。成本批次不同的持仓保持独立,因此以不同价格买入的持仓仍然分开显示。
- 数量变为零的持仓会被移除,所以余额为零的账户得到一个空库存。
int 与 decimal 一起运算,或者传给需要 decimal 的函数时,会被转换为 decimal。除此之外,只有字面量中提到的字符串转日期规则,不会发生其他隐式转换。在 position、amount 和 inventory 之间转换请使用估值函数。
ORDER BY、min 和 max 按以下规则对同类型的值排序。比较运算符 <、<=、> 和 >= 使用同样的顺序,但只接受列表中的前四种类型和 bool。
int和decimal:按数值。str:按字符编码,因此区分大小写。date:按时间先后。bool:FALSE排在TRUE之前。set:按排序后的元素逐个比较。amount:先按货币,再按数值。position:Beancount 的持仓顺序。USD、EUR、JPY、CAD、GBP、AUD、NZD和CHF按此顺序排在最前,其他货币随后,货币名较短的在前。再依次按成本数值、成本货币和单位数量排序。inventory:把其中的持仓按持仓顺序排列后逐个比较。因此只含一种货币的库存按数值排序,按收款方统计支出示例中的ORDER BY total DESC正是依赖这一点。metas:逐对比较,先比键,再比值。interval:与 beanquery 一样,间隔没有顺序(1 month既不大于也不小于30 days),所以<、<=、>、>=、ORDER BY、min、max和PIVOT BY都不接受间隔。间隔可以相等或不相等:=、!=、IN、GROUP BY和DISTINCT比较它们的月数(一年计为十二个月)和天数,所以interval('12 months') = interval('1 year')。beanquery 不接受对间隔使用=和!=。
set、inventory 和 metas 类型的值不能作为分组键。
下面每个表格中,每个重载占一行,签名与 GET /api/query/schema 返回的完全一致。any 表示任意类型的参数。需要 decimal 的地方也接受 int 参数。函数名不区分大小写。
任一参数为 NULL 时,标量函数直接返回 NULL,不会执行函数本身。聚合函数则跳过 NULL 值。
聚合函数把一个分组中所有分录的值合并成一个值,见 GROUP BY。
| 签名 | 说明 |
|---|---|
count(*) -> int |
分组中的分录数。 |
count(any) -> int |
分组中参数不为 NULL 的分录数。 |
sum(int) -> int |
整数之和。 |
sum(decimal) -> decimal |
数值之和。 |
sum(amount) -> inventory |
各金额之和,按货币分开。 |
sum(position) -> inventory |
各持仓之和,成本批次按类型中描述的方式合并。 |
sum(inventory) -> inventory |
各库存之和。 |
first(any) -> any |
按账本顺序,分组中第一个非 NULL 的值,类型与参数相同。 |
last(any) -> any |
按账本顺序,分组中最后一个非 NULL 的值,类型与参数相同。 |
min(any) -> any |
按排序与比较中的顺序,最小的非 NULL 值。 |
max(any) -> any |
最大的非 NULL 值。 |
first和last按账本顺序计算,ORDER BY不会改变哪条分录算作第一条。- 如果一个分组的值全部为
NULL,sum的结果为0(或空库存),first、last、min和max的结果为NULL。
| 签名 | 说明 |
|---|---|
units(position) -> amount |
持仓的单位,不含成本。 |
units(inventory) -> inventory |
每个持仓的单位,不含成本。同一货币的不同成本批次合并为一个持仓。 |
cost(position) -> amount |
持仓的总成本(单位数量乘以单位成本),以成本货币计。未按成本持有的持仓返回其单位。 |
cost(inventory) -> inventory |
对每个持仓应用 cost 后按货币求和。 |
convert(amount, str) -> amount |
按最新价格把金额换算为第二个参数指定的货币。 |
convert(amount, str, date) -> amount |
同上,但使用不晚于该日期的最新价格。 |
convert(position, str) -> amount |
把持仓的单位换算为指定货币。成本不会被当作价格使用,但成本货币可以作为中间步骤(见下文)。 |
convert(position, str, date) -> amount |
同上,但使用不晚于该日期的最新价格。 |
convert(inventory, str) -> inventory |
把每个持仓换算为指定货币后求和。 |
convert(inventory, str, date) -> inventory |
同上,但使用不晚于该日期的最新价格。 |
value(position) -> amount |
按最新价格计算的持仓市值,以成本货币计。未按成本持有或没有价格的持仓返回其单位。 |
value(position, date) -> amount |
同上,但使用不晚于该日期的最新价格。 |
value(inventory) -> inventory |
对每个持仓应用 value 后求和。 |
value(inventory, date) -> inventory |
同上,但使用不晚于该日期的最新价格。 |
getprice(str, str) -> decimal |
第一种货币一个单位以第二种货币计的最新价格,例如 getprice('VTI', 'USD'),没有价格时为 NULL。货币名会转换为大写。 |
getprice(str, str, date) -> decimal |
同上,但使用不晚于该日期的最新价格。 |
价格的查找规则:
- 价格来自账本中的
price指令。 - 提供了
date参数时,使用日期不晚于该日期的最新价格;未提供时,使用账本中最新的价格,即使它的日期在未来。 - 同一货币对在同一天有多个价格时,以账本中最后一个为准。
- 价格可以双向使用。
price VTI 120 USD既可以按 120 把 VTI 换算为 USD,也可以按 1/120 把 USD 换算为 VTI。如果一个货币对在两个方向上都有报价,报价点较少的方向会被取倒数,合并到另一个方向中。 convert先查找从单位货币到目标货币的价格。如果没有,而持仓是按成本持有的,就通过成本货币分两步换算:先从单位货币换算为成本货币,再从成本货币换算为目标货币。例如10 VTI {100 EUR}可以借助VTI/EUR价格和EUR/USD价格换算为 USD。- 找不到价格时,值保持不变:仍是原来的货币,不会被丢弃,也不会变成零。因此
convert或value之后的库存仍可能包含多种货币。 - 把一个值换算为它本身的货币,会原样返回。
- 与价格相乘的结果需要超过 28 位有效数字时,会像 Beancount 一样舍入到 28 位。
| 签名 | 说明 |
|---|---|
number(amount) -> decimal |
金额的数值。 |
currency(amount) -> str |
金额的货币。 |
commodity(amount) -> str |
与 currency 相同。 |
only(str, inventory) -> amount |
库存中某一种货币的单位总数,例如 only('USD', sum(position))。库存中没有该货币时为该货币的 0。 |
filter_currency(position, str) -> position |
持仓的单位是该货币时返回该持仓,否则返回 NULL。 |
filter_currency(inventory, str) -> inventory |
库存中单位为该货币的持仓。 |
abs(int) -> int |
绝对值。 |
abs(decimal) -> decimal |
绝对值。 |
abs(amount) -> amount |
数值取绝对值后的金额。 |
abs(position) -> position |
单位取绝对值后的持仓,成本保持不变。 |
abs(inventory) -> inventory |
对每个持仓应用 abs。 |
neg(int) -> int |
取相反数,与一元负号相同。 |
neg(decimal) -> decimal |
取相反数。 |
neg(amount) -> amount |
取相反数后的金额。 |
neg(position) -> position |
单位取相反数后的持仓,成本保持不变。 |
neg(inventory) -> inventory |
每个持仓都取相反数。 |
possign(decimal, str) -> decimal |
除非第二个参数给出的账户属于 Assets 或 Expenses,否则把第一个参数的符号取反。这样收入、负债和权益的金额都显示为正数。 |
possign(amount, str) -> amount |
对金额做同样的处理。 |
possign(position, str) -> position |
对持仓做同样的处理。 |
possign(inventory, str) -> inventory |
对库存做同样的处理。 |
| 签名 | 说明 | 示例 |
|---|---|---|
root(str) -> str |
账户名的第一段。 | root('Expenses:Food:Dining') 为 'Expenses' |
root(str, int) -> str |
账户名的前 n 段。如果账户只有 n 段或更少,则原样返回。 |
root('Expenses:Food:Dining', 2) 为 'Expenses:Food' |
parent(str) -> str |
去掉最后一段后的账户名。顶级账户的结果为 ''。 |
parent('Expenses:Food:Dining') 为 'Expenses:Food' |
leaf(str) -> str |
账户名的最后一段。 | leaf('Expenses:Food:Dining') 为 'Dining' |
account_sortkey(str) -> str |
一个排序键,先按账户类型排序(顺序为 Assets、Liabilities、Equity、Income、Expenses),再按名字排序。它由类型的序号(0 到 4)、- 和账户名组成。第一段不完全等于这些类型之一的名字得到 5,因此排在它们之后。BALANCES 按这个键排序。 |
account_sortkey('Expenses:Food') 为 '4-Expenses:Food' |
under(str, str) -> bool |
账户是否就是第二个参数,或者是它的子账户:等于它,或以它加 : 开头。名字只是开头相同的兄弟账户不算。张记账扩展。 |
under('Assets:Bank:Cash', 'Assets:Bank') 为 TRUE,under('Assets:Banking', 'Assets:Bank') 为 FALSE |
账户与商品指令
Section titled “账户与商品指令”这些函数与 beanquery 一样读取账本中的 open、close 和 commodity 指令,所以在每张表上都能使用。一个账户的指令是它最早的 open 和最早的 close;一种货币的指令是它最后一条 commodity 指令。未知的账户或货币得到 NULL。
| 签名 | 说明 |
|---|---|
open_date(str) -> date |
账户 open 指令的日期;没有时为 NULL。 |
close_date(str) -> date |
账户 close 指令的日期;账户未关闭时为 NULL。 |
open_meta(str, str) -> str |
账户 open 指令的某个元数据值,例如 open_meta(account, 'institution');没有设置时为 NULL。 |
open_meta(str) -> metas |
账户 open 指令的全部元数据,以结构化的键值对给出:没有元数据时为空列表,账户没有 open 指令时为 NULL。 |
commodity_meta(str, str) -> str |
货币 commodity 指令的某个元数据值,例如 commodity_meta(currency, 'name')。 |
commodity_meta(str) -> metas |
货币 commodity 指令的全部元数据:没有元数据时为空列表,没有 commodity 指令时为 NULL。 |
currency_meta(str, str) -> str、currency_meta(str) -> metas |
与 commodity_meta 相同。 |
account_budgets(str, date) -> set |
账户在某个日期所属的预算:该日期或之前最近一条 open 的 budget 元数据所指的预算,所以账户关闭后以其他预算重新开启,从重新开启起属于新的预算。在第一条 open 之前为空集合。张记账扩展。 |
元数据不会继承:即使 Assets:Bank 有 institution,open_meta('Assets:Bank:Checking', 'institution') 仍为 NULL。beanquery 的单参数形式返回字典,其中还有 filename 和 lineno;张记账只返回指令自身的元数据。
| 签名 | 说明 | 示例 |
|---|---|---|
year(date) -> int |
年份。 | year(2024-05-17) 为 2024 |
month(date) -> int |
月份,1 到 12。 | month(2024-05-17) 为 5 |
day(date) -> int |
当月的日。 | day(2024-05-17) 为 17 |
quarter(date) -> str |
年份和季度,以文本表示。 | quarter(2024-05-17) 为 '2024-Q2' |
weekday(date) -> str |
星期几的三字母英文缩写。 | weekday(2024-01-05) 为 'Fri' |
yearmonth(date) -> date |
该日期所在月份的第一天。 | yearmonth(2024-05-17) 为 2024-05-01 |
today() -> date |
账本时区(timezone 选项)中的当前日期。 |
|
date(int, int, int) -> date |
由年、月、日构成的日期;没有这一天或年份不在 1 到 9999 之间时为 NULL。 |
date(2024, 2, 29) 为 2024-02-29,date(2023, 2, 29) 为 NULL |
date(str) -> date |
文本中按 YYYY-MM-DD 写的日期,月和日可以只有一位;不是日期时为 NULL。 |
date('2024-2-9') 为 2024-02-09 |
date_add(date, int) -> date |
把日期移动若干天,负数表示往回移动。 | date_add(2024-02-28, 1) 为 2024-02-29 |
date_diff(date, date) -> int |
从第二个日期到第一个日期的天数。 | date_diff(2024-03-01, 2024-02-01) 为 29 |
date_trunc(str, date) -> date |
日期所在的 week(周一)、month、quarter、year、decade、century(1901、2001……)或 millennium(1001、2001……)的第一天;其他字段为 NULL。字段为小写。 |
date_trunc('month', 2024-05-17) 为 2024-05-01 |
date_part(str, date) -> int |
日期的某个字段:weekday 或 dow(周一为 0)、isoweekday 或 isodow(周一为 1)、week(ISO 周)、month、quarter、year、isoyear(ISO 周所在的年)、decade、century、millennium 或 epoch(自 1970-01-01 起的秒数);其他字段为 NULL。没有 day 字段,请用 day(date)。 |
date_part('week', 2016-01-03) 为 53 |
interval(str) -> interval |
由数字和单位 day、week、month 或 year(或其复数)写成的间隔,数字可带符号;其他文本为 NULL。 |
interval('3 months')、interval('-1 year') |
date_bin(interval, date, date) -> date |
从起点(第三个参数)开始按间隔划分区间,返回日期所在区间的起点。见下文。 | date_bin(interval('7 days'), date, 2024-01-01) |
date_bin(str, date, date) -> date |
同上,间隔写成文本。 | date_bin('1 month', date, 2024-01-01) |
year、month 和 day 列是简写:year 等同于 year(date)。
date_trunc 按日历周期对日期分组,date_bin 可以从任意起点按任意间隔分组:
SELECT date_trunc('week', date) AS week, sum(position) AS spentWHERE account ~ '^Expenses:'GROUP BY week ORDER BY weekdate_bin(stride, date, origin)的各区间从origin + k × stride开始(k为任意整数),每个区间的开始日期都直接由起点算出,所以从2024-01-31开始的'1 month'区间起于2024-02-29、2024-03-31、2024-04-30……早于起点的日期落在从起点往回划分的区间中。- 恰好落在区间边界上的日期属于以它开始的区间:
date_bin('1 month', 2024-02-01, 2024-01-01)为2024-02-01。 - 间隔为零或负数、间隔的月数和天数符号相反(例如
interval('2 months') - interval('61 days'),它的各区间不会依次排列),或者文本无法被interval()读取时,结果为NULL。
日期是 beancount 日历中的日期,即 1 到 9999 年。日期函数或日期运算的结果超出这个范围时(例如 date_add(9999-12-31, 1) 或 date_trunc('decade', 0002-12-15))为 NULL(beanquery 会报错)。
间隔可以用 + 和 - 与日期相加减,见算术运算。周是张记账的扩展:在 beanquery 中 interval('1 week') 为 NULL。
| 签名 | 说明 |
|---|---|
meta(str) -> str |
分录上某个元数据键的值,未设置则为 NULL。 |
entry_meta(str) -> str |
交易上某个元数据键的值,未设置则为 NULL。 |
any_meta(str) -> str |
先在分录上查找某个元数据键,找不到再查交易;都没有则为 NULL。 |
meta_values(str) -> set |
分录上某个元数据键的所有值,以集合给出;没有设置时为空集合。张记账扩展。 |
entry_meta_values(str) -> set |
交易上某个元数据键的所有值,以集合给出。张记账扩展。 |
元数据的值总是以文本形式返回。一个键重复出现时,meta、entry_meta 和 any_meta 返回它的第一个值,meta_values 和 entry_meta_values 返回所有值:'b.pdf' IN entry_meta_values('invoice') 能找到有多行 invoice 的交易。在其他表上,这三个函数都读取该行指令的元数据。在 #budgets 上读取 budget 指令的元数据,在 #budget_events 上读取该预算指令的元数据,在 #errors 上读取张记账为错误记录的信息。
交易中哪些元数据行属于分录取决于文件格式,见交易。例如对于
2024-01-02 * "Cafe" "lunch" category: "meals" Assets:Cash -10 CNY Expenses:Food 10 CNY category: "food"Expenses:Food 分录的 meta('category') 为 'food',entry_meta('category') 为 'meals',any_meta('category') 为 'food';Assets:Cash 分录则分别为 NULL、'meals' 和 'meals'。
结构化元数据
Section titled “结构化元数据”metas 列和 open_meta(account) 以 metas 类型给出元数据:由 (key, value) 对组成的列表,按键排序,重复键的每个值按书写顺序保留。值都是文本。张记账不保留不同键之间的顺序,所以按键排序。str(metas) 把各对写成 key: value,用 ; 连接;HTTP API 把它们作为 {"key": ..., "value": ...} 对象的列表发送;CSV 导出的写法与 str 相同。这种文本形式是给人读的,不做转义,所以值本身含有 ; 或 : 时会有歧义;程序应读取 HTTP API 给出的键值对,或者用 meta_values 和 entry_meta_values 读取值。metas 是张记账的扩展;要按某个键筛选,请使用 meta、entry_meta、meta_values 或 entry_meta_values。
张记账为关键字搜索提供的扩展。“忽略大小写”指把两边的文本都转换为小写(Unicode)后再比较。
| 签名 | 说明 | 示例 |
|---|---|---|
icontains(str, str) -> bool |
文本是否包含第二个参数,忽略大小写。与 ~ 不同,要找的是普通文本,不是正则表达式。 |
icontains(payee, 'café') |
any_icontains(set, str) -> bool |
集合中是否有元素包含该文本,忽略大小写。 | any_icontains(tags, 'trip') |
intersects(set, set) -> bool |
两个集合是否有共同元素。 | intersects(tags, :tags) |
set(str, ...) -> set |
由给定字符串组成的集合,字符串个数不限。set() 是空集合。 |
intersects(tags, set('trip', 'food')) |
SELECT date, payee, narration, account, positionWHERE icontains(payee, 'coffee') OR icontains(narration, 'coffee') OR any_icontains(tags, 'coffee')张记账扩展,从两个同类型的值中取一个。参数可以是 bool、int、decimal、str 或 date,即 < 能比较的类型;int 和 decimal 按数值比较。
| 签名 | 说明 | 示例 |
|---|---|---|
least(T, T) -> T |
两个值中较小的一个;相等时取第一个。 | least(date_add(date, 6), 2024-12-31) |
greatest(T, T) -> T |
两个值中较大的一个;相等时取第一个。 | greatest(date, 2024-01-01) |
- 与其他函数一样,任一参数为
NULL时结果为NULL。PostgreSQL 的LEAST和GREATEST则会跳过NULL参数。 - 只接受两个参数。需要更多时可以嵌套:
least(a, least(b, c))。 - beanquery 没有这两个函数。
下面的查询按每个月最后一天的价格估算当月的余额,但最晚只到今天,因此当前月份按今天的价格而不是月末的价格估值:
SELECT date_trunc('month', date) AS month, convert(last(balance), 'USD', least(max(date_trunc('month', date)) + interval('1 month') - 1, today())) AS valueWHERE account ~ '^Assets:'GROUP BY month ORDER BY month| 签名 | 说明 | 示例 |
|---|---|---|
str(any) -> str |
任意值的文本形式。布尔值为 TRUE 和 FALSE,集合用 , 连接,库存写在括号中。 |
str(2024-01-31) 为 '2024-01-31' |
length(str) -> int |
字符串中的字符数。 | length('Food') 为 4 |
length(set) -> int |
集合中的元素个数。 | length(tags) |
maxwidth(str, int) -> str |
把文本缩短到 n 个字符以内,类似 Python 的 textwrap.shorten,详见下文。 |
maxwidth('Paying the rent', 12) 为 'Paying [...]' |
maxwidth(text, n) 分两步处理:
- 每一段连续的空白都变成一个空格,并去掉两端的空格。如果此时文本不超过
n个字符,就原样返回:maxwidth(' Eating out ', 48)为'Eating out'。 - 更长的文本保留尽可能多的完整单词,使其连同占位符
[...]一起不超过n个字符,占位符加在末尾。如果连第一个单词都放不下,结果为'[...]'。与 Python 一样,单词也可以在两个字母之间的连字符后断开:maxwidth('abc-def-ghi jkl', 12)为'abc- [...]'。
n 至少为 5,即 [...] 的长度,更小的宽度会报错。JOURNAL 用 maxwidth 缩短收款方和描述。
HTTP API
Section titled “HTTP API”查询页面使用的就是下面这些 HTTP 接口,你也可以在脚本中调用它们。如果启用了身份认证,请先登录,或者像调用其他 API 一样,用 HTTP Basic Authorization 头携带 ZHANG_AUTH 的凭证。
用 JSON 请求体调用 POST /api/query:
curl -X POST http://localhost:8000/api/query \ -H 'Content-Type: application/json' \ -d '{"query": "SELECT account, sum(position) WHERE account ~ \"^Assets:Bank\" GROUP BY account"}'成功时返回 HTTP 状态码 200:
{ "data": { "columns": [ { "name": "account", "type": "str" }, { "name": "sum(position)", "type": "inventory" } ], "rows": [ [ "Assets:Bank:Checking", { "positions": [ { "units": { "number": "1520.35", "currency": "USD" }, "cost": null } ] } ] ] }}columns按顺序列出结果列。每列有name和type,type是null、bool、int、decimal、str、date、set、amount、position、inventory、interval和metas之一。rows是行的列表。每行是一个列表,每列一个单元格,顺序与columns相同。- 请求中带上
"count_total": true时,结果还有total,即LIMIT和OFFSET之前的总行数,用于分页。不带时没有total。
| 类型 | JSON | 示例 |
|---|---|---|
任何 NULL |
null |
null |
bool |
布尔值 | true |
int |
数字 | 2024 |
decimal |
字符串 | "1520.35" |
str |
字符串 | "Expenses:Food" |
date |
字符串,YYYY-MM-DD |
"2024-01-31" |
set |
排好序的字符串数组 | ["food", "trip"] |
amount |
对象 | {"number": "12.50", "currency": "USD"} |
position |
对象 | {"units": {"number": "10", "currency": "VTI"}, "cost": {"number": "120.00", "currency": "USD", "date": "2024-01-02", "label": null}} |
inventory |
对象 | {"positions": [ ...持仓... ]} |
interval |
字符串 | "1 year 2 months" |
metas |
对象数组,按顺序 | [{"key": "invoice", "value": "a.pdf"}, {"key": "invoice", "value": "b.pdf"}] |
- 十进制数(包括金额和成本中的
number字段)以字符串形式发送,以免损失精度。它们不使用指数写法,并保留小数位("12.50")。请用十进制数库解析,而不要解析为浮点数。 - 整数是 64 位的,以 JSON 数字发送。JavaScript 把 JSON 数字读作双精度浮点数,所以超出 ±2^53(9,007,199,254,740,992)的
int单元格在 JavaScript 客户端中会损失精度。计数和日期部分远远达不到这个范围。 - 持仓未按成本持有时
cost为null;否则包含单位成本的number和currency,以及成本批次的date和label,后两者都可能为null。 - 库存中的持仓按单位货币排序,再按成本排序,没有成本的持仓排在最前。空库存为
{"positions": []}。
无法解析、类型检查或执行的查询返回 HTTP 状态码 400。与成功响应不同,响应体没有包在 data 中。例如 SELECT nosuchcolumn, position 会返回:
{ "message": "unknown column 'nosuchcolumn'", "line": 1, "column": 8}line和column给出问题在查询文本中的位置,都从 1 开始。column按 Unicode 字符而不是字节计数,一个汉字或带重音的字母算作一列。- 错误包括语法错误、未知的列或函数、参数类型错误、不合法的
GROUP BY用法、无效的正则表达式、不支持的语句或子句,以及超出下面的限制。 - 有些错误没有位置信息,此时
line和column为null:查询过长、查询超时、结果过大,以及少数在计算各行时发现的错误(例如sum中的整数溢出)。
这些限制保护服务器,防止查询占用过多的内存或时间。超出任何一项都会返回上面描述的 HTTP 400 错误。
| 限制 | 值 | 错误 |
|---|---|---|
| 查询长度 | 64 KiB(65,536 字节的 UTF-8 文本) | the query is too long (...),没有位置信息。 |
| 嵌套深度 | 64 层 | the query is nested too deeply (at most 64 levels),位置为达到限制的地方。 |
| 单个正则表达式编译后的大小 | 1 MiB | invalid regular expression: Compiled regex exceeds size limit ...,位置指向该模式。 |
| 执行时间 | 10 秒 | the query was stopped because it ran longer than the 10s time limit,没有位置信息。 |
| 结果大小 | 默认 1,000,000 个值 | the result is too large: ...,没有位置信息。 |
- 嵌套深度统计的是相互嵌套的括号、函数调用、
IN列表、NOT和一元负号。由AND、OR、+或*连接的长链(例如account = 'A' OR account = 'B' OR ...)不算嵌套,在长度限制以内可以任意长。 - 执行时间包括构建
postings表各行以及应用会计期间子句的时间。查询运行期间会持有账本的读锁,时间限制也限定了持有读锁的时长。 - 结果大小把每个单元格计为一个值,库存中的每个持仓、集合中的每个元素、
metas值中的每个键值对以及文本中的每 64 字节(包括这些元素和键值对的文本)各再计一个值。查询在ORDER BY、DISTINCT和LIMIT之前收集的行也计算在内,聚合查询在构建过程中的分组,以及#budgets生成的月份(每个月份计一个值)同样如此。PIVOT BY生成的表计算所有单元格(包括空单元格),并在构建之前检查。超出限制的查询会报错,错误信息建议用FROM或WHERE缩小查询范围,或者加上LIMIT。 - 服务器管理员可以通过环境变量
ZHANG_QUERY_MAX_RESULT_VALUES调高或调低结果大小的限制。 LIMIT可以让结果保持较小,累计余额的计算方式也有帮助:除非查询按balance排序、分组或去重,否则只为最终出现在结果中的行构建balance。units(balance)和cost(balance)(以及JOURNAL ... AT units和AT cost)按货币累加,不保留批次。没有ORDER BY时,如果每个分组的行在账本顺序中前后相连、各分组按其键的顺序依次出现(例如GROUP BY date,或按seq, posting_index对一笔按多个批次记账的记账行的各行分组),LIMIT和OFFSET只构建它们返回的分组,无论这一页有多靠后。- 多次写出的同一个聚合,例如
last(balance), units(last(balance))中的last(balance),只计算和保存一次。在分组查询中,如果HAVING不读取first(balance)和last(balance)(例如GROUP BY date HAVING max(date) >= 2024-01-01),它们只为HAVING保留的分组构建,因此这样的查询只花费保留的分组所占的开销,无论之前的历史有多长。 - CSV 导出同样受这些限制。
CSV 导出
Section titled “CSV 导出”POST /api/query/csv 接受与 POST /api/query 相同的 JSON 请求体,并以 CSV 文件的形式返回结果:
curl -X POST http://localhost:8000/api/query/csv \ -H 'Content-Type: application/json' \ -d '{"query": "SELECT account, sum(position) AS total WHERE account ~ \"^Assets:Broker\" GROUP BY account"}'- 响应的内容类型为
text/csv; charset=utf-8,并带有Content-Disposition: attachment; filename="query.csv"头。 - 查询失败时,返回与
POST /api/query相同的 HTTP 400 错误和 JSON 响应体,见错误。
为了方便在电子表格中使用,金额会像 beanquery 的 numberify 选项(bean-query -m)那样转换为纯数字:
- 名为
name的amount、position或inventory列,按货币拆分为多个decimal列,每种货币一列,列名为name (CUR)。没有出现任何货币的列会被省略。 - 由同一列拆分出的各列,按其货币出现的行数从多到少排列;行数相同时按货币名降序排列,所以
USD排在EUR之前。 amount把数值写在其货币对应的列中。为零的金额视为缺失:单元格为空,也不计入该列的货币。position给出其单位的数值,不含成本。单位为零时写作0。inventory给出每种货币的单位合计,把所有批次相加,不计成本。合计为零时单元格为空。- 其他列保持不变。
例如,对于一个现金账户和一个黄金持仓,上面的查询得到:
account,total (USD),total (GLD)Assets:Broker:Cash,855.83,Assets:Broker:GLD,,17文件遵循 RFC 4180:
- 第一条记录是列名。每条记录都以 CRLF 结尾,最后一条也不例外。
NULL为空字段。布尔值写作TRUE和FALSE,日期写作YYYY-MM-DD,集合写作排好序、用,连接的元素,间隔写作1 year 2 months这样的形式,metas写作用;连接的key: value对(不做转义,见结构化元数据)。- 数字是精确的:保留全部数字和小数位,不会补空格、不会舍入,也不会使用指数写法。
- 含有
,、"、回车或换行的字段会用双引号括起来,其中的"写成两个。只有一个空字段的行写作"",以免成为空行。
列出保存的查询
Section titled “列出保存的查询”GET /api/query/saved 按账本顺序列出用 query 指令保存在账本中的查询。每一项包含 name、查询文本 query、指令的日期 date,以及 valid 和 error,后两者说明该查询能否被当前的查询引擎编译以及不能编译的原因。响应示例见 query 指令。要执行保存的查询,把它的 query 文本发送到 POST /api/query。
GET /api/query/builtins 列出应用中各项数字背后的查询,POST /api/query/builtins/{name}/text 把其中一个查询连同填好的参数值写成查询文本。见内置查询。
Schema
Section titled “Schema”GET /api/query/schema 描述每个表和每个函数重载。查询页面的参考面板就是根据它生成的。
{ "data": { "columns": [ { "name": "date", "type": "date", "description": "Date of the transaction." } ], "functions": [ { "name": "count", "signature": "count(*) -> int", "description": "Number of rows.", "aggregate": true } ], "tables": [ { "name": "postings", "description": "One row per posting of every transaction, ...", "columns": [{ "name": "date", "type": "date", "description": "Date of the transaction." }] } ] }}columns每列一项,共 34 项,顺序与列表格相同。tables每个表一项,先是postings,然后按其他表中列出的顺序排列,最后是budgets、budget_events和errors。name不带#。postings一项的列与columns相同;结构化列的字段以open.date这样的名字列为单独的列。functions每个重载一项,共 100 项:先是聚合函数,然后是标量函数,其中包括account_sortkey和maxwidth。signature的写法与本页表格相同;聚合函数的aggregate为true,其他函数为false。
按类别统计每月支出
Section titled “按类别统计每月支出”SELECT year, month, root(account, 2), sum(position) WHERE account ~ "^Expenses" GROUP BY 1, 2, 3 ORDER BY 1, 2, 3每个月、每个二级支出账户(如 Expenses:Food)一行,给出总支出。GROUP BY 1, 2, 3 和 ORDER BY 1, 2, 3 通过序号引用前三个目标。
超过某个金额的支出类别
Section titled “超过某个金额的支出类别”SELECT root(account, 2) AS category, sum(position) AS totalWHERE account ~ '^Expenses' AND currency = 'USD'GROUP BY categoryHAVING sum(number) > 1000ORDER BY category支出超过 1000 USD 的支出类别。HAVING 在分组合计之后过滤分组;WHERE 做不到,因为它每次只看到一条分录。
每月支出,每年一列
Section titled “每月支出,每年一列”SELECT month, year, sum(position) AS totalWHERE account ~ '^Expenses:Food'GROUP BY month, yearPIVOT BY month, year每个月一行,每年一列,不同年份的同一个月并排显示。某年某月没有分录时,单元格为空。
按收款方统计支出
Section titled “按收款方统计支出”SELECT payee, sum(cost(position)) AS total WHERE account ~ "^Expenses" GROUP BY payee ORDER BY total DESC LIMIT 20支出最多的 20 个收款方。cost(position) 让按成本购买的东西以实际付出的金额计算,ORDER BY total DESC 按别名排序。
按标签筛选分录
Section titled “按标签筛选分录”SELECT date, payee, account, position WHERE 'trip-new-york' IN tags所有带有 #trip-new-york 标签的交易的每一条分录。
持仓的成本与市值
Section titled “持仓的成本与市值”SELECT account, units(sum(position)) AS qty, cost(sum(position)) AS book, convert(units(sum(position)), "USD") AS market WHERE account ~ "^Assets:Trading" GROUP BY account每个交易账户的持有数量、账面价值(买入成本)以及按最新价格换算的美元市值。没有美元价格的持仓在 market 列中保持原来的货币。
SELECT date, payee, account, position ORDER BY date DESC LIMIT 20所有账户中最近的 20 条分录。
某段时间的支出(不含税费)
Section titled “某段时间的支出(不含税费)”SELECT account, sum(position) AS totalWHERE account ~ '^Expenses' AND account !~ ':Taxes(:|$)' AND date >= 2024-01-01 AND date < 2024-04-01GROUP BY accountORDER BY account用 !~ 排除账户,用不带引号的日期字面量限定时间范围。
按季度统计并计数
Section titled “按季度统计并计数”SELECT quarter(date) AS q, count(*) AS postings, sum(number) AS total, sum(number) / count(*) AS averageWHERE account ~ '^Expenses:Food' AND currency = 'USD'GROUP BY qORDER BY q每个季度的分录数、总额和平均金额。number 不区分货币,所以要先按 currency 过滤,sum(number) 才有意义。
用信用卡支付的交易
Section titled “用信用卡支付的交易”SELECT DISTINCT date, descriptionWHERE 'Liabilities:CreditCard' IN other_accounts AND account ~ '^Expenses'ORDER BY date DESCother_accounts 是每笔交易中其他分录的账户,因此这个查询找出用信用卡支付的支出。即使一笔交易有多条支出分录,DISTINCT 也只列出一次。
没有收款方的分录
Section titled “没有收款方的分录”SELECT date, narration, account, positionWHERE payee IS NULL AND account IN ('Expenses:Misc', 'Expenses:Uncategorized')ORDER BY date DESC演示 IS NULL 和 IN 后面的列表。
记录在元数据中的发票
Section titled “记录在元数据中的发票”SELECT date, payee, entry_meta('invoice') AS invoice, positionWHERE entry_meta('invoice') IS NOT NULL AND leaf(account) = 'Consulting'ORDER BY date列出账户名形如 ...:Consulting、且交易带有 invoice 元数据的分录。
年末的投资组合市值
Section titled “年末的投资组合市值”SELECT account, value(sum(position), 2024-12-31) AS market_valueWHERE account ~ '^Assets:Investments' AND date <= 2024-12-31GROUP BY accountORDER BY account2024 年 12 月 31 日的持仓,按当日有效的价格、以各持仓的成本货币计算市值。
以正数显示收入和支出
Section titled “以正数显示收入和支出”SELECT root(account, 1) AS type, sum(possign(position, account)) AS totalWHERE account ~ '^(Income|Expenses)' AND year = 2024GROUP BY type收入在账本中是负数。possign 在求和之前把每条收入分录的符号取反,所以两个合计都显示为正数。
某一年的损益表
Section titled “某一年的损益表”SELECT account, sum(position) AS totalFROM OPEN ON 2024-01-01 CLOSE ON 2025-01-01WHERE account ~ '^(Income|Expenses)'GROUP BY accountORDER BY accountOPEN ON 把以前各年的收入和支出转入权益,CLOSE ON 丢弃 2025 年及以后的所有内容,所以合计只包含 2024 年。
BALANCES FROM CLOSE ON 2025-01-01 CLEARWHERE account ~ '^(Assets|Liabilities|Equity)'2025 年初资产、负债和权益账户的余额,按账户类型排序。CLEAR 把所有收入和支出转入 Equity:Earnings:Current,所以权益账户中包含了收益。
按成本计的持仓
Section titled “按成本计的持仓”BALANCES AT cost WHERE account ~ '^Assets'每个资产账户的账面价值,以其持仓的成本货币计。
JOURNAL 'Assets:Bank:Checking' FROM OPEN ON 2024-01-01 CLOSE ON 2025-01-01该账户在 2024 年的每一条分录,带有累计余额。第一行是 2023 年 12 月 31 日的期初余额,所以余额列显示的是账户的真实余额。
每天结束时的余额
Section titled “每天结束时的余额”SELECT date, last(balance) AS closingWHERE account = 'Assets:Bank:Checking'GROUP BY dateORDER BY date每个有分录的日期一行,给出该账户当天结束时的余额。结果由一个日期列和一个库存列组成,所以查询页面会把它绘制成折线图。
与 BQL 和 beanquery 的差异
Section titled “与 BQL 和 beanquery 的差异”PRINT,使用时会报错。FROM后面的子查询、beanquery 用双引号写的表名(FROM "prices"),以及它的单行表FROM #。- postings 表中 beanquery 的列
filename、lineno、location、entry、accounts和type,以及#entries的lineno:张记账不保存行号。 - 下标访问,例如
meta['name']。请使用meta('name')。 BETWEEN和%运算符,以及 beanquery 的带引号标识符。- 本页未列出的函数,例如
round、safediv、has_account、grep、subst、upper、lower、joinstr、findfirst、parse_date,以及类型转换函数int、decimal和date(date)。调用它们会报错。
行为不同之处
Section titled “行为不同之处”SELECT *包含account。beanquery 把*展开为date, flag, payee, narration, position。张记账在position之前加入了account,因为没有账户的分录很难看懂。- 标准的三值逻辑。在 beanquery 中,
NOT NULL为TRUE,所以NOT (payee = 'x')会保留没有收款方的分录;NULL AND FALSE为NULL。在张记账中,与 SQL 一样,NOT NULL为NULL,NULL AND FALSE为FALSE。 - 单元素列表可以使用。
payee IN ('Amazon')在张记账中可以正常使用。beanquery 会把('Amazon')当作带括号的字符串,必须写成('Amazon',)。 - 注释以
--开头。不支持 beanquery 的;行注释和/* */块注释。只允许在查询末尾写一个;。 - 正则表达式使用 Rust 语法,不支持环视和反向引用。
- 记账方法。
NONE、AVERAGE和AVERAGE_ONLY尚未实现:使用其中之一的账户会被报告(UnsupportedBookingMethod),并按账本的默认记账方法记账,见批次记账。 - 限制。查询的长度、嵌套深度、正则表达式大小、执行时间和结果大小都有限制,见限制。
- 错误带有位置信息。只要能定位,每个查询错误都会给出出错的行和列。
- 全程使用精确小数。数字是任意精度的十进制数,金额不会以固定的小数位数存储。
- 对空输入聚合。没有分组键时,张记账在
HAVING和分页之前返回一行聚合初始值;beanquery 0.2.0 返回零行。 BALANCES和JOURNAL的列名与等价的SELECT相同:sum(position)、sum(cost(position))和maxwidth(payee, 48)。beanquery 把它们命名为SUM((position))、SUM(cost(position))和MAXWIDTH(payee, 48)。- 累计余额。
balance不能用在FROM或WHERE中,它累加的正好是通过这两个子句的行。beanquery 在每次计算该列时更新余额,所以在WHERE子句中,它累加的是被测试的行,而不是被保留的行。 account_sortkey对第一段不是账户类型的名字,返回排在所有类型之后的键。beanquery 会报错。date_bin从起点划分区间。间隔含月或年时,beanquery 把每个间隔加在上一个区间的起点上,所以从月末开始的区间会漂移(01-31、02-28、03-28……),而且它把恰好落在区间边界(起点除外)上的日期归入上一个区间:在 beanquery 中date_bin('1 month', 2000-02-01, 2000-01-01)为2000-01-01。张记账的区间是origin + k × stride(01-31、02-28、03-31……),落在边界上的日期属于以它开始的区间(2000-02-01)。间隔为零,或间隔文本无法被interval()读取时为NULL;beanquery 会出错。interval()接受周,每周七天。beanquery 对它们返回NULL。least和greatest是张记账扩展,beanquery 没有这两个函数。见比较函数。NULL参数。凡是可以写值的地方都可以写NULL字面量,函数收到NULL就返回NULL:date_add(NULL, 1)为NULL。beanquery 把NULL当作单独的类型,会拒绝这样的调用。- 间隔运算。
interval - interval得到间隔;beanquery 声明的结果类型是日期。interval - date会报错;beanquery 接受它,但执行时出错。 - 间隔比较。间隔可以用
=、!=和IN比较(beanquery 不接受),按月数和天数比较,所以GROUP BY和DISTINCT把interval('1 year') + interval('-1 month')和interval('11 months')视为同一个值(beanquery 把它们分开)。对间隔排序在 beanquery 中执行时出错,在张记账中检查查询时就会报错。 - 日期是 1 到 9999 年。日期函数或日期运算的结果超出这个范围时为
NULL;beanquery 会报错。 OFFSET是张记账的扩展;beanquery 只有LIMIT。CASE WHEN ... END是张记账的扩展;beanquery 没有条件表达式。见 CASE。- 参数。
JOURNAL的模式、OPEN ON和CLOSE ON的日期,以及LIMIT和OFFSET可以是参数。beanquery 在这些地方只接受字面量。 FROM中的表达式在会计期间子句之后过滤。这与 beanquery 一致。在 BQL v2 中,该表达式在应用OPEN、CLOSE和CLEAR之前选择交易。- 权益账户。
account_previous_*或account_current_*选项的值如果不是有效的账户名,会被忽略,并使用默认账户。 - 期间内的最后一笔条目决定
CLEAR的T交易和不带日期的CLOSE的C交易的日期。它不考虑张记账特有的预算指令,Beancount 没有这类指令。 HAVING中的列。对于HAVING中在聚合函数之外使用的列,beanquery 会从任意一条分录读取。张记账把分组键读作每个分组的值,其他列则会报错。HAVING必须是布尔表达式。beanquery 也接受其他值,保留值既不为零也不为空的分组,例如HAVING sum(number)。张记账与WHERE一样拒绝这类条件,应写成HAVING sum(number) != 0。PIVOT BY遇到NULL值。透视目标的值中既有NULL又有其他值时,beanquery 会出错。张记账把NULL排在最前,对应的列命名为NULL。- 没有分组的
PIVOT BY在张记账中会报错。beanquery 在执行这类查询时出错。 - 导出缺少单元格的透视结果为 CSV。beanquery 无法对透视后金额或库存列中的空单元格做 numberify。张记账把它们留空。
- 元数据是文本。beanquery 的
meta列是字典,其中还有filename和lineno。张记账的meta是指令自身元数据的文本key: "value", ...(在 postings 表中是分录自己的元数据),open.meta和close.meta也是如此。结构化的形式是张记账的metas类型,见结构化元数据。 #accounts的open和close不带字段时读作日期。在 beanquery 中它们是整条指令。entry_meta()和any_meta()与meta()一样可用于每个表。beanquery 只在 postings 表上接受它们。#entries包含张记账的指令。其中有张记账的预算指令;balance ... with pad是一条balance记录,后面跟着它的补齐交易,而 beancount 中是一条pad和一条balance记录。pad与 beancount 一样是一条pad记录。记录的id是张记账的 ID,不是 beancount 的哈希值。discrepancy、actual和passed遵循张记账的余额检查。与 beancount 一样,张记账从该账户及其子账户分录的合计计算余额,断言不会改变任何余额。没有~容差的断言必须精确相等,而 beancount 会根据断言金额的小数位数推断容差。#documents还列出交易的文档,排在document指令之后:即交易和分录的document元数据的值,beancount 不把它们当作文档。- CSV 导出保留精确的数字。
bean-query会为对齐而在数字前补空格(" 600.00"),把 numberify 后的数字舍入到各货币的显示精度(360.03而不是360.03016),有些数字还会用指数写法(1E+3)。张记账都不会这样做。
