Pembaruan 2014-12-18
Dengan respons yang luar biasa terhadap pertanyaan utama adalah "Tidak", respons yang lebih menarik telah difokuskan pada bagian 2, bagaimana menyelesaikan teka-teki kinerja dengan eksplisit ORDER BY
. Meskipun saya sudah menandai jawaban, saya tidak akan terkejut jika ada solusi kinerja yang lebih baik.
Asli
Pertanyaan ini muncul karena satu-satunya solusi yang sangat cepat yang dapat saya temukan untuk masalah tertentu hanya berfungsi tanpa sebuah ORDER BY
klausa. Di bawah ini adalah T-SQL lengkap yang diperlukan untuk menghasilkan masalah, bersama dengan solusi yang saya usulkan (Saya menggunakan SQL Server 2008 R2, jika itu penting.)
--Create Orders table
IF OBJECT_ID('tempdb..#Orders') IS NOT NULL DROP TABLE #Orders
CREATE TABLE #Orders
(
OrderID INT NOT NULL IDENTITY(1,1)
, CustID INT NOT NULL
, StoreID INT NOT NULL
, Amount FLOAT NOT NULL
)
CREATE CLUSTERED INDEX IX ON #Orders (StoreID, Amount DESC, CustID)
--Add 1 million rows w/ 100K Customers each of whom had 10 orders
;WITH
Cte0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows
Cte1 AS (SELECT 1 AS C FROM Cte0 AS A, Cte0 AS B),--4 rows
Cte2 AS (SELECT 1 AS C FROM Cte1 AS A ,Cte1 AS B),--16 rows
Cte3 AS (SELECT 1 AS C FROM Cte2 AS A ,Cte2 AS B),--256 rows
Cte4 AS (SELECT 1 AS C FROM Cte3 AS A ,Cte3 AS B),--65536 rows
Cte5 AS (SELECT 1 AS C FROM Cte4 AS A ,Cte2 AS B),--1048576 rows
FinalCte AS (SELECT ROW_NUMBER() OVER (ORDER BY C) AS Number FROM Cte5)
INSERT INTO #Orders (CustID, StoreID, Amount)
SELECT CustID = Number / 10
, StoreID = Number % 4
, Amount = 1000 * RAND(Number)
FROM FinalCte
WHERE Number <= 1000000
SET STATISTICS IO ON
SET STATISTICS TIME ON
--For StoreID = 1, find the top 500 customers ordered by their most expensive purchase (Amount)
--Solution A: Without ORDER BY
DECLARE @Top INT = 500
SELECT DISTINCT TOP (@Top) CustID
FROM #Orders WITH(FORCESEEK)
WHERE StoreID = 1
OPTION(OPTIMIZE FOR (@Top = 1), FAST 1);
--9 logical reads, CPU Time = 0 ms, elapsed time = 1 ms
GO
--Solution B: With ORDER BY
DECLARE @Top INT = 500
SELECT TOP (@Top) CustID
FROM #Orders
WHERE StoreID = 1
GROUP BY CustID
ORDER BY MAX(Amount) DESC
OPTION(MAXDOP 1)
--745 logical reads, CPU Time = 141 ms, elapsed time = 145 ms
--Uses Sort operator
GO
Berikut ini adalah rencana eksekusi untuk Solusi A dan B:
Solusi A memberikan kinerja yang saya butuhkan, tetapi saya tidak bisa membuatnya bekerja dengan kinerja yang sama ketika menambahkan jenis ORDER BY klausa (misalnya, lihat Solusi B). Dan sepertinya Solusi A harus mengirimkan hasilnya secara berurutan, karena 1) tabel hanya memiliki satu indeks di atasnya, 2) suatu pencarian terpaksa, sehingga menghilangkan kemungkinan menggunakan pemindaian alokasi alokasi berdasarkan halaman IAM .
Jadi pertanyaan saya adalah:
Apakah saya benar bahwa itu akan menjamin pesanan dalam kasus ini tanpa pesanan dengan klausa?
Jika tidak, apakah ada metode lain untuk memaksa rencana secepat Solusi A, lebih disukai yang menghindari jenis? Perhatikan bahwa itu harus menyelesaikan masalah yang sama persis (untuk
StoreID = 1
, temukan 500 pelanggan teratas yang dipesan dengan jumlah pembelian paling mahal). Itu juga harus tetap menggunakan#Orders
tabel, tetapi skema pengindeksan yang berbeda akan OK.
sumber
ORDER BY
.Jawaban:
Tidak . Perbedaan Arus yang menjaga ketertiban (memungkinkan
ORDER BY
tanpa pengurutan) tidak diterapkan di SQL Server hari ini. Hal ini dimungkinkan untuk dilakukan pada prinsipnya, tetapi kemudian banyak hal mungkin terjadi jika kita diizinkan untuk mengubah kode sumber SQL Server. Jika Anda dapat membuat kasus yang bagus untuk pekerjaan pengembangan ini, Anda dapat menyarankannya ke Microsoft .Iya nih. (Tabel & petunjuk kueri hanya diperlukan saat menggunakan penduga kardinalitas pra-2014):
Solusi SQL CLR
Script berikut ini menunjukkan menggunakan fungsi tabel-nilai SQL CLR untuk memenuhi persyaratan yang dinyatakan. Saya bukan pakar C #, jadi kode ini dapat mengalami peningkatan:
Tabel uji dan data sampel dari pertanyaan:
Tes fungsi:
Rencana pelaksanaan (perhatikan validasi
ORDER
jaminan):Di laptop saya, ini biasanya dijalankan dalam 80-100 ms. Ini sama sekali tidak secepat T-SQL menulis ulang di atas, tetapi harus menunjukkan stabilitas kinerja yang baik dalam menghadapi distribusi data yang berbeda.
Kode sumber:
sumber
Tanpa
ORDER BY
banyak hal bisa salah. Anda telah mengecualikan semua kemungkinan masalah yang dapat saya pikirkan, tetapi itu tidak berarti bahwa tidak ada masalah juga tidak akan ada satu di rilis mendatang.Ini seharusnya bekerja:
Tarik kumpulan 500 baris dari tabel dalam satu lingkaran dan berhenti ketika Anda memiliki 500 ID pelanggan yang berbeda. Kueri pengambilan dapat terlihat seperti ini:
Ini akan melakukan pemindaian rentang yang dipesan pada indeks. The
Amount <= @lastAmountFetched
predikat ada untuk secara bertahap menarik lebih banyak catatan. Setiap kueri hanya akan secara fisik menyentuh 500 catatan. Itu berarti O (1). Itu tidak menjadi lebih mahal semakin jauh Anda masuk ke dalam indeks.Anda harus mempertahankan variabel
@lastAmountFetched
agar berkurang ke nilai terkecil yang Anda ambil dalam pernyataan itu.Dengan cara ini Anda akan memindai indeks secara bertahap secara berurutan. Anda akan membaca paling banyak (500 - 1) baris lebih banyak dari jumlah optimal seharusnya.
Ini akan jauh lebih cepat daripada selalu mengumpulkan sekitar 100.000 pesanan untuk toko tertentu. Mungkin, hanya beberapa iterasi 500 baris yang akan dibutuhkan.
Pada dasarnya, ini adalah operator berbeda yang dikodekan secara manual.
Atau, gunakan kursor untuk mengambil baris sesedikit mungkin. Ini akan jauh lebih lambat karena mengeksekusi 500 query baris tunggal paling sering lebih lambat daripada mengeksekusi batch 500 baris.
Sebagai alternatif, cukup kueri semua baris tanpa
DISTINCT
dengan cara yang dipesan dan buat aplikasi klien mengakhiri kueri setelah cukup banyak baris dikembalikan (menggunakanSqlCommand.Cancel
).sumber
#fetchedOrders
tidak mengandung pelanggan yang telah kita lihat? Agaknya ini melibatkan pencarian indeks pada tabel temp, yang tidak cukup sama dengan "aliran berbeda" dan memang mendapatkan lebih mahal semakin banyak baris yang kita lihat (meskipun masih akan mengalahkan solusi B dalam semua tetapi kasus terburuk harus memindai semua baris karena hanya ada satu pelanggan, yang A dan B akan tampil secara identik).IGNORE_DUP_KEY
bisa melakukan itu.