Making transaction history fast instead of fragile


Introduction

PostgreSQL is a good match for blockchain data because it likes structure, and blockchains are nothing if not structured.

What it doesn’t like is a single, ever‑growing table with billions of rows, no partitioning, and half a dozen poorly chosen indexes.

Blockchain workloads look a lot like time‑series + ledger workloads: append‑only, read‑heavy, with hot “recent” data and colder history, plus strict audit requirements. Modern PostgreSQL features—declarative partitioning, multiple index types, good JSONB support, and extensions like TimescaleDB—were built exactly for that kind of data.(Amazon Web Services, Inc.)

In this article I’ll focus on how I design and tune PostgreSQL schemas for blockchain indexers and explorers: how to lay out tables, partition them, index them, and debug slow queries when things inevitably get big.


1. What blockchain data looks like to Postgres

From PostgreSQL’s point of view a chain is “just” a few very large tables with strong ordering:

blocks(height, hash, time, ...)

transactions(tx_hash, block_height, index_in_block, from, to, value, ...)

logs(tx_hash, log_index, contract, topic0, topic1, data, ...)

address_history(address, block_height, tx_hash, delta, balance, ...)

The access patterns are predictable:

- by block range        (explorer pages, reindexing, backfills)
- by transaction hash   (lookup by id)
- by address / account  (wallet history, portfolio)
- by contract + topics  (event indexing, analytics)
- by time windows       (analytics, monitoring)

That’s already enough to shape the schema: we know which columns will sit in WHERE clauses, which ones define order, and which ones are good candidates for partitioning and indexes. General schema‑design advice for scalable Postgres says the same thing: start from the queries you actually care about.(DEV Community)


2. Partitioning: breaking the monster table

Once transaction tables hit hundreds of millions of rows, you almost always want partitioning.

Conceptually, partitioning turns one logical table into many physical child tables, each holding a slice of the rows:

transactions   (parent, empty)
  |
  +-- transactions_2025_01
  +-- transactions_2025_02
  +-- transactions_2025_03
  ...

PostgreSQL’s native partitioning supports range, list, and hash strategies. Guides on high‑volume and time‑series workloads consistently recommend range partitioning on time or a monotonic key, and sometimes a second level of hash partitioning for write distribution.(Amazon Web Services, Inc.)

For blockchain data there are three natural partition keys:

- block time (e.g. monthly or weekly partitions)
- block height (numeric surrogate for time)
- chain / network id (multi-chain setups)

I usually:

  • split the largest tables (transactions, logs, address_history) by time or height;
  • optionally separate chains by schema or by list partition (one partition per chain);
  • keep blocks smaller but partition them too if I expect years of history.

A simple ASCII view:

schema mainnet
  blocks_y2025m01, blocks_y2025m02, ...
  tx_y2025m01,     tx_y2025m02,      ...
  logs_y2025m01,   logs_y2025m02,    ...

schema testnet
  ...

Why it helps:

  • queries that filter on time/height only touch relevant partitions (“pruning”);(Stormatics)
  • you can archive or drop old partitions cheaply;(ashnik.com)
  • maintenance (VACUUM, REINDEX) happens per partition instead of on a monolith.

Partitioning isn’t magic: if your queries don’t filter on the partition key, you’ll still scan everything. There’s a whole class of “partitioning didn’t help, what now?” discussions that boil down to that.(Stack Overflow)

From the trenches. On one explorer we started with a single transactions table and only added an index on (address, block_height). That worked until ~1B rows. Moving to monthly partitions on block time, with the same indexes per partition, dropped the worst‑case query from tens of seconds to under a second and made VACUUM manageable again.


3. Index types and patterns for blockchain queries

PostgreSQL ships with several index types. The docs and many deep‑dives say the same thing: B‑tree is the default, but GIN, GiST, and BRIN are extremely useful for specialized workloads.(PostgreSQL)

In blockchain schemas I mostly use three:

3.1 B‑tree: equality and range

This is the workhorse index type; it’s what you want for:

- lookups by tx_hash or block hash
- queries by block_height range
- queries by address + block_height (wallet history)
- ordering by height or time

Typical patterns conceptually look like:

- primary key: (block_height)
- unique constraint: (tx_hash)
- composite index: (address, block_height DESC)
- composite index: (contract, topic0, block_height)

