Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Ambang Pengoptimalan – Pengelompokan dan Penggabungan Data, Bagian 3

Artikel ini adalah yang ketiga dari seri tentang ambang pengoptimalan untuk mengelompokkan dan menggabungkan data. Di Bagian 1 saya membahas algoritma Stream Aggregate yang telah dipesan sebelumnya. Di Bagian 2 saya membahas algoritma Sort + Stream Aggregate yang tidak dipesan sebelumnya. Di bagian ini saya membahas algoritma Hash Match (Aggregate), yang akan saya sebut sebagai Hash Aggregate. Saya juga memberikan ringkasan dan perbandingan antara algoritma yang saya bahas di Bagian 1, Bagian 2, dan Bagian 3.

Saya akan menggunakan database sampel yang sama yang disebut PerformanceV3, yang saya gunakan di artikel sebelumnya dalam seri ini. Pastikan bahwa sebelum Anda menjalankan contoh di artikel, Anda terlebih dahulu menjalankan kode berikut untuk menghapus beberapa indeks yang tidak diperlukan:

DROP INDEX idx_nc_sid_od_cid ON dbo.Orders;
DROP INDEX idx_unc_od_oid_i_cid_eid ON dbo.Orders;

Hanya dua indeks yang harus ditinggalkan pada tabel ini adalah idx_cl_od (dikelompokkan dengan orderdate sebagai kuncinya) dan PK_Orders (tidak dikelompokkan dengan orderid sebagai kuncinya).

Hash Agregat

Algoritma Hash Aggregate mengatur grup dalam tabel hash berdasarkan beberapa fungsi hash yang dipilih secara internal. Tidak seperti algoritma Stream Aggregate, tidak perlu menggunakan baris dalam urutan grup. Pertimbangkan kueri berikut (kami akan menyebutnya Kueri 1) sebagai contoh (memaksa agregat hash dan paket serial):

SELECT empid, COUNT(*) AS numorders
  FROM dbo.Orders
  GROUP BY empid
  OPTION (HASH GROUP, MAXDOP 1);

Gambar 1 menunjukkan rencana untuk Query 1.


Gambar 1:Rencana untuk Kueri 1

Paket memindai baris dari indeks berkerumun menggunakan properti Dipesan:Salah (pemindaian tidak diperlukan untuk mengirimkan baris yang diurutkan oleh kunci indeks). Biasanya, pengoptimal akan lebih memilih untuk memindai indeks penutup tersempit, yang dalam kasus kami adalah indeks berkerumun. Rencananya membuat tabel hash dengan kolom dan agregat yang dikelompokkan. Kueri kami meminta agregat COUNT bertipe INT. Rencana tersebut sebenarnya menghitungnya sebagai nilai yang diketik BIGINT, oleh karena itu operator Hitung Skalar, menerapkan konversi implisit ke INT.

Microsoft tidak secara publik membagikan algoritme hash yang mereka gunakan. Ini adalah teknologi yang sangat eksklusif. Namun, untuk mengilustrasikan konsep tersebut, anggaplah SQL Server menggunakan fungsi hash % 250 (modulo 250) untuk kueri kita di atas. Sebelum memproses baris apa pun, tabel hash kami memiliki 250 ember yang mewakili 250 kemungkinan hasil dari fungsi hash (0 hingga 249). Saat SQL Server memproses setiap baris, SQL Server menerapkan fungsi hash % 250. Hasilnya adalah penunjuk ke salah satu ember di tabel hash kami. Jika daftar tertaut ember belum menyertakan grup baris saat ini, SQL Server menambahkan grup baru ke daftar tertaut dengan kolom grup (empid dalam kasus kami) dan nilai agregat awal (hitung 1 dalam kasus kami). Jika grup sudah ada, SQL Server memperbarui agregat (menambahkan 1 ke hitungan dalam kasus kami). Sebagai contoh, anggaplah SQL Server memproses 10 baris berikut terlebih dahulu:

orderid empid 
------- ----- 
320     3
30      5
660     253
820     3
850     1
1000    255
700     3
1240    253
350     4
400     255

Gambar 2 menunjukkan tiga status tabel hash:sebelum setiap baris diproses, setelah 5 baris pertama diproses, dan setelah 10 baris pertama diproses. Setiap item dalam daftar tertaut menyimpan tuple (empid, COUNT(*)).


Gambar 2:Status tabel hash

Setelah operator Hash Aggregate selesai menggunakan semua baris input, tabel hash memiliki semua grup dengan status akhir agregat.

