web-dev-qa-db-fra.com

Meilleure conception de bases de données et de tables pour des milliards de lignes de données

J'écris une application qui a besoin de stocker et d'analyser de grandes quantités de données électriques et de température.

Fondamentalement, je dois stocker de grandes quantités de mesures horaires de consommation d'électricité au cours des dernières années et pendant de nombreuses années à venir pour des dizaines de milliers d'emplacements, puis analyser les données de manière peu complexe.

Les informations que je dois stocker (pour l'instant) sont l'ID de l'emplacement, l'horodatage (date et heure), la température et l'utilisation de l'électricité.

À propos de la quantité de données qui doit être stockée, ceci est une approximation, mais quelque chose dans ce sens:
Plus de 20 000 emplacements, 720 enregistrements par mois (mesures horaires, environ 720 heures par mois), 120 mois (pour 10 ans en arrière) et de nombreuses années à venir. Des calculs simples donnent les résultats suivants:

20 000 emplacements x 720 enregistrements x 120 mois (10 ans en arrière) = 1 728 000 000 enregistrements.

Ce sont les enregistrements précédents, de nouveaux enregistrements seront importés chaque mois, ce qui représente environ 20 000 x 720 = 14 400 000 nouveaux enregistrements par mois.

Le nombre total d'emplacements augmentera également régulièrement.

Sur toutes ces données, les opérations suivantes devront être exécutées:

  1. Récupérez les données pour une certaine date ET période: tous les enregistrements pour un certain ID de lieu entre les dates 01.01.2013 et 01.01.2017 et entre 07:00 et 13:00.
  2. Opérations mathématiques simples pour une certaine date ET plage horaire, par ex. MIN, MAX et AVG température et consommation d'électricité pour un certain ID de lieu pendant 5 ans entre 07h00 et 13h00.

Les données seront écrites mensuellement, mais seront lues par des centaines d'utilisateurs (au moins) en permanence, de sorte que la vitesse de lecture est beaucoup plus importante.

Je n'ai aucune expérience avec les bases de données NoSQL mais d'après ce que j'ai rassemblé, elles sont la meilleure solution à utiliser ici. J'ai lu sur les bases de données NoSQL les plus populaires, mais comme elles sont assez différentes et permettent également une architecture de table très différente, je n'ai pas pu décider quelle est la meilleure base de données à utiliser.

Mes principaux choix étaient Cassandra et MongoDB, mais comme je n'ai que des connaissances très limitées et aucune expérience réelle en ce qui concerne les données volumineuses et NoSQL, je ne suis pas très certain. J'ai également lu que PostreSQL gère également de telles quantités de données bien.

Mes questions sont les suivantes:

  1. Dois-je utiliser une base de données NoSQL pour de si grandes quantités de données. Sinon, puis-je m'en tenir à MySQL?
  2. Quelle base de données dois-je utiliser?
  3. Dois-je conserver la date et l'heure dans des colonnes séparées et indexées (si possible) pour récupérer et traiter rapidement les données pour certaines périodes de temps et de date, ou cela peut-il être fait en conservant l'horodatage dans une seule colonne?
  4. Une approche de modélisation des données de séries temporelles est-elle appropriée ici, et sinon, pourriez-vous me donner des conseils pour une bonne conception de tableau?

Je vous remercie.

85
Gecata

C'est exactement ce que je fais tous les jours, sauf qu'au lieu d'utiliser les données horaires, j'utilise les données de 5 minutes. Je télécharge environ 200 millions d'enregistrements tous les jours, donc le montant dont vous parlez ici n'est pas un problème. Les données de 5 minutes sont d'environ 2 TB de taille et j'ai des données météorologiques remontant à 50 ans à un niveau horaire par emplacement. Alors permettez-moi de répondre à vos questions en fonction de mon expérience:

  1. N'utilisez pas NoSQL pour cela. Les données sont très structurées et s'adaptent parfaitement à une base de données relationnelle.
  2. J'utilise personnellement SQL Server 2016 et je n'ai aucun problème à appliquer des calculs sur ce volume de données. Il était à l'origine sur une instance PostgreSQL lorsque j'ai commencé mon travail et il ne pouvait pas gérer le volume de données car il se trouvait sur une petite instance AWS.
  3. Je voudrais fortement recommander d'extraire la partie heure de la date et de la stocker séparément de la date elle-même. Croyez-moi, apprenez de mes erreurs!
  4. Je stocke la majorité des données par liste (DATE, TIME, DATAPOINT_ID, VALUE) mais ce n'est pas ainsi que les gens voudront interpréter les données. Soyez prêt pour certaines requêtes horribles contre les données et de grandes quantités de pivotement. N'ayez pas peur de créer un tableau dénormalisé pour les jeux de résultats qui sont tout simplement trop grands pour être calculés à la volée.

Conseil général: je stocke la plupart des données entre deux bases de données, la première est des données chronologiques directes et est normalisée. Ma deuxième base de données est très dénormalisée et contient des données pré-agrégées. Aussi rapide que soit mon système, je ne suis pas aveugle au fait que les utilisateurs ne veulent même pas attendre 30 secondes pour qu'un rapport se charge - même si je pense personnellement que 30 secondes pour crunch 2 TB = des données est extrêmement rapide.

Pour expliquer pourquoi je recommande de stocker l'heure séparément de la date, voici quelques raisons pour lesquelles je le fais de cette façon:

  1. La façon dont les données électriques sont présentées est de Fin de l'heure - par conséquent, 01:00 est en fait la moyenne de la puissance électrique pour l'heure précédente et 00:00 est la Fin de l'heure 24. (Ceci est important car vous devez en fait rechercher deux dates pour inclure la valeur de 24 heures - le jour que vous recherchez plus la première marque du jour suivant.) Cependant, les données météorologiques sont en fait présentées de manière prospective (réelles et prévues pour heure suivante). D'après mon expérience avec ces données, les consommateurs souhaitent analyser l'effet de la météo sur le prix/la demande d'électricité. Si vous deviez utiliser une comparaison de date directe, vous compareriez en fait le prix moyen de l'heure précédente à la température moyenne de l'heure suivante, même si les horodatages sont les mêmes. Le stockage de l'heure distincte de la date vous permet d'appliquer des transformations à l'heure avec moins d'impact sur les performances que vous ne le verriez en appliquant un calcul à une colonne DATETIME.
  2. Performance. Je dirais qu'au moins 90% des rapports que je génère sont des graphiques, représentant normalement le prix en fonction de l'heure, soit pour une seule date, soit pour une plage de dates. Le fait de devoir séparer l'heure de la date peut ralentir la vitesse de la requête utilisée pour générer le rapport en fonction de la plage de dates que vous souhaitez voir. Il n'est pas rare que les consommateurs souhaitent voir une seule date, en glissement annuel, pour les 30 dernières années (en fait, pour les conditions météorologiques, cela est nécessaire pour générer les normales de 30 ans) - cela peut être lent. Bien sûr, vous pouvez optimiser votre requête et ajouter des index, et croyez-moi, j'ai quelques index insensés que je préfère ne pas avoir, mais cela rend le système rapide.
  3. Productivité. Je déteste devoir écrire le même morceau de code plus d'une fois. J'avais l'habitude de stocker la date et l'heure dans la même colonne, jusqu'à ce que je devais écrire la même requête encore et encore pour extraire la partie heure. Au bout d'un moment, je suis simplement tombé malade d'avoir à le faire et je l'ai extrait dans sa propre colonne. Moins vous avez de code à écrire, moins il y a de risque d'erreur. En outre, avoir à écrire moins de code signifie que vous pouvez publier vos rapports plus rapidement, personne ne veut attendre toute la journée pour les rapports.
  4. Les utilisateurs finaux. Tous les utilisateurs finaux ne sont pas des utilisateurs avancés (c'est-à-dire qu'ils savent écrire du SQL). Le fait d'avoir les données déjà stockées dans un format qu'elles peuvent importer dans Excel (ou tout autre outil similaire) avec un minimum d'effort fera de vous un héros au bureau. Si les utilisateurs ne peuvent pas accéder aux données ou les manipuler facilement, ils n'utiliseront pas votre système. Croyez-moi, j'ai conçu le système parfait il y a quelques années et personne ne l'a utilisé pour cette raison. La conception d'une base de données ne consiste pas seulement à respecter un ensemble de règles/directives prédéfinies, il s'agit de rendre le système utilisable.

Comme je l'ai dit ci-dessus, tout cela est basé sur mon expérience personnelle, et laissez-moi vous dire que cela a été quelques années difficiles et beaucoup de remaniements pour arriver là où je suis maintenant. Ne faites pas ce que j'ai fait, apprenez de mes erreurs et assurez-vous d'impliquer les utilisateurs finaux de votre système (ou les développeurs, les auteurs de rapports, etc.) lors de la prise de décisions concernant votre base de données.

102
Mr.Brownstone

Index PostgreSQL et BRIN

Testez-le par vous-même. Ce n'est pas un problème sur un ordinateur portable de 5 ans avec un SSD.

EXPLAIN ANALYZE
CREATE TABLE electrothingy
AS
  SELECT
    x::int AS id,
    (x::int % 20000)::int AS locid,  -- fake location ids in the range of 1-20000
    now() AS tsin,                   -- static timestmap
    97.5::numeric(5,2) AS temp,      -- static temp
    x::int AS usage                  -- usage the same as id not sure what we want here.
  FROM generate_series(1,1728000000) -- for 1.7 billion rows
    AS gs(x);

                                                               QUERY PLAN                                                               
----------------------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series gs  (cost=0.00..15.00 rows=1000 width=4) (actual time=173119.796..750391.668 rows=1728000000 loops=1)
 Planning time: 0.099 ms
 Execution time: 1343954.446 ms
(3 rows)

Il a donc fallu 22 minutes pour créer la table. En grande partie, parce que la table est un modeste 97 Go. Ensuite, nous créons les index,

CREATE INDEX ON electrothingy USING brin (tsin);
CREATE INDEX ON electrothingy USING brin (id);    
VACUUM ANALYZE electrothingy;

La création des index a également pris beaucoup de temps. Mais parce qu'ils sont BRIN, ils ne font que 2-3 Mo et ils se stockent facilement dans la mémoire RAM. La lecture de 96 Go n'est pas instantanée, mais ce n'est pas un vrai problème pour mon ordinateur portable à votre charge de travail.

Maintenant, nous l'interrogons.

explain analyze
SELECT max(temp)
FROM electrothingy
WHERE id BETWEEN 1000000 AND 1001000;
                                                                 QUERY PLAN                                                                  
---------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=5245.22..5245.23 rows=1 width=7) (actual time=42.317..42.317 rows=1 loops=1)
   ->  Bitmap Heap Scan on electrothingy  (cost=1282.17..5242.73 rows=993 width=7) (actual time=40.619..42.158 rows=1001 loops=1)
         Recheck Cond: ((id >= 1000000) AND (id <= 1001000))
         Rows Removed by Index Recheck: 16407
         Heap Blocks: lossy=128
         ->  Bitmap Index Scan on electrothingy_id_idx  (cost=0.00..1281.93 rows=993 width=0) (actual time=39.769..39.769 rows=1280 loops=1)
               Index Cond: ((id >= 1000000) AND (id <= 1001000))
 Planning time: 0.238 ms
 Execution time: 42.373 ms
