Skip to content

Instantly share code, notes, and snippets.

@qnighy
Last active Aug 1, 2022
Embed
What would you like to do?
マイグレーションしないRDBMS

マイグレーションしないRDBMSが欲しい!

課題

PostgreSQLなどの既存のRDBMSはスキーマを持つ。スキーマがあることは良いことだが、このスキーマのライフサイクルはアプリケーションコードのライフサイクルと乖離しがちで、結果として以下のような問題が発生する。

  • 特に自動化をしない場合はマイグレーションをデプロイとは別に行う必要が発生する。これにより、
    • シンプルに作業が面倒。
    • 承認フローが追加で必要になる。または、デプロイはレビューの管理下に置かれているのにマイグレーション側が適切に管理されないなどのミスマッチが起きる。
    • マイグレーション忘れ、マイグレーションのリバート忘れのリスクがある。
  • 異なるバージョンのアプリケーションは同時に存在できるがスキーマは同時に存在できない。これにより、
    • ある種のスキーマ変更はローリングデプロイ環境下では実質的に実行できない。 (テーブル名の変更など)
    • 開発用にDBを共有している場合、スキーマの変更が互いに衝突するリスクがある。
  • これらの問題から開発者にはマイグレーションをできるだけ行わないようにする圧力が働き、誤った設計や最適でない設計につながる。

既存の解決方法

ridgepolesqldefなどを使いマイグレーションそのものではなく収束先のスキーマを正とした上で、デプロイ前にマイグレーションを自動的に行うようにするとある程度の問題は解消される。

本提案の内容

RDBMS側の実装を変えて、以下のような挙動にする。

  • 1つのデータベースは(同じデータに対する)複数のスキーマを同時に持つことができる。異なるスキーマは同じデータの異なるビューとして振る舞う。
  • アプリケーションもスキーマ情報を持っておき、接続時にどのスキーマを使うかを指定する。
  • 使う予定のあるスキーマはあらかじめデータベースに登録 (デプロイ) しておく。逆に、必要なくなったスキーマはDBから削除 (アンデプロイ) できる。
  • スキーマ間には互換性が定義されており、非互換なスキーマは同時に登録することはできない。この場合は競合するスキーマをアンデプロイする必要がある。
  • 実際のストレージの構成は、現在デプロイされているスキーマに基づいてDBMSが自動的に決定する。デプロイ・アンデプロイ時に、通常のRDBMSでいうところのマイグレーション操作が発生する可能性がある。
    • たとえば、1つ以上のスキーマがインデックスを要求していればインデックスが張られるので、デプロイ時にインデックスが張られたり、アンデプロイ時にインデックスが削除されたりする。

本提案の「デプロイ」は「マイグレーション」と似ている部分も多いが、既に動いているアプリケーションプロセスとの干渉が最小限に抑えられている。そのため、現在広く使われているマイグレーションの仕組みよりも積極的に行うことができるようになると期待できる。

oidの露出

テーブルやカラムにはUUIDを付与して識別し、各スキーマでUUIDと人間可読な名前との対応を記述する。 (PostgreSQLのoidの概念に近いが、アプリケーション側に明示的に露出する)

スキーマにUUIDがつくというのは以下のようなイメージ。

CREATE TABLE users UUID "04b365ed-f1e6-49cc-9d7f-44a7731eb151" (
  id BIGINT PRIMARY KEY DEFAULT nextval('users_id_seq') UUID "f09e0fbb-f56f-4e84-bd89-659c9a7647db"
  name TEXT NOT NULL UUID "a3260f0b-1c47-4c92-9b9d-7dfda88627ff"
);

これにより以下の利点が得られる。

  • テーブル・カラムのリネーム時に、リネーム前とリネーム後のアプリケーションが共存できる。
  • スキーマを複数人が同時に変更したとき、カラムが衝突しない。
    • ただし、アプリケーションコードをマージしようとしたときには衝突が起きる。

