Emula la funzione scalare definita dall'utente in modo da non impedire il parallelismo


12

Sto cercando di vedere se c'è un modo per ingannare SQL Server per utilizzare un determinato piano per la query.

1. Ambiente

Immagina di avere alcuni dati condivisi tra diversi processi. Supponiamo quindi di avere alcuni risultati dell'esperimento che occupano molto spazio. Quindi, per ogni processo sappiamo quale anno / mese di risultato dell'esperimento vogliamo usare.

if object_id('dbo.SharedData') is not null
    drop table SharedData

create table dbo.SharedData (
    experiment_year int,
    experiment_month int,
    rn int,
    calculated_number int,
    primary key (experiment_year, experiment_month, rn)
)
go

Ora, per ogni processo abbiamo i parametri salvati nella tabella

if object_id('dbo.Params') is not null
    drop table dbo.Params

create table dbo.Params (
    session_id int,
    experiment_year int,
    experiment_month int,
    primary key (session_id)
)
go

2. Dati di prova

Aggiungiamo alcuni dati di test:

insert into dbo.Params (session_id, experiment_year, experiment_month)
select 1, 2014, 3 union all
select 2, 2014, 4 
go

insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
    2014, 3, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
    cross join master.dbo.spt_values as v2
go

insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
    2014, 4, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
    cross join master.dbo.spt_values as v2
go

3. Recupero dei risultati

Ora, è molto facile ottenere risultati dell'esperimento @experiment_year/@experiment_month:

create or alter function dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.SharedData as d
    where
        d.experiment_year = @experiment_year and
        d.experiment_month = @experiment_month
)
go

Il piano è carino e parallelo:

select
    calculated_number,
    count(*)
from dbo.f_GetSharedData(2014, 4)
group by
    calculated_number

query 0 piano

inserisci qui la descrizione dell'immagine

4. Problema

Ma, per rendere l'uso dei dati un po 'più generico, voglio avere un'altra funzione - dbo.f_GetSharedDataBySession(@session_id int). Quindi, il modo più semplice sarebbe quello di creare funzioni scalari, traducendo @session_id-> @experiment_year/@experiment_month:

create or alter function dbo.fn_GetExperimentYear(@session_id int)
returns int
as
begin
    return (
        select
            p.experiment_year
        from dbo.Params as p
        where
            p.session_id = @session_id
    )
end
go

create or alter function dbo.fn_GetExperimentMonth(@session_id int)
returns int
as
begin
    return (
        select
            p.experiment_month
        from dbo.Params as p
        where
            p.session_id = @session_id
    )
end
go

E ora possiamo creare la nostra funzione:

create or alter function dbo.f_GetSharedDataBySession1(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.f_GetSharedData(
        dbo.fn_GetExperimentYear(@session_id),
        dbo.fn_GetExperimentMonth(@session_id)
    ) as d
)
go

query 1 piano

inserisci qui la descrizione dell'immagine

Il piano è lo stesso tranne che, ovviamente, non è parallelo, perché le funzioni scalari che eseguono l'accesso ai dati rendono l'intero piano seriale .

Quindi ho provato diversi approcci, ad esempio usando subquery invece di funzioni scalari:

create or alter function dbo.f_GetSharedDataBySession2(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.f_GetSharedData(
       (select p.experiment_year from dbo.Params as p where p.session_id = @session_id),
       (select p.experiment_month from dbo.Params as p where p.session_id = @session_id)
    ) as d
)
go

piano query 2

inserisci qui la descrizione dell'immagine

O usando cross apply

create or alter function dbo.f_GetSharedDataBySession3(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.Params as p
        cross apply dbo.f_GetSharedData(
            p.experiment_year,
            p.experiment_month
        ) as d
    where
        p.session_id = @session_id
)
go

query 3 piano

inserisci qui la descrizione dell'immagine

Ma non riesco a trovare un modo per scrivere questa query per essere buono come quello che utilizza le funzioni scalari.

