Clickhouse学习笔记(10)—— 查询优化

单表查询

Prewhere 替代 where

prewhere与where相比,在过滤数据的时候会首先读取指定的列数据,来判断数据过滤,等待数据过滤之后再读取 select 声明的列字段来补全其余属性

简单来说就是先过滤再查询,而where过滤是先查询出对应的列字段来,再根据过滤条件过滤数据;因此对比之下,使用prewhere过滤处理的数据量要更少,效率也就更高;

但需注意,prewhere只可适用于mergetree引擎

默认情况下,where 条件会自动优化成 prewhere,通过参数optimize_move_to_prewhere来控制;

参数值为1表示使用怕prewhere


使用where和prewhere的对比:

①使用where:

关闭prewhere自动优化之后,执行语句:

select WatchID, 
 JavaEnable, 
 Title, 
 GoodEvent, 
 EventTime, 
 EventDate, 
 CounterID, 
 ClientIP, 
 ClientIP6, 
 RegionID, 
 UserID, 
 CounterClass, 
 OS, 
 UserAgent, 
 URL, 
 Referer, 
 URLDomain, 
 RefererDomain, 
 Refresh, 
 IsRobot, 
 RefererCategories, 
 URLCategories, 
 URLRegions, 
 RefererRegions, 
 ResolutionWidth, 
 ResolutionHeight, 
 ResolutionDepth, 
 FlashMajor, 
 FlashMinor, 
 FlashMinor2
from datasets.hits_v1 where UserID='3198390223272470366';

结果如下,耗时2.467s

②使用prewhere:

开启优化后执行相同语句,结果如下,耗时仅用0.172s,可以看到效率提升很明显


在一些场景下,自动优化不生效,如下:

  • 使用常量表达式
  • 使用默认值为 alias 类型的字段
  • 包含了 arrayJOIN,globalIn,globalNotIn 或者 indexHint 的查询
  • select 查询的列字段和 where 的谓词相同
  • 使用了主键字段

例如select 查询的列字段和 where 的谓词相同的情况:

explain syntax select UserID from hits_v1 where UserID='3198390223272470366';

结果如下:

Clickhouse学习笔记(10)—— 查询优化_第1张图片

可以看到并没有进行优化;

因此我们在使用prewhere的时候,最好直接在sql中使用prewhere,而不是依赖于自动优化;

例如:

explain syntax select UserID from hits_v1 prewhere UserID='3198390223272470366';

结果如下,是可以生效的:

Clickhouse学习笔记(10)—— 查询优化_第2张图片

数据采样 SAMPLE

示例:

SELECT Title,count(*) AS PageViews 
FROM hits_v1
SAMPLE 0.1
WHERE CounterID =57
GROUP BY Title
ORDER BY PageViews DESC LIMIT 1000

采样语法:SAMPLE 样本比例(1代表100%)

SAMPLE Clause | ClickHouse Docs

注意:采样修饰符只有在 MergeTree engine 表中才有效,且在创建表时需要指定采样策略

这里的采样策略是在建表时指定的:

Clickhouse学习笔记(10)—— 查询优化_第3张图片

表示根据UserID字段的哈希值进行采样

列裁剪与分区裁剪

列裁剪:数据量太大时应避免使用 select * 操作

分区裁剪:就是只读取需要的分区,在过滤条件中指定

例如:

select WatchID, 
 JavaEnable, 
 Title, 
 GoodEvent, 
 EventTime, 
 EventDate, 
 CounterID, 
 ClientIP, 
 ClientIP6, 
 RegionID, 
 UserID
from datasets.hits_v1
where EventDate='2014-03-23';

这里的EventDate就是分区字段

orderby 结合 where、limit

千万以上数据集进行 order by 查询时需要搭配 where 条件和 limit 语句一起使用

对比:

①仅使用order by:

SELECT UserID,Age
FROM hits_v1 
ORDER BY Age DESC;

②使用where和limit:

SELECT UserID,Age
FROM hits_v1 
WHERE CounterID=57
ORDER BY Age DESC LIMIT 1000;

可以看到处理的数据量大幅降低,效率提示十分明显;

③仅使用limit:

SELECT UserID,Age
FROM hits_v1 
ORDER BY Age DESC LIMIT 1000;

处理的数据量虽然没有降低,但是效率提升也比较明显

避免构建虚拟列

什么是虚拟列:在select查询的字段中,非建表时指定的字段即为虚拟列

例如:

SELECT Income,Age,Income/Age as IncRate FROM datasets.hits_v1;

这里的Income/Age就是虚拟列,由于每次查一条语句都需要进行一次计算,所以十分影响性能

使用虚拟列:

没有虚拟列:

uniqCombined 替代 distinct

uniqCombined 底层采用类似 HyperLogLog 算法实现,能接收 2%左右的数据误差,可直接使用这种去重方式提升查询性能

而Count(distinct)会使用 uniqExact精确去重


示例:

explain syntax select count(distinct UserID) from hits_v1;

Clickhouse学习笔记(10)—— 查询优化_第4张图片


对UserID进行去重:

使用uniqCombined

SELECT uniqCombined(UserID) from datasets.hits_v1;

使用uniqExact

select count(distinct UserID) from hits_v1;

其他

查询熔断

为了避免因个别慢查询引起的服务雪崩的问题,除了可以为单个查询设置超时以外,还可以配置周期熔断,在一个查询周期内,如果用户频繁进行慢查询操作超出规定阈值后将无法继续进行查询操作

