GreatSQL优化技巧:半连接(semijoin)优化

GreatSQL vs MySQL性能测试来了,速围观~ 无论ibp(innodb_buffer_pool_size)是否充足,MySQL的性能都远不如GreatSQL。MySQL的性能平均约为GreatSQL的70%(最高84.5%,最低61.7%)。在ibp充分的情况下,随着并发数的增加,MySQL并没有表现出该有的性能提升,反倒掉头向下,可见还是不够稳定。在ibp不够的情况下,GreatSQL开启thread pool性能有所提升;当ibp充足的情况下,区别就不大了。MySQL vs GreatSQL性能数据对比各数据库版本分别为。 阅读详情

何为半连接?

半连接是在GreatSQL内部采用的一种执行子查询的方式,semi join不是语法关键字,不能像使用inner join、left join、right join这种语法关键字一样提供给用户来编写SQL语句。

两个表t1表和t2表进行半连接的含义是:对于t1表的某条记录来说,我们只关心在t2表中是否存在与之匹配的记录,而不关心有多少条记录与之匹配,最终的结果集中只保留t1表的记录。

前面文章也提到过,含in、exists子查询的语句通常会采用半连接方式执行查询,但这不绝对,也有一些情况不适用半连接。比如:

(1)外查询的where子句中,存在其他搜索条件使用OR操作符与IN子查询的条件连接起来

(2)IN子查询位于Select子句中

(3)IN子查询中含有union的情况

(4)IN子查询中含group by、having或聚合函数的情况

GreatSQL执行半连接的优化策略

本文实验使用数据库版本为GreatSQL 8.0.32-25。

创建两张实验表来说明。

greatsql> create table t1(
c1 varchar(30),
c2 int
);
greatsql> create table t2(
id int primary key,
c1 varchar(30),
key idx_c1(c1)
);
--插入几条测试数据
greatsql> insert into t1 values('a',1);
greatsql> insert into t1 values('b',3);
greatsql> insert into t1 values('a',5);
greatsql> insert into t1 values('c',7);
greatsql> insert into t1 values('d',9);
greatsql> insert into t2 values(1,'a');
greatsql> insert into t2 values(2,'a');
greatsql> insert into t2 values(3,'b');
greatsql> insert into t2 values(4,'b');
greatsql> insert into t2 values(5,'c');
greatsql> insert into t2 values(6,'b');

GreatSQL执行半连接的方式大致有以下5种:

1.Table pullout(子查询中的表上拉)

当子查询的查询列表处只有主键或者唯一索引列时,可以直接把子查询中的表上拉到外层查询的FROM子句中,并把子查询的查询条件合并到外层查询的搜索条件中。所以选择这种方式是有先决条件的,子查询的查询列表处必须只有主键或唯一索引列。有没有选择这种方式,可以通过执行explain展示计划后,使用show warnings命令查看优化器改写后的语句。

例如下面这个语句:

select * from t1 where c2 in (select id from t2 where t2.c1='b');

这个语句种子查询的id列是t2表的主键列,满足这种方式的先决条件,看一下执行计划。

greatsql> explain select * from t1 where c2 in (select id from t2 where t2.c1='b');
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys  | key    | key_len | ref   | rows | filtered | Extra                                      |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t2  | NULL       | ref  | PRIMARY,idx_c1 | idx_c1 | 123     | const |    3 |   100.00 | Using index                                |
|  1 | SIMPLE      | t1  | NULL       | ALL  | NULL           | NULL   | NULL    | NULL  |    4 |    25.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.00 sec)

