A schema you can finally see

Every inherited database comes with the same archaeology kit: a migrations folder, half a wiki page, and the phone number of the one engineer who still remembers why the `user_events_backup_v2` table exists. You can reconstruct the relationships by reading forty CREATE TABLE statements and holding the graph in your head, and that works for about five tables. Somewhere around table twelve, the mental model starts lying to you, and the bug you are hunting sits precisely on the edge you forgot.

An entity-relationship diagram is the antidote, and the reason nobody has one is that drawing it by hand is miserable — an hour of dragging boxes in a diagram tool, aligning relationship lines, and updating it exactly once before it rots. The generator here takes the other path: the DDL already encodes the structure, so paste the CREATE TABLE statements and the diagram draws itself. Foreign keys become relationship lines with correct cardinality, primary keys and uniques get marked, and the whole thing exports as SVG, PNG, or Mermaid code you can drop into documentation.

The update problem also disappears. When the schema changes, you paste the fresh DDL and regenerate; a diagram that costs four seconds to rebuild is a diagram that stays current. That property, more than any visual flourish, is what makes ER diagrams actually get used instead of just getting made.

From DDL text to boxes and lines

The tool occupies the space between a full database client and a drawing program. It does not connect to any database — it reads the text you paste, the same text a migration file or a `pg_dump --schema-only` would hand you. That design choice does two jobs at once: it keeps the tool usable for students writing DDL by hand for homework, and it keeps production schemas completely offline.

On the output side, each table becomes an entity box with its columns listed in declaration order. Annotations appear next to column names: PK for primary-key columns, FK for foreign keys, UK for uniques. Relationships render as crow's-foot edges between the boxes, with the fork on the many side, and each edge is labeled with the foreign-key column that produced it. Composite primary keys — common in junction tables — show all participating columns with the PK mark.

Two rendering targets are available. The default is an interactive SVG you can nudge around before exporting, which matters because auto-layout gets you 90% of the way and the last 10% is aesthetic judgment. The second is Mermaid source code, which turns the diagram into plain text you can commit, diff, and version alongside the schema it describes.

Generating your first diagram

  1. Gather the DDL

    Paste CREATE TABLE statements directly, load a .sql file, or extract them from a migration file. Schema dumps from pg_dump, MySQL Workbench, or SQLite's .schema command all work as-is — the parser skips INSERTs, indexes on non-key columns, and other statements it does not need.

  2. Parse and review

    The parser reports each table it found, its column count, and every constraint. Errors point at the offending statement rather than the whole paste, so one malformed table does not cost you the other thirty.

  3. Check the relationship list

    Every foreign key found is listed as an edge with its source column and target table. Optional name-based inference — customer_id pointing at customers — can be switched on for schemas written without FK constraints; those edges render dashed to keep guesses visually distinct from facts.

  4. Adjust the layout

    Drag entities where you want them; edges re-route as orthogonal lines. The auto-layout places parent tables above their children, which is usually right and occasionally needs a nudge.

  5. Export

    Download the SVG for documentation, the PNG for slides, or the Mermaid code for embedding in markdown. Exports capture the layout exactly as arranged on screen.

End to end, a fifty-table schema goes from paste to exported SVG in well under a minute, most of which is you deciding whether the layout looks sane.

How the parser reads your CREATE TABLE statements

Parsing DDL sounds formal, and the formal grammar in the PostgreSQL reference is a document of considerable length. In practice a tolerant parser can be built around one observation: CREATE TABLE statements have a strictly bracketed shape. The table name sits between `CREATE TABLE` and the opening parenthesis, and everything between that parenthesis and its matching partner is a comma-separated list of column definitions and table constraints.

The tokenizer walks the statement character by character with a small stack of states:

strip comments:          -- line comments and /* block comments */
split statements:       on semicolons outside quotes
balance parentheses:    quoted strings never nest; parens must
split body on commas:   only at paren depth zero
for each fragment:
    starts with CONSTRAINT / PRIMARY KEY /
    FOREIGN KEY / UNIQUE / CHECK -> table constraint
    otherwise                      -> column definition
