MySQL 内连接与外连接:从笛卡尔积到 LEFT JOIN,一篇讲透

目录

  • [0. 开篇:一个消失的部门](#0. 开篇:一个消失的部门)
  • [1. 实验环境](#1. 实验环境)
  • [2. 内连接:你其实一直在用它](#2. 内连接:你其实一直在用它)
    • [2.1 内连接的本质](#2.1 内连接的本质)
    • [2.2 标准语法与案例](#2.2 标准语法与案例)
    • [2.3 内连接的三种写法(完全等价)](#2.3 内连接的三种写法(完全等价))
    • [2.4 为什么内连接里 ON 和 WHERE 可以互换](#2.4 为什么内连接里 ON 和 WHERE 可以互换)
  • [3. 外连接:为什么需要它](#3. 外连接:为什么需要它)
    • [3.1 一个必须回答的业务问题](#3.1 一个必须回答的业务问题)
    • [3.2 左外连接 LEFT JOIN](#3.2 左外连接 LEFT JOIN)
    • [3.3 右外连接 RIGHT JOIN](#3.3 右外连接 RIGHT JOIN)
    • [3.4 左连接和右连接可以互换](#3.4 左连接和右连接可以互换)
  • [4. ⚠️ 五个必踩的坑(本节价值最高)](#4. ⚠️ 五个必踩的坑(本节价值最高))
    • [坑 1:外连接中 ON 和 WHERE 不等价](#坑 1:外连接中 ON 和 WHERE 不等价)
    • [坑 2:left join 之后的 count(*) 会算错](#坑 2:left join 之后的 count(*) 会算错)
    • [坑 3:where 右表字段 is null 会退化成内连接](#坑 3:where 右表字段 is null 会退化成内连接)
    • [坑 4:MySQL 没有 FULL OUTER JOIN](#坑 4:MySQL 没有 FULL OUTER JOIN)
    • [坑 5:多表外连接的顺序决定结果](#坑 5:多表外连接的顺序决定结果)
  • [5. 完整图谱:JOIN 全家桶](#5. 完整图谱:JOIN 全家桶)
  • [6. 补全:JOIN 是怎么执行的(性能篇)](#6. 补全:JOIN 是怎么执行的(性能篇))
  • [7. 实战 OJ](#7. 实战 OJ)
  • [8. 报错速查](#8. 报错速查)
  • [9. 面试高频 10 问](#9. 面试高频 10 问)
  • [10. 总结:一张图 + 一句口诀](#10. 总结:一张图 + 一句口诀)
  • 附:一键可执行完整脚本

0. 开篇:一个消失的部门

先把上一篇那个「事故现场」原样复现一遍。

需求:显示每个部门的信息(部门名、编号、地址)和人员数量。

复制代码
-- 写法 A:内连接
select d.dname, d.deptno, d.loc, count(*) as 部门人数
from emp e, dept d
where e.deptno = d.deptno
group by d.deptno, d.dname, d.loc;

结果:

复制代码
+------------+--------+----------+----------+
| dname      | deptno | loc      | 部门人数 |
+------------+--------+----------+----------+
| ACCOUNTING |     10 | NEW YORK |        3 |
| RESEARCH   |     20 | DALLAS   |        5 |
| SALES      |     30 | CHICAGO  |        6 |
+------------+--------+----------+----------+

只有 3 行。dept 明明有 4 个部门,40 号 OPERATIONS 去哪了?

答案:被 where e.deptno = d.deptno 过滤掉了。

dept 里有 40 号,但 emp 里没有任何一条 deptno = 40 的记录。「配上对」这一步失败,行就消失了。

这在业务上是灾难:

  • 新成立的组织架构,还没人入职 → 组织架构图里看不到这个部门
  • 新建的分类,还没上商品 → 分类页里看不到这个分类
  • 新注册的用户,还没下单 → 用户报表里统计不到他

所以你需要一种「哪怕配不上对,也把左边(或右边)的行留下」的连接方式。

这就是外连接。


1. 实验环境

本篇用两套数据:

① 课件原版的 stu / exam(用来演示外连接最直观)

复制代码
create database if not exists join_demo default character set utf8mb4;
use join_demo;

-- 学生表
create table stu (id int, name varchar(30));
insert into stu values (1, 'jack'), (2, 'tom'), (3, 'kity'), (4, 'nono');

-- 成绩表
create table exam (id int, grade int);
insert into exam values (1, 56), (2, 76), (11, 8);

这两张表的数据是精心设计的,请看仔细:

学生 id 成绩 id
jack 1 56 分 1
tom 2 76 分 2
kity 3 8 分 11
nono 4
  • kity(3)和 nono(4)没有成绩 ← 左连接要救它们
  • 11 号成绩没有对应的学生 ← 右连接要救它

这两组「孤儿数据」,就是外连接存在的全部理由。

② 经典的 emp / dept(用来演示业务场景)

复制代码
create table dept (
    deptno int primary key,
    dname  varchar(14),
    loc    varchar(13)
);
insert into dept values
(10, 'ACCOUNTING', 'NEW YORK'),
(20, 'RESEARCH',   'DALLAS'),
(30, 'SALES',      'CHICAGO'),
(40, 'OPERATIONS', 'BOSTON');     -- ← 没有员工的部门

create table emp (
    empno    int primary key,
    ename    varchar(10),
    job      varchar(9),
    mgr      int,
    hiredate date,
    sal      decimal(7,2),
    comm     decimal(7,2),
    deptno   int
);
insert into emp values
(7369, 'SMITH',  'CLERK',     7902, '1980-12-17',  800.00,    NULL, 20),
(7499, 'ALLEN',  'SALESMAN',  7698, '1981-02-20', 1600.00,  300.00, 30),
(7521, 'WARD',   'SALESMAN',  7698, '1981-02-22', 1250.00,  500.00, 30),
(7566, 'JONES',  'MANAGER',   7839, '1981-04-02', 2975.00,    NULL, 20),
(7654, 'MARTIN', 'SALESMAN',  7698, '1981-09-28', 1250.00, 1400.00, 30),
(7698, 'BLAKE',  'MANAGER',   7839, '1981-05-01', 2850.00,    NULL, 30),
(7782, 'CLARK',  'MANAGER',   7839, '1981-06-09', 2450.00,    NULL, 10),
(7788, 'SCOTT',  'ANALYST',   7566, '1987-04-19', 3000.00,    NULL, 20),
(7839, 'KING',   'PRESIDENT', NULL, '1981-11-17', 5000.00,    NULL, 10),
(7844, 'TURNER', 'SALESMAN',  7698, '1981-09-08', 1500.00,    0.00, 30),
(7876, 'ADAMS',  'CLERK',     7788, '1987-05-23', 1100.00,    NULL, 20),
(7900, 'JAMES',  'CLERK',     7698, '1981-12-03',  950.00,    NULL, 30),
(7902, 'FORD',   'ANALYST',   7566, '1981-12-03', 3000.00,    NULL, 20),
(7934, 'MILLER', 'CLERK',     7782, '1982-01-23', 1300.00,    NULL, 10);

2. 内连接:你其实一直在用它

2.1 内连接的本质(课件原话,一字不改)

内连接实际上就是利用 where 子句对两种表形成的笛卡儿积进行筛选,我们前面学习的查询都是内连接,也是在开发过程中使用的最多的连接查询。

这句话是整篇的基石。 它包含两层意思:

① 内连接 = 笛卡尔积 + where 筛选

复制代码
emp(14 行)× dept(4 行) = 56 行(笛卡尔积)
        ↓ where 筛掉 deptno 对不上的 42 行
      14 行(真正的内连接结果)

② 你之前写的所有多表查询,都是内连接

复制代码
select * from emp, dept where emp.deptno = dept.deptno;   -- 这就是内连接!
select * from emp inner join dept on emp.deptno = dept.deptno;  -- 同一个东西

所以「内连接」不是新东西,只是给「你早就在用的写法」起了个正式名字。

2.2 标准语法与案例

复制代码
select 字段 from 表1 inner join 表2 on 连接条件 and 其他条件;

案例:显示 SMITH 的名字和部门名称

写法一(课件说的「前面的写法」):

复制代码
select ename, dname
from EMP, DEPT
where EMP.deptno = DEPT.deptno and ename = 'SMITH';

写法二(标准的内连接写法):

复制代码
select ename, dname
from EMP inner join DEPT on EMP.deptno = DEPT.deptno and ename = 'SMITH';

结果完全一样:

复制代码
+-------+------------+
| ename | dname      |
+-------+------------+
| SMITH | RESEARCH   |
+-------+------------+

⚠️ 注意 inner 可以省略:emp join dept on ... 就是内连接。

2.3 内连接的三种写法(完全等价)

这三种写法,MySQL 优化器会生成完全相同的执行计划:

复制代码
-- ① 隐式连接(逗号 + where)
select e.ename, d.dname from emp e, dept d where e.deptno = d.deptno;

-- ② 显式连接(inner join + on)
select e.ename, d.dname from emp e inner join dept d on e.deptno = d.deptno;

-- ③ 显式连接省略 inner
select e.ename, d.dname from emp e join dept d on e.deptno = d.deptno;

-- ④ 用 using 简化(同名列)
select e.ename, d.dname from emp e join dept d using (deptno);
写法 优点 缺点 推荐度
a, b where ... 老写法,兼容性好 忘记 where 就变笛卡尔积,风险高 ⚠️
a join b on ... 语义清晰,忘写 on 会报错 稍啰嗦 ⭐ 推荐
a join b using (col) 更简洁,结果只保留一个同名列 只能是同名列 ⭐
a natural join b 自动找所有同名列 危险! 会连你不想要同名列也连上 ❌ 别用

using 和 on 有一个重要区别:

复制代码
-- on 写法:deptno 会出现两次(e.deptno 和 d.deptno)
select * from emp e join dept d on e.deptno = d.deptno;
-- 结果列:empno, ename, ..., deptno, deptno, dname, loc(两个 deptno!)

-- using 写法:deptno 只出现一次
select * from emp e join dept d using (deptno);
-- 结果列:empno, ename, ..., deptno, dname, loc(只有一个 deptno)

💡 natural join 为什么危险? 它会自动把所有同名列 都当成连接条件。如果 emp 和 dept 除了 deptno 还有别的同名列(比如都加了 create_time),它就会莫名其妙多一个连接条件,结果静默错误。永远别在生产环境用。

2.4 为什么内连接里 ON 和 WHERE 可以互换

复制代码
-- 下面两条完全等价
select * from emp e join dept d on e.deptno = d.deptno where e.sal > 2000;
select * from emp e join dept d on e.deptno = d.deptno and e.sal > 2000;

为什么?因为在内连接里,ON 和 WHERE 都是「过滤」,只是执行时机上 ON 略早。

但是------这个「可以互换」在下一节的外连接里,立刻就不成立了。

这是本篇最重要的一条分界线。 请记住:

内连接:ON 和 WHERE 等价。 外连接:ON 和 WHERE 完全不等价。(详见第 4 节坑 1)


3. 外连接:为什么需要它

3.1 一个必须回答的业务问题

回到开篇那个问题:

「显示每个部门的信息和人员数量」------40 号部门去哪了?

这个需求的关键词是「每个部门」 。它要求:不管这个部门有没有员工,都得出现在结果里。

「必须出现」= 外连接。

外连接的规则非常朴素:

把「外侧」那张表的所有行都留下,如果它在另一张表里配不上对,就用 NULL 填充。

3.2 左外连接:LEFT JOIN

如果联合查询,左侧的表完全显示,我们就说是左外连接。

语法:

复制代码
select 字段名 from 表名1 left join 表名2 on 连接条件;

案例:查询所有学生的成绩,如果这个学生没有成绩,也要将学生的个人信息显示出来。

复制代码
select * from stu left join exam on stu.id = exam.id;

结果(关键看最后两行):

复制代码
+------+-------+------+-------+
| id   | name  | id   | grade |
+------+-------+------+-------+
|    1 | jack  |    1 |    56 |
|    2 | tom   |    2 |    76 |
|    3 | kity  | NULL |  NULL |   ← 没有成绩,但留下了!
|    4 | nono  | NULL |  NULL |   ← 没有成绩,但留下了!
+------+-------+------+-------+

这两个 NULL 行,就是外连接的全部意义。

解剖这个过程:

复制代码
① 左表 stu(4 行)全部保留
② 对 stu 的每一行,去 exam 里找 id 相同的
   ├─ jack(1) → 找到 exam(1, 56) → 拼上
   ├─ tom(2)  → 找到 exam(2, 76) → 拼上
   ├─ kity(3) → 找不到          → 右边补 NULL
   └─ nono(4) → 找不到          → 右边补 NULL
③ 右表 exam 中「没有对应学生」的行(id=11)被丢弃

注意第 ③ 步:left join 会丢弃右表中多余的行。

所以「谁在外侧,谁被保留」------这就是左连接和右连接的唯一区别。

3.3 右外连接:RIGHT JOIN

如果联合查询,右侧的表完全显示,我们就说是右外连接。

语法:

复制代码
select 字段 from 表名1 right join 表名2 on 连接条件;

案例:对 stu 表和 exam 表联合查询,把所有的成绩都显示出来,即使这个成绩没有学生与它对应,也要显示出来。

复制代码
select * from stu right join exam on stu.id = exam.id;

结果(关键看最后一行):

复制代码
+------+------+------+-------+
| id   | name | id   | grade |
+------+------+------+-------+
|    1 | jack |    1 |    56 |
|    2 | tom  |    2 |    76 |
| NULL | NULL |   11 |     8 |   ← 没有学生,但成绩留下了!
+------+------+------+-------+

11 号成绩没有学生,但它被保留了。而 kity 和 nono 因为没有成绩,被丢弃了。

对比一下两种情况:

查询 保留谁 丢弃谁
stu left join exam 所有学生(含没成绩的) 没有学生的成绩(11 号)
stu right join exam 所有成绩(含没学生的) 没有成绩的学生(kity、nono)

3.4 左连接和右连接可以互换

这是外连接最重要的一个认知:

A left join B ≡ B right join A

不信?看课件的这个练习:

列出部门名称和这些部门的员工信息,同时列出没有员工的部门。

方法一(左连接,以 dept 为外侧):

复制代码
select d.dname, e.*
from dept d left join emp e on d.deptno = e.deptno;

方法二(右连接,以 dept 为外侧):

复制代码
select d.dname, e.*
from emp e right join dept d on d.deptno = e.deptno;

结果完全一样:

复制代码
+------------+-------+--------+-----------+------+------------+---------+------+--------+
| dname      | empno | ename  | job       | mgr  | hiredate   | sal     | comm | deptno |
+------------+-------+--------+-----------+------+------------+---------+------+--------+
| RESEARCH   |  7369 | SMITH  | CLERK     | 7902 | 1980-12-17 |  800.00 | NULL |     20 |
| SALES      |  7499 | ALLEN  | SALESMAN  | 7698 | 1981-02-20 | 1600.00 |300.00|     30 |
| SALES      |  7521 | WARD   | SALESMAN  | 7698 | 1981-02-22 | 1250.00 |500.00|     30 |
| RESEARCH   |  7566 | JONES  | MANAGER   | 7839 | 1981-04-02 | 2975.00 | NULL |     20 |
| SALES      |  7654 | MARTIN | SALESMAN  | 7698 | 1981-09-28 | 1250.00 |1400.0|     30 |
| SALES      |  7698 | BLAKE  | MANAGER   | 7839 | 1981-05-01 | 2850.00 | NULL |     30 |
| ACCOUNTING |  7782 | CLARK  | MANAGER   | 7839 | 1981-06-09 | 2450.00 | NULL |     10 |
| RESEARCH   |  7788 | SCOTT  | ANALYST   | 7566 | 1987-04-19 | 3000.00 | NULL |     20 |
| ACCOUNTING |  7839 | KING   | PRESIDENT | NULL | 1981-11-17 | 5000.00 | NULL |     10 |
| SALES      |  7844 | TURNER | SALESMAN  | 7698 | 1981-09-08 | 1500.00 | 0.00 |     30 |
| RESEARCH   |  7876 | ADAMS  | CLERK     | 7788 | 1987-05-23 | 1100.00 | NULL |     20 |
| SALES      |  7900 | JAMES  | CLERK     | 7698 | 1981-12-03 |  950.00 | NULL |     30 |
| RESEARCH   |  7902 | FORD   | ANALYST   | 7566 | 1981-12-03 | 3000.00 | NULL |     20 |
| ACCOUNTING |  7934 | MILLER | CLERK     | 7782 | 1982-01-23 | 1300.00 | NULL |     10 |
| OPERATIONS |  NULL | NULL   | NULL      | NULL | NULL       |    NULL | NULL |   NULL |   ← 终于出现了!
+------------+-------+--------+-----------+------+------------+---------+------+--------+

注意最后一行:OPERATIONS 部门所有 emp 字段都是 NULL。

这就是「留在外侧、配不上对就用 NULL 填」的效果。

⚠️ 但这里有个细节要提醒:最后一行的 deptno 也是 NULL!

为什么?因为 select d.dname, e.* 只取了 e.deptno(e.* 里的),而这一行的 e 部分是 NULL。如果你要显示部门号,必须写 d.deptno。

复制代码
-- 更完整的写法
select d.deptno, d.dname, e.empno, e.ename, e.sal
from dept d left join emp e on d.deptno = e.deptno;

4. ⚠️ 五个必踩的坑(本节价值最高)

坑 1:外连接中 ON 和 WHERE 不等价

这是外连接第一大坑,也是面试最爱问的。

先看需求:

查询所有学生的成绩,但如果成绩不及格(< 60),就当没查到(显示 NULL)。

❌ 错误写法(把条件写在 WHERE):

复制代码
select * from stu left join exam on stu.id = exam.id
where exam.grade >= 60;

结果:

复制代码
+------+------+------+-------+
| id   | name | id   | grade |
+------+------+------+-------+
|    2 | tom  |    2 |    76 |
+------+------+------+-------+

只有 1 行!kity 和 nono 又消失了!

为什么? 因为 left join 补出来的那两行,exam.grade 是 NULL ,而 NULL >= 60 结果是 NULL(非 TRUE) ,被 WHERE 过滤掉了。

WHERE 作用在「连接完成之后」,它会把外连接补的 NULL 行也一起筛掉。 一个外连接 + 一个作用在右表字段上的 WHERE 条件 = 退化成了内连接。

✅ 正确写法(把条件写在 ON):

复制代码
select * from stu left join exam on stu.id = exam.id and exam.grade >= 60;

结果:

复制代码
+------+------+------+-------+
| id   | name | id   | grade |
+------+------+------+-------+
|    1 | jack | NULL |  NULL |   ← 56 分,不满足 grade>=60,但学生保留了
|    2 | tom  |    2 |    76 |
|    3 | kity | NULL |  NULL |
|    4 | nono | NULL |  NULL |
+------+------+------+-------+

4 行,所有学生都在,只是 jack 的成绩那一侧变成了 NULL。

这才是「左连接 + 右表条件」的正确姿势。

⭐ 记住这条铁律:

ON 决定「怎么配对」,WHERE 决定「配完之后留哪些行」。

  • ON 里的条件只影响「能不能配上对」,配不上 → 右边补 NULL,左边照样留(因为它是外连接)
  • WHERE 里的条件影响「最终输出」,它会把 NULL 行也一起筛掉

用时间轴理解:

复制代码
① FROM 生成笛卡尔积
② ON  按条件配对(外连接时,配不上的补 NULL)  ← 在这一步,左表的行已经被"锁定"了
③ WHERE 过滤最终结果                            ← 在这一步,你可能把第②步保住的行又筛掉了

这就是为什么 ON 里的条件「不影响左表的完整性」,而 WHERE 里的条件「会」。

再看一个更容易理解的版本:

复制代码
-- 需求:所有部门 + 每个部门的「工资 > 2000」的员工
-- ❌ 错:写 WHERE 会去掉没有高薪员工的部门
select d.dname, e.ename from dept d left join emp e on d.deptno = e.deptno
where e.sal > 2000;

-- ✅ 对:写 ON,保留所有部门
select d.dname, e.ename from dept d left join emp e on d.deptno = e.deptno and e.sal > 2000;

坑 2:left join 之后的 count(*) 会算错

这是外连接第二大坑,也是统计报表出错的头号原因。

需求:统计每个部门的人数(包括 0 人的部门)。

复制代码
-- ❌ 错:count(*) 会把空部门算成 1
select d.dname, count(*) as 人数
from dept d left join emp e on d.deptno = e.deptno
group by d.dname;

结果:

复制代码
+------------+------+
| dname      | 人数 |
+------------+------+
| ACCOUNTING |    3 |
| RESEARCH   |    5 |
| SALES      |    6 |
| OPERATIONS |    1 |   ← 错了!应该是 0
+------------+------+

为什么是 1? 因为 left join 给 OPERATIONS 补了一行「全 NULL 的 emp 记录」 。count(*) 数的是「行数」,这一行也算一行 → 1。

✅ 正确写法:count(具体列)

复制代码
select d.dname, count(e.empno) as 人数
from dept d left join emp e on d.deptno = e.deptno
group by d.dname;

结果:

复制代码
+------------+------+
| dname      | 人数 |
+------------+------+
| ACCOUNTING |    3 |
| RESEARCH   |    5 |
| SALES      |    6 |
| OPERATIONS |    0 |   ← 对了!
+------------+------+

因为 count(列) 不统计 NULL ,补出来的那行 e.empno 是 NULL,所以不计入。

🔗 前后照应 :这是第 06 篇 7.2 节那张「COUNT(*) / COUNT(1) / COUNT(列)」对照表的最实战的应用 。 当时我说「统计行数用 COUNT(*)」,那是在单表 场景下。 在多表外连接场景下,COUNT(*) 会算错,必须用 COUNT(右表的主键/非空列)。

一句话:join 之后,永远用 count(右表的列)。

同理,sum 也要小心:

复制代码
-- 统计每个部门的总工资(空部门应该是 0,不是 NULL)
select d.dname, sum(e.sal) as 总工资 from dept d left join emp e on d.deptno = e.deptno
group by d.dname;
-- OPERATIONS: NULL(sum 忽略 NULL,没有任何非 NULL 值 → NULL)

-- 如果要显示 0:
select d.dname, ifnull(sum(e.sal), 0) as 总工资 from dept d left join emp e on d.deptno = e.deptno
group by d.dname;
-- OPERATIONS: 0

🔗 前后照应 :第 06 篇 7.3 节讲过「SUM() 空结果返回 NULL ,COUNT() 返回 0 」。这里就是它的实战版------在外连接下,这个差异会被放大成「报表多一行错数据」或「报表少一个 0」。

坑 3:where 右表字段 is null 会退化成内连接

等等,这不是坑,这是「找差异数据」的经典写法。但前提是你要理解它。

需求:找出「一个员工都没有」的部门。

复制代码
select d.*
from dept d left join emp e on d.deptno = e.deptno
where e.empno is null;

结果:

复制代码
+--------+------------+--------+
| deptno | dname      | loc    |
+--------+------------+--------+
|     40 | OPERATIONS | BOSTON |
+--------+------------+--------+

这个写法的原理:

  • left join 后,配不上对的部门,右边全是 NULL
  • where e.empno is null 就精确地挑出了「配不上对」的那些行

⚠️ 但注意:这里筛选的是「NULL」,不是「某个具体的值」。

复制代码
-- ❌ 这里会把外连接退化成内连接
where e.sal > 2000

-- ✅ 这里不会(因为 is null 恰好是在找"没有匹配"的行)
where e.empno is null

为什么 is null 不会退化? 因为 is null 判断的就是「补出来的 NULL」,它的目的就是筛选「没配上对」的行 。而 e.sal > 2000 的目的是「筛选配上了对、且满足条件的行」,两者目的完全不同。

⭐ left join + is null 是查找「差异数据」的黄金组合,请背下来:

复制代码
-- 找出「没有订单的用户」
select u.* from users u left join orders o on u.id = o.user_id where o.id is null;

-- 找出「没有商品的分类」
select c.* from category c left join goods g on c.id = g.category_id where g.id is null;

-- 找出「没有学生的班级」
select c.* from class c left join stu s on c.id = s.class_id where s.id is null;

它比 not in 好在哪?

写法 NULL 安全 性能 可读性
where id not in (select ...) ❌ 有 NULL 就全空 一般 好
where not exists (select 1 ...) ✅ 安全 好 一般
left join ... where 右表.id is null ✅ 安全 好(可用索引) 好

🔗 前后照应 :第 06 篇 3.4.3 节那个 NOT IN + NULL 的致命坑,解法三选一------left join 是其中最不容易出错的。这也是我在上一篇 OJ 8.3 里推荐 left join + is null 的原因。

坑 4:MySQL 没有 FULL OUTER JOIN

标准 SQL 有「全外连接」------两边的行全保留:

复制代码
-- 这在 MySQL 里会报语法错误!
select * from stu full outer join exam on stu.id = exam.id;
-- ERROR 1064 (42000): You have an error in your SQL syntax

MySQL 不支持 FULL OUTER JOIN。 但可以用 left join + union 模拟:

复制代码
select s.id as sid, s.name, e.id as eid, e.grade
from stu s left join exam e on s.id = e.id
union
select s.id as sid, s.name, e.id as eid, e.grade
from stu s right join exam e on s.id = e.id;

结果:

复制代码
+------+------+------+-------+
| sid  | name | eid  | grade |
+------+------+------+-------+
|    1 | jack |    1 |    56 |
|    2 | tom  |    2 |    76 |
|    3 | kity | NULL |  NULL |
|    4 | nono | NULL |  NULL |
| NULL | NULL |   11 |     8 |
+------+------+------+-------+

5 行 = 内连接的 2 行 + 左独有 2 行 + 右独有 1 行。

为什么用 union 而不是 union all? 因为「两边都有的行」会被 left join 和 right join 各返回一次,必须去重。

🔗 前后照应 :第 08 篇 6.3 节讲过「union 会去重(慢),union all 不去重(快)」。这里必须用去重的版本。

坑 5:多表外连接的顺序决定结果

假设有三张表:dept(4 行)、emp(14 行)、salgrade(5 行)。

需求:显示所有部门,以及每个部门员工的工资等级。

复制代码
-- ✅ 正确:dept 在最左边(最外层)
select d.dname, e.ename, s.grade
from dept d
left join emp e on d.deptno = e.deptno
left join salgrade s on e.sal between s.losal and s.hisal;

结果:

复制代码
+------------+--------+-------+
| dname      | ename  | grade |
+------------+--------+-------+
| RESEARCH   | SMITH  |     1 |
| SALES      | ALLEN  |     3 |
| ...
| OPERATIONS | NULL   |  NULL |   ← 40 号部门还在
+------------+--------+-------+

OPERATIONS 部门保住了,而且它的 grade 也是 NULL------因为 e 是 NULL,e.sal 也是 NULL,NULL between losal and hisal 结果是 NULL,配不上任何等级。

⚠️ 如果你把 emp 写在外侧:

复制代码
-- ❌ 这个查询是「所有员工 + 他们的部门 + 等级」,没有员工的部门会消失
select d.dname, e.ename, s.grade
from emp e
left join dept d on d.deptno = e.deptno
left join salgrade s on e.sal between s.losal and s.hisal;

结果:14 行,OPERATIONS 又消失了。

⭐ 结论:外连接链里,「谁在第一个,谁被无条件保留」。

多表连接的通用原则:

复制代码
第一个表 = 你要「绝对不丢」的表(业务主表)
后续表 = 用 left join 一层层挂上去
每一层的「缺数据」都会自动补 NULL

⚠️ 但如果后面某一层需要「自己的一侧也全保留」,就必须用嵌套外连接或者调整顺序。


5. 完整图谱:JOIN 全家桶

一张图把 MySQL 支持的连接方式全画出来(假设 A、B 两张表各有交集):

复制代码
① INNER JOIN(内连接)
   结果 = A ∩ B(只要两边都对得上)

   ┌───────┬───────┐
   │   A   │   B   │
   │    ┌──┴──┐    │
   │    │ 结果 │    │
   │    └──┬──┘    │
   └───────┴───────┘

② LEFT JOIN(左外)
   结果 = A 全部 + B 中对得上的
   ┌───────┬───────┐
   │       │       │
   │  结果 │   B   │
   │       │       │
   └───────┴───────┘

③ RIGHT JOIN(右外)
   结果 = B 全部 + A 中对得上的
   ┌───────┬───────┐
   │   A   │       │
   │       │  结果 │
   │       │       │
   └───────┴───────┘

④ FULL OUTER JOIN(MySQL 不支持,用 union 模拟)
   结果 = A ∪ B
   ┌───────┬───────┐
   │       │       │
   │  结果 │  结果 │
   │       │       │
   └───────┴───────┘

⑤ CROSS JOIN(笛卡尔积)
   A × B,不加 on 就是它

MySQL 语法速查:

你想要 SQL 写法 MySQL 支持
交集(内连接) a inner join b on ... ✅
A 全保留 a left join b on ... ✅
B 全保留 a right join b on ... ✅
并集(全外) a left join b on ... union a right join b on ... ✅(模拟)
只 A 有(A 减 B) a left join b on ... where b.id is null ✅
只 B 有(B 减 A) a right join b on ... where a.id is null ✅
笛卡尔积 a cross join b 或 a, b ✅(⚠️ 危险)

特别注意:「A 减 B」这种需求,SQL 里没有专门的语法,只能靠 left join + is null 表达。

这是 SQL 的一个「表达力缺口」,也解释了为什么外连接这么重要------它不只是「保留不匹配行」,它还能做「集合差」。


6. 补全:JOIN 是怎么执行的(性能篇)

6.1 连接的三种算法

MySQL(InnoDB)执行 JOIN 主要有三种算法:

算法 全称 适用 复杂度
NLJ Nested-Loop Join 被驱动表的连接列有索引 O(外层行数 × log(内层))
BNL Block Nested-Loop Join 被驱动表没索引,用 join buffer 缓冲 O(外层行数 × 内层行数)
Hash Join --- 8.0.18+,等值连接且无索引 O(外层行数 + 内层行数)

NLJ 的执行过程:

复制代码
for each row in 驱动表(外层):
    用连接条件去 被驱动表 的索引里查找匹配行   ← 这里是「索引查找」,O(log n)
    拼成结果

这就是为什么「连接字段必须有索引」------没有索引就退化成 BNL,每次都要全表扫。

Hash Join(8.0.18+) 是个大杀器:把小的那张表在内存里建一个哈希表,然后扫大表逐行查哈希。等值连接、无索引的情况下,比 BNL 快几十倍。

复制代码
-- 看有没有用上 Hash Join
explain select * from a join b on a.x = b.y;
-- Extra 列里出现 Using join buffer (hash join) 就是了

6.2 外连接的执行特殊在哪

left join 的执行必须保证「左表行不丢」,所以:

① 左表天然是「驱动表」(大部分情况)

复制代码
-- dept(4 行)left join emp(14 行)
-- 外层扫 4 行,内层用 emp.deptno 索引查 → 4 × log(14)

如果优化器反转成 emp left join dept,语义就变了。所以外连接时优化器不能随意交换左右表。

② 优化器会把「某些 left join」转换成 inner join 来提速

复制代码
-- 这个 left join 会被优化器改写成 inner join
select * from dept d left join emp e on d.deptno = e.deptno where e.sal > 2000;

为什么? 因为 where e.sal > 2000 已经保证了 e 不能是 NULL------既然右表一定存在,那 left join 就没有意义了,等价于 inner join。

这不是 bug,是优化。但这也解释了坑 1:你以为写的是外连接,实际跑的是内连接。

验证方法:explain 看 select_type 和 Extra,或者直接把查询结果对比一下。

③ 外连接时,on 里的非等值条件可能让索引失效

复制代码
-- on 里用 between,被驱动表(salgrade)很难用索引
left join salgrade s on e.sal between s.losal and s.hisal

这种「区间连接」是天然不友好的,因为 B+ 树只能精确匹配前缀。数据量大时要考虑预计算(比如把工资换算成等级存下来)。

6.3 三条优化铁律

铁律一:小表驱动大表

复制代码
-- dept 4 行、emp 100 万行
-- ✅ dept 驱动:4 × log(100万) ≈ 4 × 20
-- ❌ emp 驱动:100万 × log(4) ≈ 100万 × 2
select * from dept d left join emp e on d.deptno = e.deptno;

铁律二:连接字段必须有索引

复制代码
alter table emp add index idx_deptno (deptno);

铁律三:能用 join 就别在 where 里用函数

🔗 前后照应:第 07 篇 6.3 节「让字段裸奔」原则,在 JOIN 里同样适用。

复制代码
-- ❌ 函数包字段,索引失效
left join emp e on date(e.hiredate) = d.some_date

-- ✅ 范围查询
left join emp e on e.hiredate >= d.some_date and e.hiredate < date_add(d.some_date, interval 1 day)

7. 实战 OJ

7.1 LeetCode 178:分数排名(rank-scores)

题目 :编写 SQL 查询来实现分数排名。如果两个分数相同,则两个分数排名应该相同;注意平分后的下一个名次应该是下一个连续的整数值(而不是跳跃的),即 1, 1, 1, 2, 3。

解法一:窗口函数(MySQL 8.0+,推荐)

复制代码
select score, dense_rank() over (order by score desc) as `rank`
from scores
order by score desc;

dense_rank() 就是「密集排名」------值相同名次相同,且名次不跳号。 这正是题目要的。

三个排名函数对比(必须分清):

函数 值:100, 100, 90, 80 说明
row_number() 1, 2, 3, 4 强行不重复
rank() 1, 1, 3, 4 相同名次,后续跳号
dense_rank() 1, 1, 2, 3 相同名次,后续不跳号 ← 本题要的

解法二:相关子查询(5.7 也能用,面试常考)

复制代码
select s1.score,
       (select count(distinct s2.score) from scores s2 where s2.score >= s1.score) as `rank`
from scores s1
order by s1.score desc;

思路:对每一行 s1,去数「有多少个 不同的分数 ≥ 它自己的分数」,这个数量就是它的名次。

为什么必须 distinct? 因为如果有三个 100 分,count(*) 会得到 3,名次变成 3(错),而 count(distinct score) 得到 1(对)。

这道题是「相关子查询」的教科书案例 :子查询引用了外层的 s1.score,所以外层每一行都要执行一次子查询。性能上是 O(n²),但在 LeetCode 的数据量下够用。

🔗 前后照应 :第 08 篇 5.5 节讲的「派生表(FROM 里的子查询)」是「非相关子查询」(只算一次);这里讲的是「相关子查询」(每行算一次)。两者的性能模型完全不同,要分清。

7.2 LeetCode 626:换座位(exchange-seats)

题目 :小美是一所中学的信息技术老师,她在一张 seat 表里存了学生的名字和它们对应的椅子 id。其中 id 是一个连续的递增列。小美想改变相邻俩学生的座位。请你写一个 SQL query 来输出小美改变之后的 seat 表的结果。

规则:

  • 如果学生人数是奇数,最后一个学生的 id 不变
  • 否则交换相邻两个学生的 id

解法:

复制代码
select
    case
        when id % 2 = 1 and id = (select max(id) from seat) then id        -- 奇数且是最后一个:不变
        when id % 2 = 1 then id + 1                                        -- 奇数:和下一个换
        else id - 1                                                        -- 偶数:和上一个换
    end as id,
    student
from seat
order by id;

结果示例:

复制代码
原表            结果
+----+---------+   +----+---------+
| id | student |   | id | student |
+----+---------+   +----+---------+
|  1 | Abbot   |   |  1 | Doris   |
|  2 | Doris   |   |  2 | Abbot   |
|  3 | Emerson |   |  3 | Green   |
|  4 | Green   |   |  4 | Emerson |
|  5 | Jeames  |   |  5 | Jeames  |
+----+---------+   +----+---------+

三个考点:

① id % 2(取模)区分奇偶 ------ 就是第 07 篇讲的数学函数。

② case when 表达式 ------ SQL 里的「if-else」。

③ 子查询 (select max(id) from seat) ------ 注意这里在 SELECT 列表里用了对标量子查询,和「在 UPDATE 里引用同表」不同,这是允许的。

⚠️ 一个容易踩的坑 :不能写 update seat set id = ...,因为在 UPDATE 里同时修改和引用 id 会产生冲突。这类「交换」类题目,标准做法是「用 SELECT 生成新结果」,而不是原地 UPDATE。

case when 的语法速查:

复制代码
case
    when 条件1 then 结果1
    when 条件2 then 结果2
    else 结果3
end

⚠️ case 从上到下匹配,命中第一个就停 (不是像 switch 那样全跑一遍)。所以条件顺序很重要------先写「特殊」的,再写「一般」的。本题里「奇数且是最后一个」必须先于「奇数」。
🔗 前后照应 :case when 是第 07 篇「内置函数」的延伸(那里没讲,这里补上)。它是 SQL 里做「行内条件分支」的唯一手段,配合聚合还能做「行转列」(pivot):

复制代码
select deptno,
       sum(case when job = 'CLERK' then 1 else 0 end) as 文员数,
       sum(case when job = 'MANAGER' then 1 else 0 end) as 经理数
from emp group by deptno;

7.3 补全:再来三道外连接经典题

① 找出「从没下过单」的用户

复制代码
select u.id, u.name
from users u left join orders o on u.id = o.user_id
where o.id is null;

② 统计每个商品的销量(含 0 销量的商品)

复制代码
select g.id, g.name, count(o.id) as 销量
from goods g left join orders o on g.id = o.goods_id
group by g.id, g.name;
-- 注意是 count(o.id) 不是 count(*)

③ 找出「连续两天没打卡」的员工

复制代码
select e.id, e.name
from employees e
left join attendance a on e.id = a.emp_id and a.date = current_date()
left join attendance b on e.id = b.emp_id and b.date = date_sub(current_date(), interval 1 day)
where a.id is null and b.id is null;

这三道题的共同点: 「该有却没有」= left join + where 右表 is null。


8. 报错速查

错误码 报错文本 原因 解决
1064 You have an error ... near 'full outer join' 用了 MySQL 不支持的 FULL OUTER JOIN 用 left join union right join 模拟
1052 Column 'xxx' in field list is ambiguous 同名列没加表前缀 d.deptno / e.deptno
1066 Not unique table/alias 同一张表用了两次相同别名 起不同别名
1054 Unknown column 'xxx' in 'on clause' on 里用了不存在的列,或列名写错 检查列名
1109 Unknown table 'xxx' in order clause order by 用了没在 from 里出现的别名 补上表
1111 Invalid use of group function 忘了把 count() 等聚合放进 having/select 改位置

⚠️ 最隐蔽的「不是报错的错误」:

现象 真实原因
结果少了行 用了内连接,但业务要求「全保留」→ 改 left join
结果数是 1 而不是 0 count(*) 数了 left join 补的 NULL 行 → 改 count(右表列)
left join 却像内连接 条件写在 WHERE 而不是 ON
结果重复变多 连接条件不唯一(一对多、多对多),产生了行放大

最后一条「行放大」值得单独说:

复制代码
-- 假设 emp 和 project 是多对多,一个人参加多个项目
select e.ename, p.pname from emp e left join project p on e.empno = p.leader;

-- 如果一个人是 3 个项目的 leader,他的记录会出现 3 次!
-- 这时 count(e.ename) 会统计成 3,而不是 1

解决:用 count(distinct e.empno),或者先做子查询聚合再 join。


9. 面试高频 10 问

Q1:内连接和外连接的区别? 内连接只保留「两边都能匹配」的行;外连接保留「某一侧的全部行」,另一侧匹配不上就补 NULL。

Q2:left join 和 right join 有什么区别? 只在于「保留哪一侧」:a left join b 保留 a 全部,a right join b 保留 b 全部。a left join b 等价于 b right join a。

Q3:外连接里 ON 和 WHERE 的区别? (最高频 ) ON 决定「怎么配对」,不影响外侧表的完整性;WHERE 作用在连接完成后,会筛掉补 NULL 的行------一个作用在右表字段上的 WHERE 条件会把外连接退化成内连接。

Q4:left join 之后 count(*) 为什么不对?怎么改? left join 会给没匹配的行补一行全 NULL 的记录,count(*) 会把它算成 1。应改用 count(右表的主键)。

Q5:MySQL 支持 FULL OUTER JOIN 吗? 不支持。用 left join ... union ... right join 模拟。

Q6:怎么查「没有关联数据」的记录? left join + where 右表.pk is null。比 not in 安全(不受 NULL 影响),比 not exists 更直观。

Q7:join 不加 on 会怎样? 变成笛卡尔积(等价 cross join),结果行数 = 两表行数之积。大表上等于自杀。

Q8:join 的性能怎么优化? ① 连接字段建索引(避免退化成 BNL);② 小表驱动大表;③ 尽早过滤(on / where 里减少行数);④ 8.0 能用 Hash Join;⑤ 用 explain 看执行计划。

Q9:using 和 on 的区别? using(col) 要求两边同名列,结果集只保留一个该列;on a.x=b.y 允许不同列名,结果集两列都在。

Q10:一张表能 join 自己吗? 可以,叫自连接,必须给两个不同的别名。结合外连接,可以做「找出没有上级的员工(如 KING)」这类需求。

补充一个 Q10 的实战:

复制代码
-- 所有员工 + 他的领导(包括没有领导的 KING)
select e.ename as 员工, m.ename as 领导
from emp e left join emp m on e.mgr = m.empno;
-- KING 那行的「领导」是 NULL,这正是「最大的老板没有领导」

10. 总结:一张图 + 一句口诀

复制代码
                     ┌──────────────────────────────┐
                     │      连接方式全景             │
                     └──────────────────────────────┘
                                    │
        ┌───────────────────────────┼───────────────────────────┐
        │                           │                           │
   【内连接】                  【外连接】                  【模拟全外】
        │                           │                           │
  inner join                  left join                   left join
  = 笛卡尔积 + where           right join                  + union +
  = 只留"配得上"的             = 保留一侧全部               right join
        │                           │
  on 与 where 等价            on 与 where 不等价 ⚠️
                                    │
                    ┌───────────────┴───────────────┐
                    │                               │
              【保留全部】                     【查找差异】
              count(右表列)                    where 右表.pk is null
              ifnull(sum, 0)

一句口诀:

「内连接求交集,外连接保一侧;条件放 ON 不放 WHERE,计数要数右表列。」

展开:

半句 含义
内连接求交集 inner join 只留两边都对得上的,on 和 where 等价
外连接保一侧 left join 保左、right join 保右,另一侧补 NULL
条件放 ON 不放 WHERE 外连接里,右表的过滤条件必须写 ON,写 WHERE 会退化成内连接
计数要数右表列 count(右表.pk) 才能把「空关联」正确算成 0

最后,把这五篇串起来看:

篇 主题 一句话
05 表的约束 约束是门禁,脏数据进不来
06 增删改查 执行顺序决定一切
07 内置函数 函数别碰字段,索引就活了
08 复合查询 多表 = 笛卡尔积 + 筛选,聚合结果做临时表
09 内外连接 内连接求交集,外连接保一侧;条件放 ON,计数数右表

而这一篇又埋下了新的伏笔:

我们发现 join 的性能完全取决于「连接字段有没有索引」。 没有索引,10 万行的表 join 一下就要全表扫 10 万次。

那索引到底是什么?为什么它能这么快? B+ 树是怎么长出来的?为什么 MySQL 不用 B 树、红黑树、哈希?

------这就是下一篇《索引特性》要回答的问题。 那才是这套课件里最硬核的一篇。我们下一篇见。


附:一键可执行完整脚本

复制代码
-- ============================================================
-- MySQL 内连接与外连接 · 全功能演示脚本
-- 环境:MySQL 5.7 / 8.0 均可(窗口函数需 8.0+)
-- ============================================================
drop database if exists join_demo;
create database join_demo default character set utf8mb4;
use join_demo;

-- ---------- 演示表 1:学生 / 成绩 ----------
create table stu (id int, name varchar(30));
insert into stu values (1,'jack'),(2,'tom'),(3,'kity'),(4,'nono');

create table exam (id int, grade int);
insert into exam values (1,56),(2,76),(11,8);

-- ---------- 演示表 2:部门 / 员工 ----------
create table dept (
    deptno int primary key,
    dname  varchar(14),
    loc    varchar(13)
);
insert into dept values
(10,'ACCOUNTING','NEW YORK'),(20,'RESEARCH','DALLAS'),
(30,'SALES','CHICAGO'),(40,'OPERATIONS','BOSTON');

create table emp (
    empno int primary key, ename varchar(10), job varchar(9),
    mgr int, hiredate date, sal decimal(7,2), comm decimal(7,2), deptno int
);
insert into emp values
(7369,'SMITH','CLERK',7902,'1980-12-17',800.00,NULL,20),
(7499,'ALLEN','SALESMAN',7698,'1981-02-20',1600.00,300.00,30),
(7521,'WARD','SALESMAN',7698,'1981-02-22',1250.00,500.00,30),
(7566,'JONES','MANAGER',7839,'1981-04-02',2975.00,NULL,20),
(7654,'MARTIN','SALESMAN',7698,'1981-09-28',1250.00,1400.00,30),
(7698,'BLAKE','MANAGER',7839,'1981-05-01',2850.00,NULL,30),
(7782,'CLARK','MANAGER',7839,'1981-06-09',2450.00,NULL,10),
(7788,'SCOTT','ANALYST',7566,'1987-04-19',3000.00,NULL,20),
(7839,'KING','PRESIDENT',NULL,'1981-11-17',5000.00,NULL,10),
(7844,'TURNER','SALESMAN',7698,'1981-09-08',1500.00,0.00,30),
(7876,'ADAMS','CLERK',7788,'1987-05-23',1100.00,NULL,20),
(7900,'JAMES','CLERK',7698,'1981-12-03',950.00,NULL,30),
(7902,'FORD','ANALYST',7566,'1981-12-03',3000.00,NULL,20),
(7934,'MILLER','CLERK',7782,'1982-01-23',1300.00,NULL,10);

-- ---------- 2. 内连接 ----------
-- 课件两种写法
select ename, dname from EMP, DEPT where EMP.deptno = DEPT.deptno and ename = 'SMITH';
select ename, dname from EMP inner join DEPT on EMP.deptno = DEPT.deptno and ename = 'SMITH';
-- 四种等价写法
select e.ename, d.dname from emp e, dept d where e.deptno = d.deptno;
select e.ename, d.dname from emp e inner join dept d on e.deptno = d.deptno;
select e.ename, d.dname from emp e join dept d on e.deptno = d.deptno;
select e.ename, d.dname from emp e join dept d using (deptno);
-- on 与 where 在内连接里等价
select * from emp e join dept d on e.deptno = d.deptno where e.sal > 2000;
select * from emp e join dept d on e.deptno = d.deptno and e.sal > 2000;

-- ---------- 3. 外连接 ----------
-- 左外:保留所有学生
select * from stu left join exam on stu.id = exam.id;
-- 右外:保留所有成绩
select * from stu right join exam on stu.id = exam.id;
-- 左右互换:列出所有部门(含没有员工的部门)
select d.dname, e.* from dept d left join emp e on d.deptno = e.deptno;
select d.dname, e.* from emp e right join dept d on d.deptno = e.deptno;

-- ---------- 4. 五个坑 ----------
-- 坑 1:on 与 where 在外连接中不等价
select * from stu left join exam on stu.id = exam.id and exam.grade >= 60;  -- ✅ 4 行
select * from stu left join exam on stu.id = exam.id where exam.grade >= 60; -- ❌ 1 行

-- 坑 2:count(*) vs count(右表列)
select d.dname, count(*) as 错误人数 from dept d left join emp e on d.deptno = e.deptno
group by d.dname;
select d.dname, count(e.empno) as 正确人数 from dept d left join emp e on d.deptno = e.deptno
group by d.dname;

-- 坑 3:left join + is null 找差异
select d.* from dept d left join emp e on d.deptno = e.deptno where e.empno is null;

-- 坑 4:模拟 FULL OUTER JOIN
select s.id as sid, s.name, e.id as eid, e.grade from stu s left join exam e on s.id = e.id
union
select s.id as sid, s.name, e.id as eid, e.grade from stu s right join exam e on s.id = e.id;

-- 坑 5:多表外连接的顺序
-- 以 dept 为最外层(正确)
select d.dname, e.ename, e.sal from dept d left join emp e on d.deptno = e.deptno;

-- ---------- 补充:左连接做「员工-领导」 ----------
select e.ename as 员工, m.ename as 领导
from emp e left join emp m on e.mgr = m.empno;

-- ---------- 补充:sum 的外连接兜底 ----------
select d.dname, ifnull(sum(e.sal), 0) as 总工资
from dept d left join emp e on d.deptno = e.deptno
group by d.dname;

-- ---------- 7. OJ ----------
-- LeetCode 178:分数排名(8.0+ 窗口函数)
-- select score, dense_rank() over (order by score desc) as `rank` from scores order by score desc;
-- 5.7 相关子查询写法
-- select s1.score, (select count(distinct s2.score) from scores s2 where s2.score >= s1.score) as `rank`
-- from scores s1 order by s1.score desc;

-- LeetCode 626:换座位
-- select case
--          when id % 2 = 1 and id = (select max(id) from seat) then id
--          when id % 2 = 1 then id + 1
--          else id - 1
--        end as id, student
-- from seat order by id;

-- ---------- 补充:case when 做行转列 ----------
select deptno,
       sum(case when job = 'CLERK'   then 1 else 0 end) as 文员数,
       sum(case when job = 'MANAGER' then 1 else 0 end) as 经理数,
       sum(case when job = 'ANALYST' then 1 else 0 end) as 分析师数
from emp group by deptno;

如果这篇博客帮你理清了某个点,欢迎点赞收藏;有任何疑问,评论区见,我会一条条回。

相关推荐
涛思数据(TDengine)1 小时前
支撑药企全球化合规:TDengine 发布中美欧三地监管对照白皮书
大数据·数据库·人工智能·ai·tdengine
李兆龙的博客1 小时前
从一到无穷大 #106:Nimble 文件格式——超宽表的元数据、编码与索引设计
数据库·存储·parquet
boy快快长大1 小时前
MySQL 转 PostgreSQL 使用指南
数据库·mysql·postgresql
晴天¥1 小时前
Oracle集群中是该如何管理控制文件和日志文件组?
数据库·oracle
NineData1 小时前
NineData 社区版 V5.4.0 上线:715 条复制对比链路,AI Agent 新增表结构设计发布能力
大数据·数据库·人工智能·mysql·数据库管理工具·ninedata·数据库迁移工具
云策数链1 小时前
用友 T+Cloud / U8 项目交接时的账套健康度核查清单
数据库·oracle·erp·用友·云策数链
Tt有了t1 小时前
Redis的数据结构
数据结构·数据库·redis
ss2732 小时前
PostgreSQL咨询锁的使用
数据库·postgresql
ycydynq2 小时前
django 添加信息时,对外键设置条件
数据库·python·django