Blog · SQL × Marketing

SQL for marketers: the eight queries that cover almost everything

Published September 7, 2026 · 9 min read

The SQL a marketer actually needs is a much shorter list than any course will admit. Not joins across twelve tables, not window functions, not query planners. Eight shapes — filter a window, group by something, compare two periods, compute a rate correctly — and you can answer nearly every question a channel report raises. This is that list, written against real marketing data, plus the one mistake that quietly corrupts more marketing numbers than all the others combined.

Why marketers are suddenly writing SQL

Dashboards answer the questions someone anticipated. The useful question is always the next one. Spend is up 18% — fine, but up on which campaigns, and did the conversions follow? A dashboard tile can't answer that unless somebody already built a tile for it, and by the time you've filed a ticket for one the moment has passed.

SQL is what closes that gap, and the barrier to it collapsed in the last two years for a reason that has nothing to do with SQL getting easier: an AI agent will write it for you. What you need is no longer the ability to type the syntax from memory. It's the ability to read a query and know whether it's asking the right question — because a wrong query returns a confident number, not an error.

So treat what follows as a reading list, not a typing exercise.

The shape you're querying

Almost all marketing SQL runs against one tidy table where each row is one day of one thing on one platform. In TableBI that table is facts, and the vocabulary is small enough to hold in your head:

  • Dimensions — the columns you slice by: date, platform, account, campaign, ad_group, ad, channel, country.
  • Measures — the columns you add up: impressions, clicks, cost, conversions, conversion_value.

Two things follow from that grain. First, every rate you care about — CTR, CPA, ROAS — is derived, not stored, which is where the classic mistake lives (section eight). Second, "last 28 days" has to be anchored to the data, not to your calendar, because ad platforms and Search Console finalize days late. Every query below uses the same anchor:

the date idiom
-- anchor the window to the newest day that exists, not to today
WHERE date >= (SELECT MAX(date) FROM facts) - 28

Write CURRENT_DATE instead and the last few days of your window will be partial or empty, and every trend you draw will bend downward at the right edge for no reason at all.

The eight queries

1. Totals for a window

The one you run first, every time, to see whether the data is even there.

sql
SELECT SUM(cost) AS spend, SUM(clicks) AS clicks, SUM(conversions) AS conv
FROM facts
WHERE date >= (SELECT MAX(date) FROM facts) - 28

2. The same totals, broken down

GROUP BY is the single highest-leverage keyword in marketing SQL. Swap the grouping column and the same query answers a different question: by platform it's a channel mix, by campaign it's a budget review, by country it's a geo audit.

sql
SELECT platform, SUM(cost) AS spend, SUM(conversions) AS conv
FROM facts
WHERE date >= (SELECT MAX(date) FROM facts) - 28
GROUP BY platform
ORDER BY spend DESC

3. Filter to the slice you mean

WHERE narrows rows before aggregation. The trap is filtering on a value that doesn't exist — a campaign name you half-remember, a platform label spelled differently than you assumed — which returns zero rows and looks exactly like "that campaign had no spend."

sql
SELECT campaign, SUM(cost) AS spend
FROM facts
WHERE date >= (SELECT MAX(date) FROM facts) - 28
  AND platform = 'google_ads'
  AND campaign LIKE '%brand%'
GROUP BY campaign ORDER BY spend DESC

Check the vocabulary before you filter on it — tablebi values --dimension campaign lists the values a dimension actually takes, so filters hit real data instead of your memory of it.

4. Top N, and the tail you're ignoring

ORDER BY … LIMIT is obvious. What's less obvious is that a top-10 view hides the shape of the distribution: ten campaigns at the top can be 90% of spend or 30% of it, and the difference completely changes what you should do next.

sql
SELECT campaign, SUM(cost) AS spend,
       SUM(cost) * 100.0 / (SELECT SUM(cost) FROM facts
                            WHERE date >= (SELECT MAX(date) FROM facts) - 28) AS pct_of_spend
FROM facts
WHERE date >= (SELECT MAX(date) FROM facts) - 28
GROUP BY campaign ORDER BY spend DESC LIMIT 15

5. A trend line

Group by date instead of by a category and you get a time series — the input to every "is this getting better?" conversation.

sql
SELECT date, SUM(clicks) AS clicks, SUM(cost) AS spend
FROM facts
WHERE date >= (SELECT MAX(date) FROM facts) - 90
GROUP BY date ORDER BY date

6. Compare two windows

Week-over-week is the most-requested number in marketing and the most frequently botched one, because people compare a complete week against a partial one. Label each row by which window it falls into, then group by the label — the arithmetic stays honest and both halves stay the same length.

sql
SELECT CASE WHEN date >= (SELECT MAX(date) FROM facts) - 6
            THEN 'this_week' ELSE 'last_week' END AS window,
       platform, SUM(cost) AS spend, SUM(conversions) AS conv
FROM facts
WHERE date >= (SELECT MAX(date) FROM facts) - 13
GROUP BY 1, 2
ORDER BY platform, window

7. Drop to the raw layer when the unified one can't answer it

The cross-channel table exists so that spend is spend regardless of platform — which means it can only hold columns every platform has. Search Console has query and position; Meta doesn't. Those native fields live in per-platform raw tables underneath, at full granularity, nothing averaged away:

