好的,我们来详细解释一下最左前缀匹配原则,并尽可能全面地列出典型的索引失效情况,每个情况都配上示例。

一、最左前缀匹配原则 (Leftmost Prefix Rule)

  • 核心概念: 当你在数据库表上创建了一个复合索引(也叫联合索引,包含多个列)时,这个索引可以被用来加速那些查询条件只使用了该索引最左边一个或连续多个列的查询。它并不要求查询条件必须包含索引中的所有列,但必须从最左边的列开始,并且是连续的(不能跳过中间的列)。
  • 类比理解: 想象一本电话簿,它首先按姓氏排序,姓氏相同再按名字排序。
    • 查找“姓张的人”(只用最左列姓氏) - 索引有效(快速定位到所有张姓区域)
    • 查找“姓张且名三的人”(用了连续的姓氏+名字) - 索引有效(在张姓区域内快速找到张三)
    • 查找“名叫三的人”(只用名字,跳过了姓氏) - 索引无效(必须扫描整本书,因为名字的排序只在同姓下有效)
    • 查找“姓张且出生日期是某天的人”(用了姓氏,跳过了名字,用了出生日期) - 索引无效(在张姓区域内,出生日期不是按索引排序的,除非索引包含了出生日期且名字条件用IS NULL或范围覆盖了所有可能名字,但这很特殊且通常低效)。
  • 数据库底层原理 (B+树): 复合索引在B+树中存储时,数据首先按索引定义的第一列排序,在第一列值相同的情况下,按第二列排序,以此类推。查询时,数据库只能有效地利用索引进行查找,如果它能提供一个或多个索引列的值,并且这些值是从索引定义的最左边开始的连续列。
  • 关键点总结:
    1. 必须从最左列开始。
    2. 不能跳过中间的列。 (除非跳过的列在查询条件中是IS NULL或使用了覆盖索引等特定情况,但通常视为失效或效率降低)
    3. 可以只使用最左边连续的若干列。
    4. 范围查询后的列无法使用索引排序或精确匹配。

二、典型的索引失效情况与示例

假设我们有一个用户表 users,并在其上创建了一些索引:

CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100),
age INT,
country VARCHAR(50),
city VARCHAR(50),
created_at DATETIME NOT NULL,
INDEX idx_username (username), -- 单列索引
INDEX idx_country_city (country, city), -- 复合索引
INDEX idx_age_created (age, created_at) -- 复合索引
);

