every Tech Blog

株式会社エブリーのTech Blogです。

なぜ MySQL では DDL をロールバックできず PostgreSQL ではできるのか

こんにちは、デリッシュキッチン開発部の鈴木です。

概要

Data Definition Language(DDL、データ定義言語)、つまり CREATE TABLEALTER TABLE でスキーマを変更する操作を、トランザクションの中で実行してあとからロールバックできるかは、データベースによって違います。MySQL ではロールバックできず、PostgreSQL ではロールバックできます。

ロールバックは、1 つのトランザクションの中で行った変更を、記録しておいた変更前の状態に戻す操作です。だからロールバックするには、その操作が 1 つのトランザクションに収まっていること、そしてその変更が記録されていることの 2 つが必要です。前者はどのデータベースでも同じで、1 つのトランザクションに収まらない操作(PostgreSQL の CREATE INDEX CONCURRENTLY など)は、そもそもロールバックの対象になりません。データベースによって分かれるのは、後者です。

ロールバックできるか、つまり変更が記録されるかは、メタデータの置き場所で決まります。変更前の状態は、ストレージエンジンの内部に記録されます。そのため、スキーマの定義情報であるメタデータも、その内部になければロールバックできません。古い MySQL はメタデータをエンジンの外側のファイルに置いていたため、ロールバックできません。PostgreSQL はメタデータを通常のテーブルとして内部で管理するため、ロールバックできます。MySQL 8.0 はメタデータを内部に移しましたが、互換性のために暗黙的なコミットを残したため、ロールバックできません。MySQL と PostgreSQL の差は、この置き場所の違いから生まれます。

図1:DDL をロールバックできるかの全体像。1 つのトランザクションに収まるか(DB 共通)→ 収まればロールバックできるか(DB で違う)の順に決まる。

前提:スキーマ変更をロールバックできるかは、データベースで違う

CREATE TABLEALTER TABLE といった DDL を、トランザクションの中で実行してあとからロールバックできるかは、データベースによって違います。

  • MySQL:ロールバックできません。DDL には暗黙的なコミットが伴い、ALTER TABLE を実行した時点でそれまでの変更が確定します。あとから ROLLBACK しても戻りません。
  • PostgreSQL:ロールバックできます。BEGIN; ALTER TABLE ...; ROLLBACK; で変更前の状態に戻ります。

本記事はこの差を前提とし、この違いを生んでいるものは何かを扱います。

DDL をロールバックできるかは、収まるか → ロールバックできるか の順で決まる

ロールバックは、1 つのトランザクションの中で行った変更を、記録しておいた変更前の状態に戻す操作です。だからロールバックするには、その操作が 1 つのトランザクションに収まっていること、そしてその変更が記録されていること、の 2 つが必要です。前者を満たさなければ後者には進めないので、まず収まるか、次にロールバックできるか、の順に確かめれば十分です。

  • 1 つのトランザクションに収まるか。 実行のしかたの問題で、データベースによらず一律に効きます。
  • 収まるとして、その変更をロールバックできるか。 メタデータ(テーブルや列などスキーマの定義情報)の置き場所で決まり、データベースによって変わります。

ロールバックできるかを問えるのは、1 つのトランザクションに収まる操作だけです。前提で見た MySQL と PostgreSQL の差は、ロールバックできるかで決まります。

1 つのトランザクションに収まるか(DB によらず一律)

ほとんどの DDL は 1 つのトランザクションに収まります。ただし、収まらない操作もあります。これはデータベースによりません。

代表例は PostgreSQL の CREATE INDEX CONCURRENTLY です。通常の CREATE INDEX はテーブルをロックして一気に索引を作ります。一方 CONCURRENTLY は、書き込みを止めずに索引を作るため、複数の段階に分けて実行します。各段階はそれぞれ別のトランザクションになるので、全体を 1 つのトランザクションブロックに収められません。

DDL をロールバックできる PostgreSQL でも、CREATE INDEX CONCURRENTLY はトランザクションの中では実行できません。ロールバックできるかどうか以前に、1 つのトランザクションに入らないからです。この操作はここで止まり、ロールバックできるかはそもそも問えません。

収まるとして、ロールバックできるか(ここで DB が分かれる)

