WHERE yan tümcesinde değişkenlerin kullanılması nasıl önlenir


16

Aşağıdaki gibi (basitleştirilmiş) saklı bir prosedür verildiğinde:

CREATE PROCEDURE WeeklyProc(@endDate DATE)
AS
BEGIN
  DECLARE @startDate DATE = DATEADD(DAY, -6, @endDate)
  SELECT
    -- Stuff
  FROM Sale
  WHERE SaleDate BETWEEN @startDate AND @endDate
END

Eğer Salemasa büyüktür SELECT, yürütme işleminin uzun zaman alabilir görünüşte çünkü iyileştirici olamaz optimize yerel değişkene bağlı. Parçanın SELECTdeğişken kodlarla çalıştırılmasını test ettik, sonra sabit kodlanmış tarihler ve yürütme süresi ~ 9 dakikadan ~ 1 saniyeye çıktı.

"Sabit" tarih aralıklarına (hafta, ay, 8 hafta vb.) Göre sorgulayan çok sayıda saklı yordamımız var, bu nedenle girdi parametresi yalnızca @ endDate ve @startDate yordam içinde hesaplanır.

Soru, optimize ediciden ödün vermemek için bir WHERE yan tümcesindeki değişkenlerden kaçınmanın en iyi yolu nedir?

Ortaya çıkardığımız olanaklar aşağıda gösterilmiştir. Bu en iyi uygulamalardan herhangi biri mi yoksa başka bir yolu var mı?

Değişkenleri parametrelere dönüştürmek için bir sarmalayıcı yordamı kullanın.

Parametreler, optimizasyonu yerel değişkenlerin yaptığı gibi etkilemez.

CREATE PROCEDURE WeeklyProc(@endDate DATE)
AS
BEGIN
   DECLARE @startDate DATE = DATEADD(DAY, -6, @endDate)
   EXECUTE DateRangeProc @startDate, @endDate
END

CREATE PROCEDURE DateRangeProc(@startDate DATE, @endDate DATE)
AS
BEGIN
  SELECT
    -- Stuff
  FROM Sale
  WHERE SaleDate BETWEEN @startDate AND @endDate
END

Parametreli dinamik SQL kullanın.

CREATE PROCEDURE WeeklyProc(@endDate DATE)
AS
BEGIN
  DECLARE @startDate DATE = DATEADD(DAY, -6, @endDate)
  DECLARE @sql NVARCHAR(4000) = N'
    SELECT
      -- Stuff
    FROM Sale
    WHERE SaleDate BETWEEN @startDate AND @endDate
  '
  DECLARE @param NVARCHAR(4000) = N'@startDate DATE, @endDate DATE'
  EXECUTE sp_executesql @sql, @param, @startDate = @startDate, @endDate = @endDate
END

"Sabit kodlanmış" dinamik SQL kullanın.

CREATE PROCEDURE WeeklyProc(@endDate DATE)
AS
BEGIN
  DECLARE @startDate DATE = DATEADD(DAY, -6, @endDate)
  DECLARE @sql NVARCHAR(4000) = N'
    SELECT
      -- Stuff
    FROM Sale
    WHERE SaleDate BETWEEN @startDate AND @endDate
  '
  SET @sql = REPLACE(@sql, '@startDate', CONVERT(NCHAR(10), @startDate, 126))
  SET @sql = REPLACE(@sql, '@endDate', CONVERT(NCHAR(10), @endDate, 126))
  EXECUTE sp_executesql @sql
END

Kullan DATEADD()İşlevi doğrudan .

Buna hevesli değilim çünkü WHERE'deki çağrı işlevleri de performansı etkiler.

CREATE PROCEDURE WeeklyProc(@endDate DATE)
AS
BEGIN
  SELECT
    -- Stuff
  FROM Sale
  WHERE SaleDate BETWEEN DATEADD(DAY, -6, @endDate) AND @endDate
END

İsteğe bağlı bir parametre kullanın.

Parametrelere atamanın değişkenlere atama ile aynı soruna sahip olup olmadığından emin değilim, bu yüzden bu bir seçenek olmayabilir. Bu çözümü gerçekten sevmiyorum ama eksiksizlik için dahil ediyorum.

CREATE PROCEDURE WeeklyProc(@endDate DATE, @startDate DATE = NULL)
AS
BEGIN
  SET @startDate = DATEADD(DAY, -6, @endDate)
  SELECT
    -- Stuff
  FROM Sale
  WHERE SaleDate BETWEEN @startDate AND @endDate
END

-- Güncelleme --

