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

Kejutan dan Asumsi Kinerja:TOP Sewenang-wenang 1

Dalam utas terbaru di StackExchange, pengguna mengalami masalah berikut:

Saya ingin kueri yang mengembalikan orang pertama dalam tabel dengan GroupID =2. Jika tidak ada orang dengan GroupID =2, saya ingin orang pertama dengan RoleID =2.

Mari kita singkirkan, untuk saat ini, fakta bahwa "pertama" sangat ditentukan. Sebenarnya, pengguna tidak peduli orang mana yang mereka dapatkan, apakah itu datang secara acak, sewenang-wenang, atau melalui beberapa logika eksplisit selain kriteria utama mereka. Mengabaikan itu, katakanlah Anda memiliki tabel dasar:

CREATE TABLE dbo.Users
(
  UserID  INT PRIMARY KEY,
  GroupID INT,
  RoleID  INT
);

Di dunia nyata mungkin ada kolom lain, batasan tambahan, mungkin kunci asing ke tabel lain, dan tentu saja indeks lainnya. Tapi mari kita buat ini tetap sederhana, dan buat kueri.

Kemungkinan Solusi

Dengan desain meja seperti itu, menyelesaikan masalah tampak mudah bukan? Upaya pertama yang mungkin Anda lakukan adalah:

SELECT TOP (1) UserID, GroupID, RoleID
  FROM dbo.Users
  WHERE GroupID = 2 OR RoleID = 2
  ORDER BY CASE GroupID WHEN 2 THEN 1 ELSE 2 END;

Ini menggunakan TOP dan ORDER BY . bersyarat untuk memperlakukan pengguna dengan GroupID =2 sebagai prioritas yang lebih tinggi. Rencana untuk kueri ini cukup sederhana, dengan sebagian besar biaya terjadi dalam operasi sortir. Berikut adalah metrik runtime terhadap tabel kosong:

Sepertinya ini sebaik yang bisa Anda lakukan – rencana sederhana yang hanya memindai tabel sekali, dan selain jenis sial yang seharusnya bisa Anda jalani, tidak masalah, bukan?

Nah, jawaban lain di utas menawarkan variasi yang lebih kompleks ini:

SELECT TOP (1) UserID, GroupID, RoleID FROM 
(
  SELECT TOP (1) UserID, GroupID, RoleID, o = 1
  FROM dbo.Users
  WHERE GroupId = 2 
 
  UNION ALL
 
  SELECT TOP (1) UserID, GroupID, RoleID, o = 2
  FROM dbo.Users
  WHERE RoleID = 2
) 
AS x ORDER BY o;

Pada pandangan pertama, Anda mungkin akan berpikir bahwa kueri ini sangat kurang efisien, karena memerlukan dua pemindaian indeks berkerumun. Anda pasti benar tentang itu; berikut adalah rencana dan metrik runtime terhadap tabel kosong:

Tapi sekarang, mari kita tambahkan data

Untuk menguji kueri ini, saya ingin menggunakan beberapa data realistis. Jadi pertama-tama saya mengisi 1.000 baris dari sys.all_objects, dengan operasi modulo terhadap object_id untuk mendapatkan distribusi yang layak:

INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (1000) ABS([object_id]), ABS([object_id]) % 7, ABS([object_id]) % 4
FROM sys.all_objects
ORDER BY [object_id]; 
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 126
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 248
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 26 overlap

Sekarang ketika saya menjalankan dua kueri, berikut adalah metrik waktu proses:

Versi UNION ALL hadir dengan I/O yang sedikit lebih sedikit (4 pembacaan vs. 5), durasi yang lebih rendah, dan perkiraan biaya keseluruhan yang lebih rendah, sedangkan versi ORDER BY bersyarat memiliki perkiraan biaya CPU yang lebih rendah. Data di sini cukup kecil untuk membuat kesimpulan tentang; Saya hanya menginginkannya sebagai taruhan di tanah. Sekarang, mari kita ubah distribusinya sehingga sebagian besar baris memenuhi setidaknya salah satu kriteria (dan terkadang keduanya):

DROP TABLE dbo.Users;
GO
 
CREATE TABLE dbo.Users
(
  UserID INT PRIMARY KEY,
  GroupID INT,
  RoleID INT
);
GO
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (1000) ABS([object_id]), ABS([object_id]) % 2 + 1, 
  SUBSTRING(RTRIM([object_id]),7,1) % 2 + 1
FROM sys.all_objects
WHERE ABS([object_id]) > 9999999
ORDER BY [object_id]; 
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 500
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 475
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 221 overlap

Kali ini, urutan bersyarat menurut memiliki perkiraan biaya tertinggi di CPU dan I/O:

Tetapi sekali lagi, pada ukuran data ini, ada dampak yang relatif tidak penting terhadap durasi dan pembacaan, dan selain dari perkiraan biaya (yang sebagian besar dibuat), sulit untuk menyatakan pemenang di sini.

Jadi, mari kita tambahkan lebih banyak data

