大量の電気および温度データを保存および分析する必要があるアプリケーションを作成しています。
基本的に私は過去数年、そして何年もの間何万もの場所で大量の時間ごとの電気使用量測定値を保存し、それほど複雑ではない方法でデータを分析する必要があります。
(今のところ)保存する必要のある情報は、ロケーションID、タイムスタンプ(日付と時刻)、温度、電気使用量です。
保存する必要があるデータの量については、これは概算ですが、これらの線に沿ったものです:
20 000以上の場所、1か月あたり720レコード(毎時測定、約720時間/月)、120か月(10年前)、および何年も先。単純な計算では、次の結果が得られます。
20 000の場所x 720レコードx 120か月(10年前)= 1 728 000 000レコード。
これらは過去のレコードであり、新しいレコードは毎月インポートされるため、約20 000 x 720 = 1か月あたり14 400 000の新しいレコードになります。
総店舗数も順調に拡大していきます。
そのすべてのデータで、次の操作を実行する必要があります。
データは毎月書き込まれますが、何百人ものユーザーによって(少なくとも)常に読み取られるため、読み取り速度は非常に重要です。
私はNoSQLデータベースの経験はありませんが、私が集めたものから、それらはここで使用するのに最適なソリューションです。私は最も人気のあるNoSQLデータベースを読みましたが、それらはまったく異なっており、非常に異なるテーブルアーキテクチャにも対応しているため、使用するのに最適なデータベースを決定できませんでした。
私の主な選択はCassandraとMongoDBでしたが、私は非常に限られた知識しかなく、大きなデータとNoSQLに関して実際の経験がないので、私はあまり確信がありません。PostreSQLも処理することを読んだそのような量のデータも。
私の質問は次のとおりです。
ありがとうございました。
これはまさに私が毎日行うことですが、毎時のデータを使用する代わりに、5分のデータを使用します。私は毎日約2億件のレコードをダウンロードするので、ここで話す量は問題ではありません。 5分のデータは約2 TB=サイズであり、気象データは場所ごとに1時間ごとのレベルで50年前に遡ります。したがって、私の経験に基づいて質問にお答えしましょう。
一般的なヒント:ほとんどのデータを2つのデータベース間で保存します。最初のデータはまっすぐな時系列データであり、正規化されています。 2番目のデータベースは非常に非正規化されており、事前に集計されたデータが含まれています。私のシステムと同じくらい高速ですが、ユーザーが30秒待ってレポートが読み込まれるのを望まないという事実に目をつぶっていません。たとえ個人的に30秒は処理するのが難しいと思っていても2 TB =データは非常に高速です。
時間を日付とは別に保存することをお勧めする理由を詳しく説明するために、そのように保存する理由をいくつか次に示します。
DATETIME
列に計算を適用するよりもパフォーマンスへの影響が少ない時間に変換を適用できます。上で述べたように、これはすべて私の個人的な経験に基づいています。そして、お伝えしておきますが、私が今いる場所にたどり着くまでには、数年がかかり、多くの再設計が必要でした。データベースに関する決定を行うときは、私がしたことを行わないでください。自分の過ちから学び、システムのエンドユーザー(または開発者、レポート作成者など)が関与するようにしてください。
自分でテストしてください。これは、ssdを搭載した5年前のラップトップでは問題ではありません。
_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)
_
したがって、テーブルの作成には22分かかりました。主に、テーブルが控えめな97GBであるためです。次に、インデックスを作成し、
_CREATE INDEX ON electrothingy USING brin (tsin);
CREATE INDEX ON electrothingy USING brin (id);
VACUUM ANALYZE electrothingy;
_
インデックスの作成にも時間がかかりました。 BRINなので2〜3 MBしかなく、RAMに簡単に格納できます。 96 GBを読み取るのは瞬時ではありませんが、私のラップトップでは、ワークロードでそれが実際の問題ではありません。
次にクエリを実行します。
_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)
_
ここでは、タイムスタンプ列のインデックス付けと検索の要求を満足させるために、さまざまなタイムスタンプでテーブルを生成します。to_timestamp(int)
はnow()
(トランザクション用にキャッシュされます)
_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)
_
代わりに、タイムスタンプ値に対してクエリを実行できます。
_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)
_
結果:
_ count | min | max
-------+-------+-------
86401 | 97.50 | 97.50
(1 row)
_
したがって、83.321ミリ秒で、17億行のテーブルに86,401レコードを集計できます。それは妥当なはずです。
時間の終わりの計算も非常に簡単で、タイムスタンプを切り捨ててから1時間を追加するだけです。
_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
_
集計ではインデックスを使用していませんが、使用していることに注意してください。それが通常のクエリである場合、date_trunc('hour', tsin)
のBRINがおそらく必要ですが、_date_trunc
_は不変ではないため、最初にラップしてそうする必要があるという小さな問題があります。
PostgreSQLに関するもう1つの重要な情報は、PG 10が partitioning DDL をもたらすことです。したがって、たとえば、毎年のパーティションを簡単に作成できます。ささやかなデータベースを小さな小さなデータベースに分解します。そうすることで、さらに高速になるBRINではなく、btreeインデックスを使用および維持できるようになります。
_CREATE TABLE electrothingy_y2016 PARTITION OF electrothingy
FOR VALUES FROM ('2016-01-01') TO ('2017-01-01');
_
または何でも。
ここで誰もベンチマークについて言及していないことに私は驚かされます @ EvanCarrollが彼の優れた貢献とともにやって来たまでです!
私があなただったら、少し時間をかけて(そして、はい、それは貴重な商品であることを知っています!)、システムをセットアップし、予想どおりに実行します(エンドユーザーの入力をここに!)。たとえば、最も一般的な10のクエリです。
私自身の考え:
NoSQLソリューションは特定のユースケースで非常にうまく機能しますが、アドホッククエリには柔軟性がありません。 MySQLの元チーフアーキテクトであるBrian AkerによるNoSQLの面白い見方については、 ここ を参照してください。
私はあなたのデータがリレーショナルソリューションに非常に適していることを@ Mr.Brownstoneに同意します(この意見は Evan Carrollによって確認されました )!
もし私が何らかの出費を約束したとしたら、それは私のディスク技術に対するものでしょう! NASまたはSANまたは、ほとんど書き込まれていない集約データを保持するためにSSDディスクを使用する可能性があります。
最初私は私が利用できるものを調べます今。いくつかのテストを実行し、結果を意思決定者に示します。 ECの作業 という形式のプロキシがすでにあります!しかし、あなた自身のハードウェアで一緒にホイップされた簡単なテストか2つはより説得力があります!
それからお金を使うことを考えてください!お金を使うつもりなら、ソフトウェアではなくハードウェアを最初に見てください。私の知る限り、試用期間中はディスクテクノロジーを採用することができます。あるいは、クラウド上で概念実証をいくつか開始することもできます。
このようなプロジェクトの私の個人的な最初の呼び出しポートはPostgreSQLです。それは私が独自の解決策を除外すると言っているわけではありませんが、物理学とディスクの法則は誰にとっても同じです! 「ヤエカンネビート物理法則ジム」:-)
まだ行っていない場合は、時系列DBMSを見てください。DBMSは、日付/時刻型を主な対象とするデータの保存とクエリ用に最適化されています。通常、時系列データベースは分/秒/秒以下の範囲でデータを記録するために使用されるため、1時間ごとの増分にまだ適切かどうかはわかりません。そうは言っても、このタイプのDBMSは検討する価値があるようです。現在InfluxDBは、最も確立され、広く使用されている時系列データベースのようです。
明らかにこれはNoSQLの問題ではありませんが、RDBMSソリューションは機能しますが、OLAPアプローチはより適切に適合し、関係するデータ範囲が非常に限られていることを考えると、行ベースではなく列ベースのDBの使用を調査することをお勧めします。このように考えると、17億個のデータがあるかもしれませんが、時間または月のすべての可能な値にインデックスを付けるのに5ビットしか必要ありません。
私は、Sybase IQ(現在のSAP IQ)を使用して1時間に最大3億のカウンターを1時間の通信機器パフォーマンス管理データを保存するという同様の問題の領域での経験がありますが、そのようなソリューションの予算があるかどうかは疑問です。オープンソースの分野では、MariaDB ColumnStoreは非常に有望な候補ですが、MonetDBを調査することもお勧めします。
クエリのパフォーマンスはあなたにとって主要な推進力なので、クエリがどのように表現されるかを考慮してください。これがOLAPとRDBMSが最大の違いを示す場所です。-OLAPを使用すると、繰り返しを減らしたり、ストレージを減らしたり、一貫性を強制したりせずに、クエリのパフォーマンスを正規化します。 。したがって、元のタイムスタンプに加えて(希望するタイムゾーンをキャプチャしたことを覚えていますか?)、UTCタイムスタンプ用の別のフィールド、日付と時刻用の別のフィールド、さらに年、月、日、時間のフィールドがあります。分とUTCオフセット。場所に関する追加情報がある場合は、必要に応じて検索できる別の場所テーブルに自由に保持し、メインレコードにそのテーブルのキーを保持しても、完全な場所名は保持してください。メインテーブルでも、結局のところ、可能性のあるすべての場所でインデックスを作成するのに10ビットしか必要ありません。レポートされるデータを取得するために従う必要のないすべての参照は、クエリの時間を節約できます。
最後の提案として、人気のある集計データには個別のテーブルを使用し、バッチジョブを使用してデータを入力します。これにより、集計値を使用し、現在または過去のデータと比較するクエリを作成するすべてのレポートに対して演習を繰り返す必要がなくなります。歴史的なものから歴史的なものまで、はるかに簡単ではるかに速くなります。