この記事では、SQLでFOR文(ループ処理)を実装する方法を詳しく解説します。
データベース別の構文から実践的な活用例まで、繰り返し処理の技術をマスターしましょう。
SQLにおけるFOR文の概念

標準SQLとループ処理
標準SQLの制限 標準SQLは宣言的言語として設計されており、一般的なプログラミング言語のような手続き的な制御構造(for文、while文など)は含まれていません。
なぜループが必要か
- 大量データの段階的処理
- 複雑なビジネスロジックの実装
- データ変換や集計の複雑な処理
- バッチ処理でのエラーハンドリング
拡張SQLでのループ処理
各データベースシステムは、ストアドプロシージャやPL/SQL等の拡張機能でループ処理をサポートしています:
対応状況一覧
- MySQL:WHILEループ(FOR文は非対応)
- PostgreSQL:PL/pgSQLでFOR文対応
- SQL Server:T-SQLでWHILEループ
- Oracle:PL/SQLでFOR文対応
- SQLite:ループ処理非対応
MySQL での FOR 文実装

WHILEループを使ったFOR文の代替
MySQLでは直接的なFOR文はサポートされていませんが、WHILEループとカウンタ変数を組み合わせることで同等の処理を実現できます。
基本的なループ構造
DELIMITER //
CREATE PROCEDURE basic_for_loop()
BEGIN
DECLARE counter INT DEFAULT 1;
DECLARE max_count INT DEFAULT 10;
WHILE counter <= max_count DO
-- ここにループ処理を記述
SELECT CONCAT('ループ回数: ', counter) AS message;
SET counter = counter + 1;
END WHILE;
END //
DELIMITER ;
-- プロシージャの実行
CALL basic_for_loop();
実用的な例:データの一括処理
サンプルテーブルの準備
-- ログテーブルの作成
CREATE TABLE process_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 商品テーブルの作成
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
price DECIMAL(10,2),
category_id INT
);
段階的なデータ処理
DELIMITER //
CREATE PROCEDURE process_products_batch()
BEGIN
DECLARE batch_size INT DEFAULT 100;
DECLARE offset_value INT DEFAULT 0;
DECLARE total_records INT;
DECLARE processed INT DEFAULT 0;
-- 総レコード数を取得
SELECT COUNT(*) INTO total_records FROM products;
-- バッチ処理ループ
WHILE processed < total_records DO
-- ログ記録
INSERT INTO process_logs (message)
VALUES (CONCAT('処理開始: オフセット ', offset_value));
-- 商品価格の更新(例:10%値上げ)
UPDATE products
SET price = price * 1.1
WHERE id BETWEEN (offset_value + 1) AND (offset_value + batch_size);
-- 次のバッチへ
SET offset_value = offset_value + batch_size;
SET processed = processed + batch_size;
-- 進捗ログ
INSERT INTO process_logs (message)
VALUES (CONCAT('処理完了: ', processed, '/', total_records, ' 件'));
END WHILE;
END //
DELIMITER ;
カーソルを使った行単位処理
DELIMITER //
CREATE PROCEDURE process_each_product()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE prod_id INT;
DECLARE prod_name VARCHAR(100);
DECLARE prod_price DECIMAL(10,2);
-- カーソルの宣言
DECLARE product_cursor CURSOR FOR
SELECT id, name, price FROM products WHERE price > 1000;
-- 終了ハンドラー
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- カーソルを開く
OPEN product_cursor;
-- ループ処理
read_loop: LOOP
FETCH product_cursor INTO prod_id, prod_name, prod_price;
IF done THEN
LEAVE read_loop;
END IF;
-- 各製品に対する処理
INSERT INTO process_logs (message)
VALUES (CONCAT('処理中: ', prod_name, ' (価格: ', prod_price, ')'));
END LOOP;
-- カーソルを閉じる
CLOSE product_cursor;
END //
DELIMITER ;
PostgreSQL での FOR 文実装