Öneri ve yorumlarınız için teşekkürler. Onları okuduktan sonra çeşitli zamanlamalarla bazı zamanlama testleri yaptım. Sonuçları buraya referans olarak ekliyorum.

Run 1'in planı yok. Çalışma 2, Çalışma 1'den hemen sonra tam olarak aynı parametrelere sahip olduğundan, çalışma 1'deki planı kullanacaktır.

NoProc süreleri, saklı yordam dışında SSMS'de SELECT sorgularını el ile çalıştırmak içindir.

TestProc1-7 orijinal sorudan sorgular.

TestProcA-B, Mikael Eriksson'un önerisine dayanmaktadır . Veritabanındaki sütun bir DATE olduğunu, bu yüzden parametre bir DATETIME olarak geçen ve örtük döküm (testProcA) ve açık döküm (testProcB) ile çalışan denedim.

TestProcC-D, Kenneth Fisher'ın önerisine dayanmaktadır. . Zaten başka şeyler için tarih arama tablosu kullanıyoruz, ancak her dönem aralığı için belirli bir sütuna sahip bir tablo yok. Denediğim varyasyon hala BETWEEN kullanıyor ancak daha küçük arama tablosunda yapıyor ve daha büyük tabloya katılıyor. Belirli arama tablolarını kullanıp kullanamayacağımız hakkında daha fazla araştırma yapacağım, ancak dönemlerimiz sabit olmasına rağmen birkaç farklı tablo var.

    Satış tablosundaki toplam satır sayısı: 136.424.366

                       Çalışma 1 (ms) Çalışma 2 (ms)
    CPU Geçen İşlem CPU Geçen Yorum
    NoProc sabitleri 6567 62199 2870 719 Sabitlerle manuel sorgu
    NoProc değişkenleri 9314 62424 3993 998 Değişkenlerle manuel sorgu
    testProc1 6801 62919 2871 736 Sert kodlanmış aralık
    testProc2 8955 63190 3915 979 Parametre ve değişken aralık
    testProc3 8985 63152 3932 987 Parametre aralığı ile sarma prosedürü
    testProc4 9142 63939 3931 977 Parametreli dinamik SQL
    testProc5 7269 62933 2933 728 Sabit kodlanmış dinamik SQL
    testProc6 9266 63421 3915 984 DATE tarihinde DATEADD kullanın
    testProc7 2044 13950 1092 1087 Kukla parametre
    testProcA 12120 61493 5491 1875 DATEADD değerini DATETIME tarihinde CAST olmadan kullanın
    testProcB 8612 61949 3932 978 DATEADD tarihinde DATETIME tarihinde CAST ile kullanın
    testProcC 8861 61651 3917 993 Arama tablosunu kullanın, Önce satış
    testProcD 8625 61740 3994 1031 Arama tablosu kullan, Son satış

İşte test kodu.

------ SETUP ------

IF OBJECT_ID(N'testDimDate', N'U') IS NOT NULL DROP TABLE testDimDate
IF OBJECT_ID(N'testProc1', N'P') IS NOT NULL DROP PROCEDURE testProc1
IF OBJECT_ID(N'testProc2', N'P') IS NOT NULL DROP PROCEDURE testProc2
IF OBJECT_ID(N'testProc3', N'P') IS NOT NULL DROP PROCEDURE testProc3
IF OBJECT_ID(N'testProc3a', N'P') IS NOT NULL DROP PROCEDURE testProc3a
IF OBJECT_ID(N'testProc4', N'P') IS NOT NULL DROP PROCEDURE testProc4
IF OBJECT_ID(N'testProc5', N'P') IS NOT NULL DROP PROCEDURE testProc5
IF OBJECT_ID(N'testProc6', N'P') IS NOT NULL DROP PROCEDURE testProc6
IF OBJECT_ID(N'testProc7', N'P') IS NOT NULL DROP PROCEDURE testProc7
IF OBJECT_ID(N'testProcA', N'P') IS NOT NULL DROP PROCEDURE testProcA
IF OBJECT_ID(N'testProcB', N'P') IS NOT NULL DROP PROCEDURE testProcB
IF OBJECT_ID(N'testProcC', N'P') IS NOT NULL DROP PROCEDURE testProcC
IF OBJECT_ID(N'testProcD', N'P') IS NOT NULL DROP PROCEDURE testProcD
GO

CREATE TABLE testDimDate
(
   DateKey DATE NOT NULL,
   CONSTRAINT PK_DimDate_DateKey UNIQUE NONCLUSTERED (DateKey ASC)
)
GO

