接地气的SQL优化

接地气的SQL优化

引言:为什么你的SQL会"慢"?

想象一下,你走进一个没有目录、书籍乱放的巨型图书馆,管理员让你找出所有"张三"借过的书。他只能从第一个书架开始,逐本翻看借阅记录------这就是数据库的"全表扫描"。而SQL优化的本质,就是为这个图书馆建立高效的索引目录、优化查找流程,并教会管理员最聪明的搜索方法

第一章:优化的基石------索引,即必须理解的"目录"

索引是数据库的"目录",但错误使用比没有目录更糟。

1.1 索引的本质:一本书的目录是怎么工作的?

当你通过书名在图书馆目录里查找时,目录会告诉你这本书在哪个区域、哪个书架的第几层。数据库索引(通常是B+树结构)也是如此:它存储了字段值和一个指向数据行的"指针",让数据库能快速定位,避免扫描整张表。

创建索引的基本命令:

sql 复制代码
-- 为单个字段创建索引
CREATE INDEX idx_user_email ON users(email);

-- 为多个字段创建联合索引
CREATE INDEX idx_name_age ON employees(last_name, first_name, age);

-- 创建唯一索引,确保字段值唯一
CREATE UNIQUE INDEX uni_username ON users(username);
1.2 联合索引的"最左前缀"原则:快递仓库的比喻

这是理解索引的关键!假设有一个联合索引 (city, district, street),就像快递仓库先按城市 分大区,再在每个城市按 分货架,最后在每个区按街道排序。

它可以高效处理以下查询:

sql 复制代码
-- 能用上索引:从"城市"开始查
WHERE city = '北京'
WHERE city = '上海' AND district = '浦东'
WHERE city = '广州' AND district = '天河' AND street = '体育西路'

-- 无法有效使用索引:开头就断了线索
WHERE district = '浦东'              -- 跳过了"城市"
WHERE street = '体育西路'            -- 跳过了"城市"和"区"
WHERE district = '西湖' AND city = '杭州' -- 顺序不对,应该 city 在前

黄金法则 :设计联合索引时,把最常用、区分度最高的字段放在左边

1.3 覆盖索引:一站式服务,无需"回表"

如果一个索引包含了查询所需要的所有字段,数据库就不需要再去查找原始数据行,这称为"覆盖索引",是性能优化的利器。

sql 复制代码
-- 表结构:users(id主键, name, age, city)
-- 有一个索引:idx_city_age(city, age)

-- 慢查询:需要"回表"
SELECT * FROM users WHERE city = '杭州' AND age > 25;
-- 虽然用到了idx_city_age定位,但SELECT * 需要其他字段,所以还得根据指针去主数据里查

-- 优化后:覆盖索引,一步到位
SELECT id, name, city, age FROM users WHERE city = '杭州' AND age > 25;
-- 如果索引idx_city_age恰好是(city, age, name),那所有需要的字段都在索引里,速度极快
1.4 哪些字段应该建索引?(决策清单)
  • 高优先级WHERE子句中的高频过滤条件、JOIN的连接字段、ORDER BY/GROUP BY的字段。
  • 中优先级 :常用于SELECT的字段(考虑覆盖索引)。
  • 低优先级:区分度极低的字段(如"性别")、极少查询的字段。
  • 避免创建 :大文本字段(TEXT/BLOB)、频繁修改的字段(增加维护开销)。

切记 :索引不是免费的。每个索引都会降低INSERTUPDATEDELETE的速度,并占用存储空间。它是一种用写操作成本和存储空间换取读操作速度的权衡。

第二章:SQL语句编写优化------日常开发中的"好习惯"

好的编程习惯能让你的SQL天生健壮。以下是你在写每一行SQL时都应该考虑的要点。

2.1 拒绝 SELECT *,只取所需

这是最简单也最容易被忽视的优化。

sql 复制代码
-- 反面教材:乾坤大挪移
SELECT * FROM orders;

