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.