Skip to main content
Graph Node maintains complete version history for all entities, enabling queries of subgraph state at any historical block. This powerful feature allows you to analyze how data evolved over time without storing snapshots.

Overview

Time-travel queries let you retrieve the exact state of entities as they existed at a specific block height. Graph Node achieves this by storing entity versions with block ranges indicating their validity period.

How It Works

Entity Versioning

For each entity, Graph Node maintains multiple versions in the database:
INT8
Unique version identifier for this specific entity version.
TEXT
The entity’s logical ID (can have multiple versions).
NUMERIC
Entity attribute(s) - specific to your schema.
INT4RANGE
PostgreSQL range indicating blocks where this version is valid: [start, end)
  • Start (inclusive): Block where this version was created
  • End (exclusive): Block where this version was replaced or deleted
  • Current version has unlimited upper bound: [7, )

Block Range Examples

The exclusion constraint ensures block ranges for the same entity ID never overlap:

Immutable Entities

Entities declared with @entity(immutable: true) use optimized storage:
Storage difference:
  • Uses block$ (INT) instead of block_range (INT4RANGE)
  • Stores only the creation block
  • Check becomes: block$ <= $B instead of block_range @> $B
  • Enforces UNIQUE(id) constraint
  • Enables simpler, faster BTree indexes
Immutable entities can never be updated or deleted, only created. This matches blockchain event semantics where events are permanent.

Querying Historical State

Block Height Argument

All entity queries accept an optional block argument:
Int
Block height to query state at.
Bytes
Block hash to query state at (alternative to number).

Single Entity Queries

Collection Queries

Apply time-travel to collection queries:

Nested Entity Queries

Time-travel applies to nested entities automatically:

SQL Translation

Here’s how Graph Node translates time-travel queries to SQL:

Query Entity at Block

Translates to:

Collection Query at Block

Translates to:

Immutable Entity Query

For immutable entities:

Entity Operations with Block Ranges

Create Entity

When creating an entity at block B:
Creates a version valid from block B to infinity.

Update Entity

Updating at block B:
Immutable entities cannot be updated - the operation is not allowed.

Delete Entity

Deleting at block B:
Closes the block range at B, making the entity non-existent after that block.

Rollback / Revert

Reverting to block B (removing all changes after B):

Use Cases

Analyze how metrics evolved over time:
Generate reports for specific points in time:
Verify state at specific blocks for auditing:
Build time-series data by querying multiple blocks:
Investigate issues by checking state before/after specific blocks:

Block Number vs Block Hash

Important: Block numbers do not uniquely identify blocks - only block hashes do.During chain reorganizations, different blocks can have the same block number. Graph Node tracks the canonical chain and resolves block numbers accordingly.For most use cases, block numbers are sufficient and more convenient. Use block hashes when:
  • Dealing with very recent blocks that might reorganize
  • Need cryptographic proof of specific block state
  • Querying during or immediately after a chain reorganization

Performance Considerations

Indexing

Graph Node creates indexes to optimize time-travel queries:

Query Performance

Queries for recent blocks are very fast:
  • Current state (no block specified): Fastest
  • Recent blocks (< 1000 blocks ago): Fast
  • PostgreSQL effectively caches recent data
Queries for distant historical blocks:
  • May scan more data to find correct versions
  • BRIN indexes help but queries are slower than recent blocks
  • Consider caching results for frequently-accessed historical states
  1. Query recent state when possible: Current state is always fastest
  2. Batch historical queries: If analyzing many blocks, batch them into single query
  3. Use specific filters: Narrow queries with where clauses
  4. Consider aggregations: Use aggregation features for time-series analytics instead of many point queries

Limitations

  1. Block hash lookups: Querying by block hash for distant blocks can be expensive
  2. Storage overhead: Maintaining version history increases storage requirements
  3. Immutable entities: Time-travel works but is trivial - each entity exists at only one block
  4. Cross-subgraph queries: Time-travel applies to single subgraph only

Example: Token Balance History

Track how a token’s total supply changed over time:
Response:

Best Practices

  1. Use current state by default: Only specify block when historical data is needed
  2. Cache historical queries: Results for past blocks never change - cache them
  3. Query recent blocks: More efficient than distant historical blocks
  4. Use block numbers: Simpler than block hashes for most use cases
  5. Consider aggregations: For time-series analysis, use aggregation features instead of many point queries
  6. Document block heights: When storing block numbers, document what events they represent

Next Steps