BQL Mastery: Building Custom Financial Analytics from Beginner to Advanced

Hey everyone! :waving_hand:

I want to share my journey with Beancount Query Language (BQL) because it completely transformed how I use Beancount - from passive tracking to active financial intelligence.

Where I Started

Two years ago, I was just using bean-report for basic balance checks. I’d look at my net worth, nod approvingly, and move on. But then I discovered BQL, and suddenly I could answer questions like “How much did travel expenses increase year-over-year?” or “Which investment had the best performance attribution?” in seconds.

BQL Basics: The Foundation

BQL has a deceptively simple structure:

SELECT account, SUM(position) 
FROM date >= 2025-01-01 
WHERE account ~ 'Expenses:Groceries'

The unique part is the two-level filtering:

  • FROM clause: Filters entire transactions (preserves double-entry integrity)
  • WHERE clause: Filters individual postings within those transactions

This accounting-specific design is what makes BQL powerful - it understands that transactions are atomic units.

Beginner to Intermediate: Query Recipes

Here are the queries that changed my financial life:

1. Multi-Year Spending Trends

SELECT 
  YEAR(date) as year,
  account,
  SUM(CONVERT(position, 'USD', date)) as amount
FROM date >= 2022-01-01
WHERE account ~ 'Expenses'
GROUP BY year, account
ORDER BY year, amount DESC

This revealed that my “just a few coffees” habit cost me $2,400 in 2025. Ouch.

2. Monthly Income vs Expenses

SELECT 
  YEAR(date) as year,
  MONTH(date) as month,
  SUM(CONVERT(position, 'USD', date)) as amount
WHERE account ~ 'Income' OR account ~ 'Expenses'
GROUP BY year, month

Suddenly every December’s savings dip made sense: holiday expenses + end-of-year splurging.

3. Category Spending Heatmap Data

Export BQL results to a CSV, pull into Pandas, and create a heatmap showing spending patterns by category and month. This is where BQL becomes a data pipeline, not just a reporting tool.

Pro tip: Start simple. Don’t try to build complex queries on day one. Master basic SELECT/FROM/WHERE, then add GROUP BY, then CONVERT for currencies. Build complexity gradually.

Advanced Analytics: Where It Gets Fun

Once you’re comfortable with basic queries, the real power emerges:

Investment Performance Attribution

I use beangrow (the Beancount portfolio returns tool) combined with custom BQL queries to break down performance:

  • Capital appreciation vs dividends
  • Individual asset returns
  • Tax-loss harvesting opportunities

Here’s a query I run monthly to identify underperforming assets:

SELECT 
  account,
  COST(SUM(position)) as cost_basis,
  SUM(CONVERT(position, 'USD', date)) as market_value
WHERE account ~ 'Assets:Investments'
GROUP BY account

Then I calculate unrealized gain/loss manually (or in a Jupyter notebook) to spot rebalancing opportunities.

Cash Flow Forecasting

For my FIRE dashboard, I export 3 years of expense data via BQL, feed it into a Python script using Prophet (time-series forecasting), and project future monthly burn rates. This tells me if I’m on track to hit my FI number.

Integration with Pandas

BQL outputs can be exported to CSV or piped directly into Pandas DataFrames:

import subprocess
import pandas as pd

result = subprocess.run(['bean-query', 'ledger.beancount', 'SELECT ...'], 
                       capture_output=True, text=True)
df = pd.read_csv(io.StringIO(result.stdout))
# Now you have full Pandas power for analysis

This unlocks visualization libraries (matplotlib, seaborn, plotly) and statistical analysis beyond what Fava provides.

Common Mistakes I Made (So You Don’t Have To)

  1. Forgetting the FROM clause filters transactions: If you want to exclude certain postings, use WHERE, not FROM
  2. Not using CONVERT for multi-currency: Always specify your reporting currency
  3. Over-engineering early: My first queries were 50-line monstrosities. Simpler is better.
  4. Ignoring balance assertions: If your queries show weird numbers, check your ledger integrity first

The Community Query Cookbook

Here’s my ask: Let’s build a shared query library. What BQL queries have changed your financial decisions?

  • What’s your go-to query for monthly reviews?
  • Any creative uses of BQL + external tools?
  • What analytics do you wish were easier to generate?

For me, the killer insight was realizing that 40% of my expenses were “lifestyle inflation” since my last promotion. That query motivated me to cut back and increase my savings rate from 25% to 45%.

What’s your BQL story?


Sources for learning BQL:

This is such a great post, Fred! The journey from “just checking balances” to building sophisticated analytics really resonates with me.

I remember when I first discovered BQL - I spent an entire Saturday afternoon just running queries and being amazed at what insights were hiding in my ledger. It took me about 6 months before I felt truly comfortable writing queries without constantly referring to the docs, so don’t feel discouraged if it doesn’t click immediately.

The Power of Simple Queries

One thing I love about your post is the emphasis on starting simple. The most powerful analyses I’ve done often come from combining several simple queries creatively rather than trying to build one massive query that does everything.

For example, I track rental property cash flow with a straightforward query:

SELECT 
  account,
  SUM(CONVERT(position, 'USD', date)) as amount
WHERE account ~ 'Income:Rental' OR account ~ 'Expenses:Property'
GROUP BY account

Nothing fancy - but when I export this monthly and track it over time, I can spot maintenance cost spikes, seasonal vacancy patterns, and calculate true cash-on-cash returns.

A Word of Caution

Your advice about avoiding over-engineering early is spot-on. When I migrated from GnuCash, I tried to recreate every report I had using complex BQL queries. Big mistake! I should have started with basic balance checks and gradually built up my query library as needs arose.

The beauty of plain text accounting: Your ledger is version-controlled. Experiment freely! If a query breaks something, it’s just a query - your data is safe. Try things, see what works, iterate.

One More Recipe

Here’s a query I use that might interest the FIRE folks here - tracking savings rate over time:

SELECT 
  YEAR(date) as year,
  MONTH(date) as month,
  SUM(CASE WHEN account ~ 'Income' THEN CONVERT(position, 'USD', date) ELSE 0 END) as income,
  SUM(CASE WHEN account ~ 'Expenses' THEN CONVERT(position, 'USD', date) ELSE 0 END) as expenses
FROM date >= 2024-01-01
GROUP BY year, month
ORDER BY year, month

Then I calculate savings rate in a spreadsheet to get my monthly savings rate. Watching that percentage climb from 15% to 35% over two years has been incredibly motivating.

Keep sharing these insights, Fred - this is exactly the kind of practical knowledge that helps newcomers succeed!