¿Está utilizando el conteo en MySQL correctamente? Comparación de rendimiento de un vistazo

Este artículo es el segundo artículo de la columna " Aprendizaje inductivo de MySQL ", y también es el segundo artículo sobre los puntos de conocimiento de consultas de MySQL.

Revisión pasada:

Guía divertida de MySQL: Exploración de los componentes de la capa del servidor y práctica de verificación de permisos

En MySQL, count() es una poderosa función estadística, pero ¿lo sabías? ¡Se implementa de manera diferente en diferentes motores! No solo eso, este artículo también lo llevará a comprender las diferencias de rendimiento de los diferentes usos de conteo y le dirá qué uso es el más eficiente. Además, exploraremos las diferencias entre los esquemas de uso de sistemas de almacenamiento en caché y bases de datos para llevar cuentas, revelando el contraste entre ellos.

Primero, echemos un vistazo a este mapa mental y tengamos una comprensión simple del contenido de este artículo.

Cómo se implementa count (*)

En diferentes motores de MySQL, count( ) tiene diferentes métodos de implementación, y aquí se analizan las condiciones de count() sin filtro.

  • El motor MyISAM almacena el número total de filas en una tabla en el disco, por lo que *cuando se ejecuta count(), devolverá directamente este número, lo cual es muy eficiente; si se agrega la condición where, la tabla MyISAM no puede regresar tan rápido.
  • Pero el motor InnoDB es problemático, *cuando ejecuta count(), necesita leer los datos del motor línea por línea y luego acumular el conteo.

¿Por qué InnoDB no almacena números como MyISAM?

Esto se debe a que "cuántas filas se deben devolver" para una tabla InnoDB es incierta debido al control de concurrencia multiversión (MVCC), incluso para varias consultas al mismo tiempo.

Como se muestra en el siguiente caso, los resultados del número total de filas en la tabla de consulta t de las tres sesiones son diferentes al mismo tiempo.

imagen

Esto tiene algo que ver con el diseño de transacciones de InnoDB.La lectura repetible es su nivel de aislamiento predeterminado, que se implementa en el código a través del control de concurrencia de múltiples versiones, es decir, MVCC. Cada fila de registros debe evaluarse si es visible para la sesión, por lo que para la solicitud de conteo (*), InnoDB tiene que leer los datos fila por fila y juzgar a su vez, y solo las filas visibles se pueden usar para calcular el número total de filas en la tabla "según esta consulta".

Aunque la ejecución de count() en el motor InnoDB *requiere una lectura línea por línea, la optimización de consultas aún se realiza internamente. InnoDB es una tabla organizada por índices, los nodos de hoja del árbol de índice de clave principal son datos y los nodos de hoja del árbol de índice secundario son valores de clave principal. Por lo tanto, el árbol de índice ordinario es mucho más pequeño que el árbol de índice de clave principal. Para operaciones como count(*), los resultados obtenidos al recorrer qué árbol de índices son lógicamente los mismos. Por lo tanto, el optimizador de MySQL encontrará el árbol más pequeño para recorrer. Bajo la premisa de asegurar la lógica correcta, minimizar la cantidad de datos escaneados es uno de los principios generales del diseño de sistemas de bases de datos.

Además de ejecutar el *comando count() para obtener el número de filas de datos, también hemos utilizado show table statusel comando, que se utiliza para mostrar cuántas filas hay actualmente en la tabla, pero debe tenerse en cuenta que los resultados obtenidos por este comando se estiman por muestreo, y el documento oficial dice que el error puede llegar al 40 % o al 50 %. Por lo tanto, el número de filas que muestra el comando show table status no se puede usar directamente.

Resumir

  • Aunque el recuento de tablas MyISAM ( *) es muy rápido, no admite transacciones;
  • Aunque el comando show table status regresa rápidamente, no es preciso;
  • El recuento directo de la tabla InnoDB (*) recorrerá toda la tabla, aunque el resultado es preciso, causará problemas de rendimiento.

Diferentes usos de contar

Analice el rendimiento de diferentes usos, como contar (*), contar (id de clave principal), contar (campo) y contar (1), ¿cuáles son las diferencias?

count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加。最后返回累计值。

所以,count(*)、count(主键 id) 和 count(1) 都表示返回满足条件的结果集的总行数;而 count(字段),则表示返回满足条件的数据行里面,参数“字段”不为 NULL 的总个数。

