数据库内核那些事|细说PolarDB优化器查询变换 - join消除篇

简介: 数据库的查询优化器是整个系统的"大脑",一条SQL语句执行是否高效在不同的优化决策下可能会产生几个数量级的性能差异,因此优化器也是数据库系统中最为核心的组件和竞争力之一。阿里云瑶池旗下的云原生数据库PolarDB MySQL版作为领先的云原生数据库,希望能够应对广泛用户场景、承接各类用户负载,助力企业数据业务持续在线、数据价值不断放大,因此对优化器能力的打磨是必须要做的工作之一。本系列将从PolarDB for MySQL的查询变换能力开始,介绍我们在这个优化器方向上逐步积累的一些工作。

查询优化概念

查询变换的概念非常简单,就是基于关系代数的等价变换规则,将查询的一种形式转换为另外一种等价但更为高效的形式,通过这种转换,既可以保证查询结果的正确性,又可以提升查询的执行效率。


优化器可以完成的变换非常多,如果将每一种变换视为一种改写规则的话,几百个规则也是比较常见的。其中有些变换(规则),总是可以让查询变得更为高效,我们称其为启发式变换,但有些则不一定,需要基于代价来决定。


本篇文章将介绍PolarDB实现的一个启发式查询变换——join消除。


该功能在PolarDB for MySQL 8.0.2.2.9版本上线。


join消除

join可以说是所有SQL语句中最为常见的算子,当然也是最为耗时的算子,一个join操作,需要将作为两边的关系(表),根据join条件中指定的连接列,拼接到一起向上层算子输出,笼统的来说,这是一个具有M * N运算复杂度的操作,和scan/aggregation等相比要高出一个因数,因此如何实现好的join算法、如何决定最好的join执行顺序,是每个数据库系统不得不面对的核心问题。


那么从另外一个角度出发,是不是可以基于SQL查询中的某些特定语义,从一开始就想办法消除掉不必要的join操作呢?例如如下这种非常简单的情况:

create table t1 (id int, PRIMARY KEY(id));
create table t2 (id int, t1_id int,
                 constraint `t1_id_fk` foreign key (`t1_id`) references `t1` (`id`)
                 );
select t2.id from t2 join t1 on t2.t1_id = t1.id;


可以看到t2的t1_id列是t1 id列的外键,因此对t2的每一行,一定有且只有一行t1的数据可以和t2 join上,同时整个查询最终并不需要t1表的数据,查询可以简化为:


select t2.id from t2;

这避免了对t1表的访问和大量行的连接操作,可以想见会有非常明显的性能提升。


PolarDB实现

PolarDB的优化器基于MySQL,原始是没有任何join消除能力的,在线上值班过程中,遇到有客户从MariaDB迁移过来后发现查询性能回退非常多的情况。排查后发现,MariaDB是具有join消除能力的,客户的查询从原来的3表left join变为了单表,执行时间大大缩短。


为此我们调研了MariaDB的实现,觉得其原理是有参考性的,但实现中所能覆盖的场景还不全面,在一些简单情况以及复杂嵌套的场景下(semi-join),支持的还很不够,因此基于MySQL 8.0的codebase做了自己的实现。


基本原理

基本的思路并不复杂,考虑如下这个join


outer_table LEFT JOIN (inner_table) ON condition(outer_table,inner_table)


由于是LEFT JOIN,外表的数据是不会丢失的,如果同时可以保证:


1. 对外表的任一行,内表能匹配join条件的行数有且仅有一行;

2. 在LEFT JOIN以外,不再有其他地方需要引用内表的数据。


则这个LEFT JOIN就可以安全的消除掉。


如何保证这种唯一性呢?第一个想到的自然就是唯一/主键索引,如果内表的join列是唯一索引列,自然是满足输出一行这个要求。


但实际的查询不可能总是这么简单,我们可以考虑如下几种情况:


  • 唯一索引包含多列?
  • inner table包含多张表?
  • inner table中包含新的left join?
  • inner table本身是个derived table?


看似简单的join消除问题一下子就变得复杂,但我们可以通过逐步分解,通过判定每一个子问题来完成是否可消除的判断:


  • 一个LEFT JOIN的内部(单表/多表),只有当所有表都保证唯一输出一行时,整个LEFT JOIN才能消除;
  • 对内侧的每个单表,当其join条件中涉及的所有列,是某个唯一索引的超集时,该表才能保证输出一行;
  • 如果内侧再次包含有LEFT JOIN,则要先深度递归进去,判断这个内侧的LEFT JOIN是否可以消除,消除后再返回来考察外层;
  • 如果内层包含derived table且derived table中包含group by,则从外层来看,group by列也就是derived table的主键列。

