Query
- how-to
You can query for documents in Couchbase using the SQL++ query language. SQL++ (formerly N1QL) is based on SQL, but designed for structured and flexible JSON documents.
Before You Start
You should know how to write a SQL++ query.
You should know how to connect to a Couchbase cluster.
You should know about documents and collections.
The examples on this page use the travel-sample
sample bucket.
Searching the Default Collection
This example uses a SQL++ query to get 10 documents from the default collection in the travel-sample
bucket.
val queryResult: QueryResult = cluster
.query("SELECT * FROM `travel-sample` LIMIT 10")
.execute() (1)
queryResult.rows.forEach { row: QueryRow ->
println("Found row: " + row.contentAs<Map<String, Any?>>())
}
1 | The query method returns a Flow<QueryFlowItem> .
Nothing happens until you collect the flow.
Calling execute is an easy way to collect the flow. |
Buckets and Queries Before Couchbase 6.5
If you use a version of Couchbase before 6.5, you must open a bucket before doing a query.
It does not need to be the bucket you are searching.
If you forget to open a bucket, the SDK throws |
Searching a Non-Default Collection
The travel-sample
bucket has a scope called inventory
.
This scope has copies of the documents from the default collection, organized into collections.
This example gets 10 documents from the airline
collection in the inventory
scope.
There are two versions of the example.
Both versions do the same thing, but one uses Cluster.query
, and the other uses Scope.query
.
Look at both to see the difference.
-
Cluster.query
-
Scope.query
If you use Cluster.query
to search a non-default collection, the FROM
clause must have the bucket name, scope name, and collection name.
val queryResult: QueryResult = cluster
.query("""
SELECT *
FROM `travel-sample`.inventory.airline
LIMIT 10
""")
.execute()
queryResult.rows.forEach { row: QueryRow ->
println("Found row: " + row.contentAs<Map<String, Any?>>())
}
If you use Scope.query
to search a non-default collection, the FROM
clause does not need the bucket name or scope name.
Instead, this information comes from the Scope
object.
val scope: Scope = cluster
.bucket("travel-sample")
.scope("inventory")
val queryResult: QueryResult = scope
.query("SELECT * FROM airline LIMIT 10")
.execute()
queryResult.rows.forEach { row: QueryRow ->
println("Found row: " + row.contentAs<Map<String, Any?>>())
}
Query Parameters
A "query parameter" is like a variable in a SQL++ statement. Query parameters protect you from SQL injection. They also help the server parse and plan the query.
You can give parameters names, or refer to them by position.
Some parts of a SQL++ statement cannot be parameters. If you use a parameter where it is not allowed, the SDK throws an exception. |
Named parameters
Using named parameters often make it easier to read complex queries.
val queryResult: QueryResult = cluster
.query(
statement = """
SELECT *
FROM `travel-sample`.inventory.airline
WHERE country = ${"\$country"} (1)
""",
parameters = QueryParameters.named(
"country" to "France"
)
)
.execute()
1 | In this example, the WHERE condition is country = $country .
Unfortunately, Kotlin doesn’t have a nice way to escape a dollar sign ($ ) in multi-line strings. |
When using named parameters, always escape the dollar sign ($ ) in the placeholder name.
Otherwise, Kotlin does string interpolation, which does not prevent SQL injection.
|
Positional parameters
If you use positional parameters, the order of the parameters must match the order of the question mark (?
) placeholders in the statement.
val queryResult: QueryResult = cluster
.query(
statement = """
SELECT *
FROM `travel-sample`.inventory.airline
WHERE country = ?
""",
parameters = QueryParameters.positional(
listOf("France")
)
)
.execute()
Metadata
The server returns more than just rows.
It also returns information about how the query was executed, how long it took, and any bad things that happened.
This information is called "query metadata." A QueryMetadata
object holds this information.
It is expensive to return some kinds of metadata, like metrics and profiling information. If you want that information, you must ask for it when doing the query.
This example asks for all metadata, and prints it:
val queryResult: QueryResult = cluster
.query(
statement = "SELECT * FROM `travel-sample`.inventory.airline",
metrics = true,
profile = QueryProfile.TIMINGS,
)
.execute()
val metadata: QueryMetadata = queryResult.metadata
println("Client context ID: ${metadata.clientContextId}")
println("Request ID: ${metadata.requestId}")
println("Signature: ${metadata.signature}")
println("Status: ${metadata.status}")
println("Warnings: ${metadata.warnings}")
metadata.metrics?.let { metrics: QueryMetrics ->
println("Reported execution time: ${metrics.executionTime}")
println("Other metrics: $metrics")
}
metadata.profile?.let { profile: Map<String, Any?> ->
println("Profile: $profile")
}
Streaming
The previous examples store all result rows in memory. If there are many rows, this can use a lot of memory.
To use less memory, pass a lambda to execute
and work on each row one at a time, like this:
val metadata: QueryMetadata = cluster
.query("SELECT * FROM `travel-sample`.inventory.airline")
.execute { row: QueryRow ->
println("Found row: " + row.contentAs<Map<String, Any?>>())
}
The streaming version of execute returns QueryMetadata instead of QueryResult .
|
Prepared Statements
Each time you execute a query, the server makes a plan for finding the results. You can ask the server to remember the plan. This turns your query into a "prepared statement."
To run a query as a prepared statement, pass adhoc = false
to the query
method, like this:
val queryResult: QueryResult = cluster
.query(
statement = "SELECT * FROM `travel-sample` LIMIT 10",
adhoc = false,
)
.execute()
A prepared statement is not always faster than an adhoc query. Sometimes the server can make a better plan for an adhoc query than for a prepared statement. It’s good to measure the performance of your query, so you know if it’s good to use a prepared statement.
Read-Only Queries
If a query does not change data, it’s good to tell the SDK the query is "read-only."
The server ensures a read-only query does not change data. If a read-only query fails, the SDK retries it because it knows it’s safe to retry.
To run a read-only query, pass readonly = true
to the query
method, like this:
val queryResult: QueryResult = cluster
.query(
statement = "SELECT * FROM `travel-sample` LIMIT 10",
readonly = true,
)
.execute()
Scan Consistency
When you change a document in Couchbase, it takes time for the Query service to index the document. A query index "runs behind" the KV service. When you execute a query, you get to choose if you want to wait for the index to "catch up" to the latest KV changes.
Unbounded
By default, the Query service does not wait. It only searches documents that were already indexed when the query started. This is called "unbounded" scan consistency.
This is the default value for the query
method’s consistency
parameter.
Request Plus
When you choose "request plus" scan consistency, changes that happened before you called execute
are always reflected in the query results.
"Request plus" is more expensive than "unbounded", because the server must wait for the query index to catch up.
(The "plus" in "request plus" means changes that happened after you called execute
might be reflected in the results, too.)
Consistent With
If you made some changes, you can tell the server to wait for the changes to be indexed. In other words, the query results are "consistent with" the changes you made. To use this kind of scan consistency, you must keep track of the mutation tokens from the changes you want to wait for.
val collection = cluster
.bucket("travel-sample")
.scope("inventory")
.collection("airline")
val mutationResult: MutationResult =
collection.upsert("my-fake-airline", mapOf("id" to 9000))
val mutationState = MutationState()
mutationState.add(mutationResult)
val queryResult: QueryResult = cluster
.query(
statement = "SELECT * FROM `travel-sample`.inventory.airline LIMIT 10",
consistency = QueryScanConsistency
.consistentWith(mutationState)
)
.execute()
Client Context ID
Each query has a "client context ID" that helps with debugging. If you know a query’s ID, you can search for the ID in the Couchbase Server logs or a network trace.
The ID can be any string. It’s good for the ID to be different every time you execute a query. If you do not set the ID, the SDK uses a different random UUID every time.
In this example, the client context ID includes the name of an application user, so it’s easy to search for all queries related to the user.
val queryResult: QueryResult = cluster
.query(
statement = "SELECT * FROM `travel-sample` LIMIT 10",
clientContextId = "user-44-" + UUID.randomUUID(),
)
.execute()