优化ClickHouse星型模型查询性能

大多数数据环境包括两类数据对象,基于事件对象和基于实体对象(包括属性或特征)。前者通常组织为时间序列表,后者通常基于ID表,基于行存储。这类模型称为星型规范化模型,该结构存储效率高,但读性能低。必要时采用非规范化结构会提升性能。

星型模型

下面探究ClickHouse如何使用join查询星型模型数据。假设有两张表:log(时间序列事件)、users(包括特征的实体):

log事件表使用user_id引用users表,事件log表有250m行数据,结构如下:

CREATE TABLE log (dt DateTime, user_id UInt64, session_id UInt64, platform String, label String, url String)
ENGINE = MergeTree ORDER BY (dt, user_id, session_id)

需要提醒的是:特征表通常需要修改(通常需增加、删除或修改数据),而事件表为历史静态数据(数据仅追加,不会改变)。为简化目的,users表结构如下:

CREATE TABLE users
(`id` UInt64, `name` String, `category` String, `age` UInt8 )
ENGINE = MergeTree ORDER BY (category, id)

插入演示数据:

insert into users
select number , randomPrintableASCII(5) as name,array('aa','bb','cc')[(number)%3+1] as c, number%60+18 as age
from numbers(2500000) n ;

连续执行10次log表生成测试数据:

insert into log
select (now() - number) dt , number id , number+1 sid
      ,array('win','mac','linux')[(number)%3+1] as c
      ,randomPrintableASCII(5)  as label
      ,randomPrintableASCII(10) as url
from numbers(2500000) n ;

select count(*) from log;

星型join

下面尝试实际示例——查询某类型用户及其年龄在特定范围的事件:

SELECT date(dt) AS day, count(*) FROM log l
JOIN users u ON u.id = l.user_id
WHERE
  u.category = 'aa' AND u.age > 50 AND dt > NOW() - INTERVAL 1 YEAR
GROUP BY day ORDER BY day DESC LIMIT 5

log表有250m行,用户大概25m行,查询结果如下:

┌────────day─┬─count()─┐
│ 2023-04-28 │   86546 │
│ 2023-04-27 │  129600 │
│ 2023-04-26 │  129600 │
│ 2023-04-25 │  129600 │
│ 2023-04-24 │  129600 │
└────────────┴─────────┘

5 rows in set. Elapsed: 1.564 sec. Processed 27.50 million rows, 350.00 MB (17.59 million rows/s., 223.83 MB/s.)

下面通过explain查询执行情况:

EXPLAIN 
SELECT date(dt) AS day, count(*) FROM log l
JOIN users u ON u.id = l.user_id
WHERE
  u.category = 'aa' AND u.age > 50 AND dt > NOW() - INTERVAL 1 YEAR
GROUP BY day ORDER BY day DESC LIMIT 5

使用in代替join

大多数场景可以使用in代替join,但要确保嵌套表不要返回太多数据,不能超过ClickHouse有效内存量。在我们的示例中,过滤users返回数据约为总量的15%。

SELECT count(*)
FROM users
WHERE (category = 'aa') AND (age > 50)

Query id: f54846e0-65c3-44d2-8c05-1927adb28ef4

┌─count()─┐
│  374997 │
└─────────┘

1 rows in set. Elapsed: 0.014 sec. Processed 835.58 thousand rows, 10.03 MB (60.45 million rows/s., 725.45 MB/s.)

下面使用in代替join:

SELECT
    date(dt) AS day,
    count(*)
FROM log
WHERE (user_id IN (
    SELECT id
    FROM users
    WHERE (category = 'aa') AND (age > 50)
)) AND (dt > (NOW() - toIntervalYear(1)))
GROUP BY day
ORDER BY day DESC
LIMIT 5

Query id: fb871262-d196-4043-9acb-65476a1ee8be

┌────────day─┬─count()─┐
│ 2023-04-2886546 │
│ 2023-04-27129600 │
│ 2023-04-26129600 │
│ 2023-04-25129600 │
│ 2023-04-24129600 │
└────────────┴─────────┘

5 rows in set. Elapsed: 0.166 sec. Processed 25.84 million rows, 316.71 MB (155.68 million rows/s., 1.91 GB/s.)