对于 count(主键 id) 来说,InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。

count(主键 id) 不会走主键索引,因为普通索引树比主键索引树小很多。假设表中有多个普通索引树,则由优化器来决定走哪个索引。

对于 count(1) 来说,InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。

单看这两个用法的差别的话,你能对比出来,count(1) 执行得要比 count(主键 id) 快。因为从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作。

对于 count(字段) 来说:

  1. 如果这个“字段”是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;
  2. 如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。

count(字段) 需要查询出该字段值,只能通过聚簇索引树,所以效率最差。

但是 count(\*) 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*) 肯定不是 null,直接按行累加。

主键 ID肯定非空,为什么优化器不能像优化 count()那样优化count(主键ID) 呢?答案是没必要,不做重复优化,推荐使用 count()。

根据上述分析,按照效率排序的话,count(字段)<count(主键 id)<count(1)≈count(*),所以我建议你,尽量使用 count(*)

有些文章说 count() 性能差,用词不恰当,难道其他几种计数方式就不差了,注意是计数性能差,而不是count()差。关于计数性能差,可以增加缓存,比如说 redis缓存或者本地缓存,但是不能保证完全实时一致。

用缓存系统保存计数

对于更新很频繁的库来说,你可能会第一时间想到,用缓存系统来支持。

你可以用一个 Redis 服务来保存这个表的总行数。这个表每被插入一行 Redis 计数就加 1,每被删除一行 Redis 计数就减 1。这种方式下,读和更新操作都很快,但你再想一下这种方式存在什么问题吗?

没错,缓存系统可能会丢失更新。

Redis 的数据不能永久地留在内存里,所以你会找一个地方把这个值定期地持久化存储起来。但即使这样,仍然可能丢失更新。试想如果刚刚在数据表中插入了一行,Redis 中保存的值也加了 1,然后 Redis 异常重启了,重启后你要从存储 redis 数据的地方把这个值读回来,而刚刚加 1 的这个计数操作却丢失了。

当然了,这还是有解的。比如,Redis 异常重启以后,到数据库里面单独执行一次 count(*) 获取真实的行数,再把这个值写回到 Redis 里就可以了。异常重启毕竟不是经常出现的情况,这一次全表扫描的成本,还是可以接受的。

但实际上,将计数保存在缓存系统中的方式,还不只是丢失更新的问题。即使 Redis 正常工作,这个值还是逻辑上不精确的

Redis 和 MySQL 是两个独立的数据源,我们需要解决并发环境下数据不一致的问题,一般我们都会先更新数据库,再删缓存。

我们查询如下两个时序图:

imagen

会话A在 T2时刻执行了插入操作,在 T3时刻会话B读取缓存中的计数,那么此时读取到的计数和会话A事务结束后读取到的计数就会发生不一致。

如果在会话A中调整更新计数操作和插入操作的顺序,那么是否会有所好转呢?

imagen

答案还是不行。虽然在 T3 时刻会话B 可以查询到最新的计数,但是无法获取到待插入的数据R。

因为 Redis 和 MySQL 是不同的存储构成的系统,不支持分布式事务,所以没法保证计数的精确性。

在数据库保存计数

根据上面的分析,用缓存系统保存计数有丢失数据和计数不精确的问题。那么,如果我们把这个计数直接放到数据库里单独的一张计数表 C 中,又会怎么样呢?

首先,这解决了崩溃丢失的问题,InnoDB 是支持崩溃恢复不丢数据的。

利用事务来解决时序2 图中的问题,如下所示:

imagen

因为MySQL 事务机制和 MVCC,在 T3时刻会话B进行的操作不受会话A 的影响,因为会话A在 T4才提交事务,T2做的修改对会话B不可见。

总结

在不同的存储引擎中,count(*)函数的实现方式不同。我们之前讨论过使用缓存系统来存储计数值存在的问题。现在,我来简洁地解释一下为什么将计数值存储在Redis中不能保证与MySQL表中的数据精确一致。

Redis和MySQL是不同的存储系统,它们不支持分布式事务,因此无法提供精确一致的视图。这就是为什么将计数值存储在Redis中无法确保与MySQL表中数据的一致性。相比之下,将计数值存储在MySQL中可以解决一致性视图的问题。

Supongo que te gusta

Origin juejin.im/post/7257922419319603256
Recomendado
Clasificación