SQL for Finance Teams: Pulling Data from ERP and Accounting Systems

Excel is where finance analysis happens, but it is rarely where finance data lives. ERP systems like SAP, Oracle, NetSuite, and Microsoft Dynamics store transactions across dozens of normalized tables designed for transaction processing, not reporting. SQL is the tool that reaches into those tables and pulls out exactly the numbers you need, shaped the way you need them, before they ever reach a spreadsheet or dashboard.

This guide covers how general ledger data is typically structured, the SQL patterns finance teams use most often, and the mistakes that quietly produce wrong numbers.

Why Finance Teams Need SQL, Not Just Excel

A mid-size company's general ledger can easily contain millions of line items once you include every journal entry across every subsidiary and year. Pulling that into Excel directly is slow, error-prone, and usually capped by row limits. SQL lets you filter, join, and aggregate at the database level, so Excel or Power BI only ever receives the clean, summarized result you actually need.

What you need before starting

Read access to your finance data source (a reporting database, data warehouse, or a replicated copy of your ERP data), a basic understanding of SELECT, WHERE, GROUP BY, and JOIN, and a list of the specific account codes or cost centers relevant to your reports.

How General Ledger Data Is Structured

Almost every ERP, regardless of vendor, organizes financial data the same basic way: a header table for each transaction, a line-item table with the actual debit and credit amounts, and reference tables for accounts, departments, vendors, and customers.

Typical general ledger data structure A central GL Transaction Lines table connects to a Journal Header table, a Chart of Accounts table, a Department or Cost Center table, and a Vendor or Customer table, forming a simple star schema used for financial reporting. GL Transaction Lines Account, Debit, Credit, Date, Document ID Journal Header Document ID, posting date, period Chart of Accounts Account code, name, category Department / Cost Center Department code, name Vendor / Customer Master Used for AP and AR sub-ledgers
Figure 1. Every ERP names these tables differently, but this basic shape shows up almost everywhere.

The reference table below maps this generic structure onto four widely used systems, so you know roughly what to search for once you are inside an unfamiliar schema.

Step 1: Pull Raw GL Transactions for a Period

STEP 1

Start narrow, then widen

Always filter by date range first. GL tables are typically the largest tables in the entire database, and an unfiltered query can lock up a shared reporting server.

SELECT
  gl.transaction_date,
  gl.account_code,
  gl.debit_amount,
  gl.credit_amount,
  gl.document_id
FROM gl_transaction_lines gl
WHERE gl.transaction_date BETWEEN '2026-01-01' AND '2026-01-31'
  AND gl.company_code = '1000';

Step 2: Join to the Chart of Accounts

STEP 2

Turn account codes into readable names

Raw GL data only has account codes, not labels. Join the chart of accounts table to get account names and categories, which is what makes the output usable for anyone outside the finance system team.

SELECT
  gl.transaction_date,
  coa.account_name,
  coa.account_category,
  gl.debit_amount,
  gl.credit_amount
FROM gl_transaction_lines gl
JOIN chart_of_accounts coa
  ON gl.account_code = coa.account_code
WHERE gl.transaction_date BETWEEN '2026-01-01' AND '2026-01-31'
  AND gl.company_code = '1000';

Step 3: Build a P&L Summary

STEP 3

Aggregate by account category

A profit and loss view is really just GL activity grouped by account category, with revenue and expense accounts netted appropriately. This example assumes revenue accounts are stored as credits and expense accounts as debits, which is the common convention.

SELECT
  coa.account_category,
  SUM(gl.credit_amount) - SUM(gl.debit_amount) AS net_amount
FROM gl_transaction_lines gl
JOIN chart_of_accounts coa
  ON gl.account_code = coa.account_code
WHERE gl.transaction_date BETWEEN '2026-01-01' AND '2026-01-31'
  AND coa.account_category IN ('Revenue', 'COGS', 'Operating Expense')
GROUP BY coa.account_category
ORDER BY coa.account_category;

Revenue accounts will show a positive net amount under this convention; expense and COGS accounts will show a negative one. Flip the sign in your presentation layer, not in the underlying query, so the raw numbers stay auditable.

Step 4: Build a Balance Sheet Snapshot

STEP 4

Cumulative balance as of a date

Unlike a P&L, which resets each period, a balance sheet is cumulative from the beginning of the ledger. The query below sums every transaction up to and including the snapshot date, not just within one period.

SELECT
  coa.account_name,
  coa.account_category,
  SUM(gl.debit_amount) - SUM(gl.credit_amount) AS ending_balance
FROM gl_transaction_lines gl
JOIN chart_of_accounts coa
  ON gl.account_code = coa.account_code
WHERE gl.transaction_date <= '2026-01-31'
  AND coa.account_category IN ('Asset', 'Liability', 'Equity')
GROUP BY coa.account_name, coa.account_category
ORDER BY coa.account_category, coa.account_name;

Step 5: Variance Analysis, Actual vs. Budget

STEP 5

Join actuals to a budget table

Variance analysis is almost always a join between your aggregated actuals query and a separate budget or forecast table, keyed on account and period.

WITH actuals AS (
  SELECT
    coa.account_category,
    SUM(gl.credit_amount) - SUM(gl.debit_amount) AS actual_amount
  FROM gl_transaction_lines gl
  JOIN chart_of_accounts coa ON gl.account_code = coa.account_code
  WHERE gl.transaction_date BETWEEN '2026-01-01' AND '2026-01-31'
  GROUP BY coa.account_category
)
SELECT
  b.account_category,
  b.budget_amount,
  a.actual_amount,
  a.actual_amount - b.budget_amount AS variance
