Gabungkan subset dari tabel target

71

Saya mencoba menggunakan MERGEpernyataan untuk menyisipkan atau menghapus baris dari sebuah tabel, tetapi saya hanya ingin bertindak pada subset dari baris-baris itu. Dokumentasi untuk MERGEmemiliki peringatan yang cukup kuat:

Penting untuk menentukan hanya kolom dari tabel target yang digunakan untuk tujuan pencocokan. Yaitu, tentukan kolom dari tabel target yang dibandingkan dengan kolom yang sesuai dari tabel sumber. Jangan mencoba meningkatkan kinerja kueri dengan memfilter baris di tabel target dalam klausa ON, seperti dengan menentukan DAN TIDAK target_table.column_x = nilai. Melakukannya dapat mengembalikan hasil yang tidak terduga dan salah.

tetapi inilah yang tampaknya harus saya lakukan untuk membuat MERGEpekerjaan saya .

Data yang saya miliki adalah tabel standar banyak-ke-banyak yang menggabungkan item ke kategori (misalnya item mana yang termasuk dalam kategori mana) seperti:

CategoryId   ItemId
==========   ======
1            1
1            2
1            3
2            1
2            3
3            5
3            6
4            5

Yang perlu saya lakukan adalah mengganti semua baris dalam kategori tertentu secara efektif dengan daftar item baru. Upaya awal saya untuk melakukan ini terlihat seperti ini:

MERGE INTO CategoryItem AS TARGET
USING (
  SELECT ItemId FROM SomeExternalDataSource WHERE CategoryId = 2
) AS SOURCE
ON SOURCE.ItemId = TARGET.ItemId AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
    INSERT ( CategoryId, ItemId )
    VALUES ( 2, ItemId )
WHEN NOT MATCHED BY SOURCE AND TARGET.CategoryId = 2 THEN
    DELETE ;

Tampaknya ini berfungsi dalam pengujian saya, tetapi saya melakukan persis apa yang MSDN peringatkan agar saya tidak melakukannya. Ini membuat saya khawatir bahwa saya akan mengalami masalah yang tidak terduga nanti, tetapi saya tidak dapat melihat cara lain untuk membuat MERGEsatu - satunya baris yang memengaruhi saya dengan nilai bidang spesifik ( CategoryId = 2) dan mengabaikan baris dari kategori lain.

Apakah ada cara "yang lebih benar" untuk mencapai hasil yang sama? Dan apa "hasil yang tidak terduga atau tidak benar" yang MSDN memperingatkan saya tentang?

KutuluMike
sumber
Ya, dokumentasi akan lebih bermanfaat jika memiliki contoh konkret "hasil yang tidak terduga dan salah".
AK
3
@AlexKuznetsov Ada contoh di sini .
Paul White
@SQLKiwi terima kasih atas tautannya - IMO dokumentasi akan jauh lebih baik jika dirujuk dari halaman asli.
AK
1
@AlexKuznetsov Setuju. Sayangnya, reorganisasi BOL untuk 2012 mematahkan hal itu, di antara banyak hal lainnya. Itu dikaitkan dengan sangat baik dalam dokumentasi 2008 R2.
Paul White

Jawaban:

103

The MERGEpernyataan memiliki sintaks yang kompleks dan bahkan lebih kompleks pelaksanaan, tapi pada dasarnya idenya adalah untuk bergabung dengan dua tabel, filter ke baris yang perlu diubah (dimasukkan, diperbarui, atau dihapus), dan kemudian untuk melakukan perubahan yang diminta. Diberikan data sampel berikut:

DECLARE @CategoryItem AS TABLE
(
    CategoryId  integer NOT NULL,
    ItemId      integer NOT NULL,

    PRIMARY KEY (CategoryId, ItemId),
    UNIQUE (ItemId, CategoryId)
);

DECLARE @DataSource AS TABLE
(
    CategoryId  integer NOT NULL,
    ItemId      integer NOT NULL

    PRIMARY KEY (CategoryId, ItemId)
);