greatsql> show warnings;
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                            |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t2` join `test`.`t1` where ((`test`.`t1`.`c2` = `test`.`t2`.`id`) and (`test`.`t2`.`c1` = 'b')) |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)

从warning信息可以看出,优化器改执行连接方式是,t1表与t2表通过内连接来关联,原子查询内部t2表的过滤条件放到了整个语句where条件的后面,原语句与优化器执行的语句之所以等价,是因为子查询的查询列id列是主键列,不会有重复值,跟外表t1使用inner join连接后,不会造成关联后结果集数据量的放大。一般情况下子查询的查询列表处只有主键或者唯一索引列时都会转化为这种方式来执行。对于这种业务,无论开发者怎么编写SQL,使用inner join 也好,exists也好,最后优化器执行方式可能都是一样的。

可以看一下将原语句改造为inner join 与 exists语句的执行计划,是不是都是一样的。

greatsql> explain select * from t1 where exists (select 1 from t2 where t2.id=t1.c2 and t2.c1='b');
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys  | key    | key_len | ref   | rows | filtered | Extra                                      |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t2  | NULL       | ref  | PRIMARY,idx_c1 | idx_c1 | 123     | const |    3 |   100.00 | Using index                                |
|  1 | SIMPLE      | t1  | NULL       | ALL  | NULL           | NULL   | NULL    | NULL  |    4 |    25.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
2 rows in set, 2 warnings (0.00 sec)

greatsql> show warnings;
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                            |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1276 | Field or reference 'test.t1.c2' of SELECT #2 was resolved in SELECT #1                                                                                                                           |
| Note  | 1003 | /* select#1 */ select `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t2` join `test`.`t1` where ((`test`.`t1`.`c2` = `test`.`t2`.`id`) and (`test`.`t2`.`c1` = 'b')) |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

greatsql> explain select t1.* from t1 inner join t2 on t1.c2=t2.id where t2.c1='b';
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys  | key    | key_len | ref   | rows | filtered | Extra                                      |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t2  | NULL       | ref  | PRIMARY,idx_c1 | idx_c1 | 123     | const |    3 |   100.00 | Using index                                |
|  1 | SIMPLE      | t1  | NULL       | ALL  | NULL           | NULL   | NULL    | NULL  |    4 |    25.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.01 sec)

greatsql> show warnings;
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                            |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t1` join `test`.`t2` where ((`test`.`t1`.`c2` = `test`.`t2`.`id`) and (`test`.`t2`.`c1` = 'b')) |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

这种执行方式本质上已经转换为内连接了。

2.FirstMatch(首次匹配)

这种方式先取外层查询的一条记录,到子查询的表中寻找符合匹配条件的记录,如果能找到一条,则将外层查询的记录放入到最终结果集中并且停止查找匹配更多的记录,如果找不到,则把该外层查询的记录丢弃掉,然后再开始取下一条外层查询中的记录,这个过程一直持续到外层查询获取不到记录为止。

看一个简单语句的执行计划

select * from t1 where c1 in (select c1 from t2);
greatsql> explain select * from t1 where c1 in (select c1 from t2);
+----+-------------+-------+------------+------+---------------+--------+---------+--------------+------+----------+-------------------------------+
| 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 | Using where                   |
|  1 | SIMPLE      | t2  | NULL       | ref  | idx_c1        | idx_c1 | 123     | test.t1.c1 |    2 |   100.00 | Using index; FirstMatch(t1) |
+----+-------------+-------+------------+------+---------------+--------+---------+--------------+------+----------+-------------------------------+
2 rows in set, 1 warning (0.01 sec)

