Bagaimana cara menghindari menggunakan permintaan Gabung saat memasang beberapa data menggunakan parameter xml?

9

Saya mencoba memperbarui tabel dengan array nilai. Setiap item dalam array berisi informasi yang cocok dengan baris dalam tabel di database SQL Server. Jika baris sudah ada dalam tabel, kami memperbarui baris itu dengan informasi dalam array yang diberikan. Selain itu, kami menyisipkan baris baru di tabel. Saya pada dasarnya menggambarkan upert.

Sekarang, saya mencoba untuk mencapai ini dalam prosedur tersimpan yang mengambil parameter XML. Alasan saya menggunakan XML dan bukan param tabel-nilai adalah karena, melakukan yang terakhir, saya harus membuat jenis kustom dalam SQL dan mengaitkan jenis ini dengan prosedur yang tersimpan. Jika saya pernah mengubah sesuatu dalam prosedur tersimpan saya atau skema db saya di jalan, saya harus mengulang prosedur tersimpan dan jenis kustom. Saya ingin menghindari situasi ini. Selain itu, keunggulan TVP lebih dari XML tidak berguna untuk situasi saya karena, ukuran array data saya tidak akan pernah melebihi 1000. Ini berarti saya tidak dapat menggunakan solusi yang diusulkan di sini: Cara memasukkan beberapa catatan menggunakan XML dalam SQL server 2008

Juga, diskusi serupa di sini ( UPSERT - Apakah ada alternatif yang lebih baik untuk MERGE atau @@ rowcount? ) Berbeda dari apa yang saya tanyakan karena, saya mencoba untuk menaikkan beberapa baris ke sebuah tabel.

Saya berharap bahwa saya hanya akan menggunakan set kueri berikut untuk memberikan nilai dari xml. Tapi ini tidak akan berhasil. Pendekatan ini seharusnya hanya berfungsi ketika input adalah satu baris.

begin tran
   update table with (serializable) set select * from xml_param
   where key = @key

   if @@rowcount = 0
   begin
      insert table (key, ...) values (@key,..)
   end
commit tran

Alternatif berikutnya adalah menggunakan JIKA ADA lengkap atau salah satu variasi dari bentuk berikut. Tapi, saya menolak ini dengan alasan efisiensi yang tidak optimal:

IF (SELECT COUNT ... ) > 0
    UPDATE
ELSE
    INSERT

Opsi selanjutnya menggunakan pernyataan Gabung seperti yang dijelaskan di sini: http://www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.html . Tapi, kemudian saya membaca tentang masalah dengan permintaan Gabung di sini: http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ . Untuk alasan ini, saya berusaha menghindari Penggabungan.

Jadi, sekarang pertanyaan saya adalah: apakah ada opsi lain atau cara yang lebih baik untuk mencapai beberapa upsert menggunakan parameter XML dalam prosedur tersimpan SQL Server 2008?

Harap dicatat bahwa data dalam parameter XML dapat berisi beberapa catatan yang tidak boleh di-UPSERT karena lebih tua dari catatan saat ini. Ada ModifiedDatebidang di XML dan tabel tujuan yang perlu dibandingkan untuk menentukan apakah catatan harus diperbarui atau dibuang.

GMalla
sumber
Mencoba menghindari membuat perubahan pada proc di masa depan sebenarnya bukan alasan yang baik untuk tidak menggunakan TVP. jika data yang dikirimkan berubah, Anda akan membuat perubahan pada kode dengan cara apa pun.
Max Vernon
1
@ MaxVernon Saya memiliki pemikiran yang sama pada awalnya dan hampir membuat komentar yang sangat mirip karena itu saja bukan alasan untuk menghindari TVP. Tetapi mereka memang mengambil sedikit usaha lebih, dan dengan peringatan "tidak pernah lebih dari 1000 baris" (kadang-kadang tersirat, atau bahkan mungkin sering?) Itu agak sulit. Namun, saya kira saya harus memenuhi syarat jawaban saya untuk menyatakan bahwa <1000 baris pada suatu waktu tidak terlalu berbeda dari XML selama tidak disebut 10rb kali berturut-turut. Maka perbedaan kinerja kecil tentu saja bertambah.
Solomon Rutzky
Masalah MERGEyang ditunjukkan oleh Bertrand sebagian besar adalah kasus tepi dan ketidakefisienan, bukan penghenti - MS tidak akan melepaskannya jika itu adalah ladang ranjau yang nyata. Apakah Anda yakin bahwa konvolusi yang Anda hindari MERGEtidak membuat lebih banyak kesalahan potensial daripada yang disimpan?
Jon of All Trades
@JonofAllTrades Agar adil, apa yang saya usulkan tidak terlalu berbelit-belit dibandingkan dengan MERGE. Langkah-langkah INSERT dan UPDATE dari MERGE masih diproses secara terpisah. Perbedaan utama dalam pendekatan saya adalah variabel tabel yang menyimpan ID rekaman yang diperbarui dan permintaan DELETE yang menggunakan variabel tabel itu untuk menghapus catatan-catatan itu dari tabel temp dari data yang masuk. Dan saya kira SUMBER bisa langsung dari @ XMLparam.nodes () daripada membuang ke tabel temp, tapi tetap saja, itu tidak banyak hal tambahan untuk tidak perlu khawatir tentang pernah menemukan diri Anda dalam salah satu kasus tepi; - ).
Solomon Rutzky

