[每周一更]-(第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 查询性能!

相关推荐
一叶屋檐11 分钟前
Neo4j 图书馆借阅系统知识图谱设计
服务器·数据库·cypher
好吃的肘子1 小时前
MongoDB 应用实战
大数据·开发语言·数据库·算法·mongodb·全文检索
weixin_472339461 小时前
MySQL MCP 使用案例
数据库·mysql
lqlj22332 小时前
Spark SQL 读取 CSV 文件,并将数据写入 MySQL 数据库
数据库·sql·spark
遗憾皆是温柔3 小时前
MyBatis—动态 SQL
java·数据库·ide·sql·mybatis
未来之窗软件服务3 小时前
Cacti 未经身份验证SQL注入漏洞
android·数据库·sql·服务器安全
fengye2071614 小时前
在MYSQL中导入cookbook.sql文件
数据库·mysql·adb
拓端研究室TRL4 小时前
Python与MySQL网站排名数据分析及多层感知机MLP、机器学习优化策略和地理可视化应用|附AI智能体数据代码
人工智能·python·mysql·机器学习·数据分析
Ailovelearning4 小时前
neo4j框架:ubuntu系统中neo4j安装与使用教程
数据库·neo4j
_星辰大海乀5 小时前
表的设计、聚合函数
java·数据结构·数据库·sql·mysql·数据库开发