web-dev-qa-db-ja.com

数十億行のデータに最適なデータベースとテーブルの設計

大量の電気および温度データを保存および分析する必要があるアプリケーションを作成しています。

基本的に私は過去数年、そして何年もの間何万もの場所で大量の時間ごとの電気使用量測定値を保存し、それほど複雑ではない方法でデータを分析する必要があります。

(今のところ)保存する必要のある情報は、ロケーション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の新しいレコードになります。

総店舗数も順調に拡大していきます。

そのすべてのデータで、次の操作を実行する必要があります。

  1. 特定の日付と期間のデータを取得します。2013年1月1日から2017年1月1日の間、および07:00から13:00の間の特定のロケーションIDのすべてのレコード。
  2. 特定の日付と時間範囲の単純な数学演算。 MIN、MAXおよびAVG 07:00から13:00までの5年間の特定のロケーションIDの温度および電気使用量。

データは毎月書き込まれますが、何百人ものユーザーによって(少なくとも)常に読み取られるため、読み取り速度は非常に重要です。

私はNoSQLデータベースの経験はありませんが、私が集めたものから、それらはここで使用するのに最適なソリューションです。私は最も人気のあるNoSQLデータベースを読みましたが、それらはまったく異なっており、非常に異なるテーブルアーキテクチャにも対応しているため、使用するのに最適なデータベースを決定できませんでした。

私の主な選択はCassandraとMongoDBでしたが、私は非常に限られた知識しかなく、大きなデータとNoSQLに関して実際の経験がないので、私はあまり確信がありません。PostreSQLも処理することを読んだそのような量のデータも。

私の質問は次のとおりです。

  1. このような大量のデータにはNoSQLデータベースを使用する必要がありますか。そうでない場合、MySQLを使用できますか?
  2. どのデータベースを使用すればよいですか?
  3. 特定の日時のデータをすばやく取得して処理するために、日付と時刻を別々のインデックス付けされた(可能な場合)列に保持する必要がありますか?それとも、タイムスタンプを1つの列に保持することで実現できますか?
  4. ここで時系列データモデリングのアプローチは適切ですか。適切でない場合は、適切なテーブル設計の指針を教えていただけませんか?

ありがとうございました。

85
Gecata

これはまさに私が毎日行うことですが、毎時のデータを使用する代わりに、5分のデータを使用します。私は毎日約2億件のレコードをダウンロードするので、ここで話す量は問題ではありません。 5分のデータは約2 TB=サイズであり、気象データは場所ごとに1時間ごとのレベルで50年前に遡ります。したがって、私の経験に基づいて質問にお答えしましょう。

  1. これにはNoSQLを使用しないでください。データは高度に構造化されており、リレーショナルデータベースに完全に適合します。
  2. 私はSQL Server 2016を個人的に使用しており、そのデータ量にわたって計算を適用することに問題はありません。私が仕事を始めたとき、それはもともとPostgreSQLインスタンス上にあり、小さなAWSインスタンスのようにデータのボリュームを処理することができませんでした。
  3. 非常に日付の時間部分を抽出し、日付自体とは別に保存することをお勧めします。私を信じて、私の過ちから学びなさい!
  4. 大部分のデータをリスト形式(DATE、TIME、DATAPOINT_ID、VALUE)で格納しますが、それは人々がデータを解釈する方法ではありません。データと膨大な量のピボットに対するいくつかの恐ろしいクエリに備えてください。オンザフライで計算するには大きすぎる結果セットの非正規化テーブルを作成することを恐れないでください。

一般的なヒント:ほとんどのデータを2つのデータベース間で保存します。最初のデータはまっすぐな時系列データであり、正規化されています。 2番目のデータベースは非常に非正規化されており、事前に集計されたデータが含まれています。私のシステムと同じくらい高速ですが、ユーザーが30秒待ってレポートが読み込まれるのを望まないという事実に目をつぶっていません。たとえ個人的に30秒は処理するのが難しいと思っていても2 TB =データは非常に高速です。

