Mengapa ada perbedaan rencana eksekusi antara OFFSET ... FETCH dan skema ROW_NUMBER gaya lama?

15

Model baru OFFSET ... FETCHyang diperkenalkan dengan SQL Server 2012 menawarkan paging yang sederhana dan lebih cepat. Mengapa ada perbedaan sama sekali mengingat kedua bentuk itu secara semantik identik dan sangat umum?

Orang akan berasumsi bahwa optimizer mengenali keduanya dan mengoptimalkannya (sepele) sepenuhnya.

Berikut ini adalah kasus yang sangat sederhana di mana OFFSET ... FETCH~ 2x lebih cepat sesuai dengan perkiraan biaya.

SELECT * INTO #objects FROM sys.objects

SELECT *
FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
) x
WHERE r >= 30 AND r < (30 + 10)
    ORDER BY object_id

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

offset-fetch.png

Satu dapat memvariasikan test case ini dengan membuat CI pada object_idatau menambahkan filter tetapi tidak mungkin untuk menghapus semua perbedaan rencana. OFFSET ... FETCHselalu lebih cepat karena itu kurang bekerja pada waktu eksekusi.

usr
sumber
Tidak terlalu yakin, jadi cantumkan sebagai komentar, tapi saya rasa ini karena Anda memiliki urutan berdasarkan kondisi yang sama untuk penomoran baris dan hasil akhir yang ditetapkan. Karena dalam kondisi ke-2, pengoptimal mengetahui hal ini, tidak perlu menyortir hasil lagi. Namun dalam kasus pertama, perlu memastikan hasil dari pemilihan luar diurutkan serta penomoran baris dalam hasil dalam. Membuat indeks yang tepat pada #objects harus menyelesaikan masalah
Akash

Jawaban:

13

Contoh-contoh dalam pertanyaan tidak cukup menghasilkan hasil yang sama ( OFFSETcontoh memiliki kesalahan off-by-one). Formulir yang diperbarui di bawah memperbaiki masalah itu, menghapus jenis tambahan untuk ROW_NUMBERkasus ini, dan menggunakan variabel untuk membuat solusi lebih umum:

DECLARE 
    @PageSize bigint = 10,
    @PageNumber integer = 3;

WITH Numbered AS
(
    SELECT TOP ((@PageNumber + 1) * @PageSize) 
        o.*,
        rn = ROW_NUMBER() OVER (
            ORDER BY o.[object_id])
    FROM #objects AS o
    ORDER BY 
        o.[object_id]
)
SELECT
    x.name,
    x.[object_id],
    x.principal_id,
    x.[schema_id],
    x.parent_object_id,
    x.[type],
    x.type_desc,
    x.create_date,
    x.modify_date,
    x.is_ms_shipped,
    x.is_published,
    x.is_schema_published
FROM Numbered AS x
WHERE
    x.rn >= @PageNumber * @PageSize
    AND x.rn < ((@PageNumber + 1) * @PageSize)
ORDER BY
    x.[object_id];

SELECT
    o.name,
    o.[object_id],
    o.principal_id,
    o.[schema_id],
    o.parent_object_id,
    o.[type],
    o.type_desc,
    o.create_date,
    o.modify_date,
    o.is_ms_shipped,
    o.is_published,
    o.is_schema_published
FROM #objects AS o
ORDER BY 
    o.[object_id]
    OFFSET @PageNumber * @PageSize - 1 ROWS 
    FETCH NEXT @PageSize ROWS ONLY;

Paket ROW_NUMBERtersebut memiliki perkiraan biaya 0,0197935 :

Paket Nomor Baris

Paket OFFSETtersebut memiliki perkiraan biaya 0,0196955 :

Paket Offset

Itu adalah penghematan 0,000098 unit biaya yang diperkirakan (meskipun OFFSETpaket tersebut akan membutuhkan operator tambahan jika Anda ingin mengembalikan nomor baris untuk setiap baris). ItuOFFSET rencana masih akan sedikit lebih murah, secara umum, tapi ingat bahwa estimasi biaya persis bahwa - pengujian nyata masih diperlukan. Sebagian besar biaya dalam kedua paket adalah biaya jenis input yang lengkap, sehingga indeks yang bermanfaat akan menguntungkan kedua solusi.

Di mana nilai literal konstan digunakan (misalnya OFFSET 30dalam contoh asli) pengoptimal dapat menggunakan Sortasi TopN alih-alih pengurutan lengkap diikuti oleh Top. Ketika baris yang diperlukan dari Sort TopN adalah literal konstan dan <= 100 (jumlah OFFSETdan FETCH) mesin eksekusi dapat menggunakan algoritma sort yang berbeda yang dapat melakukan lebih cepat daripada sortasi TopN yang digeneralisasi. Ketiga kasus memiliki karakteristik kinerja yang berbeda secara keseluruhan.