典型索引失效情况

  1. 未遵循最左前缀原则 (跳过了最左列):

    • 原因: 复合索引 (col1, col2, col3) 的排序依赖于 col1。跳过 col1 直接查询 col2col3,数据库无法利用索引的有序性进行快速定位。
    • 示例:
      SELECT * FROM users WHERE city = 'New York'; -- 索引 idx_country_city 失效,因为跳过了最左列 `country`
      SELECT * FROM users WHERE created_at > '2023-01-01'; -- 索引 idx_age_created 失效,因为跳过了最左列 `age`
  2. 在索引列上使用函数或表达式:

    • 原因: 索引存储的是列的原始值。对列应用函数或表达式后,数据库无法直接使用索引值进行匹配,需要计算每一行的函数结果后再比较。
    • 示例:
      SELECT * FROM users WHERE YEAR(created_at) = 2023; -- 在 created_at 上使用了 YEAR() 函数,任何包含 created_at 的索引都失效
      SELECT * FROM users WHERE age + 1 > 30; -- 在 age 上进行了计算,索引 idx_age_created 失效
      SELECT * FROM users WHERE UPPER(username) = 'JOHNDOE'; -- 在 username 上使用了 UPPER() 函数,索引 idx_username 失效
  3. 在索引列上进行运算:

    • 原因: 同函数一样,运算改变了列的原始值。
    • 示例:
      SELECT * FROM users WHERE id * 2 = 100; -- 在 id (主键索引) 上进行了乘法运算,索引失效
  4. 使用 OR 连接非索引列条件:

    • 原因: 如果 OR 连接的多个条件中,并非所有涉及的列都单独建立了索引,数据库通常无法有效合并索引扫描结果(除非优化器选择 Index Merge 策略,但这并非总是可行或高效),最终可能退化为全表扫描。
    • 示例:
      SELECT * FROM users WHERE username = 'john' OR email = 'john@example.com';
      -- 情况1: 只有 idx_username 存在,email 无索引 -> 索引 idx_username 对 OR 条件整体失效(需全表扫)。
      -- 情况2: 如果同时存在 idx_username 和 idx_email -> 优化器 *可能* 使用 Index Merge 策略(如 union),此时两个索引都可能有效。但这取决于数据库优化器选择和版本。
  5. 隐式类型转换:

    • 原因: 当查询条件中的值类型与索引列定义的类型不匹配时,数据库需要执行隐式类型转换。这相当于在列上应用了一个转换函数,导致索引失效。
    • 示例:
      -- 假设 phone 字段是 VARCHAR(20) 且有索引
      SELECT * FROM users WHERE phone = 13800138000; -- 数字 13800138000 被隐式转换为字符串,导致 phone 索引失效
      -- 假设 age 是 INT 且有索引
      SELECT * FROM users WHERE age = '30'; -- 字符串 '30' 被隐式转换为整数,通常 *可能不会* 导致索引失效(因为转换是确定性的且发生在常量端),但写法不推荐且依赖数据库实现。
  6. 使用 !=<> (不等于):

    • 原因: 不等于操作符需要查找所有不等于特定值的行。对于非唯一索引或非主键索引,数据库通常认为扫描整个索引或全表扫描比利用索引定位再过滤掉大量数据更高效(除非不等于的值匹配了极少数行,且优化器能识别)。
    • 示例:
      SELECT * FROM users WHERE username <> 'admin'; -- 索引 idx_username 很可能失效(除非 'admin' 是绝大多数行)
  7. 使用 NOT INNOT EXISTS:

    • 原因: 类似于 !=,需要排除大量数据,优化器倾向于全表扫描。
    • 示例:
      SELECT * FROM users WHERE country NOT IN ('US', 'UK'); -- 索引 idx_country_city 失效
  8. 使用 IS NULLIS NOT NULL (对非稀疏索引):

    • 原因: 标准B+树索引通常不存储 NULL 值(或将其视为特殊值)。查询 IS NULL 时,如果索引不包含 NULL 记录,则无法使用索引。查询 IS NOT NULL 时,需要排除 NULL,这通常相当于扫描所有非 NULL 值,优化器可能认为全表扫描更快。注意: 有些数据库(如 MySQL InnoDB)的二级索引是包含 NULL 值的,理论上 IS NULL 在特定条件下可能使用索引(如果 NULL 值很少),但 IS NOT NULL 通常仍会导致索引失效。实践中,通常认为两者都可能导致索引失效。
    • 示例:
      SELECT * FROM users WHERE email IS NULL; -- 索引 (如果有在 email 上) 可能有效也可能无效,取决于数据库和 NULL 比例
      SELECT * FROM users WHERE email IS NOT NULL; -- 索引 (如果有在 email 上) 很可能失效
  9. 使用前导通配符的 LIKE (%xxx):

    • 原因: 索引是按列值的完整内容排序的。以通配符 % 开头意味着模式的前缀是未知的,数据库无法利用索引的有序性进行快速定位(就像电话簿里找名字以 "son" 结尾的人一样困难)。
    • 示例:
      SELECT * FROM users WHERE username LIKE '%doe'; -- 索引 idx_username 失效
      SELECT * FROM users WHERE username LIKE '%john%'; -- 索引 idx_username 失效 (两个 %)
    • 例外: LIKE 'xxx%' (后缀通配符) 通常可以有效利用索引,因为模式的开头是固定的。
  10. 复合索引中,第一列使用范围查询后,后续列索引失效:

    • 原因: 复合索引 (col1, col2)。当 col1 使用范围查询(>, <, BETWEEN)时,数据库可以快速定位到 col1 满足范围的索引片段。但是,在这个片段内部,col2 的值是无序的(索引只在 col1 相同的情况下才按 col2 排序)。因此,对于 col2 的条件,数据库无法利用索引进行排序或高效的精确匹配/范围扫描,通常需要在 col1 的范围结果内逐行扫描过滤 col2
    • 示例:
      SELECT * FROM users WHERE country = 'US' AND city = 'New York'; --  索引 (country, city) 有效 (等值+等值)
      SELECT * FROM users WHERE country = 'US' AND city LIKE 'N%'; -- 索引有效 (等值+后缀通配符)
      SELECT * FROM users WHERE country IN ('US', 'CA') AND city = 'Seattle'; -- 对于 IN 内的每个 country,city 条件有效。但整体效率取决于 IN 列表大小和优化器。通常认为部分有效。
      SELECT * FROM users WHERE country > 'C' AND city = 'London'; -- `country` 是范围查询,`city` 条件无法利用索引排序和高效过滤。索引对 `city` 条件失效。
      SELECT * FROM users WHERE age BETWEEN 20 AND 30 AND created_at = '2023-10-01'; -- `age` 是范围查询,`created_at` 条件无法利用索引。索引 idx_age_created 对 `created_at` 失效。
  11. 查询列未被索引覆盖且需要回表,优化器判断全表扫描更快:

    • 原因: 如果 SELECT * 或者查询了不在索引中的列,数据库即使使用了索引定位行,也需要根据索引中的指针(通常是主键值)回到主键索引(聚簇索引)或数据文件中取出完整的行数据(称为 回表)。如果筛选条件过滤掉的行很少(即满足条件的行数非常多),优化器可能认为直接扫描整个表(尤其是如果表很小或大部分数据在内存中)比通过索引查找再大量回表更高效。
    • 示例:
      -- 假设表很大,但 country='XX' 是一个非常大的国家,占表中大部分数据
      SELECT * FROM users WHERE country = 'XX'; -- 虽然有索引 idx_country_city, 但优化器可能选择全表扫描,避免大量回表操作。
  12. 索引列选择性过低 (数据重复度过高):

    • 原因: 如果索引列的值几乎都一样(例如 gender 列只有 'M'/'F'),那么使用这个索引筛选出的行数仍然非常多,数据库优化器可能会认为使用索引带来的好处(减少IO)不足以抵消额外的索引查找和可能的回表开销,从而选择全表扫描。
    • 示例:
      -- 假设 country 列 90% 的值都是 'US'
      SELECT * FROM users WHERE country = 'US'; -- 虽然有索引 idx_country_city, 但优化器很可能选择全表扫描。
  13. 使用 ORDER BY 的列与索引排序方式不一致:

    • 原因: 索引默认是升序 (ASC) 存储的。如果 ORDER BY 子句使用了索引列,但是排序方向是降序 (DESC),或者混合了升序降序(且与索引定义不一致),数据库可能无法直接利用索引的有序性来避免额外的排序操作(filesort)。
    • 示例:
      SELECT * FROM users WHERE country = 'US' ORDER BY city DESC; -- 索引 (country, city ASC) 可能用于 WHERE, 但 ORDER BY city DESC 需要额外排序。
      -- 创建索引 (country, city DESC) 可以优化这个查询。
      SELECT * FROM users ORDER BY country ASC, city DESC; -- 索引 (country ASC, city ASC) 无法直接用于此混合排序。
  14. 统计信息过时:

    • 原因: 数据库优化器依赖表和索引的统计信息(如行数、不同值数量、数据分布直方图)来估算不同执行计划的成本。如果这些统计信息没有及时更新(例如在大量插入、删除、更新后),优化器可能会错误地估算使用索引的成本,从而选择次优计划(如本应使用索引却选择了全表扫描,或相反)。
    • 示例: 没有特定查询示例,这是一个维护问题。需要定期运行数据库的 ANALYZE TABLE 或类似命令更新统计信息。

