SQLの基本的なSELECT
やINSERT
は使えるようになったけれど、「集計結果に条件をつけたい」「親データを削除したら、関連する子データも自動で更新したい」といった、一歩進んだ操作で手が止まってしまった経験はありませんか?
データベーススペシャリスト試験や応用情報技術者試験の午後問題では、まさにこうした応用的な知識が問われます。WHERE
とHAVING
の使い分け、ON DELETE CASCADE
のような参照動作、NULLを巧みに扱うCOALESCE
関数、そしてTRIGGER
やCURSOR
を使った自動処理――。これらの構文は、一見すると複雑に見えるかもしれませんが、「データの整合性を保ち、処理を自動化する」という明確な目的を持った、非常に強力なツールです。
この記事では、IT系資格試験の頻出テーマであり、実務でも必須となるSQLの応用構文を、豊富な具体例とともに徹底的に解説します。それぞれの構文が「なぜ必要なのか」「どのような場面で役立つのか」を理解し、自信を持ってSQLを使いこなせるようになりましょう。
目次
1. 外部キー制約と参照整合性の基本│SQLのデータ整合性を守る仕組み
データベースを扱う上で避けては通れないのが、「データの不整合」という問題です。例えば、「存在しないはずの顧客からの注文データ」や「退職した社員が担当者のままになっている案件データ」などが生まれてしまうと、データ全体の信頼性が揺らいでしまいます。
こうした事態を防ぎ、テーブル間の関連性を正しく保つために不可欠なのが外部キー(FOREIGN KEY)制約と参照整合性の概念です。
FOREIGN KEYとは?親子関係を保ち、データ不整合を防ぐ仕組み
外部キー(FOREIGN KEY)とは、あるテーブルの列の値を、別のテーブルの主キー(PRIMARY KEY)の値に限定することで、2つのテーブル間に関連付けを持たせるための制約です。
これによって、テーブル間に「親」と「子」の関係が生まれます。
- 親テーブル: 参照される側(例:顧客マスタテーブル)
- 子テーブル: 参照する側(例:注文履歴テーブル)
子テーブルの外部キー列には、親テーブルの主キーに存在する値しか登録できなくなります。これにより、「存在しない顧客ID」を持つ注文データが登録されるのを防ぎ、データの整合性を強制的に保つことができます。これを参照整合性と呼びます。
基本構文
以下は、「customers」テーブル(親)と「orders」テーブル(子)を定義するSQLの例です。「orders」テーブルのcustomer_id
が外部キーとして設定されています。
/* 親テーブル:顧客情報 */
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
customer_name VARCHAR(255) NOT NULL
);
/* 子テーブル:注文情報 */
CREATE TABLE orders (
order_id INT PRIMARY KEY,
order_date DATE,
customer_id INT, -- この列が外部キーになる
FOREIGN KEY (customer_id)
REFERENCES customers(customer_id) -- customersテーブルのcustomer_idを参照する
);
ON DELETE / ON UPDATE の参照動作一覧
では、親テーブルのデータが更新(UPDATE)または削除(DELETE)された場合、すでに関連付けられている子テーブルのデータはどうなるのでしょうか? 何も設定しないと、親データが存在しないのに子データが残ってしまう「孤児レコード」が生まれる可能性があります。
この親の変更に子がどう振る舞うかを定義するのがON DELETE
とON UPDATE
句です。データベーススペシャリスト試験の午後問題などでも頻出の重要な設定項目です。
参照動作 | 意味 | 典型的な利用例 |
---|---|---|
RESTRICT | 親の削除・更新を拒否する。 | 子データが存在する限り親は消せない。データ整合性を最も厳密に保ちたい場合。 |
CASCADE | 親の削除・更新に追随し、子データも削除・更新する。 | 顧客が退会したら注文履歴も全削除する場合など、親と子のライフサイクルが完全に一致する場合。 |
SET NULL | 親が削除・更新されたら、子の外部キー列をNULLにする。 | 注文履歴は残したいが、顧客情報が削除された場合に「担当者なし」の状態にしたい場合など、論理的に関連を切り離すとき。 |
SET DEFAULT | 親が削除・更新されたら、子の外部キー列をデフォルト値に変更する。 | 退職した担当者の代わりに「未割り当て(ID:0)」のようなデフォルトの担当者を設定する場合。 |
NO ACTION | RESTRICT とほぼ同義。制約チェックのタイミングが異なる場合があるが、多くのDBで同じ動作をする。 |
RESTRICT と同様。 |
参照動作を指定した構文
実際のテーブル定義では、以下のようにON DELETE
とON UPDATE
を指定します。
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
FOREIGN KEY (customer_id)
REFERENCES customers(customer_id)
ON DELETE SET NULL -- 親(customers)の行が削除されたら、子のcustomer_idをNULLにする
ON UPDATE CASCADE -- 親(customers)の主キーが更新されたら、子のcustomer_idも追随して更新する
);
この設定により、顧客が削除されても注文履歴は残り(顧客IDはNULLになる)、万が一顧客IDが変更された場合でも注文履歴との関連性は保たれ続ける、という柔軟なデータ管理が実現できます。
2. HAVING句の使い方と実用例│集計結果からさらに絞り込むSQLテクニック
GROUP BY
を使って部署ごとの平均給与や顧客ごとの注文件数を集計したものの、「その集計結果の中から、さらに条件に合うものだけを絞り込みたい」という場面は頻繁に発生します。例えば、「平均給与が50万円以上の部署だけ」や「注文件数が3回以上の優良顧客だけ」を抽出するケースです。
このような集計後のデータに対する条件指定を行うのがHAVING句の役割です。
HAVING句とは? WHERE句との決定的な違い
HAVING
句は、GROUP BY
句でグループ化され、集計関数(COUNT
, SUM
, AVG
など)によって計算された結果に対して条件を指定するための句です。
ここで必ずといっていいほど登場するのが、「WHERE
句と何が違うのか?」という疑問です。両者の違いを理解することが、HAVING
句をマスターする鍵となります。
- WHERE句: 行ごとの条件を指定。
GROUP BY
で集計される前の、テーブルの元データに対して機能します。 - HAVING句: グループごと(集計後)の条件を指定。
GROUP BY
で集計された後の結果セットに対して機能します。
簡単に言えば、「絞り込んでから集計する」のがWHERE
、「集計してから絞り込む」のがHAVING
です。
基本構文
HAVING
句は必ずGROUP BY
句の後に記述します。
SELECT
customer_id,
COUNT(*) AS order_count -- 顧客ごとの注文件数を計算
FROM
orders
GROUP BY
customer_id -- 顧客IDでグループ化
HAVING
COUNT(*) >= 3; -- 集計結果(注文件数)が3以上のものだけを抽出
このSQLは、「3回以上注文した顧客のIDと、その注文件数」を抽出します。WHERE
句ではCOUNT(*)
のような集計関数を使った条件指定はできません。
よく出るHAVING句の使用例【応用情報・DBスペシャリスト頻出】
試験でも実務でも頻出する、HAVING
句の典型的な使い方を4つのパターンで見ていきましょう。
① 平均・合計などの集計条件指定
最も基本的な使い方です。部署ごとの平均給与を算出し、その平均給与が特定の額を超える部署だけを抽出します。
/* 平均給与が50万円を超える部署の名前と平均給与を抽出 */
SELECT
department,
AVG(salary) AS avg_salary
FROM
employees
GROUP BY
department
HAVING
AVG(salary) > 500000;
② 複数条件を AND や OR で組み合わせる
HAVING
句でもWHERE
句と同様にAND
やOR
を使って複数の条件を組み合わせることができます。
/* 社員数が10名以上 "かつ" 平均給与が40万円以上の部署を抽出 */
SELECT
department,
COUNT(*) AS emp_count,
AVG(salary) AS avg_salary
FROM
employees
GROUP BY
department
HAVING
COUNT(*) > 10 AND AVG(salary) >= 400000;
③ WHERE句とHAVING句を組み合わせる
WHERE
句とHAVING
句は、1つのSQLクエリの中で同時に使うことができます。先にWHERE
句で元データを絞り込み、その後にGROUP BY
で集計し、最後にHAVING
句で集計結果を絞り込む、という流れになります。
/* 2025年1月1日以降の注文に限定し、
その中で合計注文金額が10万円を超える顧客を抽出 */
SELECT
customer_id,
SUM(amount) AS total_amount
FROM
orders
WHERE
order_date >= '2025-01-01' -- まず行を絞り込む
GROUP BY
customer_id -- 集計する
HAVING
SUM(amount) > 100000; -- 集計結果を絞り込む
④ HAVINGでNULLを含むグループを除外する
COUNT
関数はNULL
を数えない特性があります。これを利用して、価格が未登録(NULL
)の商品しか持たないカテゴリなどを集計結果から除外できます。
/* 価格が1件以上登録されている商品カテゴリとその商品数を抽出 */
SELECT
category,
COUNT(price)
FROM
products
GROUP BY
category
HAVING
COUNT(price) > 0; -- priceがNULLの行はカウントされないため、結果が0のカテゴリは除外される
このようにHAVING
句を使いこなすことで、単なる集計に留まらない、より高度なデータ分析が可能になります。
3. COALESCE関数でNULLを扱いやすくする│SQLのNULL値を安全に変換
SQLを扱う上で、多くの初学者がつまずくのがNULL
の存在です。NULL
は0(ゼロ)でもなければ、空文字('')でもありません。「値が存在しない」という特殊な状態を示します。そのため、NULL
を含む列を計算に使おうとすると、結果全体がNULL
になってしまうなど、予期せぬ動作を引き起こすことがあります。
この厄介なNULL
を、より安全で扱いやすい値に変換してくれるのがCOALESCE(コアレス)関数です。
COALESCEの基本構文と動作原理
COALESCE
関数は、引数として渡された値を左から順番に評価し、最初に見つかったNULL
でない値を返します。すべての引数がNULL
の場合はNULL
を返します。
COALESCE
は「合体する、合同する」といった意味の英単語で、複数の候補から有効な値へ「合体」させるイメージを持つと分かりやすいでしょう。
基本構文
COALESCE(値1, 値2, 値3, ...)
顧客テーブルから名前と電話番号を取得する際に、電話番号が未登録(NULL
)の場合に「未登録」と表示させる例を見てみましょう。
SELECT
name,
COALESCE(phone, '未登録') AS phone_display
FROM
customers;
このSQLを実行すると、phone
列に電話番号が入っている場合はその番号が、NULL
の場合は'未登録'
という文字列がphone_display
列に表示されます。これにより、アプリケーション側でNULL
の判定処理をせずとも、画面に分かりやすい表示を出すことができます。
実用パターン(よく出る形)【試験・実務での頻出例】
COALESCE
は単純なNULL
の置き換えだけでなく、より実践的な場面で真価を発揮します。
① メイン連絡先の代替表示
複数の連絡先候補(例:携帯電話、自宅電話)があり、優先順位をつけて表示したい場合に非常に便利です。
/* 携帯番号を優先し、なければ自宅番号、それもなければ'連絡先不明'と表示 */
SELECT
user_name,
COALESCE(mobile_phone, home_phone, '連絡先不明') AS primary_contact
FROM
users;
この例では、まずmobile_phone
を評価し、NULL
なら次にhome_phone
を評価、それもNULL
なら'連絡先不明'
を返します。
② 集計時のNULL回避(SUM/AVGの前処理など)
NULL
を含む列を集計関数にかけると、意図しない結果になることがあります。例えば、売上 + ポイント
のような計算でポイント
がNULL
の場合、結果はNULL
になってしまいます。COALESCE
を使ってNULL
を0に変換しておくことで、これを防げます。
/* ポイントがNULLの場合は0として扱い、売上金額の合計を計算 */
SELECT
product_id,
SUM(price + COALESCE(bonus_point, 0)) AS total_value
FROM
sales
GROUP BY
product_id;
このようにCOALESCE
で前処理をしておくことで、NULL
が原因の計算エラーを防ぎ、堅牢なSQLクエリを作成できます。これはデータベーススペシャリスト試験の午後問題でも問われる重要なテクニックです。
4. トリガ(TRIGGER)で自動処理を実現する│SQLイベント連動の自動化手法
データベースの運用では、「特定の操作が行われたら、関連する別の処理も自動的に実行したい」という要求がよくあります。例えば、「注文テーブルに新しいレコードが追加されたら、自動で監査ログテーブルにも記録を残す」「在庫テーブルの数量が更新されたら、連動して商品ステータスを変更する」といったケースです。
このような、テーブルに対する特定のイベント(INSERT
, UPDATE
, DELETE
)をきっかけ(トリガー)として、あらかじめ定義しておいた一連の処理を自動実行する仕組みがトリガ(TRIGGER)です。
トリガの役割と基本構文
トリガは、アプリケーション側のロジックとは独立して、データベース自身がデータの整合性を保つために動作します。これにより、アプリケーションの改修なしに、データベースレベルで一貫したルールを適用できる点が大きなメリットです。
基本構文
以下は、orders
テーブルに新しい注文データがINSERT
された後に、その操作ログをlogs
テーブルに記録する、というシンプルなトリガの作成例です。
/* ordersテーブルへのINSERT後に実行されるトリガを作成 */
CREATE TRIGGER trg_after_insert_orders
AFTER INSERT ON orders -- ordersテーブルへのINSERT後
FOR EACH ROW -- 挿入された行ごとに処理を実行
BEGIN
-- 実行したい処理を記述
INSERT INTO action_logs (action, created_at)
VALUES ('新規注文が登録されました', CURRENT_TIMESTAMP);
END;
この構文のポイントは、いつ(AFTER INSERT
)、どのテーブルで(ON orders
)、何をきっかけに(行ごとFOR EACH ROW
)、何をするか(BEGIN
...END
内の処理)を定義する点です。
BEFORE
とAFTER
の違い【重要】
トリガを定義する上で最も重要なのが、処理を実行するタイミングを指定するBEFORE
とAFTER
の使い分けです。
タイミング | 処理内容 | 主な使用目的 |
---|---|---|
BEFORE | INSERT やUPDATE が実行される直前に動作する。 |
値の検証や整形処理。 例えば、入力されたデータが特定のルール(例:単価は必ず正の数)を満たしているかチェックしたり、小文字で入力された文字列を自動で大文字に変換したりする。 |
AFTER | INSERT やUPDATE , DELETE が実行された直後に動作する。 |
副作用を伴う処理。 データの変更が確定した後に、関連テーブルの更新、監査ログの記録、通知の送信などを行う。 |
BEFORE
トリガは、これから挿入・更新されるデータを操作できます(NEW
という仮のレコード名でアクセス可能)。一方、AFTER
トリガは、変更が完了した後のデータを参照できるため、ログ記録や関連データの更新に適しています。
例えば、「会員ステータスが "退会" に更新されたら、最終ログイン日時を記録する」といった処理は、更新が確定したAFTER
でないと意味がありません。逆に、「メールアドレスの形式が不正なら登録させない」といったチェックは、データが書き込まれるBEFORE
で行う必要があります。
このように、トリガの実行タイミングを適切に選択することが、データベースの自動化と整合性維持の鍵となります。
5. カーソル処理とFETCHの流れ│SQLの結果セットを1行ずつ操作する
通常のSELECT
文は、条件に一致する全ての行を一度に「結果セット」として返します。これはデータ分析や一覧表示には便利ですが、プログラムの中から「取得した結果を1行ずつ順番に取り出して、それぞれに異なる複雑な処理を行いたい」という場合には不向きです。
この課題を解決するのがカーソル(CURSOR)です。カーソルは、SQLの結果セットをファイルやリストのように扱い、先頭から1行ずつデータを読み込んで処理を進めることを可能にする仕組みです。主に、アプリケーションプログラムに組み込む埋め込みSQLや、ストアドプロシージャ内で利用されます。
カーソルとは? 検索結果を1行ずつ処理する4ステップ
カーソルを理解する鍵は、その一連の操作フローを覚えることです。カーソル処理は、常に以下の4つのステップで構成されます。
- 宣言 (DECLARE): どのようなデータを取得するかを
SELECT
文で定義し、カーソルに名前を付けます。 - オープン (OPEN):
SELECT
文を実行して結果セットを確定させ、カーソルが最初の行の直前を指すように準備します。 - フェッチ (FETCH): カーソルが指す現在の行からデータを取り出し、プログラムの変数などに格納します。そして、カーソルを次の行へ進めます。データがなくなるまで、この操作をループで繰り返します。
- クローズ (CLOSE): すべての処理が終わったら、カーソルが使用していたメモリ領域を解放します。
この「宣言→オープン→フェッチ→クローズ」という流れは、データベース製品によらず共通の考え方であり、情報処理技術者試験でも頻出のパターンです。
カーソル処理の基本構文とループ
実際のプログラムでは、FETCH
処理をループさせて、結果セットの全行を処理するのが一般的です。以下は、顧客テーブルの全顧客IDと名前を取得し、1件ずつログテーブルに挿入していく処理の例です。
-- ① カーソルの宣言:customersテーブルからidとnameを取得する「cur」を定義
DECLARE cur CURSOR FOR
SELECT customer_id, name FROM customers;
-- ② カーソルのオープン:SELECT文が実行され、結果セットが準備される
OPEN cur;
-- ③ 最初の行をフェッチ:1行目のデータを変数 :id と :name に格納
FETCH NEXT FROM cur INTO :id, :name;
-- SQLCODEが0(成功)である間、ループ処理を続ける
WHILE SQLCODE = 0 DO
-- 取り出したデータを使って、ログテーブルにINSERT
EXEC SQL INSERT INTO customer_logs VALUES(:id, :name);
-- 次の行をフェッチ
FETCH NEXT FROM cur INTO :id, :name;
END WHILE;
-- ④ カーソルのクローズ:メモリを解放
CLOSE cur;
この例のWHILE SQLCODE = 0
は、「直前のSQL(この場合はFETCH
)が成功している間」という意味の条件分岐です。FETCH
が結果セットの最終行を超えてデータを取得しようとすると失敗し、SQLCODE
が0以外の値(例: 100)になるため、ループが終了する仕組みです。
カーソルは強力な機能ですが、一行ずつの処理はオーバーヘッドが大きくなる傾向があるため、大量のデータに対してはパフォーマンスに注意が必要です。
6. EXEC SQL INTO の使い方│プログラム変数にSQL結果を格納する
これまで見てきたSQL構文は、主にデータベース管理ツール上で直接実行するものでした。しかし、実際の業務システムでは、Java, C言語, COBOLといったホスト言語(プログラム言語)の中からSQL文を実行し、その結果をプログラム側の変数で受け取って処理する、という場面が非常に多くあります。
このように、アプリケーションプログラムのコード内にSQL文を埋め込む手法を埋め込みSQLと呼びます。そして、その埋め込みSQLにおいて、SELECT
文の実行結果をホスト言語の変数に格納するために使われるのがEXEC SQL ... INTO ...
構文です。
EXEC INTO 構文と用途
EXEC SQL INTO
は、SELECT
文によって取得した単一行の結果を、プログラム内で宣言された変数(ホスト変数)に直接セットするための命令です。
構文のポイントは、SELECT
とINTO
の間に取得したいカラム名を、INTO
の後に格納先の変数名を、それぞれ対応するように記述する点です。
基本構文
以下は、IDが1
のユーザー情報をusers
テーブルから取得し、そのname
とage
をプログラム側の変数:v_name
と:v_age
に格納する例です。
EXEC SQL
SELECT name, age
INTO :v_name, :v_age
FROM users
WHERE id = 1;
:
(コロン)は、その変数がプログラム側で用意されたホスト変数であることを示すための接頭辞です。このSQLが実行されると、データベースから取得されたname
の値が:v_name
に、age
の値が:v_age
にそれぞれ代入されます。
プログラム側は、この変数に格納された値を使って、画面表示を行ったり、さらに別の計算処理を行ったりします。
カーソルとの違い
ここで、「カーソルでも1行ずつデータを取り出せるのでは?」という疑問が浮かぶかもしれません。EXEC SQL INTO
とカーソルのFETCH
は似ていますが、明確な違いがあります。
EXEC SQL INTO
: 戻り値が必ず1行である場合にのみ使用できます。SELECT
の結果が0行だったり、逆に2行以上だったりするとエラーになります。主キーで検索するなど、結果が一意に定まる場合に最適です。- カーソル (
FETCH ... INTO ...
): 戻り値が複数行になる可能性がある場合に使用します。OPEN
で結果セット全体を準備し、FETCH
で1行ずつ順番に処理していきます。
つまり、「特定の1件」のデータをピンポイントで取得したいならEXEC SQL INTO
を、「条件に合うデータ全て」を順番に処理したいならカーソルを使う、という使い分けになります。
7. まとめと実務での使い分け│応用SQL構文の使いどころ
本記事では、データベーススペシャリスト試験や応用情報技術者試験の午後問題で頻出する、一歩進んだSQL構文を解説しました。最後に、それぞれの技術がどのような目的で使われるのか、その核心的な役割を再確認しましょう。
- HAVING句は「集計後の条件指定」
WHERE
句が行データを絞り込むのに対し、HAVING
句はGROUP BY
で集計された結果セットを絞り込みます。「平均給与がX円以上の部署」のように、集計値に基づいた条件分岐で真価を発揮します。 - 参照動作 (RESTRICT/CASCADE) は「削除・更新時の整合性確保」
親テーブルの変更が子テーブルにどう影響するかを定義します。データを厳格に保護したいならRESTRICT
、親子のライフサイクルを一致させたいならCASCADE
、関連だけを切り離したいならSET NULL
と、目的に応じて使い分けます。 - COALESCE関数は「NULLの安全な処理」
NULL
のままでは計算や表示が困難な場面で、代替値を設定する最もシンプルで強力な方法です。複数の候補から優先順位をつけて有効な値を取り出す、といった柔軟な使い方も可能です。 - TRIGGER / CURSOR / EXEC INTO は「自動処理とプログラム連携」
- TRIGGER: データベース内のイベントをきっかけに、ログ記録などの処理を自動化します。
- CURSOR: 複数行の結果セットを1行ずつプログラムで処理したい場合に使います。
- EXEC SQL INTO: 単一行の結果を直接プログラム変数に格納したい場合に利用します。
これらの構文は、単なるデータ取得に留まらない、より高度なデータ操作、整合性の維持、そして処理の自動化を実現するためのものです。一つひとつの役割と構文を正しく理解しておくことが、複雑な要件に応えるデータベース設計・開発の鍵となり、資格試験の難問を突破する力にもなります。