Il existe une grande base de données, 1 000 000 000 de lignes, appelées threads (ces threads existent réellement, je ne complique pas les choses simplement parce que j'apprécie). Threads ne contient que quelques éléments pour accélérer les choses: (int id, chaîne de hachage, int replication, int dateline (timestamp), int id de forum, titre de chaîne)
Question:
select * from thread where forumid = 100 and replycount > 1 order by dateline desc limit 10000, 100
Depuis qu'il y a 1G d'enregistrements, la requête est assez lente. Alors j'ai pensé, divisons ce 1G d'enregistrements en autant de tables que de forums (catégorie) que j'ai! C'est presque parfait. Avec beaucoup de tables, j'ai moins de disques à parcourir et c'est vraiment plus rapide. La requête devient maintenant:
select * from thread_{forum_id} where replycount > 1 order by dateline desc limit 10000, 100
C’est vraiment plus rapide avec 99% des forums (catégorie) puisque la plupart d’entre eux n’ont que peu de sujets (100k-1M). Cependant, comme il y en a avec environ 10 millions d’enregistrements, certaines requêtes doivent encore être ralenties (0,1/0,2 seconde, trop pour mon application !, J'utilise déjà des index! ).
Je ne sais pas comment améliorer cela avec MySQL. Y a-t-il un moyen?
Pour ce projet, j'utiliserai 10 serveurs (12 Go de RAM, disque dur 4x7200 tr/min sur le logiciel Raid 10, quad core)
L'idée était simplement de diviser les bases de données entre les serveurs, mais le problème expliqué ci-dessus n'est toujours pas suffisant.
Si j'installe cassandra sur ces 10 serveurs (en supposant que je trouve le temps de le faire fonctionner comme prévu), devrais-je être supposé avoir un gain de performances?
Que dois-je faire? Continuer à travailler avec MySQL avec une base de données distribuée sur plusieurs machines ou créer un cluster de cassandra?
On m'a demandé d'afficher quels sont les index, les voici:
mysql> show index in thread;
PRIMARY id
forumid
dateline
replycount
Sélectionnez expliquer:
mysql> explain SELECT * FROM thread WHERE forumid = 655 AND visible = 1 AND open <> 10 ORDER BY dateline ASC LIMIT 268000, 250;
+----+-------------+--------+------+---------------+---------+---------+-------------+--------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+---------+---------+-------------+--------+-----------------------------+
| 1 | SIMPLE | thread | ref | forumid | forumid | 4 | const,const | 221575 | Using where; Using filesort |
+----+-------------+--------+------+---------------+---------+---------+-------------+--------+-----------------------------+
Vous devriez lire ce qui suit et en apprendre un peu plus sur les avantages d’une table innodb bien conçue et sur la meilleure façon d’utiliser les index clusterisés - uniquement disponibles avec innodb!
http://dev.mysql.com/doc/refman/5.0/en/innodb-index-types.html
http://www.xaprb.com/blog/2006/07/04/how-to-exploit-mysql-index-optimizations/
concevez ensuite votre système à l’aide de l’exemple simplifié suivant:
Les fonctionnalités importantes sont que les tables utilisent le moteur innodb et que la clé primaire de la table threads n'est plus une clé auto_incrementing mais une clé composite en cluster basée sur une combinaison de forum_id et thread_id. par exemple.
threads - primary key (forum_id, thread_id)
forum_id thread_id
======== =========
1 1
1 2
1 3
1 ...
1 2058300
2 1
2 2
2 3
2 ...
2 2352141
...
Chaque ligne de forum comprend un compteur appelé next_thread_id (unsigned int), géré par un déclencheur et incrémenté chaque fois qu'un fil est ajouté à un forum donné. Cela signifie également que nous pouvons stocker 4 milliards de threads par forum plutôt que 4 milliards de threads au total si nous utilisons une seule clé primaire auto_increment pour thread_id.
forum_id title next_thread_id
======== ===== ==============
1 forum 1 2058300
2 forum 2 2352141
3 forum 3 2482805
4 forum 4 3740957
...
64 forum 64 3243097
65 forum 65 15000000 -- ooh a big one
66 forum 66 5038900
67 forum 67 4449764
...
247 forum 247 0 -- still loading data for half the forums !
248 forum 248 0
249 forum 249 0
250 forum 250 0
L'inconvénient de l'utilisation d'une clé composite est que vous ne pouvez plus simplement sélectionner un thread par une valeur de clé unique comme suit:
select * from threads where thread_id = y;
tu dois faire:
select * from threads where forum_id = x and thread_id = y;
Cependant, le code de votre application doit savoir quel forum un utilisateur navigue afin que sa mise en œuvre ne soit pas vraiment difficile. Enregistrez le forum_id actuellement visualisé dans une variable de session ou un champ de formulaire masqué, etc.
Voici le schéma simplifié:
drop table if exists forums;
create table forums
(
forum_id smallint unsigned not null auto_increment primary key,
title varchar(255) unique not null,
next_thread_id int unsigned not null default 0 -- count of threads in each forum
)engine=innodb;
drop table if exists threads;
create table threads
(
forum_id smallint unsigned not null,
thread_id int unsigned not null default 0,
reply_count int unsigned not null default 0,
hash char(32) not null,
created_date datetime not null,
primary key (forum_id, thread_id, reply_count) -- composite clustered index
)engine=innodb;
delimiter #
create trigger threads_before_ins_trig before insert on threads
for each row
begin
declare v_id int unsigned default 0;
select next_thread_id + 1 into v_id from forums where forum_id = new.forum_id;
set new.thread_id = v_id;
update forums set next_thread_id = v_id where forum_id = new.forum_id;
end#
delimiter ;
Vous avez peut-être remarqué que j'ai inclus reply_count dans la clé primaire, ce qui est un peu étrange, car le composite (forum_id, thread_id) est unique en soi. Il s'agit simplement d'une optimisation d'index qui enregistre certaines E/S lorsque des requêtes utilisant reply_count sont exécutées. Veuillez vous référer aux 2 liens ci-dessus pour plus d'informations à ce sujet.
Je suis toujours en train de charger des données dans mes exemples de tables et jusqu'à présent, j'en ai chargé environ. 500 millions de lignes (deux fois moins que votre système). Lorsque le processus de chargement est terminé, je devrais m'attendre à avoir environ:
250 forums * 5 million threads = 1250 000 000 (1.2 billion rows)
J'ai délibérément fait en sorte que certains forums contiennent plus de 5 millions de threads, par exemple, le forum 65 en a 15 millions:
forum_id title next_thread_id
======== ===== ==============
65 forum 65 15000000 -- ooh a big one
select sum(next_thread_id) from forums;
sum(next_thread_id)
===================
539,155,433 (500 million threads so far and still growing...)
sous innodb, la somme des next_thread_ids pour obtenir le nombre total de threads est beaucoup plus rapide que d'habitude:
select count(*) from threads;
Combien de sujets a le forum 65:
select next_thread_id from forums where forum_id = 65
next_thread_id
==============
15,000,000 (15 million)
encore une fois c'est plus rapide que d'habitude:
select count(*) from threads where forum_id = 65
Ok, nous savons maintenant que nous avons environ 500 millions de threads jusqu'à présent et que le forum 65 en a 15 millions - voyons comment le schéma se comporte :)
select forum_id, thread_id from threads where forum_id = 65 and reply_count > 64 order by thread_id desc limit 32;
runtime = 0.022 secs
select forum_id, thread_id from threads where forum_id = 65 and reply_count > 1 order by thread_id desc limit 10000, 100;
runtime = 0.027 secs
Cela semble assez performant pour moi. Il s’agit donc d’une table unique comportant plus de 500 millions de lignes (et en croissance) avec une requête couvrant 15 millions de lignes en 0,02 seconde (sous charge!).
Ceux-ci comprennent:
partitionnement par plage
sharding
jeter de l'argent et du matériel sur elle
etc...
j'espère que vous trouverez cette réponse utile :)
EDIT: Vos index d'une colonne ne suffisent pas. Vous devez au moins couvrir les trois colonnes impliquées.
Solution plus avancée: remplacez replycount > 1
par hasreplies = 1
en créant un nouveau champ hasreplies
égal à 1 lorsque replycount > 1
. Une fois cela fait, créez un index sur les trois colonnes, dans cet ordre: INDEX(forumid, hasreplies, dateline)
. Assurez-vous que c'est un index BTREE pour prendre en charge les commandes.
Vous sélectionnez en fonction de:
forumid
donnéhasreplies
donnédateline
Une fois cette opération effectuée, l’exécution de votre requête impliquera:
forumid = X
. Ceci est une opération logarithmique (durée: log (nombre de forums)). hasreplies = 1
(tout en correspondant à forumid = X
). Il s'agit d'une opération à temps constant, car hasreplies
n'est que 0 ou 1. Ma suggestion précédente d'indexer sur replycount
était incorrecte, car elle aurait été une interrogation de plage et aurait donc empêché l'utilisation d'une dateline
pour trier les résultats (vous auriez donc sélectionné les threads avec des réponses très rapidement, mais la liste résultante aurait dû être trié complètement avant de chercher les 100 éléments dont vous aviez besoin).
IMPORTANT: bien que cela améliore les performances dans tous les cas, votre énorme valeur OFFSET (10000!) va diminuer, car MySQL ne semble pas pouvoir continuer à avancer malgré la lecture directe via BTREE. Ainsi, plus votre OFFSET est grand, plus la demande sera lente.
Je crains que le problème de OFFSET ne soit pas résolu automatiquement en étalant le calcul sur plusieurs calculs (comment ignorer un décalage en parallèle, de toute façon?) Ou en passant à NoSQL. Toutes les solutions (y compris celles de NoSQL) se résument à simuler OFFSET sur la base de dateline
(en gros, dire dateline > Y LIMIT 100
au lieu de LIMIT Z, 100
où Y
est la date de l'élément à offset Z
). Cela fonctionne et élimine tous les problèmes de performances liés au décalage, mais empêche d'aller directement à la page 100 sur 200.
Il y a une partie de la question qui a trait à l'option NoSQL ou MySQL. En fait, c’est une chose fondamentale cachée ici. Le langage SQL est facile à écrire pour les humains et un peu difficile à lire pour les ordinateurs. Dans les bases de données à volume élevé, je vous recommande d’éviter le traitement SQL car cela nécessite une analyse étape par étape supplémentaire. J'ai effectué une analyse comparative approfondie et il existe des cas où l'analyseur SQL est le point le plus lent. Vous ne pouvez rien y faire. Ok, vous pouvez éventuellement utiliser des instructions pré-analysées et y accéder.
BTW, ce n’est pas très connu, mais MySQL est né de la base de données NoSQL. La société dans laquelle les auteurs de MySQL David et Monty travaillaient était une société d’entreposage de données et ils devaient souvent écrire des solutions personnalisées pour des tâches inhabituelles. Cela a conduit à une grosse pile de bibliothèques C homebrew utilisées pour écrire manuellement des fonctions de base de données lorsque Oracle et d’autres fonctionnaient mal. SQL a été ajouté à ce zoo de près de 20 ans en 1996 pour le plaisir. Qu'est-ce qui est arrivé après que vous sachiez?.
En fait, vous pouvez éviter la surcharge de SQL avec MySQL. Mais généralement, l’analyse SQL n’est pas la partie la plus lente mais la bonne à savoir. Pour tester le temps d’analyse de l’analyseur, vous pouvez simplement effectuer un benchmark pour "SELECT 1", par exemple;).
Vous ne devez pas essayer d'adapter une architecture de base de données au matériel que vous envisagez d'acheter, mais plutôt d'acheter du matériel adapté à votre architecture de base de données.
Une fois que vous avez assez de RAM pour conserver le jeu d'index de travail en mémoire, toutes vos requêtes pouvant utiliser les index seront rapides. Assurez-vous que la mémoire tampon de votre clé est suffisamment grande pour contenir les index.
Donc, si 12 Go ne suffisent pas, n'utilisez pas 10 serveurs avec 12 Go de RAM, utilisez-en moins avec 32 ou 64 Go de RAM.
Les index sont indispensables - mais n'oubliez pas de choisir le bon type d'index: BTREE est plus approprié lorsque vous utilisez des requêtes avec "<" ou ">" dans vos clauses WHERE, tandis que HASH est plus approprié lorsque vous avez plusieurs valeurs distinctes dans une colonne et vous utilisez "=" ou "<=>" dans votre clause WHERE.
Lectures supplémentaires http://dev.mysql.com/doc/refman/5.0/en/mysql-indexes.html