关键建议

  1. 善用 EXPLAIN: 这是诊断查询执行计划和索引使用情况的最重要工具。在你写的 SQL 语句前加上 EXPLAIN (或 EXPLAIN ANALYZE),分析输出结果中的 key (使用的索引)、type (访问类型,如 ref, range, index, ALL 表示全表扫描)、Extra (额外信息,如 Using where, Using filesort, Using index) 等字段。
  2. 设计合适的索引: 根据最频繁的查询模式(WHERE, JOIN, ORDER BY, GROUP BY)来设计索引,优先考虑高选择性的列,并遵循最左前缀原则。
  3. 考虑覆盖索引: 如果查询只需要访问索引中包含的列,就可以避免回表操作,显著提升性能 (EXPLAINExtra 列会显示 Using index)。
  4. 避免过度索引: 索引会占用空间,并在数据插入、更新、删除时带来维护开销。只为必要的查询创建索引。
  5. 保持统计信息准确: 定期更新表统计信息,让优化器做出更明智的决定。
  6. 理解数据库特性: 不同数据库管理系统(MySQL, PostgreSQL, SQL Server, Oracle)在索引实现和优化器行为上可能存在细微差异,查阅官方文档了解细节。

通过理解最左前缀原则和这些典型的索引失效场景,你可以更有效地设计索引、编写高效的SQL查询,并诊断性能问题。

