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

Genera un set o una sequenza senza loop – parte 1

Esistono molti casi d'uso per la generazione di una sequenza di valori in SQL Server. Non sto parlando di un IDENTITY persistente colonna (o il nuovo SEQUENCE in SQL Server 2012), ma piuttosto un set temporaneo da utilizzare solo per la durata di una query. O anche nei casi più semplici, come l'aggiunta di un numero di riga a ciascuna riga in un set di risultati, che potrebbero comportare l'aggiunta di un ROW_NUMBER() funzione alla query (o, meglio ancora, nel livello di presentazione, che deve comunque scorrere i risultati riga per riga).

Sto parlando di casi leggermente più complicati. Ad esempio, potresti avere un rapporto che mostra le vendite per data. Una query tipica potrebbe essere:

SELECT 
  OrderDate  = CONVERT(DATE, OrderDate),
  OrderCount = COUNT(*)
FROM dbo.Orders
GROUP BY CONVERT(DATE, OrderDate)
ORDER BY OrderDate;

Il problema con questa query è che, se non ci sono ordini in un determinato giorno, non ci sarà alcuna riga per quel giorno. Ciò può portare a confusione, dati fuorvianti o persino calcoli errati (pensa alle medie giornaliere) per i consumatori a valle dei dati.

Quindi è necessario colmare quelle lacune con le date che non sono presenti nei dati. E a volte le persone inseriscono i loro dati in una tabella #temp e usano un WHILE loop o un cursore per inserire le date mancanti una per una. Non mostrerò quel codice qui perché non voglio sostenerne l'uso, ma l'ho visto dappertutto.

Prima di approfondire le date, però, parliamo innanzitutto di numeri, poiché puoi sempre utilizzare una sequenza di numeri per ricavare una sequenza di date.

Tabella dei numeri

Sono stato a lungo un sostenitore della memorizzazione di una "tabella dei numeri" ausiliaria su disco (e, del resto, anche una tabella del calendario).

Ecco un modo per generare una semplice tabella di numeri con 1.000.000 di valori:

SELECT TOP (1000000) n = CONVERT(INT, ROW_NUMBER() OVER (ORDER BY s1.[object_id]))
INTO dbo.Numbers
FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2
OPTION (MAXDOP 1);
 
CREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers(n)
-- WITH (DATA_COMPRESSION = PAGE)
;

Perché MAXDOP 1? Vedi il post sul blog di Paul White e il suo articolo Connect relativo agli obiettivi di fila.

Tuttavia, molte persone sono contrarie all'approccio del tavolo ausiliario. La loro argomentazione:perché archiviare tutti quei dati su disco (e in memoria) quando possono generare i dati al volo? Il mio contro è essere realistici e pensare a cosa stai ottimizzando; il calcolo può essere costoso e sei sicuro che calcolare un intervallo di numeri al volo sarà sempre più economico? Per quanto riguarda lo spazio, la tabella Numbers occupa solo circa 11 MB compressi e 17 MB non compressi. E se la tabella viene referenziata abbastanza frequentemente, dovrebbe essere sempre in memoria, rendendo l'accesso veloce.

Diamo un'occhiata ad alcuni esempi e ad alcuni degli approcci più comuni utilizzati per soddisfarli. Spero che possiamo essere tutti d'accordo sul fatto che, anche a 1.000 valori, non vogliamo risolvere questi problemi usando un loop o un cursore.

Generazione di una sequenza di 1.000 numeri

