2009-04-28 14 views
13

Dlaczego funkcje wycenione według skali Scalara wydają się powodować, że zapytania będą kumulatywnie wolniejsze, im więcej razy z rzędu, że są używane?Dlaczego funkcje SQL Server Scalar są wolniejsze?

Mam tę tabelę, która została zbudowana na podstawie danych zakupionych od firmy zewnętrznej.

Usunąłem kilka rzeczy, aby ten post był krótszy ... ale tylko po to, aby zorientować się, jak wszystko jest skonfigurowane.

CREATE TABLE [dbo].[GIS_Location](
     [ID] [int] IDENTITY(1,1) NOT NULL, --PK 
     [Lat] [int] NOT NULL, 
     [Lon] [int] NOT NULL, 
     [Postal_Code] [varchar](7) NOT NULL, 
     [State] [char](2) NOT NULL, 
     [City] [varchar](30) NOT NULL, 
     [Country] [char](3) NOT NULL, 

CREATE TABLE [dbo].[Address_Location](
    [ID] [int] IDENTITY(1,1) NOT NULL, --PK 
    [Address_Type_ID] [int] NULL, 
    [Location] [varchar](100) NOT NULL, 
    [State] [char](2) NOT NULL, 
    [City] [varchar](30) NOT NULL, 
    [Postal_Code] [varchar](10) NOT NULL, 
    [Postal_Extension] [varchar](10) NULL, 
    [Country_Code] [varchar](10) NULL, 

Następnie mam dwie funkcje, które wyglądają LAT i LON.

CREATE FUNCTION [dbo].[usf_GIS_GET_LAT] 
(
    @City VARCHAR(30), 
    @State CHAR(2) 
) 
RETURNS INT 
WITH EXECUTE AS CALLER 
AS 
BEGIN 
    DECLARE @LAT INT 

    SET @LAT = (SELECT TOP 1 LAT FROM GIS_Location WITH(NOLOCK) WHERE [State] = @State AND [City] = @City) 

RETURN @LAT 
END 


CREATE FUNCTION [dbo].[usf_GIS_GET_LON] 
(
    @City VARCHAR(30), 
    @State CHAR(2) 
) 
RETURNS INT 
WITH EXECUTE AS CALLER 
AS 
BEGIN 
    DECLARE @LON INT 

    SET @LON = (SELECT TOP 1 LON FROM GIS_Location WITH(NOLOCK) WHERE [State] = @State AND [City] = @City) 

RETURN @LON 
END 

Gdy uruchomię następujących ...

SET STATISTICS TIME ON 

SELECT 
    dbo.usf_GIS_GET_LAT(City,[State]) AS Lat, 
    dbo.usf_GIS_GET_LON(City,[State]) AS Lon 
FROM 
    Address_Location WITH(NOLOCK) 
WHERE 
    ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC) 

SET STATISTICS TIME OFF 

100 ~ = 8 ms, 200 ~ = 32 ms, 400 ~ = 876 ms

--edit Niestety powinienem były bardziej wyraźne. Nie chcę dostroić powyższego zapytania. To tylko próbka, która pokazuje, że czas wykonywania jest coraz wolniejszy, im więcej rekordów przechwytuje. W aplikacji świata rzeczywistego funkcje są używane jako część klauzuli where do budowania promienia wokół miasta i stanu w celu uwzględnienia wszystkich rekordów w tym regionie.

+3

Niech nie posypać podpowiedzi nolock na próbkach, które nie potrzebują go w SO, rzeczy NOLOCK naprawdę nie mają nic wspólnego z tym pytaniem. –

+0

, jeśli nie możesz pozbyć się funkcji w "prawdziwym zapytaniu", to zawsze będzie naprawdę wolno. Daj lepszy przykład, używając funkcji w WHERE i możemy dać ci pomysły na ten temat ... –

Odpowiedz

25

W większości przypadków najlepiej unikać funkcji o wartościach skalarnych, które odwołują się do tabel, ponieważ (jak mówili inni) są to w zasadzie czarne pola, które należy uruchomić raz dla każdego wiersza i nie mogą być zoptymalizowane przez silnik planu zapytań. Dlatego mają tendencję do skalowania liniowego, nawet jeśli powiązane tabele mają indeksy.

Być może warto rozważyć użycie funkcji wbudowanej w tabelę wartości, ponieważ są one oceniane w linii z zapytaniem i mogą być zoptymalizowane. Otrzymasz enkapsulację, którą chcesz, ale wydajność wklejania wyrażeń bezpośrednio w instrukcji select.

Jako efekt uboczny bycia podkreślanym, nie mogą zawierać żadnego kodu proceduralnego (nie zadeklaruj @ zmiennej, ustaw @ zmiennej = ..; powrotu). Mogą jednak zwrócić kilka wierszy i kolumn.

Można by ponownie napisać twoje funkcje coś takiego:

create function usf_GIS_GET_LAT(
    @City varchar (30), 
    @State char (2) 
) 
returns table 
as return (
    select top 1 lat 
    from GIS_Location with (nolock) 
    where [State] = @State 
    and [City] = @City 
); 

GO 

create function usf_GIS_GET_LON (
    @City varchar (30), 
    @State char (2) 
) 
returns table 
as return (
    select top 1 LON 
    from GIS_Location with (nolock) 
    where [State] = @State 
    and [City] = @City 
); 

składnię do ich stosowania jest również nieco inna:

select 
    Lat.Lat, 
    Lon.Lon 
from 
    Address_Location with (nolock) 
    cross apply dbo.usf_GIS_GET_LAT(City,[State]) AS Lat 
    cross apply dbo.usf_GIS_GET_LON(City,[State]) AS Lon 
WHERE 
    ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC) 