PL/pgSQL での基本FOR文
PostgreSQLはPL/pgSQLで直接的なFOR文をサポートしています。
数値範囲でのループ
DO $$
DECLARE
counter INTEGER;
BEGIN
FOR counter IN 1..10 LOOP
RAISE NOTICE '現在の値: %', counter;
END LOOP;
END $$;
逆順ループ
DO $$
DECLARE
counter INTEGER;
BEGIN
FOR counter IN REVERSE 10..1 LOOP
RAISE NOTICE 'カウントダウン: %', counter;
END LOOP;
END $$;
ステップ指定ループ
DO $$
DECLARE
counter INTEGER;
BEGIN
FOR counter IN 1..20 BY 2 LOOP
RAISE NOTICE '偶数ループ: %', counter;
END LOOP;
END $$;
クエリ結果でのループ処理
基本的なレコードループ
DO $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT id, name, email FROM users WHERE active = true LOOP
RAISE NOTICE 'ユーザー処理: % (ID: %)', rec.name, rec.id;
-- 各ユーザーに対する処理をここに記述
UPDATE user_stats
SET last_processed = NOW()
WHERE user_id = rec.id;
END LOOP;
END $$;
実用的な例:データ集計処理
-- 関数として定義
CREATE OR REPLACE FUNCTION generate_monthly_reports()
RETURNS INTEGER AS $$
DECLARE
month_start DATE;
month_end DATE;
total_sales DECIMAL(12,2);
report_count INTEGER := 0;
BEGIN
-- 過去12ヶ月分のレポート生成
FOR i IN 0..11 LOOP
month_start := DATE_TRUNC('month', CURRENT_DATE - INTERVAL '%s month', i);
month_end := month_start + INTERVAL '1 month' - INTERVAL '1 day';
-- 月次売上の計算
SELECT COALESCE(SUM(amount), 0)
INTO total_sales
FROM sales
WHERE sale_date BETWEEN month_start AND month_end;
-- レポートテーブルに挿入
INSERT INTO monthly_reports (report_month, total_sales, created_at)
VALUES (month_start, total_sales, NOW());
report_count := report_count + 1;
RAISE NOTICE 'レポート作成: % (売上: %)', month_start, total_sales;
END LOOP;
RETURN report_count;
END $$ LANGUAGE plpgsql;
-- 関数の実行
SELECT generate_monthly_reports();
SQL Server (T-SQL) での実装
WHILEループを使った処理
SQL ServerのT-SQLでは、WHILEループを使ってFOR文相当の処理を実装します。
基本的なループ構造
DECLARE @counter INT = 1;
DECLARE @max_count INT = 10;
WHILE @counter <= @max_count
BEGIN
PRINT 'ループ回数: ' + CAST(@counter AS VARCHAR(10));
SET @counter = @counter + 1;
END;
テーブル操作での実用例
-- ストアドプロシージャとして実装
CREATE PROCEDURE ProcessCustomerBatches
AS
BEGIN
DECLARE @batch_size INT = 1000;
DECLARE @offset INT = 0;
DECLARE @total_customers INT;
DECLARE @processed INT = 0;
-- 総件数取得
SELECT @total_customers = COUNT(*) FROM customers;
WHILE @processed < @total_customers
BEGIN
-- バッチ処理
UPDATE customers
SET last_updated = GETDATE(),
status = 'processed'
WHERE customer_id IN (
SELECT customer_id
FROM customers
ORDER BY customer_id
OFFSET @offset ROWS
FETCH NEXT @batch_size ROWS ONLY
);
SET @processed = @processed + @batch_size;
SET @offset = @offset + @batch_size;
-- 進捗表示
PRINT '処理完了: ' + CAST(@processed AS VARCHAR) + '/' + CAST(@total_customers AS VARCHAR);
-- 少し待機(システム負荷軽減)
WAITFOR DELAY '00:00:01';
END;
END;
カーソルを使った行単位処理
CREATE PROCEDURE ProcessOrdersOneByOne
AS
BEGIN
DECLARE @order_id INT;
DECLARE @customer_id INT;
DECLARE @order_amount DECIMAL(10,2);
-- カーソルの宣言
DECLARE order_cursor CURSOR FOR
SELECT order_id, customer_id, total_amount
FROM orders
WHERE status = 'pending';
OPEN order_cursor;
FETCH NEXT FROM order_cursor INTO @order_id, @customer_id, @order_amount;
WHILE @@FETCH_STATUS = 0
BEGIN
-- 注文処理ロジック
EXEC ProcessSingleOrder @order_id, @customer_id, @order_amount;
-- 次のレコードを取得
FETCH NEXT FROM order_cursor INTO @order_id, @customer_id, @order_amount;
END;
CLOSE order_cursor;
DEALLOCATE order_cursor;
END;
Oracle PL/SQL での実装

