索引的代价

空间代价:

  • 每个 B+ 树的节点使用 16KB

时间代价:

  • 维护索引是耗时的

一个表上的索引越多, 就会占用更多的存储空间, 在增删改时的性能越差.

B+ 树索引适用的条件

什么情况下能让查询利用索引?

以联合索引为例:

  1. CREATE TABLE person_info(
  2. id INT NOT NULL auto_increment,
  3. name VARCHAR(100) NOT NULL,
  4. birthday DATE NOT NULL,
  5. phone_number CHAR(11) NOT NULL,
  6. country varchar(100) NOT NULL,
  7. PRIMARY KEY (id),
  8. KEY idx_name_birthday_phone_number (name, birthday, phone_number)
  9. );

全值匹配

  • 索引列在查询条件中都出现. 顺序无关 (被优化了)
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';

匹配左边的列

  • 使用联合索引靠左边的列
SELECT * FROM person_info WHERE name = 'Ashburn';
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27';

# 这个例子用不到 B+ 树索引
SELECT * FROM person_info WHERE birthday = '1990-09-27';

# 这个例子只能使用 name
SELECT * FROM person_info WHERE name = 'Ashburn' AND phone_number = '15123983239';

匹配列前缀

SELECT * FROM person_info WHERE name LIKE 'As%';

# 无法使用索引
SELECT * FROM person_info WHERE name LIKE '%As%';

匹配范围值

  • 由于记录是排好序的, 所以直接取查好的两端值那段
  • 多个范围查找时, 只有最靠左的列的范围查找才用索引
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' AND birthday > '1980-01-01';

精确匹配某一列并范围匹配另外一列

  • 左边的列使用精确查找, 右边的列使用范围查找
# name, birthday 用到索引, phone_number 没用到
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday > '1980-01-01' AND birthday < '2000-12-31' AND phone_number > '15100000000';
# 都用到了
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1980-01-01' AND phone_number > '15100000000';

用于排序

  • 在MySQL中,把这种在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:filesort)
  • 如果ORDER BY子句里使用到了我们的索引列,就有可能省去在内存或文件中排序的步骤
# ORDER BY name, birthday, phone_number 三个字段
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;

使用联合索引进行排序注意事项

  • order by 后的列的顺序也必须是索引列的顺序
  • order by + 左边部分索引
  • where + 左边部分索引 + order by 右边部分索引:
SELECT * FROM person_info WHERE name = 'A' ORDER BY birthday, phone_number LIMIT 10;
  • 不能使用索引进行排序的情况
    • ASC, DESC 混用 (mysql8.0 支持逆序索引)
    • WHERE 子句中出现非排序使用到的索引列
    • 排序列包含非同一个索引的列
SELECT * FROM person_info WHERE country = 'China' ORDER BY name LIMIT 10;
SELECT * FROM person_info WHERE name = 'A' ORDER BY birthday, phone_number LIMIT 10;
SELECT * FROM person_info ORDER BY name, country LIMIT 10;
  • 排序列使用了复杂的表达式
SELECT * FROM person_info ORDER BY UPPER(name) LIMIT 10;

用于分组

  • 分组列的顺序需要和索引列的顺序一致
  • 可以只使用左边的列进行分组

回表的代价

  • 在使用索引的情况下, 二级索引时顺序 I/O 的, 快, 回表时是随即 I/O 的, 慢
  • 尽量避免过多的回表

一般情况下,限制查询记录数会让优化器更倾向于选择使用二级索引 + 回表的方式进行查询:

SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' LIMIT 10;

覆盖索引

彻底告别回表, 建议查询列只包含索引列:

  • 这种只查询索引列的方式称为索引覆盖
SELECT name, birthday, phone_number FROM person_info WHERE name > 'Asa' AND name < 'Barlow'

所以引出 “挑选索引”, 使用哪些列作为索引:

  • 只为用于搜索, 排序, 分组的列创建索引
    • WHERE, JOIN, ORDER BY, GROUP BY 后出现的列
  • 考虑列的基数
    • 列的基数: 某一列中不重复数据的个数
    • 基数越小越, 索引越没用, 没法排序
  • 索引列的类型尽量小
    • 时间, cpu 计算快
    • 空间, 减少磁盘 I/O
  • 索引字符串值的前缀
    • 不使用完整的字符串作为索引, 只使用前几个字符
    • 会导致无法使用索引排序
CREATE TABLE person_info(
    name VARCHAR(100) NOT NULL,
    birthday DATE NOT NULL,
    phone_number CHAR(11) NOT NULL,
    country varchar(100) NOT NULL,
    KEY idx_name_birthday_phone_number (name(10), birthday, phone_number)
);
  • 让索引列在比较表达式中单独出现
WHERE my_col * 2 < 4 # 不使用索引
WHERE my_col < 4/2

主键插入顺序

  • 页分裂
  • 记录移动

优化:

  • 设置自增 ID 的主键

冗余和重复索引

# name 上的重复索引, 没好处
CREATE TABLE person_info(
    id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    birthday DATE NOT NULL,
    phone_number CHAR(11) NOT NULL,
    country varchar(100) NOT NULL,
    PRIMARY KEY (id),
    KEY idx_name_birthday_phone_number (name(10), birthday, phone_number),
    KEY idx_name (name(10))
);

# 主键本身自动生成聚簇索引
CREATE TABLE repeat_index_demo (
    c1 INT PRIMARY KEY,
    c2 INT,
    UNIQUE uidx_c1 (c1),
    INDEX idx_c1 (c1)
);