【完全ガイド】ORA-00933: SQL command not properly ended の原因と解決方法|UPDATE JOIN・INSERT ORDER BY・MySQL 移行 徹底解説

【完全ガイド】ORA-00933: SQL command not properly ended の原因と解決方法|UPDATE JOIN・INSERT ORDER BY・MySQL 移行 徹底解説

Oracle 開発者がMySQL/PostgreSQL から移行時によく遭遇するエラー:

-- ❌ MySQL では動く、Oracle では ORA-00933
UPDATE users u
INNER JOIN orders o ON u.id = o.user_id
SET u.total = o.amount;

-- ❌ JDBC で末尾セミコロン
"SELECT * FROM users;"

**「SQL 文が正しく終わっていない」**というシンプルなメッセージ。しかし、実態は「Oracle がその構文を許可していない」ケースが多数:

  • UPDATE ... JOINOracle 非対応(MySQL は OK)
  • DELETE ... JOINOracle 非対応
  • INSERT ... ORDER BY構文的に不可
  • JDBC / Railsからの末尾セミコロン
  • UNION 内の ORDER BY(最後にのみ許可)
  • JOIN 構文の混在(+)ANSI JOIN
  • サブクエリ後の余分な文字
  • クオートの不一致
  • 予約語の誤配置
  • CREATE VIEW に ORDER BY

このエラーの厄介さは、「なぜダメか」が分かりにくい点。多くの場合、他 DB では正常に動くコードが Oracle では拒否されます。

現場では:

  • MySQL → Oracle 移行プロジェクト
  • PostgreSQL → Oracle 移行
  • JDBC / Java Spring Data JPA
  • Rails ActiveRecord(Oracle Enhanced Adapter)
  • DBeaver / DataGrip(複数文の一括実行)
  • ETL ツール(自動 SQL 生成)

多くの日本語記事が「セミコロンを取り除け」で終わりますが、実務では:

  • UPDATE ... JOIN正しい Oracle 書き換え
  • MERGEによるJOIN 更新のモダンな方法
  • EXISTS サブクエリでのフィルタ
  • JDBC の PreparedStatement の正しい書き方
  • Rails ActiveRecordupdate_all パターン
  • バッチ SQL の実行方法
  • DBeaver / DataGrip での複数文実行

さらに、SQL 構文エラー6兄弟(ORA-00936 / 00933 / 00907 / 00904 / 00942 / 00918)の中で、ORA-00933 は「余分な要素」型のエラーです:

ORA-00936: 何かが足りない(式)
ORA-00933: 何かが余分(末尾に不正な構文)
ORA-00907: `)` が足りない

本記事では、ORA-00933: SQL command not properly ended完全な原因と解決方法を、リファレンスとして実用的に整理します。10大発生パターン、UPDATE JOIN の書き換え、MERGE 文、他 DB からの移行、Rails/Java/Python 対応、実践シナリオ、FAQまで完全網羅。この1本で ORA-00933 に冷静に対処できるようになります。


目次

結論:Oracle が許可しない構文

時間がない方向けに、最速の対処を先に示します。

エラーメッセージの読み方

ORA-00933: SQL command not properly ended
                                *
ERROR at line 3, column 15:
              ↑
        余分な文字/構文が始まる位置

アスタリスク(*)で示される**「ここが余計だ」**という位置。

Oracle が非対応の構文

-- ❌ 全て ORA-00933
UPDATE ... JOIN ... SET ...
DELETE ... JOIN ...
INSERT ... ORDER BY ...
INSERT ... LIMIT ...
UPDATE ... LIMIT ...
CREATE VIEW ... ORDER BY ...  (一部制限)

最速の診断

STEP 1: エラー位置を確認(* マーク)
STEP 2: よくあるパターンをチェック
- UPDATE JOIN → サブクエリ or MERGE に書き換え
- 末尾セミコロン → 削除(JDBC/Rails 経由の場合)
- ORDER BY の位置確認(UNION の末尾、INSERT/DELETE 不可)
- クオート・カッコの対応確認
STEP 3: 段階的に SQL を短くして特定

