ClickHouse学习笔记(二):Docker部署与SQL入门实战

上一篇把 ClickHouse 理解成了“负责分析大量明细数据的数据库”。这一篇直接动手:启动服务,建一张支付事实表,再回答城市销售额、支付人数和累计金额几个问题。

这次实验的目标是:每条 SQL 都知道自己在统计什么,并且能用原始记录核对结果。

系列导航:

  1. 列式存储与 OLAP
  2. Docker 与 SQL 入门实战
  3. MergeTree、分区与索引
  4. 物化视图与聚合状态
  5. 查询优化与常见问题

一、环境准备与启动

项目 本文约定
Docker 能运行 Linux 容器;Windows 使用已启动的 Docker Desktop
镜像 clickhouse/clickhouse-server:25.8.1.5101
容器名称 clickhouse-learning
数据库 clickhouse_learning
学习用户 learner
HTTP / Native 端口 8123 / 9000
业务时间 Asia/Shanghai

Docker 命令使用单行形式,在 PowerShell 和 Bash 中都不需要续行符。

先检查服务端是否已启动:

1
docker version

需要看到 Server 信息。如果提示无法连接 Docker daemon,先启动 Docker Desktop 并确认使用 Linux 容器。

1.1 启动容器

1
docker run -d --name clickhouse-learning --ulimit nofile=262144:262144 -p 127.0.0.1:8123:8123 -p 127.0.0.1:9000:9000 -e CLICKHOUSE_DB=clickhouse_learning -e CLICKHOUSE_USER=learner -e CLICKHOUSE_PASSWORD=LearningOnly_123 -e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 -v clickhouse-learning-data:/var/lib/clickhouse clickhouse/clickhouse-server:25.8.1.5101

这条命令做了几件事:

  • 端口映射到本机的 127.0.0.1。
  • 用环境变量初始化学习用户和数据库。
  • 用命名卷保存数据库数据。
  • 提高文件描述符上限。

用户初始化和卷挂载方式见官方 Docker 指南。这里的密码是公开的本地实验值,命令也会留在终端历史中;正式部署需要另外管理凭据和权限。

如果端口已被占用,可以只修改宿主机一侧,比如 127.0.0.1:18123:8123。后续从宿主机访问时也要用 18123,容器内的端口仍是 8123。

1.2 确认服务可用

1
2
docker logs --tail 80 clickhouse-learning
docker exec clickhouse-learning clickhouse-client --user learner --password LearningOnly_123 --query "SELECT version(), timezone()"

容器显示 Up 只是进程仍在运行,以客户端能够完成查询为准。timezone() 返回的是当前会话使用的时区,不必预设它一定是上海;下面会显式设置。

进入交互客户端:

1
docker exec -it clickhouse-learning clickhouse-client --user learner --password LearningOnly_123 --database clickhouse_learning --multiquery

后续 SQL 在这个客户端中执行。 只有标为 Bash 的 Docker 命令在宿主机终端执行。

1
2
SET session_timezone = 'Asia/Shanghai';
SELECT version(), timezone();

session_timezone 只影响当前会话,新连接要重新设置。本系列的时间列还会显式带上时区,减少解析和展示歧义,见官方日期时间类型。


二、建表:先明确字段和统计口径

