索引、EXPLAIN、查询优化与 Python 接入
前置知识:窗口函数与窗口框架 · 建表、约束、写入与事务
本章目录
本章学习目标
- 按筛选与排序解释索引收益
- 读取执行计划并区分估计与实际
- 用参数化与分块读取接入 Python
索引是一条查找路径
索引维护按键组织的额外结构,以空间与写入成本换取筛选、连接和排序效率。InnoDB 主键组织数据,二级索引包含主键。给每列建立索引并不是优化,因为插入和更新也要维护所有索引。首先明确常见查询与数据规模,再选择索引,并观察计划和运行指标。
复合索引(user_id,order_date) 有利于指定用户后按日期筛选,最左前缀通常重要。只按日期查时,不应假设这个索引与(order_date) 完全相同。等值键在前、范围键随后常是有效起点,但真实选择还取决于分布和优化器。课程小表可能选择全扫描,这不等于索引失效;小表扫描的总成本可能更低。
可搜索条件与反例
WHERE DATE(order_date)='2026-01-10' 给列套函数,普通日期索引通常不能按原始范围直接搜索。改成 order_date >= '2026-01-10' AND order_date < '2026-01-11' 保持语义且利于范围访问。隐式类型转换、前置百分号文本匹配和复杂 OR 也可能影响路径,但不应把所有函数都说成永远不能索引,函数索引和优化器能力需具体检查。
先减少列和行,再连接与聚合,经常能减少处理量;但优化器可能自行下推筛选,人工重写应以等价结果和测量为依据。给 LIMIT 查询加明确排序,才能比较同一业务结果。用 DISTINCT 隐藏连接重复不是性能优化,应该先修复错误粒度。正确性始终是优化的前置条件。
EXPLAIN 与实测
EXPLAIN 展示优化器计划,包括访问方式、候选索引、实际选用索引和估计行数。rows 是估计,不是查询结果数。Extra 中的临时表或排序表示额外工作,但它们有时是正确方案,不能看到 filesort 就断言错误。比较时固定数据、查询与环境,不只记录一次耗时。
EXPLAIN ANALYZE 会真实执行 SELECT 并报告实际执行信息,不能当成纯静态检查;大查询应先控制范围。示例将 orders 复制到独立临时表并建索引,比较原列范围与 DATE 函数的计划,不修改原表。样本量小只能解释计划结构,不能证明生产规模性能提升或给出通用倍数。
Python 接入与数据边界
应用用数据库驱动连接 MySQL,凭据从环境变量读取,不写进笔记或提交到仓库。mysql.connector 的 cursor.execute 接受 SQL 与参数元组,占位符是 %s,不能用 f-string 拼接用户条件。表名等标识符通常不能作为值参数绑定,应使用明确允许的固定标识符。结束时关闭游标和连接,异常写入回滚。
pandas 可通过 SQLAlchemy 连接读取,文本查询使用 sqlalchemy.text 与命名参数,再由 read_sql_query 的 params 绑定;大结果用 chunksize 分块并注意每块之间的全局聚合。DECIMAL、时间与 NULL 进入 Python 后的类型可能变化,应检查 dtypes 与日期解析。连数据库并不意味着把全表下载到内存,尽量在数据库端先按范围选择需要的列。
从结果到验证
阅读一段查询时,先写下一行结果代表什么,再列出来源表的一行代表什么。把过滤、连接、聚合和排序分别检查,比仅凭输出看起来合理更可靠。对于金额应同时保留订单数量和用户数量,以便发现连接引起的重复累计;对于比率应同时展示分子与分母,以便发现筛选范围不同。结果少并不等于正确,结果多也不等于错误,判断依据是预先确定的业务口径。
先用纸笔构造三个用户、两三个订单的小表,再用数据库运行对应查询。至少包含没有订单的用户、同一用户多笔订单、一个未知值,以及筛选后没有记录的情形。用这些反例解释每个子句造成的变化,再扩大到课程数据。未写 ORDER BY 的结果没有稳定顺序;需要核对输出时总要明确排序。课程默认 MySQL 8.4,开启严格分组规则,不要通过关闭规则掩盖数据粒度问题。
完成练习后,先自行解释结果再查看答案。记录的检查标准应是行数、键是否唯一、是否有空值、金额是否守恒,以及日期边界是否符合定义,而不只是查询没有语法错误。答案中的每个条件都应能追溯到题目口径;如果口径改变,先调整定义再改查询。这样形成的推理过程可以直接用于面试中解释思路和实际工作中复核分析报告。
完整示例与实际输出
DROP TEMPORARY TABLE IF EXISTS scratch_perf;
CREATE TEMPORARY TABLE scratch_perf LIKE orders;
INSERT INTO scratch_perf SELECT * FROM orders;
CREATE INDEX idx_scratch_user_date ON scratch_perf(user_id,order_date);
EXPLAIN SELECT order_id,order_date,amount FROM scratch_perf
WHERE user_id=1 AND order_date>='2026-01-01' AND order_date<'2026-02-01'
ORDER BY order_date,order_id;
EXPLAIN SELECT order_id FROM scratch_perf WHERE DATE(order_date)='2026-01-10';
EXPLAIN SELECT order_id FROM scratch_perf WHERE order_date>='2026-01-10' AND order_date<'2026-01-11';
DROP TEMPORARY TABLE scratch_perf;
实际运行通过Actual MySQL 8.4.11 execution; strict grouping mode.
id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE scratch_perf NULL range idx_scratch_user_date idx_scratch_user_date 10 NULL 3 100.0 Using index condition id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE scratch_perf NULL index NULL idx_scratch_user_date 10 NULL 60 100.0 Using where; Using index id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE scratch_perf NULL index idx_scratch_user_date idx_scratch_user_date 10 NULL 60 11.11 Using where; Using index
五道自测:先回答,再核对
01 · 索引越多越好吗?
查看提示
写入成本。
查看参考答案
不是,索引占空间并增加写维护,应按真实访问模式选择。
02 · (user_id,order_date) 等同于单列日期索引吗?
查看提示
最左前缀。
查看参考答案
不等同,仅按日期查询不能假定同样高效。
03 · EXPLAIN rows 就是实际返回行数吗?
查看提示
估计与实测。
查看参考答案
不是,是计划估计扫描数量,不是最终结果数。
04 · EXPLAIN ANALYZE 会运行查询吗?
查看提示
实际执行统计。
查看参考答案
会,SELECT 会真实执行,要控制成本和范围。
05 · 为何参数化优于 f-string 拼 SQL?
查看提示
值与语法隔离。
查看参考答案
驱动正确绑定和编码值,防止输入被当 SQL 语法;标识符要用允许列表。
两项动手任务
任务 1 · 改写一月订单筛选,避免在日期列上调用 MONTH/YEAR。
提示
左闭右开范围。
完整参考解答与检查方式
SELECT order_id,order_date FROM orders WHERE order_date>='2026-01-01' AND order_date<'2026-02-01' ORDER BY order_date,order_id; 前后结果需同范围核对,优化收益要用 EXPLAIN 与实际运行验证。任务 2 · 写出参数化 Python 查询用户支付净额的完整片段。
提示
驱动采用 %s。
完整参考解答与检查方式
import os import mysql.connector conn = mysql.connector.connect(host=os.environ['MYSQL_HOST'], user=os.environ['MYSQL_USER'], password=os.environ['MYSQL_PASSWORD'], database='data_skills_course') try: cur = conn.cursor() try: cur.execute("SELECT COALESCE(SUM(amount-refund_amount),0) FROM orders WHERE user_id=%s AND status IN ('paid','refunded')", (1,)) print(cur.fetchone()[0]) finally: cur.close() finally: conn.close() 运行前安装驱动并设置三个环境变量;这是外部连接练习,SQL 脚本本身不混入 Python。官方资料与掌握状态
完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。
动手任务记录
掌握状态与复习
关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。
记录尽量保存在当前浏览器,建议定期导出备份。