New to Kestra?
Use blueprints to kickstart your first workflows.
Find PostgreSQL foreign keys without an index, alert Slack, create the indexes with CREATE INDEX CONCURRENTLY and verify every key is covered.
PostgreSQL indexes the referenced side of a foreign key (it needs a primary key or a unique constraint), but never the referencing side. So every DELETE on the parent table, and every update of its key, has to scan the whole child table for matching rows. With ON DELETE CASCADE, deleting one order can lock and scan millions of line items. It works fine in development, and becomes a nightly cleanup job that runs for hours, or a lock that blocks the checkout.
This blueprint finds every foreign key whose columns are not covered by an index, with the size of the child table and the delete rule. It alerts Slack, and can create the missing indexes with CREATE INDEX CONCURRENTLY, one at a time and without blocking writes. It then audits again, so the run only succeeds when each key is covered by a valid index.
This blueprint was created by zkasuran.
audit (io.kestra.plugin.jdbc.postgresql.Query, read-only) lists the foreign keys outside system schemas that no valid, non-partial index covers. An index covers a key when its leading columns are exactly the key's columns, in any order. Each row has the child and parent tables, the columns, the delete rule, the child row count, and a CREATE INDEX CONCURRENTLY IF NOT EXISTS statement built with format('%I') and regclass.summary logs every finding. Child tables below min_table_rows are marked report only: an index there costs more than it saves.route (io.kestra.plugin.core.flow.If):alert posts to Slack.remediate_gate runs only when auto_remediate is true and dry_run is false.remediate (io.kestra.plugin.core.flow.Loop) builds one index at a time, each in its own task.reaudit runs the audit again, and verify_result fails if an indexed key is still uncovered. A failed concurrent build leaves an INVALID index, which does not count as coverage.errors alerts Slack when the audit cannot run.The catalog query finds the keys once. Kestra runs it every week, after every migration lands, and turns the result into a Slack alert. It builds the indexes behind auto_remediate and dry_run, one per task so one failure stays isolated, verifies the result, and keeps a history of which index was added where and when.
On Kestra 2.0.5 OSS against PostgreSQL 16, with a local HTTP sink standing in for Slack. Foreign keys without an index:
order_items.order_id: 400,000 rows, ON DELETE CASCADE to orders;shipments.customer_id: 150,000 rows;invoices(region, cust_id): an empty table.shipments.order_id already has an index.
| Run | Inputs | Result |
|---|---|---|
| 1 | defaults | 3 unindexed keys, invoices marked report only (0 rows). shipments.order_id not flagged. Alert sent |
| 2 | auto_remediate: true, dry_run: true |
Same report, no index created |
| 3 | auto_remediate: true, dry_run: false |
2 indexes built concurrently and verified, both valid in pg_index. Deleting 500 orders in a transaction went from 9,734 ms to 7.8 ms |
| 4 | min_table_rows: 0, remediate |
The composite index on invoices(region, cust_id) built and verified |
| 5 | defaults | Every foreign key covered |
| 6 | unreachable database | errors alert sent |
docker run -d --name pg -p 5432:5432 -e POSTGRES_PASSWORD=pg postgres:16;REFERENCES constraint and no index.POSTGRES_USERNAME / POSTGRES_PASSWORD: credentials used by every query.SLACK_WEBHOOK_URL: used by alert and the errors block.SECRET_.| Input | Default | Purpose |
|---|---|---|
jdbc_url |
jdbc:postgresql://localhost:5432/app |
Database |
min_table_rows |
10000 |
Smallest child table that gets an index |
auto_remediate |
false |
Allow index builds |
dry_run |
true |
Must be false for an index to be created |
POSTGRES_USERNAME, POSTGRES_PASSWORD and SLACK_WEBHOOK_URL secrets.jdbc_url.auto_remediate: true and dry_run: false outside peak hours.weekly trigger, which always runs in safe mode, or call the flow at the end of your migration pipeline.n_tup_del from pg_stat_user_tables.Pause before building indexes on very large tables.