データベーススペシャリスト試験や応用情報技術者試験の午後問題で、多くの受験者を悩ませるSQLの「カーソル(CURSOR)」。特に「DECLARE CURSOR」から始まる一連の処理は、検索結果の集合から1行ずつデータを取り出して複雑な処理を行うための仕組みですが、その手続き的な流れがイメージしにくいと感じる方も多いのではないでしょうか。また、「トリガ(TRIGGER)とカーソルはどう連携するのか?」といった、より実践的な疑問もよく聞かれます。
この記事では、SQLカーソルの基本的な概念から、「DECLARE(宣言)」「OPEN(オープン)」「FETCH(フェッチ)」「CLOSE(クローズ)」という一連の処理の流れまでを、図解や身近な例えを交えて徹底的に解説します。なぜカーソルが必要なのか、そして実際の業務でどのように役立つのかを論理的に理解すれば、複雑に見えるデータベース操作もスッキリと頭に入ってくるはずです。この記事を読み終える頃には、カーソルの使い方をマスターし、試験の難問を解き明かすための確かな武器を手にしているでしょう。
目次
SQLカーソルとは?DECLAREで始まる「1行ずつ処理」の役割を解説
データベースを操作するSQLは、本来、条件に一致する複数行のデータを「集合(セット)」として一度に取得・処理するのが得意です。例えば、SELECT * FROM products WHERE stock < 10; という命令を実行すると、在庫が10未満の商品データが一度にすべて返ってきます。
しかし、実務や試験問題では、「取得したデータ1行ごとに、複雑な条件分岐をしながら別のテーブルを更新したい」といった、より手続き的な処理が求められる場面があります。このような集合処理が苦手とする「1行ずつ」の処理を実現するために使われるのがSQLカーソルです。
カーソルの身近な例え:料理本と指さし確認
カーソルの動きを理解するために、料理本を思い浮かべてみましょう。
- 通常のSQL:料理本で「カレーのレシピ」のページ全体を開くことに似ています。レシピ全体(データの集合)を一度に眺めることはできますが、手順を一つずつ実行するには不便です。
- カーソル:レシピの1行目「①玉ねぎをみじん切りにする」を指さし、その作業が終わったら、指を2行目「②人参を乱切りにする」に動かすようなものです。この「指」の役割こそがカーソルです。検索結果の集合の中から「今、どの行を処理しているか」という現在地を示し、1行ずつ順に処理を進めることを可能にします。
なぜカーソルが必要なのか?【仕事での活用例】
例えば、「ECサイトで、今月の売上上位10名の顧客に対して、過去の購入履歴に応じた個別のクーポンコードを発行し、ログテーブルに記録する」という処理を考えます。
この処理は、単純なUPDATE文一発では実現できません。顧客ID(customer_id)を1件ずつ取り出し、その顧客の購入履歴を分析し、クーポンを生成し、ログに書き込む、という一連の流れを10回繰り返す必要があります。このようなバッチ処理やストアドプロシージャ内での複雑な逐次処理に、カーソルは絶大な効果を発揮します。
通常のSQLとカーソル処理の違い
| 比較項目 | 通常のSQL(集合処理) | カーソル処理(手続き処理) |
|---|---|---|
| 処理単位 | 複数行の集合 | 1行ずつ |
| 得意なこと | データの検索、集計、一括更新・削除 | 1行ごとに複雑な条件分岐や逐次処理を行う |
| イメージ | 検索結果を一枚の表として一気に返す | 結果セット内を一行ずつ移動するポインタ(しおり) |
| 主な用途 | データ抽出、レポート作成 | バッチ処理、ログ記録、トリガやストアドプロシージャ内での複雑なロジック |
カーソル処理の4ステップ│DECLARE, OPEN, FETCH, CLOSEの使い方
SQLカーソルを使った処理には、お決まりの「型」があります。それは「①宣言 → ②オープン → ③フェッチ(繰り返し) → ④クローズ」という4つのステップです。この一連の流れを理解することが、カーソル攻略の最大のポイントです。
① DECLARE(宣言):どのレシピを読むか決める
まずはじめに、どのようなデータを1行ずつ処理したいのかを定義します。これがカーソルの宣言(DECLARE)です。
【構文のポイント】DECLARE <カーソル名> CURSOR FOR <SELECT文>;
② OPEN(オープン):レシピを開いて準備する
次に、宣言したカーソルを使える状態にします。これがカーソルのオープン(OPEN)です。
③ FETCH(フェッチ):1行読んで、処理を実行する
いよいよデータを1行ずつ取り出して処理します。この操作がフェッチ(FETCH)です。
【構文のポイント】FETCH <カーソル名> INTO <変数1>, <変数2>, ...;
④ CLOSE(クローズ):レシピを閉じて片付ける
ループ処理がすべて終わったら、最後にカーソルを閉じます。これがクローズ(CLOSE)です。
各ステップの役割と構文まとめ
| ステップ | 役割 | SQL構文例 (SQL Server / MySQL) |
|---|---|---|
| ① DECLARE | カーソルの宣言:名前と、対象となるSELECT文を定義する。 | DECLARE cur_products CURSOR FOR SELECT product_id, price FROM products; |
| ② OPEN | カーソルのオープン:SELECT文を実行し、結果セットを準備する。 | OPEN cur_products; |
| ③ FETCH | データの取得:結果セットから1行分のデータを変数に格納する。 | FETCH cur_products INTO @p_id, @p_price; |
| ④ CLOSE | カーソルのクローズ:カーソルを閉じて、関連リソースを解放する。 | CLOSE cur_products; |
| (補足) | 定義の解放:カーソル定義自体をメモリから削除する。 | DEALLOCATE cur_products; |
【実践編】具体的なコードで見るカーソルの使い方とループ処理
ここまでのステップを、具体的なSQLコード(ストアドプロシージャ)の例で見ていきましょう。ここでは、「在庫が一定数(threshold)以下の商品を抽出し、その商品ごとに発注テーブル(orders)にレコードを追加する」という処理を想定します。
-- SQL Server / MySQL でのストアドプロシージャ例
CREATE PROCEDURE sp_order_low_stock_products(IN threshold INT)
BEGIN
-- 1. 変数の宣言 (FETCHで受け取る用)
DECLARE done INT DEFAULT FALSE;
DECLARE p_id INT;
DECLARE p_name VARCHAR(255);
-- 2. カーソルの宣言 (DECLARE)
DECLARE cur_low_stock CURSOR FOR
SELECT product_id, product_name
FROM products
WHERE stock <= threshold;
-- 3. ループの終了条件を定義 (データがなくなったら done を TRUE に)
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- 4. カーソルのオープン (OPEN)
OPEN cur_low_stock;
-- 5. ループ処理でデータを1行ずつ取得 (FETCH)
read_loop: LOOP
FETCH cur_low_stock INTO p_id, p_name;
-- ループの終了判定
IF done THEN
LEAVE read_loop;
END IF;
-- 取得したデータを使って発注テーブルにINSERT
INSERT INTO orders (product_id, order_date, status)
VALUES (p_id, CURDATE(), 'pending');
END LOOP;
-- 6. カーソルのクローズ (CLOSE)
CLOSE cur_low_stock;
END;
コード解説:ループ処理の仕組み
このコードの心臓部はLOOPとFETCHの部分です。
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;: 「FETCHしようとしたけど、もう次の行がなかった(NOT FOUND)」場合に、変数doneをTRUEに設定するという意味です。これがループを抜けるための重要な合図になります。FETCH ... INTO ...;: カーソルが指す行のデータを、指定した変数に格納します。IF done THEN LEAVE ...;: もし変数doneがTRUEなら、LEAVE命令でループを脱出します。
このように、カーソルはループ処理と組み合わせることで真価を発揮します。データベーススペシャリスト試験の午後問題などでは、このループ構造を正しく読み解けるかが鍵となります。
カーソルとトリガの関係性│BEFORE/AFTERトリガでの連携と注意点
「カーソルとトリガはどう違うの?」「どう連携するの?」という疑問は、多くの学習者が抱くポイントです。結論から言うと、両者は全く異なる役割を持つ機能ですが、特定の目的のために連携させることがあります。
活用例:AFTER INSERTトリガとカーソルの連携
シナリオ:「注文テーブル(orders)に新しい注文データが1件追加された後(AFTER INSERT)に、その注文明細(order_details)を1行ずつ参照し、商品マスタ(products)の在庫数を更新する」
このような「何かが起きた後」の関連処理にAFTERトリガとカーソルの組み合わせが使われます。
活用例:BEFORE INSERTトリガでの事前チェック(コード例)
次に、データがテーブルに書き込まれる前に実行されるBEFOREトリガの例を見てみましょう。
シナリオ:「注文明細テーブル(order_details)に新しいレコードが追加される前に、その注文商品に紐づく全ての『関連商品』(例:セット販売品など)の在庫をチェックし、一つでも在庫がなければエラーを発生させて注文をキャンセルする」
このシナリオでは、BEFORE INSERTトリガ内でカーソルを使い、複雑な事前チェックロジックを実現します。
-- MySQLでのBEFORE INSERTトリガ例
CREATE TRIGGER trg_check_related_stock_before_insert
BEFORE INSERT ON order_details
FOR EACH ROW
BEGIN
-- 変数宣言
DECLARE done INT DEFAULT FALSE;
DECLARE related_p_id INT;
DECLARE current_stock INT;
-- カーソル宣言:これからINSERTされる商品ID(NEW.product_id)に紐づく関連商品を取得
DECLARE cur_related CURSOR FOR
SELECT related_product_id FROM related_products WHERE original_product_id = NEW.product_id;
-- ループ終了ハンドラ
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur_related;
check_loop: LOOP
FETCH cur_related INTO related_p_id;
IF done THEN
LEAVE check_loop;
END IF;
-- 関連商品の在庫数をチェック
SELECT stock INTO current_stock FROM products WHERE product_id = related_p_id;
-- 在庫が0ならエラーを発生させてINSERTを中止
IF current_stock <= 0 THEN
-- エラーを発生させる
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '関連商品の在庫がありません。注文をキャンセルしました。';
END IF;
END LOOP;
CLOSE cur_related;
END;
コード解説
FOR EACH ROW: INSERTされる各行に対してこのトリガが実行されることを意味します。NEW.product_id: トリガ内で使える特殊なキーワードで、これからINSERTされようとしている行のproduct_id列の値を指します。SIGNAL SQLSTATE '45000': ユーザー定義のエラーを意図的に発生させる命令です。これにより、条件を満たさないINSERT処理そのものを失敗させることができます。
このように、BEFOREトリガはデータの整合性を保つための「門番」のような役割を果たし、そのチェック処理が複雑な場合にカーソルが活用されることがあります。
BEFOREトリガ / AFTERトリガの使い分けと注意点
BEFOREトリガ: データがテーブルに書き込まれる前に実行されます。入力値のチェックや、値の自動補正などに使われます。AFTERトリガ: データが書き込まれた後に実行されます。書き込まれたデータを使って別のテーブルを更新したり、ログを残したりする場合に適しています。
【重要】パフォーマンスへの注意
トリガ、特にその内部でカーソルを使う処理は、パフォーマンスに大きな影響を与える可能性があります。トリガは1回のINSERTやUPDATEの裏で自動実行されるため、処理が重いとデータベース全体の応答速度を低下させる原因になりかねません。そのため、実務では「本当にカーソルでないと実現できないか?」「集合ベースのSQLで一括更新できないか?」を十分に検討することが非常に重要です。