swecrets

SELECT Execution: A Lifecycle Tour

Mechanics — Rung 215 min read

Learning Objectives

  • Name, in order, the four stages a SELECT passes through before returning a result, and what each stage is responsible for
  • Explain why an "unknown column" error and a syntax error are caught by two different stages, not the same one
  • Explain what vectorized execution concretely means at the block level, and why processing rows in blocks beats processing them one at a time
  • Predict whether running EXPLAIN on a query touches any table data, based on which stage it stops at

Why This Matters

Right now, "ClickHouse ran my query" is a black box: text goes in, rows come out. That black box is exactly where confusion breeds — why did a typo produce a different error than a missing table? Why does EXPLAIN seem to know what a query will do without actually doing it? Why is "vectorized execution," a term you met in Module 1 as ClickHouse's headline performance trick, not just marketing language but a specific, mechanical claim about how rows move through the engine? This lesson opens the box. Once you can name the four stages a query passes through — parse, analyze, plan, execute — those questions stop being mysteries and start being predictable consequences of which stage does what. That mental model is also the one every later lesson on EXPLAIN, performance, and debugging in this course builds on directly.

Why a SELECT needs stages at all

A SELECT statement arrives at the server as plain text. Somewhere between that text and a result set, ClickHouse has to answer several very different questions: Is this even grammatically valid SQL? Do the tables and columns it mentions actually exist? Given that they exist, what's the smartest order to do the work in? And then, mechanically, how do you actually pull the bytes off disk and turn them into an answer?

Those are four different kinds of question, and they depend on each other in a strict order. You can't check whether country is a real column until you know the query is even shaped like valid SQL. You can't decide the smartest execution order until you know which columns and tables you're actually working with. And you can't start reading data until you've decided what to read. So ClickHouse — like effectively every SQL engine — resolves these questions as four distinct stages, each one handing its output to the next:

  1. Parse
  2. Analyze
  3. Plan
  4. Execute

Each stage only knows what the previous one figured out. That separation is the whole reason the stages behave differently when something is wrong with your query — and it's worth walking through with one running example.

Take this query against the events table from the previous lesson (user_id, event_type, event_time):

Running example for this lessonsql
SELECT event_type, count() AS occurrences
FROM events
WHERE event_time >= '2024-01-15 00:00:00'
GROUP BY event_type
ORDER BY occurrences DESC
LIMIT 5;

Stage 1 — Parse: is this grammatically valid SQL?

Parsing turns the raw SQL text into an AST (abstract syntax tree) — a structured representation of what the query says, with no idea yet whether any of it is true. The parser recognizes that this text has a SELECT list, a FROM, a WHERE, a GROUP BY, an ORDER BY, and a LIMIT, and builds a tree capturing that shape. It does not check whether events exists, whether event_type is a real column, or whether count() makes sense here. Parsing is pure grammar — the same job a programming language's parser does with source code, long before anything is run.

If you write SELECT FORM events (typo on FROM), parsing is where that fails — the text doesn't match the grammar of a SELECT statement at all. That failure has nothing to do with whether events exists.

Stage 2 — Analyze: is any of this actually true?

Analysis takes the AST and checks it against the server's actual schema metadata — the same underlying catalog information system.tables, from the previous lesson, surfaces as query results. Does events exist in the current database? Does it have a column called event_type? Does count() accept the arguments it's given? Analysis also resolves things the parser couldn't: expanding a * into the table's actual column list, matching the alias occurrences to the expression it names, and figuring out concrete data types for every expression in the query.

This is the stage that throws "Unknown table" or "Unknown identifier" errors. SELECT nonexistent_column FROM events is perfectly valid grammar — the parser has no complaint — so it sails through stage 1. It fails in stage 2, once ClickHouse actually checks nonexistent_column against the table's real columns and finds nothing.

Stage 3 — Plan: what's the smartest order to do this in?

Once the query is validated, ClickHouse builds a query plan: a tree of logical steps describing what work needs to happen — read from events, filter by event_time, group by event_type, sort by occurrences, keep the top 5 — and in what order. This stage is also where ClickHouse applies optimizations before touching any data: pushing the WHERE filter as early as possible, figuring out that only event_type and event_time need to be read at all (not user_id, since nothing in this query touches it), and recognizing that aggregation has to run before the final sort, since occurrences doesn't exist as a value until aggregation produces it.

The plan is a description of intended work. Nothing has been read from disk yet.

Stage 4 — Execute: actually doing it

The plan is compiled into a running pipeline, and only now does ClickHouse touch the table's data. It reads column data from disk, applies the filter, aggregates, sorts, and limits — and it does all of that in blocks: chunks of many rows' worth of column values processed together, not one row at a time and not the entire table at once. Filtering a block means checking event_time against the cutoff for a few thousand values in one operation; aggregating a block means folding a few thousand event_type values into running counts in one pass. That block-at-a-time processing is what "vectorized execution" concretely means — the term isn't describing a vague performance philosophy, it's describing this specific unit of work.

