Qual è il modo più efficiente per ottenere il minimo di più colonne su SQL Server 2005?


29

Sono in una situazione in cui desidero ottenere il valore minimo da 6 colonne.

Finora ho trovato tre modi per raggiungere questo obiettivo, ma ho delle preoccupazioni riguardo alle prestazioni di questi metodi e vorrei sapere quale sarebbe meglio per le prestazioni.

Il primo metodo consiste nell'utilizzare un'istruzione case grande . Ecco un esempio con 3 colonne, basato sull'esempio nel link sopra. La mia dichiarazione sul caso sarebbe molto più lunga poiché guarderò 6 colonne.

Select Id,
       Case When Col1 <= Col2 And Col1 <= Col3 Then Col1
            When Col2 <= Col3 Then Col2 
            Else Col3
            End As TheMin
From   MyTable

La seconda opzione è quella di utilizzare l' UNIONoperatore con più istruzioni select . Vorrei inserire questo in un UDF che accetta un parametro Id.

select Id, dbo.GetMinimumFromMyTable(Id)
from MyTable

e

select min(col)
from
(
    select col1 [col] from MyTable where Id = @id
    union all
    select col2 from MyTable where Id = @id
    union all
    select col3 from MyTable where Id = @id
) as t

E la terza opzione che ho trovato è stata quella di utilizzare l'operatore UNPIVOT , che non sapevo nemmeno esistesse fino ad ora

with cte (ID, Col1, Col2, Col3)
as
(
    select ID, Col1, Col2, Col3
    from TestTable
)
select cte.ID, Col1, Col2, Col3, TheMin from cte
join
(
    select
        ID, min(Amount) as TheMin
    from 
        cte 
        UNPIVOT (Amount for AmountCol in (Col1, Col2, Col3)) as unpvt
    group by ID
) as minValues
on cte.ID = minValues.ID

A causa delle dimensioni della tabella e della frequenza con cui questa tabella viene interrogata e aggiornata, sono preoccupato per l'impatto sulle prestazioni che queste query avrebbero sul database.

Questa query verrà effettivamente utilizzata in un join a una tabella con alcuni milioni di record, tuttavia i record restituiti verranno ridotti a circa cento record alla volta. Verrà eseguito più volte durante il giorno e le 6 colonne che sto interrogando vengono aggiornate frequentemente (contengono statistiche giornaliere). Non credo che ci siano indici sulle 6 colonne che sto interrogando.

Quale di questi metodi è migliore per le prestazioni quando si cerca di ottenere il minimo di più colonne? O c'è un altro metodo migliore che non conosco?

Sto usando SQL Server 2005

Dati campione e risultati

Se i miei dati contenessero record come questo:

Id Col1 Col2 Col3 Col4 Col5 Col6
1 3 4 0 2 1 5
2 2 6 10 5 7 9
3 1 1 2 3 4 5
4 9 5 4 6 8 9

Il risultato finale dovrebbe essere

Valore ID
1 0
2 2
3 1
4 4

Risposte:


22

Ho testato le prestazioni di tutti e 3 i metodi ed ecco cosa ho trovato:

  • 1 record: nessuna differenza evidente
  • 10 registrazioni: nessuna differenza evidente
  • 1.000 record: nessuna differenza evidente
  • 10.000 record: la UNIONsubquery era un po 'più lenta. La CASE WHENquery è un po 'più veloce di UNPIVOTquella.
  • 100.000 record: la query UNIONsecondaria è notevolmente più lenta, ma la UNPIVOTquery diventa leggermente più veloce della CASE WHENquery
  • 500.000 record: la UNIONsubquery è ancora significativamente più lenta, ma UNPIVOTdiventa molto più veloce della CASE WHENquery

Quindi i risultati finali sembrano essere

  • Con set di dischi più piccoli non sembra esserci abbastanza differenza per la questione. Usa tutto ciò che è più facile da leggere e mantenere.

  • Una volta che inizi a entrare in set di record più grandi, la UNION ALLsubquery inizia a funzionare male rispetto agli altri due metodi.

  • L' CASEistruzione esegue il massimo fino a un certo punto (nel mio caso, circa 100.000 righe) e quale punto la UNPIVOTquery diventa la query con il rendimento migliore

Il numero effettivo in cui una query diventa migliore di un'altra probabilmente cambierà a causa dell'hardware, dello schema del database, dei dati e del carico corrente del server, quindi assicurati di testare con il tuo sistema se sei preoccupato per le prestazioni.

Ho anche eseguito alcuni test usando la risposta di Mikael ; tuttavia, è stato più lento di tutti e 3 gli altri metodi provati qui per la maggior parte delle dimensioni del recordset. L'unica eccezione era che faceva meglio di una UNION ALLquery per dimensioni di recordset molto grandi. Mi piace il fatto che mostri il nome della colonna oltre al valore più piccolo però.

