Query icon
OutputValues icon
Log icon
If icon
SlackIncomingWebhook icon
Fail icon
Pause icon
Loop icon
Schedule icon

Find and drop duplicate Couchbase GSI indexes that double every write

Group system:indexes by keyspace, keys and condition to find redundant online Couchbase GSI indexes, then drop the surplus behind a dry run and an approval.

Categories
DataInfrastructure

Two online GSI indexes covering the same keys on the same keyspace do the same work twice. Every mutation updates both, both hold their own copy in index RAM and the query service picks one of them, so the second is paid for on every write and never read. Couchbase reports no error for it. It happens after a rename that left the old index behind, a migration that was re-run or two people adding the same index. This blueprint groups system:indexes, keeps one index per group, drops the surplus behind a dry-run gate and a human approval, then re-reads system:indexes and fails the run if any group still has more than one member.

This blueprint was created by zkasuran.

Safety

Dropping an index is not reversible, so the rules are in the query and the task graph rather than only in this text.

  • A primary index is never a candidate. The detect query filters is_primary out. A separate query re-reads system:indexes for primary indexes whose name appears in the plan. One hit fails the run before any drop.
  • The last index on a keyspace is never dropped. Keeping one member per group already guarantees it. min_indexes_retained makes the floor explicit: the run fails if the online index count minus the planned drops would fall under it.
  • Only indexes whose state is online are grouped. An index that is still deferred or building is a different problem and is left alone.
  • The index condition is part of the group key. Two indexes on the same keys with different WHERE clauses are partial indexes serving different queries, not duplicates. Dropping one of them would delete a real optimisation, so they are never grouped together. partition and using are in the group key for the same reason.
  • Read-only by default. dry_run defaults to true and auto_remediate to false, so a plain run only reports. The DROP INDEX runs only when auto_remediate is true, dry_run is false and a human answers the Pause with approved: true. An unanswered resume reads as not approved.
  • The drop loop runs with concurrencyLimit: 1, so index drops are serialised rather than racing each other on the same keyspace.

How it works

  1. detect (io.kestra.plugin.couchbase.Query) groups system:indexes by keyspace, index_key, condition and partition, over online non-primary GSI indexes on the target keyspace. LETTING members = ARRAY_SORT(ARRAY_AGG(name)) plus HAVING ARRAY_LENGTH(members) > 1 leaves one row per duplicate group. members[0] is the index that is kept and members[1:] is the surplus, so the survivor is the same on every run.
  2. count_online counts every online index on the keyspace, primary included. That number feeds the floor check.
  3. plan (io.kestra.plugin.core.output.OutputValues) turns the group rows into the counts, the name lists and the drop targets the later tasks read.
  4. guard_primary asks Couchbase whether any planned name is a primary index. The answer comes from system:indexes rather than from the plan, so it is a real check and not a restatement.
  5. report (io.kestra.plugin.core.log.Log) logs the counts, the kept names, the surplus names and the full group rows.
  6. on_findings (io.kestra.plugin.core.flow.If) posts a Slack findings alert through io.kestra.plugin.slack.notifications.SlackIncomingWebhook whenever there is surplus. It fires on a dry run too.
  7. safety_guard fails the run with io.kestra.plugin.core.execution.Fail if a primary index is in the plan or if the keyspace would drop under min_indexes_retained. Nothing is dropped in that case.
  8. remediate runs only when there is surplus and auto_remediate is true and dry_run is false. approval_hold (io.kestra.plugin.core.flow.Pause) waits for a human. On approval drop_surplus (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) issues one DROP INDEX <name> ON <keyspace> per surplus index.
  9. reverify runs the grouping query again. reverify_gate fails the run if any group still has more than one member, so a partial drop never passes as healthy. On a declined approval the flow logs it and leaves the indexes alone. The errors block posts a Slack alert on any failure.

What you get

  • A scheduled read-only check that names the redundant indexes on a keyspace.
  • A survivor chosen by a rule, not at random, so two runs agree on what to keep.
  • A DROP INDEX remediation gated on dry run, an explicit flag and a human approval.
  • A re-read of system:indexes that fails the run when the drop did not land.
  • Partial indexes left alone, because the WHERE clause is part of the index identity.
  • Slack alerts on detection and on failure.

