Couchbase SQL++ tuning
A skill for diagnosing and fixing slow SQL++ / N1QL queries on Couchbase Server (7.x and 8.x). The mechanics of reading execution plans, choosing the right index type, fixing common anti-patterns, and wiring up the diagnostic tools.
Distinct from the sibling skills:
couchbase-data-modeling — how to MODEL the data (document shape, boundaries, access patterns)
couchbase-sizing — how to size the cluster
couchbase-app-integration — how to write app code that uses Couchbase
couchbase-migration-execution — how to move data INTO Couchbase
couchbase-mcp — operating the cluster (the MCP server's cb_explain_query, cb_index_advisor, cb_perf_* tools)
couchbase-sqlpp-tuning (this skill) — making queries that already exist run faster
If the conversation is "this query is slow, what do I do," this is the right skill.
When this skill applies
- "Why is this query slow?"
- "How do I read this EXPLAIN plan?"
- "Why isn't my index being used?"
- "I'm seeing PrimaryScan / IntersectScan — is that bad?"
- "How do I make this a covering index?"
- "Should I use a partial index here?"
- "How do I index an array field with ANY / EVERY / UNNEST?"
- "ADVISE recommended this index — should I create it?"
- "How do I tune pagination — LIMIT / OFFSET is slow at high offsets"
- "What does kernTime / servTime / execTime mean in the profile?"
- "Can I force the optimizer to use a specific index?"
- "What do the cost-based optimizer hints do in 7.6+?"
Core principles (read first)
These are the headline rules. Read them before diving into references.
Pareto applies to query tuning. 80% of perf problems come from 20% of queries. Use system:completed_requests (or the MCP cb_perf_longest_running / cb_perf_most_frequent tools) to find that 20% first. Don't tune the wrong queries.
CBO needs stats to help; without them it falls back to rule-based logic. Couchbase has a cost-based optimizer (GA in 7.0, EE only), but it needs statistics on indexes and collections to do its job. Without stats, single-keyspace access is rule-based — index cardinality doesn't influence the choice, the optimizer picks based on which leading keys are in the WHERE clause. In 7.6+, statistics are gathered automatically when an index is created or built; in earlier versions, you run UPDATE STATISTICS manually. Either way: design indexes so the rules pick them, and run UPDATE STATISTICS on a schedule.
The leading key of the index must appear in the WHERE clause for an index to be picked. If a field can be missing, you need INCLUDE MISSING on the leading key, or you need IS NOT MISSING / IS NOT NULL in the WHERE clause to force selection.
Cover the query when it's hot. A covering index includes every field the query SELECTs and filters on, so the query never touches the Data service. Look for "covers": [...] in the EXPLAIN plan and the absence of a Fetch operator — that's the signal.
Don't index low-cardinality fields like docType alone. It causes IntersectScans and wrong plans. Use a partial index (WHERE type = 'X') instead — the field gates the index, but isn't the leading key.
Match the query shape to the index shape for arrays. ANY ... SATISFIES and ANY AND EVERY can use array indexes; bare EVERY cannot. UNNEST must use the exact same binding variable name as the CREATE INDEX ... FOR <var> IN ....
Avoid PrimaryScan in production. A PrimaryScan is the equivalent of a full table scan. Drop primary indexes in prod, or at least confirm no production query relies on one.
Pick the right reference
| Question |
Read |
| "How do I read this EXPLAIN plan? What's PrimaryScan / IntersectScan / Fetch?" |
references/explain-plan.md |
| "What kind of index should I create? Covering / partial / array / composite / vector?" |
references/index-design.md |
| "Why isn't my index being used? Common query anti-patterns and how to fix them" |
references/query-patterns.md |
| "What does the cost-based optimizer do? What are the hints?" |
references/cost-based-optimizer.md |
"How do I wire this up with the MCP server tools (cb_explain_query, cb_index_advisor, cb_perf_*)" |
references/diagnostic-workflow.md |
| "How do I do efficient pagination on a large result set?" |
references/pagination.md |
| "How do I tune queries that join across keyspaces?" |
references/joins-and-cbo.md |
Workflow
The general approach to tuning a slow query:
1. Identify → Find the slow query (Pareto: top-20% by frequency × duration)
Tools: cb_perf_longest_running, cb_perf_most_frequent,
system:completed_requests
2. Understand → Run EXPLAIN. Read the plan.
Tools: cb_explain_query (returns plan + parsed findings)
What to look for: PrimaryScan? IntersectScan? Fetch present?
Is the leading key of an index in WHERE?
3. Hypothesize → Pick one of:
- Add a covering index (everything in the index, no Fetch)
- Add a partial index (smaller, indexed on a subset)
- Add an array index (DISTINCT ARRAY ... FOR ... IN ... END)
- Reshape the query (drop OR predicates, add IS NOT MISSING)
- Add a USE INDEX hint
Tools: cb_index_advisor (ADVISE) for index recommendations
4. Verify → Re-run EXPLAIN. Check the new index is picked.
Run the query. Check kernTime / servTime / execTime in the profile.
Tools: cb_explain_query, cb_query with profile=on
5. Iterate → Repeat until the query meets SLA, or further tuning has
diminishing returns. Most queries reach acceptable performance
in 1-3 iterations.
Anti-pattern checklist
Quick scan list — if you see any of these, jump to references/query-patterns.md:
SELECT * from a large keyspace (forces Fetch, can't cover)
WHERE docType = 'X' as the leading filter (low-cardinality leading key)
WHERE NOT (...), !=, NOT IN predicates (often not sargable)
OR across different fields (often forces IntersectScan)
EVERY x IN arr SATISFIES ... END without ANY AND EVERY (no array index)
UNNEST binding variable not matching the index definition
LIMIT 10 OFFSET 1000000 (deep pagination — use KeySet pagination)
- Raw user input concatenated into the statement (injection + can't prepare)
- A query that runs thousands of times per second with no
PREPARE
Tooling
The MCP server in this project (celticht32/MCP-Couchbase) exposes the tuning tools you need:
| Tool |
Purpose |
cb_explain_query |
EXPLAIN + parsed plan findings |
cb_index_advisor |
ADVISE statement; recommends indexes |
cb_perf_longest_running |
Top N queries by duration |
cb_perf_most_frequent |
Top N queries by frequency |
cb_perf_largest_responses |
Queries returning the most bytes |
cb_perf_large_result_count |
Queries returning the most rows |
cb_perf_using_primary_index |
Queries hitting the primary index (BAD in prod) |
cb_perf_not_using_covering_index |
Queries that could be covered but aren't |
cb_perf_not_selective |
Queries where the WHERE filter doesn't narrow much |
cb_perf_by_user |
(8.x) attribute slow queries to a specific user |
cb_get_schema_for_collection |
Sample document schema (for designing the right index) |
See references/diagnostic-workflow.md for the full step-by-step using these tools.
Version notes
- Pre-7.0: No scope/collection — indexes are bucket-level
- 7.0: Scopes and collections; covering, partial, array indexes; CBO went GA (preview was in 6.5) — requires
UPDATE STATISTICS to be useful
- 7.1+:
INCLUDE MISSING for leading index keys (so docs without the leading-key field still get indexed)
- 7.6+: CBO auto-gathers stats on index create/build; join-enumeration improvements;
/*+ ... */ optimizer hints (productivity, ORDERED, USE HASH); UPDATE STATISTICS still available for manual refresh
- 8.0+: Vector indexes (HYPERSCALE / COMPOSITE VECTOR INDEX); Auto Update Statistics (AUS) keeps stats fresh automatically; FTS synonym sets; user lock/unlock; XDCR conflict logging
Always verify against the cluster version before recommending a feature — the MCP cb_mcp_status tool reports the version it has detected.
1---2name: couchbase-sqlpp-tuning3description: Diagnose and tune slow Couchbase SQL++ / N1QL queries. Use whenever the user asks about query performance, slow queries, EXPLAIN plans, why an index isn't being used, IntersectScan, PrimaryScan, covering indexes, partial indexes, array indexes (ANY / EVERY / UNNEST), index selection, query hints, the cost-based optimizer, the Index Advisor (ADVISE), system:completed_requests, query profiling (kernTime / servTime / execTime), pagination performance, prepared statements, or 'this query is slow / how do I make it faster.' Distinct from couchbase-data-modeling (document shape) and couchbase-mcp (operating the cluster) — this skill is about reading plans, designing the right indexes, and reshaping queries that already exist. Use proactively when the user shares an EXPLAIN output or a slow query.4license: MIT5---67# Couchbase SQL++ tuning89A skill for diagnosing and fixing slow SQL++ / N1QL queries on Couchbase Server (7.x and 8.x). The mechanics of reading execution plans, choosing the right index type, fixing common anti-patterns, and wiring up the diagnostic tools.1011Distinct from the sibling skills:12- `couchbase-data-modeling` — how to MODEL the data (document shape, boundaries, access patterns)13- `couchbase-sizing` — how to size the cluster14- `couchbase-app-integration` — how to write app code that uses Couchbase15- `couchbase-migration-execution` — how to move data INTO Couchbase16- `couchbase-mcp` — operating the cluster (the MCP server's `cb_explain_query`, `cb_index_advisor`, `cb_perf_*` tools)17- **`couchbase-sqlpp-tuning` (this skill)** — making queries that already exist run faster1819If the conversation is "this query is slow, what do I do," this is the right skill.2021## When this skill applies2223- "Why is this query slow?"24- "How do I read this EXPLAIN plan?"25- "Why isn't my index being used?"26- "I'm seeing PrimaryScan / IntersectScan — is that bad?"27- "How do I make this a covering index?"28- "Should I use a partial index here?"29- "How do I index an array field with ANY / EVERY / UNNEST?"30- "ADVISE recommended this index — should I create it?"31- "How do I tune pagination — LIMIT / OFFSET is slow at high offsets"32- "What does kernTime / servTime / execTime mean in the profile?"33- "Can I force the optimizer to use a specific index?"34- "What do the cost-based optimizer hints do in 7.6+?"3536## Core principles (read first)3738These are the headline rules. Read them before diving into references.39401. **Pareto applies to query tuning.** 80% of perf problems come from 20% of queries. Use `system:completed_requests` (or the MCP `cb_perf_longest_running` / `cb_perf_most_frequent` tools) to find that 20% first. Don't tune the wrong queries.41422. **CBO needs stats to help; without them it falls back to rule-based logic.** Couchbase has a cost-based optimizer (GA in 7.0, EE only), but it needs statistics on indexes and collections to do its job. Without stats, single-keyspace access is rule-based — index cardinality doesn't influence the choice, the optimizer picks based on which leading keys are in the WHERE clause. In 7.6+, statistics are gathered automatically when an index is created or built; in earlier versions, you run `UPDATE STATISTICS` manually. Either way: design indexes so the rules pick them, and run `UPDATE STATISTICS` on a schedule.43443. **The leading key of the index must appear in the WHERE clause** for an index to be picked. If a field can be missing, you need `INCLUDE MISSING` on the leading key, or you need `IS NOT MISSING` / `IS NOT NULL` in the WHERE clause to force selection.45464. **Cover the query when it's hot.** A covering index includes every field the query SELECTs and filters on, so the query never touches the Data service. Look for `"covers": [...]` in the EXPLAIN plan and the absence of a `Fetch` operator — that's the signal.47485. **Don't index low-cardinality fields like `docType` alone.** It causes IntersectScans and wrong plans. Use a partial index (`WHERE type = 'X'`) instead — the field gates the index, but isn't the leading key.49506. **Match the query shape to the index shape for arrays.** `ANY ... SATISFIES` and `ANY AND EVERY` can use array indexes; bare `EVERY` cannot. `UNNEST` must use the **exact same binding variable name** as the `CREATE INDEX ... FOR <var> IN ...`.51527. **Avoid PrimaryScan in production.** A PrimaryScan is the equivalent of a full table scan. Drop primary indexes in prod, or at least confirm no production query relies on one.5354## Pick the right reference5556| Question | Read |57|---|---|58| "How do I read this EXPLAIN plan? What's PrimaryScan / IntersectScan / Fetch?" | `references/explain-plan.md` |59| "What kind of index should I create? Covering / partial / array / composite / vector?" | `references/index-design.md` |60| "Why isn't my index being used? Common query anti-patterns and how to fix them" | `references/query-patterns.md` |61| "What does the cost-based optimizer do? What are the hints?" | `references/cost-based-optimizer.md` |62| "How do I wire this up with the MCP server tools (`cb_explain_query`, `cb_index_advisor`, `cb_perf_*`)" | `references/diagnostic-workflow.md` |63| "How do I do efficient pagination on a large result set?" | `references/pagination.md` |64| "How do I tune queries that join across keyspaces?" | `references/joins-and-cbo.md` |6566## Workflow6768The general approach to tuning a slow query:6970```711. Identify → Find the slow query (Pareto: top-20% by frequency × duration)72 Tools: cb_perf_longest_running, cb_perf_most_frequent,73 system:completed_requests74752. Understand → Run EXPLAIN. Read the plan.76 Tools: cb_explain_query (returns plan + parsed findings)77 What to look for: PrimaryScan? IntersectScan? Fetch present?78 Is the leading key of an index in WHERE?79803. Hypothesize → Pick one of:81 - Add a covering index (everything in the index, no Fetch)82 - Add a partial index (smaller, indexed on a subset)83 - Add an array index (DISTINCT ARRAY ... FOR ... IN ... END)84 - Reshape the query (drop OR predicates, add IS NOT MISSING)85 - Add a USE INDEX hint86 Tools: cb_index_advisor (ADVISE) for index recommendations87884. Verify → Re-run EXPLAIN. Check the new index is picked.89 Run the query. Check kernTime / servTime / execTime in the profile.90 Tools: cb_explain_query, cb_query with profile=on91925. Iterate → Repeat until the query meets SLA, or further tuning has93 diminishing returns. Most queries reach acceptable performance94 in 1-3 iterations.95```9697## Anti-pattern checklist9899Quick scan list — if you see any of these, jump to `references/query-patterns.md`:100101- `SELECT *` from a large keyspace (forces Fetch, can't cover)102- `WHERE docType = 'X'` as the leading filter (low-cardinality leading key)103- `WHERE NOT (...)`, `!=`, `NOT IN` predicates (often not sargable)104- `OR` across different fields (often forces IntersectScan)105- `EVERY x IN arr SATISFIES ... END` without `ANY AND EVERY` (no array index)106- `UNNEST` binding variable not matching the index definition107- `LIMIT 10 OFFSET 1000000` (deep pagination — use KeySet pagination)108- Raw user input concatenated into the statement (injection + can't prepare)109- A query that runs thousands of times per second with no `PREPARE`110111## Tooling112113The MCP server in this project (`celticht32/MCP-Couchbase`) exposes the tuning tools you need:114115| Tool | Purpose |116|---|---|117| `cb_explain_query` | EXPLAIN + parsed plan findings |118| `cb_index_advisor` | ADVISE statement; recommends indexes |119| `cb_perf_longest_running` | Top N queries by duration |120| `cb_perf_most_frequent` | Top N queries by frequency |121| `cb_perf_largest_responses` | Queries returning the most bytes |122| `cb_perf_large_result_count` | Queries returning the most rows |123| `cb_perf_using_primary_index` | Queries hitting the primary index (BAD in prod) |124| `cb_perf_not_using_covering_index` | Queries that could be covered but aren't |125| `cb_perf_not_selective` | Queries where the WHERE filter doesn't narrow much |126| `cb_perf_by_user` | (8.x) attribute slow queries to a specific user |127| `cb_get_schema_for_collection` | Sample document schema (for designing the right index) |128129See `references/diagnostic-workflow.md` for the full step-by-step using these tools.130131## Version notes132133- **Pre-7.0:** No scope/collection — indexes are bucket-level134- **7.0:** Scopes and collections; covering, partial, array indexes; CBO went GA (preview was in 6.5) — requires `UPDATE STATISTICS` to be useful135- **7.1+:** `INCLUDE MISSING` for leading index keys (so docs without the leading-key field still get indexed)136- **7.6+:** CBO auto-gathers stats on index create/build; join-enumeration improvements; `/*+ ... */` optimizer hints (`productivity`, `ORDERED`, `USE HASH`); `UPDATE STATISTICS` still available for manual refresh137- **8.0+:** Vector indexes (HYPERSCALE / COMPOSITE VECTOR INDEX); Auto Update Statistics (AUS) keeps stats fresh automatically; FTS synonym sets; user lock/unlock; XDCR conflict logging138139Always verify against the cluster version before recommending a feature — the MCP `cb_mcp_status` tool reports the version it has detected.