본문으로 건너뛰기

Beancount 질의 언어(BQL): 원장을 질의하세요

BQL로 Beancount 원장을 질의하세요. SQL과 유사한 언어로 SELECT 문법, 컬럼, 집계 및 재고 함수, bea query로 실행하는 보고서를 다룹니다.

Beancount는 재무 데이터를 정밀하게 분류하고 분석할 수 있는 강력한 SQL 유사 쿼리 언어(BQL)를 제공합니다. 빠른 보고서를 생성하거나, 항목을 디버깅하거나, 복잡한 분석을 수행하려는 경우, 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의 세 가지 주요 절로 구성됩니다.

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"는 일치하는 전기당 한 행을 반환합니다. 브로커 매수가 두 건인 장부에서 첫 번째는 전체 항목을 반환하고 두 번째는 정확히 두 개의 브로커 행을 반환합니다.


데이터 모델​

데이터를 효과적으로 쿼리하려면 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"
 
-- 최신 가격 데이터를 사용하여 시장 가치 계산
-- (보유 자산에 대한 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
-- 계정당 한 행, 예: 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/ko/docs/Basics/beancount-query-language