Производительность SQL с использованием ROW_NUMBER и динамического порядка

Я понимаю, что это распространенная проблема и ранее обсуждалась на SO , но я подумал, что снова подниму этот вопрос в надежде, что какая-то жизнеспособная альтернатива может быть найдено.

Возьмите следующий SQL, который объединяет пейджинг с динамическим порядком:

WITH CTE AS (
  SELECT 
      OrderID,
      ROW_NUMBER() OVER (ORDER BY
        CASE WHEN @SortCol='OrderID' THEN OrderID END ASC,
        CASE WHEN @SortCol='CustomerName' THEN Surname END ASC 
      ) AS ROW_ID
  FROM Orders WHERE X
)

SELECT Orders.* FROM CTE
INNER JOIN Orders ON CTE.OrderID = Orders.OrderID
WHERE ROW_ID BETWEEN @RowStart AND @RowStart + @RowCount -1;

Как известно, метод ROW_NUMBER() плохо работает на больших таблицах, поскольку индексы таблицы не могут быть правильно использованы при использовании нескольких операторов CASE в предложении ORDER BY (см. ссылка).

Решение, которое мы использовали в течение ряда лет, - это построить строку, которая затем выполняется с помощью sp_executesql. Производительность хороша при использовании динамического SQL, подобного этому, но полученный код ужасен с точки зрения удобочитаемости.

Я слышал о методе ROWCOUNT, но насколько я знаю, он все еще подвержен тем же проблемам, когда вы вводите динамический порядок по элементу.

Итак, рискуя спросить невозможное, какие существуют другие варианты?

ИЗМЕНИТЬ

Чтобы сделать некоторый полезный прогресс, я собрал три запроса, в которых были освещены различные предлагаемые методы:

  • Текущее, динамическое решение SQL (время выполнения 147 мс)

  • gbn Решение (время выполнения 1687 мс)

  • Решение Anders (время выполнения 1604 мс)

  • Решение Мухмуда (время выполнения 46 мс)

Ответ 1

Как насчет этого:

WITH data as (
    SELECT OrderID,
            ROW_NUMBER() OVER ( ORDER BY OrderID asc) as OrderID_ROW_ID,
            ROW_NUMBER() OVER ( ORDER BY Surname asc) as Surname_ROW_ID
        FROM Orders --WHERE X   
), CTE AS (               
        SELECT OrderID, OrderID_ROW_ID as ROW_ID
        FROM data
        where @SortCol = 'OrderID'

        union all

        SELECT OrderID, Surname_ROW_ID
        FROM data
        where @SortCol = 'Surname'
)
SELECT Orders.*, ROW_ID FROM CTE
INNER JOIN Orders ON CTE.OrderID = Orders.OrderID       
WHERE ROW_ID BETWEEN @RowStart AND @RowStart + @RowCount -1
order by ROW_ID
option (recompile);

Изменить: Использование option (recompile) в примере запроса в сообщении делает его намного быстрее. Однако case не может использоваться таким образом, чтобы выбирать между восходящим/убывающим порядком.

Причина этого заключается в том, что создается план для значений переменных, которые являются неуместными, а затем этот план кэшируется. Принуждение перекомпиляции позволяет использовать фактические значения переменных.

Ответ 2

(Edited)

DECLARE 
      @OrderColumnName SYSNAME
    , @RowStart INT
    , @RowCount INT
    , @TopCount INT

SELECT 
      @OrderColumnName = 'EmployeeID'
    , @RowStart = 5
    , @RowCount = 50
    , @TopCount = @RowStart + @RowCount – 1

Решение @muhmud -

; WITH data AS 
(
    SELECT 
          wo.WorkOutID
        , RowIDByEmployee = ROW_NUMBER() OVER (ORDER BY wo.EmployeeID)
        , RowIDByDateOut = ROW_NUMBER() OVER (ORDER BY wo.DateOut)  
    FROM dbo.WorkOut wo
), CTE AS 
(         
    SELECT
          wo.WorkOutID
        , RowID = RowIDByEmployee 
    FROM data wo
    WHERE @OrderColumnName = 'EmployeeID'

    UNION ALL

    SELECT
          wo.WorkOutID
        , RowID = RowIDByDateOut
    FROM data wo
    WHERE @OrderColumnName = 'DateOut'
)
SELECT wo.*  
FROM CTE t
JOIN dbo.WorkOut wo ON t.WorkOutID = wo.WorkOutID
WHERE t.RowID BETWEEN @RowStart AND @RowCount + @RowStart - 1
ORDER BY t.RowID
OPTION (RECOMPILE)

Table 'WorkOut'. Scan count 3, logical reads 14254, physical reads 1,
read-ahead reads 14017, lob logical reads 0, lob physical reads 0, lob
read-ahead reads 0. Table 'Worktable'. Scan count 0, logical reads 0,
physical reads 0, read-ahead reads 0, lob logical reads 0, lob
physical reads 0, lob read-ahead reads 0.
 SQL Server Execution Times:
   CPU time = 1295 ms,  elapsed time = 3048 ms.

