web-dev-qa-db-fra.com

Performances de comptage Mysql sur de très grandes tables

J'ai une table avec plus de 100 millions de lignes dans Innodb.

Je dois savoir s'il y a plus de 5000 lignes où la clé étrangère = 1. Je n'ai pas besoin du nombre exact.

J'ai fait quelques tests:

SELECT COUNT(*) FROM table WHERE fk = 1 => 16 secondes
SELECT COUNT(*) FROM table WHERE fk = 1 LIMIT 5000 => 16 secondes
SELECT primary FROM table WHERE fk = 1 => 0,6 seconde

J'aurai un réseau et un temps de traitement plus importants mais cela peut être une surcharge de 15,4 secondes!

As-tu une meilleure idée ?

Merci

Modifier: [Ajout des commentaires pertinents de l'OP]

J'ai essayé SELECT SQL_NO_CACHE COUNT (fk) FROM table WHERE fk = 1 mais cela a pris 25 secondes

Mysql a été réglé pour Innodb avec Mysql Tuner.

CREATE TABLE table ( pk bigint(20) NOT NULL AUTO_INCREMENT,
fk tinyint(3) unsigned DEFAULT '0', 
PRIMARY KEY (pk), KEY idx_fk (fk) USING BTREE ) 
ENGINE=InnoDB AUTO_INCREMENT=100380914 DEFAULT CHARSET=latin1

DB Stuff:

'have_innodb', 'YES' 'ignore_builtin_innodb', 'OFF' 'innodb_adaptive_hash_index', 'ON'    
'innodb_additional_mem_pool_size', '20971520' 'innodb_autoextend_increment', '8' 
'innodb_autoinc_lock_mode', '1' 'innodb_buffer_pool_size', '25769803776' 
'innodb_checksums', 'ON' 'innodb_commit_concurrency', '0',
'innodb_concurrency_tickets', '500' 'innodb_data_file_path',
'ibdata1:10M:autoextend' 'innodb_data_home_dir', '', 'innodb_doublewrite', 'ON'     
'innodb_fast_shutdown', '1' 'innodb_file_io_threads', '4' 
'innodb_file_per_table', 'OFF', 'innodb_flush_log_at_trx_commit', '1' 
'innodb_flush_method', '' 'innodb_force_recovery', '0' 'innodb_lock_wait_timeout', '50' 
'innodb_locks_unsafe_for_binlog', 'OFF' 'innodb_log_buffer_size', '8388608' 
'innodb_log_file_size', '26214400' 'innodb_log_files_in_group', '2' 
'innodb_log_group_home_dir', './' 'innodb_max_dirty_pages_pct', '90'     
'innodb_max_purge_lag', '0' 'innodb_mirrored_log_groups', '1' 'innodb_open_files', 
'300' 'innodb_rollback_on_timeout', 'OFF' 'innodb_stats_on_metadata', 'ON' 
'innodb_support_xa', 'ON' 'innodb_sync_spin_loops', '20' 'innodb_table_locks', 'ON' 
'innodb_thread_concurrency', '8' 'innodb_thread_sleep_delay', '10000'      
'innodb_use_legacy_cardinality_algorithm', 'ON'

pdate '15: J'ai utilisé jusqu'à présent la même méthode avec 600 millions de lignes et 640 000 nouvelles lignes par jour. Ça marche toujours bien.

35
hotips

Enfin, le plus rapide a été d'interroger les X premières lignes à l'aide de C # et de compter le nombre de lignes.

Mon application traite les données par lots. Le temps entre deux lots dépend du nombre de lignes à traiter

SELECT pk FROM table WHERE fk = 1 LIMIT X

J'ai obtenu le résultat en 0,9 seconde.

Merci à tous pour vos idées!

1
hotips

Vous ne semblez pas intéressé par le nombre réel, essayez donc ceci:

SELECT 1 FROM table WHERE fk = 1 LIMIT 5000, 1

Si une ligne est renvoyée, vous avez 5000 enregistrements et plus. Je suppose que la colonne fk est indexée.

19
Salman A

Les tables de compteur ou tout autre mécanisme de mise en cache sont la solution:

InnoDB ne conserve pas de nombre interne de lignes dans une table car les transactions simultanées peuvent "voir" différents nombres de lignes en même temps. Pour traiter une instruction SELECT COUNT (*) FROM t, InnoDB analyse un index de la table, ce qui prend un certain temps si l'index n'est pas entièrement dans le pool de tampons. Si votre table ne change pas souvent, l'utilisation du cache de requêtes MySQL est une bonne solution. Pour obtenir un décompte rapide, vous devez utiliser une table de compteurs que vous créez vous-même et laisser votre application la mettre à jour en fonction des insertions et des suppressions. Si un nombre approximatif de lignes est suffisant, SHOW TABLE STATUS peut être utilisé. Voir Section 14.3.14.1, "Astuces de réglage des performances InnoDB" .

19
scriptin

Je dois ajouter une autre réponse - j'ai de nombreuses corrections/ajouts aux commentaires et réponses jusqu'à présent.

Pour MyISAM, SELECT COUNT(*) sans WHERE est compté - très rapidement. Toutes les autres situations (y compris InnoDB dans la question) doivent compter via le BTree des données ou le BTree d'un index pour obtenir la réponse. Nous devons donc voir combien compter.

InnoDB met en cache les blocs de données et d'index (16 Ko chacun). Mais lorsque les données ou l'index BTree de la table sont supérieurs à innodb_buffer_pool_size, Vous êtes assuré de toucher le disque. Frapper le disque est presque toujours la partie la plus lente de tout SQL.

Le cache de requête, lorsqu'il est impliqué, entraîne généralement des temps de requête d'environ 1 milliseconde; cela ne semble pas être un problème avec les horaires cités. Je ne m'y attarderai donc pas.

Mais ... Exécuter deux fois la même requête ) exposition:

  • Première manche: 10 secondes
  • Deuxième manche: 1 seconde

