SQL Server'da, aşağıdaki durumda bir LOOP JOIN zorlamak gerekir?


15

Genellikle, tüm standart nedenlerle birleştirme ipuçlarını kullanmamanızı öneririm. Ancak son zamanlarda, neredeyse her zaman daha iyi performans için zorla döngü birleştirme bulduğum bir desen buldum. Aslında, o kadar çok kullanmaya ve tavsiye etmeye başlıyorum ki, bir şey eksik olmadığımdan emin olmak için ikinci bir görüş almak istedim. İşte temsili bir senaryo (bir örnek oluşturmak için çok özel kod sonunda):

--Case 1: NO HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
JOIN SampleTable AS S ON S.ID = D.ID

--Case 2: LOOP JOIN HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID

SampleTable'ın 1 milyon satırı vardır ve PK'sı ID'dir.
Sıcaklık tablosu # Sürücüde yalnızca bir sütun, ID, dizin yok ve 50K satır var.

Sürekli olarak bulduğum şey şu:

Durum 1:
SampleTable
Karma üzerinde İPUCU Dizin Taraması
Daha Yüksek Süreye Katıl (ortalama 333ms)
Daha yüksek CPU (ortalama 331ms)
Düşük Mantıksal Okumalar (4714)

Durum 2: LOOP BİRLEŞTİRME İPUCU
Dizini SampleTable
Döngüsüne Ara
Daha Düşük Süreye Katılın (ortalama 204ms,% 39 daha az)
Daha düşük CPU (ortalama 206,% 38 daha az)
Çok Daha Yüksek Mantıksal Okuma (160015, 34X daha fazla)

İlk başta, ikinci davanın çok daha yüksek okumaları beni korkuttu, çünkü okumaları düşürmek genellikle iyi bir performans ölçüsü olarak kabul edilir. Ama gerçekte ne olduğu hakkında ne kadar çok düşünürsem, beni ilgilendirmiyor. İşte benim düşüncem:

SampleTable yaklaşık 36MB alan 4714 sayfada yer almaktadır. Durum 1 hepsini tarar, bu yüzden 4714 okuma elde ederiz. Ayrıca, CPU yoğun olan ve sonuçta zamanı orantılı olarak artıran 1 milyon karma gerçekleştirmelidir. Vaka 1'de zamanı hızlandıran tüm bu karmadır.

Şimdi durum 2'yi düşünün. Herhangi bir karma işi yapmıyor, bunun yerine 50000 ayrı arama yapıyor, bu da okumaları artıran şey. Ancak okumalar karşılaştırmalı olarak ne kadar pahalıdır? Bunların fiziksel okumalar olması oldukça pahalı olabileceğini söyleyebiliriz. Ancak unutmayın 1) belirli bir sayfanın yalnızca ilk okunması fiziksel olabilir ve 2) buna rağmen, durum 1'in her sayfaya vurulacağı garanti edildiği için aynı veya daha kötü bir soruna sahip olacaktır.

Her iki durumun da her sayfaya en az bir kez erişmek zorunda olduğu gerçeğini hesaba katarsak, sorusu daha hızlı, 1 milyon karması ya da belleğe karşı yaklaşık 155000 okuma meselesi gibi görünüyor? Testlerim ikincisini söylüyor gibi görünüyor, ancak SQL Server sürekli olarak öncekini seçiyor.

Soru

Bu yüzden soruma geri dönelim: Testler bu tür sonuçları gösterdiğinde bu LOOP JOIN ipucunu zorlamaya devam etmeli miyim yoksa analizimde bir şey mi eksik? SQL Server'ın optimizer karşı gitmek için tereddüt, ama bu gibi durumlarda olması gerektiği daha erken bir karma birleştirmek kullanmaya geçer gibi geliyor.

2014-04-28 Güncellemesi

