背景と状況
新システムにバージョンアップする際、端境期には一定期間、現システムと新システムの両方にデータ入力が必要になる場合があります。マスター登録がその代表ですが往々にして、まだ本番稼働していない新システムに入力をし忘れてしまいます。そうなると入力漏れしたデータを探さなければなりません。
やりたいこと
2つのテーブルを比較して異なるレコードのみを出力したい。要はSQLでデータを比較したい。
※IBM Db2 V11.1 Windowsで確認していますが他のDBMSでもできるはずです。
比較するテーブル
同じスキーマで内容の異なるテーブルを比較します。別のデータベースから一方のテーブルをコピーして名前を変更します。
<-主キー->
MST_ID      MST_TEXT        MST_PRICE   MST_DATE
----------- --------------- ----------- ----------
          1 リンゴ                  200 2020-01-01
          2 バナナ                  150 2020-01-01
          3 イチゴ                  500 2020-01-01
          4 ミカン                  300 2020-01-01
          6 メロン                  900 2020-01-01 <-- 新システムにないレコード
<-主キー->
MST_ID      MST_TEXT        MST_PRICE   MST_DATE
----------- --------------- ----------- ----------
          1 リンゴ                  200 2020-01-01
          2 バナナ                  150 2020-09-15 <-- 日付が違う
          3 イチゴ    価格が違う --> 650 2020-01-01
          4 ミカン                  300 2020-01-01
          5 スイカ                  850 2020-01-01 <-- 現システムにないレコード
簡単な比較方法
exceptを使うと簡単に比較できます。
SELECT * 
FROM   mst_tbl_current 
EXCEPT 
SELECT * 
FROM   mst_tbl_new 
ORDER  BY mst_id ; 
現システムのテーブルを基準に新システムと異なるレコードのみを照会しています。
F:\>db2 -tvf except_mst_tbl_current.sql
SELECT * FROM   mst_tbl_current EXCEPT SELECT * FROM   mst_tbl_new ORDER  BY mst_id 
MST_ID      MST_TEXT        MST_PRICE   MST_DATE
----------- --------------- ----------- ----------
          2 バナナ                  150 2020-01-01
          3 イチゴ                  500 2020-01-01
          6 メロン                  900 2020-01-01
  3 レコードが選択されました。
バナナは日付、イチゴは価格が異なり、メロンは現システムのみにあるレコードです。
問題点
単純にexceptを使うと新システムだけにあるスイカが表示されません。
逆に新システムを基準にして照会すると現システムだけにあるメロンが表示されません。
直感的にわかりにくく、コレジャナイ感がありますね。
対策
現システムを基準にしたexceptと新システムを基準にしたexceptとを主キーでフルジョインします。
SELECT CASE 
         WHEN c.mst_id IS NULL THEN n.mst_id 
         ELSE c.mst_id 
       END         AS MST_ID, 
       c.mst_text  AS Cur_MST_TEXT, 
       c.mst_price AS Cur_MST_PRICE, 
       c.mst_date  AS Cur_MST_DATE, 
       n.mst_text  AS New_MST_TEXT, 
       n.mst_price AS New_MST_PRICE, 
       n.mst_date  AS New_MST_DATE 
FROM   (SELECT * 
        FROM   mst_tbl_current 
        EXCEPT 
        SELECT * 
        FROM   mst_tbl_new) c 
       FULL JOIN (SELECT * 
                  FROM   mst_tbl_new 
                  EXCEPT 
                  SELECT * 
                  FROM   mst_tbl_current) n 
              ON n.mst_id = c.mst_id 
