Polymarket Data in Postgres

Polymarket Data in Postgres

Postgres is the right tank for Polymarket data when your stack is already relational: a wide table, a BRIN index on (ts), and joins with market metadata instead of file-path bookkeeping.

Figures measured as of 2026-10-02 on the published PolyOrderbooks archive.

Schema

The table

  • books(ts timestamptz, market ref, bid numeric, ask numeric, bid_depth int, ask_depth int, vol int) — one wide row per 250ms frame.
  • Index with BRIN on ts: it exploits the natural sort of append-only loads and stays small for terrabyte-scale tables.
  • Normalize metadata separately: markets(slug, asset, type, resolution_time, outcome) joined on demand.
  • For time-series workloads, TimescaleDB hypertable on ts gives you time_bucket for free — the same schema, chunked.

Queries

Typical queries

  • Spread: SELECT time_bucket(1m, ts), median(ask - bid) FROM books WHERE market = ? GROUP BY 1.
  • Last-frame replay: SELECT DISTINCT ON (market) * FROM books WHERE ts <= $1 ORDER BY market, ts DESC;
  • One-sided ratio at settlement: count(*) FILTER (WHERE ask_depth = 0)/count(*) GROUP BY market over final windows.
  • Cross-market: JOIN on date_trunc(second, ts) against exchange rows for same-second studies.

Ops

Operations that stay sane

Bulk load via COPY from the exported CSV/Parquet, or from the s3 export directly; transactional INSERT ... ON CONFLICT for appends.

Set a retention: raw 250ms rows are worth keeping; hourly aggregates handle the rest — the archive sells raw access, your Postgres is your cache.

Free Starter reads 3 days of history at 250ms; Pro extends windows to 30–120 days for crypto and 30 for sports.

Worked example

Schema that survives scale

The wide books table with a BRIN index on ts is the core; add markets metadata (slug, type, resolution time) as a small normalized side table for joins in every dashboard.

Load with COPY from the exported CSV/Parquet in one pass; appends use ON CONFLICT (ts, market) DO NOTHING so replays are safe.

Sanity-checks

The numbers to sanity-check

Insert counts per pull should equal (end - start) in seconds x 4 for 250ms; any drift means window math is off and downstream settlement stats inherit the error.

Notes

Going further

TimescaleDB converts the same schema to a hypertable with time_bucket for free when the series becomes the primary workload.

Honest fit

Where this tool wins

Postgres is the boring, correct choice: a wide table, a BRIN index on ts, and a metadata join are enough for months of 250ms rows queried in milliseconds, and the whole stack is maintenance everyone on the team already knows.

The design honesty is in the healthy split — the SQL server holds the working copy; the archive remains the canonical full record beyond what a transactional database wants to carry.

Notes

First repro

The last-frame replay via DISTINCT ON is the single most-used query in practice; it covers mid-over-time for any historical instant without an index change.

Get started

Your first solid pull

First load uses the CSV export for one market, one day at 250ms (34,560 rows), COPY into the wide table, then query DISTINCT ON for the last frame of the day to confirm the window boundary.

Add the CHECK constraint on price bounds before the second load, so any parse slip fails at ingest, and confirm BRIN is actually being used with EXPLAIN on a ts range query.

Conclusion

How to take it further

Postgres is the middle step that most teams should actually use: a wide table with a BRIN index, a tiny dimension table, and the DISTINCT ON replay cover months of 250ms rows in milliseconds with zero new infrastructure.

The design choices that cost nothing at load and everything later are the constraint (0 to 1 prices), the UTC timestamp discipline, and the one-sided view that downstream dashboards read. Each is a one-time decision with a permanent payoff.

Move to TimescaleDB only when time_bucket becomes a daily habit; until then the plain schema is simpler to explain, back up, and hire for.

FAQ

Is Postgres fast enough for 250ms Polymarket data?

With BRIN on ts and wide rows, multi-month tables query in milliseconds for point-in-time and windowed queries. For billion-row analytics, ClickHouse is the upgrade.

What index do I use on timestamp series?

BRIN on (ts) — the rows load in order, BRIN pages stay tiny, and range queries remain fast at scale. TimescaleDB hypertables add time_bucket.

Can Postgres hold the full archive?

It can hold a lot; the archive backends are separately engineered at source, and your Postgres is best treated as the working copy, not the archive itself.