S SCHEMA.BIZ

Last updated:

SQL to Diagram

Paste your CREATE TABLE statements and instantly generate an interactive entity-relationship diagram. Reads common PostgreSQL, MySQL, SQLite, and SQL Server syntax.

What this tool does

Most production databases were not built from a diagram. They started with a single table, grew an INSERT trigger, sprouted a junction table, absorbed a side project, and arrived at the present as a set of migration files that nobody has read end-to-end since 2019. SQL to Diagram is the inverse operation: paste the CREATE TABLE statements, in any order, with their inline and table-level constraints, and the tool reconstructs the entity-relationship graph that has been implicit in the SQL all along.

The parser handles the SQL most migration files contain: inline references on a column, foreign-key constraints declared at the table level, composite primary keys, parameterized data types like NUMERIC(10,2), dialect-specific identity declarations, backtick, double-quote, and bracket quoting, and embedded comments. It doesn't yet read schema-qualified names (public.users), named PRIMARY KEY or UNIQUE constraints, or foreign keys added later with ALTER TABLE, which pg_dump and SSMS scripts use. The layout engine (ELK) then arranges the tables to minimize edge crossings, so even a 30-table schema stays readable on a single page. None of your DDL leaves the browser; we record only that a diagram was generated, with the dialect and counts of tables, columns and foreign keys.

When to use SQL to Diagram

Onboarding to a database the previous owner never documented. Pull every CREATE TABLE from the migration history, paste them in, and you have a starting map of the domain in under a minute. Use the diagram to identify the central tables (the ones with the most incoming foreign keys) and start your reading from there.

Producing a diagram for a project handover. A README with an embedded ER diagram is more durable than a Confluence page that nobody updates. Paste the DDL and take a screenshot of the diagram; there's no image export yet.

Reverse-engineering a third-party schema. When you have credentials to a vendor's reporting database and need to build dashboards on top of it, dumping the schema as DDL (with schema prefixes such as public. removed) and visualizing the relationships is faster than clicking through every table in their admin UI.

Reviewing a migration pull request. A migration that adds two tables and a junction table is hard to evaluate from the SQL diff alone. Paste the proposed final state of the affected tables to see the new relationships rendered, and confirm the relationships match the design intent.

Walkthrough with a real example

Paste this DDL for a minimal blog backend with users, posts, tags (many-to-many), and threaded comments:

