Mysql 将整数列连接到另一个表中的范围并避免表扫描-从B.lo和B.hi之间的A.val上的连接B

Mysql 将整数列连接到另一个表中的范围并避免表扫描-从B.lo和B.hi之间的A.val上的连接B,mysql,join,optimization,indexing,range,Mysql,Join,Optimization,Indexing,Range,在强制优化器使用索引而不是表扫描的同时,是否可以通过将第一个表中的值与第二个表中的范围相匹配来连接两个表 表A有一个整数列val。表B有表示范围的lo和hi列。B中的范围不重叠 DDL示例: drop schema if exists dropMe; create schema dropMe; use dropMe; create table A ( id serial, val int ); create table B ( id serial, lo int, hi int, primary

在强制优化器使用索引而不是表扫描的同时,是否可以通过将第一个表中的值与第二个表中的范围相匹配来连接两个表

表A有一个整数列val。表B有表示范围的lo和hi列。B中的范围不重叠

DDL示例:

drop schema if exists dropMe; create schema dropMe; use dropMe;
create table A ( id serial, val int );
create table B ( id serial, lo int, hi int, primary key ( lo, hi ) );
查询示例:

select A.id aId, B.id bId from A join B on A.val between B.lo and B.hi;
问题是复杂性。如果不使用b树索引,复杂性为*M,其中表A为N=700K,表b为M=200万,因此DB引擎在返回结果之前处理1.4万亿个组合。它在合理的时间内是不可计算的

我的目标是强制优化器使用索引并获得ON*log2M的复杂度,从而获得1000万个步骤。换言之,快速执行计划的速度提高了140000倍,或者说每秒钟将等于慢速执行计划的38小时

我在这里,试图在一秒钟内挤出两天。请帮忙

测试代码如下。运行递归需要MySQL版本8或更高版本

# init
set @testRecotds = 100000;
set cte_max_recursion_depth = @testRecotds;

# DDL - creates tmp schema then creates A and B tables
drop schema if exists dropMe; create schema dropMe; use dropMe;
create table A ( id serial, val int unique );
create table B ( id serial, lo int, hi int, primary key ( lo, hi ) );

# DML - inserts semi-random 100k integers in A table and ranges in B table
insert into A( val ) with recursive r as ( select 1 i, 1 n union all select i + 1, n + 1 + 80 * rand() from r where i < @testRecotds ) select n from r;

insert into B( lo, hi )
  with recursive
    r as (
          select 1 i, 1 lo, 1 + 40 * rand() hi
        union all
          select i + 1, lo + 41 + 40 * rand() nLo, ( select nLo ) + 40 * rand()
            from r
            where i < @testRecotds
      )
  select lo, hi from r;

# The actual query - optimize the join
select count( * ) from A join B on val between lo and hi;

# MySQL uses full table scan on A and full index scan B on id column, which has no practical performance improvement

drop schema dropMe;
我试图找到一个解决办法,发现Postgres有一个简单的解决方案,但没有找到MySQL的解决方案

下面的测试代码针对上述问题的一个子集,它被简化了。如果没有更好的直接解决方案,解决它将有助于解决上述问题

两张桌子x和y。两者都包含100k个半随机整数记录。目标是让y表中的每个整数在x表中找到等于或小于y表中当前整数的最大整数

Postgres加入所有整数并求和1秒,MySQL 27分6秒。MySQL在内部扫描两个表,而PG扫描一个表并在第二个表上使用索引

-MySQL-

set cte_max_recursion_depth = 100000;

drop schema if exists dropMe; create schema dropMe; use dropMe;
create table x( x int primary key );
create table y( y int );

insert into x with recursive r as ( select 1 i, 1 n union all select i + 1, n + 1 + 40 * rand() from r where i < 100000 ) select n from r;
insert into y with recursive r as ( select 1 i, 1 n union all select i + 1, n + 1 + 40 * rand() from r where i < 100000 ) select n from r;

select sum( y ), sum( x ) from ( select y, ( select max( x ) from x where x <= y ) x from y ) z;

drop schema dropMe;
-PG-

drop schema if exists dropMe; create schema dropMe;
create table dropMe.x( x int primary key );
create table dropMe.y( y int );

insert into dropMe.x with recursive r as ( select 1 i, 1 n union all select i + 1, n + 1 + ( 40 * random() ) :: int from r where i < 100000 ) select n from r;
insert into dropMe.y with recursive r as ( select 1 i, 1 n union all select i + 1, n + 1 + ( 40 * random() ) :: int from r where i < 100000 ) select n from r;

select sum( y ), sum( x ) from ( select y, ( select max( x ) from dropMe.x where x <= y ) x from dropMe.y ) z; -- 1 second

drop schema dropMe;

祝你们玩得开心。它给了我很多,所以我在这里分享 有一种O1解决方案可以确定一个A是否在B中,但它需要一种不同的方法来存储表B的非重叠范围。方法是将所有可能的范围包括在B中,然后保持lo或hi,因为另一个与相邻行冗余。我选罗。每一行都会说明它是包含的还是一个间隙。然后是一个简单的

SELECT included FROM B WHERE some_val >= lo ORDER BY lo DESC LIMIT 1
将说明val是否包含在B中。注意:B应具有主键LO

有关更多讨论,请参阅,特别是IPv4范围的代码,如果范围包括整个INT UNSIGNED范围,则可以作为B的模型

可以想见,对A中的所有值执行此操作的最佳方法是

我假设包含的值为真1或假0。在引用中,我返回了一个owner_id,对于not owned,它是0

这应该包括对a的扫描,但对于每个a,大致是对B的点查询

OP设计了此测试用例:

设置:

create table A (
    id serial, 
    val int );
create table B (
    id serial, 
    lo int primary key, 
    included boolean );
insert into A( val )
  with recursive r as 
  ( select 1 i, 1 n 
    union all
    select i + 1, n + 1 + 80 * rand() 
        from r where i < 100000
  ) select n from r;
insert into B( lo, included )
  with recursive r as
  ( select 1 i, 1 n
    union all
    select i + 1, n + 1 + 40 * rand() from r where i < 200000
  ) select n, i % 2 from r;

作为证明,我看到Handler\u read\u prev之前大约为N*M,但之后Handler\u read\u prev和Handler\u read\u rnd\u next的大小大约为N。刷新状态和显示状态如“Handler%”对于使用小数据集进行性能测试非常方便。

什么版本的MySQL,表的引擎是什么?你真的能这样创建B表吗?串行列不是必须是主键吗?在A.val上添加索引不是吗?@草莓,我的游乐场在MySQL 8.0.18上运行。@Barmar-不,串行自动使其唯一,您也可以使其成为主键或使其他列成为主键。将val列作为主键对性能没有实际影响。请运行我添加的示例代码。感谢您的建议,但是,它似乎无法解决问题。我用表A中的100k值和100k范围测试它。每个范围由B中的2行表示。包括奇数行,排除偶数行。lo列上有一个主键。MySQL优化器忽略可能的索引搜索,并对两个表进行连续扫描。因此,复杂度为O N*M->100k*200k=200亿步,下面的示例运行36分钟,而不是亚秒时间,所需复杂度为O N*log2 M->100k*17.6->~180万步;如果存在A、B,则删除表格;创建表A id序列,val int;创建表B id序列,lo int主键,包含布尔值;将递归r作为select 1 i,1 n union all select i+1,n+1+80*rand from r插入val,其中i<100000 select n from r;插入到B lo中,包括递归r作为选择1 i,1 n并集所有选择i+1,n+1+40*rand from r,其中i<200000选择n,i%2 from r;从A中选择计数*,其中从B中选择,其中lo@user9526573-请参阅更新。使用函数似乎对性能很重要。好吧,用内部独立查询的用户定义函数替换连接可以将查询速度提高406倍。强制执行计划员使用索引感觉是一种相当笨拙的方法,但它确实有效。
我删除了缓存中的确定性删除,在我的情况下不需要这个函数变量,并获得了额外15%的性能提升。@user9526573-很好。我感到失望的是,加入未能很好地优化,我无法找到一种方法来重新制定它。所以,我也很惊讶这个函数有多好。我不太确定。请发布该功能的工作版本。
create table A (
    id serial, 
    val int );
create table B (
    id serial, 
    lo int primary key, 
    included boolean );
insert into A( val )
  with recursive r as 
  ( select 1 i, 1 n 
    union all
    select i + 1, n + 1 + 80 * rand() 
        from r where i < 100000
  ) select n from r;
insert into B( lo, included )
  with recursive r as
  ( select 1 i, 1 n
    union all
    select i + 1, n + 1 + 40 * rand() from r where i < 200000
  ) select n, i % 2 from r;
select count( * ) from A where
   ( select B.included from B
        where B.lo <= A.val order by B.lo desc limit 1
   );
CREATE DEFINER = `ip`@`localhost` FUNCTION Included(
        _val INT UNSIGNED)
    RETURNS BOOLEAN
    DETERMINISTIC
BEGIN
    DECLARE _included BOOLEAN;
    SELECT included INTO _included
        FROM B
        WHERE lo <= _val
        ORDER BY lo DESC
        LIMIT 1;
    RETURN _included;
END //
DELIMITER ;
    select count( * ) from A
        WHERE Included(A.val);