Query calls
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 withdfx 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
- Motoko
- Rust
- TypeScript
- Python
// Get the value of the counter.
public query func get() : async Nat {
return counter;
};
/// Get the value of the counter.
#[ic_cdk_macros::query]
fn get() -> Nat {
COUNTER.with(|counter| (*counter.borrow()).clone())
}
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:
Query | Update | Composite query |
---|---|---|
Cannot call other queries or composite queries | Can call other updates and queries ; Cannot call composite queries | Can call other queries and composite queries |
Can be called as an update | Cannot be called as a query | Cannot be called as an update |
Can call canisters on another subnet | Can call canisters on another subnet | Cannot 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
orput
function of that canister and parses the result.
- Motoko
- Rust
- TypeScript
- Python
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;
};
};
};
use ic_cdk::api::call::{call};
use ic_cdk::api::management_canister::main::{CreateCanisterArgument, create_canister, InstallCodeArgument, install_code, CanisterInstallMode};
use ic_cdk::api::management_canister::provisional::CanisterSettings;
use ic_cdk_macros::{query, update};
use candid::Principal;
use std::sync::Arc;
use std::sync::RwLock;
const NUM_PARTITIONS: usize = 5;
// Inline wasm binary of data partition canister
pub const WASM: &[u8] =
include_bytes!("../../target/wasm32-unknown-unknown/release/data_partition.wasm");
thread_local! {
// A list of canister IDs for data partitions
static CANISTER_IDS: Arc<RwLock<Vec<Principal>>> = Arc::new(RwLock::new(vec![]));
}
#[update]
async fn put(key: u128, value: u128) -> Option<u128> {
// Create partitions if they don't exist yet
if CANISTER_IDS.with(|canister_ids| {
let canister_ids = canister_ids.read().unwrap();
canister_ids.len() == 0
}) {
for _ in 0..NUM_PARTITIONS {
create_data_partition_canister_from_wasm().await;
}
}
let canister_id = get_partition_for_key(key);
ic_cdk::println!("Put in frontend for key={} .. using backend={}", key, canister_id.to_text());
match call(canister_id, "put", (key, value), ).await {
Ok(r) => {
let (res,): (Option<u128>,) = r;
res
},
Err(_) => None,
}
}
#[query(composite = true)]
async fn get(key: u128) -> Option<u128> {
let canister_id = get_partition_for_key(key);
ic_cdk::println!("Get in frontend for key={} .. using backend={}", key, canister_id.to_text());
match call(canister_id, "get", (key, ), ).await {
Ok(r) => {
let (res,): (Option<u128>,) = r;
res
},
Err(_) => None,
}
}
#[update]
async fn get_update(key: u128) -> Option<u128> {
let canister_id = get_partition_for_key(key);
ic_cdk::println!("Get as update in frontend for key={} .. using backend={}", key, canister_id.to_text());
match call(canister_id, "get", (key, ), ).await {
Ok(r) => {
let (res,): (Option<u128>,) = r;
res
},
Err(_) => None,
}
}
fn get_partition_for_key(key: u128) -> Principal {
let canister_id = CANISTER_IDS.with(|canister_ids| {
let canister_ids = canister_ids.read().unwrap();
canister_ids[lookup(key).0 as usize]
});
canister_id
}
#[query(composite = true)]
fn lookup(key: u128) -> (u128, String) {
let r = key % NUM_PARTITIONS as u128;
(r, CANISTER_IDS.with(|canister_ids| {
let canister_ids = canister_ids.read().unwrap();
canister_ids[r as usize].to_text()
}))
}
async fn create_data_partition_canister_from_wasm() {
let create_args = CreateCanisterArgument {
settings: Some(CanisterSettings {
controllers: Some(vec![ic_cdk::id()]),
compute_allocation: Some(0.into()),
memory_allocation: Some(0.into()),
freezing_threshold: Some(0.into()),
})
};
let canister_record = create_canister(create_args).await.unwrap();
let canister_id = canister_record.0.canister_id;
ic_cdk::println!("Created canister {}", canister_id);
let install_args = InstallCodeArgument {
mode: CanisterInstallMode::Install,
canister_id,
wasm_module: WASM.to_vec(),
arg: vec![],
};
install_code(install_args).await.unwrap();
CANISTER_IDS.with(|canister_ids| {
let mut canister_ids = canister_ids.write().unwrap();
canister_ids.push(canister_id);
});
}
Resources
The following example canisters demonstrate how to use composite queries:
Feedback and suggestions can be contributed on the forum.