Mengapa permintaan SELECT DISTINCT TOP N saya memindai seluruh tabel?


28

Saya telah mengalami beberapa SELECT DISTINCT TOP Npertanyaan yang tampaknya kurang dioptimalkan oleh pengoptimal query SQL Server. Mari kita mulai dengan mempertimbangkan contoh sepele: tabel sejuta baris dengan dua nilai bergantian. Saya akan menggunakan fungsi GetNums untuk menghasilkan data:

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

Untuk kueri berikut:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL Server dapat menemukan dua nilai berbeda hanya dengan memindai halaman data pertama dari tabel tetapi memindai semua data sebagai gantinya . Mengapa SQL Server tidak memindai sampai menemukan jumlah nilai berbeda yang diminta?

Untuk pertanyaan ini, harap gunakan data pengujian berikut yang berisi 10 juta baris dengan 10 nilai berbeda yang dihasilkan dalam blok:

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

Jawaban untuk tabel dengan indeks berkerumun juga dapat diterima:

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

Kueri berikut memindai semua 10 juta baris dari tabel . Bagaimana saya bisa mendapatkan sesuatu yang tidak memindai seluruh tabel? Saya menggunakan SQL Server 2016 SP1.

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

Jawaban:


30

Tampaknya ada tiga aturan pengoptimal yang berbeda yang dapat melakukan DISTINCToperasi dalam kueri di atas. Kueri berikut melempar kesalahan yang menunjukkan bahwa daftar ini lengkap:

SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);

Msg 8622, Lantai 16, Negara Bagian 1, Jalur 1

Prosesor kueri tidak dapat menghasilkan rencana kueri karena petunjuk yang ditentukan dalam kueri ini. Kirim ulang kueri tanpa menentukan petunjuk apa pun dan tanpa menggunakan SET FORCEPLAN.

GbAggToSortmengimplementasikan kelompok-oleh agregat (berbeda) sebagai jenis berbeda. Ini adalah operator pemblokiran yang akan membaca semua data dari input sebelum menghasilkan baris apa pun. GbAggToStrmmengimplementasikan agregat grup-per sebagai agregat aliran (yang juga membutuhkan semacam input dalam hal ini). Ini juga operator pemblokiran. GbAggToHSmengimplementasikan sebagai hash cocok, yang adalah apa yang kita lihat dalam rencana buruk dari pertanyaan, tetapi dapat diimplementasikan sebagai hash cocok (agregat) atau hash cocok (mengalir berbeda).

Operator hash match ( flow berbeda ) adalah salah satu cara untuk menyelesaikan masalah ini karena tidak memblokir. SQL Server harus dapat menghentikan pemindaian setelah menemukan nilai yang cukup berbeda.

Operator logis Flow Distinct memindai input, menghapus duplikat. Sedangkan operator Distinct mengkonsumsi semua input sebelum menghasilkan output, operator Flow Distinct mengembalikan setiap baris karena diperoleh dari input (kecuali jika baris itu adalah duplikat, dalam hal ini dibuang).

Mengapa kueri dalam pertanyaan menggunakan pencocokan hash (agregat) alih-alih pencocokan hash (berbeda alur)? Karena jumlah nilai-nilai yang berbeda berubah dalam tabel saya akan mengharapkan biaya permintaan hash (aliran berbeda) menurun karena estimasi jumlah baris yang perlu dipindai ke tabel akan berkurang. Saya berharap biaya rencana hash match (agregat) meningkat karena tabel hash yang dibutuhkan untuk membangun akan semakin besar. Salah satu cara untuk menyelidiki ini adalah dengan membuat panduan rencana . Jika saya membuat dua salinan data tetapi menerapkan panduan rencana untuk salah satunya, saya harus dapat membandingkan kecocokan hash (agregat) dengan kecocokan hash (berbeda) berdampingan dengan data yang sama. Perhatikan bahwa saya tidak dapat melakukan ini dengan menonaktifkan aturan optimizer kueri karena aturan yang sama berlaku untuk kedua paket ( GbAggToHS).

Inilah salah satu cara untuk mendapatkan panduan rencana yang saya cari:

DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;

CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;

-- run this query
SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

Dapatkan pegangan paket dan gunakan untuk membuat panduan rencana:

-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM 
sys.dm_exec_query_stats AS qs   
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st  
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;

EXEC sp_create_plan_guide_from_handle 
'EVIL_PLAN_GUIDE', 
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;

Panduan paket hanya berfungsi pada teks kueri yang tepat, jadi mari kita salin kembali dari panduan paket:

SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';

Setel ulang data:

TRUNCATE TABLE X_PLAN_GUIDE_TARGET;

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

Dapatkan paket permintaan untuk permintaan dengan panduan paket yang diterapkan:

SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

