Which database is best for futures tick data? Choose one that preserves the events your application needs before comparing query speed. Two trades can share a timestamp, continuous symbols can change their underlying contracts, and database deduplication rules differ. A small synthetic replay test can expose these mistakes before a fast query turns them into plausible but incorrect results.
For trade analytics, start with a trustworthy trade-event archive and a database whose duplicate and correction handling you can verify. For order-book reconstruction, trade records alone are insufficient: you also need the relevant book events and recovery rules. A fast database cannot reconstruct events that its upstream feed never supplied.
This is an evaluation method, not a database speed ranking or a claim that one market-data service covers every futures venue.
Separate the feed from the database
A market-data API delivers a documented representation of the market. A raw event log preserves what your collector received. A query database organizes that material for analysis. These responsibilities can share infrastructure, but they should have separate acceptance criteria.
TickDB is a useful example of the first layer. Its current documentation describes REST and WebSocket trade data for China and Hong Kong futures. That establishes a documented data-source option for those markets; it does not establish a user-queryable SQL database, exchange-native packet capture, or complete historical replay.
Preserve the original message, source, receive time and a durable collector receipt identifier before normalization. Keep source event time separately, with its documented unit and meaning. Receipt order can reproduce what your collector observed; sorting by event time answers a different question. Neither ordering should silently replace the other.
Suggested architecture: delivery, preservation and analytical queries are separate responsibilities, not a vendor architecture diagram.
Decide what identifies a futures event
A symbol plus a millisecond timestamp is not a universal event key. Two genuine trades can arrive with the same timestamp and price. Collapsing them can undercount volume while leaving a chart looking reasonable.
Use a documented event identifier within its documented scope when one exists. That scope may require a source, venue, session and instrument. Do not assume an ID is globally unique or survives a session reset. If the source provides no reliable identifier, preserve separate receipts and mark identity unresolved instead of silently deleting apparent duplicates.
The distinction matters when adapting TickDB messages. Its WebSocket documentation specifies single-trade objects for China and Hong Kong futures, and explicitly lists a trade ID for Hong Kong futures. The China futures example does not establish the same identifier guarantee. One universal adapter that assumes every message has an ID would overstate the documented contract.
Two events can share a timestamp. This synthetic example illustrates identity, not measured exchange traffic.
Contract identity needs the same care. Store the concrete contract and the effective mapping of any continuous alias separately. TickDB's data specification distinguishes regular China futures contracts from continuous symbols intended for market tracking, not delivery. Do not treat a continuous series as one permanent tradable contract. Record the roll rule and any price adjustment before comparing results across a rollover.
Test the projection without destroying the evidence
Keep the raw deliveries immutable and build a separate analytical projection. The following standard-library Python example uses a deliberately synthetic schema. It assumes source-defined IDs scoped by source, session and contract, plus explicitly ordered revision numbers. Those are test assumptions, not fields promised by every provider.
The seven deliveries include two trades at the same time, a repeated delivery, a correction, a late older revision and two receipts without IDs. The projection retains both identified trades, applies the correction, and leaves the unidentified receipts unresolved.
def project(events): """Synthetic schema: IDs and revision order are source-defined.""" latest, unresolved = {}, [] for event in events: row = dict(event) if row.get("event_id") is None: unresolved.append(row) # Never guess identity from time/price. continue key = tuple(row[k] for k in ("source", "session", "contract", "event_id")) old = latest.get(key) if old is None or row["revision"] > old["revision"]: latest[key] = row elif row["revision"] == old["revision"]: payload = lambda r: {k: v for k, v in r.items() if k != "receipt_id"} if payload(row) != payload(old): raise ValueError("Conflicting payload at the same revision") return list(latest.values()), unresolved def example(): base = dict(source="synthetic", session="S1", contract="FUT-A", event_ms=1000, price="100.00", revision=1) a = dict(base, event_id="A", receipt_id=1) b = dict(base, event_id="B", receipt_id=2) deliveries = [a, b, dict(a, receipt_id=3), dict(a, revision=2, price="100.25", receipt_id=4), dict(a, receipt_id=5), dict(base, event_id=None, receipt_id=6), dict(base, event_id=None, receipt_id=7)] resolved, unresolved = project(deliveries) assert len(resolved) == 2 and len(unresolved) == 2 assert next(r for r in resolved if r["event_id"] == "A")["price"] == "100.25" print("7 deliveries: 2 resolved events, 2 unresolved receipts") if __name__ == "__main__": example() The local synthetic run prints 7 deliveries: 2 resolved events, 2 unresolved receipts. It does not prove market completeness, throughput or database durability. The unresolved receipts may include duplicates; retaining them avoids making an unsupported identity decision. Do not aggregate them as confirmed distinct trades without further reconciliation.
A changed payload at the same revision raises an error rather than silently picking a winner. Production ingestion should quarantine that conflict while retaining its raw message. This example does not implement cancellations, persistent checkpoints or a source-specific sequence protocol. Add those cases according to the feed contract, including sequence resets and recovery after a collector restart.
Compare duplicate semantics before performance
QuestDB and ClickHouse illustrate why “supports deduplication” is not a sufficient selection criterion.
QuestDB's current deduplication documentation describes explicit key columns, including the designated timestamp, for WAL tables. Its UPSERT mode keeps the latest write for matching keys. That is write order, not necessarily the highest source revision: a late old correction needs deliberate handling. Enabling deduplication also does not clean up pre-existing duplicates retroactively.
ClickHouse ReplacingMergeTree identifies duplicates through the sorting key and removes them during background merges whose timing is not guaranteed. A query cannot assume those merges have already completed. Evaluate query-time replacement semantics, including FINAL where appropriate, and measure the actual query shape you will use.
Neither behavior is inherently the right answer for every workload. An immutable audit archive and a latest-corrected analytical view have different requirements. Feed the same test deliveries into each candidate and compare the results immediately after ingestion, after maintenance, and after replay. Check contract separation, retained revisions and unresolved records, not just the final row count.
Benchmark only the candidates that preserve meaning
Once the semantic tests pass, measure your real workload: sustained ingestion, late arrivals, narrow contract-and-time queries, broader aggregates, and recovery. Hold the input dataset, hardware, retention policy and query results constant. Report cold and warm query behavior, concurrency and storage usage rather than publishing a single unexplained latency number.
Validate backfill separately. TickDB's documented REST trades endpoint accepts a symbol and a limit of up to 1,000 records; that alone does not demonstrate arbitrary time-range recovery or lossless polling. Ask what can be recovered after a longer outage, and test the exact boundary. Storage and redistribution rights also need separate confirmation.
Choose the simplest system that passes these checks at your required scale. If a candidate cannot explain which events survived replay, a faster query is answering an uncertain question.
Documentation checked November 14, 2026: TickDB Data Specification and Trade documentation; QuestDB Deduplication; ClickHouse ReplacingMergeTree. Code results are synthetic local validation, not a provider benchmark.