DECLARE @dateTimeStart DATETIME = '2000-01-01'
DECLARE @dateTimeEnd DATETIME = '2100-01-01'
;WITH CTE AS
(
   --Anchor member defined
   SELECT @dateTimeStart FullDate
   UNION ALL
   --Recursive member defined referencing CTE
   SELECT FullDate + 1 FROM CTE WHERE FullDate + 1 <= @dateTimeEnd
)
SELECT
   CAST(FullDate AS DATE) AS DateKey
INTO #DimDate
FROM CTE
OPTION (MAXRECURSION 0)

INSERT INTO testDimDate (DateKey)
SELECT DateKey FROM #DimDate ORDER BY DateKey ASC

DROP TABLE #DimDate
GO

-- Hard coded date range.
CREATE PROCEDURE testProc1 AS
BEGIN
   SET NOCOUNT ON
   SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN '2012-12-09' AND '2012-12-10'
END
GO

-- Parameter and variable date range.
CREATE PROCEDURE testProc2(@endDate DATE) AS
BEGIN
   SET NOCOUNT ON
   DECLARE @startDate DATE = DATEADD(DAY, -1, @endDate)
   SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN @startDate AND @endDate
END
GO

-- Parameter date range.
CREATE PROCEDURE testProc3a(@startDate DATE, @endDate DATE) AS
BEGIN
   SET NOCOUNT ON
   SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN @startDate AND @endDate
END
GO

-- Wrapper procedure.
CREATE PROCEDURE testProc3(@endDate DATE) AS
BEGIN
   SET NOCOUNT ON
   DECLARE @startDate DATE = DATEADD(DAY, -1, @endDate)
   EXEC testProc3a @startDate, @endDate
END
GO

-- Parameterized dynamic SQL.
CREATE PROCEDURE testProc4(@endDate DATE) AS
BEGIN
   SET NOCOUNT ON
   DECLARE @startDate DATE = DATEADD(DAY, -1, @endDate)
   DECLARE @sql NVARCHAR(4000) = N'SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN @startDate AND @endDate'
   DECLARE @param NVARCHAR(4000) = N'@startDate DATE, @endDate DATE'
   EXEC sp_executesql @sql, @param, @startDate = @startDate, @endDate = @endDate
END
GO

-- Hard coded dynamic SQL.
CREATE PROCEDURE testProc5(@endDate DATE) AS
BEGIN
   SET NOCOUNT ON
   DECLARE @startDate DATE = DATEADD(DAY, -1, @endDate)
   DECLARE @sql NVARCHAR(4000) = N'SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN ''@startDate'' AND ''@endDate'''
   SET @sql = REPLACE(@sql, '@startDate', CONVERT(NCHAR(10), @startDate, 126))
   SET @sql = REPLACE(@sql, '@endDate', CONVERT(NCHAR(10), @endDate, 126))
   EXEC sp_executesql @sql
END
GO

-- Explicitly use DATEADD on a DATE.
CREATE PROCEDURE testProc6(@endDate DATE) AS
BEGIN
   SET NOCOUNT ON
   SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN DATEADD(DAY, -1, @endDate) AND @endDate
END
GO

-- Dummy parameter.
CREATE PROCEDURE testProc7(@endDate DATE, @startDate DATE = NULL) AS
BEGIN
   SET NOCOUNT ON
   SET @startDate = DATEADD(DAY, -1, @endDate)
   SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN @startDate AND @endDate
END
GO

-- Explicitly use DATEADD on a DATETIME with implicit CAST for comparison with SaleDate.
-- Based on the answer from Mikael Eriksson.
CREATE PROCEDURE testProcA(@endDateTime DATETIME) AS
BEGIN
   SET NOCOUNT ON
   SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN DATEADD(DAY, -1, @endDateTime) AND @endDateTime
END
GO

-- Explicitly use DATEADD on a DATETIME but CAST to DATE for comparison with SaleDate.
-- Based on the answer from Mikael Eriksson.
CREATE PROCEDURE testProcB(@endDateTime DATETIME) AS
BEGIN
   SET NOCOUNT ON
   SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN CAST(DATEADD(DAY, -1, @endDateTime) AS DATE) AND CAST(@endDateTime AS DATE)
END
GO

-- Use a date lookup table, Sale first.
-- Based on the answer from Kenneth Fisher.
CREATE PROCEDURE testProcC(@endDate DATE) AS
BEGIN
   SET NOCOUNT ON
   DECLARE @startDate DATE = DATEADD(DAY, -1, @endDate)
   SELECT SUM(Value) FROM Sale J INNER JOIN testDimDate D ON D.DateKey = J.SaleDate WHERE D.DateKey BETWEEN @startDate AND @endDate