Seperti operator Sortir, operator Hash Aggregate membutuhkan hibah memori, dan jika kehabisan memori, perlu tumpah ke tempdb; namun, sementara pengurutan membutuhkan pemberian memori yang sebanding dengan jumlah baris yang akan diurutkan, hashing membutuhkan pemberian memori yang sebanding dengan jumlah grup. Jadi, terutama ketika kumpulan pengelompokan memiliki kepadatan tinggi (jumlah grup kecil), algoritme ini membutuhkan memori yang jauh lebih sedikit daripada saat pengurutan eksplisit diperlukan.

Pertimbangkan dua kueri berikut (sebut saja Kueri 1 dan Kueri 2):

SELECT empid, COUNT(*) AS numorders
  FROM dbo.Orders
  GROUP BY empid
  OPTION (HASH GROUP, MAXDOP 1);
 
SELECT empid, COUNT(*) AS numorders
  FROM dbo.Orders
  GROUP BY empid
  OPTION (ORDER GROUP, MAXDOP 1);

Gambar 3 membandingkan pemberian memori untuk kueri ini.


Gambar 3:Rencana untuk Kueri 1 dan Kueri 2

Perhatikan perbedaan besar antara pemberian memori dalam dua kasus.

Untuk biaya operator Hash Aggregate, kembali ke Gambar 1, perhatikan bahwa tidak ada biaya I/O, melainkan hanya biaya CPU. Selanjutnya, cobalah untuk merekayasa balik rumus penetapan biaya CPU menggunakan teknik serupa dengan yang saya bahas di bagian sebelumnya dalam seri ini. Faktor-faktor yang berpotensi mempengaruhi biaya operator adalah jumlah baris input, jumlah grup keluaran, fungsi agregat yang digunakan, dan apa yang Anda kelompokkan (kardinalitas kumpulan pengelompokan, tipe data yang digunakan).

Anda mengharapkan operator ini memiliki biaya awal dalam persiapan untuk membangun tabel hash. Anda juga mengharapkannya untuk menskalakan secara linier sehubungan dengan jumlah baris dan grup. Itu memang yang saya temukan. Namun, sementara biaya operator Stream Agregat dan Sortir tidak dipengaruhi oleh apa yang Anda kelompokkan, tampaknya biaya operator Hash Aggregate adalah—baik dalam hal kardinalitas kumpulan pengelompokan dan tipe data yang digunakan.

Untuk mengamati bahwa kardinalitas kumpulan pengelompokan memengaruhi biaya operator, periksa biaya CPU operator Hash Aggregate dalam paket untuk kueri berikut (sebut saja Kueri 3 dan Kueri 4):

