Ich versuche zu sehen, ob es eine Möglichkeit gibt, SQL Server dazu zu bringen, einen bestimmten Plan für die Abfrage zu verwenden.
1. Umwelt
Stellen Sie sich vor, Sie haben einige Daten, die von verschiedenen Prozessen gemeinsam genutzt werden. Nehmen wir also an, wir haben einige Versuchsergebnisse, die viel Platz beanspruchen. Dann wissen wir für jeden Prozess, welches Jahr / Monat des Versuchsergebnisses wir verwenden möchten.
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
Jetzt haben wir für jeden Prozess Parameter in der Tabelle gespeichert
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. Testdaten
Fügen wir einige Testdaten hinzu:
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. Ergebnisse abrufen
Nun ist es sehr einfach, Versuchsergebnisse zu erhalten, indem Sie @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
Der Plan ist schön und parallel:
select
calculated_number,
count(*)
from dbo.f_GetSharedData(2014, 4)
group by
calculated_number
Abfrage 0 Plan
4. Problem
Aber um die Nutzung der Daten etwas allgemeiner zu gestalten, möchte ich eine andere Funktion haben - dbo.f_GetSharedDataBySession(@session_id int)
. Ein einfacher Weg wäre also, skalare Funktionen zu erstellen, die übersetzen @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
Und jetzt können wir unsere Funktion erstellen:
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
Abfrage 1 Plan
Der Plan ist derselbe, außer dass er natürlich nicht parallel ist, da skalare Funktionen, die den Datenzugriff ausführen, den gesamten Plan seriell machen .
Daher habe ich verschiedene Ansätze ausprobiert, z. B. Unterabfragen anstelle von Skalarfunktionen zu verwenden:
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
Abfrage 2 Plan
Oder mit 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
Abfrage 3 Plan
Aber ich kann keinen Weg finden, diese Abfrage so gut zu schreiben wie die, die Skalarfunktionen verwendet.
Ein paar Gedanken:
- Grundsätzlich möchte ich, dass ich SQL Server irgendwie anweisen kann, bestimmte Werte vorab zu berechnen und sie dann als Konstanten weiterzugeben.
- Was hilfreich sein könnte, wäre, wenn wir einen Zwischen-Materialisierungs- Hinweis hätten. Ich habe einige Varianten geprüft (TVF mit mehreren Anweisungen oder cte mit top), aber bisher ist kein Plan so gut wie der mit skalaren Funktionen
- Ich weiß, dass die Verbesserung von SQL Server 2017 - Froid: Optimierung von Imperativen Programmen in einer relationalen Datenbank kommen wird. Ich bin mir jedoch nicht sicher, ob es helfen wird. Es wäre allerdings schön gewesen, hier das Gegenteil zu beweisen.
Zusätzliche Information
Ich benutze eine Funktion (anstatt Daten direkt aus den Tabellen auszuwählen), weil es viel einfacher ist, sie in vielen verschiedenen Abfragen zu verwenden, die normalerweise @session_id
als Parameter dienen.
Ich wurde gebeten, die tatsächlichen Ausführungszeiten zu vergleichen. In diesem speziellen Fall
- Abfrage 0 läuft für ~ 500ms
- Abfrage 1 läuft für ~ 1500ms
- Abfrage 2 läuft für ~ 1500ms
- Abfrage 3 läuft für ~ 2000ms.
Plan Nr. 2 hat einen Index-Scan anstelle eines Suchlaufs, der dann nach Prädikaten in verschachtelten Schleifen gefiltert wird. Plan Nr. 3 ist nicht so schlimm, leistet aber noch mehr Arbeit und arbeitet langsamer als Plan Nr. 0.
Nehmen wir an, dass dies dbo.Params
nur selten geändert wird und in der Regel 1-200 Zeilen, also nicht mehr als 2000, erwartet werden. Es sind jetzt ungefähr 10 Spalten und ich erwarte nicht, dass die Spalte zu oft hinzugefügt wird.
Die Anzahl der Zeilen in Params ist nicht festgelegt, sodass für jede @session_id
Zeile eine Zeile vorhanden ist. Die Anzahl der Spalten ist nicht festgelegt. dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
Dies ist einer der Gründe, warum ich nicht von überall aus anrufen möchte. Daher kann ich dieser Abfrage intern eine neue Spalte hinzufügen. Ich würde mich über Meinungen / Vorschläge zu diesem Thema freuen, auch wenn es einige Einschränkungen gibt.
quelle
Antworten:
Sie können nicht wirklich sicher erreichen, was Sie heute in SQL Server wollen, dh in einer einzigen Anweisung und bei paralleler Ausführung, innerhalb der in der Frage festgelegten Einschränkungen (wie ich sie wahrnehme).
Meine einfache Antwort lautet also nein . Der Rest dieser Antwort ist meist eine Diskussion darüber, warum das so ist, falls es von Interesse ist.
Es ist möglich, einen parallelen Plan zu erstellen, wie in der Frage angegeben. Es gibt jedoch zwei Hauptsorten, von denen keine für Ihre Anforderungen geeignet ist:
Eine korrelierte verschachtelte Schleife wird mit einem Round-Robin-Verteilungsstream auf der obersten Ebene verbunden. Da garantiert ist, dass eine einzelne Zeile
Params
für einen bestimmtensession_id
Wert stammt, wird die Innenseite auf einem einzelnen Thread ausgeführt, obwohl sie mit dem Parallelitätssymbol gekennzeichnet ist. Dies ist der Grund, warum der scheinbar parallele Plan 3 nicht so gut funktioniert. es ist in der Tat seriell.Die andere Alternative ist die unabhängige Parallelität auf der Innenseite der verschachtelten Schleifenverbindung. Unabhängig bedeutet hier, dass Threads auf der Innenseite gestartet werden und nicht nur die Threads, die auf der Außenseite der verschachtelten Schleifen ausgeführt werden. SQL Server unterstützt die Parallelität unabhängiger innerer geschachtelter Schleifen nur, wenn eine äußerliche Zeile garantiert ist und keine korrelierten Verknüpfungsparameter vorhanden sind ( Plan 2 ).
Wir haben also die Wahl zwischen einem seriellen Parallelplan (aufgrund eines Threads) mit den gewünschten korrelierten Werten. oder ein innerer paralleler Plan, der gescannt werden muss, weil er keine Parameter hat, mit denen gesucht werden kann. (Abgesehen davon: Es sollte eigentlich erlaubt sein, die innere Parallelität mit genau einem Satz korrelierter Parameter zu steuern, aber es wurde wahrscheinlich aus gutem Grund nie implementiert.)
Eine natürliche Frage lautet dann: Warum brauchen wir überhaupt korrelierte Parameter? Warum kann SQL Server nicht einfach direkt nach den Skalarwerten suchen, die z. B. von einer Unterabfrage bereitgestellt werden?
Nun, SQL Server kann nur mit einfachen Skalarreferenzen, z. B. einer Konstanten-, Variablen-, Spalten- oder Ausdrucksreferenz, nach Indizes suchen (daher kann sich auch ein Skalarfunktionsergebnis qualifizieren). Eine Unterabfrage (oder eine ähnliche Konstruktion) ist einfach zu komplex (und möglicherweise unsicher), um sie als Ganzes in die Speicher-Engine zu verschieben. Daher sind separate Abfrageplanoperatoren erforderlich. Dies erfordert wiederum Korrelation, was bedeutet, dass keine Parallelität der gewünschten Art vorliegt.
Alles in allem gibt es derzeit wirklich keine bessere Lösung als Methoden wie das Zuweisen der Nachschlagewerte zu Variablen und die anschließende Verwendung der Werte in den Funktionsparametern in einer separaten Anweisung.
Möglicherweise haben Sie jetzt bestimmte lokale Überlegungen, sodass sich das Zwischenspeichern der aktuellen Werte für Jahr und Monat
SESSION_CONTEXT
lohnt, z. B .:Dies fällt jedoch in die Kategorie der Problemumgehung.
Wenn andererseits die Aggregationsleistung von größter Bedeutung ist, können Sie die Inline-Funktionen beibehalten und einen Columnstore-Index (primär oder sekundär) für die Tabelle erstellen. Möglicherweise bieten die Vorteile des Speichers im Spaltenspeicher, der Stapelverarbeitung und des aggregierten Pushdown größere Vorteile als eine parallele Suche im Zeilenmodus.
Achten Sie jedoch auf skalare T-SQL-Funktionen, insbesondere beim Speichern von Spaltenspeichern, da die Funktion leicht zeilenweise in einem separaten Filter im Zeilenmodus ausgewertet wird. Es ist im Allgemeinen recht schwierig zu gewährleisten, wie oft SQL Server Skalare auswertet, und es besser nicht zu versuchen.
quelle
session_context
aber ich bin der Meinung , dass es eine etwas zu verrückte Idee für mich ist und ich bin mir nicht sicher, wie sie zu meiner aktuellen Architektur passen wird. Was jedoch nützlich wäre, könnte ein Hinweis sein, den ich verwenden könnte, um dem Optimierer mitzuteilen, dass das Ergebnis der Unterabfrage wie eine einfache Skalarreferenz behandelt werden soll.Soweit ich weiß, ist die gewünschte Planform nicht nur mit T-SQL möglich. Anscheinend möchten Sie die ursprüngliche Planform (Abfrage 0-Plan), wobei die Unterabfragen Ihrer Funktionen als Filter direkt auf den Clustered-Index-Scan angewendet werden. Sie werden niemals einen solchen Abfrageplan erhalten, wenn Sie keine lokalen Variablen verwenden, um die Rückgabewerte der Skalarfunktionen zu speichern. Die Filterung wird stattdessen als verschachtelter Loop-Join implementiert. Es gibt drei verschiedene Möglichkeiten (aus der Sicht der Parallelität), wie der Loop-Join implementiert werden kann:
Dies sind die einzigen möglichen Planformen, die mir bekannt sind. Wenn Sie eine temporäre Tabelle verwenden, können Sie einige andere abrufen, aber keine davon löst Ihr grundlegendes Problem, wenn die Abfrageleistung genauso gut sein soll wie für Abfrage 0.
Sie können eine äquivalente Abfrageleistung erzielen, indem Sie die skalaren UDFs verwenden, um lokalen Variablen Rückgabewerte zuzuweisen, und diese lokalen Variablen in Ihrer Abfrage verwenden. Sie können diesen Code in eine gespeicherte Prozedur oder eine UDF mit mehreren Anweisungen einschließen, um Wartbarkeitsprobleme zu vermeiden. Beispielsweise:
Die skalaren UDFs wurden außerhalb der Abfrage verschoben, die Sie für die Parallelisierung zulassen möchten. Der Abfrageplan, den ich erhalte, scheint der gewünschte zu sein:
Beide Ansätze haben Nachteile, wenn Sie diese Ergebnismenge in anderen Abfragen verwenden müssen. Sie können einer gespeicherten Prozedur nicht direkt beitreten. Sie müssten die Ergebnisse in einer temporären Tabelle speichern, die ihre eigenen Probleme hat. Sie können einer MS-TVF beitreten, in SQL Server 2016 können jedoch Probleme mit der Schätzung der Kardinalität auftreten. SQL Server 2017 bietet Interleaved-Ausführung für MS-TVF, wodurch das Problem vollständig gelöst werden könnte.
Nur um ein paar Dinge zu klären: T-SQL Scalar UDFs verbieten immer Parallelität und Microsoft hat nicht gesagt, dass FROID in SQL Server 2017 verfügbar sein wird.
quelle
Dies kann höchstwahrscheinlich mit SQLCLR erfolgen. Ein Vorteil von skalaren SQLCLR-UDFs besteht darin, dass sie keine Parallelität verhindern, wenn sie dies nicht tun alle Daten Zugriff tun (und manchmal müssen auch als „deterministisch “ gekennzeichnet werden). Wie können Sie also etwas nutzen, das keinen Datenzugriff erfordert, wenn der Vorgang selbst Datenzugriff erfordert?
Nun, denn es
dbo.Params
wird erwartet , dass der Tisch:INT
Spalten habenEs ist möglich, die drei Spalten -
session_id, experiment_year int, experiment_month
- in einer statischen Sammlung (z. B. einem Dictionary) zwischenzuspeichern, die außerhalb des Prozesses erstellt und von den Scalar-UDFs gelesen wird, die die Werteexperiment_year int
undexperiment_month
erhalten. Was ich mit "Out-of-Process" meine, ist: Sie können eine vollständig separate SQLCLR Scalar-UDF oder Stored Procedure verwenden, die den Datenzugriff und das Lesen aus derdbo.Params
Tabelle zum Auffüllen der statischen Auflistung ermöglicht. Diese UDF oder gespeicherte Prozedur wird ausgeführt, bevor die UDFs verwendet werden, die die Werte "Jahr" und "Monat" erhalten. Auf diese Weise führen die UDFs, die die Werte "Jahr" und "Monat" erhalten, keinen DB-Datenzugriff aus.Die UDF oder gespeicherte Prozedur, die die Daten liest, kann zuerst überprüfen, ob die Auflistung 0 Einträge enthält. Wenn dies der Fall ist, füllen Sie sie aus, andernfalls überspringen Sie sie. Sie können sogar den Zeitpunkt verfolgen, zu dem die Auflistung erstellt wurde. Wenn der Zeitraum länger als X Minuten (oder so ähnlich) war, können Sie die Auflistung auch dann löschen und neu auffüllen, wenn Einträge in der Auflistung vorhanden sind. Das Überspringen der Grundgesamtheit ist jedoch hilfreich, da sie häufig ausgeführt werden muss, um sicherzustellen, dass die beiden Haupt-UDFs immer mit Daten gefüllt sind, von denen die Werte abgerufen werden.
Die Hauptsorge ist, wenn SQL Server die App Domain aus irgendeinem Grund entlädt (oder wenn sie durch etwas ausgelöst wird, das verwendet)
DBCC FREESYSTEMCACHE('ALL');
). Sie möchten nicht riskieren, dass die Sammlung zwischen der Ausführung der UDF oder gespeicherten Prozedur "Auffüllen" und den UDFs gelöscht wird, um die Werte "Jahr" und "Monat" zu erhalten. In diesem Fall können Sie ganz am Anfang dieser beiden UDFs prüfen, ob eine Ausnahme ausgelöst wird, wenn die Auflistung leer ist, da es besser ist, einen Fehler zu machen, als falsche Ergebnisse zu liefern.Das oben erwähnte Anliegen geht natürlich davon aus, dass die Versammlung als "" gekennzeichnet werden soll
SAFE
. Wenn die Assembly als markiert werden kann, kannEXTERNAL_ACCESS
ein statischer Konstruktor die Methode ausführen, die die Daten liest und die Auflistung auffüllt, sodass Sie diese Methode immer nur manuell ausführen müssen, um die Zeilen zu aktualisieren. Sie werden jedoch immer aufgefüllt (weil der statische Klassenkonstruktor immer ausgeführt wird, wenn die Klasse geladen wird, was immer dann der Fall ist, wenn eine Methode in dieser Klasse nach einem Neustart ausgeführt wird oder die App-Domäne entladen wird). Dies erfordert die Verwendung einer regulären Verbindung und nicht der In-Process-Kontextverbindung (die statischen Konstruktoren nicht zur Verfügung steht, daher ist dies erforderlichEXTERNAL_ACCESS
).Bitte beachten Sie: Um die Assembly nicht als kennzeichnen zu müssen
UNSAFE
, müssen Sie alle statischen Klassenvariablen als kennzeichnenreadonly
. Dies bedeutet zumindest die Sammlung. Dies ist kein Problem, da schreibgeschützte Sammlungen Elemente hinzufügen oder daraus entfernen können. Sie können nur außerhalb des Konstruktors oder beim erstmaligen Laden nicht initialisiert werden. Das Verfolgen der Zeit, zu der die Auflistung geladen wurde, um sie nach X Minuten ablaufen zu lassen, ist schwieriger, da einestatic readonly DateTime
Klassenvariable nicht außerhalb des Konstruktors oder des ursprünglichen Ladevorgangs geändert werden kann. Um diese Einschränkung zu umgehen, müssen Sie eine statische schreibgeschützte Auflistung verwenden, die ein einzelnes Element enthält, das derDateTime
Wert ist, damit es bei einer Aktualisierung entfernt und erneut hinzugefügt werden kann.quelle
readonly statics
in der SQLCLR sicher oder sinnvoll ist. Viel weniger bin ich davon überzeugt, das System dann zum Narren zu halten, indem ich es zureadonly
einem Referenztyp mache , den Sie dann ändern . Gibt mir die absoluten Willen tbh.static
Objekten) in SQL Server besteht ein Risiko für die Race-Bedingungen. Aus diesem Grund habe ich aus dem OP heraus festgestellt, dass diese Daten minimal und stabil sind, und diesen Ansatz als "selten änderungsbedürftig" eingestuft und bei Bedarf ein Mittel zur Aktualisierung angegeben. In diesem Anwendungsfall sehe ich nicht viel, wenn überhaupt ein Risiko. Ich habe vor Jahren einen Beitrag über die Fähigkeit gefunden, schreibgeschützte Sammlungen als beabsichtigt zu aktualisieren (in C # keine Diskussion zu: SQLCLR). Ich werde versuchen, es zu finden.