Mengapa pengoptimal tidak secara otomatis mengubah ROW_NUMBERpola sintaksis untuk digunakan OFFSET, ada beberapa alasan:

  1. Hampir tidak mungkin untuk menulis transformasi yang akan cocok dengan semua kegunaan yang ada
  2. Memiliki beberapa kueri paging secara otomatis berubah dan tidak yang lain dapat membingungkan
  3. The OFFSETrencana tidak dijamin untuk menjadi lebih baik dalam semua kasus

Salah satu contoh untuk titik ketiga di atas terjadi di mana set paging cukup lebar. Ini bisa menjadi jauh lebih efisien untuk mencari kunci yang diperlukan menggunakan indeks nonclustered dan pencarian secara manual terhadap indeks berkerumun dibandingkan dengan memindai indeks dengan OFFSETatau ROW_NUMBER. Ada masalah tambahan untuk dipertimbangkan jika aplikasi paging perlu tahu berapa banyak baris atau halaman yang ada secara total. Ada diskusi bagus lainnya tentang manfaat relatif dari metode 'pencarian kunci' dan 'offset' di sini .

Secara keseluruhan, mungkin lebih baik bahwa orang membuat keputusan untuk mengubah permintaan halaman mereka OFFSET, jika perlu, setelah pengujian menyeluruh.

Paul White Reinstate Monica
sumber
1
Jadi alasan transformasi tidak dilakukan dalam kasus-kasus umum mungkin karena terlalu sulit untuk menemukan pertukaran teknik yang dapat diterima. Anda memberikan alasan bagus mengapa itu bisa terjadi; Saya harus mengatakan bahwa ini adalah jawaban yang bagus. Banyak wawasan dan pemikiran baru. Saya akan membiarkan pertanyaan terbuka sedikit dan kemudian memilih jawaban terbaik.
usr
5

Dengan sedikit mengutak-atik kueri Anda, saya mendapatkan taksiran biaya yang sama (50/50) dan statistik IO yang sama:

; WITH cte AS
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
)
SELECT *
FROM cte
WHERE r >= 30 AND r < 40
ORDER BY r

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

Ini menghindari pengurutan tambahan yang muncul di versi Anda dengan mengurutkan ralih-alih object_id.

Mark Storey-Smith
sumber
Terima kasih atas wawasan ini. Sekarang saya berpikir tentang hal ini saya telah melihat pengoptimal tidak memahami sifat yang diurutkan dari output ROW_NUMBER sebelumnya. Itu menganggap set untuk diurungkan oleh object_id. Atau setidaknya tidak diurutkan berdasarkan r dan object_id.
usr
2
@atau ORDER OLEH ROW_NUMBER () yang digunakan menentukan bagaimana ia menetapkan angka. Itu tidak melakukan apa pun untuk menjanjikan urutan output - itu terpisah. Kebetulan itu sering terjadi bersamaan, tetapi tidak dijamin.
Aaron Bertrand
@ AaronBertrand Saya mengerti bahwa ROW_NUMBER tidak memesan output. Tapi jika ROW_NUMBER diperintahkan oleh kolom yang sama seperti output, maka urutan yang sama yang dijamin, kan? Jadi pengoptimal kueri dapat memanfaatkan fakta itu. Jadi dua operasi semacam selalu tidak dibutuhkan dalam permintaan ini.
usr
1
@ usr Anda telah menemukan kasus penggunaan umum yang tidak diperhitungkan oleh optimizer, tetapi ini bukan satu - satunya kasus penggunaan. Pertimbangkan kasus di mana pesanan oleh di dalam ROW_NUMBER () adalah kolom itu dan yang lainnya. Atau ketika urutan luar dengan melakukan pengurutan sekunder pada kolom lain. Atau ketika Anda ingin memesan turun. Atau dengan sesuatu yang lain sama sekali. Saya suka memesan dengan ekspresi rdaripada kolom dasar, jika hanya karena cocok dengan apa yang akan saya lakukan dalam permintaan non-bersarang dan memesan dengan ekspresi - Saya akan menggunakan alias yang ditugaskan untuk ekspresi alih-alih mengulangi ekspresi.
Aaron Bertrand
4
@ usr Dan untuk poin Paul, akan ada kasus di mana Anda dapat menemukan celah dalam fungsionalitas dalam pengoptimal. Jika itu tidak akan diperbaiki, dan Anda tahu cara yang lebih baik untuk menulis kueri, gunakan cara yang lebih baik. Pasien: "Dokter, sakit ketika saya melakukan x." Dokter: "Jangan lakukan x." :-)
Aaron Bertrand
-3

Mereka memodifikasi pengoptimal kueri untuk menambahkan fitur ini. Berarti mereka menerapkan mekanisme khusus untuk mendukung perintah offset ... fetch. Dengan kata lain untuk permintaan teratas SQL Server harus melakukan lebih banyak pekerjaan. Demikian perbedaan dalam rencana kueri.

Brandon melepaskan
sumber