Решение без общего выражения таблицы данных -

;WITH CTE AS 
(         
    SELECT
          wo.WorkOutID
        , RowID = ROW_NUMBER() OVER (ORDER BY wo.EmployeeID) 
    FROM dbo.WorkOut wo
    WHERE @OrderColumnName = 'EmployeeID'

    UNION ALL

    SELECT
          wo.WorkOutID
        , RowID = ROW_NUMBER() OVER (ORDER BY wo.DateOut) 
    FROM dbo.WorkOut wo
    WHERE @OrderColumnName = 'DateOut'
)
SELECT wo.*  
FROM CTE t
JOIN dbo.WorkOut wo ON t.WorkOutID = wo.WorkOutID
WHERE t.RowID BETWEEN @RowStart AND @RowCount + @RowStart - 1
ORDER BY t.RowID
OPTION (RECOMPILE)

Table 'WorkOut'. Scan count 3, logical reads 14254, physical reads 1, read-ahead reads 14017, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 1296 ms,  elapsed time = 3049 ms.

Решение с TOP -

;WITH CTE AS 
(         
    SELECT TOP (@TopCount)
          wo.WorkOutID
        , RowID = ROW_NUMBER() OVER (ORDER BY wo.EmployeeID) 
    FROM dbo.WorkOut wo
    WHERE @OrderColumnName = 'EmployeeID'

    UNION ALL

    SELECT TOP (@TopCount)
          wo.WorkOutID
        , RowID = ROW_NUMBER() OVER (ORDER BY wo.DateOut) 
    FROM dbo.WorkOut wo
    WHERE @OrderColumnName = 'DateOut'
)
SELECT wo.*  
FROM CTE t
JOIN dbo.WorkOut wo ON t.WorkOutID = wo.WorkOutID
WHERE t.RowID > @RowStart - 1
ORDER BY t.RowID
OPTION (RECOMPILE)

Table 'WorkOut'. Scan count 3, logical reads 14246, physical reads 1, read-ahead reads 14017, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 1248 ms,  elapsed time = 2864 ms.

enter image description here

Ответ 3

Попробуйте это. Это должно использовать индексы, которые у вас есть

WITH CTE AS (               
        SELECT 
            Orders.*,
            ROW_NUMBER() OVER (ORDER BY OrderID) AS rnOrderID,
            ROW_NUMBER() OVER (ORDER BY Surname) AS rnSurname                                                                         
        FROM Orders WHERE X                                         
    )
SELECT CTE.*
FROM CTE
WHERE
   CASE @SortCol
       WHEN 'OrderID' THEN rnOrderID
   END BETWEEN @RowStart AND @RowStart + @RowCount -1; 

Однако для больших наборов данных (100 000 и более) существуют другие методы, такие как http://www.4guysfromrolla.com/webtech/042606-1.shtml

Ответ 4

Я бы попробовал нечто похожее на это:

WITH CTEOrder AS (
  SELECT 
     OrderID,
     ROW_NUMBER() OVER (ORDER BY OrderID ASC) AS ROW_ID
  FROM Orders 
)
, CTECustomerName AS (
  SELECT 
     OrderID,
     ROW_NUMBER() OVER (ORDER BY Surname ASC) AS ROW_ID
  FROM Orders 
)
, CTECombined AS
(
  SELECT 'OrderID' OrderByType, OrderID, Row_ID
    FROM CTEOrder
   WHERE Row_id BETWEEN @RowStart AND @RowStart + @RowCount -1
  UNION
  SELECT 'CustomerName' OrderByType, OrderID, Row_ID
    FROM CTECustomerName
   WHERE row_id BETWEEN @RowStart AND @RowStart + @RowCount -1
)
SELECT Orders.* FROM CTECombined
INNER JOIN Orders ON CTECombined.OrderID = Orders.OrderID       
WHERE ROW_ID BETWEEN @RowStart AND @RowStart + @RowCount -1;  
AND OrderByType = @SortCol

Я попробовал это с одной из моих собственных таблиц, в которой есть приложение. 4 миллиона записей. Очевидно, что у него разные имена полей, поэтому я извиняюсь, если я не "правильно" перевел это в ответ, и SQL не работает для вас. Однако идея должна быть очевидной.

С кодом в вашем вопросе я получаю приложение 200000 логических чтений и процессор 6068 мс на моей таблице, а с приведенным выше я получаю 1422 логических чтения и 78 мс CPU.

Я не очищал кеш или другие вещи, необходимые для реального теста, но я пробовал его с разных страниц, страниц и т.д., и мои результаты были согласованы с самого начала.

Если у вас есть запросы со многими полями, которые вы хотите заказать, это решение может не масштабироваться достаточно, так как вам придется расширять число CTE, но вы можете сделать это в коде, если вы все равно строите SQL - и для примера с сортировкой по двум различным полям он работает как прелесть для меня.