Ceci est symptomatique de la première exécution devant récupérer la plupart des blocs du disque, tandis que la seconde a tout trouvé dans RAM (le buffer_pool). Je soupçonne que certains des timings répertoriés sont faux parce que de ne pas réaliser ce problème de mise en cache. (16 sec vs 0,6 sec peut être expliqué par cela.)

Je vais harpper les "hits de disque" ou les "blocs à toucher" comme métrique réelle dont SQL est plus rapide.

COUNT(x) vérifie x pour IS NOT NULL avant le décompte. Cela ajoute une petite quantité de traitement, mais ne modifie pas le nombre d'accès au disque.

Le tableau proposé a un PK et une deuxième colonne. Je me demande si c'est la vraie table ?? Cela fait une différence -

  • Si l'Optimiseur décide de lire les données - c'est-à-dire, numérisez dans l'ordre PRIMARY KEY - il lira les données BTree, qui est généralement (mais pas dans cet exemple boiteux) beaucoup plus large que les BTrees d'index secondaire.
  • Si l'Optimiseur décide de lire un index secondaire (mais pas besoin de faire un tri), il y aura moins de blocs à toucher. Par conséquent, plus vite.

Commentaires sur les requêtes originales:

SELECT COUNT(*) FROM table WHERE fk = 1 => 16 seconds
    -- INDEX(fk) is optimal, but see below
SELECT COUNT(*) FROM table WHERE fk = 1 LIMIT 5000 => 16 seconds
    -- the LIMIT does nothing, since there is only one row in the result
SELECT primary FROM table WHERE fk = 1 => 0.6 seconds
    -- Again INDEX(fk), but see below

WHERE fk = 1 Demande INDEX(fk, ...), de préférence juste INDEX(fk). Notez que dans InnoDB, chaque index secondaire contient une copie du pk. Autrement dit, INDEX(fk) est effectivement INDEX(fk, primary). Par conséquent, la troisième requête peut l'utiliser comme "couverture" et n'a pas besoin de toucher aux données.

Si le tableau n'est vraiment que les deux colonnes, alors probablement l'indice secondaire BTree sera plus gros que les données BTree. Mais dans les tableaux réalistes, l'indice secondaire sera plus petit. Par conséquent, un balayage d'index sera plus rapide (moins de blocs à toucher) qu'un balayage de table.

