2009-04-28 8 views
13

Warum scheinen Scalar-bewertete Funktionen dazu zu führen, dass Abfragen kumulativ langsamer ausgeführt werden, je öfter sie nacheinander verwendet werden?Warum werden Skalarwertfunktionen von SQL Server langsamer?

Ich habe diese Tabelle, die mit Daten von einer Drittpartei erworben wurde.

Ich habe einige Sachen zurechtgeschnitten, um diesen Beitrag kürzer zu machen ... aber nur damit Sie die Idee bekommen, wie die Dinge aufgebaut sind.

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, 

Dann habe ich zwei Funktionen, die LAT und LON nachschlagen.

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 

Wenn ich laufen die folgenden ...

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 Leider sollte ich waren klarer. Ich möchte die oben aufgelistete Abfrage nicht optimieren. Dies ist nur ein Beispiel, um zu zeigen, dass die Ausführungszeit umso langsamer wird, je mehr Datensätze durchgebrochen werden. In der realen Weltanwendung werden die Funktionen als Teil einer where-Klausel verwendet, um einen Radius um eine Stadt und einen Bundesstaat herum zu erstellen, um alle Datensätze mit in diese Region aufzunehmen.

+3

nicht die NOLOCK Hinweise Lassen bestreuen auf Samples, die es in SO nicht brauchen, hat das NOLOCK-Zeug wirklich nichts mit dieser Frage zu tun. –

+0

Wenn Sie die Funktionen in der "echten Abfrage" nicht loswerden können, wird es immer sehr langsam sein. Geben Sie ein besseres Beispiel, mit den Funktionen, die in der WHERE verwendet werden, und wir können Ihnen dazu Ideen geben ... –

Antwort

25

In den meisten Fällen ist es am besten, skalarwertige Funktionen zu vermeiden, die Tabellen referenzieren, weil sie (wie andere bereits sagten) im Grunde schwarze Felder sind, die einmal für jede Zeile ausgeführt werden müssen und nicht von der Abfrageplan-Engine optimiert werden können. Daher tendieren sie dazu, linear zu skalieren, selbst wenn die zugeordneten Tabellen Indizes haben.

Möglicherweise möchten Sie eine Inline-Tabellenwertfunktion verwenden, da sie inline mit der Abfrage ausgewertet und optimiert werden können. Sie erhalten die gewünschte Verkapselung, aber die Leistung, die Ausdrücke direkt in die select-Anweisung einzufügen.

Als Nebeneffekt von Inlined können sie keinen prozeduralen Code enthalten (no declare @variable; set @variable = ..; return). Sie können jedoch mehrere Zeilen und Spalten zurückgeben.

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 
); 

Die Syntax, sie zu verwenden, ist auch ein wenig anders:

Sie könnten Ihre Funktionen so etwas wie dieses neu schreiben

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

Dies ist zwar eine gute * Lösung * für das Leistungsproblem von OP, aber es beantwortet nicht wirklich die Frage: "WARUM Skalarfunktionen verschlechtern sich nichtlinear? " (Sie haben sogar in Ihrer Antwort gesagt: "Sie tendieren dazu, linear zu skalieren"). Ich frage nur, weil ich das gleiche Verhalten wie OP sehe und extrem neugierig bin, WARUM es nicht linear ist. – tbone

+0

@Tbone, die Frage nie erwähnt, dass sie nichtlinear degradiert. Sie sollten linear im Verhältnis zur Anzahl der zurückgegebenen Zeilen skaliert werden, da sie einmal pro Zeile ausgeführt werden. Siehe Sam Safrans Antwort, um ein Beispiel zu sehen, wie sie linear skalieren. –

+0

Die von ihm geposteten Statistiken zeigen nichtlinear: 100 ~ = 8 ms, 200 ~ = 32 ms, 400 ~ = 876 ms – tbone

2

Sie rufen die Funktion zweimal (zwei Treffer für den DB) für jede Zeile in der Ergebnismenge auf.

Ihre Anfrage schneller machen verbinden Recht auf GIS_Location und die Funktionen überspringen:

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) 

Ich bin mir nicht sicher, warum die NOLOCK oder die verrückt where-Klausel, die ich von der Frage nur kopiert ...

+0

Die Daten ändern sich kaum, so dass der Hinweis auf die nolock-Tabelle die Ausführungszeit reduziert, da keine gemeinsamen Sperren ausgegeben werden müssen. Die crazy where-Klausel dient nur dazu, x-Datensätze zu samplen, so dass ich es immer langsamer und langsamer anzeigen konnte, je mehr Datensätze es durchmachte. Dies ist nur ein Beispiel und nicht die reale Anwendung. In der echten habe ich nicht den Luxus, mich an einen anderen Tisch zu setzen, da der, mit dem ich es zu tun habe, ein flaggenormalisierter Tisch ist. – DBAndrew

+0

@DBAndrew, wenn Sie die Funktionen in der "echten Abfrage" nicht loswerden können, dann wird es immer wirklich langsam sein. Geben Sie ein besseres Beispiel, mit den Funktionen, die in der WHERE verwendet werden, und wir können Ihnen dazu Ideen geben ... –

0

Einfach gesagt, weil SQL-Ausdrücke mit benutzerdefinierten Funktionen weniger effizient als SQL-Ausdrücke ohne sie sind. Die Ausführungslogik kann nicht optimiert werden. und der Funktionsaufwand (einschließlich Aufrufprotokollen) muss für jede Zeile anfallen.

KMike's Ratschläge sind gut. WHERE .. IN (SELECT something) ist wahrscheinlich kein effizientes Muster und kann in diesem Fall leicht durch einen JOIN ersetzt werden.

0

Sehen Sie, ob das besser funktioniert ... Oder vielleicht eine deutliche innere Verbindung?

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) 

Wie für die Skalarfunktion Leistung, bin ich mir nicht sicher.

6

Sie nicht.

Es gibt keinen Fehler in Skalarfunktionen, der zu einer exponentiellen Verschlechterung der Leistung führt, abhängig von der Anzahl der Zeilen, in denen die Skalarfunktion ausgeführt wird. Versuchen Sie Ihre Tests erneut und werfen Sie einen Blick auf den SQL Profiler. Schauen Sie sich die Spalten CPU und READS und DURATION an. Erhöhen Sie die Testgröße um Tests, die länger als eine Sekunde, zwei Sekunden oder fünf Sekunden dauern.

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 

Ausgabe

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. 

Beachten Sie, dass, wenn Sie eine Skalarfunktion gegen Zeilen in der Ergebnismenge ausgeführt wird, wird die skalare Funktion ohne globale Optimierung pro-Zeile ausgeführt werden.

Verwandte Themen