Iniziando semplicemente, generiamo un insieme di numeri da 1 a 1.000.

    Tabella dei numeri

    Ovviamente con una tabella di numeri questo compito è piuttosto semplice:

    SELECT TOP (1000) n FROM dbo.Numbers ORDER BY n;

    Piano:

    valori_spt

    Questa è una tabella utilizzata dalle stored procedure interne per vari scopi. Il suo utilizzo online sembra essere piuttosto diffuso, anche se non è documentato, non supportato, potrebbe scomparire un giorno e perché contiene solo un insieme di valori finito, non unico e non contiguo. Esistono 2.164 valori univoci e 2.508 totali in SQL Server 2008 R2; nel 2012 sono 2.167 unici e 2.515 totali. Ciò include duplicati, valori negativi e anche se si utilizza DISTINCT , molte lacune una volta superato il numero 2.048. Quindi la soluzione alternativa è usare ROW_NUMBER() per generare una sequenza contigua, a partire da 1, in base ai valori nella tabella.

    SELECT TOP (1000) n = ROW_NUMBER() OVER (ORDER BY number) 
      FROM [master]..spt_values ORDER BY n;

    Piano:

    Detto questo, per soli 1.000 valori, potresti scrivere una query leggermente più semplice per generare la stessa sequenza:

    SELECT DISTINCT n = number FROM master..[spt_values] WHERE number BETWEEN 1 AND 1000;

    Questo porta a un piano più semplice, ovviamente, ma si interrompe abbastanza rapidamente (una volta che la sequenza deve essere più di 2.048 righe):

    In ogni caso sconsiglio l'utilizzo di questa tabella; Lo includo a scopo di confronto, solo perché so quanto di questo è disponibile e quanto potrebbe essere allettante riutilizzare semplicemente il codice che incontri.

    sys.all_objects

    Un altro approccio che è stato uno dei miei preferiti nel corso degli anni è usare sys.all_objects . Come spt_values , non esiste un modo affidabile per generare direttamente una sequenza contigua e abbiamo gli stessi problemi relativi a un set finito (poco meno di 2.000 righe in SQL Server 2008 R2 e poco più di 2.000 righe in SQL Server 2012), ma per 1.000 righe possiamo usare lo stesso ROW_NUMBER() trucco. Il motivo per cui mi piace questo approccio è che (a) c'è meno preoccupazione che questa vista scompaia presto, (b) la vista stessa è documentata e supportata e (c) verrà eseguita su qualsiasi database su qualsiasi versione da SQL Server 2005 senza dover attraversare i confini del database (compresi i database contenuti).

    SELECT TOP (1000) n = ROW_NUMBER() OVER (ORDER BY [object_id]) FROM sys.all_objects ORDER BY n;

    Piano:

    CTE impilati

    Credo che Itzik Ben-Gan meriti il ​​massimo merito per questo approccio; in pratica costruisci un CTE con un piccolo insieme di valori, quindi crei il prodotto cartesiano contro se stesso per generare il numero di righe di cui hai bisogno. E ancora, invece di provare a generare un insieme contiguo come parte della query sottostante, possiamo semplicemente applicare ROW_NUMBER() al risultato finale.

    ;WITH e1(n) AS
    (
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
    ), -- 10
    e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b), -- 10*10
    e3(n) AS (SELECT 1 FROM e1 CROSS JOIN e2) -- 10*100
      SELECT n = ROW_NUMBER() OVER (ORDER BY n) FROM e3 ORDER BY n;

    Piano:

    CTE ricorsivo

    Infine, abbiamo un CTE ricorsivo, che usa 1 come ancoraggio e aggiunge 1 fino a raggiungere il massimo. Per sicurezza specifico il massimo sia in WHERE clausola della parte ricorsiva, e nel MAXRECURSION collocamento. A seconda di quanti numeri hai bisogno, potresti dover impostare MAXRECURSION a 0 .

    ;WITH n(n) AS
    (
        SELECT 1
        UNION ALL
        SELECT n+1 FROM n WHERE n < 1000
    )
    SELECT n FROM n ORDER BY n
    OPTION (MAXRECURSION 1000);

    Piano:

Prestazioni

Ovviamente con 1.000 valori le differenze nelle prestazioni sono trascurabili, ma può essere utile vedere come si comportano queste diverse opzioni:


Runtime, in millisecondi, per generare 1.000 numeri contigui

Ho eseguito ogni query 20 volte e ho impiegato tempi di esecuzione medi. Ho anche testato dbo.Numbers tabella, sia in formato compresso che non compresso, e con una cache a freddo e una cache a caldo. Con una cache calda, rivaleggia molto da vicino con le altre opzioni più veloci disponibili (spt_values , non consigliato e CTE impilati), ma il primo colpo è relativamente costoso (anche se quasi rido chiamandolo così).

Continua…

Se questo è il tuo caso d'uso tipico e non ti avventurerai molto oltre le 1.000 righe, spero di aver mostrato i modi più veloci per generare quei numeri. Se il tuo caso d'uso è un numero maggiore o se stai cercando soluzioni per generare sequenze di date, resta sintonizzato. Più avanti in questa serie, esplorerò la generazione di sequenze di 50.000 e 1.000.000 di numeri e di intervalli di date che vanno da una settimana a un anno.

[ Parte 1 | Parte 2 | Parte 3]