5つの解決策

#手法使う場面
サブクエリで書き換えUPDATE/DELETE JOIN
MERGE 文複雑な JOIN 更新
EXISTS サブクエリフィルタ
セミコロン削除JDBC/Rails
ANSI JOIN 統一混在解消

姉妹エラーとの厳密な違い

エラー意味
ORA-00933余分な構文が末尾「余分」
ORA-00936式が欠けている「不足」
ORA-00907) がない「対応不足」
ORA-00904識別子無効「名前」
ORA-00942テーブル不在「存在」
ORA-00918列名曖昧「曖昧」

SQL 構文エラー6兄弟、位置と型で区別。

詳細は以下で解説します。


Oracle の SQL 構文制限

Oracle 特有のルール

1. UPDATE / DELETE で JOIN 構文不可
2. INSERT で ORDER BY 不可
3. INSERT / UPDATE / DELETE で LIMIT 不可(ROWNUM は可)
4. UNION 内で ORDER BY 不可(最後だけ OK)
5. サブクエリ内の ORDER BY は限定的

他 DB との差異

構文MySQLPostgreSQLOracle
UPDATE ... JOIN
DELETE ... JOIN
INSERT ... ORDER BY
LIMIT❌(FETCH FIRST)
末尾セミコロン削除必要(JDBC)

Oracle は最も厳格。他 DB からの移行時は要注意。


【原因①】UPDATE … JOIN の書き換え

症状(MySQL からの移行)

-- ❌ MySQL では OK、Oracle では ORA-00933
UPDATE employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
SET e.dept_name = d.dept_name;

解決A: サブクエリ(相関)

UPDATE employees e
SET e.dept_name = (
  SELECT d.dept_name 
  FROM departments d 
  WHERE d.dept_id = e.dept_id
);

⚠️ 全行が更新される(該当なしは NULL)。

解決B: WHERE + EXISTS

UPDATE employees e
SET e.dept_name = (
  SELECT d.dept_name 
  FROM departments d 
  WHERE d.dept_id = e.dept_id
)
WHERE EXISTS (
  SELECT 1 FROM departments d WHERE d.dept_id = e.dept_id
);
-- 該当あり行のみ更新

解決C: MERGE 文(推奨)

MERGE INTO employees e
USING departments d ON (e.dept_id = d.dept_id)
WHEN MATCHED THEN
  UPDATE SET e.dept_name = d.dept_name;

モダンで効率的、MERGE 推奨。

MERGE の詳細は Oracle MERGE 文 使い方の記事も参照してください。

解決D: 更新可能ビュー(レガシー)

UPDATE (
  SELECT e.dept_name AS e_name, d.dept_name AS d_name
  FROM employees e
  JOIN departments d ON e.dept_id = d.dept_id
)
SET e_name = d_name;

JOIN が Key-Preserved である必要(制約あり)。


【原因②】DELETE … JOIN の書き換え

症状

-- ❌ Oracle 非対応
DELETE FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
WHERE c.status = 'INACTIVE';

解決A: サブクエリ

DELETE FROM orders 
WHERE customer_id IN (
  SELECT customer_id FROM customers WHERE status = 'INACTIVE'
);

解決B: EXISTS

DELETE FROM orders o
WHERE EXISTS (
  SELECT 1 FROM customers c 
  WHERE c.customer_id = o.customer_id 
    AND c.status = 'INACTIVE'
);

大量削除の場合、EXISTS が速い


【原因③】INSERT … ORDER BY

症状

-- ❌ INSERT に ORDER BY は不要(意味がない)
INSERT INTO log_table
SELECT * FROM source_table ORDER BY created_at;
-- 場合により ORA-00933

解決

-- ✅ ORDER BY を削除
INSERT INTO log_table SELECT * FROM source_table;

-- 順序が重要ならログ用の別カラムに RN 保存
INSERT INTO log_table (id, name, rn)
SELECT id, name, ROW_NUMBER() OVER (ORDER BY created_at)
FROM source_table;

【原因④】JDBC / Rails の末尾セミコロン

