Sailfish

Coming soon

Your MySQL index history, kept past the restart.

performance_schema only ever tells you what happened since the server booted. Sailfish reads it every five minutes and stores what moved, so when was this index last used? and did scans on this table start after Tuesday's deploy? still have answers after a restart.

Tell me when it ships

Not on sale yet. We will email you once, when it is.

/sailfish/indexes 30 days
Index Status Last used Size
orders.orders_customer_id_created_at_index hot 2 minutes ago 184 MB
orders.orders_status_index cold 19 days ago 96 MB
bookings.bookings_legacy_ref_index never used never 1.2 GB
users.users_last_login_at_index hot just now 41 MB
An illustration of the Indexes screen, not a screenshot. PRIMARY and unique keys are hidden until you ask for them.
Every 5 min

sailfish:collect schedules itself and stores what moved since the last snapshot.

Restarts

A reboot starts a new generation of counters instead of a history that goes negative.

sailfish_*

The history lives in tables in your own database, on the connection you choose.

/sailfish

A dashboard served by your own app, behind a gate you define.

Which indexes earn their keep

Every index costs you on each write. Sailfish keeps enough history to tell an index read every minute from one that stopped being read last month.

A snapshot of performance_schema every five minutes

sailfish:collect schedules itself with withoutOverlapping and onOneServer, reads the counters and stores what moved since the last run. An index nothing touched writes nothing.

Deltas that survive a restart

When uptime goes down, a new generation starts and every counter is measured from zero again. A truncated summary table, a dropped and recreated index and a reset digest table are each detected the same way.

Index status: hot, cold, never used, dropped

Every index with its last use, its size, its columns and 30 days of reads and writes. PRIMARY and unique keys are hidden until you ask, because an unused constraint is still doing its job.

Rows read by index against rows read by scan

Per table and over time, broken down by index, with the time spent waiting for table locks. The five most scanned tables sit on the overview.

Errors counted by number

Errors raised over time, the most frequent and when each first appeared. Deadlocks (1213), lock wait timeouts (1205) and duplicate keys (1062) are called out.

Hourly and daily rollups

Raw snapshots are kept for 7 days, then merged to one per hour. Hourly ones are kept for 90 days, then one per day, and daily ones for good. Charts read any window the same way.

Counters that reset, turned into a history that does not.

The first collection only records a baseline, because counters running since boot describe no particular interval. From the second one on, each snapshot stores the difference, and every rate on the dashboard is that difference over the seconds it covers.

  • ▸ Server restart: uptime went down, so a new generation starts and old cursors are measured from zero
  • ▸ TRUNCATE of a summary table: any counter going backwards resets its whole row
  • ▸ A dropped and recreated table or index: the old cursor is discarded
  • ▸ A reset digest table: a new FIRST_SEEN resets that digest
.env
# Read performance_schema here, store sailfish_* here
SAILFISH_DB_CONNECTION=mysql
SAILFISH_SCHEMAS=app,billing

# Where the events go
SAILFISH_NOTIFY_CHANNELS=mail,slack
SAILFISH_NOTIFY_MAIL=db-team@example.com
SAILFISH_DAILY_DIGEST_AT=09:00

php artisan about gets a Sailfish section with the connection, the schemas watched, the last snapshot and the retention.

Told when usage changes

Each collection ends by running the detectors. An event stays open while its condition holds and resolves once it clears. You can acknowledge it, or mute it until a date or for good.

Event When Sent
Table-scan surge Rows scanned without an index over the last hour are at least 3× the average for that hour over the previous 7 days, and at least 100,000. Immediately
Index went cold Read in the 14 days before the last 7, and not fetched since. Daily digest
Never used No fetch in 30 days of history. Says how big the index is, what its writes cost and whether another index covers it. Daily digest
Schema change A table or index was added or dropped. Daily digest

The first three need their window of history before they can fire, and until then the dashboard says how many more days each one needs. A muted event stays muted if it clears and comes back, and an immediate event that flaps notifies at most once every six hours.

Statement digests need emulated prepares. MySQL 8.0 gives no digest to statements sent as native prepared statements, which is what Laravel's MySQL connection does by default. Index and table counters see every statement either way. For digests of your application's queries, set PDO::ATTR_EMULATE_PREPARES to true on the connection.

MySQL 8.0 · Laravel 12 or 13 · PHP 8.2+ · SELECT on performance_schema

Frequently asked questions

What is Sailfish?

+

Sailfish is a Laravel package that keeps a long-term history of MySQL's performance_schema counters: index usage, full table scans, table lock waits, errors and statement digests. It keeps that history across the server restarts that reset those counters, serves a dashboard of index and table usage, and raises events when usage changes. It is made by Boring Observability, who also make Skyline and Requizon.

Why not just query performance_schema directly?

+

performance_schema only ever tells you what happened since the server booted. After a restart or a failover, "this index has zero reads" means nothing, and you cannot ask whether scans on a table started after last Tuesday's deploy. Sailfish stores what moved in each five-minute interval, so those questions still have answers.

Can Sailfish tell me which indexes are unused?

+

Yes. An index with no fetch in 30 days of observed history opens a "never used" event, which says how big the index is, what its writes cost and whether another index covers it. An index that was read in the 14 days before the last 7 and not since opens an "index went cold" event. PRIMARY keys are left out of both.

Does Sailfish send data anywhere?

+

No. It is a package running inside your application. It reads performance_schema over your own connection, writes to sailfish_* tables in your own database and serves its dashboard from your own domain. Notifications go only to the mail addresses and Slack webhook you configure.

What does Sailfish require?

+

MySQL 8.0, Laravel 12 or 13 and PHP 8.2 or newer, and a database user that can SELECT from performance_schema. An application user limited to its own schema cannot, and sailfish:collect names the grant it needs. SELECT on mysql.innodb_index_stats is optional and adds index sizes.

Why are my statement digests empty?

+

MySQL 8.0 gives no digest to statements sent through the binary prepared-statement protocol, and Laravel's MySQL connection uses native prepared statements by default. Index and table counters see every statement either way. For digests of application queries, set PDO::ATTR_EMULATE_PREPARES to true on the connection.

Who can see the dashboard?

+

Whoever your viewSailfish gate allows. Outside the local environment nobody gets in until you publish the service provider and define that gate.

When can I use Sailfish?

+

Not yet. It is in development and not on sale. Email tech@boring-observability.dev and we will write to you when it ships.

Sailfish is coming soon.

Send us an email and we will write back when it is ready to install. If there is something you would want it to catch in your own database, put that in the email too.

Tell me when it ships

In the meantime, our other tools are available now.