Práctica de MySQL de alto rendimiento (2): índice

 Ya hemos establecido la estructura de la tabla en el artículo anterior  Práctica de MySQL de alto rendimiento (1): estructura de la tabla . En este artículo, crearemos un índice para la tabla basado en la estructura de la tabla existente y las condiciones de búsqueda.



1. Cree un índice basado en las condiciones de búsqueda.

Primero obtengamos el SQL de inicialización de la estructura de la tabla:

  
  
  
  
  
CREATE TABLE `service_log` (  `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',  `service_type` smallint NOT NULL DEFAULT -1 COMMENT '接口类型',  `service_name` varchar(30) DEFAULT '' COMMENT '接口名称',  `service_method` tinyint NOT NULL DEFAULT -1 COMMENT '接口方式 1-HTTP 2-TCP',  `serial_no` int DEFAULT -1 COMMENT '消息序号',  `service_caller` tinyint DEFAULT -1 COMMENT '调用方',  `service_receiver` tinyint DEFAULT -1 COMMENT '接收方',  `status` tinyint DEFAULT 10 COMMENT '状态 10-成功 20-异常',  `error_message` varchar(200) DEFAULT '' COMMENT '异常信息',  `message` varchinar(1000) DEFAULT '' COMMENT '报文内容',  `create_user` varchar(50) DEFAULT '' COMMENT '创建者',  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',  `update_user` varchar(50) DEFAULT '' COMMENT '更新者',  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',  `is_delete` tinyint NOT NULL DEFAULT 0 COMMENT '刪除标志',  `ts` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '时间戳',  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='接口调用日志';

Están disponibles las siguientes condiciones de búsqueda:

  1.  Busque el registro de llamadas correspondiente según  el nombre de la interfaz

  2.  Consultar registros de llamadas exitosas o anormales según  el estado

  3.  Consultar registros de llamadas según  el nombre  y  el estado de la interfaz

  4.  Obtenga un conjunto de registros de llamadas según  el número de secuencia del mensaje

  5.  Consultar el registro de llamadas según  el rango de tiempo del tiempo de creación

  6.  Consultar registros de llamadas según el  contenido del mensaje

El índice es la forma más eficaz de mejorar el rendimiento de las consultas . Puede localizar registros rápidamente, reducir en gran medida la cantidad de datos que deben escanearse y convertir E/S aleatorias en E/S secuenciales. Además, se almacenará el índice del árbol B+. secuencialmente según el valor del índice, por lo que también se puede utilizar para ordenar y agrupar .

Para tener una mejor experiencia al ver estos registros de llamadas de interfaz, necesitamos crear un índice basado en las condiciones de búsqueda.

El tipo de índice debe ser lo más pequeño posible.

Primero centrémonos en las condiciones de búsqueda del nombre de la interfaz. Podemos encontrar que los dos campos de nombre de interfaz y tipo de interfaz pueden consultar los datos de registro del mismo tipo de interfaz, pero sus tipos son diferentes. El primero es un tipo de cadena, y este último es de tipo Entero.

En este momento debemos prestar atención: el tipo de columna seleccionada para crear un índice debe ser lo más pequeña posible . Debido a que cada creación de un índice es equivalente a la creación de un "árbol B", cuanto más pequeño sea el tipo de datos, menos espacio de almacenamiento ocupará el índice y se pueden almacenar más registros en una página de datos, por lo que la E/S del disco La pérdida de rendimiento será menor.

Además, la comparación de datos enteros dentro de MySQL es más simple y eficiente que la comparación de tipos de cadenas. Por lo tanto, elegiremos crear un índice para el tipo de interfaz en lugar de crear un índice para el nombre de la interfaz.

El SQL para agregar un índice a la columna de tipo de interfaz es el siguiente:

  
  
  
  
  
alter table service_log add index index_service_type(`service_type`);

Según la condición 4, la columna del número de secuencia del mensaje también debe indexarse:

  
  
  
  
  
alter table service_log add index index_serial_no(`serial_no`);

Índices redundantes y duplicados

De manera similar, según la condición de búsqueda 2, agregamos un índice a la columna de estado:

  
  
  
  
  
alter table service_log add index index_status(`status`);

En este momento, veamos la condición 3. Necesitamos agregar un índice conjunto para el tipo y estado de la interfaz . Sin embargo, cabe señalar que el índice conjunto y el índice de tipo de interfaz agregado son índices duplicados. De acuerdo con el principio de coincidencia más a la izquierda del índice conjunto , el índice conjunto cuya primera columna es el tipo de interfaz también puede atender consultas solo con tipos de interfaz. Por lo tanto, debemos eliminar el índice original agregado para el tipo de interfaz y crear un nuevo índice conjunto para el tipo y estado de la interfaz.

  
  
  
  
  
-- 删除 index_service_typealter table service_log drop index index_service_type;
-- 添加联合索引alter table service_log add index index_service_type_status(`service_type`, `status`);

Existe una regla general importante al crear un índice conjunto: coloque el valor de la columna con la tasa de repetición más baja al principio del índice. Si hay demasiados valores repetidos, se escanearán más filas de datos, lo que ejercerá mucha presión para devolver la tabla.

Por lo general, crear múltiples índices de una sola columna de forma independiente para las columnas en la condición WHERE no mejora el rendimiento de las consultas MySQL en la mayoría de los casos . Deberíamos considerar el orden de las columnas del índice o crear un índice de cobertura completa siempre que sea posible .

Cree índices para columnas con baja tasa de repetición

在我们的实际业务中,接口调用的状态几乎所有都是成功,很少会出现失败的情况,所以这时我们为状态列创建索引并不是很合适。因为如果我们查询所有状态为成功的数据,那么它可能会执行太多次的回表操作,导致查询效率下降,可能还不如执行全表扫描来的快。但是我们再考虑另一种情况,有时我们会根据状态为失败的记录做业务分析或排查问题,失败的数据是比较少的,如果我们通过索引查询就会非常高效,所以该列索引还有必要保留。

只不过我们在这里需要做一个处理:如果状态为成功时,我们为生成的 SQL 语句添加上忽略索引的关键字 ignore index(index_name),那么这样我们就能达到在查询成功状态的数据时全表扫描,而在查询失败状态的数据时使用索引了。

  
  
  
  
  
select * from service_log ignore index(index_status)where status = 10;

全值匹配和按值范围匹配的时间列

条件 5 根据创建时间来进行全值匹配和按值范围匹配非常适合创建索引:

  
  
  
  
  
alter table service_log add index index_create_time(`create_time`);

全文索引

FULLTEXT 全文索引是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值,更类似于搜索引擎所做的事情。在查询时适用于 MATCH AGAINST 操作,而不是普通的 WHERE 条件。

对于条件 5,我们需要在接口请求的报文中根据关键字,比如说包裹号来查询特定的数据,这就使得我们需要为报文内容列创建全文索引,SQL 如下:

  
  
  
  
  
alter table service_log add fulltext fulltext_message(`message`);
-- 执行查询时的语句select * from service_log where match(message) against('123456');

全文索引在日常使用的并不多,它有许多需要注意的细节,如停用词、词干、复数和布尔搜索等,具体的详情信息可以查看文末的参考文献。

那么,最终初始化表结构的 DDL 语句如下:

  
  
  
  
  
CREATE TABLE `service_log` (  `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',  `service_type` smallint NOT NULL DEFAULT -1 COMMENT '接口类型',  `service_name` varchar(30) DEFAULT '' COMMENT '接口名称',  `service_method` tinyint NOT NULL DEFAULT -1 COMMENT '接口方式 1-HTTP 2-TCP',  `serial_no` int DEFAULT -1 COMMENT '消息序号',  `service_caller` tinyint DEFAULT -1 COMMENT '调用方',  `service_receiver` tinyint DEFAULT -1 COMMENT '接收方',  `status` tinyint DEFAULT 10 COMMENT '状态 10-成功 20-异常',  `error_message` varchar(200) DEFAULT '' COMMENT '异常信息',  `message` varchar(1000) DEFAULT '' COMMENT '报文内容',  `create_user` varchar(50) DEFAULT '' COMMENT '创建者',  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',  `update_user` varchar(50) DEFAULT '' COMMENT '更新者',  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',  `is_delete` tinyint NOT NULL DEFAULT 0 COMMENT '刪除标志',  `ts` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '时间戳',  PRIMARY KEY (`id`),  index index_serial_no(`serial_no`),  index index_status(`status`),  index index_create_time(`create_time`),  index index_service_type_status(`service_type`, `status`),  fulltext fulltext_message(`message`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='接口调用日志';

前缀索引

一般情况下,对于 VARCHAR、BLOB 和 TEXT 等相关类型的列创建索引时,为了提升索引的性能和节省索引空间,会只对字段的前一部分字符进行索引,不过这样做的缺点是使得索引的选择性降低。

索引的选择性是指不重复的索引值和记录总数的比值,可以理解为重复率越低选择性越高,唯一索引的选择性为 1。

在我们的数据库表示例中,并没有字段适合建立前缀索引。其中报文内容列也并不适合创建前缀索引,因为这些报文的前缀都很相似,而且我们在执行查询时并不会带上前缀,而是只使用关键词信息查询。

但是,前缀索引比较重要,所以我们在这里也对创建前缀索引的方法介绍一下。

MySQL 并不支持对这些长字符类型列的完整内容进行索引,我们选择前缀长度的关键点在于:既要保证选择足够长的前缀使得选择性较高,同时又不能太长防止占用太多的空间。

可以根据如下方法来确定前缀的长度:

首先,查看要添加索引的列出现最频繁的一些值:

  
  
  
  
  
select count(0) as c, specific_columnfrom specific_tablegroup by specific_columnorder by c desclimit 10;

之后先从 3 个前缀字母开始匹配尝试:

  
  
  
  
  
select count(0) as c, left(specific_column, 3) as preffrom specific_tablegroup by preforder by c desclimit 10;

慢慢地增加前缀长度,直到这个前缀的选择性接近我们首次查询的完整列的选择性即可。

或者,采用如下的方法,先计算出完整列的选择性:

  
  
  
  
  
select count(distinct specific_column) / count(0)from specific_table;

然后分别计算不同前缀的选择性,直到找到与完整列接近的选择性前缀长度即可:

  
  
  
  
  
select count(distinct left(specific_column, 3)) / count(0) as sel3,count(distinct left(specific_column, 4)) / count(0) as sel4,count(distinct left(specific_column, 5)) / count(0) as sel5,count(distinct left(specific_column, 6)) / count(0) as sel6,count(distinct left(specific_column, 7)) / count(0) as sel7from specific_table;

不过,也有例外的情况,那就是即使现在我们选择了比较接近完整列选择性的前缀,但数据的分布仍然很不均匀。

这时我们需要用该前缀执行如下查询,并与完整列查询出的数目作比较,观察这些出现频率最高的前缀值与完整列出现频率是否接近,否的话需要再将前缀值调大。

  
  
  
  
  
select count(0) as c, left(specific_column, 5) as preffrom specific_tablegroup by preforder by c desclimit 10;
-- 完整列的出现频率select count(0) as c, specific_columnfrom specific_tablegroup by specific_columnorder by c desclimit 10;

最后,找到合适的前缀数创建前缀索引可以使用如下 SQL:

  
  
  
  
  
alter table specific_table add index index_specific_column(specific_column(7));

虽然前缀索引能够使索引更小,更快,但是我们不能使用前缀索引做 ORDER BY 和 GROUP BY 操作,也无法使用前缀索引做索引覆盖。



二、关于索引必须知道的事儿

下文中我们所说的索引如果没有特别指明类型,那么就代表我们说的是 B+ Tree 索引,它使用 B+ Tree 数据结构来保存数据。

B+ Tree 会将所有的数据保存在叶子节点上,并且通过双向链表将叶子节点连接起来。

聚簇索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式,InnoDB 聚簇索引在数据页中同时保存索引和数据行,这使得它的数据访问相比于非聚簇索引(二级索引)要快。

聚簇的意思是说数据行和相邻的键值紧凑的存储在一起,因为无法同时把数据行放在两个不同的地方,所以一个表只能有一个聚簇索引。InnoDB 根据 主键 聚簇数据,如果没有定义主键,InnoDB 会自动生成一个唯一的隐式主键作为聚簇索引。

我们创建一个简单的表,并插入一些数据,来看一下 B+ Tree 索引的数据结构图:

  
  
  
  
  
create table demo (    c1 int,    c2 int,    c3 char(1),    primary key(c1))engine=InnoDB;

MySQL 是通过数据页来保存数据的,每个页的大小默认为 16KB,在每个数据页中都默认有最小记录Infimum和最大记录Supremum,如下图所示:

我们可以发现在叶子节点中保存了所有数据行,每个页之间通过页文件头部(File Header)记录的双向链表指针进行连接,数据记录之间通过单向链表连接,单向链表的指针记录在每行数据记录的记录头信息中。

在非叶子节点中,我们可以发现记录的信息只有主键值和对应的页号,因此数据页能存放的数据更多,B+ Tree 也就能更加 “矮胖”,这样就能使得磁盘 I/O 更少。一般情况下我们用到的 B+ Tree 不会超过 4 层。

B+ Tree 按照索引列数据的大小顺序排序存储,所以很适合按照范围来查询。每次搜索数据都从索引的根节点开始,通过比较节点中的值和要查找的值来找到合适的指针进入下层子节点,最终在叶子节点中找到或找不到对应的记录。

聚簇索引能够加快我们访问数据的速度,但是它也有一些局限性我们需要了解一下:

  • 聚簇索引最大限度地提高了 I/O 密集型应用的性能,但如果数据全部都放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优势了

随着 RAM 变得更便宜,而且许多数据集不是那么大,所以将它们全部保存在内存中是非常可行的,包括可能分布在多个服务器上,这也促进了内存数据库的发展。

  • 插入速度严重依赖于插入顺序按照主键的顺序插入行是将数据加载到 InnoDB 表中最快的方式。但如果不是按照主键的顺序插入,会因页分裂影响插入速度。最好避免随机的聚簇索引,特别是对于 I/O 密集型的应用

  • 聚簇索引列更新的代价很高,因为它会强制 InnoDB 将每个被更新的行移动到新的位置,这也会发生页分裂,导致性能下降

二级索引

二级索引是非聚簇索引,InnoDB 引擎在 B+ Tree 的叶子节点存储的不是完成的数据记录,而只是索引列和主键列的值。如果在查询时没有发生覆盖索引的话,需要根据主键值进行回表操作以获取需要的结果。

二级索引是关系型数据库的基础,并且在文档数据库中也很普遍。许多键值存储(如 HBase 和 Volde-mort)为了减少实现的复杂度而放弃了二级索引,但是一些(如 Riak)已经开始添加它们,因为它们对于数据模型实在是太有用了。并且次级索引也是 Solr 和 Elasticsearch 等搜索服务器的基石。

实际上,有两种用二级索引对文档数据库进行分区的方法:基于文档(document-based)的分区和基于关键词(term-based)的分区。

*基于文档的分区

假设我们有一个汽车销售网站,每条数据都有唯一的 ID,我们称之为文档 ID。我们使用文档 ID 进行分区,并为汽车颜色字段创建二级索引,分区结果如下图所示:

这样的二级索引分配方法,使得每个分区都是独立的:每个分区自己维护自己的索引,它不关心其他分区的数据,这种文档分区索引也被称为本地索引

当我们查询红色的汽车时,需要将请求发布到所有的分区,并合并所有返回的结果,这种查询数据库的方法被称为 分散/聚集,可能会使得二级索引查询数据比较耗时。

*基于关键词的分区

我们也可以构建一个覆盖所有分区数据的全局索引,比如我们将 a 到 r 开头的颜色的二级索引保存在分区 0 中,将 s 到 z 的保存在分区 1 中,如下图所示:

我们将这种分区方法称为关键词分区,根据关键词本身分区对于范围扫描非常有用,比如说我现在想获取 a 到 r 开头的颜色的所有汽车数据;而对关键词的哈希分区又能够提供分区负载均衡的能力。

基于关键词分区的全局索引优于文档分区索引的地方在于它的读取更加高效,并不需要将请求打到所有分区上,只需要将请求发送到含有对应关键词的分区即可,而它的缺点在于对单个分区文档的写入可能会产生多个分区的索引的数据变更,需要协调跨分区的分布式事务。

覆盖索引

覆盖索引可以简单地理解成查询只需要访问索引列而无需访问其他数据列

优秀的索引设计不单单只考虑 WHERE 条件,也会根据想要查询的列去综合分析。如果只需要索引列的话,那么覆盖索引是非常有用的工具,它能避免回表操作,这样 MySQL 就会极大地减少数据访问量,而且索引占用的空间很小,将这些数据缓存在内存中的压力远小于缓存所有相关数据行。

如果业务无需查询其他列,那么我们最好把业务需要的列放在查询列表中,以实现覆盖索引,而不是简单地以 * 来替代;在某些情况下,可以根据想要查询的列,对所使用的索引进行扩展,即增加想要查询的列达到覆盖索引的目的。

当执行一个覆盖索引的查询时,在 EXPLAIN 的 Extra 列可以看到 Using index 的信息。

自适应哈希索引

它是 InnoDB 的一个特性,当 InnoDB 发现某些索引值被非常频繁的访问时,它会在原有的 B+ Tree 索引之上,再在内存中构建一个哈希索引,以此来加快对应数据的访问。这个过程是自动化的,我们无法进行干预,不过可以通过参数配置将其关闭。

参考资料:

[1] 《数据密集型应用系统设计》:第三章、第六章

[2] 《高性能 MySQL 第四版》:第七章

[3] 《MySQL 是怎样运行的》:第四、五、六、七章

[4] 14.6.2.4 InnoDB Full-Text Indexes


-end-

本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

罚款 200 元,没收 100 多万 尤雨溪:高质量中文文档的重要性 马斯克硬核迁移服务器 TCP 拥塞控制拯救了互联网 Apache OpenOffice 是事实上的“无人维护”项目 Google 庆祝成立 25 周年 微软开源 windows-drivers-rs,用 Rust 开发 Windows 驱动程序 Raspberry Pi 5 将于 10 月底发布,60 美元起售 macOS Containers:在 macOS 用 Docker 运行 macOS 镜像 IntelliJ IDEA 2023.3 EAP 发布
{{o.name}}
{{m.name}}

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/10114790
Recomendado
Clasificación