Skip to content

Browsing an un-indexed Couchbase collection

On recent server versions a sequential scan makes an un-indexed keyspace open slowly, so a primary index is a recommendation now rather than a prerequisite.

Opening a Couchbase collection that has no index does one of two things, and which one happens is decided by the version of the cluster rather than by anything in the statement. On Server 7.0 to 7.2 it fails with error 4000. On Server 7.6 and later it returns rows, and takes its time about it.

What happens when you open a collection with no index

Clicking a collection in the schema tree runs a generated statement. On Couchbase that statement projects the document key explicitly, because SELECT * nests the document under the keyspace name and leaves the key out of the result entirely:

SELECT META(d).id AS __id, d.*
FROM `travel`.`inventory`.`hotel` AS d
LIMIT 50;

Nothing in that statement names an index. Whether it can be answered at all is a question the Query Service resolves on its own, and until Server 7.6 the answer on a keyspace with no index was no.

Before anything runs, the explorer has already told you the state you are in. getSchemaRelations() reads system:indexes, so an un-indexed collection shows an empty index list next to it before you press anything. That read is the one monitoring source here that is deliberately not allowed to degrade to empty on failure: everywhere else a denied catalog read falls back to nothing, but an empty index list is the un-indexed signal, and silently faking it for the whole bucket would be worse than an error.

The fallback that changed the answer

From Server 7.6 the Query Service answers an index-less keyspace with a sequential scan, which uses a KV range scan underneath to enumerate keys. So SELECT, the write statements and JOIN all succeed with no primary and no secondary index present. Verified on Community Edition 8.0.2: selecting from a collection that has no index at all returns rows, and EXPLAIN names the fallback rather than leaving you to infer it:

{ "#operator": "PrimaryScan3", "index": "#sequentialscan", "using": "sequentialscan" }

That is the evidence, and it is worth clicking Explain to see it, because the plan is the only place the fallback is visible. The result grid looks the same either way; it is just slower to fill.

EXPLAIN is available on this engine and renders a plan tree, but note what it is: an estimate, in both modes. SQL++ has no EXPLAIN ANALYZE. Real timings come only from the request-level profile: "timings" parameter, which the explain strategy does not emit by design, so the direct Explain action and the background pre-warm show the same estimated plan. A cost or cardinality of -1 is read as “no estimate” rather than as a number.

Where the hard failure still applies

On Server 7.0 to 7.2 there is no sequential scan, so the same statement comes back as error 4000, “No index available on keyspace”. This is the limit, and it does not soften with a newer client: the fallback is a server feature, and a provider that speaks the documented REST endpoints cannot supply one the cluster does not have. On those lines a primary index is a prerequisite, not a recommendation, and a collection without one cannot be browsed at all.

What the provider does instead is refuse usefully. Error 4000 is re-raised as a QueryError carrying the remedy already quoted for the exact keyspace the statement read from:

No index available on keyspace `travel`.`inventory`.`hotel` that matches your query.
Create one first: CREATE PRIMARY INDEX ON `travel`.`inventory`.`hotel`

The second line is runnable. Paste it into the editor and press Run. Getting the backticks right matters more here than it looks: SQL++ has no bind parameter for identifiers, so keyspace paths are assembled by concatenation, and quoteIdentifier() doubles embedded backticks so an identifier cannot terminate its own quoting. bucket and scope are also reserved words in SQL++ - an unquoted projection over system:keyspaces fails with error 3000, verified on Server 8.0.2.

Error 4000 is one row in a map that turns the cluster’s single numeric error space into the shared error classes:

Code Meaning What you see
4000 No index available on keyspace Query error plus the runnable CREATE PRIMARY INDEX remedy
3000 Syntax error Query error carrying the cluster’s own message
1080 Request timeout Timeout error
13014 Missing or invalid credentials Authentication error
503 Query service unavailable, node warming up Connection error

One thing that map depends on: HTTP 200 does not mean success. The Query Service returns syntax and semantic errors inside a 200 response with status: "errors", so the transport inspects the payload before the HTTP code. Skipping that check reports a failed statement as “0 rows”, which is exactly the shape an un-indexed collection would otherwise take.

Reading one document with no index at all

If you know the key, you do not need an index on any supported version. USE KEYS bypasses index lookup and reads straight from KV:

SELECT META(d).id AS __id, d.*
FROM `travel`.`inventory`.`hotel` AS d
USE KEYS ["hotel::1"];

That succeeds on a keyspace that has no index whatsoever, on 7.0 as much as on 8.0. Error 4000 never fires for it. This is worth knowing before creating an index just to look at one document during an incident.

The boundary is precise, and it is the useful half of the sentence: what still needs an index is discovering keys you do not already know. USE KEYS answers “show me this document”. It cannot answer “show me the documents where city is Istanbul”, and no amount of key-shaped cleverness turns it into that.

When to create the index anyway

Both halves of the version story point the same way.

On 7.0 to 7.2 you have no choice. On 7.6 and later the choice is real but it is not close: a sequential scan is not optimised for throughput and degrades sharply on large collections, to the point of query timeouts. Creating an index remains the right thing to do for anything beyond a small or throwaway collection. It is a recommendation now, not a prerequisite - and a recommendation you should usually take.

CREATE PRIMARY INDEX ON `travel`.`inventory`.`hotel`;

A primary index carries no index_key, so it is reported with the synthetic column META().id, and unique is true only for primary indexes, because no global secondary index enforces uniqueness. After it builds, EXPLAIN stops saying #sequentialscan, which is the confirmation to look for rather than a stopwatch.

Two things about this engine make the un-indexed case a common one.

Columns here are inferred, not declared: schema loading runs INFER <keyspace> WITH {"sample_size": 100} per collection. A failed INFER yields empty columns rather than an error, because both common causes - no SELECT on the collection, and an empty collection, error 7014 - are states the explorer should render rather than fail on. A brand-new collection therefore appears in the tree with no columns and no indexes, which is the exact shape the scan fallback exists to make browsable.

And every statement is sent with scan_consistency: "request_plus", so you always see your own writes. The measurement behind that default, taken against Server 8.0.2: immediately after an INSERT, a SELECT returned zero rows while COUNT(*) already returned three, and the same SELECT returned three rows seconds later. The trade-off is stated rather than hidden - request_plus makes the query wait for the index to catch up, which costs latency on a write-heavy cluster, and callers opt out per statement with not_bounded.

Everything above is the un-indexed story only. The other Couchbase boundaries - one bucket per connection, no inline row editing, no transactions over stateless HTTP, no foreign keys and therefore no ER diagram edges, and Agent AUTO mode ending engine-unsupported because the read-only profile is database-native and exists only on PostgreSQL, SQLite and DuckDB - are published on the engine pages, beside what each engine’s transport and default port actually are. The rule that produces those lines, and what it costs to keep, is on the features page.