[SQL]INSERT SELECTでテーブルから直接INSERTする

先日投稿した記事のテーブルを使って、商品別売上実績のレコードが必ず存在するようにしてみます。

月末処理で売上平均金額を求める前に売上のない商品について0円の商品別売上実績レコードを作成する状況を想定しています。

ここではSELECTした結果をINSERTするINSERT SELECTを使ってデータを登録します。

INSERT SELECTとは

INSERT SELECTとは、INSERTするデータをSELECTで作成する手法のことです。例えば、テーブルAにテーブルBから抽出したレコードをそのままINSERTする場合、以下のように書きます。

INSERT INTO A
SELECT
  *
FROM
  B

この例では、テーブルAとテーブルBは同じカラムを持っており、テーブルBから全件抽出してそのままテーブルAにINSERTしています。このやり方はバックアップ対象と同じカラムを持つバックアップ用のテーブルを作成して、そこにテックアップ対象のレコードを全件抽出しておく、というときによく使います。

SELECT時に対象のカラムと行を絞り込む

今回実現したいのは、SELECTするテーブルとINSERTするテーブルのカラムは異なっています。これはSELECTするカラムをINSERTするカラムに合わせることで対応します。

もう一つ、すべてのカラムを抽出するのではなく、すでにINSERTするテーブルに存在している商品コードはINSERTする必要がない(INSERTすると一意制約違反となる)ため、商品テーブルにあって商品別売上実績テーブルにない商品コードだけINSERTするように対象レコードを絞り込みます。

作成したSQLは以下のようになります。

INSERT INTO sales_by_product
SELECT
    T1.product_code,
    0 total_sales_amount
FROM
    products T1
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            sales_by_product T2
        WHERE
            T1.product_code = T2.product_code
    );

INSERTするテーブルのカラムと同じカラムを抽出する

SELECTではINSERTするテーブルと同じカラムを抽出します。

SELECT
    T1.product_code,
    0 total_sales_amount
FROM
    products T1

INSERT先の商品別売上実績テーブルはproduct_codetotal_sales_amountを持っているため、これらのカラムだけになるように抽出しています。product_codeproductsテーブルのカラムを使用し、total_sales_amountは今回やりたいことに合わせてデフォルト値の0を設定するようにしています。0 total_sales_amountのように固定値を直接記述することができるかどうかは使用しているRDBMSによって異なりますので注意してください。

INSERTするテーブルに存在していないレコードのみ抽出する

少しややこしいですが、NOT EXISTSを使ってsales_by_productに存在しない商品コードのみ抽出するようにします。

FROM
    products T1
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            sales_by_product T2
        WHERE
            T1.product_code = T2.product_code
    );

SELECTしているproducts T1NOT EXISTS内のSELECT文のsales_by_product T2product_codeで結合することで、T1T2双方に存在するsales_by_productが抽出できます。ということは、T1NOT EXISTS以外に絞り込み条件を持たないので、すべてのT1のレコード(すべての商品)について、それぞれのレコードに対応するT2(商品別売上実績)を抽出してみて、それが存在しなかったら(NOT EXISTS)、SELECTされるということになります。

NOT EXISTS内のSELECTしている列が1となっていますが、これは行が抽出できていること自体にしか意味がないので、無駄なデータを抽出しないために適当な1という値を指定しているだけです。'x'とか3とかでも問題ありませんし、データ量のことを気にしないのであれば*でも構いません。

INSERTする前にSELECTだけを実行してみる

実際にINSERTをする前にINSERT部分以外のSELECT文単独で実行結果を確認しておきます。実務においてもまずはINSERTしたい形と同じかをSELECTだけで確認し、それが確認できたらINSERTするのが安全です。

SELECT
    T1.product_code,
    0 total_sales_amount
FROM
    products T1
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            sales_by_product T2
        WHERE
            T1.product_code = T2.product_code
    );

まずは、INSERT前の商品別売上実績テーブルを確認してみます。

SELECT * FROM sales_by_product;

=>
S001,50
S003,250
S004,350
S006,450

商品コードS002S005が欠損していることがわかります。これが売上合計金額0で抽出できればINSERTしたい内容と一致します。

SELECT
    T1.product_code,
    0 total_sales_amount
FROM
    products T1
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            sales_by_product T2
        WHERE
            T1.product_code = T2.product_code
    );

=>
S002,0
S005,0

問題なさそうです。

では実際にINSERTまでやって、再度全件抽出してみます。

INSERT INTO sales_by_product
SELECT
    T1.product_code,
    0 total_sales_amount