for a column definition:
    first token          -> column name
    next token(s)        -> type (with size/precision)
    remaining tokens     -> flags: NOT NULL, DEFAULT,
                             REFERENCES t(c), AUTO_INCREMENT,
                             GENERATED ..., UNIQUE

The parenthesis-balance rule is what makes the whole thing work. A DEFAULT like `'Smith, John'` contains a comma inside quotes, and a type like `DECIMAL(10, 2)` contains one inside parentheses; splitting on commas only at depth zero, with quote awareness, handles both without drama. The same discipline applies to spotting REFERENCES clauses whether they appear inline on a column (`customer_id BIGINT NOT NULL REFERENCES customers(id)`) or as a table-level constraint (`FOREIGN KEY (customer_id) REFERENCES customers(id)`).

Both spellings produce the same edge in the model, because they mean the same thing in the schema. That normalization — not the parsing mechanics — is where most hand-rolled DDL readers fall over, since real-world schemas freely mix the two styles within a single file.

Dialect extras get absorbed rather than rejected. MySQL's `AUTO_INCREMENT` and `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4` trailers, SQLite's `INTEGER PRIMARY KEY` shorthand for rowids, Postgres `GENERATED ALWAYS AS IDENTITY`, and SQL Server's bracketed identifiers all pass through the flag scanner as things the model either marks or ignores. The parser's contract is narrow on purpose: it promises to extract tables, columns, keys, and references accurately, and to stay silent about everything else a real grammar would know. That narrowness is what lets one parser survive contact with five dialects instead of shattering into five special cases.

Turning foreign keys into relationship lines

An edge between two entities is easy; the crow's-foot symbols on its ends carry the actual information. The generator reads cardinality straight from the constraints, using the rules that relational theory prescribes:

