Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Genera tutte le combinazioni in SQL

Combinazioni di ritorno

Utilizzando una tabella di numeri o una CTE che genera numeri, selezionare da 0 a 2^n - 1. Utilizzando le posizioni dei bit contenenti 1 in questi numeri per indicare la presenza o l'assenza dei membri relativi nella combinazione ed eliminando quelli che non hanno il numero corretto di valori, dovresti essere in grado di restituire un set di risultati con tutte le combinazioni che desideri.

WITH Nums (Num) AS (
   SELECT Num
   FROM Numbers
   WHERE Num BETWEEN 0 AND POWER(2, @n) - 1
), BaseSet AS (
   SELECT ind = Power(2, Row_Number() OVER (ORDER BY Value) - 1), *
   FROM @set
), Combos AS (
   SELECT
      ComboID = N.Num,
      S.Value,
      Cnt = Count(*) OVER (PARTITION BY N.Num)
   FROM
      Nums N
      INNER JOIN BaseSet S ON N.Num & S.ind <> 0
)
SELECT
   ComboID,
   Value
FROM Combos
WHERE Cnt = @k
ORDER BY ComboID, Value;

Questa query funziona abbastanza bene, ma ho pensato a un modo per ottimizzarla, sfruttando il Nifty Parallel Bit Count per ottenere prima il giusto numero di oggetti presi alla volta. Questo ha prestazioni da 3 a 3,5 volte più veloci (sia CPU che tempo):

WITH Nums AS (
   SELECT Num, P1 = (Num & 0x55555555) + ((Num / 2) & 0x55555555)
   FROM dbo.Numbers
   WHERE Num BETWEEN 0 AND POWER(2, @n) - 1
), Nums2 AS (
   SELECT Num, P2 = (P1 & 0x33333333) + ((P1 / 4) & 0x33333333)
   FROM Nums
), Nums3 AS (
   SELECT Num, P3 = (P2 & 0x0f0f0f0f) + ((P2 / 16) & 0x0f0f0f0f)
   FROM Nums2
), BaseSet AS (
   SELECT ind = Power(2, Row_Number() OVER (ORDER BY Value) - 1), *
   FROM @set
)
SELECT
   ComboID = N.Num,
   S.Value
FROM
   Nums3 N
   INNER JOIN BaseSet S ON N.Num & S.ind <> 0
WHERE P3 % 255 = @k
ORDER BY ComboID, Value;

Sono andato a leggere la pagina di conteggio dei bit e penso che questo potrebbe funzionare meglio se non faccio il % 255 ma vado fino in fondo con un po' di aritmetica. Quando ne avrò la possibilità, lo proverò e vedrò come si accumula.

Le mie dichiarazioni di prestazione si basano sulle query eseguite senza la clausola ORDER BY. Per chiarezza, ciò che sta facendo questo codice è contare il numero di 1 bit impostati in Num dai Numbers tavolo. Questo perché il numero viene utilizzato come una sorta di indicizzatore per scegliere quali elementi del set sono nella combinazione corrente, quindi il numero di 1 bit sarà lo stesso.

Spero vi piaccia!

Per la cronaca, questa tecnica di utilizzo del modello di bit di numeri interi per selezionare i membri di un set è ciò che ho coniato "Vertical Cross Join". Risulta effettivamente nel cross join di più set di dati, in cui il numero di set e cross join è arbitrario. Qui, il numero di set è il numero di elementi presi alla volta.

In realtà l'unione incrociata nel solito senso orizzontale (di aggiungere più colonne all'elenco esistente di colonne con ogni unione) sarebbe simile a questa:

SELECT
   A.Value,
   B.Value,
   C.Value
FROM
   @Set A
   CROSS JOIN @Set B
   CROSS JOIN @Set C
WHERE
   A.Value = 'A'
   AND B.Value = 'B'
   AND C.Value = 'C'

Le mie query sopra "incrociate" in modo efficace tutte le volte necessarie con un solo join. I risultati sono privi di pivot rispetto ai cross join effettivi, certo, ma è una questione minore.

Critica al tuo codice

Innanzitutto, posso suggerire questa modifica alla tua UDF fattoriale:

ALTER FUNCTION dbo.Factorial (
   @x bigint
)
RETURNS bigint
AS
BEGIN
   IF @x <= 1 RETURN 1
   RETURN @x * dbo.Factorial(@x - 1)
END