END
GO

-- Use a date lookup table, Sale last.
-- Based on the answer from Kenneth Fisher.
CREATE PROCEDURE testProcD(@endDate DATE) AS
BEGIN
   SET NOCOUNT ON
   DECLARE @startDate DATE = DATEADD(DAY, -1, @endDate)
   SELECT SUM(Value) FROM testDimDate D INNER JOIN Sale J ON J.SaleDate = D.DateKey WHERE D.DateKey BETWEEN @startDate AND @endDate
END
GO

------ TEST ------

SET STATISTICS TIME OFF

DECLARE @endDate DATE = '2012-12-10'
DECLARE @startDate DATE = DATEADD(DAY, -1, @endDate)

DBCC FREEPROCCACHE WITH NO_INFOMSGS
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS

RAISERROR('Run 1: NoProc with constants', 0, 0) WITH NOWAIT
SET STATISTICS TIME ON
SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN '2012-12-09' AND '2012-12-10'
SET STATISTICS TIME OFF

RAISERROR('Run 2: NoProc with constants', 0, 0) WITH NOWAIT
SET STATISTICS TIME ON
SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN '2012-12-09' AND '2012-12-10'
SET STATISTICS TIME OFF

DBCC FREEPROCCACHE WITH NO_INFOMSGS
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS

RAISERROR('Run 1: NoProc with variables', 0, 0) WITH NOWAIT
SET STATISTICS TIME ON
SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN @startDate AND @endDate
SET STATISTICS TIME OFF

RAISERROR('Run 2: NoProc with variables', 0, 0) WITH NOWAIT
SET STATISTICS TIME ON
SELECT SUM(Value) FROM Sale WHERE SaleDate BETWEEN @startDate AND @endDate
SET STATISTICS TIME OFF

DECLARE @sql NVARCHAR(4000)

DECLARE _cursor CURSOR LOCAL FAST_FORWARD FOR
   SELECT
      procedures.name,
      procedures.object_id
   FROM sys.procedures
   WHERE procedures.name LIKE 'testProc_'
   ORDER BY procedures.name ASC

OPEN _cursor

DECLARE @name SYSNAME
DECLARE @object_id INT

FETCH NEXT FROM _cursor INTO @name, @object_id
WHILE @@FETCH_STATUS = 0
BEGIN
   SET @sql = CASE (SELECT COUNT(*) FROM sys.parameters WHERE object_id = @object_id)
      WHEN 0 THEN @name
      WHEN 1 THEN @name + ' ''@endDate'''
      WHEN 2 THEN @name + ' ''@startDate'', ''@endDate'''
   END

   SET @sql = REPLACE(@sql, '@name', @name)
   SET @sql = REPLACE(@sql, '@startDate', CONVERT(NVARCHAR(10), @startDate, 126))
   SET @sql = REPLACE(@sql, '@endDate', CONVERT(NVARCHAR(10), @endDate, 126))

   DBCC FREEPROCCACHE WITH NO_INFOMSGS
   DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS

   RAISERROR('Run 1: %s', 0, 0, @sql) WITH NOWAIT
   SET STATISTICS TIME ON
   EXEC sp_executesql @sql
   SET STATISTICS TIME OFF

   RAISERROR('Run 2: %s', 0, 0, @sql) WITH NOWAIT
   SET STATISTICS TIME ON
   EXEC sp_executesql @sql
   SET STATISTICS TIME OFF

   FETCH NEXT FROM _cursor INTO @name, @object_id
END

CLOSE _cursor
DEALLOCATE _cursor

Yanıtlar:


9

Parametre kokusu neredeyse her zaman arkadaşınızdır ve sorgularınızı kullanabilmeniz için yazmalısınız. Parametre koklaması, sorgu derlendiğinde mevcut parametre değerlerini kullanarak planın sizin için oluşturulmasına yardımcı olur. Parametre koklamanın karanlık tarafı, sorgu derlenirken kullanılan değerlerin sorguların gelmesi için uygun olmadığı zamandır.

Saklı yordamdaki sorgu, SQL Server'ın burada ele alması gereken değerler için sorgu yürütüldüğünde değil, saklı yordam yürütüldüğünde derlenir ...

CREATE PROCEDURE WeeklyProc(@endDate DATE)
AS
BEGIN
  DECLARE @startDate DATE = DATEADD(DAY, -6, @endDate)
  SELECT
    -- Stuff
  FROM Sale
  WHERE SaleDate BETWEEN @startDate AND @endDate