CREATE TABLE users (
  id            BIGSERIAL PRIMARY KEY,
  username      VARCHAR(64) NOT NULL UNIQUE,
  email         VARCHAR(255) NOT NULL UNIQUE,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE posts (
  id            BIGSERIAL PRIMARY KEY,
  author_id     BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title         VARCHAR(200) NOT NULL,
  body          TEXT NOT NULL,
  published_at  TIMESTAMPTZ
);

CREATE TABLE tags (
  id    SERIAL PRIMARY KEY,
  slug  VARCHAR(64) NOT NULL UNIQUE
);

CREATE TABLE post_tags (
  post_id  BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  tag_id   INTEGER NOT NULL REFERENCES tags(id) ON DELETE RESTRICT,
  PRIMARY KEY (post_id, tag_id)
);

CREATE TABLE comments (
  id           BIGSERIAL PRIMARY KEY,
  post_id      BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  author_id    BIGINT NOT NULL REFERENCES users(id),
  parent_id    BIGINT REFERENCES comments(id) ON DELETE CASCADE,
  body         TEXT NOT NULL,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

The parser identifies five tables. Five rectangular nodes appear: users, posts, tags, post_tags, and comments. Each row inside a node shows a column name with its type and badges for constraints — PK in amber for primary keys, FK in blue for foreign keys, NN for NOT NULL, UQ for UNIQUE. So users.id is marked PK; users.username and users.email get NN and UQ; posts.author_id is marked FK and an edge runs from it to users.id; both columns of post_tags are simultaneously PK and FK because the table uses a composite primary key over its two foreign keys.

Each edge is labeled with its foreign-key column, so the edge from posts to users reads author_id. The diagram doesn't show ON DELETE actions yet, so read those from the DDL: here, deleting a user deletes their posts, and deleting a post deletes its comments. The comments.parent_id column produces a self-referencing edge that loops from the table back to itself — the visual signature of a tree-of-comments structure. The post_tags table has an edge to both posts and tags, and both of its columns are PK and FK — the signature of a junction table for a many-to-many relationship.

Database schema concepts you should know

DDL versus DML. Data Definition Language statements (CREATE, ALTER, DROP, TRUNCATE) define the structure of the database. Data Manipulation Language statements (INSERT, UPDATE, DELETE, SELECT) operate on the data inside that structure. SQL to Diagram only reads CREATE TABLE statements; the data is irrelevant to the shape.

Surrogate versus natural primary keys. A surrogate key is a meaningless identifier (a BIGSERIAL, a UUID) generated by the database. A natural key is a real-world identifier (an email address, an SKU, an ISBN) that uniquely identifies the row in its domain. Surrogate keys are easier to evolve — you can change a customer's email without rewriting every foreign key — but natural keys make joins more readable. Most modern schemas use surrogate keys with a UNIQUE constraint on the natural key.

Foreign keys and referential actions. A foreign key constraint says "the value in this column must exist as a primary key in another table." The ON DELETE and ON UPDATE clauses say what to do when the referenced row is removed or its key changes: CASCADE propagates the change, RESTRICT blocks it, SET NULL nullifies the referencing column, SET DEFAULT falls back to the column default, and NO ACTION (the default) defers the check to commit time. Choosing the right action is a domain decision — CASCADE is correct for child rows that have no meaning without their parent (line items of a deleted order) and dangerous everywhere else.

Junction tables and many-to-many relationships. A junction table (also called an associative entity or link table) materializes a many-to-many relationship as two foreign keys. The convention is to make the composite of those two foreign keys the primary key of the junction table itself. Adding a payload column (a created_at timestamp, a role label, an ordering integer) turns it from a pure association into a relationship-with-attributes, which is a common evolution.

Constraints, named and anonymous. Every constraint can be given a name with the CONSTRAINT keyword. Named constraints are easier to drop and recreate during migrations and produce more informative error messages when violated. Anonymous constraints work but get auto-generated names like posts_author_id_fkey that change between database versions and dialects, which makes migrations across environments harder to reason about.

Data type selection. Choose the narrowest type that fits: SMALLINT for small enums, INTEGER for most counts, BIGINT for surrogate keys you expect to outlive 2 billion rows. Use VARCHAR with a real maximum length only when the length is a domain constraint (an ISO country code is two characters); use TEXT otherwise. NUMERIC for money. TIMESTAMPTZ rather than TIMESTAMP for anything time-zone-aware. Type choices are visible in the diagram and influence both storage cost and JOIN performance.

Common mistakes

Using TEXT for every string column. TEXT works in PostgreSQL and SQLite but loses domain meaning. A column intentionally bounded to 64 characters carries information that downstream code can rely on; an unbounded TEXT invites a future row that breaks an HTML form's max-length attribute. Pick a real maximum when one exists.

Forgetting indexes on foreign-key columns. Most databases automatically index the primary key but not the columns that reference it. The first time you delete a parent row, the database has to scan the entire child table to enforce the constraint. Always index FK columns explicitly.

Circular foreign-key references with NOT NULL on both sides. Two tables that each require a row in the other create a chicken-and-egg problem at insert time. Either make one side nullable and populate it after both rows exist, or use deferrable constraints with a single transaction.

ON DELETE CASCADE on tables you cannot afford to lose. CASCADE propagates silently. A misclick that deletes a tenant row can take the tenant's entire dataset with it before anyone has time to react. Reserve CASCADE for child-of-parent relationships where the child is meaningless without the parent, and use RESTRICT or NO ACTION elsewhere.

Reserved words as table or column names.

-- Will fail in PostgreSQL: 'user' is a reserved keyword
CREATE TABLE user (
  id INT PRIMARY KEY,
  name TEXT
);

-- Quote it, or pick a less ambiguous name
CREATE TABLE "user" (
  id INT PRIMARY KEY,
  name TEXT
);

-- Better: rename to avoid the dance entirely
CREATE TABLE app_user (
  id INT PRIMARY KEY,
  name TEXT
);

Quoting works but turns every reference to the table into a dance with quotes for the rest of the project. Pick a name that does not collide with the SQL grammar to begin with.

FAQ

Does the parser handle partitioned tables and table inheritance?

Not yet. A partition declared with PARTITION OF shows up as a table with no columns, and INHERITS is ignored, so neither relationship is drawn. Foreign keys between regular tables are unaffected.

What does it do with views, stored procedures, triggers, and indexes?

The parser reads CREATE TABLE statements and skips everything else, so views aren't drawn and indexes aren't shown yet. Functions, triggers, and stored procedures are skipped too, and the tables around them still parse.

How large a schema can this handle in the browser?

The parser and ELK layout engine handle several hundred tables comfortably. Around a thousand tables you will start to see the layout step take a few seconds and pan-zoom slow down on lower-end machines. For databases that large the diagram is usually only useful as a per-domain subset anyway, so paste in just the tables for one bounded context.

Can I paste a partial schema with foreign keys to tables I have not included?

Yes. Foreign keys that point at tables you haven't included aren't drawn; a note on the diagram lists each one (for example posts.user_id → users). That lets you focus on one slice of a large schema and still see which links leave it.

Which SQL dialects does the auto-detection actually distinguish?

It checks for a few keywords, in order: AUTO_INCREMENT means MySQL; SERIAL means PostgreSQL; AUTOINCREMENT or INTEGER PRIMARY KEY means SQLite; IDENTITY( or NVARCHAR means SQL Server; anything else is treated as PostgreSQL. You can override it from the dropdown. The setting picks the dialect for Copy SQL; the diagram is parsed the same way either way.

Related tools and guides

  • Visual Schema Designer — design schemas on a visual canvas instead of writing DDL by hand. Useful when you are starting from a whiteboard sketch rather than an existing database.
  • SQL Query Visualizer — once you have the schema diagrammed, paste a SELECT query in here to highlight which tables and columns the query touches.
  • Schema Diff — compare two snapshots of a schema and produce the migration SQL that transforms one into the other, for you to review before running it.
  • Database Design Fundamentals — covers normalization, key strategies, and indexing patterns that produce the kind of schemas that diagram cleanly.
  • Complete Guide to JSON Schema — for when your database stores JSON columns, the schema for those columns is its own modeling problem.