SQL to ER Diagram
Turn CREATE TABLE DDL into a crow's-foot ER diagram — keys, foreign-key cardinality and SVG export, entirely on this device.
Reads CREATE TABLE and ALTER TABLE … ADD CONSTRAINT (PRIMARY KEY, FOREIGN KEY, UNIQUE) — every other statement is listed as skipped, never silently dropped.
The diagram appears here as you paste DDL.
Legendcrow's foot = manybar = onedashed = not defined here, or heuristic
Press ? for shortcuts
Do it offline:$ pg_dump --schema-only --table=orders mydb
How to use it
- Pick the dialect that wrote your DDL — quoting is what differs: PostgreSQL double-quotes identifiers, MySQL backticks them, T-SQL brackets them, SQLite accepts all three.
- Paste CREATE TABLE statements (pg_dump --schema-only output works as-is) or drop a .sql file; files are read locally and a non-UTF-8 byte gets a visible replacement notice.
- Read the strip above the diagram — the table and relationship counts are computed from what actually parsed, and anything skipped is itemized below the canvas.
- Pan by dragging, zoom with the buttons or + and − on the focused canvas, then export the exact drawing as an .svg file or a .png raster at up to 2× — a diagram too large for the browser's canvas limit scales down, and the export note names the factor used.
Paste the CREATE TABLE half of a schema dump and watch it become a crow's-foot diagram: one card per table, key badges on the columns, and an edge for every foreign key with its cardinality read from your constraints. Layout, parsing and SVG/PNG export all run on this device — the DDL never leaves the tab. Statements outside the declared subset are counted and listed, never silently ignored.
How it works
First a span scan walks the paste under the selected dialect's quoting rules and marks what is string, comment or code — the same shared scanner layer the other SQL pages here build on. That pass is why a semicolon inside 'a;b', a # comment in MySQL or a PostgreSQL $body$ … $body$ block never splits a statement in the wrong place: statement boundaries only count in code spans.
Next the DDL parser reads a declared subset — CREATE TABLE with inline and table-level PRIMARY KEY, FOREIGN KEY, UNIQUE and REFERENCES clauses, plus ALTER TABLE … ADD CONSTRAINT — into a table model: columns with types, nullability and defaults, key sets, and every foreign key with its ON DELETE / ON UPDATE actions. Whatever falls outside that subset (views, indexes, functions, CHECK bodies, DML) becomes a counted skip entry with its character span, so the diagram's silence is never ambiguous.
Cardinality is then inferred from keys alone: a plain foreign key draws one-to-many; a foreign key whose columns are the child's own primary key or a UNIQUE set draws one-to-one; and a table with exactly two foreign keys and no columns of its own prints an extra dashed many-to-many edge between the two targets — a heuristic, labeled as such, because SQL has no join-table keyword.
Finally a layered layout assigns each table a column: referenced parents sit a layer before their children, cycles are broken deterministically, and every coordinate is integer arithmetic — the same DDL always yields byte-identical geometry, which the test suite asserts by running the layout twice and comparing bytes. The result is pure draw data (rectangles, paths, text positions) that the page renders as SVG elements; nothing is ever assembled from markup strings.
Examples
CREATE TABLE customers (id int PRIMARY KEY);
CREATE TABLE orders (id int PRIMARY KEY, customer_id int REFERENCES customers (id));2 tables · 1 relationship
orders -> customers [one-to-many] customer_idThe starter shape: an inline REFERENCES clause is enough. The edge leaves orders at customer_id, lands on customers, and the crow's foot sits on the orders side because many rows may share one customer.
CREATE TABLE students (id int PRIMARY KEY);
CREATE TABLE courses (id int PRIMARY KEY);
CREATE TABLE enrollments (student_id int, course_id int, PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES students (id), FOREIGN KEY (course_id) REFERENCES courses (id));3 tables · 2 relationships
enrollments -> students [one-to-many] student_id
enrollments -> courses [one-to-many] course_id
students -> courses [many-to-many, heuristic] enrollmentsenrollments carries two foreign keys and nothing else, so beyond its two real edges the diagram adds a dashed students–courses edge labeled with the join table's name. The relationship count stays 2 — the heuristic edge is annotation, not a constraint.
CREATE TABLE employees (id integer PRIMARY KEY, name text NOT NULL, manager_id integer REFERENCES employees (id));1 table · 1 relationship
employees -> employees [one-to-many, self] manager_idA manager column that references its own table draws as a loop on the employees card, and the parse notes call the self-reference out — an org chart in one table is a shape worth flagging, not an error.
CREATE TABLE users (id int PRIMARY KEY);
CREATE TABLE profiles (user_id int PRIMARY KEY REFERENCES users (id), bio text);
CREATE TABLE avatars (id int PRIMARY KEY, media_id int REFERENCES media (id));3 tables · 2 relationships
profiles -> users [one-to-one] user_id
avatars -> media [one-to-many, dangling] media_idTwo inferences at once: profiles.user_id is both primary key and foreign key, so at most one profile exists per user — one-to-one. And media is never defined here, so its edge draws dashed to a placeholder card instead of vanishing.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Dangling foreign keys stay visible
A partial dump often references tables you didn't paste. Instead of dropping the constraint, the diagram draws a dashed edge to a ghost card marked not defined, and the parse notes name the missing target — paste the rest of the dump and the ghost becomes a real table.
Composite keys badge every member
PRIMARY KEY (order_id, product_id) puts a PK badge on both columns, and a foreign key over the same pair counts as covering the primary key — which is exactly how the one-to-one inference decides. Column order inside the constraint is preserved, since key order is meaningful in every dialect here.
The join-table edge is a guess, and says so
Only a table whose every column belongs to one of exactly two foreign keys triggers the many-to-many heuristic. Add a grade or created_at column to a link table and the dashed edge disappears — the two real one-to-many edges still tell the truth.
Self-references draw as loops
manager_id integer REFERENCES employees (id) inside employees is legal DDL and a legitimate design; the edge arcs out of the card and back into its header rather than connecting two cards, and a note notifies you in case the self-reference was a paste mistake.
Quoting is the dialect selector's whole job
The subset grammar is shared; what changes per dialect is which quotes hide what. Under MySQL, "order details" is a string literal and `order details` is a table name; under PostgreSQL the reverse; T-SQL adds [brackets]; SQLite accepts all three. Pick the wrong dialect and a quoted name may parse as data.
Skips are counted, spans and all
CREATE INDEX, CREATE VIEW, triggers, CHECK bodies and MySQL KEY lines don't belong in an entity diagram, so they are skipped — but each one lands in the list under the canvas with its character range, and the note above it carries the computed count. Nothing is dropped without a receipt.
Common errors
No CREATE TABLE statements found in this input. This looks like DML (SELECT/INSERT/UPDATE/DELETE) — paste schema DDL instead, e.g. the output of pg_dump --schema-only.- Queries describe questions, not structure — there is no table shape inside a SELECT. Export the schema itself: pg_dump --schema-only, SHOW CREATE TABLE in MySQL, or .schema in the sqlite3 shell all produce the CREATE TABLE text this page reads.
Input is 600,000 characters — over the 524,288-character (512 KiB) limit for this tool.- The refusal names both the cap and your actual size, and your paste stays in the editor untouched. Half a megabyte of pure CREATE TABLE text is an enormous schema — trim the dump to the tables you care about, or split it by schema.
This DDL defines 121 tables — more than the 120 this diagram can render.- Past 120 tables a single picture stops being readable long before it stops being computable, so the tool refuses rather than render a smear. Diagram one subsystem at a time — foreign keys to tables you left out will still show as dashed ghosts.
orders.customer_id references "customers", which is not defined in this DDL — the relationship is drawn dashed to a placeholder.- Not a failure — a disclosure. The constraint is drawn, dashed, to a ghost card, and this note names exactly which definition is missing so you know what else to paste. It disappears the moment customers is defined in the same input.
FAQ
Is my schema uploaded to render the diagram?
No. Parsing, layout and both exports run in this tab; the page makes no network request carrying your DDL, which you can confirm from the browser's network panel. Credential-looking pastes are additionally kept out of between-visit storage, and share links are only created when you click Share.
Which SQL statements does it understand?
A declared subset: CREATE TABLE with inline and table-level PRIMARY KEY, FOREIGN KEY, UNIQUE and REFERENCES (including ON DELETE / ON UPDATE actions), plus ALTER TABLE … ADD CONSTRAINT in the same shapes, across PostgreSQL, MySQL, SQLite, T-SQL and ANSI quoting rules. Everything else is itemized as skipped rather than parsed — this page diagrams structure, it does not validate SQL.
How is cardinality decided?
From your keys, nothing else. Every foreign key starts as one-to-many with the crow's foot on the referencing side; it narrows to one-to-one when the referencing columns are that table's primary key or a declared UNIQUE set; and many-to-many appears only as a labeled heuristic between the two targets of a pure join table. No row counts are guessed, ever.
Why did a table I pasted come out with no columns?
Usually the dialect. A bracket-quoted or backtick-quoted identifier under the wrong ruleset can swallow text as a string and derail the column list; switching the selector re-parses instantly. The other common cause is CREATE TABLE … AS SELECT, which defines columns implicitly — that form is outside the subset and lands in the skipped list instead.
Can I use the exported SVG in documentation?
Yes — the .svg download is the mounted diagram serialized as-is, with fonts and colors written as plain attributes so it renders standalone in wikis, READMEs and design docs. The .png export rasterizes the same drawing at up to double resolution for places that reject vector images; when a very large diagram would overrun the browser's canvas ceiling it is scaled to fit and the page reports the actual factor. Both filenames carry a UTC timestamp.
What happens to CHECK constraints and indexes?
They are read past and receipted: CHECK bodies and index definitions (CREATE INDEX, MySQL KEY lines) don't change what an entity-relationship picture shows, so each becomes a skip entry with its character span instead of a parse error. Column attributes like COLLATE, AUTO_INCREMENT and GENERATED are likewise consumed without affecting the drawing.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Re-render the diagram and re-fit it to the canvas.
- ⌘/Ctrl + S
- Download the diagram as an .svg file.
- + − 0
- Zoom in, zoom out, or fit to view when the diagram has focus.
- ← ↑ ↓ →
- Pan the focused diagram.
- t
- Flip this page between light and dark themes.
- ? or ⌘/Ctrl + /
- Open the shortcut reference in an overlay.
- Esc
- Close the open dialog.