The key trick is to align index columns with your most common WHERE and ORDER BY clauses. Postgres performance guides and indexing tutorials emphasize choosing composite indexes that match query patterns instead of indexing each column alone.(The Quest Blog)

3.2 GIN: JSONB and arrays for logs and metadata

Events and rich metadata often end up in JSONB or arrays: topics, tags, attributes, token metadata.

GIN indexes are built for this: they index elements inside JSONB documents or arrays, allowing fast containment and key existence queries. AWS and Postgres JSON articles show how powerful GIN is for JSON‑heavy workloads.(Amazon Web Services, Inc.)

In practice:

  • logs table often gets a GIN index on topics[] or on a JSONB column of decoded event fields;
  • search endpoints hit those columns with containment operators;
  • the B‑tree index on (contract, block_height) handles range and ordering.

The combination makes “find all swaps on pool X for address Y last week” feasible without scanning the entire logs table.

3.3 BRIN: huge sequential tables

For very large, append‑only tables where rows are physically ordered by block time or height, BRIN indexes can be tiny and surprisingly effective.

BRIN indexes store summaries per block of table pages rather than individual row pointers. They work best when there is a strong correlation between the indexed column and physical row order. Blockchain data is almost a textbook case: you append new blocks in increasing height/time most of the time.(Learnomate Technologies)

I like BRIN for:

- coarse filters on block_height or block_time over massive tables
- “latest N blocks” queries where you only need recent ranges fast

In some deployments I’ve used a small BRIN index on (block_height) alongside more targeted B‑tree indexes for addresses and contracts. BRIN narrows the page range; B‑tree pinpoints rows inside.


4. Query optimisation for transaction history

With tables and indexes in place, the next step is to keep queries honest.

A few habits pay off repeatedly.

Short paragraphs, one idea each.

Use EXPLAIN ANALYZE routinely. You want to see which indexes are used, whether partition pruning is happening, and how many rows are actually scanned. Indexing articles and Postgres best‑practice guides are blunt about this: you can’t tune what you don’t inspect.(FreeCodeCamp)

Avoid SELECT * on large tables. Blockchain rows are wide: hashes, addresses, values, JSON, metadata. Fetching all columns when you only need a handful wastes I/O and memory. Recent “top Postgres practices” posts explicitly call out selecting only the needed columns as a simple but potent optimisation.(Instaclustr)

Push filters down and include partition keys. A query like “all transactions for address X” with no time constraint will touch every partition. Adding a time window or block range lets PostgreSQL prune most partitions, which is where partitioning’s benefits actually come from. Real‑world write‑ups on partitioning note that many “partitioning didn’t help” stories come from queries that ignore the partition key entirely.(Stormatics)

Make heavy endpoints use covering indexes where it makes sense. If an API endpoint always needs (tx_hash, block_height, from, to, value), you can include those columns in a single composite index so Postgres can answer the query from the index alone, without hitting the table. Indexing guides show this pattern (“index‑only scans”) as a strong option for read‑heavy workloads with stable columns.(FreeCodeCamp)

From the trenches. On a busy address‑history endpoint we initially indexed only (address, block_height) and selected many extra columns. The planner had to hit the table for every row. Adding a slightly wider covering index turned the plan into an index‑only scan and cut response times by more than half, without changing the schema otherwise.


5. Lifecycle, archiving, and repartitioning

Blockchain data doesn’t stop. If you don’t plan for lifecycle, all the nice partitioning and indexes will eventually sag under the weight.

Partitioning is also your main tool here. Guides on large Postgres datasets recommend exactly this pattern: new partitions for fresh data, old partitions for archiving or cheaper storage.(ashnik.com)

A typical lifecycle looks like:

- hot partitions:
    recent months, full indexes, aggressive VACUUM and ANALYZE

- warm partitions:
    older, still queried, maybe fewer indexes or compressed storage

- cold partitions:
    detached from the main schema, moved to cheaper storage or another cluster

Extensions like pg_partman can automate creation and maintenance of new partitions, and there are published examples of “live repartitioning” where you create a new partitioned structure, backfill in batches, then swap it in with minimal downtime.(Architecture Weekly)

This matters for blockchain because history is both sacred and often less frequently accessed:

- you rarely touch blocks from six years ago,
- but you must never lose them,
- and you occasionally need them for audits or analytics.

Planning that story from day one—hot vs warm vs cold partitions—prevents painful “big‑bang migrations” later.


6. Blockchain‑specific schema tweaks

Two blockchain‑specific themes influence schema design.

First is immutability and auditability. You almost never want to update or delete transaction rows. Instead, you append new facts (e.g. “this chain was reorged at height H”), or maintain derived tables that can be rebuilt from the immutable base. Ledger‑oriented Postgres articles show how append‑only design plus verification (hashing, signatures) can turn a normal database into something much closer to a ledger.(Architecture Weekly)

Second is denormalized read models. For explorers and wallets, it’s often worth keeping extra, pre‑aggregated tables such as address_latest_balance or token_holders that are updated by the indexer. Schema best‑practice articles describe this “normalised core + denormalised projections” approach as a good compromise for read‑heavy systems.(DEV Community)

I tend to keep:

- raw, immutable tables for blocks, transactions, logs,
- derived tables optimised for specific queries (per-address history, per-token stats),
- clear rebuild paths from raw data to each projection.

That way, if you ever need to change a projection schema or fix a bug in aggregation, you can replay from the raw chain without touching the base tables.

From the trenches. On one project we initially tried to answer everything from a single, highly normalised schema. Explorer queries became horrible JOIN chains and fell over at scale. Introducing a few focused projection tables—designed exactly for “address page” and “pool stats” views—moved complexity into the indexer and made the database much happier.


Conclusion

PostgreSQL can handle blockchain‑scale data, but only if you treat it like the high‑volume, append‑only workload it is:

  • design tables around real access patterns;
  • partition large tables by time or height so that queries and maintenance touch only what they must;(Amazon Web Services, Inc.)
  • choose index types deliberately: B‑tree for equality and ranges, GIN for JSONB and arrays, BRIN for huge sequential tables;(PostgreSQL)
  • use EXPLAIN, covering indexes, and carefully constrained queries to keep transaction history fast;(FreeCodeCamp)
  • plan data lifecycle from day one so old partitions can be archived without drama.(ashnik.com)

If you respect those constraints, PostgreSQL stops being “just a store for decoded blocks” and becomes a proper blockchain analytics engine: fast for the present, honest about the past, and ready to keep growing with the chain.

Completion scope and production contract

This completion review turns the earlier conceptual treatment into a release-oriented engineering contract. It treats PostgreSQL for Blockchain Data: Schema Design and Optimization as a Kotlin and Spring backend component, follows a request, domain command, event, database transaction, coroutine, or stream record through validation and durable state, and separates normative requirements from implementation policy. The normative baseline is the domain contract, published API schema, database constraints, and framework lifecycle; deployment defaults, caching, retry limits, and operator thresholds are explicitly local policy. 14

The intended audience is experienced developers and architects. Readers should understand the surrounding chain or application model, typed data structures, persistence, and basic security engineering. The scope includes correctness, implementation boundaries, deterministic tests, failure recovery, security, performance, and observability. It does not claim that the educational companion is a drop-in replacement for a maintained protocol or cryptographic library. Production adoption requires an independent threat model, compatibility testing against the authoritative implementation, and operational ownership. 15

The mental model used throughout is deliberately strict: untrusted input crosses HTTP, authentication, messaging, domain, database, and downstream-node boundaries; a validator derives facts under the domain contract, published API schema, database constraints, and framework lifecycle; accepted transitions update transactional domain state plus replayable processing progress; and observers consume committed facts, never optimistic intermediate mutations. A guarantee is stated only when it follows from those rules and assumptions. Heuristics such as fee selection, caching, peer scoring, timeouts, user messaging, or alert thresholds remain policy and may be tuned without redefining validity. 16

Reader contract and scope

For PostgreSQL for Blockchain Data: Schema Design and Optimization, this review makes the exact user decision and the prerequisites needed to make it safely explicit. Start from one request, domain command, event, database transaction, coroutine, or stream record and write down its origin, canonical representation, validation context, authority, and durable outcome. The Kotlin and Spring backend component must not infer a stronger fact from transport success, cache presence, or an upstream acknowledgement. Its authoritative state is transactional domain state plus replayable processing progress, and every projection must remain rebuildable or reconcilable from committed facts. This framing distinguishes a protocol guarantee from an operational convenience and gives reviewers a concrete place to challenge an assumption. 14

