SQL Server, paralel olarak birleştirme birleşimini iki eşit bölümlenmiş tabloda en iyi duruma getirmiyor


21

Çok detaylı soru için şimdiden özür dilerim. Sorunu yeniden oluşturmak için tam bir veri kümesi oluşturmak için sorgular dahil ettik ve 32 çekirdekli bir makinede SQL Server 2012 çalıştırıyorum. Ancak bunun SQL Server 2012'ye özgü olduğunu sanmıyorum ve bu belirli örnek için 10'luk bir MAXDOP zorladım.

Aynı bölüm şeması kullanılarak bölümlenmiş iki tablom var. Bölümleme için kullanılan sütunda bir araya getirilirken, SQL Server'ın paralel bir birleştirme birleşimini beklendiği kadar optimize edemediğini ve bunun yerine HASH JOIN kullanmayı seçtiğini fark ettim. Bu özel durumda, sorguyu bölüm işlevine göre 10 ayrık aralığa bölerek ve bu sorguların her birini SSMS'de aynı anda çalıştırarak, daha uygun bir paralel MERGE JOIN'i elle simüle edebiliyorum. Hepsini aynı anda çalıştırmak için WAITFOR kullanılması, sonuçta tüm sorguların orijinal paralel HASH JOIN tarafından kullanılan toplam sürenin ~% 40'ını tamamlıyor.

Eşit olarak bölümlenmiş tablolar durumunda SQL Server'ın bu optimizasyonu kendi başına yapması için bir yol var mı? MERGE BİRLEŞTİRMESİNİ paralel hale getirmek için SQL Server'ın genellikle çok fazla masrafa maruz kalacağını anlıyorum, ancak bu durumda en az ek yükü olan çok doğal bir kesme yöntemi var gibi görünüyor. Belki de, optimize edicinin henüz tanıyacak kadar zeki olmadığı özel bir durumdur?

Bu sorunu yeniden oluşturmak için basitleştirilmiş bir veri seti oluşturacak SQL:

/* Create the first test data table */
CREATE TABLE test_transaction_properties 
    ( transactionID INT NOT NULL IDENTITY(1,1)
    , prop1 INT NULL
    , prop2 FLOAT NULL
    )

/* Populate table with pseudo-random data (the specific data doesn't matter too much for this example) */
;WITH E1(N) AS (
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 
    UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)
, E2(N) AS (SELECT 1 FROM E1 a CROSS JOIN E1 b)
, E4(N) AS (SELECT 1 FROM E2 a CROSS JOIN E2 b)
, E8(N) AS (SELECT 1 FROM E4 a CROSS JOIN E4 b)
INSERT INTO test_transaction_properties WITH (TABLOCK) (prop1, prop2)
SELECT TOP 10000000 (ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) % 5) + 1 AS prop1
                , ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) * rand() AS prop2
FROM E8

/* Create the second test data table */
CREATE TABLE test_transaction_item_detail
    ( transactionID INT NOT NULL
    , productID INT NOT NULL
    , sales FLOAT NULL
    , units INT NULL
    )

 /* Populate the second table such that each transaction has one or more items
     (again, the specific data doesn't matter too much for this example) */
INSERT INTO test_transaction_item_detail WITH (TABLOCK) (transactionID, productID, sales, units)
SELECT t.transactionID, p.productID, 100 AS sales, 1 AS units
FROM test_transaction_properties t
JOIN (
    SELECT 1 as productRank, 1 as productId
    UNION ALL SELECT 2 as productRank, 12 as productId
    UNION ALL SELECT 3 as productRank, 123 as productId
    UNION ALL SELECT 4 as productRank, 1234 as productId
    UNION ALL SELECT 5 as productRank, 12345 as productId
) p
    ON p.productRank <= t.prop1

/* Divides the transactions evenly into 10 partitions */
CREATE PARTITION FUNCTION [pf_test_transactionId] (INT)
AS RANGE RIGHT
FOR VALUES
(1,1000001,2000001,3000001,4000001,5000001,6000001,7000001,8000001,9000001)

CREATE PARTITION SCHEME [ps_test_transactionId]
AS PARTITION [pf_test_transactionId]
ALL TO ( [PRIMARY] )

/* Apply the same partition scheme to both test data tables */
ALTER TABLE test_transaction_properties
ADD CONSTRAINT PK_test_transaction_properties
PRIMARY KEY (transactionID)
ON ps_test_transactionId (transactionID)

ALTER TABLE test_transaction_item_detail
ADD CONSTRAINT PK_test_transaction_item_detail
PRIMARY KEY (transactionID, productID)
ON ps_test_transactionId (transactionID)

Şimdi nihayet alt-optimal sorguyu yeniden oluşturmaya hazırız!

/* This query produces a HASH JOIN using 20 threads without the MAXDOP hint,
    and the same behavior holds in that case.
    For simplicity here, I have limited it to 10 threads. */
