掌握MySQL索引优化,提升查询性能

通过我们全面的MySQL索引指南,解锁更快的数据库性能。了解关键索引类型(主键、唯一索引、普通索引、全文索引),创建和管理复合索引的最佳实践,以及如何使用强大的EXPLAIN语句分析索引使用情况。优化查询,显著加快数据检索速度,打造更高效的MySQL数据库。

掌握MySQL索引优化,提升查询性能

当数据库响应变慢时,我通常会首先检查MySQL索引,但这也是最容易犯错的地方之一。一个索引可以将全表扫描转变为快速查找,但也可能拖慢写入速度、浪费内存,甚至如果查询优化器从未使用它,还会给你一种虚假的进步感。

实际的问题不是“这个列应该加索引吗?”,更好的问题是“我想让哪个查询变得更高效,以及如何证明索引确实有帮助?”。阅读时请牢记这个问题。好的索引始于真实的查询,而不是看起来重要的列列表。

什么是MySQL索引?

MySQL索引是一种数据结构,用于提高数据库表上数据检索操作的速度。可以把它想象成书中的索引:无需阅读整本书来查找特定主题,你可以在索引中查找该主题,它会告诉你确切的页码。类似地,数据库索引允许MySQL快速定位满足特定查询条件的行,而无需扫描整个表。

当你查询一个表时,MySQL可以使用索引来更快地找到相关行,这比逐行检查要高效得多。这对于包含大量行的表或涉及过滤(WHERE子句)、表连接(JOIN子句)或排序(ORDER BY子句)的查询尤其有益。

索引的工作原理

MySQL通常对普通的InnoDB索引使用B-tree索引。B-tree按键排序,因此它非常适合等值查找、范围查询、有序扫描以及许多连接操作。当你在一个或多个列上创建索引时,MySQL会构建一个结构,其中:

  • 叶子节点包含实际的数据指针,或者在聚簇索引(如InnoDB的主键)的情况下,包含数据行本身。
  • 内部节点包含帮助导航树以找到正确叶子节点的键。

当查询可以使用索引的左侧部分时,MySQL可以导航到树的狭窄部分,而不是读取整个表。这才是真正的优势。索引并不能使每个查询都变快,它只是让某些访问模式变得廉价。

MySQL索引的类型

MySQL支持多种类型的索引,每种都有其优势和用例。

1. 主键

  • PRIMARY KEY约束确保列中的每个值都是唯一的且不为NULL。它隐式地创建索引。
  • 一个表只能有一个PRIMARY KEY
  • InnoDB表在物理上按主键排序(聚簇索引)。

示例:

CREATE TABLE users (
    user_id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100)
);

2. 唯一索引

  • UNIQUE索引强制索引列中的所有值都是唯一的。它允许NULL值,但允许多个NULL(除非该列也是PRIMARY KEY或另一个阻止它的UNIQUE约束的一部分)。
  • 用于确保数据完整性,当列必须唯一但不是主标识符时。

示例:

CREATE TABLE products (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(100) NOT NULL,
    sku VARCHAR(50) UNIQUE
);

3. 普通索引(INDEX或KEY)

  • 标准索引,也称为非唯一索引。
  • 用于加速数据检索。不强制唯一性。

示例:

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    INDEX idx_customer_id (customer_id)
);

4. 全文索引

  • 用于对CHARVARCHARTEXT列进行全文搜索。
  • 允许在大型文本字段中进行关键字搜索。
  • 现代MySQL版本中的InnoDB支持。较旧的MySQL安装可能有不同的限制,因此在设计之前请检查您的确切版本。

示例:

CREATE TABLE articles (
    article_id INT PRIMARY KEY,
    title VARCHAR(255),
    body TEXT,
    FULLTEXT (title, body)
);

5. 空间索引

  • 用于索引空间数据类型,如点、线和多边形。
  • 行为和需求因MySQL版本和存储引擎而异。测试您计划运行的确切空间查询;不要假设空间索引会对每个GIS风格的谓词都有帮助。

6. 哈希索引(有限使用)

  • MySQL的MEMORY存储引擎支持HASH索引。它们专为等值查找而设计,不适用于范围扫描或排序。
  • 对于大多数常见场景来说,这不是一种通用索引类型。

创建和管理索引

如何创建索引

您可以在创建表时创建索引,也可以通过修改现有表来创建。

1. 在创建表时:

CREATE TABLE employees (
    employee_id INT PRIMARY KEY,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    department_id INT,
    hire_date DATE,
    INDEX idx_department (department_id),
    INDEX idx_hire_date (hire_date)
);

2. 修改现有表:

-- 添加单列索引
ALTER TABLE customers
ADD INDEX idx_email (email);

-- 添加唯一索引
ALTER TABLE users
ADD UNIQUE INDEX uidx_username (username);

-- 添加多列(复合)索引
ALTER TABLE orders
ADD INDEX idx_customer_date (customer_id, order_date);

如何删除索引

如果索引不再需要或对性能产生负面影响(例如,在写入期间),您可以删除它。

-- 删除普通索引
ALTER TABLE customers
DROP INDEX idx_email;

-- 删除唯一索引
ALTER TABLE users
DROP INDEX uidx_username;

多列(复合)索引

复合索引是在两个或多个列上创建的。复合索引中列的顺序至关重要。

  • (col1, col2)上的复合索引可用于仅过滤col1的查询,或同时过滤col1col2的查询。
  • 通常用于仅过滤col2的查询。

示例:

考虑一个在(customer_id, order_date)上的索引。此索引对以下查询最有效:

SELECT * FROM orders WHERE customer_id = 123;
SELECT * FROM orders WHERE customer_id = 123 AND order_date = '2023-10-27';
SELECT * FROM orders WHERE customer_id = 123 ORDER BY order_date;