基本的なFOR文
OracleのPL/SQLは最も豊富なループ構文をサポートしています。
数値範囲でのループ
BEGIN
FOR i IN 1..10 LOOP
DBMS_OUTPUT.PUT_LINE('ループ回数: ' || i);
END LOOP;
END;
/
逆順ループ
BEGIN
FOR i IN REVERSE 1..10 LOOP
DBMS_OUTPUT.PUT_LINE('逆順: ' || i);
END LOOP;
END;
/
カーソルFORループ
明示的カーソルの使用
DECLARE
CURSOR emp_cursor IS
SELECT employee_id, first_name, last_name, salary
FROM employees
WHERE department_id = 10;
BEGIN
FOR emp_rec IN emp_cursor LOOP
DBMS_OUTPUT.PUT_LINE('従業員: ' || emp_rec.first_name || ' ' || emp_rec.last_name);
-- 給与更新処理
UPDATE employees
SET salary = salary * 1.05
WHERE employee_id = emp_rec.employee_id;
END LOOP;
COMMIT;
END;
/
暗黙的カーソルの使用
BEGIN
FOR rec IN (SELECT product_id, product_name, unit_price
FROM products
WHERE category_id = 5) LOOP
DBMS_OUTPUT.PUT_LINE('商品処理: ' || rec.product_name);
-- 在庫更新処理
UPDATE inventory
SET last_check_date = SYSDATE
WHERE product_id = rec.product_id;
END LOOP;
COMMIT;
END;
/
複雑な制御構造の例
CREATE OR REPLACE PROCEDURE process_sales_data
AS
TYPE sales_array IS TABLE OF sales%ROWTYPE;
sales_data sales_array;
batch_size CONSTANT INTEGER := 1000;
total_processed INTEGER := 0;
BEGIN
-- バッチごとの処理
FOR batch_num IN 1..10 LOOP
-- データの取得
SELECT * BULK COLLECT INTO sales_data
FROM sales
WHERE ROWNUM <= batch_size
AND processed_flag = 'N';
-- バッチが空でない場合のみ処理
IF sales_data.COUNT > 0 THEN
-- 各レコードの処理
FOR i IN 1..sales_data.COUNT LOOP
-- 売上データの処理ロジック
UPDATE sales
SET processed_flag = 'Y',
processed_date = SYSDATE
WHERE sales_id = sales_data(i).sales_id;
total_processed := total_processed + 1;
END LOOP;
COMMIT;
DBMS_OUTPUT.PUT_LINE('バッチ ' || batch_num || ' 完了: ' || sales_data.COUNT || ' 件処理');
ELSE
DBMS_OUTPUT.PUT_LINE('処理対象データなし');
EXIT; -- ループを抜ける
END IF;
END LOOP;
DBMS_OUTPUT.PUT_LINE('総処理件数: ' || total_processed);
END;
/
高度なループパターン
条件分岐を含むループ
MySQL での実装例
DELIMITER //
CREATE PROCEDURE complex_processing()
BEGIN
DECLARE counter INT DEFAULT 1;
DECLARE current_value INT;
DECLARE processing_mode VARCHAR(20);
WHILE counter <= 100 DO
-- 現在の値に基づく処理分岐
IF counter % 10 = 0 THEN
SET processing_mode = 'BATCH';
SET current_value = counter * 10;
ELSEIF counter % 5 = 0 THEN
SET processing_mode = 'SEMI_BATCH';
SET current_value = counter * 5;
ELSE
SET processing_mode = 'SINGLE';
SET current_value = counter;
END IF;
-- 処理モードに応じた処理
CASE processing_mode
WHEN 'BATCH' THEN
INSERT INTO batch_logs (message, value)
VALUES ('バッチ処理実行', current_value);
WHEN 'SEMI_BATCH' THEN
INSERT INTO batch_logs (message, value)
VALUES ('セミバッチ処理実行', current_value);
ELSE
INSERT INTO batch_logs (message, value)
VALUES ('単一処理実行', current_value);
END CASE;
SET counter = counter + 1;
END WHILE;
END //
DELIMITER ;
ネストしたループ
PostgreSQL での実装例
DO $$
DECLARE
outer_counter INTEGER;
inner_counter INTEGER;
result_value INTEGER;
BEGIN
FOR outer_counter IN 1..5 LOOP
RAISE NOTICE 'アウターループ: %', outer_counter;
FOR inner_counter IN 1..3 LOOP
result_value := outer_counter * inner_counter;
RAISE NOTICE ' インナーループ: % × % = %',
outer_counter, inner_counter, result_value;
-- 結果をテーブルに保存
INSERT INTO calculation_results (outer_val, inner_val, result)
VALUES (outer_counter, inner_counter, result_value);
END LOOP;
END LOOP;
END $$;
パフォーマンス考慮事項

