Skip to content
In this guide

Guide · Reading

What is in this database?

8 min read

Checked on 0.6.1

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.

The database brief for the billing schema: the masthead with its vitals, and the Areas map with invoices numbered 1.

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, because invoices has 8,100,000 rows. This is the three-minute DELETE.
  • Key-named, no FK: invoice_lines.invoice_id, invoice_events.invoice_id and event_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:

SQL
-- 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 FK without index row opened in the Smells matrix, with payments.invoice_id ticked and its CREATE INDEX CONCURRENTLY script on the right.

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.

The brief's Ledger view for billing, every table under its area with role, row estimate, size bar, link butterfly, write mix, time columns and smell glyphs.

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.
  • ≈Rows is reltuples, as fresh as the last ANALYZE. 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_value from pg_sequences, so Sequence near its limit stays silent.
  • MySQL and MariaDB keep no per-table write counters, so there is no Writes column and only empty tables count as dormant. AUTO_INCREMENT columns 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, or VIEW SERVER STATE before SQL Server 2022, and reset at restart. Without the permission the brief leaves writes out.
  • Timestamp without zone is PostgreSQL only.
  • The buildable sample keeps only the last 500,000 rows of billing.events, not the 1.5 billion other pages quote, so its rows and on disk are 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.

Last checked against Seqora 0.6.1, PostgreSQL 17 on .

Bugs and small complaints both go to this address. I read every email myself.