遵循以上的思路依次处理每个子问题,就可以实现对各种复杂场景的可消除性的判定。具体算法和代码这里就先略过了。


看下上面几个问题的具体效果:


create table t1 (a int);
create table t2 (a int primary key, b int);
create table t3 (a int primary key, b int);
1. inner table包含多张表?
select t1.a from t1 left join (t2 join t3) on t2.a=t1.a and t3.a=t1.a;
=>
select t1.a from t1;
2. inner table中包含新的left join?
select t1.* from t1 left join (t2 left join t3 on t3.a=t2.b) on t2.a=t1.a;
=>
select t1.* from t1;
3. inner table本身是个derived table?
select t1.* from t1 left join
(
  select t2.b as v2b, count(*) as v2c
  from t2 left join t3 on t3.a=t2.b
  group by t2.b
) v2
on v2.v2b=t1.a;
=>
select t1.* from t1;


当然各种其他场景还有很多,但只要遵循前面提到的几个判定原则,就都可以逐一的完成消除。


与PostgreSQL的对比


PG在计算层的能力一直是其比较引以为傲的点,其统计信息和代价模型是非常优秀的,加上更完备的join ordering算法,确实可以生成较高质量的执行计划。


不过在查询变换方面,Postgres的能力也只能说相当一般(有机会后续会写文章介绍PG的优化器),它也实现了left join消除的功能,基本思路与PolarDB的一致,基于内表的唯一性判定,但其支持的场景就更为简单了:


只能支持left join的内侧是一个base relation,或者是一个subquery;

这严重限制了join消除应用的范围,不过好的一点是,当内侧是个subquery(derived table)时,它不仅支持基于group by列的唯一性检查,对于distinct / set operation,都有相应的判断机制,具体实现可参见:


query_is_distinct_for(Query *query, List *colnos, List *opids)


这个函数,不过其思路和实现都非常简单,PolarDB后续也会类似去扩展下。


与MariaDB的对比

我们也针对各种场景和MariaDB做了一些对比,发现在对一些场景的支持上,MariaDB的策略比较粗糙或不太合理:

对semi-join的处理


在MariaDB中,如果一个子查询在LEFT JOIN的ON条件中,就直接阻止了它转为semi-join,目的是为了让它仍然保留为一个谓词条件,从而在join消除的逻辑中避免对semijoin的复杂处理,但很明显这个存在误伤:如果这个LEFT JOIN本身无法被消除,semi-join岂不是也不能用了?


仍然沿用上面t0/t1/t2/t3的schema:


EXPLAIN SELECT count(*)
FROM t1
  LEFT JOIN (t2
    LEFT JOIN t3
    ON t2.b = t3.b
      AND EXISTS (
        SELECT t0.a
        FROM t0
        WHERE t0.a = t3.b
      )
    ) ON t1.a = t2.a;


这里由于t2.b = t3.b这样的join条件,t3.b不是唯一键,这个查询是无法消除join的,而MariaDB的执行计划如下:


+------+--------------------+-------+--------+---------------+---------+---------+-----------+------+-------------+
| id   | select_type        | table | type   | possible_keys | key     | key_len | ref       | rows | Extra       |
+------+--------------------+-------+--------+---------------+---------+---------+-----------+------+-------------+
|    1 | PRIMARY            | t1    | ALL    | NULL          | NULL    | NULL    | NULL      | 4    |             |
|    1 | PRIMARY            | t2    | eq_ref | PRIMARY       | PRIMARY | 4       | test.t1.a | 1    | Using where |
|    1 | PRIMARY            | t3    | ALL    | NULL          | NULL    | NULL    | NULL      | 2    | Using where |
|    2 | DEPENDENT SUBQUERY | t0    | ALL    | NULL          | NULL    | NULL    | NULL      | 4    | Using where |
+------+--------------------+-------+--------+---------------+---------+---------+-----------+------+-------------+


可以看到t0的相关子查询选择了最原始的执行方式,如果t0表的数据量大,性能会非常糟糕,而PolarDB仍然支持:


+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------+------+----------+-------------+
| id | select_type  | table       | partitions | type   | possible_keys       | key                 | key_len | ref          | rows | filtered | Extra       |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------+------+----------+-------------+
|  1 | SIMPLE       | t1          | NULL       | ALL    | NULL                | NULL                | NULL    | NULL         |    4 |   100.00 | NULL        |
|  1 | SIMPLE       | t2          | NULL       | ALL    | PRIMARY             | NULL                | NULL    | NULL         |    2 |   100.00 | Using where |
|  1 | SIMPLE       | t3          | NULL       | ALL    | NULL                | NULL                | NULL    | NULL         |    2 |   100.00 | Using where |
|  1 | SIMPLE       | <subquery2> | NULL       | eq_ref | <auto_distinct_key> | <auto_distinct_key> | 5       | je_test.t3.b |    1 |   100.00 | Using where |
|  2 | MATERIALIZED | t0          | NULL       | ALL    | NULL                | NULL                | NULL    | NULL         |    4 |   100.00 | NULL        |
+----+--------------+-------------+------------+--------+------------------


t0还是通过semi-join MATERIALIZATION的方式来实现,效率会高很多。


对序列LEFT JOIN的处理


即使在一些简单场景下,MariaDB的处理也不完备:


EXPLAIN SELECT count(*)
FROM t1
  LEFT JOIN t2 ON t1.a = t2.a
  LEFT JOIN t3 ON t2.b = t3.a;


MariaDB由于算法和实现的限制,效果如下:


+------+-------------+-------+--------+---------------+---------+---------+-----------+------+-------------+
| id   | select_type | table | type   | possible_keys | key     | key_len | ref       | rows | Extra       |
+------+-------------+-------+--------+---------------+---------+---------+-----------+------+-------------+
|    1 | SIMPLE      | t1    | ALL    | NULL          | NULL    | NULL    | NULL      | 4    |             |
|    1 | SIMPLE      | t2    | eq_ref | PRIMARY       | PRIMARY | 4       | test.t1.a | 1    | Using where |
+------+-------------+-------+--------+---------------+---------+---------+-----------+------+-------------+


在这样一个简单查询中,很明显t2/t3都是可以消除掉的,但MariaDB竟然只能处理t3表的join消除。


PolarDB实现的则更为彻底:


+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | t1    | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    4 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+


性能提升


join的消除是一个必然产生收益的启发式变换,根据表的数据量、访问方式的不同,产生的性能差异可能千差万别,但一般来说,性能都会有很大提升,这里就用一个线上客户的实际查询做个对比:


SELECT count(*)
FROM `shop_customer` `sc`
  LEFT JOIN `car` `ca` ON `sc`.`car_id` = `ca`.`id`
  LEFT JOIN `company` `co` ON `sc`.`company_id` = `co`.`id`;
=>
SELECT count(*) FROM `shop_customer` `sc`;

很明显最后可以变成单表的查询,在消除前,执行时间是7.5s,消除后是0.1s,提升了75倍。


比上述效果更凸显的例子比比皆是,尤其是内层包含多张表的嵌套时。总体来说,join的开销通常情况下都比较大,能够消除都会有明显提升。


总结


我们目前还在增加很多更先进的查询变换能力,毕竟MySQL原生可以支持的还太少了,但这也是逐步补充的过程,需要针对线上的客户场景和实际需求作为素材不断积累。


在这个过程中我们遇到了3个最基础的问题:

1. MySQL的各个原有变换,加的都比较"随意",其自身和原有处理流程的耦合导致增加新变换很困难;

2. 变换执行时机不合理,不同变换之间是存在一定前后依赖关系的,这种关系并没有很好的被利用;

3. 某些变换并不一定带来收益,需要基于统计信息 + 代价来做决定,这种决定MySQL是完全不支持的,变了就是变了,无论效果好坏。


为了解决这3个基本问题,团队在做的一个非常重要和复杂的事情,就是重构MySQL的查询变换流程:

1. 解耦原有的resolve + transform交错的流程,把变换作为单独的步骤,以"规则"为单位,各自独立完成,这样变换就可以通过添加新规则而不断独立扩展;

2. 在基于规则的抽象后,引用枚举框架来枚举不同变换(规则)间的先后顺序,通过反复迭代也可使得相互依赖的规则都可以按照更优的顺序被执行;

