Sign in to save your place.

⚡

ClickHouse – Analytics at Speed

Data For Dummies – Explained like you're 5. Install with Docker on Mac, step by step.

🎯 Welcome to the ClickHouse Course

This course is designed to hand-hold you from zero to confident. You will learn what ClickHouse is, why it’s fast, how to install and run it, and then how to create tables, write SQL, use aggregations and window functions, and connect it to the rest of your stack. Each section builds on the previous one, with clear examples and explanations.

What you’ll learn (course outline)

  1. What is ClickHouse? – Database for analytics, row vs column storage, when to use it.
  2. Why is it so fast? – Columnar storage, pruning, vectorized execution, parts and merges.
  3. History & benchmarks – Where it came from and real-world performance numbers.
  4. Install with Docker on Mac – Step-by-step so you have a running server and can run your first query.
  5. Tables and data types – How to create tables, choose types, use MergeTree, ORDER BY and PARTITION BY.
  6. SQL basics – SELECT, WHERE, ORDER BY, LIMIT, with several examples you can run yourself.
  7. Aggregations – SUM, COUNT, AVG, GROUP BY, HAVING, and ClickHouse-specific functions.
  8. Window functions – PARTITION BY, ORDER BY in windows, ROW_NUMBER, RANK, LAG/LEAD.
  9. Real-time and integrations – Inserting data via HTTP, dashboards, and pipelines.

In one sentence

ClickHouse is a database that answers analytics questions (sums, counts, trends) very fast over huge data – events, logs, sales – often in milliseconds. You’ll learn how to run it, model data, and query it like a pro.

What you’ll see after installation

Once ClickHouse is running (we’ll do that in the Install section), you’ll open http://localhost:8123 in your browser. You’ll see:

localhost:8123
Ok.

That single word Ok. means the server is running. You can send a SQL query (e.g. SELECT 1+1) and get the result (2). We’ll do that together in the Install section. Let’s start with understanding what ClickHouse is.

👶 What is ClickHouse?

ClickHouse is a database built for analytics. Unlike the database that powers your app (where you read and update one row at a time), ClickHouse is built for questions like: “What was our total revenue last month?”, “How many events per country in the last hour?”, “What’s the 99th percentile latency?” over huge amounts of data – billions of rows – and it answers them very fast, often in milliseconds.

Why “analytics” is different

In an analytics workload you usually add new rows (events, logs, sales) and then run queries that scan and aggregate (SUM, COUNT, AVG, GROUP BY). You rarely “update one row” or “delete one row.” ClickHouse is optimized for exactly that: lots of inserts and heavy read queries that touch many rows and many columns.

The main idea: columnar storage

Most databases (PostgreSQL, MySQL) store data row by row: each row is stored as one block (id, name, revenue, date together). So to get “sum of revenue,” the engine has to read every row and then take the revenue column. ClickHouse stores data column by column: all revenue values are stored together, all dates together, and so on. So when you ask for SUM(revenue), it only reads the revenue column – much less data from disk, so the query is much faster.

ROW STORE (e.g. PostgreSQL)       COLUMNAR STORE (ClickHouse)
Row1: [id, name, revenue, date]    Column revenue: [100, 200, 150, ...]
Row2: [id, name, revenue, date]   Column date:    [2024-01-01, ...]
Row3: [id, name, revenue, date]   Column name:   [Alice, Bob, ...]

SUM(revenue) → read every row,     SUM(revenue) → read only the
then take revenue.                 revenue column. ✓ Much faster.

Real use cases

People use ClickHouse for:

When to use ClickHouse – and when not to

Use ClickHouse when: you have (or will have) a lot of events, logs, or metrics; you need fast aggregations (SUM, COUNT, AVG) and filters over that data; and you’re okay with “mostly insert, sometimes query” – not updating single rows constantly.

Don’t use it for: your main application database where you constantly read/update one row (e.g. user profile, order status, shopping cart). For that, use PostgreSQL, MySQL, or similar. ClickHouse is the wrong tool for high-frequency single-row updates.

Key takeaway

ClickHouse = analytics database. Columnar storage = read only the columns you need = fast SUM/COUNT/AVG over billions of rows. Use it for events, logs, metrics; not for “get/update one user row.”

⚡ Why Is It So Fast?

ClickHouse is built so that analytics queries over huge tables run in milliseconds instead of minutes. Here are the four main ideas (simplified from how the engine actually works). You don’t need to remember every detail – just get the intuition: read less, do more per CPU cycle, and don’t block reads with writes.