EDIT: подумайте об этом, вам, вероятно, не понадобятся отдельные CTE для каждого столбца OrderBy, вы, вероятно, сможете просто иметь один, где вы делаете как ROW_NUMBER(), так и UNION в том же CTE, Принцип тот же, и я бы подумал, что оптимизатор в конечном итоге сделает то же самое, но я пока не тестировал это. Я обновлю ответ, если и когда я получу время, чтобы проверить это.

EDIT 2: Как и ожидалось, вы можете сделать UNION в пределах одного CTE. Я не буду обновлять код, но я дам некоторые контрольные значения для кода, как есть. Я сделал серию из 10000 строк, чтобы узнать, не изменилось ли это, но это не так. (Два прогона cusinar9 и мой код соответственно выполнялись одинаково, поэтому у холодного запуска были одинаковые параметры для обеих версий кода, а во втором прогоне были разные параметры, но то же самое для двух версий кода):

код cusimar9, холодный старт:

Table 'TestTable'. Scan count 10009, logical reads 43080, physical reads 189, read-ahead reads 12915, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times:   CPU time = 3037 ms,  elapsed time = 2206 ms.

код cusimar9, второй запуск, различные параметры:

Table 'TestTable'. Scan count 10009, logical reads 43096, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times:   CPU time = 4132 ms,  elapsed time = 1012 ms.

Мое предложение, холодный старт:

Table 'TestTable'. Scan count 10001, logical reads 31963, physical reads 12, read-ahead reads 6984, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times:   CPU time = 218 ms,  elapsed time = 1410 ms.

Мое предложение, 2-й прогон:

Table 'TestTable'. Scan count 10001, logical reads 31963, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times:   CPU time = 218 ms,  elapsed time = 358 ms.

Изменить 3: увидев ваш опубликованный код, я отметил эту конструкцию:

PagedCTE AS (
    SELECT (SELECT Max(ROW_ID) FROM OrderByCTE) AS TOTAL_ROWS, OrderID 
    FROM OrderByCTE             
    WHERE
        OrderByCTE.SortCol = @SortCol AND OrderByCTE.SortDir = @SortDir AND
        OrderByCTE.ROW_ID BETWEEN @RowStart AND @RowStart + @RowCount -1
)

Я не на 100% уверен в цели этого, но я предполагаю, что вы хотите вернуть общее количество строк, чтобы вы могли вычислять (и показывать пользователю), насколько далеко мы находимся в наборе записей. Так почему бы просто не получить этот номер за пределами всего шума ORDER BY? Чтобы придерживаться текущего стиля, почему бы не сделать CTE с SELECT COUNT(*)? А затем присоединитесь к этому в своем окончательном выборе?

Ответ 5

Это один случай, когда процедурные шаблоны терпят неудачу для хранимых процедур. Ваша попытка параметризовать атрибуты, используемые оптимизатором, не позволяет оптимизатору выполнять эту работу.

В этом случае я бы использовал 2 хранимых процедуры.

Если вы действительно настроены на использование параметров, то:

DECLARE @strSQL varchar(1000) = 
    'SELECT OrderID,ROW_NUMBER() OVER ( ORDER BY '                                                                                        
    + @SortCol +
    ' ASC) AS ROW_ID FROM Orders WHERE X ' +
    ' AND ROW_ID BETWEEN @RowStart and @RowStart + @RowCount - 1;'                                        

EXECUTE (@StrSQL)

Ответ 6

Без CTE Генерация номера строки при наличии условий динамического порядка

select TotalCount = COUNT(U.UnitID) OVER() ,

ROW_NUMBER() over(
    order by 
     (CASE @OrderBy WHEN '1' THEN m.Title  END) ASC ,
     (CASE @OrderBy WHEN '2' THEN m.Title  END) DESC,
     (CASE @OrderBy WHEN '3' THEN Stock.Stock  END) DESC,
     (CASE @OrderBy WHEN '4' THEN Stock.Stock   END) DESC

) as RowNumber,

M.Title,U.ColorCode,U.ColorName,U.UnitID, ISNULL(Stock.Stock,0) as Stock

 from tblBuyOnlineMaster M

    inner join BuyOnlineProductUnitIn U on U.BuyOnlineID=M.BuyOnlineID 

    left join 
            ( select IT.BuyOnlineID,IT.UnitID,ISNULL(sum(IT.UnitIn),0)-ISNULL(sum(IT.UnitOut),0) as Stock 
                from [dbo].[BuyOnlineItemTransaction] IT 
                group by IT.BuyOnlineID,IT.UnitID
             ) as Stock

        on U.UnitID=Stock.UnitID


order by 
 (CASE @OrderBy WHEN '1' THEN m.Title  END) ASC ,
 (CASE @OrderBy WHEN '2' THEN m.Title  END) DESC,
 (CASE @OrderBy WHEN '3' THEN Stock.Stock  END) DESC,
 (CASE @OrderBy WHEN '4' THEN Stock.Stock   END) DESC


offset  @offsetCount rows fetch next 6 rows only