3. 实现CBO模块的可重入+可重用能力,这样可以基于代价来决定是否执行变换。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
6月前
|
人工智能 安全 机器人
无代码革命:10分钟打造企业专属数据库查询AI机器人
随着数字化转型加速,企业对高效智能交互解决方案的需求日益增长。阿里云AppFlow推出的AI助手产品,借助创新网页集成技术,助力企业打造专业数据库查询助手。本文详细介绍通过三步流程将AI助手转化为数据库交互工具的核心优势与操作指南,包括全场景适配、智能渲染引擎及零代码配置等三大技术突破。同时提供Web集成与企业微信集成方案,帮助企业实现便捷部署与安全管理,提升内外部用户体验。
670 12
无代码革命:10分钟打造企业专属数据库查询AI机器人
|
8月前
|
Cloud Native 关系型数据库 分布式数据库
|
8月前
|
并行计算 关系型数据库 MySQL
如何用 esProc 将数据库表转储提速查询
当数据库查询因数据量大或繁忙变慢时,可借助 esProc 将数据导出为文件进行计算,大幅提升性能。以 MySQL 的 3000 万行订单数据为例,两个典型查询分别耗时 17.69s 和 63.22s。使用 esProc 转储为二进制行存文件 (btx) 或列存文件 (ctx),结合游标过滤与并行计算,性能显著提升。例如,ctx 并行计算将原查询时间缩短至 0.566s,TopN 运算提速达 30 倍。esProc 的简洁语法和高效文件格式,特别适合历史数据的复杂分析场景。
|
7月前
|
存储 算法 Java
实现不同数据库的表间的 JOIN 运算的极简方法
跨库计算是数据分析中的常见难题,尤其涉及多数据库系统时,表间 JOIN 操作复杂度显著提升。esProc 提供了一种高效解决方案,能够简化跨库 JOIN 的实现。例如,在车辆管理、交管和公民信息系统中,通过 esProc 可轻松完成如下任务:按城市统计有车公民事件数量、找出近一年获表彰的车主信息,以及按年份和品牌统计车辆违章次数。esProc 支持不同关联场景(如维表关联与主子表关联)的优化算法,如内存索引、游标处理和有序归并,从而大幅提升编码和运算效率。无论是同构还是异构数据源,esProc 均能灵活应对,为复杂数据分析提供强大支持。
|
8月前
|
监控 数据库
【YashanDB 知识库】ycm 托管数据库时报错 OM host ip:127.0.0.1 is not support join to YCM
在托管数据库时,若 OM 的 IP 被设置为 127.0.0.1,将导致无法托管至 YCM,并使数据库失去监控。此问题源于安装时修改了 OM 的监听 IP。解决方法包括:将 OM 的 IP 修改为本机实际 IP 或 0.0.0.0,同时更新 env 文件及 yasom 后台数据库中的相关配置。经验总结指出,应避免非必要的后台 IP 修改,且数据库安装需遵循规范,不使用仅限本机访问的 IP(如 127.0.0.1)。
|
9月前
|
SQL 关系型数据库 MySQL
如何优化SQL查询以提高数据库性能?
这篇文章以生动的比喻介绍了优化SQL查询的重要性及方法。它首先将未优化的SQL查询比作在自助餐厅贪多嚼不烂的行为,强调了只获取必要数据的必要性。接着,文章详细讲解了四种优化策略:**精简选择**(避免使用`SELECT *`)、**专业筛选**(利用`WHERE`缩小范围)、**高效联接**(索引和限制数据量)以及**使用索引**(加速搜索)。此外,还探讨了如何避免N+1查询问题、使用分页限制结果、理解执行计划以及定期维护数据库健康。通过这些技巧,可以显著提升数据库性能,让查询更高效流畅。
|
8月前
|
关系型数据库 分布式数据库 数据库
一库多能:阿里云PolarDB三大引擎、四种输出形态,覆盖企业数据库全场景
PolarDB是阿里云自研的新一代云原生数据库,提供极致弹性、高性能和海量存储。它包含三个版本:PolarDB-M(兼容MySQL)、PolarDB-PG(兼容PostgreSQL及Oracle语法)和PolarDB-X(分布式数据库)。支持公有云、专有云、DBStack及轻量版等多种形态,满足不同场景需求。2021年,PolarDB-PG与PolarDB-X开源,内核与商业版一致,推动国产数据库生态发展,同时兼容主流国产操作系统与芯片,获得权威安全认证。
|
5月前
|
存储 关系型数据库 分布式数据库
喜报|阿里云PolarDB数据库(分布式版)荣获国内首台(套)产品奖项
阿里云PolarDB数据库管理软件(分布式版)荣获「2024年度国内首版次软件」称号,并跻身《2024年度浙江省首台(套)推广应用典型案例》。

相关产品

  • 云原生数据库 PolarDB