[每周一更]-(第138期):MySQL 子查询详解:原理、应用及优化方案

文章目录

在 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 语句中的查询,它的执行结果可以作为主查询的输入。子查询可以用于 SELECTINSERTUPDATEDELETE 语句中,通常用于筛选数据、计算聚合值、去重等场景。

子查询的主要作用:

  1. 数据筛选 (在 WHEREHAVING 子句中使用)
  2. 计算某些字段的聚合值 (如 MAX()AVG()
  3. EXISTS 结合,用于检查数据是否存在
  4. 用作派生表(Derived Table),作为 FROM 子句的临时表
  5. 用于数据更新和删除

2. 子查询的分类

MySQL 中的子查询可以按照不同的执行方式分类:

(1) 按结果类型分类

  1. 标量子查询(Scalar Subquery):返回单个值
  2. 行子查询(Row Subquery):返回一行数据
  3. 表子查询(Table Subquery):返回多行多列数据

(2) 按执行位置分类

  1. WHERE 子查询:用于筛选数据
  2. SELECT 子查询:用于计算某个字段的值
  3. FROM 子查询(派生表):作为临时表供主查询使用
  4. 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) 结合 UPDATEDELETE 语句

子查询可以用于更新或删除特定数据。

示例:将工资低于部门平均工资的员工加薪 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 慢,以下是对比分析:

子查询的性能劣势

  1. 子查询可能执行多次

    复制代码
    SELECT * FROM employees WHERE department_id IN (SELECT department_id FROM departments WHERE location = 'New York');
    • 如果 departments 有 1000 行,每次主查询执行时,子查询可能执行 1000 次。
  2. 子查询无法使用索引优化

    • IN (SELECT ... FROM ...) 可能会导致全表扫描。

JOIN 的优势

  1. 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(),建议创建索引:

    mysql 复制代码
    CREATE INDEX idx_times ON employees(times);

6. 结论

  • 子查询是 MySQL 强大的查询工具,可用于数据筛选、计算、存在性检查等。

  • 在数据量较大时,子查询可能比 JOIN,应合理选择优化方案。

  • MySQL 8.0+ 提供了更强的优化机制,如 WITH RECURSIVE,可以进一步优化查询性能。

在实际项目中,结合 JOINEXISTS 和索引优化,可以提升 MySQL 查询性能!

相关推荐
快来卷java26 分钟前
JVM虚拟机篇(三):JVM运行时数据区与方法区详解
java·jvm·mysql
共享家952728 分钟前
Linux常用命令详解:从基础到进阶
linux·服务器·数据库
我是个假程序员4 小时前
sql server数据库可疑修复
数据库
极限实验室6 小时前
如何使用 Nginx 代理 Easysearch 服务
数据库·nginx
whn19776 小时前
selectdb修改表副本
数据库
TDengine (老段)7 小时前
TDengine 中的视图
数据库·物联网·oracle·时序数据库·tdengine·iotdb
Kyrie_Li8 小时前
Redis-Sentinel(哨兵模式)
数据库·redis·sentinel
计算机毕设定制辅导-无忧学长8 小时前
TDengine 数据写入优化:协议选择与批量操作(一)
网络·数据库·tdengine
Mr.洛 白8 小时前
OpenEuler/CentOS一键部署OpenGauss数据库教程(脚本+视频)
数据库·opengauss·gaussdb·国产数据库安装·安装脚本
炬火初现9 小时前
redis-cpp-cpp如何使用lua脚本
数据库·redis·lua