Cara paling efisien untuk menghasilkan diff


8

Saya punya tabel di SQL server yang terlihat seperti ini:

Id    |Version  |Name    |date    |fieldA   |fieldB ..|fieldZ
1     |1        |Foo     |20120101|23       |       ..|25334123
2     |2        |Foo     |20120101|23       |NULL   ..|NULL
3     |2        |Bar     |20120303|24       |123......|NULL
4     |2        |Bee     |20120303|34       |-34......|NULL

Saya sedang mengerjakan prosedur tersimpan untuk diff, yang membutuhkan input data dan nomor versi. Data input memiliki kolom dari kolom Name uptilZ. Sebagian besar kolom bidang diharapkan menjadi NULL, yaitu, setiap baris biasanya memiliki data hanya untuk beberapa bidang pertama, sisanya adalah NULL. Nama, tanggal dan versi membentuk kendala unik di atas meja.

Saya perlu diff data yang dimasukkan sehubungan dengan tabel ini, untuk versi yang diberikan. Setiap baris harus di-diff - baris diidentifikasi oleh nama, tanggal dan versi, dan setiap perubahan dalam nilai-nilai apa pun di kolom bidang harus ditampilkan di dalam diff.

Pembaruan: semua bidang tidak harus bertipe desimal. Beberapa dari mereka mungkin nvarchars. Saya lebih suka diff terjadi tanpa mengubah tipe, walaupun output diff dapat mengubah segalanya menjadi nvarchar karena hanya digunakan untuk tampilan yang dimaksudkan.

Misalkan inputnya adalah sebagai berikut, dan versi yang diminta adalah 2 ,:

Name    |date    |fieldA   |fieldB|..|fieldZ
Foo     |20120101|25       |NULL  |.. |NULL
Foo     |20120102|26       |27    |.. |NULL
Bar     |20120303|24       |126   |.. |NULL
Baz     |20120101|15       |NULL  |.. |NULL

Perbedaannya harus dalam format berikut:

name    |date    |field    |oldValue    |newValue
Foo     |20120101|FieldA   |23          |25
Foo     |20120102|FieldA   |NULL        |26
Foo     |20120102|FieldB   |NULL        |27
Bar     |20120303|FieldB   |123         |126
Baz     |20120101|FieldA   |NULL        |15

Solusi saya sejauh ini adalah pertama-tama menghasilkan diff, menggunakan KECUALI dan UNION. Kemudian konversikan diff ke format output yang diinginkan menggunakan JOIN dan CROSS APPLY. Meskipun ini tampaknya berhasil, saya bertanya-tanya apakah ada cara yang lebih bersih dan lebih efisien untuk melakukan ini. Jumlah bidang mendekati 100, dan setiap tempat dalam kode yang memiliki ... sebenarnya adalah sejumlah besar garis. Baik tabel input maupun tabel yang ada diharapkan cukup besar dari waktu ke waktu. Saya baru mengenal SQL dan masih mencoba mempelajari penyetelan kinerja.

Ini SQL untuknya:

CREATE TABLE #diff
(   [change] [nvarchar](50) NOT NULL,
    [name] [nvarchar](50) NOT NULL,
    [date] [int] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    .....
    [FieldZ] [decimal](38, 10) NULL
)

--Generate the diff in a temporary table
INSERT INTO #diff
SELECT * FROM
(

(
    SELECT
        'old' as change,
        name,
        date,
        FieldA,
        FieldB,
        ...,
        FieldZ
    FROM 
        myTable mt 
    WHERE 
        version = @version
        AND mt.name + '_' + CAST(mt.date AS VARCHAR) IN (SELECT name + '_' + CAST(date AS VARCHAR) FROM @diffInput) 
    EXCEPT
    SELECT 'old' as change,* FROM @diffInput
)
UNION

(
    SELECT 'new' as change, * FROM @diffInput
    EXCEPT
    SELECT
        'new' as change,
        name,
        date,
        FieldA, 
        FieldB,
        ...,
        FieldZ
    FROM 
        myTable mt 
    WHERE 
        version = @version 
        AND mt.name + '_' + CAST(mt.date AS VARCHAR) IN (SELECT name + '_' + CAST(date AS VARCHAR) FROM @diffInput) 
) 
) AS myDiff

SELECT 
d3.name, d3.date, CrossApplied.field, CrossApplied.oldValue, CrossApplied.newValue
FROM
(
    SELECT 
        d2.name, d2.date, 
        d1.FieldA AS oldFieldA, d2.FieldA AS newFieldA, 
        d1.FieldB AS oldFieldB, d2.FieldB AS newFieldB,
        ...
        d1.FieldZ AS oldFieldZ, d2.FieldZ AS newFieldZ,
    FROM #diff AS d1
    RIGHT OUTER JOIN #diff AS d2
    ON 
        d1.name = d2.name
        AND d1.date = d2.date
        AND d1.change = 'old'
    WHERE d2.change = 'new'
) AS d3
CROSS APPLY (VALUES ('FieldA', oldFieldA, newFieldA), 
                ('FieldB', oldFieldB, newFieldB),
                ...
                ('FieldZ', oldFieldZ, newFieldZ))
                CrossApplied (field, oldValue, newValue)
