CertSafari
    Snowflake SnowPro Core Certification (COF-C03)· Lessons

    Domain 4 · Lesson 16/19

    Aggregate Functions, Window Functions and QUALIFY in Snowflake SQL

    Perform data transformation techniques

    14 min read
    5.25% of exam
    9 sources
    Published 5 Oct 2026
    Docs as of 4 Oct 2026

    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.

    Selected aggregate function families and what they are for
    FamilyExample functionsUse
    General aggregationSUM, AVG, COUNT, MEDIAN, LISTAGGExact totals, averages, counts and string concatenation
    Semi-structured data aggregationARRAY_AGG, OBJECT_AGGBuild 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_PERCENTILEApproximate percentiles
    Aggregation utilitiesGROUPING (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:

    COUNT over two columns ignores every row with a NULL in either onesql
    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?

    Sources12

    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:

    AVG as a window function: one average repeated on each row of its partitionsql
    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.

    The two window frame modes
    ModeWhat the frame containsConstraints on explicit offsets
    ROWSA physical number of rows around the current rown PRECEDING / n FOLLOWING count rows
    RANGEA logically computed set of rows, based on the ORDER BY valueOnly 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.

    RANK over one partition, highest sales firstsql
    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;

    Checkpoint 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?

    Sources34

    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. 1.DISTINCT
    2. 2.HAVING
    3. 3.LIMIT
    4. 4.WHERE
    5. 5.ORDER BY
    6. 6.QUALIFY

    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?

    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?

    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?

    Sources5678

    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. 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.

      Covered in Aggregate functions: many rows in, one row out

    2. 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. 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.

      Covered in Shaping SQL for simpler, leaner transformations

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 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. 2.
      “Uses HyperLogLog to return an approximation of the distinct cardinality of the input”
      ↩︎ Aggregate functions: many rows in, one row out
    3. 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. 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. 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. 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. 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. 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

    Ready to test yourself?

    Practise the 21 questions on this subdomain.

    Spotted a mistake, or was something unclear? Tell us.