The principal failure to design against is an attractive example being mistaken for a complete production design. Address it before optimizing by defining a narrow ownership boundary, stable identities, bounded resource use, and a machine-readable outcome for every rejected transition. Record a scope statement, excluded concerns, and a reviewable acceptance criterion. A reviewer should be able to trace each accepted result to input bytes, rule or policy version, prior state, and commit identity without relying on prose logs. When the authority cannot be reached or context is incomplete, return an explicit unavailable or pending state; do not convert uncertainty into acceptance.

Precise vocabulary and authority

Treat precise vocabulary and authority as part of the executable design of PostgreSQL for Blockchain Data: Schema Design and Optimization, not as documentation added after coding. The relevant operating envelope includes historical ingestion, bursty APIs, retries, rolling deployment, backfill, and downstream degradation. For each mode, identify which state is authoritative, which work may be retried, what is bounded, and which observation proves progress. This is especially important across HTTP, authentication, messaging, domain, database, and downstream-node boundaries, where delivery and processing are different events and where local time or arrival order may not reflect authoritative order. 15

A useful review asks how the design behaves under lost update, duplicate event, poison record, pool exhaustion, timeout, schema skew, and inconsistent retry. The unsafe outcome is teams using the same word for incompatible states or guarantees. Prevent it with explicit preconditions and postconditions, and retain a glossary tied to the normative authority for every overloaded term as release evidence. Use stable codes rather than exception text, keep policy configuration versioned, and attach the accepted policy or rule version to durable results. An service or data-platform operator must be able to stop intake, drain or quarantine work, compare local state with authority, and resume without inventing a second side effect.

Trust assumptions

The implementation of PostgreSQL for Blockchain Data: Schema Design and Optimization should expose which actors, clocks, stores, libraries, and upstream systems may fail or act maliciously through types and module boundaries. Parse external representations once, preserve the original identity when audit or replay needs it, and pass validated domain values inward. Mutations of transactional domain state plus replayable processing progress belong behind one authoritative transition function or transaction boundary. Network clients, storage adapters, user interfaces, and telemetry exporters must not duplicate consensus or business rules. That separation keeps deterministic logic testable and prevents a library upgrade from silently redefining validity. 16

Assume that an implicit trusted component invalidating the claimed guarantee will eventually occur in a staging fault test or production incident. The control is not a catch-all retry: classify the outcome, decide whether the identical operation is safe to repeat, bound attempts and elapsed time, and surface terminal work for reconciliation. The minimum evidence is a trust-boundary diagram and an assumption register with owners. Keep credentials, bearer tokens, signing requests, customer identifiers, and database change history out of ordinary logs, and prefer hashes, version identifiers, counts, and sanitized reason codes. Any emergency bypass must be narrow, time-limited, approved, and observable.

Architecture and ownership

Verification for PostgreSQL for Blockchain Data: Schema Design and Optimization must demonstrate component responsibilities and the direction in which facts and commands move at several layers. Small tests cover deterministic transforms and boundary values; contract tests pin serialized forms; integration tests exercise real adapters; replay tests compare state roots or projections; and fault tests interrupt work at every commit boundary. The dataset must include normal history, malformed input, duplicates, reordering, maximum-size values, and changes of rule or schema version. A passing example is evidence about that environment and dataset, not a universal performance claim. 14

Make two components both believing they own the same transition a named negative test. The release packet should retain a context diagram, ownership table, and dependency rule, exact dependency and tool versions, the deterministic command, and its result. For performance work, report warm-up, repetitions, concurrency, percentiles, resource limits, and the point where backpressure begins. For security work, include abuse cases and independent review. A change is ready only when failures leave transactional domain state plus replayable processing progress safe, recovery is rehearsed, and telemetry explains both user-visible outcome and operator action.

Canonical representation