-- 优化实践:精准打击
SELECT order_id, customer_id, amount, status FROM orders;

为什么?

  1. 减少网络传输 :尤其是当表中有TEXTBLOB等大字段时。
  2. 提升缓存效率:更少的数据意味着内存中可以缓存更多的结果集。
  3. 为覆盖索引创造条件:只查询必要的字段,更容易让索引覆盖整个查询。
2.2 永远使用 LIMIT,特别是后台查询

LIMIT不仅是分页工具,更是安全护栏。

sql 复制代码
-- 危险操作:一个不小心可能拖垮数据库
SELECT * FROM user_logs WHERE action_type = 'login';

-- 安全做法:明确需求上限
SELECT * FROM user_logs WHERE action_type = 'login' LIMIT 1000;
2.3 谨慎使用 LIKE,前置百分号是性能杀手

LIKE '%关键字%' 会导致索引失效。

sql 复制代码
-- 无法使用索引,全表扫描
SELECT * FROM products WHERE name LIKE '%苹果%';

-- 可以使用索引(如果name字段有索引)
SELECT * FROM products WHERE name LIKE '苹果%';

-- 解决方案:对于复杂搜索,考虑全文索引(如MySQL的FULLTEXT)
ALTER TABLE products ADD FULLTEXT INDEX ft_idx_name (name);
SELECT * FROM products WHERE MATCH(name) AGAINST('苹果' IN NATURAL LANGUAGE MODE);
2.4 避免在WHERE子句中对字段进行操作

对字段进行计算或函数处理,会让索引望而却步。

sql 复制代码
-- 反面教材:索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2024;
SELECT * FROM users WHERE salary * 0.8 > 10000;

-- 优化方案:将计算转移到常量端
SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
SELECT * FROM users WHERE salary > 10000 / 0.8;
2.5 理解 EXISTSIN 的选择
  • IN :适合子查询结果集很小的情况。它先执行子查询,将结果物化,再进行主查询匹配。
  • EXISTS :适合子查询结果集很大,而主查询结果集较小的情况。它是关联子查询,更侧重于判断是否存在。
sql 复制代码
-- 当"已支付订单"数量很少时,IN可能更直观
SELECT * FROM customers 
WHERE id IN (SELECT DISTINCT customer_id FROM orders WHERE status = 'paid');

-- 当"活跃用户"定义复杂或子查询结果大时,EXISTS通常更高效
SELECT * FROM users u 
WHERE EXISTS (
    SELECT 1 FROM login_logs l 
    WHERE l.user_id = u.id AND l.login_time > CURDATE() - INTERVAL 7 DAY
);
2.6 分页优化:破解"越往后翻越慢"的魔咒

传统 LIMIT offset, size 在 offset 很大时极其低效,因为它会先查询出 offset+size 条记录,然后丢弃前 offset 条。

优化方案1:基于游标的分页(用于"加载更多")

sql 复制代码
-- 传统慢查询
SELECT * FROM articles ORDER BY publish_time DESC LIMIT 10000, 20;

-- 游标分页(记住上一页最后一条的publish_time和id)
SELECT * FROM articles 
WHERE publish_time < '2023-10-25 15:30:00' 
   OR (publish_time = '2023-10-25 15:30:00' AND id < 12345)
ORDER BY publish_time DESC, id DESC
LIMIT 20;

优化方案2:延迟关联(用于仍需跳页的场景)

sql 复制代码
SELECT a.* FROM articles a
INNER JOIN (
    SELECT id FROM articles 
    ORDER BY publish_time DESC 
    LIMIT 10000, 20  -- 在覆盖索引(如(id, publish_time))中快速定位ID
) tmp ON a.id = tmp.id; -- 再用ID高效地回表取详细数据

第三章:数据库设计与连接优化------打好地基

3.1 为每个外键手动添加索引

