Utilisez-vous correctement le nombre dans MySQL ? Comparaison des performances en un coup d'œil

Cet article est le deuxième article de la rubrique " MySQL Inductive Learning ", et c'est aussi le deuxième article sur les points de connaissance des requêtes MySQL.

Avis passé :

MySQL Fun Guide : Explorer les composants de la couche serveur et la pratique de vérification des privilèges

Dans MySQL, count() est une puissante fonction statistique, mais le saviez-vous ? Il est implémenté différemment dans différents moteurs ! Non seulement cela, cet article vous amènera également à comprendre les différences de performances des différentes utilisations de comptage et vous indiquera quelle utilisation est la plus efficace. De plus, nous explorerons les différences entre les schémas d'utilisation des systèmes de mise en cache et des bases de données pour tenir les comptes, révélant le contraste entre eux.

Tout d'abord, jetons un coup d'œil à cette carte mentale et comprenons simplement le contenu de cet article.

Comment count(*) est implémenté

Dans différents moteurs MySQL, count( ) a différentes méthodes d'implémentation, et ici nous discutons de count( ) sans conditions de filtre.

  • Le moteur MyISAM stocke le nombre total de lignes dans une table sur le disque, donc *quand count( ) est exécuté, il renverra directement ce nombre, ce qui est très efficace ; si la condition where est ajoutée, la table MyISAM ne peut pas revenir aussi rapidement.
  • Mais le moteur InnoDB est gênant. *Lorsqu'il exécute count(), il doit lire les données du moteur ligne par ligne, puis accumuler le décompte.

Pourquoi InnoDB ne stocke-t-il pas les numéros comme MyISAM ?

En effet, "combien de lignes doivent être renvoyées" pour une table InnoDB est incertain en raison du contrôle de la concurrence multiversion (MVCC), même pour plusieurs requêtes en même temps.

Comme illustré dans le cas suivant, les résultats du nombre total de lignes dans la table de requête t des trois sessions sont différents en même temps.

image

Cela a quelque chose à voir avec la conception des transactions d'InnoDB.La lecture répétable est son niveau d'isolement par défaut, qui est implémenté dans le code via le contrôle de la concurrence multi-version, c'est-à-dire MVCC. Chaque ligne d'enregistrements doit être jugée si elle est visible pour la session, donc pour la requête count(*), InnoDB doit lire les données ligne par ligne et juger tour à tour, et seules les lignes visibles peuvent être utilisées pour calculer le nombre total de lignes dans la table "basé sur cette requête".

Bien que l'exécution de count() dans le moteur InnoDB *nécessite une lecture ligne par ligne, l'optimisation des requêtes est toujours effectuée en interne. InnoDB est une table organisée en index, les nœuds feuille de l'arbre d'index de clé primaire sont des données et les nœuds feuille de l'arbre d'index secondaire sont des valeurs de clé primaire. Par conséquent, l'arborescence d'index ordinaire est beaucoup plus petite que l'arborescence d'index de clé primaire. Pour des opérations telles que count(*), les résultats obtenus en parcourant quel arbre d'index sont logiquement les mêmes. Par conséquent, l'optimiseur MySQL trouvera le plus petit arbre à parcourir. Sous le principe d'assurer la bonne logique, la minimisation de la quantité de données numérisées est l'un des principes généraux de la conception d'un système de base de données.

En plus d'exécuter la *commande count( ) pour obtenir le nombre de lignes de données, nous avons également utilisé show table statusla commande, qui sert à afficher le nombre de lignes actuellement présentes dans le tableau, mais il convient de noter que les résultats obtenus par cette commande sont estimés par échantillonnage, et le document officiel indique que l'erreur peut atteindre 40 % à 50 %. Par conséquent, le nombre de lignes affichées par la commande show table status ne peut pas être utilisé directement.

Résumer

  • Bien que la table MyISAM count( *) soit très rapide, elle ne prend pas en charge les transactions ;
  • Bien que la commande show table status retourne rapidement, elle n'est pas précise ;
  • InnoDB table direct count(*) traversera toute la table, bien que le résultat soit précis, cela causera des problèmes de performances.

Différentes utilisations du comptage

Analysez les performances de différents usages tels que count(*), count(primary key id), count(field) et count(1), quelles sont les différences ?

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 是两个独立的数据源,我们需要解决并发环境下数据不一致的问题,一般我们都会先更新数据库,再删缓存。

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

image

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

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

image

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

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

在数据库保存计数

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

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

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

image

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

总结

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

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

Je suppose que tu aimes

Origine juejin.im/post/7257922419319603256
conseillé
Classement