Invierò una risposta per iniziare. Il mio primo pensiero è stato che dovrebbe essere possibile sfruttare la natura che preserva l'ordine di un join ad anello nidificato insieme ad alcune tabelle helper che hanno una riga per ogni lettera. La parte difficile stava andando in loop in modo tale che i risultati fossero ordinati per lunghezza oltre a evitare duplicati. Ad esempio, quando si unisce un CTE che include tutte le 26 lettere maiuscole insieme a '', si può finire per generare 'A' + '' + 'A'
e '' + 'A' + 'A'
che è ovviamente la stessa stringa.
La prima decisione è stata dove archiviare i dati di supporto. Ho provato a utilizzare una tabella temporanea, ma questo ha avuto un impatto sorprendentemente negativo sulle prestazioni, anche se i dati si adattano a una singola pagina. La tabella temporanea conteneva i seguenti dati:
SELECT 'A'
UNION ALL SELECT 'B'
...
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
Rispetto all'utilizzo di un CTE, la query impiegava 3 volte più a lungo con una tabella cluster e 4 volte più a lungo con un heap. Non credo che il problema sia che i dati sono su disco. Dovrebbe essere letto in memoria come una singola pagina ed elaborato in memoria per l'intero piano. Forse SQL Server può funzionare con i dati di un operatore Constant Scan in modo più efficiente di quanto non possa fare con i dati archiviati nelle tipiche pagine del negozio di file.
È interessante notare che SQL Server sceglie di inserire i risultati ordinati da una tabella tempdb a pagina singola con i dati ordinati in uno spool di tabella:
SQL Server inserisce spesso i risultati per la tabella interna di un cross join in uno spool di tabella, anche se sembra assurdo farlo. Penso che l'ottimizzatore abbia bisogno di un po 'di lavoro in questo settore. Ho eseguito la query con ilNO_PERFORMANCE_SPOOL
per evitare l'hit prestazioni.
Un problema con l'utilizzo di un CTE per memorizzare i dati di supporto è che non è garantito che i dati vengano ordinati. Non riesco a pensare al motivo per cui l'ottimizzatore avrebbe scelto di non ordinarlo e in tutti i miei test i dati sono stati elaborati nell'ordine in cui ho scritto il CTE:
Tuttavia, è meglio non correre rischi, soprattutto se c'è un modo per farlo senza un grande sovraccarico di prestazioni. È possibile ordinare i dati in una tabella derivata aggiungendo un TOP
operatore superfluo . Per esempio:
(SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR)
Tale aggiunta alla query dovrebbe garantire che i risultati vengano restituiti nell'ordine corretto. Mi aspettavo che tutti i tipi avessero un grande impatto negativo sulle prestazioni. Query Optimizer si aspettava questo anche in base ai costi stimati:
Molto sorprendentemente, non ho potuto osservare alcuna differenza statisticamente significativa nel tempo della CPU o nel tempo di esecuzione con o senza ordinamento esplicito. Semmai, la query sembrava funzionare più velocemente con ORDER BY
! Non ho spiegazioni per questo comportamento.
La parte difficile del problema era capire come inserire caratteri vuoti nei posti giusti. Come accennato in precedenza, un semplice CROSS JOIN
avrebbe come risultato dati duplicati. Sappiamo che la stringa 100000000th avrà una lunghezza di sei caratteri perché:
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 = 914654 <100000000
ma
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 + 26 ^ 6 = 321272406> 100000000
Pertanto, dobbiamo solo aderire alla lettera CTE sei volte. Supponiamo di unirci al CTE sei volte, prendere una lettera da ogni CTE e concatenarli tutti insieme. Supponiamo che la lettera più a sinistra non sia vuota. Se una delle lettere successive è vuota significa che la stringa è lunga meno di sei caratteri, quindi è un duplicato. Pertanto, possiamo impedire i duplicati trovando il primo carattere non vuoto e richiedendo che tutti i caratteri dopo non siano vuoti. Ho scelto di rintracciarlo assegnando una FLAG
colonna a uno dei CTE e aggiungendo un segno di spunta alla WHERE
clausola. Ciò dovrebbe essere più chiaro dopo aver esaminato la query. La query finale è la seguente:
WITH FIRST_CHAR (CHR) AS
(
SELECT 'A'
UNION ALL SELECT 'B'
UNION ALL SELECT 'C'
UNION ALL SELECT 'D'
UNION ALL SELECT 'E'
UNION ALL SELECT 'F'
UNION ALL SELECT 'G'
UNION ALL SELECT 'H'
UNION ALL SELECT 'I'
UNION ALL SELECT 'J'
UNION ALL SELECT 'K'
UNION ALL SELECT 'L'
UNION ALL SELECT 'M'
UNION ALL SELECT 'N'
UNION ALL SELECT 'O'
UNION ALL SELECT 'P'
UNION ALL SELECT 'Q'
UNION ALL SELECT 'R'
UNION ALL SELECT 'S'
UNION ALL SELECT 'T'
UNION ALL SELECT 'U'
UNION ALL SELECT 'V'
UNION ALL SELECT 'W'
UNION ALL SELECT 'X'
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
)
, ALL_CHAR (CHR, FLAG) AS
(
SELECT '', 0 CHR
UNION ALL SELECT 'A', 1
UNION ALL SELECT 'B', 1
UNION ALL SELECT 'C', 1
UNION ALL SELECT 'D', 1
UNION ALL SELECT 'E', 1
UNION ALL SELECT 'F', 1
UNION ALL SELECT 'G', 1
UNION ALL SELECT 'H', 1
UNION ALL SELECT 'I', 1
UNION ALL SELECT 'J', 1
UNION ALL SELECT 'K', 1
UNION ALL SELECT 'L', 1
UNION ALL SELECT 'M', 1
UNION ALL SELECT 'N', 1
UNION ALL SELECT 'O', 1
UNION ALL SELECT 'P', 1
UNION ALL SELECT 'Q', 1
UNION ALL SELECT 'R', 1
UNION ALL SELECT 'S', 1
UNION ALL SELECT 'T', 1
UNION ALL SELECT 'U', 1
UNION ALL SELECT 'V', 1
UNION ALL SELECT 'W', 1
UNION ALL SELECT 'X', 1
UNION ALL SELECT 'Y', 1
UNION ALL SELECT 'Z', 1
)
SELECT TOP (100000000)
d6.CHR + d5.CHR + d4.CHR + d3.CHR + d2.CHR + d1.CHR
FROM (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d6
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d5
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d4
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d3
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d2
CROSS JOIN (SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR) d1
WHERE (d2.FLAG + d3.FLAG + d4.FLAG + d5.FLAG + d6.FLAG) =
CASE
WHEN d6.FLAG = 1 THEN 5
WHEN d5.FLAG = 1 THEN 4
WHEN d4.FLAG = 1 THEN 3
WHEN d3.FLAG = 1 THEN 2
WHEN d2.FLAG = 1 THEN 1
ELSE 0 END
OPTION (MAXDOP 1, FORCE ORDER, LOOP JOIN, NO_PERFORMANCE_SPOOL);
I CTE sono come sopra descritti. ALL_CHAR
viene unito a cinque volte perché include una riga per un carattere vuoto. Il carattere finale della stringa non deve mai essere vuoto così un CTE separata è definito per esso, FIRST_CHAR
. La colonna flag aggiuntiva ALL_CHAR
viene utilizzata per impedire i duplicati come descritto sopra. Potrebbe esserci un modo più efficiente per fare questo controllo, ma ci sono sicuramente modi più inefficienti per farlo. Un tentativo da parte mia LEN()
e ha POWER()
reso la query eseguita sei volte più lenta della versione corrente.
I suggerimenti MAXDOP 1
e FORCE ORDER
sono essenziali per assicurarsi che l'ordine venga conservato nella query. Un piano stimato con annotazioni potrebbe essere utile per capire perché i join sono nel loro ordine corrente:
I piani di query vengono spesso letti da destra a sinistra, ma le richieste di riga si verificano da sinistra a destra. Idealmente, SQL Server richiederà esattamente 100 milioni di righe dall'operatore di d1
scansione costante. Mentre ti sposti da sinistra a destra, mi aspetto che vengano richieste meno righe da ciascun operatore. Possiamo vederlo nel piano di esecuzione effettivo . Inoltre, di seguito è riportato uno screenshot di SQL Sentry Plan Explorer:
Abbiamo ottenuto esattamente 100 milioni di righe da d1, il che è una buona cosa. Si noti che il rapporto tra le righe tra d2 e d3 è quasi esattamente 27: 1 (165336 * 27 = 4464072), il che ha senso se si pensa a come funzionerà il cross join. Il rapporto tra le file tra d1 e d2 è 22,4 che rappresenta un lavoro sprecato. Credo che le righe extra provengano da duplicati (a causa dei caratteri vuoti nel mezzo delle stringhe) che non superano l'operatore di loop nidificato che esegue il filtro.
Il LOOP JOIN
suggerimento non è tecnicamente necessario poiché a CROSS JOIN
può essere implementato solo come loop loop in SQL Server. IlNO_PERFORMANCE_SPOOL
è quello di evitare che la tabella di spooling inutili. L'omissione del suggerimento di spool ha reso la query più lunga di 3 volte sul mio computer.
La query finale ha un tempo di CPU di circa 17 secondi e un tempo totale trascorso di 18 secondi. Ciò avveniva quando si eseguiva la query tramite SSMS e si scartava il set di risultati. Sono molto interessato a vedere altri metodi per generare i dati.