Skip to main content

Query calls

Beginner
Tutorial

ICP supports two types of calls: updates and queries. Query calls, also referred to as non-replicated queries, are executed on a single node and return a synchronous response.

A query call discards state changes and typically executes on a single node. An update call is executed on all nodes and persists canister state changes. Since they execute on a single node, they do not go through consensus and can be much faster than update calls.

It is possible to execute a query call as an update. In such a case, the query still discards the state changes, but the execution happens on all nodes, and the result of execution goes through consensus. This “query-as-update” execution mode is also known as a replicated query.

Query calls are:

  • Fast (200-400ms).
  • Can't modify state.
  • Do not go through consensus.
  • Return a synchronous response.
  • Executed on a single node.
  • Currently free.

Making query calls with dfx

To make a query call to a canister, use the dfx canister call command with the --query flag:

  • dfx canister call --query <canister-name> <method_name>: Make a query call to a canister deployed locally. The local replica must be running to create a canister locally. Start it with dfx start --background.

  • dfx canister call --query <canister-name> <method_name> --network=playground: Make a query call to a canister deployed on the playground. Query calls are free, but canisters are temporary and will be removed after 20 minutes.

  • dfx canister call --query <canister-name> <method_name> --network=ic: Make a query call to a canister deployed on the mainnet. Query calls are free.

The downside of query calls is that the response is not trusted since it's coming from a single node. An update call or a certified query should be used for security-critical calls.

Making query calls from within canisters

  // Get the value of the counter.
public query func get() : async Nat {
return counter;
};

Query stats

The query stats feature gives developers information about the use of each canister's query calls. An approximation of some statistics related to query stats is made available to developers as part of the existing canister status API.

The statistics collected in the canister status are an approximation. They might not capture all query calls, especially for infrequently used canisters. Query stats are collected in intervals by the system and therefore may lag behind the actual query execution by up to 30 mins.

Statistics of composite queries are not currently collected.

Canister status entries

The feature exposes the following fields to the canister status endpoint:

  • A counter for the total number of query calls executed by that canister.
  • The sum of all instructions executed by the canister for query calls.
  • The sum of the payload sizes of all query requests to the canister.
  • The sum of the payload sizes of all query responses from the canister.

Each value represents the total count since the canister has been created. Rates for these values can be calculated from multiple calls to the canister status and observing the difference between the values in different calls. All values are monotonically increasing.

Retrieve query stats using dfx

dfx can be used to return a canister's query stats:

dfx canister status sample_canister

This will return output such as:

Canister status call result for sample_canister.
Status: Running
[..]
Number of queries: 0
Instructions spent in queries: 0
Total query request paylod size (bytes): 0
Total query response payload size (bytes): 0

Query stats are available via dfx since version 0.16.1.

Using query stats programmatically

It's also possible to programmatically retrieve query stats from within the canister via the canister status method. The following is an example of how to do this in Rust:

let canister_status = canister_status(CanisterIdRecord {
    canister_id: ic_cdk::id(),
})
.await.unwrap();
let query_stats = canister_status.0.query_stats;

Query stats are supported in the ic-cdk since version 0.12.1.

Composite queries

An update can call other updates and queries. However, a query cannot make any calls, which can hinder the development of scalable decentralized applications, especially those that shard data across multiple canisters.

Composite queries solve this problem. You can add composite queries to your canister using the following annotations:

  • Candid: composite_query
  • Motoko: composite query
  • Rust: #[query(composite = true)]

Users and the client-side JavaScript code can invoke a composite query endpoint of a canister using the same query URL for existing regular queries. In contrast to regular queries, a composite query can call other composite and regular queries. Due to limitations of the current implementation, composite queries have two restrictions:

QueryUpdateComposite query
Cannot call other queries or composite queriesCan call other updates and queries ; Cannot call composite queriesCan call other queries and composite queries
Can be called as an updateCannot be called as a queryCannot be called as an update
Can call canisters on another subnetCan call canisters on another subnetCannot call canisters on another subnet

Composite queries were enabled in the following releases:

Platform / Language        Version
Internet computer mainnet  Release 7742d96ddd30aa6b607c9d2d4093a7b714f5b25b    
Candid                    2023-06-30 (Rust 0.9.0)    
Motoko                    0.9.4, revision: 2d9902f    
Rust                      0.6.8    

Sample code

As an example, consider a partitioned key-value store, where a single frontend does the following for a put and get call:

  • First, it determines the ID of the data partition canister that holds the value with the given key.
  • Then, it makes a call into the get or put function of that canister and parses the result.
import Debug "mo:base/Debug";
import Array "mo:base/Array";
import Cycles "mo:base/ExperimentalCycles";
import Buckets "Buckets";

actor Map {

let n = 4; // number of buckets

// divide initial balance amongst self and buckets
let cycleShare = Cycles.balance() / (n + 1);

type Key = Nat;
type Value = Text;

type Bucket = Buckets.Bucket;

let buckets : [var ?Bucket] = Array.init(n, null);

public func getUpdate(k : Key) : async ?Value {
switch (buckets[k % n]) {
case null null;
case (?bucket) await bucket.get(k);
};
};

public composite query func get(k : Key) : async ?Value {
switch (buckets[k % n]) {
case null null;
case (?bucket) await bucket.get(k);
};
};

public func put(k : Key, v : Value) : async () {
let i = k % n;
let bucket = switch (buckets[i]) {
case null {
// provision next send, i.e. Bucket(n, i), with cycles
Cycles.add(cycleShare);
let b = await Buckets.Bucket(n, i); // dynamically install a new Bucket
buckets[i] := ?b;
b;
};
case (?bucket) bucket;
};
await bucket.put(k, v);
};

public func test() : async () {
var i = 0;
while (i < 16) {
let t = debug_show(i);
assert (null == (await getUpdate(i)));
Debug.print("putting: " # debug_show(i, t));
await Map.put(i, t);
assert (?t == (await getUpdate(i)));
i += 1;
};
};

};

Resources

The following example canisters demonstrate how to use composite queries:

Feedback and suggestions can be contributed on the forum.