Skip to main content
Querying is how applications retrieve indexed blockchain data from Graph Node. Graph Node automatically generates a GraphQL API for each subgraph and translates queries into optimized SQL that executes against PostgreSQL.

Query Overview

Graph Node provides a rich GraphQL API for each deployed subgraph without requiring any additional code. The API is automatically generated from the subgraph’s schema and supports:
  • Filtering: Filter entities by any field
  • Sorting: Order results by any field
  • Pagination: Efficient cursor-based and offset-based pagination
  • Nested queries: Load related entities in a single query
  • Time-travel: Query historical state at any block
  • Full-text search: Search text fields with ranking
  • Aggregations: Compute sums, counts, averages, etc.
All queries are read-only. Graph Node does not support mutations via GraphQL - data is only modified by indexing blockchain events.

Query Endpoints

Subgraphs are queryable via HTTP POST to:
http://localhost:8000/subgraphs/name/<subgraph-name>
http://localhost:8000/subgraphs/id/<deployment-ipfs-hash>

GraphQL Playground

Open the endpoint in a browser to access GraphiQL, an interactive query playground with:
  • Schema documentation
  • Autocomplete
  • Query validation
  • Result visualization

Query Structure

Queries follow standard GraphQL syntax and are automatically validated against the subgraph’s schema.

Basic Query

{
  users(first: 10, where: {balance_gt: "1000000000000000000"}) {
    id
    address
    balance
    createdAt
  }
}

Nested Queries

Load related entities in a single request:
{
  users(first: 5, orderBy: balance, orderDirection: desc) {
    id
    address
    balance
    transactions(first: 10, orderBy: timestamp, orderDirection: desc) {
      id
      hash
      value
      timestamp
      to {
        id
        address
      }
    }
  }
}

Query by ID

Fetch a specific entity by its ID:
{
  user(id: "0x1234...") {
    id
    address
    balance
  }
}

Query Features

Filtering

Graph Node generates comprehensive filter arguments for every entity field:
Available for numeric, string, and byte fields:
  • field: Equals
  • field_not: Not equals
  • field_gt: Greater than
  • field_gte: Greater than or equal
  • field_lt: Less than
  • field_lte: Less than or equal
  • field_in: In array
  • field_not_in: Not in array
{
  users(where: {
    balance_gte: "1000000000000000000"
    balance_lt: "10000000000000000000"
  }) {
    id
    balance
  }
}
Additional operators for string fields:
  • field_contains: Contains substring
  • field_not_contains: Does not contain substring
  • field_starts_with: Starts with prefix
  • field_not_starts_with: Does not start with prefix
  • field_ends_with: Ends with suffix
  • field_not_ends_with: Does not end with suffix
{
  tokens(where: {
    name_contains: "USD"
    symbol_starts_with: "W"
  }) {
    id
    name
    symbol
  }
}
Combine multiple conditions:
  • and: All conditions must match (implicit)
  • or: At least one condition must match
{
  users(where: {
    or: [
      { balance_gt: "10000000000000000000" }
      { transactions_: { value_gt: "1000000000000000000" } }
    ]
  }) {
    id
    balance
  }
}
Implementation: store/postgres/src/relational_queries.rs translates to SQL WHERE clause
Filter by related entity fields using _ suffix:
{
  transactions(where: {
    from_: { balance_gt: "1000000000000000000" }
    to_: { address: "0x1234..." }
  }) {
    id
    value
    from {
      address
    }
    to {
      address
    }
  }
}
SQL generation: Generates JOINs to related tables

Sorting

Sort results by any field:
{
  users(
    orderBy: balance
    orderDirection: desc
    first: 10
  ) {
    id
    balance
  }
}
Parameters:
  • orderBy: Field to sort by
  • orderDirection: asc (ascending) or desc (descending)
Multi-field sorting: Not yet supported in API, but planned

Pagination

Graph Node supports two pagination strategies:
Use first and skip for offset-based pagination:
# Page 1
{ users(first: 10, skip: 0) { id } }

# Page 2
{ users(first: 10, skip: 10) { id } }

# Page 3
{ users(first: 10, skip: 20) { id } }
Limitation: Large offsets are inefficient (must scan all skipped rows)Best for: Small datasets or low page numbers
Use first with where filter for efficient pagination:
# Page 1
{
  users(first: 10, orderBy: id) {
    id
    address
  }
}

# Page 2 (using last ID from page 1)
{
  users(
    first: 10
    orderBy: id
    where: { id_gt: "last_id_from_page_1" }
  ) {
    id
    address
  }
}
Benefit: Consistent performance regardless of page depthBest for: Large datasets and deep pagination

Time-Travel Queries

