本篇来自数月前对外分享的文稿整理,并进行了一些扩展。
希望通过简单的方式,来介绍新手如何一步一步上手 ClickHouse,如果你有潜在的数据分析的需求,但是不知道从哪里开始,那么希望本文能够帮助到你。
写在前面
关于 ClickHouse 在追求性能的场景下的溢美之词,我觉得没有必要再重复了。以过往经验来看,你可以使用极其低的成本来完成以往 RDBMS(比如MySQL)做不到的准实时级别的数据分析,也可以用它来做远程多个数据库实例的数据迁移或者归档存储。
感谢两年前一位好朋友对我进行的技术选型推荐,使用 ClickHouse 可以简化非常多的不必要的基础设施搭建和维护。我们曾搭建过一台比较奢华的机器(256核心512GB内存)来进行准实时的数据分析(花费万分之几秒从海量数据中查结果),以及支持每秒落地几十万条以上数据,而丝毫不影响服务器查询性能;也曾实践过从两千块的 NUC 上跑边缘计算任务,相对快速的拿到需要分析的结果(花费千分之一到百分之一秒),以及在16核心64GB内存的普通笔记本上,跑超过十亿数据集的复杂计算的尝试(分钟级)。
所以,如果你有以下需求,ClickHouse 可能也会非常适合你:
- 快速分析一些离线数据,做数据计算、聚合、筛选。
- 有大量读取需求,并且针对原始数据修改的需求非常少,如果存在这类需求,可以接受“追加数据”配合“版本过滤”的方式处理。
- 数据字段比较丰富,数据存在非常多“列”。
- 业务并发需求不高,查询者(消费者)只有几个或者一两百个以下。
先来聊聊硬件选择。
硬件选择策略
会考虑选择 ClickHouse 的同学,一般应该是遇到了当前业务,到了需要或者不得不“考虑效率”的时刻。
一般情况,很少有需要直接把 ClickHouse 返回数据作为同步结果直接返回给调用方的场景,勤俭节约的程序员们一般都会使用异步模式,所以在极少并发的情况下,我们对于 ClickHouse 的硬件要求也就越来越低了: 亿级别以下的数据,最低只要 4核心16GB 的虚拟机也能轻松搞定;而亿级别到百亿级别的数据,只要你能搞定32~64G内存,计算出来的时间也只几乎只和你设备的核心数数量、CPU缓存大小是多少有关而已 。
所以,在考虑使用 ClickHouse 的时候,如果你是用来做一个快速或者相对快速的“离线”数据分析,那么优先需要考虑的是你的数据量有多大,以及需要满足快速计算的内存门槛下限是否足够,接着才是考虑你需要多快的拿到计算结果,尽量在成本预算之内,优先选择拥有更多的核心数的 CPU、以及更大的 CPU 缓存 。至于 Cluster 模式,除非你需要提供实时接口,对于服务可用性有极高依赖和要求,有特别大的数据写入压力,不然默认情况是不需要配置的。当然,如果你有需求配置 Cluster,不推荐使用默认的分布式模式,因为数据并非完整镜像,而是均匀分布在每一个节点,如果某一个节点跪掉,你将“实时”损失 N 分之 1 的数据,导致最终计算结果不能说不准确,只能说压根可能是错的。官网为此推出了一个“Replicated”的数据库引擎,这个数据库引擎基于 Atomic 引擎,借助 ZooKeeper 进行完整的数据复制,虽然目前还处于实验阶段,但是总比“丢数据”强吧。
除此之外,还有一个因素会极大的影响 ClickHouse 帮助我们拿到计算结果的时间,就是存储介质,这里推荐使用 SSD 作为存储介质,如果你是用于小样本分析,甚至可以使用 TB 规格、便宜的民用存储。如果追求极致成本,甚至可以参考我之前的内容《廉价的家用工作站方案:前篇》、《NUC 折腾笔记 - 储存能力测试》,如果你是进行高频次、海量数据的计算,有比较大的存储量下限要求和可预期的大容量数据增长,考虑到成本和更高的数据存储可靠性,Raid 50 模式的机械磁盘会更适合你。
当然,如果你目前啥都没有,只是用于学习,本地起一个 Docker 容器,也能开始学习之旅,以及百万到千万级别的数据计算和分析。
软件环境选择
我目前所有的机器都运行在 Ubuntu + 容器环境,为什么这么选择呢,因为“Ubuntu 是容器世界里的一等公民”,本文考虑到快速上手,也同样选择使用套环境。
当然,如果你选择裸机直接安装 ClickHouse,使用更稳定的 Debian 也是个不错的选择,至于 CentOS ,时至今日,真的是没有推荐的理由和必要了(企业付费购买 RHEL 是另外一个话题)。
在容器环境内跑 ClickHouse 会损失比较多的“转换”性能,在存储和网络转发上都会存在一定的体现,所以实际生产环境能够裸机安装的,请脱离容器使用。
如果你已经安装好了 Docker环境,那么我们可以继续下一个章节啦。如果你还不熟悉如何安装 Docker,可以参考本站知识地图中的关于容器安装的内容,自行了解学习。
前置准备:测试使用的数据集
为了熟悉和了解基础语法和进行 ClickHouse 高性能体验,我们可以先使用官方提供的 Yandex.Metrica Data 来进行试验。(更多的性能测试,可以从官方仓库的 测试数据集 中了解)
https://datasets.clickhouse.tech/hits/partitions/hits_v1.tar
https://datasets.clickhouse.tech/visits/partitions/visits_v1.tar
此外,为了演示如何在不纠结数据类型转换的情况下,快速完成数据导入,我们还需要使用一个传统类型的数据库的数据集进行操作,这里选择网友开源项目中使用的“人人影视”数据库(MySQL) https://yyets.dmesg.app/database
。
数据下载完毕之后,我们需要先对数据进行解压缩。
1 | bash复制代码mkdir data |
通过 du
命令可以看到使用的数据实际使用了 1.7GB空间,顺便提一下,这些数据如果存储在 MySQL 中,存储空间可能会膨胀 3~5倍以上。
1 | bash复制代码du -hs data |
数据解压完毕,就可以开始准备对 ClickHouse 的容器运行配置了。
前置准备:准备 ClickHouse 运行配置
1 | yaml复制代码version: "2" |
将上面的配置保存为 docker-compose.yml
,并使用 docker-compose up -d
启动 ClickHouse,以备稍后使用。
额外说一下,ClickHouse 的版本更新很快,建议升级的时候先做一些小样本测试,测试常用场景是否正常,再进行版本更新替换。
ClickHouse 初体验
ClickHouse 使用的 SQL 语法相比较 MySQL 等数据库会宽松许多,类比的话,就像是之前写 Java 的选手一下子步入了 Python 和 JavaScript 的世界。
因为使用容器启动 ClickHouse,所以我们可以通过 docker exec
命令进入 ClickHouse 的交互式终端。
1 | bash复制代码docker exec -it clickhouse clickhouse-client |
进入终端后,先来看看有哪些“数据库”和数据表:
1 | bash复制代码# 查看数据库 |
上面的结果中的 datasets
就是我们导入的数据集。ClickHouse 对于数据存放比较“佛系”,如果你查看本地目录可以看到上面的数据和 data/datasets
目录保持一致,实际操作使用的时候,只要把 data 目录打个压缩包就能完成数据备份了,是不是很简单。
1 | bash复制代码 tree -L 3 data/data/datasets |
为了后续敲的命令能简单些,我们针对数据表先进行一个重命名操作。
1 | bash复制代码# 分别去掉两张表的版本后缀 |
将数据表重命名之后,接下来,来看看这两张表里到底有多少数据。
1 | sql复制代码SELECT |
可以看到两张表数据量都不大,百万到千万级别。
1 | sql复制代码┌────hits─┬──visits─┐ |
接着我们来查看一下两张表的表结构,可以看到两张表,分别有133个、181个列,是“一般意义上的”宽表,非常适合进行分析使用。
1 | sql复制代码desc hits |
再来看看另外一张表:
1 | sql复制代码cc1b062138da :) desc visits |
数据查询操作
接下来,我们来针对这两张表里的数据进行一些基础的查询操作。
分组查询取Top N记录
日常业务中比较频繁的一个操作是有一大堆数据,我们需要筛选其中比较“热门”的数据,假设我们要统计网站具备不同“计数器 ID”的子模块的访问次数,那么可以通过下面的语句来完成:
1 | sql复制代码cc1b062138da :) SELECT CounterID, count() AS nums FROM hits GROUP BY CounterID ORDER BY nums DESC LIMIT 10; |
或者如果我们要实现一个诸如“新浪、知乎搜索热榜”之类的功能,可以通过下面的方式,来展示前十条热搜关键词,以及对应的搜索次数:
1 | sql复制代码cc1b062138da :) SELECT SearchPhrase AS keyword, count() AS nums FROM hits GROUP BY keyword ORDER BY nums DESC, keyword LIMIT 10 |
根据数值进行精确筛选
日常业务中,我们也很容易遇到需要看看到底有多少数值不为空的记录,查询方法类似 MySQL ,使用下面的语句可以快速统计 AdvEngineID
不为零的数据的总数:
1 | sql复制代码cc1b062138da :) SELECT count() FROM hits WHERE AdvEngineID != 0 |
数据计算
除了过滤掉为空的数据之外,还容易遇到需要对总访问次数进行求和的操作,以及计算所有来源的平均值,ClickHouse 中为我们提供了大量高性能的计算函数:
1 | sql复制代码cc1b062138da :) SELECT sum(AdvEngineID), count(), avg(ResolutionWidth) FROM hits |
数据去重
当然,有的时候,我们在应用侧会存储一些冗余的数据,为了计算的准确,我们需要以某个字段为准,进行去重操作,进行更精准的计算。
下面展示从“计数器ID”为 800784 的计数器中筛选数据,并根据 RegionID 字段进行分组,最终以降序排列展示去重后的用户ID。
1 | sql复制代码cc1b062138da :) SELECT RegionID, uniq(UserID) AS UID FROM hits WHERE CounterID = 800784 GROUP BY RegionID ORDER BY UID DESC, RegionID LIMIT 10 |
再来一个更复杂一些的例子,你可以使用 uniqIf
同时设置多个去重条件。
1 | sql复制代码cc1b062138da :) SELECT uniq(UserID), uniqIf(UserID, CounterID = 800784), uniqIf(FUniqID, RegionID = 213) FROM hits |
数据排序
数据排序也是一个常见需求,比如我们根据数据时间,展示最近十条写入的数据。
1 | sql复制代码cc1b062138da :) SELECT EventTime FROM hits ORDER BY EventTime DESC LIMIT 10 |
复合查询条件的排序
ClickHouse 对于结构化的数据的查询支持也非常好,你可以使用类似下面的语句对于数组类型的数据进行查询,以及直接对结果进行排序:
1 | sql复制代码cc1b062138da :) SELECT ParsedParams.Key1 FROM visits FINAL WHERE VisitID != 0 AND notEmpty(ParsedParams.Key1) ORDER BY VisitID LIMIT 10 |
范围查询
限制范围查询除了使用数学的比较符号外,一般分为两类,第一类是设置“白名单”进行查询:
1 | sql复制代码cc1b062138da :) select sum(Sign) from visits where CounterID in (942285); |
第二类则是,根据给定的时间范围进行查询,在 ClickHouse 里,如果我们想知道一段数据的记录时间,甚至可以这样做:
1 | sql复制代码cc1b062138da :) SELECT min(EventDate), max(EventDate) FROM hits |
Join 查询
联合查询也是一个常见需求,关于 Join ,一张图胜过千言万语。
如果我们希望将一张百万,一张近千万的数据表进行联合查询,计算相同日期里,的用户UV和点击PV的话,可以这样操作:
1 | sql复制代码SELECT |
子查询
子查询同样是一个非常常见的需求,可以帮助我们减少非常多不必要的手动步骤,比如像下面这样,先筛选一批用户,然后再计算这批用户的访问情况:
1 | sql复制代码cc1b062138da :) SELECT count() FROM hits PREWHERE UserID IN (SELECT UserID FROM hits WHERE CounterID = 800784); |
上面的例子中,我们使用了 PREWHERE 来进行数据查询优化,可以过滤大量不必要的数据读取,不过如果你的数据量比较少,或者并发压力小,一律 WHERE 也问题不大,以下是两个语句对于数据读取量的差异:
1 | sql复制代码# PREWHERE |
字符串模糊搜索
有一些时候,我们需要进行快速的模糊搜索,ClickHouse 同样能够提供非常令人惊讶的性能:
1 | sql复制代码cc1b062138da :) SELECT count() FROM hits WHERE URL LIKE '%avtomobili%'; |
彩蛋:偷懒画个图
除此之外,有的时候,我们会要求绘制一些图表来帮助我们直观的看到数据量级的差异,也可以使用 ClickHouse 自带的 bar
函数来处理。
1 | sql复制代码cc1b062138da :) SELECT CounterID, count() AS c, bar(c, 0, 523264) FROM hits GROUP BY CounterID ORDER BY c DESC, CounterID ASC LIMIT 100; |
MySQL 数据导入
接下来,我们来了解下如何进行数据导入,分别以常见的 MySQL 、 CSV 为例。(官方目前支持几十种数据的直接导入或导出格式:clickhouse.tech/docs/zh/int… ,你可以根据实际需求进行调整,减少不必要数据转换操作。)
启动 MySQL 数据库
为了能够让数据库实例能够互通,我们需要先创建一个虚拟的容器网络:
1 | bash复制代码docker network create dbtest |
和上文一样,将下面的内容保存为 docker-compose.yml
(可以存放另外一个目录,如果存放相同目录,需要调整数据目录名称):
1 | yaml复制代码version: '3' |
接着同样使用 docker-compose up -d
命令,将 MySQL 启动起来,稍后使用。
初始化测试数据
将前文提到的测试数据下载完成后,先进行解压缩,解压缩之后可以看到压缩比差不多 1:6(早些时候的例子是 1:10)。
1 | bash复制代码unzip yyets_mysql.zip |
将数据扔到数据库容器可以访问到的目录中(推荐使用另外一个方式,方便更直观的对比迁移前后的数据体积):
1 | bash复制代码mv yyets-mysql5.7.sql data |
或者使用 docker cp
命令,将数据复制进 MySQL 容器的数据文件夹中:
1 | bash复制代码docker cp yyets.sql mysql:/var/lib/mysql-files/ |
接着使用 docker exec
命令登陆容器内的数据终端,先来创建一个空的数据库。
1 | bash复制代码docker exec -it mysql mysql -uroot -ptest |
接着,将下载好的外部数据源进行初始化,用来 ClickHouse 导入的数据源。
1 | bash复制代码docker exec -it mysql bash |
稍等片刻数据就都被恢复到 MySQL 实例中了。
导入完毕后,再次使用 du
看看磁盘占用空间,对比导入前的 SQL 文件,磁盘使用空间膨胀了270MB左右(包含索引)。
1 | bash复制代码du -hs data |
我们再次进入数据库,来看看数据表和数据总量。
1 | sql复制代码docker exec -it mysql mysql -uroot -ptest |
可以看到数据库内,只有不到2万条的数据,虽然比较少,但是足够我们用来测试啦。
调整 ClickHouse 实例
为了直观对比数据存储差异,我们可以关闭原来的 ClickHouse 实例后,换一个干净的数据目录,再启动一个新的 ClickHouse 实例。
不过,我们也需要简单调整一下 ClickHouse 配置,让它加入相同的容器网络中:
1 | yaml复制代码version: "2" |
过程我就不多赘述了,参考前文即可,不过这里我们先记录一下空的 ClickHouse 目录占用多少磁盘空间。
1 | yaml复制代码clickhouse du -hs * |
导入 MySQL 数据到 ClickHouse
为了简化导入数据,减少不必要的“数据转换”操作,我们可以通过先创建一个“在线表”的方式来让 CH 自动创建 scheme
。
- 官方文档中的数据类型:clickhouse.tech/docs/zh/sql…
先使用容器登陆终端 :
1 | bash复制代码docker exec -it clickhouse clickhouse-client |
接着“挂载” MySQL 到 ClickHouse;
1 | sql复制代码981a7a68eb35 :) CREATE DATABASE yyets_online ENGINE = MySQL('mysql:3306', 'yyets', 'root', 'test') |
切换数据库,查看表数据量,可以看到和 MySQL 中看到的一样。
1 | sql复制代码981a7a68eb35 :) use yyets_online; |
继续查看表结构,会看到数据类型已经转换好了。
1 | sql复制代码981a7a68eb35 :) desc yyets |
继续“偷懒”,直接使用 show create
语句得到创建当前表结构的命令。
1 | sql复制代码981a7a68eb35 :) show create yyets |
然后将 数据库引擎
部分进行修改,并添加主键即可完成我们后续用于数据计算的离线表的创建。
1 | sql复制代码981a7a68eb35 :) CREATE DATABASE yyets_offline; |
最后,使用 insert into
语句将在线表中的数据导入离线表即可。
1 | sql复制代码981a7a68eb35 :) insert INTO yyets_offline.yyets select * from yyets_online.yyets |
在导入完成后,我们可以先做一个简单的查询,确认数据是否传输完毕。
1 | sql复制代码SELECT |
可以看到两个表的数据量一致,代表数据导入就完成了。可以随便试一条简单的关键词匹配。
1 | sql复制代码466b62ff5dae :) SELECT id, name FROM yyets_offline.`yyets` WHERE `data` LIKE '%漫威%' limit 10 |
数据导入完毕后,记得“打扫卫生”,删除挂载的远程“ online ”数据库的连接即可。
1 | bash复制代码981a7a68eb35 :) drop database yyets_online |
最后,使用 du 查看一下 ClickHouse 使用的磁盘空间吧。
1 | bash复制代码du -hs data |
相比 MySQL 的 530M 数据存储量来说, ClickHouse 的存储占用真的是非常轻量啦。
CSV 数据导入
一般情况下, CSV 类型数据的导入非常简单,但是 CSV 数据中存在非常多“非标准数据”:比如非严格转义、非严格数值类型标记,混合多系统换行符, field 数据包含“预期之外的数据”等,针对这种情况,我们需要先对数据进行“修复”,因为不同的场景数据完全不同,所以这里没有办法提供一个“万能”的通用方案,不过以过来人的经验,使用 Golang 或者 Node 编写一个清洗工具,执行效率真的蛮快的,感兴趣的同学可以试试看。
我们说回“数据导入”,根据是否还需要 pipeline 处理过程,通常我们有两种方式来进行 CSV 类型的数据导入,如果你的数据都是预先处理过的,可以采取下面的方式来直接导入:
1 | bash复制代码clickhouse-client --format_csv_delimiter="|" --format_csv_allow_double_quotes=0 --format_csv_allow_single_quotes=0 --query="INSERT INTO yyets_csv.resource FORMAT CSV" < /var/lib/clickhouse/dump.csv |
但是如果你的数据需要使用过程工具来做转换,则可以使用类似下面的方式来导入,过程中可以使用 awk
、sed
、你自己的工具
:
1 | bash复制代码cat /var/lib/clickhouse/dump.csv | clickhouse-client --format_csv_delimiter="|" --format_csv_allow_double_quotes=0 --format_csv_allow_single_quotes=0 --query="INSERT INTO yyets_csv.resource FORMAT CSV" |
最后
不知不觉写了近万字,先聊到这里吧,后面有时间再聊聊使用过程中踩到的坑。
–EOF
我们有一个小小的折腾群,里面聚集了几百位喜欢折腾的小伙伴。
在不发广告的情况下,我们在里面会一起聊聊软硬件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术沙龙的资料。
喜欢折腾的小伙伴欢迎扫码添加好友。(添加好友,请备注实名,注明来源和目的,否则不会通过审核)
如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。
本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)
本文作者: 苏洋
创建时间: 2021年10月16日
统计字数: 33858字
阅读时间: 68分钟阅读
本文链接: soulteary.com/2021/10/16/…
本文转载自: 掘金