Subquery berkinerja buruk dengan perbandingan tanggal

15

Saat menggunakan subquery untuk menemukan jumlah total semua catatan sebelumnya dengan bidang yang cocok, kinerjanya mengerikan di atas meja dengan hanya 50k catatan. Tanpa subquery, query dieksekusi dalam beberapa milidetik. Dengan subquery, waktu eksekusi lebih dari satu menit.

Untuk kueri ini, hasilnya harus:

  • Hanya sertakan catatan-catatan itu dalam rentang tanggal tertentu.
  • Sertakan hitungan semua catatan sebelumnya, tidak termasuk catatan saat ini, terlepas dari rentang tanggal.

Skema Tabel Dasar

Activity
======================
Id int Identifier
Address varchar(25)
ActionDate datetime2
Process varchar(50)
-- 7 other columns

Contoh Data

Id  Address     ActionDate (Time part excluded for simplicity)
===========================
99  000         2017-05-30
98  111         2017-05-30
97  000         2017-05-29
96  000         2017-05-28
95  111         2017-05-19
94  222         2017-05-30

hasil yang diharapkan

Untuk rentang tanggal 2017-05-29hingga2017-05-30

Id  Address     ActionDate    PriorCount
=========================================
99  000         2017-05-30    2  (3 total, 2 prior to ActionDate)
98  111         2017-05-30    1  (2 total, 1 prior to ActionDate)
94  222         2017-05-30    0  (1 total, 0 prior to ActionDate)
97  000         2017-05-29    1  (3 total, 1 prior to ActionDate)

Catatan 96 dan 95 dikecualikan dari hasil, tetapi dimasukkan dalam PriorCountsubquery

Permintaan Saat Ini

select 
    *.a
    , ( select count(*) 
        from Activity
        where 
            Activity.Address = a.Address
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc

Indeks saat ini

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON [dbo].[Activity]
(
    [ActionDate] ASC
)
INCLUDE ([Address]) WITH (
    PAD_INDEX = OFF, 
    STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, 
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON
)

Pertanyaan

  • Strategi apa yang dapat digunakan untuk meningkatkan kinerja permintaan ini?

Sunting 1
Sebagai jawaban untuk pertanyaan tentang apa yang dapat saya modifikasi pada DB: Saya dapat mengubah indeks, hanya saja bukan struktur tabelnya.

Sunting 2
Saya sekarang telah menambahkan indeks dasar pada Addresskolom, tetapi tampaknya tidak banyak membaik. Saat ini saya menemukan kinerja yang jauh lebih baik dengan membuat tabel temp dan memasukkan nilai tanpa PriorCountdan kemudian memperbarui setiap baris dengan jumlah spesifik mereka.

Sunting 3
Indeks Spool Joe Obbish (jawaban yang diterima) ditemukan adalah masalahnya. Setelah saya menambahkan yang baru nonclustered index [xyz] on [Activity] (Address) include (ActionDate), waktu kueri turun dari atas satu menit menjadi kurang dari satu detik tanpa menggunakan tabel temp (lihat edit 2).

Metro Smurf
sumber

Jawaban:

17

Dengan definisi indeks yang Anda miliki IDX_my_nme, SQL Server akan dapat mencari menggunakan ActionDatekolom tetapi tidak dengan Addresskolom. Indeks berisi semua kolom yang diperlukan untuk menutup subquery tetapi kemungkinan tidak terlalu selektif untuk subquery itu. Misalkan hampir semua data dalam tabel memiliki ActionDatenilai lebih awal dari '2017-05-30'. Pencarian ActionDate < '2017-05-30'akan mengembalikan hampir semua baris dari indeks, yang selanjutnya disaring setelah baris diambil dari indeks. Jika kueri Anda mengembalikan 200 baris maka Anda mungkin akan melakukan hampir 200 pemindaian indeks penuh IDX_my_nme, yang berarti Anda akan membaca sekitar 50.000 * 200 = 10 juta baris dari indeks.

Kemungkinan mencari Addressakan jauh lebih selektif untuk subkueri Anda, meskipun Anda belum memberi kami informasi statistik lengkap tentang permintaan sehingga itu asumsi saya. Namun, anggaplah Anda membuat indeks hanya Addressdan tabel Anda memiliki 10k nilai unik untuk Address. Dengan indeks baru, SQL Server hanya perlu mencari 5 baris dari indeks untuk setiap eksekusi subquery, sehingga Anda akan membaca sekitar 200 * 5 = 1000 baris dari indeks.

Saya sedang menguji terhadap SQL Server 2016 sehingga mungkin ada beberapa perbedaan sintaksis kecil. Di bawah ini adalah beberapa contoh data di mana saya membuat asumsi yang sama dengan di atas untuk distribusi data:

CREATE TABLE #Activity (
    Id int NOT NULL,
    [Address] varchar(25) NULL,
    ActionDate datetime2 NULL,
    FILLER varchar(100),
    PRIMARY KEY (Id)
);

