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


74

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

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

(今のところ)保存する必要がある情報は、ロケーションID、タイムスタンプ(日付と時刻)、温度と電気使用量です。

格納する必要があるデータの量については、これは概算ですが、これらの行に沿ったもの:
20 000以上の場所、1か月あたり720レコード(1時間あたりの測定、1か月あたり約720時間)、120か月(10年前) )そして何年も先。簡単な計算により、次の結果が得られます。

20の000の位置は、720のレコード(10年前)×120ヶ月= X 1つの728 000 000レコード

これらは過去のレコードです。新しいレコードは毎月インポートされるため、1か月あたり約20 000 x 720 = 14 400 000の新しいレコードになります。

合計ロケーションも着実に成長します。

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

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

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

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

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

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

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

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


29
2017.小さくはありませんが、これは適切なハードウェアにとって特に大量のデータではありません。そして、私はあなたに話すことを嫌いますが、これまでのところあなたがそこにあるものはリレーショナルデータのように聞こえます。
TomTomの

6
MS SQL Server 2008-2014には、適切なキー(エポック日付)、圧縮、パーティション分割を使用し、クエリ/インデックスがパーティションに合わせられるようにすることで、数百億行のマルチTBテーブルを格納しました。ペタバイトのデータを分析して異なるインデックスを作成するようになったとき、NoSQL(Hadoop)に移行する必要がありました。NoSQLには他の考慮事項が必要であり、この場合は適合しないようです。
アリラゼギ

3
@AliRazeghi Hadoopは、SQLやNoSQLとは関係ありません。これは単なるストレージエンジンです。Hadoopが支援するSQLインターフェースはたくさんあります。
mustaccio

3
ソフトウェア/ライセンスに費やすお金の制約は何ですか?
user3067860

1
無限のお金がある場合、SAP HANAアプライアンスを購入することをお勧めします。大規模なデータセットの集計に最適です。しかし、あなたはおそらく無限のお金を持っていません。
フィリップ

回答:


90

これは、毎時データを使用する代わりに、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秒も待たないという事実を盲目的ではありません。個人的に2 TBのデータを処理するのに30秒と非常に高速だと思っていても。

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

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

上で言ったように、これはすべて私の個人的な経験に基づいており、私に言わせてください、それは私が今いる場所に到達するのは難しい数年と多くの再設計でした。データベースに関する決定を下す際に、私がしたことをしないで、私の間違いから学び、システムのエンドユーザー(または開発者、レポート作成者など)が関与するようにしてください。


私はエポック日付を使用するだけで幸運でしたが、あなたの推奨事項はあなたのユースケースにとって興味深いものです。共有してくれてありがとう。
アリラゼギ

4
私はこれの多くに同意しません。ここで実際の数で示されるように、これのどれも現代のデータベースで本当の関心ではありません。データのユーザーがsqlを使用するには愚かすぎる場合、インターフェイスを作成する必要があります。スキーマを変更しないでください。時間を抽出することは悪いアイデアです
エヴァンキャロル

1
ハードウェアはどのようなものですか?
ケネス

1
@kennes物理、16コア、256GB RAM、100GB OSドライブ、TempDBデータを含む500GBローカルSSD、8TB SSDキャッシュを備えたハイブリッドSAN、および100,000 iops /秒が可能な40TBのスピンドルディスク。データベースの実装では、ColumnStore、圧縮、メモリ内テーブル、パーティション分割、および表形式のSSASインスタンスが使用されます。
Mr.Brownstone

1
これは、サービスを提供するユーザーの数に応じて信じられないほどのハードウェアです。これは疑似最適化の応答なので、テクノロジーを含めることは有用だと思います。30秒で2TBを処理できると聞いてショックを受けました。これは非常に高速です。私自身の個人的な判断はさておき、時系列データの最適化を検討している将来の人々に役立つと思います!
ケネス

57

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レコードを集約できます。それは合理的なはずです。

時間終了

時間の終了の計算も非常に簡単で、タイムスタンプを切り捨ててから時間を追加するだけです。

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

集約にはインデックスを使用していませんが、使用できますが、注意することが重要です。それがあなたの通常のクエリである場合、おそらくBRINをdate_trunc('hour', tsin)そこにdate_trunc入れたいので、不変ではないという小さな問題があるため、最初にラップする必要があります。

パーティショニング

PostgreSQLに関するもう1つの重要な情報は、PG 10がパーティション分割DDLをもたらすことです。したがって、たとえば、毎年簡単にパーティションを作成できます。控えめなデータベースを小さな小さなデータベースに分割します。そうすることで、BRINではなくbtreeインデックスの使用と保守を使用できるようになるはずです。

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

または何でも。


13

ここで誰もベンチマークに言及していないことに驚かさます -それは@EvanCarrollが彼の素晴らしい貢献と一緒になるまでです!

もし私があなただったら、私はしばらく時間を費やし(そして、はい、それは貴重な商品だとわかっています!)、システムをセットアップし、あなたが思うものを実行します(ここでエンドユーザーの入力を取得してください!)

私自身の考え:

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

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

もし私が出費をするなら、それは私のディスク技術になります!まれにしか書き込まれない集計データを保持するために、NASまたはSANまたは多分いくつかのSSDディスクに自由に使えるお金を費やします!

最初に私が利用できるものを見ます。いくつかのテストを実行し、結果を意思決定者に示します。あなたはすでにECの仕事の形でプロキシを持っています!しかし、自分のハードウェアで簡単なテストを1つまたは2つ組み合わせれば、より説得力があります。

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

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


6

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


1
時系列DBMSの例は何ですか?
ビショップ

2
見ていこちらを
ベレース

4

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

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

クエリのパフォーマンスはあなたにとって重要な要因であるため、クエリの表現方法を考慮してください。OLAPとRDBMSの最大の違いは次のとおりです。OLAPを使用すると、繰り返しを減らしたり、ストレージを減らしたり、一貫性を維持したりすることなく、クエリのパフォーマンスを正規化できます。したがって、元のタイムスタンプに加えて(タイムゾーンをキャプチャすることを覚えていますか?)、UTCタイムスタンプ用の別のフィールド、日付と時刻用の他のフィールド、さらに年、月、日、時間、分用のフィールドがありますおよびUTCオフセット。場所に関する追加情報がある場合は、必要に応じて検索できる別の場所テーブルに自由に保管し、メインレコードにそのテーブルのキーを保管し、メインテーブルには完全な場所名を保管してくださいまあ、結局のところ、

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


あなたがそれらを見ているなら、あなたはGreenplumを円柱店とみなすかもしれません!「ボーナス」として-それはPostgreSQLに基づいています!
ベラス

HP Verticaの使用経験は豊富です。チューニングをあまり行わずに、1300億行の9つの列を持つ単一のテーブルがありました。うまくいきました。
ThatDataGuy
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.