Ini memiliki operator pencocokan hash (berbeda aliran) yang kami inginkan dengan data pengujian kami. Perhatikan bahwa SQL Server mengharapkan untuk membaca semua baris dari tabel dan bahwa perkiraan biaya sama persis seperti untuk rencana dengan pencocokan hash (agregat). Pengujian yang saya lakukan menyarankan bahwa biaya untuk dua paket identik ketika tujuan baris untuk paket lebih besar atau sama dengan jumlah nilai yang berbeda yang diharapkan SQL Server dari tabel, yang dalam hal ini dapat dengan mudah diturunkan dari statistik. Sayangnya (untuk kueri kami) pengoptimal memilih kecocokan hash (agregat) di atas kecocokan hash (berbeda aliran) saat biayanya sama. Jadi kita 0,0000001 unit pengoptimal sihir jauh dari rencana yang kita inginkan.

Salah satu cara untuk mengatasi masalah ini adalah dengan mengurangi tujuan baris. Jika sasaran baris dari sudut pandang pengoptimal kurang dari jumlah baris yang berbeda, kami mungkin akan mendapatkan pencocokan hash (berbeda alur). Ini dapat dilakukan dengan OPTIMIZE FORpetunjuk kueri:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Untuk kueri ini, pengoptimal membuat rencana seolah-olah kueri hanya membutuhkan baris pertama, tetapi ketika kueri dijalankan, akan mengembalikan 10 baris pertama. Di komputer saya, kueri ini memindai 892800 baris dari X_10_DISTINCT_HEAPdan menyelesaikan dalam 299 ms dengan waktu CPU 250 ms dan 2537 pembacaan logis.

Perhatikan bahwa teknik ini tidak akan berfungsi jika statistik melaporkan hanya satu nilai berbeda, yang bisa terjadi untuk statistik sampel terhadap data yang miring. Namun, dalam kasus itu, data Anda tidak cukup padat untuk dibenarkan menggunakan teknik seperti ini. Anda mungkin tidak kehilangan banyak dengan memindai semua data dalam tabel, terutama jika itu bisa dilakukan secara paralel.

Cara lain untuk menyerang masalah ini adalah dengan menggembungkan jumlah nilai berbeda yang diperkirakan SQL Server dapatkan dari tabel dasar. Ini lebih sulit dari yang diharapkan. Menerapkan fungsi deterministik tidak mungkin dapat meningkatkan jumlah hasil yang berbeda. Jika pengoptimal kueri menyadari fakta matematika itu (beberapa pengujian menyarankan setidaknya untuk tujuan kami) maka menerapkan fungsi deterministik (yang mencakup semua fungsi string ) tidak akan meningkatkan perkiraan jumlah baris yang berbeda.

Banyak fungsi nondeterministic tidak berfungsi, termasuk pilihan yang jelas NEWID()dan RAND(). Namun, LAG()lakukan trik untuk kueri ini. Pengoptimal kueri mengharapkan 10 juta nilai berbeda terhadap LAGekspresi yang akan mendorong rencana pencocokan hash (berbeda aliran) :

SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

Di mesin saya, kueri ini memindai 892800 baris dari X_10_DISTINCT_HEAPdan menyelesaikan dalam 1165 ms dengan waktu CPU 1109 ms dan 2537 membaca logis, sehingga LAG()menambahkan sedikit overhead relatif. @ Paul White menyarankan untuk mencoba pemrosesan mode batch untuk permintaan ini. Pada SQL Server 2016 kita bisa mendapatkan pemrosesan mode batch bahkan dengan MAXDOP 1. Salah satu cara untuk mendapatkan pemrosesan mode batch untuk tabel rowstore adalah dengan bergabung ke CCI kosong sebagai berikut:

CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;

SELECT DISTINCT TOP 10 VAL
FROM
(
    SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
    FROM X_10_DISTINCT_HEAP
    LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);

Kode itu menghasilkan rencana kueri ini .

Paul menunjukkan bahwa saya harus mengubah kueri untuk digunakan LAG(..., 1)karena LAG(..., 0)tampaknya tidak memenuhi syarat untuk optimasi Window Aggregate. Perubahan ini mengurangi waktu yang telah berlalu menjadi 520 ms dan waktu CPU menjadi 454 ms.

Perhatikan bahwa LAG()pendekatannya bukan yang paling stabil. Jika Microsoft mengubah asumsi keunikan terhadap fungsi tersebut, maka Microsoft mungkin tidak lagi berfungsi. Ini memiliki perkiraan yang berbeda dengan warisan CE. Juga jenis pengoptimalan terhadap tumpukan tidak perlu ide yang baik. Jika tabel dibangun kembali mungkin berakhir dalam skenario kasus terburuk di mana hampir semua baris perlu dibaca dari tabel.

