IPA情報処理試験キーワード

IPA|情報処理技術者試験

【SQL中級者への道】HAVING・トリガ・カーソルとは?頻出応用構文を具体例でマスター

SQLの基本的なSELECTINSERTは使えるようになったけれど、「集計結果に条件をつけたい」「親データを削除したら、関連する子データも自動で更新したい」といった、一歩進んだ操作で手が止まってしまった経験はありませんか?

データベーススペシャリスト試験や応用情報技術者試験の午後問題では、まさにこうした応用的な知識が問われます。WHEREHAVINGの使い分け、ON DELETE CASCADEのような参照動作、NULLを巧みに扱うCOALESCE関数、そしてTRIGGERCURSORを使った自動処理――。これらの構文は、一見すると複雑に見えるかもしれませんが、「データの整合性を保ち、処理を自動化する」という明確な目的を持った、非常に強力なツールです。

この記事では、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 DELETEON UPDATE句です。データベーススペシャリスト試験の午後問題などでも頻出の重要な設定項目です。

参照動作 意味 典型的な利用例
RESTRICT 親の削除・更新を拒否する。 子データが存在する限り親は消せない。データ整合性を最も厳密に保ちたい場合。
CASCADE 親の削除・更新に追随し、子データも削除・更新する。 顧客が退会したら注文履歴も全削除する場合など、親と子のライフサイクルが完全に一致する場合。
SET NULL 親が削除・更新されたら、子の外部キー列をNULLにする。 注文履歴は残したいが、顧客情報が削除された場合に「担当者なし」の状態にしたい場合など、論理的に関連を切り離すとき。
SET DEFAULT 親が削除・更新されたら、子の外部キー列をデフォルト値に変更する。 退職した担当者の代わりに「未割り当て(ID:0)」のようなデフォルトの担当者を設定する場合。
NO ACTION RESTRICTとほぼ同義。制約チェックのタイミングが異なる場合があるが、多くのDBで同じ動作をする。 RESTRICTと同様。

参照動作を指定した構文

実際のテーブル定義では、以下のようにON DELETEON 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句と同様にANDORを使って複数の条件を組み合わせることができます。

/* 社員数が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内の処理)を定義する点です。

BEFOREAFTERの違い【重要】

トリガを定義する上で最も重要なのが、処理を実行するタイミングを指定するBEFOREAFTERの使い分けです。

タイミング 処理内容 主な使用目的
BEFORE INSERTUPDATE実行される直前に動作する。 値の検証や整形処理。
例えば、入力されたデータが特定のルール(例:単価は必ず正の数)を満たしているかチェックしたり、小文字で入力された文字列を自動で大文字に変換したりする。
AFTER INSERTUPDATE, DELETE実行された直後に動作する。 副作用を伴う処理。
データの変更が確定した後に、関連テーブルの更新、監査ログの記録、通知の送信などを行う。

BEFOREトリガは、これから挿入・更新されるデータを操作できます(NEWという仮のレコード名でアクセス可能)。一方、AFTERトリガは、変更が完了した後のデータを参照できるため、ログ記録や関連データの更新に適しています。

例えば、「会員ステータスが "退会" に更新されたら、最終ログイン日時を記録する」といった処理は、更新が確定したAFTERでないと意味がありません。逆に、「メールアドレスの形式が不正なら登録させない」といったチェックは、データが書き込まれるBEFOREで行う必要があります。

このように、トリガの実行タイミングを適切に選択することが、データベースの自動化と整合性維持の鍵となります。

5. カーソル処理とFETCHの流れ│SQLの結果セットを1行ずつ操作する

通常のSELECT文は、条件に一致する全ての行を一度に「結果セット」として返します。これはデータ分析や一覧表示には便利ですが、プログラムの中から「取得した結果を1行ずつ順番に取り出して、それぞれに異なる複雑な処理を行いたい」という場合には不向きです。

この課題を解決するのがカーソル(CURSOR)です。カーソルは、SQLの結果セットをファイルやリストのように扱い、先頭から1行ずつデータを読み込んで処理を進めることを可能にする仕組みです。主に、アプリケーションプログラムに組み込む埋め込みSQLや、ストアドプロシージャ内で利用されます。

カーソルとは? 検索結果を1行ずつ処理する4ステップ

カーソルを理解する鍵は、その一連の操作フローを覚えることです。カーソル処理は、常に以下の4つのステップで構成されます。

  1. 宣言 (DECLARE): どのようなデータを取得するかをSELECT文で定義し、カーソルに名前を付けます。
  2. オープン (OPEN): SELECT文を実行して結果セットを確定させ、カーソルが最初の行の直前を指すように準備します。
  3. フェッチ (FETCH): カーソルが指す現在の行からデータを取り出し、プログラムの変数などに格納します。そして、カーソルを次の行へ進めます。データがなくなるまで、この操作をループで繰り返します。
  4. クローズ (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文によって取得した単一行の結果を、プログラム内で宣言された変数(ホスト変数)に直接セットするための命令です。

構文のポイントは、SELECTINTOの間に取得したいカラム名を、INTOの後に格納先の変数名を、それぞれ対応するように記述する点です。

基本構文

以下は、IDが1のユーザー情報をusersテーブルから取得し、そのnameageをプログラム側の変数: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: 単一行の結果を直接プログラム変数に格納したい場合に利用します。

これらの構文は、単なるデータ取得に留まらない、より高度なデータ操作、整合性の維持、そして処理の自動化を実現するためのものです。一つひとつの役割と構文を正しく理解しておくことが、複雑な要件に応えるデータベース設計・開発の鍵となり、資格試験の難問を突破する力にもなります。

-IPA|情報処理技術者試験
-