Time & dates
Concept  ·  SQL & Querying

generate_series / a calendar table fills gap-free reporting where data has missing days.

GROUP BY can only group what's there — quiet days aren't rows, so charts skip them, moving averages contract, and 'days since last sale' becomes uncomputable. The spine inverts the direction: start from time itself (a generated series or a calendar table), attach data where it exists, COALESCE the rest. The COALESCE is the sum_empty_null decision wearing dates; the spine is also the honest enabler for ROWS-frame rolling metrics over gapped data.

See it break
fees
damount
2024-02-0280
The trap
SELECT
  strftime(d, '%m-%d') AS day,
  SUM(amount)AS total
FROM
  fees GROUP BY
  d
ORDER BY
  d;
The fix
SELECT
  strftime(t.g, '%m-%d') AS day,
  COALESCE(SUM(f.amount), 0) AS total
FROM
  generate_series(
    DATE '2024-02-01',
    DATE '2024-02-04',
    INTERVAL 1 DAY
  ) AS t (g)
  LEFT JOIN fees f ON f.d = t.g
GROUP BY
  t.g
ORDER BY
  t.g;
The trap
02-0280
The fix
02-010
02-0280
02-030
02-040

The timeline skipped the quiet days — a bare GROUP BY only produces rows for dates that have data, so three zero-enrolment days simply vanished until a generated spine LEFT JOINed them back.

The rule

generate_series (or a persistent calendar table — the production habit) + LEFT JOIN + COALESCE.

Shows up elsewhere
ROWS counts rows, RANGE counts timeSUM over an empty / all-NULL group
Try one →Prove it in practice →