Guide · Reading
What is in this database?
Read this when you have connected to a database you have never opened and want the shape of a schema in a minute: which tables matter, where the data is, and what is wrong with it. You don't need it for a schema you wrote last week. For one table at a time, What does this table do? goes deeper.
The symptom
You add the commerce sample and connect. The explorer lists six schemas. billing alone has invoices, invoice_lines, payments, events, invoice_events and event_attachments, and nothing says which of them the others hang off, which column has no index, or which key is closest to the end of its integer range.
When the brief opens
The brief opens the first time you connect to a database, once per connection. Saving a new connection, connecting from the sidebar and opening something on an offline connection all count. Reconnects at launch or after a drop don't, and neither do the connections compare and drift open. It waits for the table list, then opens on public if that has tables, else on the schema with the most. The commerce sample has no public, so you get Brief · billing.
To open it again, right-click a schema and choose Database brief, or pick Database brief in the command palette. Open the brief of a new database, in Settings › General, turns the first opening off.
The brief is per schema. A foreign key into another schema, like billing.invoices.customer_id to crm.customers, is not part of any area.
The masthead
While it builds, three stations run across the top, catalog, stats and brief, each with its time. Then come the build time, Refresh, a Page and Ledger switch, and Export. The title is the database name, with tags for the schema, the server version and any framework or extensions found. Below are the vitals: tables, on disk, rows, areas, islands and links. On billing, tables is 6 and links is 2 with +3: two declared foreign keys inside the schema, three links inferred from names. rows carries ≈ because it is the planner's estimate.
links reads 2 declared plus 3 inferred. The badge on invoices marks the table to read first.
Areas
Areas is a metro map. Each line is an area: tables that link to each other more than to the rest. Solid track is a declared foreign key and dashed track is inferred. A large ring marks a hub: the table an area is laid out around, and any table that at least five others point at. A numbered badge is read first: up to six tables, the most pointed-at table of each area first. Tables with no link to another table in the schema are islands, counted in the vitals.
billing splits into two areas, ringed at invoices and events. invoices has four tables pointing at it, two by name only, and gets the badge 1: start there. Click a station to open its table.
Weight and tables
Weight is a waffle of 100 squares over the bytes on disk. The six largest tables get a colour and a legend line, and the rest share a hatched n others. Tables below it is one dot per table, by area, hollow when dormant. With twelve tables or fewer the dots become names.
House style
House style writes a CREATE TABLE in the schema's own conventions: table names, the primary key, foreign key names, timestamps, JSON. Each line carries the share of tables that follow it and a count of outliers, whose names show on hover.
Smells
Smells is a matrix. Each row is one kind of problem, with its glyph coloured by severity, most severe first. Each column is an area, then all. A dot's size is the count. Click a row to list its objects.
On billing it raises four, each a known flaw of the sample:
FK without index:payments.invoice_id,→ invoices.id. Critical, becauseinvoiceshas 8,100,000 rows. This is the three-minute DELETE.Key-named, no FK:invoice_lines.invoice_id,invoice_events.invoice_idandevent_attachments.event_id. Each looks like a foreign key and has no constraint.Duplicate index:invoices.invoices_customer_id_idx,covered by invoices_customer_created_idx.Sequence near its limit:events.id,71% of integer.
Every smell the brief can raise, and what it reads:
| Smell | What it means | Read from |
|---|---|---|
FK without index |
No valid B-tree index leads with the key's columns, so each delete on the referenced table scans this one. Critical from 1,000,000 referenced rows. | Declared foreign keys, indexes |
Sequence near its limit |
A serial or identity column has used half its type's range or more. Warning at 75%, critical at 90%. | pg_depend, pg_sequences |
No primary key |
No primary key, and no unique key on NOT NULL columns. |
Keys, nullability |
Key-named, no FK |
A customer_id column with no constraint, and a customers table with a one-column key of the same type family. With a customer_type partner it is polymorphic. |
Names, keys, types |
FK type mismatch |
A foreign key column whose type differs from the column it references. | Declared keys, types |
Duplicate index |
A plain index whose columns lead another index of the same kind. | Indexes, not partial or expression ones |
Cascade reach |
Deleting one row cascades through 5 tables or more, or about 10,000,000 rows. | ON DELETE CASCADE keys, row estimates |
Same name, different type |
A key-named column on three tables or more, with a type most of them don't use. | Names, types |
Timestamp without zone |
A timestamp column, which stores no time zone. |
Types |
Dormant or empty |
No writes since the statistics reset, or no rows. | pg_stat_user_tables, row estimates |
Naming outlier |
A name that breaks the house case or plural. | Names |
FK cycle |
Foreign keys that run in a loop between tables, self-references left out. | Declared keys |
Fixing what it flags
A row with a script ends in Fix. Fix scripts, the orphan check and Open as design are Pro. The smells and their objects are not.
Fix opens the row with every fixable object ticked and the script for them on the right, headed 1 chosen. For FK without index on billing it reads:
-- payments.invoice_id · → invoices.id
CREATE INDEX CONCURRENTLY payments_invoice_id_idx
ON billing.payments (invoice_id);
The scripts are written for a live server. Indexes are built and dropped CONCURRENTLY. An inferred foreign key is added NOT VALID, then validated, with an index first when it needs one. A retype warns in a comment that it rewrites the table under an exclusive lock. Nothing runs from the brief: Copy takes the script, and Open in SQL editor puts it in a new editor for you to read and run. For the two key smells, Open as design starts a schema design instead.
The script is only shown. Open in SQL editor is the way to run it.
Before you add a key the brief inferred, press Check orphans. It counts the rows whose value has no match in the target, and stops at 10,000 (10000+ orphans). A key with orphans would fail to validate, so the brief unticks it. Each check runs in a read-only transaction and gives up after 5 seconds with timed out.
For indexes beyond foreign keys, see Which indexes are missing?.
The ledger
Ledger lists every table under its area, islands last, with Role, ≈Rows, Size, In · Links · Out, Writes as a mix of ins, upd and del, Time cols and Smells. A role is hub, entity, join, lookup, log or island, read from links, size and writes. Filters along the top pick a role, has smell or no updated_at. No billing table has an updated_at, so that last one keeps all six.
Export copies the brief as Markdown or agent context, or saves it as Markdown or HTML.
Caveats
- The brief reads catalog and statistics views, never table data. On PostgreSQL that is one round trip of four queries, on top of what the ER diagram reads.
≈Rowsisreltuples, as fresh as the lastANALYZE. Writes count from the last statistics reset, or server start, and a table needs seven days of them to be called dormant.- A role that cannot read a sequence gets no
last_valuefrompg_sequences, soSequence near its limitstays silent. - MySQL and MariaDB keep no per-table write counters, so there is no
Writescolumn and only empty tables count as dormant.AUTO_INCREMENTcolumns stand in for sequences. - SQLite keeps no per-table sizes or write counters, so the waffle weighs rows and there is no sequence smell. Its fix scripts rebuild the table to add a key or change a type.
- SQL Server's write counters need
VIEW DATABASE STATE, orVIEW SERVER STATEbefore SQL Server 2022, and reset at restart. Without the permission the brief leaves writes out. Timestamp without zoneis PostgreSQL only.- The buildable sample keeps only the last 500,000 rows of
billing.events, not the 1.5 billion other pages quote, so itsrowsandon diskare smaller. The smells are the same.
When the brief has told you where to start, A schema you did not write walks the rest of the first hour.