1. Columnar storage – read only what you need

We already said: ClickHouse stores by column. So SUM(revenue) only reads the revenue column from disk, not every column of every row. That means less data to read. On top of that, values in one column are often similar (e.g. many repeated dates or small integers), so they compress very well. Less data and better compression mean faster reads and lower storage cost.

2. Skip what you don’t need – pruning

ClickHouse doesn’t blindly scan every row. Data is sorted by a key (you choose it when you create the table, e.g. ORDER BY date). So when you run WHERE date >= '2024-01-01', the engine can skip whole chunks of data that only contain older dates. There are also “skipping indexes” (e.g. min/max per block): if a block’s maximum value is 10 and your query asks for clicks > 15, that block is skipped. The slogan: the fastest scan is not scanning at all.

3. Vectorized execution (SIMD)

Instead of “add two numbers, then two more, then two more,” the CPU can do the same operation on 8 or 16 numbers at once (SIMD: Single Instruction, Multiple Data). ClickHouse uses this for aggregations: summing millions of rows becomes a tight loop over chunks of 8 or 16 values. That’s why SUM(...) over a billion rows can still be very fast.

4. Parts and merges – append-friendly storage

New data is written into parts: sorted, immutable files. So inserts don’t rewrite existing data and don’t block reads. In the background, ClickHouse merges small parts into bigger ones (and can do things like “keep only the latest row per key” or “delete old data by TTL”). You get fast writes and fast reads at the same time.

Key takeaway

Fast = (1) read only the columns you use, (2) skip chunks that can’t match your filter, (3) use the CPU in bulk (SIMD), (4) write to new parts so reads stay fast. When you create tables and write queries, thinking “column-first” and “filter by the sort key” will help you get the best performance.

📊 History & Real Numbers

Where ClickHouse came from

OLAP vs OLTP

ClickHouse is for OLAP (analytics), not OLTP (day-to-day transactions).

OLTP (PostgreSQL, MySQL)     OLAP (ClickHouse)
Short queries, many updates   Long queries, few updates
Row storage                   Column storage
"Get one user"               "Sum revenue by region"

Benchmarks (Lund University study)

A Master’s thesis compared ClickHouse, Elasticsearch, and MinIO on real IoT-style data (billions of rows). In short:

So “ClickHouse is fast” is backed by real experiments, not just marketing. (Source: Göransson & Wändesjö, Lund 2022.)

Key takeaway

ClickHouse came from Yandex (2009), went open source (2016), and is now a $2B company. It’s built for OLAP (analytics), not OLTP. Independent benchmarks show it’s much faster than Elasticsearch for ingest, storage, and typical analytics queries.

⚙️ Install with Docker on Mac

We use Docker on a Mac. Docker runs ClickHouse in a container so you don’t have to install it directly on your system. Follow each step and use the screenshots to confirm you’re in the right place.

1

Install Docker Desktop for Mac (if needed)

If you don't have Docker yet, download it from docker.com/products/docker-desktop. Choose the Mac version (Apple Silicon or Intel). Install and open Docker Desktop; wait until it says “Engine running.”

What you'll see

Docker Desktop shows a whale icon in the menu bar. When you open the app, the left sidebar shows Containers, Images, and at the bottom a green dot or “Engine running.” The main area may show “No containers running” or a quick-start message. That’s correct—we'll run our first container in Step 4.

2

Open Terminal

On your Mac, open Terminal (Applications → Utilities → Terminal, or press Cmd+Space and type “Terminal”). You'll run all commands here.

user@MacBook-Pro ~ %

The prompt usually shows your username, machine name, current directory (~ = home), and % (or $). Type the commands in the steps below at this prompt.

3

Pull the ClickHouse image

Download the official ClickHouse server image from Docker Hub:

docker pull clickhouse/clickhouse-server

Wait until the download finishes. You'll see “Status: Downloaded newer image for clickhouse/clickhouse-server:latest” (or a version tag).

user@MacBook-Pro ~ % docker pull clickhouse/clickhouse-server
latest: Pulling from clickhouse/clickhouse-server
a1b2c3d4e5f6: Pull complete
...
Status: Downloaded newer image for clickhouse/clickhouse-server:latest
4

Run ClickHouse in a container

Start a ClickHouse server that listens on port 8123 (HTTP) and 9000 (native). We give the container a name so we can stop it later:

docker run -d --name clickhouse-server \
  -p 8123:8123 -p 9000:9000 \
  clickhouse/clickhouse-server

-d = run in background. --name clickhouse-server = name of the container. -p 8123:8123 = map port 8123 (HTTP interface).

user@MacBook-Pro ~ % docker run -d --name clickhouse-server \
  -p 8123:8123 -p 9000:9000 \
  clickhouse/clickhouse-server
a7f3b2c1d4e5968708192f3b4c5d6e7f8a9b0c1d2

The long string is the container ID. Your ID will differ; that's normal. The container is now running in the background.

5

Check that the container is running

List running containers:

docker ps

You should see clickhouse-server with status “Up” and ports “0.0.0.0:8123->8123/tcp”.

user@MacBook-Pro ~ % docker ps
CONTAINER ID   IMAGE                         STATUS         PORTS
a7f3b2c1d4e5   clickhouse/clickhouse-server   Up 2 minutes   0.0.0.0:8123->8123/tcp, 0.0.0.0:9000->9000/tcp
NAMES
clickhouse-server
6

Test the HTTP interface

ClickHouse has a simple HTTP API. In your browser, open:

http://localhost:8123

You should see a plain “Ok.” or similar. Or from Terminal:

curl http://localhost:8123
user@MacBook-Pro ~ % curl http://localhost:8123
Ok.

In the browser, the page body is just the word Ok. (no HTML). That confirms ClickHouse's HTTP interface is up.

7

Run a simple SQL query

Send a query via HTTP:

curl 'http://localhost:8123/?query=SELECT%201%2B1'

You should get 2 in the response. Or use the clickhouse-client inside the container:

docker exec -it clickhouse-server clickhouse-client --query "SELECT 1+1"

Expected output: 2.

user@MacBook-Pro ~ % curl 'http://localhost:8123/?query=SELECT%201%2B1'
2
user@MacBook-Pro ~ % docker exec -it clickhouse-server clickhouse-client --query "SELECT 1+1"
2

Both methods return 2. Your ClickHouse server is working and answering SQL.

8

Optional: Install clickhouse-client on your Mac

To run clickhouse-client directly on your Mac (without docker exec), you can install it with Homebrew:

brew install clickhouse

Then connect to the server in Docker:

clickhouse-client --host localhost

You get an interactive prompt. Run a couple of queries to confirm:

user@MacBook-Pro ~ % clickhouse-client --host localhost
ClickHouse client version 24.x.x.
Connecting to localhost:9000 as user default.
Connected to ClickHouse server.

localhost :) SELECT 1
1

localhost :) SELECT now()
2024-02-14 12:34:56

The localhost :) prompt means you're connected. Type your SQL and press Enter; results appear below. Type quit or press Ctrl+D to exit.

9

Stopping and starting the server

To stop ClickHouse when you're done:

docker stop clickhouse-server

To start it again later (your data is still there if you used a volume, or it’s gone if you didn’t):

docker start clickhouse-server

To remove the container (and lose data inside it): docker rm -f clickhouse-server. For real use, run the container with a volume so data persists: add -v /path/on/mac:/var/lib/clickhouse to the docker run command.

Next

Go to Tables and Data Types and create your first table, then run the INSERT and SELECT examples. That’s how you’ll use ClickHouse every day.

📋 Tables and Data Types

In ClickHouse, all data lives in tables. Each column has a type (number, string, date, etc.). Tables are stored in a columnar way, so reading one column across many rows is very fast. This section teaches you how to create tables, choose types, and understand the options that affect performance.

Creating your first table

You need: (1) table name, (2) column names and types, (3) an engine, and (4) for MergeTree, an ORDER BY (how rows are sorted on disk). Here’s a minimal example:

CREATE TABLE events (
  time DateTime,
  name String,
  value Float64
) ENGINE = MergeTree()
ORDER BY time;

What this does: Creates a table events with three columns. ENGINE = MergeTree() is the standard engine for analytics (we’ll come back to it). ORDER BY time means data is sorted by time on disk – so queries like “last 7 days” can skip old data quickly.

Engine: MergeTree

ClickHouse has several table engines. For learning and for most analytics, you use MergeTree. MergeTree tables:

Other engines (e.g. Log, Memory) exist for special cases; for now stick with MergeTree.

ORDER BY – the “primary key” for sorting

