ClickHouse for Quantitative Finance: Market Data at Scale in C++

by cppforquants
Clickhouse for Quantitative Finance

Quantitative finance produces a lot of data. Market ticks, order books, historical prices, risk scenarios, P&L series, backtest results and model outputs can quickly grow from millions to billions of rows. At that scale, the database starts to matter: ClickHouse is a high-performance columnar database written in C++ and designed for analytical workloads over very large datasets. For all those reasons, ClickHouse for quantitative finance is a good fit.

Instead of optimizing for frequent row-by-row updates, it is built to scan, aggregate and filter huge amounts of data quickly which makes it particularly interesting for quantitative research, market data analysis and risk systems. In this article, we will look at how ClickHouse can be used to store and query financial market data, why its architecture fits many quant workloads, and how a C++ application can interact with it in practice.

1. What is ClickHouse?

ClickHouse is an open-source, column-oriented database designed for analytical workloads. More specifically, it is an OLAP database (Online Analytical Processing) built to scan, filter and aggregate very large datasets quickly.

This is quite different from a traditional transactional database such as PostgreSQL or MySQL.

In a row-oriented database, the values belonging to one record are typically stored together. This works well when an application frequently reads or updates individual rows.

ClickHouse instead stores data by column. If a table contains:

  • timestamp
  • symbol
  • bid
  • ask
  • volume
  • exchange

and a query only needs timestamp, symbol and bid, ClickHouse can largely avoid reading the other columns. This reduces the amount of data that needs to be read and also allows similar values within each column to be compressed efficiently.

This architecture is particularly effective for queries such as:

SELECT
    symbol,
    avg(price)
FROM trades
WHERE timestamp >= now() - INTERVAL 1 DAY
GROUP BY symbol;

Such a query may need to inspect millions or billions of observations but only a small number of columns.

That is exactly the kind of workload that appears frequently in quantitative finance: querying historical prices, aggregating trades, analysing market data, calculating statistics over time windows, or examining large sets of risk and simulation results.

ClickHouse for quantitative finance is therefore not necessarily a replacement for the transactional database behind an application. It is better thought of as a high-performance analytical engine for datasets where fast reads, filtering and aggregation at scale matter more than frequent row-by-row updates.

How fast? Very fast.

2.Why columnar databases fit quantitative finance

Quantitative finance is naturally data-intensive. A market data table may contain billions of observations across prices, volumes, instruments, venues and timestamps. Risk systems can generate similarly large datasets from Monte Carlo scenarios, sensitivities, exposures and P&L calculations.

The important point is that quants rarely need every field for every observation.

Consider a table containing:

timestamp
symbol
bid
ask
volume
exchange
currency

A query calculating the average spread for a given instrument may only need timestamp, bid and ask. A row-oriented database typically stores all the values belonging to each observation together. A columnar database stores values from the same column together instead.

Conceptually:

Row-oriented

09:30:00 | AAPL | 230.10 | 230.12 | 1500 | NASDAQ
09:30:01 | AAPL | 230.11 | 230.13 | 1200 | NASDAQ
09:30:02 | AAPL | 230.09 | 230.11 | 1800 | NASDAQ

versus:

Column-oriented

timestamp: 09:30:00, 09:30:01, 09:30:02, ...
symbol:    AAPL, AAPL, AAPL, ...
bid:       230.10, 230.11, 230.09, ...
ask:       230.12, 230.13, 230.11, ...
volume:    1500, 1200, 1800, ...

This has several advantages for quantitative workloads.

Reading only what is needed

Many financial queries operate on a subset of the available variables. If we want to calculate the average bid-ask spread:

SELECT avg(ask - bid)
FROM market_data
WHERE symbol = 'AAPL';

the database mainly needs the symbol, bid and ask columns.

With billions of rows, avoiding unnecessary columns can significantly reduce the amount of data that must be read from storage.

Efficient compression

Financial datasets also contain a lot of repeated or highly structured information.

A symbol column may contain the same ticker millions of times. Exchange and currency fields have low cardinality. Timestamps are ordered and numerical values often change gradually.

Storing similar values together makes this data highly compressible. Better compression means less disk space, but more importantly it can mean less data to read from disk when executing analytical queries.