write-only制約

読み取り時は保証されないが、書き込み時はチェックされる制約を記述できるようにする。PostgreSQLのNOT VALIDに相当。

これはスキーマの互換性を保証しながら漸進的にスキーマを変更するために必要。つまり、

  • スキーマ1: 制約なし
  • スキーマ2: CHECK NOT VALID 制約
  • スキーマ3: CHECK 制約

があったとき、スキーマ1とスキーマ3は共存できないため、漸進的に移行できない。スキーマ2はいずれとも共存できるので、以下の移行戦略が可能になる。

  • 初期状態: スキーマ1がデプロイされている。スキーマ1を使うアプリケーションがデプロイされている。
  • スキーマ2をデプロイする。
  • スキーマ2を使うアプリケーションをローリングデプロイする。
  • スキーマ1をアンデプロイする。
  • スキーマ3をデプロイする。
  • スキーマ3を使うアプリケーションをローリングデプロイする。
  • スキーマ2をアンデプロイする。

使わなくなったテーブル・カラムの削除

使われなくなったテーブル・カラム等は、それを参照しているスキーマが全てアンデプロイされた時点で削除する。これにより以下の要件をおおよそ両立できる。

  • 削除は明示的に行いたい。
    • 自動的に削除されてほしくはない。
    • 逆に、アプリケーションコードから記述が消えたにもかかわらず、DBからは削除されずに残り続けるというのも望ましくない。
  • 削除も収束処理の一部として行いたい。 (削除マイグレーションという操作があってはいけない)
  • 削除を実行するためのスキーマエントリを書きたくない。
    • この方法だと、単にアプリケーションコード側の変更をrevertするだけではカラムの追加がrevertされないという非対称性が生じてしまう。
  • スキーマが同時に変更されたとき、追加したつもりのカラムが削除されてほしくない。

開発時にカラムを増やしたいときは、上の要件に反して自動的に削除されてほしい場合がある。この場合、一定時間でアンデプロイされる短命なスキーマを使うなどの方法が考えられる。

コンソールからの利用

コンソールから入るときは、デプロイされているスキーマのいずれかを (自動的にまたは手動で) 選択して、そのスキーマのもとでクエリやコマンドを発行することになる。

デプロイの設定例

trunk (master, main) に入ったアプリケーションコードがそのままデプロイされるという環境では、本DBMSのスキーマに関しては以下のようなデプロイフローが考えられる。

  • trunkへのマージ前に、実DBにスキーマの互換性を問い合わせる。
    • 互換でない場合は、古いスキーマのアンデプロイ操作を手動で行う必要がある。 (これはいい体験ではないが、データの整合性を保護するためにどうしても必要)
  • マージ後のCDでスキーマをデプロイする。
  • 使わなくなったスキーマのアンデプロイは、要件に応じて手動で行ったり、あまりにも古いものは時間経過でアンデプロイするように設定したりする。

問題点

色々ありそう。

  • 既存のO/Rマッパーやマイグレーションヘルパーの仕組みに乗れない。
  • 複雑。

反響

結局 RDBMS であってもデータの物理表現から自由になれていないのが悪いということなので、パフォーマンスペナルティを無視すれば VIEW などを駆使して実現できそうな気もするが、本当の困りごとはそこではない気もする

