ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ClickHouse实战详解:从列式存储原理到部署与SQL优化

ClickHouse实战详解:从列式存储原理到部署与SQL优化 1. ClickHouse 到底是个什么货色第一次接触ClickHouse的人多半是听说了“列式存储”“查询巨快”这些标签然后被网上各种性能评测忽悠得心痒痒。但真正在数据量上吃过苦头的人会告诉你clickhouse不是万能的OLAP数据库它是那种“用对了场景爽到飞起用错场景想砸键盘”的典型代表。ClickHouse是俄罗斯搜索巨头Yandex开源的列式数据库管理系统定位是在线分析处理也就是OLAP。它的核心本事是处理海量数据的聚合、过滤、排序这类分析型查询单机性能吊打传统关系型数据库是家常便饭。我在生产环境跑过亿级行的group by查询从提交SQL到拿到结果通常在秒级以内这在MySQL里想都不敢想。那它能干点啥举几个实际场景用户行为日志分析比如埋点数据、访问日志、点击流监控指标存储与聚合比如服务器指标、业务大盘、实时告警大数据量下的报表查询、运营分析比如订单明细、流量统计需要按时间范围快速过滤和聚合的时序类数据至于它不擅长的事后面会专门讲很多人就是没搞明白这个边界才踩了大坑。但先记住一句话ClickHouse不是用来替代MySQL做业务交易系统的它是用来做分析查询的。你如果把订单表丢进去做单条更新那就是自找麻烦。这篇文章我会按“讲原理 → 教你装 → 挨个盘点数据类型 → 手把手写SQL”这条线走一遍全程用我实际跑过的案例和踩过的坑说话保证你看完能直接上手干活。2. 技术原理列式存储和向量化执行是两块基石2.1 为什么列式存储能快这么多传统关系型数据库MySQL、PostgreSQL用的是行式存储数据以行为单位落盘。查某几列时你得把整行数据都读进内存哪怕只要两个字段也得扫全行。数据量小没关系一旦到亿级行磁盘I/O直接成了瓶颈。ClickHouse把数据按列分别存储每个列独立成文件。查询时只需要加载涉及的列磁盘读取量能减少一个数量级。再加上列内数据同质化程度高压缩比非常可观。我压过一批业务日志磁盘占用从MySQL的几百GB降到ClickHouse里的几十GB就是这个道理。打个比方行式存储像在超市里按购物车结账不管买了几样东西每辆车的全部商品都要过一遍列式存储像按货架盘点想查牙膏库存只需要看牙膏区那一排就行。2.2 向量化执行让CPU也参与加速ClickHouse不只是省了磁盘I/O它还把CPU的潜力榨得很干。传统数据库逐行处理数据每处理一行都有一次循环、一次函数调用、一次判断ClickHouse用SIMD指令一次处理一批数据比如一次算几百个数字的和这种“向量化执行”让CPU在同样时间内干了更多的活。这也是为什么同样是聚合查询ClickHouse可以比一些MPP数据库占用更少CPU资源的原因之一。它把“批处理”思想做到了极致数据处理的最小单位不是行而是数据块Block。2.3 主键索引和稀疏索引的配合逻辑很多人第一次看ClickHouse建表语句都会愣住它竟然可以直接用字段做“主键”但这个主键居然允许重复。没错ClickHouse的主键本质上不是唯一约束而是稀疏索引。默认情况下每8192行记录生成一条索引标记查询时先通过稀疏索引快速定位到可能的数据块再在块内部扫描。这跟MySQL那种每行都建索引的稠密索引完全不同。好处是索引文件极小几十亿行的表索引只有几百MB坏处是如果查询条件没法命中索引前缀就会退化成全表扫描。所以建表时排序键ORDER BY选什么字段极其关键这直接决定了这张表的查询性能底子。后面讲SQL的时候我再展开说怎么选排序键。2.4 再说说ClickHouse的短板和边界我自己用下来有几个地方是ClickHouse明显不适合碰的高频单行更新/删除ClickHouse的更新走的是异步merge机制不是真正意义上的原地UPDATE小事务高频更新效率很差事务场景它只支持limited的事务保证没有完整ACID不适合做订单交易系统稀疏索引决定了它的点查能力很差非要按某个ID精确过滤大量随机值性能会很难看并发更新能力弱单机写入并发建议不要开太高一般控制在个位数到十几个并发还有个常见的认知误区是“用了ClickHouse就不需要优化了”大错特错。SQL写得烂、排序键选得烂照样能给你跑出几分钟的查询。工具好不代表可以瞎写。3. 手把手部署一套ClickHouse 21.8.15.7热词里出现了“linux 部署 clickhouse 21.8.15.7”这个具体版本号我就以它为例讲安装。这个版本是我线上跑过的稳定性和特性兼顾得不错既有新版的一些好用功能又不至于太新导致周边生态没跟上。提醒一点ClickHouse版本迭代极快不同版本之间SQL语法偶有差异建议生产环境选定一个版本后用半年以上再考虑升级。3.1 安装前需要准备和确认的事硬件层面ClickHouse对配置的包容度其实挺高的普通4核8G的机器也能跑得动千万级数据。但如果你要做正经生产内存建议至少16G起步磁盘优先SSD。因为列式存储的压缩特性它对磁盘容量的需求反而比传统数据库小很多。需要特别注意的一点是内存不能太小因为很多聚合操作会在内存中完成内存不够就会疯狂用磁盘临时文件速度掉好几个档次。操作系统我建议用CentOS 7.x或Ubuntu 20.04以上的版本。另外建议提前配好yum或apt源避免安装时因为网络问题卡住。当然你可以直接用官方仓库的地址也可以先下载RPM包再本地安装两种方式我都在下面写清楚。3.2 通过RPM包安装的具体步骤我这里以CentOS 7环境为例。先到ClickHouse官方仓库下载对应版本的RPM包或者直接配好官方yum源。用yum方式安装执行下面几条命令sudo yum install -y yum-utils sudo rpm --import https://packages.clickhouse.com/rpm/pubkey.gpg sudo yum-config-manager --add-repo https://packages.clickhouse.com/rpm/clickhouse.repo sudo yum install -y clickhouse-server clickhouse-client如果你想把版本锁死在21.8.15.7可以用指定版本号的方式安装sudo yum install -y clickhouse-server-21.8.15.7 clickhouse-client-21.8.15.7装完后确认一下版本clickhouse-server --version clickhouse-client --version我实际遇到过一个问题就是装完以后server启动时提示配置文件找不到或目录权限不对。这多半是你用非root用户执行了启动命令。官方默认会用clickhouse用户来跑服务如果手动改了安装路径记得把新路径所属用户和权限都改对。3.3 必须要改的几处核心配置安装完成先别急着启动把配置文件的坑提前避掉。主配置文件在/etc/clickhouse-server/config.xml另外还有一个users.xml负责用户权限配置。我建议重点关注以下几项第一监听地址。默认情况下ClickHouse只监听127.0.0.1只允许本机访问。你要让其他机器连接就需要修改监听配置。但直接暴露公网非常危险建议内网部署并配合防火墙策略。listen_host0.0.0.0/listen_host第二数据存储路径。默认数据目录在/var/lib/clickhouse/如果数据盘挂载在其他位置一定要改掉。改完记得确保目录属主是clickhouse用户否则启动会报错。path/data/clickhouse//path tmp_path/data/clickhouse/tmp//tmp_path第三内存限制。在users.xml里可以给用户设置max_memory_usage防止某个大查询把机器内存耗尽。我一般生产环境限制单查询16G或按物理内存的50%来设置max_memory_usage16000000000/max_memory_usage第四同时运行的查询数限制。默认并发是100左右小机器建议调低不然内存容易扛不住。3.4 启动、自检和常用运维命令配置改好以后启动服务sudo systemctl start clickhouse-server sudo systemctl enable clickhouse-server检查服务状态sudo systemctl status clickhouse-server然后用客户端连接试试clickhouse-client --host 127.0.0.1 --port 9000 --user default --password第一次登录默认用户是default默认没有密码直接回车就能进去。进去后先跑一个最简单的查询验证SELECT 1;能返回1就说明服务正常了。我再给你几个日常运维用得上的命令# 查看数据库列表 SHOW DATABASES; # 查看当前版本 SELECT version(); # 查看进程运行状况 ps aux | grep clickhouse3.5 使用Docker部署的参考方案如果你不想折腾系统级安装Docker方式也很省事特别适合本地开发环境和快速验证docker run -d --name clickhouse-server \ -p 8123:8123 \ -p 9000:9000 \ -v /data/clickhouse:/var/lib/clickhouse \ -v /etc/clickhouse-server:/etc/clickhouse-server \ clickhouse/clickhouse-server:21.8.15.7端口说明一下8123是HTTP接口适合用来做REST调用、对接BI工具9000是原生TCP接口clickhouse-client和多数编程语言的驱动走这个端口。9009是复制集群用的单机部署用不上。Docker方式的坑主要在数据卷权限上容器里的clickhouse用户UID是101宿主机上的数据目录权限要对应放好否则容器一启动就报Permission denied。还有个需要留意的点容器内存限制一定要设置否则默认容器吃满宿主机内存不是没可能。3.6 安装阶段常见的坑速查我结合自己这些年装过的机器把最容易出问题的地方整理成了一张表现象原因解决办法服务启动失败日志提示Permission denied数据目录属主错误chown -R clickhouse:clickhouse 数据目录远程连不上监听地址还是127.0.0.1改listen_host并重启客户端连接超时防火墙挡了9000端口开放内网端口或关防火墙测试查询报内存超限默认内存限制太小在users.xml中调大max_memory_usage启动时提示端口被占9000或8123被其他程序占用检查占用并改端口或停掉冲突进程安装这件事没啥技术含量但别掉以轻心排障时先看日志最重要。日志默认在/var/log/clickhouse-server/目录下绝大多数问题看一眼日志就明白了。4. 数据类型盘点常用类型与使用禁忌4.1 最常用的三类基础数据类型ClickHouse的数据类型跟传统数据库长得差不多但细节上有不少差异。先说最常用的数值类型整数有Int8到Int256好几种分别对应1字节到32字节。UInt8、UInt64表示无符号整数。实际开发中能用小类型别用大类型这句话是省存储的硬道理。比如状态码0到100用UInt8就够了别拿Int32硬撑。浮点数用Float32和Float64注意浮点有精度问题涉及金额别用浮点数存。字符串类型分成String和FixedString。String不限长度类似MySQL的VARCHARFixedString(N)是定长存取更快但长度超出会报错不够则补零。我通常用String只有明确知道长度固定的场景比如MD5哈希值、订单号才用FixedString。日期时间类型Date存日期精确到天DateTime精确到秒DateTime64可以精确到毫秒、微秒甚至纳秒。做日志分析时我强烈建议直接用DateTime64(3)存毫秒别用字符串存时间写入和查询性能都有差距。4.2 数组、枚举和布尔——容易用错的地方Array(T)表示数组类型比如Array(Int32)、Array(String)。ClickHouse支持数组类型这给一些标签类、聚合类的数据结构带来了很大的便利。Enum8和Enum16是枚举类型存储时用整数查询时显示成字符串。适合存状态这类固定取值的字段比如订单状态、设备类型。定义方式比较特别CREATE TABLE events ( status Enum8(pending 1, success 2, failed 3) ) ENGINE Memory;这里有个坑枚举定义好了以后新增枚举值需要ALTER TABLE改表结构没法直接往里插一个没定义的值。所以枚举设计初期就要把取值想全或者留好扩展位。ClickHouse没有独立的布尔类型官方推荐用UInt8代替0表示false1表示true。有些驱动会自动帮你做转换但手写SQL时用0/1最稳。4.3 高精度数值Decimal的正确打开方式金额、费率、百分比这类业务字段用浮点数是灾难用Decimal才是正解。Decimal(P, S)里的P是精度总位数S是小数位。比如Decimal(18, 4)表示最多18位数字其中4位是小数。我举一个实际例子一个订单金额字段用Decimal(18, 2)存这样不会出现0.1 0.2 0.30000000000000004这种浮点笑话。计算精度时注意乘除法会扩大精度范围比如两个Decimal(18, 2)相乘结果精度可能会变成Decimal(36, 4)级别在做后续比较时要留意。4.4 数据类型的自动转换与显式转换操作中经常遇到类型不一致的问题ClickHouse有自己的类型推断规则但别指望它处处合理。举几个常见场景String和数值比较时有时会自动转数值有时直接报错整数和浮点数运算会倾向浮点数日期字符串转Date类型需要显式用toDate()或toDateTime()函数显式转换用CAST或者toXxx系列函数SELECT CAST(2025-01-01 AS Date); SELECT toUInt32(42); SELECT toString(3.14);我在实际工作中几乎只用toXxx系列原因有二一是写起来短二是支持的格式判断比CAST更宽容。比如toDateTime(2025-01-01 10:30:00)能直接解析CAST的写法需要先转字符串再转时间麻烦得不行。4.5 一张图表格帮你选字段类型我在内部培训时常给同事发这么一张选型表要存的内容推荐类型备注ID、计数值、序号UInt64 / Int64自增数值一般用UInt64状态、枚举、标签Enum8 / Enum16固定取值才用注意扩展性金额、比率Decimal(18, 4)别用Float存钱短文本、名称String普遍通用哈希值、定长编码FixedString(32)等长场景用日期Date精确到天时间戳带毫秒DateTime64(3)日志必用标签集合、多值Array(String)注意去重用arrayDistinct布尔值UInt80/1表示还有一个很实用的小技巧如果字段拿不准就先用String。ClickHouse对String的查询性能并不差后面数据量大了确认了实际取值范围再改类型也不迟它的ALTER操作成本比其他数据库低不少。5. SQL实操从建表到复杂查询一整套5.1 建表引擎的选择——MergeTree家族唱主角刚上手ClickHouse的人面对一堆表引擎容易懵MergeTree、ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree、Memory、Log、Kafka等等。我建议新手记住一个判断逻辑默认首选MergeTree需要去重和更新场景用ReplacingMergeTree需要预聚合用SummingMergeTree或AggregatingMergeTree。其他引擎基本属于特殊场景别一开始就扎进去。MergeTree的核心特征是数据按主键排序存储支持分区支持TTL自动清理。写入时数据先落到内存再异步merge到磁盘段所以插入速度特别快。建表语句示例CREATE TABLE user_events ( event_date Date, user_id UInt64, event_type String, event_count UInt64, amount Decimal(18, 2), event_time DateTime64(3) ) ENGINE MergeTree() PARTITION BY toYYYYMM(event_date) ORDER BY (event_date, user_id);注意看两个关键点PARTITION BY决定分区粒度。例子中按月分区查询时如果带了月份条件可以直接跳过无关分区。分区太细比如按天小文件太多管理麻烦太粗则数据量太集中分区优势不明显。经验值是单分区数据控制在百万到千万行左右比较合适。ORDER BY就是排序键也是隐式索引。例子里选了event_date和user_id作为排序键意味着查询条件优先匹配这两个字段时会走索引加速。常量过滤条件放前面高基数字段放后面这是一条核心经验。5.2 数据写入的三种方式和注意事项ClickHouse写入方式跟MySQL很不一样它不推荐频繁小批量插入。最佳实践是攒一批一次写入比如每批几万行到几十万行性能能差出几个数量级。第一种方式是clickhouse-client直接插入INSERT INTO user_events VALUES (2025-01-01, 1001, click, 1, 99.90, 2025-01-01 10:00:00.123);第二种方式是从文件导入适合初始化数据clickhouse-client --query INSERT INTO user_events FORMAT CSV data.csv或者用FORMAT TabSeparated导入TSV文件。第三种是使用Kafka引擎表或者Flink来同步数据。热词里提到了“使用flink实现mysql同步到clickhouse”这确实是大数据链路里最常见的做法。思路是这样的用Flink CDC抓取MySQL的binlog变更把数据实时写入ClickHouse。实践中ClickHouse侧的写入策略通常有两种直接写入MergeTree表适合数据量中等、不需要去重的场景先写Kafka再由ClickHouse的Kafka引擎表消费写入适合解耦和削峰用Flink写ClickHouse时最需要关注的坑是ClickHouse不适合高频小批量写入所以在Flink的sink端必须开启批量buffer攒够几千条再批量flush一次。我见过不少数据同步延迟问题十有八九是没做批量写入导致的。5.3 查询语法快狠准过滤、聚合、窗口都安排上基本查询语法和标准SQL差别不大但有几个ClickHouse特色的点值得单独说。过滤条件要尽量打中排序键。假设上面那张表按(event_date, user_id)排序那么查询时带event_date条件就很快不带就只能全分区扫描SELECT event_type, sum(event_count) FROM user_events WHERE event_date 2025-01-01 AND event_date 2025-02-01 GROUP BY event_type;GROUP BY聚合建议配合WITH ROLLUP或WITH CUBE用可以一次查出多级汇总省得写多条SQLSELECT event_date, event_type, sum(event_count) FROM user_events GROUP BY event_date, event_type WITH ROLLUP;这样能同时拿到event_date event_type级别、event_date级别、总计三级聚合结果运营看数据特别省事。窗口函数在较新的版本中已经支持得很好。比如算每一天相对前一天的增量SELECT event_date, sum(event_count) AS cnt, cnt - lagInFrame(cnt) OVER (ORDER BY event_date) AS diff FROM user_events GROUP BY event_date;关于去重有两个函数要分清uniq()是近似去重量大时速度快但结果有误差uniqExact()是精确去重速度慢但数值精确。业务场景要求精确的比如算用户数精确值用uniqExact大规模估算场景比如UV大盘用uniq就够了。5.4 常用高阶特性TTL、物化视图和投影TTL自动过期删除数据是时序日志场景的救命功能。比如只保留90天的订单日志ALTER TABLE user_events MODIFY TTL event_date INTERVAL 90 DAY;或者建表时就定义好CREATE TABLE user_events ( event_date Date, ... ) ENGINE MergeTree() TTL event_date INTERVAL 90 DAY;TTL的删除是后台异步的别指望加了TTL磁盘立刻变小通常要等一段触发周期。如果要立即触发可以跑OPTIMIZE TABLE user_events FINAL注意大表谨慎使用会消耗比较多资源。物化视图Materialized View是ClickHouse做实时预聚合的利器。写入原表的数据会自动同步跑一段SQL把结果写入视图目标表。举个例子按天预计算每个用户的总事件数CREATE MATERIALIZED VIEW user_event_daily_mv ENGINE SummingMergeTree() PARTITION BY toYYYYMM(stat_date) ORDER BY (stat_date, user_id) AS SELECT toDate(event_time) AS stat_date, user_id, sum(event_count) AS total_count FROM user_events GROUP BY stat_date, user_id;查询时直接查物化视图表就行秒级出结果。注意物化视图本质是“写入触发计算”历史数据的增量不会自动回填要先手动把历史数据跑一遍再建视图或者建之前先灌好数据。再提一句新版本支持**投影Projection**功能可以给同一张表定义不同的排序维度让查询优化器自动选择最优的投影。比如明细表默认按event_date排序但你想让按user_id查询也快可以加一个投影。这个功能很香但有一定学习成本新手可以先绕过后面进阶再研究。5.5 SQL性能优化的几条硬经验这部分是我压箱底的干货都是生产环境中真实踩过的坑。经验一绝对避免SELECT *。列式存储的优势就在按列读数据你SELECT *就是自废武功把每一列全读了出来。查询时只写必要的字段哪怕能省一列性能都有感知提升。经验二小心玩坏HAVING。ClickHouse对WHERE条件能做到分区裁剪和索引过滤但HAVING是在GROUP BY之后才过滤的效率远低于WHERE。能写在WHERE里的条件一定别拖到HAVING。经验三字符串模糊查询要谨慎。LIKE %xxx%在大量数据下会触发全表扫描能不用就不用。实在要搜索建议使用倒排索引功能或把文本拆解成数组进行匹配。经验四注意数据压缩率。长文本、重复度高的字段压缩率高随机数、高基数ID压缩率低。设计表结构时可以考虑把低基数状态字段用枚举存、时间用整型存都是为了压缩率服务。经验五限制单次返回行数。查询结果动不动几百万行对分析端压力很大用LIMIT包一层可以很大程度降低网络传输量。很多BI工具连ClickHouse都支持查询下推和分页配合LIMIT效果很好。5.6 再提一嘴Doris和ClickHouse怎么选热词里有“doris和clickhouse的选型”这俩确实是经常被放在一起比较的OLAP数据库。以我的实际感受数据规模在千万到十亿级、主要是单表分析、追求极致写入和查询性能的ClickHouse很稳需要支撑复杂多表Join、高频点查、且需要更强的标准SQL兼容性Doris可能更顺手从生态成熟度来说ClickHouse的周边工具和社区资料更丰富从运维友好度来说Doris在部分场景上手可能更快选型不是选谁更强而是选谁更匹配你的场景。我的习惯是先看查询类型若是“大宽表 聚合分析”就选ClickHouse若是“多表建模 明细探索”就认真评估Doris。6. 实际操作中一定会遇到的疑难杂症6.1 超时、内存炸掉、数据不一致的典型场景写生产环境的实际经历。有一次跑一个跨全表的大规模聚合查询直接报内存超限退出。排查后确认原因查询里用了大范围GROUP BY聚合中间态太大超过了用户配置的内存上限。解决方法是三层齐下先优化SQL减少中间态然后在users.xml调高max_memory_usage最后给这个查询加了SET max_bytes_before_external_group_by参数让它超过阈值后落盘而不是直接报错。还有一次遇到数据重复问题。原因是业务端在数据同步时偶发重复写入而MergeTree本身不保证去重。解决思路是换成ReplacingMergeTree引擎并设置合理的version列让后台merge时自动保留最新版本。6.2 操作踩过的坑速查表问题原因排查思路解决方法数据写不进去报too many parts小批量插入太频繁parts碎片过多看system.parts表攒批写入insert线程数调低单条SQL把机器拖死没做防呆控制超大查询全表扫看system.query_log设置max_memory_usage、max_execution_time时区对不上数据差8小时DateTime默认按服务器时区解析查timezone配置全局统一用UTC存展示层再转各种少数据、多数据写入了副本数不足就丢了查system.replicas设置写入quorum或检查副本配置稀疏索引没生效查询变慢排序键设计不合理EXPLAIN看索引重设ORDER BY或加投影6.3 别忘了定期做这几件维护工作ClickHouse虽然宣称“免维护”但生产环境该做的运维动作一件都不能少定期OPTIMIZE TABLE ... FINAL合并一下过碎的数据part能大幅提升查询稳定性和效率观察system.parts表如果part数量长期几百上千说明写入节奏需要调整用system.query_log看慢查询统计哪些SQL经常超时反向推动SQL或表结构调整磁盘监控不能丢虽然压缩率不错但日志类数据涨量很快提前设好水位线告警定期备份元数据和数据文件虽然分布式副本能扛故障但防呆备份总得有一份这些操作不复杂但缺了哪一样都有可能在某个深夜给你惊喜。7. 关于ClickHouse使用最后聊一点我的个人体会踩了这么多年ClickHouse的坑最大的感受是它确实是把分析查询的体验拉到了一个前所未有的高度但前提是你得先理解它的脾气。所谓脾气无非三大件列式存储让你读得更聪明稀疏索引逼你把排序键想清楚merge机制要求你注意写入节奏。这三件事只要都顺了它回报给你的性能会让你兴奋到发朋友圈任何一件不顾线上告警也会让你怀疑人生。如果看到这里你准备动手了我给一个最小化启动路径装一台虚拟机或Docker环境导入几百万行日志数据试着写按天聚合的SQL再随便建一个物化视图看看效果。整个过程一个小时就能跑通但对理解ClickHouse的威力非常直观。等你把基本的读写跑顺了再去啃分布式集群、副本同步、ZooKeeper协调这些进阶内容心里会踏实很多。最后送大家一句话工具可以绕开短板但你得先知道短板在哪里。先想清楚你的场景是不是OLAP再决定要不要上ClickHouse这比什么都重要。
返回列表