SELECT COUNT(*)
FROM test_transaction_item_detail i
JOIN test_transaction_properties t
    ON t.transactionID = i.transactionID
OPTION (MAXDOP 10)

görüntü tanımını buraya girin

görüntü tanımını buraya girin

Bununla birlikte, her bir bölümü işlemek için tek bir iplik kullanmak (aşağıdaki birinci bölüm için örnek) çok daha verimli bir plana yol açacaktır. Bunu tam olarak aynı anda 10 bölümün her biri için aşağıdakine benzer bir sorgu çalıştırarak test ettim ve 10'un tamamı 1 saniyede bitti:

SELECT COUNT(*)
FROM test_transaction_item_detail i
INNER MERGE JOIN test_transaction_properties t
    ON t.transactionID = i.transactionID
WHERE t.transactionID BETWEEN 1 AND 1000000
OPTION (MAXDOP 1)

görüntü tanımını buraya girin görüntü tanımını buraya girin

Yanıtlar:


18

Haklısın, SQL Server Doktoru paralel MERGEbirleştirme planları oluşturmamayı tercih ediyor (bu alternatifi oldukça pahalıya mal ediyor). ParalelMERGE her zaman birleştirme girişlerinde her zaman yeniden bölümlendirme borsaları gerektirir ve daha da önemlisi, satır sıralarının bu borsalar arasında korunmasını gerektirir.

Paralellik, her bir iplik bağımsız olarak çalışabildiğinde en verimlidir; siparişi koruma sık sık senkronizasyon beklemelerine yol açar ve sonuçta değişimlerin dökülmesine neden olabilirtempdb içi kilitlenme durumunu çözmek için .

Bu problemler, her bir iş parçacığının her biri özel bir veri aralığını işlerken, her bir iş parçacığında tüm sorgunun birden fazla örneğini çalıştırarak çözülebilir . Ancak, bu, optimizer’in yerel olarak dikkate aldığı bir strateji değildir. Olduğu gibi, paralellik için orijinal SQL Server modeli borsalarda borsaları kırar ve bu bölünmelerin oluşturduğu plan segmentlerini çoklu iş parçacıklarında çalıştırır.

Özel veri kümesi aralıkları üzerinden birden fazla iş parçacığı üzerinde tüm sorgu planlarını çalıştırmanın yolları vardır, ancak herkesin mutlu olamayacağı (ve Microsoft tarafından desteklenmeyecek veya gelecekte çalışacağını garanti etmeyecekleri) kandırmaca gerektirir. Böyle bir yaklaşım, bölümlenmiş bir tablonun bölümlerini tekrarlamak ve her iş parçacığına bir alt toplam üretme görevi vermektir. Sonuç, SUMher bağımsız iş parçacığı tarafından döndürülen satır sayılarıdır:

Bölüm numaralarını almak meta verilerden yeterince kolaydır:

DECLARE @P AS TABLE
(
    partition_number integer PRIMARY KEY
);

INSERT @P (partition_number)
SELECT
    p.partition_number
FROM sys.partitions AS p 
WHERE 
    p.[object_id] = OBJECT_ID(N'test_transaction_properties', N'U')
    AND p.index_id = 1;

Daha sonra bu sayıları korelasyonlu bir birleşim ( APPLY) ve $PARTITIONher iş parçacığını geçerli bölüm numarası ile sınırlandırma işlevini kullanmak için kullanırız:

SELECT
    row_count = SUM(Subtotals.cnt)
FROM @P AS p
CROSS APPLY
(
    SELECT
        cnt = COUNT_BIG(*)
    FROM dbo.test_transaction_item_detail AS i
    JOIN dbo.test_transaction_properties AS t ON
        t.transactionID = i.transactionID
    WHERE 
        $PARTITION.pf_test_transactionId(t.transactionID) = p.partition_number
        AND $PARTITION.pf_test_transactionId(i.transactionID) = p.partition_number
) AS SubTotals;

Sorgu planı, MERGEtablodaki her satır için gerçekleştirilen bir birleşimi gösterir @P. Kümelenmiş dizin tarama özellikleri, her yinelemede yalnızca tek bir bölümün işlendiğini doğrular:

Seri plan uygula