Jawaban:

11

Apakah sumbernya XML atau TVP tidak membuat perbedaan besar. Operasi keseluruhan pada dasarnya adalah:

  1. Perbarui baris yang ada
  2. Masukkan baris yang hilang

Anda melakukannya dalam urutan itu karena jika Anda MASUKKAN terlebih dahulu, maka semua baris ada untuk mendapatkan PEMBARUAN dan Anda akan melakukan pekerjaan berulang untuk setiap baris yang baru saja dimasukkan.

Di luar itu ada berbagai cara untuk mencapai hal ini dan berbagai cara untuk menyesuaikan beberapa efisiensi tambahan darinya.

Mari kita mulai dengan minimum. Karena mengekstraksi XML kemungkinan menjadi salah satu bagian yang lebih mahal dari operasi ini (jika bukan yang paling mahal), kami tidak ingin harus melakukannya dua kali (karena kami memiliki dua operasi untuk melakukan). Jadi, kami membuat tabel temp dan mengekstrak data dari XML ke dalamnya:

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

INSERT INTO #TempImport (Field1, Field2, ...)
  SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
         tab.col.value('XQueryForField2', 'DataType') AS [Field2],
         ...
  FROM   @XmlInputParam.nodes('XQuery') tab(col);

Dari sana kami melakukan PEMBARUAN dan kemudian MASUK:

UPDATE tab
SET    tab.Field1 = tmp.Field1,
       tab.Field2 = tmp.Field2,
       ...
FROM   [SchemaName].[TableName] tab
INNER JOIN #TempImport tmp
        ON tmp.IDField = tab.IDField
        ... -- more fields if PK or alternate key is composite

INSERT INTO [SchemaName].[TableName]
  (Field1, Field2, ...)
  SELECT tmp.Field1, tmp.Field2, ...
  FROM   #TempImport tmp
  WHERE  NOT EXISTS (
                       SELECT  *
                       FROM    [SchemaName].[TableName] tab
                       WHERE   tab.IDField = tmp.IDField
                       ... -- more fields if PK or alternate key is composite
                     );

Sekarang setelah operasi dasar tidak berfungsi, kami dapat melakukan beberapa hal untuk mengoptimalkan:

  1. ambil @@ ROWCOUNT dari insert ke tabel temp dan bandingkan dengan @@ ROWCOUNT dari UPDATE. Jika mereka sama maka kita dapat melewati INSERT

  2. ambil nilai ID yang diperbarui melalui klausa OUTPUT dan HAPUS nilai-nilai dari tabel temp. Maka INSERT tidak membutuhkanWHERE NOT EXISTS(...)

  3. JIKA ada baris dalam data yang masuk yang tidak boleh disinkronkan (yaitu tidak dimasukkan atau diperbarui), maka catatan-catatan itu harus dihapus sebelum melakukan UPDATE

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

DECLARE @ImportRows INT;
DECLARE @UpdatedIDs TABLE ([IDField] INT NOT NULL);