sql
SELECT query, SUM(impressions) AS imp, SUM(clicks) AS clicks
FROM search_console_raw
WHERE date >= (SELECT MAX(date) FROM search_console_raw) - 28
GROUP BY query ORDER BY imp DESC LIMIT 30

Knowing which altitude a question belongs to is most of the skill. "How did paid do?" is a unified-table question. "Which queries lost impressions after the core update?" is a raw-table one. We walked that split in detail in Claude Code analytics.

8. Rate metrics — the one you must not get wrong

Here is the mistake. CTR is clicks divided by impressions. So the CTR of a group of campaigns is the group's total clicks divided by the group's total impressions. It is not the average of the individual campaigns' CTRs.

sql
-- WRONG: averages an average. A campaign with 12 impressions
-- and a freak 50% CTR now outvotes one with 400,000 impressions.
SELECT campaign, AVG(ctr) FROM facts GROUP BY campaign

-- RIGHT: aggregate the parts, then divide.
SELECT campaign,
       ctr(SUM(clicks), SUM(impressions)) AS ctr,
       cpa(SUM(cost), SUM(conversions))   AS cpa,
       roas(SUM(conversion_value), SUM(cost)) AS roas
FROM facts
WHERE date >= (SELECT MAX(date) FROM facts) - 28
GROUP BY campaign ORDER BY roas DESC

The same trap has a specifically SEO-flavoured version: average position in Search Console must be weighted by impressions, or a keyword that showed up once at rank 3 drags your reported average up past a keyword that showed 40,000 times at rank 12. Those definitions are worth storing once rather than rederiving per query — TableBI exposes them as macros (ctr, cpc, cpm, cpa, cvr, roas, aov, avg_position) so the arithmetic is identical in every query, every dashboard, and every report. That's the same argument as defining KPIs once instead of rebuilding them, one layer down.

Rule of thumb: if a metric has the word "per" or "rate" in its definition, never let AVG() near it. Sum the numerator, sum the denominator, divide last.

You don't have to type any of this

The reason to learn the eight shapes is to audit queries, not to author them. In practice you say what you want in English and your agent writes the SQL — as long as it has something to run it against:

terminal
# install the CLI, teach the agent it exists, connect a source
npm i -g @tablebi/cli
tablebi login
tablebi install
tablebi connect gsc --site sc-domain:example.com

After that, "which campaigns spent more than last week without adding conversions?" is a sentence you type into Claude Code, and what comes back is a query you can read — which is exactly the skill this article is for. Under the hood it's one command:

claude code → tablebi
tablebi ask "SELECT platform, SUM(cost) AS spend,
             cpa(SUM(cost), SUM(conversions)) AS cpa
             FROM facts WHERE date >= (SELECT MAX(date) FROM facts) - 28
             GROUP BY platform ORDER BY spend DESC"

Two habits make agent-written SQL trustworthy. Have it interrogate the warehouse before it queries — tablebi schema for the fields that exist, tablebi values for the values a dimension takes, tablebi sample for the actual row shapes — so it filters on real data rather than plausible-sounding guesses. And read the trust block that comes back with every answer: how fresh each source is, and the caveats that matter, such as Search Console finalizing two to three days late and anonymizing rare queries, or GSC clicks and GA4 sessions being different populations that were never going to match.

When a query is worth keeping, it stops being a query and becomes a URL — the dashboard stores the SQL, not a snapshot, so it re-runs itself as data arrives. Here's a real one, built exactly this way from live Search Console data:

Where SQL is the wrong tool

Being honest about the boundary saves a lot of wasted evenings:

  • One-off arithmetic on twelve numbers. A spreadsheet is faster and nobody will ever re-run it.
  • Questions your data can't answer. No query recovers a conversion the pixel never fired, or splits credit between two channels that both claim it — that's an attribution problem, not a SQL problem.
  • Anything needing a pixel-placed layout. SQL produces a result set. If the deliverable is a branded PDF with a logo in the corner, the query is only the first half of the job.

FAQ

How much SQL does a marketer actually need?

Realistically: SELECT, WHERE, GROUP BY, ORDER BY, LIMIT, the aggregate functions, and CASE WHEN for period comparisons. That covers the eight query shapes above, which in turn cover the large majority of channel-reporting questions. Joins and window functions are genuinely optional if your data already lands in one tidy table.

What's the most common SQL mistake in marketing reports?

Averaging a rate. AVG(ctr) weights a campaign with twelve impressions the same as one with four hundred thousand. Always sum the numerator and the denominator separately, then divide — and weight average position by impressions for the same reason.

Can AI just write my marketing SQL for me?

Yes, and that's the practical workflow — but an agent needs a data backend to query, and you need to be able to read what it produced. A wrong query returns a confident number rather than an error, so the value of knowing these eight shapes is in the review, not the typing.

Why not filter on today's date?

Because ad platforms and Search Console finalize data days late, so the newest rows are partial or missing. Anchoring to (SELECT MAX(date) FROM facts) keeps the window pinned to data that actually exists, instead of bending every trend downward at the right edge.

Try it

Point these eight queries at your own channels.

terminal
npm i -g @tablebi/cli && tablebi install