提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- [一、SQL 各关键字执行顺序](#一、SQL 各关键字执行顺序)
-
- 1.基本sql查询顺序
- 2.相关子查询
- 3.非相关子查询
- [4.FROM 中的子查询(派生表)](#4.FROM 中的子查询(派生表))
- 二、SQL中各索引整理
- 总结
前言
在面试或者日常开发中有关sql 知识是必须要掌握的,现总结下mysql 中一些基本知识,为后面的sql 优化做一些知识准备,该篇文章主要讲了sql中关键字的执行顺序,如何编写一个好sql及 索引基础知识的整理,有关索引的学习非常重要。
一、SQL 各关键字执行顺序
1.基本sql查询顺序
执行顺序从 上到下 依次执行
| 关键字 | 解释含义 |
|---|---|
| FROM | 从指定表/视图 加载数据 |
| ON | 表字段间的连接条件 |
| JOIN | 表之间的连接操作 |
| WHERE | 过滤基础表/结果集 中的数据 |
| GROUP BY | 对筛选后的结果集进行分组 |
| HAVING | 对分组后的结果集再进行过滤 |
| SELECT(包括计算表达式) | 选择返回的列 |
| DISTINCT | 去除列的重复数据 |
| ORDER BY | 对结果集进行排序 |
| LIMIT / OFFSET | 限制或偏移要返回的数据量 |
1.下面是相关示例的sql,理解执行顺序
sql
SELECT DISTINCT
d.name AS department,
COUNT(*) AS emp_count,
AVG(e.salary) AS avg_salary
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE e.hire_date > '2020-01-01'
AND d.location = '上海'
GROUP BY d.id, d.name
HAVING AVG(e.salary) > 8000
AND COUNT(*) >= 3
ORDER BY avg_salary DESC
LIMIT 5;
分阶段执执行演示:
1.1 数据准备阶段
sql
-- 1. FROM 和 JOIN:确定数据源和连接
FROM employees e
JOIN departments d ON e.dept_id = d.id
-- 执行流程:
-- a. 执行笛卡尔积(如果没有ON条件)
-- b. 应用ON条件过滤连接结果
-- c. 如果是外连接,添加NULL行
1.2 数据过滤阶段
sql
-- 2. WHERE:行级过滤
WHERE e.salary > 5000
AND e.status = 'active'
AND d.location = '北京'
-- ⚠️ 注意:WHERE 在 GROUP BY 之前执行
-- 所以不能在这里使用聚合函数
-- ❌ WHERE SUM(salary) > 10000 -- 错误!
1.3 分组聚合阶段
sql
-- 3. GROUP BY:分组
GROUP BY e.dept_id, d.name
-- 4. HAVING:分组后过滤
HAVING AVG(e.salary) > 10000
AND COUNT(*) > 5
-- WHERE vs HAVING 的区别:
-- WHERE: 分组前过滤,不能使用聚合函数
-- HAVING: 分组后过滤,可以使用聚合函数
1.4 结果处理阶段
sql
-- 5. SELECT:选择列并计算
SELECT
d.name AS dept_name,
COUNT(*) AS emp_count,
AVG(e.salary) AS avg_salary,
AVG(e.salary) * 1.1 AS new_salary -- 可以计算表达式
-- 6. DISTINCT:去重
SELECT DISTINCT dept_name, emp_count
-- 重要:DISTINCT 在 SELECT 之后执行
-- 所以可以对计算后的结果去重
1.5 排序和限制阶段
sql
-- 7. ORDER BY:排序
ORDER BY avg_salary DESC,
emp_count ASC
-- 8. LIMIT:限制行数
LIMIT 10 OFFSET 20
2.相关子查询
每次都执行
sql
-- 主查询的每一行都会执行子查询
SELECT e.name,
e.salary,
(SELECT AVG(salary)
FROM employees
WHERE dept_id = e.dept_id) AS dept_avg
FROM employees e;
-- 执行顺序:
-- 1. FROM employees e (获取一行)
-- 2. 执行子查询(使用当前行的dept_id),
-- 相当于 e.dept_id 当成一个参数传递到子查询sql中
-- 3. SELECT 选择列
-- 4. 重复第一步到第三步直到所有行处理完
3.非相关子查询
执行一次
sql
-- 子查询先执行,结果作为常量
SELECT name, salary
FROM employees
WHERE dept_id IN (
SELECT id
FROM departments
WHERE location = '北京' -- 先执行
);
-- 执行顺序:
-- 1. 执行子查询:SELECT id FROM departments...
-- 2. 主查询WHERE:dept_id IN (子查询结果)
-- 3. 主查询其他部分
4.FROM 中的子查询(派生表)
在mysql5.7版本中派生表中不会给字段添加索引 比如 t.dept_id 没有索引,派生表内部的where 条件会走索引,通过派生表连接其他表,查询时不会索引,派生表数据会全表扫描。
sql
SELECT d.name, t.avg_salary
FROM departments d
JOIN (
SELECT dept_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept_id
) t ON d.id = t.dept_id; -- 先执行子查询
-- 执行顺序:
-- 1. 执行派生表子查询(先分组计算)
-- 2. FROM + JOIN(连接派生表和departments)
对于派生表:派生表通常是性能瓶颈,应该尽量避免或者优化,如果可能,尽量将查询重写为join形式或者保证 派生表的结果集尽量小,或者对于派生表写成视图,如果项目需要,做成物化视图也是一个很好优化方案。
二、SQL中各索引整理
1.什么是索引(B+Tree)
**索引是数据库中帮助快速查找数据的数据结构,这个数据结构就是B+tree:**
就像书籍的目录:
1.没有目录:需要一页页的翻找(全表扫描)
2.有目录:直接翻到对应页码(快速定位)
3.如果一张表中,有多个索引,那么每一个索引就像书的一种目录编排方式。
4.组合索引是多级排序的目录,有严格的顺序规则
5.索引不包含完整数据,只包含索引字段的值和指向数据的指针
6.索引越多,查询时可选的方式越多,但维护成本也越高
eg:如一张user 表中有3个字段(id,name,age),其中id 是主键索引 并且只有三条数据 (1,"张三",20) ,(2,"李四",21) ,(3,"王五",22) ,那么他完整的B+tree 结构图是:
sql
┌─────────────────┐
│ 根节点 │
│ 指针|2|指针 │
└─────┬─────┬─────┘
│ │
┌───────┘ └───────┐
↓ ↓
┌───────────────┐ ┌───────────────┐
│ 叶子节点1 │────→│ 叶子节点2 │
│ id=1,张三,20 │ │ id=3,王五,22 │
│ id=2,李四,21 │ └───────────────┘
└───────────────┘
说明:
1. 根节点存储指针和键值2(代表id 为2 的数据:这里mysql 取),表示:
- 第一个指针(如hash值0xa):指向id<=2的数据
- 键值2:分界点(这里取2 保证了树的平衡,和数据分散的合理性,方便查询)
- 第二个指针(如hash值0xb):指向id>2的数据
2. 叶子节点存储实际数据(在实际B+tree结构中 是不会存具体的数据)
3. 叶子节点之间有双向链表连接(这里简化为单向)
2.按照数据结构分类
| 索引名称 | 特点 | 相关示例sql |
|---|---|---|
| B+Tree 索引(默认索引类型) | 1.InnoDB 和 MyISAM 的默认索引类型。 2.平衡树结构,适合范围查询和排序。 3.叶子节点存储实际数据(InnoDB)或数据指针(MyISAM)支持全键值、键值范围和键前缀查找 | CREATE INDEX idx_name ON table_name (column_name); 或者 alter table table_name add index idx_name (column_name); |
| 哈希索引 | 1.只有 MEMORY 和 NDB 引擎显式支持 2.精确查找速度快 O(1) 3.不支持范围查询、排序和模糊查询 4.存在哈希冲突问题 | CREATE TABLE hash_table (id INT(1), INDEX USING HASH(id)) ENGINE=MEMORY; |
| 全文索引(FULLTEXT) | 1.用于文本内容的全文搜索。 2.支持 MATCH AGAINST 语法。 3.只有 InnoDB(5.6+)和 MyISAM 支持 4.有最小词长限制 | CREATE FULLTEXT INDEX idx_name ON table_name (column_name); |
| 空间索引(SPATIAL) | 1.用于地理空间数据。 2.遵循 R-Tree 数据结构。 3.支持 GIS 函数 | CREATE SPATIAL INDEX idx_name ON table_name (column_name); |
3.按照功能特性分类
| 索引名称 | 特点 | 相关示例sql |
|---|---|---|
| 主键索引(PRIMARY KEY) | 1.唯一且非空。 2.一个表只能有一个。 3.InnoDB 中作为聚集索引 | 无 |
| 唯一索引(UNIQUE) | 1.保证列值唯一性 2.允许 NULL 值(但只能有一个 NULL) 3.用于业务唯一性约束 | 无 |
| 普通索引(INDEX) | 1.最基本的索引类型。 2.允许重复值和 NULL。 3.提高查询性能 | 无 |
| 组合索引(复合索引) | 1.多列组成的索引。 2.遵循最左前缀原则。 3.可以减少单列索引数量 |
无 |
4.两个特殊索引类型
| 索引名称 | 特点 | 相关示例sql |
|---|---|---|
| 前缀索引 | 1.对文本列前 N 个字符建立索引。 2.节省存储空间。 3.可能降低选择性 | 无 |
| 覆盖索引 | 1.索引包含查询所需的所有字段。 2.无需回表查询. 3.显著提升性能,但是增加索引维护成本 |
无 |
5.组合索引和覆盖索引的区别
组合索引是"武器",覆盖索引是"使用武器的技巧"。一把好武器(组合索引)加上正确的使用技巧(覆盖索引),才能发挥最大威力,这里的覆盖索引全称应该称为 覆盖索引查询。
sql
-- 不是一个索引类型,而是一种使用方式
-- 当查询所需的所有列都包含在索引中时
SELECT name, age FROM users WHERE name = '张三';
-- 如果存在 idx_name_age (name, age),这就是覆盖索引查询
-- 知道了覆盖索引,也就知道了平常写查询不要用select * 这样,会加大回表的概率
组合索引和覆盖索引的核心区别对比:
| 维度 | 组合索引 | 覆盖索引 |
|---|---|---|
| 本质 | 索引类型(数据结构) | 索引使用方式(查询优化) |
| 物理存在 | ✅ 是实际创建的索引 | ❌ 不是独立的索引类型 |
| 创建方式 | CREATE INDEX idx ON table(a,b,c) | 无法直接创建,是查询使用索引的方式 |
| 包含列数 | 可以是2列或更多 | 可以是单列索引,也可以是组合索引 |
| 主要目的 | 支持多列查询条件 | 避免回表,提升查询性能 |
| 能否回表 | 可能回表,也可能不回表 | 一定不回表(查询时不需要再访问主键/数据行) |
回表:当使用索引查找数据时,如果索引中没有包含查询所需的所有列,就需要根据索引中的指针(主键值)回到主键索引(或数据行)中获取完整数据的过程。
总结
了解完上面的sql执行顺序及索引的相关知识后,后面的sql 优化在表设计基本合理的情况下,都是对索引使用的深理解,为此我们也总结了索引的几点性能影响。
1.索引的性能影响:
优点:
1.大幅提高查询速度
2.加速表连接
3.减少排序和分组时间
4.保证数据唯一性
缺点:
1.占用额外存储空间
2.降低插入、更新、删除速度
3.维护成本高(因此对于建组合索引、单个索引需要根据业务去权衡)
4.需要定期优化