For PostgreSQL for Blockchain Data: Schema Design and Optimization, this review makes the byte-level or schema-level representation used for hashing, comparison, storage, and transport explicit. Start from one request, domain command, event, database transaction, coroutine, or stream record and write down its origin, canonical representation, validation context, authority, and durable outcome. The Kotlin and Spring backend component must not infer a stronger fact from transport success, cache presence, or an upstream acknowledgement. Its authoritative state is transactional domain state plus replayable processing progress, and every projection must remain rebuildable or reconcilable from committed facts. This framing distinguishes a protocol guarantee from an operational convenience and gives reviewers a concrete place to challenge an assumption. 15

The principal failure to design against is semantically equal values producing different identifiers or verification outcomes. Address it before optimizing by defining a narrow ownership boundary, stable identities, bounded resource use, and a machine-readable outcome for every rejected transition. Record golden encodings, round-trip tests, and rejection of non-canonical forms. A reviewer should be able to trace each accepted result to input bytes, rule or policy version, prior state, and commit identity without relying on prose logs. When the authority cannot be reached or context is incomplete, return an explicit unavailable or pending state; do not convert uncertainty into acceptance.

State-machine model

Treat state-machine model as part of the executable design of PostgreSQL for Blockchain Data: Schema Design and Optimization, not as documentation added after coding. The relevant operating envelope includes historical ingestion, bursty APIs, retries, rolling deployment, backfill, and downstream degradation. For each mode, identify which state is authoritative, which work may be retried, what is bounded, and which observation proves progress. This is especially important across HTTP, authentication, messaging, domain, database, and downstream-node boundaries, where delivery and processing are different events and where local time or arrival order may not reflect authoritative order. 16

A useful review asks how the design behaves under lost update, duplicate event, poison record, pool exhaustion, timeout, schema skew, and inconsistent retry. The unsafe outcome is an impossible intermediate state becoming durable after interruption. Prevent it with explicit preconditions and postconditions, and retain a transition table exercised by positive, negative, and replay tests as release evidence. Use stable codes rather than exception text, keep policy configuration versioned, and attach the accepted policy or rule version to durable results. An service or data-platform operator must be able to stop intake, drain or quarantine work, compare local state with authority, and resume without inventing a second side effect.

Invariants

The implementation of PostgreSQL for Blockchain Data: Schema Design and Optimization should expose properties that must hold before and after every accepted operation through types and module boundaries. Parse external representations once, preserve the original identity when audit or replay needs it, and pass validated domain values inward. Mutations of transactional domain state plus replayable processing progress belong behind one authoritative transition function or transaction boundary. Network clients, storage adapters, user interfaces, and telemetry exporters must not duplicate consensus or business rules. That separation keeps deterministic logic testable and prevents a library upgrade from silently redefining validity. 14

Assume that local success concealing corruption in a related aggregate or index will eventually occur in a staging fault test or production incident. The control is not a catch-all retry: classify the outcome, decide whether the identical operation is safe to repeat, bound attempts and elapsed time, and surface terminal work for reconciliation. The minimum evidence is executable assertions at the narrowest authoritative boundary. Keep credentials, bearer tokens, signing requests, customer identifiers, and database change history out of ordinary logs, and prefer hashes, version identifiers, counts, and sanitized reason codes. Any emergency bypass must be narrow, time-limited, approved, and observable.

Validation pipeline

Verification for PostgreSQL for Blockchain Data: Schema Design and Optimization must demonstrate cheap structural checks, contextual checks, authoritative verification, and commit order at several layers. Small tests cover deterministic transforms and boundary values; contract tests pin serialized forms; integration tests exercise real adapters; replay tests compare state roots or projections; and fault tests interrupt work at every commit boundary. The dataset must include normal history, malformed input, duplicates, reordering, maximum-size values, and changes of rule or schema version. A passing example is evidence about that environment and dataset, not a universal performance claim. 15

Make expensive or stateful work running before malformed input is rejected a named negative test. The release packet should retain ordered validation stages with stable machine-readable rejection codes, exact dependency and tool versions, the deterministic command, and its result. For performance work, report warm-up, repetitions, concurrency, percentiles, resource limits, and the point where backpressure begins. For security work, include abuse cases and independent review. A change is ready only when failures leave transactional domain state plus replayable processing progress safe, recovery is rehearsed, and telemetry explains both user-visible outcome and operator action.

Error semantics