INSERT @CategoryItem
    (CategoryId, ItemId)
VALUES
    (1, 1),
    (1, 2),
    (1, 3),
    (2, 1),
    (2, 3),
    (3, 5),
    (3, 6),
    (4, 5);

INSERT @DataSource
    (CategoryId, ItemId)
VALUES
    (2, 2);

Target

╔════════════╦════════╗
 CategoryId  ItemId 
╠════════════╬════════╣
          1       1 
          2       1 
          1       2 
          1       3 
          2       3 
          3       5 
          4       5 
          3       6 
╚════════════╩════════╝

Sumber

╔════════════╦════════╗
 CategoryId  ItemId 
╠════════════╬════════╣
          2       2 
╚════════════╩════════╝

Hasil yang diinginkan adalah mengganti data dalam target dengan data dari sumber, tetapi hanya untuk CategoryId = 2. Dengan mengikuti uraian yang MERGEdiberikan di atas, kita harus menulis kueri yang menggabungkan sumber dan target pada kunci saja, dan menyaring baris hanya dalam WHENklausa:

MERGE INTO @CategoryItem AS TARGET
USING @DataSource AS SOURCE ON 
    SOURCE.ItemId = TARGET.ItemId 
    AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY SOURCE 
    AND TARGET.CategoryId = 2 
    THEN DELETE
WHEN NOT MATCHED BY TARGET 
    AND SOURCE.CategoryId = 2 
    THEN INSERT (CategoryId, ItemId)
        VALUES (CategoryId, ItemId)
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

Ini memberikan hasil sebagai berikut:

╔═════════╦════════════╦════════╗
 $ACTION  CategoryId  ItemId 
╠═════════╬════════════╬════════╣
 DELETE            2       1 
 INSERT            2       2 
 DELETE            2       3 
╚═════════╩════════════╩════════╝
╔════════════╦════════╗
 CategoryId  ItemId 
╠════════════╬════════╣
          1       1 
          1       2 
          1       3 
          2       2 
          3       5 
          3       6 
          4       5 
╚════════════╩════════╝

Rencana pelaksanaannya adalah: Gabungkan rencana

Perhatikan bahwa kedua tabel dipindai sepenuhnya. Kami mungkin menganggap ini tidak efisien, karena hanya baris yang CategoryId = 2akan terpengaruh dalam tabel target. Di sinilah peringatan di Books Online masuk. Salah satu upaya salah kaprah untuk mengoptimalkan hanya menyentuh baris yang diperlukan dalam target adalah:

MERGE INTO @CategoryItem AS TARGET
USING 
(
    SELECT CategoryId, ItemId
    FROM @DataSource AS ds 
    WHERE CategoryId = 2
) AS SOURCE ON
    SOURCE.ItemId = TARGET.ItemId
    AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
    INSERT (CategoryId, ItemId)
    VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

Logika dalam ONklausa diterapkan sebagai bagian dari gabungan. Dalam hal ini, join adalah join luar penuh (lihat entri Books Online ini untuk alasannya). Menerapkan tanda centang untuk kategori 2 pada baris target sebagai bagian dari gabungan luar akhirnya menghasilkan baris dengan nilai yang berbeda dihapus (karena mereka tidak cocok dengan sumber):

╔═════════╦════════════╦════════╗
 $ACTION  CategoryId  ItemId 
╠═════════╬════════════╬════════╣
 DELETE            1       1 
 DELETE            1       2 
 DELETE            1       3 
 DELETE            2       1 
 INSERT            2       2 
 DELETE            2       3 
 DELETE            3       5 
 DELETE            3       6 
 DELETE            4       5 
╚═════════╩════════════╩════════╝

╔════════════╦════════╗
 CategoryId  ItemId 
╠════════════╬════════╣
          2       2 
╚════════════╩════════╝