Paio di pensieri:

  1. Fondamentalmente quello che vorrei è poter dire in qualche modo a SQL Server di pre-calcolare determinati valori e poi passarli ulteriormente come costanti.
  2. Ciò che potrebbe essere utile è se avessimo qualche suggerimento sulla materializzazione intermedia . Ho controllato un paio di varianti (TVF multi-statement o cte con top), ma finora nessun piano è buono come quello con funzioni scalari
  3. Sono a conoscenza del prossimo miglioramento di SQL Server 2017 - Froid: ottimizzazione dei programmi imperativi in ​​un database relazionale. Non sono sicuro che possa aiutare, però. Sarebbe stato bello essere smentito qui, però.

Informazioni aggiuntive

Sto usando una funzione (piuttosto che selezionare i dati direttamente dalle tabelle) perché è molto più facile da usare in molte query diverse, che di solito hanno @session_idcome parametro.

Mi è stato chiesto di confrontare i tempi di esecuzione effettivi. In questo caso particolare

  • la query 0 viene eseguita per ~ 500 ms
  • la query 1 viene eseguita per ~ 1500ms
  • la query 2 viene eseguita per ~ 1500ms
  • la query 3 viene eseguita per ~ 2000 ms.

Il piano n. 2 ha una scansione dell'indice anziché una ricerca, che viene quindi filtrata dai predicati sui cicli nidificati. Il piano n. 3 non è poi così male, ma fa ancora più lavoro e rallenta il piano n. 0.

Supponiamo che dbo.Paramssia cambiato raramente e di solito hanno circa 1-200 righe, non più di, supponiamo che 2000 sia mai previsto. Sono circa 10 colonne e non mi aspetto di aggiungere colonne troppo spesso.

Il numero di righe in Params non è fisso, quindi per ogni @session_idci sarà una riga. Il numero di colonne non è stato risolto, è uno dei motivi per cui non voglio chiamare dbo.f_GetSharedData(@experiment_year int, @experiment_month int)da nessuna parte, quindi posso aggiungere una nuova colonna a questa query internamente. Sarei felice di sentire qualsiasi opinione / suggerimento su questo, anche se ha alcune restrizioni.


Il piano di query con Froid sarebbe simile a quello di query2 sopra, quindi sì, non ti porterà alla soluzione che vuoi ottenere in questo caso.
Karthik,

Risposte:


13

Non è possibile ottenere in modo sicuro esattamente ciò che si desidera in SQL Server oggi, ovvero in un'unica istruzione e con esecuzione parallela, entro le restrizioni stabilite nella domanda (come le percepisco).

Quindi la mia semplice risposta è no . Il resto di questa risposta è principalmente una discussione del perché, nel caso sia interessante.

È possibile ottenere un piano parallelo, come indicato nella domanda, ma ci sono due varietà principali, nessuna delle quali è adatta alle tue esigenze:

  1. Un loop annidato correlato si unisce, con un round robin distribuisce stream al livello superiore. Dato che una singola riga proviene da Paramsun session_idvalore specifico , il lato interno verrà eseguito su un singolo thread, anche se è contrassegnato dall'icona di parallelismo. Questo è il motivo per cui il piano apparentemente parallelo 3 non funziona altrettanto bene; è infatti seriale.

  2. L'altra alternativa è per il parallelismo indipendente sul lato interno dei loop di loop nidificati. Indipendente qui significa che i thread vengono avviati sul lato interno e non semplicemente gli stessi thread che stanno eseguendo il lato esterno dei loop di loop nidificati. SQL Server supporta il parallelismo di cicli nidificati indipendenti sul lato interno solo quando è garantita la presenza di una riga sul lato esterno e non sono presenti parametri di join correlati ( piano 2 ).

Quindi, abbiamo una scelta di un piano parallelo che è seriale (a causa di un thread) con i valori correlati desiderati; o un piano parallelo sul lato interno che deve essere scansionato perché non ha parametri con cui cercare. (A parte: dovrebbe davvero essere permesso di guidare il parallelismo sul lato interno usando esattamente un set di parametri correlati, ma non è mai stato implementato, probabilmente per una buona ragione).

