头图

字节跳动基于ClickHouse优化实践之“多表关联查询”

更多技术交流、求职机会、试用福利,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群

相信大家都对大名鼎鼎的ClickHouse有一定的了解了,它强大的数据分析性能让人印象深刻。但在字节大量生产使用中,发现了ClickHouse依然存在了一定的限制。例如:

  • 缺少完整的upsert和delete操作
  • 多表关联查询能力弱
  • 集群规模较大时可用性下降(对字节尤其如此)
  • 没有资源隔离能力

因此,我们决定将ClickHouse能力进行全方位加强,打造一款更强大的数据分析平台。本篇将详细介绍我们是如何加强ClickHouse多表关联查询能力。

大宽表的局限

数据分析的发展历程,可以看作是不断追求分析效率和分析灵活的过程。分析效率是非常重要的,但是并不是需要无限提升的。1秒返回结果和1分钟返回结果的体验是天壤之别,但是0.1秒返回结果和1秒返回结果的差距就没那么大了。因此,在满足了一定时效的情况下,分析的灵活性就显得额外重要了。

起初,数据分析都采用了固定报表的形式,格式更新频率低,依赖定制化的开发,查询逻辑是写死的。对于业务和数据需求相对稳定、不会频繁变化的场景来说固定报表确实就足够了,但是以如今的视角来看,完全固定的查询逻辑不能充分发挥数据的价值,只有通过灵活的数据分析,才能帮助业务人员化被动为主动,探索各数据间的相关关系,快速找到问题背后的原因,极大地提升工作效率。

后面,基于预计算思想的cube建模方案被提出。通过将数据ETL加工后存储在cube中,保证领导和业务人员能够快速得到分析结果基础上,获得了一定的分析灵活性。不过由于维度固定,以及数据聚合后基本无法查询明细数据,依然无法满足Adhoc这类即席查询的场景需求。

近些年,以ClickHouse为代表的具备强大单表性能的查询引擎,带来了大宽表分析的风潮。所谓的大宽表,就是在数据加工的过程中,将多张表通过一些关联字段打平成一张宽表,通过一张表对外提供分析能力。基于ClickHouse单表性能支撑的大宽表模式,既能提升分析时效性又能提高数据查询和分析操作的灵活性,是目前非常流行的一种模式。

然而大宽表依然有它的局限性,具体有:

  • 生成每一张大宽表都需要数据开发人员不小的工作量,而且生成过程也需要一定的时间
  • 生成宽表会产生大量的数据冗余

刚才有提到,数据分析的发展历程可以看作是不断追求分析效率和分析灵活的过程,那么大宽表的下一个阶段呢?如果ClickHouse的多表关联查询能力足够强,是不是连“将数据打平成宽表”这个步骤也可以省略,只需要维护好对外服务的接口,任何业务人员的需求都现场直接关联查询就可以了呢?

如何强化多表关联查询能力的?ClickHouse 的执行模式相对比较简单,其基本查询模式分为 2 个阶段:
图片
ByteHouse 进行多表关联的复杂查询时,采用分 Stage 的方式,替换目前 ClickHouse的2阶段执行方式。将一个复杂的 Query 按照数据交换情况切分成多个 Stage,Stage 和 Stage 之间通过 exchange 完成数据的交换,单个 Stage 内不存在数据交换。Stage 间的数据交换主要有以下三种形式:

  • 按照单(多)个 key 进行 Shuffle
  • 由 1 个或者多个节点汇聚到一个节点 (我们称为 gather)
  • 同一份数据复制到多个节点(也称为 broadcast 或者说广播)
    单个 Stage 执行会继续复用 ClickHouse 的底层的执行方式。

按照不同的功能切分不同的模块,设计目标如下:

各个模块约定好接口,尽量减少彼此的依赖和耦合。一旦某个模块有变动不会影响别的模块,例如 Stage 生成逻辑的调整不影响调度的逻辑。

模块采用插件的架构,允许模块根据配置灵活支持不同的策略。