(9 rows)

Mise à jour avec horodatages

Ici, nous générons une table avec différents horodatages afin de satisfaire la demande d'indexation et de recherche sur une colonne d'horodatage, la création prend un peu plus de temps car to_timestamp(int) est sensiblement plus lent que now() (qui est mis en cache pour la transaction)

EXPLAIN ANALYZE
CREATE TABLE electrothingy
AS
  SELECT
    x::int AS id,
    (x::int % 20000)::int AS locid,
    -- here we use to_timestamp rather than now(), we
    -- this calculates seconds since Epoch using the gs(x) as the offset
    to_timestamp(x::int) AS tsin,
    97.5::numeric(5,2) AS temp,
    x::int AS usage
  FROM generate_series(1,1728000000)
    AS gs(x);

                                                               QUERY PLAN                                                                
-----------------------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series gs  (cost=0.00..17.50 rows=1000 width=4) (actual time=176163.107..5891430.759 rows=1728000000 loops=1)
 Planning time: 0.607 ms
 Execution time: 7147449.908 ms
(3 rows)

Maintenant, nous pouvons exécuter une requête sur une valeur d'horodatage à la place,

explain analyze
SELECT count(*), min(temp), max(temp)
FROM electrothingy WHERE tsin BETWEEN '1974-01-01' AND '1974-01-02';
                                                                        QUERY PLAN                                                                         
-----------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=296073.83..296073.84 rows=1 width=7) (actual time=83.243..83.243 rows=1 loops=1)
   ->  Bitmap Heap Scan on electrothingy  (cost=2460.86..295490.76 rows=77743 width=7) (actual time=41.466..59.442 rows=86401 loops=1)
         Recheck Cond: ((tsin >= '1974-01-01 00:00:00-06'::timestamp with time zone) AND (tsin <= '1974-01-02 00:00:00-06'::timestamp with time zone))
         Rows Removed by Index Recheck: 18047
         Heap Blocks: lossy=768
         ->  Bitmap Index Scan on electrothingy_tsin_idx  (cost=0.00..2441.43 rows=77743 width=0) (actual time=40.217..40.217 rows=7680 loops=1)
               Index Cond: ((tsin >= '1974-01-01 00:00:00-06'::timestamp with time zone) AND (tsin <= '1974-01-02 00:00:00-06'::timestamp with time zone))
 Planning time: 0.140 ms
 Execution time: 83.321 ms
(9 rows)

Résultat:

 count |  min  |  max  
-------+-------+-------
 86401 | 97.50 | 97.50
(1 row)

Ainsi, en 83,321 ms, nous pouvons agréger 86 401 enregistrements dans un tableau avec 1,7 milliard de lignes. Cela devrait être raisonnable.

Fin de l'heure

Le calcul de la fin de l'heure est également assez facile, tronquez les horodatages et ajoutez simplement une heure.

SELECT date_trunc('hour', tsin) + '1 hour' AS tsin,
  count(*),
  min(temp),
  max(temp)
FROM electrothingy
WHERE tsin >= '1974-01-01'
  AND tsin < '1974-01-02'
GROUP BY date_trunc('hour', tsin)
ORDER BY 1;
          tsin          | count |  min  |  max  
------------------------+-------+-------+-------
 1974-01-01 01:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 02:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 03:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 04:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 05:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 06:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 07:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 08:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 09:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 10:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 11:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 12:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 13:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 14:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 15:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 16:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 17:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 18:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 19:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 20:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 21:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 22:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-01 23:00:00-06 |  3600 | 97.50 | 97.50
 1974-01-02 00:00:00-06 |  3600 | 97.50 | 97.50
(24 rows)

Time: 116.695 ms

Il est important de noter qu'il n'utilise pas d'index sur l'agrégation, bien qu'il le puisse. Si c'est généralement votre requête, vous voulez probablement un BRIN sur date_trunc('hour', tsin), c'est là que réside un petit problème en ce que date_trunc N'est pas immuable, vous devez donc d'abord le boucler pour le faire.

Partitionnement

Un autre point d'information important sur PostgreSQL est que PG 10 apporte partitionnement DDL . Ainsi, vous pouvez, par exemple, créer facilement des partitions pour chaque année. Décomposer votre modeste base de données en petites qui sont minuscules. Ce faisant, vous devriez pouvoir utiliser et maintenir les index btree plutôt que BRIN, ce qui serait encore plus rapide.

CREATE TABLE electrothingy_y2016 PARTITION OF electrothingy
    FOR VALUES FROM ('2016-01-01') TO ('2017-01-01');

Ou peu importe.

63
Evan Carroll

Cela m'étonne que personne ici n'ait mentionné l'analyse comparative - c'est-à-dire jusqu'à @ EvanCarroll est venu avec son excellente contribution!

