Optimalkan pilih pada subquery dengan COALESCE (...)

8

Saya memiliki tampilan besar yang saya gunakan dari dalam suatu aplikasi. Saya pikir saya sudah mempersempit masalah kinerja saya, tetapi saya tidak yakin bagaimana cara memperbaikinya. Versi tampilan yang disederhanakan terlihat seperti ini:

SELECT ISNULL(SEId + '-' + PEId, '0-0') AS Id,
   *,
   DATEADD(minute, Duration, EventTime) AS EventEndTime
FROM (
    SELECT se.SEId, pe.PEId,
        COALESCE(pe.StaffName, se.StaffName) AS StaffName, -- << Problem!
        COALESCE(pe.EventTime, se.EventTime) AS EventTime,
        COALESCE(pe.EventType, se.EventType) AS EventType,
        COALESCE(pe.Duration, se.Duration) AS Duration,
        COALESCE(pe.Data, se.Data) AS Data,
        COALESCE(pe.Field, se.Field) AS Field,
        pe.ThisThing, se.OtherThing
    FROM PE pe FULL OUTER JOIN SE se 
      ON pe.StaffName = se.StaffName
     AND pe.Duration = se.Duration
     AND pe.EventTime = se.EventTime
    WHERE NOT(pe.ThisThing = 1 AND se.OtherThing = 0)
) Z

Itu mungkin tidak membenarkan seluruh alasan untuk struktur kueri, tetapi mungkin memberi Anda ide - pandangan ini bergabung dengan dua tabel yang dirancang sangat buruk yang saya tidak punya kontrol atas dan mencoba untuk mensintesiskan beberapa informasi dari itu.

Jadi, karena ini adalah tampilan yang digunakan dari aplikasi, ketika mencoba mengoptimalkan saya membungkusnya dalam SELECT lain, seperti ini:

SELECT * FROM (
    -- … above code …
) Q
WHERE StaffName = 'SMITH, JOHN Q'

karena aplikasi sedang mencari anggota staf tertentu dalam hasilnya.

Masalahnya tampaknya adalah COALESCE(pe.StaffName, se.StaffName) AS StaffNamebagian, dan saya memilih dari tampilan StaffName. Jika saya mengubahnya ke pe.StaffName AS StaffNameatau se.StaffName AS StaffName, masalah kinerja hilang (tetapi lihat pembaruan 2 di bawah) . Tetapi itu tidak akan berhasil karena satu sisi atau sisi yang lain FULL OUTER JOINbisa hilang, sehingga satu atau bidang lainnya mungkin NULL.

Bisakah saya refactor ini mengganti COALESCE(…)dengan sesuatu yang lain, yang akan ditulis ulang ke dalam subquery?

Catatan lain:

  • Saya sudah menambahkan beberapa indeks untuk memperbaiki masalah kinerja dengan sisa kueri - tanpa COALESCEitu sangat cepat.
  • Agak mengherankan saya, melihat rencana eksekusi tidak menaikkan bendera, bahkan ketika subquery dan WHEREpernyataan pembungkus disertakan. Total biaya subkueri dalam penganalisa adalah 0.0065736. Hmph. Diperlukan empat detik untuk mengeksekusi.
  • Mengubah aplikasi ke permintaan secara berbeda (mis. Kembali pe.StaffName AS PEStaffName, se.StaffName AS SEStaffNamedan melakukan WHERE PEStaffName = 'X' OR SEStaffName = 'X') mungkin berhasil, tetapi sebagai upaya terakhir - saya benar-benar berharap saya dapat mengoptimalkan tampilan tanpa harus resor untuk menyentuh aplikasi.
  • Prosedur tersimpan mungkin akan lebih masuk akal untuk ini, tetapi aplikasi ini dibuat dengan Entity Framework, dan saya tidak tahu bagaimana membuatnya bagus dengan SP yang mengembalikan tipe tabel (topik lain seluruhnya).

Indeks

Indeks yang saya tambahkan sejauh ini terlihat seperti ini:

CREATE NONCLUSTERED INDEX [IX_PE_EventTime]
ON [dbo].[PE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[ThisThing])

CREATE NONCLUSTERED INDEX [IX_SE_EventTime]
ON [dbo].[SE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[OtherThing])

Memperbarui

