Czy SQL Server buforuje obliczone wartości w zapytaniu?

10

Za każdym razem, gdy napotykam na tego typu zapytania, zawsze zastanawiam się, jak by to zadziałał SQL Server. Jeśli uruchomię dowolny typ zapytania, który wymaga obliczenia, a następnie użyję tej wartości w wielu miejscach, na przykład w selecti order by, czy SQL Server obliczy ją dwukrotnie dla każdego wiersza, czy będzie buforowana? Ponadto, jak to działa z funkcjami zdefiniowanymi przez użytkownika?

Przykłady:

SELECT CompanyId, Count(*)
FROM Sales
ORDER BY Count(*) desc

SELECT Geom.BufferWithTolerance(@radius, 0.01, 0).STEnvelope().STPointN(1).STX, Geom.BufferWithTolerance(@radius, 0.01, 0).STEnvelope().STPointN(1).STY
FROM Table

SELECT Id, udf.MyFunction(Id)
FROM Table
ORDER BY udf.MyFunction(Id)

Czy istnieje sposób, aby uczynić go bardziej wydajnym, czy też SQL Server jest wystarczająco inteligentny, aby go obsłużyć?

Jonas Stawski
źródło
„to zależy” to jedna wystawa rextester.com/DXOB90032
Martin Smith
Które możesz porównać z rextester.com/ARSO25902
Martin Smith
@MartinSmith nie używasz funkcji niedeterministycznej? Jeśli tak, to spodziewałbym się, że SQL wykona go dwukrotnie.
Jonas Stawski
zawsze jest wyjątek! Możesz spróbować SELECT RAND() FROM Sales order by RAND()- jest to oceniane tylko raz, ponieważ jest zarówno niedeterministyczne, jak i stała czasowa.
Martin Smith

Odpowiedzi:

11

Optymalizator zapytań SQL Server może łączyć powtarzane obliczone wartości w jednym operatorze obliczania skalarnego. To, czy to zrobi, zależy od kosztu planu zapytania i właściwości obliczonej wartości. Zgodnie z oczekiwaniami nie zrobi tego w przypadku wartości obliczeniowych, które nie są deterministyczne, z kilkoma wyjątkami takimi jak RAND(). Nie zrobi tego również w przypadku funkcji zdefiniowanych przez użytkownika.

Zacznę od przykładu funkcji zdefiniowanej przez użytkownika. Oto doskonały przykład funkcji zdefiniowanej przez użytkownika:

CREATE OR ALTER FUNCTION dbo.NULL_FUNCTION (@N BIGINT) RETURNS BIGINT
WITH SCHEMABINDING
AS
BEGIN
RETURN NULL;
END;

Chcę również utworzyć tabelę i umieścić w niej 100 wierszy:

CREATE TABLE X_100 (N BIGINT NOT NULL);

