web-dev-qa-db-fra.com

MySQL et NoSQL: aidez-moi à choisir le bon

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 | 
+----+-------------+--------+------+---------------+---------+---------+-------------+--------+-----------------------------+
39
cedivad

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:

Exemple de schéma (simplifié)

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.

Exemple de requêtes

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

Durée d'exécution des requêtes

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!).

Optimisations ultérieures

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 :)

77
Jon Black

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é
  • ordonné par dateline

Une fois cette opération effectuée, l’exécution de votre requête impliquera: 

  • en descendant dans BTREE pour trouver le sous-arbre qui correspond à forumid = X. Ceci est une opération logarithmique (durée: log (nombre de forums)). 
  • déplacez-vous plus bas dans BTREE pour trouver la sous-arborescence qui correspond à hasreplies = 1 (tout en correspondant à forumid = X). Il s'agit d'une opération à temps constant, car hasreplies n'est que 0 ou 1. 
  • passer par la sous-arborescence triée par date de référence afin d'obtenir les résultats requis, sans avoir à lire et à trier de nouveau la liste complète des éléments du forum.

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, 100Y 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.

24
Victor Nicollet

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;).

3
Tõnu Samuel

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.

2
Dan Grossman

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

0
descent89