Hmm ... Saya mencoba mensimulasikan perubahan yang terjadi di atas, dan itu tidak membantu. Yaitu, sebelum di ) Zatas, saya menambahkan AND (pe.StaffName = 'SMITH, JOHN Q' OR se.StaffName = 'SMITH, JOHN Q'), tetapi kinerjanya sama. Sekarang saya benar-benar tidak tahu harus mulai dari mana.

Perbarui 2

Komentar @ypercube tentang perlunya bergabung penuh membuat saya menyadari bahwa permintaan saya yang disintesis meninggalkan komponen yang mungkin penting. Sementara, ya, saya perlu bergabung penuh, tes yang saya lakukan di atas dengan menjatuhkan COALESCEdan menguji hanya satu sisi bergabung untuk nilai non-nol akan membuat sisi lain dari bergabung penuh tidak relevan , dan pengoptimal mungkin menggunakan ini fakta untuk mempercepat permintaan. Juga, saya telah memperbarui contoh untuk menunjukkan bahwa StaffNamesebenarnya adalah salah satu kunci bergabung - yang mungkin memiliki pengaruh signifikan pada pertanyaan. Saya juga sekarang condong ke sarannya bahwa memecah ini menjadi serikat tiga arah, bukan bergabung penuh mungkin jawabannya, dan akan menyederhanakan banyak hal yang COALESCEsaya lakukan. Cobalah sekarang.

S'pht'Kr
sumber
Indeks apa yang telah Anda tambahkan? Apakah Anda memasukkan Nama Staf dalam indeks?
Mark Sinkinson
@MarkSinkinson Saya memiliki indeks nonclustered di setiap meja pada KeyField, kedua indeks INCLUDEtersebut StaffNamelapangan dan beberapa bidang lainnya. Saya dapat memposting definisi indeks dalam pertanyaan. Saya sedang mengerjakan ini pada server uji sehingga saya dapat menambahkan indeks yang menurut Anda mungkin berguna untuk dicoba!
S'pht'Kr
1
Anda memiliki WHERE pe.ThisThing = 1 AND se.OtherThing = 0kondisi yang membatalkan FULL OUTERgabung dan membuat kueri setara dengan gabung dalam. Apakah Anda yakin Anda perlu bergabung LENGKAP?
ypercubeᵀᴹ
@ ypercube Maaf, itu pengkodean udara yang buruk di pihak saya, poin lebih dari itu saya punya kondisi di kedua tabel, tapi ya saya memperhitungkan nol di kedua sisi dalam permintaan sebenarnya. Saya menggabungkan dua tabel dan mencari yang cocok, tetapi saya membutuhkan data yang tersedia dari kedua tabel ketika tidak ada catatan yang cocok di kiri atau kanan - jadi ya, saya perlu bergabung penuh.
S'pht'Kr
1
Sebuah pemikiran: ini adalah sebuah kesalahan besar tetapi Anda dapat mencoba memecah kueri internal menjadi tiga bagian ( INNER JOIN, LEFT JOINdengan WHERE IS NULLcek, KANAN BERGABUNG dengan IS NULL) dan kemudian UNION ALLtiga bagian. Dengan cara ini tidak perlu menggunakan COALESCE()dan mungkin (mungkin saja) membantu pengoptimal untuk mengetahui penulisan ulang.
ypercubeᵀᴹ

Jawaban:

4

Ini agak panjang tetapi karena OP mengatakan itu bekerja, saya menambahkannya sebagai jawaban (jangan ragu untuk memperbaikinya jika Anda menemukan sesuatu yang salah).

Cobalah untuk memecah kueri internal menjadi tiga bagian ( INNER JOIN, LEFT JOINdengan WHERE IS NULLcek, RIGHT JOINdengan IS NULLcek) dan kemudian UNION ALLtiga bagian. Ini memiliki keuntungan sebagai berikut:

  • Pengoptimal memiliki lebih sedikit opsi transformasi yang tersedia untuk FULLbergabung daripada untuk (lebih umum) INNERdan LEFTbergabung.

  • The Ztable berasal dapat dihapus (Anda dapat melakukannya pula) dari tampilan definisi.

  • The NOT(pe.ThisThing = 1 AND se.OtherThing = 0)akan dibutuhkan hanya pada INNERbagian bergabung.

  • Perbaikan kecil, penggunaannya COALESCE()akan minimal jika ada sama sekali (saya berasumsi bahwa se.SEIddan pe.PEIdtidak dapat dibatalkan. Jika lebih banyak kolom tidak dapat dibatalkan, Anda akan dapat menghapus lebih banyak COALESCE()panggilan.)
    Lebih penting, pengoptimal dapat menekan segala kondisi di kueri Anda yang melibatkan kolom-kolom ini (sekarang COALESCE()tidak menghalangi dorongan)

  • Semua hal di atas akan memberikan lebih banyak opsi kepada pengoptimal untuk mengubah / menulis ulang kueri apa pun yang menggunakan tampilan sehingga dapat menemukan rencana eksekusi yang dapat digunakan indeks pada tabel yang mendasari.