WITH
L0   AS(SELECT 1 AS c UNION ALL SELECT 1),
L1   AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
L2   AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
L3   AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
L4   AS(SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
L5   AS(SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM L5)
INSERT INTO X_100 WITH (TABLOCK)
SELECT n
FROM Nums WHERE n <= 100;

dbo.NULL_FUNCTIONFunkcja jest determistic. Ile razy będzie wykonywany dla następującego zapytania?

SELECT n, dbo.NULL_FUNCTION(n)
FROM X_100;

Na podstawie planu zapytań zostanie to wykonane raz dla każdego wiersza lub 100 razy:

plan zapytań 1

SQL Server 2016 wprowadził DMV sys.dm_exec_function_stats . Możemy zrobić migawki tego DMV, aby zobaczyć, ile razy UDF jest wykonywane przez zapytanie.

SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('NULL_FUNCTION');

Wynikiem tego jest 100, więc funkcja została wykonana 100 razy.

Spróbujmy innego prostego zapytania:

SELECT n, dbo.NULL_FUNCTION(n), dbo.NULL_FUNCTION(n) 
FROM X_100;

Plan zapytań sugeruje, że funkcja zostanie wykonana 200 razy:

plan zapytań 2

Wyniki sys.dm_exec_function_statssugerują, że funkcja została wykonana 200 razy.

Pamiętaj, że nie zawsze możesz użyć planu zapytań, aby dowiedzieć się, ile razy skalar obliczeniowy jest wykonywany. Poniższy cytat pochodzi z „ Obliczenia skalarów, wyrażeń i wydajności planu wykonania ”:

To powoduje, że ludzie myślą, że Compal Scalar zachowuje się jak większość innych operatorów: gdy przepływają przez niego rzędy, wyniki wszystkich obliczeń zawartych w Compal Scalar są dodawane do strumienia. Zasadniczo nie jest to prawdą. Mimo nazwy Compal Scalar nie zawsze niczego oblicza i nie zawsze zawiera pojedynczą wartość skalarną (może to być na przykład wektor, alias, a nawet predykat logiczny). Skalar obliczeniowy najczęściej definiuje wyrażenie; faktyczne obliczenia są odraczane do momentu, gdy coś później w planie wykonania będzie wymagało wyniku.

Spróbujmy innego przykładu. W przypadku następującego zapytania mam nadzieję, że UDF jest obliczany jeden raz:

WITH NULL_FUNCTION_CTE (NULL_VALUE) AS
(
SELECT DISTINCT dbo.NULL_FUNCTION(0)
)
SELECT n , cte.NULL_VALUE
FROM X_100
CROSS JOIN NULL_FUNCTION_CTE cte;

Plan zapytań sugeruje, że zostanie obliczony jeden raz:

plan zapytań

Jednak DMV ujawnia prawdę. Skalar obliczeniowy jest odraczany, dopóki nie jest potrzebny, czyli w operatorze łączenia. Jest oceniany 100 razy.

Zapytałeś również, co możesz zrobić, aby zachęcić optymalizator do unikania wielokrotnego ponownego obliczania tego samego wyrażenia. Najlepszą rzeczą, jaką możesz zrobić, to uniknąć skalarnego UDF w kodzie. Mają one szereg problemów z wydajnością poza tym pytaniem, w tym nadmuchiwanie przydziałów pamięci, zmuszanie do uruchomienia całego zapytania MAXDOP 1, złe oszacowanie liczności i prowadzące do dodatkowego wykorzystania procesora. Jeśli potrzebujesz użyć UDF, a wartość tego UDF jest stała, możesz obliczyć go poza zapytaniem i umieścić w zmiennej lokalnej.

W przypadku zapytań bez UDF można spróbować uniknąć pisania wyrażeń, które zwracają ten sam wynik, ale nie są wpisywane dokładnie w ten sam sposób. W następnym przykładzie korzystam z publicznie dostępnej bazy danych AdventureworksDW2016CTP3, ale tak naprawdę każda baza danych będzie wystarczająca. Ile razy zostanie COUNT(*)obliczone dla tego zapytania?

SELECT OrderDateKey, COUNT(*) 
FROM dbo.FactResellerSales
GROUP BY OrderDateKey
ORDER BY COUNT(*) DESC;

W przypadku tego zapytania możemy to zrozumieć, patrząc na operator dopasowania (agregacji) mieszania.

dopasowanie skrótu

COUNT(*)Jest obliczana raz dla każdej unikatowej wartości OrderDateKey. Dołączenie ORDER BYklauzuli nie powoduje dwukrotnego obliczenia. Można zobaczyć plan wykonania tutaj .

Teraz rozważ zapytanie, które zwróci dokładnie te same wyniki, ale zostało napisane w inny sposób:

SELECT OrderDateKey, SUM(1)
FROM dbo.FactResellerSales
GROUP BY OrderDateKey
ORDER BY COUNT(*) DESC;

Optymalizator zapytań nie jest wystarczająco inteligentny, aby je połączyć, więc zostaną wykonane dodatkowe prace:

dopasowanie mieszania 2

Joe Obbish
źródło