FROM
    products T1
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            sales_by_product T2
        WHERE
            T1.product_code = T2.product_code
    );

SELECT * FROM sales_by_product;

=>
S001,50
S002,0
S003,250
S004,350
S005,0
S006,450

商品コードS002S005が売上合計金額0で登録されていることが確認できました。

トランザクション機能を使って確認後にコミットする

今回は自動トランザクションでINSERTしたら自動コミットされるようにしています。

実務では手動トランザクションで明示的にコミットするまではDBにコミットされないようにしておくことで、INSERT後の確認でOKになるまでは作業のやり直しができるようにしておくことが重要です。

まとめ

INSERT SELECTを使ってあるテーブルから直接INSERTする方法について確認しました。バッチなどでのバルクインサートやテーブルのバックアップ・リストアなどINSERT SELECTを行うシーンは意外とあります。

INSERTしたい結果を抽出できるSELECT文を書いて、その前にINSERT INTO テーブル名を付けるということを覚えておけば、それほど悩むこともないと思います。細かい点を抜きにすればUPDATE SELECTとは異なり、RDBMSによって構文が変わるということもないので、どれかのRDBMSでやり方を覚えておけば他のRDBMSでもほぼそのまま適用できます。

[Oracle]WITH句で階層問い合わせを行う

OracleではWITH句を使って階層問い合わせを行うことができます。

サンプルデータ

説明として、階層を持つフォルダを管理するFOLDER_TBLとフォルダに格納するファイルを管理するFILE_TBLを用意します。

CREATE TABLE FOLDER_TBL (
  FOLDER_ID NUMBER(10) NOT NULL,
  PARENT_ID NUMBER(10),
  FOLDER_NAME VARCHAR2(255) NOT NULL,
  PRIMARY KEY (FOLDER_ID),
  FOREIGN KEY (FOLDER_ID) REFERENCES FOLDER_TBL(FOLDER_ID) ON DELETE CASCADE
);

FOLDER_TBLはフォルダを識別するためのFOLDER_IDとあるフォルダとその上位フォルダを紐づけるためのPARENT_IDを持っています。PARENT_IDは最上位フォルダの場合はNULLになります。

CREATE TABLE FILE_TBL (
  FILE_ID NUMBER(10) NOT NULL,
  FOLDER_ID NUMBER(10) NOT NULL,
  FILE_NAME VARCHAR2(255) NOT NULL,
  FILE_SIZE NUMBER(10),
  PRIMARY KEY (FILE_ID),
  FOREIGN KEY (FOLDER_ID) REFERENCES FOLDER_TBL(FOLDER_ID) ON DELETE CASCADE
);

ファイルはFILE_IDによって識別され、格納先のフォルダに紐づけるためのFOLDER_IDを持っています。FOLDER_IDNULLになることはなく、必ずどこかのフォルダに紐づきます。

-- FOLDER_TBLテーブルにデータを登録するSQL
INSERT INTO FOLDER_TBL (FOLDER_ID, parent_id, FOLDER_NAME) VALUES (1, NULL, 'Folder 1');
INSERT INTO FOLDER_TBL (FOLDER_ID, parent_id, FOLDER_NAME) VALUES (2, 1, 'Folder 1-1');
INSERT INTO FOLDER_TBL (FOLDER_ID, parent_id, FOLDER_NAME) VALUES (3, NULL, 'Folder 2');

-- FILE_TBLテーブルにデータを登録するSQL
INSERT INTO FILE_TBL (FILE_ID, FOLDER_ID, FILE_NAME, FILE_SIZE) VALUES (1, 1, 'file1.txt', 100);
INSERT INTO FILE_TBL (FILE_ID, FOLDER_ID, FILE_NAME, FILE_SIZE) VALUES (2, 1, 'file2.txt', 200);
INSERT INTO FILE_TBL (FILE_ID, FOLDER_ID, FILE_NAME, FILE_SIZE) VALUES (3, 2, 'file3.txt', 150);
INSERT INTO FILE_TBL (FILE_ID, FOLDER_ID, FILE_NAME, FILE_SIZE) VALUES (4, 2, 'file4.txt', 300);
INSERT INTO FILE_TBL (FILE_ID, FOLDER_ID, FILE_NAME, FILE_SIZE) VALUES (5, 3, 'file5.txt', 50);
INSERT INTO FILE_TBL (FILE_ID, FOLDER_ID, FILE_NAME, FILE_SIZE) VALUES (6, 3, 'file6.txt', 250);