症状

// ❌ JDBC で末尾セミコロン
String sql = "SELECT * FROM users;";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
-- ORA-00933

解決

// ✅ セミコロン削除
String sql = "SELECT * FROM users";

JDBC / oracledb / Rails は文の終わり自動判定、セミコロン不要。

SQL*Plus / SQLcl での違い

-- SQL*Plus では末尾セミコロン必須
SELECT * FROM users;

-- JDBC / アプリ経由では削除
"SELECT * FROM users"

【原因⑤】UNION 内の ORDER BY

症状

-- ❌ UNION 内に ORDER BY
SELECT * FROM active_users ORDER BY name
UNION
SELECT * FROM inactive_users ORDER BY name;
-- ORA-00933

解決

-- ✅ 最後だけ ORDER BY
SELECT * FROM active_users
UNION
SELECT * FROM inactive_users
ORDER BY name;

UNION の ORDER BY は全体に対する1回のみ


【原因⑥】JOIN 構文の混在

症状

-- ❌ 古い (+) と ANSI JOIN の混在
SELECT * FROM a, b LEFT JOIN c ON b.id = c.id
WHERE a.id = b.a_id(+);
-- ORA-00933 or ORA-25156

解決

ANSI JOIN に統一:

SELECT * FROM a
LEFT JOIN b ON a.id = b.a_id
LEFT JOIN c ON b.id = c.id;

【原因⑦】CREATE VIEW に ORDER BY

症状

-- 一部の Oracle バージョンで
CREATE VIEW v_active AS
SELECT * FROM users WHERE status = 'A' ORDER BY name;
-- ORA-00933(バージョン/権限による)

解決

ORDER BY を削除、SELECT 時に指定:

CREATE VIEW v_active AS
SELECT * FROM users WHERE status = 'A';

-- 使用時
SELECT * FROM v_active ORDER BY name;

【原因⑧】複数 SQL 文の一括実行

症状

// ❌ 複数文を一度に
String sql = "INSERT INTO t VALUES (1); INSERT INTO t VALUES (2);";
stmt.execute(sql);
-- ORA-00933

解決

PL/SQL ブロック:

String sql = "BEGIN " +
             "INSERT INTO t VALUES (1); " +
             "INSERT INTO t VALUES (2); " +
             "END;";
stmt.execute(sql);

または個別実行:

stmt.execute("INSERT INTO t VALUES (1)");
stmt.execute("INSERT INTO t VALUES (2)");

【原因⑨】GROUP BY の後の余分

症状

-- ❌ HAVING の後に何もない
SELECT dept_id, COUNT(*) FROM employees
GROUP BY dept_id HAVING;
-- ORA-00933 or ORA-00936

解決

SELECT dept_id, COUNT(*) FROM employees
GROUP BY dept_id 
HAVING COUNT(*) > 10;

【原因⑩】サブクエリの後の余分

症状

-- ❌ サブクエリの後に不正な文字
SELECT * FROM (SELECT * FROM users) t.name;
--                                    ↑ 余分

解決

SELECT t.name FROM (SELECT * FROM users) t;

5つの解決策 完全リファレンス

解決策① サブクエリで書き換え

UPDATE JOIN → 相関サブクエリ:

UPDATE t1
SET col = (SELECT val FROM t2 WHERE t2.id = t1.id)
WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id);

DELETE JOIN → IN サブクエリ:

DELETE FROM t1 WHERE id IN (SELECT id FROM t2 WHERE ...);

解決策② MERGE 文

MERGE INTO target t
USING source s ON (t.id = s.id)
WHEN MATCHED THEN UPDATE SET t.val = s.val;

JOIN 更新のモダンな方法、Oracle の標準。

解決策③ EXISTS サブクエリ

UPDATE t1
SET col = 'A'
WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id);

DELETE FROM t1
WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id);

大量データで効率的

解決策④ セミコロン削除(JDBC)

// ✅ JDBC / Rails / oracledb
String sql = "SELECT * FROM users";   -- セミコロンなし

解決策⑤ ANSI JOIN 統一

