Field manual · SQL traps
WHERE runs before the columns computed with OVER even exist. Filtering on a window function needs QUALIFY, or wrapping the query in a CTE and filtering from the outside.
Any filter on lag(), row_number(), sum() OVER (…) or another window function: 'the previous row less than 10 minutes ago', 'only the first of each group', 'only rows ranked first'.
Wrong: DuckDB rejects it, it won't even run
SELECT card, moment,
lag(moment) OVER (PARTITION BY card ORDER BY moment) AS previous
FROM validations
WHERE date_diff('minute', lag(moment) OVER (PARTITION BY card ORDER BY moment), moment) < 10;
-- error: a window function can't be called inside WHERE
Right: QUALIFY filters after the window is computed
SELECT card, moment,
lag(moment) OVER (PARTITION BY card ORDER BY moment) AS previous
FROM validations
QUALIFY date_diff('minute', previous, moment) < 10;
A query's real execution order is FROM → WHERE → GROUP BY → HAVING → *windows* → QUALIFY → SELECT. Window functions get computed after WHERE, so a WHERE can't reference lag(...) OVER (...): the column doesn't exist yet at that point, and DuckDB rejects it with an error, not a wrong answer. QUALIFY is exactly HAVING for windows: it runs after they're computed and can use their result directly, including the alias you gave it in the SELECT. On engines without QUALIFY (nearly all except DuckDB, Snowflake and BigQuery) the equivalent is wrapping the query in a CTE and putting the filter in the outer query's WHERE, where the computed column already exists as an ordinary column.
This trap is hiding inside a real case file, with real data.
Play the first case free →