Terhadap tabel dengan kolom unik (seperti contoh indeks berkerumun dalam pertanyaan) kami memiliki opsi yang lebih baik. Misalnya kita dapat menipu pengoptimal dengan menggunakan SUBSTRINGekspresi yang selalu mengembalikan string kosong. SQL Server tidak berpikir bahwa SUBSTRINGakan mengubah jumlah nilai yang berbeda jadi jika kita menerapkannya pada kolom unik, seperti PK, maka jumlah baris yang berbeda diperkirakan adalah 10 juta. Kueri berikut ini mendapatkan operator pencocokan hash (berbeda aliran):

SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);

Di mesin saya, kueri ini memindai 900.000 baris dari X_10_DISTINCT_CIdan menyelesaikan dalam 333 ms dengan waktu CPU 297 ms dan 3011 pembacaan logis.

Singkatnya, pengoptimal kueri muncul untuk menganggap bahwa semua baris akan dibaca dari tabel untuk SELECT DISTINCT TOP Nkueri ketika N> = jumlah taksiran baris berbeda dari tabel. Operator hash cocok (agregat) mungkin memiliki biaya yang sama seperti operator hash cocok (berbeda aliran) tetapi pengoptimal selalu memilih operator agregat. Hal ini dapat menyebabkan pembacaan logis yang tidak perlu ketika nilai yang cukup berbeda terletak di dekat awal pemindaian tabel. Dua cara untuk menipu pengoptimal agar menggunakan operator hash match (flow berbeda) adalah dengan menurunkan tujuan baris menggunakan OPTIMIZE FORpetunjuk atau untuk menambah perkiraan jumlah baris berbeda menggunakan LAG()atau SUBSTRINGpada kolom unik.


12

Anda sudah menjawab pertanyaan Anda dengan benar.

Saya hanya ingin menambahkan pengamatan bahwa cara paling efisien sebenarnya adalah memindai seluruh tabel - jika dapat diatur sebagai 'tumpukan' kolomstore :

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

Pertanyaan sederhana:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

lalu berikan:

Rencana eksekusi

Tabel 'X_10_DISTINCT_HEAP'. Pindai hitungan 1,
 logis membaca 0, fisik membaca 0, baca-depan membaca 0, 
 lob logical reads 66 , lob fisik read 0, lob read-ahead membaca 0.
Tabel 'X_10_DISTINCT_HEAP'. Segmen berbunyi 13, segmen dilewati 0.

 Waktu Eksekusi SQL Server:
   Waktu CPU = 0 ms, waktu yang berlalu = 11 ms.

Hash Match (Flow Distinct) saat ini tidak dapat mengeksekusi dalam mode batch. Metode yang menggunakan ini jauh lebih lambat karena transisi (tidak terlihat) mahal dari batch ke pemrosesan baris. Sebagai contoh:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

Memberi:

Rencana Eksekusi Aliran Berbeda

Tabel 'X_10_DISTINCT_HEAP'. Pindai hitungan 1,
 logis membaca 0, fisik membaca 0, baca-depan membaca 0, 
 Lob membaca logis 20 , lob fisik membaca 0, lob membaca-depan membaca 0.
Tabel 'X_10_DISTINCT_HEAP'. Segmen berbunyi 4 , segmen dilewati 0.

 Waktu Eksekusi SQL Server:
   Waktu CPU = 640 ms, waktu yang berlalu = 680 ms.

Ini lebih lambat daripada ketika tabel disusun sebagai tumpukan rowstore.


5

Berikut adalah upaya untuk meniru pemindaian parsial berulang (mirip dengan tetapi tidak sama dengan pemindaian lompatan) menggunakan CTE rekursif. Tujuannya - karena kami tidak memiliki indeks (id)- untuk menghindari pengurutan dan beberapa pemindaian di atas meja.

Ini melakukan beberapa trik untuk melewati beberapa pembatasan CTE rekursif:

  • Tidak TOPdiperbolehkan di bagian rekursif. Kami menggunakan subquery dan ROW_NUMBER()sebagai gantinya.
  • Kami tidak dapat memiliki beberapa referensi ke bagian konstan atau menggunakan LEFT JOINatau menggunakan NOT IN (SELECT id FROM cte)dari bagian rekursif. Untuk memotong, kami membuat VARCHARstring yang mengakumulasi semua idnilai, mirip dengan STRING_AGGatau ke hierarkiID dan kemudian membandingkan dengan LIKE.

Untuk Heap (dengan asumsi kolom bernama id) test-1 di rextester.com .

Ini - seperti yang ditunjukkan oleh tes - tidak menghindari banyak pemindaian tetapi berkinerja baik ketika nilai yang berbeda ditemukan di beberapa halaman pertama. Namun jika nilai-nilai tidak terdistribusi secara merata, ini dapat melakukan beberapa pemindaian pada sebagian besar tabel - yang tentu saja mengakibatkan kinerja yang buruk.

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

