01 · Advanced SQL for Pipelines¶
Level 1 got you writing SELECT, WHERE, and basic JOINs. Production
pipelines lean on a handful of SQL patterns far more often: window functions
for running calculations, CTEs for readable multi-step logic, and MERGE/
UPSERT for idempotent loads. This lesson builds all three against a small
in-memory SQLite database.
What actually ran
Queries were run against SQLite via Python's sqlite3 module. SQLite
doesn't support MERGE, so the upsert section uses INSERT ... ON
CONFLICT, which is standard in SQLite, Postgres, and most warehouses.
Setup¶
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("""
CREATE TABLE sales (
sale_id INTEGER PRIMARY KEY,
region TEXT,
rep TEXT,
amount REAL,
sale_date TEXT
)
""")
rows = [
(1, "east", "alice", 500, "2026-01-05"),
(2, "east", "alice", 300, "2026-01-12"),
(3, "east", "bob", 700, "2026-01-06"),
(4, "west", "carla", 900, "2026-01-07"),
(5, "west", "carla", 400, "2026-01-20"),
(6, "west", "dan", 200, "2026-01-21"),
]
conn.executemany("INSERT INTO sales VALUES (?,?,?,?,?)", rows)
conn.commit()
Window functions: running totals and ranks¶
A window function computes a value per row while still having access to
a group of related rows (the "window") — unlike GROUP BY, it doesn't
collapse rows into one.
query = """
SELECT
region, rep, amount, sale_date,
SUM(amount) OVER (PARTITION BY region ORDER BY sale_date) AS running_total,
RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rank_in_region
FROM sales
ORDER BY region, sale_date
"""
for row in conn.execute(query):
print(row)
('east', 'alice', 500.0, '2026-01-05', 500.0, 2)
('east', 'bob', 700.0, '2026-01-06', 1200.0, 1)
('east', 'alice', 300.0, '2026-01-12', 1500.0, 3)
('west', 'carla', 900.0, '2026-01-07', 900.0, 1)
('west', 'dan', 200.0, '2026-01-21', 1100.0, 3)
('west', 'carla', 400.0, '2026-01-21', 1500.0, 2)
PARTITION BY region resets the window per region — exactly like GROUP BY
would, except every source row still appears in the output. This is the
pattern behind "running revenue per day," "rank customers by spend," and
"percent of total per category" — all without a self-join.
CTEs: making multi-step logic readable¶
A pipeline that computes "top rep per region, only regions with >$1000 total"
naively becomes a tangle of subqueries. A CTE (WITH ... AS) names each step:
query = """
WITH region_totals AS (
SELECT region, SUM(amount) AS total
FROM sales
GROUP BY region
),
qualifying_regions AS (
SELECT region FROM region_totals WHERE total > 1000
),
ranked_reps AS (
SELECT region, rep, SUM(amount) AS rep_total,
RANK() OVER (PARTITION BY region ORDER BY SUM(amount) DESC) AS rnk
FROM sales
GROUP BY region, rep
)
SELECT r.region, r.rep, r.rep_total
FROM ranked_reps r
JOIN qualifying_regions q ON r.region = q.region
WHERE r.rnk = 1
"""
for row in conn.execute(query):
print(row)
Each CTE is a named, testable step — you can run region_totals alone to
sanity-check it before trusting the final JOIN. This is the same discipline
as breaking a Python script into functions, applied to SQL.
Idempotent loads: INSERT ... ON CONFLICT¶
Pipelines rerun — after failures, on manual backfills, on scheduler retries. An upsert makes reruns safe by updating existing rows instead of duplicating them:
conn.execute("""
CREATE TABLE rep_summary (
region TEXT,
rep TEXT,
total_amount REAL,
PRIMARY KEY (region, rep)
)
""")
def upsert_summary():
conn.execute("""
INSERT INTO rep_summary (region, rep, total_amount)
SELECT region, rep, SUM(amount) FROM sales GROUP BY region, rep
ON CONFLICT(region, rep) DO UPDATE SET
total_amount = excluded.total_amount
""")
conn.commit()
upsert_summary()
upsert_summary() # run twice — must not duplicate or error
for row in conn.execute("SELECT * FROM rep_summary ORDER BY region, rep"):
print(row)
excluded.total_amount refers to the value that would have been inserted —
standard syntax across SQLite, Postgres, and most cloud warehouses (Snowflake
and BigQuery instead use a MERGE statement with the same intent).
Traps¶
- Confusing
WHEREandHAVING.WHEREfilters rows before aggregation;HAVINGfilters afterGROUP BY. Trying to referenceSUM(amount)inWHEREraises a "no such column" error in most engines. - Window functions in
WHERE. You cannot filter directly on a window function's result in the same query level (WHERE rank_in_region = 1fails) — wrap it in a CTE or subquery first, as done above. - Forgetting
ORDER BYinsideOVER(). Without it,SUM(...) OVER (PARTITION BY region)computes the whole partition's total on every row, not a running total — a subtle bug that silently gives wrong numbers. - Assuming every warehouse supports
MERGE. SQLite and Postgres useINSERT ... ON CONFLICT; MySQL usesON DUPLICATE KEY UPDATE; Snowflake and BigQuery useMERGE INTO. Check your target's dialect before copying upsert SQL between projects.
Cheat sheet¶
| Pattern | Use for | Key clause |
|---|---|---|
| Window function | Per-row calc needing group context | OVER (PARTITION BY ... ORDER BY ...) |
| CTE | Readable multi-step logic | WITH name AS (...) |
| Upsert | Idempotent loads | INSERT ... ON CONFLICT DO UPDATE |
| Rank vs. row_number | Ties share a rank vs. never tie | RANK() vs ROW_NUMBER() |
How It Actually Works¶
Window functions (ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)) execute in a phase distinct from GROUP BY: the engine partitions rows into the specified buckets, sorts each partition by the ORDER BY clause, then walks each sorted partition computing the running value (rank, running sum, lag/lead) per row — critically, without collapsing rows the way GROUP BY does. That's the mechanical reason a window function can sit alongside ungrouped, row-level columns in the same SELECT: it's a per-row annotation computed over a sorted, partitioned view of the data, not an aggregation that reduces cardinality.
INSERT ... ON CONFLICT (key) DO UPDATE (an upsert) is atomic at the row level because the database checks the unique/primary-key constraint on the conflict target and chooses insert-or-update behavior within the same statement's execution, under the same transaction and locks — there's no window where a concurrent writer could see a duplicate-key error and a stale row simultaneously. CTEs (WITH x AS (...)) are (in most engines) either inlined into the outer query plan or materialized as a temporary result set depending on the optimizer's cost estimate; either way they don't change what gets computed, only how the planner names and potentially reuses intermediate results, which is why CTEs are safe to reach for purely for readability.
Exercise¶
Add a discount_pct window calculation: for each sale, compute what percent
of that rep's total the sale represents (amount / SUM(amount) OVER
(PARTITION BY rep) * 100). Then extend the CTE query so it also excludes
reps whose single largest sale is more than 80% of their total (a sign of an
unreliable rep total based on one outlier deal).