
文章目录
-
- [**0.实际场景:查询每个 `rs` 值对应的最新记录**](#0.实际场景:查询每个
rs
值对应的最新记录) - [**1. 什么是子查询?**](#1. 什么是子查询?)
- [**2. 子查询的分类**](#2. 子查询的分类)
-
- [**(1) 按结果类型分类**](#(1) 按结果类型分类)
- [**(2) 按执行位置分类**](#(2) 按执行位置分类)
- [**3. 子查询的实际应用**](#3. 子查询的实际应用)
-
- [**(1) 在 `WHERE` 子句中使用子查询**](#(1) 在
WHERE
子句中使用子查询) - [**(2) 在 `SELECT` 语句中使用子查询**](#(2) 在
SELECT
语句中使用子查询) - [**(3) 在 `FROM` 子句中使用子查询(派生表)**](#(3) 在
FROM
子句中使用子查询(派生表)) - [**(4) 使用 `EXISTS` 进行数据检查**](#(4) 使用
EXISTS
进行数据检查) - [**(5) 结合 `UPDATE` 或 `DELETE` 语句**](#(5) 结合
UPDATE
或DELETE
语句) -
- [**示例:将工资低于部门平均工资的员工加薪 10%**](#示例:将工资低于部门平均工资的员工加薪 10%)
- **示例:删除未分配员工的部门**
- [**(1) 在 `WHERE` 子句中使用子查询**](#(1) 在
- [**4. 子查询 vs `JOIN` 性能对比**](#4. 子查询 vs
JOIN
性能对比) -
- **子查询的性能劣势**
- [**`JOIN` 的优势**](#
JOIN
的优势) - **结论**
- [**5. 子查询优化技巧**](#5. 子查询优化技巧)
- [**6. 结论**](#6. 结论)
- [**0.实际场景:查询每个 `rs` 值对应的最新记录**](#0.实际场景:查询每个
在 MySQL 开发过程中,我们经常需要从同一张表中获取特定条件下的最新记录或进行去重查询。
这种场景下,子查询(Subquery) 是一种常用且强大的 SQL 技术。本文将通过一个实际案例,介绍子查询的概念、应用及优化方式。
0.实际场景:查询每个 rs
值对应的最新记录
场景描述
假设有一个 hd_com_locinfo
表,存储了一些位置信息,每个 rs
可能有多条记录,但 times
字段表示数据的时间,我们希望对 rs
进行去重,并保留 times
最新的那一条数据。
表结构示例
CREATE TABLE `hd_com_locinfo` (
`locid` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID,自增,chr+start+end+type可唯一确定一个rsid',
`rs` varchar(15) DEFAULT NULL COMMENT 'rs号,位点信息表',
`chr` char(5) NOT NULL COMMENT '染色体',
`start` int(11) NOT NULL COMMENT '起始位置',
`end` int(11) NOT NULL COMMENT '结束位置',
`type` varchar(30) NOT NULL COMMENT '突变类型,插入缺失dbsnp',
`ref` varchar(200) DEFAULT NULL COMMENT '参考基因,有时可能很长',
`gene_name` varchar(100) DEFAULT NULL COMMENT '基因名称',
`cchange` varchar(100) DEFAULT NULL COMMENT 'c.变化',
`pchange` varchar(100) DEFAULT NULL COMMENT 'p.变化',
`alt` varchar(200) DEFAULT NULL COMMENT '等位基因,多个使用英文逗号,分割',
`gchange` varchar(100) DEFAULT NULL COMMENT 'g.变化,主要是药物使用',
`frequency` varchar(15) DEFAULT NULL,
`gene_region` varchar(150) DEFAULT NULL COMMENT '基因区域',
`times` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`isshow` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用该项,0:关闭,1:开启',
PRIMARY KEY (`locid`) USING BTREE,
KEY `rs` (`rs`) USING BTREE,
KEY `start` (`start`) USING BTREE,
KEY `end` (`end`) USING BTREE,
KEY `type` (`type`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='位点信息表';
公司业务中实际场景
在小程序开发中,针对基因报告数据中,位点信息当两条就差一个一字段就相同的sql语句,现在业务场景需要只能保留最新的一条数据。如下图:
改造前:
Go中gorm的操作
go
func (g *Gabd3Dao) FindLikeLocinfoByRs(rs string) (result []map[string]any, err error) {
var hdLocinfo []model.HdComLocinfo
if err := g.Gabd3.Where("rs LIKE ?", rs+"%").Find(&hdLocinfo).Order("times desc").Group("rs").Error; err != nil {
return []map[string]any{}, err
}
for _, v := range hdLocinfo {
result = append(result, tools.JSONMethod(v))
}
return result, nil
}
改造后:
go
func (g *Gabd3Dao) FindLikeLocinfoByRs(rs string) (result []map[string]any, err error) {
var hdLocinfo []model.HdComLocinfo
// 使用子查询保留时间最新的记录
subQuery := g.Gabd3.Model(&model.HdComLocinfo{}).
Select("MAX(times) as latest_time, rs").
Where("rs LIKE ?", rs+"%").
Group("rs")
if err := g.Gabd3.Table("hd_com_locinfo h").
Select("h.*").
Joins("JOIN (?) sub ON h.rs = sub.rs AND h.times = sub.latest_time", subQuery).
Find(&hdLocinfo).Error; err != nil {
return nil, err
}
// 转换结果
for _, v := range hdLocinfo {
result = append(result, tools.JSONMethod(v))
}
return result, nil
}
或者是:
func (g *Gabd3Dao) FindLikeLocinfoByRs(rs string) (result []map[string]any, err error) {
var hdLocinfo []model.HdComLocinfo
query := `
SELECT h.*
FROM hd_com_locinfo h
JOIN (
SELECT rs, MAX(times) AS latest_time
FROM hd_com_locinfo
WHERE rs LIKE ?
GROUP BY rs
) sub ON h.rs = sub.rs AND h.times = sub.latest_time
`
if err := g.Gabd3.Raw(query, rs+"%").Scan(&hdLocinfo).Error; err != nil {
return nil, err
}
// 转换结果
for _, v := range hdLocinfo {
result = append(result, tools.JSONMethod(v))
}
return result, nil
}
改造后的纯SQL
使用
MAX(times)
计算每个rs
最新的times
,然后用JOIN
筛选出对应的完整记录:
sql
SELECT h.*
FROM hd_com_locinfo h
JOIN (
SELECT rs, MAX(times) AS latest_time
FROM hd_com_locinfo
WHERE rs LIKE "rs716274%"
GROUP BY rs
) sub ON h.rs = sub.rs AND h.times = sub.latest_time;
1. 什么是子查询?
子查询(Subquery) 是指嵌套在另一个 SQL 语句中的查询,它的执行结果可以作为主查询的输入。子查询可以用于 SELECT
、INSERT
、UPDATE
和 DELETE
语句中,通常用于筛选数据、计算聚合值、去重等场景。
子查询的主要作用:
- 数据筛选 (在
WHERE
或HAVING
子句中使用) - 计算某些字段的聚合值 (如
MAX()
、AVG()
) - 与
EXISTS
结合,用于检查数据是否存在 - 用作派生表(Derived Table),作为
FROM
子句的临时表 - 用于数据更新和删除
2. 子查询的分类
MySQL 中的子查询可以按照不同的执行方式分类:
(1) 按结果类型分类
- 标量子查询(Scalar Subquery):返回单个值
- 行子查询(Row Subquery):返回一行数据
- 表子查询(Table Subquery):返回多行多列数据
(2) 按执行位置分类
WHERE
子查询:用于筛选数据SELECT
子查询:用于计算某个字段的值FROM
子查询(派生表):作为临时表供主查询使用EXISTS
子查询:用于判断是否存在匹配数据
3. 子查询的实际应用
(1) 在 WHERE
子句中使用子查询
用于筛选符合特定条件的数据。
示例:查找工资高于公司平均工资的员工
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
- 子查询
SELECT AVG(salary) FROM employees
计算公司平均工资 - 主查询
WHERE salary >
只保留工资高于平均值的员工
(2) 在 SELECT
语句中使用子查询
子查询可以作为 SELECT
的一部分,为每条记录计算某些值。
示例:为每位员工显示其所在部门的平均工资
SELECT name, salary,
(SELECT AVG(salary) FROM employees e2 WHERE e2.department_id = e1.department_id) AS dept_avg_salary
FROM employees e1;
SELECT AVG(salary) ...
计算每个部门的平均工资AS dept_avg_salary
让主查询展示这个计算值
(3) 在 FROM
子句中使用子查询(派生表)
子查询可以在 FROM
语句中作为一个临时表使用,称为"派生表"(Derived Table)。
示例:查询每个部门工资最高的员工
SELECT department_id, MAX(salary) AS max_salary
FROM employees
GROUP BY department_id;
我们可以用这个子查询作为临时表:
SELECT e.name, e.salary, max_salaries.department_id
FROM employees e
JOIN (
SELECT department_id, MAX(salary) AS max_salary
FROM employees
GROUP BY department_id
) max_salaries
ON e.department_id = max_salaries.department_id AND e.salary = max_salaries.max_salary;
SELECT department_id, MAX(salary)
作为派生表JOIN
让主查询匹配工资最高的员工
(4) 使用 EXISTS
进行数据检查
EXISTS
用于判断子查询是否返回数据,而不是具体的数据内容。
示例:查询至少有一名员工的部门
SELECT department_id, department_name
FROM departments d
WHERE EXISTS (
SELECT 1 FROM employees e WHERE e.department_id = d.department_id
);
EXISTS
只关心子查询是否返回数据,提高查询效率
(5) 结合 UPDATE
或 DELETE
语句
子查询可以用于更新或删除特定数据。
示例:将工资低于部门平均工资的员工加薪 10%
UPDATE employees e1
SET salary = salary * 1.1
WHERE salary < (SELECT AVG(salary) FROM employees e2 WHERE e1.department_id = e2.department_id);
- 子查询
SELECT AVG(salary) ...
计算每个部门的平均工资 UPDATE
只更新工资低于平均值的员工
示例:删除未分配员工的部门
DELETE FROM departments
WHERE department_id NOT IN (SELECT DISTINCT department_id FROM employees);
- 子查询
SELECT DISTINCT department_id FROM employees
获取所有已分配的部门 DELETE
删除未在员工表中的部门
4. 子查询 vs JOIN
性能对比
子查询有时比 JOIN
慢,以下是对比分析:
子查询的性能劣势
-
子查询可能执行多次:
SELECT * FROM employees WHERE department_id IN (SELECT department_id FROM departments WHERE location = 'New York');
- 如果
departments
有 1000 行,每次主查询执行时,子查询可能执行 1000 次。
- 如果
-
子查询无法使用索引优化:
IN (SELECT ... FROM ...)
可能会导致全表扫描。
JOIN
的优势
-
JOIN 只执行一次:
SELECT e.* FROM employees e JOIN departments d ON e.department_id = d.department_id WHERE d.location = 'New York';
- 通过
JOIN
,MySQL 只执行一次表连接,提高效率。
- 通过
结论
- 当子查询返回大量数据时,建议使用
JOIN
,减少查询次数,提高性能。 - 如果子查询结果较小,并且仅用于存在性检查(
EXISTS
),子查询可能更高效。
5. 子查询优化技巧
1. 避免 IN
,优先使用 JOIN
mysql
-- 低效:
SELECT * FROM employees WHERE department_id IN (SELECT department_id FROM departments WHERE location = 'New York');
-- 推荐:
SELECT e.* FROM employees e JOIN departments d ON e.department_id = d.department_id WHERE d.location = 'New York';
2. 使用 EXISTS
替代 IN
mysql
-- 低效:
SELECT * FROM employees WHERE department_id IN (SELECT department_id FROM departments WHERE location = 'New York');
-- 更快:
SELECT * FROM employees e WHERE EXISTS (SELECT 1 FROM departments d WHERE e.department_id = d.department_id AND d.location = 'New York');
3. 添加适当的索引
-
如果
times
频繁用于MAX()
,建议创建索引:mysqlCREATE INDEX idx_times ON employees(times);
6. 结论
-
子查询是 MySQL 强大的查询工具,可用于数据筛选、计算、存在性检查等。
-
在数据量较大时,子查询可能比
JOIN
慢,应合理选择优化方案。 -
MySQL 8.0+ 提供了更强的优化机制,如
WITH RECURSIVE
,可以进一步优化查询性能。
在实际项目中,结合 JOIN
、EXISTS
和索引优化,可以提升 MySQL 查询性能!