La troisième requête fournit également un grand ensemble de résultats; cela pourrait entraîner une longue période de requête - mais elle ne sera pas incluse dans le "temps" cité; c'est le temps réseau, pas le temps de requête.

innodb_buffer_pool_size = 25,769,803,776 Je suppose que la table et son index secondaire (du FK) font chacun environ 3-4 Go. Ainsi, tout timing pourrait d'abord devoir charger beaucoup de choses. Ensuite, une seconde exécution serait entièrement mise en cache. (Bien sûr, je ne sais pas combien de lignes ont fk=1; Probablement moins que toutes les lignes?)

Mais ... À 600 millions de lignes, la table et son index sont chacun s'approchant de la mémoire tampon de 25 Go. Ainsi, le jour viendra peut-être bientôt où il devient lié aux E/S - cela vous donnera envie de revenir à 16 (ou 25) secondes; mais vous ne pourrez pas. On peut alors parler d'alternatives à faire le COUNT.

SELECT 1 FROM tbl WHERE fk = 1 LIMIT 5000,1 - Analysons cela. Il va scanner l'index, mais il s'arrêtera après 5000 lignes. De tout ce dont vous avez besoin est "plus de 5K", c'est la meilleure façon de l'obtenir. Il sera toujours rapide (ne touchant qu'une douzaine de blocs), quel que soit le nombre total de lignes dans le tableau. (Il est toujours soumis aux caractéristiques buffer_pool_size et cache du système. Mais une douzaine de blocs prend beaucoup moins d'une seconde, même avec un cache froid.)

MariaDB LIMIT ROWS_EXAMINED peut valoir la peine d'être étudié. Sans cela, vous pourriez faire

SELECT COUNT(*) AS count_if_less_than_5K
    FROM ( SELECT 1 FROM tbl WHERE fk = 1 LIMIT 5000 );

Il peut être plus rapide que la livraison des lignes au client; il devra collecter les lignes en interne dans une table tmp, mais ne livrer que le COUNT.

Remarque: 640 000 lignes insérées par jour - cela approche la limite pour une seule ligne INSERTs dans MySQL avec vos paramètres actuels sur un disque dur (pas SDD). Si vous devez discuter de la catastrophe potentielle, ouvrez une autre question.

Conclusion:

  • Assurez-vous d'éviter le cache de requête. (en utilisant SQL_NO_CACHE ou en désactivant le QC)
  • Exécutez deux fois n'importe quelle requête de synchronisation; utiliser la deuxième fois.
  • Comprendre la structure et la taille des BTree impliqués.
  • N'utilisez pas COUNT(x) sauf si vous avez besoin de la vérification nulle.
  • N'utilisez pas l'interface mysql_* De PHP; passez à mysqli_* ou PDO.
6
Rick James

Si vous utilisez PHP vous pouvez faire mysql_num_rows sur le résultat que vous avez obtenu de SELECT primary FROM table WHERE fk = 1 => 0.6 seconds, Je pense que ce sera efficace.

Mais cela dépend de la langue côté serveur que vous utilisez

1
nischayn22

Si vous n'êtes pas intéressé à connaître le nombre de lignes et que vous souhaitez simplement tester COUNT par rapport à une certaine valeur, vous pouvez utiliser le script standard ci-dessous:

SELECT 'X'
FROM mytable
WHERE myfield='A'
HAVING COUNT(*) >5

Cela renverra une seule ligne ou aucune ligne du tout, selon que la condition est remplie.

Ce script est conforme à ANSI et peut être entièrement exécuté sans évaluer la valeur complète de COUNT (*). Si MySQL a implémenté l'optimisation pour arrêter l'évaluation des lignes une fois qu'une condition est remplie (j'espère vraiment que oui), alors vous obtiendrez une amélioration des performances. Malheureusement, je ne peux pas tester ce comportement moi-même car je n'ai pas de grande base de données MySQL disponible. Si vous faites ce test, veuillez partager le résultat ici :)

0
Gerardo Lima