MySQL因为没有实现hashjoin而受到批评。最新的8.0.18版本带来了这一功能,令人欣慰。有时候我想知道为什么MySQL不支持hashjoin?我认为这可能是因为MySQL主要用于简单的OLTP场景,而且它广泛应用于Internet应用程序中,所以需求并不那么迫切。另一方面,这可能是因为以前完全依赖社区。毕竟MySQL的进化速度是有限的。甲骨文收购mysql后,mysql发布的演进速度明显加快。

  • 源点:提供流的节点(入度为0),类比成为一个无限放水的水厂
  • 汇点:接受流的节点(出度为0),类比成为一个无限收水的小区
  • 弧:类比为水管
  • 弧的容量:类比为水管的容量;用函数c(x,y)c(x,y)表示弧(x,y)(x,y)的容量
  • 弧的流量:类比为当前在水管中水的量;用函数f(x,y)f(x,y)表示弧(x,y)(x,y)的流量
  • 弧的残量:即容量-流量
  • 容量网络:对于一个网络流模型,每一条弧都给出了容量,则构成一个容量网络。
  • 流量网络:对于一个网络流模型,每一条弧都给出了流量,则构成一个流量网络。
  • 残量网络:对于一个网络流模型,每一条弧都给出了残量,则构成一个残量网络。最初的残量网络就是容量网络。

hashjoin本身的算法实现并不复杂。要说它很复杂,可能是优化器选择执行计划时,是否选择hashjoin,选择外观,内部表可能更复杂。无论如何,现在使用hashjoin,优化器在选择join算法时还有另一个选择。MySQL基于实用主义。我相信这个增强也回答了一些问题。有些职能并非无能,而是有优先权。

在8.0.18之前,MySQL只支持nestlopjoin算法。最简单的是简单的nestloop连接。MySQL对该算法进行了一些优化,包括块嵌套循环连接、索引嵌套循环连接和批密钥访问。通过这些优化,可以在一定程度上缓解hashjoin的紧迫性。接下来,我们将用一个单独的章节来讨论MySQL的这些连接优化。接下来,我们将讨论hashjoin。

Hash Join算法

Nestloopjoin algorithm is simply a double loop, which traverses the surface (drive table), for each row of records on the surface, then traverses the inner table, and then determines whether the join conditions are met, and then determines whether to spit out the records to the last execution node. In terms of algorithm, this is a complexity of M * n. Hash join is an optimization for equal join scenarios. The basic idea is to load the external data into memory and establish a hash table. In this way, you can complete the join operation and output the matching records only by traversing the internal table once. If all the data can be loaded into memory, of course, the logic is simple. Generally speaking, this kind of join is called CHJ (classic hash join). MariaDB has implemented this kind of hash join algorithm before. If all the data cannot be loaded into memory, it needs to be loaded into memory in batches, and then joined in batches. The following describes the implementation of these join algorithms.

In-Memory Join(CHJ)

HashJoin一般包括两个过程,创建hash表的build过程和探测hash表的probe过程。

1).build phase

遍历外表,以join条件为key,查询需要的列作为value创建hash表。这里涉及到一个选择外表的依据,主要是评估参与join的两个表(结果集)的大小来判断,谁小就选择谁,这样有限的内存更容易放下hash表。

2).probe phase

hash表build完成后,然后逐行遍历内表,对于内表的每个记录,对join条件计算hash值,并在hash表中查找,如果匹配,则输出,否则跳过。所有内表记录遍历完,则整个过程就结束了。过程参照下图

mysql技术与应用答案 mysql数据库应用答案3868_后端

左侧是build过程,右侧是probe过程,country_id是equal_join条件,countries表是外表,persons表是内表。

On-Disk Hash Join

CHJ的局限性在于需要内存来适应整个曲面。在mysql中,join可以使用的内存由join buffer size参数控制。如果一个连接所需的内存超过了连接缓冲区的大小,CHJ会忍不住将曲面分成几个段,逐个构建每个段,然后遍历内部表,再次探测每个段。假设表面被分成n块,然后扫描内表n次。当然,这种方式比较弱。在MySQL 8.0中,如果一个join所需的内存超过了join缓冲区的大小,那么构建阶段将首先使用哈希计算来划分外表面并生成一个临时的磁盘分区;然后在探测阶段,使用相同的哈希算法来划分内表。由于相同的哈希函数,相同的键(相同的连接条件)必须在相同的分区号中。接下来,对外部表和内部表中具有相同分区号的数据执行CHJ。在所有的CHJ片段完成之后,整个连接过程就完成了。该算法的代价是外部表读IO两次,内部表写IO一次。与以往的n扫描内表IO相比,目前的处理方法更好。

#include<cstdio>

#include<algorithm>

#include<queue>

#include<cstring>

using namespace std;

struct data

{

    int to,next,val;

}e[2*100005];

int cnt,head[10005],prep[10005],pree[10005],flow[10005],ans;

queue<int> que;

int n,m,s,t,u,v,w;

void add(int u,int v,int w)

{

    e[++cnt].to=v;

    e[cnt].next=head[u];

    head[u]=cnt;

    e[cnt].val=w;

}

int bfs(int s,int t)

{

    while (!que.empty()) que.pop();

    flow[s]=0x3f3f3f3f;//flow记录的是在增广路上经过该点的流量

    que.push(s);

    for (int i=1;i<=n;i++) 

    {

        prep[i]=-1;//用于记录前驱节点

        pree[i]=0;//用于记录前驱边的编号

    }

    prep[s]=0;

    while (!que.empty())

    {

        int now=que.front();

        que.pop();

        if (now==t) break;

        for (int i=head[now];i;i=e[i].next)

        {

            if (e[i].val>0&&prep[e[i].to]==-1)

            {

                que.push(e[i].to);

                flow[e[i].to]=min(flow[now],e[i].val);

                pree[e[i].to]=i;

                prep[e[i].to]=now;

            }

        }

    }

    if (prep[t]!=-1) return flow[t];

    else return -1;

}

void EK(int s,int t)

{

    int delta=bfs(s,t);//寻找最短增广路的最大流量

    while (delta!=-1)

    {

        ans+=delta;

        for (int j=t;j;j=prep[j])

        {

            e[pree[j]].val-=delta;

            e[pree[j]^1].val+=delta;

            //链式前向星存边从编号2开始存储可以通过异或1快速取得反向边的编号。

        }

        delta=bfs(s,t);

    }

}

int main()

{

    scanf("%d%d%d%d",&n,&m,&s,&t);

    cnt=1;

    for (int i=1;i<=m;i++)

    {

        scanf("%d%d%d",&u,&v,&w);

        add(v,u,0);

        add(u,v,w);

        //加入正反边

    }