Hiçbir satır döndürmeyen sorguya ORDER BY eklenmesi performansı önemli ölçüde etkiler


15

Basit üç tablo birleşimi göz önüne alındığında, ORDER BY dahil edildiğinde, hiçbir satır döndürülmemiş olsa bile sorgu performansı önemli ölçüde değişir. Gerçek sorun senaryosu sıfır satır döndürmek için 30 saniye sürer, ancak ORDER BY dahil edilmediğinde anında olur. Neden?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

Ben bigtable.smallGuidId bir dizin olabilir anlıyorum, ama, aslında bu durumda daha da kötüleştireceğine inanıyorum.

İşte test için tabloları oluşturmak / doldurmak için komut dosyası. İlginçtir ki, smalltable'ın bir nvarchar (max) alanına sahip olduğu önemli gibi görünüyor. Ayrıca bigtable üzerinde bir rehber ile katılmamın önemli olduğu anlaşılıyor (sanırım hash eşleşmesini kullanmak istiyor).

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

SQL 2005, 2008 ve 2008R2'de aynı sonuçları test ettim.

Yanıtlar:


32

Martin Smith'in cevabına katılıyorum, ancak sorun sadece istatistiklerden biri değil. ForeignId sütununun istatistikleri (otomatik istatistiklerin etkin olduğu varsayılarak), 3 değeri için hiç satır bulunmadığını doğru bir şekilde gösterir (7 değeri olan yalnızca bir tane vardır):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

istatistik çıktısı

SQL Server, istatistiklerin alınmasından bu yana bazı şeylerin değişmiş olabileceğini bilir, bu nedenle plan yürütüldüğünde değer 3 için bir satır olabilir . Ek olarak, plan derleme ve yürütme arasında herhangi bir süre geçebilir (sonuçta planlar yeniden kullanılmak üzere önbelleğe alınır). Martin'in dediği gibi, SQL Server, önbellek planını en iyi duruma getirme nedenleriyle yeniden derlemek için yeterli değişikliklerin ne zaman yapıldığını tespit etmek için mantık içerir.

Ancak bunların hiçbiri önemli değil. Bir kenar durumu istisnasıyla, iyileştirici hiçbir zaman bir tablo işlemi tarafından üretilen satır sayısını sıfır olarak tahmin etmez. Çıktının her zaman sıfır satır olması gerektiğini statik olarak belirleyebilirse, işlem gereksizdir ve tamamen kaldırılır.

Optimize edicinin modeli bunun yerine en az bir satır tahmin eder . Bu buluşsal yöntemi kullanmak, daha düşük bir tahmin mümkün olduğunda ortalamadan daha iyi planlar üretme eğilimindedir. Bir aşamada sıfır satır tahmini üreten bir plan, işleme akışındaki o noktadan itibaren işe yaramaz olacaktır, çünkü maliyete dayalı kararlar vermenin bir temeli olmayacaktır (sıfır satır ne olursa olsun sıfır satırdır). Tahminin yanlış olduğu ortaya çıkarsa, sıfır satır tahmininin üstündeki plan şekli, makul olma şansının neredeyse hiç olmadığını gösterir.

İkinci faktör, Sınırlama Varsayımı adı verilen başka bir modelleme varsayımıdır. Bu aslında, bir sorgu bir dizi değeri başka bir değer aralığıyla birleştirirse, bunun nedeninin aralıkların çakışması olduğu anlamına gelir. Bunu koymanın bir başka yolu, satırların döndürülmesi beklenen birleştirme işleminin belirtildiğini söylemektir. Bu gerekçe olmadan, maliyetler genellikle hafife alınacaktır ve bu da çok çeşitli ortak sorgular için zayıf planlara yol açacaktır.

Esasen, burada sahip olduğunuz şey, optimize edicinin modeline uymayan bir sorgudur. Çok sütunlu veya filtrelenmiş dizinlerle tahminleri 'iyileştirmek' için yapabileceğimiz hiçbir şey yoktur; burada 1 satırdan daha düşük bir tahmin almanın bir yolu yoktur. Gerçek bir veritabanı, bu durumun ortaya çıkamamasını sağlamak için yabancı anahtarlara sahip olabilir, ancak burada geçerli olmadığı varsayılarak, model dışı koşulu düzeltmek için ipuçlarını kullanmamız gerekir. Herhangi bir sayıda farklı ipucu yaklaşımı bu sorgu ile çalışacaktır. OPTION (FORCE ORDER)sorgusu yazılı olarak iyi sonuç verir.


