ClickHouse使用过程中的一些查询优化(六)

  • 1 Prewhere 替代 where
    • 2 数据采样
      • 3 指定select字段查询与分区裁剪
        • 4 orderby结合 where、limit
          • 5 避免构建虚拟列
            • 6 uniqCombined 替代 distinct
              • 7 使用物化视图
                • 8 其他注意事项
                  • (1)查询熔断
                  • (2)关闭虚拟内存
                  • (3)配置 join_use_nulls
                  • (4)批量写入时先排序
                  • (5)关注 CPU
                • 1 准备表和数据
                  • 2 用 IN 代替 JOIN(强烈建议)
                    • 3 大小表 JOIN
                      • 4 注意谓词下推(版本差异)
                        • 5 分布式表使用 GLOBAL
                          • 6 使用字典表
                            • 7 提前过滤

                              1 本文简介

                              本文的意义是在使用过程中,对一些查询进行一些优化,使查询效率提升。
                              无论是在单表查询,还是在多表查询,或者是分布式表的查询。

                              2 单表查询优化

                              1 Prewhere 替代 where

                              • Prewhere 和 where 语句的作用相同,用来过滤数据。
                              • 不同之处在于 prewhere 只支持 MergeTree 族系列引擎的表首先会读取指定的列数据,来判断数据过滤,等待数据过滤 之后再读取 select 声明的列字段来补全其余属性。
                              • 当查询列明显多于筛选列时使用 Prewhere 可十倍提升查询性能,Prewhere 会自动优化 执行过滤阶段的数据读取方式,降低 io 操作。
                              • 在某些场合下,prewhere 语句比 where 语句处理的数据量更少性能更高。
                              #关闭 where 自动转 prewhere(默认情况下, where 条件会自动优化成 prewhere) set optimize_move_to_prewhere=0;
                              # 使用 where
                              select WatchID,
                                 JavaEnable,
                                 Title,
                                 GoodEvent,
                                 ... ...
                              from datasets.hits_v1 **where** UserID='3198390223272470366';
                              
                              # 使用 prewhere 关键字 
                              select WatchID,
                              	JavaEnable,
                              	Title,
                              	GoodEvent,
                              	... ...
                              from datasets.hits_v1 **prewhere** UserID='3198390223272470366';
                              

                              **默认情况,我们肯定不会关闭 where 自动优化成 prewhere,**但是某些场景即使开启优
                              化,也不会自动转换成prewhere,需要手动指定 prewhere:

                              1. ⚫ 使用常量表达式

                              2. ⚫ 使用默认值为alias类型的字段

                              3. ⚫ 包含了arrayJOIN,globalIn,globalNotIn或者indexHint的查询

                              4. ⚫ select查询的列字段和where的谓词相同

                                select UserID from datasets.hits_v1 where UserID='3198390223272470366'
                                
                              5. ⚫ 使用了主键字段

                                主键字段:order by 字段

                              2 数据采样

                              通过采样运算可极大提升数据分析的性能

                              SELECT 
                              	Title,
                              	count(*) AS PageViews
                              FROM hits_v1
                              SAMPLE 0.1 #代表采样 10%的数据,也可以是具体的条数 
                              WHERE CounterID =57
                              GROUP BY Title
                              ORDER BY PageViews DESC LIMIT 1000
                              

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

                              3 指定select字段查询与分区裁剪

                              数据量太大时应避免使用 select * 操作,查询的性能会与查询的字段大小和数量成线性
                              表换,字段越少,消耗的 io 资源越少,性能就会越高。

                              反例:

                              select * from datasets.hits_v1;
                              

                              正例:

                              select WatchID,
                                 JavaEnable,
                                 Title,
                                 GoodEvent,EventTime,
                                 EventDate,
                                 CounterID,
                                 ClientIP,
                                 ClientIP6,
                                 RegionID,
                                 UserID
                              from datasets.hits_v1;
                              

                              分区裁剪就是只读取需要的分区,在过滤条件中指定。也就是说:每次查询要在where后加上order by的字段进行查询,比如创建表时指定

                              partition by toYYYYMMDD(EventDate) order by (EventDate,UserID),那么在查询时where条件指定分区查询。比如:

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

                              4 orderby结合 where、limit

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

                              正例:

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

                              反例:

                              SELECT UserID,Age FROM hits_v1 ORDER BY Age DESC
                              

                              5 避免构建虚拟列

                              虚拟列:原始表不存在的字段,查询语句虚拟出来的字段

                              如非必须,不要在结果集上构建虚拟列,虚拟列非常消耗资源浪费性能,可以考虑在前端进行处理,或者在表中构造实际字段进行额外存储。
                              反例:

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

                              正例:

                              拿到 Income 和 Age 后,考虑在前端进行处理,或者在表中构造实际字段进行额外存储

                              SELECT Income,Age FROM datasets.hits_v1;
                              

                              6 uniqCombined 替代 distinct

                              性能可提升 10 倍以上,uniqCombined 底层采用类似 HyperLogLog 算法实现,**能接收 2% 左右的数据误差,可直接使用这种去重方式提升查询性能。**Count(distinct )会使用 uniqExact 精确去重。
                              不建议在千万级不同数据上执行 distinct 去重查询,改为近似去重 uniqCombined

                              反例:

                              select count(distinct userName) from hits_v1;
                              

                              正例:

                              SELECT uniqCombined(userName) from datasets.hits_v1
                              

                              7 使用物化视图

                              什么是物化视图请关注本博客,或者关注博主公众号

                              img

                              8 其他注意事项

                              (1)查询熔断

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

                              (2)关闭虚拟内存

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

                              (3)配置 join_use_nulls

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

                              (4)批量写入时先排序

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

                              (5)关注 CPU

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

                              3 多表查询优化

                              1 准备表和数据

                              #创建小表
                              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;
                              

                              2 用 IN 代替 JOIN(强烈建议)

                              当多表联查时,查询的数据仅从其中一张表出时,可考虑用 IN 操作而不是 JOIN

                              原因:无论是left join、right join或者是inner join,CK都会把join右边的表全部加载到内存中,然后和左边的表一一匹配,如果右表足够大,内存将会爆掉。

                              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;
                              

                              3 大小表 JOIN

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

                              (1)小表在右

                              select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b.CounterID;
                              

                              (2)大表在右

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

                              4 注意谓词下推(版本差异)

                              ClickHouse 在join 查询时不会主动发起谓词下推的操作,需要每个子查询提前完成过滤操作,需要注意的是,是否执行谓词下推,对性能影响差别很大(新版本中已经不存在此问 题,但是需要注意谓词的位置的不同依然有性能的差异)

                              #使用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;
                              

                              5 分布式表使用 GLOBAL

                              两张分布式表上的 IN 和 JOIN 之前必须加上 GLOBAL 关键字,右表只会在接收查询请求 的那个节点查询一次,并将其分发到其他节点上。如果不加GLOBAL 关键字的话,每个节点 都会单独发起一次对右表的查询,而右表又是分布式表,就导致右表一共会被查询 N2次(N 是该分布式表的分片数量),这就是查询放大,会带来很大开销。

                              img

                              如上图所示,

                              1. 红色箭头是使用global关键字的结果,也就是如果在分片1查询时会对分布式表B发起N次查询
                              2. 黄色箭头是未使用global关键字,一个箭头代表发起N次查询请求

                              6 使用字典表

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

                              7 提前过滤

                              通过增加逻辑过滤可以减少数据扫描,达到提高执行速度及降低内存消耗的目的

                              本文作者:好名字
                              原文链接:https://www.cuizb.top/myblog/article/1639839719
                              版权声明: 本博客所有文章除特别声明外,均采用 CC BY 3.0 CN协议进行许可。转载请署名作者且注明文章出处。

                              posted @   Java技术债务  阅读(437)  评论(0编辑  收藏  举报
                              相关博文:
                              阅读排行:
                              · 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
                              · C#/.NET/.NET Core优秀项目和框架2025年2月简报
                              · 一文读懂知识蒸馏
                              · Manus爆火,是硬核还是营销?
                              · 终于写完轮子一部分:tcp代理 了,记录一下
                              点击右上角即可分享
                              微信分享提示