#database#postgresql#mysql#redis#performance

Architecting Latency-Critical Systems: Relational Storage Internals, Query Optimization, and Resilient Caching

"In high-throughput distributed architectures, tail latency degradation rarely originates in stateless compute tiers. Ephemeral application runtimes scale horiz"

•By huud•
0 views
~9 min read
Architecting Latency-Critical Systems: Relational Storage Internals, Query Optimization, and Resilient Caching

In high-throughput distributed architectures, tail latency degradation rarely originates in stateless compute tiers. Ephemeral application runtimes scale horizontally behind load balancers with predictable linear overhead. Persistent state engines do not. Under sustained read/write pressure, latency spikes (p99p99 and p99.9p99.9) are almost always driven by physical storage bottlenecks: write amplification on NVMe devices, lock contention inside the engine's concurrency manager, suboptimal execution plans generated by stale statistics, and cache stampedes that overwhelm connection pools.

Achieving deterministic single-digit millisecond performance from relational engines such as PostgreSQL and MySQL (InnoDB) requires mechanical sympathy for low-level storage primitives, cost-based optimizer mechanics, MVCC garbage collection models, and resilient edge cache topologies.


1. Storage Engine Primitives: B+B^+Trees, Lehman-Yao, and GiST

Relational database engines rely on variations of balanced search trees to provide logarithmic random access. PostgreSQL implements the Lehman-Yao B-Tree algorithm (nbtree), which introduces right-sibling pointers ("right-links") and page-level high keys. This design allows read traversals to proceed concurrently without taking shared read locks on internal nodes during concurrent page splits. If a reader descends into a node concurrently split by a writer, it detects that the target key exceeds the high key and follows the right-link to the newly allocated sibling.

The theoretical height hh for an index containing NN keys with minimum branching fan-out BB is bounded by:

h≤⌈log⁡⌈B/2⌉(N+12)⌉h \le \left\lceil \log_{\lceil B/2 \rceil} \left( \frac{N+1}{2} \right) \right\rceil

On a standard 16KB InnoDB page with 8-byte keys and 6-byte child page pointers, fan-out BB routinely exceeds 1,000. A three-level index addresses over one billion records within three random I/O operations (O(log⁡BN)O(\log_B N)).

However, the physical relationship between the index and underlying data diverges sharply across engines:

  • InnoDB Clustered Index: Data is organized directly within the leaf pages of the primary key B+B^+Tree. Secondary indexes store the primary key value as their leaf payload. Consequently, non-covering secondary index queries require a secondary traversal ("bookmark lookup") through the clustered index to retrieve non-indexed columns.
  • PostgreSQL Heap Engine: Table data is stored in unordered 8KB heap pages, referenced via a 6-byte Tuple Identifier (ctid comprising block number and line pointer offset). Secondary indexes point directly to ctids. When an UPDATE occurs, PostgreSQL writes a new physical tuple. To prevent write amplification across all secondary indexes, PostgreSQL utilizes Heap-Only Tuple (HOT) optimization: if no indexed column is modified and the new tuple fits within the existing heap page, the new version is chained directly from the original tuple via a local pointer, bypassing index updates entirely.

For non-scalar data—such as geographic coordinates, multidimensional ranges, and bounding polygons—B-Trees are inadequate because they enforce a strict one-dimensional scalar order. PostgreSQL addresses this via the Generalized Search Tree (GiST) interface:

sql
-- Spatial indexing via GiST using R-Tree bounding boxes
CREATE INDEX idx_deliveries_location ON deliveries USING GIST (location_geom);

-- Multidimensional containment query using spatial overlap operator (&&)
SELECT delivery_id, customer_id
FROM deliveries
WHERE location_geom && ST_MakeEnvelope(-73.98, 40.75, -73.95, 40.78, 4326);

GiST organizes data hierarchically using invariant predicates (e.g., minimum bounding boxes). Because bounding regions within internal nodes can overlap, GiST traversal may require searching multiple branch paths. When a leaf entry satisfies the bounding predicate, the query engine marks it as lossy and executes a recheck against the actual tuple geometry in the heap.


2. Cost-Based Optimizer Diagnostics and Plan Execution

The query optimizer evaluates alternative relational algebra plans by computing an abstract cost metric:

Total Cost=(Nseq⋅cseq)+(Nrand⋅crand)+(Ntuples⋅ccpu_tuple)+(Nops⋅ccpu_op)\text{Total Cost} = (N_{\text{seq}} \cdot c_{\text{seq}}) + (N_{\text{rand}} \cdot c_{\text{rand}}) + (N_{\text{tuples}} \cdot c_{\text{cpu\_tuple}}) + (N_{\text{ops}} \cdot c_{\text{cpu\_op}})

On modern NVMe infrastructure capable of hundreds of thousands of random IOPS, default engine cost parameters often skew execution choices. For example, PostgreSQL defaults random_page_cost to 4.0 while seq_page_cost is 1.0. On fast solid-state drives, lowering random_page_cost to 1.1 accurately reflects hardware realities and prevents the optimizer from defaulting to sequential table scans over indexed lookups.

Diagnosing real plan behavior requires inspecting memory buffers alongside execution timing:

sql
-- PostgreSQL: Comprehensive execution profiling with I/O buffer telemetry
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT u.id, o.order_uuid, o.total_amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.tier = 'enterprise'
  AND o.created_at >= NOW() - INTERVAL '7 days';

Key diagnostics in optimizer output:

  1. Buffer Hit Ratio: A high ratio of shared hit to shared read indicates queries run out of the operating system cache and PostgreSQL shared_buffers. Persistent shared read operations denote cold cache misses, saturating disk queue depths.
  2. Join Selection Mechanics:
    • Nested Loop: Optimal when the outer set is small and the inner set is indexed (O(Mlog⁡N)O(M \log N)).
    • Hash Join: Builds an in-memory hash table of the inner relation. If the hash table exceeds work_mem, it spills to temporary disk files (hybrid hash join), degrading execution speed by orders of magnitude.
    • Merge Join: Requires pre-sorted inputs (via index or explicit sort). Performs linearly across large datasets when inputs align with index order.

In MySQL 8, run EXPLAIN ANALYZE FORMAT=TREE to inspect row iteration counts and cost metrics across the volcano iterator execution tree, watching explicitly for Using temporary; Using filesort.


3. Declarative Partitioning and Pruning

When an index working set outgrows system RAM, traversal efficiency drops as leaf pages are continuously evicted. Declarative table partitioning mitigates this by horizontally splitting the logical relation into physical child tables:

sql
-- PostgreSQL Range Partitioning on Event Timestamps
CREATE TABLE audit_logs (
    id BIGSERIAL,
    event_time TIMESTAMPTZ NOT NULL,
    payload JSONB,
    PRIMARY KEY (id, event_time)
) PARTITION BY RANGE (event_time);

CREATE TABLE audit_logs_2024_q1 PARTITION OF audit_logs
    FOR VALUES FROM ('2024-01-01 00:00:00+00') TO ('2024-04-01 00:00:00+00');
CREATE TABLE audit_logs_2024_q2 PARTITION OF audit_logs
    FOR VALUES FROM ('2024-04-01 00:00:00+00') TO ('2024-07-01 00:00:00+00');

Partitioning is beneficial only when queries invoke partition pruning. Static pruning occurs during planning when literal constants bound the scan space. Dynamic pruning occurs at runtime when executor parameters or parameterized nested loop joins resolve partition targets. Omitting the partition key (event_time) forces the engine to scan every partition sequentially, multiplying metadata locking overhead and page read volume.


4. MVCC, Concurrency Anomalies, and Lock Contention

PostgreSQL and MySQL resolve read-write concurrency through fundamentally different physical architectures:

  • PostgreSQL: Implements append-only multi-versioning. Updates insert new physical tuple revisions into the heap with header attributes xmin (creating transaction ID) and xmax (invalidating transaction ID). Obsolete tuples remain in place until reclaimed by VACUUM. Long-running transactions delay the global vacuum horizon, leading to table bloat and transaction ID wraparound risks.
  • InnoDB: Performs in-place record updates. Previous tuple versions are written as reverse deltas into the rollback segment (Undo Log). Concurrent transactions reconstruct older snapshots by traversing the undo chain. Unpurged undo logs inflate the undo tablespace and degrade read performance across long chains.