Sementara saya lebih suka membangun sampel data dari tampilan katalog, karena semua orang memilikinya, kali ini saya akan menggambar di atas tabel Sales.SalesOrderHeaderEnlarged dari AdventureWorks2012, diperluas menggunakan skrip ini dari Jonathan Kehayias. Di sistem saya, tabel ini memiliki 1.258.600 baris. Skrip berikut akan menyisipkan satu juta baris tersebut ke dalam tabel dbo.Users kami:

-- DROP and CREATE, as before
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (1000000) SalesOrderID, SalesOrderID % 7, SalesOrderID % 4
FROM Sales.SalesOrderHeaderEnlarged;
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 142,857
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 250,000
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 35,714 overlap

Oke, sekarang ketika kita menjalankan kueri, kita melihat masalah:variasi ORDER BY telah menjadi paralel dan telah menghapus pembacaan dan CPU, menghasilkan perbedaan durasi hampir 120X:

Menghilangkan paralelisme (menggunakan MAXDOP) tidak membantu:

(Paket UNION ALL masih terlihat sama.)

Dan jika kita mengubah kemiringan menjadi genap, di mana 95% baris memenuhi setidaknya satu kriteria:

-- DROP and CREATE, as before
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (475000) SalesOrderID, 2, SalesOrderID % 7
FROM Sales.SalesOrderHeaderEnlarged
WHERE SalesOrderID % 2 = 1
UNION ALL
SELECT TOP (475000) SalesOrderID, SalesOrderID % 7, 2
FROM Sales.SalesOrderHeaderEnlarged
WHERE SalesOrderID % 2 = 0;
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (50000) SalesOrderID, 1, 1
FROM Sales.SalesOrderHeaderEnlarged AS h
WHERE NOT EXISTS (SELECT 1 FROM dbo.Users
  WHERE UserID = h.SalesOrderID);
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 542,851
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 542,851
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 135,702 overlap

Kueri masih menunjukkan bahwa pengurutan itu sangat mahal:

Dan dengan MAXDOP =1 jauh lebih buruk (lihat saja durasinya):

Terakhir, bagaimana kira-kira 95% condong ke kedua arah (mis. sebagian besar baris memenuhi kriteria GroupID, atau sebagian besar baris memenuhi kriteria RoleID)? Skrip ini akan memastikan setidaknya 95% data memiliki GroupID =2:

-- DROP and CREATE, as before
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (950000) SalesOrderID, 2, SalesOrderID % 7
FROM Sales.SalesOrderHeaderEnlarged;
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (50000) SalesOrderID, SalesOrderID % 7, 2
FROM Sales.SalesOrderHeaderEnlarged AS h
WHERE NOT EXISTS (SELECT 1 FROM dbo.Users
  WHERE UserID = h.SalesOrderID);
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 957,143
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 185,714
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 142,857 overlap

Hasilnya cukup mirip (saya hanya akan berhenti mencoba MAXDOP mulai sekarang):

Dan kemudian jika kita condong ke arah lain, di mana setidaknya 95% data memiliki RoleID =2:

-- DROP and CREATE, as before
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (950000) SalesOrderID, 2, SalesOrderID % 7
FROM Sales.SalesOrderHeaderEnlarged;
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (50000) SalesOrderID, SalesOrderID % 7, 2
FROM Sales.SalesOrderHeaderEnlarged AS h
WHERE NOT EXISTS (SELECT 1 FROM dbo.Users
  WHERE UserID = h.SalesOrderID);
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 185,714
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 957,143
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 142,857 overlap

Hasil:

Kesimpulan

Tidak ada satu pun kasus yang dapat saya buat melakukan kueri ORDER BY "lebih sederhana" – bahkan dengan satu pemindaian indeks yang lebih sedikit – mengungguli kueri UNION ALL yang lebih kompleks. Terkadang Anda harus sangat berhati-hati tentang apa yang harus dilakukan SQL Server saat Anda memperkenalkan operasi seperti sort ke dalam semantik kueri Anda, dan tidak bergantung pada kesederhanaan rencana saja (tidak peduli bias apa pun yang mungkin Anda miliki berdasarkan skenario sebelumnya).

Naluri pertama Anda mungkin sering benar, tetapi saya yakin ada kalanya ada opsi yang lebih baik yang terlihat, di permukaan, seperti itu tidak mungkin berhasil dengan lebih baik. Seperti dalam contoh ini. Saya menjadi sedikit lebih baik dalam mempertanyakan asumsi yang saya buat dari pengamatan, dan tidak membuat pernyataan menyeluruh seperti "pemindaian tidak pernah berkinerja baik" dan "kueri yang lebih sederhana selalu berjalan lebih cepat." Jika Anda menghilangkan kata tidak pernah dan selalu dari kosakata Anda, Anda mungkin mendapati diri Anda lebih banyak menguji asumsi dan pernyataan tersebut, dan berakhir dengan jauh lebih baik.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. SQL Setara (=) Operator untuk Pemula

  2. Pengantar Model Data ER

  3. Enkripsi Data Transparan dan Selalu Terenkripsi

  4. Apakah Anda selalu membutuhkan database untuk aplikasi Anda?

  5. Prosedur Tersimpan untuk Mendapatkan Pengaturan Instans