对于SELECT * FROM orders WHERE order_date = '2023-10-27';可能不太有用。

MySQL索引的最佳实践

1. 为WHEREJOINORDER BY子句中使用的列建立索引

这些是索引提供显著性能提升的最常见位置。

  • WHERE子句: 过滤条件是主要用例。
  • JOIN条件:JOIN语句的ON子句中使用的列建立索引可以显著加速表连接。
  • ORDER BYGROUP BY子句: 索引可以帮助MySQL避免排序操作。

2. 明智地使用复合索引

  • 顺序很重要: 将匹配查询形状的列放在前面。等值过滤器通常放在范围过滤器之前。用于排序的列可以在过滤列之后提供帮助。
  • 如果您的实际查询不按选择性最高的列进行过滤,不要盲目地将其放在第一位。一个在(status, created_at)上的索引对于WHERE status = 'paid' ORDER BY created_at DESC LIMIT 50可能非常出色,即使status的基数较低,因为它匹配了访问模式。

3. 在添加索引前后使用EXPLAIN

永远不要凭感觉判断索引。运行EXPLAIN,在暂存或维护安全的环境中添加上索引,然后再次运行EXPLAIN

EXPLAIN
SELECT order_id, total, created_at
FROM orders
WHERE customer_id = 123
  AND created_at >= '2025-01-01'
ORDER BY created_at DESC
LIMIT 20;

查看typekeyrowsExtra列。如果keyNULL,则MySQL未选择索引。如果rows仍然接近表大小,则索引可能对该查询的选择性不够。如果Extra显示Using filesort,这并不自动意味着不好,但它告诉你MySQL无法从所选索引按请求顺序返回行。

对于MySQL 8.0.18及更高版本,EXPLAIN ANALYZE可能更有用,因为它会实际执行查询并报告实际时间和行数:

EXPLAIN ANALYZE
SELECT order_id, total
FROM orders
WHERE customer_id = 123
ORDER BY created_at DESC
LIMIT 20;

在生产系统上谨慎使用它,因为它会执行语句。

4. 围绕工作流构建索引,而不是表

想象一个管理屏幕,列出最近失败的付款:

SELECT id, customer_id, failure_code, created_at
FROM payments
WHERE status = 'failed'
ORDER BY created_at DESC
LIMIT 100;

仅在status上的索引可能仍然会让MySQL对大量失败行进行排序。一个在(status, created_at)上的索引通常是更好的匹配,因为MySQL可以找到失败的行并按时间顺序读取它们。如果查询只返回索引中的列,您可以考虑覆盖索引:

CREATE INDEX idx_payments_status_created_cover
ON payments (status, created_at, id, customer_id, failure_code);

这可能会很快,但并非免费。索引更宽,占用更多存储空间,并且每次写入的成本更高。我只在频繁运行的热查询上使用覆盖索引,以证明额外的维护成本是合理的。

5. 注意那些看起来正确但实际上无用的索引

这些是常见的陷阱:

  • 函数隐藏了索引值:WHERE DATE(created_at) = '2025-01-01'
  • 前导通配符阻止了正常的B-tree使用:WHERE email LIKE '%@example.com'
  • 类型不匹配强制转换:将整数列与带引号的字符串进行比较可能仍然有效,但在实际模式中可能会混淆计划。
  • 索引以错误的列开头:(created_at, customer_id)(customer_id, created_at)不同。

尽可能重写谓词:

WHERE created_at >= '2025-01-01'
  AND created_at <  '2025-01-02'

这种形式允许MySQL在created_at上使用范围扫描。

6. 谨慎删除冗余和未使用的索引

过度索引是一个隐蔽的性能问题。每个额外的二级索引在INSERTUPDATEDELETE期间都需要维护。在写入密集的表上,五个未使用的索引可能比一个慢速SELECT更成问题。

在MySQL 5.7和8.0中,sys模式可以帮助您找到候选索引:

SELECT *
FROM sys.schema_unused_indexes
WHERE object_schema = 'app';

将该输出视为线索,而不是命令。一个索引可能看起来未使用,因为服务器最近重启,因为月度报告尚未运行,或者因为暂存环境的流量与生产环境不匹配。在删除索引之前,请检查部署历史、计划任务和外键要求。

7. 在大表上安全地添加索引

在小表上,ALTER TABLE ... ADD INDEX通常不会引起问题。但在大型生产表上,这可能是一个真正的操作。根据MySQL版本、存储引擎、表定义和确切的DDL,添加索引可能使用在线DDL,或者可能通过元数据锁、临时空间、重做日志生成和复制延迟造成压力。

在添加大型索引之前,请检查:

  • 表和现有索引的大小。
  • 副本能否跟上。
  • 您的MySQL版本是否支持您期望的在线算法。
  • 如果长时间事务阻塞DDL,应用程序能否容忍元数据锁。

对于敏感系统,请使用迁移工具,如pt-online-schema-changegh-ost,或者在低流量时段安排DDL。

实用的索引审查流程

当我审查一个慢速MySQL查询时,我按以下顺序进行:

  1. 捕获带有实际绑定值的精确SQL。
  2. 运行EXPLAIN,并在安全的情况下运行EXPLAIN ANALYZE
  3. 检查现有索引是否匹配WHEREJOINORDER BY模式。
  4. 在暂存环境中添加最小的有用复合索引。
  5. 比较检查的行数、查询时间和写入影响。
  6. 谨慎推出,并监控慢查询日志和复制延迟。

这个流程让索引保持诚实。你不是在收集索引,而是试图减少MySQL为应用程序实际运行的查询所做的工作量。