MySQL_流程控制_分组查询与连接查询详解

MySQL流程控制函数、分组查询与连接查询详解(附示例与运行结果)

  • 前言
  • 数据准备
  • 一、流程控制函数
    • [1.1 IF(expr1,expr2,expr3)](#1.1 IF(expr1,expr2,expr3))
    • [1.2 CASE函数](#1.2 CASE函数)
    • [1.3 实战案例](#1.3 实战案例)
  • 二、分组函数(聚合函数)
    • [2.1 五大聚合函数](#2.1 五大聚合函数)
    • [2.2 各函数适用数据类型与NULL处理总结](#2.2 各函数适用数据类型与NULL处理总结)
    • [2.3 聚合函数都忽略NULL值的验证](#2.3 聚合函数都忽略NULL值的验证)
    • [2.4 搭配DISTINCT去重](#2.4 搭配DISTINCT去重)
    • [2.5 COUNT(*) vs COUNT(字段) vs COUNT(1) 的区别](#2.5 COUNT(*) vs COUNT(字段) vs COUNT(1) 的区别)
  • [三、分组查询(GROUP BY)](#三、分组查询(GROUP BY))
    • [3.1 语法格式总结(含执行顺序标注)](#3.1 语法格式总结(含执行顺序标注))
    • [3.2 分组前筛选(WHERE)vs 分组后筛选(HAVING)](#3.2 分组前筛选(WHERE)vs 分组后筛选(HAVING))
    • [3.3 基础示例:按部门求平均工资、按领导求最高工资](#3.3 基础示例:按部门求平均工资、按领导求最高工资)
    • [3.4 复杂示例:查询员工个数大于2的部门](#3.4 复杂示例:查询员工个数大于2的部门)
    • [3.5 按表达式/函数分组(按姓名长度分组)](#3.5 按表达式/函数分组(按姓名长度分组))
    • [3.6 按多个字段分组(部门+工种)](#3.6 按多个字段分组(部门+工种))
    • [3.7 分组查询+排序组合](#3.7 分组查询+排序组合)
  • 四、连接查询
    • [4.1 连接查询概念与笛卡尔积现象](#4.1 连接查询概念与笛卡尔积现象)
    • [4.2 连接查询分类](#4.2 连接查询分类)
    • [4.3 SQL99语法格式总结](#4.3 SQL99语法格式总结)
    • [4.4 内连接(INNER JOIN)](#4.4 内连接(INNER JOIN))
    • [4.5 外连接](#4.5 外连接)
    • [4.6 交叉连接(CROSS JOIN)](#4.6 交叉连接(CROSS JOIN))
  • 总结

前言

在上一篇基础查询文章中,我们学习了 SELECTWHEREORDER BY 等基础查询语法,能够完成简单的单表数据检索。然而,真实的业务场景往往更加复杂:比如根据不同部门动态调整显示的工资、统计每个部门的平均工资、把员工表和部门表关联起来查询员工所在的部门名等。这些需求单靠基础查询是无法完成的,需要借助流程控制函数分组函数分组查询 以及连接查询来实现。

本文将围绕以下四大核心模块展开讲解:

  1. 流程控制函数IFCASE,用于在查询中做条件判断。
  2. 分组函数(聚合函数)SUMAVGMAXMINCOUNT,用于统计计算。
  3. 分组查询(GROUP BY) :配合聚合函数实现按组统计,并讲解 WHEREHAVING 的区别。
  4. 连接查询:内连接、外连接、交叉连接,把多个表的数据关联起来,是本文的重点也是实际开发中最常用的部分。

本文所有示例均基于 employeesdepartmentsjobsjob_gradesbeautyboys 等表,建议读者边看边在本地环境执行验证,加深理解。


数据准备

本文示例基于以下数据表(完整建表 SQL 见配套文件 00_数据准备_建表与数据.sql)。

employees 表(共 29 行,下表展示前 10 行代表性数据):

employee_id first_name last_name job_id salary commission_pct manager_id department_id hiredate
100 Steven King AD_PRES 24000 NULL NULL 90 1987-06-17
101 Neena Kochhar AD_VP 17000 NULL 100 90 1989-09-21
102 Lex De Haan AD_VP 17000 NULL 100 90 1993-01-13
103 Alexander Hunold IT_PROG 9000 NULL 102 60 1990-01-03
104 Bruce Ernst IT_PROG 6000 NULL 103 60 1991-05-21
105 David Austin IT_PROG 4800 NULL 103 60 1997-06-25
108 Nancy Greenberg FI_MGR 12000 NULL 101 100 1994-08-17
114 Den Raphaely PU_MAN 11000 NULL 100 30 1994-12-07
120 Matthew Weiss ST_MAN 8000 NULL 100 50 1996-07-01
145 John Russell SA_MAN 14000 0.40 100 80 1996-10-01
... ... ... ... ... ... ... ... ...

提示:其中 7 人有奖金率(employee_id 145-152),其余 22 人 commission_pct 为 NULL。工资范围 2500~24000。

departments 表(共 10 行):

department_id department_name manager_id location_id
10 Administration 200 1700
20 Marketing 201 1800
30 Purchasing 114 1700
40 Human Resources 203 2400
50 Shipping 121 1500
60 IT 103 1400
80 Sales 145 2500
90 Executive 100 1700
100 Finance 108 1700
110 Accounting 205 1700

job_grades 表(共 5 行):

grade_level lowest_sal highest_sal
A 10000 99999
B 8000 9999
C 6000 7999
D 4000 5999
E 0 3999

beauty 表(共 7 行):

id name sex borndate phone boyfriend_id
1 迪丽热巴 1992-06-03 17000000000 1
2 赵丽颖 1987-10-16 17000000001 2
3 杨幂 1986-08-12 17000000002 3
4 刘亦菲 1987-08-25 17000000003 NULL
5 Anglelababy 1989-02-28 17000000004 4
6 古力娜扎 1992-05-02 17000000005 NULL
7 景甜 1988-07-21 17000000006 5

boys 表(共 5 行):

id boyName userCP
1 鹿晗 800
2 冯绍峰 700
3 刘恺威 600
4 黄晓明 900
5 张继科 850

一、流程控制函数

流程控制函数允许我们在 SELECT 查询过程中根据条件动态地返回不同的值,类似于编程语言中的 if-elseswitch-case。MySQL 中常用的流程控制函数主要有两个:IFCASE

1.1 IF(expr1,expr2,expr3)

语法说明:

sql 复制代码
IF(expr1, expr2, expr3)
  • expr1:要判断的条件表达式,如果条件为真(非零或非空),则返回 expr2,否则返回 expr3
  • expr2:条件为真时返回的值。
  • expr3:条件为假时返回的值。

简单示例:

sql 复制代码
-- 判断2是否小于3,是则返回"大",否则返回"小"
SELECT IF(2 < 3, '大', '小') AS out_put;

运行结果:

out_put

业务示例: 查询员工的姓和名以及奖金率,如果有奖金率则返回"有",没有则返回"无",并以"备注"为列名。

sql 复制代码
/*
分析:
  查询的表:employees
  查询的字段:last_name、first_name、commission_pct、备注
  查询的条件:无
  排序的条件:无
*/
SELECT
  last_name,
  first_name,
  commission_pct,
  IF(commission_pct IS NULL, '无', '有') AS 备注
FROM
  employees;

运行结果(部分):

last_name first_name commission_pct 备注
King Steven NULL
Kochhar Neena NULL
De Haan Lex NULL
Russell John 0.40
Partners Karen 0.30
... ... ... ...

说明:commission_pctNULL 的员工(22人),备注列显示"无";有奖金率的员工(7人)显示"有"。IF 函数非常适合这种二选一的判断场景。


1.2 CASE函数

CASE 是一种流程控制函数,类似编程语言中的 switch 语法。它支持两种语法格式,分别适用于"等值匹配"和"条件判断"两种场景。

语法格式1:CASE value WHEN compare_value THEN result ...
sql 复制代码
CASE value
  WHEN compare_value1 THEN result1
  WHEN compare_value2 THEN result2
  ...
  [ELSE result]
END
  • value:需要进行比较的表达式或者列。
  • compare_value:进行比较的表达式或值。
  • result:当 value 等于 compare_value 时返回的结果。
  • [ELSE result]:如果没有任何 WHEN 条件匹配,则返回的默认结果。

这种格式适合"某列等于某个值就返回某个结果"的等值匹配场景。

语法格式2:CASE WHEN condition THEN result ...
sql 复制代码
CASE
  WHEN condition1 THEN result1
  WHEN condition2 THEN result2
  ...
  [ELSE result]
END
  • condition:条件表达式,可以是任何布尔表达式。
  • result:当条件表达式为真时返回的结果。
  • ELSE result:如果没有条件表达式为真时,返回的默认结果。

这种格式适合"满足某个条件就返回某个结果"的区间判断场景,更灵活,可以写复杂的逻辑判断。


1.3 实战案例

案例1:按部门号调整工资倍数(使用语法格式1)

需求: 查询员工的工资,要求如下:

  • 部门号 = 30,显示的工资为 1.1 倍
  • 部门号 = 40,显示的工资为 1.2 倍
  • 部门号 = 50,显示的工资为 1.3 倍
  • 其他部门,显示的工资为原工资
sql 复制代码
/*
分析:
  查询的表:employees
  查询的字段:salary、department_id、新工资
  查询的条件:无
  排序的条件:无
*/
-- 使用语法格式1:CASE department_id WHEN 30 THEN ...
SELECT
  salary,
  department_id,
  CASE department_id
    WHEN 30 THEN salary * 1.1
    WHEN 40 THEN salary * 1.2
    WHEN 50 THEN salary * 1.3
    ELSE salary
  END AS 新工资
FROM
  employees;

运行结果(部分):

salary department_id 新工资
24000 90 24000
17000 90 17000
3100 30 3410
2900 30 3190
8000 50 10400
... ... ...

这个案例用的是"语法格式1",因为判断条件是 department_id 等于某个值,属于等值匹配,用格式1更简洁。

案例1补充:使用语法格式2实现同样的效果

读者可以自行尝试使用语法格式2来改写上面的查询:

sql 复制代码
-- 使用语法格式2:CASE WHEN department_id = 30 THEN ...
SELECT
  salary,
  department_id,
  CASE
    WHEN department_id = 30 THEN salary * 1.1
    WHEN department_id = 40 THEN salary * 1.2
    WHEN department_id = 50 THEN salary * 1.3
    ELSE salary
  END AS 新工资
FROM
  employees;

两种写法效果完全一致。等值匹配场景推荐格式1,更简洁;区间判断场景只能用格式2。

案例2:按工资划分级别A/B/C/D(使用语法格式2)

需求: 查询员工的工资情况:

  • 如果工资大于 20000,显示 A 级别
  • 如果工资大于 15000,显示 B 级别
  • 如果工资大于 10000,显示 C 级别
  • 否则,显示 D 级别
sql 复制代码
SELECT
  salary,
  CASE
    WHEN salary > 20000 THEN 'A'
    WHEN salary > 15000 THEN 'B'
    WHEN salary > 10000 THEN 'C'
    ELSE 'D'
  END AS 工资级别
FROM
  employees;

运行结果(部分):

salary 工资级别
24000 A
17000 B
17000 B
9000 D
14000 C
... ...

注意:这里必须用"语法格式2",因为判断条件是 salary > 20000 这样的区间判断,而不是等于某个具体值。CASE WHEN 会从上到下依次判断,一旦某个 WHEN 条件为真就返回对应结果,不再往下匹配,所以要把更严格的条件(更大的数值)写在前面。


至此,流程控制函数就讲解完毕。它解决了"查询时根据条件动态返回不同值"的问题。接下来我们进入分组函数的学习,它是统计计算的基础。


二、分组函数(聚合函数)

分组函数,又称为聚合函数或统计函数或组函数,主要用于对一组数据进行统计计算,比如求总和、平均值、最大值、最小值、个数等。

2.1 五大聚合函数

MySQL 提供了五个常用的聚合函数:

函数 功能说明
SUM() 返回某列的总和
AVG() 返回某列的平均值
MAX() 返回某列的最大值
MIN() 返回某列的最小值
COUNT() 返回查询的行数

基础示例:

sql 复制代码
-- 1、SUM():查询所有员工的工资总和
SELECT SUM(salary) AS sum_sal FROM employees;

-- 2、AVG():查询所有员工的平均工资(保留2位小数)
SELECT ROUND(AVG(salary), 2) AS avg_sal FROM employees;

-- 3、MAX():查询最高工资
SELECT MAX(salary) AS max_sal FROM employees;

-- 4、MIN():查询最低工资
SELECT MIN(salary) AS min_sal FROM employees;

-- 5、COUNT():查询有工资的员工个数
SELECT COUNT(salary) AS count_sal FROM employees;

运行结果:

sum_sal avg_sal max_sal min_sal count_sal
251400 8668.97 24000 2500 29

也可以在一条语句中同时查询多个统计值:

sql 复制代码
-- 查询所有员工的工资总和、平均值、最高值、最低值以及总个数
SELECT
  SUM(salary),
  AVG(salary),
  MAX(salary),
  MIN(salary),
  COUNT(salary)
FROM
  employees;

运行结果:

SUM(salary) AVG(salary) MAX(salary) MIN(salary) COUNT(salary)
251400 8668.965517 24000 2500 29

2.2 各函数适用数据类型与NULL处理总结

不同聚合函数适用的数据类型不同,对 NULL 值的处理方式也不同,下面通过表格总结:

函数 适用数据类型 是否忽略NULL 说明
SUM() 数值型 不适合处理字符型和日期型
AVG() 数值型 不适合处理字符型和日期型
MAX() 数值型、字符型、日期型 适合处理任何类型
MIN() 数值型、字符型、日期型 适合处理任何类型
COUNT() 任何类型 适合处理任何类型,统计非NULL值的行数

核心结论:

  • SUMAVG 一般用于处理数值型,不建议用处理字符型和日期型。
  • MAXMINCOUNT 可以处理任何类型

验证示例:

sql 复制代码
-- ***************** 处理数值型 *****************
-- sum、avg不建议用处理字符型(结果无意义)
SELECT SUM(last_name), AVG(last_name) FROM employees;

-- sum、avg不建议用处理日期型(结果无意义)
SELECT SUM(hiredate), AVG(hiredate) FROM employees;

-- max、min适合处理任何类型(字符型按字典序比较)
SELECT MAX(last_name), MIN(last_name) FROM employees;

-- max、min适合处理任何类型(日期型按时间先后比较)
SELECT MAX(hiredate), MIN(hiredate) FROM employees;

-- count忽略null值,只统计非null的行
SELECT COUNT(commission_pct) FROM employees;

-- count适合处理任何类型
SELECT COUNT(last_name) FROM employees;

运行结果说明:

  • SUM(last_name)AVG(last_name) 虽然能执行,但结果是无意义的数字,不推荐使用。
  • MAX(last_name) 返回字典序最大的姓氏,MIN(last_name) 返回字典序最小的姓氏,结果有意义。
  • MAX(hiredate) 返回最晚入职日期,MIN(hiredate) 返回最早入职日期。
  • COUNT(commission_pct) 返回的是非 NULL 的奖金率行数(7),说明 COUNT 忽略了 NULL

2.3 聚合函数都忽略NULL值的验证

上面提到所有聚合函数都忽略 NULL 值,这里专门做一次验证。我们以 commission_pct(奖金率)字段为例,该字段有部分员工为 NULL

sql 复制代码
-- 验证sum、avg忽略null
-- employees表共29条记录,其中commission_pct不为null的有7条
-- SUM(commission_pct)/7 是实际平均值
-- SUM(commission_pct)/29 是错误平均值(除以了总行数)
SELECT
  SUM(commission_pct),
  AVG(commission_pct),
  SUM(commission_pct) / 7,
  SUM(commission_pct) / 29
FROM
  employees;

运行结果:

SUM(commission_pct) AVG(commission_pct) SUM/7 SUM/29
2.00 0.285714 0.2857 0.0690

说明:AVG(commission_pct) 的值等于 SUM(commission_pct) / 7,而不是 SUM(commission_pct) / 29,证明 AVGSUM 在计算时自动忽略了 NULL 值,分母用的是非 NULL 行数(7),而不是总行数(29)。

sql 复制代码
-- 验证max、min忽略null
SELECT MAX(commission_pct), MIN(commission_pct) FROM employees;

运行结果:

MAX(commission_pct) MIN(commission_pct)
0.40 0.20

MAXMIN 只在非 NULL 值中找最大最小,忽略 NULL

sql 复制代码
-- 验证count忽略null
SELECT COUNT(commission_pct) FROM employees;  -- 结果为7,而非29,说明忽略null
SELECT COUNT(last_name) FROM employees;       -- last_name无null,结果为29

运行结果:

COUNT(commission_pct) COUNT(last_name)
7 29

COUNT(commission_pct) 返回 7 而非 29,证明 COUNT(字段) 会忽略 NULL 值。


2.4 搭配DISTINCT去重

聚合函数可以和 DISTINCT 搭配使用,实现去重后再统计。

sql 复制代码
-- max、min搭配distinct去重
SELECT MAX(DISTINCT salary), MIN(salary) FROM employees;

-- sum搭配distinct去重,对比去重前后的总和
SELECT SUM(DISTINCT salary), SUM(salary) FROM employees;

运行结果:

MAX(DISTINCT salary) MIN(salary)
24000 2500
SUM(DISTINCT salary) SUM(salary)
196500 251400

说明:SUM(DISTINCT salary) 会先对 salary 去重再求和(23种不同工资),结果小于 SUM(salary)(含重复工资)。

sql 复制代码
-- count搭配distinct去重,统计有多少种不同的工资
SELECT COUNT(DISTINCT salary), COUNT(salary) FROM employees;

运行结果:

COUNT(DISTINCT salary) COUNT(salary)
23 29

COUNT(DISTINCT salary) 返回不同工资的种数(23),小于 COUNT(salary) 的 29。


2.5 COUNT(*) vs COUNT(字段) vs COUNT(1) 的区别

这是一个经典面试题,三者之间有细微但重要的区别:

sql 复制代码
-- 三种count写法
SELECT COUNT(salary) FROM employees;  -- 统计salary字段非null的行数
SELECT COUNT(*) FROM employees;      -- 统计总行数(包含null)
SELECT COUNT(1) FROM employees;       -- 统计总行数(包含null)

运行结果:

COUNT(salary) COUNT(*) COUNT(1)
29 29 29

本例中 salary 字段没有 NULL 值,所以三者结果相同。如果字段有 NULLCOUNT(字段) 会小于 COUNT(*)

三者区别总结:

写法 含义 是否忽略NULL 适用场景
COUNT(字段) 统计该字段非 NULL 的行数 需要排除 NULL 值时使用
COUNT(*) 统计表中总行数(包含 NULL 一般做统计时推荐使用
COUNT(1) 统计表中总行数(包含 NULL 效果等同 COUNT(*)

核心结论:

  • 一般使用 COUNT(*) 做统计函数,它统计的是总行数,不会忽略 NULL
  • COUNT(字段) 会忽略该字段为 NULL 的行。
  • COUNT(1)COUNT(*) 效果完全一致,在 MySQL 中 COUNT(*) 已经过优化,性能不低于 COUNT(1),推荐使用 COUNT(*)
  • 和聚合函数一同查询的字段,要求是 GROUP BY 关键字后面的字段,否则结果无意义(这一点在下一节分组查询中会详细说明)。

聚合函数本身只能对整个表或某个分组做统计。如果我们想"按部门分别统计平均工资",就需要把数据先分组,再对每一组做统计,这就是下一节"分组查询"要解决的问题。


三、分组查询(GROUP BY)

分组查询的作用是:将表中的数据按照某个或某些字段分成若干组,然后对每一组使用聚合函数进行统计。比如"查询每个部门的平均工资",就是先按 department_id 分组,再对每组求 AVG(salary)

3.1 语法格式总结(含执行顺序标注)

分组查询的完整语法格式如下,括号中的数字标注了各子句的执行顺序

sql 复制代码
SELECT                         -- (5) 最后执行,选择要查询的列
  分组函数,
  列名(要求出现在GROUP BY后面)
FROM                           -- (1) 先确定数据来源表
  表名
[WHERE 筛选条件]               -- (2) 分组前筛选
GROUP BY                       -- (3) 执行分组
  分组的列表
[HAVING 子句]                  -- (4) 分组后筛选
[ORDER BY 子句];               -- (6) 最后排序

执行顺序解读:

  1. FROM(1):首先确定从哪个表查询数据。
  2. WHERE(2):对原始表的数据进行分组前筛选,只保留满足条件的行。
  3. GROUP BY(3):按指定字段对筛选后的数据进行分组。
  4. HAVING(4):对分组后的结果集(虚表)进行筛选,只保留满足条件的分组。
  5. SELECT(5):选择要查询的列,包括分组函数和分组字段。
  6. ORDER BY(6):对最终结果进行排序。

注意:SELECTGROUP BY 之后执行,所以 SELECT 中出现的非聚合字段必须出现在 GROUP BY 后面,否则结果无意义。


3.2 分组前筛选(WHERE)vs 分组后筛选(HAVING)

这是分组查询中最重要的一个知识点。筛选条件分为两类:

对比维度 分组前筛选 分组后筛选
筛选的数据源 原始表 分组后的结果集(虚表)
子句位置 GROUP BY 子句的前面 GROUP BY 子句的后面
关键字 WHERE HAVING
能否用聚合函数 不能(此时还没分组) 能(此时已经分组完毕)

选择原则:

  1. 分组函数做条件,肯定是放在 HAVING 子句中。 因为分组函数必须等分组完成后才能计算,而 WHERE 在分组前执行,此时还没有分组结果,无法使用聚合函数。
  2. 能用分组前筛选(WHERE)的,优先考虑使用分组前筛选。 因为 WHERE 先过滤数据再分组,可以减少参与分组的数据量,提高性能。

简单记忆:WHERE 过滤的是行(分组前),HAVING 过滤的是组(分组后)。能用 WHERE 就别用 HAVING

另外,GROUP BY 子句还支持以下用法:

  • 单字段分组:按一个字段分组。
  • 多字段分组:多个字段之间使用逗号隔开,没有先后顺序(相当于多级分组)。
  • 按表达式或函数分组 :如按 LENGTH(last_name) 分组。
  • 添加排序 :排序放在整个分组查询的最后(ORDER BY)。

3.3 基础示例:按部门求平均工资、按领导求最高工资

示例1:查询邮箱中包含a字符的每个部门的平均工资
sql 复制代码
/*
分析:
  查询的表:employees
  查询的字段:salary、department_id
  查询的条件:email like '%a%'(分组前筛选,用WHERE)
  分组的字段:department_id
  排序的条件:无
*/
SELECT
  AVG(salary) AS avg_sal,
  department_id
FROM
  employees
WHERE
  email LIKE '%a%'
GROUP BY
  department_id;

运行结果(部分):

avg_sal department_id
5666.67 30
4900.00 50
6200.00 60
11500.00 80
17000.00 90
9000.00 100

分析:先用 WHERE email LIKE '%a%' 从原始表中筛选出邮箱包含 a 的员工(MySQL 中 LIKE 默认不区分大小写),再按 department_id 分组,对每组求 AVG(salary)。这里"邮箱包含a"是对原始行的筛选,所以用 WHERE

示例2:查询有奖金率的每个领导手下员工的最高工资
sql 复制代码
/*
分析:
  查询的表:employees
  查询的字段:salary、manager_id
  查询的条件:commission_pct is not null(分组前筛选,用WHERE)
  分组的字段:manager_id
  排序的条件:无
*/
SELECT
  MAX(salary) AS max_sal,
  manager_id
FROM
  employees
WHERE
  commission_pct IS NOT NULL
GROUP BY
  manager_id;

运行结果:

max_sal manager_id
14000 100
10000 145

分析:先用 WHERE commission_pct IS NOT NULL 筛选出有奖金率的员工(7人),再按 manager_id 分组。manager_id=100 有 4 人(145,146,147,149),最高工资 14000;manager_id=145 有 3 人(150,151,152),最高工资 10000。


3.4 复杂示例:查询员工个数大于2的部门

这个示例同时用到了分组前筛选和分组后筛选,是理解 WHEREHAVING 区别的经典案例。我们分步拆解:

第一步:查询每个部门的员工个数

sql 复制代码
-- 分析(1):查询每个部门的员工个数
SELECT
  COUNT(*),
  department_id
FROM
  employees
GROUP BY
  department_id;

运行结果:

COUNT(*) department_id
3 30
9 50
5 60
7 80
3 90
2 100

此时得到了每个部门的员工个数,但还没办法直接筛选"个数大于2"的部门,因为 COUNT(*) 是分组后才计算出来的。

第二步:根据第一步的结果进行筛选,员工个数大于2

sql 复制代码
-- 分析(2):根据(1)的查询结果进行筛选,员工个数大于2
SELECT
  COUNT(*),
  department_id
FROM
  employees
GROUP BY
  department_id
HAVING
  COUNT(*) > 2;

运行结果:

COUNT(*) department_id
3 30
9 50
5 60
7 80
3 90

关键点:COUNT(*) > 2 这个条件是对分组后的结果做筛选,因为 COUNT(*) 是聚合函数,必须放在 HAVING 中,不能用 WHERE。部门 100 只有 2 人,被过滤掉了。

更多复杂筛选示例

示例:查询每个工种有奖金率的员工的最高工资大于12000的工种编号和最高工资

分步拆解:

sql 复制代码
-- 分析(1):查询每个工种有奖金率的员工的最高工资
SELECT
  MAX(salary) AS max_sal,
  job_id
FROM
  employees
WHERE
  commission_pct IS NOT NULL    -- 分组前筛选:有奖金率
GROUP BY
  job_id;
sql 复制代码
-- 分析(2):根据(1)的结果继续筛选,最高工资大于12000
SELECT
  MAX(salary) AS max_sal,
  job_id
FROM
  employees
WHERE
  commission_pct IS NOT NULL    -- 分组前筛选:用WHERE
GROUP BY
  job_id
HAVING
  max_sal > 12000;              -- 分组后筛选:用HAVING(因为max_sal是聚合结果)

运行结果:

max_sal job_id
14000 SA_MAN

同时用到了 WHERE(分组前筛选有奖金率的员工)和 HAVING(分组后筛选最高工资大于12000的工种),两者各司其职。SA_REP 工种最高工资 10000 不满足 >12000,被过滤。

示例:查询领导编号大于102的每个领导手下的最低工资大于5000的领导编号以及最低工资

分步拆解:

sql 复制代码
-- 分析(1):查询每个领导手下的员工最低工资
SELECT
  MIN(salary),
  manager_id
FROM
  employees
GROUP BY
  manager_id;
sql 复制代码
-- 分析(2):根据(1)的结果继续添加筛选条件:领导编号大于102
SELECT
  MIN(salary),
  manager_id
FROM
  employees
WHERE
  manager_id > 102              -- 分组前筛选:用WHERE
GROUP BY
  manager_id;
sql 复制代码
-- 分析(3):根据(2)的结果继续筛选,最低工资大于5000
SELECT
  MIN(salary),
  manager_id
FROM
  employees
WHERE
  manager_id > 102              -- 分组前筛选:用WHERE
GROUP BY
  manager_id
HAVING
  MIN(salary) > 5000;          -- 分组后筛选:用HAVING

运行结果:

MIN(salary) manager_id
9000 108
5800 120
9000 145

这个示例完美展示了 WHEREHAVING 的配合使用:manager_id > 102 是对原始行的筛选用 WHEREMIN(salary) > 5000 是对分组结果的筛选用 HAVING


3.5 按表达式/函数分组(按姓名长度分组)

GROUP BY 不仅支持按字段分组,还支持按表达式或函数的结果分组。

需求: 按员工姓名的长度分组,查询每一组的员工个数,筛选员工个数大于5的有哪些。

分步拆解:

sql 复制代码
-- 分析(1):查询每个姓名长度的员工个数
SELECT
  COUNT(*),
  LENGTH(CONCAT(last_name, first_name)) AS len_name
FROM
  employees
GROUP BY
  LENGTH(CONCAT(last_name, first_name));
sql 复制代码
-- 分析(2):添加筛选条件,员工个数大于5
SELECT
  COUNT(*),
  LENGTH(CONCAT(last_name, first_name)) AS len_name
FROM
  employees
GROUP BY
  LENGTH(CONCAT(last_name, first_name))
HAVING
  COUNT(*) > 5;

运行结果:

COUNT(*) len_name
6 12
8 13

分析:先用 CONCAT(last_name, first_name) 把姓和名拼接,再用 LENGTH() 计算姓名长度,按这个长度值分组,统计每组人数,最后用 HAVING 筛选人数大于5的组。姓名长度为12的有6人,为13的有8人。


3.6 按多个字段分组(部门+工种)

GROUP BY 支持按多个字段分组,字段之间用逗号隔开,没有先后顺序之分。多字段分组相当于"多级分组",先按第一个字段分大组,再在大组内按第二个字段分小组。

需求: 查询每个部门每个工种的员工的平均工资

sql 复制代码
SELECT
  AVG(salary),
  department_id,
  job_id
FROM
  employees
GROUP BY
  department_id, job_id;

运行结果(部分):

AVG(salary) department_id job_id
11000 30 PU_MAN
3000 30 PU_CLERK
7650 50 ST_MAN
3500 50 ST_CLERK
5760 60 IT_PROG
12500 80 SA_MAN
9500 80 SA_REP
24000 90 AD_PRES
17000 90 AD_VP
12000 100 FI_MGR
9000 100 FI_ACCOUNT
... ... ...

分析:先按 department_id 分部门,再在每个部门内按 job_id 分工种,最后对每个"部门+工种"小组求平均工资。


3.7 分组查询+排序组合

排序(ORDER BY)放在整个分组查询的最后,对最终结果进行排序。

需求: 查询每个部门每个工种的平均工资,筛选平均工资大于10000的,并按平均工资降序显示。

sql 复制代码
SELECT
  AVG(salary),
  department_id,
  job_id
FROM
  employees
WHERE
  department_id IS NOT NULL      -- 分组前筛选:排除没有部门的员工
GROUP BY
  department_id, job_id          -- 按部门+工种分组
HAVING
  AVG(salary) > 10000           -- 分组后筛选:平均工资大于10000
ORDER BY
  AVG(salary) DESC;              -- 按平均工资降序排序

运行结果:

AVG(salary) department_id job_id
24000 90 AD_PRES
17000 90 AD_VP
12500 80 SA_MAN
12000 100 FI_MGR

这个示例完整地展示了分组查询的全部子句:FROMWHERE(分组前筛选)→ GROUP BY(分组)→ HAVING(分组后筛选)→ SELECT(选择列)→ ORDER BY(排序)。是分组查询的综合应用。


到目前为止,我们学习的所有查询都是单表查询。但在实际开发中,数据往往分散在多个表中,需要把多个表关联起来查询,这就是下一节"连接查询"的内容,也是本文的核心重点。


四、连接查询

4.1 连接查询概念与笛卡尔积现象

连接查询 ,又称为多表查询,当查询的字段来自多个表时,就会用到连接查询。比如查询"员工名和部门名",员工名在 employees 表,部门名在 departments 表,需要把两张表关联起来。

但是,如果直接这样写:

sql 复制代码
-- 需求:查询所有女明星对应的男朋友(错误写法,会产生笛卡尔积)
SELECT `name`, boyName
FROM beauty, boys;   -- 没有连接条件,产生笛卡尔积!

就会产生笛卡尔积现象

什么是笛卡尔积?

笛卡尔积是两个或多个表之间的连接操作。当表1有 m 行,表2有 n 行,如果没有有效的连接条件,结果会变成 m × n 行。比如 beauty 表有 7 行,boys 表有 5 行,不加连接条件直接查,结果就是 7 × 5 = 35 行,其中大部分是无意义的组合。

笛卡尔积产生的条件:

  1. 省略连接条件。
  2. 连接条件无效。
  3. 所有表中的所有行互相连接。

避免笛卡尔积的方法:

WHERE 子句后面加入有效的连接条件:

sql 复制代码
-- 正确写法:添加连接条件
SELECT `name`, boyName
FROM beauty, boys
WHERE beauty.boyfriend_id = boys.id;

运行结果:

name boyName
迪丽热巴 鹿晗
赵丽颖 冯绍峰
杨幂 刘恺威
Anglelababy 黄晓明
景甜 张继科

此时只返回满足 beauty.boyfriend_id = boys.id 的记录(5条),避免了无意义的笛卡尔积。刘亦菲和古力娜扎的 boyfriend_id 为 NULL,不匹配。


4.2 连接查询分类

连接查询可以从两个维度进行分类:

按年代分类:

分类 说明
SQL92标准 老标准,已被淘汰,了解即可。连接条件写在 WHERE 后面
SQL99标准 推荐使用。支持内连接 + 外连接(左外、右外)+ 交叉连接。连接条件写在 ON 后面

按功能分类:

复制代码
(1)内连接
    a、等值连接
    b、非等值连接
    c、自连接
(2)外连接
    a、左外连接
    b、右外连接
    c、全外连接
(3)交叉连接

本文重点讲解 SQL99 标准(推荐使用),连接条件放在 ON 后面,筛选条件放在 WHERE 后面,分离性好,便于阅读。


4.3 SQL99语法格式总结

SQL99 标准的连接查询语法格式如下,括号中的数字标注了执行顺序:

sql 复制代码
SELECT                         -- (7) 选择要查询的列
  查询列表
FROM                           -- (1) 确定主表
  表1 别名
[连接类型] JOIN                -- (3) 关联从表
  表2 别名
ON                             -- (2) 连接条件
  连接条件
[WHERE 筛选条件]               -- (4) 分组前筛选
[GROUP BY 子句]                -- (5) 分组
[HAVING 筛选条件]              -- (6) 分组后筛选
[ORDER BY 子句];               -- (8) 排序

连接类型关键字:

连接类型 关键字
内连接 INNER(可省略)
左外连接 LEFT [OUTER]
右外连接 RIGHT [OUTER]
全外连接 FULL [OUTER](MySQL不支持)
交叉连接 CROSS

4.4 内连接(INNER JOIN)

内连接查询的是两个表的交集,即只返回满足连接条件的记录。

语法格式:

sql 复制代码
SELECT 查询列表
FROM 表1 别名
INNER JOIN 表2 别名
ON 连接条件;

内连接特点:

  1. 可以添加排序、分组、筛选。
  2. INNER 关键字可以省略(省略后默认就是内连接)。
  3. 筛选条件放在 WHERE 后面,连接条件放在 ON 后面,提高分离性,便于阅读。

内连接分为三种:等值连接非等值连接自连接


4.4.1 等值连接(员工名+部门名)

等值连接是指连接条件使用 = 进行判断的连接。

示例1:查询员工名、部门名

sql 复制代码
/*
分析:
  查询的表:employees、departments
  查询的字段:first_name、department_name
  查询条件:无
*/
SELECT
  e.first_name,
  d.department_name
FROM employees AS e
INNER JOIN departments AS d
  ON e.department_id = d.department_id;

运行结果(部分):

first_name department_name
Steven Executive
Neena Executive
Lex Executive
Alexander IT
Bruce IT
... ...

分析:连接条件是 e.department_id = d.department_id,用 = 判断两个表的部门编号是否相等,属于等值连接。使用表别名 ed 简化书写。

示例2:查询名字中包含a的员工名和工种名(添加筛选条件)

sql 复制代码
SELECT
  first_name,
  job_title
FROM employees e
INNER JOIN jobs j
  ON e.job_id = j.job_id
WHERE
  e.first_name LIKE '%a%';

运行结果(部分):

first_name job_title
Neena Administration Vice President
Alexander Programmer
David Programmer
Valli Programmer
... ...

分析:连接条件 e.job_id = j.job_id 放在 ON 后面,筛选条件 e.first_name LIKE '%a%' 放在 WHERE 后面,分离清晰。


4.4.2 非等值连接(工资级别匹配,BETWEEN AND)

非等值连接是指连接条件不使用 =,而是使用 BETWEEN AND>< 等区间判断的连接。

示例1:查询员工的工资级别

sql 复制代码
/*
分析:
  查询的表:employees、job_grades
  查询思路:查询employees表中的salary字段在job_grades的最低工资和最高工资区间内,确定等级
*/
SELECT
  salary,
  grade_level
FROM
  employees e
INNER JOIN job_grades g
  ON e.salary BETWEEN g.lowest_sal AND g.highest_sal;

运行结果(部分):

salary grade_level
24000 A
17000 A
9000 B
6000 C
4800 D
4200 D
14000 A
3500 E
2500 E
... ...

分析:连接条件是 e.salary BETWEEN g.lowest_sal AND g.highest_sal,用 BETWEEN AND 判断员工工资落在哪个工资级别区间,属于非等值连接。

示例2:查询工资级别的个数大于20的级别,并按工资级别降序(内连接+分组+筛选+排序)

sql 复制代码
SELECT
  COUNT(*),
  grade_level
FROM
  employees e
INNER JOIN job_grades g
  ON e.salary BETWEEN g.lowest_sal AND g.highest_sal
GROUP BY
  grade_level
HAVING
  COUNT(*) > 20
ORDER BY
  grade_level DESC;

运行结果:

(无结果。各等级人数:A=10, B=6, C=3, D=4, E=6,均不超过20。)

分析:先内连接确定每个员工的工资级别,再按 grade_level 分组统计人数,用 HAVING 筛选人数大于20的级别。本数据集中各等级人数均不超过20,故返回空结果。读者可将条件改为 COUNT(*) > 5 来测试。


4.4.3 自连接(员工-上级关系)

自连接是指一张表自己和自己连接,通过给同一张表起不同的别名来区分。常用于树形结构或层级关系查询。

示例1:查询员工的名字、上级的名字

sql 复制代码
SELECT
  e.first_name AS 员工,
  m.first_name AS 领导
FROM
  employees e
INNER JOIN employees m
  ON e.manager_id = m.employee_id;

运行结果(部分):

员工 领导
Neena Steven
Lex Steven
Alexander Lex
Bruce Alexander
David Alexander
Nancy Neena
... ...

分析:employees 表既当员工表(别名 e)又当领导表(别名 m),连接条件是 e.manager_id = m.employee_id(员工的领导编号等于领导的员工编号)。注意 King 是最高领导没有上级(manager_id 为 NULL),所以不会出现在结果中(内连接只返回有匹配的记录)。共返回 28 条记录。

示例2:查询名字中包含字符a的员工名字和上级名字

sql 复制代码
SELECT
  e.first_name AS 员工,
  m.first_name AS 领导
FROM
  employees e
INNER JOIN employees m
  ON e.manager_id = m.employee_id
WHERE
  e.first_name LIKE '%a%';

运行结果(部分):

员工 领导
Neena Steven
Alexander Lex
David Alexander
Valli Alexander
Den Steven
Adam Steven
... ...

分析:在自连接的基础上增加 WHERE 筛选,只查名字中包含 a 的员工。


4.4.4 多表内连接(三表JOIN)

内连接可以连续关联多张表,语法上就是连续使用多个 INNER JOIN ... ON

示例:查询员工名、部门名、工种名,并按部门名降序

sql 复制代码
/*
分析:
  查询的表:employees、departments、jobs
  查询的字段:first_name、department_name、job_title
  连接条件:
    e.department_id = d.department_id(员工-部门)
    e.job_id = j.job_id(员工-工种)
  排序:department_name DESC
*/
SELECT
  first_name,
  department_name,
  job_title
FROM employees e
INNER JOIN departments d
  ON e.department_id = d.department_id
INNER JOIN jobs j
  ON e.job_id = j.job_id
ORDER BY
  department_name DESC;

运行结果(部分):

first_name department_name job_title
John Sales Sales Manager
Karen Sales Sales Manager
Steven Shipping Stock Manager
... ... ...

分析:三表内连接,先用 e.department_id = d.department_id 关联员工和部门,再用 e.job_id = j.job_id 关联员工和工种,最后按部门名降序排序。


4.4.5 内连接+分组+筛选+排序综合示例

示例1:查询部门个数大于3的城市名和部门个数(内连接+分组+筛选)

分步拆解:

sql 复制代码
-- 分析(1):查询每个城市的部门个数
SELECT
  COUNT(*) AS num,
  city
FROM departments d
INNER JOIN locations l
  ON d.location_id = l.location_id
GROUP BY
  city;
sql 复制代码
-- 分析(2):根据(1)的结果筛选部门个数大于3的城市
SELECT
  COUNT(*) AS num,
  city
FROM departments d
INNER JOIN locations l
  ON d.location_id = l.location_id
GROUP BY
  city
HAVING
  num > 3;

运行结果:

num city
5 Seattle

Seattle(location_id=1700)有 5 个部门(10, 30, 90, 100, 110),是唯一满足个数大于 3 的城市。

示例2:查询哪个部门的员工个数大于3的部门名和员工个数,并按个数降序(内连接+分组+筛选+排序)

分步拆解:

sql 复制代码
-- 分析(1):查询每个部门的员工个数
SELECT
  COUNT(*) AS num,
  department_name
FROM employees e
INNER JOIN departments d
  ON e.department_id = d.department_id
GROUP BY
  department_name;
sql 复制代码
-- 分析(2):根据(1)的结果筛选员工个数大于3的并排序
SELECT
  COUNT(*) AS num,
  department_name
FROM employees e
INNER JOIN departments d
  ON e.department_id = d.department_id
GROUP BY
  department_name
HAVING
  num > 3
ORDER BY
  COUNT(*) DESC;

运行结果:

num department_name
9 Shipping
7 Sales
5 IT

分析:先内连接员工表和部门表,按部门名分组统计人数,用 HAVING 筛选人数大于3,最后按人数降序排序。Shipping(50)有9人,Sales(80)有7人,IT(60)有5人。


4.5 外连接

外连接用于查询一个表中有,但另一个表中没有的记录,以及两表匹配的记录。

应用场景与主表/从表概念

外连接的应用场景是:用于查询一个表中有、另一个表中没有的记录。

主表和从表:

  • 左外连接(LEFT JOIN)LEFT JOIN 左边是主表,右边是从表。
  • 右外连接(RIGHT JOIN)RIGHT JOIN 右边是主表,左边是从表。
外连接特点总结

外连接的核心特点:

  1. 外连接的查询结果为主表中的所有记录。 如果从表中有和它匹配的,则显示匹配的值;如果从表中没有和它匹配的,则显示 NULL

    • 外连接查询结果 = 内连接结果 + 主表中有但从表中没有的记录
  2. 左外连接LEFT JOIN 左边是主表,右边是从表;右外连接RIGHT JOIN 右边是主表,左边是从表。

  3. 左外和右外交换两个表的顺序,可以实现相同的结果。

  4. 全外连接 = 内连接结果 + 表1中有但表2中没有的 + 表2中有但表1中没有的。

维恩图概念理解(文字描述)

如果把表A和表B想象成两个相交的圆(维恩图):

  • 左外连接(LEFT JOIN):左圆(A)的全部 ------ 包括A独有的部分 + A和B的交集。
  • 右外连接(RIGHT JOIN):右圆(B)的全部 ------ 包括B独有的部分 + A和B的交集。
  • 内连接(INNER JOIN):中间交集部分 ------ 只有A和B共有的部分。
  • 全外连接(FULL JOIN):两个圆的并集 ------ A独有 + 交集 + B独有。
  • LEFT JOIN + WHERE B IS NULL:左圆独有部分(A - B)------ A有但B没有的记录。
  • RIGHT JOIN + WHERE A IS NULL:右圆独有部分(B - A)------ B有但A没有的记录。

用表格对比LEFT/RIGHT/FULL JOIN
连接类型 关键字 保留的记录 未匹配时的处理
左外连接 LEFT JOINLEFT OUTER JOIN 左表全部记录 右表字段填充 NULL
右外连接 RIGHT JOINRIGHT OUTER JOIN 右表全部记录 左表字段填充 NULL
全外连接 FULL JOINFULL OUTER JOIN 两表全部记录 缺失方字段填充 NULL
对比维度 LEFT JOIN RIGHT JOIN FULL JOIN
保留表 左表全部 右表全部 两表全部
匹配失败填充 右表字段为 NULL 左表字段为 NULL 对应方字段为 NULL
是否可互换 可以(调换表顺序) 可以(调换表顺序) 不可以(需 UNION 模拟)
MySQL原生支持 支持 支持 不支持
用表格对比各JOIN的结果集范围
查询 包含A独有 包含交集 包含B独有 结果集范围
LEFT JOIN A全部
RIGHT JOIN B全部
INNER JOIN 交集
LEFT JOIN + WHERE B IS NULL A独有(A-B)
RIGHT JOIN + WHERE A IS NULL B独有(B-A)
FULL JOIN 并集
MySQL不支持全外连接的说明

MySQL 原生不支持 FULL JOIN(全外连接),如果执行 SELECT * FROM A FULL JOIN B ON ... 会报语法错误。如果需要实现全外连接的效果,可以使用 LEFT JOINRIGHT JOIN 的结果通过 UNION 合并来模拟:

sql 复制代码
-- MySQL模拟全外连接
SELECT * FROM A LEFT JOIN B ON A.id = B.id
UNION
SELECT * FROM A RIGHT JOIN B ON A.id = B.id;

外连接示例

引入示例:查询没有男朋友的女神(男朋友不在男神表中的女神名)

左外连接写法:

sql 复制代码
-- 左外连接:beauty是主表,boys是从表
SELECT
  b.name, bo.boyName
FROM
  beauty b
  LEFT JOIN boys bo
    ON b.boyfriend_id = bo.id
WHERE
  bo.id IS NULL;

运行结果:

name boyName
刘亦菲 NULL
古力娜扎 NULL

分析:先用左外连接保留 beauty 表的全部记录(包括没有匹配男朋友的女神),再用 WHERE bo.id IS NULL 筛选出"从表没有匹配"的记录,即没有男朋友的女神。刘亦菲和古力娜扎的 boyfriend_id 为 NULL,所以没有匹配。

示例:查询哪个部门没有员工

sql 复制代码
-- 左外连接:部门表是主表,员工表是从表
SELECT
  d.department_id,
  d.department_name,
  e.employee_id
FROM
  departments d
  LEFT JOIN employees e
    ON d.department_id = e.department_id
WHERE
  e.employee_id IS NULL;

运行结果:

department_id department_name employee_id
10 Administration NULL
20 Marketing NULL
40 Human Resources NULL
110 Accounting NULL

分析:先用左外连接保留 departments 表的全部记录(包括没有员工的部门),再用 WHERE e.employee_id IS NULL 筛选出"员工表没有匹配"的记录,即没有员工的部门。部门 10、20、40、110 在 employees 表中没有对应记录。


4.6 交叉连接(CROSS JOIN)

交叉连接就是返回两个表的笛卡尔积,即表1有 m 行、表2有 n 行,结果为 m × n 行。实际开发中很少使用,但需要了解。

sql 复制代码
-- 交叉连接(笛卡尔积)
SELECT
  *
FROM
  beauty b
  CROSS JOIN boys bo;

运行结果:

(返回 7 × 5 = 35 条记录,即 beauty 表的每一行都与 boys 表的每一行组合。)

说明:CROSS JOIN 不需要 ON 连接条件,结果就是两表的笛卡尔积。在实际开发中应尽量避免无意义的交叉连接,除非确实需要生成所有组合。


总结

本文系统讲解了 MySQL 中四大核心查询知识,回顾如下:

  1. 流程控制函数

    • IF(expr1, expr2, expr3):简单的二选一条件判断,条件为真返回 expr2,为假返回 expr3
    • CASE 函数:支持两种语法格式。格式1适合等值匹配(CASE value WHEN ...),格式2适合区间判断(CASE WHEN condition THEN ...)。注意 CASE WHEN 从上到下匹配,更严格的条件要写在前面。
  2. 分组函数(聚合函数)

    • 五大函数:SUMAVGMAXMINCOUNT
    • SUMAVG 适合数值型;MAXMINCOUNT 适合任何类型。
    • 所有聚合函数都忽略 NULL 值。
    • 可搭配 DISTINCT 去重。
    • COUNT(*) 统计总行数(推荐),COUNT(字段) 忽略 NULLCOUNT(1) 等效 COUNT(*)
  3. 分组查询(GROUP BY)

    • 语法执行顺序:FROMWHEREGROUP BYHAVINGSELECTORDER BY
    • WHERE 是分组前筛选(对原始行),HAVING 是分组后筛选(对分组结果)。能用 WHERE 优先用 WHERE
    • 支持按单字段、多字段、表达式/函数分组,支持与排序组合。
  4. 连接查询(核心重点):

    • 笛卡尔积现象:多表查询不加有效连接条件会产生 m×n 的无意义结果。
    • SQL99 标准:连接条件放 ON,筛选条件放 WHERE,分离性好。
    • 内连接INNER JOIN):查交集,分为等值连接、非等值连接(BETWEEN AND)、自连接、多表连接。
    • 外连接 :左外(LEFT JOIN,左表全部)、右外(RIGHT JOIN,右表全部)。结果 = 内连接 + 主表有但从表没有的记录。MySQL 不支持全外连接(可用 UNION 模拟)。
    • 交叉连接CROSS JOIN):笛卡尔积。

其中,连接查询是本文的核心重点,也是实际开发中最常用的查询技术。特别是内连接的等值连接、非等值连接、自连接三种形式,以及左外连接和右外连接的区别与互换,需要重点掌握。建议读者结合本文示例,在自己的 MySQL 环境中反复练习,才能真正融会贯通。

相关推荐
Bode_20021 小时前
全生命周期 SQL 控制
数据库·智能制造
西西弗Sisyphus1 小时前
Qt 在无边框窗口上做一套换肤系统
开发语言·数据库·qt
神仙别闹1 小时前
基于 QT(C++)实现操作系统
数据库·c++·qt
Broccoli523026651 小时前
FastAPI + SQLAlchemy 异步会话(AsyncSession)核心方法
数据库·oracle·fastapi
harmony&1 小时前
MySQL 运维实战:从 DQL 查询到 MHA 高可用架构全解析
mysql
坐吃山猪2 小时前
【多线程】FutureTask多线程底层实现
java·开发语言·数据库
weixin199701080162 小时前
[特殊字符]《从0到1:闲鱼开放平台授权登录 + AccessToken 刷新 + 聚石塔部署完整链路》(附Python源码)
java·数据库·python
cmes_love2 小时前
国内期货Level2五档行情与逐笔成交数据介绍
数据库·区块链
cmes_love2 小时前
CME和CBOT外盘期货数据下载资源介绍
数据库·区块链