Query historical state at any block number:
{
  users(
    block: { number: 15000000 }
    first: 10
  ) {
    id
    balance  # Balance at block 15000000
  }
}
How it works: Block range filter in SQL:
WHERE block_range @> 15000000
This leverages the block range stored with each entity version. Location in codebase: store/postgres/src/block_range.rs Search text fields with relevance ranking (when enabled in schema):
{
  articleSearch(text: "blockchain ethereum") {
    id
    title
    content
    rank  # Relevance score
  }
}
Schema definition:
type Article @entity {
  id: ID!
  title: String! @fulltext(name: "articleSearch", language: en)
  content: String! @fulltext(name: "articleSearch")
}
Implementation: Uses PostgreSQL full-text search features Reference: docs/aggregations.md

Aggregations

Compute aggregate values across entities:
{
  userAggregates {
    count
    sum {
      balance
    }
    avg {
      balance
    }
    min {
      balance
    }
    max {
      balance
    }
  }
}
Available aggregations:
  • count: Number of entities
  • sum: Sum of numeric fields
  • avg: Average of numeric fields
  • min: Minimum value
  • max: Maximum value
Reference: docs/aggregations.md

Meta Field

Query indexing metadata:
{
  _meta {
    block {
      number
      hash
      timestamp
    }
    deployment
    hasIndexingErrors
  }
}
Use cases:
  • Check indexing progress
  • Verify data freshness
  • Detect indexing errors

Query Processing Pipeline

Graph Node translates GraphQL queries into SQL through several stages:
┌─────────────────┐
│  GraphQL Query  │
└────────┬────────┘


┌─────────────────┐
│  Parse & Validate│
└────────┬────────┘


┌─────────────────┐
│  Query Planning │
└────────┬────────┘


┌─────────────────┐
│ SQL Generation  │
└────────┬────────┘


┌─────────────────┐
│  Execute Query  │
└────────┬────────┘


┌─────────────────┐
│ Format Response │
└─────────────────┘

1. Parsing and Validation

The GraphQL query is parsed into an AST and validated:
pub struct Query {
    // Parsed GraphQL document
    document: Document,
    // Subgraph's schema
    schema: Arc<ApiSchema>,
    // Query root type
    root_type: String,
}
Validation checks:
  • Query syntax is valid GraphQL
  • All fields exist in schema
  • Arguments are correctly typed
  • Required arguments are present
Location in codebase: graphql/src/execution/query.rs

2. Query Planning

The query planner determines optimal execution strategy:
  • Entity resolution: Map GraphQL types to database tables
  • Filter analysis: Identify indexes that can be used
  • Join planning: Optimize relationship traversals
  • Pagination strategy: Choose offset vs range-based pagination

3. SQL Generation

GraphQL query is translated to SQL:
pub trait QueryFragment {
    fn walk_ast<'a>(&'a self, pass: AstPass<'a>);
}
The SQL generator:
  • Constructs SELECT with requested fields
  • Adds JOIN clauses for relationships
  • Applies WHERE filters
  • Adds ORDER BY and LIMIT clauses
  • Includes block range filters for time-travel
Example translation:
{
  users(first: 5, where: {balance_gt: "1000"}, orderBy: balance) {
    id
    balance
    transactions(first: 3) {
      value
    }
  }
}
Generated SQL:
-- Main query
SELECT u.id, u.balance
FROM sgd1.user u
WHERE u.balance > 1000
  AND u.block_range @> 2147483647  -- Current block range
ORDER BY u.balance
LIMIT 5;

-- For each user, fetch transactions
SELECT t.value, t."from"
FROM sgd1.transaction t
WHERE t."from" = ANY($1)  -- Array of user IDs
  AND t.block_range @> 2147483647
LIMIT 3;
Location in codebase: store/postgres/src/relational_queries.rs

4. Query Execution

SQL executes against PostgreSQL:
pub trait QueryStore: Send + Sync {
    fn find_query_values(
        &self,
        query: EntityQuery,
    ) -> Result<Vec<BTreeMap<Word, Value>>, QueryExecutionError>;
}
Connection pooling: Each shard has a connection pool sized by configuration Read replicas: Queries are distributed across replicas based on weights Location in codebase: store/postgres/src/query_store.rs

5. Response Formatting

SQL results are transformed into GraphQL response:
  • Deserialization: PostgreSQL types → Rust types → GraphQL types
  • Relationship assembly: Join nested entities from separate queries
  • Type coercion: Apply GraphQL type rules
  • JSON formatting: Convert to GraphQL JSON response

Query Optimization

Indexing Strategy

PostgreSQL indexes are critical for query performance: Automatic indexes:
  • Primary key (id)
  • Version ID (vid)
  • Block range (block_range) using GiST index
Generated index example:
CREATE INDEX user_id_block_range_excl 
  ON sgd1.user 
  USING gist (id, block_range);

CREATE INDEX user_block_range_idx
  ON sgd1.user
  USING gist (block_range);
Location in codebase: store/postgres/src/relational.rs:generate_indexes()

Query Complexity Limits

Graph Node enforces limits to prevent resource exhaustion:
  • Max query depth: Limits nested query levels (default: 8)
  • Max result size: Limits returned entities (default: 1000 per query)
  • Query timeout: Maximum execution time (configurable)