( https://twitter.com/KOBA789/status/1553079369312063494 )

PostgreSQLの例しか知らないけど、書き込みできるViewも作れるからViewだけでもそれなりには近いことはできそうですね。ただ、「登録された全てのスキーマが互いに互換性であることをチェックする機構」と「登録されたスキーマの要求するテーブル・カラム・インデックスの合併に対して、ストレージ側の表現を収束させていく機構」まで合わせることで必要な開発体験が手に入るのかなと思っています。

PlanetScaleのBranching機能がさらにめちゃくちゃ発展すればあるいは…?同じアプリが新旧ブランチを同時に利用して最終的に問題なければデータもマージされる世界 (?)

https://planetscale.com/docs/concepts/branching

( https://twitter.com/tmokmss/status/1553213386694881280 )

おお! これは説明を読む限りでは本提案とすごく近そうですね。細かいところは色々違うかもしれないけど、少なくとも解決しようとしている問題は同じといっていい気がします。情報ありがとうございます!

reader/write schema や schema のバージョニングは Avro を連想させるな。既存の VIEW では reader schema の劣化版しかエミュれないので、この路線で進めてほしい。

( https://twitter.com/wint7/status/1553383180144889856 )

そうですよねー。データシリアライゼーションフォーマットとの類似については自分自身でも指摘しようと思いつつ書きそびれていました。 (比較対象としてはprotobufを想定していた)

内製化とCI/CDの進化で開発スピードが上がってRDBMSの変更が大変なのがボトルネックになるのわかるが、それは受容するべき、と思ってるのよな。スキーマレスやこのアイデアは面白いけど、後のデータ分析きつくない?

( https://b.hatena.ne.jp/entry/4723088525943487522/comment/khtno73 )

大変だったり手間だったりすること自体は受容してもいいと思うんですが、その結果としてアプリケーションの設計が間違った方向に進みやすいのであればやはりソフトウェアの力で解決するのが望ましいのではないかなと思いました。

データ分析は広い意味ではアプリケーションの一種ともみなせるので、基本的には以下のどちらかの方法で組み込まれることになると思います。

  • データ分析側にもスキーマを組み込んでおき、スキーマのバージョン管理システムの一部に取り込む。アプリケーション側の成長にあわせてデータ分析側もスキーマを明示的に更新していく。
  • コンソールと同様、アプリケーションの最新スキーマを参照するだけで自身はスキーマをpinしない。アプリケーション側が成長したときにデータ分析側も壊れることがある。 (これは既存の仕組みでも同じことが起きうる)

少なくとも後者で考えると、既存の仕組みと比べて何かが特別に難しくなることはないと考えています。むしろ、前者のアプローチでいけばアプリケーションのデプロイによってデータ分析基盤が意図せず壊れるような事故を防ぐことができるので、データ分析の観点からもメリットはあるのではないかと思います。

それで一時期MongoとかのNoSQLとかがかなり流行ったよね。RDBMSは時代遅れって叩かれて。でも結局揺り戻しが来たってことは何か理由があるわけで。入れ物(スキーマ)に柔軟性持たせすぎると収拾付かなくなるんだよなあ。

( https://b.hatena.ne.jp/entry/4723088525943487522/comment/findup )

悪名高いMongoDBは複数のスキーマを同時に持つことをjsonで表現しようとしたといえなくもない/パーティションなしで数億レコードを持つDBは個人的にはその時点で設計ミスと断じた

( https://b.hatena.ne.jp/entry/4723088525943487522/comment/Wafer )

本質的にはMongoDBのようなスキーマレスのNoSQLとアプリケーションレイヤーでのスキーマ管理の組み合わせと同質に見える。

( https://b.hatena.ne.jp/entry/4723088525943487522/comment/snowcrush )

「スキーマレスDB + ミドルウェアじゃないか」という指摘は半分くらいはごもっともなんですが、その「スキーマレスDB + ミドルウェア」というパッケージが外から見て何を守ってくれるかが本提案の関心です。もしそういうミドルウェアで本提案で書いたような丁寧なスキーマバージョニングができるものがあればぜひ教えてもらえると嬉しいです。

一方で、本稿で書いたものの中身が「スキーマレスDB + ミドルウェア」という構成でなければいけないということもなくて、中でちゃんとスキーマに合わせてストレージの構成を最適化する余地はあると考えています。 (PostgreSQLをバックエンドにしてスキーマバージョニングをするプロキシみたいな実現方法もありえる) なので、本提案は必ずしもパフォーマンスを犠牲にするものではないのかなと思います。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment