conception de table / index mysql efficace pour 35 millions de lignes + table, avec plus de 200 colonnes correspondantes (double), dont toute combinaison peut être interrogée

17

Je recherche des conseils sur la conception de table / index pour la situation suivante:

J'ai une grande table (données d'historique des cours boursiers, InnoDB, 35 millions de lignes et en croissance) avec une clé primaire composée (assetid (int), date (date)). en plus des informations de prix, j'ai 200 valeurs doubles qui doivent correspondre à chaque enregistrement.

CREATE TABLE `mytable` (
`assetid` int(11) NOT NULL,
`date` date NOT NULL,
`close` double NOT NULL,
`f1` double DEFAULT NULL,   
`f2` double DEFAULT NULL,
`f3` double DEFAULT NULL,   
`f4` double DEFAULT NULL,
 ... skip a few 
`f200` double DEFAULT NULL, 
PRIMARY KEY (`assetid`, `date`)) ENGINE=`InnoDB` DEFAULT CHARACTER SET latin1 COLLATE
    latin1_swedish_ci ROW_FORMAT=COMPACT CHECKSUM=0 DELAY_KEY_WRITE=0 
    PARTITION BY RANGE COLUMNS(`date`) PARTITIONS 51;

J'ai initialement stocké les 200 doubles colonnes directement dans cette table pour faciliter la mise à jour et la récupération, et cela fonctionnait bien, car la seule requête effectuée sur cette table était par l'actif et la date (elles sont religieusement incluses dans toute requête contre cette table). ), et les 200 doubles colonnes ont seulement été lues. La taille de ma base de données était d'environ 45 Gig

Cependant, j'ai maintenant l'exigence où je dois pouvoir interroger cette table par n'importe quelle combinaison de ces 200 colonnes (nommées f1, f2, ... f200), par exemple:

select from mytable 
where assetid in (1,2,3,4,5,6,7,....)
and date > '2010-1-1' and date < '2013-4-5'
and f1 > -0.23 and f1 < 0.9
and f117 > 0.012 and f117 < .877
etc,etc

je n'ai jamais eu à traiter une telle quantité de données auparavant, donc mon premier réflexe était que des index étaient nécessaires sur chacune de ces 200 colonnes, ou je finirais avec de grandes analyses de table, etc. Pour moi, cela signifiait que j'avais besoin d'une table pour chacune des 200 colonnes avec la clé primaire, la valeur et l'indexation des valeurs. Je suis donc allé avec ça.

CREATE TABLE `f1` (
`assetid` int(11) NOT NULL DEFAULT '0',
`date` date NOT NULL DEFAULT '0000-00-00',
`value` double NOT NULL DEFAULT '0',
PRIMARY KEY (`assetid`, `date`),
INDEX `val` (`value`)
) ENGINE=`InnoDB` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci ROW_FORMAT=COMPACT CHECKSUM=0 DELAY_KEY_WRITE=0;

j'ai rempli et indexé les 200 tables. J'ai laissé le tableau principal intact avec les 200 colonnes, car il est régulièrement interrogé sur la plage d'actifs et de dates et les 200 colonnes sont sélectionnées. J'ai pensé que laisser ces colonnes dans la table parent (non indexées) à des fins de lecture, puis les avoir indexées dans leurs propres tables (pour le filtrage des jointures) serait plus performant. J'ai couru explique la nouvelle forme de la requête

select count(p.assetid) as total 
from mytable p 
inner join f1 f1 on f1.assetid = p.assetid and f1.date = p.date
inner join f2 f2 on f2.assetid = p.assetid and f2.date = p.date 
where p.assetid in(1,2,3,4,5,6,7)
and p.date >= '2011-01-01' and p.date < '2013-03-14' 
and(f1.value >= 0.96 and f1.value <= 0.97 and f2.value >= 0.96 and f2.value <= 0.97) 

En effet, mon résultat souhaité a été atteint, explique-moi montre que les lignes numérisées sont beaucoup plus petites pour cette requête. Cependant, je me suis retrouvé avec des effets secondaires indésirables.

1) ma base de données est passée de 45 Gig à 110 Gig. Je ne peux plus garder la base de données en RAM. (J'ai cependant 256 Go de RAM en route)

2) Les insertions nocturnes de nouvelles données doivent maintenant être effectuées 200 fois au lieu d'une fois

3) la maintenance / défragmentation des 200 nouvelles tables prend 200 fois plus de temps que la 1 seule table. Il ne peut pas être terminé en une nuit.

4) les requêtes contre les tables f1, etc. ne sont pas nécessairement performantes. par exemple:

 select min(value) from f1 
 where assetid in (1,2,3,4,5,6,7) 
 and date >= '2013-3-18' and date < '2013-3-19'

la requête ci-dessus, bien qu'expliquer montre qu'elle ressemble à <1000 lignes, peut prendre plus de 30 secondes. Je suppose que c'est parce que les index sont trop grands pour tenir en mémoire.

Comme c'était beaucoup de mauvaises nouvelles, j'ai regardé plus loin et j'ai trouvé le partitionnement. J'ai implémenté des partitions sur la table principale, partitionnées à date tous les 3 mois. Le mensuel semblait avoir du sens pour moi, mais j'ai lu qu'une fois que vous obtenez plus de 120 partitions, les performances en souffrent. le partitionnement trimestriel me laissera en dessous pour les 20 prochaines années. chaque partition est un peu moins de 2 Gig. J'ai couru expliquer les partitions et tout semble se tailler correctement, donc peu importe que je pense que le partitionnement était une bonne étape, à tout le moins à des fins d'analyse / d'optimisation / de réparation.

J'ai passé beaucoup de temps avec cet article

http://ftp.nchu.edu.tw/MySQL/tech-resources/articles/testing-partitions-large-db.html

ma table est actuellement partitionnée avec la clé primaire toujours dessus. L'article mentionne que les clés primaires peuvent ralentir une table partitionnée, mais si vous avez une machine qui peut la gérer, les clés primaires de la table partitionnée seront plus rapides. Sachant que j'ai une grosse machine en route (256 G de RAM), j'ai laissé les clés allumées.

donc comme je le vois, voici mes options

Option 1

1) supprimez les 200 tables supplémentaires et laissez la requête effectuer des analyses de table pour trouver les valeurs f1, f2, etc. les index non uniques peuvent en fait nuire aux performances sur une table correctement partitionnée. exécutez une explication avant que l'utilisateur n'exécute la requête et refusez-les si le nombre de lignes analysées dépasse un certain seuil que je définis. me sauver la douleur de la base de données géante. Heck, tout sera bientôt en mémoire de toute façon.

sous-question:

cela ressemble-t-il à avoir choisi un schéma de partition approprié?

Option 2

Partitionnez les 200 tables en utilisant le même schéma de 3 mois. profiter des analyses de lignes plus petites et permettre aux utilisateurs d'exécuter des requêtes plus volumineuses. maintenant qu'ils sont partitionnés au moins, je peux les gérer 1 partition à la fois à des fins de maintenance. Heck, tout sera bientôt en mémoire de toute façon. Développez un moyen efficace de les mettre à jour tous les soirs.

sous-question:

Voyez-vous une raison pour laquelle je peux éviter les index de clé primaire sur ces tables f1, f2, f3, f4 ..., sachant que j'ai toujours un identifiant et une date lors de la requête? me semble contre-intuitif mais je ne suis pas habitué aux ensembles de données de cette taille. cela réduirait la base de données un tas je suppose

Option 3

Supprimez les colonnes f1, f2, f3 dans la table principale pour récupérer cet espace. faire 200 jointures si j'ai besoin de lire 200 fonctionnalités, peut-être que ce ne sera pas aussi lent que cela puisse paraître.

Option 4

Vous avez tous une meilleure façon de structurer cela que je ne le pensais jusqu'à présent.

* REMARQUE: j'ajouterai bientôt 50 à 100 de ces valeurs doubles à chaque élément, je dois donc concevoir en sachant que cela arrive.

Merci pour toute aide

Mise à jour # 1 - 24/03/2013

Je suis allé avec l'idée suggérée dans les commentaires ci-dessous et j'ai créé un nouveau tableau avec la configuration suivante:

create table 'features'{
  assetid int,
  date    date,
  feature varchar(4),
  value   double
}

J'ai partitionné la table à 3 mois d'intervalle.

J'ai fait sauter les 200 tables précédentes afin que ma base de données soit redescendue à 45 Gig et j'ai commencé à remplir cette nouvelle table. Un jour et demi plus tard, il s'est terminé, et ma base de données se trouve maintenant à 220 concerts joufflus !

Cela permet de supprimer ces 200 valeurs de la table principale, car je peux les obtenir à partir d'une jointure, mais cela ne me redonnerait vraiment que 25 Gigs ou alors peut-être

Je lui ai demandé de créer une clé primaire sur l'actif, la date, la fonctionnalité et un index sur la valeur, et après 9 heures de calage, cela n'avait vraiment pas fait de bosses et semblait geler, alors j'ai tué cette partie.

J'ai reconstruit quelques partitions mais cela ne semble pas récupérer beaucoup / aucun espace.

Cette solution semble donc ne pas être idéale. Les lignes occupent-elles beaucoup plus d'espace que les colonnes, je me demande, cela pourrait-il être la raison pour laquelle cette solution a pris beaucoup plus d'espace?

Je suis tombé sur cet article:

http://www.chrismoos.com/2010/01/31/mysql-partitioning-tables-with-millions-of-rows

ça m'a donné une idée. Ça dit:

Au début, j'ai pensé au partitionnement RANGE par date, et pendant que j'utilise la date dans mes requêtes, il est très courant qu'une requête ait une très grande plage de dates, ce qui signifie qu'elle pourrait facilement s'étendre sur toutes les partitions.

Maintenant, je partitionne également la plage par date, mais je vais également autoriser les recherches par plage de dates étendue, ce qui diminuera l'efficacité de mon partitionnement. J'aurai toujours une plage de dates lorsque je recherche, mais j'aurai également toujours une liste d'actifs. Peut-être que ma solution devrait être de partitionner par identifiant d'actif et par date, où j'identifie les plages d'actifs généralement recherchées (que je peux trouver, il existe des listes standard, S&P 500, Russell 2000, etc.). De cette façon, je ne regarderais presque jamais l'ensemble des données.

Là encore, je suis principalement sur Assetid et date de toute façon, alors peut-être que cela n'aiderait pas beaucoup.

Toute autre réflexion / commentaire serait apprécié.

Dyeryn
la source
2
Je ne vois pas pourquoi vous avez besoin de 200 tables. Une seule table avec (value_name varchar(20), value double)serait en mesure de tout magasin ( value_nameêtre f1, f2...)
a_horse_with_no_name
Merci. la raison pour laquelle je les ai mis individuellement était d'obtenir la limite de 50 index sur une table. J'avais pensé à les mettre dans 5 tables, 40 valeurs chacune, mais j'insère environ 17000 enregistrements par jour pour chacune et je ne savais pas à quoi ressembleraient les performances d'une insertion sur une table avec 40 index. notez que chaque combinaison de assetid, date obtient ses propres valeurs f1, f2 .... Suggérez-vous une table unique avec (assetid, date, value_name, value), avec la clé primaire assetid, date, peut-être index sur (value_name, value)? cette table aurait 35 mil * 200 = 7 milliards de lignes mais peut-être bien partitionnée fonctionnerait-elle?
Dyeryn
article mis à jour avec mes expériences en essayant cette méthode
dyeryn
j'ai la solution finale en développement, je mettrai à jour quand j'aurai fini. il s'agit essentiellement de la solution à table unique proposée ici avec partitionnement spécifique et partitionnement logique.
dyeryn
Un moteur de stockage différent pourrait-il aider? Au lieu d'InnoDb, essayez peut-être InfiniDB? Les données en colonnes, les modèles d'accès ressemblent à une mise à jour par lots, à des lectures basées sur des plages et à une maintenance minimale des tables.
désordre

Réponses:

1