关闭虚拟内存

物理内存和虚拟内存的数据交换,会导致查询变慢,资源允许的情况下关闭虚拟内存

配置 join_use_nulls

为每一个账户添加 join_use_nulls 配置,左表中的一条记录在右表中不存在,右表的相应字段会返回该字段相应数据类型的默认值,而不是标准 SQL 中的 Null 值

批量写入时先排序

批量写入数据时,必须控制每个批次的数据中涉及到的分区的数量,在写入之前最好对需要导入的数据进行排序。无序的数据或者涉及的分区太多,会导致 ClickHouse 无法及时对新导入的数据进行合并,从而影响查询性能

关注 CPU

cpu 一般在 50%左右会出现查询波动,达到 70%会出现大范围的查询超时,cpu 是最关键的指标,要非常关注

使用top命令查看CPU使用情况

参数含义如下:

%us:表示用户空间程序的cpu使用率(没有通过nice调度)

%sy:表示系统空间的cpu使用率,主要是内核程序。

%ni:表示用户空间且通过nice调度过的程序的cpu使用率。

%id:空闲cpu

%wa:cpu运行时在等待io的时间

%hi:cpu处理硬中断的数量

%si:cpu处理软中断的数量

%st:被虚拟机偷走的cpu

注:99.0 id,表示空闲CPU,即CPU未使用率,100%-99.0%=1%,即系统的cpu使用率为1%。

多表关联

准备数据

由于clickhouse的join操作效率很低,因此根据官方提供的数据集准备两张小表

#创建小表

CREATE TABLE visits_v2 
ENGINE = CollapsingMergeTree(Sign)
PARTITION BY toYYYYMM(StartDate)
ORDER BY (CounterID, StartDate, intHash32(UserID), VisitID)
SAMPLE BY intHash32(UserID)
SETTINGS index_granularity = 8192
as select * from visits_v1 limit 10000;

#创建 join 结果表:避免控制台疯狂打印数据
CREATE TABLE hits_v2 
ENGINE = MergeTree()
PARTITION BY toYYYYMM(EventDate)
ORDER BY (CounterID, EventDate, intHash32(UserID))
SAMPLE BY intHash32(UserID)
SETTINGS index_granularity = 8192
as select * from hits_v1 where 1=0;

这里的visits_v2是一张小表,用于join测试

hits_v2是一张结果表,用于保存join之后的数据;

注意where 1=0这个条件,保留了hits_v1这张表的结构,但里面没有数据

用 IN 代替 JOIN

①使用IN:

insert into hits_v2
select a.* from hits_v1 a where a. CounterID in (select CounterID from visits_v1);

②使用JOIN:

insert into table hits_v2
select a.* from hits_v1 a left join visits_v1 b on a. CounterID=b. CounterID;

对比可知使用IN效率提升很多;

同时在执行join的时候观察CPU使用情况:

发现CPU的占用率接近80%

小表在右原则

多表 join 时要满足小表在右的原则,右表关联时被加载到内存中与左表进行比较,ClickHouse 中无论是 Left join 、Right join 还是 Inner join 永远都是拿着右表中的每一条记录到左表中查找该记录是否存在,所以右表必须是小表


对比以下:

①小表在右

insert into table hits_v2
select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b. CounterID;

②大表在右

insert into table hits_v2
select a.* from visits_v2 b left join hits_v1 a on a. CounterID=b. CounterID;

执行没几秒就内存超限了:

谓词下推(join查询无法自动实现)

ClickHouse 在 join 查询时不会主动发起谓词下推的操作,需要每个子查询提前完成过滤操作

需要注意的是,是否执行谓词下推,对性能影响差别很大

子查询不提前过滤:

insert into hits_v2
select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b. CounterID
where a.EventDate = '2014-03-17';

子查询提前过滤:

insert into hits_v2
select a.* from (
 select * from 
 hits_v1 
 where EventDate = '2014-03-17'
) a left join visits_v2 b on a. CounterID=b. CounterID;

可以看出效率上有一定差别


所谓谓词下推,就是在join之前根据条件进行过滤,从而筛选出一部分数据来,减少进行join的数据量,从而提升join的效率

查看上面两个select语句的优化计划,分别如下:

Clickhouse学习笔记(10)—— 查询优化_第5张图片

Clickhouse学习笔记(10)—— 查询优化_第6张图片

可以看到,如果不手动通过子查询提前完成过滤,是不会自动进行谓词下推的

分布式表使用GLOBAL关键字

两张分布式表上的 IN 和 JOIN 之前必须加上 GLOBAL 关键字,这样的话,右表只会在接收查询请求的那个节点查询一次,并将其分发到其他节点上

如果不加 GLOBAL 关键字的话,每个节点都会单独发起一次对右表的查询,而右表又是分布式表,就导致右表一共会被查询 N²次(N是该分布式表的分片数量),这就是查询放大,会带来很大开销


官网相关描述:

详细用法可见:IN Operators | ClickHouse Docs

字典表

将一些需要关联分析的业务创建成字典表进行 join 操作,前提是字典表不宜太大,因为字典表会常驻内存

参考:Dictionaries | ClickHouse Docs

你可能感兴趣的:(数据库,大数据,clickhouse,学习,笔记,大数据,数据库,服务器)