Aggregate first, then filter
Group and aggregate first, then filter on the aggregated values withHAVING.
Compute rates as a ratio of sums
For conversion rate, ROI, engagement rate, and anything else that’s a fraction: divide the total numerator by the total denominator.SUM(conversions) / NULLIF(SUM(visits), 0) never crashes on an empty period; SAFE_DIVIDE does the same on BigQuery.
Keep valid zeros
A day with 0 purchases still belongs in your analysis. Filtering out zero rows creates survivorship bias — the metric looks better because the bad days are gone, not because anything improved.One timeframe, one attribution rule
A metric should use one date range and one time anchor. “Revenue from January” is a metric; “revenue from January divided by conversions from February” is a bug, unless the offset is deliberate and stated. When timing is ambiguous, pick the anchor explicitly — created date or resolved date, order date or payment date — and stick to it. The same goes for currency: “Revenue Jan 1–31 (USD)” leaves nothing to interpretation.Watch the grain of your joins
Decide what one row of the result means — one row per date, per customer, per campaign — and check that joins preserve it. A join to a table with multiple matching rows multiplies your data (a fan-out join), and everySUM downstream of it is silently inflated.
The safe pattern: aggregate → join → final result. Collapse each side to the grain you need first, then join.
Return names, not just IDs
reads a lot better than the same table without
campaign_name. Include the human-readable label whenever one exists.
Before you save
A thirty-second check that catches most of the above:- Completeness — right scope, right date range, right units and filters
- Bias — no dropped zeros, no mixed time windows or attribution rules
- Grain — joins didn’t multiply rows
- Safety — no PII or sensitive columns in the output unless explicitly required and permitted
- One query — a single query (or one structured set of CTEs), not fragments the reader has to assemble
FAQ
Can I skip filtering a particular table in a sub-query? Yes. You can keep parts of your query unaffected by dashboard filters — just add$$ in front of the table reference. See Partial Filters.