サンプルデータを用意しました。

FOLDER_TBLは以下のようなデータとなっています。最上位階層は二つ、うち一つにはサブフォルダがあります。

FOLDER_IDPARENT_IDFOLDER_NAME
1NULLFolder 1
21Folder 1-1
3NULLFolder 2

FILE_TBLは以下のようなデータになっています。各フォルダに2ファイルずつファイルが格納されています。

FILE_IDFOLDER_IDFILE_NAMEFILE_SIZE
11file1.txt100
21file2.txt200
32file3.txt150
42file4.txt300
53file5.txt50
63file6.txt250

下層から上層に向かって階層問い合わせをする

あるファイルを指定し、その格納フォルダから最上位フォルダまでの階層を取得してみます。ここではサブフォルダにあるfile3.txtが格納されているフォルダから上位フォルダへ向かって階層問い合わせを行います。

WITH FOLDER_HIERARCY (
    FOLDER_ID,
    PARENT_ID,
    FOLDER_NAME,
    FOLDER_PATH
) AS (
  -- 最下層のフォルダ
  SELECT
    FOLDER_ID,
    PARENT_ID,
    FOLDER_NAME,
    FOLDER_NAME FOLDER_PATH
  FROM
    FOLDER_TBL FD1
  WHERE
    -- フォルダに格納されているファイルのファイル名が'file3.txt'
    EXISTS(SELECT 1 FROM FILE_TBL FL1 WHERE FD1.FOLDER_ID = FL1.FOLDER_ID AND FL1.FILE_NAME = 'file3.txt')
  UNION ALL
  SELECT
    FD2.FOLDER_ID,
    FD2.PARENT_ID,
    FD2.FOLDER_NAME,
    FD2.FOLDER_NAME || ' / ' || BASE.FOLDER_PATH
  FROM
    FOLDER_HIERARCY BASE -- このテーブルに対して
    INNER JOIN FOLDER_TBL FD2 -- このテーブルを結合していく
    ON
      BASE.PARENT_ID = FD2.FOLDER_ID -- 下位フォルダのPARENT_IDに一致するFOLDER_IDのフォルダは1つ上位のフォルダとなる
)
SELECT
  FOLDER_PATH || ' / file3.txt'
FROM
  FOLDER_HIERARCY
WHERE
  PARENT_ID IS NULL -- PARENT_IDがNULLのレコードに絞り込むと最上位フォルダまでのパスが得られる
;

ポイントは、

  • 階層問い合わせをするときは項目を明示する(しないとエラーになる)
  • 基準となるレコードに対して上位のレコードを結合していく

です。下層から上層に向かって階層問い合わせする場合は、基準となるテーブル(BASE)の親IDと一つ上位となるテーブル(FD2)のIDを結合する点に気をつけましょう。

上層から下層に向かって階層問い合わせをする

同じことを逆からやってみましょう。

WITH FOLDER_HIERARCY (
    FOLDER_ID,
    PARENT_ID,
    FOLDER_NAME,
    FOLDER_PATH
) AS (
  -- 最上層のフォルダ
  SELECT
    FOLDER_ID,
    PARENT_ID,
    FOLDER_NAME,
    FOLDER_NAME FOLDER_PATH
  FROM
    FOLDER_TBL FD1
  WHERE
    FD1.PARENT_ID IS NULL
  UNION ALL
  SELECT
    FD2.FOLDER_ID,
    FD2.PARENT_ID,
    FD2.FOLDER_NAME,
    BASE.FOLDER_PATH || ' / ' || FD2.FOLDER_NAME
  FROM
    FOLDER_HIERARCY BASE -- このテーブルに対して
    INNER JOIN FOLDER_TBL FD2 -- このテーブルを結合していく
    ON
      BASE.FOLDER_ID = FD2.PARENT_ID -- 上位フォルダのFOLDER_IDに一致するPARENT_IDのフォルダは1つ下位のフォルダとなる
)
SELECT
  FOLDER_PATH || ' / ' || FILE_NAME
FROM
  FOLDER_HIERARCY T1
  INNER JOIN FILE_TBL T2
  ON
    T1.FOLDER_ID = T2.FOLDER_ID
WHERE
  FILE_NAME = 'file3.txt'
;

上層から下層に向かって階層問い合わせする場合は、基準となるテーブル(BASE)のIDと一つ下位となるテーブル(FD2)の親IDを結合する点に気をつけましょう。

いずれの検索結果もFolder 1 / Folder 1-1 / file3.txtとなります。

モバイルバージョンを終了