WHERE 
    crossApplied.oldValue != crossApplied.newValue 
    OR (crossApplied.oldValue IS NULL AND crossApplied.newValue IS NOT NULL) 
    OR (crossApplied.oldValue IS NOT NULL AND crossApplied.newValue IS NULL)  

Terima kasih!

Jawaban:


5

Berikut ini pendekatan lain:

SELECT
  di.name,
  di.date,
  x.field,
  x.oldValue,
  x.newValue
FROM
  @diffInput AS di
  LEFT JOIN dbo.myTable AS mt ON
    mt.version = @version
    AND mt.name = di.name
    AND mt.date = di.date
  CROSS APPLY
  (
    SELECT
      'fieldA',
      mt.fieldA,
      di.fieldA
    WHERE
      NOT EXISTS (SELECT mt.fieldA INTERSECT SELECT di.fieldA)

    UNION ALL

    SELECT
      'fieldB',
      mt.fieldB,
      di.fieldB
    WHERE
      NOT EXISTS (SELECT mt.fieldB INTERSECT SELECT di.fieldB)

    UNION ALL

    SELECT
      'fieldC',
      mt.fieldC,
      di.fieldC
    WHERE
      NOT EXISTS (SELECT mt.fieldC INTERSECT SELECT di.fieldC)

    UNION ALL

    ...
  ) AS x (field, oldValue, newValue)
;

Begini Cara kerjanya:

  1. Kedua tabel digabung menggunakan gabungan luar, @diffInputberada di sisi luar agar sesuai dengan gabungan kanan Anda.

  2. Hasil sambungan tidak kondisional diproteksi menggunakan CROSS APPLY, di mana "kondisional" berarti bahwa setiap pasangan kolom diuji secara individual dan dikembalikan hanya jika kolom berbeda.

  3. Pola setiap kondisi pengujian

    NOT EXISTS (SELECT oldValue INTERSECT SELECT newValue)

    setara dengan Anda

    oldValue != newValue
    OR (oldValue IS NULL AND newValue IS NOT NULL)
    OR (oldValue IS NOT NULL AND newValue IS NULL)

    hanya lebih ringkas. Anda dapat membaca lebih lanjut tentang penggunaan INTERSECT ini secara mendetail di artikel Paul White Rencana Undocumented Query: Perbandingan Kesetaraan .

Pada nada yang berbeda, karena Anda mengatakan,

Baik tabel input maupun tabel yang ada diharapkan cukup besar dari waktu ke waktu

Anda mungkin ingin mempertimbangkan mengganti variabel tabel yang Anda gunakan untuk tabel input dengan tabel sementara. Ada jawaban yang sangat komprehensif oleh Martin Smith yang mengeksplorasi perbedaan antara keduanya:

Singkatnya, sifat-sifat tertentu dari variabel tabel, seperti misalnya tidak adanya statistik kolom, dapat membuatnya kurang ramah optimizer untuk skenario Anda daripada tabel sementara.


Jika tipe data tidak sama untuk bidang AZ 2 bidang dalam pernyataan pilih perlu dikonversi ke varchar atau pernyataan serikat tidak akan berfungsi.
Andre

5

Edit bidang yang memiliki jenis berbeda, bukan hanya decimal.

Anda dapat mencoba menggunakan sql_varianttipe. Saya tidak pernah menggunakannya secara pribadi, tetapi ini mungkin solusi yang bagus untuk kasus Anda. Untuk mencobanya cukup ganti semua [decimal](38, 10)dengan sql_variantskrip SQL. Permintaan itu sendiri tetap persis seperti apa adanya, tidak ada konversi eksplisit diperlukan untuk melakukan perbandingan. Hasil akhirnya akan memiliki kolom dengan nilai dari berbagai jenis di dalamnya. Kemungkinan besar, pada akhirnya Anda harus tahu entah bagaimana jenis di bidang mana untuk memproses hasil dalam aplikasi Anda, tetapi kueri itu sendiri akan berfungsi dengan baik tanpa konversi.


Ngomong-ngomong, menyimpan tanggal sebagai ide yang buruk int.

Alih-alih menggunakan EXCEPTdan UNIONmenghitung diff, saya akan menggunakan FULL JOIN. Bagi saya, secara pribadi, sulit untuk mengikuti logika di belakang EXCEPTdan UNIONpendekatan.

