[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でもほぼそのまま適用できます。

[SQL]AVG関数の集計対象にNULLの値は含まれるのか?

データベーススペシャリスト試験の過去問で気になった問題があったので実機で試すことにしました。

現場ではこのような状況にならないようにSQLを書くので気にしたこともありませんでしたが、AVG関数の対象にNULLが含まれている場合、それは分子・分母から除外されて計算されます。

この点について実際にSQLを実行しながら確認していきます。

環境構築

動作確認用のMySQLのコンテナを作成します。

$ docker run -d --name test-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password mysql:8.3

コンソールまたはGUIツールで接続し、データベースを作成します。

CREATE DATABASE test;
USE test;

今回、動作確認はすべてDataGripで行っていますが、その他のツールを使用しても結果は変わりません。

商品テーブルと商品別売上実績テーブルを作成する

問題文自体は記載しませんが、令和4年秋期 データベーススペシャリスト試験 午前IIの問7の問題となります。手元に問題集がある方はそちらを参照してください。

概要を簡単に説明すると、商品テーブルと商品別売上実績テーブルが提示され、それらに対して問題文で与えられたSQLを実行するとどのような結果が返ってくるかを問う問題です。

AVG関数やGROUP BYの働き、左外部結合LEFT OUTER JOINによる結合についての理解度を問う問題となっています。

問題文と同様の商品テーブルと商品別売上実績テーブルを作成します。

-- 商品テーブル
CREATE TABLE products (
    product_code VARCHAR(4), -- 商品コード
    product_name VARCHAR(255), -- 商品名
    product_rank CHAR(1), -- 商品ランク
    PRIMARY KEY (product_code)
);

商品テーブルには商品を一意に特定する商品コードに加え、商品を格付けするための商品ランクというカラムがあることがわかります。

-- 商品別売上実績テーブル
CREATE TABLE sales_by_product (
    product_code VARCHAR(4), -- 商品コード
    total_sales_amount integer, -- 売上合計金額
    PRIMARY KEY (product_code)
);

商品別売上実績には商品ごとに売上合計金額が格納されていることがわかります。この問題ではこの売上合計金額から売上平均金額を求めています。 

次に問題文と同じデータをインサートします。

-- 商品テーブル
INSERT INTO products VALUES ('S001', 'PPP', 'A');
INSERT INTO products VALUES ('S002', 'QQQ', 'A');
INSERT INTO products VALUES ('S003', 'RRR', 'A');
INSERT INTO products VALUES ('S004', 'SSS', 'B');
INSERT INTO products VALUES ('S005', 'TTT', 'C');
INSERT INTO products VALUES ('S006', 'UUU', 'C');

-- 商品別売上実績テーブル
INSERT INTO sales_by_product VALUES ('S001', 50);
INSERT INTO sales_by_product VALUES ('S003', 250);
INSERT INTO sales_by_product VALUES ('S004', 350);
INSERT INTO sales_by_product VALUES ('S006', 450);

登録したデータを確認しておきます。

まずは商品テーブルです。

SELECT * FROM products;

=>
S001,PPP,A
S002,QQQ,A
S003,RRR,A
S004,SSS,B
S005,TTT,C
S006,UUU,C

次に商品別売上実績テーブルです。すべての商品について売上実績があるというわけではないことがわかります。

SELECT * FROM sales_by_product;

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

問題のSQLを確認する

まずは問題文のSQLを確認します。

SELECT
    AVG(T2.total_sales_amount) AS `売上平均金額`
FROM
    products T1
    LEFT OUTER JOIN sales_by_product T2
    ON
        T1.product_code = T2.product_code
WHERE
    T1.product_rank = 'A'
GROUP BY
    T1.product_rank
;

商品ランクごとに売上合計金額の平均を取得し、商品ランクがAのレコードのみ表示しています。

結果を確認するまえに、商品ランクがAのレコードを集計せずに抽出するとどうなるか確認してみます。

SELECT
    T1.product_code AS `商品コード`,
    T1.product_name AS `商品名`,
    T2.total_sales_amount AS `売上合計金額`
FROM
    products T1
    LEFT OUTER JOIN sales_by_product T2
    ON
        T1.product_code = T2.product_code
WHERE
    T1.product_rank = 'A'
;

=>
S001,PPP,50
S002,QQQ,
S003,RRR,250

上記の実行すると、商品ランクAの商品コードS002の売上合計金額がNULLになっていることがわかります。

このSQLで抽出された3レコードについてAVG関数を適用したときに、分子は50+250=300であることははっきりしていますが、分母は2でしょうか?3でしょうか?

その答えを確認するために問題文のSQLを実行してみます。

SELECT
    AVG(T2.total_sales_amount) AS `売上平均金額`
FROM
    products T1
    LEFT OUTER JOIN sales_by_product T2
    ON
        T1.product_code = T2.product_code
WHERE
    T1.product_rank = 'A'
GROUP BY
    T1.product_rank
;

=> 150.0000

実行結果をみると、150となっているので、2で割っていることがわかります。すなわち、NULLでない値について平均を取得していることがわかります。

COUNT関数の動作を確認する

もし、AVG関数を使わずにSUM関数とCOUNT関数を使うとどうなるのでしょうか?

COUNT関数について確認してみましょう。

COUNT(*)COUNT(T1.product_code)COUNT(T2.product_code)COUNT(T2.total_sales_amount)のそれぞれの結果を確認してみます。説明の都合上、COUNT(*)を最後に掲載します。

COUNT(T1.product_code)の結果

COUNT(T1.product_code)でカウントしてみると、3が返ってきます。

SELECT
    COUNT(T1.product_code) AS `商品数`
FROM
    products T1
    LEFT OUTER JOIN sales_by_product T2
    ON
        T1.product_code = T2.product_code
WHERE
    T1.product_rank = 'A'
;

=> 3

これは特に問題ないと思います。

COUNT(T2.product_code)の結果

次にCOUNT(T2.product_code)でカウントしてみます。

SELECT
    COUNT(T2.product_code) AS `売上実績数`
FROM
    products T1
    LEFT OUTER JOIN sales_by_product T2
    ON
        T1.product_code = T2.product_code
WHERE
    T1.product_rank = 'A'
;

=> 2

すると、2が返ってきます。商品コードS002に対応する商品別売上実績テーブルのレコードがないため、2とカウントされます。

COUNT(T2.total_sales_amount)の結果

COUNT(T2.total_sales_amount)についても確認します。

SELECT
    COUNT(T2.total_sales_amount) AS `売上実績数`
FROM
    products T1
    LEFT OUTER JOIN sales_by_product T2
    ON
        T1.product_code = T2.product_code
WHERE
    T1.product_rank = 'A'
;

=> 2

これも2と返ってきます。

COUNT(*)の結果

最後にCOUNT(*)でカウントしてみます。

SELECT
    COUNT(*) AS `レコード数`
FROM
    products T1
    LEFT OUTER JOIN sales_by_product T2
    ON
        T1.product_code = T2.product_code
WHERE
    T1.product_rank = 'A'
;

=> 3

これは駆動表である商品テーブルのレコード数が返ってきます。COUNT関数を使うとき、特に意識せずにCOUNT(*)を使いがちですが、何をカウントしたいのか明確にしないと意図しない結果が得られる場合がある点に注意が必要です。

そもそも売上平均金額はいくらなのか?

問題文で問われている内容から離れて、そもそも「売上平均金額」はいくらと計算されるのが正しいのでしょうか?

商品コードS002の売上合計金額を0と考えるなら、(50+0+250)÷3=100と計算されるのが正しいと思います。そう考えると、このデータの場合、NULLを含むカラムに対して安易にAVG関数を使うのは悪手のように思います。

100と計算されるにはどうすればいいでしょうか?

商品別売上実績のレコードが必ず存在するようにする

商品別売上実績に売上のない商品のレコードが存在しないことが問題ですので、すべての商品のレコードが存在する(売上実績がなければ売上合計金額は0になる)ようにするのが解決方法となります。

必ず存在するようにデータが登録されていれば、LEFT OUTER JOINではなくINNER JOINで結合できるようになりますが、テスト環境のようなデータが十分にメンテナンスされていない環境では正しく取得できない場合がある点に気をつける必要があります。

NVL関数を使用してNULL0とみなす

今回の問題文のテーブルおよびデータで運用するなら、NVL関数を使ってNULL0として計算対象に含める方法がよいでしょう。MySQLにはNVL関数がないため、IFNULL関数を使います。

SELECT
    AVG(IFNULL(T2.total_sales_amount, 0)) AS `売上平均金額`
FROM
    products T1
    LEFT OUTER JOIN sales_by_product T2
    ON
        T1.product_code = T2.product_code
WHERE
    T1.product_rank = 'A'
GROUP BY
    T1.product_rank
;

=> 100.0000

AVG関数に渡す前にNVL関数(IFNULL関数)でNULL0に変換することで売上合計金額÷ランク別商品数で平均を求めることができるようになります。

まとめ

NULLを含むAVG関数およびCOUNT関数のふるまいについて確認しました。

「平均」と一口に言ってもNULLを集計に含めるのかどうかによって計算結果が変わります。

  • 「平均」の計算方法についての正しい仕様を確認する
  • 結合先が必ず存在するのか存在しないことがあるのかを確認する
  • 集計にNULLが含まれることがあるのであれば、NULLを含む場合の集計が正しいことを確認する

といった基本的な点をしっかりと確認していくことが重要です。

[Oracle]SQL*Plusで実行計画を取得する(SET AUTOTRACE)

実行計画の確認にはいくつかを方法がありますが、ここでは最も手軽に実施できるSQL*Plusでの実行計画の取得の方法について解説します。

実行計画を取得するための設定を行う

私がよく使っている設定をご紹介します。とりあえずこの設定を使っていただければ実行計画を取得できるようになります。

SQL> set lines 1000
SQL> set pages 1000
SQL> set timing on
SQL> set autotrace traceonly

以上です。

この設定を行った状態でSQLを実行すると以下のようになります。

SQL> select * from dept
  2  /

レコードが選択されませんでした。

経過: 00:00:00.02

実行計画
----------------------------------------------------------
Plan hash value: 3383998547

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     1 |    30 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS FULL| DEPT |     1 |    30 |     2   (0)| 00:00:01 |
--------------------------------------------------------------------------


統計
----------------------------------------------------------
          0  recursive calls
          0  db block gets
          0  consistent gets
          0  physical reads
          0  redo size
        449  bytes sent via SQL*Net to client
        369  bytes received via SQL*Net from client
          1  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          0  rows processed

SQL>

実行計画と統計情報が出力されていることがわかります。

上記手順を実施しているときにエラーが出る場合があります。その場合は後述の「手順の実施時にエラーが出る場合」を参照してください。

これだけだとよくわからないと思いますので、各コマンドについて解説します。

表示幅を広げる

linesizeで結果行の幅を設定します。実行計画の出力内容は表を含むため、途中で折り返されるとかなり見にくいです。そこでとりあえず折り返されないような大きな値を設定しています。

正式にはlinesizeですが、linesでも設定できます。タイプ数が少ない方が楽なのでlinesを使用しています。

set lines 1000

1ページの行数を増やす

実行計画の出力の途中で改ページすると見づらいため、1ページの行数も増やしています。こちらもとりあえずで増やしていますが、普通に使う分には問題は起こらないと思いますが、あまりにも長いSQLの実行計画を確認するときに、改ページされるようであれば値を増やしてください。

先ほどと同様にpagesizeが正式な設定ですが、pagesでも設定できます。

set pages 1000

タイミング統計をONにする

タイミング統計をONにすることで、実行時間を10ミリ秒単位で出力できるようになります。

経過: 00:00:00.02

実行時間もチューニングの重要な情報源となりますし、出力結果が邪魔になることもないので、ONにしておきましょう。

set timing on

実行計画の設定を行う

実行計画の出力設定は、autotraceで行います。出力できる内容は、

  • 実行結果
  • 実行計画
  • 統計情報

の3つがありますが、実行結果を必要としない場合はtraceonlyを設定し、実行結果を確認したい場合はoffに切り替えるのがおすすめです。

設定説明
set autotrace on explain実行結果と実行計画を表示する
set autotrace on statistics実行結果と統計情報を表示し、実行計画をは表示しない
set autotrace on実行結果、実行計画、統計情報を表示する
set autotrace traceonly実行計画と統計情報を表示し、実行結果は表示しない
set autotrace offautotraceの設定をOFFにする
set autotrace traceonly

手順の実行時にエラーが出る場合

これから説明する手順を実行したときに、以下のようなエラーが出る場合があります。

SQL> set autotrace traceonly
SP2-0618: セッション識別子が見つかりません。PLUSTRACEロールが有効かを確認してください。
SP2-0611: STATISTICSレポートを使用可能にするときにエラーが発生しました。

このエラーが出る場合は、以下の手順を実行してPLUSTRACEロールを有効にしてください。

SYSDBAでCDBにログインする

ロールを追加するため、SYSDBACDBにログインします。

ORACLE_SID=ORCLCDB sqlplus / as sysdba

SQL*Plus: Release 19.0.0.0.0 - Production on Tue Mar 29 15:44:05 2022
Version 19.3.0.0.0

Copyright (c) 1982, 2019, Oracle.  All rights reserved.


Connected to:
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.3.0.0.0

SQL> 

PDBに切り替える

ロールを作成するPDBに切り替えます。ここではpdb1というPDBに切り替えています。

SQL> alter session set container=pdb1;

Session altered.

SQL> 

ロールを作成するPDBというのは、実行計画を取得するPDBのことです。

PLUSTRACEロールを作成するSQLファイルを実行する

OracleDatabaseに用意されているSQLファイルを実行します。SQLファイルのファイル名はplustrace.sqlではなくplustrce.sqlですので、打ち間違いに注意してください(aがない)。

SQL> @?/sqlplus/admin/plustrce.sql
SQL> 
SQL> drop role plustrace;
drop role plustrace
          *
ERROR at line 1:
ORA-01919: role 'PLUSTRACE' does not exist


SQL> create role plustrace;

Role created.

SQL> 
SQL> grant select on v_$sesstat to plustrace;

Grant succeeded.

SQL> grant select on v_$statname to plustrace;

Grant succeeded.

SQL> grant select on v_$mystat to plustrace;

Grant succeeded.

SQL> grant plustrace to dba with admin option;

Grant succeeded.

SQL> 
SQL> set echo off

PLUSTRACEロールをユーザーに付与する

作成したPLUSTRACEロールをユーザーに付与します。ここでは、user01ユーザーに設定しています。

SQL> grant plustrace to user01;

Grant succeeded.

SQL>
SQL> exit
Disconnected from Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.3.0.0.0

再度、該当ユーザーでログインしてautotraceの設定を行ってください。

外部キー制約でのON DELETE CASCADEオプションの使い方

外部キー制約でのON DELETE CASCADEオプションの使い方を具体的な例を使って解説します。

ON DELETE CASCADEオプションは親テーブル(参照先のテーブル)を削除するときに子テーブル(参照元のテーブル)も一緒に削除するために使用するオプションになります。

解説に使用するデータベースとテーブルについて

解説に使用するデータベースとテーブルについて説明します。

確認にはOracle Database 19cを使用しています。若干の構文の違いはありますが、他の主要なデータベースでも同様の操作が可能です。

まずは親テーブルを作成します。親テーブルのMAINテーブルは主キーIDのみを持ったテーブルです。

CREATE TABLE MAIN
(
  ID NUMBER(3),
  CONSTRAINT MAIN_PK PRIMARY KEY (ID)
)

次に子テーブルを作成します。子テーブルのSUBテーブルはIDとSEQを主キーに持つテーブルです。

CREATE TABLE SUB
(
  ID  NUMBER(3),
  SEQ NUMBER(2),
  CONSTRAINT SUB_PK PRIMARY KEY (ID, SEQ)
)

ON DELETE CASCADE オプションなしの場合

では、ON DELETE CASCADEオプションを使用しない場合の動作を見てみましょう。

SUBテーブルのIDに外部キー制約を設定します。

SQL> ALTER TABLE SUB ADD CONSTRAINT SUB_ID_FK
   2 FOREIGN KEY (ID) REFERENCES MAIN(ID)

表が変更されました。

では、データを投入して削除時の動作を確認します。

SQL> INSERT INTO MAIN VALUES (1)

1行が作成されました。
SQL> INSERT INTO SUB VALUES (1, 1)

1行が作成されました。
SQL> INSERT INTO SUB VALUES (1, 2)

1行が作成されました。

削除する前にデータを確認しておきましょう。

SQL> SELECT * FROM MAIN

ID
--
 1

1行が選択されました。
SQL> SELECT * FROM SUB

ID SEQ
-- ---
 1   1
 1   2

2行が選択されました。

MAINテーブルのID=1のレコードにSUBテーブルのID=1のレコードが2件ぶら下がっている形になっています。

データが投入できましたので、親テーブルMAINテーブルのレコードを削除してみます。

SQL> DELETE FROM MAIN WHERE ID = 1
DELETE FROM MAIN WHERE ID = 1
*
行1でエラーが発生しました。:
ORA-02292: 整合性制約(USER01.SUB_ID_FK)に違反しました - 子レコードがあります

整合性制約(外部キー制約)で削除に失敗しました。ON DELETE CASCADEオプションがない場合は、子テーブルを削除してからでないと親テーブルを削除できません。

念のためデータが削除されていないことを確認しておきます。

SQL> SELECT * FROM MAIN

ID
--
 1

1行が選択されました。
SQL> SELECT * FROM SUB

ID SEQ
-- ---
 1   1
 1   2

2行が選択されました。

ON DELETE CASCADEオプションありの場合

では、ON DELETE CASCADEオプションをつけた場合の削除の動きを確認します。

すでに設定済みの外部キーを削除しておきます。

SQL> ALTER TABLE SUB DROP CONSTRAINT  SUB_ID_FK

表が変更されました。

今度はON DELETE CASCADEオプション付きで外部キー制約を設定します。

SQL> ALTER TABLE SUB ADD CONSTRAINT SUB_ID_FK
   2 FOREIGN KEY (ID) REFERENCES MAIN(ID)
   3 ON DELETE CASCADE

表が変更されました。

では、もう一度削除を試します。

> DELETE FROM MAIN WHERE ID = 1

1行が削除されました。

今度は削除できました。

削除後のデータを確認します。

SQL> SELECT * FROM MAIN

レコードが選択されませんでした。
SQL> SELECT * FROM SUB

レコードが選択されませんでした。

削除したMAINテーブルのレコードだけでなく、SUBテーブルのID=1のレコードも削除されています。

どういった場合にON DELETE CASCADEオプションを使うか

まず、外部キー制約を使用する場合に限られているということが前提となります。

上記の前提の上で、以下の2つのどちらの効果を狙いたいかで決めるとよいでしょう。

  • 子テーブルにデータがあるときに親テーブルが削除されるのを防ぎたい
  • 親テーブルが削除されるときに子テーブルをまとめて削除したい

もう少し具体的な例で説明します。

ON DELETE CASCADEオプションを使いたくないケース

ON DELETE CASCADEオプションを使いたくないケース、使うべきでないケースとして挙げられるのが、マスタテーブルに対して外部キー制約を設定しているケースです。

例えば、注文テーブルにある顧客番号に対して、顧客マスタの顧客番号への外部キーを設定している場合、ON DELETE CASCADEオプションを設定していると、注文テーブルのレコードの削除によって顧客マスタのレコードが削除される場合があります。
これは望ましい挙動でないことから、マスタテーブルへの外部キー設定の場合はON DELETE CASCADEオプションを使用するべきではありません。

ON DELETE CASCADE オプションを使いたいケース

ON DELETE CASCADEオプションを使いたいケースとして挙げられるのが、子テーブルが親テーブルに対して外部キー制約を設定しているケースです。

例えば、注文テーブルと注文明細テーブルがあり、注文明細の注文番号に対して、注文テーブルの注文番号への外部キーを設定している場合は、ON DELETE CASCADEオプションを設定していると、注文テーブルのレコード削除時に注文明細テーブルのレコードをまとめて削除してくれるため便利です。
ON DELETE CASCADEオプションを設定していない場合は、先に注文明細テーブルの該当する注文番号のレコードを先に削除してからでないと注文テーブルのレコードを削除できないため、ON DELETE CASCADEオプションを使うことが削除を行うプログラムの構造をすっきりとさせることができる効果があります。

まとめ

前述の例をまとめると以下のようになります。

CREATE TABLE 注文
(
  注文番号 NUMBER(10),
  顧客番号 NUMBER(3),
  CONSTRAINT 注文_PK PRIMARY KEY (ID)
)

CREATE TABLE 注文明細
(
  注文番号 NUMBER(10),
  注文明細連番 NUMBER(10),
  CONSTRAINT 注文明細_PK PRIMARY KEY (注文番号, 注文明細番号)
)

CREATE TABLE 顧客マスタ
(
  顧客番号 NUMBER(3),
  CONSTRAINT 顧客マスタ_PK PRIMARY KEY (顧客番号)

-- 注文から顧客マスタへの外部キーではON DELETE CASCADEオプションをつけない
ALTER TABLE SUB ADD CONSTRAINT 注文_顧客番号_FK
FOREIGN KEY (顧客番号) REFERENCES 顧客マスタ(顧客番号)

-- 注文明細から注文への外部キーではON DELETE CASCADEオプションをつける
ALTER TABLE SUB ADD CONSTRAINT 注文明細_注文番号_FK
FOREIGN KEY (注文番号) REFERENCES 注文(注文番号)
ON DELETE CASCADE

このようにON DELETE CASCADEオプションを適切に設定する/しないことでデータを安全に、効率よく管理できるようになります。

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