MariaDB「BEGIN END」のよくあるエラーと解決策:構文・デリミタの落とし穴

2025-05-21

BEGIN...END の基本的な考え方

BEGIN...END ブロックは、複数のステートメントを1つの複合ステートメントとして定義します。これにより、以下のようなことが可能になります。

  1. 制御フローの記述: BEGIN...END ブロックの中で、IFCASELOOPWHILEREPEAT などの制御フロー構文を使用して、条件分岐や繰り返し処理を実装できます。これにより、より複雑なロジックをSQLで記述することが可能になります。

  2. ローカル変数の宣言: BEGIN...END ブロックの中で、DECLARE ステートメントを使ってローカル変数を宣言し、そのブロック内でのみ有効な変数として使用できます。

使用例

ストアドプロシージャでの使用例

最も一般的な使用例は、ストアドプロシージャの中です。

DELIMITER // -- 区切り文字を一時的に変更

CREATE PROCEDURE calculate_total_sales(IN customer_id INT)
BEGIN
    -- ローカル変数の宣言
    DECLARE total_amount DECIMAL(10, 2);
    DECLARE order_count INT;

    -- 特定の顧客の合計売上を計算
    SELECT SUM(amount), COUNT(order_id)
    INTO total_amount, order_count
    FROM orders
    WHERE customer_id = customer_id;

    -- 結果の表示(または他の処理)
    IF total_amount IS NOT NULL THEN
        SELECT CONCAT('顧客ID ', customer_id, ' の合計売上は ', total_amount, ' 円です。注文数は ', order_count, ' 件です。') AS message;
    ELSE
        SELECT CONCAT('顧客ID ', customer_id, ' の注文はありません。') AS message;
    END IF;
END //

DELIMITER ; -- 区切り文字を元に戻す

-- ストアドプロシージャの呼び出し
CALL calculate_total_sales(101);

解説:

  • DELIMITER //DELIMITER ; は、ストアドプロシージャの定義内に含まれるセミコロンが、MariaDBクライアントによってステートメントの区切りとして誤って解釈されないように、一時的に区切り文字を変更するためのものです。

ストアドプログラム外での使用例(MariaDB 10.1.1以降)

MariaDB 10.1.1以降では、BEGIN NOT ATOMIC...END を使用することで、ストアドプログラムの外部でも複合ステートメントを使用できます。

DELIMITER |

BEGIN NOT ATOMIC
    DECLARE x INT DEFAULT 0;
    WHILE x < 5 DO
        SELECT x;
        SET x = x + 1;
    END WHILE;
END |

DELIMITER ;

解説:

  • この例では、WHILE ループを使って、変数 x の値を0から4まで順番に表示しています。
  • BEGIN NOT ATOMIC は、トランザクションの開始と混同されないように明示的に複合ステートメントであることを示します。

