InnoDB连接

解释连接前先创建两张表插入一些数据:

CREATE TABLE t_player (
    PlayerID INT NOT NULL AUTO_INCREMENT COMMENT '玩家id全局唯一id',
    NAME VARCHAR(8) NOT NULL DEFAULT '' COMMENT '玩家名称',
    Lv INT NOT NULL DEFAULT 0 COMMENT '等级',
    EXP INT NOT NULL DEFAULT 0 COMMENT '经验',
    Hp INT NOT NULL DEFAULT 0 COMMENT '血量',
    PRIMARY KEY (`PlayerID`),
    KEY `idx_lv` (`Lv`)
)ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT '玩家表';

CREATE TABLE t_equipment (
    PlayerID INT NOT NULL COMMENT '玩家id全局唯一id',
    EquipID INT NOT NULL DEFAULT 0 COMMENT '装备id',
    Lv INT NOT NULL DEFAULT 0 COMMENT '装备等级',
    PRIMARY KEY (`PlayerID`),
    KEY `idx_EquipID` (`EquipID`)
)ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT '玩家装备表';

INSERT INTO t_player(NAME,Lv,EXP,Hp) VALUES ('全网第一',1,0,100),('毁天灭地',1,0,100),('无名',1,0,100);
INSERT INTO t_equipment(PlayerID,EquipID,Lv) VALUES (1,1001,10),(3,1003,13);

连接查询其实就是将两个表组合起来按照搜索条件将结果返回给用户,下面就是一个连接查询:

SELECT t1.PlayerID,t1.name,t1.Lv,t2.EquipID,t2.Lv FROM t_player AS t1,t_equipment AS t2 WHERE t1.PlayerID >=1 AND t1.PlayerID = t2.PlayerID;

连接查询的过程:

  1. 选择驱动表

    确定第一个需要查询的表,这个表称之为驱动表,对于内连接来说,驱动表的选取过程可以参照这篇文章

  2. 查询符合条件的记录

    假设驱动表是t_player,那么先按照t1.PlayerID >= 1条件查询出记录,类似下面查询:

     SELECT t1.PlayerID,t1.name,t1.Lv FROM t_player AS t1 WHERE t1.PlayerID >=1;
    

    查询结果:

    PlayerID name Lv
    1 全网第一 1
    2 毁天灭地 1
    3 无名 1
  3. 根据第2步的记录到被驱动表查询结果

    第2步的每条记录依次作为查询被驱动表的查询条件,分解后:

     SELECT t2.EquipID,t2.Lv FROM t_equipment AS t2 WHERE t2.PlayerID = 1;
     SELECT t2.EquipID,t2.Lv FROM t_equipment AS t2 WHERE t2.PlayerID = 2;
     SELECT t2.EquipID,t2.Lv FROM t_equipment AS t2 WHERE t2.PlayerID = 3;
    

    最后把所有结果返回给用户,结果为:

    PlayerID name Lv EquipID Lv
    1 全网第一 1 1001 10
    3 无名 1 1003 13

上面的查询例子驱动表t1访问1次,被驱动表t2访问3次。当然这是在特定的过滤条件下的结果,被驱动表可能被访问多次

内连接


内连接语法

SELECT * FROM t1 [INNER | CROSS] JOIN t2 [ON 连接条件] [WHERE 普通过滤条件];

以下写法等价

SELECT t1.PlayerID,t1.name,t1.Lv,t2.EquipID,t2.Lv FROM t_player AS t1,t_equipment AS t2

SELECT t1.PlayerID,t1.name,t1.Lv,t2.EquipID,t2.Lv FROM t_player AS t1 JOIN t_equipment AS t2;

SELECT t1.PlayerID,t1.name,t1.Lv,t2.EquipID,t2.Lv FROM t_player AS t1 INNER JOIN t_equipment AS t2;

SELECT t1.PlayerID,t1.name,t1.Lv,t2.EquipID,t2.Lv FROM t_player AS t1 CROSS JOIN t_equipment AS t2;

上一小节的查询就是用内连接方式,内连接有几个特性:

  • 对于内连接的两个表,驱动表中的记录在被驱动表中找不到匹配的记录,该记录不会加入到最后的结果集

  • 对于内连接来说,驱动表和被驱动表是可以互换的,并不会影响最后的查询结果

  • 内连接中的WHERE子句和ON子句是等价的

结合这些特性,就是知道在上一小节的查询为什么从驱动表查询出来的是3条记录而最终结果只有2条记录