这是很多数据库不会自动为你做的事,但却是保证连接性能的生命线

sql 复制代码
ALTER TABLE orders ADD INDEX idx_customer_id (customer_id); -- 外键字段索引
3.2 选择合适的数据类型
  • INT 而非 VARCHAR 存储数字ID。
  • DATETIMETIMESTAMP 存储时间。
  • DECIMAL 精确存储金额,避免浮点数精度问题。
  • 为短文本使用 VARCHAR(n) 并指定合理长度,而非直接使用 TEXT
3.3 解释与分析:使用 EXPLAIN 读懂执行计划

EXPLAIN 是你的SQL性能诊断仪。学会看它,你就不再是"盲人摸象"。

sql 复制代码
EXPLAIN SELECT u.name, o.order_no 
FROM users u 
INNER JOIN orders o ON u.id = o.user_id 
WHERE u.city = '杭州' 
ORDER BY o.create_time DESC 
LIMIT 10;

关注输出中的关键列:

  • type :从优到劣:system > const > eq_ref > ref > range > index > ALL。见到 ALL(全表扫描)就要警惕。
  • key :实际使用的索引。如果为 NULL,说明没用到索引。
  • rows:预估需要扫描的行数。越小越好。
  • Extra :重要提示。出现 Using filesort(需额外排序)或 Using temporary(需创建临时表),通常意味着需要优化。

第四章:实战优化清单------你的SQL健康检查表

下次写完SQL后,请快速对照此清单:

  1. [检查索引]WHEREORDER BYGROUP BYJOIN ON 里的字段是否有合适索引?联合索引顺序是否满足最左前缀?
  2. [拒绝SELECT *]:是否只查询了必要的字段?
  3. [带上LIMIT]:查询是否有限制返回行数?
  4. [审视LIKE] :是否使用了前置 %?能否用 =LIKE 'value%' 或全文索引替代?
  5. [保护WHERE字段]:是否在WHERE中对字段进行了函数或计算操作?
  6. [验证连接]:连接查询的关联字段是否有索引?外键是否已索引?
  7. [分析执行计划] :对于复杂查询,是否用 EXPLAIN 查看过执行计划?

结语:优化是一种思维习惯

SQL优化不是一堆死记硬背的规则,而是一种贯穿于数据库设计、代码编写和性能分析的系统性思维。它始于对业务需求的清晰理解,成于对数据特性的准确把握,终于对执行效果的持续验证。

最好的优化,有时发生在写出SQL之前------在需求评审时多问一句"真的需要这么多数据吗?",在设计表时思考"未来会怎样查询它?",这些往往比事后补救有效得多。

最快的查询,是那些不需要发生的查询;而不得不发生的查询,应当如闪电般迅速。

相关推荐
云老大TG:@yunlaoda3605 小时前
华为云国际站代理商TaurusDB的成本优化体现在哪些方面?
大数据·网络·数据库·华为云
TG:@yunlaoda360 云老大5 小时前
华为云国际站代理商GeminiDB的企业级高可用具体是如何实现的?
服务器·网络·数据库·华为云
QQ14220784497 小时前
没有这个数据库账户,难道受到了sql注入式攻击?
数据库·sql
残 风8 小时前
pg兼容mysql框架之语法解析层(openHalo开源项目解析)
数据库·mysql·开源
勇往直前plus8 小时前
MyBatis/MyBatis-Plus类型转换器深度解析:从基础原理到自定义实践
数据库·oracle·mybatis
cyhysr8 小时前
sql将表字段不相关的内容关联到一起
数据库·sql
九皇叔叔8 小时前
MySQL 数据库 MVCC 机制
数据库·mysql
此生只爱蛋8 小时前
【Redis】Set 集合
数据库·redis·缓存
bjzhang758 小时前
C#操作SQLite数据库
数据库·sqlite·c#
无名-CODING9 小时前
SQL 注入指南
sql·mybatis