ORDER  BY mst_id; 
片方にしかないレコードも含めて異なるレコードのみが出力されます。
F:\>db2 -tvf except_mst_tbl_comp.sql
SELECT CASE WHEN c.mst_id IS NULL THEN n.mst_id ELSE c.mst_id END         AS MST_ID, c.mst_text  AS Cur_MST_TEXT, c.mst_price AS Cur_MST_PRICE, c.mst_date  AS Cur_MST_DATE, n.mst_text  AS New_MST_TEXT, n.mst_price AS New_MST_PRICE, n.mst_date  AS New_MST_DATE FROM   (SELECT * FROM   mst_tbl_current EXCEPT SELECT * FROM   mst_tbl_new) c FULL JOIN (SELECT * FROM   mst_tbl_new EXCEPT SELECT * FROM   mst_tbl_current) n ON n.mst_id = c.mst_id ORDER  BY mst_id
MST_ID      CUR_MST_TEXT    CUR_MST_PRICE CUR_MST_DATE NEW_MST_TEXT    NEW_MST_PRICE NEW_MST_DATE
----------- --------------- ------------- ------------ --------------- ------------- ------------
          2 バナナ                    150 2020-01-01   バナナ                    150 2020-09-15
          3 イチゴ                    500 2020-01-01   イチゴ                    650 2020-01-01
          5 -                           - -            スイカ                    850 2020-01-01
          6 メロン                    900 2020-01-01   -                           - -
  4 レコードが選択されました。
思っていたイメージのものができました。さらに差分を比較したいなら以下のSQLでdiffを取ることもできます。
(SELECT 'MST_TBL_CURRENT' AS SOURCE, 
        * 
 FROM   (SELECT * 
         FROM   mst_tbl_current 
         EXCEPT 
         SELECT * 
         FROM   mst_tbl_new)) 
UNION ALL 
(SELECT 'MST_TBL_NEW' AS SOURCE, 
        * 
 FROM   (SELECT * 
         FROM   mst_tbl_new 
         EXCEPT 
         SELECT * 
         FROM   mst_tbl_current)) 
ORDER  BY mst_id; 
差異があるレコードは2行で、片方にしかないレコードは1行で出力されます。
F:\>db2 -tvf except_mst_tbl_diff.sql
(SELECT 'MST_TBL_CURRENT' AS SOURCE, * FROM   (SELECT * FROM   mst_tbl_current EXCEPT SELECT * FROM   mst_tbl_new)) UNION ALL (SELECT 'MST_TBL_NEW' AS SOURCE, * FROM   (SELECT * FROM   mst_tbl_new EXCEPT SELECT * FROM   mst_tbl_current)) ORDER  BY mst_id
SOURCE          MST_ID      MST_TEXT        MST_PRICE   MST_DATE
--------------- ----------- --------------- ----------- ----------
MST_TBL_NEW               2 バナナ                  150 2020-09-15
MST_TBL_CURRENT           2 バナナ                  150 2020-01-01
MST_TBL_NEW               3 イチゴ                  650 2020-01-01
MST_TBL_CURRENT           3 イチゴ                  500 2020-01-01
MST_TBL_NEW               5 スイカ                  850 2020-01-01
MST_TBL_CURRENT           6 メロン                  900 2020-01-01
  6 レコードが選択されました。
さらに
全ての列ではなく特定の列だけ比較したい場合はどうすればいいでしょうか。例えば、日付の列は比較対象から外してみます。方法としてはexceptでselectする際、日付の列以外を指定にします。
SELECT mst_id, 
       mst_text, 
       mst_price
FROM   mst_tbl_current 
EXCEPT 
SELECT mst_id, 
       mst_text, 
       mst_price 
FROM   mst_tbl_new 
ORDER  BY mst_id ; 
バナナは日付が異なりますが比較対象外なので同じとみなされ、出力されません。
F:\>db2 -tvf except_mst_tbl_current_part.sql
SELECT mst_id, mst_text, mst_price FROM   mst_tbl_current EXCEPT SELECT mst_id, mst_text, mst_price FROM   mst_tbl_new ORDER  BY mst_id 
MST_ID      MST_TEXT        MST_PRICE
----------- --------------- -----------
          3 イチゴ                  500
          6 メロン                  900
  2 レコードが選択されました。
但し、これだと日付列を表示することができません。比較対象外にしたいが列は表示したいときはnot existsを使います。
SELECT * 
FROM   mst_tbl_current c 
WHERE  NOT EXISTS (SELECT * 
                   FROM   mst_tbl_new n 
                   WHERE  c.mst_id = n.mst_id 
                          AND c.mst_text = n.mst_text 
                          AND c.mst_price = n.mst_price) 
