Early development · PostgreSQL tiered storage

Keep hot data in PostgreSQL. Query one table.

KoldStore is an open-source tiered-storage extension for tables that grow forever. Hot rows stay in the PostgreSQL heap; older rows flush to compressed Parquet; supported reads continue through the original relation using an experimental custom scan.

89%
smaller footprint
99%
smaller hot table
49×
faster VACUUM FULL
  • Open source
  • Apache 2.0
  • Built with Rust
  • PostgreSQL 15–18
Live query path

Same table. Hot and cold. One query.

Current implementation reference: Architecture docs

KoldStore PostgreSQL tiered-storage features

Hot tier in PostgreSQL

Active rows stay in the heap and native indexes for low-latency reads and writes.

Cold tier in open Parquet

Historical rows flush to filesystem, S3/MinIO, GCS, or Azure Blob — storage you control.

Original relation, supported reads

KoldMergeScan experimentally merges hot and cold tiers for documented SELECT shapes.

Change feed by seq

koldstore.changes_since catches up from hot and cold with exclusive seq cursors or last_rows rewind.

Tiered storage on one PostgreSQL table.

  1. 1

    Keep the hot working set in PostgreSQL

    Manage a normal heap table; the hot tier keeps active rows on native indexes.

  2. 2

    Flush older rows to the cold Parquet tier

    Auto-flush or flush_table moves excess history to compressed storage you control.

  3. 3

    Query one table — and catch up with changes_since

    KoldMergeScan merges hot and cold for supported SELECT shapes. changes_since streams latest-state updates from both tiers by seq.

Enable KoldStore with ALTER TABLE.

Create a normal heap table with a primary key, point it at registered storage, and use documented SELECT shapes on the same relation after flush.

KoldStore quickstartSQL
CREATE EXTENSION IF NOT EXISTS koldstore; SELECT koldstore.register_storage(  name         => 'local-dev',  storage_type => 'filesystem',  base_path    => '/tmp/koldstore-demo',  credentials  => '{}'::jsonb,  config       => '{}'::jsonb); CREATE TABLE messages (  id bigint PRIMARY KEY,  body text NOT NULL,  created_at timestamptz NOT NULL DEFAULT now()); ALTER TABLE messages SET (  koldstore_enabled = true,  koldstore_storage = 'local-dev',  koldstore_hot_row_limit = 1000,  koldstore_min_flush_rows = 1,  koldstore_max_rows_per_file = 1000); INSERT INTO messages (id, body)SELECT gs, 'row ' || gs FROM generate_series(1, 1012) AS gs; -- Fence committed WAL so mirror seq values are durable.SELECT koldstore.wait_for_async_mirror(); -- Change feed: exclusive seq cursor over hot + cold latest-state.SELECT seq, op, pk, deleted, sourceFROM koldstore.changes_since(  table_name => 'messages'::regclass,  since_seq  => 0,  limit_rows => 100); -- Or rewind to the newest N changes (delivered oldest→newest).SELECT seq, op, pk, deleted, sourceFROM koldstore.changes_since(  table_name => 'messages'::regclass,  since_seq  => 0,  limit_rows => 1000,  last_rows  => 50); SELECT koldstore.flush_table(table_name => 'messages'::regclass); -- After flush, the same cursor still returns flushed rows from cold.SELECT seq, op, pk, deleted, sourceFROM koldstore.changes_since('messages'::regclass, 0, 100); SELECT count(*) FROM messages;  -- supported hot+cold read via KoldMergeScan

Storage and whole-table maintenance wins after flush.

KoldStore is a storage lifecycle tool for tiered PostgreSQL tables — not a universal query accelerator. After flush, the hot working set and its indexes stay small; cold history lives in zstd Parquet outside the primary heap.

The maintenance timing is specifically VACUUM (FULL, ANALYZE), a whole-table rewrite; routine autovacuum was disabled for the benchmark.

View full benchmarks

89% smaller total footprint

99% smaller PostgreSQL hot table

49× faster VACUUM FULL

KoldStore storage wins before and after flush
ResultBefore → after flushTradeoff
Total footprint (hot + cold)5.85 GiB → 671 MiB89% smaller
└ hot in PostgreSQL (heap + __cl)5.85 GiB → 72 MiB99% smaller
└ cold Parquet— → 599 MiBoutside the database
Indexes (hot + __cl)415 MiB → 11.5 MiB97% smaller
VACUUM (FULL, ANALYZE)158.7 s → 3.24 s49× faster

DML and query path (contextual)

Foreground DML and PK lookups are not guaranteed to improve. The figures below come from a clean-tree single sample on the 10M-row storage run (draft publication) and are shown for workload context only — treat them as indicative, not as a release claim. Cold-path lookups can be slower than heap-only PostgreSQL; WAL capture reports mirror catch-up as separate work. INSERT is equalized for WAL retention and is ≈ identical.

KoldStore DML and query path sample versus PostgreSQL only
OperationPostgreSQL onlyKoldStore (WAL)Trade-off
INSERT100,809 ops/s100,818 ops/s≈ identical (fair WAL-retention seed)
UPDATE81,791 ops/s55,164 ops/ssingle sample; 33% lower
DELETE130,331 ops/s145,691 ops/ssingle-sample — do not claim faster
Hot-only PK lookup3,851 ops/s4,076 ops/s≈ same
Hot+cold PK lookup3,997 ops/s1,055 ops/s74% slower (Parquet vs full-heap baseline)
Cold-only PK lookup4,032 ops/s662 ops/s84% slower (Parquet vs full-heap baseline)

Sample: 10M wide rows, hot_row_limit = 100000, max_rows_per_file = 1000000, --dml-sample 50000, warmup_rows = 1000000 (local PG16.13, 2026-08-01, single wiped pgrx instance per side). Managed sizes include the hot heap and change-log mirror. Storage wins are the primary result; DML/query rows are a single-sample snapshot. Re-run with your own --rows count. Results vary by schema, hardware, storage backend, and workload.

For tables that keep growing.

AI memory

Keep model outputs and conversation history in a hot/cold tiered table while supported retrieval queries use the original relation.

Messages & chat

Keep recent messages hot in PostgreSQL while older threads stay queryable from cold Parquet.

Audit logs

Retain append-heavy event history in open Parquet instead of growing the heap forever.

Events & IoT

Store notifications, activity, and telemetry while the PostgreSQL heap, indexes, and backup set stay manageable.

Keep more data. Keep PostgreSQL.

No replacement database. No proprietary archive format. Supported reads continue through the original PostgreSQL relation.