# Query Optimization Write efficient GraphQL queries that return exactly what you need, faster. ## Thinking in Graphs GraphQL is designed around a graph data model. In Kadindexer, entities like `Block`, `Transaction`, `Account`, and `Transfer` are interconnected nodes. Query these relationships in a single request rather than making multiple round trips. **Example: Account with nested data** ```graphql query { fungibleAccount( accountName: "k:5a2afbc4564b76b2c27ce5a644cab643c43663835ea0be22433b209d3351f937" fungibleName: "coin" ) { totalBalance transactions(first: 10) { edges { node { hash cmd { meta { creationTime } } } } } transfers(first: 10) { edges { node { amount receiverAccount } } } } } ``` This single query replaces 3 separate API calls. ## Request Only What You Need Every field you request adds to response size and processing time. Be explicit about fields. **❌ Over-fetching:** ```graphql query { transactions(first: 10) { edges { node { hash cmd { meta { chainId creationTime gasLimit gasPrice sender ttl } networkId nonce payload { ... on ExecutionPayload { code data } } } result { ... on TransactionResult { gas goodResult badResult logs } } } } } } ``` **✅ Optimized:** ```graphql query { transactions(first: 10) { edges { node { hash cmd { meta { sender creationTime } } } } } } ``` **Impact:** 85% smaller payload, 4x faster parsing. ## Pagination Always paginate large result sets. Kadindexer uses cursor-based pagination following the [Relay specification](https://relay.dev/graphql/connections.htm). **Pattern:** ```graphql query GetTransactions($cursor: String) { transactions(first: 50, after: $cursor) { edges { node { hash } cursor } pageInfo { hasNextPage endCursor } } } ``` **Best practices:** - Start with `first: 50` (optimal balance) - Maximum `first: 100` per request - Use `totalCount` sparingly (adds complexity cost) - Store `endCursor` for subsequent pages **Example: Paginating through account transfers** ```javascript let cursor = null; let allTransfers = []; do { const result = await client.request(query, { accountName: "k:abc...", cursor }); allTransfers = allTransfers.concat( result.transfers.edges.map(e => e.node) ); cursor = result.transfers.pageInfo.endCursor; } while (result.transfers.pageInfo.hasNextPage); ``` ## Server-Side Filtering Push filtering logic to the server rather than fetching everything and filtering client-side. **❌ Client-side:** ```javascript const all = await client.request(` query { transactions(first: 1000) { edges { node { hash cmd { meta { sender chainId } } } } } } `); const filtered = all.transactions.edges .filter(e => e.node.cmd.meta.sender === "k:abc..." && e.node.cmd.meta.chainId === "1"); ``` **✅ Server-side:** ```graphql query { transactions( accountName: "k:5a2afbc4564b76b2c27ce5a644cab643c43663835ea0be22433b209d3351f937" chainId: "1" first: 50 ) { edges { node { hash } } } } ``` **Available filters:** | Query | Key Filters | | --- | --- | | `transactions` | `accountName`, `chainId`, `minHeight`, `maxHeight`, `blockHash`, `requestKey`, `fungibleName` | | `events` | `qualifiedEventName`, `chainId`, `minHeight`, `maxHeight`, `blockHash`, `requestKey`, `minimumDepth` | | `transfers` | `accountName`, `chainId`, `blockHash`, `requestKey`, `fungibleName`, `isNFT` | | `blocksFromHeight` | `chainIds`, `startHeight`, `endHeight` | | `blocksFromDepth` | `chainIds`, `minimumDepth` | ## Use Variables for Safety Never interpolate user input directly into queries. Use GraphQL variables. **❌ String interpolation (SQL injection risk):** ```javascript const query = ` query { fungibleAccount(accountName: "${userInput}") { balance } } `; ``` **✅ Parameterized:** ```graphql query GetAccount($accountName: String!, $chainId: String!) { fungibleChainAccount( accountName: $accountName chainId: $chainId fungibleName: "coin" ) { balance guard { ... on KeysetGuard { keys predicate } } } } ``` ```javascript const result = await client.request(query, { accountName: userInput, chainId: "1" }); ``` ## Query Complexity Management Kadindexer enforces complexity limits using the `@complexity` directive. Deeply nested queries with large pagination multipliers can exceed limits. **Complexity formula:** ``` complexity = base_value × (multiplier1 × multiplier2 × ...) ``` **Example: High complexity query** ```graphql query { blocks(first: 100) { # complexity: 100 edges { node { transactions(first: 100) { # complexity: 100 × 100 = 10,000 edges { node { result { ... on TransactionResult { events(first: 100) { # complexity: 10,000 × 100 = 1,000,000! edges { node { parameters } } } } } } } } } } } } ``` **Response:** `400 Query complexity exceeds maximum` **Solutions:** 1. **Reduce pagination sizes:** ```graphql query { blocks(first: 20) { edges { node { transactions(first: 20) { edges { node { hash } } } } } } } ``` 1. **Split into multiple queries:** ```graphql # Query 1: Get blocks query { blocks(first: 20) { edges { node { hash } } } } # Query 2: Get transactions for specific block query { block(hash: "ABC...") { transactions(first: 50) { ... } } } ``` 1. **Use targeted queries:** ```graphql # Instead of blocks -> transactions -> events # Query events directly with filters query { events( qualifiedEventName: "coin.TRANSFER" minHeight: 5000000 first: 50 ) { edges { node { parameterText block { height } } } } } ``` **Guidelines:** - Maximum query depth: 5 levels - Pagination limit: 100 items per request - Avoid nesting multiple paginated lists - Prefer direct queries over deep nesting ## Batching with Aliases Combine multiple queries into one request using aliases. **Example: Multi-chain dashboard** ```graphql query DashboardData { chain0: transactions(chainId: "0", first: 5, minHeight: 5000000) { edges { node { hash cmd { meta { creationTime } } } } } chain1: transactions(chainId: "1", first: 5, minHeight: 5000000) { edges { node { hash cmd { meta { creationTime } } } } } networkStats: networkInfo { transactionCount coinsInCirculation } accountBalance: fungibleChainAccount( accountName: "k:5a2afbc4564b76b2c27ce5a644cab643c43663835ea0be22433b209d3351f937" chainId: "1" fungibleName: "coin" ) { balance } } ``` **Benefits:** - 1 HTTP request instead of 4 - Lower rate limit consumption - Consistent snapshot of data ## Global Object Identification All major types implement the `Node` interface with a globally unique `id`. Use this for caching and efficient lookups. **Example: Direct node lookup** ```graphql query { node(id: "VHJhbnNhY3Rpb246MTIzNDU2") { ... on Transaction { hash cmd { meta { sender } } } } } ``` **Example: Batch node lookup** ```graphql query { nodes(ids: [ "QmxvY2s6YWJjMTIz", "VHJhbnNhY3Rpb246eHl6Nzg5" ]) { ... on Block { hash height } ... on Transaction { hash } } } ``` This enables efficient client-side caching strategies (e.g., Apollo Client, urql). ## Performance Targets Monitor query performance and optimize when thresholds are exceeded: | Query Type | Target | Warning Threshold | | --- | --- | --- | | Simple account query | <200ms | >500ms | | Paginated list (50 items) | <300ms | >800ms | | Complex nested query | <500ms | >1000ms | | Historical range scan | <800ms | >2000ms | **Tracking performance:** ```javascript const start = Date.now(); const result = await client.request(query, variables); const duration = Date.now() - start; if (duration > 1000) { console.warn('Slow query', { duration, queryName: query.definitions[0].name.value, variables }); } ``` ## Common Mistakes ### 1. Fetching without pagination ```graphql # ❌ Could return millions of records query { transactions { edges { node { hash } } } } # ✅ Always paginate query { transactions(first: 50) { edges { node { hash } } } } ``` ### 2. Sequential dependent queries ```javascript // ❌ N+1 problem for (const chainId of ['0', '1', '2']) { await getChainData(chainId); } // ✅ Use batching with aliases query { c0: transactions(chainId: "0", first: 10) { ... } c1: transactions(chainId: "1", first: 10) { ... } c2: transactions(chainId: "2", first: 10) { ... } } ``` ### 3. Ignoring `minimumDepth` ```graphql # ❌ May return non-finalized blocks query { blocks(first: 10) { ... } } # ✅ Ensure finality query { blocksFromDepth(chainIds: ["0"], minimumDepth: 20, first: 10) { ... } } ``` ### 4. Over-requesting nested data ```graphql # ❌ Fetching full transaction details for every transfer query { transfers(first: 50) { edges { node { amount transaction { cmd { ... } # Heavy nested object result { ... } # Heavy nested object } } } } } # ✅ Request only what's needed query { transfers(first: 50) { edges { node { amount requestKey # Lighter - just the hash } } } } ``` ## Checklist Before deploying queries: - [ ] Only selected necessary fields - [ ] Pagination implemented (`first: 50`) - [ ] Server-side filters applied (chainId, accountName, minHeight) - [ ] Query variables used for all user input - [ ] Query complexity tested with realistic data - [ ] Related queries batched with aliases - [ ] N+1 patterns avoided with nested queries - [ ] Query depth under 5 levels - [ ] `minimumDepth` used for finalized data ## Next Steps - **Scale your queries:** [Performance & Scaling →](/guides/advanced/performance-scaling) - **Deploy safely:** [Production Readiness →](/guides/advanced/production-readiness) - **Explore schema:** [GraphQL API Reference →](https://docs.kadindexer.io/apis) **Need help?** [toni@hackachain.io](mailto:toni@hackachain.io)