Una domanda naturale è quindi: perché abbiamo bisogno di parametri correlati? Perché SQL Server non può semplicemente cercare direttamente i valori scalari forniti ad esempio da una sottoquery?

Bene, SQL Server può solo "indicizzare la ricerca" usando semplici riferimenti scalari, ad esempio un riferimento costante, variabile, colonna o espressione (quindi anche un risultato di una funzione scalare può qualificarsi). Una sottoquery (o altra costruzione simile) è semplicemente troppo complessa (e potenzialmente non sicura) per spingere l'intero motore di archiviazione. Pertanto, sono richiesti operatori del piano di query separati. Questo è il turno richiede correlazione, il che significa che non c'è parallelismo del tipo desiderato.

Tutto sommato, attualmente non esiste una soluzione migliore di metodi come l'assegnazione dei valori di ricerca alle variabili e quindi l'utilizzo di quelli nei parametri della funzione in un'istruzione separata.

Ora potresti avere delle considerazioni locali specifiche che significa che SESSION_CONTEXTvale la pena memorizzare nella cache i valori correnti dell'anno e del mese, ovvero:

SELECT FGSD.calculated_number, COUNT_BIG(*)
FROM dbo.f_GetSharedData
(
    CONVERT(integer, SESSION_CONTEXT(N'experiment_year')), 
    CONVERT(integer, SESSION_CONTEXT(N'experiment_month'))
) AS FGSD
GROUP BY FGSD.calculated_number;

Ma questo rientra nella categoria della soluzione alternativa.

D'altra parte, se le prestazioni di aggregazione sono di primaria importanza, è possibile considerare di attenersi alle funzioni incorporate e creare un indice columnstore (primario o secondario) sulla tabella. Potresti trovare i vantaggi dell'archiviazione columnstore, dell'elaborazione in modalità batch e del pushdown aggregato che offrono comunque maggiori vantaggi di una ricerca parallela in modalità riga.

Ma attenzione alle funzioni scalari T-SQL, in particolare con l'archiviazione columnstore, poiché è facile finire con la funzione valutata per riga in un filtro separato in modalità riga. In genere è abbastanza complicato garantire il numero di volte in cui SQL Server sceglierà di valutare gli scalari e meglio non provarci.


Grazie Paul, ottima risposta! Ho pensato di usarlo session_contextma decido che è un'idea un po 'troppo folle per me e non sono sicuro di come si adatterà alla mia architettura attuale. Ciò che potrebbe essere utile è, potrebbe essere, qualche suggerimento che potrei usare per far sapere all'ottimizzatore che dovrebbe trattare il risultato della subquery come un semplice riferimento scalare.
Roman Pekar,

8

Per quanto ne so, la forma del piano che desideri non è possibile solo con T-SQL. Sembra che tu voglia la forma del piano originale (piano di query 0) con le sottoquery delle tue funzioni applicate come filtri direttamente contro la scansione dell'indice cluster. Non otterrai mai un piano di query del genere se non usi le variabili locali per contenere i valori di ritorno delle funzioni scalari. Il filtro verrà invece implementato come join loop nidificato. Esistono tre modi diversi (dal punto di vista del parallelismo) che il loop loop può essere implementato:

  1. L'intero piano è seriale. Questo non è accettabile per te. Questo è il piano che ottieni per la query 1.
  2. Il loop join viene eseguito in serie. Credo che in questo caso il lato interno possa correre in parallelo, ma non è possibile passare alcun predicato ad esso. Quindi la maggior parte del lavoro verrà eseguita in parallelo, ma stai analizzando l'intera tabella e l'aggregazione parziale è molto più costosa di prima. Questo è il piano che ottieni per la query 2.
  3. Il join loop viene eseguito in parallelo. Con un ciclo nidificato parallelo si unisce il lato interno del ciclo in serie ma è possibile eseguire contemporaneamente fino a thread DOP in esecuzione sul lato interno. Il tuo set di risultati esterno avrà una sola riga, quindi il tuo piano parallelo sarà effettivamente seriale. Questo è il piano che ottieni per la query 3.

