2013-04-04 13 views
11

Mi rendo conto che questo è un problema comune ed è stato discusso in SO previously, ma ho pensato di sollevare nuovamente la domanda nella speranza che fosse possibile trovare qualche alternativa valida.Prestazioni SQL utilizzando ROW_NUMBER e ordine dinamico di

Prendere il seguente SQL che combina paging con ordinamento dinamico:

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; 

Come è ben noto, il metodo ROW_NUMBER() non funziona bene su grandi tavoli poiché i rilevamenti sul tavolo non possono essere utilizzati a causa utilizzando più istruzioni CASE nella clausola ORDER BY (vedere link).

La soluzione che abbiamo utilizzato per un certo numero di anni è quella di costruire una stringa che viene poi eseguita utilizzando sp_executesql. Le prestazioni sono buone quando si utilizza SQL dinamico come questo, ma il codice risultante è terribile dal punto di vista della leggibilità.

Ho sentito parlare dello ROWCOUNT method ma, per quanto ne so, è ancora suscettibile agli stessi problemi quando si introduce l'ordine dinamico per elemento.

Quindi, a rischio di chiedere l'impossibile, quali altre opzioni ci sono?

EDIT

Per fare qualche progresso utile qui ho messo insieme tre domande evidenziando i vari metodi suggeriti:

  1. corrente, soluzione SQL dinamica (tempo di esecuzione 147ms)

  2. gbn Soluzione (tempo di esecuzione 1687 ms)

  3. Anders Solution (tempo di esecuzione 1604ms)

  4. Muhmud Solution (tempo di esecuzione 46ms)

+0

SQL Server 2012 offre 'Offset/FETCH'. Ma è necessario decidere quale valore apprezzi di più: prestazioni o manutenibilità. Devi sceglierne uno. –

+0

E perché stai facendo un 'JOIN' alla tabella' Orders' sull'ultimo 'SELECT' ?, non è necessario – Lamak

+0

Lamak il join indietro sulla tabella Orders è un miglioramento delle prestazioni, il CTE restituisce solo le colonne minime richieste – cusimar9

risposta

1

Prova questo. Questo dovrebbe sfruttare gli indici avete

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; 

Tuttavia, per grandi serie di dati (100,000s e altri) ci sono altre tecniche come http://www.4guysfromrolla.com/webtech/042606-1.shtml

+0

Che è dolorosamente inefficiente, OGNI ordine per colonna deve essere valutato, piuttosto che solo quello richiesto. L'ho appena provato e funziona più lentamente del mio esempio. Ho anche fatto riferimento al tuo link nella mia domanda. – cusimar9

0

Questo è un caso in cui i modelli procedurali falliscono per le stored procedure. Il tuo tentativo di parametrizzare gli attributi utilizzati dall'ottimizzatore non lascia che l'ottimizzatore faccia il suo lavoro.

In questo caso, vorrei utilizzare 2 stored procedure.

Se siete veramente impostato sull'utilizzo di parametri, quindi:

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) 
+0

Non sono sicuro di aver capito il tuo punto. L'OP ha già menzionato che attualmente stanno usando SQL dinamico. Cosa stai specificatamente proponendo con la tua risposta? –

1

vorrei provare qualcosa di simile a questo:

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 

ho provato questo con una delle mie tabelle, che ha app . 4 milioni di dischi. Ovviamente, ha nomi di campo diversi, quindi mi scuso se non ho "tradotto" correttamente nella risposta e SQL non funziona per te. Tuttavia, l'idea dovrebbe essere ovvia.

Con il codice nella tua domanda, ottengo 200000 letture logiche e 6068 ms CPU sulla mia tabella e con quanto sopra ottengo 1422 letture logiche e 78 ms CPU.

non ho la cache a filo o altre cose necessarie per un vero e proprio punto di riferimento, ma l'ho fatto provare con diverse pagine, pagesizes, ecc ei miei risultati sono stati coerenti fin dall'inizio.

Se si dispone di query con molti campi diversi che si desidera ordinare, questa soluzione potrebbe non essere sufficientemente scalabile dal momento che sarà necessario espandere il numero di CTE, ma è possibile farlo in codice se si sta costruendo SQL comunque - e per l'esempio con l'ordinamento su due campi diversi funziona come un incantesimo per me.

