ARTICLE DETAIL

资讯详情

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

ClickHouse入门:从列式存储到实时数仓实践

ClickHouse入门:从列式存储到实时数仓实践 1. 项目概述1.1 为什么大家都在聊ClickHouseClickHouse是这两年OLAP领域绕不开的名字。如果你所在的公司有大数据分析需求——比如用户行为分析、订单统计、日志查询、监控指标聚合——那你大概率已经在某个技术会上听到过它。很多团队甚至已经在用Flink把MySQL数据实时同步到ClickHouse就是为了解决业务库查询太慢、报表出不来这个老大难问题。我最早接触ClickHouse是因为一个实际业务痛点一张几千万行的订单明细表在MySQL里跑一个多维度聚合查询要几十秒业务方说你能不能优化一下但那种SQL在MySQL里加索引也救不回来——因为查询条件太灵活组合维度太多。后来换成ClickHouse同样的数据量、同样的SQL逻辑响应时间从几十秒降到几百毫秒。这个差距给我的第一感受是这玩意儿确实不是营销吹出来的。这篇文章就围绕ClickHouse的完整入门路径展开它是什么、为什么快、怎么装、有哪些数据类型、SQL怎么写。目标读者是刚接触ClickHouse、准备在项目里引入OLAP方案的工程师。读完你应该能独立完成一套ClickHouse的Linux部署并且对它的数据模型和查询语法有一个立体的认识不至于一上来就被各种术语劝退。1.2 这个项目解决的核心问题ClickHouse解决的问题可以用一句话概括在海量数据亿级、十亿级上做快速的在线分析查询。注意这里的关键词是在线——它强调的是查询要快要能扛住并发而不是像Hive那样跑个批处理任务等几分钟出结果。它与传统OLTP数据库的本质区别在于OLTP如MySQL、PostgreSQL面向的是一行一行地增删改查OLAP如ClickHouse、Doris、Greenplum面向的是对成批成批的数据做统计计算。如果你正在纠结ClickHouse和Doris怎么选我的建议很直接如果你们的分析场景以单表聚合、宽表查询、日志分析为主ClickHouse的生态和稳定性更有优势如果需求偏多表关联建模、且团队更熟悉MySQL语法Doris的上手成本可能更低。2. 核心设计思路为什么ClickHouse这么快2.1 列式存储才是灵魂ClickHouse最底层的设计决策是列式存储。传统关系型数据库以行为单位存储数据一条记录的所有字段在磁盘上连续存放ClickHouse以一列为单位存储同一列的所有值在磁盘上连续存放。这个差异在上亿行数据上会被无限放大。举一个生活化的例子你去超市买东西收银小票是一行一行的行式存储你有几百张小票堆在一起想知道这几个月总共花了多少钱你只能一张一张地翻小票每张都要从上到下扫一遍列式存储相当于你平时就把所有小票的金额这一栏单独抄在一个本子上算总账的时候直接把本子翻一遍就行。在没有索引的情况下行式存储扫描几亿行取一列要把所有列的数据都读一遍列式存储只需要读取目标列的数据块。ClickHouse在列式存储之上还叠加了稀疏索引——它对排序键的每N行记录一个索引项而不是每一行都建索引。这种索引形态在OLAP场景下恰到好处既能快速跳过大量无效数据块又不会像B树索引那样占用大量内存和磁盘空间。2.2 向量化执行与数据压缩光有列式存储还不够。ClickHouse的第二个杀手锏是向量化执行。普通的数据库执行查询时是一行一行地处理——读一行算一行返回一行ClickHouse是一批一批地处理——每次从列中读取一批数据比如1024行然后在这批数据上做批量计算。批量计算的好处是CPU可以连续处理内存中地址相邻的数据充分利用CPU缓存和SIMD指令集单条指令处理多个数据大幅压低了单条数据的处理成本。在压缩能力上列式存储也天然比行式存储更有优势。同一列的数据类型相同、值的分布往往有规律压缩率可以做到很高。我实际测过一份10GB的原始数据导入ClickHouse后磁盘占用通常在1GB到2GB之间压缩比在5到10倍之间这在降低存储成本的同时也减少了查询时的磁盘IO量。这两个设计一叠加效果就是数据量越大ClickHouse相对传统方案的优势越明显。小数据量几十万行时MySQL未必输但数据量到了亿级ClickHouse的性能优势就不是一点半点了。3. 安装部署实操Linux环境快速起一个可用实例3.1 环境准备与版本选择ClickHouse官方支持Debian/Ubuntudeb包和CentOS/RHELrpm包两种主流Linux发行版也提供tar.gz免安装包和Docker镜像。我这次以Linux部署ClickHouse 21.8.15.7为例这个版本是社区里口碑比较稳的版本功能完整性、稳定性都经过了大量生产环境验证。建议部署前先确认操作系统CentOS 7.6 或 Ubuntu 18.0464位CPU2核起步生产环境建议4核以上内存4GB起步生产环境建议16GB以上内存越大大查询越稳磁盘SSD最好机械盘也能跑但导入性能和查询性能会打折如果你只是想本地试用一下Docker是最快的路径docker run -d --name clickhouse-server \ -p 8123:8123 -p 9000:9000 \ -v /data/clickhouse:/var/lib/clickhouse \ yandex/clickhouse-server:21.8.15.7注意我把数据目录挂载到了宿主机的/data/clickhouse这样容器删了数据还在。3.2 用RPM包在CentOS上安装生产环境我更推荐直接用rpm包安装管理更透明也不依赖Docker内部网络。操作步骤如下先添加官方源sudo yum install -y yum-utils sudo rpm --import https://packages.clickhouse.com/rpm/clickhouse.asc wget https://packages.clickhouse.com/rpm/stable/clickhouse-common-static-21.8.15.7.tgz这里有个小坑ClickHouse的rpm安装包不是一个文件而是拆成了好几个——clickhouse-common-static核心引擎、clickhouse-server服务端、clickhouse-client命令行客户端。你直接从yum源装会自动处理依赖sudo yum-config-manager --add-repo https://packages.clickhouse.com/rpm/clickhouse-rpm.repo sudo yum install -y clickhouse-server clickhouse-client装完之后服务端的配置文件分别在/etc/clickhouse-server/config.xml主配置监听端口、数据目录、内存限制都在这里/etc/clickhouse-server/users.xml用户配置密码、权限、查询配额在这里启动前先看一眼数据目录的磁盘空间默认数据落在/var/lib/clickhouse/日志在/var/log/clickhouse-server/。启动和验证sudo systemctl start clickhouse-server sudo systemctl enable clickhouse-server clickhouse-client --query SELECT version()如果看到类似21.8.15.7的输出说明服务已经正常工作了。3.3 配置要点与启动检查我每次装完ClickHouse都会第一时间检查三个配置项这三个直接决定后续用起来是否顺手。第一是内存限制。默认情况下ClickHouse的max_memory_usage设置为10GB如果你的机器只有4GB内存跑大查询很容易OOM。在users.xml里找到profiles节点的default配置把它调低profiles default max_memory_usage4000000000/max_memory_usage /default /profiles第二是时区设置。在config.xml里找到timezone标签如果你的服务器是UTC时区但业务数据是北京时间建议设置为timezoneAsia/Shanghai/timezone第三是外部访问。默认ClickHouse只监听本地地址如果你需要远程连接在config.xml里把listen_host改成0.0.0.0然后重启服务。注意这一步需要配合防火墙规则一起处理别把端口裸奔在公网上。检查服务是否正常还有一个实用技巧直接访问HTTP端口8123ClickHouse内置了一个简单Web界面浏览器打开http://服务器IP:8123/play能看到一个可以执行SQL的网页控制台排查问题比敲命令行快很多。4. 数据类型全景从基础到进阶4.1 数值类型整数、浮点数和定点数ClickHouse的数值类型设计比MySQL更碎这是为了精确控制存储大小和计算效率。整数类型分布的规律是位数越大取值范围越大占的存储空间也越大。生产环境里最常见的坑是默认用了Int64导致存储翻倍——如果你明确知道某个字段的取值范围在正负21亿以内用Int32就够了一亿行的表能省下近400MB空间。我整理了一张常用的数值类型速查表类型名称字节数取值范围典型场景Int81-128~127状态码、标记位Int162-32768~32767端口号Int324-21亿~21亿订单ID、用户IDInt648-922京~922京时间戳、金额分UInt8/UInt32/UInt641/4/8无负数范围扩大ID自增、计数器Float324约±3.4e38温度、比例Float648约±1.7e308经纬度、科学计算Decimal(P,S)可变由精度决定金额、汇率如果你是做交易类业务的金额字段一定不要用Float因为二进制浮点数无法精确表示十进制小数比如0.1 0.2在Float下会得到0.30000000000000004。正确的做法是用Decimal(18, 2)这种定点数类型P是总位数S是小数位数Decimal(18, 2)表示最多16位整数2位小数精确无误差。4.2 字符串类型与日期时间类型ClickHouse的字符串类型只有两种String和FixedString(N)。String是变长的类似于MySQL的VARCHAR但不限长度FixedString(N)是定长的读取时性能更高但如果存入的字符串长度小于N末尾会用零字节补齐。我实际使用中FixedString用得不多因为如果业务字符串长度波动较大定长类型反而浪费空间。一个经验能用String就用String别为了那一点点性能盲目用FixedString。日期时间类型有三个层级Date只存日期2024-01-15占2字节DateTime存日期和时间2024-01-15 10:30:00占4字节DateTime64支持毫秒/微秒精度2024-01-15 10:30:00.123占8字节这里有个容易踩的坑ClickHouse的DateTime类型不做时区转换它存的是写入时的时间。如果你在config.xml里设置了Asia/Shanghai那么now()函数返回的是北京时间但如果你在不同的时区读同一份数据显示的时间字符串会不一样。存储层面的建议是统一存UTC时间展示层再做转换这样避免不同机器时区不一致导致的数据混乱。4.3 复合类型与特殊类型ClickHouse还支持一些很有特色的复合类型这在传统数据库里很少见。Array(T)用于数组比如Array(Int32)、Array(String)。这在存储标签、特征向量时非常方便不需要单独建关联表。Tuple(T1, T2, ...)用于元组元素类型可以不同适合表达一个复合键。Map(K, V)是键值对类型适合存储属性集合但使用时要注意——ClickHouse对Map的查询性能远不如展开成多列或嵌套结构。还有一个重磅类型Nullable(T)。它允许某个字段为空值。但这里要强调ClickHouse非常不适合大量使用Nullable字段因为Nullable列无法参与索引且存储时会额外增加一位标记是否为NULL严重影响压缩率和查询性能。有没有值这个状态请尽量用默认值表达比如数值类型用0、字符串用空串而不是用NULL。Enum8和Enum16也值得提一下。它们把字符串映射为整数存储比如定义Enum8(success 0, fail 1)存储的是0和1查询时显示的是success和fail。这在状态类字段订单状态、任务状态上能显著减少存储空间。4.4 类型选择与转换经验关于字段类型的选择我在实际项目里形成的几个铁律能用整数绝不用字符串。比如状态码、分类ID用Int8或Int16能用定长整数绝不用变长字符串。用户ID、订单号同时出现在两张表里时类型必须完全一致否则等值关联会失效金额全部用Decimal禁止用Float时间戳建议存DateTime或UInt32不要存为字符串格式布尔值用UInt80/1ClickHouse没有Bool类型类型转换有两种方式隐式转换少用依赖规则和显式转换函数。常用的显式转换写法-- 字符串转整型 SELECT toInt64(123456); -- 字符串转日期 SELECT toDate(2024-01-15); SELECT toDateTime(2024-01-15 10:30:00); -- 字符串转Decimal SELECT toDecimal64(3.14, 2); -- 数值转字符串 SELECT toString(123456);一个专用技巧如果你需要把字符串时间戳转成DateTime用于分区筛选可以用parseDateTimeBestEffort()它能自动识别各种常见格式比如2021/08/15 10:00:00、2021-08-15T10:00:00Z等。5. SQL核心能力与经典写法5.1 DDL建表与MergeTree引擎ClickHouse的建表语法和MySQL神似但有一个关键差异必须指定ENGINE。刚开始从MySQL转过来的同事经常忘记写引擎直接报错。最常用的引擎家族是MergeTree几乎所有生产表都建立在它之上。标准建表语句格式如下CREATE TABLE ods_order_detail ( order_id UInt64, user_id UInt64, product_id UInt32, amount Decimal(18,2), pay_status Int8, create_time DateTime, modify_time DateTime ) ENGINE MergeTree() PARTITION BY toYYYYMM(create_time) ORDER BY (user_id, create_time) TTL toDateTime(create_time) INTERVAL 180 DAY;拆开看这几个关键配置PARTITION BY toYYYYMM(create_time)按月分区物理上每个月一个数据目录。分区是管理数据生命周期和加速查询的重要手段查询时带上月份条件ClickHouse可以跳过不相关的分区目录直接定位目标数据ORDER BY (user_id, create_time)排序键这是ClickHouse最重要的设计之一。数据落盘时按这个顺序排序同时自动生成稀疏索引TTL数据过期策略180天以外的数据自动删除。这是我最喜欢的功能——日志类表不需要自己写定时任务清理ClickHouse内部就搞定了这里要特别强调一点MySQL用户的惯性思维是把索引建在查询条件上但ClickHouse的ORDER BY不是MySQL的索引它既是物理排序也是索引的排序依据。所以排序键的选择依据是高频查询会按哪些字段做过滤和聚合而不是哪些字段是高基数的。5.2 INSERT与数据导入ClickHouse的INSERT语法和MySQL基本一致INSERT INTO ods_order_detail VALUES (1024, 2048, 1001, 59.90, 1, 2024-09-13 10:00:00, now());但在生产环境单条INSERT效率极低。导入数据的正确姿势是批量写入一次INSERT最好在几万到几十万行。从文件导入最常用的方式是clickhouse-client --query INSERT INTO ods_order_detail FORMAT CSV data.csv或者通过HTTP接口curl -X POST http://localhost:8123/?queryINSERTINTOods_order_detailFORMATCSV --data-binary data.csv如果你需要从MySQL全量同步一张表可以用官方工具clickhouse-copier或者干脆导出成CSV再导入。增量同步的话社区里最热门的方案就是使用Flink实现MySQL同步到ClickHouse——Flink CDCChange Data Capture监听MySQL的binlog变化再把变更数据实时写入ClickHouse延迟可以做到秒级。这套链路已经是很多公司实时数仓的标准配置了。5.3 查询语法聚合、过滤、窗口函数ClickHouse的查询语法90%和标准SQL相同SELECT ... FROM ... WHERE ... GROUP BY ... ORDER BY ... LIMIT这些基本结构都能直接跑。但它有几个特有的函数和写法值得重点掌握。聚合查询SELECT user_id, count() AS order_cnt, sum(amount) AS total_amount, avg(amount) AS avg_amount FROM ods_order_detail WHERE create_time 2024-01-01 AND create_time 2024-02-01 GROUP BY user_id ORDER BY total_amount DESC LIMIT 100;注意count()不带参数也能计数ClickHouse还提供countDistinct()用于精确去重统计SELECT uniqExact(user_id) AS uv FROM ods_event;uniqExact是精确去重代价是内存占用稍高如果数据量极大几十亿行以上可以换uniq()或uniqHLL12()它们是基于HyperLogLog的近似去重误差在1%以内但内存消耗会小几个数量级。数组与嵌套结构查询-- 判断数组是否包含某元素 SELECT has(tags, 热门) FROM article_table; -- 数组长度 SELECT length(tags) FROM article_table; -- 数组展开成多行 SELECT tag, count() FROM article_table ARRAY JOIN tags AS tag GROUP BY tag;ARRAY JOIN是ClickHouse非常实用的语法一根数组字段字段直接展开成多行参与聚合这在标签分析场景中几乎天天用。窗口函数ClickHouse从21.x版本开始正式支持窗口函数包括ROW_NUMBER()、RANK()、LAG()等。典型的使用场景是每个用户最近一笔订单SELECT user_id, order_id, create_time FROM ( SELECT user_id, order_id, create_time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) AS rn FROM ods_order_detail ) WHERE rn 1;5.4 UPDATE、DELETE与那场著名的异步删除很多从MySQL转过来的同学第一次用ClickHouse执行UPDATE会懵住——语法没问题但执行时间特别慢甚至感觉像卡住了。原因在于ClickHouse的UPDATE和DELETE都是异步的mutation操作。它们不会真的去改原有的数据文件而是标记哪些数据行需要变化后台的mutation线程再逐个part重写数据文件。这个过程的性能远不如MySQL的B树就地更新所以ClickHouse在设计上就不适合频繁地单行更新。-- 这是可行的但很慢且会重新写整个part文件 ALTER TABLE ods_order_detail UPDATE pay_status 2 WHERE order_id 1024; -- 删除同理 ALTER TABLE ods_order_detail DELETE WHERE order_id 1024;那生产环境怎么处理数据修正我的经验分三类如果是一次性的数据订正用ALTER ... UPDATE忍受一次慢操作没关系如果业务需要频繁更新状态字段建议在设计阶段就把最新状态单独存到一个小表或者用ReplacingMergeTree来处理如果只是过期数据清理直接靠TTL别写UPDATE。5.5 多表JOIN的现实与妥协ClickHouse不能像MySQL那样随便JOIN。虽然语法支持JOIN但在大数据量下多表关联会消耗大量内存而且右表必须能被放进内存默认max_bytes_in_join200MB超限就会报错。我的建议是两条路走一是在建模阶段把宽表铺平。把需要关联的维度字段冗余到事实表里查询时只查一张表。这是OLAP场景最常见的做法也是ClickHouse官方推荐的用法。维度变化频率低的时候宽表完全够用。二是用GLOBAL JOIN加IN子查询规避内存瓶颈。对于小维表可以直接放到JOIN的右侧对于太大的维表就用子查询圈定范围后再关联SELECT o.user_id, u.user_name FROM ods_order_detail AS o GLOBAL JOIN dim_user AS u ON o.user_id u.user_id WHERE o.create_time 2024-01-01 AND o.create_time 2024-02-01 AND u.user_id IN (SELECT user_id FROM active_user LIMIT 1000);注意我在IN子查询里加了LIMIT这一步是为了主动控制查询范围。如果业务上确实需要一个大维表全量关联建议把维表加载到内存后用Join表引擎ENGINE Join(ANY, LEFT, user_id)查询时直接作为字典查询用。6. 常见问题与排查技巧实录6.1 安装启动阶段的典型故障报错一Cannot find column或DB::Exception: Table ... doesnt exist这类问题多半是你用了clickhouse-client连接后没指定数据库。默认当前库是default如果数据建在别的库要么写全称库名.表名要么先执行USE 库名。报错二Memory limit (total) exceeded前面提过的内存限制问题。先看free -h确认机器剩余内存然后调高或调低users.xml里的max_memory_usage。排查内存还有个办法在查询前用SELECT * FROM system.processes看看当前正在跑的查询找到最吃内存的进程用KILL QUERY WHERE query_id ...来终止。报错三Cannot allocate memory这个通常是OS层面的overcommit_memory设置问题或者进程数/文件句柄数限制。CentOS默认对单进程的max user processes限制可能过低。临时调整ulimit -n 65535 ulimit -u 65535持久化修改在/etc/security/limits.conf里加clickhouse soft nofile 65535 clickhouse hard nofile 65535 clickhouse soft nproc 65535 clickhouse hard nproc 65535报错四启动失败日志里全是Segmentation fault或std::bad_alloc先怀疑内存不足再看是不是CPU指令集问题。有些旧CPU不支持ClickHouse依赖的指令集这种情况建议换新硬件或找一台满足要求的机器。6.2 使用过程中的性能瓶颈自查如果你发现ClickHouse查询变慢不要急着加索引——它压根没有传统意义的索引。我有一个固定的排查路径第一步用EXPLAIN看执行计划确认表是否走对了分区裁剪EXPLAIN SELECT ... FROM ods_order_detail WHERE create_time 2024-01-01;第二步看system.query_log里这条查询的扫过数据量。如果扫过的行数和表的全量行数一样说明你的WHERE条件没走索引检查过滤字段是否在排序键里或者分区裁剪的表达式是否对得上。比如排序键是user_id和create_time但WHERE里只写了product_id 1那就只能全表扫。第三步检查part数量。ClickHouse定期做合并OPTIMIZE TABLE ... FINAL如果长时间不合并part数量过多会导致查询时打开大量文件性能下降。生产环境建议设置merge_tree的parts_to_throw_insert参数或者定期在低峰期手动OPTIMIZE TABLE。第四步看看并发。ClickHouse并发能力很强但也不能无限并发。max_concurrent_queries默认100如果业务方把报表接口直接怼在ClickHouse上大量并发下查询延迟必然上升。给不同业务账号设置不同的max_concurrent_queries配额是必要的。6.3 关于慢SQL优化的几条实战建议技术上ClickHouse的慢SQL优化和MySQL完全是两个套路。我的经验可以浓缩成四条减少扫描数据量优先于减少返回行数。一个查询扫1000万行然后过滤出100行与一个查询只扫10万行就返回100行前者的成本可能是后者的几十倍。所以优化第一刀永远是让WHERE条件能够命中分区裁剪和排序键。GROUP BY的代价远比ORDER BY小。ClickHouse对聚合的优化非常激进聚合状态在内存中合并、部分预聚合但对大结果集排序ORDER BY ... LIMIT的限制更严格。能提前用WHERE缩小数据量就别靠最后的排序硬扛。多用PREWHERE代替WHERE。在列式存储中PREWHERE会先只读取过滤条件涉及的列再读取其他列大幅减少IO。特别是当主表有几十个字段而你的过滤条件只依赖其中一两个字段时PREWHERE的优化效果非常明显。SELECT user_id, amount FROM ods_order_detail PREWHERE pay_status 1;大查询拆小查询。如果一个聚合查询要扫描全表且涉及大量维度考虑是否能用物化视图Materialized View做预聚合。ClickHouse的物化视图在数据插入时同步更新聚合结果查询时直接读结果表速度和压力都小很多。这四条是我在实际优化中反复用到的保命组合技。踩过几次坑之后我现在的习惯是在建表阶段就把查询场景想清楚——排序键怎么设计、分区怎么定、是否要物化视图——而不是等慢查询出现了再亡羊补牢。6.4 数据一致性相关的避坑指南ClickHouse的ReplacingMergeTree和SummingMergeTree这两个引擎容易被误当成自动更新/自动求和工具。实际上它们的合并是后台异步的而且合并结果依赖数据到达顺序——同一排序键下的新旧数据可能因为合并时机不同导致结果不一致。如果你的业务依赖ReplacingMergeTree去重建议查询时加上FINAL关键字SELECT user_id, name FROM user_replacing_table FINAL WHERE user_id 1024;但注意FINAL会把查询性能拉低不少特别是大表上。一个折中方案是利用version或sign列在应用层做取最新的判断而不是完全依赖引擎自动去重。关于周边生态还有一个很多人问的问题ClickHouse数据备份怎么做。最土但最有效的方案是定期把表导出成Parquet或CSV到冷备存储如果集群做了副本ReplicatedMergeTree数据本身已经有多副本冗余这已经能满足绝大多数场景。7. 写在最后从第一次接触到正式上线ClickHouse给我的整体印象是学习曲线不算陡但坑确实不少。安装和基本操作可以一个下午搞定数据模型设计、排序键选择、分区规划这些才真正决定你的系统能跑多快、多稳。我个人在实际操作中的体会是刚上手阶段不要追求把所有特性一次学完先把它当成一个速度非常快的宽表查询引擎来用把表设计好、分区和排序键选对SQL按常规写法来就能解决80%的报表需求。等业务复杂度上去了再逐步研究物化视图、聚合状态、副本集群这些进阶能力。最后分享一个小技巧在你第一次导入大数据量之前先用clickhouse-benchmark跑几个压测查询确定你机器的内存和CPU在什么查询规模下会到瓶颈。这比上线后突然被慢查询打爆要体面得多。
返回列表