Chris Collins

Tools · infra

Schema diff

Paste two SQL schemas (PostgreSQL or MySQL, hand-written or from pg_dump / mysqldump) and get the migration from one to the other as SQL, Flyway versioned files or a Liquibase changelog (YAML or XML), with warnings for the changes that rewrite or lock tables — by database version — and an ER diagram of what changed.

Added

Works with

How to use it

You have the schema the database has now and the one you want. This page works out the ALTER statements between them, puts them in an order that runs (views dropped before the columns they read, foreign keys after the keys they point at, enum values before they are used), and tells you which of them will hurt on a big table in production.

How to use it

  1. Paste the current schema into A and the wanted one into B — or Open file. Both can be hand-written DDL or a schema-only dump (pg_dump --schema-only, mysqldump --no-data). The dialect is detected; choose it if the guess is wrong.

  2. Pick the server version. It changes the advice: adding a column with a default rewrites the table before PostgreSQL 11 and not after, SET NOT NULL can skip its scan from 12, ALTER TYPE … ADD VALUE can run in a transaction from 12, a generated column’s expression can be changed from 17, and MySQL 8.0 before 8.0.29 adds columns instantly only at the end.

  3. Online patterns (PostgreSQL, on by default) writes the migration the way you would for a busy table: CREATE INDEX CONCURRENTLY; foreign keys and checks added NOT VALID then validated (which does not block writes); NOT NULL through a validated CHECK (col IS NOT NULL); unique and primary keys built as a concurrent unique index and attached with USING INDEX. Turn it off for the plain version. lock_timeout is put at the top so a migration that cannot get its lock fails quickly instead of queueing every other query behind it.

  4. Read the warnings. Risks lose data or rewrite a table under an exclusive lock; warnings block writes or scan the table; notes explain why something that looks risky is not.

  5. Take the migration as plain SQL, as Flyway files (V2__description.sql; statements that cannot run in a transaction get their own versioned file, because Flyway runs each file in one transaction), or as a Liquibase changelog in YAML or XML (native change types where Liquibase has one — dropColumn, dropTable, addNotNullConstraint, dropDefaultValue, addForeignKeyConstraint with validate: false and so on — and sql changes for the rest; concurrent index builds get runInTransaction: false). Set the version, description and author above the output.

  6. ER diagram draws B with what changed marked, or A or B alone. Download it as SVG, copy it as Mermaid, or Open in Diagrams to edit and export it in the Diagrams tool.

  7. With PostgreSQL, Try it in the Postgres playground opens the Postgres playground with schema A and the migration ready to run in real PostgreSQL 18 in your browser.

What it compares

Tables; columns with their type, NOT NULL, default, identity / serial / AUTO_INCREMENT, generated expression (and, in MySQL, ON UPDATE and comments); primary keys, unique, check and foreign-key constraints (with ON DELETE / ON UPDATE); indexes, including unique, partial (WHERE), expression, INCLUDE and non-btree ones; PostgreSQL enum types; views and materialized views. Unnamed constraints and indexes get the names PostgreSQL would give them (orders_customer_id_fkey, orders_customer_id_idx) or MySQL would (orders_ibfk_1), so a hand-written script and a dump of the same database compare equal. Types are compared by meaning (int4 = integer, character varying(20) = varchar(20)), defaults and conditions without casts and redundant parentheses ('new'::order_status = 'new'), and view bodies after removing what pg_dump adds when it re-prints them. Functions, triggers, sequences' own options, grants, partitions and comments are not compared; the statements skipped are listed under the output.

Renames cannot be told apart from a drop and an add, so the migration drops and adds — and warns when a dropped and an added column (or table) look like the same thing, with the RENAME to use instead.

The warnings, in short

  • Table rewrites (everything blocked until done): ALTER COLUMN … TYPE unless the types are binary-compatible (a longer varchar, varchar → text, more numeric precision); adding a column with a volatile default (gen_random_uuid(), random(), clock_timestamp(), nextval), an identity or a stored generated column; any default before PostgreSQL 11; recreating an enum to remove or reorder values. In MySQL: MODIFY with a new type (except enum values added at the end, and varchar growth that keeps its length-prefix size).

  • Locks and scans: CREATE INDEX without CONCURRENTLY blocks writes; ADD FOREIGN KEY, ADD CHECK and SET NOT NULL scan the table while holding a lock; DROP INDEX waits for every query on the table.

  • Data loss: dropped tables and columns. Deploy code that no longer uses a column before dropping it.

  • Dependencies: views that read a column being dropped or retyped are dropped and recreated around the change; foreign keys without an index on the referencing side.

How it was checked

The PostgreSQL migrations are run against PostgreSQL itself in the tests: schema A is created in PGlite (PostgreSQL 18) with some rows, the migration runs statement by statement (the CONCURRENTLY ones outside a transaction), and the catalog — columns, types, nullability, defaults, identity, constraints, indexes, enums, views — must come out identical to a database built from B, with the rows still there. That is done for the examples in both directions, with and without online patterns, and from pg_dump output. The MySQL examples were applied to MySQL 8.4 and mysqldump of the result compared with B’s.

Related: Explain plan (what the new index does to your queries), Postgres playground, Diagrams, SQL formatter, DynamoDB and MongoDB playgrounds.

Browser support

Any current browser, on a phone too.

Privacy

Your schemas are parsed and compared in this page and never sent anywhere. They are kept in this browser’s local storage; a share link carries them only if you make one. Open in Diagrams and Try it in the Postgres playground hand them to those pages on this site, in your browser.

Runs entirely in your browser; nothing you type leaves this page.