メインコンテンツへスキップ

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を実行するとインタラクティブシェルが開きます。このガイドのすべてのクエリは、Beancount 3.2.3とbeanquery 0.2.0に対して実行したものです。

基本的なクエリ形式​

BQLクエリは、SELECT、FROM、WHEREという3つの主要な句で構成されます。

SELECT <target1>, <target2>, ...
FROM <entry-filter-expression>
WHERE <posting-filter-expression>;
  • SELECT: 取得したいデータの列を指定します。
  • FROM: トランザクションが処理される_前に_、トランザクション全体をフィルタリングします。
  • WHERE: トランザクションが選択された_後に_、個々のポスティング行をフィルタリングします。

2段階フィルタリングシステム​

FROM句とWHERE句の違いを理解することは、正確なクエリを書くために極めて重要です。BQLは2段階のフィルタリングプロセスを採用しています。

  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"は一致するポスティングごとに1行を返します。ブローカーでの購入が2件ある帳簿では、前者は完全なエントリを返し、後者はちょうど2つのブローカー行を返します。


データモデル​

データを効果的にクエリするには、Beancountがデータをどのように構造化しているかを理解する必要があります。帳簿はディレクティブのリストですが、BQLは主にTransactionエントリに焦点を当てています。

トランザクション構造​

各Transactionは、トップレベルの属性とPostingオブジェクトのリストを持つコンテナです。

Transaction
├── date
├── flag
├── payee
├── narration
├── tags
├── links
└── Postings[]
    ├── account
    ├── units
    ├── cost
    ├── price
    └── metadata

利用可能な列タイプ​

トランザクションまたはそのポスティングの任意の属性をSELECTできます。

  1. トランザクション属性 これらの列は、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(*)
 
-- すべてのポスティングを1つのInventoryに合計; 通貨とロットは保持され、換算はされない
SELECT SUM(position)
-- 1行、例: (-2300.00 USD, 10 HOOL {150.00 USD, 2024-09-05}, 5 HOOL {160.00 USD, 2024-11-02})
 
-- 1つの勘定を明示的に単一通貨で合計 (価格のないポジションは通貨を保持)
SELECT SUM(CONVERT(position, 'USD')) WHERE account ~ "Assets:Checking"
-- 1行、例: (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"
 
-- 最新の価格データを使用して市場価値を計算
-- (保有資産のpriceディレクティブが必要; なければポジションはそのまま返される)
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
-- 勘定ごとに1行、例: 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文は、1つ以上の勘定の詳細な活動を表示し、従来の元帳ビューに似ています。

-- 当座預金のすべての活動を取得原価で表示
JOURNAL "Assets:Checking" AT COST
 
-- すべての401kトランザクションを、数量(株数)のみ表示
JOURNAL "Assets:.*:401k" AT UNITS

印刷操作​

PRINT文は、一致するトランザクションを元のBeancountファイル形式で出力するデバッグツールです。エントリフィルタのみを受け付けます。ここでWHERE句は構文エラーになります。出力を各エントリの片側に絞り込むには、代わりにポスティングフィルタを伴うSELECTを使用してください。これは一致するポスティングごとに1行を返します。

-- 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 -- balanceディレクティブの日付を使用
    WHERE account = "Assets:Checking";

出典: https://beancount.io/ja/docs/Basics/beancount-query-language