+0

Chociaż jest to dobre * rozwiązanie * problemu z wydajnością OP, to tak naprawdę nie odpowiada na pytanie: "DLACZEGO funkcje skalarne rozkładają się nieliniowo? " (Powiedziałeś nawet w swojej odpowiedzi: "mają tendencję do skalowania liniowego") Po prostu pytają, ponieważ widzę to samo zachowanie co OP i jestem bardzo ciekawy, DLACZEGO jest to nieliniowe. – tbone

+0

@tbone, pytanie nigdy nie wspomniało o ich poniżającym nieliniowości. Powinny one być skalowane liniowo w stosunku do liczby zwracanych wierszy, ponieważ będą one uruchamiane raz na wiersz. Zobacz odpowiedź sam Szafran, aby zobaczyć ich przykład skalowania liniowego. –

+0

Opublikowane statystyki pokazują nieliniowość: 100 ~ = 8 ms, 200 ~ = 32 ms, 400 ~ = 876 ms – tbone

2

wywołujesz funkcję dwa razy (dwa wybrane trafienia do DB) dla każdego wiersza w zestawie wyników.

aby zapytanie szybciej przystąpić prawo GIS_Location i pominąć funkcje:

SELECT 
    g.Lat, 
    g.Lon 
FROM 
    Address_Location  l WITH(NOLOCK) 
    INNER JOIN GIS_Location g WITH(NOLOCK) WHERE l.State = g.State AND l.City = g.City 
WHERE 
    ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC) 

Nie jestem pewien, dlaczego NOLOCK lub szalony, gdzie klauzula, po prostu kopiowane z pytaniem ...

+0

Dane prawie się nie zmieniają, więc podpowiedź tabeli nolock skraca czas wykonania, ponieważ nie musi wydawać wspólnych blokad. Zwariowana klauzula where służy tylko do wypróbowania x rekordów, więc mogłem wyświetlać, że jest wolniejszy i wolniejszy, im więcej rekordów przechodzą. To tylko przykład, a nie prawdziwa aplikacja na świecie. W prawdziwym nie mam luksusu dołączania do innego stołu, ponieważ ten, z którym mam do czynienia, to flagowy, całkowicie zdenormalizowany dotychczasowy stół. – DBAndrew

+0

@DBAndrew, jeśli nie możesz pozbyć się funkcji w "prawdziwym zapytaniu", to zawsze będzie naprawdę wolno. Daj lepszy przykład, używając funkcji w WHERE i możemy dać ci pomysły na ten temat ... –

0

Po prostu, ponieważ wyrażenia SQL z funkcjami zdefiniowanymi przez użytkownika są mniej wydajne niż wyrażenia SQL bez nich. Logika wykonania nie może być zoptymalizowana; i narzut funkcji (w tym protokołów wywoływania) musi zostać poniesiony dla każdego wiersza.

Porady KMike są dobre. GDZIE ... IN (WYBIERZ Coś) nie jest najprawdopodobniej wydajnym wzorem, w tym przypadku można go łatwo zastąpić JOINem.

0

Sprawdź, czy to działa lepiej ... A może wyraźne sprzężenie wewnętrzne?

select a.*, 
(select top 1 g.Lat from GIS_Location g where g.City = a.City and g.State = a.State) as Lat, 
(select top 1 g.Lon from GIS_Location g where g.City = a.City and g.State = a.State) as Lon 
from Address_Location a 
where a.ID in (select top 100 ID from Address_Location order by ID desc) 

Jeśli chodzi o wydajność funkcji skalarnej, nie jestem pewien.

6

Nie robią tego.

Nie ma błędów w funkcjach skalarnych, które powodują wykładniczo degradację wydajności w zależności od liczby wierszy w funkcji skalarnej. Wypróbuj swoje testy ponownie i spójrz na profiler SQL, patrząc na kolumny CPU i READS i DURATION. Zwiększ rozmiar testu, aby uwzględnić testy trwające dłużej niż sekundę, dwie sekundy, pięć sekund.

CREATE FUNCTION dbo.slow 
(
    @ignore int 
) 
RETURNS INT 
AS 
BEGIN 
    DECLARE @slow INT 
    SET @slow = (select count(*) from sysobjects a 
     cross join sysobjects b 
     cross join sysobjects c 
     cross join sysobjects d 
     cross join sysobjects e 
     cross join sysobjects f 
    where a.id = @ignore) 

    RETURN @slow 
END 
go 
SET STATISTICS TIME ON 

select top 1 dbo.slow(id) 
from sysobjects 
go 
select top 5 dbo.slow(id) 
from sysobjects 
go 
select top 10 dbo.slow(id) 
from sysobjects 
go 
select top 20 dbo.slow(id) 
from sysobjects 
go 
select top 40 dbo.slow(id) 
from sysobjects 

SET STATISTICS TIME OFF 

Wyjście

SQL Server Execution Times: 
    CPU time = 203 ms, elapsed time = 202 ms. 


SQL Server Execution Times: 
    CPU time = 889 ms, elapsed time = 939 ms. 

SQL Server Execution Times: 
    CPU time = 1748 ms, elapsed time = 1855 ms. 

SQL Server Execution Times: 
    CPU time = 3541 ms, elapsed time = 3696 ms. 


SQL Server Execution Times: 
    CPU time = 7207 ms, elapsed time = 7392 ms. 

Należy pamiętać, że jeśli używasz funkcji skalarnej przeciwko wierszy w zestawie wyników, funkcja skalarna będą realizowane za wierszu bez optymalizacji globalnej.

Powiązane problemy