The Power BI Fellowship
Reference · Reference

SQL you need to survive as a BI developer

Not SQL from scratch: the queries a BI developer reads and writes every week, why each matters for Power BI, and a decision guide for where each transformation should happen.

BI DeveloperSenior BI DeveloperBI EngineerSQL engineSSMSChecked 2 Oct 2026Download .md
On this page (5)

The SQL that matters, and why

Examples use the course's company pack (orders, order_lines, customers, products, regions; amounts are in each order's currency). The SQL track has assignments with answers that are executed in CI.

TopicWhat it's for in BILearn it
SELECT, WHEREExtract only the rows and columns the model needsQuerying for BI
JOINAssemble facts with their keys; find orphans with LEFT JOIN … WHERE x IS NULLQuerying for BI
GROUP BYPre-aggregate before import; reconcile totals with the sourceQuerying for BI
CTEsReadable, testable transformation stepsCTEs and windows
Window functionsDeduplicate, rank, running totals, "latest row per key"CTEs and windows
CASEClassify and clean in one passQuerying for BI
ViewsA stable, documented interface between source and modelViews and loads
Stored proceduresControlled, parameterised extracts and loadsViews and loads
IndexesMake DirectQuery and incremental-refresh queries fastSlow source queries
Execution plansSee why a query is slow before guessingSlow source queries
Temp tablesStage multi-step transformationsViews and loads
SCD patternsKeep dimension history correctKeys and SCDs
Incremental predicatesLoad only what changedViews and loads
Date functionsCalendars, fiscal periods, time zonesCalendars and time zones

Patterns you'll use weekly

One row per key (deduplicate)

sql
-- order_lines contains a re-exported batch: the same (OrderID, LineNo) appears twice
WITH ranked AS (
  SELECT ol.*,
         ROW_NUMBER() OVER (PARTITION BY OrderID, "LineNo" ORDER BY ProductKey) AS rn
  FROM order_lines AS ol
)
SELECT * FROM ranked WHERE rn = 1;

When the source has a load timestamp or version column, order by it descending so you keep the latest copy rather than an arbitrary one.

Orphans: facts with no dimension row

sql
SELECT ol.ProductKey, COUNT(*) AS lines
FROM order_lines AS ol
LEFT JOIN products AS p ON p.ProductKey = ol.ProductKey
WHERE p.ProductKey IS NULL
GROUP BY ol.ProductKey;

Reconcile the report with the source

sql
SELECT strftime('%Y-%m', o.OrderDate) AS month,          -- FORMAT(o.OrderDate, 'yyyy-MM') in T-SQL
       ROUND(SUM(ol.Qty * ol.UnitPrice * (1 - ol.DiscountPct / 100.0)), 2) AS net_sales,
       COUNT(DISTINCT o.OrderID) AS orders
FROM orders AS o
JOIN order_lines AS ol ON ol.OrderID = o.OrderID
GROUP BY 1
ORDER BY 1;

Put the result next to the same breakdown from your model. If they differ, the model is wrong until proven otherwise.

Running total

sql
SELECT month, net_sales,
       SUM(net_sales) OVER (ORDER BY month ROWS UNBOUNDED PRECEDING) AS running
FROM monthly_sales;

Incremental extract

sql
-- the predicate Power BI's incremental refresh folds into each partition query
SELECT * FROM orders
WHERE OrderDate >= @RangeStart AND OrderDate < @RangeEnd;

Note the >= and <: with <= on both ends a row exactly on the boundary loads into two partitions.

Reading an execution plan

When a DirectQuery visual or a refresh is slow, capture the SQL Power BI sends (Performance Analyzer → Copy query, or a trace in DAX Studio), run it in SSMS with Include Actual Execution Plan, and look for:

Then fix the source (index, view, pre-aggregated table) before you fix the model.

Where should this transformation happen?

Do work as far upstream as is practical, and as far downstream as is necessary. Roche's maxim, in practice:

OperationUsually preferWhy
Rename columns for business usersPower Query or the modelPresentation concern; cheap anywhere
Aggregate 300 million rowsSQL or the warehouseThe engine built for it, close to the data
A business metric (Revenue, Margin %)DAX measure in a shared semantic modelMust respond to filters; one definition for every report
Slowly changing dimension type 2WarehouseNeeds history the model can't reconstruct
A display label ("Q1 2026")Date table columnComputed once, sortable
Fix bad source dataAt the source, or the first upstream layerEvery consumer benefits; the model only hides it
A KPI shared across reportsMeasure in a certified modelPrevents three different "Revenue" numbers (Sprint 05)
One-off clean-up for an ad-hoc analysisPower QueryFast to change, nobody else depends on it
Joins that create the star schemaWarehouse views, or Power Query when there's no warehouseFolds to the source; keeps the model simple
Currency conversionUpstream, stored as converted amountsPer-row lookups in DAX are slow

Dialects

The course uses portable SQL, and CI runs it on SQLite. At work you'll mostly meet T-SQL (SQL Server, Azure SQL, Fabric Warehouse and SQL analytics endpoints). The differences you'll hit first: date formatting (FORMAT vs strftime), TOP n vs LIMIT n, string concatenation (+ or CONCAT vs ||), and ISNULL vs COALESCE (use COALESCE: it's standard).

Something missing or out of date? Open an issue or edit content/toolkit/guides/sql-for-bi.md.