Introduction to Beancount Query Language (BQL)
After years of working with Beancount, I’ve come to appreciate BQL (Beancount Query Language) as one of its most powerful features. While it looks like SQL at first glance, it’s specifically designed for double-entry accounting and has some unique characteristics worth understanding deeply.
What Makes BQL Different from SQL?
BQL operates on a two-level filtering process that’s crucial to understand:
- FROM clause: Filters at the transaction level
- WHERE clause: Filters at the posting level (individual entries within transactions)
This distinction is important because in double-entry bookkeeping, every transaction has multiple postings. You might want to filter transactions by date, but then further filter which specific postings within those transactions you care about.
Real-World Query Examples
Example 1: Monthly Expense Analysis
SELECT
account,
MONTH(date) as month,
sum(position) as total
FROM OPEN ON 2024-01-01 CLOSE ON 2025-01-01
WHERE account ~ 'Expenses:'
GROUP BY account, month
ORDER BY month, total
This query:
- Opens a period from Jan 1, 2024 to Jan 1, 2025 (FROM clause)
- Filters only expense account postings (WHERE clause)
- Groups by account and month
- Returns monthly expense totals by category
Example 2: Holdings Report with Cost Basis
SELECT
account,
sum(cost(position)) as cost_basis,
sum(units(position)) as units,
sum(convert(position, 'USD')) as market_value
WHERE account ~ 'Assets:Investments'
AND currency != 'USD'
Here we use three critical BQL functions:
cost(position): Returns the cost basis (what you paid)units(position): Returns just the quantity of holdingsconvert(position, 'USD'): Converts to current market value using price directives
Example 3: Payee Analysis
SELECT
payee,
count(*) as num_transactions,
sum(position) as total
WHERE account ~ 'Expenses:'
AND year = 2024
GROUP BY payee
ORDER BY total DESC
LIMIT 20
This shows your top 20 payees by expense amount—incredibly useful for budget analysis.
Advanced Aggregation and Inventory Handling
One of BQL’s most powerful features is sum(position), which doesn’t just add numbers—it maintains inventories. This means:
SELECT account, sum(position)
WHERE account ~ 'Assets:Investments:Vanguard'
Will return something like:
account sum_position
------------------------------------ ------------------
Assets:Investments:Vanguard 450.00 VTSAX {250.00 USD}
+320.00 VTIAX {180.50 USD}
It preserves lot information, cost basis, and handles multiple currencies/commodities automatically. This is fundamentally different from SQL’s SUM(), which would just error on non-numeric data.
Integration with Fava
What makes BQL even more powerful is integration with Fava (the web interface). You can:
- Write BQL queries in Fava’s query interface
- Save them as custom reports
- Generate charts directly from query results
- Export to CSV for further analysis
In Fava, navigate to “Query” and run:
SELECT year, sum(position)
WHERE account ~ 'Income:'
GROUP BY year
Then click “Chart” to visualize income trends over time.
Key Resources
For those wanting to dive deeper:
- Official BQL documentation: Beancount Query Language - Beancount Documentation
- aumayr’s example queries: GitHub - aumayr/beancount-sql-queries: Examples for beancount SQL queries (comprehensive real-world examples)
- Fava query interface: Built into your Fava installation at
/query
Advanced Topics Worth Exploring
- Using
OPEN ONandCLOSE ONfor period analysis - The
PIVOTfunctionality for cross-tab reports - Combining BQL with Python scripts for automation
- Performance optimization for large ledgers (100k+ transactions)
What BQL queries do you find most useful? I’m particularly interested in hearing about creative uses of the inventory aggregation features.