dan ketika tabel dikelompokkan (CI aktif unique_key), uji-2 di rextester.com .

Ini menggunakan indeks berkerumun ( WHERE x.unique_key > ct.unique_key) untuk menghindari beberapa pemindaian:

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;

Ada masalah kinerja yang cukup halus dengan solusi ini. Akhirnya melakukan pencarian ekstra di atas meja setelah menemukan nilai N. Jadi, jika ada 10 nilai berbeda untuk 10 teratas, ia akan mencari nilai 11 yang tidak ada. Anda berakhir dengan pemindaian penuh tambahan dan 10 juta perhitungan ROW_NUMBER () benar-benar bertambah. Saya punya solusi di sini yang mempercepat kueri 20X di komputer saya. Apa yang kamu pikirkan? brentozar.com/pastetheplan/?id=SkDhAmFKe
Joe Obbish

2

Untuk kelengkapan, cara lain untuk mendekati masalah ini adalah dengan menggunakan OUTER BERLAKU . Kami dapat menambahkan OUTER APPLYoperator untuk setiap nilai berbeda yang perlu kami temukan. Ini serupa dalam konsep dengan pendekatan rekursif ypercube, tetapi secara efektif rekursi ditulis dengan tangan. Satu keuntungan adalah bahwa kita dapat menggunakan TOPtabel turunan alih-alih ROW_NUMBER()solusinya. Salah satu kelemahan besar adalah teks kueri bertambah panjang seiring Nbertambahnya.

Berikut ini adalah satu implementasi untuk kueri terhadap heap:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Berikut adalah rencana permintaan aktual untuk permintaan di atas. Di mesin saya permintaan ini selesai dalam 713 ms dengan 625 ms waktu CPU dan 12605 membaca logis. Kami mendapatkan nilai berbeda baru setiap baris 100rb jadi saya berharap permintaan ini memindai sekitar 900000 * 10 * 0,5 = 4500000 baris. Secara teori, kueri ini harus melakukan lima kali pembacaan logis dari kueri ini dari jawaban lain:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Kueri itu dibaca 2537 secara logis. 2537 * 5 = 12685 yang hampir mendekati 12605.

Untuk tabel dengan indeks berkerumun kami bisa melakukan yang lebih baik. Ini karena kita bisa meneruskan nilai kunci yang dikelompokkan terakhir ke tabel turunan untuk menghindari pemindaian baris yang sama dua kali. Satu implementasi:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Berikut adalah rencana permintaan aktual untuk permintaan di atas. Di komputer saya, kueri ini selesai dalam 154 ms dengan waktu CPU 140 ms dan 3203 pembacaan logis. Ini tampaknya berjalan sedikit lebih cepat daripada OPTIMIZE FORpermintaan terhadap tabel indeks berkerumun. Saya tidak berharap itu jadi saya mencoba mengukur kinerja lebih hati-hati. Metodologi saya adalah menjalankan setiap kueri sepuluh kali tanpa hasil dan untuk melihat angka agregat dari sys.dm_exec_sessionsdan sys.dm_exec_session_wait_stats. Sesi 56 adalah APPLYkueri dan sesi 63 adalah OPTIMIZE FORkueri.

Output dari sys.dm_exec_sessions:

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

Tampaknya ada keuntungan yang jelas di cpu_time dan elapsed_time untuk APPLYkueri.

Output dari sys.dm_exec_session_wait_stats:

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

The OPTIMIZE FORquery memiliki jenis menunggu tambahan, RESERVED_MEMORY_ALLOCATION_EXT . Saya tidak tahu persis apa artinya ini. Ini mungkin hanya pengukuran overhead dalam operator hash match (flow berbeda). Bagaimanapun, mungkin tidak ada gunanya mengkhawatirkan perbedaan 70 ms dalam waktu CPU.


1

Saya pikir Anda memiliki jawaban tentang mengapa
ini mungkin cara untuk mengatasinya.
Saya tahu itu terlihat berantakan tetapi rencana eksekusi mengatakan berbeda 2 teratas adalah 84% dari biaya

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;

Kode ini memakan waktu 5 detik di mesin saya. Sepertinya gabungan ke variabel tabel menambahkan sedikit overhead. Dalam kueri terakhir variabel tabel dipindai 892800 kali. Permintaan itu membutuhkan waktu CPU 1359 ms dan waktu yang telah berlalu 1374 ms. Jelas lebih dari yang saya harapkan. Menambahkan kunci utama ke variabel tabel tampaknya membantu, meskipun saya tidak yakin mengapa. Mungkin ada kemungkinan optimasi lainnya.
Joe Obbish
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.