开始文章前先扯个淡
最近在思考统计功能的时候,一开始是寻思着用程序配合mysql来进行存储,但是如果用mysql来存一些比较细致化的数据然后再做统计,数据量会非常庞大,然后又思考用mysql存储数据,然后用定时脚本或队列来按照具体要求更新到redis中,这样查询就很快了。
后来想起我们本身用的plausible
来记录数据,然后寻思着用plausible
的event
来记录自定义的goals
,发现的确可行,模拟了500万数据之后,表现良好,感觉自己棒棒哒。谁知道数据量起来之后,2000万的数据就能让服务炸了。期间,也遇到了一些其他问题,也给官方提了一些建议,逼不得已不得不学了elixir
,当然这是后话了。这里提一嘴这个plausible
主要是给后来者提个醒,如果也想用plausible
来做自定义的一些统计功能,最好是放弃掉,改为直接用ClickHouse
来处理。
最后没有办法,我观察了plausible
的数据存储结构,发现根本原因就是event
的存储类型导致的,性能误解,从而萌生了去研究一下ClickHouse
,看看自己实现能不能得到很好的性能,于是便有了这篇文章,如果对大家有用,欢迎探讨,如果理解有错的话,欢迎各位给我斧正。
行式数据库 vs 列式数据库结构对比
以下先以官方文档的表格来抛砖引玉,记录一下这段时间对于clickhouse的学习成果。
行式数据库
Row | WatchID | JavaEnable | Title | GoodEvent | EventTime |
---|---|---|---|---|---|
#0 | 89354350662 | 1 | Investor Relations | 1 | 2016-05-18 05:19:20 |
#1 | 90329509958 | 0 | Contact us | 1 | 2016-05-18 08:10:20 |
#2 | 89953706054 | 1 | Mission | 1 | 2016-05-18 07:38:00 |
#N | … | … | … | … | … |
列式数据库
Row: | #0 | #1 | #2 | #N |
---|---|---|---|---|
WatchID: | 89354350662 | 90329509958 | 89953706054 | … |
JavaEnable: | 1 | 0 | 1 | … |
Title: | Investor Relations | Contact us | Mission | … |
GoodEvent: | 1 | 1 | 1 | … |
EventTime: | 2016-05-18 05:19:20 | 2016-05-18 08:10:20 | 2016-05-18 07:38:00 | … |
从表格上看,一下子比较难以理解,行式数据库和我们电子表格看到的表现一样,但是存储是一行一行的存储,列式数据库是以一列一列的存储。以下是以文件存储结构来做一个示例:
也就是说,对于行数据库,一行数据是形成一个完整的文件(实际底层具体存储不是这样子,这里是方便说明整个思想),这个文件包含了这一整行的每个数据字段,取到这一行就能拿到完整的数据。
而对于列式存储来说,一列数据为一个文件,如以上所展示的,WatchId
的数据全部存在WatchId
文件里面。
然后我做了个脑图再次对这个结构做个对比。
以下用json来描述两种数据结构的区别:
行式数据库存储结构
[ { "id": 1, "title": "t1", "content": "c1" }, { "id": 2, "title": "t2", "content": "c2" }, { "id": 2, "title": "t2", "content": "c2" }, { "id": 3, "title": "t3", "content": "c3" } ]
列式数据库结构
{ "id": [ 1, 2, 3, 4 ], "title": [ "t1", "t2", "t3", "t4" ], "content": [ "c1", "c2", "c3", "c4" ] }
列式数据库主要应用场景
列式数据库主要运用场景为OLAP
(联机分析),也就是说主要应用于数据分析这一块,通常不用来处理具体的业务数据存储。行式数据库主要用于处理业务数据的存储,业务数据的特点是对单条数据经常会产生更新,会对数据一致性有非常严格的要求。对于OLAP来说,数据很少会有变动,所以通常来说,列式数据库主要目的是为了更快的写入以及更快的数据查询和统计。
以下我列出一些OLAP
场景的关键特征:
- 绝大多数是读请求
- 数据以相当大的批次(> 1000行)更新,而不是单行更新;或者根本没有更新。
- 已添加到数据库的数据不能修改。
- 对于读取,从数据库中提取相当多的行,但只提取列的一小部分。
- 宽表,即每个表包含着大量的列
- 查询相对较少(通常每台服务器每秒查询数百次或更少)
- 对于简单查询,允许延迟大约50毫秒
- 列中的数据相对较小:数字和短字符串(例如,每个URL 60个字节)
- 处理单个查询时需要高吞吐量(每台服务器每秒可达数十亿行)
- 事务不是必须的
- 对数据一致性要求低
- 每个查询有一个大表。除了他以外,其他的都很小。
- 查询结果明显小于源数据。换句话说,数据经过过滤或聚合,因此结果适合于单个服务器的RAM中
MergeTree(合并树) 数据表引擎
主要特点
MergeTree
是ClickHouse
最基础的数据存储引擎,绝大部分时候都是用这个数据引擎。
MergeTree
系列的引擎被设计用于插入极大量的数据到一张表当中。数据可以以数据片段的形式一个接着一个的快速写入,数据片段在后台按照一定的规则进行合并。相比在插入时不断修改(重写)已存储的数据,这种策略会高效很多。
主要特点:
存储的数据按主键排序。
这使得您能够创建一个小型的稀疏索引来加快数据检索。
如果指定了
分区键
的话,可以使用分区。在相同数据集和相同结果集的情况下 ClickHouse 中某些带分区的操作会比普通操作更快。查询中指定了分区键时 ClickHouse 会自动截取分区数据。这也有效增加了查询性能。
通常来说,都是以时间来进行分区,从表象来看的话,就是一个分区是一个文件夹,这样可以避免一个文件存储大量的数据。
支持数据副本。
ReplicatedMergeTree
系列的表提供了数据副本功能。支持数据采样。
需要的话,您可以给表设置一个采样方法。
结构说明
这段时间为了研究plausible
的统计性能问题,主要也是围绕业务展开的,对其他几个数据引擎没有做深入的了解,其他引擎也是继承于MergeTree
引擎,如果大家感兴趣可以了解一下,以下我大致列出其他几个数据引擎:
VersionedCollapsingMergeTree
- 允许快速写入不断变化的对象状态。
- 删除后台中的旧对象状态。 这显着降低了存储体积。
GraphiteMergeTree
- 该引擎用来对 Graphite数据进行瘦身及汇总。对于想使用CH来存储Graphite数据的开发者来说可能有用。
- 如果不需要对Graphite数据做汇总,那么可以使用任意的CH表引擎;但若需要,那就采用
GraphiteMergeTree
引擎。它能减少存储空间,同时能提高Graphite数据的查询效率。
AggregatingMergeTree
CollapsingMergeTree
- 该引擎继承于 MergeTree,并在数据块合并算法中添加了折叠行的逻辑。
ReplacingMergeTree
- 该引擎和 MergeTree 的不同之处在于它会删除排序键值相同的重复项
SummingMergeTree
- 当合并
SummingMergeTree
表的数据片段时,ClickHouse 会把所有具有相同主键的行合并为一行,该行包含了被合并的行中具有数值数据类型的列的汇总值。如果主键的组合方式使得单个键值对应于大量的行,则可以显著的减少存储空间并加快数据查询的速度。
- 当合并
ClickHouse 数据库目前梳理的一些注意点
- 不适合处理事务,没有完整的事务支持。
- 缺少高频率,低延迟的修改或删除已存在数据的能力。仅能用于批量删除或修改数据,数据更新是异步的
- 大部分数据查询适用标准的SQL语句,如果一直使用
MySQL
这种行式数据库,没太多切换成本 - 定义排序字段或者说是定义索引,存储时候就会按照排序存储,范围查询速度非常快
ClickHouse
提供各种各样在允许牺牲数据精度的情况下对查询进行加速的方法(目前我没有用过,暂时不知道应用场景)- 稀疏索引使得
ClickHouse
不适合通过其键检索单行的点查询。 - 数据类型丰富,支持
Map(key,value)
,Tuple(T1,T2,...)
,Array(T)
,Geo
,Nested嵌套数据结构(类似于嵌套表)
等 - Datetime 类型
group by
有toStartOf*
开头的转换函数,如toStartOfDay
,相比用DATE
函数转换,能提升几十倍性能
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。