Próbuję sprawdzić, czy istnieje sposób, aby oszukać SQL Server, aby używał określonego planu dla zapytania.
1. Środowisko
Wyobraź sobie, że masz jakieś dane, które są współużytkowane przez różne procesy. Załóżmy, że mamy wyniki eksperymentów, które zajmują dużo miejsca. Następnie dla każdego procesu wiemy, który rok / miesiąc wyniku eksperymentu chcemy zastosować.
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
Teraz dla każdego procesu mamy parametry zapisane w tabeli
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. Dane testowe
Dodajmy dane testowe:
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. Pobieranie wyników
Teraz bardzo łatwo jest uzyskać wyniki eksperymentu poprzez @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
Plan jest ładny i równoległy:
select
calculated_number,
count(*)
from dbo.f_GetSharedData(2014, 4)
group by
calculated_number
zapytanie 0 plan
4. Problem
Ale aby korzystanie z danych było nieco bardziej ogólne, chcę mieć inną funkcję - dbo.f_GetSharedDataBySession(@session_id int)
. Tak więc najprostszym sposobem byłoby utworzenie funkcji skalarnych, tłumacząc @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
A teraz możemy stworzyć naszą funkcję:
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
zapytanie 1 plan
Plan jest taki sam, ale oczywiście nie jest równoległy, ponieważ funkcje skalarne wykonujące dostęp do danych sprawiają, że cały plan jest szeregowy .
Wypróbowałem więc kilka różnych podejść, na przykład używając podkwerend zamiast funkcji skalarnych:
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
zapytanie 2 plan
Lub używając 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
plan zapytania 3
Ale nie mogę znaleźć sposobu, aby napisać to zapytanie tak dobre, jak to przy użyciu funkcji skalarnych.
Kilka myśli:
- Zasadniczo chciałbym móc w jakiś sposób powiedzieć SQL Serverowi, aby wstępnie obliczył pewne wartości, a następnie przekazał je dalej jako stałe.
- Pomocne może być, gdybyśmy mieli pośrednią wskazówkę dotyczącą materializacji . Sprawdziłem kilka wariantów (TVF z wieloma instrukcjami lub Cte z górą), ale żaden plan nie jest tak dobry jak ten z funkcjami skalarnymi
- Wiem o nadchodzącej poprawie SQL Server 2017 - Froid: Optymalizacja programów imperatywnych w relacyjnej bazie danych. Nie jestem jednak pewien, czy to pomoże. Fajnie byłoby jednak udowodnić, że się tutaj mylą.
Dodatkowe informacje
Korzystam z funkcji (zamiast wybierać dane bezpośrednio z tabel), ponieważ jest o wiele łatwiejsze w użyciu w wielu różnych zapytaniach, które zwykle mają @session_id
jako parametr.
Poproszono mnie o porównanie faktycznych czasów wykonania. W tym konkretnym przypadku
- zapytanie 0 działa przez ~ 500ms
- zapytanie 1 działa przez ~ 1500ms
- zapytanie 2 działa przez ~ 1500ms
- zapytanie 3 działa przez ~ 2000ms.
Plan nr 2 ma skanowanie indeksu zamiast wyszukiwania, które jest następnie filtrowane według predykatów zagnieżdżonych pętli. Plan nr 3 nie jest taki zły, ale nadal działa więcej i działa wolniej niż plan nr 0.
Załóżmy, że dbo.Params
zmienia się to rzadko i zwykle ma około 1–200 wierszy, nie więcej niż, powiedzmy, że 2000 się kiedykolwiek spodziewamy. Teraz jest około 10 kolumn i nie spodziewam się, aby dodawać kolumnę zbyt często.
Liczba wierszy w Params nie jest stała, więc dla każdego @session_id
będzie wiersz. Liczba kolumn, które nie zostały naprawione, jest to jeden z powodów, dla których nie chcę dzwonić dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
zewsząd, dlatego mogę wewnętrznie dodać nową kolumnę do tego zapytania. Z przyjemnością usłyszę wszelkie opinie / sugestie na ten temat, nawet jeśli ma pewne ograniczenia.
źródło
Odpowiedzi:
Nie można naprawdę bezpiecznie osiągnąć dokładnie tego, co chcesz dzisiaj w SQL Server, tj. W pojedynczej instrukcji i przy równoległym wykonywaniu, w ramach ograniczeń określonych w pytaniu (tak jak je postrzegam).
Więc moja prosta odpowiedź brzmi: nie . Reszta tej odpowiedzi to głównie dyskusja, dlaczego tak jest, jeśli jest to interesujące.
Możliwe jest uzyskanie równoległego planu, jak zauważono w pytaniu, ale istnieją dwie główne odmiany, z których żadna nie jest odpowiednia dla twoich potrzeb:
Skorelowane zagnieżdżone pętle łączą się z okrągłym robinem, który rozprowadza strumienie na najwyższym poziomie. Biorąc pod uwagę, że gwarantowany jest pojedynczy wiersz
Params
dla określonejsession_id
wartości, wewnętrzna strona będzie działać na jednym wątku, nawet jeśli jest oznaczona ikoną równoległości. Właśnie dlatego pozornie równoległy plan 3 nie działa tak dobrze; w rzeczywistości jest to serial.Inną alternatywą jest niezależna równoległość po wewnętrznej stronie połączenia zagnieżdżonych pętli. Niezależny tutaj oznacza, że wątki są uruchamiane po wewnętrznej stronie, a nie tylko te same wątki, które wykonują zewnętrzną stronę zagnieżdżonych pętli. SQL Server obsługuje niezależne równoległości zagnieżdżonych pętli wewnętrznych tylko wtedy, gdy gwarantowany jest jeden rząd strony zewnętrznej i nie ma skorelowanych parametrów łączenia ( plan 2 ).
Mamy więc wybór planu równoległego, który jest szeregowy (z powodu jednego wątku) z pożądanymi skorelowanymi wartościami; lub plan równoległy po wewnętrznej stronie, który musi skanować, ponieważ nie ma parametrów do wyszukania. (Poza tym: naprawdę należy zezwolić na sterowanie równoległością od strony wewnętrznej przy użyciu dokładnie jednego zestawu skorelowanych parametrów, ale nigdy nie zostało to wdrożone, prawdopodobnie z ważnego powodu).
Naturalne pytanie brzmi zatem: dlaczego w ogóle potrzebujemy skorelowanych parametrów? Dlaczego SQL Server nie może po prostu szukać bezpośrednio wartości skalarnych dostarczanych np. Przez podzapytanie?
Cóż, SQL Server może „indeksować wyszukiwanie” tylko przy użyciu prostych odniesień skalarnych, np. Stałych, zmiennych, kolumn lub odwołań do wyrażeń (więc wynik funkcji skalarnej może się również kwalifikować). Podzapytanie (lub inna podobna konstrukcja) jest po prostu zbyt skomplikowane (i potencjalnie niebezpieczne), aby wcisnąć je do całego silnika pamięci masowej. Tak więc wymagane są osobne operatory planu zapytań. To z kolei wymaga korelacji, co oznacza brak równoległości, jakiej byś chciał.
Podsumowując, naprawdę nie ma obecnie lepszego rozwiązania niż metody takie jak przypisywanie wartości odnośników do zmiennych, a następnie używanie ich w parametrach funkcji w osobnej instrukcji.
Teraz możesz mieć specyficzne uwarunkowania lokalne, co oznacza, że
SESSION_CONTEXT
warto buforować bieżące wartości roku i miesiąca, np .:Ale należy to do kategorii obejścia.
Z drugiej strony, jeśli wydajność agregacji ma podstawowe znaczenie, można rozważyć zastosowanie funkcji wbudowanych i utworzenie indeksu magazynu kolumn (podstawowego lub dodatkowego) w tabeli. Może się okazać, że zalety przechowywania magazynu kolumn, przetwarzania w trybie wsadowym i przesuwania agregacji zapewniają większe korzyści niż w przypadku wyszukiwania równoległego w trybie wierszowym.
Ale uważaj na skalarne funkcje T-SQL, szczególnie w przypadku magazynu magazynu kolumn, ponieważ łatwo jest skończyć z ocenianą funkcją dla każdego wiersza w osobnym filtrze w trybie wiersza. Generalnie dość trudne jest zagwarantowanie, ile razy SQL Server wybierze ocenę skalarów i lepiej nie próbować.
źródło
session_context
ale zdecydowałem, że to dla mnie trochę zbyt szalony pomysł i nie jestem pewien, jak będzie pasować do mojej obecnej architektury. Przydałaby się jednak pewna wskazówka, której mógłbym użyć, aby poinformować optymalizatora, że powinien traktować wynik podzapytania jak zwykłe odwołanie skalarne.O ile wiem, pożądany kształt planu nie jest możliwy tylko za pomocą T-SQL. Wygląda na to, że chcesz, aby oryginalny kształt planu (plan kwerendy 0) z podzapytaniami z twoich funkcji był stosowany jako filtry bezpośrednio względem skanowania indeksu klastrowanego. Nigdy nie otrzymasz takiego planu zapytań, jeśli nie użyjesz zmiennych lokalnych do przechowywania wartości zwracanych przez funkcje skalarne. Filtrowanie zostanie zamiast tego zaimplementowane jako zagnieżdżone połączenie pętli. Istnieją trzy różne sposoby (z punktu widzenia paralelizmu), w których można zastosować łączenie w pętli:
To jedyne możliwe kształty planu, o których wiem. Możesz uzyskać inne, jeśli używasz tabeli tymczasowej, ale żadna z nich nie rozwiązuje podstawowego problemu, jeśli chcesz, aby wydajność zapytania była tak dobra, jak w przypadku zapytania 0.
Można osiągnąć równoważną wydajność zapytania, używając skalarnych funkcji UDF do przypisywania zwracanych wartości do zmiennych lokalnych i używając tych zmiennych lokalnych w zapytaniu. Możesz owinąć ten kod w procedurę składowaną lub w UDF z wieloma instrukcjami, aby uniknąć problemów z utrzymaniem. Na przykład:
Skalarne funkcje UDF zostały przeniesione poza zapytanie, które ma kwalifikować się do równoległości. Wydaje mi się, że otrzymuję plan zapytań:
Oba podejścia mają wady, jeśli trzeba użyć tego zestawu wyników w innych zapytaniach. Nie można bezpośrednio dołączyć do procedury składowanej. Będziesz musiał zapisać wyniki do tabeli tymczasowej, która ma własny zestaw problemów. Możesz dołączyć do MS-TVF, ale w SQL Server 2016 mogą wystąpić problemy z oszacowaniem liczności. SQL Server 2017 oferuje przeplatane wykonywanie MS-TVF, co może całkowicie rozwiązać problem.
Aby wyjaśnić kilka rzeczy: Skalarne UDF T-SQL zawsze zabraniają równoległości, a Microsoft nie powiedział, że FROID będzie dostępny w SQL Server 2017.
źródło
Najprawdopodobniej można to zrobić za pomocą SQLCLR. Jedną z zalet skalarnych UDF SQLCLR jest to, że nie zapobiegają one równoległości, jeśli nie mają dostępu do danych (a czasami muszą być również oznaczone jako „deterministyczne”). Jak więc wykorzystać coś, co nie wymaga dostępu do danych, gdy sama operacja wymaga dostępu do danych?
Ponieważ
dbo.Params
tabela ma:INT
kolumnymożliwe jest buforowanie trzech kolumn -
session_id, experiment_year int, experiment_month
- w zbiorczej kolekcji (np. Słowniku), która jest zapełniana poza procesem i odczytywana przez Skalarne UDF, które otrzymują wartościexperiment_year int
iexperiment_month
. Rozumiem przez „poza procesem”: możesz mieć całkowicie oddzielny Skalarny UDF SQLCLR lub Procedurę Składowaną, która może uzyskiwać dostęp do danych i odczyty zdbo.Params
tabeli w celu zapełnienia kolekcji statycznej. Ta funkcja UDF lub procedura przechowywana byłaby wykonana przed użyciem funkcji UDF, które otrzymują wartości „rok” i „miesiąc”, w ten sposób funkcje UDF, które otrzymują wartości „rok” i „miesiąc”, nie zapewniają dostępu do danych DB.Procedura UDF lub procedura zapisana, która odczytuje dane, może najpierw sprawdzić, czy kolekcja ma 0 wpisów, a jeśli tak, to zapełnij, w przeciwnym razie pomiń. Możesz nawet śledzić czas, w którym był wypełniany, a jeśli minął ponad X minut (lub coś w tym rodzaju), a następnie wyczyść i ponownie wypełnij, nawet jeśli w kolekcji znajdują się wpisy. Ale pomijanie populacji pomoże, ponieważ będzie musiała być często wykonywana, aby upewnić się, że zawsze jest zapełniana dla dwóch głównych UDF, z których można uzyskać wartości.
Głównym problemem jest, gdy SQL Server zdecyduje się zwolnić domenę aplikacji z dowolnego powodu (lub jest wyzwalany przez coś przy użyciu
DBCC FREESYSTEMCACHE('ALL');
). Nie chcesz ryzykować, że kolekcja zostanie wyczyszczona między wykonaniem funkcji „wypełniania” UDF lub procedury składowanej a UDF, aby uzyskać wartości „rok” i „miesiąc”. W takim przypadku można sprawdzić na samym początku tych dwóch UDF, aby zgłosić wyjątek, jeśli kolekcja jest pusta, ponieważ lepiej jest pomylić błędy, niż zapewnić fałszywe wyniki.Oczywiście wyżej wspomniane obawy zakładają, że pragnieniem jest, aby Zgromadzenie oznaczono jako
SAFE
. JeśliEXTERNAL_ACCESS
zestaw można oznaczyć jako , możliwe jest, aby konstruktor statyczny wykonał metodę, która odczytuje dane i zapełnia kolekcję, dzięki czemu wystarczy tylko ręcznie wykonać to, aby odświeżyć wiersze, ale zawsze będą one wypełnione (ponieważ konstruktor klasy statycznej zawsze działa, gdy klasa jest ładowana, co dzieje się za każdym razem, gdy metoda w tej klasie jest wykonywana po restarcie lub domenie App jest zwolnione). Wymaga to użycia zwykłego połączenia, a nie bezpośredniego połączenia kontekstowego (które nie jest dostępne dla konstruktorów statycznych, stąd potrzebaEXTERNAL_ACCESS
).Uwaga: aby nie wymagać oznaczania zestawu jako
UNSAFE
, należy oznaczyć dowolne zmienne klasy statycznej jakoreadonly
. Oznacza to przynajmniej kolekcję. Nie stanowi to problemu, ponieważ w kolekcjach tylko do odczytu można dodawać lub usuwać z nich elementy, po prostu nie można ich zainicjować poza konstruktorem lub początkowym ładowaniem. Śledzenie czasu ładowania kolekcji w celu jej wygaśnięcia po X minutach jest trudniejsze, ponieważstatic readonly DateTime
zmiennej klasy nie można zmienić poza konstruktorem lub ładowaniem początkowym. Aby obejść to ograniczenie, musisz użyć statycznej kolekcji tylko do odczytu, która zawiera pojedynczy element oDateTime
wartości umożliwiającej usunięcie i ponowne dodanie podczas odświeżania.źródło
readonly statics
jest bezpieczne lub mądre w SQLCLR. Tym bardziej nie jestem przekonany, by oszukać system, czyniącreadonly
go typem odniesienia, który następnie zmieniacie . Daje mi absolutną wolę.static
obiektów) w SQL Server, tak, istnieje ryzyko warunków wyścigu. Właśnie dlatego po raz pierwszy ustaliłem z PO, że te dane są minimalne i stabilne, i dlatego zakwalifikowałem to podejście jako wymagające „rzadko zmieniających się” i dałem możliwość odświeżenia w razie potrzeby. W tym przypadku użycia nie widzę zbyt dużego ryzyka. Znalazłem post wiele lat temu na temat możliwości aktualizacji kolekcji tylko do odczytu jako zaprojektowanych (w C #, brak dyskusji na temat: SQLCLR). Spróbuję to znaleźć.