Ora puoi calcolare insiemi di combinazioni molto più grandi, inoltre è più efficiente. Potresti anche considerare di utilizzare decimal(38, 0) per consentire calcoli intermedi più grandi nei tuoi calcoli di combinazione.

In secondo luogo, la query data non restituisce i risultati corretti. Ad esempio, utilizzando i miei dati di test dal test delle prestazioni di seguito, il set 1 è lo stesso del set 18. Sembra che la tua query prenda una striscia scorrevole che si avvolge:ogni set è sempre 5 membri adiacenti, con un aspetto simile a questo (ho ruotato per renderlo più facile da vedere):

 1 ABCDE            
 2 ABCD            Q
 3 ABC            PQ
 4 AB            OPQ
 5 A            NOPQ
 6             MNOPQ
 7            LMNOP 
 8           KLMNO  
 9          JKLMN   
10         IJKLM    
11        HIJKL     
12       GHIJK      
13      FGHIJ       
14     EFGHI        
15    DEFGH         
16   CDEFG          
17  BCDEF           
18 ABCDE            
19 ABCD            Q

Confronta il modello dalle mie query:

 31 ABCDE  
 47 ABCD F 
 55 ABC EF 
 59 AB DEF 
 61 A CDEF 
 62  BCDEF 
 79 ABCD  G
 87 ABC E G
 91 AB DE G
 93 A CDE G
 94  BCDE G
103 ABC  FG
107 AB D FG
109 A CD FG
110  BCD FG
115 AB  EFG
117 A C EFG
118  BC EFG
121 A  DEFG
...

Solo per portare a casa il bit-pattern -> index of combination per chiunque sia interessato, nota che 31 in binary =11111 e il pattern è ABCDE. 121 in binario è 1111001 e il modello è A__DEFG (mappato al contrario).

Risultati delle prestazioni con una tabella di numeri reali

Ho eseguito alcuni test delle prestazioni con grandi set sulla mia seconda query sopra. Non ho un record in questo momento della versione del server utilizzata. Ecco i miei dati di prova:

DECLARE
   @k int,
   @n int;

DECLARE @set TABLE (value varchar(24));
INSERT @set VALUES ('A'),('B'),('C'),('D'),('E'),('F'),('G'),('H'),('I'),('J'),('K'),('L'),('M'),('N'),('O'),('P'),('Q');
SET @n = @@RowCount;
SET @k = 5;

DECLARE @combinations bigint = dbo.Factorial(@n) / (dbo.Factorial(@k) * dbo.Factorial(@n - @k));
SELECT CAST(@combinations as varchar(max)) + ' combinations', MaxNumUsedFromNumbersTable = POWER(2, @n);

Peter ha mostrato che questo "incrocio incrociato verticale" non funziona così come la semplice scrittura di SQL dinamico per eseguire effettivamente i CROSS JOIN che evita. Al banale costo di qualche lettura in più, la sua soluzione ha metriche tra 10 e 17 volte migliori. Le prestazioni della sua query diminuiscono più velocemente della mia all'aumentare della quantità di lavoro, ma non abbastanza da impedire a chiunque di utilizzarla.

La seconda serie di numeri di seguito è il fattore diviso per la prima riga della tabella, solo per mostrare come si ridimensiona.

Erik

Items  CPU   Writes  Reads  Duration |  CPU  Writes  Reads Duration
----- ------ ------ ------- -------- | ----- ------ ------ --------
17•5    7344     0     3861    8531  |
18•9   17141     0     7748   18536  |   2.3          2.0      2.2
20•10  76657     0    34078   84614  |  10.4          8.8      9.9
21•11 163859     0    73426  176969  |  22.3         19.0     20.7
21•20 142172     0    71198  154441  |  19.4         18.4     18.1

Pietro

Items  CPU   Writes  Reads  Duration |  CPU  Writes  Reads Duration
----- ------ ------ ------- -------- | ----- ------ ------ --------
17•5     422    70    10263     794  | 
18•9    6046   980   219180   11148  |  14.3   14.0   21.4    14.0
20•10  24422  4126   901172   46106  |  57.9   58.9   87.8    58.1
21•11  58266  8560  2295116  104210  | 138.1  122.3  223.6   131.3
21•20  51391     5  6291273   55169  | 121.8    0.1  613.0    69.5