INSERT INTO #Activity WITH (TABLOCK)
SELECT TOP (50000) -- 50k total rows
x.RN
, x.RN % 10000 -- 10k unique addresses
, DATEADD(DAY, x.RN / 100, '20160201') -- 100 rows per day
, REPLICATE('Z', 100)
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) x;

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([ActionDate] ASC) INCLUDE ([Address]);

Saya telah membuat indeks Anda seperti yang dijelaskan dalam pertanyaan. Saya menguji terhadap permintaan ini yang mengembalikan data yang sama dengan yang ada di pertanyaan:

select 
    a.*
    , ( select count(*) 
        from #Activity Activity
        where 
            Activity.[Address] = a.[Address]
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from #Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc;

Saya mendapatkan spool indeks. Apa artinya itu pada tingkat dasar adalah bahwa pengoptimal permintaan membuat indeks sementara on-the-fly karena tidak ada indeks yang ada terhadap tabel yang sesuai.

spool indeks

Permintaan masih selesai dengan cepat untuk saya. Mungkin Anda tidak mendapatkan optimasi spool indeks pada sistem Anda atau ada sesuatu yang berbeda tentang definisi tabel atau kueri. Untuk tujuan pendidikan, saya dapat menggunakan fitur tidak berdokumen OPTION (QUERYRULEOFF BuildSpool)untuk menonaktifkan spool indeks. Seperti apa rencananya:

indeks pencarian buruk

Jangan tertipu oleh penampilan pencarian indeks sederhana. SQL Server membaca hampir 10 juta baris dari indeks:

10M baris dari indeks

Jika saya akan menjalankan kueri lebih dari sekali, maka mungkin tidak masuk akal bagi pengoptimal kueri untuk membuat indeks setiap kali dijalankan. Saya dapat membuat indeks muka yang akan lebih selektif untuk kueri ini:

CREATE NONCLUSTERED INDEX [IDX_my_nme_2] ON #Activity
([Address] ASC) INCLUDE (ActionDate);

Rencananya mirip dengan sebelumnya:

pencarian indeks

Namun, dengan indeks baru SQL Server hanya membaca 1000 baris dari indeks. 800 baris dikembalikan untuk dihitung. Indeks dapat didefinisikan lebih selektif tetapi ini bisa cukup baik tergantung pada distribusi data Anda.

mencari yang baik

Jika Anda tidak dapat menentukan indeks tambahan apa pun pada tabel, saya akan mempertimbangkan untuk menggunakan fungsi jendela. Tampaknya ini berfungsi:

SELECT t.*
FROM
(
    select 
        a.*
        , -1 + ROW_NUMBER() OVER (PARTITION BY [Address] ORDER BY ActionDate) PriorCount
    from #Activity a
) t
where t.ActionDate between '2017-05-29' and '2017-05-30'
order by t.ActionDate desc;

Kueri itu melakukan satu pemindaian data tetapi melakukan pengurutan yang mahal dan menghitung ROW_NUMBER()fungsi untuk setiap baris dalam tabel, jadi rasanya ada beberapa pekerjaan tambahan yang dilakukan di sini:

jenis yang buruk

Namun, jika Anda benar-benar menyukai pola kode itu, Anda dapat menentukan indeks untuk membuatnya lebih efisien:

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([Address], [ActionDate]) INCLUDE (FILLER);

Itu menggerakkan semacam menjelang akhir yang akan jauh lebih murah:

semacam bagus

Jika tidak ada yang membantu, Anda perlu menambahkan lebih banyak informasi ke pertanyaan, lebih disukai termasuk rencana pelaksanaan yang sebenarnya.

Joe Obbish
sumber
1
Indeks Spool yang Anda temukan adalah masalahnya. Setelah saya menambahkan yang baru nonclustered index [xyz] on [Activity] (Address) include (ActionDate), waktu kueri turun dari atas satu menit menjadi kurang dari satu detik. +10 jika aku bisa. Terima kasih!
Metro Smurf