greatsql> show warnings;
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                  |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t1` semi join (`test`.`t2`) where (`test`.`t2`.`c1` = `test`.`t1`.`c1`) |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

从warning信息可以看到 semi join 的字样,优化器使用半连接方式执行的子查询。从执行计划可以看到 extra 列有FirstMatch(t1) 的字样,表示对t1表外查询传入的每个c1值在t2表上都进行了首次匹配,这种方式也是我最初理解的in子查询的含义,只关心有无匹配上,不关心匹配上多少。

3.LooseScan(松散扫描)

LooseScan是使用子查询的查询列上的索引,只针对相同索引列值的第一条记录,去外查询找对应的记录。使用了这种优化方式的半连接,在explain的计划的Extra列会有LooseScan字样。

还是上面的语句,使用semijoin的hint干涉优化器,使其选择LooseScan的优化策略。

select /*+ semijoin(@subq1 loosescan) */  * from t1 where c1 in (select /*+ qb_name(subq1)*/ c1 from t2 );
greatsql> explain select /*+ semijoin(@subq1 loosescan) */  * from t1 where c1 in (select /*+ qb_name(subq1)*/ c1 from t2 );            
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type  | possible_keys | key    | key_len | ref  | rows | filtered | Extra                                      |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t2  | NULL       | index | idx_c1        | idx_c1 | 123     | NULL |    6 |    50.00 | Using index; LooseScan                     |
|  1 | SIMPLE      | t1  | NULL       | ALL   | NULL          | NULL   | NULL    | NULL |    5 |    20.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.01 sec)

greatsql> show warnings;
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                      |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select /*+ SEMIJOIN(@`subq1` LOOSESCAN) */ `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t1` semi join (`test`.`t2`) where (`test`.`t1`.`c1` = `test`.`t2`.`c1`) |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

从执行计划可以看出,子查询的表t2作为驱动表,t2表的c1列上有索引,对表t2进行访问时,使用其c1列的索引,对相同的索引列值只取第一条记录去t1表中找对应记录,将所有外查询表t1对应的记录都加入到最终结果集,可以理解为对子查询t2表的索引扫描方式是跳跃式的。

4.Duplicate Weedout重复值消除

这种方式是借助临时表来消除重复值,explain展示计划时,在extra列会出现Start temporary 和 End temporary的字样。

还是上面的语句,我们使用semijoin的hint干涉优化器,使其选择dupsweedout优化策略。

greatsql> explain select /*+ semijoin(@subq1 dupsweedout)*/ * from t1 where c1 in (select /*+ qb_name(subq1)*/ c1 from t2);
+----+-------------+-------+------------+------+---------------+--------+---------+--------------+------+----------+---------------------------------------------+
| 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 | Using where                                 |
|  1 | SIMPLE      | t2  | NULL       | ref  | idx_c1        | idx_c1 | 123     | test.t1.c1 |    2 |   100.00 | Using index; Start temporary; End temporary |
+----+-------------+-------+------------+------+---------------+--------+---------+--------------+------+----------+---------------------------------------------+
2 rows in set, 1 warning (0.00 sec)

greatsql> show warnings;
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                        |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select /*+ SEMIJOIN(@`subq1` DUPSWEEDOUT) */ `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t1` semi join (`test`.`t2`) where (`test`.`t2`.`c1` = `test`.`t1`.`c1`) |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

例如:t1表的记录('b',3),可以匹配上t2表的两条记录(3,'b'),(4,'b'),为了消除关联结果的重复值,可以想象建立这样一个临时表:

create table tmp(rowid int primary key);

当把t1表的记录加入到结果集时,先把这条记录的rowid加入到临时表中,如果添加成功,说明这条记录并没有加入到最后的结果集,如果添加失败,则说明t1表的这条记录已经加入到最终结果集了

个人感觉这种方式比其他方式效率低。

5.Semi-join Materialization(半连接物化)

先把IN 子句中的不相关子查询进行物化,然后再将外层查询的表与物化表进行连接。子查询内部有分组聚合运算时通常会先进行物化处理。

还是上面的语句,使用semijoin的hint干涉优化器,使其选择materialization的优化策略。

select /*+ semijoin(@subq1 materialization) */  * from t1 where c1 in (select /*+ qb_name(subq1)*/ c1 from t2 );
greatsql> explain select /*+ semijoin(@subq1 materialization) */  * from t1 where c1 in (select /*+ qb_name(subq1)*/ c1 from t2 );
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------+------+----------+-------------+
| id | select_type  | table       | partitions | type   | possible_keys       | key                 | key_len | ref          | rows | filtered | Extra       |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------+------+----------+-------------+
|  1 | SIMPLE       | t1        | NULL       | ALL    | NULL                | NULL                | NULL    | NULL         |    5 |   100.00 | Using where |
|  1 | SIMPLE       | <subquery2> | NULL       | eq_ref | <auto_distinct_key> | <auto_distinct_key> | 123     | test.t1.c1 |    1 |   100.00 | NULL        |
|  2 | MATERIALIZED | t2        | NULL       | index  | idx_c1              | idx_c1              | 123     | NULL         |    6 |   100.00 | Using index |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------+------+----------+-------------+
3 rows in set, 1 warning (0.00 sec)

greatsql> show warnings;
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                            |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select /*+ SEMIJOIN(@`subq1` MATERIALIZATION) */ `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t1` semi join (`test`.`t2`) where (`<subquery2>`.`c1` = `test`.`t1`.`c1`) |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

从执行计划可以看出,先对子查询t2表做了物化表处理,物化表会生成自动索引 ,外查询表t1再与物化表做Nest loop连接。

补充说明

对于上面的语句 select * from t1 where c1 in (select c1 from t2);,优化器默认选择了firstmatch方式,其他方式都是使用hint来干涉的优化器的选择,可以看到这个hint包含两部分,一个是使用qb_name()给子查询分配一个名称,一个是使用semijoin([@query_block_name] [strategy]),指定子查询块使用半连接策略,可以指定多个策略。同时semijoin的优化策略的选择还受优化开关参数optimize_switch的影响,该参数里有semijoin,loosescan,firstmatch,duplicateweedout的开关,默认都是开启的,所以也可以使用优化开关来干涉优化器的选择。

优化举例

select count(*)
  from t1 a
 where substr(a.modifytime, 1, 8) = '20240301'
   and a.sospecnumber in
       (select a.sospecnumber
          from t1 a
         where substr(a.modifytime, 1, 8) < '20240301');

这条SQL只涉及一张表t1,表中数据200万左右,modify_time为字符类型,存储从2009年开始的时间串。看一下该表的索引情况。

greatsql> show index from t1;
+-------+------------+------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name         | Seq_in_index | Column_name  | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------+------------+------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| t1    |          1 | idx_sospecnumber |            1 | SOSPECNUMBER | A         |         133 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| t1    |          1 | idx_modifytime   |            1 | MODIFYTIME   | A         |      634186 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
+-------+------------+------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
2 rows in set (0.01 sec)

explain的执行计划如下:

greatsql> explain
    -> select count(*)
    ->   from t1 a
    ->  where substr(a.modifytime, 1, 8) ='20240301'
    ->    and a.sospecnumber  in
    ->        (select a.sospecnumber
    ->           from t1 a
    ->          where substr(a.modifytime, 1, 8) < '20240301') ;
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+---------------------+---------+----------+-------------+
| id | select_type  | table       | partitions | type   | possible_keys       | key                 | key_len | ref                 | rows    | filtered | Extra       |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+---------------------+---------+----------+-------------+
|  1 | SIMPLE       | a           | NULL       | ALL    | idx_sospecnumber    | NULL                | NULL    | NULL                | 2426414 |   100.00 | Using where |
|  1 | SIMPLE       | <subquery2> | NULL       | eq_ref | <auto_distinct_key> | <auto_distinct_key> | 131     | test.a.SOSPECNUMBER |       1 |   100.00 | NULL        |
|  2 | MATERIALIZED | a           | NULL       | ALL    | idx_sospecnumber    | NULL                | NULL    | NULL                | 2426414 |   100.00 | Using where |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+---------------------+---------+----------+-------------+
3 rows in set, 1 warning (0.00 sec)

优化器选择的半连接优化策略是物化的方式。

explain analyze的实际计划如下:

greatsql> explain analyze
    -> select count(*)
    ->   from t1 a
    ->  where substr(a.modifytime, 1, 8) ='20240301'
    ->    and a.sospecnumber  in
    ->        (select a.sospecnumber
    ->           from t1 a
    ->          where substr(a.modifytime, 1, 8) < '20240301') \G
*************************** 1. row ***************************
EXPLAIN: -> Aggregate: count(0)  (cost=1177497474524.58 rows=1) (actual time=4442.499..4442.500 rows=1 loops=1)
    -> Nested loop inner join  (cost=588748984584.98 rows=5887484899396) (actual time=4438.967..4442.408 rows=1346 loops=1)
        -> Filter: ((substr(a.MODIFYTIME,1,8) = '20240301') and (a.SOSPECNUMBER is not null))  (cost=252003.98 rows=2426414) (actual time=1550.096..1552.027 rows=1346 loops=1)
            -> Table scan on a  (cost=252003.98 rows=2426414) (actual time=0.050..1189.136 rows=2493198 loops=1)
        -> Single-row index lookup on <subquery2> using <auto_distinct_key> (sospecnumber=a.SOSPECNUMBER)  (cost=494645.48..494645.48 rows=1) (actual time=2.147..2.147 rows=1 loops=1346)
            -> Materialize with deduplication  (cost=494645.38..494645.38 rows=2426414) (actual time=2888.845..2888.845 rows=165 loops=1)
                -> Filter: (a.SOSPECNUMBER is not null)  (cost=252003.98 rows=2426414) (actual time=0.215..1927.315 rows=2487547 loops=1)
                    -> Filter: (substr(a.MODIFYTIME,1,8) < '20240301')  (cost=252003.98 rows=2426414) (actual time=0.214..1745.562 rows=2487547 loops=1)
                        -> Table scan on a  (cost=252003.98 rows=2426414) (actual time=0.211..1235.738 rows=2493198 loops=1)

1 row in set (4.45 sec)

优化分析:

这条SQL总体耗时4.45s,耗时主要分布在两处:

一处消耗在外表的查询,对t1进行了全表扫描,回表过滤后剩余1346行数据,耗时1552ms,此处虽然modifytime列有索引,但是因为在条件列上施加了substr函数,导致索引用不上,改为modifytime like '20240301%'的方式,也表示了查询2024年3月1日的数据,同时用上了索引。

另一处消耗在子查询的物化上,子查询结果集有2487547行数据,表扫描、过滤、物化整个过程耗时约2888ms,对大结果集进行物化消耗比较大,同时IN子查询的查询列sospecnumber列上是有索引的,虽然选择性不好,但是这个子查询的含义是只需要判断子查询结果集中有无记录能匹配上,而不关心匹配上多少条,所以这种情况采用first match方式比较好。

SQL改写如下:

select /*+ semijoin(@subq firstmatch)*/
 count(*)
  from t1 a
 where a.modifytime like '20240301%'
   and a.sospecnumber in
       (select /*+ qb_name(subq)*/
         a.sospecnumber
          from t1 a
         where substr(a.modifytime, 1, 8) < '20240301')

改写后执行计划如下:

*************************** 1. row ***************************
EXPLAIN: -> Aggregate: count(0)  (cost=11052513.72 rows=1) (actual time=157.570..157.570 rows=1 loops=1)
    -> Nested loop semijoin  (cost=8596909.70 rows=24556040) (actual time=0.203..157.450 rows=1346 loops=1)
        -> Filter: (a.SOSPECNUMBER is not null)  (cost=606.05 rows=1346) (actual time=0.057..7.610 rows=1346 loops=1)
            -> Index range scan on a using idx_modifytime over ('20240301' <= MODIFYTIME <= '20240301????????????????????????????????????????????????'), with index condition: (a.MODIFYTIME like '20240301%')  (cost=606.05 rows=1346) (actual time=0.055..7.406 rows=1346 loops=1)
        -> Filter: (substr(a.MODIFYTIME,1,8) < '20240301')  (cost=83255911.06 rows=18244) (actual time=0.111..0.111 rows=1 loops=1346)
            -> Index lookup on a using idx_sospecnumber (SOSPECNUMBER=a.SOSPECNUMBER)  (cost=83255911.06 rows=18244) (actual time=0.111..0.111 rows=1 loops=1346)

1 row in set, 1 warning (0.16 sec)

改写后耗时0.16s,性能提升近30倍,在对子查询通过索引idx_sospecnumber搜索数据时,查到一条就会停止继续搜索了。

结语

GreatSQL的 IN 子查询适用于半连接时,优化器提供了5种优化策略:Table pullout、FirstMatch、LooseScan、Duplicate weedout、materialize。

一般外查询表结果集小,子查询结果集太大时,不希望通过物化这种方式来执行连接,因为物化表的代价太大,可能通过FirstMatch或者LooseScan很快就可以执行出结果了。那反之外查询结果集大,子查询结果集小时,通过物化表这种方式可能就会取得很好的效果。很多时候都不用过多干涉优化器做选择,但是如果懂得原理,当优化器选错的时候我们也可以通过hint来稳定计划,让SQL保持高效的执行。

Enjoy GreatSQL :)

关于 GreatSQL

GreatSQL是适用于金融级应用的国内自主开源数据库,具备高性能、高可靠、高易用性、高安全等多个核心特性,可以作为MySQL或Percona Server的可选替换,用于线上生产环境,且完全免费并兼容MySQL或Percona Server。

相关链接: GreatSQL社区 Gitee GitHub Bilibili

GreatSQL社区:

image

社区有奖建议反馈: https://greatsql.cn/thread-54-1-1.html

社区博客有奖征稿详情: https://greatsql.cn/thread-100-1-1.html

(对文章有疑问或者有独到见解都可以去社区官网提出或分享哦~)

技术交流群:

微信&QQ群:

QQ群:533341697

微信群:添加GreatSQL社区助手(微信号:wanlidbc )好友,待社区助手拉您进群。

GreatSQL与MySQL性能对比测试,带您一同探索数据库的速度差异 在选择适合自己的数据库时,性能是一个至关重要的考虑因素。本文将介绍GreatSQL和MySQL两个常见的关系型数据库,并通过性能测试来比较它们之间的速度差异。除了查询性能之外,我们还可以比较GreatSQL和MySQL在插入和更新操作方面的性能差异。除了查询性能之外,我们还可以比较GreatSQL和MySQL在插入和更新操作方面的性能差异。然后,我们分别在GreatSQL和MySQL中执行了一个查询,并记录了执行时间。然后,我们分别在GreatSQL和MySQL中执行了一个查询,并记录了执行时间。 阅读详情

相关推荐

GreatSQL安装篇

操作系统:CentOS7 CPU:X86_64架构 数据库:GreatSQL-8.0.32-26-Linux-glibc2.17-x86_64.tar.xz。

BUTCHER 1123

GreatSQL是一个国内自主的开源数据库,可替代mysql

此外,GreatSQL社区还提供了MySQL Shell for GreatSQL,这是一个高级客户端和代码编辑器,支持JavaScript和Python脚本功能,以及与MySQL一起使用的API。它可以用来管理数据库,特别是对MGR的支持,可以方便地进行搭建、管理和配置。GreatSQL的架构可以分为连接层、服务层和存储引擎层。连接层负责与客户端建立连接,服务层是GreatSQL的核心,包括连接器、分析器、优化器、执行器等,而存储引擎层则负责数据的存储和提取。

地球空间 3193

GreatSQL数据库8.0快速安装部署

本文介绍了GreatSQL数据库在Linux系统上的安装配置流程。主要内容包括:1)系统兼容性检查和环境准备;2)关闭SELinux和防火墙;3)安装依赖包;4)创建安装目录并上传安装包;5)解压和安装RPM包;6)配置数据目录权限;7)初始化数据库并获取临时密码;8)修改配置文件;9)启动服务并修改密码;10)创建远程访问用户。最后还说明了如何使用Navicat客户端连接数据库。整个流程详细说明了从安装到配置的完整步骤。

m0_61389590的博客 707

mysql semi join详解_MySQL中的semi-join

http://www.linuxidc.com/Linux/2015-05/117523.htm1. 背景介绍什么是semi-join?所谓的semi-join是指semi-join子查询。 当一张表在另一张表找到匹配的记录之后,半连接(semi-jion)返回第一张表中的记录。与条件连接相反,即使在右节点中找到几条匹配的记录,左节点 的表也只会返回一条记录。另外,右节点的表一条记录也不会返回。半...

weixin_28769407的博客 4031

保姆级教程:用CellOracle 0.10.13从单细胞数据构建基因调控网络(附完整代码)

本文提供了一份详细的CellOracle 0.10.13使用指南,帮助用户从单细胞数据构建基因调控网络。通过Python接口,即使是生物信息学新手也能快速掌握数据准备、KNN插补、GRN计算及可视化等关键步骤。文章包含完整代码和实用技巧,助力单细胞转录组数据分析。

weixin_42563415的博客 723

ArcGIS实战:地形湿度指数(TWI)计算全流程解析

本文详细解析了ArcGIS中地形湿度指数(TWI)的计算全流程,从DEM数据获取、填洼处理到水流方向与汇流累积量计算,最终实现TWI的精准计算。通过实战步骤指导,帮助读者掌握水文建模和土壤湿度预测的关键技术,适用于农业规划、生态研究等领域。

weixin_29231725的博客 869

GreatSQL优化技巧:半连接(semijoin)优化

*GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源。何为半连接?半连接是在GreatSQL内部采用的一种执行子查询的方式,semi join不是语法关键字,不能像使用inner join、left join、right join这种语法关键字一样提供给用户来编写SQL语句。两个表t1表和t2表进行半连接的含义是:对于t1表的某条记录来说,我们只关心在t2表中是否存在与之...

老叶茶馆 394

ESP32-S3开发环境避坑指南:VS Code+ESP-IDF离线安装全流程(附国内镜像加速)

本文提供了一份详细的ESP32-S3开发环境离线安装指南,重点解决网络依赖问题。通过整合国内镜像加速,手把手教你完成VS Code、ESP-IDF框架、工具链及Python依赖的本地化部署,实现稳定、可重复的开发环境搭建,大幅提升IoT开发效率。

peach的博客 962

技术干货 | 图文结合带你搞懂GreatSQL体系架构

所有的数据,数据库、表的定义,表的每一行的内容,索引,都是存在 文件系统上,以文件的方式存在的,并完成与存储引擎的交互。服务层是GreatSQL Server的核心,主要包含连接器、分析器、优化器、执行器等,涵盖 GreatSQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。GreatSQL数据库区别于其他数据库的一个特点就是其可插拔的表存储引擎,特别需要注意的是,存储引擎是基于表的,而不是数据库。

GreatDB的博客 437

RK3588嵌入式开发实战:U-Boot设备树文件修改全指南(含常见踩坑点)

本文为RK3588嵌入式开发者提供了一份详尽的U-Boot设备树文件修改实战指南。文章深入解析了设备树的核心架构,从基础硬件适配到时钟、电源管理等进阶配置,并重点总结了内存节点、串口输出、电源时序等常见踩坑点与解决方案,旨在帮助开发者高效、稳健地完成U-Boot移植与定制工作。

yhn456789的博客 732

别再让充电器‘罢工’了!聊聊Charger里VIN-DPM这个不起眼但关键的功能

本文深入探讨了充电IC中的VIN-DPM(输入电压动态电源管理)功能,揭示其如何智能调节充电策略以避免电源适配器过载。通过实际案例和实验室数据,展示了VIN-DPM在提升充电稳定性和效率中的关键作用,并提供了选型、设计和故障排查的实用指南。

weixin_33738578的博客 416

MySQL · Semi-join原理及源码分析

Semi-join 语义 在MySQL中,semi-join是专门针对SPJ(没有group by/order by等操作) 的IN/Exists子查询进行优化的一种join语义,起到了对外层表的过滤作用,通过将相关/非相关subquery转换为semi join来充分利用join reordering的灵活性,以期获取最高的执行效率。 MySQL的开发历史中重度依赖于nested-loop join这唯一一种join方式,这导致了其在做join ordering优化时采用了greedy search的

kimichen123的专栏 2234

零基础入门篇③ PyCharm使用指南(个性化设置、开发必备插件、常用快捷键)

Python从入门到精通👉零基础入门篇③

hacker707的博客 4375

CentOS7安装GreatSQL

GreatSQL 数据库是一款 开源免费 数据库,可在普通硬件上满足金融级应用场景,具有 高可用、高性能、高兼容、高安全 等特性,可作为 MySQL 或 Percona Server for MySQL 的理想可选替换。

落的博客 1701

【100%通过率】华为OD机试真题 C 实现【最多等和不相交连续子序列】【2022.11 Q4 新题】

给定一个数组,我们称其中连续的元素为连续子序列,称这些元素的和为连续子序列的和。数组中可能存在几组连续子序列,组内的连续子序列互不相交且有相同的和。求一组连续子序列,组内子序列的数目最多。输出这个数目。

MISAYAONE的博客 1万+

YOLOv11改进 | 细节涨点篇 | DySample一种最新的轻量化动态上采样算子(效果完爆CARAFE)

本文给大家带来的最新改进机制是一种号称超轻量级且有效的动态上采样器——DySample。与传统的基于内核的动态上采样器相比,DySample采用了一种基于点采样的方法,相比于以前的基于内核的动态上采样器,DySample具有更少的参数、浮点运算次数、GPU内存和延迟。此外,DySample在包括语义分割、目标检测、实例分割、全景分割和单目深度估计在内的五个预测任务中,性能均优于其他上采样器(截至目前最有效的上采样算子),

Snu77的博客 4211

PostgreSQL之SEMI-JOIN半连接

Semi-Join半连接,当外表在内表中找到匹配的记录之后,Semi-Join会返回外表中的记录。但即使在内表中找到多条匹配的记录,外表也只会返回已经存在于外表中的记录。而对于子查询,外表的每个符合条件的元组都要执行一轮子查询,效率比较低下。此时使用半连接操作优化子查询,会减少查询次数,提高查询性能。其主要思路是将子查询上拉到父查询中,这样内表和外表是并列关系,外表的每个符合条件的元组,只需要在内表中找符合条件的元组即可,所以效率会大大提高。当参与等值JOIN的表达式存在有重复值。

★【World Of Moshow 郑锴】★ 2406

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

探索卓越性能:GreatSQL - 高效数据库解决方案

探索卓越性能:GreatSQL - 高效数据库解决方案 项目简介 是一个开源的、高性能的关系型数据库管理系统,它源自 MySQL 的 Percona Server for MySQL 分支,并在此基础上进行了一系列优化和增强,旨在提供更优秀的事务处理能力、更高的并发性能以及更好的大数据处理能力。 技术分析 性能提升 GreatSQL 在 MySQL 的基础上,针对 InnoDB 存储引擎进行了深度...

gitblog_00021的博客 645

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

GreatSQL体系架构的了解

通过学习《图文结合带你搞懂GreatSQL体系架构》,能对GreatSQL的基本底层原理有所了解。一张经典的体系架构图,图1_GreatSQL5.7 版本体系架构图由此可以发现,GreatSQL5.7由以下几部分组成,连接池组件系统管理和控制工具SQL接口组件查询解析器查询优化器缓存组件可插拔存储引擎系统和日志文件GreatSQL数据库区别于其他数据库的一个特点就是其可插拔的表存储引擎,特别需要注...

bisal的专栏 409

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

MariaDB 和 GreatSQL 性能差异背后的真相

最近项目上遇到了两次 MariaDB 和 GreatSQL 的对比,GreatSQL受到客户质疑,最后经过排查抓到性能差异背后的真相。基于此做个分享。无论是问题一中的100倍性能背后还是插入hang住,其实背后都是对产品的不了解,MariaDB 10.3.39其实对应的是5.7的产品,而且其本身的默认参数调整有作弊嫌疑,默认关闭binlog ,sync_binlog设置为0,checkpoint阈值降低,提高checkpoint的频率等设置,面向测试环境数据是很好看的,但是增加了生产环境的风险和硬件成本。

GreatSQL2021的博客 1668

腾讯 GreatSQL 全链路实战:从国产化选型到 MGR 集群部署

【摘要】GreatSQL是由腾讯主导开发的开源MySQL分支数据库,基于MySQL 8.0内核深度优化,完全兼容MySQL协议与语法,特别适配国产信创环境。其核心优势包括:增强的MGR高可用架构,支持仲裁节点避免脑裂;性能较原生MySQL提升30%;提供国密加密等安全特性;完美适配鲲鹏、飞腾等国产CPU及麒麟、统信等操作系统。安装部署支持二进制包方式,配置与MySQL高度一致,同时针对MGR集群进行了优化预设。GreatSQL的零代码迁移特性使其成为金融、政务等领域替代Oracle/MySQL的理想选择,特

一火的专栏 842

图文解读MySQL/GreatSQL体系架构

*GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源。往期系列回顾图文结合系列之带你搞懂MySQL日志系列很多小伙伴使用了GreatSQL,但是对GreatSQL的底层原理还不是很了解,今天就带大家一起揭开GreatSQL体系架构的神秘面纱!首先来回顾一张经典的体系架构图:图1_GreatSQL5.7 版本体系架构图由此可以发现,GreatSQL5.7 由以下几部分组成连...

老叶茶馆 520

关于GreatSQL字符集的总结

最近的SQL优化工作中经常遇到因字符集或校验规则不一致导致索引使用不了的问题,修改表的字符集或校验规则相当于把表重构,表中数据量大时,处理起来费时费力,希望应用开发者在设计之初时注意到此问题,让后期接手运维的小伙伴少一些负担。GreatSQL的字符集和校验规则种类繁多,提供灵活性的同时,也带来使用混乱的烦恼。本文对字符集做一个总结,让读者对GreatSQL的字符集有一个全面的了解。

GreatSQL2021的博客 1143

面向金融级应用的GreatSQL正式开源

本文首发于 GreatSQL社区 微信公众号。经过几个月的紧张筹备,GreatSQL宣布正式开源。GreatSQL是源于Percona Server的分支版本,除了Percona Serv...

老叶茶馆 510

GreatSQL单机环境安装

《国产数据库之-GreatSQL》介绍了万里的GreatSQL数据库,在这篇文章中,我们尝试搭建一个单机版的环境。GreatSQL是源于Percona Server的分支版本,因此从整体安装的流程上,和MySQL基本一致。1. 下载安装文件可以从这个链接,下载对应平台的安装文件,如果是CentOS,可以选择GreatSQL-8.0.25-15-Linux-glibc2.1...

bisal的专栏 2843

图文结合带你搞懂GreatSQL体系架构

往期系列回顾很多小伙伴使用了GreatSQL,但是对GreatSQL的底层原理还不是很了解,今天就带大家一起揭开GreatSQL体系架构的神秘面纱!首先来回顾一张经典的体系架构图:图1_GreatSQL5.7 版本体系架构图由此可以发现,GreatSQL5.7 由以下几部分组成GreatSQL数据库区别于其他数据库的一个特点就是其可插拔的表存储引擎,特别需要注意的是,存储引擎是基于表的,而不是数据库。然而,经典同时也意味着这幅图已经相当陈旧了。在GreatSQL8.0 及更高版本中,查询缓存这一功能已经被移

GreatSQL2021的博客 569
上一篇: 主从延迟调优思路
下一篇: GreatSQL 死锁案例分析
GreatSQL社区
博客等级 码龄5年 1210粉丝 390原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值