文章目录
-
- 索引知识介绍
- 创建索引
-
- [一、MySQL 有哪些索引类型?](#一、MySQL 有哪些索引类型?)
- 二、在什么字段上建索引?
- 三、添加、查看、删除索引的命令
-
- [1. 添加索引](#1. 添加索引)
- [2. 查看索引](#2. 查看索引)
- [3. 删除索引](#3. 删除索引)
- 四、常见问题
-
- [PRIMARY KEY (主键索引)需要我们每次都手动创建吗?](#PRIMARY KEY (主键索引)需要我们每次都手动创建吗?)
- 一个字段可以重复添加几种不同类型的索引吗?
- 我们可不可以用唯一索引替代主键索引?
- [我们知道表主键可以设置多个。但是PRIMARY KEY (主键索引)又是唯一且非空的索引,每张表只能有一个。这两个概念不是冲突了吗?](#我们知道表主键可以设置多个。但是PRIMARY KEY (主键索引)又是唯一且非空的索引,每张表只能有一个。这两个概念不是冲突了吗?)
- 冗余索引
- 创建索引实例
- [MySQL 索引选择机制](#MySQL 索引选择机制)
- [MySQL索引最佳实战 & 避坑指南](#MySQL索引最佳实战 & 避坑指南)
-
- 一、索引设计的最佳实践
- 二、导致索引失效的常见陷阱及解决方案
-
- [1. 【陷阱】对索引列进行运算或使用函数](#1. 【陷阱】对索引列进行运算或使用函数)
- [2. 【陷阱】隐式类型转换](#2. 【陷阱】隐式类型转换)
- [3. 【陷阱】`LIKE` 以通配符 `%` 开头](#3. 【陷阱】
LIKE
以通配符%
开头) - [4. 【陷阱】错误使用 `OR`](#4. 【陷阱】错误使用
OR
) - [5. 【陷阱】在复合索引中违背最左前缀原则](#5. 【陷阱】在复合索引中违背最左前缀原则)
- [6. 【陷阱】使用 `NOT LIKE`, `!=`, `<>`, `NOT IN`](#6. 【陷阱】使用
NOT LIKE
,!=
,<>
,NOT IN
) - [7. 【陷阱】索引列使用 `IS NULL` 或 `IS NOT NULL`](#7. 【陷阱】索引列使用
IS NULL
或IS NOT NULL
)
- [FULLTEXT 全文索引的妙用](#FULLTEXT 全文索引的妙用)
-
- 一、什么是FULLTEXT索引?
- 二、核心妙用与优势
-
- [1. 终极解决方案:替代低效的 `LIKE '%...%'`](#1. 终极解决方案:替代低效的
LIKE '%...%'
) - [2. 实现智能分词搜索 (Tokenization)](#2. 实现智能分词搜索 (Tokenization))
- [3. 支持多种搜索模式](#3. 支持多种搜索模式)
-
- a) 自然语言模式 (IN NATURAL LANGUAGE MODE) - 最常用 自然语言模式 (IN NATURAL LANGUAGE MODE) - 最常用)
- b) 布尔模式 (IN BOOLEAN MODE) - 功能最强 布尔模式 (IN BOOLEAN MODE) - 功能最强)
- c) 查询扩展模式 (WITH QUERY EXPANSION) 查询扩展模式 (WITH QUERY EXPANSION))
- [4. 按相关度排序](#4. 按相关度排序)
- [1. 终极解决方案:替代低效的 `LIKE '%...%'`](#1. 终极解决方案:替代低效的
- 三、如何使用:实战步骤
-
- [1. 创建FULLTEXT索引](#1. 创建FULLTEXT索引)
- [2. 执行全文搜索查询](#2. 执行全文搜索查询)
- 四、适用场景与限制
-
- [✅ 完美适用场景:](#✅ 完美适用场景:)
- [⚠️ 重要限制与注意事项:](#⚠️ 重要限制与注意事项:)
- [五、那字符串全部都用FULLTEXT 索引好了?](#五、那字符串全部都用FULLTEXT 索引好了?)
索引知识介绍
一、什么是索引?(把它想象成书的目录)
你有一本非常厚的、没有目录的《现代汉语词典》(这本书就是数据库的表)。现在,你想查找 "索引" 这个词的含义。
- 没有索引(目录)的情况 :你只能从第一页开始,一页一页地往后翻(这叫做 "全表扫描"),直到找到"索引"这个词为止。这个过程非常慢,尤其当书很厚的时候。
- 有索引(目录)的情况 :你直接翻到书最后的目录 。目录已经按拼音顺序排好了所有词条,并且告诉你"索引"这个词在 第 1005 页。你就能直接、快速地翻到第 1005 页,找到你想要的内容。
在这个比喻里:
- 那本厚厚的《词典》 就是数据库里的表。
- 词典里的一个个词条(如"索引"、"数据库") 就是表里某一行数据的某个字段 (比如
username
字段)。 - 书最后的目录 就是数据库的索引。
所以,MySQL 索引就是一种帮助数据库快速查找数据的数据结构。
二、索引有什么用?(它的两大核心作用)
-
大大加快数据的查询速度
这是最主要的作用。就像有了目录,你查词条的速度飙升一样。当你的用户表有上亿条数据时,用索引查找一个用户可能只需要几毫秒,而全表扫描可能需要几分钟甚至更久。
-
保证数据的唯一性
你可以在数据库里设置唯一索引。这就像规定字典里每个词条只能出现一次。如果你试图往"用户名"字段上创建了唯一索引的表中插入两个叫 "张三" 的用户,数据库就会拒绝第二个操作,从而避免出现重复数据。
三、那索引是越多越好吗?
需要注意的是 :索引就像一把双刃剑。它虽然极大地提升了查询速度,但也会占用额外的存储空间,并且会在插入、删除、更新数据时拖慢速度(因为不仅要改数据,还要维护索引这个"目录")。所以不能乱加索引
,通常只给经常用于查询条件的字段加索引。
四、它是如何发挥作用的?(目录是怎么工作的?)
数据库索引最常用的数据结构是 B+树。你不用理解它复杂的细节,可以继续用我们的"目录"比喻来理解其原理:
-
排序 :当你对某个字段(比如
username
)创建索引时,MySQL 会偷偷地为这个字段的所有数据做一个排序,并记录下每条数据的位置。就像目录会把所有词条按拼音顺序排列好。 -
快速定位:因为索引是排好序的,所以它可以使用非常高效的查找算法(比如二分查找)。
- 想象一下在目录里找"索"字,你知道它肯定在 "S" 开头的部分,而不是 "A" 或 "Z",你会快速跳过无关的部分。
- 数据库也一样,它不会傻傻地从头开始找,而是直接在索引结构里快速定位到"索"这个范围,然后在这个小范围里精细查找,最终找到"索引"这个词条以及它对应的数据所在的物理地址(第1005页)。
-
直接访问数据:索引找到了目标数据的位置后,数据库就可以直接去那个位置把完整的数据行拿出来了,任务完成。
整个过程可以总结为:
收到查询指令 -> 去索引(目录)里查找 -> 快速定位到数据地址 -> 根据地址去硬盘上拿到完整数据 -> 返回结果。
总结
- 是什么 :索引是数据的目录。
- 有什么用 :极大提高查询速度 ,并能保证数据唯一性。
- 如何工作 :通过对数据排序 ,实现快速定位,避免低效的"全表扫描"。
所以,当你发现某个查询很慢时,第一个应该考虑的就是:"我是否给查询条件用到的字段加上了合适的'目录'(索引)"
好的,我们来深入聊聊MySQL的索引类型、如何选择,以及具体的操作命令。
创建索引
一、MySQL 有哪些索引类型?
MySQL支持多种索引类型,每种都有其特定的用途。以下是主要的几种:
索引类型 | 描述 | 通俗解释 |
---|---|---|
PRIMARY KEY (主键索引) | 唯一且非空的索引,每张表只能有一个。 | 就像你的身份证号,唯一标识你这个人,且不能为空。它是表中数据最重要的组织方式。 |
UNIQUE (唯一索引) | 保证索引列的值都是唯一的,允许有空值(NULL)。 | 就像用户名 或手机号,要求不能重复,但允许有人还没填写手机号(NULL)。 |
NORMAL / INDEX / KEY (普通索引) | 最基本的索引类型,没有任何唯一性限制。 | 就像一本技术书最后的术语索引,只是为了帮你快速找到内容,允许同一个术语出现在多页。 |
FULLTEXT (全文索引) | 专门用于全文搜索,针对文本内容(如文章正文)进行词语匹配。 | 就像搜索引擎,你输入几个关键词,它能帮你找到所有包含这些词的文章,而不是精确匹配。 |
SPATIAL (空间索引) | 用于地理空间数据类型(如点、线、面)。 | 就像地图软件,可以快速搜索"我附近1公里内的所有餐厅"。 |
复合索引 (联合索引) | 基于多个列 combined 创建的索引。 | 就像电话簿 ,首先按姓 排序,同姓的再按名排序。想找"张三",你得同时用姓和名来查才最快。 |
效率排名(从高到低):主键查询 (PRIMARY KEY) > 2. 唯一索引 (UNIQUE) ≈ 普通索引 (INDEX) > 3. 全表扫描 (No Index)
二、在什么字段上建索引?
创建索引的原则是:在合适的字段上创建合适的索引,而不是越多越好。
-
最适合建索引的字段:
WHERE
子句中的字段 :这是最核心的原则。查询中经常作为条件的字段,SELECT * FROM users WHERE username = 'john'
,那username
就应该是索引。- 连接表 (
JOIN ... ON ...
) 的字段 :例如FROM orders JOIN users ON orders.user_id = users.id
,orders.user_id
和users.id
上都应该有索引。 - 排序 (
ORDER BY
) 或分组 (GROUP BY
) 的字段:索引本身有序,可以极大加快排序和分组操作。
-
考虑索引的 selectivity (选择性):
- 高选择性:字段的值几乎都不相同(如身份证号、用户名)。索引能快速过滤掉大部分数据,效率极高。
- 低选择性:字段的值大量重复(如性别、状态标志:男/女,0/1)。为这种字段建索引,数据库依然需要从大量重复值中遍历,效果很差,通常不建议建。
-
谨慎使用索引的情况:
- 频繁更新的表/字段:索引虽然加快读,但会减慢写(INSERT/UPDATE/DELETE)速度,因为每次写操作都要更新索引。
- 非常小的表:数据量很小,全表扫描可能比通过索引查找更快。
- 很少被用于查询的字段:不要为了建而建,浪费空间和拖慢写入。
-
复合索引的经典原则:最左前缀原则
- 如果创建了复合索引
(col1, col2, col3)
,那么它相当于同时创建了三个索引:(col1)
、(col1, col2)
和(col1, col2, col3)
。 - 查询时,必须从最左边的列开始 使用才会生效。例如:
WHERE col1 = 1 AND col2 = 2
(生效,使用了前两列)WHERE col1 = 1
(生效,使用了第一列)WHERE col2 = 2
(不生效,跳过了第一列)WHERE col2 = 2 AND col3 = 3
(不生效,跳过了第一列)
- 如果创建了复合索引
记住核心思想:索引是"目录",用在
WHERE
、JOIN
、ORDER BY
的字段上,对值重复率低(高选择性)的列效果最好。
三、添加、查看、删除索引的命令
假设我们有一张 users
表,有以下结构:
sql
CREATE TABLE users (
id INT,
username VARCHAR(50),
email VARCHAR(100),
age INT,
created_at DATETIME
);
1. 添加索引
a) 创建表时直接添加
sql
CREATE TABLE users (
id INT PRIMARY KEY, -- 主键索引
username VARCHAR(50) UNIQUE, -- 唯一索引
email VARCHAR(100),
age INT,
created_at DATETIME,
INDEX idx_email (email), -- 普通索引
INDEX idx_age_created (age, created_at) -- 复合索引
);
b) 使用 ALTER TABLE
添加(最常用)
sql
-- 添加普通索引
ALTER TABLE users ADD INDEX idx_username (username);
-- 添加唯一索引
ALTER TABLE users ADD UNIQUE INDEX idx_unique_email (email);
-- 添加复合索引
ALTER TABLE users ADD INDEX idx_age_created (age, created_at);
-- 添加主键索引 (如果建表时没设)
ALTER TABLE users ADD PRIMARY KEY (id);
c) 使用 CREATE INDEX
添加 (不能用于创建PRIMARY KEY)
sql
CREATE INDEX idx_username ON users (username);
CREATE UNIQUE INDEX idx_unique_email ON users (email);
2. 查看索引
sql
SHOW INDEX FROM users;
或者(更清晰一些):
sql
SHOW INDEX FROM users\G -- 在MySQL命令行中使用`\G`代替`;`可以纵向显示结果,更易读。
这条命令会列出 users
表上的所有索引信息,包括索引名(Key_name)、字段名(Column_name)、索引类型(Index_type)等。
3. 删除索引
a) 使用 DROP INDEX
删除
sql
-- 删除普通索引或唯一索引
DROP INDEX idx_username ON users;
DROP INDEX idx_unique_email ON users;
-- 删除主键索引 (比较特殊)
DROP PRIMARY KEY ON users;
b) 使用 ALTER TABLE
删除
sql
-- 删除普通索引或唯一索引
ALTER TABLE users DROP INDEX idx_username;
-- 删除主键索引
ALTER TABLE users DROP PRIMARY KEY;
四、常见问题
PRIMARY KEY (主键索引)需要我们每次都手动创建吗?
PRIMARY KEY (主键约束) 和 主键索引 在 MySQL 中是一个硬币的两面,当你定义了其中一个,另一个就会自动、隐式地创建。你无法将它们分开。
正因为这种绑定关系,我们从来不会说"手动添加主键索引",而是说"定义主键"或"设置主键"。我们通常不需要手动添加一个主键索引,也无法直接操作。只需要定义主键约束,索引会自动生成。
一个字段可以重复添加几种不同类型的索引吗?
我们当然可以这么做,但是回到前面创建索引的原则是:在合适的字段上创建合适的索引,而不是越多越好。这种操作是100%的冗余和浪费,没有任何好处,只有坏处。
我们可不可以用唯一索引替代主键索引?
技术上可以,但强烈不建议这样做。你应该总是为表定义一个显式的、简短的自增主键(通常是id INT AUTO_INCREMENT PRIMARY KEY
),然后再根据业务需要创建唯一索引。
我们知道表主键可以设置多个。但是PRIMARY KEY (主键索引)又是唯一且非空的索引,每张表只能有一个。这两个概念不是冲突了吗?
这里有一个概念需要理清:复合主键
和主键索引
。复合主键:主键可以由多个列组成,它仍然对应一个主键索引(聚簇索引)。PRIMARY KEY (a, b) 是创建一个索引,而不是两个。
它们并不冲突:一套主键规则对应一个主键索引(聚簇索引)。复合主键只是意味着这套规则由多个列组成。
冗余索引
比如我们创建两个索引
sql
ALTER TABLE users_test ADD INDEX idx_status (status);
ALTER TABLE users_test ADD INDEX idx_status_createtime (status, create_time);
按复合索引的最左前缀原则,idx_status (status) 这个索引是100%冗余的,完全不需要创建。在已经存在 idx_status_createtime (status, create_time) 的情况下,再创建 idx_status (status) 是一种常见的索引设计反模式。
创建索引实例
我们创建一个表结构如下:
sql
CREATE TABLE `users_test` (
`id` int(11) NOT NULL,
`sex` tinyint(1) NOT NULL DEFAULT '0' COMMENT '年龄:0=男,1=女',
`username` varchar(50) COLLATE utf8_unicode_ci NOT NULL,
`email` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
`age` int(11) NOT NULL DEFAULT '0',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '年龄:0=隐藏,1=正常',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
表字段分析
-
id: 主键 (已存在索引)
-
高选择性字段 (适合建索引): username, email, phone, name(看情况)
-
低选择性字段 (单独建索引效果差): sex, status
-
范围查询/排序字段: age, create_time
-
组合查询常用字段: status + create_time, sex + age 等
场景一:用户认证与唯一性校验 (最优先)
这类查询要求速度极快,且需要保证业务唯一性。
查询示例:
sql
-- 用户登录
SELECT * FROM users_test WHERE username = 'zhangsan';
-- 邮箱是否已被注册
SELECT COUNT(*) FROM users_test WHERE email = 'abc@example.com';
-- 手机号是否已被注册
SELECT id FROM users_test WHERE phone = '13800138000';
索引策略 :
为这些字段创建唯一索引,防止重复数据,并加速查找。
sql
-- 用户名必须唯一
ALTER TABLE users_test ADD UNIQUE INDEX uk_username (username);
-- 邮箱必须唯一(允许NULL,但非NULL的值必须唯一)
ALTER TABLE users_test ADD UNIQUE INDEX uk_email (email);
-- 手机号必须唯一(允许NULL,但非NULL的值必须唯一)
ALTER TABLE users_test ADD UNIQUE INDEX uk_phone (phone);
场景二:后台用户管理列表
查询示例:
sql
-- 按状态筛选并按创建时间倒序排列
SELECT * FROM users_test WHERE status = 1 ORDER BY create_time DESC;
-- 按性别和状态联合筛选
SELECT * FROM users_test WHERE sex = 0 AND status = 1;
-- 按年龄范围查询
SELECT * FROM users_test WHERE age BETWEEN 18 AND 30;
索引策略:
-
对于status这种低选择性字段,单独建索引效果不大。但如果经常配合其他字段查询或排序,适合建复合索引。
-
create_time经常用于排序,适合建索引。
sql
-- 为常用的管理查询创建复合索引:状态+创建时间
-- 这个索引完美支持 WHERE status = 1 ORDER BY create_time DESC
ALTER TABLE users_test ADD INDEX idx_status_createtime (status, create_time);
-- 为性别和状态的联合查询创建索引
ALTER TABLE users_test ADD INDEX idx_sex_status (sex, status);
-- 为年龄查询创建索引(支持范围查询)
ALTER TABLE users_test ADD INDEX idx_age (age);
场景三:个人资料页面的复杂查询
查询示例:
sql
-- 查找某个年龄段、某个性别、状态正常的用户
SELECT id, username FROM users_test
WHERE age BETWEEN 20 AND 30
AND sex = 1
AND status = 1
ORDER BY create_time DESC;
索引策略:
- 这种多条件查询+排序,需要精心设计复合索引。要遵循最左前缀原则。
sql
-- 推荐的复合索引:将等值查询条件放前面,范围查询和排序字段放后面
ALTER TABLE users_test ADD INDEX idx_sex_status_age_time (sex, status, age, create_time);
为什么这样设计?
-
sex和status是等值查询(=),放在最左边。
-
age是范围查询(BETWEEN),放在等值条件之后。
-
create_time用于排序(ORDER BY),放在最后。
场景四:统计和分析报表
查询示例:
sql
-- 按性别统计用户数
SELECT sex, COUNT(*) FROM users_test GROUP BY sex;
-- 按状态和性别分组统计
SELECT status, sex, COUNT(*) FROM users_test GROUP BY status, sex;
索引策略 :
GROUP BY操作和ORDER BY一样,如果能用上索引,会大大加快速度。
sql
-- 为分组统计创建索引
ALTER TABLE users_test ADD INDEX idx_sex_count (sex);
ALTER TABLE users_test ADD INDEX idx_status_sex (status, sex);
场景五:联表查询
联表查询的性能严重依赖于索引。没有索引的联表操作(尤其是大表之间)可能是灾难性的。
查询示例 :
联表查询的本质是循环嵌套。以 INNER JOIN 为例:
sql
SELECT *
FROM orders
INNER JOIN users ON orders.user_id = users.id
WHERE users.country = 'CN';
数据库的执行过程(简化):
-
从 users 表中找出所有 country = 'CN' 的用户(驱动表)。
-
对于找到的每一个用户,根据其 id 值去 orders 表中查找所有 user_id 等于该 id 的订单。
索引策略 :
在驱动表上:users.country 上的索引可以快速缩小要循环的用户范围。
在被驱动表上:orders.user_id 上的索引可以让你快速找到某个用户的所有订单,避免全表扫描。
sql
-- 1. 为驱动表的连接条件和筛选条件添加索引
-- (假设country字段选择性高)
ALTER TABLE users ADD INDEX idx_country (country);
-- 2. 为被驱动表的连接条件添加索引 (这是必须的!)
ALTER TABLE orders ADD INDEX idx_user_id (user_id);
-- 3. 如果连接条件是复合字段,则创建复合索引
-- ON table_a (col1, col2) 和 ON table_b (colx, coly)
永远确保连接条件(ON子句)中的字段上有索引,尤其是在被驱动表上。否则会导致循环全表扫描。
场景六:索引复用
这是体现数据库设计艺术的地方。一个设计良好的复合索引可以被多个不同的查询复用,遵循最左前缀原则。
假设我们为 users_test 表创建了这样一个复合索引:
sql
ALTER TABLE users_test ADD INDEX idx_status_sex_createtime (status, sex, create_time);
这个 (status, sex, create_time)
索引可以被以下多个查询复用:
-
示例1:完全利用所有列
sql-- 查询1: status和sex是等值查询,create_time是范围查询 SELECT * FROM users_test WHERE status = 1 AND sex = 0 AND create_time > '2023-01-01'; -- 索引使用: (status, sex, create_time) 【全部利用】
-
示例2:利用前两列 (跳过最后一列)
sql-- 查询2: 只使用status和sex进行筛选 SELECT * FROM users_test WHERE status = 1 AND sex = 0; -- 索引使用: (status, sex) 【利用前两列】
-
示例3:利用第一列 + 排序
sql-- 查询3: 使用status筛选,并按create_time排序 SELECT * FROM users_test WHERE status = 1 ORDER BY create_time DESC; -- 索引使用: (status, create_time) -- 因为status是等值,索引在status=1的分支内,create_time依然是有序的,可以避免排序。
-
示例4:仅利用第一列
sql-- 查询4: 只使用status进行筛选 SELECT * FROM users_test WHERE status = 1; -- 索引使用: (status) 【仅利用第一列】
-
示例5:利用第一列和第三列 (但效率可能不高)
sql-- 查询5: 使用status和create_time进行筛选 SELECT * FROM users_test WHERE status = 1 AND create_time > '2023-01-01'; -- 索引使用: (status, create_time) -- 能利用索引,因为最左前缀status是等值匹配。
-
无法复用的例子:
sql
-- 错误查询1: 跳过了最左的status字段
SELECT * FROM users_test WHERE sex = 0;
-- 索引使用: 无法使用idx_status_sex_createtime索引,因为不满足最左前缀。
-- 错误查询2: 跳过了sex字段
SELECT * FROM users_test
WHERE status = 1
AND create_time > '2023-01-01';
-- 索引使用: 可以使用索引的第一列status,但无法高效利用create_time进行筛选。
重要提醒
-
不要一开始就添加所有索引:根据实际的、频繁的查询请求来逐步添加索引。用EXPLAIN命令分析慢查询。
-
监控并调整:使用MySQL的慢查询日志工具,找到真正的性能瓶颈后再创建索引。
-
权衡利弊:每个索引都会降低INSERT、UPDATE、DELETE的速度。在读性能和写性能之间找到平衡。
非常好的总结!您已经准确地列出了 MySQL 优化器选择索引时的几个核心考量因素。
下面我将对您列出的每一点进行详细解释,并提供一个综合性的例子来说明优化器是如何权衡这些因素做出最终决定的。
MySQL 索引选择机制
一、索引选择机制
1. 索引选择性 (Selectivity)
定义:索引选择性是指索引列中不同值的数量与表中总记录数的比例。比例越高(越接近 1),选择性越好,意味着该索引的过滤能力越强。
- 高选择性 :例如
user_id
(用户ID)、order_no
(订单号)、email
(邮箱)等几乎唯一的字段。create_time
这类时间戳,如果精度很高(如毫秒级),也属于高选择性列。 - 低选择性 :例如
gender
(性别)、status
(状态标志,如 0/1)等,不同值很少。
优化器行为:优化器倾向于选择高选择性的索引,因为它能快速缩小扫描范围,排除大部分不相关的数据。
2. 索引覆盖 (Covering Index)
定义 :如果一个索引包含了查询语句所需要的所有字段(包括 SELECT
、WHERE
、GROUP BY
、ORDER BY
子句中的字段),则查询只需要扫描索引而无需回表(无需根据主键ID再去主键索引中查找数据行),这可以极大地提升性能。
优化器行为:即使某个索引的选择性不是最高的,但如果它是一个覆盖索引,优化器也可能会选择它,因为避免回表的成本收益可能远大于扫描稍多索引条目的成本。
3. WHERE 条件匹配 (最左前缀原则)
定义:MySQL 使用 B+Tree 索引,索引键是从左到右存储的。查询条件必须匹配索引的最左前缀才能有效利用该索引。
- 例如,有一个联合索引
(a, b, c)
:WHERE a = 1
能使用索引。WHERE a = 1 AND b = 2
能使用索引。WHERE b = 2
或WHERE c = 3
不能使用这个索引(或只能部分使用,效率低下)。
优化器行为 :优化器会寻找能够与 WHERE
子句中的条件进行最佳匹配的索引,优先选择匹配最左前缀更多的索引。
4. 统计信息 (Statistics)
定义 :MySQL 会为每个索引收集统计信息,例如表中数据的分布情况、每个索引的基数(Cardinality,即索引列中不同值的估计数量)等。这些信息存储在 information_schema
库中,并不完全精确,是通过采样估算得来的。
优化器行为 :优化器基于这些统计信息来估算不同执行路径的成本(Cost)。它会计算"全表扫描的成本"、"使用索引A的成本"、"使用索引B的成本",然后选择它认为成本最低的方案。你可以使用 ANALYZE TABLE table_name;
来更新表的统计信息,以防优化器因为统计信息过时而做出错误判断。
二、举例说明
假设我们有一张用户订单表 orders
,包含约 1000 万条数据,结构如下:
sql
CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`product_id` int(11) NOT NULL,
`status` tinyint(4) DEFAULT '0' COMMENT '0未支付,1已支付,2已发货',
`create_time` datetime NOT NULL,
`amount` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_status_created` (`status`,`create_time`),
KEY `idx_user_product` (`user_id`,`product_id`)
) ENGINE=InnoDB;
场景:我们要查询某个用户在某个时间段内的所有订单。
sql
SELECT *
FROM orders
WHERE user_id = 123
AND create_time BETWEEN '2023-10-01 00:00:00' AND '2023-10-31 23:59:59';
现在,优化器面临多个索引选择,它会进行成本估算:
-
候选索引
idx_user_id
(user_id
)- 选择性 :
user_id
选择性通常很高(很多用户,每个用户订单数相对较少)。条件user_id = 123
能快速定位到该用户的所有订单,假设有 100 条。 - 匹配度 :完美匹配
WHERE
条件的user_id
部分。 - 索引覆盖 :
SELECT *
需要所有字段,该索引不覆盖查询。找到这 100 条记录后,需要回表 100 次 去主键索引获取完整数据行,再在这些行中过滤create_time
。 - 成本:100 次回表操作。
- 选择性 :
-
候选索引
idx_create_time
(create_time
)- 选择性 :
create_time
选择性极高(几乎唯一)。但BETWEEN
范围查询可能覆盖很多行,比如 10 月份有 31 天,假设扫描到 10 万条记录。 - 匹配度 :完美匹配
WHERE
条件的create_time
部分。 - 索引覆盖 :不覆盖查询。需要回表 10 万次 ,并在每次回表后检查
user_id = 123
这个条件。 - 成本 :10 万次回表操作 + 10 万次
user_id
过滤。成本显然远高于方案1。
- 选择性 :
-
候选索引
idx_status_created
(status
,create_time
)- 匹配度 :
WHERE
条件中没有status
,无法利用最左前缀。这个索引基本无效(除非索引条件下推ICP有一定帮助,但效率依然低下)。 - 成本:可能近乎全表扫描。
- 匹配度 :
-
候选索引
idx_user_product
(user_id
,product_id
)- 选择性 :联合索引的首字段是
user_id
,选择性很高。 - 匹配度 :完美匹配最左前缀
user_id
。 - 索引覆盖 :不覆盖查询(包含了
user_id, product_id
,但缺少create_time
等字段)。同样需要回表 100 次,然后在回表后的数据行中过滤create_time
。 - 成本 :和方案1
idx_user_id
的成本几乎一样。
- 选择性 :联合索引的首字段是
优化器的决策:
通过对比,优化器会估算出:
- 使用
idx_user_id
的成本 ≈ (扫描100条索引记录) + (100次回表) - 使用
idx_create_time
的成本 ≈ (扫描10万条索引记录) + (10万次回表 + 10万次过滤) - ...其他方案成本更高。
因此,优化器毫无疑问会选择 idx_user_id
索引,因为它的成本最低。
场景变体:索引覆盖的威力
现在让我们修改一下查询语句,只查询索引中已有的字段:
sql
SELECT user_id, product_id -- 这两个字段在 idx_user_product 索引中都有
FROM orders
WHERE user_id = 123
AND create_time BETWEEN '2023-10-01 00:00:00' AND '2023-10-31 23:59:59';
这时,情况发生了改变:
idx_user_product
(user_id
,product_id
) 现在是一个覆盖索引!它包含了所有需要查询的字段。- 优化器会优先选择这个索引。虽然它仍然需要在索引扫描后根据
create_time
进行过滤(因为create_time
不在索引中),但这个过滤是在索引页中完成的,避免了昂贵的回表操作。100次索引条目的过滤成本远低于100次回表成本。
在这个新场景下,优化器很可能从 idx_user_id
转向 idx_user_product
,因为覆盖索引带来的性能提升非常大。
总结
MySQL 优化器的索引选择是一个基于成本的复杂决策过程。它会:
- 列出所有可能使用的索引。
- 根据统计信息,估算每个索引需要扫描的行数(基数估算)。
- 考虑是否满足索引覆盖,这能极大降低成本。
- 综合扫描行数、回表成本、排序成本等因素,计算出每个计划的总成本。
- 选择成本最低的执行计划。
你可以使用 EXPLAIN
命令来查看优化器最终选择的索引(key
字段)以及其做出该决定的理由(如 rows
, Extra
中的 Using index
等),EXPLAIN的使用具体可以参考:MySQL关于EXPLAIN进行Sql优化命令详解
好的,这是一份非常实用的MySQL索引最佳实战指南,涵盖了如何高效使用索引以及如何避免常见的索引失效陷阱。
MySQL索引最佳实战 & 避坑指南
一、索引设计的最佳实践
-
只为高选择性字段建索引
- 好的:用户名、手机号、邮箱、订单号(唯一或近乎唯一)
- 避免 :性别、状态、布尔类型字段(如
is_deleted
)。除非它们经常与其他高选择性字段组成复合索引。
-
优先使用复合索引,避免冗余索引
- 一个设计良好的复合索引
(a, b, c)
远比三个单列索引(a)
,(b)
,(c)
更高效。 - 遵循最左前缀原则,将最常用作等值查询的字段放在左边,范围查询和排序的字段放在右边。
- 示例 :
INDEX idx_status_createtime (status, create_time)
可以同时优化WHERE status=1
和WHERE status=1 ORDER BY create_time
。
- 一个设计良好的复合索引
-
尽量使用覆盖索引
- 让索引包含查询所需的所有字段,避免回表。
- 示例 :如果常用查询是
SELECT id, name, status FROM users WHERE status=1
,那么创建INDEX (status, name, id)
就是一个覆盖索引,性能极佳。
-
控制索引数量
- 索引不是越多越好。每个索引都会增加写操作(INSERT/UPDATE/DELETE)的开销和磁盘空间消耗。
- 通常建议,单表的索引数量不超过5-6个。
二、导致索引失效的常见陷阱及解决方案
以下情况会导致索引失效,变成全表扫描(type: ALL
)。
1. 【陷阱】对索引列进行运算或使用函数
-
失效写法 :
sqlSELECT * FROM users WHERE YEAR(create_time) = 2023; -- 对字段使用函数 SELECT * FROM users WHERE age + 1 > 30; -- 对字段进行运算
-
优化策略 :
-
将运算或函数操作转移到常量一侧。
-
优化后 :
sqlSELECT * FROM users WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01'; SELECT * FROM users WHERE age > 29; -- (30 - 1)
-
2. 【陷阱】隐式类型转换
-
失效写法 :
sqlSELECT * FROM users WHERE phone = 13800138000; -- phone是字符串类型(varchar),而条件是数字
-
优化策略 :
-
确保查询条件的类型与字段定义的类型完全一致。
-
优化后 :
sqlSELECT * FROM users WHERE phone = '13800138000';
-
3. 【陷阱】LIKE
以通配符 %
开头
-
失效写法 :
sqlSELECT * FROM users WHERE name LIKE '%张%'; -- 全表扫描 SELECT * FROM users WHERE name LIKE '%张'; -- 全表扫描
-
优化策略 :
-
尽量使用左前缀匹配。
-
如果必须进行全文搜索,使用MySQL的
FULLTEXT
全文索引或专业的搜索引擎(如Elasticsearch)。 -
优化后 :
sqlSELECT * FROM users WHERE name LIKE '张%'; -- 可以使用索引
-
4. 【陷阱】错误使用 OR
-
失效写法 :
sqlSELECT * FROM users WHERE status = 1 OR age > 20; -- 如果age无索引,则全表扫描
-
优化策略 :
-
使用
UNION
或UNION ALL
来拆分OR
条件,确保每个部分都能使用索引。 -
优化后 :
sqlSELECT * FROM users WHERE status = 1 UNION ALL SELECT * FROM users WHERE age > 20;
-
注意 :确保
age
字段也有索引。
-
5. 【陷阱】在复合索引中违背最左前缀原则
-
失效写法 :
sql-- 假设有复合索引 (status, create_time) SELECT * FROM users WHERE create_time > '2024-01-01'; -- 跳过了最左的status字段,索引失效
-
优化策略 :
-
查询条件必须包含复合索引的最左列。
-
优化后 :
sqlSELECT * FROM users WHERE status = 1 AND create_time > '2024-01-01'; -- 索引生效
-
6. 【陷阱】使用 NOT LIKE
, !=
, <>
, NOT IN
-
失效写法 :
sqlSELECT * FROM users WHERE status != 1; SELECT * FROM users WHERE name NOT LIKE '张%'; SELECT * FROM users WHERE id NOT IN (1, 2, 3);
-
优化策略 :
-
这类否定查询很难利用索引。通常需要重写业务逻辑,或尝试改为范围查询。
-
示例 :
sql-- 有时可以改为范围查询 SELECT * FROM users WHERE status > 1 OR status < 1; -- 可能有效,但不如=高效
-
7. 【陷阱】索引列使用 IS NULL
或 IS NOT NULL
-
失效写法 :
sqlSELECT * FROM users WHERE phone IS NULL; -- 可能失效
-
优化策略 :
- 建议将字段定义为
NOT NULL
并设置默认值(如空字符串),从根本上避免NULL值。 - 如果业务必须允许NULL,可尝试测试其使用索引的情况,但不要抱太高期望。
- 建议将字段定义为
FULLTEXT 全文索引的妙用
一、什么是FULLTEXT索引?
FULLTEXT
索引是一种特殊类型的索引,它不像B-Tree索引那样进行精确匹配或前缀匹配,而是设计用于在文本内容中执行自然语言搜索。它更像一个内置的微型搜索引擎。
核心支持:仅适用于 MyISAM 和 InnoDB 存储引擎(MySQL 5.6+ 开始支持 InnoDB 全文索引)。现在普遍使用 InnoDB,所以无需担心兼容性问题。
支持的数据类型:CHAR, VARCHAR, TEXT
二、核心妙用与优势
1. 终极解决方案:替代低效的 LIKE '%...%'
- 问题 :
SELECT * FROM articles WHERE content LIKE '%数据库%';
会导致全表扫描,性能极差。 - 解决方案 :使用
FULLTEXT
索引后,同样的查询速度极快,即使是在百万行的大表中。
2. 实现智能分词搜索 (Tokenization)
这是最强大的功能。FULLTEXT
索引不会简单地进行字符串匹配,而是:
- 自动分词:将文本按词进行拆分(如:"I love databases" -> "I", "love", "databases")。
- 忽略停用词:自动忽略常见的无意义词(如 "the", "a", "an", "in" 等)。
- 词干提取(部分支持):例如搜索 "running" 也能匹配到 "run"。
3. 支持多种搜索模式
a) 自然语言模式 (IN NATURAL LANGUAGE MODE) - 最常用
sql
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('数据库优化' IN NATURAL LANGUAGE MODE);
- 返回相关度最高的结果。
- 结果默认按相关度分数降序排列。
b) 布尔模式 (IN BOOLEAN MODE) - 功能最强
支持丰富的操作符来实现高级搜索:
+
:必须包含 (AND)-
:必须不包含 (NOT)~
:相关性降低*
:通配符 (后缀)""
:短语搜索
示例:
sql
-- 必须包含"MySQL",必须不包含"Oracle",可以包含"SQL"(有则加分)
SELECT * FROM articles
WHERE MATCH(content) AGAINST('+MySQL -Oracle ~SQL' IN BOOLEAN MODE);
-- 搜索完整的短语 "MySQL database"
SELECT * FROM articles
WHERE MATCH(content) AGAINST('"MySQL database"' IN BOOLEAN MODE);
-- 搜索所有以 "data" 开头的词 (data, database, databse等)
SELECT * FROM articles
WHERE MATCH(content) AGAINST('data*' IN BOOLEAN MODE);
c) 查询扩展模式 (WITH QUERY EXPANSION)
- 先执行一次搜索,然后用结果中的高频词再次搜索,以扩大搜索范围。
- 用于解决"一词多义"问题,但可能会引入一些不相关的结果。
4. 按相关度排序
可以获取并按相关度分数排序,这是 LIKE
完全无法实现的。
sql
SELECT
id,
title,
MATCH(title, content) AGAINST('数据库') AS score -- 计算相关度分数
FROM articles
WHERE MATCH(title, content) AGAINST('数据库')
ORDER BY score DESC;
三、如何使用:实战步骤
1. 创建FULLTEXT索引
sql
-- 建表时创建
CREATE TABLE articles (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255),
content TEXT,
FULLTEXT ft_index (title, content) -- 对title和content创建联合全文索引
) ENGINE=InnoDB;
-- 或者后期添加
ALTER TABLE articles ADD FULLTEXT ft_index_title_content (title, content);
-- 也可以为单个列创建
ALTER TABLE articles ADD FULLTEXT ft_index_content (content);
2. 执行全文搜索查询
基本查询:
sql
-- 简单查询(默认自然语言模式)
SELECT * FROM articles WHERE MATCH(title, content) AGAINST('数据库');
-- 明确指定模式
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('MySQL -Oracle' IN BOOLEAN MODE);
四、适用场景与限制
✅ 完美适用场景:
- 文章、博客系统:搜索标题和正文。
- 产品目录:搜索产品名称和描述。
- 文档管理系统:搜索文档内容。
- 论坛和评论系统:搜索帖子内容和评论。
⚠️ 重要限制与注意事项:
- 存储引擎 :仅支持 InnoDB (MySQL 5.6+)和 MyISAM。
- 语言支持 :对中文的支持需要依赖分词器。原生的FULLTEXT对中文分词效果不佳,因为它默认按空格分词。
- 中文解决方案 :
-
使用MySQL内置的ngram分词器 (推荐):
sqlCREATE TABLE articles ( content TEXT, FULLTEXT ft_index (content) WITH PARSER ngram -- 使用ngram分词器 ) ENGINE=InnoDB;
-
或者将中文文本预先分词,用空格隔开再存入数据库。
-
对于大型项目,仍建议使用 Elasticsearch 或 Solr 等专业搜索引擎。
-
- 最小词长 :
ft_min_word_len
(MyISAM)和innodb_ft_min_token_size
(InnoDB)参数定义了被索引的最小词长,默认通常是3或4个字符。 - 停用词:常见虚词不会被索引。
五、那字符串全部都用FULLTEXT 索引好了?
这是一个非常自然的想法,但千万不要这么做! 这是一个经典的"用锤子拧螺丝"的问题。虽然FULLTEXT索引很强大,但它和普通索引(B-Tree)是用途完全不同的两种工具。
把它们比作工具箱里的工具就很好理解了:
- 普通索引 (B-Tree) :像一把精准的螺丝刀。用于精确匹配、排序、范围查找。
- 全文索引 (FULLTEXT) :像一把强力的电锯。用于在大量文本中模糊地切割和搜索关键词。
1、功能不匹配:它做不了普通索引的工作
场景一:精确查询 ( Equality Query )
- 你需要 :
SELECT * FROM users WHERE username = 'john123';
- FULLTEXT :
... WHERE MATCH(username) AGAINST('john123')
- 它会把 'john123' 当成一个词去搜索,但无法保证精确唯一匹配。
- 如果表中存在 'john1234' 或 'john123abc',它们也可能被匹配出来,因为FULLTEXT的本质是"包含"。
- 普通索引:完美解决。快速定位到唯一一条记录。
场景二:前缀查询 ( Prefix Query )
- 你需要 :
SELECT * FROM users WHERE phone LIKE '138%';
(查找138开头的所有手机号) - FULLTEXT:无法实现。它不支持左前缀匹配,只支持"包含"或"词条"匹配。
场景三:范围查询 ( Range Query ) 和排序
- 你需要 :
SELECT * FROM products WHERE price > 100 AND price < 200 ORDER BY price DESC;
- FULLTEXT:完全无法处理数字和日期的范围查询与排序。
2、性能问题:杀鸡用牛刀,反而更慢
- 开销巨大 :FULLTEXT索引的结构比B-Tree索引复杂得多,创建和维护它的CPU和磁盘开销非常大。
- 查询延迟:对于简单的精确匹配查询,使用FULLTEXT的 overhead(解析查询词、计算相关度等)远比直接在B-Tree上做一次简单查找要高。
- 空间占用:FULLTEXT索引通常比B-Tree索引占用更多的磁盘空间。
3、唯一性约束失效
- 普通索引 可以设置为
UNIQUE
,保证字段值的唯一性(如用户名、手机号)。 - FULLTEXT索引****没有唯一性约束的概念。你无法用它来防止重复数据的插入。