Who it's for

  • Couchbase operators who want index RAM and write amplification back without guessing.
  • Platform teams whose index DDL is applied by migrations that can be re-run.
  • On-call engineers looking for the write-path cost that no error message reports.

Why orchestrate this with Kestra

Couchbase does not warn about a redundant index. It builds it, maintains it on every mutation and keeps it resident. Finding the overlap needs a grouped read of system:indexes. Acting on it needs a judgement call about which copy survives. Kestra runs the read on a schedule, keeps the DROP INDEX behind an approval, re-reads the cluster to confirm the result and routes findings and failures to Slack. Every run is an execution you can replay with the plan it used.

Tested end to end

Verified on Kestra 2.0.5 against a real Couchbase cluster (couchbase/server:community) with the data, index and query services enabled, confirmed before and after with direct N1QL queries against system:indexes.

  • Healthy keyspace: the detector returned 0 groups, no alert fired, nothing was dropped.
  • Degraded keyspace with two identical indexes on the same key: the detector returned exactly one group with two members. The index on different keys and the two same-key indexes with different WHERE clauses were not in it.
  • dry_run=true with auto_remediate=true: the findings alert posted, no DROP INDEX ran and system:indexes was unchanged afterward.
  • dry_run=false with auto_remediate=true, approved: the surplus index was dropped, the re-read returned 0 groups and the keyspace kept the differently keyed index, both partial indexes and the primary index.
  • Approval declined: the flow logged the decline and the surplus index was still there afterward.
  • Floor refused: with min_indexes_retained above what the keyspace would keep, the run failed at safety_guard and nothing was dropped.
  • Re-verify failure: a second identical index created during the Pause meant the drop left a duplicate group behind, the re-read saw it and the run failed instead of reporting success.
  • Failure path: a wrong password failed detect and the errors block posted a Slack alert.

Prerequisites

Couchbase needs the query and index services enabled, otherwise system:indexes and DROP INDEX both fail. The user needs privileges to read system:indexes and to drop indexes on the target keyspace. Use _default for both scope and collection on a bucket that has no named scopes.

Secrets

  • COUCHBASE_PASSWORD: password for the Couchbase user. It is the default for the password input.
  • SLACK_WEBHOOK_URL: incoming webhook used by the findings alert and the error alert.

Inputs

  • connection_string: Couchbase connection string, for example couchbase://host1,host2.
  • username: Couchbase user with query and index privileges.
  • bucket: bucket holding the keyspace to inspect.
  • scope: scope inside the bucket. Defaults to _default.
  • collection: collection inside the scope. Defaults to _default.
  • password: user password, defaulting to the COUCHBASE_PASSWORD secret.
  • dry_run: report only when true. Defaults to true.
  • auto_remediate: drop the surplus when true and dry_run is false. Defaults to false.
  • min_indexes_retained: floor on the online index count after the drops. Defaults to 1.

Quick start

  1. Start Couchbase with the data, index and query services, then create a bucket.
  2. Create two indexes on the same key with different names so there is something to find.
  3. Set the COUCHBASE_PASSWORD and SLACK_WEBHOOK_URL secrets in Kestra.
  4. Run with dry_run true to see the groups, the kept name and the surplus names.
  5. Re-run with dry_run false and auto_remediate true, then answer the Pause with approved: true.
  6. Enable the schedule for a weekly safe-mode check.

How to extend

  • Loop the flow over every keyspace in a bucket by reading the collections first and calling this flow per keyspace as a subflow.
  • Keep the index with the most recent definition instead of the first name by sorting on your own naming convention in the LETTING clause.
  • Treat a prefix of a composite index as covered by the longer one, which is a wider definition of redundant than exact key equality.
  • Add the index num_replica to the group key when you run replicated indexes and want replica counts to differ.
  • Raise min_indexes_retained on a keyspace that must always keep a known number of indexes.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.