Non sono un dba, quindi potrei non aver ottimizzato i miei test e perso qualcosa. Stavo testando con i dati attuali reali, quindi ciò potrebbe aver influito sui risultati. Ho provato a renderlo conto eseguendo ogni query un paio di volte diverse, ma non lo sai mai. Sarei sicuramente interessato se qualcuno scrivesse un test pulito di questo e condividesse i loro risultati.


6

Non so cosa sia più veloce ma potresti provare qualcosa del genere.

declare @T table
(
  Col1 int,
  Col2 int,
  Col3 int,
  Col4 int,
  Col5 int,
  Col6 int
)

insert into @T values(1, 2, 3, 4, 5, 6)
insert into @T values(2, 3, 1, 4, 5, 6)

select T4.ColName, T4.ColValue
from @T as T1
  cross apply (
                select T3.ColValue, T3.ColName
                from (
                       select row_number() over(order by T2.ColValue) as rn,
                              T2.ColValue,
                              T2.ColName
                       from (
                              select T1.Col1, 'Col1' union all
                              select T1.Col2, 'Col2' union all
                              select T1.Col3, 'Col3' union all
                              select T1.Col4, 'Col4' union all
                              select T1.Col5, 'Col5' union all
                              select T1.Col6, 'Col6'
                            ) as T2(ColValue, ColName)
                     ) as T3
                where T3.rn = 1
              ) as T4

Risultato:

ColName ColValue
------- -----------
Col1    1
Col3    1

Se non sei interessato a quale colonna ha il valore minimo, puoi invece usarlo.

declare @T table
(
  Id int,
  Col1 int,
  Col2 int,
  Col3 int,
  Col4 int,
  Col5 int,
  Col6 int
)

insert into @T
select 1,        3,       4,       0,       2,       1,       5 union all
select 2,        2,       6,      10,       5,       7,       9 union all
select 3,        1,       1,       2,       3,       4,       5 union all
select 4,        9,       5,       4,       6,       8,       9

select T.Id, (select min(T1.ColValue)
              from (
                      select T.Col1 union all
                      select T.Col2 union all
                      select T.Col3 union all
                      select T.Col4 union all
                      select T.Col5 union all
                      select T.Col6
                    ) as T1(ColValue)
             ) as ColValue
from @T as T

Una query non pivot semplificata.

select Id, min(ColValue) as ColValue
from @T
unpivot (ColValue for Col in (Col1, Col2, Col3, Col4, Col5, Col6)) as U
group by Id

6

Aggiungi una colonna calcolata persistente che utilizza CASEun'istruzione per eseguire la logica di cui hai bisogno.

Il valore minimo sarà quindi sempre disponibile in modo efficiente quando è necessario eseguire un join (o qualsiasi altra cosa) in base a quel valore.

Il valore verrà ricalcolato ogni volta che cambia uno dei valori di origine ( INSERT/ UPDATE/ MERGE). Non sto dicendo che questo è necessariamente la migliore soluzione per il carico di lavoro, ho solo offro come una soluzione, proprio come le altre risposte. Solo l'OP può determinare quale sia la migliore per il carico di lavoro.


1

Dichiarazione di caso per 6 date. Per fare di meno, copia il ramo vero dalla prima istruzione case. Il caso peggiore è quando Date1 è il valore più basso, il caso migliore è quando Date6 è il valore più basso, quindi inserisci la data più probabile in Date6. Ho scritto questo a causa dei limiti delle colonne calcolate.

CASE WHEN Date1 IS NULL OR Date1 > Date2 THEN
        CASE WHEN Date2 IS NULL OR Date2 > Date3 THEN
            CASE WHEN Date3 IS NULL OR Date3 > Date4 THEN
                CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                        Date6
                    ELSE
                        Date4
                    END
                END
            ELSE
                CASE WHEN Date3 IS NULL OR Date3 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date3 IS NULL OR Date3 > Date6 THEN
                        Date6
                    ELSE
                        Date3
                    END
                END
            END
        ELSE
            CASE WHEN Date2 IS NULL OR Date2 > Date4 THEN
                CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                        CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                            Date6
                        ELSE
                            Date5
                        END
                    ELSE
                        CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                            Date6
                        ELSE
                            Date4
                        END
                    END
                END
            ELSE
                CASE WHEN Date2 IS NULL OR Date2 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date2 IS NULL OR Date2 > Date6 THEN
                        Date6
                    ELSE
                        Date2
                    END
                END
            END
        END
ELSE
    CASE WHEN Date1 IS NULL OR Date1 > Date3 THEN
        CASE WHEN Date3 IS NULL OR Date3 > Date4 THEN
            CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                    Date6
                ELSE
                    Date4
                END
            END
        ELSE
            CASE WHEN Date3 IS NULL OR Date3 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date3 IS NULL OR Date3 > Date6 THEN
                    Date6
                ELSE
                    Date3
                END
            END
        END
    ELSE
        CASE WHEN Date1 IS NULL OR Date1 > Date4 THEN
            CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                    Date6
                ELSE
                    Date4
                END
            END
        ELSE
            CASE WHEN Date1 IS NULL OR Date1 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date1 IS NULL OR Date1 > Date6 THEN
                    Date6
                ELSE
                    Date1
                END
            END
        END
    END