Estrapolando, alla fine la mia query sarà più economica (sebbene sia fin dall'inizio in letture), ma non per molto tempo. Per utilizzare 21 articoli nel set è già necessaria una tabella di numeri fino a 2097152...

Ecco un commento che ho fatto inizialmente prima di rendermi conto che la mia soluzione avrebbe prestazioni nettamente migliori con una tabella numerica al volo:

Risultati delle prestazioni con una tabella dei numeri al volo

Quando inizialmente ho scritto questa risposta, ho detto:

Bene, l'ho provato e i risultati sono stati che ha funzionato molto meglio! Ecco la query che ho usato:

DECLARE @N int = 16, @K int = 12;

CREATE TABLE #Set (Value char(1) PRIMARY KEY CLUSTERED);
CREATE TABLE #Items (Num int);
INSERT #Items VALUES (@K);
INSERT #Set 
SELECT TOP (@N) V
FROM
   (VALUES ('A'),('B'),('C'),('D'),('E'),('F'),('G'),('H'),('I'),('J'),('K'),('L'),('M'),('N'),('O'),('P'),('Q'),('R'),('S'),('T'),('U'),('V'),('W'),('X'),('Y'),('Z')) X (V);
GO
DECLARE
   @N int = (SELECT Count(*) FROM #Set),
   @K int = (SELECT TOP 1 Num FROM #Items);

DECLARE @combination int, @value char(1);
WITH L0 AS (SELECT 1 N UNION ALL SELECT 1),
L1 AS (SELECT 1 N FROM L0, L0 B),
L2 AS (SELECT 1 N FROM L1, L1 B),
L3 AS (SELECT 1 N FROM L2, L2 B),
L4 AS (SELECT 1 N FROM L3, L3 B),
L5 AS (SELECT 1 N FROM L4, L4 B),
Nums AS (SELECT Row_Number() OVER(ORDER BY (SELECT 1)) Num FROM L5),
Nums1 AS (
   SELECT Num, P1 = (Num & 0x55555555) + ((Num / 2) & 0x55555555)
   FROM Nums
   WHERE Num BETWEEN 0 AND Power(2, @N) - 1
), Nums2 AS (
   SELECT Num, P2 = (P1 & 0x33333333) + ((P1 / 4) & 0x33333333)
   FROM Nums1
), Nums3 AS (
   SELECT Num, P3 = (P2 & 0x0F0F0F0F) + ((P2 / 16) & 0x0F0F0F0F)
   FROM Nums2
), BaseSet AS (
   SELECT Ind = Power(2, Row_Number() OVER (ORDER BY Value) - 1), *
   FROM #Set
)
SELECT
   @Combination = N.Num,
   @Value = S.Value
FROM
   Nums3 N
   INNER JOIN BaseSet S
      ON N.Num & S.Ind <> 0
WHERE P3 % 255 = @K;

Nota che ho selezionato i valori in variabili per ridurre il tempo e la memoria necessari per testare tutto. Il server fa ancora lo stesso lavoro. Ho modificato la versione di Peter in modo che fosse simile e ho rimosso gli extra non necessari in modo che fossero entrambi il più snelli possibile. La versione del server utilizzata per questi test è Microsoft SQL Server 2008 (RTM) - 10.0.1600.22 (Intel X86) Standard Edition on Windows NT 5.2 <X86> (Build 3790: Service Pack 2) (VM) in esecuzione su una macchina virtuale.

Di seguito sono riportati i grafici che mostrano le curve di prestazione per valori di N e K fino a 21. I relativi dati di base si trovano in un'altra risposta in questa pagina . I valori sono il risultato di 5 esecuzioni di ciascuna query a ciascun valore K e N, seguite dall'eliminazione dei valori migliori e peggiori per ciascuna metrica e dalla media dei restanti 3.

Fondamentalmente, la mia versione ha una "spalla" (nell'angolo più a sinistra del grafico) con valori alti di N e valori bassi di K che lo rendono peggiore rispetto alla versione SQL dinamica. Tuttavia, questo rimane abbastanza basso e costante e il picco centrale intorno a N =21 e K =11 è molto più basso per Durata, CPU e Letture rispetto alla versione SQL dinamica.

Ho incluso un grafico del numero di righe che ogni elemento dovrebbe restituire in modo da poter vedere come si comporta la query in base alla quantità di lavoro che deve svolgere.

Si prega di consultare la mia risposta aggiuntiva in questa pagina per i risultati completi delle prestazioni. Ho raggiunto il limite di caratteri del post e non ho potuto includerlo qui. (Qualche idea su dove altro metterla?) Per mettere le cose in prospettiva rispetto ai risultati delle prestazioni della mia prima versione, ecco lo stesso formato di prima:

Erik

Items  CPU  Duration  Reads  Writes |  CPU  Duration  Reads 
----- ----- -------- ------- ------ | ----- -------- -------
17•5    354      378   12382      0 |
18•9   1849     1893   97246      0 |   5.2      5.0     7.9
20•10  7119     7357  369518      0 |  20.1     19.5    29.8
21•11 13531    13807  705438      0 |  38.2     36.5    57.0
21•20  3234     3295     48       0 |   9.1      8.7     0.0

Pietro

Items  CPU  Duration  Reads  Writes |  CPU  Duration  Reads 
----- ----- -------- ------- ------ | ----- -------- -------
17•5     41       45    6433      0 | 
18•9   2051     1522  214021      0 |  50.0     33.8    33.3
20•10  8271     6685  864455      0 | 201.7    148.6   134.4
21•11 18823    15502 2097909      0 | 459.1    344.5   326.1
21•20 25688    17653 4195863      0 | 626.5    392.3   652.2

Conclusioni

  • Le tabelle numeriche al volo sono migliori di una tabella reale contenente righe, poiché leggerne una con un numero di righe enorme richiede molto I/O. È meglio usare un po' di CPU.
  • I miei test iniziali non erano abbastanza ampi per mostrare realmente le caratteristiche prestazionali delle due versioni.
  • La versione di Peter potrebbe essere migliorata facendo in modo che ogni JOIN non solo sia maggiore dell'oggetto precedente, ma limiti anche il valore massimo in base a quanti altri oggetti devono essere inseriti nel set. Ad esempio, a 21 elementi presi 21 alla volta, c'è solo una risposta di 21 righe (tutti i 21 elementi, una volta), ma i set di righe intermedi nella versione SQL dinamica, all'inizio del piano di esecuzione, contengono combinazioni come " AU" al passaggio 2 anche se questo verrà scartato al prossimo join poiché non è disponibile un valore superiore a "U". Allo stesso modo, un set di righe intermedio al passaggio 5 conterrà "ARSTU" ma l'unica combinazione valida a questo punto è "ABCDE". Questa versione migliorata non avrebbe un picco più basso al centro, quindi forse non lo migliorerebbe abbastanza per diventare il chiaro vincitore, ma diventerebbe almeno simmetrico in modo che i grafici non rimangano al massimo oltre la metà della regione ma ricadano indietro vicino a 0 come fa la mia versione (vedi l'angolo superiore dei picchi per ogni query).

Analisi della durata

  • Non c'è alcuna differenza davvero significativa tra le versioni in termini di durata (>100 ms) fino a 14 elementi presi 12 alla volta. Fino a questo punto, la mia versione vince 30 volte e la versione SQL dinamica vince 43 volte.
  • A partire da 14•12, la mia versione era 65 volte più veloce (59>100 ms), la versione SQL dinamica 64 volte (60>100 ms). Tuttavia, tutte le volte che la mia versione era più veloce, ha risparmiato una durata media totale di 256,5 secondi e quando la versione SQL dinamica era più veloce, ha risparmiato 80,2 secondi.
  • La durata media totale di tutte le prove è stata di 270,3 secondi per Erik, 446,2 secondi per Peter.
  • Se viene creata una tabella di ricerca per determinare quale versione utilizzare (scegliendo quella più veloce per gli input), tutti i risultati potrebbero essere eseguiti in 188,7 secondi. L'utilizzo di quello più lento ogni volta richiederebbe 527,7 secondi.

Legge l'analisi

L'analisi della durata ha mostrato che la mia query ha vinto per un importo significativo ma non eccessivamente grande. Quando la metrica passa alle letture, emerge un'immagine molto diversa:la mia query utilizza in media 1/10 delle letture.

  • Non c'è alcuna differenza davvero significativa tra le versioni in letture (>1000) fino a 9 elementi presi 9 alla volta. Fino a questo punto, la mia versione vince 30 volte e la versione SQL dinamica vince 17 volte.
  • A partire da 9•9, la mia versione ha utilizzato meno letture 118 volte (113>1000), la versione SQL dinamica 69 volte (31>1000). Tuttavia, tutte le volte che la mia versione ha utilizzato meno letture, ha salvato una media totale di 75,9 milioni di letture e quando la versione SQL dinamica era più veloce, ha salvato 380.000 letture.
  • La media totale delle letture per tutte le prove è stata di 8,4 milioni di Erik, 84 milioni di Peter.
  • Se viene creata una tabella di ricerca per determinare quale versione utilizzare (scegliendo quella migliore per gli input), tutti i risultati potrebbero essere eseguiti in 8 milioni di letture. L'utilizzo del peggiore ogni volta richiederebbe 84,3 milioni di letture.

Sarei molto interessato a vedere i risultati di una versione SQL dinamica aggiornata che pone il limite superiore aggiuntivo sugli elementi scelti ad ogni passaggio come ho descritto sopra.

Addendum

La versione seguente della mia query ottiene un miglioramento di circa il 2,25% rispetto ai risultati delle prestazioni sopra elencati. Ho usato il metodo di conteggio dei bit HAKMEM del MIT e ho aggiunto un Convert(int) sul risultato di row_number() poiché restituisce un bigint. Ovviamente vorrei che questa fosse la versione con cui avevo usato per tutti i test delle prestazioni, i grafici e i dati di cui sopra, ma è improbabile che la rifarò mai perché era laboriosa.

WITH L0 AS (SELECT 1 N UNION ALL SELECT 1),
L1 AS (SELECT 1 N FROM L0, L0 B),
L2 AS (SELECT 1 N FROM L1, L1 B),
L3 AS (SELECT 1 N FROM L2, L2 B),
L4 AS (SELECT 1 N FROM L3, L3 B),
L5 AS (SELECT 1 N FROM L4, L4 B),
Nums AS (SELECT Row_Number() OVER(ORDER BY (SELECT 1)) Num FROM L5),
Nums1 AS (
   SELECT Convert(int, Num) Num
   FROM Nums
   WHERE Num BETWEEN 1 AND Power(2, @N) - 1
), Nums2 AS (
   SELECT
      Num,
      P1 = Num - ((Num / 2) & 0xDB6DB6DB) - ((Num / 4) & 0x49249249)
   FROM Nums1
),
Nums3 AS (SELECT Num, Bits = ((P1 + P1 / 8) & 0xC71C71C7) % 63 FROM Nums2),
BaseSet AS (SELECT Ind = Power(2, Row_Number() OVER (ORDER BY Value) - 1), * FROM #Set)
SELECT
   N.Num,
   S.Value
FROM
   Nums3 N
   INNER JOIN BaseSet S
      ON N.Num & S.Ind <> 0
WHERE
   Bits = @K

E non ho potuto resistere a mostrare un'altra versione che esegue una ricerca per ottenere il conteggio dei bit. Potrebbe anche essere più veloce di altre versioni:

DECLARE @BitCounts binary(255) = 
   0x01010201020203010202030203030401020203020303040203030403040405
   + 0x0102020302030304020303040304040502030304030404050304040504050506
   + 0x0102020302030304020303040304040502030304030404050304040504050506
   + 0x0203030403040405030404050405050603040405040505060405050605060607
   + 0x0102020302030304020303040304040502030304030404050304040504050506
   + 0x0203030403040405030404050405050603040405040505060405050605060607
   + 0x0203030403040405030404050405050603040405040505060405050605060607
   + 0x0304040504050506040505060506060704050506050606070506060706070708;
WITH L0 AS (SELECT 1 N UNION ALL SELECT 1),
L1 AS (SELECT 1 N FROM L0, L0 B),
L2 AS (SELECT 1 N FROM L1, L1 B),
L3 AS (SELECT 1 N FROM L2, L2 B),
L4 AS (SELECT 1 N FROM L3, L3 B),
L5 AS (SELECT 1 N FROM L4, L4 B),
Nums AS (SELECT Row_Number() OVER(ORDER BY (SELECT 1)) Num FROM L5),
Nums1 AS (SELECT Convert(int, Num) Num FROM Nums WHERE Num BETWEEN 1 AND Power(2, @N) - 1),
BaseSet AS (SELECT Ind = Power(2, Row_Number() OVER (ORDER BY Value) - 1), * FROM ComboSet)
SELECT
   @Combination = N.Num,
   @Value = S.Value
FROM
   Nums1 N
   INNER JOIN BaseSet S
      ON N.Num & S.Ind <> 0
WHERE
   @K =
      Convert(int, Substring(@BitCounts, N.Num & 0xFF, 1))
      + Convert(int, Substring(@BitCounts, N.Num / 256 & 0xFF, 1))
      + Convert(int, Substring(@BitCounts, N.Num / 65536 & 0xFF, 1))
      + Convert(int, Substring(@BitCounts, N.Num / 16777216, 1))