Using the ORDER BY Clause Inside a CTE Query in SQL Server

A common table expression (CTE) in SQL Server is used to create a temporary result set that can be referenced within a SELECT, INSERT, UPDATE, or DELETE statement. A CTE can simplify complex queries and improve readability. However, there are specific rules for using the ORDER BY clause inside a CTE.

Basic Syntax of a CTE

Just so we’re on the same page, the syntax of a CTE goes something like this:

WITH CTE_Name (Column1, Column2, ...)
AS
(
    SELECT Column1, Column2, ...
    FROM SomeTable
    -- Optional WHERE, JOIN, etc.
)
SELECT * FROM CTE_Name;

So, we start with the WITH keyword, then the CTE’s name and definition, and then after all that, we can use a SELECT statement to query the results from the CTE.

We can quite easily use an ORDER BY clause in the main/outer query (in this example that would be the last SELECT statement), but if we want to include one in the CTE itself, we need to adhere to some rules.

The Rules for Using ORDER BY Inside a CTE

The Microsoft documentation states that the ORDER BY clause cannot be used inside a CTE unless it’s part of a TOP clause (to limit the result set). I’ve found that we can also use the OFFSET clause, FOR XML, or FOR JSON instead of TOP if required.

If we include the ORDER BY clause without any of these clauses, we’ll get an error.

Here’s an example of the error:

WITH TopEmployees AS
(
    SELECT EmployeeID, FirstName, LastName, Salary
    FROM Employees
    ORDER BY Salary DESC
)
SELECT * FROM TopEmployees;

Output:

Msg 1033, Level 15, State 1, Line 6
The ORDER BY clause is invalid in views, inline functions, derived tables, subqueries, and common table expressions, unless TOP, OFFSET or FOR XML is also specified.

As the error message states, we can only use the ORDER BY clause inside the CTE if there’s also a TOP, OFFSET, or FOR XML clause. Maybe the error message should also mention FOR JSON, as I’ve found that that clause can also be used without causing this error.

Perhaps this limitation is because the CTE’s purpose is to build a result set for the subsequent query to manipulate. So we would normally sort the results in the subsequent query rather than inside the CTE itself.

In any case, let’s walk through the different ways we can use the ORDER BY clause inside the actual CTE query.

Example 1: Using ORDER BY with TOP Inside a CTE

Here’s an example of valid usage of ORDER BY with the TOP clause within a CTE:

WITH TopEmployees AS
(
    SELECT TOP 5 EmployeeID, FirstName, LastName, Salary
    FROM Employees
    ORDER BY Salary DESC
)
SELECT * FROM TopEmployees;

In this example, the CTE returns the top 5 employees with the highest salary, using ORDER BY in conjunction with TOP. The outer/subsequent query can then use those 5 results as it requires. In this case it’s simply selecting everything, but in practice we would normally do more than that. This is just an overly simplistic example to demonstrate the point.

Example 2: Using ORDER BY with OFFSET Inside a CTE

Here’s an example of valid usage of ORDER BY with the OFFSET clause within the CTE:

WITH PaginatedEmployees AS
(
    SELECT EmployeeID, FirstName, LastName, Salary
    FROM Employees
    ORDER BY Salary DESC
    OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY
)
SELECT * FROM PaginatedEmployees;

Here, I’m using OFFSET and FETCH to achieve return 5 rows, ordered by salary. These clauses are often used to perform pagination, as it allows us to continue the sequence for subsequent rows. For example, the next page could be OFFSET 5 FETCH NEXT 5 ROWS ONLY, and so on.

Example 3: Using ORDER BY with FOR XML Inside a CTE

If you need to output the CTE query’s results in XML format, you can use the FOR XML clause. This will enable you to sort the results without having to use TOP or OFFSET.

Here’s an example of valid usage of ORDER BY with the FOR XML clause within the CTE:

WITH EmployeesXML (EmployeeData) AS
(
    SELECT EmployeeID, FirstName, LastName, Salary
    FROM Employees
    ORDER BY Salary DESC
    FOR XML PATH('Employee')
)
SELECT * FROM EmployeesXML;

This returns the results in XML format.

Example 4: Using ORDER BY with FOR JSON Inside a CTE

If you need to output the CTE query’s results in JSON format, you can use the FOR JSON clause.

Example:

WITH EmployeesJSON (EmployeeData) AS
(
    SELECT EmployeeID, FirstName, LastName, Salary
    FROM Employees
    ORDER BY Salary DESC
    FOR JSON PATH, ROOT('Employees')
)
SELECT * FROM EmployeesJSON;

This returns the results in JSON format.

Sorting in the Outer Query

Another option is to use ORDER BY in the outer query. This removes the ORDER BY clause from the CTE and places it in the subsequent query. When we do this, there aren’t any restrictions like there were when we had the ORDER BY clause inside the CTE.

Here’s an example of putting it in the outer query:

WITH EmployeesCTE AS
(
    SELECT EmployeeID, FirstName, LastName, Salary
    FROM Employees
)
SELECT * 
FROM EmployeesCTE
ORDER BY Salary DESC;

In this case, the sorting is applied after the CTE’s result set is returned.

Implications of Using ORDER BY in a CTE vs Outer Query

  • Performance: Sorting in the outer query may offer better performance. Check your query plan to compare each method.
  • Flexibility: Sorting in the outer query allows you to reuse the CTE’s result set in different ways, with different ORDER BY clauses, without modifying the CTE itself.
  • Readability: Sorting in the outer query can improve readability by separating data extraction (in the CTE) from presentation (in the outer query).
  • Limitations: ORDER BY inside a CTE is only allowed when combined with the clauses mentioned above, which limits its use cases.