Bagian jawaban
Ada berbagai cara untuk menulis ulang ini menggunakan konstruksi T-SQL yang berbeda. Kami akan melihat pro dan kontra dan melakukan perbandingan secara keseluruhan di bawah ini.
Pertama : MenggunakanOR
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;
Menggunakan OR
memberi kita rencana pencarian yang lebih efisien, yang membaca jumlah baris yang tepat yang kita butuhkan, namun menambahkan apa yang dunia teknis sebut a whole mess of malarkey
dengan rencana kueri.
Perhatikan juga bahwa Seek dieksekusi dua kali di sini, yang seharusnya lebih jelas dari operator grafis:
Table 'Users'. Scan count 2, logical reads 8233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 469 ms, elapsed time = 473 ms.
Kedua : Menggunakan tabel turunan dengan UNION ALL
kueri kami juga dapat ditulis ulang seperti ini
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records);
Ini menghasilkan jenis rencana yang sama, dengan malarkey jauh lebih sedikit, dan tingkat kejujuran yang lebih jelas tentang berapa kali indeks dicari (dicari?) Menjadi.
Ia melakukan jumlah pembacaan yang sama (8233) dengan OR
kueri, tetapi mencukur waktu istirahat CPU sekitar 100 ms.
CPU time = 313 ms, elapsed time = 315 ms.
Namun, Anda harus benar - benar berhati - hati di sini, karena jika rencana ini mencoba berjalan paralel, dua COUNT
operasi terpisah akan diserialisasi, karena masing-masing dianggap sebagai agregat skalar global. Jika kita memaksakan rencana paralel menggunakan Trace Flag 8649, masalahnya menjadi jelas.
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Ini dapat dihindari dengan sedikit mengubah kueri kami.
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Sekarang kedua node melakukan Seek sepenuhnya diparalelkan sampai kita menekan operator gabungan.
Untuk apa nilainya, versi paralel sepenuhnya memiliki beberapa manfaat baik. Dengan biaya sekitar 100 lebih banyak bacaan, dan sekitar 90ms waktu CPU tambahan, waktu yang telah berlalu menyusut menjadi 93ms.
Table 'Users'. Scan count 12, logical reads 8317, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 500 ms, elapsed time = 93 ms.
Bagaimana dengan CROSS BERLAKU?
Tidak ada jawaban yang lengkap tanpa keajaiban CROSS APPLY
!
Sayangnya, kami mengalami lebih banyak masalah dengan COUNT
.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Rencana ini mengerikan. Ini adalah jenis rencana yang Anda miliki ketika Anda muncul terakhir untuk Hari St. Patrick. Meskipun paralel dengan baik, untuk beberapa alasan pemindaian PK / CX. Ew. Paket tersebut memiliki biaya 2.198 dolar permintaan.
Table 'Users'. Scan count 7, logical reads 31676233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 29532 ms, elapsed time = 5828 ms.
Yang merupakan pilihan aneh, karena jika kita memaksanya untuk menggunakan indeks nonclustered, biaya turun agak signifikan menjadi 1798 dolar permintaan.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Hei, cari! Lihat kamu di sana. Juga perhatikan bahwa dengan keajaiban CROSS APPLY
, kita tidak perlu melakukan apa pun yang konyol untuk memiliki rencana yang sepenuhnya paralel.
Table 'Users'. Scan count 5277838, logical reads 31685303, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 27625 ms, elapsed time = 4909 ms.
Silang berlaku tidak berakhir lebih baik tanpa COUNT
barang - barang di sana.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Rencananya terlihat bagus, tetapi pembacaan dan CPU bukan perbaikan.
Table 'Users'. Scan count 20, logical reads 17564, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 4844 ms, elapsed time = 863 ms.
Menulis ulang tanda silang berlaku untuk hasil gabungan turunan dalam segala hal yang persis sama. Saya tidak akan memposting ulang paket kueri dan info statistik - mereka benar-benar tidak berubah.
SELECT COUNT(u.Id)
FROM dbo.Users AS u
JOIN
(
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x ON x.Id = u.Id;
Aljabar Relasional : Agar teliti, dan untuk mencegah Joe Celko menghantui mimpiku, setidaknya kita perlu mencoba beberapa hal relasional yang aneh. Ini dia!
Upaya dengan INTERSECT
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
INTERSECT
SELECT u.Age WHERE u.Age IS NOT NULL );
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 1094 ms, elapsed time = 1090 ms.
Dan inilah upaya dengan EXCEPT
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
EXCEPT
SELECT u.Age WHERE u.Age IS NULL);
Table 'Users'. Scan count 7, logical reads 9247, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 2126 ms, elapsed time = 376 ms.
Mungkin ada cara lain untuk menulis ini, tetapi saya akan menyerahkannya kepada orang-orang yang mungkin menggunakan EXCEPT
dan INTERSECT
lebih sering daripada saya.
Jika Anda benar-benar hanya perlu hitungan yang
saya gunakan COUNT
dalam pertanyaan saya sebagai sedikit singkatan (baca: Saya terlalu malas untuk datang dengan skenario yang lebih terlibat kadang-kadang). Jika Anda hanya perlu menghitung, Anda dapat menggunakan CASE
ekspresi untuk melakukan hal yang sama.
SELECT SUM(CASE WHEN u.Age < 18 THEN 1
WHEN u.Age IS NULL THEN 1
ELSE 0 END)
FROM dbo.Users AS u
SELECT SUM(CASE WHEN u.Age < 18 OR u.Age IS NULL THEN 1
ELSE 0 END)
FROM dbo.Users AS u
Keduanya mendapatkan paket yang sama dan memiliki karakteristik CPU dan membaca yang sama.
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 719 ms, elapsed time = 719 ms.
Pemenang?
Dalam pengujian saya, rencana paralel yang dipaksakan dengan SUM atas tabel turunan menunjukkan kinerja terbaik. Dan ya, banyak dari pertanyaan ini dapat dibantu dengan menambahkan beberapa indeks yang disaring untuk menjelaskan kedua predikat, tetapi saya ingin meninggalkan beberapa eksperimen kepada yang lain.
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Terima kasih!
NOT EXISTS ( INTERSECT / EXCEPT )
query dapat bekerja tanpaINTERSECT / EXCEPT
bagian:WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );
Cara lain - yang menggunakanEXCEPT
:SELECT COUNT(*) FROM (SELECT UserID FROM dbo.Users EXCEPT SELECT UserID FROM dbo.Users WHERE u.Age >= 18) AS u ;
(di mana UserID adalah PK atau tidak kolom nol unik (s)).SELECT result = (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age IS NULL) ;
Maaf jika saya ketinggalan dalam jutaan versi yang telah Anda uji!UNION ALL
rencana (360ms CPU, 11k dibaca).Saya bukan game untuk mengembalikan database 110 GB hanya untuk satu tabel jadi saya membuat data saya sendiri . Distribusi usia harus cocok dengan apa yang ada di Stack Overflow tetapi jelas tabel itu sendiri tidak akan cocok. Saya tidak berpikir bahwa itu terlalu banyak masalah karena permintaan akan mencapai indeks. Saya sedang menguji pada komputer 4 CPU dengan SQL Server 2016 SP1. Satu hal yang perlu diperhatikan adalah bahwa untuk kueri yang menyelesaikan ini dengan cepat, penting untuk tidak menyertakan rencana eksekusi yang sebenarnya. Itu bisa memperlambat segalanya sedikit.
Saya mulai dengan menelusuri beberapa solusi dalam jawaban Erik yang luar biasa. Untuk yang ini:
Saya mendapat hasil berikut dari sys.dm_exec_sessions selama 10 percobaan (kueri secara alami berjalan paralel untuk saya):
Permintaan yang bekerja lebih baik untuk Erik sebenarnya berkinerja lebih buruk di mesin saya:
Hasil dari 10 percobaan:
Saya tidak dapat segera menjelaskan mengapa itu seburuk itu, tetapi tidak jelas mengapa kami ingin memaksa hampir setiap operator dalam rencana kueri untuk paralel. Dalam paket awal kami memiliki zona seri yang menemukan semua baris
AGE < 18
. Hanya ada beberapa ribu baris. Di mesin saya, saya mendapatkan 9 pembacaan logis untuk bagian dari permintaan dan 9 ms dari waktu CPU yang dilaporkan dan waktu yang berlalu. Ada juga zona serial untuk agregat global untuk baris denganAGE IS NULL
tetapi yang hanya memproses satu baris per DOP. Di mesin saya ini hanya empat baris.Hasil saya adalah bahwa yang paling penting adalah mengoptimalkan bagian dari kueri yang menemukan baris dengan
NULL
untukAge
karena ada jutaan baris itu. Saya tidak dapat membuat indeks dengan halaman yang lebih sedikit yang mencakup data daripada halaman yang dikompres pada kolom. Saya berasumsi bahwa ada ukuran indeks minimum per baris atau banyak ruang indeks tidak dapat dihindari dengan trik yang saya coba. Jadi jika kita terjebak dengan jumlah pembacaan logis yang sama untuk mendapatkan data maka satu-satunya cara untuk membuatnya lebih cepat adalah dengan membuat kueri lebih paralel, tetapi ini perlu dilakukan dengan cara yang berbeda dari kueri Erik yang menggunakan TF 8649. Dalam kueri di atas kami memiliki rasio 3,62 untuk waktu CPU dengan waktu yang berlalu yang cukup bagus. Idealnya adalah rasio 4.0 pada mesin saya.Salah satu bidang peningkatan yang mungkin adalah untuk membagi pekerjaan lebih merata di antara utas. Pada tangkapan layar di bawah ini, kita dapat melihat bahwa salah satu CPU saya memutuskan untuk beristirahat sejenak:
Pemindaian indeks adalah salah satu dari sedikit operator yang dapat diimplementasikan secara paralel dan kami tidak dapat melakukan apa pun tentang bagaimana baris didistribusikan ke utas. Ada elemen peluang untuk itu juga, tetapi cukup konsisten saya melihat satu utas yang masih berjalan. Salah satu cara untuk mengatasi ini adalah dengan melakukan paralelisme dengan cara yang sulit: pada bagian dalam dari loop bersarang bergabung. Apa pun di bagian dalam loop bersarang akan diimplementasikan secara serial tetapi banyak utas serial dapat berjalan secara bersamaan. Selama kita mendapatkan metode distribusi paralel yang menguntungkan (seperti round robin), kita dapat mengontrol dengan tepat berapa banyak baris yang dikirim ke setiap utas.
Saya menjalankan kueri dengan DOP 4 jadi saya harus membagi
NULL
baris dalam tabel menjadi empat ember secara merata . Salah satu cara untuk melakukan ini adalah dengan membuat sekelompok indeks pada kolom yang dihitung:Saya tidak yakin mengapa empat indeks terpisah sedikit lebih cepat dari satu indeks, tetapi itulah yang saya temukan dalam pengujian saya.
Untuk mendapatkan paket loop bersarang paralel, saya akan menggunakan flag jejak tidak berdokumen 8649 . Saya juga akan menulis kode sedikit aneh untuk mendorong pengoptimal agar tidak memproses lebih banyak baris daripada yang diperlukan. Di bawah ini adalah salah satu implementasi yang tampaknya berfungsi dengan baik:
Hasil dari sepuluh percobaan:
Dengan kueri itu, kami memiliki rasio waktu CPU dengan waktu berlalu 3,85! Kami mencukur 17 ms dari runtime dan hanya butuh 4 kolom dan indeks yang dihitung untuk melakukannya! Setiap utas memproses sangat dekat dengan jumlah baris yang sama secara keseluruhan karena setiap indeks memiliki sangat dekat dengan jumlah baris yang sama dan setiap utas hanya memindai satu indeks:
Pada catatan akhir, kita juga dapat menekan tombol mudah dan menambahkan CCI yang tidak tercakup ke dalam
Age
kolom:Kueri berikut selesai dalam 3 ms pada mesin saya:
Itu akan sulit dikalahkan.
sumber
Meskipun saya tidak memiliki salinan lokal dari database Stack Overflow, saya dapat mencoba beberapa pertanyaan. Pikir saya adalah untuk mendapatkan hitungan pengguna dari tampilan katalog sistem (sebagai lawan langsung mendapatkan hitungan baris dari tabel yang mendasarinya). Kemudian dapatkan hitungan baris yang sesuai (atau mungkin tidak) sesuai dengan kriteria Erik, dan lakukan beberapa matematika sederhana.
Saya menggunakan Stack Exchange Data Explorer (Bersama
SET STATISTICS TIME ON;
danSET STATISTICS IO ON;
) untuk menguji kueri. Untuk referensi, berikut adalah beberapa pertanyaan dan statistik CPU / IO:QUERY 1
QUERY 2
QUERY 3
Percobaan 1
Ini lebih lambat dari semua pertanyaan Erik yang saya daftarkan di sini ... setidaknya dalam hal waktu yang telah berlalu.
Percobaan ke-2
Di sini saya memilih variabel untuk menyimpan jumlah total pengguna (bukan sub-kueri). Jumlah pindaian meningkat dari 1 menjadi 17 dibandingkan dengan upaya pertama. Bacaan logis tetap sama. Namun, waktu yang berlalu menurun drastis.
Catatan Lain: DBCC TRACEON tidak diizinkan di Stack Exchange Data Explorer, seperti disebutkan di bawah ini:
sumber
SELECT SUM(p.Rows) - (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age >= 18 ) FROM sys.partitions p WHERE p.index_id < 2 AND p.object_id = OBJECT_ID('dbo.Users')
Gunakan variabel?
Per komentar dapat melewati variabel
sumber
SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
Menggunakan dengan baik
SET ANSI_NULLS OFF;
Ini adalah sesuatu yang baru saja muncul di pikiran saya. Hanya menjalankan ini di https://data.stackexchange.com
Tapi tidak seefisien @blitz_erik
sumber
Solusi sepele adalah menghitung hitung (*) - hitung (usia> = 18):
Atau:
Hasil di sini
sumber