本篇来自数月前对外分享的文稿整理,并进行了一些扩展。
希望通过简单的方式,来介绍新手如何一步一步上手 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
。
数据下载完毕之后,我们需要先对数据进行解压缩。
代码语言:txt复制mkdir data
tar xvf hits_v1.tar -C data
tar xvf visits_v1.tar -C data
通过 du
命令可以看到使用的数据实际使用了 1.7GB空间,顺便提一下,这些数据如果存储在 MySQL 中,存储空间可能会膨胀 3~5倍以上。
du -hs data
1.7G data
数据解压完毕,就可以开始准备对 ClickHouse 的容器运行配置了。
前置准备:准备 ClickHouse 运行配置
代码语言:txt复制version: "2"
services:
server:
image: yandex/clickhouse-server:21.9.4.35
container_name: clickhouse
expose:
- 9000
- 8123
- 9009
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
environment:
- TZ=Asia/Shanghai
# - CLICKHOUSE_USER=root
# - CLICKHOUSE_PASSWORD=xmnzdwH5
volumes:
- ./data:/var/lib/clickhouse
# 按需使用
# - ./config.xml:/etc/clickhouse-server/config.xml
# - ./users.xml:/etc/clickhouse-server/users.xml
将上面的配置保存为 docker-compose.yml
,并使用 docker-compose up -d
启动 ClickHouse,以备稍后使用。
额外说一下,ClickHouse 的版本更新很快,建议升级的时候先做一些小样本测试,测试常用场景是否正常,再进行版本更新替换。
ClickHouse 初体验
ClickHouse 使用的 SQL 语法相比较 MySQL 等数据库会宽松许多,类比的话,就像是之前写 Java 的选手一下子步入了 Python 和 JavaScript 的世界。
因为使用容器启动 ClickHouse,所以我们可以通过 docker exec
命令进入 ClickHouse 的交互式终端。
docker exec -it clickhouse clickhouse-client
进入终端后,先来看看有哪些“数据库”和数据表:
代码语言:txt复制# 查看数据库
cc1b062138da :) show databases
SHOW DATABASES
Query id: efaa1c51-e112-43d6-b803-1e6dd86ad43b
┌─name─────┐
│ datasets │
│ default │
│ system │
└──────────┘
3 rows in set. Elapsed: 0.003 sec.
# 切换数据库
cc1b062138da :) use datasets
USE datasets
Query id: b10ff8f3-0743-42f4-9ee1-663b9a2c4955
Ok.
0 rows in set. Elapsed: 0.002 sec.
# 查看数据表
cc1b062138da :) show tables
SHOW TABLES
Query id: c6eb8203-6ea2-4576-9bb7-74ad4e1c7de9
┌─name──────┐
│ hits_v1 │
│ visits_v1 │
└───────────┘
2 rows in set. Elapsed: 0.005 sec.
上面的结果中的 datasets
就是我们导入的数据集。ClickHouse 对于数据存放比较“佛系”,如果你查看本地目录可以看到上面的数据和 data/datasets
目录保持一致,实际操作使用的时候,只要把 data 目录打个压缩包就能完成数据备份了,是不是很简单。
tree -L 3 data/data/datasets
data/data/datasets
├── hits_v1
│ ├── 201403_10_18_2
│ │ ├── AdvEngineID.bin
│ │ ├── AdvEngineID.mrk
│ │ ├── Age.bin
│ │ ├── Age.mrk
│ │ ├── UserID.bin
│ │ ├── UserID.mrk
...
│ │ ├── WatchID.bin
│ │ ├── WatchID.mrk
│ │ ├── YCLID.bin
│ │ ├── YCLID.mrk
│ │ ├── checksums.txt
│ │ ├── columns.txt
│ │ ├── count.txt
│ │ ├── minmax_EventDate.idx
│ │ ├── partition.dat
│ │ └── primary.idx
│ ├── detached
│ └── format_version.txt
└── visits_v1
├── 20140317_20140323_3_4_1
│ ├── AdvEngineID.bin
│ ├── AdvEngineID.mrk
│ ├── Age.bin
│ ├── Age.mrk
│ ├── Attendance.bin
│ ├── Attendance.mrk
...
│ ├── ClickBannerID.bin
│ ├── ClickBannerID.mrk
│ ├── ClickClientIP.bin
│ ├── ClickClientIP.mrk
│ ├── YCLID.bin
│ ├── YCLID.mrk
│ ├── checksums.txt
│ ├── columns.txt
│ └── primary.idx
├── detached
└── format_version.txt
6 directories, 675 files
为了后续敲的命令能简单些,我们针对数据表先进行一个重命名操作。
代码语言:txt复制# 分别去掉两张表的版本后缀
cc1b062138da :) rename table hits_v1 to hits
RENAME TABLE hits_v1 TO hits
Query id: dba1405a-1836-4d5a-af23-3ce0f5b31d41
Ok.
0 rows in set. Elapsed: 0.014 sec.
cc1b062138da :) rename table visits_v1 to visits
RENAME TABLE visits_v1 TO visits
Query id: 9ffc039c-86c3-42a9-91a6-3ed165254e0b
Ok.
0 rows in set. Elapsed: 0.012 sec.
# 再次查看数据表
cc1b062138da :) show tables
SHOW TABLES
Query id: da91fb7c-5224-4c7f-9a6c-ce4cf37f9fa8
┌─name───┐
│ hits │
│ visits │
└────────┘
2 rows in set. Elapsed: 0.004 sec.
将数据表重命名之后,接下来,来看看这两张表里到底有多少数据。
代码语言:txt复制SELECT
hits,
visits
FROM
(
SELECT count() AS hits
FROM hits
) AS table_hits
,
(
SELECT count() AS visits
FROM visits
) AS table_visits
可以看到两张表数据量都不大,百万到千万级别。
代码语言:txt复制┌────hits─┬──visits─┐
│ 8873898 │ 1676861 │
└─────────┴─────────┘
1 rows in set. Elapsed: 0.005 sec.
接着我们来查看一下两张表的表结构,可以看到两张表,分别有133个、181个列,是“一般意义上的”宽表,非常适合进行分析使用。
代码语言:txt复制desc hits
cc1b062138da :) desc hits
:-]
:-]
DESCRIBE TABLE hits
Query id: b8d8b650-2395-4207-b2ce-4200dc9c0fce
┌─name───────────────────────┬─type────────────┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐
│ WatchID │ UInt64 │ │ │ │ │ │
│ JavaEnable │ UInt8 │ │ │ │ │ │
│ Title │ String │ │ │ │ │ │
│ GoodEvent │ Int16 │ │ │ │ │ │
│ EventTime │ DateTime │ │ │ │ │ │
│ EventDate │ Date │ │ │ │ │ │
│ CounterID │ UInt32 │ │ │ │ │ │
│ ClientIP │ UInt32 │ │ │ │ │ │
│ ClientIP6 │ FixedString(16) │ │ │ │ │ │
│ RegionID │ UInt32 │ │ │ │ │ │
│ UserID │ UInt64 │ │ │ │ │ │
│ CounterClass │ Int8 │ │ │ │ │ │
│ OS │ UInt8 │ │ │ │ │ │
│ UserAgent │ UInt8 │ │ │ │ │ │
│ URL │ String │ │ │ │ │ │
│ Referer │ String │ │ │ │ │ │
│ URLDomain │ String │ │ │ │ │ │
│ RefererDomain │ String │ │ │ │ │ │
│ Refresh │ UInt8 │ │ │ │ │ │
│ IsRobot │ UInt8 │ │ │ │ │ │
│ RefererCategories │ Array(UInt16) │ │ │ │ │ │
│ URLCategories │ Array(UInt16) │ │ │ │ │ │
│ URLRegions │ Array(UInt32) │ │ │ │ │ │
│ RefererRegions │ Array(UInt32) │ │ │ │ │ │
│ ResolutionWidth │ UInt16 │ │ │ │ │ │
│ ResolutionHeight │ UInt16 │ │ │ │ │ │
│ ResolutionDepth │ UInt8 │ │ │ │ │ │
│ FlashMajor │ UInt8 │ │ │ │ │ │
│ FlashMinor │ UInt8 │ │ │ │ │ │
│ FlashMinor2 │ String │ │ │ │ │ │
│ NetMajor │ UInt8 │ │ │ │ │ │
│ NetMinor │ UInt8 │ │ │ │ │ │
│ UserAgentMajor │ UInt16 │ │ │ │ │ │
│ UserAgentMinor │ FixedString(2) │ │ │ │ │ │
│ CookieEnable │ UInt8 │ │ │ │ │ │
│ JavascriptEnable │ UInt8 │ │ │ │ │ │
│ IsMobile │ UInt8 │ │ │ │ │ │
│ MobilePhone │ UInt8 │ │ │ │ │ │
│ MobilePhoneModel │ String │ │ │ │ │ │
│ Params │ String │ │ │ │ │ │
│ IPNetworkID │ UInt32 │ │ │ │ │ │
│ TraficSourceID │ Int8 │ │ │ │ │ │
│ SearchEngineID │ UInt16 │ │ │ │ │ │
│ SearchPhrase │ String │ │ │ │ │ │
│ AdvEngineID │ UInt8 │ │ │ │ │ │
│ IsArtifical │ UInt8 │ │ │ │ │ │
│ WindowClientWidth │ UInt16 │ │ │ │ │ │
│ WindowClientHeight │ UInt16 │ │ │ │ │ │
│ ClientTimeZone │ Int16 │ │ │ │ │ │
│ ClientEventTime │ DateTime │ │ │ │ │ │
│ SilverlightVersion1 │ UInt8 │ │ │ │ │ │
│ SilverlightVersion2 │ UInt8 │ │ │ │ │ │
│ SilverlightVersion3 │ UInt32 │ │ │ │ │ │
│ SilverlightVersion4 │ UInt16 │ │ │ │ │ │
│ PageCharset │ String │ │ │ │ │ │
│ CodeVersion │ UInt32 │ │ │ │ │ │
│ IsLink │ UInt8 │ │ │ │ │ │
│ IsDownload │ UInt8 │ │ │ │ │ │
│ IsNotBounce │ UInt8 │ │ │ │ │ │
│ FUniqID │ UInt64 │ │ │ │ │ │
│ HID │ UInt32 │ │ │ │ │ │
│ IsOldCounter │ UInt8 │ │ │ │ │ │
│ IsEvent │ UInt8 │ │ │ │ │ │
│ IsParameter │ UInt8 │ │ │ │ │ │
│ DontCountHits │ UInt8 │ │ │ │ │ │
│ WithHash │ UInt8 │ │ │ │ │ │
│ HitColor │ FixedString(1) │ │ │ │ │ │
│ UTCEventTime │ DateTime │ │ │ │ │ │
│ Age │ UInt8 │ │ │ │ │ │
│ Sex │ UInt8 │ │ │ │ │ │
│ Income │ UInt8 │ │ │ │ │ │
│ Interests │ UInt16 │ │ │ │ │ │
│ Robotness │ UInt8 │ │ │ │ │ │
│ GeneralInterests │ Array(UInt16) │ │ │ │ │ │
│ RemoteIP │ UInt32 │ │ │ │ │ │
│ RemoteIP6 │ FixedString(16) │ │ │ │ │ │
│ WindowName │ Int32 │ │ │ │ │ │
│ OpenerName │ Int32 │ │ │ │ │ │
│ HistoryLength │ Int16 │ │ │ │ │ │
│ BrowserLanguage │ FixedString(2) │ │ │ │ │ │
│ BrowserCountry │ FixedString(2) │ │ │ │ │ │
│ SocialNetwork │ String │ │ │ │ │ │
│ SocialAction │ String │ │ │ │ │ │
│ HTTPError │ UInt16 │ │ │ │ │ │
│ SendTiming │ Int32 │ │ │ │ │ │
│ DNSTiming │ Int32 │ │ │ │ │ │
│ ConnectTiming │ Int32 │ │ │ │ │ │
│ ResponseStartTiming │ Int32 │ │ │ │ │ │
│ ResponseEndTiming │ Int32 │ │ │ │ │ │
│ FetchTiming │ Int32 │ │ │ │ │ │
│ RedirectTiming │ Int32 │ │ │ │ │ │
│ DOMInteractiveTiming │ Int32 │ │ │ │ │ │
│ DOMContentLoadedTiming │ Int32 │ │ │ │ │ │
│ DOMCompleteTiming │ Int32 │ │ │ │ │ │
│ LoadEventStartTiming │ Int32 │ │ │ │ │ │
│ LoadEventEndTiming │ Int32 │ │ │ │ │ │
│ NSToDOMContentLoadedTiming │ Int32 │ │ │ │ │ │
│ FirstPaintTiming │ Int32 │ │ │ │ │ │
│ RedirectCount │ Int8 │ │ │ │ │ │
│ SocialSourceNetworkID │ UInt8 │ │ │ │ │ │
│ SocialSourcePage │ String │ │ │ │ │ │
│ ParamPrice │ Int64 │ │ │ │ │ │
│ ParamOrderID │ String │ │ │ │ │ │
│ ParamCurrency │ FixedString(3) │ │ │ │ │ │
│ ParamCurrencyID │ UInt16 │ │ │ │ │ │
│ GoalsReached │ Array(UInt32) │ │ │ │ │ │
│ OpenstatServiceName │ String │ │ │ │ │ │
│ OpenstatCampaignID │ String │ │ │ │ │ │
│ OpenstatAdID │ String │ │ │ │ │ │
│ OpenstatSourceID │ String │ │ │ │ │ │
│ UTMSource │ String │ │ │ │ │ │
│ UTMMedium │ String │ │ │ │ │ │
│ UTMCampaign │ String │ │ │ │ │ │
│ UTMContent │ String │ │ │ │ │ │
│ UTMTerm │ String │ │ │ │ │ │
│ FromTag │ String │ │ │ │ │ │
│ HasGCLID │ UInt8 │ │ │ │ │ │
│ RefererHash │ UInt64 │ │ │ │ │ │
│ URLHash │ UInt64 │ │ │ │ │ │
│ CLID │ UInt32 │ │ │ │ │ │
│ YCLID │ UInt64 │ │ │ │ │ │
│ ShareService │ String │ │ │ │ │ │
│ ShareURL │ String │ │ │ │ │ │
│ ShareTitle │ String │ │ │ │ │ │
│ ParsedParams.Key1 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key2 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key3 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key4 │ Array(String) │ │ │ │ │ │
│ ParsedParams.Key5 │ Array(String) │ │ │ │ │ │
│ ParsedParams.ValueDouble │ Array(Float64) │ │ │ │ │ │
│ IslandID │ FixedString(16) │ │ │ │ │ │
│ RequestNum │ UInt32 │ │ │ │ │ │
│ RequestTry │ UInt8 │ │ │ │ │ │
└────────────────────────────┴─────────────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘
133 rows in set. Elapsed: 0.003 sec.
cc1b062138da :)
再来看看另外一张表:
文章字数超过编辑器5万字限制,需要继续阅读的同学,可以查阅文末底部的链接。
如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。
本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)
本文作者: 苏洋
创建时间: 2021年10月16日
统计字数: 33858字
阅读时间: 68分钟阅读
本文链接: https://soulteary.com/2021/10/16/get-started-quickly-with-clickhouse.html