DEV Community

Cover image for Column Comments in PostgreSQL and MySQL: How to Document Columns Without a Migration
Son Tran
Son Tran

Posted on Originally published at schemity.com

Column Comments in PostgreSQL and MySQL: How to Document Columns Without a Migration

Disclosure: I build Schemity, a desktop ERD tool - this post is from our blog and uses it for the examples.

TL;DR: The database has a built-in place to document a column - COMMENT ON COLUMN in PostgreSQL, the COMMENT attribute in MySQL - and almost nobody fills it in, because a sentence of prose has to travel the same path as a schema change: a migration file, a review, a deploy. Schemity keeps field descriptions in the diagram instead, where editing one generates no SQL, reads existing database comments in on import, and exports the result as a data dictionary.

You can document a database column without touching the database: write the description in the model rather than in the schema. That sounds like a dodge until you price the alternative. The database's own mechanism for column documentation, COMMENT ON COLUMN in PostgreSQL and the COMMENT attribute in MySQL, sends a sentence of prose down exactly the same path as a change to how data is stored - a migration file, a code review, an approval, a deploy window - and on MySQL it does something worse than that. Schemity keeps field descriptions in the diagram, where editing one produces no SQL at all.

This is why so many production schemas have thousands of columns and almost no comments. Not because nobody wanted to write them. Because writing one costs a deploy.

How do I document a database column without running a migration?

Keep the description in the model rather than in the storage engine. A field description is a fact about what the column means to your team; it changes no type, no constraint, no index, and nothing about what the database will accept. When it lives in the diagram, editing it is like editing a comment in a code file: you change it, review it in the same pull request as everything else, and nothing has to run against production for it to take effect.

The moment that description is a column comment, it stops being prose and becomes DDL. Now it needs a migration file, and the migration needs a reviewer, and the reviewer is looking at an ALTER TABLE against a live table. Everyone in that chain is correct to be careful, which is the problem: the care is proportionate to a schema change, and this is not one.

Why a column comment is priced like a schema change

PostgreSQL gets this closest to right. COMMENT ON COLUMN invoices.voided_at IS 'set when finance reverses an invoice' is a standalone statement that touches only the catalog. It is still a migration in every workflow where migrations own schema changes, but it is a cheap and safe one.

MySQL has no equivalent. The comment is an attribute inside the column definition, so changing it means ALTER TABLE ... MODIFY, and MySQL's own manual states the trap plainly: "Attributes present in the original definition but not specified for the new definition are not carried forward." The manual's example is a column defined as INT UNSIGNED DEFAULT 1 COMMENT 'my column', modified with the intention of changing only the type:

ALTER TABLE t1 MODIFY col1 BIGINT;
Enter fullscreen mode Exit fullscreen mode
Attribute In the original definition After that statement
Data type INT BIGINT
UNSIGNED present dropped
DEFAULT 1 present dropped
COMMENT 'my column' present dropped

Three attributes vanish and the statement is perfectly valid. There is no error, no warning, and no comment-only syntax to reach for. To attach one sentence of documentation to a MySQL column, the migration has to restate the column's entire definition correctly, which means the documentation change is now capable of altering how data is stored. That is the reverse of what anyone wanted.

Why ORMs keep declining to support column comments

The obvious escape is to let the ORM manage comments alongside everything else, and the ORMs have been declining for years. Drizzle has three separate open requests for it - issues 886, 1840 and 5203, the last opened on 1 January 2026 - and the newest one argues from an angle that did not exist when the first was filed: "In modern projects, database comments are no longer only for humans - they are increasingly important machine-readable context for AI-powered tooling." node-db-migrate's request, issue 558, has been open since March 2018 carrying the labels "Nice to have" and "Not Planned". Doctrine's migrations have their own long-running report of comments disappearing from generated migrations.

None of these maintainers are wrong. From inside a migration tool, column comments genuinely are niche: they are the only part of a column definition that no query result depends on. The pattern that emerges from eight years of open issues is not neglect, it is a category error. Documentation keeps being filed as a schema feature, gets ranked against schema features, and loses every time.

Where a field description belongs instead