21

Buradaki temel sorun istatistiklerden biridir.

Her iki sorgu için de tahmin edilen satır sayısı, finalin gerçekte gerçekleşen 0 yerine SELECT1.048.580 satır (var olduğu tahmin edilen aynı sayıda satır) döndüreceğine inandığını gösterir bigtable.

Her iki JOINkoşulunuz da eşleşir ve tüm satırları korur. Tek satırın yüklemle tinytableeşleşmediği için ortadan kaldırılırlar t.foreignId=3.

Eğer koşarsan

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

ve öyle satır tahmini sayısı bakmak 1yerine 0ve planın boyunca bu hata yayılırken. tinytableşu anda 1 satır içeriyor. 500 satır değişikliği yapılana kadar istatistikler bu tablo için yeniden derlenmeyecek, böylece eşleşen bir satır eklenebilecek ve yeniden derleme tetiklenmeyecektir.

Cümle eklediğinizde ORDER BYve bir varchar(max)sütun bulunduğunda Birleştirme Sırasının değişmesinin smalltablenedeni, varchar(max)sütunların satır boyutunu ortalama 4.000 bayt artıracağını tahmin etmesidir. Bunu 1048580 satır ile çarpın ve sıralama işleminin tahmini 4GB'ye ihtiyacı olacağı anlamına gelir, böylece SORTişlemi daha önce yapmaya karar verir JOIN.

Aşağıdaki ipuçlarını kullanarak ORDER BYsorguyu ORDER BYkatılmama stratejisini benimsemeye zorlayabilirsiniz .

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

Plan, tahmini alt ağaç maliyeti neredeyse 12,000ve hatalı tahmini satır sayısı ve tahmini veri boyutu olan bir sıralama operatörünü gösterir .

Plan

BTW UNIQUEIDENTIFIERTestlerimde sütunları tamsayı olanlarla değiştirdiğim şeyleri bulamadım .


2

Yürütme Planını Göster düğmesini açtığınızda neler olduğunu görebilirsiniz. "Yavaş" sorgusu için plan: resim açıklamasını buraya girin

Ve işte "hızlı" sorgu: resim açıklamasını buraya girin

Şuna bak - birlikte koş, ilk sorgu ~ 33x daha "pahalı" (97: 3 oranı). SQL, BigTable'ı tarih saatine göre sıralamak için ilk sorguyu optimize eder, ardından SmallTable ve TinyTable üzerinde küçük bir "arama" döngüsü çalıştırır ve her birini 1 milyon kez yürütür (daha fazla istatistik elde etmek için "Kümelenmiş Dizin Araması" simgesinin üzerine gelebilirsiniz). Bu nedenle, sıralama (% 27) ve 2 x 1 milyon küçük masalarda (% 23 ve% 46) "araştırıyor", pahalı sorgunun büyük kısmıdır. Buna karşılık, ORDER BYsorgu olmayan toplam 3 tarama gerçekleştirir.

Temel olarak, senaryo için SQL optimizer mantığında bir delik buldunuz. Ancak, TysHTTP tarafından belirtildiği gibi, bir dizin eklerseniz (eklentinizi yavaşlatır / bazılarını günceller), taramanız çok hızlı olur.


2

Ne oluyor SQL kısıtlamadan önce siparişi çalıştırmaya karar veriyor.

Bunu dene:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

Bu, aslında başka bir dizin eklemekten performans isabetine girmeden gelişmiş performansı (döndürülen sonuç sayısının çok küçük olduğu durumlarda) verir. SQL optimizer birleştirmeden önce siparişi gerçekleştirmeye karar verdiğinde tuhaf olsa da, aslında dönüş verileri varsa, birleştirmeden sonra sıralama yapmadan sıralamadan daha uzun sürecektir.

Son olarak, aşağıdaki komut dosyasını çalıştırmayı deneyin ve ardından güncellenmiş istatistiklerin ve dizinlerin yaşadığınız sorunu çözüp çözmediğine bakın:

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

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.