一般情况下,把只涉及单表的过滤条件放到WHERE子句中,把涉及两表的过滤条件都放到ON子句中,把放到ON子句中的过滤条件称之为连接条件

外连接


根据选取驱动表的不同,外连接仍然可以细分为2种:

  • 左外连接

    选取左侧的表为驱动表,右侧的表为被驱动表,语法:

      SELECT * FROM t1 LEFT [OUTER] JOIN t2 ON 连接条件 [WHERE 普通过滤条件];
    
  • 右外连接

    选取右侧的表为驱动表,左侧的表为被驱动表,语法:

      SELECT * FROM t1 RIGHT [OUTER] JOIN t2 ON 连接条件 [WHERE 普通过滤条件];
    

对于左外连接和右外连接来说,必须使用ON子句来指出连接条件

对于外连接的驱动表记录来说,如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用NULL值填充, 正是因为这样的属性左外连接和右外连接的驱动表和被驱动表如果互换会影响查询的最终结果

这个ON子句是专门为外连接驱动表中的记录在被驱动表找不到匹配记录时应不应该把该记录加入结果集这个场景下提出的, 所以如果把ON子句放到内连接中,MySQL会把它和WHERE子句一样对待,也就是上面提到的:内连接中的WHERE子句和ON子句是等价的

从第一小节中的查询结果发现在驱动表中的记录但是在被驱动表中没有匹配的记录并没有加入到结果集,现在如果需要驱动表中的记录即使在被驱动表中没有匹配的记录,也仍然需要加入到结果集,这时就可以使用外连接:

SELECT t1.PlayerID,t1.name,t1.Lv,t2.EquipID,t2.Lv FROM t_player AS t1 LEFT JOIN t_equipment AS t2 ON t1.PlayerID = t2.PlayerID WHERE t1.PlayerID >=1;

结果:

PlayerID name Lv EquipID Lv
1 全网第一 1 1001 10
3 无名 1 1003 13
2 毁天灭地 1 NULL NULL

连接的本质


在第一小节提到对驱动表访问一次再到被驱动表访问多次,这个过程用伪代码描述类似下面这样:

for _,v range t1驱动表查询的结果集{
	// 对于某条t1表的记录来说,对t2表进行单表查询
	...
}

这个过程就像一个嵌套的循环,所以这种驱动表只访问一次,但被驱动表却可能被多次访问,访问次数取决于对驱动表执行单表查询后的结果集中的记录条数的连接执行方式称之为Nested-Loop Join嵌套循环连接

这个过程优化的策略可以在被驱动表建立合适的索引,促使查询用到代价最小的访问方法,关于访问方法可以参考这篇文章

在连接查询中对被驱动表使用主键值或者唯一二级索引列的值进行等值查找的查询执行方式称之为:eq_ref

上面这个访问被驱动表的次数取决于驱动表查询的结果数量,扫描一个表的过程其实是先把这个表从磁盘上加载到内存中, 然后从内存中比较匹配条件是否满足。现实生产过程中我们有一个表就有106242514条记录。 而如果把这些记录加载到内存里可能并不能完全存放的下表中所有的记录,所以在扫描表前边记录的时候后边的记录可能还在磁盘上,等扫描到后边记录的时候可能内存不足,所以需要把前边的记录从内存中释放掉。 连接过程中如果这个被驱动表中的数据特别多而且不能使用索引进行访问,那就相当于要从磁盘上读好几次这个表,这个I/O代价就非常大了,所以尽量减少访问被驱动表的次数

MySQL里有个join buffer的概念,join buffer就是执行连接查询前申请的一块固定大小的内存, 先把若干条驱动表结果集中的记录装在这个join buffer中,然后开始扫描被驱动表,每一条被驱动表的记录一次性和join buffer中的多条驱动表记录做匹配, 因为匹配的过程都是在内存中完成的,所以这样可以显著减少被驱动表的I/O代价

这种加入了join buffer的嵌套循环连接算法称之为Block Nested-Loop Join基于块的嵌套连接算法

join buffer的大小可以通过启动参数或者系统变量join_buffer_size进行配置,默认大小为262144字节(也就是256KB),最小可以设置为128字节。 对于优化被驱动表的查询来说,最好是为被驱动表加上效率高的索引,驱动表的记录并不是所有列都会被放到join buffer中,只有查询列表中的列和过滤条件中的列才会被放到join buffer中, 这也是为什么我们要避免使用*作为查询列表,所以尽量把需要的列放到查询列表,避免浪费join buffer空间