In Schemity, a field carries a description of its own, the same way entities and legends already carry markdown descriptions. It is a text box in the field editor, next to the type and the default, and the sentence it holds is the one a MySQL migration would have had to restate a whole column definition to attach.

The field editor for articles.content in Schemity, with the Description box reading: String if we choose HTML WYSIWYG or Markdown editor. Use JSON if we choose structure content like Editor.js - alongside the field name, TEXT type, BLANK default and the PK, Unique and Nullable checkboxes

That description is the kind nobody ever writes into a column comment, because it is the reasoning behind a type choice rather than a definition of the column, and it would need a deploy. It also answers the question a reader of content TEXT actually has.

A documented field is then visible without opening anything: a bar on the leading edge of the row marks any field that has one, so which parts of a table are documented is a glance down a column rather than an audit, and the bar is drawn into SVG exports too.

The articles entity on the Schemity canvas, nine fields listed, where only the content row carries a short pale bar on its leading edge marking it as the one field with a description

One marked row out of nine is the honest picture of most schemas, and it is readable at a glance precisely because the mark is absence-shaped: you are looking for the fields that have no bar.

The important property is what does not happen. Descriptions live in the diagram's JSON file and never reach the database, so writing one produces no migration to review and no statement to run. This was a deliberate reversal on our side rather than a design we got right first time. Schemity used to write descriptions back as column comments, and that produced exactly the migration described above - on MySQL, the worst-shaped statement in the whole product, restating an entire column definition from the diagram just to attach a sentence. It also lost work: because comments came back from the database on every re-sync, a description could be silently overwritten, invisibly on PostgreSQL and MySQL and permanently on SQL Server and SQLite.

Reading still goes one way. Importing an already documented schema arrives documented, because comments that exist in the database are read in. They are simply never written back.

When the comment really does belong in the database

There is a real case on the other side, and the Drizzle issue names it: if the reader is a program that introspects the live database - an agent connecting to a schema it has never seen, a catalog crawler, a BI tool reading the information schema - then the comment has to be in the database, because that is the only place the reader looks. A description in a diagram file it cannot open is worth nothing to it.

If that is your goal, write the comments as migrations and treat them as schema changes, deliberately. What you should not do is adopt that cost by accident for documentation that only people will ever read, which is the situation almost every team is actually in.

Comment in the database Description in the model
Read by Anything that introspects the catalog People, and any export you generate
Cost of an edit Migration, review, deploy Save the file
MySQL cost of an edit Restating the full column definition The same as any other engine
Survives a schema refresh Yes, it is the source Yes, it is not overwritten
Reviewed in the pull request As DDL As a diff in the diagram JSON

Getting the documentation to the people who need it

A description nobody can reach is not documentation, which is the fair objection to keeping it out of the database. The answer is export rather than storage: the diagram exports as a data dictionary in HTML, Markdown and Excel, covering every column with its type, key role, nullability, default and description alongside the constraints and relationships. The export follows the active view, and a context view is a saved, focused subset of the schema showing only some entities and the relationships between them, so documenting one context produces a document about that context rather than the whole database.

It also ends by counting what is not written down yet: how many entities and fields carry a description, and the names of those that do not. It states the numbers and stops there, which is the only honest way to report on documentation coverage.

That closing count is the part that changes behaviour, because the reason columns go undocumented was never that people did not care. It was that the cheapest place to write it down had a deploy attached, so the note went into a wiki page instead and drifted. Take the deploy off the description and the note goes where the schema is - and then the data dictionary is a document you generate rather than one you maintain.

Top comments (1)

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

The human-vs-machine reader distinction is the key trade-off here.

If teams keep the richer description outside the engine but also need live-schema consumers, I would treat database comments as a compiled artifact, not a second authoring surface. Version the model description, generate dialect-specific comment migrations, and run CI that diffs the expected catalog text against PostgreSQL/MySQL introspection. For MySQL, generate the full column definition from the live parsed schema and fail if any type, nullability, default, charset, collation, or generated-column attribute would change.

That gives teams one source of truth while still serving BI tools and agents. I would also attach a description version or digest to schema-context snapshots, because a correct comment attached to an old column meaning is worse than an explicit “undocumented/stale” state.

The dangerous state is not choosing either location. It is letting diagram prose and catalog comments drift independently.