2009-04-28 85 views
13

爲什麼標量值函數似乎會導致查詢的累積運行速度越慢,連續使用它們的次數越多?爲什麼SQL Server標量值函數變慢?

我有這個表是用從第三方購買的數據構建的。

我已經修剪了一些東西,使這篇文章更短...但只是讓你瞭解如何設置的東西。

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, 

然後我有兩個函數,查找LAT和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 

當我運行以下...

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毫秒,200〜= 32毫秒,400〜876 =毫秒

- 編輯 抱歉,我應該已經更清楚了。我不想調整上面列出的查詢。這只是一個示例,顯示執行時間越慢,記錄越多。在真實世界的應用程序中,這些函數被用作where子句的一部分來在城市和州周圍建立一個半徑來包含該地區的所有記錄。

+3

不要灑NOLOCK提示對於不需要它的樣本,NOLOCK的東西實際上與這個問題無關。 – 2009-04-29 02:04:39

+0

如果你不能擺脫「真正的查詢」中的功能,那麼它總是會很慢。舉一個更好的例子,在WHERE中使用函數,我們可以給你一些想法...... – 2009-04-29 15:53:50

回答

25

在大多數情況下,最好避免引用表的標量值函數,因爲(正如其他人所說的),它們基本上都是黑盒子,每個行都需要運行一次,並且不能通過查詢計劃引擎進行優化。因此,即使關聯表具有索引,它們也傾向於線性縮放。

您可能需要考慮使用內聯表值函數,因爲它們是與查詢內聯一起評估的,並且可以進行優化。你得到你想要的封裝,但是在select語句中正確粘貼表達式的性能。作爲內聯的副作用,它們不能包含任何程序代碼(不聲明@variable; set @variable = ..; return)。但是,它們可以返回多行和多列。

你可以重新寫你的函數是這樣的:

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

使用它們的語法也有一點不同:

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

雖然這是對OP性能問題的一個很好的解決方案,但它並沒有真正回答這個問題:「爲什麼要做標量函數非線性退化?「 (你甚至在你的回答中說:「他們傾向於線性縮放」)只是問,因爲我看到了與OP相同的行爲,並且非常好奇它爲什麼是非線性的。 – tbone 2015-04-09 23:17:40

+0

@tbone,這個問題從來沒有提到他們非線性退化。它們應該根據要返回的行數進行線性縮放,因爲它們將在每行中運行一次。請參閱sam saffron的回答,以查看它們線性縮放的示例。 – 2015-04-10 15:04:46

+0

他發佈的統計數據顯示非線性:100〜= 8 ms,200〜= 32 ms,400〜= 876 ms – tbone 2015-04-11 18:33:41

2

您可以爲結果集中的每一行調用該函數兩次(兩個選擇到數據庫的命中)。

,讓您更快的查詢加盟權GIS_Location和跳過功能:

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) 

我不知道爲什麼NOLOCK,或瘋狂的where子句中,我只是從問題複製...

+0

數據幾乎沒有變化,所以nolock表提示減少了執行時間,因爲它不需要發出共享鎖。瘋狂的where子句只是對x個記錄進行採樣,所以我可以顯示它越慢,記錄越多。這只是一個示例,而不是真實世界的應用程序。在真正的一箇中,我沒有加入另一張桌子的奢望,因爲我正在處理的是一面旗幟廣泛的非規範化遺留表格。 – DBAndrew 2009-04-28 22:29:03

+0

@DBAndrew,如果你不能擺脫「真正的查詢」中的功能,那麼它總是會很慢。舉一個更好的例子,在WHERE中使用函數,我們可以給你一些想法...... – 2009-04-29 13:09:37

0

簡而言之,因爲帶有用戶定義函數的SQL表達式比沒有它們的SQL表達式效率低。執行邏輯不能被優化;並且每行都必須產生函數開銷(包括調用協議)。

KMike的建議很好。 WHERE .. IN(SELECT something)不可能是一個有效的模式,在這種情況下可以很容易地用JOIN替換。

0

看看這個效果是否更好...或者是一個獨特的內部連接?

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) 

至於標量函數的性能,我不確定。

6

他們沒有。

標量函數中沒有導致其性能按指數級降級的錯誤,具體取決於標量函數執行的行數。再次嘗試您的測試並查看SQL分析器,查看CPU和READS以及DURATION列。增加測試大小以包含花費時間超過一秒,兩秒,五秒的測試。

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 

輸出

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. 

請記住,如果你對結果集行運行一個標量函數,標量函數將每行沒有全局優化執行。

相關問題