Docs & Code Guide

Keep database schema docs in sync: comments in DDL, generated markdown, and a diff gate in CI

Schema documentation drifts because it is usually a second, parallel record of something the database already knows. The durable setup inverts that: descriptions live in the DDL as comments, readable docs are generated from the live database, and CI fails when the generated docs and the schema disagree. Nothing is transcribed twice, so nothing can drift.

8 min readFor backend and data engineers maintaining a schema wiki nobody trusts

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

202/20003 credits left
Try:

No account needed · Editable canvas, not a picture

Put the documentation in the DDL, where migrations carry it

Every mainstream database supports comments as schema objects: COMMENT ON TABLE and COMMENT ON COLUMN in PostgreSQL, COMMENT clauses in MySQL and Snowflake DDL. A comment written in a migration travels with the schema through every environment, survives dumps and restores, and sits exactly where the next engineer looks when they wonder what orders.status means.

This beats documenting in the application layer for one structural reason: ORM-level descriptions, like help text on a model field, are visible only to that codebase. A column comment is visible to every client of the database: the BI tool, the analyst in a SQL editor, the catalog crawler, the DBA at 2am. Write the description once, in the migration that creates the column, and review it in the same pull request.

Generate the readable docs from the live database

Comments cover the column level; people also need per-table pages with relationships and indexes. Generate those instead of writing them. tbls is the reference tool for this pattern: point it at a connection string and it writes a markdown document per table into a directory in your repo, including columns, types, comments, constraints, indexes and inferred relationships, driven by a .tbls.yml config. The command is a single line, tbls doc, and the output is plain markdown that renders in GitHub and every docs site.

Because the docs are generated from the database, they are correct at generation time by construction. The remaining question is keeping them regenerated, which is a CI problem, not a writing problem.

Gate CI on the diff between docs and database

tbls diff compares the committed docs directory against the live schema and exits nonzero when they disagree. Run it in CI after migrations apply and the schema can no longer change without the docs changing in the same pull request: the build fails until someone reruns tbls doc and commits the result. tbls lint goes a step further with rules like requiring a comment on every table and column, which turns the undocumented-column problem into a failing check instead of a quarterly cleanup.

This is the same ratchet logic that works for code quality: the gate does not demand a backfill, it just refuses to let the gap grow.

Find the columns nobody documented

For the backfill itself, ask the catalog. In PostgreSQL, comments live in pg_description, so a three-way join lists every column in a schema with no description attached. In MySQL the equivalent is one line, since information_schema.columns carries a COLUMN_COMMENT field you can filter on empty. Sort the result by table importance and you have a documentation debt list that is exact rather than estimated.

-- Documentation that travels with the migration
COMMENT ON TABLE orders IS 'One row per checkout attempt, including abandoned carts';
COMMENT ON COLUMN orders.status IS 'pending | paid | refunded | cancelled';

-- Every undocumented column in a PostgreSQL schema
SELECT c.relname AS table_name, a.attname AS column_name
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_description d
  ON d.objoid = a.attrelid AND d.objsubid = a.attnum
WHERE n.nspname = 'public'
  AND c.relkind = 'r'
  AND a.attnum > 0
  AND NOT a.attisdropped
  AND d.description IS NULL
ORDER BY 1, 2;

The diagram is a separate artifact with its own loop

Everything above keeps the written record current: comments, per-table markdown, the CI gate. The entity-relationship diagram is a different artifact with a different audience, the picture people open to understand how the tables relate before they read any of them, and it has its own keep-current loop covered in keep ER diagrams in sync with the database.

The short version: generate the first diagram by pasting your DDL into the database schema diagram generator, then regenerate on migration merges, either by hand or through an agent connected to the Datadef MCP server that reads the changed DDL, as described in living diagram from SQL DDL. A diagram in a project shared public serves a permanent image URL that follows edits within minutes, so the copy in your README stays the current one. Datadef does not watch the repository, and the live embed requires that public sharing; for a confidential schema, keep the project private and export instead.

Division of labor

Comments answer "what is this column". Generated markdown answers "what is in this table". The ER diagram answers "how does this schema fit together". Keep all three generated, none transcribed.

FAQ

How do I keep database schema documentation up to date?

Make the database the only source. Write descriptions as COMMENT ON statements inside migrations so they travel with the schema, generate readable markdown from the live database with a tool like tbls, and run tbls diff in CI so a schema change that does not regenerate the docs fails the build. Nothing is maintained by hand, so nothing drifts.

What is the best database schema documentation generator?

For docs-in-the-repo workflows, tbls is the strongest fit: one command generates a markdown page per table from a live connection, including columns, comments, constraints, indexes and relationships, and its diff and lint modes are built for CI. Warehouse-centric teams sometimes prefer catalog tools instead, but those solve discovery, not repo-hosted docs.

How do I find undocumented columns in my database?

Query the system catalog. In PostgreSQL, join pg_attribute, pg_class and pg_namespace with a left join to pg_description and filter where the description is null; the result lists every column without a comment. In MySQL, filter information_schema.columns on an empty COLUMN_COMMENT. Either query turns documentation debt into an exact list.

Do column comments affect database performance?

No. Comments are metadata stored in the system catalog, they are not read during query planning or execution. The only practical cost is a few bytes of catalog storage, which is why there is no performance argument against documenting every column in the DDL.

Should schema docs and the ER diagram be the same artifact?

No, and forcing them together weakens both. Written docs carry per-column detail no diagram should show, and the diagram carries the relational overview no table of prose can. Generate both from the schema, on the same migration-merge trigger, and link them to each other so readers can move between the levels.