Configuration:
GRAPH_GRAPHQL_MAX_DEPTH=15
GRAPH_GRAPHQL_MAX_COMPLEXITY=1000000
GRAPH_GRAPHQL_QUERY_TIMEOUT=60

Query Caching

GraphNode includes query result caching:
  • Cache key: Query hash + block number
  • Invalidation: Automatic on new blocks
  • Storage: In-memory cache per query node
Configuration:
GRAPH_QUERY_CACHE_BLOCKS=2
GRAPH_QUERY_CACHE_MAX_MEM=1000  # MB

Prepared Statements

Frequently-executed queries use prepared statements:
  • Query plan reuse: Avoids repeated planning overhead
  • Parameter binding: Safely inject filter values
  • Connection affinity: Same connection reuses prepared statement

Pagination Performance

Best practice: Use cursor-based pagination with indexed fields for large result sets. Avoid skip with large offsets.
Inefficient:
{ users(first: 10, skip: 10000) { id } }
Efficient:
{ users(first: 10, where: { id_gt: "<last_id>" }, orderBy: id) { id } }

Query Performance Monitoring

Metrics

Graph Node exposes query metrics:
  • query_execution_time: Query duration histogram
  • query_validation_time: Validation time
  • query_cache_hit_rate: Cache effectiveness
  • query_parsing_errors: Failed query parsing
Endpoint: http://localhost:8040/metrics

Query Logging

Enable query logging for debugging:
GRAPH_LOG_QUERY_TIMING=gql
Logs include:
  • Query text
  • Execution time
  • Number of results
  • SQL execution time

Slow Query Analysis

Identify slow queries:
# Enable PostgreSQL slow query log
GRAPH_SQL_STATEMENT_TIMEOUT=30000  # 30 seconds
Analyze with PostgreSQL:
-- Find slow queries
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

GraphQL Schema Generation

Graph Node automatically generates a rich GraphQL schema from the subgraph’s entity schema.

Generated Types

For each entity, Graph Node generates: Query types:
  • entity(id: ID!): Fetch by ID
  • entities(...): Fetch multiple with filtering
Filter input types:
  • Entity_filter: All available filters for entity
Order types:
  • Entity_orderBy: Enum of sortable fields
Example:
type Query {
  user(id: ID!, block: Block_height): User
  users(
    first: Int
    skip: Int
    orderBy: User_orderBy
    orderDirection: OrderDirection
    where: User_filter
    block: Block_height
  ): [User!]!
}

input User_filter {
  id: ID
  id_not: ID
  id_gt: ID
  id_lt: ID
  id_gte: ID
  id_lte: ID
  id_in: [ID!]
  id_not_in: [ID!]
  balance: BigInt
  balance_not: BigInt
  balance_gt: BigInt
  balance_lt: BigInt
  balance_gte: BigInt
  balance_lte: BigInt
  # ... more fields
}

enum User_orderBy {
  id
  address
  balance
  createdAt
  updatedAt
}
Location in codebase: graphql/src/schema/ generates schema from entity definitions

Advanced Query Patterns

Union Queries

Query multiple entity types (when schema supports interfaces):
{
  activities(first: 10) {
    ... on Transfer {
      from
      to
      value
    }
    ... on Approval {
      owner
      spender
      value
    }
  }
}

Subscriptions

WebSocket-based real-time subscriptions:
subscription {
  users(where: {balance_gt: "1000000"}) {
    id
    balance
  }
}
Implementation: Server pushes updates when entities change Endpoint: ws://localhost:8001/subgraphs/name/<name>

Query Best Practices

Request Only Needed Fields

Don’t fetch unnecessary data. Specify exact fields required.

Use Cursor Pagination

Prefer cursor-based pagination over large offsets for better performance.

Limit Nested Queries

Deep nesting impacts performance. Flatten queries when possible.

Filter Early

Apply filters at the earliest level to reduce data processed.

Troubleshooting

Query Too Complex

Error: “Query exceeds maximum complexity” Solution:
  • Reduce query depth
  • Fetch fewer entities per query
  • Split into multiple smaller queries
  • Increase GRAPH_GRAPHQL_MAX_COMPLEXITY

Query Timeout

Error: “Query timeout” Solution:
  • Optimize filters (ensure indexed fields)
  • Reduce result size with first
  • Check PostgreSQL for missing indexes
  • Increase GRAPH_GRAPHQL_QUERY_TIMEOUT

Inconsistent Results

Cause: Querying during active indexing Solution:
  • Check _meta.hasIndexingErrors
  • Wait for subgraph to sync (_meta.block.number)
  • Use block: {number: X} for consistent snapshots

Next Steps

Indexing

Understand how data is indexed before querying

Architecture

Learn about Graph Node’s overall architecture

Subgraphs

Deep dive into subgraph structure and manifests

SQL Query Generation

Technical details on SQL generation

Build docs developers (and LLMs) love