時間を日付とは別に保存することをお勧めする理由を詳しく説明するために、そのように保存する理由をいくつか次に示します。

  1. 電気データの表示方法は時間終了 –なので、01:00は実際には直前の1時間の電力の平均であり、00:00は時間終了24です(これは重要です)実際には、24時間の値を含む2つの日付を検索する必要があるためです。つまり、探している日と翌日の最初のマークです。ただし、気象データは実際には前向きに表示されます(実際の予測)。次の時間)。このデータに関する私の経験では、消費者は天候が電力価格/需要に与える影響を分析したいと考えています。日付をそのまま比較すると、タイムスタンプが同じであっても、実際には前の1時間の平均価格と次の1時間の平均気温を比較することになります。日付とは別に時間を格納すると、DATETIME列に計算を適用するよりもパフォーマンスへの影響が少ない時間に変換を適用できます。
  2. パフォーマンス。私が生成するレポートの少なくとも90%はグラフであり、通常は単一の日付または日付の範囲のいずれかに対して時間に対する価格をプロットします。日付から時間を分割する必要があると、表示する日付範囲に応じて、レポートの生成に使用されるクエリの速度が低下する可能性があります。消費者が過去30年間の前年比で単一の日付を見たいと思うことは珍しくありません(実際には、天候の場合、これは30年の法線を生成するために必要です)。これは遅くなる可能性があります。もちろん、クエリを最適化してインデックスを追加することもできますが、私が必要としない非常識なインデックスがいくつかありますが、システムが高速に動作します。
  3. 生産性。同じコードを何度も書く必要がありません。以前は同じ列に日付と時刻を格納していましたが、時間の部分を抽出するために同じクエリを何度も記述する必要がありました。しばらくして、私はこれを行う必要があることにうんざりして、それを独自の列に抽出しました。記述するコードが少ないほど、エラーが発生する可能性が低くなります。また、コードを少なくする必要があるということは、レポートをより早く出力できることを意味し、誰もがレポートを1日中待ちたくないことを意味します。
  4. 利用者。すべてのエンドユーザーがパワーユーザーであるとは限りません(SQLの記述方法を知っているなど)。最小限の労力でExcel(または他の同様のツール)に取り込むことができる形式でデータが既に格納されていると、オフィスのヒーローになります。ユーザーがデータに簡単にアクセスしたり操作したりできない場合、ユーザーはシステムを使用しません。私を信じて、私は数年前に完璧なシステムを設計しました、そしてこの理由のために誰もそれを使用しませんでした。データベースの設計は、事前に定義された一連のルール/ガイドラインを順守するだけでなく、システムを使用可能にすることでもあります。

上で述べたように、これはすべて私の個人的な経験に基づいています。そして、お伝えしておきますが、私が今いる場所にたどり着くまでには、数年がかかり、多くの再設計が必要でした。データベースに関する決定を行うときは、私がしたことを行わないでください。自分の過ちから学び、システムのエンドユーザー(または開発者、レポート作成者など)が関与するようにしてください。

102
Mr.Brownstone

PostgreSQLおよびBRINインデックス

自分でテストしてください。これは、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');
_

または何でも。

63
Evan Carroll

ここで誰もベンチマークについて言及していないことに私は驚かされます @ EvanCarrollが彼の優れた貢献とともにやって来たまでです!

私があなただったら、少し時間をかけて(そして、はい、それは貴重な商品であることを知っています!)、システムをセットアップし、予想どおりに実行します(エンドユーザーの入力をここに!)。たとえば、最も一般的な10のクエリです。

私自身の考え:

NoSQLソリューションは特定のユースケースで非常にうまく機能しますが、アドホッククエリには柔軟性がありません。 MySQLの元チーフアーキテクトであるBrian AkerによるNoSQLの面白い見方については、 ここ を参照してください。