-- ✅ すべて ANSI JOIN
SELECT * FROM a
INNER JOIN b ON a.id = b.a_id
LEFT JOIN c ON b.id = c.b_id;

Rails / Java / Python 対応

Rails ActiveRecord

update_all で JOIN 相当:

# ❌ Rails の JOIN 更新は SQL レベルで書けない
User.joins(:orders).update_all(...)  # Oracle で失敗

# ✅ サブクエリ相当
User.where(id: Order.select(:user_id)).update_all(active: true)

# ✅ 個別 UPDATE ループ
Order.where(status: 'completed').find_each do |order|
  order.user.update(total: order.amount)
end

# ✅ 生 SQL で MERGE
ActiveRecord::Base.connection.execute(<<-SQL)
  MERGE INTO users u
  USING (SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id) o
    ON (u.id = o.user_id)
  WHEN MATCHED THEN UPDATE SET u.total = o.total
SQL

セミコロン不要:

# Rails は自動処理、セミコロン不要
User.find_by_sql("SELECT * FROM users WHERE id = 1")

Rails 8 系の詳細は Rails 8 アップグレードガイドの記事、find/find_by/where の記事、Solid Queue 使い方の記事も参照してください。

Java (JDBC)

// ✅ セミコロン削除
String sql = "SELECT * FROM users WHERE id = ?";

// ✅ UPDATE JOIN → MERGE
String merge = """
    MERGE INTO users u
    USING orders o ON (u.id = o.user_id)
    WHEN MATCHED THEN UPDATE SET u.total = o.amount
    """;

Python (oracledb)

import oracledb

# ✅ セミコロン削除
cursor.execute("SELECT * FROM users WHERE id = :1", (1,))

# ✅ UPDATE JOIN → サブクエリ
cursor.execute("""
    UPDATE users u
    SET u.total = (SELECT SUM(amount) FROM orders o WHERE o.user_id = u.id)
    WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
""")

実践シナリオ

シナリオ1:MySQL → Oracle 移行

-- MySQL
UPDATE users u
JOIN orders o ON u.id = o.user_id
SET u.last_order_at = o.created_at
WHERE o.created_at > '2026-06-01';

-- Oracle(MERGE)
MERGE INTO users u
USING (
  SELECT user_id, MAX(created_at) AS last_at
  FROM orders
  WHERE created_at > DATE '2026-06-01'
  GROUP BY user_id
) o ON (u.id = o.user_id)
WHEN MATCHED THEN 
  UPDATE SET u.last_order_at = o.last_at;

シナリオ2:PostgreSQL → Oracle 移行

-- PostgreSQL
UPDATE orders SET status = 'closed'
FROM customers
WHERE orders.customer_id = customers.id
  AND customers.deleted_at IS NOT NULL;

-- Oracle
UPDATE orders o SET status = 'closed'
WHERE EXISTS (
  SELECT 1 FROM customers c
  WHERE c.id = o.customer_id AND c.deleted_at IS NOT NULL
);

シナリオ3:Rails でのバッチ処理

# ❌ Rails でも Oracle 環境で失敗
User.joins(:orders).where(orders: { status: 'active' }).update_all(status: 'busy')

# ✅ サブクエリ相当
User.where(id: Order.where(status: 'active').select(:user_id)).update_all(status: 'busy')

# ✅ 直接 SQL
ActiveRecord::Base.connection.execute(<<-SQL)
  UPDATE users SET status = 'busy'
  WHERE EXISTS (
    SELECT 1 FROM orders 
    WHERE orders.user_id = users.id 
      AND orders.status = 'active'
  )
SQL

シナリオ4:Data Pump インポート後の一括更新

-- インポート後、参照テーブルから値をコピー
MERGE INTO staging_orders s
USING product_catalog p ON (s.product_code = p.code)
WHEN MATCHED THEN UPDATE SET 
  s.product_name = p.name,
  s.unit_price = p.price;

Data Pump の詳細は Oracle Data Pump 使い方の記事も参照してください。

シナリオ5:ETL ツール(Informatica 等)

