Hands-On Participant Guide · Follow along at your own pace

Real-Time Market Analytics with ClickHouse A 90-minute hands-on lab

What you'll build
A live ClickHouse service with 26.5M forex ticks
Time
~45-60 minutes, ready in the first 15
You need
A laptop + a ClickHouse Cloud account
Cost
$0 — free trial credits

In the next 90 minutes you'll stand up your own ClickHouse Cloud service, load ~26.5 million foreign-exchange ticks from public cloud storage, run real market-analytics queries on it, and then let AI write and fix SQL for you.

Everything is copy-paste. Each query block has a Copy button — paste it into the SQL Console and run it. Green boxes tell you what you should see, so if you fall behind you can always catch up on your own. Let's go.

Before you start Have your ClickHouse Cloud login working. If you got the pre-workshop email, you should already have an account and a service running — jump to Step 2. If not, Step 1 takes about 5 minutes.
01

Sign in to ClickHouse Cloud

You need a running service before you can load data. If you already have one, skip ahead.

  1. Go to clickhouse.cloud and sign up with email, Google, or GitHub. The current free trial includes $300 in credits for 30 days.
  2. Verify your email if prompted.
  3. Create one service. Any cloud/region is fine; pick the one closest to Thailand (e.g. ap-southeast-1 Singapore) for the snappiest experience.
  4. Wait for the service to show as running, then open the SQL Console from the left-hand menu. This is where you'll run everything below.
Stuck here? Raise your hand — a helper will walk you through it. In the meantime, shadow a neighbour's screen so you don't miss anything. There's also a fallback at the end of this guide.
02

Create the table

First you'll create an empty table to hold the forex ticks. Open the SQL Console, paste the statement below, and run it.

SQL · create the table
-- 26.5M forex ticks (12 pairs incl. gold, Jan 2020); base+quote+time lead the sort key
CREATE TABLE forex
(
    datetime DateTime64(3, 'UTC'),    -- millisecond quote timestamp
    bid      Float64,
    ask      Float64,
    base     LowCardinality(String),  -- e.g. 'EUR', 'XAU' (gold)
    quote    LowCardinality(String),  -- e.g. 'USD', 'JPY'
    pair     String  ALIAS concat(base, '/', quote),   -- handy label, computed on read
    spread   Float64 ALIAS ask - bid,                  -- bid/ask spread
    mid      Float64 ALIAS (bid + ask) / 2             -- mid price
)
ENGINE = MergeTree
ORDER BY (base, quote, datetime);