【MySQL索引失效场景】索引失效原因及最左前缀原则详解的更多相关文章

  1. MySQL数据库中的索引(二)——索引的使用,最左前缀原则

    上文中,我们了解了MySQL不同引擎下索引的实现原理,在本文我们将继续探讨一下索引的使用以及优化. 创建索引可以大大提高系统的性能. 第一,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性. ...

  2. MySQL 的索引和最左前缀原则

    这两天看<构建高性能Web站点>这本书,感觉写的真是不错,很多实际项目中会碰到的问题都有所提及,今天看到一个最左前缀原则,以前也听说过,不过一直没搞明白,今天查了下. 通过实例理解单列索引 ...

  3. MySQL索引 索引分类 最左前缀原则 覆盖索引 索引下推 联合索引顺序

    MySQL索引 索引分类 最左前缀原则 覆盖索引 索引下推 联合索引顺序   What's Index ? 索引就是帮助RDBMS高效获取数据的数据结构. 索引可以让我们避免一行一行进行全表扫描.它的 ...

  4. MySql最左前缀原则

    简单整理记录下,之前一直都没有关注过这个问题 最左前缀原则:顾名思义是最左优先,以最左边的为起点任何连续的索引都能匹配上, 注:如果第一个字段是范围查询需要单独建一个索引 注:在创建多列索引时,要根据 ...

  5. MySQL高可用架构之Mycat-关于Mycat安装和参数设置详解

    MySQL高可用架构之Mycat-关于Mycat安装和参数设置详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Mycat介绍 1>.什么是Mycat Mycat背后是 ...

  6. MySQL中tinytext、text、mediumtext和longtext等各个类型详解

    转: MySQL中tinytext.text.mediumtext和longtext等各个类型详解 2018年06月13日 08:55:24 youcijibi 阅读数 26900更多 个人分类: 每 ...

  7. 深入浅出分析MySQL MyISAM与INNODB索引原理、优缺点、主程面试常问问题详解

    本文浅显的分析了MySQL索引的原理及针对主程面试的一些问题,对各种资料进行了分析总结,分享给大家,希望祝大家早上走上属于自己的"成金之路". 学习知识最好的方式是带着问题去研究所 ...

  8. 【转】MYSQL数据库四种索引类型的简单使用--MYSQL组合索引“最左前缀”原则

    MYSQL数据库索引类型包括普通索引,唯一索引,主键索引与组合索引,这里对这些索引的做一些简单描述: (1)普通索引 这是最基本的MySQL数据库索引,它没有任何限制.它有以下几种创建方式: 创建索引 ...

  9. MySQL基础篇(04):存储过程和视图,用法和特性详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.存储过程 1.概念简介 存储程序是被存储在服务器中的组合SQL语句,经编译创建并保存在数据库中,用户可通过存储过程的名字调用执行.存储过程 ...

  10. MySQL基础篇(05):逻辑架构图解和InnoDB存储引擎详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.MySQL逻辑架构 1.逻辑架构图 基于下面的逻辑架构图,可以大致熟悉MySQL各个架构组件之间的协同工作关系. 很经典的C/S架构风格, ...

随机推荐

  1. jFinal 使用 SolonMCP 开发 MCP(拥抱新潮流)

    MCP 官方的 java-sdk 目前只支持 java17+.直接基于 mcp-java-sdk 也比较复杂.使用 SolonMCP,可以基于 java8 开发(像 MVC 的开发风格),且比较简单. ...

  2. ListBox横向排布Item

    <Window x:Class="TwoColumnListBox.MainWindow" xmlns="http://schemas.microsoft.com/ ...

  3. Hystrix 服务的隔离策略对比,信号量与线程池隔离的差异

    支持的隔离策略 Hystrix支持的 hytrix支持线程池隔离和信号量隔离 信号量的隔离: it executes on the calling thread and concurrent requ ...

  4. 使用aqt安装Qt历史版本

    首先是安装aqt: Windows 端 pip install -U pip pip install aqtinstall Mac端(可以用pip,也可以用brew) brew install aqt ...

  5. ssh免密登录设置 (普通用户和root用户)

    普通用户如何设置免密码ssh到另一台服务器 一.场景描述: 我有A,B两台服务器,需要实现让A服务器的普通用户(ftpuser)不用输入密码就可以连接到B的(ftpuser)(此处A,B服务器的账户可 ...

  6. C#/.NET/.NET Core技术前沿周刊 | 第 40 期(2025年5.26-5.31)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  7. gRPC-go的一些tips

    1.请注意,在 gRPC-Go 中,RPC 以阻塞/同步模式运行,这意味着 RPC 调用等待服务器响应,并且将返回响应或错误. Note that in gRPC-Go, RPCs operate i ...

  8. P5749 [IOI2019] 排列鞋子

    算是一种新思路吧. 题目要求我们求最少的对调次数,想到了什么?求逆序对个数,我们只需将原来的 \(S_i\) 数组转化一下,求其逆序对个数即可. 转化规则为:从头开始,对于每个还未被赋值的 \(S_i ...

  9. Git镜像网站和Git网站提速方法

    最近开始学习使用git,但是因为git是国外的网站,所以基本就是无法访问.如下图: 通过在网上查找资料,我发现了几个访问git的方法. 方法一.通过镜像网站 镜像网站一: https://github ...

  10. Squid代理配置使用

    1.Squid简单说明 Squid Cache(简称为Squid)是HTTP代理服务器软件.Squid用途广泛,可以作为缓存服务器,可以过滤流量帮助网络安全,也可以作为代理服务器链中的一环,向上级代理 ...