Stop drawing ER diagrams — write them

When you draw an entity-relationship diagram in a diagramming tool, every new table means reconnecting lines by hand, and sooner or later the picture drifts away from the actual schema. With Mermaid’s erDiagram, the diagram is plain text: it lives in your repository, shows up in diffs, and renders automatically in GitHub READMEs, pull requests, Notion, and most Markdown viewers.

This article is a reference you can skim: a cardinality cheat sheet, attribute syntax, a complete real-world example, and finally a way to generate all of it automatically from existing SQL.

The minimal example: start with three lines

erDiagram
    USERS ||--o{ POSTS : "has"

That alone renders “USERS to POSTS, one-to-many (one user has zero or more posts)”. The general form is:

<entity1> <left-cardinality><line><right-cardinality> <entity2> : "label"

One thing to know up front: the relationship label after the colon is mandatory. If you don’t want visible text, write : "" — omitting it entirely is a syntax error, and it’s the most common first stumble.

Cardinality symbols: the cheat sheet

erDiagram cardinalities map to crow’s foot notation. The symbols always point outward, toward their entity.

Left symbolRight symbolMeaning
|oo|Zero or one
||||Exactly one
}oo{Zero or more
}||{One or more

Concrete combinations are the fastest way to memorize them:

SyntaxReading
A ||--o{ BOne A, zero or more B (the everyday one-to-many)
A ||--|{ BOne A, at least one B
A |o--o| BOptional one-to-one
A ||--|| BStrict one-to-one
A }o--o{ BMany-to-many (conceptual, before adding a junction table)

Solid vs. dashed: identifying vs. non-identifying

The line segment comes in two flavors:

LineMeaningTypical use
--Identifying (solid)The child’s primary key includes the parent’s PK (e.g., order line items)
..Non-identifying (dashed)The child references the parent through an ordinary FK (e.g., a post and its author)
ORDERS ||--|{ ORDER_ITEMS : "contains"     %% identifying (solid)
USERS  ||..o{ POSTS       : "writes"       %% non-identifying (dashed)

%% starts a comment. Decide as a team whether you distinguish identifying relationships or standardize on solid lines — either works, but consistency keeps diagrams readable.

Defining attributes (columns)

Append a {} block to an entity to render its columns as a table:

erDiagram
    USERS {
        int id PK "auto increment"
        varchar(255) email UK "login ID"
        varchar(100) name
        datetime created_at
    }

Each line is type name [key] ["comment"]:

  • Keys are PK / FK / UK, and you can combine them comma-separated (PK, FK) — essential for junction tables with composite keys
  • Types may contain parentheses, like varchar(255) or decimal(10)
  • Comments go in double quotes

A real-world example: e-commerce orders

Everything above in one copy-paste-ready diagram:

erDiagram
    USERS ||--o{ ORDERS : "places"
    ORDERS ||--|{ ORDER_ITEMS : "contains"
    PRODUCTS ||--o{ ORDER_ITEMS : "included in"

    USERS {
        int id PK
        varchar(255) email UK
        varchar(100) name
        datetime created_at
    }
    ORDERS {
        int id PK
        int user_id FK
        varchar(20) status
        datetime ordered_at
    }
    ORDER_ITEMS {
        int order_id PK, FK
        int product_id PK, FK
        int quantity
        decimal(10) unit_price
    }
    PRODUCTS {
        int id PK
        varchar(200) name
        decimal(10) price
    }

The interesting part is ORDER_ITEMS: the many-to-many between orders and products is resolved into a junction table with a composite primary key (two PK, FK columns), and the business rule “an order always has at least one line item” is expressed by the ||--|{ cardinality.

Pitfalls and how to avoid them

  • The relationship label is required. Use : "" to hide it
  • Prefer ASCII entity names. Rendering of non-ASCII entity names varies between environments; keep table names in English and put local language in labels or comments
  • Many-to-many (}o--o{) is for concept-level diagrams only. Resolve it into a junction table before implementation
  • Diagram not rendering on GitHub? Check that the code fence language is exactly ```mermaid

Generate it from CREATE TABLE statements

Past ten tables or so, hand-writing this syntax gets tedious. If you already have the DDL, paste it into the SQL to ER Diagram converter — it parses primary keys, foreign keys, and relationships, and emits a Mermaid ER diagram automatically.

Generating a Mermaid ER diagram from CREATE TABLE statements with the SQL to ER tool

Everything runs entirely in your browser, so confidential schema information never leaves your machine. Generate the base diagram, then fine-tune it with the cheat sheet above. For a walkthrough of the tool itself, see How to generate an ER diagram from SQL DDL.

FAQ

What is the correct way to write a one-to-many relationship?

PARENT ||--o{ CHILD is the standard form (one parent, zero or more children). Use ||--|{ only when “at least one child must exist” is a real business rule you want the diagram to state. When in doubt, ||--o{ is the safe default.

Can I draw relationships without defining attributes?

Yes. erDiagram renders entities referenced in relationship lines even without {} blocks. A practical workflow is to sketch relationships only during early design, then add attribute blocks once the schema stabilizes.

Where can Mermaid ER diagrams be rendered?

GitHub (READMEs, issues, pull requests), GitLab, Notion, VS Code (with extensions), Obsidian, and most modern Markdown environments render Mermaid code fences natively. For anything else, export SVG/PNG via the Mermaid Live Editor.

Can a many-to-many relationship be implemented directly?

No — A }o--o{ B is a conceptual notation. Relational databases require a junction table (composite primary key plus two foreign keys), as shown in the e-commerce example above. Writing the resolved form into the diagram early saves rework at implementation time.