Why this shape? base and quote lead the ORDER BY (the table's sort key). That means filtering by a currency pair — and a time range — lets ClickHouse skip almost all the data. You'll see exactly how much this matters in the last query of Step 4.

You should see Ok. — the table is created and empty. Next you'll fill it.
03

Load the data — two ways

The data lives in a public S3 bucket, so no keys or credentials are needed — you just point at the URL. The transfer runs server-side from S3 to ClickHouse, so it does not stream 26.5 million rows through your laptop or depend on the venue Wi-Fi.

You'll load the same 26.5M ticks two different ways so you can see both. Method 1 is ClickPipes, the managed, click-through pipeline you'd use in production. Method 2 is a single SQL line — the fastest way to get data in during a demo. Do them in order; a TRUNCATE in between keeps the row count clean.

Method 1ClickPipes — the managed, production way

ClickPipes is a fully managed ingestion service: point it at object storage or a stream and it keeps loading, no connector to build. Here's the whole flow.

  1. In the left menu, click Data sources, then the Create ClickPipe button.
ClickHouse Cloud Data sources page with the Create ClickPipe button highlighted
Step 1 · Data sources → Create ClickPipe. This is also where "Upload file" and "Add sample data" live.
  1. Under Select the data source, choose Amazon S3 (top of the Popular list).
Select the data source step showing Amazon S3 as the first popular option
Step 2 · Pick Amazon S3 under "Select the data source".
  1. On Setup your ClickPipe connection, give it any name, set Authentication method → Public (the bucket is public), and paste this into S3 file path:
S3 file path
https://partner-workshop.s3.ap-southeast-1.amazonaws.com/fx/ticks.parquet

Leave Continuous ingestion off (this is a one-time file), then click Incoming data →.

Setup your ClickPipe connection: name, Authentication method set to Public, and the S3 file path filled in
Step 3 · Name it, set Authentication method → Public, paste the S3 file path, then Incoming data →.
  1. On Incoming data, ClickHouse previews the matching file — you'll see fx/ticks.parquet at 158.43 MB. Confirm File type → Parquet and leave compression on Detect automatically, then click Parse information →.
Incoming data step previewing fx/ticks.parquet at 158.43 MB with File type set to Parquet
Step 4 · Confirm the file (fx/ticks.parquet, 158.43 MB) and Parquet format, then Parse information →.
  1. On Parse information, ClickHouse previews a sample row and detects the columns. Under Upload data to, choose Existing table, pick the Database that holds your table (usually default), and set Table → forex. Check that the source fields (datetime, bid, ask, base, quote) line up with the matching columns — they should map automatically. Leave the extra _path / _file / _size fields unmapped. Then click Details and settings →.
Parse information step: Upload data to Existing table, database and forex table selected, source fields mapped to columns
Step 5 · Existing table → your database + forex, with the source fields mapped to columns. (Shown here with the presenter's techthai database — use whichever one holds your table.)
  1. On Details and settings, leave the default Permissions as-is (ClickPipes creates a dedicated writer user for you), then click Create ClickPipe.
Details and settings step showing Permissions and the Create ClickPipe button
Step 6 · Keep the default permissions and click Create ClickPipe. No Spark job, no custom loader.
  1. You're taken back to Data sources, where your ClickPipe appears. In a few seconds its Status turns to Completed and Records shows 26,488,218 — all the ticks loaded from object storage.
Data sources list showing the ClickPipe with status Completed and 26,488,218 records
Done · Status Completed, 26,488,218 records. Click Preview to peek at the data, or head to the SQL Console.

Check what loaded. Run this in the SQL Console:

SQL · sanity check
-- expect 26,488,218 ticks across 12 pairs
SELECT count() AS ticks, uniqExact(concat(base,'/',quote)) AS pairs FROM forex;
You should see ticks = 26,488,218 and pairs = 12. That's ~26.5 million rows loaded from object storage in seconds.

Method 2The s3() one-liner — fastest in a demo

Now load the exact same data with a single SQL statement, straight from the public file. First empty the table so the count doesn't double, then insert:

SQL · reload with s3()
-- clear the rows ClickPipes just loaded so we don't double up
TRUNCATE TABLE forex;

-- load all ~26.5M ticks from the public S3 file in one line (server-side)
INSERT INTO forex
SELECT * FROM s3('https://partner-workshop.s3.ap-southeast-1.amazonaws.com/fx/ticks.parquet', NOSIGN, 'Parquet');

-- and check again (expect 26,488,218)
SELECT count() AS ticks, uniqExact(concat(base,'/',quote)) AS pairs FROM forex;

NOSIGN means "no credentials" — that's all it takes to read a public bucket. SELECT * just works because the table's column order matches the file.

Two ways, same result Same 26,488,218 rows, loaded two ways. So which should you reach for?

ClickPipes vs the s3() function — when to use which

Both got the same data in. The difference is what happens after the first load — whether you want a managed, ongoing pipeline or a quick one-shot read.

 ClickPipess3() table function
What it isA fully managed ingestion service you set up in the consoleA SQL function you call inline in a query
Best forProduction & ongoing loads you want to run and forgetQuick one-off loads, ad-hoc exploration, scripts
Ongoing / new filesCan keep watching a bucket or stream and load new data continuouslyOne-shot — reads only what's there each time you run it
SetupGuided UI, no SQL neededA single INSERT … SELECT statement
Monitoring & retriesBuilt in — status, error handling and retries in the consoleNone — you re-run it yourself if it fails
SourcesMany: S3, GCS, Azure, Kafka and other streams, Postgres/MySQL CDC, and moreObject storage only (siblings: gcs(), azureBlobStorage(), url())

Rule of thumb: reach for ClickPipes when data keeps arriving and you want it managed; reach for s3() when you just want to pull a file in right now. Today you used the one-liner to get querying fast — now let's query it.

04

Run the market queries

These are ClickHouse's "functions that replace a page of SQL." Paste each block, run it, and read the green box for what to expect. All of them run against the forex table you just loaded.

4.1  Approximate vs exact — the headline trick

How many distinct quote timestamps are in 26.5M ticks? Three functions answer the same question with different trade-offs. Run them one at a time and watch the timer in the query stats — running them separately is the whole point.

SQL · approximate (fast)
-- Approximate count of distinct timestamps (HyperLogLog)
SELECT uniq(datetime) AS distinct_ts FROM forex;
SQL · exact (more work)
-- Exact count — precise, but scans every value
SELECT uniqExact(datetime) AS distinct_ts FROM forex;
SQL · tunable middle ground
-- Adaptive + tunable accuracy vs memory
SELECT uniqCombined(datetime) AS distinct_ts FROM forex;
You should see About 24.6 million distinct timestamps from all three. But uniq returns in ~0.2 s, uniqExact takes ~3 s (about 17× slower) for the precise number, and uniqCombined lands within ~0.4% in well under a second. At billions of rows, that gap is the difference between instant and a coffee break.

4.2  Top-N in one function

The most actively quoted pairs — no GROUP BY / ORDER BY / LIMIT needed:

SQL
-- One row: an array of the 8 most actively quoted pairs (by tick count)
SELECT topK(8)(pair) AS most_active FROM forex;
You should see A single cell holding an array of 8 pairs, most-quoted first (gold, XAU/USD, tops it). topK collapses the whole table into that ranked list in one pass — it stands in for a GROUP BY … ORDER BY count() DESC LIMIT 8.

4.3  Time-series in one pass — OHLC candlesticks

Daily open / high / low / close for gold — the bread-and-butter market query:

SQL
-- One row per day: gold (XAU/USD) open, high, low, close — a candlestick chart
SELECT
    toDate(datetime)      AS day,
    argMin(bid, datetime) AS open,    -- bid at the day's first tick
    max(bid)              AS high,
    min(bid)              AS low,
    argMax(bid, datetime) AS close    -- bid at the day's last tick
FROM forex
WHERE base = 'XAU' AND quote = 'USD'
GROUP BY day
ORDER BY day;
You should see One row per day for January 2020. argMin(bid, datetime) is the bid at the day's first tick (the open) and argMax the last (the close) — no window functions, no self-joins. Watch gold climb from ~1,520 to ~1,610 across the month.

4.4  Percentiles without window gymnastics

The bid/ask spread is a liquidity gauge — and averages hide the tail, so use percentiles:

SQL
-- One row per pair: median and 99th-percentile bid/ask spread (tighter = more liquid)
SELECT
    pair,
    round(quantile(0.5)(ask - bid), 6)  AS median_spread,
    round(quantile(0.99)(ask - bid), 6) AS p99_spread
FROM forex
GROUP BY pair
ORDER BY median_spread ASC;
You should see One row per pair, tightest spread first. EUR/USD comes out most liquid (~0.00002, sub-pip); gold the widest. quantile is computed approximately in one pass — no sorting the whole column.

4.5  Combinators — one scan, two answers

Average spread during active vs quiet trading hours, side by side, in a single scan:

SQL
-- One row per pair: avg spread during the active window vs quiet hours
SELECT
    pair,
    count()                                                       AS ticks,
    round(avgIf(ask - bid, toHour(datetime) BETWEEN 7 AND 20), 6)     AS spread_active,
    round(avgIf(ask - bid, toHour(datetime) NOT BETWEEN 7 AND 20), 6) AS spread_quiet
FROM forex
GROUP BY pair
ORDER BY ticks DESC;
You should see Active-window and quiet-hours spreads in the same result. The -If combinator bolts a condition onto any aggregate: avgIf(x, cond) averages x only where cond is true — no two-pass query, no CASE WHEN. Nearly every aggregate takes it (countIf, sumIf, quantileIf, …).

4.6  The latest value — argMax

The most recent quote for every pair, in one pass:

SQL
-- One row per pair: the latest quoted bid and the timestamp it was seen
SELECT
    pair,
    argMax(bid, datetime) AS last_bid,
    max(datetime)         AS as_of
FROM forex
GROUP BY pair
ORDER BY pair;
You should see The latest bid per pair. argMax(bid, datetime) returns the bid from the row with the greatest datetime — the everyday "last price / latest status" query, without a window function or self-join.

4.7  The finale — sort key vs no sort key

Same query shape — count the ticks matching a condition — but one filter hits the sort key and the other doesn't. First, run both counts:

SQL
-- Optimized: base+quote ARE the leading sort key — the index skips to that pair
SELECT count() FROM forex WHERE base = 'XAU' AND quote = 'USD';

-- Non-optimized: bid is NOT in the sort key — no index, scans all 26.5M ticks.
-- (cache off so the full scan shows on every run)
SELECT count() FROM forex WHERE bid > 1.5
SETTINGS use_query_condition_cache = 0;
Hard to spot the difference? Both counts come back in a few milliseconds, so the timing alone barely moves — count() is fast either way. The real difference is how much data each one has to touch. To actually see it, ask ClickHouse to show its plan with EXPLAIN indexes = 1, which reports how many granules (blocks of ~8,192 rows) it will read:
SQL · see the plan
-- Optimized: the primary index skips straight to the XAU/USD rows
EXPLAIN indexes = 1
SELECT count() FROM forex WHERE base = 'XAU' AND quote = 'USD';

-- Non-optimized: bid isn't in the sort key, so nothing can be skipped
EXPLAIN indexes = 1
SELECT count() FROM forex WHERE bid > 1.5;
You should see In the optimized plan, the Indexes → PrimaryKey section shows only a slice of granules selected — about 492 / 3,234 (~16K rows). In the non-optimized plan there's no index to apply, so it reads 3,234 / 3,234 granules — every single row. Same query shape, wildly different work. The lesson: put the columns you filter on most at the front of your ORDER BY.
Tip Keep that non-optimized WHERE bid > 1.5 query on your clipboard — you'll hand it to the AI in the next step.
05

Ask the AI to fix a query

ClickHouse Cloud has a built-in co-pilot, the ClickHouse Assistant, right in the SQL Console. It knows ClickHouse functions and best practices — not generic SQL.

SQL Console with a count query in the editor and the Assistant sparkle icon in the top-right corner
Where it lives · Open the SQL Console and click the sparkle icon in the top-right corner (arrowed) to open the Assistant. Your query stays in the editor while you chat.
  1. Open the Assistant in the SQL Console (look for the assistant / sparkle icon).
  2. Paste the non-optimized query from Step 4.7:
    SQL · the slow one
    SELECT count() FROM forex WHERE bid > 1.5;
  3. Ask it, in plain English: "Why does this query scan the whole table, and how can I make it faster?"
  4. Read its answer — it diagnoses why the query scans everything (bid isn't in the sort key), then proposes concrete ClickHouse fixes with ready-to-run SQL: a lightweight minmax skip index on bid, or a projection that keeps a copy of the data pre-sorted by bid.
What to notice The Assistant explains that bid isn't in the sort key, so nothing can be pruned — then offers ClickHouse-specific fixes, each with copy-paste ALTER TABLE SQL and an "Open in editor" button. Not generic advice — it reaches for the exact features ClickHouse gives you (skip indexes, projections).
ClickHouse Assistant panel suggesting a minmax skip index and a projection on the bid column, each with ready-to-run ALTER TABLE SQL
The Assistant's answer · It diagnoses the full scan, then hands you two concrete options — a minmax skip index and a projection on bid — each with ready-to-run ALTER TABLE SQL and an Open in editor button.
06

Ask in plain English

ClickHouse Agents is the Claude-powered agent platform (currently in public beta). It answers questions in natural language — generating the SQL, running it, and drawing a chart. Open it from the SQL Console, or go directly to ai.clickhouse.cloud, and point it at the service containing the data you just loaded.

Try these, one at a time:

Watch it generate the SQL, run it, and produce a visualization — then ask it a question of your own.

The takeaway Same engine, same data you loaded ten minutes ago — now answerable in plain English. That's what "agent-facing analytics" means in practice.
07

Build a live dashboard on your data

Want to feel the speed? Run a small dashboard on your own laptop that queries the forex table in your ClickHouse Cloud service. It draws an interactive candlestick + volume chart, and every time you change a filter it shows the ClickHouse query time and how many rows were scanned — usually a few milliseconds. That's the whole point: change the pair, drag the date range, and the data comes back instantly.

Why Docker? The app runs inside a Docker container with its own pinned Python, so it doesn't matter which Python (if any) you have installed. You only need Docker.

Step 1 — Install Docker

If you don't already have it, install Docker Desktop and start it. It includes the docker compose command you'll use below. Check it's running:

bash · check docker
docker --version

Step 2 — Clone the repo

Clone the workshop repository and move into the dashboard folder:

bash · clone
git clone https://github.com/ClickHouse/ClickHouse_Demos.git
cd ClickHouse_Demos
git switch build-workshop-v1
cd workshops/RTA-mini-workshop/dashboard

Step 3 — Add your connection details

Copy the template to a real .env file, then open it in any text editor and fill in your service's details:

bash · configure
cp .env.example .env
# now edit .env and fill in the values below

Find these in the Cloud console under Connect (left menu) → HTTPS:

VariableWhat to put
CLICKHOUSE_HOSTe.g. abc123.ap-southeast-1.aws.clickhouse.cloudno https://, no port
CLICKHOUSE_PORT8443
CLICKHOUSE_USERdefault
CLICKHOUSE_PASSWORDthe password you set when you created the service
CLICKHOUSE_DATABASEthe database holding forex (usually default)
CLICKHOUSE_SECUREtrue

Step 4 — Run it

Build and start the container. The first run downloads the base image and installs dependencies (a minute or two); after that it's instant.

bash · run
docker compose up --build

When you see it listening on port 8000, open http://localhost:8000 in your browser. Stop it later with Ctrl-C.

What to try Switch the currency pair, drag the date range, or hit a quick range button — each change re-queries ClickHouse and the badge in the top-right flashes the new query time and rows scanned. Narrow the range to a single pair and watch it drop to just a few thousand rows and a handful of milliseconds — the sort key at work, exactly like Step 4.7.
Not seeing data? A red banner means the app can't reach ClickHouse — re-check the values in .env (host with no https:// and no port; port is 8443) and make sure your forex table is loaded. Full setup notes and troubleshooting are in the dashboard's README.md.
08

You're done — what next

If you got stuck at any point

Fallbacks No service yet? Shadow a neighbour's screen so you don't miss the queries, and flag a helper.
Can't create a service at all? Use the free SQL Playground — you can run read-only queries against sample data there while you sort out your account.
A query errored? Re-copy it with the Copy button (partial pastes are the usual culprit) and make sure the forex table loaded (Step 3 sanity check).