Ne yazık ki, bu yalnızca bölümlerin sıralı seri işlenmesiyle sonuçlanır. Sağladığınız veri setinde 4 çekirdekli (8'e kadar yüksek) dizüstü bilgisayarım tüm verileri bellekte 7 saniyede doğru sonucu veriyor .

Almak için MERGEaynı anda çalıştırmak için alt planlar, biz bölme kimlikleri mevcut parçacığı (yayılmıştır paralel plana ihtiyacımız MAXDOP) ve her MERGEbiri bölümdeki verileri kullanarak tek iş parçacığı üzerinde alt planı çalışır. Ne yazık ki, optimizer sık ​​sık MERGEmaliyet gerekçesiyle paralel olarak karar verir ve paralel bir plan zorlamak için belgelenmiş bir yol yoktur. 8649 izleme bayrağını kullanarak belgelenmemiş (ve desteklenmeyen) bir yol var :

SELECT
    row_count = SUM(Subtotals.cnt)
FROM @P AS p
CROSS APPLY
(
    SELECT
        cnt = COUNT_BIG(*)
    FROM dbo.test_transaction_item_detail AS i
    JOIN dbo.test_transaction_properties AS t ON
        t.transactionID = i.transactionID
    WHERE 
        $PARTITION.pf_test_transactionId(t.transactionID) = p.partition_number
        AND $PARTITION.pf_test_transactionId(i.transactionID) = p.partition_number
) AS SubTotals
OPTION (QUERYTRACEON 8649);

Şimdi sorgu planı, bölüm numaralarının @Pyuvarlak-robin bazında dişler arasında dağılmasını göstermektedir. Her iş parçacığı iç içe ilmeklerin iç tarafını tek bir bölüm için birleştirerek eşzamanlı verileri eşzamanlı olarak işleme hedefimizi gerçekleştirir. Aynı sonuç şimdi sekiz saniyede 8 hiper çekirdeğimde 3 saniyede geri döndü, sekizinin de% 100 kullanımıyla.

Paralel UYGULAMA

Bu tekniği mutlaka kullanmanızı tavsiye etmiyorum - önceki uyarılarıma bakın - ancak bu sorunuzu ele alıyor.

Daha fazla ayrıntı için Bölümlenmiş Tablosu İyileştirme başlıklı makaleme bakın.

Columnstore

SQL Server 2012 kullandığınızı görünce (ve Enterprise olduğu varsayılırsa) bir columntore dizini kullanma seçeneğiniz de vardır. Bu, yeterli modun mevcut olduğu toplu iş modu karma birleşimlerinin potansiyelini gösterir:

CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
ON dbo.test_transaction_properties (transactionID);

CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
ON dbo.test_transaction_item_detail (transactionID);

Bu indeksleri ile sorguda ...

SELECT
    COUNT_BIG(*)
FROM dbo.test_transaction_properties AS ttp
JOIN dbo.test_transaction_item_detail AS ttid ON
    ttid.transactionID = ttp.transactionID;

... optimize ediciden hile yapmadan aşağıdaki uygulama planını verir:

Sütun mağazası planı 1

2 saniye içinde sonuçları düzeltin , ancak skaler agrega için sıra modu işlemenin ortadan kaldırılması daha da yardımcı olur:

SELECT
    COUNT_BIG(*)
FROM dbo.test_transaction_properties AS ttp
JOIN dbo.test_transaction_item_detail AS ttid ON
    ttid.transactionID = ttp.transactionID
GROUP BY
    ttp.transactionID % 1;

Optimize edilmiş sütun deposu

En iyi duruma getirilmiş sütun deposu sorgusu 851ms'de çalışır .

Geoff Patterson, Partition Wise Joins adlı hata raporunu oluşturdu ancak Won't Fix't gibi kapatıldı.


5
Burada mükemmel bir öğrenme deneyimi. teşekkür ederim. +1
Edward Dortland,

1
Paul teşekkürler! Buradaki harika bilgiler ve kesinlikle soruyu ayrıntılı olarak ele alıyor.
Geoff Patterson

2
Paul teşekkürler! Buradaki harika bilgiler ve kesinlikle soruyu ayrıntılı olarak ele alıyor. Karma bir SQL 2008/2012 ortamındayız, ancak gelecek dönem için sütun deposunu daha fazla araştırmayı düşüneceğim. Elbette, SQL Server'ın paralel bir birleştirme birleşiminden ve sahip olabileceği daha düşük bellek gereksinimlerinden etkili bir şekilde yararlanabilmesini diliyorum. Kullanım durumumda :) Herhangi birinin bakması ve yorum yapması umrunda olursa aşağıdaki Connect konusunu dosyaladım veya oyla: connect.microsoft.com/SQLServer/feedback/details/759266/…
Geoff Patterson

0

İyileştiriciyi daha iyi düşündüğünüz şekilde çalıştırmanın yolu, sorgu ipuçlarıdır.

Bu durumda, OPTION (MERGE JOIN)

Ya da bütün domuz gidip kullanabilirsiniz USE PLAN


Bunu kişisel olarak yapmazdım: ipucu sadece mevcut veri hacmi ve dağıtım için faydalı olacaktır.
gbn

İlginç olan, OPTION (MERGE JOIN) kullanarak çok daha kötü bir plana yol açmasıdır. Optimizer, MERGE JOIN'in bölüm işlevi tarafından paylaşılabileceğini fark edecek kadar akıllı değildir ve bu ipucunu uygulamak sorguyu ~ 46 saniye sürebilir. Çok sinir bozucu!

@gbn hangisi muhtemelen optimizer'ın ilk aşamada karma katılım için gidiyor?

@gpatterson Ne kadar sinir bozucu! :)

Bölümlemeyi el ile bir sendika aracılığıyla zorlarsanız ne olur?
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.