What you will be able to do
- Predict how aggregate functions treat NULLs and zero-row input
- Write window functions with PARTITION BY, ORDER BY and an explicit ROWS or RANGE frame
- Use QUALIFY to filter on window function results instead of wrapping the query in a subquery
- Use CTEs and higher-order functions to keep transformation SQL simpler
1.Aggregate functions: many rows in, one row out
An aggregate function reads many input rows and returns one output value. A scalar function such as COS returns one value for each input row. Run SUM(x) over a three-row table and you get one row. Run COS(x) over the same table and you get three. One detail is easy to miss: an aggregate always returns exactly one row, even when there are no input rows. Usually that row contains NULL, but some functions return 0 or an empty string instead.
Snowflake's list of aggregates goes well beyond SUM, AVG and COUNT. Several groups are directly useful in transformation work. The estimation families (HyperLogLog, t-Digest) return approximations rather than exact values: for example, HLL returns an approximation of COUNT(DISTINCT ...), so choose them only where an estimate is acceptable.
| Family | Example functions | Use |
|---|---|---|
| General aggregation | SUM, AVG, COUNT, MEDIAN, LISTAGG | Exact totals, averages, counts and string concatenation |
| Semi-structured data aggregation | ARRAY_AGG, OBJECT_AGG | Build ARRAY or OBJECT values from rows |
| Cardinality estimation (HyperLogLog) | APPROX_COUNT_DISTINCT (alias for HLL) | Approximate distinct counts, an approximation of COUNT(DISTINCT ...) |
| Percentile estimation (t-Digest) | APPROX_PERCENTILE | Approximate percentiles |
| Aggregation utilities | GROUPING (alias GROUPING_ID) | Not an aggregate itself; shows the aggregation level of a GROUP BY row |
NULL handling is the area exam questions most often test. Some aggregates ignore NULLs: AVG over 1, 5 and NULL gives 3. If every input value is NULL, the result is NULL. When you pass more than one column, as in COUNT(col1, col2), a row is ignored if any one of those columns is NULL. Expressions behave the same way. In SUM(x + y), a NULL in either column makes the expression NULL, so that row is skipped. GROUP BY is different: it keeps rows where some columns are NULL and gives them their own groups. The test table below has four rows, but only one has no NULLs:
SELECT COUNT(x, y) FROM test_null_aggregate_functions;
+-------------+
| COUNT(X, Y) |
|-------------|
| 1 |
+-------------+Checkpoint 1 of 7· Check yourself
A table has the rows (1, 2), (3, NULL), (NULL, 6) and (NULL, NULL). What does SELECT x, y FROM t GROUP BY x, y return?
Multi-column aggregates such as COUNT(x, y) skip rows that contain a NULL, but GROUP BY does not. All four combinations become groups.
“This behavior differs from the behavior of GROUP BY, which does not discard rows when some columns are NULL”Source: docs.snowflake.com
2.Window functions: one result per row, computed over a partition
A window function computes over a set of related rows, but it does not collapse them. It still returns one row for every input row. Many aggregates, including SUM, COUNT and AVG, have window versions with the same name. What turns one into a window function is the OVER clause. OVER has three optional parts: PARTITION BY, which divides rows into groups; ORDER BY, which sorts rows within each partition; and a window frame, which picks the rows around the current row. An empty OVER() is valid. It treats all rows as a single partition.
The regular AVG(menu_cogs_usd) ... GROUP BY 1 query returns one row per category. The window version below returns the category average on every one of the 60 rows, and the calculation starts again when the category changes:
SELECT menu_category,
AVG(menu_cogs_usd) OVER(PARTITION BY menu_category) avg_cogs
FROM menu_items
ORDER BY menu_category
LIMIT 15;With ORDER BY and a frame added, the same function becomes a moving average. Two kinds of window function need ORDER BY. The first is any function with an explicit frame, such as a running total or a moving average, because "preceding" and "following" mean nothing until the rows are sorted. The second is ranking functions such as RANK, DENSE_RANK and NTILE. The ORDER BY inside OVER only controls the order in which the function processes rows. It does not sort the query's output, so you often need both. Three more rules apply. Make the ordering deterministic by adding tiebreaker columns. Do not use ordinal positions inside OVER, because ORDER BY 2 there means the constant 2. And with some functions, ORDER BY implies a default frame, which is why Snowflake recommends always declaring the frame explicitly.
| Mode | What the frame contains | Constraints on explicit offsets |
|---|---|---|
| ROWS | A physical number of rows around the current row | n PRECEDING / n FOLLOWING count rows |
| RANGE | A logically computed set of rows, based on the ORDER BY value | Only one ORDER BY expression, of type DATE, TIMESTAMP (including TIMESTAMP_LTZ, TIMESTAMP_NTZ and TIMESTAMP_TZ) or NUMBER; n is an unsigned constant or an INTERVAL constant such as INTERVAL '3 days' |
Ranking works the same way. The query below ranks salespeople by total sales: the ORDER BY inside OVER sets the ranking order, and there is no PARTITION BY, so all rows form one partition. Jones (1000) gets rank 1 and Smith (600) gets rank 4. Add a separate ORDER BY to the query itself if you want the output displayed by rank.
SELECT
salesperson_name,
sales_in_dollars,
RANK() OVER (ORDER BY sales_in_dollars DESC) AS sales_rank
FROM sales_table;Checkpoint 2 of 7· Fill the gap
This moving average should average the current row and exactly the two physical rows after it, sorted on two columns. Which frame mode completes it?
SELECT menu_category, menu_price_usd, menu_cogs_usd,
AVG(menu_cogs_usd) OVER(PARTITION BY menu_category
ORDER BY menu_price_usd, menu_cogs_usd ? BETWEEN CURRENT ROW and 2 FOLLOWING) avg_cogs
FROM menu_items
ORDER BY menu_category, menu_price_usd, menu_cogs_usd
LIMIT 15;ROWS counts physical rows. RANGE with explicit offsets would also be wrong here, because it allows only one ORDER BY expression and this query sorts on two.
Source: docs.snowflake.comCheckpoint 3 of 7· Check yourself
A query has ORDER BY menu_price_usd inside OVER(...) but no ORDER BY at the end. What does the window ORDER BY guarantee?
The ORDER BY inside OVER is separate from the query's final ORDER BY. It only affects how the window function processes rows.
“An ORDER BY clause within an OVER clause controls only the order in which the window function processes rows”Source: docs.snowflake.com
3.Shaping SQL for simpler, leaner transformations
Snowflake gives you several ways to remove extra layers from transformation SQL. The first is QUALIFY, which filters on window function results. It does for window functions what HAVING does for aggregates and GROUP BY. Clauses in a SELECT normally run in this order: FROM, WHERE, GROUP BY, HAVING, WINDOW, QUALIFY, DISTINCT, ORDER BY, LIMIT. WHERE runs too early to see a window result. QUALIFY runs after the window step, so QUALIFY ROW_NUMBER() OVER (...) = 1 replaces the wrapping subquery. QUALIFY needs at least one window function, either in the SELECT list or in the QUALIFY predicate itself.
Checkpoint 4 of 7· Put it in order
Put these SELECT clauses in the order Snowflake usually evaluates them
- 1.DISTINCT
- 2.HAVING
- 3.LIMIT
- 4.WHERE
- 5.ORDER BY
- 6.QUALIFY
HAVING filters after grouping, QUALIFY filters after window functions are computed, and DISTINCT, ORDER BY and LIMIT come last.
“FROM WHERE GROUP BY HAVING WINDOW QUALIFY DISTINCT ORDER BY LIMIT”Source: docs.snowflake.com
The second is the WITH clause. It comes before the body of a SELECT and defines one or more common table expressions (CTEs). Each CTE is a named query that later parts of the statement can refer to, for example in the FROM clause. If a report needs the same aggregation in three places, you can write it once as a CTE and refer to it by name, instead of copying the logic into three subqueries. WITH RECURSIVE handles hierarchies by joining an anchor clause and a recursive clause with UNION ALL.
The third applies to array data. Higher-order functions (FILTER, REDUCE, TRANSFORM) change array elements inside the row. The documentation names avoiding LATERAL FLATTEN and one-off UDFs as a benefit, so you have fewer objects to create, maintain and grant access to.
Checkpoint 5 of 7· Check yourself
A report statement repeats one SALES aggregation in three subqueries. Which construct lets the developer define that logic once and refer to it by name everywhere else in the statement?
The WITH clause defines named CTEs that later parts of the same statement can refer to. QUALIFY filters window results, and the other two options do not name reusable queries.
“defines one or more CTEs (common table expressions) that can be used later in the statement.”Source: docs.snowflake.com
Beyond simplifying syntax, the documentation points to concrete ways to make SQL leaner. The guidance below comes from Snowflake's dynamic-table optimization page, but the underlying ideas apply to transformation queries generally:
- Filter early. Apply WHERE clauses as close to the base tables as possible, so later steps process fewer rows. - Watch blocking operators. Window functions, GROUP BY and DISTINCT must see all input rows before producing output. When one query combines several of them, each waits for the previous one, which reduces parallelism and increases memory pressure. Splitting a pipeline so each stage does one blocking step is the recommended remedy. - Remove redundant DISTINCT. Where data is already unique, or you deduplicate upstream, drop DISTINCT. QUALIFY with ROW_NUMBER = 1 is also explicit about which duplicate to keep. - Improve data locality. When rows with matching keys sit in fewer micro-partitions, less data is scanned. The page recommends clustering base tables by the JOIN, GROUP BY or PARTITION BY keys, and including PARTITION BY in window functions so the whole dataset is not treated as one partition.
The general query guide also lists techniques that improve performance: eliminating redundant joins (joins on a key column that refer to tables not needed), top-K pruning for SELECT statements with LIMIT and ORDER BY, reusing persisted query results, and reading query profiles and query insights to find what to fix. The search optimization service is also recommended for querying VARIANT data. Estimation functions such as HLL and APPROX_PERCENTILE trade exactness for an estimate, so use them only where an approximation is acceptable.
Checkpoint 6 of 7· Check yourself
A pipeline joins two large tables, aggregates the result and ranks it with a window function, all in one query, and it runs slowly with high memory pressure. Which change does the documentation recommend?
Joins, aggregations and window functions are blocking operators. Splitting them into separate stages and filtering early reduces the rows each stage handles.
“Filter early. Apply WHERE clauses in the dynamic tables closest to your base tables so that downstream tables process fewer rows.”Source: docs.snowflake.com
Checkpoint 7 of 7· Exam question
A CUSTOMER_EVENTS table logs multiple status updates per customer with an `updated_at` timestamp. A report needs exactly one row per customer, the row with the most recent `updated_at`, without collapsing any other columns through aggregation. Which pattern is the standard way to write this in Snowflake?
Correct answer: A — Use `ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC)` in a QUALIFY clause filtering for `row_number = 1`, skipping a wrapping subquery.
- A. ROW_NUMBER assigns a rank within each customer's partition ordered by recency, and QUALIFY filters directly on that window function result in the same query, which is the standard latest-row-per-group pattern.
- B. The MAX-and-self-join approach works but requires an extra join step and can return duplicate rows if two events share the exact same maximum timestamp for a customer, unlike a direct QUALIFY filter.
- C. Filtering on a COUNT window function only isolates customers with exactly one event total; it does nothing to identify the most recent row for customers who have multiple status updates.
- D. DISTINCT on `customer_id` alone is not valid alongside other selected columns and does not guarantee which row's values are kept, so it cannot reliably select the most recent event.
4.Reading VARIANT paths: case and quoting
Semi-structured data stored in a VARIANT column is read with a path: a colon after the column name, then dot notation (src:salesperson.name) or bracket notation (src['salesperson']['name']). Two rules matter for exam questions. First, the column name is case-insensitive but element names are case-sensitive, so SRC:salesperson.name matches src:salesperson.name but SRC:Salesperson.Name does not. Second, JSON keys follow different rules from SQL identifiers: if an element name does not conform to identifier rules, for example because it contains a space, enclose it in double quotes.
Sources6
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.COUNT(x, y) counts every row where at least one of the two columns has a value.Why is that wrong?
When an aggregate is given several columns, it skips any row where even one of them is NULL. Only rows with every column filled in are counted.
2.OVER (PARTITION BY 1 ORDER BY 2) partitions by the first column and sorts by the second, just like a query-level ORDER BY 2.Why is that wrong?
Ordinal positions are not supported inside a window's ORDER BY. There, the number is read as a constant.
Covered in Window functions: one result per row, computed over a partition
3.You can filter on a ROW_NUMBER() result in WHERE, or with HAVING, in the same SELECT.Why is that wrong?
WHERE and HAVING both run before window functions are computed. QUALIFY is the clause that filters on window results in the same SELECT.
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“An aggregate function always returns exactly one row, even when the input contains zero rows.”
↩︎ Aggregate functions: many rows in, one row out“If all of the values passed to the aggregate function are NULL, then the aggregate function returns NULL.”
↩︎ Aggregate functions: many rows in, one row out“In these instances, the aggregate function ignores a row if any individual column is NULL.”
↩︎ Exam trap 1“AVG calculates the average of values 1, 5, and NULL to be 3”
↩︎ Prediction“This behavior differs from the behavior of GROUP BY, which does not discard rows when some columns are NULL”
↩︎ Checkpoint - 2.
“Uses HyperLogLog to return an approximation of the distinct cardinality of the input”
↩︎ Aggregate functions: many rows in, one row out - 3.
“A window function is an analytic SQL function that operates on a group of related rows known as a partition.”
↩︎ Window functions: one result per row, computed over a partition“Snowflake recommends declaring window frames explicitly.”
↩︎ Window functions: one result per row, computed over a partition“If multiple rows have the same value for the ORDER BY columns, add additional columns as tiebreakers”
↩︎ Window functions: one result per row, computed over a partition“In this context, 2 is interpreted as the constant 2; it does not refer to the second column in the query.”
↩︎ Exam trap 2“An ORDER BY clause within an OVER clause controls only the order in which the window function processes rows”
↩︎ Checkpoint - 4.
“RANGE BETWEEN window frames with explicit offsets must have only one ORDER BY expression.”
↩︎ Window functions: one result per row, computed over a partition“The following example shows how to rank sales based on the total amount (in dollars) that each salesperson has sold.”
↩︎ Window functions: one result per row, computed over a partition - 5.
“In a SELECT statement, the QUALIFY clause filters the results of window functions.”
↩︎ Shaping SQL for simpler, leaner transformations“The QUALIFY clause requires at least one window function to be specified”
↩︎ Shaping SQL for simpler, leaner transformations“QUALIFY does with window functions what HAVING does with aggregate functions and GROUP BY clauses.”
↩︎ Exam trap 3“QUALIFY is therefore evaluated after window functions are computed.”
↩︎ Prediction“FROM WHERE GROUP BY HAVING WINDOW QUALIFY DISTINCT ORDER BY LIMIT”
↩︎ Checkpoint - 6.
“Without higher-order functions, this type of manipulation requires LATERAL FLATTEN operations or user-defined functions (UDFs).”
↩︎ Shaping SQL for simpler, leaner transformations“the column name is case-insensitive but element names are case-sensitive.”
↩︎ Reading VARIANT paths: case and quoting“then you must enclose the name in double quotes”
↩︎ Reading VARIANT paths: case and quoting - 7.
“When a query combines multiple blocking operators, each one must wait for the previous to finish.”
↩︎ Shaping SQL for simpler, leaner transformations“Filter early. Apply WHERE clauses in the dynamic tables closest to your base tables so that downstream tables process fewer rows.”
↩︎ Shaping SQL for simpler, leaner transformations - 8.
“Learn about redundant joins, and how to eliminate them to improve query performance.”
↩︎ Shaping SQL for simpler, leaner transformations“SELECT statements that use top-K pruning scan a subset of rows, which can improve performance.”
↩︎ Shaping SQL for simpler, leaner transformations
Also cited
“defines one or more CTEs (common table expressions) that can be used later in the statement.”
↩︎ Checkpoint