Si j'étais vous, je passerais un certain temps (et oui, je sais que c'est une denrée précieuse!) À configurer des systèmes, à exécuter ce que vous pensez être (obtenir la contribution de l'utilisateur final ici!), Disons, vos 10 requêtes les plus courantes.

Mes propres pensées:

Les solutions NoSQL peuvent très bien fonctionner pour des cas d'utilisation particuliers mais sont souvent inflexibles pour les requêtes ad hoc. Pour une version amusante de NoSQL par Brian Aker - ancien architecte en chef de MySQL, voir ici !

Je suis d'accord avec @ Mr.Brownstone que vos données sont éminemment adaptées à une solution relationnelle (et cette opinion a été confirmée par Evan Carroll )!

Si je devais engager des dépenses, ce serait pour ma technologie de disque! Je dépenserais tout l'argent dont je disposais pour NAS ou SAN ou peut-être quelques disques SSD pour contenir mes données agrégées rarement écrites!)

D'abord Je regarderais ce que j'ai à disposition maintenant . Exécutez des tests et montrez les résultats aux décideurs. Vous avez déjà un proxy sous la forme de travail d'EC ! Mais, un test rapide ou deux fouettés ensemble sur votre propre matériel serait plus convaincant!

Ensuite pensez à dépenser de l'argent! Si vous allez dépenser de l'argent, regardez d'abord le matériel plutôt que le logiciel. AFAIK, vous pouvez louer la technologie du disque pour une période d'essai, ou mieux encore, faire tourner quelques preuves de concept sur le cloud.

Mon premier port d'escale personnel pour un projet comme celui-ci serait PostgreSQL. Cela ne veut pas dire que j'exclurais une solution propriétaire, mais les lois de la physique et des disques sont les mêmes pour tout le monde! "Yae cannae betterave les lois de la physique Jim" :-)

14
Vérace

Si vous ne l'avez pas déjà fait, jetez un œil à un SGBD de séries chronologiques, car il est optimisé pour le stockage et l'interrogation de données où le focus principal est le type date/heure. En règle générale, les bases de données de séries chronologiques sont utilisées pour enregistrer des données dans les plages de minutes/secondes/sous-secondes, donc je ne sais pas si elles sont toujours appropriées pour les incréments horaires. Cela dit, ce type de SGBD semble mériter d'être étudié. Actuellement, InfluxDB semble être la base de données chronologiques la plus établie et la plus utilisée.

6
FloorDivision

De toute évidence, ce n'est pas un problème NoSQL, mais je dirais que même si une solution SGBDR fonctionnerait, je pense qu'une approche OLAP conviendrait beaucoup mieux et étant donné les plages de données très limitées impliquées, je serais fortement suggérez d'étudier l'utilisation d'une base de données basée sur des colonnes plutôt que sur une base basée sur des lignes. Pensez-y de cette façon, vous pouvez avoir 1,7 milliard de données, mais vous n'avez besoin que de 5 bits pour indexer chaque valeur possible d'heure ou de jour du mois.

J'ai de l'expérience avec un domaine de problème similaire où Sybase IQ (maintenant SAP IQ) est utilisé pour stocker jusqu'à 300 millions de compteurs par heure de données de gestion des performances des équipements de télécommunications, mais je doute que vous ayez le budget pour ce type de solution. Dans l'arène open source, MariaDB ColumnStore est un candidat très prometteur, mais je recommanderais également d'enquêter sur MonetDB.

Étant donné que les performances des requêtes sont un moteur majeur pour vous, réfléchissez à la façon dont les requêtes seront formulées. C'est là que OLAP et RDBMS montrent leurs plus grandes différences: - avec OLAP vous normalisez pour les performances des requêtes, pas pour réduire la répétition, réduire le stockage ou même pour renforcer la cohérence) Donc, en plus de l'horodatage d'origine (vous vous souveniez de capturer son fuseau horaire, j'espère?), Vous disposez d'un champ distinct pour l'horodatage UTC, d'autres pour la date et l'heure, et plus encore pour l'année, le mois, le jour, l'heure, minute et décalage UTC. Si vous avez des informations supplémentaires sur les emplacements, n'hésitez pas à les conserver dans une table d'emplacement distincte qui peut être consultée à la demande et n'hésitez pas à conserver la clé de cette table dans votre enregistrement principal, mais conservez le nom complet de l'emplacement dans votre table principale, après tout, tous les emplacements possibles ne prennent encore que 10 bits à indexer et chaque référence que vous n'avez pas à suivre pour obtenir les données à signaler est un gain de temps sur votre requête.

En guise de suggestion finale, utilisez des tableaux séparés pour les données agrégées populaires et utilisez des travaux par lots pour les remplir, de cette façon vous n'avez pas à répéter l'exercice pour chaque rapport qui utilise une valeur agrégée et effectue des requêtes qui comparent l'actuel à l'historique ou historique à historique beaucoup plus facile et beaucoup plus rapide.

4
Paul Smith