Constraint evidence in the DDLRendered notationReading it aloud
FK column, no UNIQUE, NOT NULLone ||--o{ (one to zero-or-more)"each customer places zero or more orders; every order has exactly one customer"
FK column, nullableone --o| (parent optional)"an order belongs to zero or one customer — orphan rows allowed"
FK column with UNIQUE constraintone --|| one (one-to-one)"each user has at most one profile row"
FK inside a composite PRIMARY KEYone --|{ (one-or-more)"an order contains one or more line items"
Self-referencing FK (manager_id → same table)loop edge back to the entity"employees report to employees"
Constraint shapes to crow's-foot notation, with the sentence each one encodes.

The one-to-many row deserves the extra look, because it is the default case and the one people misread most often. The fork goes on the child side — the table holding the foreign key. `orders.customer_id` points at `customers.id`, so customers sits at the single-stripe end and orders wears the fork. Draw it backwards and the diagram quietly asserts that each order has many customers, which is a schema you would definitely remember debugging.

SQLite's documentation on foreign keys states the underlying rule cleanly: the child table is the one containing the REFERENCES clause, and the parent is the table it points at. The generator adopts that terminology in its relationship list for the same reason — saying "child table" and "parent table" kills a whole class of conversation about which end of the line is which.

A four-table schema, end to end

Here is a small order-processing schema, written the way a Postgres migration might actually write it — identity columns, a natural-key product table, and a junction table with a composite primary key. Paste this into the generator:

CREATE TABLE customers (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  full_name VARCHAR(120) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE orders (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES customers(id),
  status VARCHAR(20) NOT NULL DEFAULT 'new',
  total_cents INTEGER NOT NULL,
  placed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE products (
  sku VARCHAR(40) PRIMARY KEY,
  title VARCHAR(200) NOT NULL,
  price_cents INTEGER NOT NULL
);

CREATE TABLE order_items (
  order_id BIGINT NOT NULL REFERENCES orders(id),
  product_sku VARCHAR(40) NOT NULL REFERENCES products(sku),
  quantity SMALLINT NOT NULL CHECK (quantity > 0),
  PRIMARY KEY (order_id, product_sku)
);

The parser finds four entities, three foreign keys, four primary keys (three single-column, one composite), and one unique constraint. It renders the following Mermaid diagram, and the SVG view is the same graph with nicer geometry:

erDiagram
  CUSTOMERS ||--o{ ORDERS : places
  ORDERS ||--|{ ORDER_ITEMS : contains
  PRODUCTS ||--o{ ORDER_ITEMS : "sold as"
  CUSTOMERS {
    bigint id PK
    varchar email UK
    varchar full_name
    timestamp created_at
  }
  ORDERS {
    bigint id PK
    bigint customer_id FK
    varchar status
    integer total_cents
    timestamp placed_at
  }
  PRODUCTS {
    varchar sku PK
    varchar title
    integer price_cents
  }
  ORDER_ITEMS {
    bigint order_id PK, FK
    varchar product_sku PK, FK
    smallint quantity
  }
Entity and edge counts after parsing

Input

4 CREATE TABLE statements, 15 columns, 3 REFERENCES clauses, 1 table-level composite PRIMARY KEY

Output

4 entities, 3 relationship edges, 5 PK-marked columns, 1 UK mark, 3 FK marks
The junction table's two edges, reasoned out

Input

order_items.order_id → orders.id (NOT NULL, part of PK)
order_items.product_sku → products.sku (NOT NULL, part of PK)

Output

ORDERS ||--|{ ORDER_ITEMS  (an order contains one or more items)
PRODUCTS ||--o{ ORDER_ITEMS  (a product is sold in zero or more items)

The two edges from the junction table get different notations, and the DDL explains why. Both foreign keys are NOT NULL, so an order item always belongs to an existing order and an existing product. But `order_id` participates in the composite primary key together with `product_sku`, and a primary key cannot be null — meaning every order that appears at all appears with at least one distinct item. `product_sku`, by contrast, can be a product nobody has ever ordered, so the products side gets the zero-or-more fork. Same table, two honest cardinalities, both derived from constraints rather than guessed.

This is the level of detail a text-only read of the schema makes you reconstruct manually. Four tables is a classroom example; the value shows up when the paste is forty tables and you need the same reasoning applied three hundred times without getting tired.

When the DDL hides things from you

A generator is only as honest as its input, and real schemas withhold information in well-documented ways:

  • No foreign keys at all. Plenty of legacy MySQL schemas were built on MyISAM, which enforced nothing, so the DDL carries zero REFERENCES clauses. The name-inference pass catches the obvious ones (user_id, order_id) and draws them dashed; the rest need a human who knows the domain.
  • Polymorphic associations. Columns like `commentable_type` + `commentable_id` point at different tables per row, which no constraint can express. These never become edges, and the relationship list says why.
  • Cross-database references. DDL that names `other_db.customers(id)` renders an edge to a dangling parent, flagged rather than dropped — the schema really does have that dependency, even if this dump cannot resolve it.
  • Views and denormalized tables. Only CREATE TABLE is parsed. A materialized view that mirrors three tables will not show its lineage; that story lives in the view definition, not the schema.
  • CHECK constraints and business rules. A CHECK (status IN ('new','paid','shipped')) is real structure, but it constrains values, not relationships, so it appears in the entity box detail rather than as an edge.
  • Identifier weirdness. Reserved words and spaces in column names (`"order"`, `select`) survive through quoted-identifier handling; unbalanced quotes in a paste are reported with the statement number.

None of these are parser bugs; they are the boundary between what a schema says and what a system does. The tool draws the first category and stays quiet about the second, which is the correct division of labor.

Who draws ER diagrams on purpose

Three groups arrive here with different needs. Students working through database courses need diagrams for assignments and exams, and generating one from hand-written DDL doubles as a syntax check — if the diagram shows a one-to-many where the assignment asked for one-to-one, the missing UNIQUE constraint just announced itself.

Working engineers use it for onboarding and documentation. The paste-and-render loop fits neatly into wiki workflows: dump the schema, generate the Mermaid code, commit it next to the migration files, and the docs stay a rebuild away from truth. Consultants and auditors doing reverse-engineering appreciate the same loop from the other side, since they rarely get schema access beyond a sanitized DDL export and cannot install anything on the client machine to draw it.

The third group is anyone staring at an ORM's migration history trying to understand what the framework actually built. Reading twelve migration files and imagining the result is a puzzle; pasting the current schema DDL is an answer.

Neighbor tools in the cluster cover the surrounding workflow: the SQL CREATE TABLE Generator goes the other direction — visual design to DDL — while SQL on CSV and the SQL Query Explainer pick up once the schema exists and the questions turn to data.

Drawing options, compared without mercy

ApproachSetup costStays in sync with DDLSchema privacyLearning curve
This generatorPaste DDLYes — regenerate on demandFull, nothing uploadedNone
dbdiagram.ioAccount + learn their DBML, retype schemaManual re-entryCloud-stored by defaultLow but real
drawSQL / erdgoAccount, then click-and-dragManualCloud-storedMedium
MySQL Workbench / pgAdmin reverse-engineeringConnect to the live DBRefresh from sourceRequires DB access from your machineHigh — full IDE
Whiteboard or draw.ioHours of aligning boxesNeverYoursHigh tedium, low skill
Ways to get an ER diagram, from paste-and-render to hand-alignment.

The dedicated diagram platforms are pleasant to use once the schema is already drawn — their weakness is the import tax. Typing a 40-table schema into a web editor by hand costs an afternoon and starts out stale. The database clients solve sync by connecting directly to production, which is exactly the privilege an auditor or a student does not have.

Paste-and-render occupies the useful corner: no account, no connection, no transcription, at the cost of not being an interactive design surface. For documenting what exists rather than designing what does not, that trade is a bargain.

The schema stays on your machine

Table names, column names, and constraint shapes are a fingerprint of an organization's data model. Dumped DDL from an internal system can reveal module names, compliance-relevant flags, and integration points, which makes it odd that so many diagramming services ask you to paste exactly that into a text box bound for a server. This generator runs its whole pipeline — tokenizing, model building, layout, SVG serialization — locally in the tab, with no upload path to disable because none exists.

Offline operation follows for free. Air-gapped machines, customer environments with no outbound internet, and plain airplanes all still get diagrams. The one network request the page ever makes is fetching itself.

For teams with a strict review process, the Mermaid export doubles as an audit trail: the diagram's source is text, the text lives in your repository, and any diff in the schema shows up as a diff in the diagram source the next time someone regenerates it.

ER diagram questions, answered

Which SQL dialects does the generator accept?

Any CREATE TABLE statement that is close to standard form: PostgreSQL, MySQL, MariaDB, SQLite, and SQL Server all work. The parser is tolerant of dialect extras — AUTO_INCREMENT, ENGINE=InnoDB, generated columns, table comments, COLLATE clauses — and skips what it does not need rather than rejecting the statement. If a paste fails, it is almost always a semicolon or parenthesis issue in the text itself, and the parser tells you which statement it choked on.

Can it infer relationships when there are no FOREIGN KEY constraints?

Yes, as an optional pass. Naming-convention inference looks for columns like customer_id and connects them to a table named customers, defaulting the join to the target's primary key. It is a guess, so every inferred edge is drawn dashed and listed separately from constraint-derived edges — you accept or ignore them rather than trusting them blindly.

What can I download?

Three things: an SVG of the diagram, ready for docs and slides; a PNG at 2x resolution for pasting into chat or slides; and the raw Mermaid erDiagram code, which keeps the diagram editable in any Mermaid-aware editor like GitHub markdown or Notion.

How many tables can one diagram hold?

The parser itself has no fixed ceiling — a few hundred CREATE TABLE statements parse in milliseconds. Layout is the real constraint: entity diagrams stay readable up to roughly 40-60 tables, past which any tool's output turns into spaghetti. For bigger schemas, render subsets by pasting only the tables of one domain at a time.

Does the tool change my SQL in any way?

No. The input is read-only text. The generator parses it, builds an internal model, and draws from that model; your DDL is never rewritten, reordered, or sent back to you in altered form.

Why does a one-to-one relationship show up as one-to-many?

Because the foreign key column lacks a UNIQUE constraint. Cardinality is read from constraints, not vibes: a UNIQUE FK renders one-to-one, a plain NOT NULL FK renders one-to-many. If you meant one-to-one, the fix is in the schema — add the UNIQUE constraint — and the diagram will agree.

Are my table definitions uploaded to a server?

No. Parsing, layout, and rendering all run in the browser tab. Schema definitions describe the shape of internal systems — table names alone can be sensitive — so this tool was built with no upload path at all.

References

Sources and further reading

We cite primary sources (standards bodies, government agencies, official documentation) wherever they exist so you can verify every number on this page yourself.