BEGIN...END の主な特徴

  • ラベル: [begin_label:] BEGIN ... END [end_label] のようにラベルを付けることができ、LEAVEITERATE ステートメントで特定のブロックを抜け出したり、繰り返したりする際に使用できます。
  • ネスト可能: BEGIN...END ブロックはネスト(入れ子)にすることができます。
  • ローカルスコープ: ブロック内で宣言された変数は、そのブロック内でのみ有効です。
  • 制御フロー: IF, CASE, LOOP, WHILE, REPEAT などを使用して、プログラムの実行フローを制御できます。
  • 複合ステートメント: 複数のSQLステートメントをグループ化します。
  • トランザクションとの違い: BEGIN は、トランザクションを開始するためのSQLコマンド(START TRANSACTION や単に BEGIN)としても使われますが、BEGIN...END ブロックは複合ステートメントを定義するためのもので、直接トランザクションを意味するわけではありません。ストアドプロシージャ内で明示的に START TRANSACTIONCOMMITROLLBACK を記述することで、トランザクション制御を行うことができます。
  • 区切り文字の変更: ストアドプロシージャや関数を作成する際、BEGIN...END ブロック内のセミコロンがMariaDBクライアントによってステートメントの終わりと解釈されないように、一時的に区切り文字を変更する必要があります(例: DELIMITER //)。


DELIMITER の使い忘れまたは誤用

エラーの症状
ストアドプロシージャや関数を定義しようとすると、BEGIN の直後やブロック内のセミコロンで構文エラー (SQL Error [1064] [42000]: You have an error in your SQL syntax; check the manual...) が発生する。

原因
MariaDB クライアントは、デフォルトでセミコロン (;) をステートメントの区切り文字として認識します。しかし、BEGIN...END ブロック内には複数のステートメントが含まれるため、ブロック内のセミコロンが意図せずステートメントの終わりと解釈されてしまいます。

解決策
ストアドプログラムを定義する際には、一時的にステートメントの区切り文字を別のものに変更する必要があります。一般的には DELIMITER //DELIMITER $$ などが使われます。定義が完了したら、DELIMITER ; で元の区切り文字に戻します。


-- 誤った例 (DELIMITER を使わない)
CREATE PROCEDURE my_proc()
BEGIN
    SELECT 'Hello';
    SELECT 'World';
END; -- ここでエラーになる可能性が高い

-- 正しい例
DELIMITER // -- 区切り文字を「//」に変更
CREATE PROCEDURE my_proc()
BEGIN
    SELECT 'Hello'; -- ここはブロック内のステートメントなのでエラーにならない
    SELECT 'World';
END // -- 変更した区切り文字でステートメントの終わりを示す
DELIMITER ; -- 区切り文字を元に戻す

DECLARE ステートメントの位置の誤り

エラーの症状
DECLARE を使って変数を宣言しようとすると、SQL Error [1064] [42000]: You have an error in your SQL syntax; check the manual... near 'DECLARE...' のようなエラーが発生する。

原因
DECLARE ステートメントは、BEGIN...END ブロックの先頭でなければなりません。他のステートメント(SELECT など)の後に DECLARE を記述することはできません。


-- 誤った例
CREATE PROCEDURE my_proc()
BEGIN
    SELECT 'Before declare';
    DECLARE my_var INT DEFAULT 0; -- ここでエラー
    SET my_var = 1;
END;

-- 正しい例
CREATE PROCEDURE my_proc()
BEGIN
    DECLARE my_var INT DEFAULT 0; -- BEGIN の直後
    SELECT 'Before declare';
    SET my_var = 1;
END;

ストアドプログラム外での BEGIN...END の使用 (MariaDB 10.1.1 未満)

エラーの症状
ストアドプロシージャや関数などの定義外で BEGIN...END を使用しようとすると、構文エラーが発生する。

原因
MariaDB 10.1.1 より前のバージョンでは、BEGIN...END はストアドプログラムのコンテキスト内でのみ使用できる複合ステートメントでした。

解決策

  • また、MariaDB 10.1.1 以降であっても、ストアドプログラムの外部で BEGIN...END を使用する場合は、BEGIN NOT ATOMIC を明示的に指定する必要があります。これにより、通常のトランザクション開始の BEGIN と区別されます。
  • アップグレードできない場合は、そのコードをストアドプロシージャなどのストアドプログラム内に記述する必要があります。
  • MariaDB のバージョンを 10.1.1 以降にアップグレードすることを検討してください。


-- MariaDB 10.1.1 以降で推奨される形式(ストアドプログラム外)
DELIMITER |
BEGIN NOT ATOMIC
    DECLARE i INT DEFAULT 0;
    WHILE i < 3 DO
        SELECT i;
        SET i = i + 1;
    END WHILE;
END |
DELIMITER ;

-- 以前のバージョンまたは BEGIN NOT ATOMIC を省略した場合、エラーになる可能性あり

予約語の衝突

エラーの症状
変数名やラベル名に MariaDB の予約語を使用すると、構文エラーが発生する。

原因
BEGINEND 自体も予約語ですが、BEGIN...END ブロック内で宣言する変数名や、ラベル名に SELECTINSERT といったSQLのキーワードを使うと問題が発生します。

解決策
変数名やラベル名には、予約語ではない名前を選ぶか、バッククォート (`) で囲ってエスケープします。ただし、エスケープは構文を読みにくくするため、できるだけ予約語ではない名前を使うのが良いでしょう。


-- 誤った例
DECLARE SELECT INT DEFAULT 0; -- 'SELECT' は予約語なのでエラー

-- 正しい例
DECLARE my_select_var INT DEFAULT 0;
-- または
DECLARE `SELECT` INT DEFAULT 0; -- 非推奨

文法の誤り (IF, LOOP, WHILE など)

エラーの症状
BEGIN...END ブロック内で使用する IF, LOOP, WHILE などの制御フロー文で構文エラーが発生する。

原因
各制御フロー文には固有の文法があり、例えば IF には THENEND IF が必要だったり、WHILE には DOEND WHILE が必要だったりします。これらが不足しているとエラーになります。

解決策
MariaDB の公式ドキュメントを参照し、使用している制御フロー文の正しい構文を確認します。


-- 誤った例 (IF の END IF がない)
IF some_condition THEN
    SELECT 'Condition is true';
END; -- ここでエラー

-- 正しい例
IF some_condition THEN
    SELECT 'Condition is true';
END IF; -- IF には END IF が必要

エラーの症状
ストアドプロシージャ実行中にエラーが発生したが、適切なエラーメッセージが表示されない、または予期せぬ動作をする。

解決策
DECLARE ... HANDLER FOR ... 構文を使用して、エラーハンドラを定義することで、特定のSQLSTATEやMySQLエラーコードが発生した場合に、そのエラーを捕捉して適切な処理を行うことができます。

DELIMITER //
CREATE PROCEDURE example_with_error_handling()
BEGIN
    -- エラーハンドラの宣言
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
    BEGIN
        -- エラーが発生した場合に実行される処理
        SELECT 'エラーが発生しました!' AS ErrorMessage;
        -- 必要に応じてエラー情報を取得
        GET DIAGNOSTICS CONDITION 1 @sqlstate = RETURNED_SQLSTATE, @errno = MYSQL_ERRNO, @text = MESSAGE_TEXT;
        SELECT CONCAT('SQLSTATE: ', @sqlstate, ', ErrorNo: ', @errno, ', Message: ', @text) AS ErrorDetails;
    END;

    -- エラーが発生する可能性のある処理
    INSERT INTO non_existent_table (id) VALUES (1); -- 存在しないテーブルへの挿入

    SELECT '処理が完了しました。' AS Message; -- エラーハンドラがなければここまで来ない
END //
DELIMITER ;

CALL example_with_error_handling();


例1: 基本的なストアドプロシージャ(単純な処理)

DELIMITER //

CREATE PROCEDURE greet_users()
BEGIN
    -- メッセージをいくつか表示
    SELECT 'Hello, MariaDB Users!' AS Message1;
    SELECT 'Welcome to the world of stored procedures!' AS Message2;
    -- 現在の日付と時刻を表示
    SELECT NOW() AS CurrentDateTime;
END //

DELIMITER ;

-- プロシージャの呼び出し
CALL greet_users();

解説

  • DELIMITER //DELIMITER ; は、プロシージャ定義内のセミコロンを MariaDB クライアントがステートメントの区切りと誤解しないように、一時的に区切り文字を変更し、元に戻すために使用します。

例2: 変数の宣言と使用

BEGIN...END ブロック内でローカル変数を宣言し、使用する例です。

DELIMITER //

CREATE PROCEDURE calculate_area(IN length DECIMAL(10, 2), IN width DECIMAL(10, 2))
BEGIN
    -- ローカル変数 area を宣言し、データ型 DECIMAL(10, 2) を指定
    DECLARE area DECIMAL(10, 2);

    -- 面積を計算し、変数 area に格納
    SET area = length * width;

    -- 結果を表示
    SELECT CONCAT('The area is: ', area) AS CalculatedArea;
END //

DELIMITER ;

-- プロシージャの呼び出し
CALL calculate_area(10.5, 5.2);
CALL calculate_area(7, 3);

解説

  • IN length DECIMAL(10, 2), IN width DECIMAL(10, 2) は、プロシージャが受け取る入力パラメータです。
  • SET area = length * width; で、計算結果を変数に代入しています。
  • DECLARE area DECIMAL(10, 2); で、area という名前のローカル変数を宣言しています。この変数は BEGIN...END ブロック内でのみ有効です。

例3: 条件分岐 (IF...THEN...ELSE...END IF)

IF ステートメントを使って、条件に基づいて異なる処理を実行する例です。

DELIMITER //

CREATE PROCEDURE check_number_parity(IN num INT)
BEGIN
    DECLARE message VARCHAR(100);

    IF num % 2 = 0 THEN
        SET message = CONCAT(num, ' is an even number.');
    ELSE
        SET message = CONCAT(num, ' is an odd number.');
    END IF;

    SELECT message AS Result;
END //

DELIMITER ;

-- プロシージャの呼び出し
CALL check_number_parity(10);
CALL check_number_parity(7);
CALL check_number_parity(0);

解説

  • END IF;IF ステートメントの終了を示す必須のキーワードです。
  • IF num % 2 = 0 THEN ... ELSE ... END IF; で、num が偶数か奇数かを判定し、それに応じて message 変数に異なる文字列を代入しています。

例4: ループ処理 (WHILE...DO...END WHILE)

WHILE ループを使って、条件が真である限り処理を繰り返す例です。

DELIMITER //

CREATE PROCEDURE simple_loop(IN max_count INT)
BEGIN
    DECLARE counter INT DEFAULT 0;
    DECLARE result_message VARCHAR(255) DEFAULT '';

    WHILE counter < max_count DO
        SET result_message = CONCAT(result_message, 'Count: ', counter, '; ');
        SET counter = counter + 1;
    END WHILE;

    SELECT result_message AS LoopResult;
END //

DELIMITER ;

-- プロシージャの呼び出し
CALL simple_loop(5);
CALL simple_loop(0);

解説

  • SET counter = counter + 1; でカウンタをインクリメントし、無限ループにならないようにします。
  • WHILE counter < max_count DO ... END WHILE; で、countermax_count より小さい間、ループを繰り返します。

例5: エラーハンドリング (DECLARE HANDLER)

BEGIN...END ブロック内でエラーが発生した場合に、そのエラーを捕捉して処理する例です。

DELIMITER //

CREATE PROCEDURE safe_insert(IN p_id INT, IN p_name VARCHAR(100))
BEGIN
    -- エラーハンドラの宣言: 重複キーエラー (SQLSTATE '23000') が発生した場合
    DECLARE CONTINUE HANDLER FOR 1062 -- MySQL error code for Duplicate entry for key
    BEGIN
        SELECT CONCAT('Error: Duplicate ID (', p_id, ') detected. Skipping insertion.') AS ErrorMessage;
        -- エラー詳細の取得(オプション)
        GET DIAGNOSTICS CONDITION 1 @sqlstate = RETURNED_SQLSTATE, @errno = MYSQL_ERRNO, @text = MESSAGE_TEXT;
        SELECT CONCAT('SQLSTATE: ', @sqlstate, ', Errno: ', @errno, ', Message: ', @text) AS ErrorDetails;
    END;

    -- テスト用のテーブルが存在しない場合は作成
    CREATE TABLE IF NOT EXISTS users (
        id INT PRIMARY KEY,
        name VARCHAR(100)
    );

    -- データを挿入
    INSERT INTO users (id, name) VALUES (p_id, p_name);

    SELECT CONCAT('Successfully inserted ID: ', p_id) AS Status;

END //

DELIMITER ;

-- プロシージャの呼び出し
-- 1回目:正常に挿入
CALL safe_insert(1, 'Alice');
-- 2回目:重複キーエラーが発生し、エラーハンドラが処理
CALL safe_insert(1, 'Bob');
-- 3回目:正常に挿入
CALL safe_insert(2, 'Charlie');

-- 挿入されたデータを確認
SELECT * FROM users;

-- テーブルをクリーンアップ(オプション)
DROP TABLE IF EXISTS users;

解説

  • この例では、重複キーエラーが発生してもプロシージャの実行が中断されず、エラーメッセージが表示されて次の処理(この場合は SELECT 'Successfully inserted...')に進みます。ただし、CONTINUE HANDLER なので、INSERT ステートメント自体は失敗しています。EXIT HANDLER を使うと、ハンドラ処理後にプロシージャが終了します。
  • DECLARE CONTINUE HANDLER FOR 1062 は、「エラーコード 1062 (重複キー) が発生した場合、処理を中断せずに(CONTINUE)指定された BEGIN...END ブロック内の処理を実行する」という意味です。

例6: ストアドファンクションでの使用

BEGIN...END はストアドファンクションでも使用されます。ファンクションは値を返します。

DELIMITER //

CREATE FUNCTION get_full_name(first_name VARCHAR(50), last_name VARCHAR(50))
RETURNS VARCHAR(100)
DETERMINISTIC -- 同じ入力に対して常に同じ出力を返すことを示す
BEGIN
    DECLARE full_name VARCHAR(100);
    SET full_name = CONCAT(first_name, ' ', last_name);
    RETURN full_name; -- 結果を返す
END //

DELIMITER ;

-- ファンクションの呼び出し
SELECT get_full_name('John', 'Doe') AS FullName;
SELECT get_full_name('Jane', 'Smith') AS FullName;

解説

  • ファンクションは必ず RETURN ステートメントで値を返さなければなりません。
  • DETERMINISTIC は、ファンクションが同じ入力に対して常に同じ結果を返すことを MariaDB に伝えます。これはオプティマイザが最適化を行う上で役立ちます。
  • RETURNS VARCHAR(100) は、ファンクションが VARCHAR(100) 型の値を返すことを宣言しています。
DELIMITER |

BEGIN NOT ATOMIC
    DECLARE i INT DEFAULT 1;
    WHILE i <= 3 DO
        SELECT CONCAT('Current value of i: ', i) AS Message;
        SET i = i + 1;
    END WHILE;
    SELECT 'Loop finished.' AS FinalMessage;
END |

DELIMITER ;
  • このブロック内の変数は、ブロックの実行中にのみ存在します。
  • BEGIN NOT ATOMIC は、これがトランザクションではなく、単なる複合ステートメントブロックであることを示します。


主な代替方法と、それぞれの適応ケースについて説明します。

単一のSQLステートメント

最も基本的な代替であり、もし目的が1つのSQLステートメントの実行だけであれば、BEGIN...END は不要です。

適用ケース

  • CREATE TABLE, ALTER TABLE などでスキーマを変更する
  • INSERT, UPDATE, DELETE で単一の操作を行う
  • SELECT 文でデータを取得する


-- BEGIN...END なしで単一のSELECTステートメントを実行
SELECT customer_name, total_orders FROM customers WHERE customer_id = 101;

-- BEGIN...END なしで単一のUPDATEステートメントを実行
UPDATE products SET price = price * 1.10 WHERE category = 'Electronics';

クライアントサイドのプログラミング言語

最も柔軟で強力な代替手段です。PHP, Python, Java, Node.js, C# などのプログラミング言語から MariaDB に接続し、複数の SQL ステートメントを実行したり、複雑なロジックを実装したりできます。

BEGIN...END (ストアドプログラム) の代わりにクライアントサイドを選ぶ理由

  • 開発者の慣れ: チームが特定のプログラミング言語に習熟している場合。
  • リソースの分散: データベースサーバーのCPUリソースを消費するような重い計算を、アプリケーションサーバーにオフロードしたい場合。
  • デバッグのしやすさ: クライアントサイドのコードは、IDEやデバッガを使ってデバッグしやすいことが多い。
  • 外部システムとの連携: API呼び出し、ファイル操作、メール送信など、データベース単体では難しい外部システムとの連携が必要な場合。
  • 複雑なビジネスロジック: データベースに依存しない、より複雑なビジネスロジックやアルゴリズムを実装する場合。

例 (Python の例)

import mariadb
import sys

try:
    conn = mariadb.connect(
        user="your_user",
        password="your_password",
        host="127.0.0.1",
        port=3306,
        database="your_database"
    )
except mariadb.Error as e:
    print(f"Error connecting to MariaDB Platform: {e}")
    sys.exit(1)

cur = conn.cursor()

customer_id = 101

try:
    # 複数のSQLステートメントを順次実行
    cur.execute(f"SELECT customer_name, email FROM customers WHERE customer_id = {customer_id}")
    customer_info = cur.fetchone()

    if customer_info:
        customer_name, email = customer_info
        print(f"Customer Name: {customer_name}, Email: {email}")

        # 別のテーブルに対する操作
        cur.execute(f"SELECT order_id, order_date, total_amount FROM orders WHERE customer_id = {customer_id}")
        orders = cur.fetchall()

        if orders:
            print("Orders:")
            for order in orders:
                print(f"  Order ID: {order[0]}, Date: {order[1]}, Amount: {order[2]}")
        else:
            print("No orders found for this customer.")
    else:
        print(f"Customer with ID {customer_id} not found.")

except mariadb.Error as e:
    print(f"Error: {e}")

finally:
    conn.close()

トランザクション (START TRANSACTION / COMMIT / ROLLBACK)

複数のSQLステートメントを「アトミック(不可分)」な単位として実行したい場合、つまり、全てのステートメントが成功するか、全てが失敗して元に戻るか、という保証が必要な場合にトランザクションを使用します。

BEGIN...END とトランザクションの違い

  • START TRANSACTION (または単に BEGIN) はトランザクションの開始を意味します。
  • BEGIN...END複合ステートメントの定義であり、それ自体はトランザクションを意味しません。

ストアドプログラム内でトランザクションを制御したい場合は、BEGIN...END ブロックの中に START TRANSACTION, COMMIT, ROLLBACK を記述します。

適用ケース

  • データの整合性を厳密に保ちたい場合
  • 注文処理(注文ヘッダの作成と注文明細の作成が同時に成功する必要がある)
  • 銀行口座の送金処理(引き出しと預け入れが両方成功するか、両方失敗するか)


DELIMITER //

CREATE PROCEDURE transfer_funds(
    IN from_account_id INT,
    IN to_account_id INT,
    IN amount DECIMAL(10, 2)
)
BEGIN
    DECLARE exit handler for sqlexception
    BEGIN
        ROLLBACK; -- エラーが発生したらロールバック
        RESIGNAL; -- エラーを呼び出し元に再通知
    END;

    START TRANSACTION; -- トランザクション開始

    -- 送金元から引き出し
    UPDATE accounts SET balance = balance - amount WHERE account_id = from_account_id;

    -- 送金先へ預け入れ
    UPDATE accounts SET balance = balance + amount WHERE account_id = to_account_id;

    COMMIT; -- 全て成功したらコミット
    SELECT 'Funds transferred successfully.' AS Status;

END //

DELIMITER ;

-- accounts テーブルの準備 (例)
-- CREATE TABLE accounts (account_id INT PRIMARY KEY, balance DECIMAL(10, 2));
-- INSERT INTO accounts VALUES (1, 1000), (2, 500);

-- プロシージャの呼び出し
CALL transfer_funds(1, 2, 200);

-- 結果確認
SELECT * FROM accounts;

複数の独立した SQL ステートメント

非常にシンプルなスクリプトで、各ステートメントが独立しており、前のステートメントの成功/失敗に依存しない場合。

適用ケース

  • データのエクスポート/インポートスクリプト。
  • 複数の INSERT 文を一度に実行するが、どれか一つが失敗しても他は続行してほしい場合。


-- 複数の独立したINSERTステートメント
INSERT INTO users (id, name) VALUES (1, 'Alice');
INSERT INTO users (id, name) VALUES (2, 'Bob');
INSERT INTO users (id, name) VALUES (3, 'Charlie');

この場合、2番目の INSERT が失敗しても、3番目の INSERT は実行されます。BEGIN...END やトランザクションは不要です。

  • 複数の操作をアトミックにしたい: トランザクション (START TRANSACTION)
    • データの整合性を保証する必要がある場合
  • データベース外部でロジックを実装したい: クライアントサイドのプログラミング言語
    • 複雑なビジネスロジック、外部連携
    • デバッグやテストのしやすさ
    • 開発チームの技術スタック
  • データベース内部でロジックを完結させたい: ストアドプロシージャ/関数 (BEGIN...END を含む)
    • ネットワークI/Oの削減
    • セキュリティ(直接テーブルアクセスさせたくない場合)
    • データベースに特化した複雑なロジック(カーソル、トリガーなど)