1 つのトランザクションに収まる操作、つまり普通の DDL について、ロールバックできるかを考えます。これがデータベースによって違います。

行の更新や削除といった Data Manipulation Language(DML、データ操作言語)をロールバックできるのは、MVCC(変更前の古い行を残しておく仕組み)が古い行を保持しているからです。この記録の仕組みはストレージエンジンの内部にあります。だから DDL をロールバックするには、スキーマを記述するメタデータも、ストレージエンジンの内部、つまりこの仕組みの管理下になければなりません。メタデータがこの管理下にあるかどうかが、データベースごとの差を生みます。

  • 古い MySQL(5.7 以前):メタデータは .frm というファイルにあり、ストレージエンジンの外側にありました。ロールバックの仕組みの管理下にないので、トランザクションに参加できません。だから暗黙的にコミットされ、ロールバックできません。
  • MySQL 8.0:メタデータを InnoDB の内側に取り込み、データディクショナリとして管理するようにしました。これで DDL がアトミックになりました(途中で中断しても中途半端なスキーマが残りません)。ただし、この変更が狙ったのはクラッシュ時の安全性であって、ユーザーがロールバックできるようにすることではありません。従来の挙動との互換性から、暗黙的なコミットはそのまま残されました。アトミック(壊れない)と、ユーザーが好きなときにロールバックできることは別物です。 内側に入れても、まだロールバックできません。
  • PostgreSQL:スキーマ情報をシステムカタログ(pg_class など)として、通常のテーブルと同じく MVCC で管理します。CREATE TABLE はシステムカタログに 1 行追加するだけの操作になります。行の追加をロールバックするのと同じ仕組みがそのまま効くので、ロールバックできます。

3 つは別々のケースではなく、メタデータの置き場所が、外側(古い MySQL)から、内側だが暗黙コミット(MySQL 8.0)、内側で完全管理(PostgreSQL)へと段階的に変わっているだけです。前提で見たとおり、MySQL はロールバックできず、PostgreSQL はロールバックできます。これは、このメタデータの置き場所による結果です。

図2(詳細):DDL をロールバックできるかは、1 つのトランザクションに収まるか(DB 共通)→ 収まるとしてロールバックできるか(メタデータの置き場所・DB で違う)の順に決まる。

結論:DDL かどうかではなく、収まるか → ロールバックできるか で見る

DDL をロールバックできるかは、収まるか、次にロールバックできるか、を順に確かめれば分かります。DDL かどうかで一律に判断すると、PostgreSQL ではロールバックできることや、PostgreSQL でも CREATE INDEX CONCURRENTLY はロールバックできないことを見落とします。この順で見れば、そうした例外も正しく判断できます。

  • 1 つのトランザクションに収まるか。 収まらなければ(CREATE INDEX CONCURRENTLY など)、どのデータベースでもロールバックできません。
  • 収まるとして、メタデータがロールバックの仕組みの管理下にあるか。 使っているデータベースによって、ロールバックできるかが決まります。

最後に、具体的な落とし穴を一つ挙げます。MySQL では DDL と DML を同じトランザクションに混ぜてはいけません。これはロールバックできるか(メタデータの置き場所)に関わる問題です。MySQL では DDL のメタデータがロールバックの仕組みの外側にあるため、ALTER の暗黙的なコミットによって、直前の INSERT などが意図せず確定してしまいます。この挙動は、DDL がロールバックの仕組みの外側にあることから説明できます。

まとめ

今回は、なぜ MySQL では DDL をロールバックできず PostgreSQL ではできるのかについて考えました。MySQL は DDL をロールバックできない、PostgreSQL はできるということを覚えてもいいとは思うのですが、それではツールの使い方に閉じてしまいます。なぜそのような設計になっているのかを理解することで、MySQL や PostgreSQL という個別の知識にとどまらず、初めて触れる操作やデータベースでも、1 つのトランザクションに収まるか、そしてメタデータがロールバックの仕組みの管理下にあるか、という同じ見方で自分で判断できるようになります。実際、同じ PostgreSQL でも CREATE INDEX CONCURRENTLY はロールバックできない、といった例外にも自分で気づけるでしょう。