For PostgreSQL for Blockchain Data: Schema Design and Optimization, this review makes the distinction between invalid input, conflict, unavailable dependency, retryable interruption, and internal defect explicit. Start from one request, domain command, event, database transaction, coroutine, or stream record and write down its origin, canonical representation, validation context, authority, and durable outcome. The Kotlin and Spring backend component must not infer a stronger fact from transport success, cache presence, or an upstream acknowledgement. Its authoritative state is transactional domain state plus replayable processing progress, and every projection must remain rebuildable or reconcilable from committed facts. This framing distinguishes a protocol guarantee from an operational convenience and gives reviewers a concrete place to challenge an assumption. 16

The principal failure to design against is blind retries amplifying a permanent failure or changing user intent. Address it before optimizing by defining a narrow ownership boundary, stable identities, bounded resource use, and a machine-readable outcome for every rejected transition. Record typed errors mapped consistently across logs, metrics, APIs, and queues. A reviewer should be able to trace each accepted result to input bytes, rule or policy version, prior state, and commit identity without relying on prose logs. When the authority cannot be reached or context is incomplete, return an explicit unavailable or pending state; do not convert uncertainty into acceptance.

Concurrency control

Treat concurrency control as part of the executable design of PostgreSQL for Blockchain Data: Schema Design and Optimization, not as documentation added after coding. The relevant operating envelope includes historical ingestion, bursty APIs, retries, rolling deployment, backfill, and downstream degradation. For each mode, identify which state is authoritative, which work may be retried, what is bounded, and which observation proves progress. This is especially important across HTTP, authentication, messaging, domain, database, and downstream-node boundaries, where delivery and processing are different events and where local time or arrival order may not reflect authoritative order. 14

A useful review asks how the design behaves under lost update, duplicate event, poison record, pool exhaustion, timeout, schema skew, and inconsistent retry. The unsafe outcome is a check-then-act race accepting two individually plausible operations. Prevent it with explicit preconditions and postconditions, and retain a linearization argument plus stress tests at the chosen contention boundary as release evidence. Use stable codes rather than exception text, keep policy configuration versioned, and attach the accepted policy or rule version to durable results. An service or data-platform operator must be able to stop intake, drain or quarantine work, compare local state with authority, and resume without inventing a second side effect.

Idempotency and replay

The implementation of PostgreSQL for Blockchain Data: Schema Design and Optimization should expose how duplicate delivery, process restart, and historical backfill preserve the same result through types and module boundaries. Parse external representations once, preserve the original identity when audit or replay needs it, and pass validated domain values inward. Mutations of transactional domain state plus replayable processing progress belong behind one authoritative transition function or transaction boundary. Network clients, storage adapters, user interfaces, and telemetry exporters must not duplicate consensus or business rules. That separation keeps deterministic logic testable and prevents a library upgrade from silently redefining validity. 15

Assume that at-least-once delivery creating a second side effect will eventually occur in a staging fault test or production incident. The control is not a catch-all retry: classify the outcome, decide whether the identical operation is safe to repeat, bound attempts and elapsed time, and surface terminal work for reconciliation. The minimum evidence is stable operation identities, deduplication state, and deterministic replay fixtures. Keep credentials, bearer tokens, signing requests, customer identifiers, and database change history out of ordinary logs, and prefer hashes, version identifiers, counts, and sanitized reason codes. Any emergency bypass must be narrow, time-limited, approved, and observable.

Persistence and atomicity

Verification for PostgreSQL for Blockchain Data: Schema Design and Optimization must demonstrate which facts commit together and how derived views catch up at several layers. Small tests cover deterministic transforms and boundary values; contract tests pin serialized forms; integration tests exercise real adapters; replay tests compare state roots or projections; and fault tests interrupt work at every commit boundary. The dataset must include normal history, malformed input, duplicates, reordering, maximum-size values, and changes of rule or schema version. A passing example is evidence about that environment and dataset, not a universal performance claim. 16

Make a crash exposing a cursor that claims work whose state was not committed a named negative test. The release packet should retain transaction boundaries, durable checkpoints, and reconciliation queries, exact dependency and tool versions, the deterministic command, and its result. For performance work, report warm-up, repetitions, concurrency, percentiles, resource limits, and the point where backpressure begins. For security work, include abuse cases and independent review. A change is ready only when failures leave transactional domain state plus replayable processing progress safe, recovery is rehearsed, and telemetry explains both user-visible outcome and operator action.

References