FROM budget b
JOIN actuals a ON b.account_category = a.account_category
WHERE b.period = '2026-01';

Using a common table expression (CTE) for the actuals keeps the aggregation logic separate and readable, rather than burying it inside a larger join.

Step 6: Month-over-Month and YTD with Window Functions

STEP 6

Compare periods without a self-join

Window functions let you reference a prior period's value in the same row, which avoids the row-multiplying problems that come from self-joining a large GL table.

SELECT
  period_month,
  monthly_total,
  LAG(monthly_total) OVER (ORDER BY period_month) AS prior_month_total,
  monthly_total - LAG(monthly_total) OVER (ORDER BY period_month) AS month_over_month_change,
  SUM(monthly_total) OVER (ORDER BY period_month
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS year_to_date_total
FROM monthly_revenue_summary
WHERE period_month BETWEEN '2026-01-01' AND '2026-12-31'
ORDER BY period_month;

This assumes a pre-aggregated monthly_revenue_summary table or view. In practice, this is usually built as a view on top of the Step 3 query, rather than repeating the full GL join every time.

Common ERP Table Reference

Table 1. Where the generic structure above maps to real systems.
SystemHeader tableLine-item tableChart of accountsNotes
SAP ECC / S/4HANABKPFBSEG (or ACDOCA in S/4HANA)SKA1 / SKATS/4HANA consolidates most reporting onto the ACDOCA table; BSEG is retained mainly for compatibility
Oracle E-Business Suite / FusionGL_JE_HEADERSGL_JE_LINESGL_CODE_COMBINATIONSAccount segments are often combined into a single code combination ID
NetSuiteTransactionTransactionLineAccountQueried through SuiteQL rather than a direct database connection
Microsoft Dynamics 365 F&OLedgerJournalTransGeneralJournalAccountEntryMainAccountNewer versions route most reporting through GeneralJournalAccountEntry
QuickBooks OnlineNot directly exposedNot directly exposedNot directly exposedData is pulled through the QuickBooks reporting API or a third-party connector, not a direct SQL connection
Common mistakes to avoid

Row-multiplying joins. Joining a header table to two different one-to-many child tables in the same query (for example, line items and tax details) multiplies rows and inflates totals. Aggregate each child table separately before joining, or join them one at a time and check row counts as you go.

Getting debit and credit signs backwards. Whether an account increases with a debit or a credit depends on its account type. Assets and expenses normally increase with debits; liabilities, equity, and revenue normally increase with credits. Reversing this silently flips your entire report.

Forgetting to exclude reversed or voided entries. Many ERPs keep reversed journal entries in the same table as the original, flagged by a status or document type field. Missing that filter double-counts activity that was later reversed.

Mixing fiscal year and calendar year. A company with a fiscal year starting in April will have period numbers that do not match calendar months. Always confirm which calendar the period fields actually follow before filtering by date.

Querying the live transactional database directly. Large aggregation queries against a production ERP database can slow down the system for everyone entering transactions. Use a reporting replica or data warehouse whenever one is available.

Key Takeaways

  • Almost every ERP separates transactions into a header table and a line-item table, with a separate chart of accounts for labels.
  • Always filter by date range first; GL tables are typically the largest in the database.
  • A P&L is GL activity grouped by account category within a period; a balance sheet is a cumulative balance as of a date.
  • Use a CTE to keep actuals aggregation separate from the budget join in variance analysis.
  • Window functions handle month-over-month and year-to-date comparisons without the row-multiplying risk of a self-join.
  • Table names differ by ERP, but the underlying header, line-item, and chart-of-accounts pattern is close to universal.

Frequently Asked Questions

Do finance teams really need SQL if they already use Excel?

Excel works well once data is already extracted, but most ERP and accounting systems store millions of transaction rows across normalized tables. SQL is what actually retrieves and shapes that data before it ever reaches Excel or Power BI.

What is the difference between the general ledger and sub-ledgers?

The general ledger holds summarized financial transactions by account. Sub-ledgers, such as accounts payable or accounts receivable, hold the detailed supporting transactions that roll up into the general ledger.

Why do debits and credits matter in SQL queries against a general ledger?

Most general ledger tables store amounts as separate debit and credit columns, or as a single signed amount depending on account type. Getting the sign convention wrong is one of the most common causes of a report that does not match the actual financial statements.

Can I connect SQL directly to QuickBooks?

QuickBooks Online does not expose a direct SQL connection. Data is typically pulled through its reporting API or through a third-party connector that replicates the data into a warehouse you can query with SQL.

What is the most common mistake when joining GL tables?

Joining a header table to a line-item table and then joining in another one-to-many table, such as tax details, without realizing it multiplies rows. This silently inflates totals unless you aggregate before joining or use a pre-aggregated subquery.

Do I need to learn a specific ERP's schema, or is general SQL enough?

General SQL skills, joins, aggregation, and window functions, transfer across every ERP. What changes between systems is table and column naming, which you can look up once you know what you are searching for.

Related Articles

External References

  • SAP Help Portal, Finance Tables and General Ledger Line Items: help.sap.com
  • Oracle Fusion Cloud Applications, General Ledger documentation: docs.oracle.com

About this guide. Table and column names vary by ERP version and configuration. Always confirm the exact schema with your system administrator before building production reports.