EDIT: Vieni a pensarci bene, probabilmente non avrà bisogno di CTE individuali per ogni OrderBy colonne, probabilmente sarete in grado di avere un solo dove si fa sia ROW_NUMBER() e UNION nella stessa CTE. Il principio è lo stesso, e penserei che l'ottimizzatore finirebbe per fare lo stesso, ma non l'ho ancora analizzato. Aggiornerò la risposta se e quando avrò il tempo di verificarlo.

MODIFICA 2: Come previsto, è possibile eseguire UNION all'interno di un CTE. Non ho intenzione di aggiornare il codice, ma fornirò alcuni benchmark sul codice così com'è. Ho fatto un pageize di 10000 righe per vedere se ciò ha fatto una grande differenza, ma non è così. (Le due esecuzioni di cusinar9 e il mio codice rispettivamente sono state eseguite in modo equivalente, quindi l'avvio a freddo aveva parametri identici per entrambe le versioni del codice e la seconda esecuzione aveva parametri diversi, ma lo stesso per le due versioni del codice):

codice di cusimar9, avviamento a freddo:

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. 

codice di cusimar9, 2 ° periodo, diversi parametri:

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. 

Il mio suggerimento, avviamento a freddo:

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. 

Il mio suggerimento, 2 ° periodo:

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. 

Edit 3: Vedendo il codice scritto, ho notato questa costruzione:

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 
) 

Non sono sicuro al 100% circa lo scopo di questo, ma suppongo che tu voglia restituire il totale delle righe in modo che tu possa calcolare (e mostrare all'utente) quanto siamo lontani nel recordset. Quindi, perché non ottenere quel numero al di fuori di tutto il rumore ORDER BY? Per mantenere lo stile attuale, perché non creare un CTE con un SELECT COUNT(*)? E poi partecipa alla selezione finale?

+0

Ciao Anders, grazie per questo, il confronto di velocità sembra certamente promettente, tuttavia temo che la manutenibilità di questo metodo sia compromessa, in particolare quando si considerano alcuni SQL complessi e forse 8+ metodi Order By. Hai suggerito che un metodo semplificato potrebbe essere possibile, potresti fare un esempio? – cusimar9

+0

@ cusimar9 Cercherò di fare un esempio per te stasera, ma il punto è semplice, quindi abbozzerò il concetto e vedrai se riesci a farlo funzionare da solo: in pratica, puoi fare il ' UNION's all'interno di un solo CTE.Cioè con CTE AS (SELEZIONA 'OrderID', OrderId ROW_NUMBER() OVER (ORDER BY OrderId) ... UNION SELECT 'CustomerName', OrderId ROW_NUMBER() OVER (ORDER BY cognome) ...) SELECT Order. * FROM CTE ... 'Fammi sapere se riesci a farlo funzionare, o scriverò qualcosa che viene eseguito e spostato da questo commento alla risposta. –

+0

Ho integrato questo in un paio di modi diversi e sto ottenendo costantemente un tempo di esecuzione di 4 secondi, contro 2 secondi per la soluzione di gbn e 0 secondi per la mia soluzione dinamica sql esistente – cusimar9

4

ne dite di questo:

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); 

Edit: Utilizzando option (recompile) sulla query ad esempio nel post fa andare molto più veloce. Tuttavia, case non può essere utilizzato esattamente in questo modo per scegliere tra ordine crescente/decrescente.

La ragione di ciò è che viene generato un piano per i valori delle variabili che sono inappropriati e quindi questo piano viene memorizzato nella cache. Forzare una ricompilazione consente di utilizzare i valori effettivi delle variabili.

+0

Ho aggiornato la domanda con i risultati del vostro suggerimento – cusimar9

+0

Vedere la risposta aggiornata, ovvero l'ultima riga. – muhmud

+0

è più lento con la ricompilazione, il test arriva a 2430 ms – cusimar9

2

(Edited)

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

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

@ soluzione 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. 

soluzione senza dati della tabella espressione comune -

;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. 

Soluzione con TOP -

012.351.641.061.
;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

+0

'@ orderID' verrà trattato come una costante qui, non un riferimento a una colonna. – muhmud

+0

Non seguo il motivo per cui la vostra soluzione propone di interrogare sys.columns? – cusimar9

+0

Accetto con l'osservazione di @muhmud. Ho provato un'altra variante. Ecco i risultati su una tabella con 2 milioni di righe, controlla la risposta. – Devart

0

Senza numero CTE Generazione Row quando le condizioni di ordine dinamiche sono presenti

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 
Problemi correlati