TT Lab
开始
学习 学习路径 课程

ClickHouse — 从内部理解列式分析数据库

写入两百万行,看看列文件里有什么

在 TT Lab 中继续学习

目标

向 MergeTree 表写入 200 万行,并从 system 表中读出数据片段、颗粒和各列的压缩率。用 rows_read、bytes_read 确认,即使读取相同的行数,成本也会因触碰的列和排序键而不同。

为什么重要

在列式数据库中,查询成本不取决于“选了多少行”,而取决于“读了哪些列、多少个颗粒”。没有这种感觉,就会选错排序键,习惯性地使用 SELECT *,并按原始大小来估算磁盘容量。本实验的评分器不会相信你写的数字——它会直接读取服务器的 system.parts、system.columns、system.query_log,并把你保存的 SELECT 以只读方式重新运行,测量读取的行数和字节数进行核对。评分器自己的查询不会留在 query_log 中。

步骤

  1. 创建数据库 col 和表 col.events——列为 ts DateTime, site LowCardinality(String), user_id UInt64, url String, dur_ms UInt32, country FixedString(2)(按此顺序),引擎为 MergeTree,排序键为 ORDER BY (site, ts)。
  2. 运行一次 /opt/lab/fixtures/columnar/events.sql,写入 200 万行。
  3. 用 OPTIMIZE TABLE col.events FINAL 把数据片段合并成一个,然后把该数据片段的 part(名称)、rows、marks、part_type 写入 /root/ch/columnar/parts.json。
  4. 从 system.columns 中找出未压缩/压缩比值最大的列(most_compressed)、最小的列(least_compressed)以及压缩后的字节合计(total_compressed_bytes),写入 /root/ch/columnar/columns.json。
  5. 创建只求 dur_ms 之和的 /root/ch/columnar/q_one.sql,以及读取 url 字符串内容的 /root/ch/columnar/q_url.sql(例如 max(url)),并把两个查询的 bytes_read 以 one_col_bytes、url_bytes 写入 /root/ch/columnar/bytes.json。
  6. 创建求 site = 'docs.example' 的行的 dur_ms 之和的 /root/ch/columnar/q_key.sql,以及求 country = 'KR' 的行的 dur_ms 之和的 /root/ch/columnar/q_nokey.sql,并把两个查询的 rows_read 以 key_rows_read、nokey_rows_read 写入 /root/ch/columnar/rows.json。
  7. 加上 SETTINGS log_comment = 'chs-col-07' 运行 SELECT uniqExact(user_id) FROM col.events WHERE site = 'shop.example',然后在 system.query_log 中找到这个查询,把 query_id、read_rows、read_bytes、result_rows 写入 /root/ch/columnar/qlog.json。
  8. 创建与 col.events 列相同的表 col.small,用一次 INSERT 写入 col.events 的 1000 行,并把两张表的数据片段格式以 events、small 写入 /root/ch/columnar/part_types.json。

参考

创建带排序键的 MergeTree 表

创建数据库 col 和表 col.events。列依次为 ts DateTime, site LowCardinality(String), user_id UInt64, url String, dur_ms UInt32, country FixedString(2),引擎为 MergeTree,排序键为 ORDER BY (site, ts)。

就是 CREATE DATABASE 和 CREATE TABLE 两条语句。没有写排序键的 MergeTree 是建不出来的——因为键就是行在磁盘上的顺序。列名和类型哪怕有一个字符不同,后面步骤中的源脚本就写不进去。

写入 200 万行源数据

运行一次 /opt/lab/fixtures/columnar/events.sql,向 col.events 写入 2,000,000 行。

用 --queries-file 传给 clickhouse-client 即可。这个脚本用 numbers() 生成行号,并用哈希取值,所以不管运行多少次,得到的行都相同。如果行数是 400 万,说明写了两次。

合并成一个数据片段并读出名称

用 OPTIMIZE TABLE col.events FINAL 把数据片段合并成一个,然后从 system.parts 中读出该活动数据片段的 part(名称)、rows、marks、part_type,写入 /root/ch/columnar/parts.json。

一次 INSERT 会根据块大小产生多个数据片段。合并之后名称会变(合并级别提高)。只看 active = 1 的行——被合并的旧数据片段也会在列表里留一小会儿。把 system.parts 的 name 列用别名命名为 part,以 JSONEachRow 取出,就可以直接保存。

各列的压缩率相差多大

从 system.columns 中为 col.events 的每一列计算 data_uncompressed_bytes / data_compressed_bytes,把最大的列作为 most_compressed,最小的列作为 least_compressed,压缩后的字节合计作为 total_compressed_bytes,写入 /root/ch/columnar/columns.json。

用 argMax(name, 比率) 和 argMin(name, 比率),一条查询就能完成。压缩得最好的列通常位于排序键的前部,或者是只有几种取值的列;压缩得最差的列,是不断增大的值或接近随机的值。

同样的行,不同的列——读取的字节数

创建只求 dur_ms 之和的 /root/ch/columnar/q_one.sql,以及读取 url 字符串内容的 /root/ch/columnar/q_url.sql,并用 --format JSON 运行这两个查询,把 statistics.bytes_read 以 one_col_bytes、url_bytes 写入 /root/ch/columnar/bytes.json。

两个查询都会读取全部 200 万行。不同的是所触碰的列的大小。一个 UInt32 列,每行 4 字节。url 那边不能用 length(url)——这个函数会被改为只读取仅含长度的子列,而不读取内容。请使用 max(url) 这样会比较内容的函数。

只有按排序键过滤才会跳过

创建求 site = 'docs.example' 的行的 dur_ms 之和的 /root/ch/columnar/q_key.sql,以及求 country = 'KR' 的行的 dur_ms 之和的 /root/ch/columnar/q_nokey.sql,并把两个查询的 statistics.rows_read 以 key_rows_read、nokey_rows_read 写入 /root/ch/columnar/rows.json。

site 是排序键的第一列,所以索引会跳过不可能含有该值的颗粒。country 不在键中,没有可以跳过的依据。想一想,为什么读取的行数会接近 8192 的倍数——跳过的单位不是行,而是颗粒。

在 query_log 中找到自己的查询

运行 SELECT uniqExact(user_id) FROM col.events WHERE site = 'shop.example' SETTINGS log_comment = 'chs-col-07',然后在 system.query_log 中找到该查询的结束(type = 'QueryFinish')记录,把 query_id、read_rows、read_bytes、result_rows 写入 /root/ch/columnar/qlog.json。

query_log 大约每 1 秒写入磁盘一次。想立刻找到,就先执行 SYSTEM FLUSH LOGS。一个查询会留下开始(QueryStart)和结束(QueryFinish)两行,读取的行数只在结束那一行。用 log_comment 过滤,就能在众多查询中只留下自己的。

小数据片段会成为 Compact

创建与 col.events 列和引擎相同的表 col.small,用一次 INSERT 写入 col.events 的 1000 行,并把两张表的活动数据片段格式(part_type)以 events、small 写入 /root/ch/columnar/part_types.json。

CREATE TABLE ... AS 另一张表 会原样复制列和引擎。数据片段格式取决于:数据片段的字节数、行数小于表设置 min_bytes_for_wide_part、min_rows_for_wide_part 就是 Compact,大于则是 Wide。请不要修改表设置。