Par coïncidence, j'examine également l'un des supports client où nous avons conçu une structure de paires clé-valeur pour plus de flexibilité et où la table dépasse actuellement 1,5 milliard de lignes et ETL est beaucoup trop lent. Eh bien, il y a beaucoup d'autres choses dans mon cas, mais avez-vous pensé à cette conception. vous aurez une ligne avec la valeur actuelle des 200 colonnes, cette ligne se convertira en 200 lignes dans la conception de paire valeur / clé. vous obtiendrez un avantage d'espace avec cette conception en fonction d'un AssetID et d'une Date donnés, combien de lignes ont réellement toutes les valeurs 200 f1 à f200 présentes? si vous dites que même 30% des colonnes ont une valeur NULL, cela vous permet d'économiser de l'espace. car dans la conception de paire clé-valeur si la valeur id NULL, cette ligne n'a pas besoin d'être dans la table. mais dans la conception de structure de colonnes existante, même NULL prend de la place. (Je ne suis pas sûr à 100% mais si vous avez plus de 30 colonnes NULL dans le tableau, alors NULL prend 4 octets). si vous voyez cette conception et supposez que toutes les 35 millions de lignes ont des valeurs dans les 200 colonnes, votre base de données actuelle deviendra immédiatement 200 * 35 millions = 700 millions de lignes dans le tableau. mais il n'y aura pas beaucoup d'espace dans la table ce que vous aviez avec toutes les colonnes dans une seule table car nous transposons simplement les colonnes dans la ligne. dans cette opération de transposition, nous n'aurons pas de lignes où les valeurs sont NULL. afin que vous puissiez réellement exécuter une requête sur cette table et voir le nombre de valeurs NULL et estimer la taille de la table cible avant de l'implémenter. mais il n'y aura pas beaucoup d'espace dans la table ce que vous aviez avec toutes les colonnes dans une seule table car nous transposons simplement les colonnes dans la ligne. dans cette opération de transposition, nous n'aurons pas de lignes où les valeurs sont NULL. afin que vous puissiez réellement exécuter une requête sur cette table et voir le nombre de valeurs NULL et estimer la taille de la table cible avant de l'implémenter. mais il n'y aura pas beaucoup d'espace dans la table ce que vous aviez avec toutes les colonnes dans une seule table car nous transposons simplement les colonnes dans la ligne. dans cette opération de transposition, nous n'aurons pas de lignes où les valeurs sont NULL. afin que vous puissiez réellement exécuter une requête sur cette table et voir le nombre de valeurs NULL et estimer la taille de la table cible avant de l'implémenter.

le deuxième avantage est la performance de lecture. comme vous l'avez mentionné, la nouvelle façon d'interroger les données est toute combinaison de cette colonne f1 à f200 dans la clause where. avec la conception de paire de valeurs clés f1 à f200 sont présentes dans une colonne, disons "FildName" et leurs valeurs sont présentes dans la deuxième colonne, disons "FieldValue". vous pouvez avoir un index CLUSTERED sur les deux colonnes. votre requête sera UNION de ces Selects.

OERE (FiledName = 'f1' et FieldValue ENTRE 5 ET 6)

SYNDICAT

(FiledName = 'f2' et FieldValue ENTRE 8 ET 10)

etc.....

Je vais vous donner quelques chiffres de performance du serveur de prod réel. nous avons 75 colonnes de prix pour chaque TICKER de sécurité.

Anup Shah
la source
1

En traitant ce type de données où vous devez insérer beaucoup de lignes et vous avez également besoin de très bonnes performances de requête analytique (je fais l'hypothèse que c'est le cas ici), vous pourriez trouver qu'un SGBDR en colonne est un bon ajustement . Jetez un œil à Infobright CE et InfiniDB CE (les deux moteurs de stockage en colonnes connectés à MySQL), et à Vertica CE également (plus de type PostgreSQL au lieu de MySQL) ... toutes ces éditions de communauté sont gratuites (bien que Vertica ne soit pas open source, il évolue à 3 nœuds et 1 To de données gratuitement). Les SGBDR à colonnes offrent généralement des temps de réponse «grandes requêtes» qui sont 10 à 100 fois meilleurs que ceux basés sur des lignes, et des temps de chargement qui sont 5 à 50 fois meilleurs. Vous devez les utiliser correctement ou ils puent (ne faites pas d'opérations sur une seule ligne ... faites toutes les opérations dans une approche en vrac), mais utilisés correctement, ils basculent vraiment. ;-)

HTH, Dave Sisk

Dave Sisk
la source
1
Nous avons près d'un milliard de lignes de données de type clickstream (pas très différentes des données boursières) dans une installation Vertica à 3 nœuds ... nous pouvons charger une journée entière de données en environ 15 secondes, et nous obtenons des temps de réponse aux requêtes dans la plage de 500 millisecondes. Dans votre cas, il semble certainement que cela vaut le coup d'œil.
Dave Sisk
Je peux en garantir la même chose. Dans ma dernière entreprise, nous avions un cluster Vertica à 8 nœuds avec environ le même nombre de lignes et des requêtes d'agrégation simple sur l'ensemble complet renvoyées en 1 à 3 secondes (en moyenne). C'était également environ 1/4 du coût de notre cluster Greenplum précédent.
bma