Fast aggregation

Quantitative analysis frequently involves operations such as:

AVG
SUM
MIN
MAX
COUNT
quantiles
GROUP BY

For example:

SELECT
    symbol,
    avg(price),
    max(price),
    min(price)
FROM trades
WHERE timestamp >= '2026-01-01'
GROUP BY symbol;

Instead of retrieving individual transactions one at a time, the objective is to process a very large number of observations and derive statistics from them.

That is exactly the workload column-oriented databases are designed to handle.

A natural match for time-series market data

Market data is also usually append-heavy: that’s why Clickhouse for quantitative finance is interesting.

New ticks arrive continuously:

t1 → price
t2 → price
t3 → price
...
tn → price

Historical observations are then queried repeatedly for research, backtesting, monitoring and analysis.

This pattern — large volumes of data being appended and subsequently scanned or aggregated — fits ClickHouse particularly well.

The same idea applies beyond market ticks. A quant platform might use a columnar database to analyse:

  • historical prices and trades,
  • order-book observations,
  • Greeks and sensitivities,
  • P&L histories,
  • backtest results,
  • Monte Carlo scenarios,
  • counterparty exposures,
  • model predictions,
  • risk metrics.

This does not mean every financial database should be columnar. Transactional systems still need databases optimized for individual inserts, updates and lookups.

But when the problem becomes “analyse hundreds of millions or billions of observations quickly”, the columnar model becomes particularly attractive — and this is the type of problem ClickHouse was built to solve.

3. Accessing ClickHouse from C++

For a C++ quant application, ClickHouse can be accessed using the official clickhouse-cpp client library.

The client is written in C++17 and communicates with ClickHouse through its native binary protocol. It provides direct support for executing SQL queries and sending or receiving data in ClickHouse’s native columnar Block format.

A minimal connection looks like this:

#include <clickhouse/client.h>

using namespace clickhouse;

int main()
{
    Client client(ClientOptions()
        .SetHost("localhost")
        .SetPort(9000));

    client.Execute("SELECT 1");
}

The interesting part for quantitative applications is that the C++ API is also column-oriented.

Imagine that we want to store some market data:

CREATE TABLE market_data
(
    timestamp DateTime64(3),
    symbol String,
    bid Float64,
    ask Float64,
    volume UInt64
)
ENGINE = MergeTree
ORDER BY (symbol, timestamp);

We can construct the corresponding columns directly in C++:

#include <clickhouse/client.h>

using namespace clickhouse;

int main()
{
    Client client(ClientOptions().SetHost("localhost"));

    auto symbol = std::make_shared<ColumnString>();
    auto bid    = std::make_shared<ColumnFloat64>();
    auto ask    = std::make_shared<ColumnFloat64>();
    auto volume = std::make_shared<ColumnUInt64>();

    symbol->Append("AAPL");
    symbol->Append("AAPL");
    symbol->Append("MSFT");

    bid->Append(230.10);
    bid->Append(230.11);
    bid->Append(415.20);

    ask->Append(230.12);
    ask->Append(230.13);
    ask->Append(415.24);

    volume->Append(1500);
    volume->Append(1200);
    volume->Append(900);

    Block block;

    block.AppendColumn("symbol", symbol);
    block.AppendColumn("bid", bid);
    block.AppendColumn("ask", ask);
    block.AppendColumn("volume", volume);

    client.Insert("market_data", block);
}

Instead of inserting one trade or tick at a time, we construct a batch of columns and send the entire block to ClickHouse.

This is a natural model for market data pipelines:

Market feed
    ↓
C++ application
    ↓
Accumulate ticks in memory
    ↓
Build ClickHouse Block
    ↓
Batch insert
    ↓
ClickHouse

For large datasets, clickhouse-cpp also supports a streaming batch pattern using BeginInsert, SendInsertBlock and EndInsert, allowing applications to send multiple blocks without keeping the entire dataset in memory.

Querying market data

Reading data follows the same idea. Query results are returned as blocks containing typed columns.

For example:

client.Select(
    R"(
        SELECT
            symbol,
            avg(ask - bid) AS avg_spread
        FROM market_data
        GROUP BY symbol
    )",
    [](const Block& block)
    {
        auto symbol =
            block[0]->As<ColumnString>();

        auto spread =
            block[1]->As<ColumnFloat64>();

        for (size_t i = 0; i < block.GetRowCount(); ++i)
        {
            std::cout
                << symbol->At(i)
                << " "
                << spread->At(i)
                << '\n';
        }
    });

The SQL computation happens inside ClickHouse, while the C++ application receives only the aggregated result.

This distinction is important.

Rather than loading hundreds of millions of market observations into C++ and calculating statistics locally, we can push filtering and aggregation to ClickHouse:

Bad approach

ClickHouse
   ↓
500 million rows
   ↓
C++
   ↓
Calculate statistics


Better approach

ClickHouse
   ↓
Filter + aggregate
   ↓
Small result
   ↓
C++

For a quant system, C++ can therefore remain responsible for pricing, simulation or trading logic, while ClickHouse handles the large analytical dataset behind it.

This combination is particularly attractive when an application needs to process large volumes of market data without turning the C++ process itself into a database engine.


4. An Example of Low-Latency Architecture with ClickHouse

ClickHouse can fit very well into a low-latency trading or market-data architecture, but usually not in the critical execution path.

A trading system may need to react to market data in microseconds or milliseconds. That part is typically handled in memory, inside C++ processes optimized for deterministic latency.

ClickHouse for quantitative finance instead sits next to the trading system as a fast analytical store.

A simplified architecture could look like this:



The key idea is to separate the hot path from the analytical path.

The hot path

The hot path contains everything that directly affects a trading decision:

Market data
    ↓
C++ feed handler
    ↓
Pricing / signal
    ↓
Risk checks
    ↓
Order

This path should avoid unnecessary network calls, disk access and database queries.

For the most latency-sensitive systems, the relevant state is usually held directly in memory.

The analytical path

At the same time, the same market data can be copied into an in-memory buffer or message queue:

Market data
    ↓
Buffer
    ↓
Batch
    ↓
ClickHouse

The C++ application may accumulate several thousand observations and periodically send them to ClickHouse as a batch.

ClickHouse can then be used for queries such as:

SELECT
    symbol,
    avg(ask - bid) AS avg_spread,
    sum(volume) AS total_volume
FROM market_data
WHERE timestamp >= now() - INTERVAL 5 MINUTE
GROUP BY symbol;

This gives researchers, monitoring systems and risk processes access to very recent data without adding latency to the trading engine itself.

Why batching matters

A common mistake would be to insert every market tick individually:

Tick
 ↓
INSERT
 ↓
Tick
 ↓
INSERT

That creates unnecessary network and database overhead.

A better approach is:

Tick
Tick
Tick
Tick
...
   ↓
In-memory batch
   ↓
Single ClickHouse insert

For example, a C++ process could buffer 10,000 observations before sending them as one block.

This preserves low latency in the producer while allowing ClickHouse to ingest data efficiently.

A typical division of responsibilities

In such an architecture:

ComponentResponsibility
C++Market-data processing, pricing, signals, execution
Memory / queueDecoupling the trading path from persistence
ClickHouseHistorical storage and analytical queries
Python / C++ / dashboardsResearch, monitoring and risk analytics

This separation is important.

ClickHouse is not replacing the low-latency C++ engine. Instead, it provides a high-performance analytical layer around it.

For quantitative systems, this can be a very effective combination:

C++ for decisions in microseconds or milliseconds, ClickHouse for analysing millions or billions of observations shortly afterwards.

5. Conclusion on Clickhouse

So, how to conclude? Maybe with a comparison with other databases and a list of strengths/weaknesses.

Different systems are optimized for different workloads. PostgreSQL is a strong general-purpose relational database, DuckDB is excellent for local analytical work, kdb+ has a long history in high-performance market data, and InfluxDB is designed around time-series workloads.

ClickHouse sits in a different part of the spectrum. Its main strength is large-scale analytical processing: scanning, filtering and aggregating very large datasets quickly.

The comparison below is therefore not about choosing a universal winner, but about understanding where ClickHouse fits relative to other common database choices in quantitative systems.

The summary below highlights the main strengths and weaknesses of ClickHouse and helps place it in the broader context of quantitative finance infrastructure:

You may also like