根据数据的规模和分布,ByteHouse支持了多种关联查询的实现,目前已经支持的有:

  • Shuffle Join,最通用的 Join
  • Broadcast Join,针对大表 Join 小表的场景,通过把右表广播到左表的所有 worker 节点来减少左表的传输
  • Colocate Join,针对左右表按照 Join key 保持相通分布的场景,减少左右表数据传输

Join 算子通常是 OLAP 引擎中最耗时的算子。如果想优化 Join 算子,可以有两种思路,一方面可以提升 Join 算子的性能,例如更好的 Hash Table 实现和 Hash 算法,以及更好的并行。另一方面可以尽可能减少参与 Join 计算的数据。

Runtime Filter 在一些场景特别是事实表 join 维度表的星型模型场景下会有比较大的效果。因为这种情况下通常事实表规模比较大,而大部分过滤条件都在维度表上,事实表可能要全量 join 维度表。Runtime Filter 的作用是通过在 Join 的 probe 端(就是左表)提前过滤掉那些不会命中 Join 的输入数据来大幅减少 Join 中的数据传输和计算,从而减少整体的执行时间。以下图为例:
image.png

改善后的效果以SSB 100G测试集为例,不把数据打成大宽表的情况下,分别使用 ClickHouse 22.2.3.1版本和ByteHouse 2.0.1版本,在相同硬件环境下进行测试。(无数据表示无法返回结果或超过60s)
图片
可以看到大多数测试中,ClickHouse都会发生报错无法返回结果的情况,而ByteHouse能够稳定的在1s内跑出结果。只看SSB的多表测试有些抽象,下面从两个具体的case来看一下优化后的效果:

Case1:Hash Join 右表为大表

经过优化后,query 执行时间从17.210s降低至1.749s。

lineorder 是一张大表,通过 shuffle 可以将大表数据按照 join key shuffle 到每个 worker 节点,减少了右表构建的压力。

SELECT
    sum(LO_REVENUE) - sum(LO_SUPPLYCOST) AS profit
FROM 
    customer
INNER JOIN
(
    SELECT
    LO_REVENUE,
    LO_SUPPLYCOST,
    LO_CUSTKEY
    from
    lineorder
    WHERE toYear(LO_ORDERDATE) = 1997 and toMonth(LO_ORDERDATE) = 1
) as lineorder
ON LO_CUSTKEY = C_CUSTKEY
WHERE C_REGION = 'AMERICA'

Case 2:5张表 Join(未开启runtime filter)

经优化后,query 执行时间从8.583s降低至4.464s。

所有的右表可同时开始数据读取和构建。为了和现有模式做对比,ByteHouse这里并没有开启 runtime filter,开启 runtime filter 后效果会更快。

SELECT                                                                                                                                              
    D_YEAR,                                                                                                                                         
    S_CITY,                                                                                                                                         
    P_BRAND,                                                                                                                                        
    sum(LO_REVENUE) - sum(LO_SUPPLYCOST) AS profit                                                                                                  
FROM ssb1000.lineorder                                                                                                                              
INNER JOIN                                                                                                                                   
(                                                                                                                                                   
    SELECT C_CUSTKEY                                                                                                                                
    FROM ssb1000.customer                                                                                                                           
    WHERE C_REGION = 'AMERICA'                                                                                                                      
) AS customer ON LO_CUSTKEY = C_CUSTKEY                                                                                                             
INNER JOIN                                                                                                                                   
(                                                                                                                                                   
    SELECT                                                                                                                                          
        D_DATEKEY,                                                                                                                                  
        D_YEAR                                                                                                                                      
    FROM date                                                                                                                             
    WHERE (D_YEAR = 1997) OR (D_YEAR = 1998)                                                                                                        
) AS dates ON LO_ORDERDATE = toDate(D_DATEKEY)                                                                                                      
INNER JOIN                                                                                                                                   
(                                                                                                                                                   
    SELECT                                                                                                                                          
        S_SUPPKEY,                                                                                                                                  
        S_CITY                                                                                                                                      
    FROM ssb1000.supplier                                                                                                                           
    WHERE S_NATION = 'UNITED STATES'                                                                                                                
) AS supplier ON LO_SUPPKEY = S_SUPPKEY                                                                                                             
INNER JOIN                                                                                                                                   
(                                                                                                                                                   
    SELECT                                                                                                                                          
        P_PARTKEY,                                                                                                                                  
        P_BRAND                                                                                                                                     
    FROM ssb1000.part                                                                                                                               
    WHERE P_CATEGORY = 'MFGR#14'                                                                                                                    
) AS part ON LO_PARTKEY = P_PARTKEY                                                                                                                 
GROUP BY                                                                                                                                            
    D_YEAR,                                                                                                                                         
    S_CITY,                                                                                                                                         
    P_BRAND                                                                                                                                         
