November 4 · Analyze across rows and inspect the work
For every station-day, how unusual was precipitation, how did it change, and which records rank highest?
We need comparisons across rows without losing the individual rows.
This course follows and adapts Alex Reinhart’s MADS Computing course. Today’s window-function concepts follow his Advanced SQL chapter; ranking, frames, indexing, and plan-reading use current PostgreSQL behavior and the Fall 2026 weather database.
You should be able to:
EXPLAIN to justify an index decision.GROUP BY station_id
AVG(precip) OVER (PARTITION BY station_id)
Choose based on the result grain you need.
OVER turns an aggregate into a window functionEvery row remains visible.
means:
Without PARTITION BY, the window contains all result rows.
WHEREfollowed by:
calculates each station’s 2025 mean, not its all-time mean.
For each known 2025 observation, attach its station’s 2025 mean precipitation and calculate precip - station_mean.
Return station ID, date, precipitation, mean, and anomaly. Keep every station-day.
orders rows for the calculation.
The final result still needs its own:
ROW_NUMBER creates a within-group orderThe extra date term makes ties deterministic.
| Function | Tied values | Next rank |
|---|---|---|
ROW_NUMBER() |
different numbers | next integer |
RANK() |
same rank | leaves gaps |
DENSE_RANK() |
same rank | no gaps |
Pick the meaning your question requires.
WHEREThis is too early:
Create the rank in a CTE, then filter in the outer query.
The same evaluation-order issue appeared with CASE aliases.
Return the three wettest known 2025 days for every station. Break precipitation ties by earlier date.
Use ROW_NUMBER() inside a CTE and filter wet_rank outside it.
LAG looks backward in the ordered partitionThe first row in each station has no previous value.
Check the dates too. The previous row may not be the previous calendar day when observations are missing.
This is seven rows—not necessarily seven calendar days.
An ordered aggregate has a default frame, but defaults are easy to misread.
Use an explicit frame for:
The frame is part of the analytical definition.
One named specification keeps related calculations aligned.
On a large table, PostgreSQL must choose how to find those rows.
For equality by station followed by a date range:
But first inspect the schema: UNIQUE (station_id, date) already creates a supporting unique index.
Do not create a redundant index.
An index can reduce rows scanned, but it also:
“Add an index” is a hypothesis to test, not a ritual.
EXPLAIN shows the proposed planLook for nodes such as Seq Scan, Index Scan, Sort, and WindowAgg.
EXPLAIN ANALYZE runs the queryIt reports actual rows and timing—but executes the statement.
In class, use it only with safe SELECT queries in your team schema.
Compare estimated versus actual rows first. Then inspect the scan, rows removed by filters, repeated loops, and execution time.
Large estimate errors can lead to a poor plan and may indicate stale statistics or skewed data.
Run EXPLAIN on the station-and-date query. Determine whether the existing schema already supplies an index, then predict whether PostgreSQL should use it.
Record the scan node and estimated rows. If permitted, run EXPLAIN ANALYZE and compare actual rows.
PostgreSQL may choose a sequential scan when:
The same SQL can receive a different plan after the data or statistics change.
Add one window query and one plan note to the project query library.
Document the partition, order, frame, and result grain. Explain why a window is needed, identify the key plan node, and decide whether an index change is justified.
Save today’s project-transfer query.
Add two checks:
Include the EXPLAIN output as evidence, not decoration.
Next: send parameterized SQL from code and turn queries into a pipeline.
Adapted from Alex Reinhart · MADS Computing