Khronos
Khronos

Reputation: 381

Sorting a row limited SQL query with stored procedures

I'm using a stored procedure to try to get the first few rows from a sorted SQL query where the input paramaters specifiy the amount of rows to retrieve and the sorting criteria.

I've been able to properly retrieve the first few rows however when I try to sort the results, they simply appear sorted using the default criteria (Primary key).

So I was wondering if anyone could take a look at it?

USE [database]
GO
SET ANSI_NULLS OFF
GO
SET QUOTED_IDENTIFIER OFF
GO
ALTER PROCEDURE [dbo].[x0] 
@username nvarchar(50),
@sortBy varchar(50),
@sortDirection varchar(4),
@startRow int,
@endRow int

AS
With Ordering AS(

    SELECT ROW_NUMBER() OVER (Order by
        CASE    WHEN @sortBy='datecreate' THEN 'datecreate'
                WHEN @sortBy='id' THEN 'id'
                WHEN @sortBy='DisplayName' THEN 'DisplayName'
        END,
        CASE    WHEN @sortDirection='asc' THEN 'asc'
                WHEN @sortDirection='desc' THEN 'desc'
        END) AS RowNumber,

    dbo.x1.*, dbo.x2.* 
    FROM  dbo.x1 INNER JOIN dbo.x2 ON dbo.x1.type = dbo.x2.type
    where username = @username
)
SELECT     RowNumber, *, DisplayName AS DisplayName
FROM         Ordering
Where RowNumber BETWEEN @startRow AND @endRow 

I've also tried moving the sorting criteria to the outer SQL query (Where RowNumber BETWEEN @startRow AND @endRow) without much luck.

Upvotes: 0

Views: 827

Answers (2)

Gordon Linoff
Gordon Linoff

Reputation: 1269443

Your problem is the use of constants in the query. You are sorting by constant strings, rather than columns. I think you are mixing up dynamic SQL and regular SQL.

To get the names, you would want to use:

SELECT ROW_NUMBER() OVER (Order by
        CASE    WHEN @sortBy='datecreate' THEN datecreate
            WHEN @sortBy='id' THEN id
            WHEN @sortBy='DisplayName' THEN DisplayName
        END,

However, this won't work, because of the type conflict. You have two options. First, either convert all the types to string with the right sort order (YYYY-MM-DD for dates, zero pads for numbers). Or, since this is a stored procedure, create a separate query for each one.

The issue is that you might get everything as a string, but I'm still not sure how to get the ASC and DESC in. I think you will need two queries for this, which is feasible since this is inside a stored procedure.

I like Aaron's answer, but I think it can be improved.

WITH Ordering AS (
   SELECT ROW_NUMBER() OVER
                 (ORDER BY (case when @sortBy = 'datecreate' and @sortdirection = 'ASC'
                                 then datecreate
                            end),
                           (case when @sortBy = 'datecreate' and @sortdirection = 'DESC'
                                 then datecreate
                            end) desc,
                           (case when @sortBy = 'id' and @sortdirection = 'ASC'
                                 then id
                            end),
                           (case when @sortBy = 'id' and @sortdirection = 'DESC'
                                 then id
                            end) desc,
                           (case when @sortBy = 'DisplayName' and @sortdirection = 'ASC'
                                 then DisplayName
                            end),
                           (case when @sortBy = 'DisplayName' and @sortdirection = 'DESC'
                                 then DisplayName
                            end) desc
                 ) as rn,
          dbo.x1.*, dbo.x2.*
   FROM dbo.x1 INNER JOIN dbo.x2 ON dbo.x1.type = dbo.x2.type
   where username = @username
)

And so on. The difference is that there is only one row_number() call, with a more complicated ordering expression. All the values will be NULL except for the one actually being used for the sort.

Upvotes: 2

Aaron Bertrand
Aaron Bertrand

Reputation: 280252

You could do it this way:

;WITH Ordering AS
(
   SELECT rnd  = ROW_NUMBER() OVER (ORDER BY datecreate),
          rni  = ROW_NUMBER() OVER (ORDER BY id),
          rnn  = ROW_NUMBER() OVER (ORDER BY DisplayName),
          rndd = ROW_NUMBER() OVER (ORDER BY datecreate DESC),
          rnid = ROW_NUMBER() OVER (ORDER BY id DESC),
          rnnd = ROW_NUMBER() OVER (ORDER BY DisplayName DESC),
    dbo.x1.*, dbo.x2.* 
    FROM dbo.x1 INNER JOIN dbo.x2 ON dbo.x1.type = dbo.x2.type
    where username = @username
),
x AS 
(
  SELECT RowNumber = CASE @SortDirection 
    WHEN 'asc' THEN CASE @SortBy
      WHEN 'datecreate'   THEN rnd
      WHEN 'id'           THEN rni
      WHEN 'DisplayName'  THEN rnn 
    END
    WHEN 'desc' THEN CASE @sortBy
      WHEN 'datecreate'   THEN rndd
      WHEN 'id'           THEN rnid
      WHEN 'DisplayName'  THEN rnnd
    END, *, DisplayName AS DisplayName
  FROM Ordering
)
SELECT * FROM x
WHERE RowNumber BETWEEN @startRow AND @endRow 
ORDER BY RowNumber;

But quite honestly I think you'll get better performance from dynamic SQL (and if the optimize for ad hoc workloads setting is enabled, it won't suffer from the parameter sniffing issues inherent in the above solution, or plan cache bloat):

DECLARE @sql NVARCHAR(MAX);

SET @sql = N';WITH Ordering AS
  (
    SELECT ROW_NUMBER() OVER (ORDER BY ' + @SortBy + ' '
      + @SortDirection + ') AS RowNumber,
     dbo.x1.*, dbo.x2.* 
   FROM  dbo.x1 INNER JOIN dbo.x2 ON dbo.x1.type = dbo.x2.type
   where username = @username
  )
  SELECT RowNumber, *, DisplayName AS DisplayName
   FROM Ordering
   Where RowNumber BETWEEN @startRow AND @endRow 
   ORDER BY RowNumber;';

EXEC sp_executesql @sql, 
  N'@username NVARCHAR(50), @startRow INT, @endRow INT',
  @username, @startRow, @endRow;

Upvotes: 2

Related Questions