An alias for a window result is unavailable to WHERE at the same query level. Filtering without QUALIFY requires another query level in SQL Server. Calculate the window value first, then apply the filter where that value is part of the input.

Follow the Logical Query Order
SQL Server does not provide a QUALIFY clause. Window functions are evaluated in permitted SELECT and ORDER BY contexts after the input filtering stages. A direct WHERE condition containing ROW_NUMBER is invalid and produces the window-function placement error associated with error 4108.
I explain this as a query-level problem rather than an alias problem. Replacing the alias with the full window expression in the same WHERE does not fix the evaluation order. The calculation needs another query level before it can become a normal filterable column.
The examples use synthetic orders with repeated customer identifiers and tied dates. OrderID supplies a deterministic tie breaker when choosing one latest order. Decide that rule before writing the window. SQL Server's logical processing order is consistent. It does not make an exception because the alias looked very convenient on the previous line.
Writing a windowed filter without QUALIFY requires a separate query scope in which the calculated window value becomes available.
Filter Without QUALIFY Using a CTE
A common table expression makes the two levels easy to read. Its SELECT calculates ROW_NUMBER within each customer, ordered by newest date and then newest identifier. The outer SELECT keeps rows numbered one. The window definition and filter remain next to each other without mixing their evaluation stages.
The CTE is a named query expression, not an instruction to materialize a temporary table. The optimizer can transform the combined statement. Read the actual plan when performance matters rather than assuming the extra level adds a separate physical scan.
I retain the full latest-row ordering in the test. A date-only order can select different tied rows between executions. If the business wants all orders on the latest date instead of one order, ROW_NUMBER is the wrong tie policy. Use an appropriate ranking or date comparison that preserves those ties. The technique solves filtering placement, while the ranking choice still defines the result.
CREATE TABLE #QualifyOrders(OrderID int PRIMARY KEY,CustomerID int,OrderDate date,Amount decimal(12,2));
INSERT #QualifyOrders VALUES(1,10,'20260920',10),(2,10,'20260921',20),
(3,10,'20260921',30),(4,20,'20260920',15);
WITH Ranked AS
(
SELECT *,ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY OrderDate DESC,OrderID DESC) AS RowNumber
FROM #QualifyOrders
)
SELECT OrderID,CustomerID,OrderDate,Amount
FROM Ranked WHERE RowNumber=1 ORDER BY CustomerID;Use a Derived Table When Local Nesting Reads Better
A derived table provides the same two-level structure inside FROM. Give it an alias and expose the window result as a named column. The outer WHERE then filters that column like any other input value. Choose the form that makes the surrounding query easier to maintain.
The next query preserves the CTE version's partition and ordering rules. Compare their outputs by identifier. They are alternatives expressing the same rule, not two different ways to define latest. In many cases, SQL Server produces equivalent or closely related plans after optimization.
What scope should the window see? A filter inside the derived table changes its input before ranking. A filter outside sees the ranked result. If you rank only orders in one date range, latest means latest within that range. If you rank all orders and then filter the winning row, the result has a different meaning. Place each predicate according to that requirement.
SELECT OrderID,CustomerID,OrderDate,Amount
FROM
(
SELECT *,ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY OrderDate DESC,OrderID DESC) AS RowNumber
FROM #QualifyOrders
) AS Ranked
WHERE RowNumber=1
ORDER BY CustomerID;
Understand the TOP WITH TIES Pattern
TOP(1) WITH TIES ordered by ROW_NUMBER can return the first-ranked row from each partition. Every partition contributes a row whose row number is one, so those rows tie on the ordering expression. The pattern is compact, but its logic deserves explanation before it enters shared code.
The next query wraps that result for a separate presentation order. Adding another tie-breaking expression directly to the inner TOP ORDER BY would change which rows tie and could defeat the intended per-partition selection. Keep the row-selection order and display order distinct.
This pattern can introduce a sort across the qualifying input. Check the actual plan on a large table and compare it with the straightforward CTE version. A shorter statement is not automatically a cheaper plan. Also remember that ROW_NUMBER still chooses one row per partition according to its own tie breakers. WITH TIES here keeps equal row numbers across partitions, not every tied latest date within one partition.
SELECT OrderID,CustomerID,OrderDate,Amount
FROM
(
SELECT TOP(1) WITH TIES OrderID,CustomerID,OrderDate,Amount
FROM #QualifyOrders
ORDER BY ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY OrderDate DESC,OrderID DESC)
) AS Latest
ORDER BY CustomerID;Filter Aggregate Windows Without QUALIFY at the Outer Level
The same approach works for COUNT or SUM window results. Calculate the customer-level count and total while retaining every detail row, then filter the computed values outside. The outer predicate does not collapse the detail rows into one customer record.
The following query returns every order belonging to customers meeting the synthetic count and amount thresholds. If the report needs one row per customer instead, a grouped aggregate or an additional deliberate row-selection step is clearer. Keep the intended result shape visible.
Window aggregates also inherit NULL and numeric-type rules. COUNT(*) counts input rows, while SUM ignores NULL amounts. Select an appropriate return type and precision for the real data. Filtering an incorrectly defined total at the correct query level still produces an incorrect report. Correct window placement is necessary, but it is only one part of the calculation's contract.
WITH Totals AS
(
SELECT *,COUNT_BIG(*) OVER(PARTITION BY CustomerID) AS CustomerOrders,
SUM(Amount) OVER(PARTITION BY CustomerID) AS CustomerAmount
FROM #QualifyOrders
)
SELECT OrderID,CustomerID,CustomerOrders,CustomerAmount
FROM Totals WHERE CustomerOrders>=2 AND CustomerAmount>25;Verify the Rule and Then the Access Path
Test one-row partitions, repeated dates, missing values allowed by the schema, and filters inside versus outside the window input. Include the tie cases the business actually cares about. SQL Server 2012 and later support the window features used in these examples.
For recurring latest-order requests, inspect an index beginning with the partition key and the required ordering keys. That can reduce sorting work, but it also has write and storage costs. Compare actual plans and IO with representative population rather than adding an index from a tiny sample.
Filtering without QUALIFY is straightforward when the query levels are explicit. Calculate the window, expose its column, and filter it outside. Keep partition scope, tie policy, and presentation order separate, then choose the clear form whose actual plan serves the workload.
Related reading on this blog: What’s the Difference between ROW_NUMBER, RANK, and DENSE_RANK? Notes from the Field #096 and ROWS vs RANGE in Window Frames: Why Running Totals Differ.

A window filter is not a special WHERE shortcut, it is a predicate applied after the window value exists.
Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.