Secara keseluruhan, tampilan dapat ditulis sebagai:

SELECT 
    se.SEId + '-' + pe.PEId AS Id,
    se.SEId, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    COALESCE(pe.EventType, se.EventType) AS EventType,
    pe.Duration,
    COALESCE(pe.Data, se.Data) AS Data,
    COALESCE(pe.Field, se.Field) AS Field,
    pe.ThisThing, se.OtherThing,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe INNER JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1 AND se.OtherThing = 0) 

UNION ALL

SELECT 
    '0-0',
    NULL, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    pe.EventType,
    pe.Duration,
    pe.Data,
    pe.Field,
    pe.ThisThing, NULL,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe LEFT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1)
  AND se.StaffName IS NULL

UNION ALL

SELECT 
    '0-0',
    se.SEId, NULL,
    se.StaffName, 
    se.EventTime,
    se.EventType,
    se.Duration,
    se.Data,
    se.Field,
    NULL, se.OtherThing, 
    DATEADD(minute, se.Duration, se.EventTime) AS EventEndTime
FROM PE pe RIGHT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (se.OtherThing = 0)
  AND pe.StaffName IS NULL ;
ypercubeᵀᴹ
sumber
0

Intuisi saya adalah bahwa ini seharusnya tidak menjadi masalah karena pada saat COALESCE(pe.StaffName, se.StaffName) AS StaffNamemelakukan apa pun semua baris dari dua sumber harus sudah ditarik dan dicocokkan sehingga pemanggilan fungsi dalam memori sederhana dibandingkan-ke-null-dan -memilih. Jelas ini bukan masalahnya jadi mungkin sesuatu di salah satu sumber (jika mereka adalah tampilan atau tabel turunan inline) atau tabel dasar (yaitu kurangnya indeks) membuat perencana kueri berpikir perlu memindai kolom ini secara terpisah.

Tanpa lebih detail dari kueri persis yang Anda jalankan, struktur pendukung, dan rencana kueri yang dihasilkan, apa pun yang kami sarankan adalah dugaan.

Untuk mencoba memaksa perbandingan dilakukan setelah semua yang lain, Anda bisa mencoba cukup memilih kedua nilai dalam tabel deribed ( pe.StaffName AS pe.StaffName, se.StaffName AS seStaffName) kemudian melakukan pick dalam permintaan luar ( COALESCE(peStaffName, seStaffName) AS StaffName), atau Anda bahkan bisa mendorong data dari permintaan dalam ke tabel sementara kemudian melakukan query luar dengan memilih dari itu (tapi itu akan membutuhkan prosedur tersimpan, dan tergantung pada jumlah baris dump-to-tempdb ini bisa mahal dan karenanya bermasalah dalam dirinya sendiri).

David Spillett
sumber
Terima kasih David, saya telah berbuat salah pada sisi paranoia mengenai seberapa banyak saya harus mengungkapkan hal ini bahkan sejauh struktur (pe => PatientEvent, jadi ...) tapi saya tahu itu membuatnya lebih sulit. Saya pikir itu sebenarnya melakukan gabungan berdasarkan indeks dan kemudian melakukan "perbandingan sederhana dalam memori" untuk menyaring ... tapi tabel turunan tanpa filter Zsaat ini kembali dengan baris ~ 1.5m. Apa yang saya ingin lakukan adalah menulis ulang predikat itu ke dalam query Zsehingga akan menggunakan indeks ... tapi sekarang saya juga bingung karena ketika saya secara manual meletakkan predikat di sana, masih tidak menggunakan indeks ... jadi sekarang Saya tidak yakin.
S'pht'Kr