ORDER BY                                                                                                                                            
    D_YEAR ASC,                                                                                                                                     
    S_CITY ASC,                                                                                                                                     
    P_BRAND ASC                                                                                                                                     
SETTINGS enable_distributed_stages = 1, exchange_source_pipeline_threads = 32      

经过多表关联查询能力的增强,ByteHouse能够更加全面的支撑各类业务,用户可以根据场景选择是否将数据打成大宽表,均能获得非常良好的分析体验。
之所以ByteHouse在多表关联场景表现如此出色,其中一大原因就是因为字节自研了查询优化器,弥补了社区ClickHouse的一大不足。

立即跳转火山引擎BytHouse官网了解详情!

347 声望
49 粉丝
0 条评论
推荐阅读
ByteHouse:基于 ClickHouse 的实时计算能力升级
ByteHouse 是火山引擎数智平台旗下云原生数据分析平台,为用户带来极速分析体验,能够支撑实时数据分析和海量离线数据分析;便捷的弹性扩缩容能力,极致的分析性能和丰富的企业级特性,助力客户数字化转型。

字节跳动数据平台

封面图
花了几个月时间把 MySQL 重新巩固了一遍,梳理了一篇几万字 “超硬核” 的保姆式学习教程!(持续更新中~)
MySQL 是最流行的关系型数据库管理系统,在 WEB 应用方面 MySQL 是最好的 RDBMS(Relational Database Management System:关系数据库管理系统)应用软件之一。

民工哥11阅读 1k

封面图
一次偶然机会发现的MySQL“负优化”
今天要讲的这件事和上述的两个sql有关,是数年前遇到的一个关于MySQL查询性能的问题。主要是最近刷到了一些关于MySQL查询性能的文章,大部分文章中讲到的都只是一些常见的索引失效场合,于是我回想起了当初被那个...

骑牛上青山8阅读 2.2k评论 2

程序员英语学习指南
动机为什么程序员要学习英语?工作:我们每天接触的代码都是英文的、包括很多技术文档也是英文的学习:最新最前沿的技术最开始都是只有English版本就业:学好英语让你的就业范围扩大到全球,而不只限于国内目标读...

九旬6阅读 618

又一款内存数据库横空出世,比 Redis 更强,性能直接飙升一倍!杀疯了
KeyDB是Redis的高性能分支,专注于多线程,内存效率和高吞吐量。除了多线程之外,KeyDB还具有仅在Redis Enterprise中可用的功能,例如Active Replication,FLASH存储支持以及一些根本不可用的功能,例如直接备份...

民工哥4阅读 674评论 1

封面图
Mysql索引覆盖
通常情况下,我们创建索引的时候只关注where条件,不过这只是索引优化的一个方向。优秀的索引设计应该纵观整个查询,而不仅仅是where条件部分,还应该关注查询所包含的列。索引确实是一种高效的查找数据方式,但...

京东云开发者2阅读 916

封面图
面试官:请说一下如何优化结构体的性能?
使用内存对齐机制优化结构体性能,妙啊!前言之前分享过2篇结构体文章:10秒改struct性能直接提升15%,产品姐姐都夸我好棒 和 Go语言空结构体这3种妙用,你知道吗? 得到了大家的好评。这篇继续分享进阶内容:结...

王中阳Go4阅读 1.6k评论 2

封面图
347 声望
49 粉丝
宣传栏