ClickHouse实战:如何用列式存储优化你的数据分析报表(附MySQL对比案例)
最近和几个做数据的朋友聊天,大家普遍有个头疼的问题:业务跑得飞快,数据量蹭蹭往上涨,但一到出报表的时候,系统就卡得像老牛拉破车。尤其是那些需要聚合大量历史数据、按天/周/月统计的运营看板,查询时间从几分钟拖到几十分钟,业务方等得着急,技术团队也压力山大。如果你也正被这类问题困扰,觉得MySQL在分析场景下越来越力不从心,那么今天聊的ClickHouse,很可能就是你一直在找的那把“快刀”。
ClickHouse并不是一个万能数据库,它专为联机分析处理(OLAP)而生。简单理解,OLAP场景的特点是读多写少、查询复杂、数据海量。它不关心你刚刚插入的那一条订单记录,而是关心“过去一个月,来自华东地区的用户,在晚上8点到10点之间,购买金额超过500元的订单总数和平均客单价是多少”这类问题。传统的行式数据库(如MySQL)在处理这类需要扫描大量行、但只关心少数几个列的查询时,会读取大量无关数据,造成巨大的I/O和内存浪费。而ClickHouse采用的列式存储,恰恰是解决这个痛点的绝佳设计。接下来,我将通过一个完整的对比案例,带你深入理解ClickHouse的威力,并手把手展示如何将你的分析报表迁移过来,实现性能的飞跃。
1. 核心原理:为什么列式存储是分析查询的“加速器”?
要理解ClickHouse为什么快,我们必须先抛开“数据库”这个抽象概念,从数据在磁盘上的物理存储方式说起。这决定了数据被读取和计算的最底层效率。
1.1 行存与列存的根本差异
想象一张巨大的电商订单表,它有几十个字段:order_id, user_id, product_id, amount, province, city, create_time... 在MySQL这类行式数据库中,数据是这样一行接一行紧凑排列在磁盘上的:
[订单1的ID, 用户1的ID, 产品A的ID, 100.00, 浙江, 杭州, 2023-10-01...]
[订单2的ID, 用户2的ID, 产品B的ID, 200.00, 上海, 浦东, 2023-10-01...]
[订单3的ID, 用户3的ID, 产品C的ID, 150.00, 江苏, 南京, 2023-10-01...]
...
当你执行一个分析查询,例如 SELECT province, SUM(amount) FROM orders GROUP BY province,数据库引擎需要把每一行的所有数据(包括你不需要的order_id, user_id等)从磁盘读到内存,然后再从中提取出province和amount两个字段进行聚合。对于十亿行数据,这意味着十亿次完整的行读取,I/O开销巨大。
而在ClickHouse的列式存储中,同一列的数据被物理上存储在一起:
文件1 (order_id列): [订单1的ID, 订单2的ID, 订单3的ID, ...]
文件2 (amount列): [100.00, 200.00, 150.00, ...]
文件3 (province列): [浙江, 上海, 江苏, ...]
...
执行同样的聚合查询时,系统只需要读取province.col和amount.col这两个文件。由于同一列的数据类型一致,ClickHouse可以采用效率极高的压缩算法(如LZ4、ZSTD),通常能达到5-15倍的压缩比。这意味着从磁盘读取的数据量急剧减少,同时,连续读取同一类型的数据也非常利于现代CPU的预取和向量化执行。
提示:向量化执行是ClickHouse性能的另一大支柱。它允许CPU使用SIMD指令,一次性对一个数据块(比如1024行)中的同一列数据进行同一种操作(比如相加),而不是传统的一次操作一行,这极大地提升了CPU缓存利用率和计算吞吐量。
1.2 为分析而生的表引擎:MergeTree家族
ClickHouse的强大不止于列存,其核心在于一系列为分析优化的表引擎,尤其是MergeTree家族。它引入了几个关键概念:
- 分区(Partition):数据按分区键(通常是日期)被划分到不同的物理目录。查询时,可以快速跳过不相关分区的数据,这是实现高效时间范围查询的基础。
- 主键(Primary Key)与索引:ClickHouse的主键并不唯一,它定义了数据的排序顺序。数据在分区内按主键排序存储,并基于此生成稀疏索引,使得在亿级数据中定位特定范围的数据极快。
- 数据合并(Merge):数据写入时先进入小的数据片段,后台线程会定期合并这些片段,优化存储结构和索引。这也是“MergeTree”名字的由来。
理解这些原理,我们就能在设计表结构时做出正确决策,为后续的性能打下基础。
2. 实战准备:从MySQL到ClickHouse的环境与数据迁移
理论讲得再多,不如亲手试一试。我们假设一个经典场景:一个电商公司的订单分析系统。原有的MySQL订单表已经积累了数亿条记录,每日聚合查询变得异常缓慢。现在,我们要在ClickHouse中重建这个分析模型。
2.1 环境搭建与基础配置
首先,你需要一个ClickHouse服务。对于本地测试,Docker是最快捷的方式。
# 拉取最新稳定版ClickHouse镜像
docker pull clickhouse/clickhouse-server:latest
# 运行ClickHouse容器
docker run -d \
--name some-clickhouse-server \
-p 8123:8123 \ # HTTP API端口,用于连接和查询
-p 9000:9000 \ # 原生TCP协议端口
--ulimit nofile=262144:262144 \
clickhouse/clickhouse-server:latest
启动后,你可以使用clickhouse-client命令行工具,或者任何支持HTTP接口的客户端(如DBeaver、Tabix)进行连接。连接成功后,创建一个用于测试的数据库:
CREATE DATABASE IF NOT EXISTS ecommerce_analysis;
USE ecommerce_analysis;
2.2 表结构设计与DDL语句
在ClickHouse中建表,需要仔细考虑分区键、主键和排序键。根据我们的查询模式(经常按天、按省份分析销售额),设计如下:
CREATE TABLE orders_analysis
(
`order_id` UInt64,
`user_id` UInt32,
`product_id` UInt32,
`amount` Decimal(10, 2),
`province` String,
`city` String,
`create_time` DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(create_time) -- 按月分区,平衡分区数量与查询粒度
ORDER BY (province, city, create_time) -- 排序键,优先按常用过滤和分组字段排序
SETTINGS index_granularity = 8192; -- 索引粒度,默认8192,表示每8192行一个索引标记
这里有几个关键点:
- PARTITION BY:我们选择按月分区。如果按天分区,对于数年数据会产生上千个分区,管理开销增大;按年分区则单个分区过大,查询时无法有效剪枝。按月是一个常见且平衡的选择。
- ORDER BY:这可能是最重要的设计。数据在磁盘上按
(province, city, create_time)的顺序物理排序。这意味着,所有按province或(province, city)进行过滤或GROUP BY的查询,都能获得最佳的读取性能,因为相关数据紧密排列在一起。 - ENGINE:使用基础的
MergeTree引擎,它已经能提供强大的分析能力。对于更特殊的场景(如需要去重、聚合),还有SummingMergeTree、AggregatingMergeTree等变种。
2.3 数据迁移策略与工具
将数据从MySQL迁移到ClickHouse,有几种常用方法:
- 使用CSV文件中转:从MySQL用
SELECT ... INTO OUTFILE导出为CSV,然后用ClickHouse的INSERT INTO ... FORMAT CSV导入。适合一次性迁移历史数据。 - 使用
clickhouse-mysql数据读取引擎:ClickHouse可以创建一个虚拟表映射到MySQL,然后通过INSERT ... SELECT直接读取数据。适合数据量不大或初次迁移。 - 使用流式处理工具:对于持续同步,可以使用Canal/Kafka + ClickHouse的
Kafka引擎表,或者使用MaterializeMySQL数据库引擎(实验性)进行Binlog同步。
这里演示第二种方式,进行一次性历史数据迁移:
-- 在ClickHouse中创建一个指向MySQL的引擎表
CREATE TABLE mysql_orders
(
`order_id` UInt64,
`user_id` UInt32,
-- ... 其他字段定义需与MySQL类型匹配
)
ENGINE = MySQL('mysql-host:3306', 'your_db', 'orders', 'user', 'password');
-- 将数据从MySQL表插入到ClickHouse目标表
INSERT INTO orders_analysis SELECT * FROM mysql_orders;
迁移完成后,你可以立即执行一个简单的查询来验证数据量和基本统计:
SELECT count(), formatReadableSize(total_bytes) as size
FROM system.parts
WHERE table = 'orders_analysis' AND active;
这个查询会告诉你表中有多少活跃的数据部件及其总大小,你可以直观感受到列式存储带来的压缩效果。
3. 性能对决:相同查询,不同数据库的响应时间实测
数据就位,是时候进行一场真刀真枪的对比测试了。我们设计几个典型的分析查询,分别在MySQL(假设已建立合适索引)和ClickHouse上执行,观察其性能差异。为了模拟真实压力,我们假设表中已有约1亿条订单数据。
3.1 测试用例设计
我们设计三个复杂度递增的查询:
- 简单聚合:统计总销售额。
- 带分组的维度分析:按省份统计销售额排名。
- 复杂时间窗口分析:统计最近7天,每日每个省份的销售额,并与前一日对比增长率。
3.2 查询对比与结果分析
查询1:统计总销售额
- MySQL查询:
SELECT SUM(amount) AS total_sales FROM orders;- 执行分析:即使
amount字段有索引,SUM聚合也需要扫描整个索引或全表。在亿级数据下,这通常是一个缓慢的操作。
- 执行分析:即使
- ClickHouse查询:
SELECT SUM(amount) AS total_sales FROM orders_analysis;- 执行分析:只需读取
amount这一列的数据文件。由于数据高度压缩且连续存储,I/O量极小。向量化引擎能快速对压缩后的数据块进行求和。
- 执行分析:只需读取
查询2:按省份统计销售额排名
- MySQL查询:
SELECT province, SUM(amount) AS sales FROM orders GROUP BY province ORDER BY sales DESC LIMIT 10;- 潜在瓶颈:需要临时排序和聚合大量数据,如果内存不足会使用磁盘临时表,速度急剧下降。
- ClickHouse查询:
SELECT province, SUM(amount) AS sales FROM orders_analysis GROUP BY province ORDER BY sales DESC LIMIT 10;- 性能优势:由于表按
(province, ...)排序,相同省份的数据物理相邻,GROUP BY操作效率极高。聚合计算在数据读取过程中即可部分完成。
- 性能优势:由于表按
查询3:最近7天每日分省份销售额及环比
这是一个更贴近实际看板需求的复杂查询。
-- ClickHouse 查询示例
WITH daily_sales AS (
SELECT
toDate(create_time) AS day,
province,
SUM(amount) AS sales
FROM orders_analysis
WHERE create_time >= today() - 7
GROUP BY day, province
)
SELECT
day,
province,
sales,
lagInFrame(sales) OVER (PARTITION BY province ORDER BY day) AS prev_day_sales,
if(prev_day_sales > 0, round((sales - prev_day_sales) / prev_day_sales * 100, 2), NULL) AS growth_rate_percent
FROM daily_sales
ORDER BY province, day;
在MySQL中执行类似的查询(涉及窗口函数LAG和多层聚合),在亿级数据上很可能超时或耗尽资源。而在ClickHouse中,得益于其对窗口函数和复杂子查询的优秀支持,以及底层列存和向量化执行,此类查询通常在秒级甚至亚秒级返回结果。
为了更直观,我们用一个简化的性能对比表格来概括:
| 查询类型 | 数据规模 | MySQL (带索引) 预估耗时 | ClickHouse 预估耗时 | 核心差异点 |
|---|---|---|---|---|
| 全表聚合 (SUM/COUNT) | 1亿行 | 数十秒至分钟级 | < 1秒 | I/O量:行存需读所有列 vs 列存仅读目标列 |
| 维度分组聚合 | 1亿行 | 十秒级,可能用临时表 | 1-3秒 | 数据局部性:随机读取 vs 有序列的连续读取 |
| 复杂时间窗口分析 | 1亿行 | 分钟级或超时 | 2-5秒 | 计算模型:逐行处理 vs 向量化块处理 |
注意:以上时间为基于典型硬件配置的估算,旨在说明数量级差异。实际性能取决于数据分布、索引设计、硬件配置(特别是SSD和内存)等多种因素。但趋势是明确的:在OLAP场景下,ClickHouse具有压倒性优势。
4. 进阶优化:让ClickHouse报表查询快上加快
基础迁移就能带来巨大提升,但ClickHouse的潜力远不止于此。通过一些进阶优化技巧,你可以让查询性能再上一个台阶。
4.1 利用物化视图实现预聚合
对于固定维度的、查询非常频繁的报表(如每小时销售大盘、Top10商品榜),每次查询都扫描原始表进行聚合仍然是一种浪费。ClickHouse的物化视图可以自动在数据写入时进行预聚合。
例如,创建一张按小时和省份预聚合销售额的物化视图:
CREATE MATERIALIZED VIEW orders_hourly_mv
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (hour, province)
POPULATE AS -- 对已有数据立即计算
SELECT
toStartOfHour(create_time) AS hour,
province,
SUM(amount) AS sales,
COUNT() AS order_count
FROM orders_analysis
GROUP BY hour, province;
创建后,每当有数据写入orders_analysis表,物化视图会自动计算并存储聚合结果。业务查询直接针对orders_hourly_mv进行,数据量可能从亿级降至万级,查询速度实现毫秒级响应。
4.2 选择合适的表引擎变种
根据业务场景选择更专用的MergeTree引擎,可以简化查询并提升性能。
- SummingMergeTree:如上例,专门用于预聚合求和场景,自动合并时对指定数值列进行SUM。
- AggregatingMergeTree:用于存储预计算的聚合函数状态(如
uniqState,quantileState),可以高效计算去重计数、分位数等复杂聚合。 - ReplacingMergeTree:用于处理“最后状态”类型的数据,通过指定版本列,在后台合并时保留最新版本的数据。
4.3 查询编写最佳实践
即使有了好的表设计,不当的查询语句也会拖慢速度。以下是一些关键原则:
- 避免使用
SELECT *:始终只选择需要的列。这是使用列存数据库的第一铁律。 - 善用分区和主键剪枝:确保
WHERE条件中包含分区键和主键的前缀列,让ClickHouse能最大限度地跳过不需要的数据块。 - 谨慎使用JOIN:ClickHouse的JOIN操作不同于MySQL,它通常是将右表全部加载到内存中进行哈希连接。大表JOIN大表性能很差。应优先考虑宽表设计,或者确保右表足够小。
- 使用合适的函数:使用ClickHouse内置的、针对列存优化的函数,如
sumIf,countIf等条件聚合函数,有时比WHERE ... SUM(...)更高效。
-- 较好的写法:利用分区和主键剪枝
SELECT province, SUM(amount)
FROM orders_analysis
WHERE create_time >= '2023-10-01' -- 利用分区剪枝
AND province IN ('浙江', '上海') -- 利用主键前缀剪枝
GROUP BY province;
-- 较差的写法:无法有效剪枝
SELECT province, SUM(amount)
FROM orders_analysis
WHERE city = '杭州' -- city是主键第二列,单独使用它无法有效利用索引
GROUP BY province;
5. 架构融入:在现有系统中引入ClickHouse的几种模式
将ClickHouse引入现有技术栈,并不意味着要替换掉MySQL。两者是互补关系,通常采用混合架构。以下是几种常见的集成模式:
5.1 模式一:读写分离,专库专用
这是最清晰的架构。MySQL继续作为业务源数据库(OLTP),处理订单创建、用户更新等事务。通过数据同步工具(如Debezium、Canal、或业务双写),将需要分析的数据实时或准实时地同步到ClickHouse。ClickHouse作为独立的分析数据库(OLAP),所有报表、BI工具、即席查询都指向它。
优点:职责分离,互不影响。OLTP库轻装上阵,OLAP库为查询优化。 挑战:需要维护一套可靠的数据同步链路。
5.2 模式二:Lambda架构下的加速层
在已有的批处理大数据平台(如Hive/Spark)基础上,引入ClickHouse作为加速查询层。每日的T+1批量分析结果仍由大数据平台产出,存入HDFS或数据湖。同时,将核心的、需要实时交互查询的维度聚合结果,导入ClickHouse。前端应用根据查询的实时性要求,决定访问Hive(历史深度分析)还是ClickHouse(近期实时看板)。
优点:兼顾历史数据批处理能力和近期数据的交互式查询速度。 挑战:需要维护两套数据管道,可能存在数据口径一致性问题。
5.3 关键考量与避坑指南
在引入ClickHouse前,务必明确它的边界:
- 它不是OLTP数据库:不要用它支撑高并发下单、扣库存等业务。它的事务支持很弱,高频单条写入性能差。
- 批量写入是朋友:尽量以大批次(如每秒或每批数万条)的方式写入数据,才能发挥其最大吞吐。可以使用Kafka作为缓冲层。
- 监控与运维:关注ZooKeeper的负载(如果用了复制表)、磁盘空间、慢查询日志。ClickHouse集群的运维复杂度高于单机MySQL。
- 数据一致性理解:由于是最终一致性模型(特别是在分布式和物化视图场景下),查询结果可能有短暂延迟,这对监控类场景通常可接受,但对绝对精确的财务对账可能不适用。
在我经历过的几个迁移项目中,最大的收益往往来自于思维模式的转变——从“用一个数据库解决所有问题”到“为不同的工作选择最合适的工具”。当那些曾经需要定时任务预计算、结果缓存才能勉强运行的报表,现在可以直接在ClickHouse上执行实时交互查询并秒级返回时,数据分析师和业务人员的体验提升是立竿见影的。当然,初期在表结构设计和数据同步上会花一些功夫,但相比于它带来的长期性能解放和业务敏捷性,这笔投资非常值得。如果你正在为分析系统的性能瓶颈寻找解决方案,不妨从一个小而具体的报表开始,尝试用ClickHouse重构它,亲身体验一下这种速度的飞跃。
1750



被折叠的 条评论
为什么被折叠?