Daha fazla test yaptım ve yukarıda elde ettiğim sonuçların (2 CPU'lu bir VM'de) diğer ortamlarda çoğaltılamadığımı keşfettim (8 ve 12 CPU'lu 2 farklı fiziksel makinede denedim). Optimize edici, sonraki durumlarda böyle belirgin bir sorunun olmadığı noktaya kadar çok daha iyi bir performans gösterdi. Sanırım geçmişe bakıldığında bariz görünen öğrenilen ders, ortamın optimize edicinin ne kadar iyi çalıştığını önemli ölçüde etkileyebileceğidir.

Uygulama Planları

Uygulama Planı Vaka 1 Plan 1 Uygulama Planı Vaka 2 resim açıklamasını buraya girin

Örnek Vaka Oluşturma Kodu

------------------------------------------------------------
-- 1. Create SampleTable with 1,000,000 rows
------------------------------------------------------------    

CREATE TABLE SampleTable
    (  
       ID         INT NOT NULL PRIMARY KEY CLUSTERED
     , Number1    INT NOT NULL
     , Number2    INT NOT NULL
     , Number3    INT NOT NULL
     , Number4    INT NOT NULL
     , Number5    INT NOT NULL
    )

--Add 1 million rows
;WITH  
    Cte0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows  
    Cte1 AS (SELECT 1 AS C FROM Cte0 AS A, Cte0 AS B),--4 rows  
    Cte2 AS (SELECT 1 AS C FROM Cte1 AS A ,Cte1 AS B),--16 rows 
    Cte3 AS (SELECT 1 AS C FROM Cte2 AS A ,Cte2 AS B),--256 rows 
    Cte4 AS (SELECT 1 AS C FROM Cte3 AS A ,Cte3 AS B),--65536 rows 
    Cte5 AS (SELECT 1 AS C FROM Cte4 AS A ,Cte2 AS B),--1048576 rows 
    FinalCte AS (SELECT  ROW_NUMBER() OVER (ORDER BY C) AS Number FROM   Cte5)
INSERT INTO SampleTable
SELECT Number, Number, Number, Number, Number, Number
FROM  FinalCte
WHERE Number <= 1000000

------------------------------------------------------------
-- Create 2 SPs that join from #Driver to SampleTable.
------------------------------------------------------------    
GO
IF OBJECT_ID('JoinTest_NoHint') IS NOT NULL DROP PROCEDURE JoinTest_NoHint
GO
CREATE PROC JoinTest_NoHint
AS
    SELECT S.*
    INTO #Results
    FROM #Driver AS D
    JOIN SampleTable AS S ON S.ID = D.ID
GO
IF OBJECT_ID('JoinTest_LoopHint') IS NOT NULL DROP PROCEDURE JoinTest_LoopHint
GO
CREATE PROC JoinTest_LoopHint
AS
    SELECT S.*
    INTO #Results
    FROM #Driver AS D
    INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID
GO

------------------------------------------------------------
-- Create driver table with 50K rows
------------------------------------------------------------    
GO
IF OBJECT_ID('tempdb..#Driver') IS NOT NULL DROP TABLE #Driver
SELECT ID
INTO #Driver
FROM SampleTable
WHERE ID % 20 = 0

------------------------------------------------------------
-- Run each test and run Profiler
------------------------------------------------------------    

GO
/*Reg*/  EXEC JoinTest_NoHint
GO
/*Loop*/ EXEC JoinTest_LoopHint


------------------------------------------------------------
-- Results
------------------------------------------------------------    

/*

Duration CPU   Reads    TextData
315      313   4714     /*Reg*/  EXEC JoinTest_NoHint
309      296   4713     /*Reg*/  EXEC JoinTest_NoHint
327      329   4713     /*Reg*/  EXEC JoinTest_NoHint
398      406   4715     /*Reg*/  EXEC JoinTest_NoHint
316      312   4714     /*Reg*/  EXEC JoinTest_NoHint
217      219   160017   /*Loop*/ EXEC JoinTest_LoopHint
211      219   160014   /*Loop*/ EXEC JoinTest_LoopHint
217      219   160013   /*Loop*/ EXEC JoinTest_LoopHint
190      188   160013   /*Loop*/ EXEC JoinTest_LoopHint
187      187   160015   /*Loop*/ EXEC JoinTest_LoopHint

*/

Yanıtlar:


13

SampleTable yaklaşık 36MB alan 4714 sayfada yer almaktadır. Durum 1 hepsini tarar, bu yüzden 4714 okuma elde ederiz. Ayrıca, CPU yoğun olan ve sonuçta zamanı orantılı olarak artıran 1 milyon karma gerçekleştirmelidir. Vaka 1'de zamanı hızlandıran tüm bu karmadır.

Bir karma birleştirme için bir başlangıç ​​maliyeti vardır (aynı zamanda bir engelleme işlemi olan karma tablosunu oluşturmak), ancak karma birleştirme , SQL Server tarafından desteklenen üç fiziksel birleştirme türünün en düşük teorik per-satır maliyetine sahiptir. IO ve CPU terimleri. Karma birleştirme, nispeten küçük bir yapı girişi ve büyük bir prob girişi ile gerçekten kendi başına gelir. Bununla birlikte, hiçbir fiziksel birleştirme türü tüm senaryolarda 'daha iyi' değildir.

Şimdi durum 2'yi düşünün. Herhangi bir karma işi yapmıyor, bunun yerine 50000 ayrı arama yapıyor, bu da okumaları artıran şey. Fakat okumalar nispeten ne kadar pahalı? Bunların fiziksel okumalar olması durumunda oldukça pahalı olabileceği söylenebilir. Ancak unutmayın 1) belirli bir sayfanın yalnızca ilk okunması fiziksel olabilir ve 2) buna rağmen, durum 1'in her sayfaya vurulacağı garanti edildiği için aynı veya daha kötü bir soruna sahip olacaktır.

Her arayış, tek bir hash probuna kıyasla hesaplamalı olarak pahalı olan bir b-ağacının köke gitmesini gerektirir. Ek olarak, iç içe ilmek döngülerinin iç tarafı için genel GÇ paterni, bir karma birleştirmeye sonda tarafındaki tarama girişinin sıralı erişim paterni ile karşılaştırıldığında, rastgeledir. Temeldeki fiziksel G / Ç alt sistemine bağlı olarak, sıralı okumalar rastgele okumalardan daha hızlı olabilir. Ayrıca, SQL Server ileri okuma mekanizması, daha büyük okumalar veren sıralı IO ile daha iyi çalışır.

Her iki durumun da her sayfaya en az bir kez erişmek zorunda olduğu gerçeğini hesaba katarsak, sorusu daha hızlı, 1 milyon karması ya da belleğe karşı yaklaşık 155000 okuma meselesi gibi görünüyor? Testlerim ikincisini söylüyor gibi görünüyor, ancak SQL Server sürekli olarak öncekini seçiyor.

SQL Server sorgu iyileştiricisi bir dizi varsayım yapar. Birincisi, bir sorgu tarafından yapılan bir sayfaya ilk erişimin fiziksel bir ES'ye ('soğuk önbellek varsayımı') neden olacağıdır. Daha sonra okumanın aynı sorgu tarafından önceden belleğe okunan bir sayfadan gelme olasılığı modellenmiştir, ancak bu eğitimli bir tahminden başka bir şey değildir.

Optimize edicinin modelinin bu şekilde çalışmasının nedeni, en kötü durum için optimize etmenin genellikle daha iyi olmasıdır (fiziksel GÇ gereklidir). Birçok eksiklik, paralellik ve hafızada bir şeyler yürütmeyle örtülebilir. Optimize edicinin, tüm verilerin bellekte olduğunu varsayarsa üreteceği sorgu planları, bu varsayımın geçersiz olduğu kanıtlanırsa çok kötü performans gösterebilir.

Soğuk önbellek varsayımı kullanılarak üretilen plan, bunun yerine sıcak bir önbellek varsayıldığı kadar iyi performans göstermeyebilir, ancak en kötü durum performansı genellikle daha üstün olacaktır.

Testler bu tür sonuçları gösterdiğinde bu LOOP JOIN ipucunu zorlamaya devam etmeli miyim yoksa analizimde bir şey mi eksik? SQL Server'ın optimizer karşı gitmek için tereddüt, ama bu gibi durumlarda olması gerektiği daha erken bir karma birleştirmek kullanmaya geçer gibi geliyor.

Bunu iki nedenden dolayı yapmak konusunda çok dikkatli olmalısınız. İlk olarak, ipuçları da sessizce fiziksel Ayrıca belirtilen vardı sadece sanki sorgunun (yazılı emir maç için sipariş katılmak zorlamak katılmak OPTION (FORCE ORDER)Bu ciddi optimizer mevcut alternatifleri sınırlar., Ve her zaman istediğini olmayabilir. OPTION (LOOP JOIN)Kuvvetlerinin iç içe döngüler sorgusuna katılır, ancak yazılı birleştirme sırasını zorunlu kılmaz.

İkincisi, veri kümesi boyutunun küçük kalacağını ve mantıksal okumaların çoğunun önbellekten geleceğini varsayıyorsunuz. Bu varsayımlar geçersiz hale gelirse (belki de zamanla), performans düşecektir. Yerleşik sorgu iyileştirici, değişen koşullara tepki verme konusunda oldukça iyidir; bu özgürlüğü ortadan kaldırmak çok düşünmeniz gereken bir şeydir.

Genel olarak, döngüler birleşimlerini zorlamak için zorlayıcı bir neden yoksa, bundan kaçınırdım. Varsayılan planlar genellikle optimal seviyeye oldukça yakındır ve değişen koşullar karşısında daha esnek olma eğilimindedir.


Teşekkürler Paul. Mükemmel detaylı analiz. Yaptığım bazı testlere dayanarak, olan şey, geçici tablonun boyutu 5K ile 100K arasında olduğunda optimizasyonun eğitimli tahminlerinin bu özel örnek için sürekli olarak kapalı olduğunu düşünüyorum. Gereksinimlerimizin geçici tablonun <50K olacağını garanti ettiği göz önüne alındığında, benim için güvenli görünüyor. Merak ediyorum, hala bunu bilen herhangi bir katılma ipucundan kaçınır mısınız?
JohnnyM

1
@JohnnyM Bir nedenden dolayı ipuçları var. Bunları yapmak için sağlam nedenleriniz olduğunda kullanmak iyidir. Bununla birlikte, ima edilen nedenlerden dolayı nadiren birleştirme ipuçları kullanıyorum FORCE ORDER. Garip vesileyle birleştirme ipucu kullanıyorum, genellikle OPTION (FORCE ORDER)nedenini açıklamak için bir yorum ekliyorum .
Paul White 9

0

Milyon satırlık bir tabloya katılan 50.000 satır, dizini olmayan herhangi bir tablo için çok fazla gibi görünüyor.

Bu durumda tam olarak ne yapacağınızı söylemek zor, çünkü aslında çözmeye çalıştığınız problemden çok izole. Kesinlikle önemli miktarda satır içeren birçok dizine alınmamış geçici tablolara katıldığınız kodunuzda genel bir desen olmadığını umuyorum.

Örneği sadece söylediklerine göre, neden sadece #Driver'a bir dizin koymuyoruz? D.ID gerçekten benzersiz mi? Öyleyse, bu anlamsal olarak bir EXISTS deyimine eşdeğerdir, bu da en azından SQL Server'ın S'nin yinelenen D değerleri için arama yapmaya devam etmek istemediğinizi bildirmesini sağlar:

SELECT S.*
INTO #Results
FROM SampleTable S
WHERE EXISTS (SELECT * #Driver D WHERE S.ID = D.ID);

Kısacası, bu model için bir LOOP ipucu kullanmam. Ben sadece bu kalıbı kullanmam. Mümkünse, öncelik sırasına göre aşağıdakilerden birini yaparım:

  • Mümkünse # Sürücü için geçici tablo yerine CTE kullanın
  • Benzersiz ise #Driver'da benzersiz bir kümelenmemiş dizin kullanın (#Driver'ı yalnızca bu kez kullandığınızı ve tablonun kendisinden herhangi bir veri istemediğinizi varsayarsak - aslında bu tablodan verilere ihtiyacınız varsa, kümelenmiş bir dizin yapmak için iyi olabilir)
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.