SELECT orderid % 1000 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (20000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 1000
  OPTION(HASH GROUP, MAXDOP 1);
 
SELECT orderid % 50 AS grp1, orderid % 20 AS grp2, MAX(orderdate) AS maxod
  FROM (SELECT TOP (20000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 50, orderid % 20 
  OPTION(HASH GROUP, MAXDOP 1);

Tentu saja, Anda ingin memastikan bahwa perkiraan jumlah baris input dan grup output sama dalam kedua kasus. Perkiraan rencana untuk kueri ini ditunjukkan pada Gambar 4.


Gambar 4:Rencana untuk Kueri 3 dan Kueri 4

Seperti yang Anda lihat, biaya CPU dari operator Hash Aggregate adalah 0,16903 saat mengelompokkan menurut satu kolom bilangan bulat, dan 0,174016 saat mengelompokkan dengan dua kolom bilangan bulat, dengan yang lainnya sama. Artinya, kardinalitas himpunan pengelompokan memang mempengaruhi biaya.

Mengenai apakah tipe data dari elemen yang dikelompokkan memengaruhi biaya, saya menggunakan kueri berikut untuk memeriksanya (sebut saja Kueri 5, Kueri 6, dan Kueri 7):

SELECT CAST(orderid AS SMALLINT) % CAST(1000 AS SMALLINT) AS grp,
    MAX(orderdate) AS maxod
  FROM (SELECT TOP (20000) * FROM dbo.Orders) AS D
  GROUP BY CAST(orderid AS SMALLINT) % CAST(1000 AS SMALLINT)
  OPTION(HASH GROUP, MAXDOP 1);
 
SELECT orderid % 1000 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (20000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 1000
  OPTION(HASH GROUP, MAXDOP 1);
 
SELECT CAST(orderid AS BIGINT) % CAST(1000 AS BIGINT) AS grp,
    MAX(orderdate) AS maxod
  FROM (SELECT TOP (20000) * FROM dbo.Orders) AS D
  GROUP BY CAST(orderid AS BIGINT) % CAST(1000 AS BIGINT)
  OPTION(HASH GROUP, MAXDOP 1);

Paket untuk ketiga kueri memiliki perkiraan jumlah baris input dan grup keluaran yang sama, namun semuanya mendapatkan perkiraan biaya CPU yang berbeda (0,121766, 0,16903, dan 0,171716), oleh karena itu jenis data yang digunakan memang memengaruhi biaya.

Jenis fungsi agregat juga tampaknya berdampak pada biaya. Misalnya, pertimbangkan dua kueri berikut (sebut saja Kueri 8 dan Kueri 9):

SELECT orderid % 1000 AS grp, COUNT(*) AS numorders
  FROM (SELECT TOP (20000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 1000
  OPTION(HASH GROUP, MAXDOP 1);
 
SELECT orderid % 1000 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (20000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 1000
  OPTION(HASH GROUP, MAXDOP 1);

Perkiraan biaya CPU untuk Agregat Hash dalam paket untuk Kueri 8 adalah 0,166344, dan di Kueri 9 adalah 0,16903.

Ini bisa menjadi latihan yang menarik untuk mencoba dan mencari tahu dengan tepat bagaimana kardinalitas himpunan pengelompokan, tipe data, dan fungsi agregat yang digunakan mempengaruhi biaya; Saya hanya tidak mengejar aspek penetapan biaya ini. Jadi, setelah membuat pilihan kumpulan pengelompokan dan fungsi agregat untuk kueri Anda, Anda dapat merekayasa balik rumus penetapan biaya. Misalnya, mari kita rekayasa balik rumus biaya CPU untuk operator Hash Aggregate saat mengelompokkan berdasarkan satu kolom integer dan mengembalikan agregat MAX(orderdate). Rumusnya harus:

Biaya CPU operator = + @jumlah* + @numgroups *

Dengan menggunakan teknik yang saya tunjukkan dalam artikel sebelumnya di seri ini, saya mendapatkan rumus rekayasa terbalik berikut:

Biaya CPU operator =0,017749 + @numrows * 0,00000667857 + @numgroups * 0,0000177087

Anda dapat memeriksa keakuratan rumus menggunakan kueri berikut:

SELECT orderid % 1000 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (100000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 1000
  OPTION(HASH GROUP, MAXDOP 1);
 
SELECT orderid % 2000 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (100000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 2000
  OPTION(HASH GROUP, MAXDOP 1);
 
SELECT orderid % 3000 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (200000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 3000
  OPTION(HASH GROUP, MAXDOP 1);
 
SELECT orderid % 6000 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (200000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 6000
  OPTION(HASH GROUP, MAXDOP 1);
 
SELECT orderid % 5000 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (500000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 5000
  OPTION(HASH GROUP, MAXDOP 1);
 
SELECT orderid % 10000 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (500000) * FROM dbo.Orders) AS D
  GROUP BY orderid % 10000
  OPTION(HASH GROUP, MAXDOP 1);

Saya mendapatkan hasil berikut:

numrows     numgroups   predictedcost  estimatedcost
----------- ----------- -------------- --------------
100000      1000        0.703315       0.703316
100000      2000        0.721023       0.721024
200000      3000        1.40659        1.40659
200000      6000        1.45972        1.45972
500000      5000        3.44558        3.44558
500000      10000       3.53412        3.53412

Rumusnya tampaknya tepat.

Ringkasan dan perbandingan biaya

Sekarang kami memiliki rumus biaya untuk tiga strategi yang tersedia:Stream Aggregate yang telah dipesan sebelumnya, Sort + Stream Aggregate, dan Hash Aggregate. Tabel berikut merangkum dan membandingkan karakteristik penetapan biaya dari ketiga algoritme:

Agregat Aliran yang Dipesan di muka

Urutkan + Aliran Agregat

Hash Agregat

Rumus

@numrows * 0,0000006 +

@numgroups * 0,0000005

0,0112613 +

Sejumlah kecil baris:

9.99127891201865E-05 + @numrows * LOG(@numrows) * 2.25061348918698E-06

Jumlah baris yang banyak:

1.35166186417734E-04 + @numrows * LOG(@numrows) * 6.62193536908588E-06

+

@numrows * 0,0000006 +

@numgroups * 0,0000005

0,017749 +

@numrows * 0,00000667857 +

@numgroups * 0,0000177087

* Pengelompokan menurut kolom bilangan bulat tunggal, menghasilkan MAX()

Penskalaan

linier

n log n

linier

Biaya I/O Startup

tidak ada

0,0112613

tidak ada

Biaya CPU Startup

tidak ada

~ 0,0001

0,017749

Menurut rumus ini, Gambar 5 menunjukkan cara masing-masing strategi menskalakan jumlah baris input yang berbeda, menggunakan jumlah grup tetap 500 sebagai contoh.


Gambar 5:Biaya algoritme agregat

Anda dapat dengan jelas melihat bahwa jika data telah dipesan sebelumnya, misalnya, dalam indeks penutup, strategi Stream Aggregate yang telah dipesan sebelumnya adalah pilihan terbaik, terlepas dari berapa banyak baris yang terlibat. Misalnya, Anda perlu membuat kueri tabel Pesanan, dan menghitung tanggal pesanan maksimum untuk setiap karyawan. Anda membuat indeks penutup berikut:

CREATE INDEX idx_eid_od ON dbo.Orders(empid, orderdate);

Berikut adalah lima kueri, meniru tabel Pesanan dengan jumlah baris yang berbeda (10.000, 20.000, 30.000, 40.000, dan 50.000):

SELECT empid, MAX(orderdate) AS maxod
  FROM (SELECT TOP (10000) * FROM dbo.Orders) AS D
  GROUP BY empid;
 
SELECT empid, MAX(orderdate) AS maxod
  FROM (SELECT TOP (20000) * FROM dbo.Orders) AS D
  GROUP BY empid;
 
SELECT empid, MAX(orderdate) AS maxod
  FROM (SELECT TOP (30000) * FROM dbo.Orders) AS D
  GROUP BY empid;
 
SELECT empid, MAX(orderdate) AS maxod
  FROM (SELECT TOP (40000) * FROM dbo.Orders) AS D
  GROUP BY empid;
 
SELECT empid, MAX(orderdate) AS maxod
  FROM (SELECT TOP (50000) * FROM dbo.Orders) AS D
  GROUP BY empid;

Gambar 6 menunjukkan rencana untuk kueri ini.


Gambar 6:Rencana dengan strategi Stream Agregat yang telah dipesan sebelumnya

Dalam semua kasus, paket melakukan pemindaian berurutan dari indeks penutup, dan menerapkan operator Stream Aggregate tanpa perlu penyortiran eksplisit.

Gunakan kode berikut untuk menghapus indeks yang Anda buat untuk contoh ini:

DROP INDEX idx_eid_od ON dbo.Orders;

Hal penting lainnya yang perlu diperhatikan tentang grafik pada Gambar 5 adalah apa yang terjadi ketika data tidak diurutkan sebelumnya. Ini terjadi saat tidak ada indeks penutup, serta saat Anda mengelompokkan menurut ekspresi yang dimanipulasi sebagai lawan dari kolom dasar. Ada ambang pengoptimalan—dalam kasus kami di 1454.046 baris—di bawahnya strategi Sort + Stream Aggregate memiliki biaya lebih rendah, dan pada atau di atasnya strategi Hash Aggregate memiliki biaya lebih rendah. Ini ada hubungannya dengan fakta bahwa hash yang pertama memiliki biaya startup yang lebih rendah, tetapi skala dengan cara n log n, sedangkan yang terakhir memiliki biaya startup yang lebih tinggi tetapi skala secara linier. Ini membuat yang pertama lebih disukai dengan sejumlah kecil baris input. Jika rumus rekayasa balik kami akurat, dua kueri berikut (sebut saja Kueri 10 dan Kueri 11) akan mendapatkan paket yang berbeda:

SELECT orderid % 500 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (1454) * FROM dbo.Orders) AS D
  GROUP BY orderid % 500;
 
SELECT orderid % 500 AS grp, MAX(orderdate) AS maxod
  FROM (SELECT TOP (1455) * FROM dbo.Orders) AS D
  GROUP BY orderid % 500;

Rencana untuk kueri ini ditunjukkan pada Gambar 7.


Gambar 7:Rencana untuk Kueri 10 dan Kueri 11

Memang, dengan 1.454 baris input (di bawah ambang pengoptimalan), pengoptimal secara alami lebih memilih algoritma Sortir + Aliran Agregat untuk Kueri 10, dan dengan 1.455 baris input (di atas ambang pengoptimalan), pengoptimal secara alami lebih memilih algoritme Hash Agregat untuk Kueri 11 .

Potensi untuk operator Agregat Adaptif

Salah satu aspek rumit dari ambang optimasi adalah masalah parameter sniffing saat menggunakan kueri sensitif parameter dalam prosedur tersimpan. Pertimbangkan prosedur tersimpan berikut sebagai contoh:

CREATE OR ALTER PROC dbo.EmpOrders
  @initialorderid AS INT
AS
  SELECT empid, COUNT(*) AS numorders
  FROM dbo.Orders
  WHERE orderid >= @initialorderid
  GROUP BY empid;
GO

Anda membuat indeks optimal berikut untuk mendukung kueri prosedur tersimpan:

CREATE INDEX idx_oid_i_eid ON dbo.Orders(orderid) INCLUDE(empid);

Anda membuat indeks dengan orderid sebagai kunci untuk mendukung filter rentang kueri, dan menyertakan empid untuk cakupan. Ini adalah situasi di mana Anda tidak dapat benar-benar membuat indeks yang akan mendukung filter rentang dan baris yang difilter diurutkan sebelumnya oleh kumpulan pengelompokan. Ini berarti bahwa pengoptimal harus membuat pilihan antara Sortir + aliran Agregat dan algoritma Agregat Hash. Berdasarkan rumus penetapan biaya kami, ambang pengoptimalan adalah antara 937 dan 938 baris masukan (misalnya, 937,5 baris).

Misalkan Anda menjalankan prosedur tersimpan pertama kali dengan masukan ID pesanan awal yang memberi Anda sedikit kecocokan (di bawah ambang batas):

EXEC dbo.EmpOrders @initialorderid = 999991;

SQL Server menghasilkan rencana yang menggunakan algoritma Sort + Stream Agregat, dan menyimpan rencana tersebut. Eksekusi selanjutnya menggunakan kembali paket yang di-cache, terlepas dari berapa banyak baris yang terlibat. Misalnya, eksekusi berikut memiliki sejumlah kecocokan di atas ambang pengoptimalan:

EXEC dbo.EmpOrders @initialorderid = 990001;

Namun, itu menggunakan kembali paket yang di-cache meskipun faktanya tidak optimal untuk eksekusi ini. Itu adalah masalah sniffing parameter klasik.

SQL Server 2017 memperkenalkan kemampuan pemrosesan kueri adaptif, yang dirancang untuk mengatasi situasi seperti itu dengan menentukan selama eksekusi kueri strategi mana yang akan digunakan. Di antara peningkatan tersebut adalah operator Adaptive Join yang selama eksekusi menentukan apakah akan mengaktifkan algoritma Loop atau Hash berdasarkan ambang pengoptimalan yang dihitung. Cerita agregat kami meminta operator Agregat Adaptif yang serupa, yang selama eksekusi akan membuat pilihan antara strategi Urutkan + Aliran Agregat dan strategi Agregat Hash, berdasarkan ambang pengoptimalan yang dihitung. Gambar 8 mengilustrasikan rencana semu berdasarkan ide ini.


Gambar 8:Paket semu dengan operator Adaptive Aggregate

Untuk saat ini, untuk mendapatkan paket seperti itu, Anda perlu menggunakan Microsoft Paint. Namun karena konsep pemrosesan kueri adaptif sangat cerdas dan bekerja dengan sangat baik, masuk akal untuk mengharapkan peningkatan lebih lanjut di area ini di masa mendatang.

Jalankan kode berikut untuk menghapus indeks yang Anda buat untuk contoh ini:

DROP INDEX idx_oid_i_eid ON dbo.Orders;

Kesimpulan

Dalam artikel ini saya membahas penetapan biaya dan penskalaan algoritma Hash Aggregate dan membandingkannya dengan strategi Stream Aggregate dan Sort + Stream Aggregate. Saya menjelaskan ambang pengoptimalan yang ada antara strategi Sort + Stream Aggregate dan Hash Aggregate. Dengan sejumlah kecil baris input, yang pertama lebih disukai dan dengan jumlah besar yang terakhir. Saya juga menjelaskan potensi untuk menambahkan operator Adaptive Aggregate, mirip dengan operator Adaptive Join yang sudah diterapkan, untuk membantu menangani masalah sniffing parameter saat menggunakan kueri sensitif parameter. Bulan depan saya akan melanjutkan diskusi dengan membahas pertimbangan paralelisme dan kasus yang memerlukan penulisan ulang kueri.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Cara Membatasi Hasil di T-SQL

  2. Model Basis Data untuk Survei Online. Bagian 1

  3. Meminimalkan dampak pelebaran kolom IDENTITAS – bagian 1

  4. Panduan untuk Fungsi PubNub

  5. Cara Mengelompokkan Berdasarkan Tahun di SQL