BEGIN TRY

  INSERT INTO #TempImport (Field1, Field2, ...)
    SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
           tab.col.value('XQueryForField2', 'DataType') AS [Field2],
           ...
    FROM   @XmlInputParam.nodes('XQuery') tab(col);

  SET @ImportRows = @@ROWCOUNT;

  IF (@ImportRows = 0)
  BEGIN
    RAISERROR('Seriously?', 16, 1); -- no rows to import
  END;

  -- optional: test to see if it helps or hurts
  -- ALTER TABLE #TempImport
  --   ADD CONSTRAINT [PK_#TempImport]
  --   PRIMARY KEY CLUSTERED (PKField ASC)
  --   WITH FILLFACTOR = 100;


  -- optional: remove any records that should not be synced
  DELETE tmp
  FROM   #TempImport tmp
  INNER JOIN [SchemaName].[TableName] tab
          ON tab.IDField = tmp.IDField
          ... -- more fields if PK or alternate key is composite
  WHERE  tmp.ModifiedDate < tab.ModifiedDate;

  BEGIN TRAN;

  UPDATE tab
  SET    tab.Field1 = tmp.Field1,
         tab.Field2 = tmp.Field2,
         ...
  OUTPUT INSERTED.IDField
  INTO   @UpdatedIDs ([IDField]) -- capture IDs that are updated
  FROM   [SchemaName].[TableName] tab
  INNER JOIN #TempImport tmp
          ON tmp.IDField = tab.IDField
          ... -- more fields if PK or alternate key is composite

  IF (@@ROWCOUNT < @ImportRows) -- if all rows were updates then skip, else insert remaining
  BEGIN
    -- get rid of rows that were updates, leaving only the ones to insert
    DELETE tmp
    FROM   #TempImport tmp
    INNER JOIN @UpdatedIDs del
            ON del.[IDField] = tmp.[IDField];

    -- OR, rather than the DELETE, maybe add a column to #TempImport for:
    -- [IsUpdate] BIT NOT NULL DEFAULT (0)
    -- Then UPDATE #TempImport SET [IsUpdate] = 1 JOIN @UpdatedIDs ON [IDField]
    -- Then, in below INSERT, add:  WHERE [IsUpdate] = 0

    INSERT INTO [SchemaName].[TableName]
      (Field1, Field2, ...)
      SELECT tmp.Field1, tmp.Field2, ...
      FROM   #TempImport tmp
  END;

  COMMIT TRAN;

END TRY
BEGIN CATCH
  IF (@@TRANCOUNT > 0)
  BEGIN
    ROLLBACK;
  END;

  -- THROW; -- if using SQL 2012 or newer, use this and remove the following 3 lines
  DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
  RAISERROR(@ErrorMessage, 16, 1);
  RETURN;
END CATCH;

Saya telah menggunakan model ini beberapa kali pada Impor / ETL yang memiliki lebih dari 1000 baris atau mungkin 500 dalam batch dari total 20k - lebih dari satu juta baris. Namun, saya belum menguji perbedaan kinerja antara DELETE dari baris yang diperbarui dari tabel temp vs hanya memperbarui bidang [IsUpdate].


Harap perhatikan tentang keputusan untuk menggunakan XML over TVP karena ada, paling banyak, 1000 baris untuk diimpor sekaligus (disebutkan dalam pertanyaan):

Jika ini dipanggil beberapa kali di sana-sini, maka sangat mungkin kenaikan kinerja minor di TVP mungkin tidak sebanding dengan biaya pemeliharaan tambahan (perlu membatalkan proc sebelum mengubah Jenis Tabel yang Didefinisikan Pengguna, perubahan kode aplikasi, dll) . Tetapi jika Anda mengimpor 4 juta baris, mengirimkan 1000 sekaligus, yaitu 4000 eksekusi (dan 4 juta baris XML untuk diuraikan tidak peduli bagaimana itu dipecah), dan bahkan perbedaan kinerja kecil ketika dijalankan hanya beberapa kali akan tambahkan hingga perbedaan yang nyata.

Yang sedang berkata, metode seperti yang saya jelaskan tidak berubah di luar mengganti SELECT FROM @XmlInputParam menjadi SELECT FROM @TVP. Karena TVP hanya baca, Anda tidak akan dapat menghapusnya. Saya kira Anda bisa menambahkan sebuah WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)ke SELECT akhir (diikat ke INSERT) bukan sederhana WHERE IsUpdate = 0. Jika Anda menggunakan @UpdateIDsvariabel tabel dengan cara ini, maka Anda bahkan bisa lolos dengan tidak membuang baris yang masuk ke tabel temp.

Solomon Rutzky
sumber