Quelle sono le uniche forme di piano possibili di cui sono a conoscenza. È possibile ottenerne altri se si utilizza una tabella temporanea, ma nessuno di essi risolve il problema fondamentale se si desidera che le prestazioni della query siano uguali a quelle della query 0.

È possibile ottenere prestazioni di query equivalenti utilizzando gli UDF scalari per assegnare valori di ritorno alle variabili locali e utilizzando tali variabili locali nella query. È possibile racchiudere quel codice in una procedura memorizzata o in un UDF multiistruzione per evitare problemi di manutenibilità. Per esempio:

DECLARE @experiment_year int = dbo.fn_GetExperimentYear(@session_id);
DECLARE @experiment_month int = dbo.fn_GetExperimentMonth(@session_id);

select
    calculated_number,
    count(*)
from dbo.f_GetSharedData(@experiment_year, @experiment_month)
group by
    calculated_number;

Gli UDF scalari sono stati spostati al di fuori della query che si desidera essere idonei al parallelismo. Il piano di query che ottengo sembra essere quello desiderato:

piano di query parallelo

Entrambi gli approcci presentano degli svantaggi se è necessario utilizzare questo set di risultati in altre query. Non è possibile partecipare direttamente a una stored procedure. Dovresti salvare i risultati in una tabella temporanea che presenta una serie di problemi. Puoi unirti a un MS-TVF, ma in SQL Server 2016 potresti riscontrare problemi di stima della cardinalità. SQL Server 2017 offre l' esecuzione interfogliata per MS-TVF che potrebbe risolvere completamente il problema.

Giusto per chiarire alcune cose: gli UDF scalari T-SQL proibiscono sempre il parallelismo e Microsoft non ha detto che FROID sarà disponibile in SQL Server 2017.


riguardo a Froid in SQL 2017 - non so perché ho pensato che fosse lì. È confermato per essere in vNext - brentozar.com/archive/2018/01/…
Roman Pekar

4

Molto probabilmente questo può essere fatto usando SQLCLR. Uno dei vantaggi di SQLCLR scalari UDF è che essi non impediscono il parallelismo se essi non fanno alcun accesso ai dati (e, talvolta, devono anche essere contrassegnato come "deterministico"). Quindi, come si fa a utilizzare qualcosa che non richiede l'accesso ai dati quando l'operazione stessa richiede l'accesso ai dati?

Bene, perché la dbo.Paramstabella dovrebbe:

  1. generalmente non ci sono più di 2000 righe al suo interno,
  2. cambia raramente la struttura,
  3. solo (attualmente) deve avere due INTcolonne

è possibile memorizzare nella cache le tre colonne - session_id, experiment_year int, experiment_monthin una raccolta statica (ad esempio un dizionario, forse) che viene popolata fuori processo e letta dagli UDF scalari che ottengono i valori experiment_year inte experiment_month. Quello che intendo con "out-of-process" è: puoi avere un UDF scalare SQLCLR completamente separato o Stored Procedure che può fare l'accesso ai dati e leggere dalla dbo.Paramstabella per popolare la raccolta statica. Quella UDF o Stored Procedure verrebbe eseguita prima di usare le UDF che ottengono i valori "year" e "month", in questo modo le UDFs che ottengono i valori "year" e "month" non stanno facendo alcun accesso ai dati DB.

L'UDF o la Stored procedure che legge i dati può prima verificare se la raccolta ha 0 voci e, in tal caso, quindi compilare, altrimenti saltare. Puoi anche tenere traccia del tempo in cui è stata popolata e se è stata oltre X minuti (o qualcosa del genere), quindi cancellare e ripopolare anche se ci sono voci nella raccolta. Saltare la popolazione aiuterà, poiché dovrà essere eseguito frequentemente per assicurarsi che sia sempre popolato per i due principali UDF per ottenere i valori.

La preoccupazione principale è quando SQL Server decide di scaricare il dominio app per qualsiasi motivo (o è attivato da qualcosa che utilizza DBCC FREESYSTEMCACHE('ALL');). Non si desidera rischiare che la raccolta venga cancellata tra l'esecuzione dell'UDF "popolato" o Stored Procedure e gli UDF per ottenere i valori "year" e "month". Nel qual caso puoi avere un controllo all'inizio di quei due UDF per generare un'eccezione se la raccolta è vuota, poiché è meglio sbagliare che fornire correttamente risultati falsi.

