Learning resources
David Sterling  

Dasha 1.8 Tutorial: Postgres Index Advice and Schema Lint

Dasha 1.8 shipped on September 29, 2026, and it is worth an afternoon if you run more than one Postgres cluster. Dasha is an open-source performance dashboard that connects to your databases with a read-only role, installs no agent on the database hosts, and needs no extension you are not already running. One instance can serve a whole fleet and every replica of it.

This tutorial sets it up from scratch on a small fleet, then walks through the three reports that justify the effort: missing-index advice, structural schema checks, and the PostgreSQL 16+ I/O page.

What version 1.8 actually adds

  • Index recommendations built from pg_stat_statements read across every host of a cluster, with optional auto_explain plan evidence from your logs.
  • An I/O page on pg_stat_io (PostgreSQL 16 and newer), split by backend type, object and context, with a live mode plus snapshot history.
  • Schema checks — 17 checks for defects that stay invisible until something breaks.
  • Log insights across OpenSearch, Elasticsearch and VictoriaLogs, including plan regressions between two time windows.

Requirements are modest: PostgreSQL 14 or newer on the monitored clusters (tested through 18), reachable with a role holding pg_monitor and CONNECT on every monitored database.

Step 1 — Create the monitoring role

CREATE ROLE monitoring_user LOGIN PASSWORD 'use-a-secret-manager';
GRANT pg_monitor TO monitoring_user;
GRANT CONNECT ON DATABASE myapp TO monitoring_user;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO monitoring_user;

Two details matter. First, pg_monitor carries pg_read_all_stats: without it the query report shows only the monitoring role’s own statements, and Dasha says so above the tables — a silent way to conclude your database is idle. Second, the sequence grant is what lets the sequence-exhaustion check read last_value; without it the schema-lint page names the grant it had to skip.

Do not widen beyond that. Dasha reads your databases and changes nothing in them.

Step 2 — Make sure query statistics exist

# in postgresql.conf: shared_preload_libraries = 'pg_stat_statements'
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

The extension belongs in one database of the instance — its contents cover the whole instance, and Dasha reads them through whichever database carries it, in whatever schema it sits. On Postgres Pro, pgpro_stats is detected and used automatically, with no configuration.

Step 3 — Write dasha.yaml

clusters:
  - name: production
    username: monitoring_user
    password_from_env: PROD_PG_PASSWORD
    port: 5432
    databases:
      - myapp
    hosts:
      - pg-master.example.com
      - pg-replica-1.example.com

  - name: staging
    username: monitoring_user
    password_from_env: STAGING_PG_PASSWORD
    databases:
      - myapp
    hosts:
      - pg-staging.example.com

Dasha searches for dasha.yaml in the working directory, $HOME/.dasha/ and /etc/dasha/. The password_from_env form keeps credentials out of the file. If listing databases by hand gets tedious, switch a cluster to discovery and Dasha will ask the server for its database list and keep it current:

discovery:
  production:
    type: postgres
    config:
      hosts: [pg-master.example.com, pg-replica-1.example.com]
      user: monitoring_user
      password_from_env: PROD_PG_PASSWORD
      bootstrap_db: postgres
      refresh_interval: 5
      db: ".*"
      exclude_db: "(template.*)"

Step 4 — Deploy it

cd deploy/compose
# edit dasha.yaml with your cluster settings
docker compose up -d
# then open http://localhost:3000

Multi-architecture images (amd64 and arm64) are published on every release as dbulashev/dasha-backend, dbulashev/dasha-frontend and dbulashev/dasha-mcp. On Kubernetes the chart is an OCI artifact:

helm install dasha oci://ghcr.io/dbulashev/charts/dasha --version 0.1.5

The chart renders dasha.yaml as a ConfigMap with passwords injected from an existing Secret or an ExternalSecret, and can run backend-only if you only want the API. Point the ingress at a hostname and enable TLS before any of this leaves a private network — the dashboard exposes query text.

Step 5 — Read the index recommendations safely

Candidates arrive with their column order, the statements they cover and each statement’s share of execution time, plus a ready CREATE INDEX CONCURRENTLY. Write counters are taken into account, and partitioned tables are handled through ATTACH PARTITION, so a candidate is not simply “this index is missing”.

Two guardrails before you paste anything into psql:

  • If a candidate carries a stale_statistics warning, run ANALYZE on that table first and regenerate the report. The recommendation was built on row estimates that are wrong.
  • Plan evidence is off by default. Turning on log_insights.index_advisor_evidence: true attaches sequential-scan counts and discarded rows from auto_explain plans, but the report then waits on your log store.
index_advisor:
  enabled: true
  max_queries: 500
  max_candidates: 50
  min_table_rows: 10000
  max_index_columns: 3
  timeout: 60s

Those defaults are sensible: tables under 10,000 rows yield no candidates, keys stop at three columns, and one report is capped at 60 seconds.

Step 6 — Run schema lint before the next incident

Dasha’s 17 structural checks catch what monitoring never sees: sequences close to exhaustion, tables with no primary or unique key, forgotten UNLOGGED relations, a public schema anyone can create objects in (the CVE-2018-1058 pattern), UUIDs stored as varchar, foreign keys that are nullable, NOT VALID constraints that were never validated, and reserved keywords used as object names. Each finding carries a severity and remediation SQL.

Silence the noise on day one rather than turning the page off:

schema_lint:
  disabled_checks: [uuid_in_non_uuid_type]
  ignore_schemas: ["_timescaledb*", "cron"]
  sequence_thresholds:
    error: 5
    warning: 10
    notice: 20

Sequence thresholds are percentages of values still free, so error: 5 fires when an integer sequence has less than 5% of its range left. That is the check that saves a Friday deploy.

Step 7 — Size the connection footprint

Dasha opens one connection pool per host and database. On a cluster with dozens of databases, narrow the list with db and exclude_db in discovery and check db_pool.max_conns before you point it at production — the monitoring role competes for the same connection slots as your application. The I/O page needs PostgreSQL 16 or newer; on 14 and 15 everything else still works.

Why per-host counters matter

Most PostgreSQL statistics are per-instance and are not replicated. An index that looks unused on the primary may be serving the entire read workload on a standby, so idx_scan = 0 on the primary is a poor reason to drop anything. Dasha reads every host of a cluster and reasons over the combined picture for exactly that reason, and the same rule applies to any index audit you run by hand before or after you install it.

Start with the schema-lint page — it needs no tuning and usually finds something real in the first ten minutes. Add index recommendations after pg_stat_statements is enabled fleet-wide and the counters have had a week to accumulate.

Sources

Leave A Comment