Saya akan mulai dengan tidak memvoting data, daripada melakukannya terakhir (menggunakan CROSS APPLY(VALUES)seperti yang Anda lakukan). Anda dapat menghilangkan pembekuan input, jika Anda melakukannya di muka, di sisi pemanggil.

Anda harus mendaftar semua 100 kolom hanya di CROSS APPLY(VALUES).

Query terakhir cukup sederhana, sehingga tabel temp tidak benar-benar diperlukan. Saya pikir lebih mudah untuk menulis dan memelihara daripada versi Anda. Ini SQL Fiddle .

Siapkan data sampel

DECLARE @TMain TABLE (
    [ID] [int] NOT NULL,
    [Version] [int] NOT NULL,
    [Name] [nvarchar](50) NOT NULL,
    [dt] [date] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    [FieldZ] [decimal](38, 10) NULL
);

INSERT INTO @TMain ([ID],[Version],[Name],[dt],[FieldA],[FieldB],[FieldZ]) VALUES
(1,1,'Foo','20120101',23,23  ,25334123),
(2,2,'Foo','20120101',23,NULL,NULL),
(3,2,'Bar','20120303',24,123 ,NULL),
(4,2,'Bee','20120303',34,-34 ,NULL);

DECLARE @TInput TABLE (
    [Name] [nvarchar](50) NOT NULL,
    [dt] [date] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    [FieldZ] [decimal](38, 10) NULL
);

INSERT INTO @TInput ([Name],[dt],[FieldA],[FieldB],[FieldZ]) VALUES
('Foo','20120101',25,NULL,NULL),
('Foo','20120102',26,27  ,NULL),
('Bar','20120303',24,126 ,NULL),
('Baz','20120101',15,NULL,NULL);

DECLARE @VarVersion int = 2;

Permintaan utama

CTE_Mainadalah data asli yang tidak diproteksi difilter ke yang diberikan Version. CTE_Inputadalah tabel input, yang dapat disediakan dalam format ini. Menggunakan kueri utama FULL JOIN, yang menambah baris hasil dengan Bee. Saya pikir mereka harus dikembalikan, tetapi jika Anda tidak ingin melihat mereka, Anda dapat menyaring mereka keluar dengan menambahkan AND CTE_Input.FieldValue IS NOT NULLatau mungkin menggunakan LEFT JOINbukan FULL JOIN, saya tidak melihat ke dalam rincian di sana, karena saya pikir mereka harus dikembalikan.

WITH
CTE_Main
AS
(
    SELECT
        Main.ID
        ,Main.Version
        ,Main.Name
        ,Main.dt
        ,FieldName
        ,FieldValue
    FROM
        @TMain AS Main
        CROSS APPLY
        (
            VALUES
                ('FieldA', Main.FieldA),
                ('FieldB', Main.FieldB),
                ('FieldZ', Main.FieldZ)
        ) AS CA(FieldName, FieldValue)
    WHERE
        Main.Version = @VarVersion
)
,CTE_Input
AS
(
    SELECT
        Input.Name
        ,Input.dt
        ,FieldName
        ,FieldValue
    FROM
        @TInput AS Input
        CROSS APPLY
        (
            VALUES
                ('FieldA', Input.FieldA),
                ('FieldB', Input.FieldB),
                ('FieldZ', Input.FieldZ)
        ) AS CA(FieldName, FieldValue)
)

SELECT
    ISNULL(CTE_Main.Name, CTE_Input.Name) AS FullName
    ,ISNULL(CTE_Main.dt, CTE_Input.dt) AS FullDate
    ,ISNULL(CTE_Main.FieldName, CTE_Input.FieldName) AS FullFieldName
    ,CTE_Main.FieldValue AS OldValue
    ,CTE_Input.FieldValue AS NewValue
FROM
    CTE_Main
    FULL JOIN CTE_Input ON 
        CTE_Input.Name = CTE_Main.Name
        AND CTE_Input.dt = CTE_Main.dt
        AND CTE_Input.FieldName = CTE_Main.FieldName
WHERE
    (CTE_Main.FieldValue <> CTE_Input.FieldValue)
    OR (CTE_Main.FieldValue IS NULL AND CTE_Input.FieldValue IS NOT NULL)
    OR (CTE_Main.FieldValue IS NOT NULL AND CTE_Input.FieldValue IS NULL)
--ORDER BY FullName, FullDate, FullFieldName;

Hasil

FullName    FullDate    FullFieldName   OldValue        NewValue
Foo         2012-01-01  FieldA          23.0000000000   25.0000000000
Foo         2012-01-02  FieldA          NULL            26.0000000000
Foo         2012-01-02  FieldB          NULL            27.0000000000
Bar         2012-03-03  FieldB          123.0000000000  126.0000000000
Baz         2012-01-01  FieldA          NULL            15.0000000000
Bee         2012-03-03  FieldB          -34.0000000000  NULL
Bee         2012-03-03  FieldA          34.0000000000   NULL
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.