Akar penyebabnya adalah alasan yang sama mengapa predikat berperilaku berbeda di ONklausa join luar daripada yang mereka lakukan jika ditentukan dalam WHEREklausa. The MERGEsintaks (dan pelaksanaan bergabung tergantung pada klausul tertentu) hanya membuat lebih sulit untuk melihat bahwa ini adalah begitu.

The bimbingan dalam buku Online (diperluas dalam Kinerja Mengoptimalkan entry) menawarkan panduan yang akan menjamin semantik yang benar dinyatakan menggunakan MERGEsintaks, tanpa pengguna harus harus memahami semua rincian pelaksanaan, atau akun untuk cara di mana optimizer mungkin sah mengatur ulang hal-hal untuk alasan efisiensi eksekusi.

Dokumentasi menawarkan tiga cara potensial untuk menerapkan penyaringan awal:

Menentukan kondisi pemfilteran dalam WHENklausa menjamin hasil yang benar, tetapi dapat berarti bahwa lebih banyak baris dibaca dan diproses dari tabel sumber dan target daripada yang sangat diperlukan (seperti yang terlihat pada contoh pertama).

Memutakhirkan melalui tampilan yang berisi kondisi pemfilteran juga menjamin hasil yang benar (karena baris yang diubah harus dapat diakses untuk pembaruan melalui tampilan) tetapi ini memang memerlukan tampilan khusus, dan yang mengikuti kondisi aneh untuk memperbarui tampilan.

Menggunakan ekspresi tabel umum memiliki risiko yang sama dengan menambahkan predikat ke ONklausa, tetapi untuk alasan yang sedikit berbeda. Dalam banyak kasus itu akan aman, tetapi membutuhkan analisis ahli dari rencana eksekusi untuk mengkonfirmasi ini (dan pengujian praktis yang luas). Sebagai contoh:

WITH TARGET AS 
(
    SELECT * 
    FROM @CategoryItem
    WHERE CategoryId = 2
)
MERGE INTO TARGET
USING 
(
    SELECT CategoryId, ItemId
    FROM @DataSource
    WHERE CategoryId = 2
) AS SOURCE ON
    SOURCE.ItemId = TARGET.ItemId
    AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY TARGET THEN
    INSERT (CategoryId, ItemId)
    VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

Ini menghasilkan hasil yang benar (tidak diulang) dengan rencana yang lebih optimal:

Menggabungkan rencana 2

Paket hanya membaca baris untuk kategori 2 dari tabel target. Ini mungkin pertimbangan kinerja yang penting jika tabel target besar, tetapi terlalu mudah untuk mendapatkan kesalahan ini menggunakan MERGEsintaks.

Terkadang, lebih mudah untuk menulis MERGEoperasi DML yang terpisah. Pendekatan ini bahkan dapat berkinerja lebih baik daripada tunggal MERGE, sebuah fakta yang sering mengejutkan orang.

DELETE ci
FROM @CategoryItem AS ci
WHERE ci.CategoryId = 2
AND NOT EXISTS 
(
    SELECT 1 
    FROM @DataSource AS ds 
    WHERE 
        ds.ItemId = ci.ItemId
        AND ds.CategoryId = ci.CategoryId
);

INSERT @CategoryItem
SELECT 
    ds.CategoryId, 
    ds.ItemId
FROM @DataSource AS ds
WHERE
    ds.CategoryId = 2;
Paul White
sumber
Saya tahu ini adalah pertanyaan yang sangat lama ... tetapi setiap kesempatan Anda dapat menguraikan "Menggunakan tabel ekspresi umum membawa risiko yang sama dengan menambahkan predikat ke klausa ON, tetapi untuk alasan yang sedikit berbeda." Saya tahu BOL juga memiliki peringatan yang samar-samar "Metode ini mirip dengan menentukan kriteria pencarian tambahan dalam klausa ON dan dapat menghasilkan hasil yang salah. Kami menyarankan Anda menghindari menggunakan metode ini ...". Metode CTE muncul untuk menyelesaikan kasus penggunaan saya, namun saya bertanya-tanya apakah ada skenario yang tidak saya pertimbangkan.
Henry Lee