END

Se ti sei imbattuto in questa pagina semplicemente cercando di confrontare le date e non sei così preoccupato per le prestazioni o la compatibilità, puoi utilizzare un costruttore di valori di tabella, che può essere utilizzato ovunque siano consentite le sottoselezioni (SQL Server 2008 e versioni successive):

Lowest =    
(
    SELECT MIN(TVC.d) 
    FROM 
    (
        VALUES
            (Date1), 
            (Date2), 
            (Date3), 
            (Date4), 
            (Date5), 
            (Date6)
    ) 
    AS TVC(d)
)

1

La tua casedichiarazione non è efficiente. Stai facendo 5 confronti nel peggiore dei casi e 2 nel migliore dei casi; mentre trovare il minimo di ndovrebbe fare al massimo i n-1confronti.

Per ogni riga, in media stai facendo 3,5 confronti invece di 2. Quindi ci vuole più tempo cpu ed è lento. Riprova i tuoi test usando la seguente casedichiarazione. Sta solo usando 2 confronti per riga e dovrebbe essere più efficiente di unpivote union all.

Select Id, 
       Case 
           When Col1 <= Col2 then case when Col1 <= Col3 Then Col1  else col3 end
            When  Col2 <= Col3 Then Col2  
            Else Col3 
            End As TheMin 
From   YourTableNameHere

Il union allmetodo è errato nel tuo caso poiché stai ottenendo il valore minimo non per riga ma per l'intera tabella. Inoltre, non sarà efficiente in quanto eseguirai la scansione della stessa tabella 3 volte. Quando la tabella è piccola, l'I / O non farà molta differenza, ma per le tabelle di grandi dimensioni lo farà. Non usare questo metodo.

Unpivotè buono e prova anche a sbloccare manualmente usando cross join con il tuo tavolo (select 1 union all select 2 union all select 3). Dovrebbe essere efficiente come il unpivot.

La soluzione migliore sarebbe avere una colonna persistente calcolata, se non si hanno problemi di spazio. Aggiungerà alla dimensione della riga di 4 byte (suppongo che tu abbia il inttipo), che a sua volta aumenterà la dimensione della tabella.

Tuttavia, lo spazio e la memoria sono problemi nel sistema e la CPU non lo rende persistente ma utilizza una colonna calcolata semplice utilizzando l'istruzione case. Renderà il codice più semplice.


-1

Immagino che la prima opzione sia più veloce (anche se non sembra molto liscia dal punto di vista della programmazione!). Questo perché si occupa esattamente di N righe (dove N è la dimensione della tabella) e non deve effettuare ricerche o ordinare come il metodo 2 o 3.

Un test con un campione di grandi dimensioni dovrebbe dimostrare il punto.

Ancora un'altra opzione da considerare (come se avessi bisogno di più!), È quella di creare una vista materializzata sul tuo tavolo. se la dimensione del tavolo è in centinaia di migliaia o più. In questo modo, il valore minimo viene calcolato mentre la riga viene modificata e l'intera tabella non dovrebbe essere elaborata con ogni query. In SQL Server, le viste materializzate sono denominate Viste indicizzate


-1
Create table #temp
   (
    id int identity(1,1),
    Name varchar(30),
    Year1 int,
    Year2 int,
    Year3 int,
    Year4 int
   )

   Insert into #temp values ('A' ,2015,2016,2014,2010)
   Insert into #temp values ('B' ,2016,2013,2017,2018)
   Insert into #temp values ('C' ,2010,2016,2014,2017)
   Insert into #temp values ('D' ,2017,2016,2014,2015)
   Insert into #temp values ('E' ,2016,2016,2016,2016)
   Insert into #temp values ('F' ,2016,2017,2018,2019)
   Insert into #temp values ('G' ,2016,2017,2020,2019)

   Select *, Case 
                 when Year1 >= Year2 and Year1 >= Year3 and Year1 >= Year4 then Year1
                 when Year2 >= Year3 and Year2 >= Year4 and Year2 >= Year1 then Year2
                 when Year3 >= Year4 and Year3 >= Year1 and Year3 >= Year2 then Year3
                 when Year4 >= Year1 and Year4 >= Year2 and Year4 >= Year3 then Year4  
                 else Year1 end as maxscore  
                 from #temp

Non stai prendendo in considerazione i NULL, il che rende la tua espressione CASE relativamente semplice. Tuttavia, se almeno una delle colonne è effettivamente NULL, la soluzione verrà restituita Year1come risultato, il che potrebbe non essere necessariamente corretto.
Andriy M,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.