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:
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:
Je vous remercie.
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:
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:
DATETIME
.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.
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)
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.
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.
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.
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" :-)
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.
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.