本文的数据是支付成功事实:一个订单只写一条成功支付记录,不包含退款。

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE clickhouse_learning.order_payments
(
order_id UInt64,
user_id UInt64,
city LowCardinality(String),
channel LowCardinality(String),
amount Decimal(12, 2),
paid_at DateTime('Asia/Shanghai')
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(paid_at)
ORDER BY (city, paid_at, order_id);

这里故意不用 IF NOT EXISTS:如果同名实验表已经存在,先查看 SHOW CREATE TABLE 和数据,避免以为自己创建了新表,却继续向旧表重复插入。

2.1 为什么这样选类型?

字段 类型 原因
订单、用户 ID UInt64 示例 ID 非负,使用整数表示
城市、渠道 LowCardinality(String) 不同值较少,适合考虑字典编码
金额 Decimal(12, 2) 示例需要保留两位小数
支付时间 DateTime('Asia/Shanghai') 以秒为精度,明确业务时区

Decimal(12, 2) 中 12 是总位数,2 是小数位数。金额也可以按“分”保存为整数;Float64 的二进制浮点表示不适合随意承担需要精确到分的核算。Decimal 文档介绍了精度与运算规则。

LowCardinality 不是“所有字符串都必须使用”的模板,用户 ID、随机文本等高基数字段需要结合真实分布评估,见官方类型说明。

2.2 三个建表子句先记住含义

1
2
3
ENGINE       → 使用什么表引擎
PARTITION BY → 如何把数据划分成分区
ORDER BY → 各个数据 part 内如何排序

下一篇专门讲它们的关系。这里先记住:ORDER BY 不提供订单 ID 唯一约束,查询输出也不因此自动按这个顺序返回。


三、插入八条固定记录

以下语句在新建的空表中执行一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INSERT INTO clickhouse_learning.order_payments
(order_id, user_id, city, channel, amount, paid_at)
VALUES
(1001, 101, '上海', 'app', 120.00, '2026-10-01 09:00:00'),
(1002, 102, '北京', 'web', 80.00, '2026-10-01 09:10:00'),
(1003, 101, '上海', 'app', 200.00, '2026-10-01 10:00:00'),
(1004, 103, '广州', 'app', 50.00, '2026-10-01 11:00:00'),
(1005, 104, '上海', 'web', 150.00, '2026-10-02 09:00:00'),
(1006, 102, '北京', 'app', 100.00, '2026-10-02 10:00:00'),
(1007, 105, '上海', 'app', 100.00, '2026-10-02 11:00:00'),
(1008, 103, '广州', 'web', 200.00, '2026-10-02 12:00:00');

SELECT count() AS rows, sum(amount) AS total_amount
FROM clickhouse_learning.order_payments;

按这八条输入手工计算,应该得到 8 行、1000.00 元。下面的结果表同样是依据固定数据计算的预期值,不是性能测试输出。

普通 MergeTree 不会按 order_id 自动去重。重复执行 INSERT 会重复记账;本实验的查询结果依赖“初始为空、只插入一次”这个前提。


四、用 SQL 回答三个分析问题

4.1 各城市支付金额是多少?

1
2
3
4
5
6
7
8
SELECT
city,
count() AS payment_count,
sum(amount) AS total_amount,
uniqExact(user_id) AS paying_users
FROM clickhouse_learning.order_payments
GROUP BY city
ORDER BY total_amount DESC, city;
城市 支付笔数 支付金额 去重支付人数
上海 4 570.00 3
广州 2 250.00 1
北京 2 180.00 1

上海的用户 101 支付了两次,所以四笔支付对应三个人。uniqExact 做精确去重,状态大小会随不同用户数量增长;它与近似去重函数的取舍见官方函数说明。

4.2 每天 App 渠道卖了多少?

1
2
3
4
5
6
7
8
SELECT
toDate(paid_at) AS day,
sum(amount) AS total_amount,
sumIf(amount, channel = 'app') AS app_amount,
countIf(channel = 'app') AS app_payments
FROM clickhouse_learning.order_payments
GROUP BY day
ORDER BY day;
日期 全部支付金额 App 支付金额 App 支付笔数
2026-10-01 450.00 370.00 3
2026-10-02 550.00 200.00 2

sumIf、countIf 是聚合函数与 -If 组合器的用法,只将满足条件的行计入对应指标,见官方组合器说明。

这里不能先写 WHERE channel = 'app',否则 total_amount 也只剩下 App 的金额。条件放在 WHERE 还是某个聚合函数里,会改变统计范围。

4.3 截至每天的累计金额是多少?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT
day,
day_amount,
sum(day_amount) OVER
(
ORDER BY day
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_amount
FROM
(
SELECT toDate(paid_at) AS day, sum(amount) AS day_amount
FROM clickhouse_learning.order_payments
GROUP BY day
)
ORDER BY day;
日期 当天金额 累计金额
2026-10-01 450.00 450.00
2026-10-02 550.00 1000.00

先把明细汇总为每天一行,再用窗口函数累计。窗口的 ORDER BY 决定计算顺序,外层的 ORDER BY 决定展示顺序,见窗口函数文档。

如果某一天没有支付,查询不会自动补出那一天,需要额外补齐日期。累计金额的时间范围也只覆盖子查询实际选入的数据。


五、时间筛选与 HTTP 访问

5.1 用左闭右开的区间

查询 10 月 1 日的上海支付:

1
2
3
4
5
6
SELECT order_id, amount, paid_at
FROM clickhouse_learning.order_payments
WHERE city = '上海'
AND paid_at >= toDateTime('2026-10-01 00:00:00', 'Asia/Shanghai')
AND paid_at < toDateTime('2026-10-02 00:00:00', 'Asia/Shanghai')
ORDER BY paid_at, order_id;

预期返回 1001 和 1003。相比拼接 23:59:59,[当天零点, 次日零点) 更容易在以后改用毫秒时间时保持边界正确。

5.2 区分 HTTP 与 Native 协议

1
2
宿主机上的 HTTP 客户端 → 127.0.0.1:8123 → ClickHouse HTTP 接口
容器内 clickhouse-client → localhost:9000 → Native 接口

宿主机终端可以执行:

1
curl.exe --user learner:LearningOnly_123 --data-binary "SELECT count() FROM clickhouse_learning.order_payments FORMAT JSON" http://127.0.0.1:8123/

Windows 使用 curl.exe,避免某些 PowerShell 环境中的 curl 别名;Linux 或 macOS 改成 curl。返回 JSON 的查询方式见官方 HTTP 接口。

以后接入 Java 或其他客户端时,先核对驱动使用的协议和端口。如果应用运行在另一个容器中,应用里的 localhost 指向它自己,需要使用可达的容器服务名。


六、重复实验与排障

现象 优先检查
无法连接 Docker Docker Desktop 是否启动、是否为 Linux 容器
容器创建失败 名称或宿主机端口是否已被占用
用户认证失败 用户名、密码和容器初始化配置是否一致
金额变成预期的两倍 是否重复插入了八条记录
同名表已存在 查看旧表结构,使用另一个实验表名或数据库
时间展示不符合预期 列时区、会话时区和输入字符串是否一致

查看表结构和数据:

1
2
SHOW CREATE TABLE clickhouse_learning.order_payments;
SELECT * FROM clickhouse_learning.order_payments ORDER BY order_id;

结束交互客户端输入 exit。需要暂停实验时,在宿主机执行:

1
docker stop clickhouse-learning

下次执行 docker start clickhouse-learning,再连接客户端即可。命名卷和实验数据仍然保留;单纯重启容器不会让表恢复为空。

完成这篇之后,应该已经能建表、导入数据并解释指标。下一篇继续拆解:PARTITION BY、ORDER BY 和主键索引到底各自负责什么?