ParseSQL text → AST.Pure grammar.AnalyzeAST checked againstreal schema metadataPlanLogical steps built,optimized. No data read.ExecutePlan runs as a pipeline —data read in blocks
One SELECT, four stages. A horizontal timeline with four labeled points connected by a line, left to right. Point one: 'Parse — SQL text becomes an AST. Pure grammar; nothing checked against real tables or columns yet.' Point two: 'Analyze — AST is checked against real schema metadata (system.tables). Unknown tables or columns fail here, not in parsing.' Point three: 'Plan — a tree of logical steps (read, filter, aggregate, sort, limit) is built, with optimizations like filter pushdown and column pruning applied. No data has been read yet.' Point four: 'Execute — the plan becomes a running pipeline that reads, filters, and aggregates actual column data in blocks of many rows at a time — vectorized execution.'

EXPLAIN: watching a stage without running the query

EXPLAIN lets you see the output of a stage directly, instead of inferring it from behavior. Run it in front of any SELECT, and instead of executing that query, ClickHouse shows you what one of the earlier stages produced. Used on its own, with no extra keyword, EXPLAIN shows the query plan — the output of stage 3, before any data has been touched.

Seeing the plan for the running examplesql
EXPLAIN
SELECT event_type, count() AS occurrences
FROM events
WHERE event_time >= '2024-01-15 00:00:00'
GROUP BY event_type
ORDER BY occurrences DESC
LIMIT 5;
Resulttext
┌─explain──────────────────────────────────────────┐
│ Expression (Projection)                           │
│   Limit (preliminary LIMIT (without OFFSET))      │
│     Sorting (Sorting for ORDER BY)                │
│       Expression (Before ORDER BY)                │
│         Aggregating                               │
│           Expression (Before GROUP BY)            │
│             Filter (WHERE)                        │
│               ReadFromMergeTree (events)          │
└────────────────────────────────────────────────────┘

Read from the bottom up, this is the shape of the query plan built in stage 3: read from events (a MergeTree-family table — the engine this course spends all of Module 3 on), filter by the WHERE clause, compute the pre-aggregation expression, aggregate by event_type, compute the pre-sort expression, sort by occurrences, and finally limit to 5 rows before projecting the output columns. Every line is a plan step, not a record of anything that has actually happened — EXPLAIN stops at the end of stage 3. There are other EXPLAIN variants that show earlier stages too (EXPLAIN AST shows the parse tree from stage 1, for instance), and later in this course you'll use EXPLAIN variants that show what execution itself looks like — but for now, the default is your window into the plan.

Common Misconception

"Since the parser is the first thing that looks at my query, a typo in a column name and a typo in the SQL grammar itself would fail at the same point."

They don't, and the difference is exactly the parse/analyze boundary. SELECT FORM events — a broken keyword — fails in stage 1, because the text doesn't match valid SELECT grammar at all; ClickHouse never gets far enough to ask what events even is. SELECT nonexistant_typo FROM events is a completely different failure: the grammar is fine, so it sails through parsing without complaint, and only fails in stage 2, once analysis checks nonexistant_typo against the table's real column list and finds nothing there. If you've ever been confused that two different kinds of "broken query" produced differently worded errors, this is why — they were caught by two different stages doing two different jobs.

Predict, Then Verify

The events table has 50 million rows. You run EXPLAIN SELECT event_type, count() FROM events WHERE event_time >= '2024-01-15 00:00:00' GROUP BY event_type against it.

Predict: does this EXPLAIN statement read any of those 50 million rows off disk? Why or why not, based on which stage EXPLAIN shows you?

Reveal the answer →

No — it reads none of the table's row data. Plain EXPLAIN shows the output of stage 3 (the plan), and the plan is a description of intended work: which steps will run, in what order, built entirely from schema metadata and query structure. Actually reading column values off disk is stage 4's job, and EXPLAIN never reaches stage 4 — it stops as soon as the plan exists. That's exactly why EXPLAIN is safe to run against a huge or expensive query without hesitation: no matter how large events is, asking "what would this query do" costs nothing close to what actually doing it would cost.

Summary

A SELECT passes through four ordered stages before producing a result. Parse turns SQL text into an AST based on grammar alone, with no knowledge of whether the tables or columns mentioned are real. Analyze checks that AST against actual schema metadata, resolving aliases and expanding wildcards along the way — this is the stage that catches unknown tables and columns, which is why a typo in a column name fails differently than a typo in SQL syntax. Plan builds a tree of logical steps (read, filter, aggregate, sort, limit) and applies optimizations like filter pushdown and column pruning, without touching any data. Execute turns that plan into a running pipeline that reads and processes actual column data in blocks of many rows at once — the concrete mechanism behind "vectorized execution." EXPLAIN, run plain, shows you the output of the plan stage directly, which is why it never reads a single row no matter how large the table is.

Quiz

  1. 1. A query has a typo: SELECT FORM events (FROM misspelled). At which stage does this fail, and why?

  2. 2. SELECT typo_column FROM events is grammatically valid but typo_column doesn't exist. At which stage does this fail?

  3. 3. What does "vectorized execution" concretely refer to, in terms of the four-stage lifecycle?

  4. 4. You run plain EXPLAIN (no extra keyword) in front of a SELECT against a table with a billion rows. What does ClickHouse show you, and does it touch the table's row data to produce it?

  5. 5. Put these in the order a SELECT actually passes through them: Analyze, Execute, Parse, Plan.

  6. 6. Why does it matter, practically, that Plan (stage 3) and Execute (stage 4) are separate stages rather than one combined step?