-- Informatica が生成する SQL
-- UPDATE JOIN パターンを検出したら
-- ソース設計で MERGE 相当に変換

シナリオ6:DBeaver / DataGrip での複数文

-- ❌ DBeaver で複数文を一括実行
CREATE TABLE t (id NUMBER);
INSERT INTO t VALUES (1);
-- ORA-00933 の可能性(実行モード依存)

-- ✅ PL/SQL ブロック
BEGIN
  EXECUTE IMMEDIATE 'CREATE TABLE t (id NUMBER)';
  INSERT INTO t VALUES (1);
END;
/

シナリオ7:Docker Oracle でのテスト

docker exec -it oracle-xe sqlplus scott/tiger <<EOF
-- UPDATE JOIN テスト
UPDATE emp e
INNER JOIN dept d ON e.deptno = d.deptno
SET e.dname = d.dname;
-- ORA-00933

-- MERGE で正解
MERGE INTO emp e
USING dept d ON (e.deptno = d.deptno)
WHEN MATCHED THEN UPDATE SET e.dname = d.dname;
EOF

Docker 関連は docker daemon 接続エラーの記事、Docker no space left on device の記事も参照してください。

シナリオ8:Java Spring Data JPA

// ❌ JPQL でも Oracle 制約
@Modifying
@Query("UPDATE User u JOIN u.orders o SET u.total = o.amount")
void updateTotal();

// ✅ ネイティブ SQL + MERGE
@Modifying
@Query(value = """
    MERGE INTO users u
    USING orders o ON (u.id = o.user_id)
    WHEN MATCHED THEN UPDATE SET u.total = o.amount
    """, nativeQuery = true)
void updateTotal();

シナリオ9:Autonomous DB

-- Autonomous DB でも同じ制約
-- MERGE 推奨

シナリオ10:Kamal デプロイ後の検証

docker exec -it db-container sqlplus / as sysdba <<EOF
-- SQL 実行確認
BEGIN
  EXECUTE IMMEDIATE 'MERGE INTO users u
    USING new_orders o ON (u.id = o.user_id)
    WHEN MATCHED THEN UPDATE SET u.total = o.amount';
END;
/
EOF

Kamal 2 デプロイの詳細は Kamal 2 デプロイの記事を参照してください。


トラブルシューティング

エラー位置が分からない

UPDATE t SET col = 1 
JOIN t2 ON t.id = t2.id
     *
ERROR at line 2:
ORA-00933: SQL command not properly ended
     ↑ JOIN が余分(Oracle 非対応)

JDBC でエラー

セミコロン削除が基本。または;\nのような複数文。

DBeaver / DataGrip

  • Statement Separator 設定確認
  • PL/SQL モードで複数文実行

Rails マイグレーション

# execute の中は個別に
class MyMigration < ActiveRecord::Migration[8.0]
  def up
    execute "CREATE TABLE ..."   -- セミコロンなし
    execute "INSERT INTO ..."    -- セミコロンなし
  end
end

Java Spring Data JPA

ネイティブ SQLは末尾セミコロン不要:

@Query(value = "SELECT * FROM users", nativeQuery = true)

PostgreSQL RETURNING

-- PG では
INSERT INTO t VALUES (...) RETURNING id;

-- Oracle
INSERT INTO t VALUES (...) RETURNING id INTO :out_id;
-- または OUTPUT 変数

よくある質問(FAQ)

Q1. ORA-00933 と ORA-00936 の違い

  • 00933: 余分な構文(末尾に何かある)
  • 00936: 式が欠けている

Q2. なぜ UPDATE JOIN 非対応?

Oracle の設計判断MERGEサブクエリで代替。

Q3. MERGE と UPDATE JOIN の違い

  • UPDATE JOIN: MySQL 拡張
  • MERGE: ANSI SQL 標準、Oracle 対応

Q4. Rails 側での対応

生 SQL で MERGE を書くか、個別 UPDATE ループ。

Q5. JDBC でセミコロン削除の理由

JDBC はステートメント単位、セミコロン不要。

Q6. UNION の ORDER BY