ORDER BY is required for MergeTree. It defines the order of rows on disk. Queries that filter or sort by a prefix of this key are very efficient (e.g. ORDER BY (date, user_id) makes WHERE date = today() and ORDER BY date, user_id fast). You can use a single column or a tuple, e.g.:

ORDER BY (toDate(time), name)
-- Good for: WHERE toDate(time) = '2024-01-15' or GROUP BY toDate(time), name

Pick columns you often filter or group by (e.g. date, region, user_id).

PARTITION BY (optional)

You can split data into partitions so that old partitions can be dropped or moved. For time-series, a common choice is by month:

CREATE TABLE events (
  time DateTime,
  name String,
  value Float64
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(time)
ORDER BY time;

Then all rows from January 2024 go in one partition, February in another, etc. Don’t over-partition (e.g. by day for years of data can create too many parts).

Common data types

Choosing the right type saves space and speeds up queries. Here are the ones you’ll use most:

Type             Example values           Use case
─────────────────────────────────────────────────────────────────
Int8/16/32/64    42, -100                 Counts, IDs (signed)
UInt8/16/32/64    0, 65535                 Counts, quantities (non‑negative)
Float32/64        3.14, -0.5               Prices, ratios
String            'hello'                  Names, IDs, text
Date              2024-01-15               Day only (no time)
DateTime          2024-01-15 14:30:00      Date + time
Nullable(Int64)   NULL or 42               When the column can be missing

Use the smallest type that fits your data (e.g. UInt8 for age 0–255, Int32 for a 4-byte ID). Nullable(...) allows NULL in that column; use it only when you really need nulls, because it adds overhead.

Inserting data

You can insert with VALUES (a few rows) or with a format like TabSeparated for bulk load. Example:

INSERT INTO events (time, name, value) VALUES
  (now(), 'signup', 1.0),
  (now(), 'purchase', 29.99),
  (now() - INTERVAL 1 HOUR, 'click', 0.5);

Then read them back:

SELECT * FROM events ORDER BY time DESC;
┌─────────────────────┬──────────┬───────┐
│ time                │ name     │ value │
├─────────────────────┼──────────┼───────┤
│ 2024-02-14 13:00:00 │ signup   │   1   │
│ 2024-02-14 13:00:00 │ purchase │ 29.99 │
│ 2024-02-14 12:00:00 │ click    │  0.5  │
└─────────────────────┴──────────┴───────┘

Changing a table (ALTER)

You can add or drop columns without recreating the table:

ALTER TABLE events ADD COLUMN source String DEFAULT '';
ALTER TABLE events DROP COLUMN source;

Add column is lightweight; drop column is also supported. For big schema changes, check the docs (e.g. modifying column type has limitations).

Key takeaway

Create MergeTree tables with ENGINE = MergeTree(), choose ORDER BY to match how you filter/group, optionally use PARTITION BY for time-series. Pick the smallest types that fit your data. Insert with INSERT INTO ... VALUES or bulk formats.

🔤 SQL Basics

ClickHouse uses SQL. You write SELECT ... FROM table WHERE ... ORDER BY ... LIMIT ... and get results. This section builds from simple selects to filters, sorting, and limits so you can query your tables confidently.

SELECT and FROM

SELECT lists the columns (or expressions) you want. FROM is the table name. The simplest query returns all columns and all rows:

SELECT * FROM events;

To return only some columns and give one an alias:

SELECT time, name, value * 2 AS double_value
FROM events;

You can use expressions: value * 2, toDate(time), upper(name), etc. AS double_value names the result column.

WHERE – filtering rows

WHERE keeps only rows that satisfy a condition. Use comparison and logical operators:

SELECT * FROM events
WHERE time >= now() - INTERVAL 1 DAY
  AND name = 'purchase';

Common patterns:

Filtering by columns that appear in your ORDER BY (e.g. time) lets ClickHouse skip data efficiently.

ORDER BY and LIMIT

ORDER BY column sorts the result. Add DESC for descending:

SELECT * FROM events
ORDER BY time DESC
LIMIT 10;

LIMIT 10 returns only the first 10 rows. Very useful for “top N” or “last N” without scanning the whole table.

Example: list recent events of one type

“Show me the last 20 purchase events, newest first.”

SELECT time, name, value
FROM events
WHERE name = 'purchase'
ORDER BY time DESC
LIMIT 20;

Example: top event types by total value (last day)

“What were my top event types by total value in the last 24 hours?” This uses GROUP BY and sum() (we’ll cover aggregations next); the structure is the same:

SELECT name, sum(value) AS total
FROM events
WHERE time >= now() - INTERVAL 1 DAY
GROUP BY name
ORDER BY total DESC
LIMIT 10;

What each part does: WHERE keeps only the last day. GROUP BY name puts all rows with the same name into one group. sum(value) adds up value in each group. ORDER BY total DESC sorts by that sum, biggest first. LIMIT 10 returns the top 10.

┌──────────┬────────┐
│ name     │ total  │
├──────────┼────────┤
│ purchase │ 459.80 │
│ click    │ 120.50 │
│ signup   │  85.00 │
└──────────┴────────┘

Key takeaway

Use SELECT columns FROM table, then WHERE to filter, ORDER BY to sort, and LIMIT to cap rows. Filter by the sort key when you can for best performance.

📊 Aggregations

Aggregations turn many rows into one (or one per group): “total revenue,” “number of events,” “average value.” ClickHouse is built for this: sum, count, avg, min, max, and more run very fast over billions of rows because data is stored by column. This section teaches you how to use them with GROUP BY and HAVING.

What is aggregation?

An aggregate function takes many values and returns one: sum(revenue) → total, count() → number of rows, avg(value) → average. If you use an aggregate without GROUP BY, you get one row for the whole table. With GROUP BY column, you get one row per distinct value of column.

GROUP BY

GROUP BY splits rows into groups (e.g. by date, by product). Every column in SELECT must either be in GROUP BY or inside an aggregate function. Example:

SELECT name, count() AS cnt, sum(value) AS total
FROM events
WHERE time >= now() - INTERVAL 7 DAY
GROUP BY name
ORDER BY total DESC;

Here we get one row per name, with the count of rows and the sum of value for that name.

Basic aggregate functions

Useful ClickHouse aggregates

HAVING – filter after aggregation

WHERE filters rows before aggregation. HAVING filters groups after aggregation. Example: “only groups where total > 100”:

SELECT name, sum(value) AS total
FROM events
GROUP BY name
HAVING total > 100
ORDER BY total DESC;

Example: daily summary

“Per day, how many rows, total value, and average value?” (assuming you have an events table with time and value.)

SELECT
  toDate(time) AS day,
  count() AS row_count,
  sum(value) AS total_revenue,
  avg(value) AS avg_revenue
FROM events
WHERE time >= today() - 7
GROUP BY day
ORDER BY day;

Example: top 5 event types by count

“Which event types had the most rows in the last day?”

SELECT name, count() AS cnt
FROM events
WHERE time >= now() - INTERVAL 1 DAY
GROUP BY name
ORDER BY cnt DESC
LIMIT 5;

Key takeaway

Use GROUP BY to get one row per group; combine it with sum(), count(), avg(), etc. Use HAVING to filter by aggregate results. uniq() and quantile() are especially useful in analytics.

🪟 Window Functions

Window functions let you compute something per row while looking at a “window” of related rows – for example “rank within each day,” “running total so far,” or “previous row’s value.” They are powerful for rankings, trends, and time-series analysis.

What is a window?

A window is defined by:

So “for each row, consider only rows in the same partition, in this order.”

ROW_NUMBER, RANK, DENSE_RANK

These assign a number to each row within the partition:

Example: rank products by revenue within each day.

SELECT
  sale_date,
  product,
  revenue,
  row_number() OVER (PARTITION BY sale_date ORDER BY revenue DESC) AS rank
FROM sales
WHERE sale_date = today() - 1
ORDER BY sale_date, rank;

What this does: PARTITION BY sale_date means “per day.” ORDER BY revenue DESC means “best revenue first.” row_number() assigns 1 to the top product that day, 2 to the second, and so on.

LAG and LEAD

LAG(column) = value of column from the previous row (in the window order). LEAD(column) = value from the next row. Useful for “compare to previous” or “next value.”

SELECT
  time,
  value,
  LAG(value) OVER (ORDER BY time) AS prev_value,
  value - LAG(value) OVER (ORDER BY time) AS diff_from_prev
FROM events
ORDER BY time;

Here prev_value is the previous row’s value, and diff_from_prev is the change. You can use LAG(value, 2) to look two rows back.

Running sum (cumulative total)

Use sum(...) OVER (ORDER BY ...) to get a running total:

SELECT
  time,
  value,
  sum(value) OVER (ORDER BY time) AS running_total
FROM events
ORDER BY time;

Each row’s running_total is the sum of value from the first row up to that row.

Key takeaway

Window functions use OVER (PARTITION BY ... ORDER BY ...). Use row_number() for rankings, LAG/LEAD for previous/next value, and sum() OVER (ORDER BY ...) for running totals.

🔌 Real-time and Integrations

ClickHouse can ingest data in real time (HTTP, native protocol, or Kafka) and power dashboards, APIs, and data pipelines. This section shows you the main ways to get data in and connect ClickHouse to the rest of your stack.

Inserting data via HTTP

The HTTP interface (port 8123) accepts INSERTs. You send the query in the URL and the data in the body. Any language that can do HTTP POST can insert rows. Example with curl using TabSeparated format (columns separated by tab, rows by newline):

echo -e "2024-02-14 12:00:00\tpage_view\t1.0\n2024-02-14 12:01:00\tclick\t0.5" \
  | curl -X POST 'http://localhost:8123/?query=INSERT%20INTO%20events%20FORMAT%20TabSeparated' --data-binary @-

From Python you can use the clickhouse-driver or clickhouse-connect library, or plain HTTP with requests. For very high throughput, use the native protocol (port 9000) instead of HTTP.

Using the clickhouse-client for bulk load

For loading files (CSV, Parquet, etc.), you can pipe data into the client:

clickhouse-client --host localhost --query "INSERT INTO events FORMAT TabSeparated" < data.tsv

Or from inside the Docker container:

docker exec -i clickhouse-server clickhouse-client --query "INSERT INTO events FORMAT TabSeparated" < data.tsv

Supported formats include TabSeparated, CSV, JSONEachRow, Parquet. Choose the one that matches your file.

Kafka and streaming (concept)

ClickHouse can read directly from Apache Kafka using a Kafka table engine: you define a table that reads from a Kafka topic, and optionally a materialized view that writes from that table into a MergeTree table. That way, events flowing into Kafka are continuously ingested into ClickHouse. Setting this up requires Kafka and some configuration; the docs have step-by-step examples. For learning, start with HTTP or client inserts; add Kafka once you need real-time streaming.

Materialized views (concept)

A materialized view in ClickHouse is a trigger: whenever data is inserted into a source table, a query runs and its result is written into the materialized view’s underlying table. So you can keep a “pre-aggregated” table (e.g. “daily totals per event type”) that updates automatically as new rows arrive. Useful for dashboards that need fast answers over large tables.

Dashboards and BI tools

Connect ClickHouse to your favorite tool and run SQL to build charts:

All of them ultimately run SELECT queries; your job is to write efficient SQL (filter by sort key, use aggregations).

Pipelines: Airflow, dbt

Airflow: Use a ClickHouseHook or a task that runs clickhouse-client or sends HTTP requests to run ETL (e.g. “every hour, insert from S3 into ClickHouse”). dbt: Use the dbt-clickhouse adapter to define models (SQL) that build and transform tables in ClickHouse. Good for “raw table → cleaned table → aggregated table” workflows.

Key takeaway

Insert via HTTP (any app), clickhouse-client (bulk load), or Kafka (streaming). Use Grafana/Metabase/Superset for dashboards; use Airflow/dbt for scheduled pipelines. Start simple with HTTP inserts and SELECTs, then add streaming and materialized views as needed.

What’s next?

You now have a full path: install → create tables → query with SQL → aggregate and use windows → plug into dashboards and pipelines. Practice by building a small project: e.g. a table of events (time, name, value), insert some rows, then write queries for “totals per day,” “top 10 event types,” and a running sum. Use the official ClickHouse docs for reference and advanced features (replication, clusters, more table engines). Have fun with it.

October 2026 practice

Checked against the ClickHouse documentation in October 2026. The JSON type has been production-ready since version 25.3. ClickHouse Cloud rewrites a MergeTree create onto the matching SharedMergeTree engine.

This URL stays as the long reference. The modules are on hub.html. New tables use MergeTree with an ORDER BY you meant. Read JSON as the JSON type. Prefer argMax over FINAL. Use a refreshable materialized view when the result should be recomputed on a clock. On Cloud, expect SharedMergeTree even though the SQL says MergeTree.

SELECT user_id, argMax(email, updated_at)
FROM user_profiles
GROUP BY user_id

Knowledge check

What is the current way to store a JSON document?