END

için bilinen bir değer @endDateve için bilinmeyen bir değerdir @startDate. Bu, SQL Server'ın, filtre için döndürülen satırların% 30'unu @startDate, istatistiklerin söylediği her şeyle birlikte tahmin etmeye bırakacaktır @endDate. Bir arama işleminden en fazla yararlanabileceğiniz bir tarama işlemi verebilecek çok sayıda satır içeren büyük bir tablonuz varsa.

Sarıcı yordam çözümünüz SQL Server'ın DateRangeProcderlendiğinde değerleri görmesini ve böylece hem @endDateve hem de bilinen değerleri kullanabilmesini sağlar @startDate.

Her iki dinamik sorgunuz da aynı şeye yol açar, değerler derleme zamanında bilinir.

Varsayılan null değerine sahip olanı biraz özeldir. SQL Server tarafından derleme zamanında bilinen değerler, @endDateve nulliçin bilinen bir değerdir @startDate. Bir arada kullanmak null0 satır verir, ancak SQL Server bu durumlarda her zaman 1 değerini tahmin eder. Bu durumda bu iyi bir şey olabilir, ancak saklı yordamı, taramanın en iyi seçim olacağı geniş bir tarih aralığıyla çağırırsanız, bir sürü arama yapabilirsiniz.

"DATEADD () işlevini doğrudan kullan" ı bu cevabın sonuna bıraktım çünkü kullanacağım ve onunla garip bir şey de var.

İlk olarak, SQL Server işlevi nerede deyiminde kullanıldığında işlevi birden çok kez çağırmaz. DATEADD, çalışma zamanı sabiti olarak kabul edilir .

Ve ben DATEADDsorgu derlendi zaman değerlendirilir düşünüyorum böylece döndürülen satır sayısı hakkında iyi bir tahmin olsun. Ama bu durumda öyle değil.
SQL Server, ne yaptığınıza bakılmaksızın DATEADD(SQL Server 2012'de test edilmiştir) parametredeki değeri temel alarak tahmin eder; bu durumda, tahminde kayıtlı olan satır sayısı olacaktır @endDate. Neden bilmiyorum ama veri tipinin kullanımı ile ilgili DATE. Shift DATETIMEsaklı yordam ve tabloda ve tahmin yani doğru olacaktır DATEADDderlenmesi sırasında kabul edilir DATETIMEdeğil DATE.

Bu oldukça uzun cevabı özetlemek için sargı prosedürü çözümünü tavsiye ederim. SQL Server'ın, sorguyu derlerken dinamik SQL kullanma zahmetine girmeden sağlanan değerleri kullanmasına her zaman izin verir.

Not:

Yorumlarda iki öneriniz var.

OPTION (OPTIMIZE FOR UNKNOWN)döndürülen satırların% 9'unu tahmin OPTION (RECOMPILE)eder ve sorgu her seferinde yeniden derlendiğinden SQL Server'ın parametre değerlerini görmesini sağlar.


3

Tamam, senin için iki olası çözümüm var.

Öncelikle bunun artan parametreleştirmeye izin verip vermeyeceğini merak ediyorum. Test etme şansım olmadı ama işe yarayabilir.

CREATE PROCEDURE WeeklyProc(@endDate DATE, @startDate DATE)
AS
BEGIN
  IF @startDate IS NULL
    SET @startDate = DATEADD(DAY, -6, @endDate)
  SELECT
    -- Stuff
  FROM Sale
  WHERE SaleDate BETWEEN @startDate AND @endDate
END

Diğer seçenek, sabit zaman çerçeveleri kullandığınız gerçeğinden yararlanır. Önce bir DateLookup tablosu oluşturun. Böyle bir şey

CurrentDate    8WeekStartDate    8WeekEndDate    etc

Şimdi ve gelecek yüzyıl arasındaki her tarih için doldurun. Bu sadece ~ 36500 satır yani oldukça küçük bir tablo. Ardından sorgunuzu böyle değiştirin

IF @Range = '8WeekRange' 
    SELECT
      -- Stuff
    FROM Sale
    JOIN DateLookup
        ON SaleDate BETWEEN [8WeekStartDate] AND [8WeekEndDate]
    WHERE DateLookup.CurrentDate = GetDate()

Açıkçası bu sadece bir örnek ve kesinlikle daha iyi yazılabilirdi ama bu tür bir masada çok şansım vardı. Özellikle statik bir tablo olduğu ve deli gibi dizine alınabildiğinden.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.