ループ処理の最適化
バッチサイズの調整
-- 適切なバッチサイズでの処理
CREATE PROCEDURE optimized_batch_process()
BEGIN
DECLARE batch_size INT DEFAULT 1000; -- システムに応じて調整
DECLARE processed INT DEFAULT 0;
DECLARE total_records INT;
SELECT COUNT(*) INTO total_records FROM large_table WHERE status = 'pending';
WHILE processed < total_records DO
UPDATE large_table
SET status = 'processed',
updated_at = NOW()
WHERE status = 'pending'
LIMIT batch_size;
SET processed = processed + batch_size;
-- メモリ解放とロック軽減のための小休止
DO SLEEP(0.1);
END WHILE;
END;
インデックスの活用
-- ループ処理前にインデックスを確認・作成
CREATE INDEX idx_status_date ON large_table(status, created_date);
-- インデックスを活用したループ処理
FOR rec IN (SELECT id, data_column
FROM large_table
WHERE status = 'pending'
ORDER BY created_date) LOOP
-- 処理内容
END LOOP;
エラーハンドリング
例外処理を含むループ
PostgreSQL での例外処理
DO $$
DECLARE
rec RECORD;
error_count INTEGER := 0;
success_count INTEGER := 0;
BEGIN
FOR rec IN SELECT id, email FROM users WHERE active = true LOOP
BEGIN
-- メール送信処理(例外が発生する可能性)
PERFORM send_notification_email(rec.email);
success_count := success_count + 1;
EXCEPTION
WHEN OTHERS THEN
-- エラーログの記録
INSERT INTO error_logs (user_id, error_message, occurred_at)
VALUES (rec.id, SQLERRM, NOW());
error_count := error_count + 1;
END;
END LOOP;
RAISE NOTICE '処理完了: 成功 %, エラー %', success_count, error_count;
END $$;
トランザクション制御
CREATE PROCEDURE safe_batch_process()
BEGIN
DECLARE counter INT DEFAULT 1;
DECLARE batch_size INT DEFAULT 100;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
WHILE counter <= 1000 DO
START TRANSACTION;
-- バッチ処理
UPDATE products
SET last_updated = NOW()
WHERE id BETWEEN counter AND (counter + batch_size - 1);
COMMIT;
SET counter = counter + batch_size;
END WHILE;
END;
よくある質問
SQLのループ処理は性能が悪いと聞きましたが本当ですか?
一般的にはその通りです。SQLは集合処理に最適化されており、行単位のループ処理は以下の理由で性能が劣る場合があります:
性能問題の要因:
- 行単位でのI/O発生
- オプティマイザの最適化が効きにくい
- カーソルのオーバーヘッド
代替案:
- 集合操作:可能な限りUPDATE文のJOINを使用
- ウィンドウ関数:ROW_NUMBER()、RANK()等の活用
- CTE(Common Table Expression):再帰クエリの使用
どのような場合にループ処理を使うべきですか?
適切な使用場面:
- 複雑なビジネスロジック:集合操作では表現困難な処理
- 段階的処理:大量データを小分けして処理
- 外部システム連携:API呼び出し等の外部処理
- エラーハンドリング重視:処理途中でのエラー回復が必要
ループ処理中にデッドロックが発生した場合の対処法は?
対処方法:
- テーブルロック順序の統一
- トランザクション時間の短縮
- 適切な分離レベルの設定
- リトライロジックの実装
-- リトライロジックの例(PostgreSQL)
DO $$
DECLARE
retry_count INTEGER := 0;
max_retries INTEGER := 3;
BEGIN
LOOP
BEGIN
-- 処理内容
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
EXIT; -- 成功時はループを抜ける
EXCEPTION
WHEN deadlock_detected THEN
retry_count := retry_count + 1;
IF retry_count >= max_retries THEN
RAISE;
END IF;
PERFORM pg_sleep(random() * 0.1); -- ランダム待機
END;
END LOOP;
END $$;
まとめ
SQLでのFOR文実装は、データベースシステムごとに異なるアプローチが必要です。
重要なポイント
- 標準SQL:FOR文は存在しない
- 拡張機能:各DBの独自構文を使用
- パフォーマンス:集合処理との使い分けが重要
- エラーハンドリング:適切な例外処理とトランザクション制御
データベース別対応まとめ
データベース | FOR文対応 | 代替構文 | 特徴 |
---|---|---|---|
MySQL | × | WHILE | シンプルな構文 |
PostgreSQL | ○ | FOR…LOOP | 最も柔軟な制御構造 |
SQL Server | × | WHILE | T-SQL独自の機能 |
Oracle | ○ | FOR…LOOP | 豊富なループオプション |
SQLite | × | なし | ループ処理非対応 |
ベストプラクティス
- 集合処理を優先検討
- 適切なバッチサイズの設定
- インデックスの効果的活用
- エラーハンドリングの実装
- トランザクション制御の最適化
コメント