ORDER  BY mst_id ; 
こちらは日付を比較対象にせず、日付列は表示します。
F:\>db2 -tvf not_exists_mst_tbl_current.sql
SELECT * FROM   mst_tbl_current c WHERE  NOT EXISTS (SELECT * FROM   mst_tbl_new n WHERE  c.mst_id = n.mst_id AND c.mst_text = n.mst_text AND c.mst_price = n.mst_price) ORDER  BY mst_id 
MST_ID      MST_TEXT        MST_PRICE   MST_DATE
----------- --------------- ----------- ----------
          3 イチゴ                  500 2020-01-01
          6 メロン                  900 2020-01-01
  2 レコードが選択されました。
最終形
指定した列のみを比較して異なるレコードのみを表示させたい場合、SQLは複雑になりますがnot existsを使うと出力が見やすいと思います。
SELECT CASE 
         WHEN c.mst_id IS NULL THEN n.mst_id 
         ELSE c.mst_id 
       END         AS MST_ID, 
       c.mst_text  AS Cur_MST_TEXT, 
       c.mst_price AS Cur_MST_PRICE, 
       c.mst_date  AS Cur_MST_DATE, 
       n.mst_text  AS New_MST_TEXT, 
       n.mst_price AS New_MST_PRICE, 
       n.mst_date  AS New_MST_DATE 
FROM   (SELECT * 
        FROM   mst_tbl_current c 
        WHERE  NOT EXISTS (SELECT * 
                           FROM   mst_tbl_new n 
                           WHERE  c.mst_id = n.mst_id 
                                  AND c.mst_text = n.mst_text 
                                  AND c.mst_price = n.mst_price)) c 
       FULL JOIN (SELECT * 
                  FROM   mst_tbl_new n 
                  WHERE  NOT EXISTS (SELECT * 
                                     FROM   mst_tbl_current c 
                                     WHERE  n.mst_id = c.mst_id 
                                            AND n.mst_text = c.mst_text 
                                            AND n.mst_price = c.mst_price)) n 
              ON n.mst_id = c.mst_id 
ORDER  BY mst_id; 
以下が出力結果です。バナナは日付が異なりますが比較対象外ですので同じとみなされ、出力されません。
F:\>db2 -tvf not_exists_mst_tbl_comp.sql
SELECT CASE WHEN c.mst_id IS NULL THEN n.mst_id ELSE c.mst_id END         AS MST_ID, c.mst_text  AS Cur_MST_TEXT, c.mst_price AS Cur_MST_PRICE, c.mst_date  AS Cur_MST_DATE, n.mst_text  AS New_MST_TEXT, n.mst_price AS New_MST_PRICE, n.mst_date  AS New_MST_DATE FROM   (SELECT * FROM   mst_tbl_current c WHERE  NOT EXISTS (SELECT * FROM   mst_tbl_new n WHERE  c.mst_id = n.mst_id AND c.mst_text = n.mst_text AND c.mst_price = n.mst_price)) c FULL JOIN (SELECT * FROM   mst_tbl_new n WHERE  NOT EXISTS (SELECT * FROM   mst_tbl_current c WHERE  n.mst_id = c.mst_id AND n.mst_text = c.mst_text AND n.mst_price = c.mst_price)) n ON n.mst_id = c.mst_id ORDER  BY mst_id
MST_ID      CUR_MST_TEXT    CUR_MST_PRICE CUR_MST_DATE NEW_MST_TEXT    NEW_MST_PRICE NEW_MST_DATE
----------- --------------- ------------- ------------ --------------- ------------- ------------
          3 イチゴ                    500 2020-01-01   イチゴ                    650 2020-01-01
          5 -                           - -            スイカ                    850 2020-01-01
          6 メロン                    900 2020-01-01   -                           - -
  3 レコードが選択されました。
まとめ
- 簡単に比較したいならexcept
- 主キーでフルジョインすることによる見やすい出力
- 列指定で細かく制御したいならnot exists
参考
以下の書籍を参考にしています。
達人に学ぶSQL徹底指南書
