重複データからユニークな行を取り出すSQL

昨日のエントリで N対M になるようなテーブルを結合するときは、N対1 にする必要がある旨書いたが、具体的にどうするかまでは書かなかった。

例えば、全銀CSVをそのまま取り込んだ銀行マスタ(銀行コード、銀行名称、銀行支店コード、銀行支店名称)から銀行コードと銀行名称を取るSQLを考える。

この場合、銀行コード、銀行名称は常識的に考えて同じ物が入っていることは明らかであるので、次のように書ける。

SELECT
    GM.銀行コード,
    MIN(GM.銀行名称) AS 銀行名称
FROM
    銀行マスタ GM
GROUP BY
    GM.銀行コード

昨日のエントリで注意したように、銀行名称を GROUP BY に加えてはならない。もし何らかの不具合により同じ銀行コードに対し異なる銀行名称が混在してしまった場合に、トランザクションが重複してしまう。不適切なマスタ名が表示されることとトランザクションが重複することでは後者の方が重大な不具合あるから、絶対に避ける必要がある。

問題は、名称が同じコードに対しある条件で混在する仕様である場合である。例えば、バージョン番号が用意されており、同じコードに対し複数の名称が存在する場合は最新バージョンを表示すること、という仕様の場合は次のように書く必要がある。

SELECT
    GM.銀行コード,
    MIN(GM.銀行名称) AS 銀行名称
FROM
    銀行マスタ GM
WHERE
    GM.バージョン番号 = (
        SELECT
            MAX(GM2.バージョン番号)
        FROM
            銀行マスタ GM2
        WHERE
            GM2.銀行コード = GM.銀行コード
    )
GROUP BY
    GM.銀行コード

ただ、サブクエリーを含むこともあり決して見通しの良いSQLではない。もし OracleDB2SQL Server と言った商用データベースを利用しているのであれば、ウィンドウ関数を使う方がより柔軟で直感的だと思う(ここで使用した ROW_NUMBER 関数はとても有用なので覚えておいた方がよい)。

SELECT
    GM.銀行コード,
    GM.銀行名称
FROM
    (
        SELECT
            GM.銀行コード,
            GM.銀行名称,
            ROW_NUMBER () OVER (
                PARTITION BY
                  GM.銀行コード
                ORDER BY
                  GM.バージョン番号 DESC,
                  GM.銀行支店コード
            ) AS 行番号
        FROM
            銀行マスタ GM
    ) GM
WHERE
    GM.行番号 = 1

ROW_NUMBER関数使用時の注意点は、PARTITION BY と ORDER BY の項目に行を特定するユニークな項目の組み合わせを入れる必要があるということだ。項目が欠けると実行する度に異なる順序が返ってくることになる。