SQLでFOR文を使う方法 – ループ処理完全ガイド

データベース・SQL

この記事では、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):再帰クエリの使用

どのような場合にループ処理を使うべきですか?

適切な使用場面

  1. 複雑なビジネスロジック:集合操作では表現困難な処理
  2. 段階的処理:大量データを小分けして処理
  3. 外部システム連携:API呼び出し等の外部処理
  4. エラーハンドリング重視:処理途中でのエラー回復が必要

ループ処理中にデッドロックが発生した場合の対処法は?

対処方法

  1. テーブルロック順序の統一
  2. トランザクション時間の短縮
  3. 適切な分離レベルの設定
  4. リトライロジックの実装
-- リトライロジックの例(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シンプルな構文
PostgreSQLFOR…LOOP最も柔軟な制御構造
SQL Server×WHILET-SQL独自の機能
OracleFOR…LOOP豊富なループオプション
SQLite×なしループ処理非対応

ベストプラクティス

  1. 集合処理を優先検討
  2. 適切なバッチサイズの設定
  3. インデックスの効果的活用
  4. エラーハンドリングの実装
  5. トランザクション制御の最適化

コメント

タイトルとURLをコピーしました