私はあなたのデータがリレーショナルソリューションに非常に適していることを@ Mr.Brownstoneに同意します(この意見は Evan Carrollによって確認されました )!

もし私が何らかの出費を約束したとしたら、それは私のディスク技術に対するものでしょう! NASまたはSANまたは、ほとんど書き込まれていない集約データを保持するためにSSDディスクを使用する可能性があります。

最初私は私が利用できるものを調べます。いくつかのテストを実行し、結果を意思決定者に示します。 ECの作業 という形式のプロキシがすでにあります!しかし、あなた自身のハードウェアで一緒にホイップされた簡単なテストか2つはより説得力があります!

それからお金を使うことを考えてください!お金を使うつもりなら、ソフトウェアではなくハードウェアを最初に見てください。私の知る限り、試用期間中はディスクテクノロジーを採用することができます。あるいは、クラウド上で概念実証をいくつか開始することもできます。

このようなプロジェクトの私の個人的な最初の呼び出しポートはPostgreSQLです。それは私が独自の解決策を除外すると言っているわけではありませんが、物理学とディスクの法則は誰にとっても同じです! 「ヤエカンネビート物理法則ジム」:-)

14
Vérace

まだ行っていない場合は、時系列DBMSを見てください。DBMSは、日付/時刻型を主な対象とするデータの保存とクエリ用に最適化されています。通常、時系列データベースは分/秒/秒以下の範囲でデータを記録するために使用されるため、1時間ごとの増分にまだ適切かどうかはわかりません。そうは言っても、このタイプのDBMSは検討する価値があるようです。現在InfluxDBは、最も確立され、広く使用されている時系列データベースのようです。

6
FloorDivision

明らかにこれはNoSQLの問題ではありませんが、RDBMSソリューションは機能しますが、OLAPアプローチはより適切に適合し、関係するデータ範囲が非常に限られていることを考えると、行ベースではなく列ベースのDBの使用を調査することをお勧めします。このように考えると、17億個のデータがあるかもしれませんが、時間または月のすべての可能な値にインデックスを付けるのに5ビットしか必要ありません。

私は、Sybase IQ(現在のSAP IQ)を使用して1時間に最大3億のカウンターを1時間の通信機器パフォーマンス管理データを保存するという同様の問題の領域での経験がありますが、そのようなソリューションの予算があるかどうかは疑問です。オープンソースの分野では、MariaDB ColumnStoreは非常に有望な候補ですが、MonetDBを調査することもお勧めします。

クエリのパフォーマンスはあなたにとって主要な推進力なので、クエリがどのように表現されるかを考慮してください。これがOLAPとRDBMSが最大の違いを示す場所です。-OLAPを使用すると、繰り返しを減らしたり、ストレージを減らしたり、一貫性を強制したりせずに、クエリのパフォーマンスを正規化します。 。したがって、元のタイムスタンプに加えて(希望するタイムゾーンをキャプチャしたことを覚えていますか?)、UTCタイムスタンプ用の別のフィールド、日付と時刻用の別のフィールド、さらに年、月、日、時間のフィールドがあります。分とUTCオフセット。場所に関する追加情報がある場合は、必要に応じて検索できる別の場所テーブルに自由に保持し、メインレコードにそのテーブルのキーを保持しても、完全な場所名は保持してください。メインテーブルでも、結局のところ、可能性のあるすべての場所でインデックスを作成するのに10ビットしか必要ありません。レポートされるデータを取得するために従う必要のないすべての参照は、クエリの時間を節約できます。

最後の提案として、人気のある集計データには個別のテーブルを使用し、バッチジョブを使用してデータを入力します。これにより、集計値を使用し、現在または過去のデータと比較するクエリを作成するすべてのレポートに対して演習を繰り返す必要がなくなります。歴史的なものから歴史的なものまで、はるかに簡単ではるかに速くなります。

4
Paul Smith