Naturalmente, la preoccupazione sopra menzionata presuppone che il desiderio sia quello di contrassegnare l'Assemblea come SAFE. Se l'Assemblea può essere contrassegnata come EXTERNAL_ACCESS, allora è possibile che un costruttore statico esegua il metodo che legge i dati e popola la raccolta, in modo che sia sempre necessario eseguirlo manualmente per aggiornare le righe, ma sarebbero sempre popolate (poiché il costruttore della classe statica viene sempre eseguito quando viene caricata la classe, cosa che accade ogni volta che un metodo in questa classe viene eseguito dopo un riavvio o viene scaricato il dominio App). Ciò richiede l'utilizzo di una connessione regolare e non della connessione al contesto in-process (che non è disponibile per i costruttori statici, quindi la necessità di EXTERNAL_ACCESS).

Nota: per non essere obbligato a contrassegnare l'Assemblea come UNSAFE, è necessario contrassegnare tutte le variabili di classe statiche come readonly. Questo significa, almeno, la collezione. Questo non è un problema poiché le raccolte di sola lettura possono avere elementi aggiunti o rimossi da essi, semplicemente non possono essere inizializzati al di fuori del costruttore o del carico iniziale. Tenere traccia del tempo di caricamento della raccolta allo scopo di scaderlo dopo X minuti è più complicato poiché una static readonly DateTimevariabile di classe non può essere modificata al di fuori del costruttore o del carico iniziale. Per aggirare questa restrizione, è necessario utilizzare una raccolta statica di sola lettura che contiene un singolo elemento che rappresenta il DateTimevalore in modo che possa essere rimosso e aggiunto nuovamente durante un aggiornamento.


Non so perché qualcuno abbia votato in negativo. Anche se non molto generico, penso che potrebbe essere applicabile nel mio caso attuale. Preferirei avere una soluzione SQL pura, ma lo esaminerò sicuramente più da vicino e proverò a vedere se funziona
Roman Pekar

@RomanPekar Non sono sicuro, ma ci sono molte persone che sono anti-SQLCLR. E forse alcuni che sono anti-me ;-). Ad ogni modo, non riesco a pensare al motivo per cui questa soluzione non funzionerebbe. Capisco la preferenza per il puro T-SQL, ma non so come farlo, e se non c'è una risposta concorrente, allora forse neanche altri lo fanno. Non so se le tabelle ottimizzate per la memoria e le UDF compilate in modo nativo farebbero meglio qui. Inoltre, ho appena aggiunto un paragrafo con alcune note di implementazione da tenere a mente.
Solomon Rutzky,

1
Non sono mai stato del tutto convinto che l'utilizzo di readonly staticsSQLCLR sia sicuro o saggio. Molto meno sono convinto che poi continuerò a ingannare il sistema trasformandolo in readonlyun tipo di riferimento, che poi vai e cambi . Mi dà i willies assoluti tbh.
Paul White 9

@PaulWhite Capito, e ricordo questo in discussione privata anni fa. Data la natura condivisa dei domini app (e quindi degli staticoggetti) in SQL Server, sì, esiste il rischio di condizioni di competizione. Questo è il motivo per cui ho stabilito per la prima volta dall'OP che questi dati sono minimi e stabili e che ho qualificato questo approccio come "rara modifica" e ho fornito un mezzo di aggiornamento quando necessario. In questo caso d'uso non vedo molto o nessun rischio. Ho scoperto un post anni fa sulla possibilità di aggiornare le raccolte di sola lettura come da progetto (in C #, nessuna discussione in merito a: SQLCLR). Proverò a trovarlo.
Solomon Rutzky,

2
Non c'è bisogno, non c'è modo di farmi sentire a mio agio con questo, a parte la documentazione ufficiale di SQL Server che dice che va bene, che sono abbastanza sicuro che non esiste.
Paul White 9
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.