从处理数据量和执行时间来看,有了较大提升。虽然这个示例效果不错,但我们不能确保in表达式一定有较大的性能提升,这时我们可以考虑使用反规范化。

反规范化数据

反规范化意味着基于join查询表创建单个表,下面创建并填充单个反规范化表:

CREATE TABLE log_users (
  `dt` DateTime, `user_id` UInt64, `session_id` UInt64,
  `platform` String, `label` String, `url` String,
  `user_name` String, `user_category` String, `user_age` UInt8
)
ENGINE = MergeTree
ORDER BY (dt, user_id, session_id, user_category, platform, label)

可以看到,我们使用了跟多排序键,从而覆盖跟多查询场景。下面填充数据:

INSERT INTO log_users
SELECT l.*, u.name, u.category, u.age
FROM log l JOIN users u ON (l.user_id = u.id)

再次执行前面的查询,性能明细有了提升:

SELECT
date(dt) As day, count(*)
FROM log_users
WHERE(user_category = 'aa') AND(user_age > 50) AND (dt > (now() - toIntervalMonth(1)))
GROUP BY day
ORDER BY day DESC
LIMIT 5


Query id: 2b7575bc-1244-4042-907f-4f6ee8929ade

┌────────day─┬─count()─┐
│ 2023-04-2886546 │
│ 2023-04-27129600 │
│ 2023-04-26129600 │
│ 2023-04-25129600 │
│ 2023-04-24129600 │
└────────────┴─────────┘

5 rows in set. Elapsed: 0.039 sec. Processed 5.25 million rows, 83.99 MB (134.61 million rows/s., 2.15 GB/s.)

显然,把所有字段放在单个表中,是一种强大的优化工具,包括:更好调优排序键和投影。但不要忘了需要更多的磁盘空间。

实时反规范化

前面示例在反规范化过程中需要等待,我们可以使用 物化视图实现实时填充:

CREATE MATERIALIZED VIEW log_users_rt (
  `dt` DateTime, `user_id` UInt64, `session_id` UInt64,
  `platform` String, `label` String, `url` String,
  `user_name` String, `user_category` String, `user_age` UInt8
)
ENGINE = MergeTree ORDER BY (dt, user_id, session_id) AS
SELECT l.*, u.name AS user_name, u.category AS user_category,
       u.age AS user_age
FROM log AS l INNER JOIN users AS u ON l.user_id = u.id

这里定义物化视图,让ClickHouse自动连接log和users表,填充连接查询结果至log_users_rt物化视图中。注意,现在ClickHouse在每次执行插入时日志时,需要join查询users,这会影响插入性能:

insert into log values(now(), 12,15,'aa', 'aa', 'http://baidu.com'),(now(), 12,15,'bb', 'bb', 'http://sina.com')

INSERT INTO log FORMAT Values

Query id: 8c3c8034-7c9e-460d-9f32-887754aea6ea

Ok.

2 rows in set. Elapsed: 0.533 sec.

插入耗时会更高,因此最好批量插入,且插入间隔周期更长。

使用MySQL外部表

更酷的事情,in查询可以使用MySQL表引擎,从而可以使用MySQL特性管理数据,如增加索引,更新或删除数据。

假设已经在MySQL的db数据库中创建了users表,结构保持一致,下面在ClickHouse中创建MySQL引擎表:

CREATE TABLE users_mysql
(`id` UInt64, `name` String, `category` String, `age` UInt8 )
ENGINE = MySQL('127.0.0.1:3306', 'db', 'users', 'usr', 'pwd')

现在使用MySQL外部表执行查询:

SELECT date(dt) AS day, count(*) FROM log
WHERE (user_id IN (
  SELECT id FROM users_mysql WHERE category = 'aa' AND age > 50
)) AND (dt > (NOW() - toIntervalYear(1)))
GROUP BY day ORDER BY day DESC LIMIT 5

注意,这里的嵌套查询在MySQL中执行,因此最好增加相应索引提升性能:

CREATE INDEX c_a ON users(category, age)

这里最好不要使用join查询,因为ClickHouse首先会从MySQL中加载数据,这仅为了数据过滤。

总结

ClickHouse中in查询大多数场景执行效果较好,另外反规范化也能提升性能。物化视图可以实现在插入阶段实时反规范化,但物化视图会增加磁盘空间、并影响插入性能。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值