Neither engine escalates row locks to table locks under memory pressure (unlike Microsoft SQL Server). Instead, throughput collapse under load stems from lock contention, deadlocks, and gap locking:

  • Under MySQL REPEATABLE READ, InnoDB uses Next-Key Locking (combining a record lock with a gap lock on the preceding index space) to prevent phantom reads. Concurrent inserts into identical index gaps block, creating severe throughput serialization.
  • Under PostgreSQL REPEATABLE READ (Snapshot Isolation), concurrent updates to the same row trigger serialization failures:
bash
ERROR: could not serialize access due to concurrent update

For high-throughput worker queues, eliminate lock waits and deadlocks using non-blocking row consumption:

sql
-- Atomic queue dispatch avoiding worker lock contention
SELECT id, payload
FROM worker_queue
WHERE status = 'queued'
ORDER BY scheduled_at ASC
LIMIT 100
FOR UPDATE SKIP LOCKED;

SKIP LOCKED instructs the engine to bypass rows currently locked by concurrent transactions, allowing multiple consumers to drain queues in parallel without blocking.


5. Distributed Caching and the XFetch Algorithm

Placing an in-memory cache (e.g., Redis) in front of the database protects connection pools and reduces read traffic. However, static Time-To-Live (TTL) strategies create cache stampedes (dogpiling): when a hot key expires, hundreds of concurrent application threads experience a cache miss simultaneously, forwarding identical queries to the database backend.

To eliminate stampedes without distributed locking, employ the XFetch probabilistic early expiration algorithm. An entry is proactively recomputed in the background if a pseudo-random value indicates that remaining TTL is dangerously close relative to the measured computation latency:

−β⋅δ⋅ln⁡(U)>TTLremaining-\beta \cdot \delta \cdot \ln(U) > \text{TTL}_{\text{remaining}}

Where:

  • β>0\beta > 0 is an aggressiveness constant (values >1> 1 trigger earlier refresh).
  • δ\delta is the compute duration (time taken to execute the query).
  • U∼Uniform(0,1)U \sim \text{Uniform}(0, 1) generates a uniform random variable.
  • TTLremaining=expiry−now\text{TTL}_{\text{remaining}} = \text{expiry} - \text{now}.
typescript
import Redis from "ioredis";

const redis = new Redis();

interface CacheRecord<T> {
  value: T;
  delta: number;   // Computation latency in ms
  expiry: number;  // Absolute epoch timestamp (ms)
}

export async function getWithProbabilisticRefresh<T>(
  key: string,
  ttlMs: number,
  beta: number,
  compute: () => Promise<T>
): Promise<T> {
  const cached = await redis.get(key);
  const now = Date.now();

  if (cached) {
    try {
      const record: CacheRecord<T> = JSON.parse(cached);
      const remainingTtl = record.expiry - now;

      // XFetch evaluation: recompute early if random draw exceeds remaining TTL
      if (-beta * record.delta * Math.log(Math.random()) < remainingTtl) {
        return record.value;
      }
    } catch {
      // Fall through to recompute on parse error
    }
  }

  const start = Date.now();
  const freshValue = await compute();
  const delta = Date.now() - start;

  const record: CacheRecord<T> = {
    value: freshValue,
    delta,
    expiry: now + ttlMs,
  };

  await redis.set(key, JSON.stringify(record), "PX", ttlMs);
  return freshValue;
}

By executing probabilistic refresh, the key is refreshed asynchronously before expiration as read frequency increases, keeping database load flat across traffic peaks.


6. Structural Takeaways

Deterministic low-latency engineering requires coordinated optimization across storage and caching layers:

  1. Tune Cost Models to Storage Hardware: Align random_page_cost with NVMe profiles to prevent the query planner from evading index scans.
  2. Mitigate Table and Index Bloat: Design workloads around HOT updates in PostgreSQL and monitor Undo Log growth in MySQL to prevent deep version-chain traversals.
  3. Enforce Non-Blocking Concurrency: Use FOR UPDATE SKIP LOCKED for task processing and maintain narrow index scopes to avoid cascading Next-Key lock waits.
  4. Eliminate Stampedes Probabilistically: Deploy XFetch across caching layers to prevent coordinated cache invalidation from saturating persistence connections.

Reliability at scale is achieved by controlling physical page allocation, minimizing index maintenance amplification, and preventing lock contention along the persistent storage path.

About the Author

huud

huud

@huud

About →

Systems architect and software engineer building high-performance distributed platforms.