最後の SELECT の後にのみ許可。

Q7. INSERT の ORDER BY

構文的に不可。ORDER BY は SELECT 用。

Q8. パフォーマンスへの影響

MERGE > EXISTS > IN サブクエリ(一般的に)。

Q9. Autonomous DB での挙動

同じ制約。MERGE 推奨。

Q10. 他 DB からの移行

Oracle SQL Developer Migration Workbench 活用。

Q11. 複数文の実行

PL/SQL ブロックBEGIN ... END;)で。

Q12. E-Business Suite でよく発生?

カスタムレポートの SQL でよく遭遇。


参考リンク

Oracle 公式


まとめ

ORA-00933: SQL command not properly ended の要点を再整理します。

エラーの本質

SQL 文の末尾に余分な構文がある
→ Oracle が期待しない要素
→ 他 DB では動くが Oracle では拒否

エラーメッセージの読み方

SELECT ... UPDATE ... JOIN
                       *
ERROR at line 3:
ORA-00933: SQL command not properly ended
                       ↑ 余分な位置

SQL 構文エラー6兄弟

エラー意味
ORA-00933余分な構文過剰
ORA-00936式が欠けている不足
ORA-00907) がない対応
ORA-00904識別子無効名前
ORA-00942テーブル不在存在
ORA-00918列名曖昧曖昧

10大原因

#原因対処
UPDATE JOINMERGE or サブクエリ
DELETE JOININ サブクエリ or EXISTS
INSERT ORDER BYORDER BY 削除
JDBC セミコロン削除
UNION 内 ORDER BY最後だけ
JOIN 構文混在ANSI 統一
CREATE VIEW ORDER BY削除
複数文実行PL/SQL ブロック
HAVING の後条件追加
サブクエリ後構文修正

Oracle と他 DB の差異

構文MySQLPGOracle
UPDATE JOIN
DELETE JOIN
INSERT ORDER BY
LIMIT
末尾 ; (JDBC)

5つの解決策

-- ① サブクエリ書き換え
UPDATE t1 SET col = (SELECT val FROM t2 WHERE t2.id = t1.id);

-- ② MERGE 文(推奨)
MERGE INTO t1 USING t2 ON (t1.id = t2.id)
WHEN MATCHED THEN UPDATE SET t1.col = t2.val;

-- ③ EXISTS
UPDATE t1 SET col = 'A' 
WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id);

-- ④ セミコロン削除(JDBC)
"SELECT * FROM t"

-- ⑤ ANSI JOIN 統一
SELECT * FROM a JOIN b ON ... LEFT JOIN c ON ...;

MERGE 文の重要性

-- Oracle の JOIN 更新の標準
MERGE INTO target t
USING source s ON (t.id = s.id)
WHEN MATCHED THEN UPDATE SET t.col = s.val
WHEN NOT MATCHED THEN INSERT (id, col) VALUES (s.id, s.val);

UPDATE JOIN の代替、より柔軟。

予防のポイント

1. 他 DB からの移行時は構文チェック
2. MERGE 文を積極的に使う
3. JDBC / Rails ではセミコロン不要
4. ANSI JOIN で統一((+)を使わない)
5. UPDATE / DELETE で JOIN は使わない
6. UNION の ORDER BY は最後のみ
7. IDE のフォーマッター活用
8. Oracle SQL Developer で構文チェック
9. CI/CD で SQL 検証
10. PL/SQL ブロックで複数文実行

これらの知識は、Oracle での MySQL/PostgreSQL からの移行・レガシー ETL・Rails / Java / Python アプリ開発・MERGE 文活用・ETL パイプライン・DBeaver/DataGrip 使用など、あらゆる場面で活用できます。本記事をブックマークしておけば、ORA-00933 に出会っても冷静に的確に対処できるようになります。


本記事は2026年6月時点の情報をもとに、Oracle Database 19c〜23ai での動作確認・公式ドキュメントに基づき作成しています。Oracle のバージョンにより挙動が異なる場合があるため、最新の情報は Oracle 公式ドキュメント(docs.oracle.com)もあわせてご確認ください。