金融、物流、 IoT などのビジネスシナリオでは、システムは大量の時系列データ (トランザクションレコード、軌跡データ、モニタリングログ) を生成します。このテラバイト規模のデータをリアルタイムで分析することは、一般的なパフォーマンス上の課題です。PolarDB for PostgreSQL および は、パーティションテーブルやホット/コールド階層化などの機能を使用して、大量の時系列データを保存するためのコスト効率の高いソリューションを提供します。さらに、インメモリー列指向インデックス (IMCI) 機能により、複雑なデータ前処理を行うことなく、大量の時系列データに対してリアルタイムで高性能分析を実行できるため、データ価値を効果的に引き出すことができます。
ソリューション概要
ワークフロー
-
データ書き込み:ビジネスアプリケーションが時系列データ (トランザクションレコードなど) を PolarDB for PostgreSQL または クラスターに書き込みます。
-
列ストアインデックス:ベーステーブルに列ストアインデックスを作成します。 PolarDB for PostgreSQL または は、ローストアと並行して列形式のデータを自動的に維持します。 行ストレージと比較して、列ストレージはデータを列単位で編成するため、より高い圧縮率を実現し、集計クエリの実行時には関連する列のみを読み取ることで I/O を削減します。
-
クエリの高速化:分析クエリ (ローソク足の集計など) は、オプティマイザまたは明示的な
ヒントを通じて、優先的に列ストアインデックスにルーティングされます。 クエリエンジンは列ストレージと並列処理を使用してデータをスキャンおよび集計し、結果を返します。
ソリューションのメリット
-
使いやすさ:ビジネスのリファクタリングや複雑な ETL は不要です。 ベーステーブルに列ストアインデックスを作成するだけで、分析クエリが透過的に高速化されます。
-
豊富な機能:パーティションテーブルをネイティブにサポートし、
time_bucket、first、lastなどの豊富な時系列分析関数を搭載しているため、SQL 開発を簡素化できます。
結果
-
データ量:2 日間で 1 億行 (1 日あたり約 5,000 万行)。
-
ローソク足集約クエリ:指定したタイムウィンドウ内の 5 つの指標 (高値、安値、始値、終値、総出来高)。
-
列ストアインデックスの並列度: 8。
-
クエリ時間 (秒):
シナリオ
秒単位のローソク足集約
分単位のローソク足集約
時間単位のローソク足集約
日単位のローソク足集約
全データ集約 (1 億行)
3.41
0.95
0.93
0.91
1 日分のデータ集約 (約 5,000 万行)
1.88
0.82
0.81
0.76
12 時間分のデータ集約 (約 2,500 万行)
0.89
0.55
0.53
N/A
1 時間分のデータ集約 (約 210 万行)
0.41
0.39
0.37
N/A
操作手順
ステップ 1: 環境の準備
-
クラスターのバージョンと設定が次の要件を満たしていることを確認します。
-
クラスターのバージョン:
-
PostgreSQL 14 (マイナーエンジンバージョン 2.0.14.10.20.0 以降)
-
PostgreSQL 15 (マイナーエンジンバージョン 2.0.15.15.7.0 以降)
-
PostgreSQL 16 (マイナーエンジンバージョン 2.0.16.8.3.0 以降)
-
PostgreSQL 17 (マイナーエンジンバージョン 2.0.17.7.5.0 以降)
説明コンソールで、または
SHOW polardb_version;ステートメントを実行してマイナーエンジンバージョンを確認できます。マイナーエンジンバージョンが要件を満たさない場合は、マイナーエンジンバージョンをアップグレードしてください。 -
-
wal_levelパラメーターをlogicalに設定する必要があります。この設定は、ロジカルデコーディングに必要な情報を先行書き込みログ (WAL) に追加します。説明コンソールでwal_level パラメーターを設定することができます。このパラメーターを変更するとクラスターが再起動されるため、ビジネスへの影響を考慮し、慎重に操作を行ってください。
-
ソーステーブルにはプライマリキーが必要で、列ストアインデックスの作成時にはそのプライマリキー列を含める必要があります。プライマリキーに
SERIALまたはBIGSERIALデータ型を使用すると、データ同期の効率が大幅に向上するため、推奨します。 -
各テーブルに作成できる列ストアインデックスは 1 つだけです。
-
-
列ストアインデックス機能を有効にします。
IMCI を有効化する方法は、PolarDB for PostgreSQL または クラスターのマイナーエンジンバージョンによって異なります。
ステップ 2: データの準備
-
このソリューションでは、トランザクションレコードテーブルを使用し、約 2 日間にわたる 1 億件のトランザクションレコードの生成をシミュレートします。取引時間は毎日 08:00~16:00 で、1 日あたり約 4,000 万件のレコードが生成されます。
-- トランザクションレコードテーブル CREATE TABLE market_trades ( trade_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- 自動インクリメントプライマリキー trade_ts TIMESTAMP, -- トランザクションタイムスタンプ market_id VARCHAR, -- マーケット ID price DECIMAL, -- トランザクション価格 amount DECIMAL, -- トランザクション量 insert_ts TIMESTAMP -- システム書き込みタイムスタンプ ); INSERT INTO market_trades(trade_ts, market_id, price, amount, insert_ts) SELECT trade_ts, market_id, price, amount, trade_ts + (random() * 500)::INT * INTERVAL '1 millisecond' AS insert_ts FROM ( -- ======================== -- 1. 1 日目のピーク:2025-06-01 8:00 - 16:00、4,000 万行 -- ======================== SELECT '2025-06-01 08:00:00'::TIMESTAMP + (random() * 28800)::INT * INTERVAL '1 second' + -- 28800 秒 = 8 時間 (random() * 1000)::INT * INTERVAL '1 millisecond' AS trade_ts, CASE WHEN random() < 0.6 THEN 'BTC-USDT' ELSE 'ETH-USDT' END AS market_id, CASE WHEN random() < 0.6 THEN 30000 + (random() * 1000) ELSE 2000 + (random() * 100) END AS price, random() * 10 + 0.1 AS amount FROM generate_series(1, 40000000) UNION ALL -- ======================== -- 2. 1 日目のオフピーク:2025-06-01 16:00 - 2025-06-02 08:00、1,000 万行 -- ======================== SELECT CASE WHEN random() < 0.5 THEN -- 16:00 - 24:00 '2025-06-01 16:00:00'::TIMESTAMP + (random() * 28800)::INT * INTERVAL '1 second' ELSE -- 00:00 - 08:00 (2 日目の早朝) '2025-06-02 00:00:00'::TIMESTAMP + (random() * 28800)::INT * INTERVAL '1 second' END + (random() * 1000)::INT * INTERVAL '1 millisecond' AS trade_ts, CASE WHEN random() < 0.6 THEN 'BTC-USDT' ELSE 'ETH-USDT' END AS market_id, CASE WHEN random() < 0.6 THEN 30000 + (random() * 1000) ELSE 2000 + (random() * 100) END AS price, random() * 10 + 0.1 AS amount FROM generate_series(1, 10000000) UNION ALL -- ======================== -- 3. 2 日目のピーク:2025-06-02 8:00 - 16:00、4,000 万行 -- ======================== SELECT '2025-06-02 08:00:00'::TIMESTAMP + (random() * 28800)::INT * INTERVAL '1 second' + (random() * 1000)::INT * INTERVAL '1 millisecond' AS trade_ts, CASE WHEN random() < 0.6 THEN 'BTC-USDT' ELSE 'ETH-USDT' END AS market_id, CASE WHEN random() < 0.6 THEN 30000 + (random() * 1000) ELSE 2000 + (random() * 100) END AS price, random() * 10 + 0.1 AS amount FROM generate_series(1, 40000000) UNION ALL -- ======================== -- 4. 2 日目のオフピーク:2025-06-02 16:00 - 2025-06-03 08:00、1,000 万行 -- ======================== SELECT CASE WHEN random() < 0.5 THEN -- 16:00 - 24:00 '2025-06-02 16:00:00'::TIMESTAMP + (random() * 28800)::INT * INTERVAL '1 second' ELSE -- 00:00 - 08:00 (3 日目の早朝) '2025-06-03 00:00:00'::TIMESTAMP + (random() * 28800)::INT * INTERVAL '1 second' END + (random() * 1000)::INT * INTERVAL '1 millisecond' AS trade_ts, CASE WHEN random() < 0.6 THEN 'BTC-USDT' ELSE 'ETH-USDT' END AS market_id, CASE WHEN random() < 0.6 THEN 30000 + (random() * 1000) ELSE 2000 + (random() * 100) END AS price, random() * 10 + 0.1 AS amount FROM generate_series(1, 10000000) ) AS data; -
トランザクションレコードテーブルに列ストアインデックスを作成します。
CREATE INDEX idx_csi_market_trades ON market_trades USING CSI;
ステップ 3: ローソク足集計クエリの実行
ユースケース:固定の時間枠でローソク足を計算します。
アプリケーション例:秒ごとの高値、安値、始値、終値、および総取引量を計算します。
以下の例では、それぞれ秒単位、分単位、時間単位、日単位のローソク足データを計算します。
秒レベルのローソク足集計
-- 秒レベルのローソク足集計
/*+ SET (polar_csi.enable_query on) */
SELECT
time_bucket('1 second', trade_ts) AS candle_ts, -- 1 秒以内のデータ
market_id,
MIN(price) AS low, -- 1 秒以内の安値
MAX(price) AS high, -- 1 秒以内の高値
FIRST(price ORDER BY trade_ts) AS open, -- 1 秒以内の始値
LAST(price ORDER BY trade_ts) AS close, -- 1 秒以内の終値
SUM(amount) AS vol -- 1 秒以内の総取引量
FROM market_trades
WHERE trade_ts >= '2025-06-01 00:00:00' AND trade_ts <= '2025-06-02 00:00:00'
GROUP BY candle_ts, market_id
ORDER BY candle_ts, market_id;
分レベルのローソク足集計
-- 分レベルのローソク足集計
/*+ SET (polar_csi.enable_query on) */
SELECT
time_bucket('1 minute', trade_ts) AS candle_ts, -- 1 分以内のデータ
market_id,
MIN(price) AS low, -- 1 分以内の安値
MAX(price) AS high, -- 1 分以内の高値
FIRST(price ORDER BY trade_ts) AS open, -- 1 分以内の始値
LAST(price ORDER BY trade_ts) AS close, -- 1 分以内の終値
SUM(amount) AS vol -- 1 分以内の総取引量
FROM market_trades
WHERE trade_ts >= '2025-06-01 00:00:00' AND trade_ts <= '2025-06-02 00:00:00'
GROUP BY candle_ts, market_id
ORDER BY candle_ts, market_id;
時間レベルのローソク足集計
-- 時間レベルのローソク足集計
/*+ SET (polar_csi.enable_query on) */
SELECT
time_bucket('1 hour', trade_ts) AS candle_ts, -- 1 時間以内のデータ
market_id,
MIN(price) AS low, -- 1 時間以内の安値
MAX(price) AS high, -- 1 時間以内の高値
FIRST(price ORDER BY trade_ts) AS open, -- 1 時間以内の始値
LAST(price ORDER BY trade_ts) AS close, -- 1 時間以内の終値
SUM(amount) AS vol -- 1 時間以内の総取引量
FROM market_trades
WHERE trade_ts >= '2025-06-01 00:00:00' AND trade_ts <= '2025-06-02 00:00:00'
GROUP BY candle_ts, market_id
ORDER BY candle_ts, market_id;
日レベルのローソク足集計
-- 日レベルのローソク足集計
/*+ SET (polar_csi.enable_query on) */
SELECT
time_bucket('1 day', trade_ts) AS candle_ts, -- 1 日以内のデータ
market_id,
MIN(price) AS low, -- 1 日以内の安値
MAX(price) AS high, -- 1 日以内の高値
FIRST(price ORDER BY trade_ts) AS open, -- 1 日以内の始値
LAST(price ORDER BY trade_ts) AS close, -- 1 日以内の終値
SUM(amount) AS vol -- 1 日以内の総取引量
FROM market_trades
WHERE trade_ts >= '2025-06-01 00:00:00' AND trade_ts <= '2025-06-02 00:00:00'
GROUP BY candle_ts, market_id
ORDER BY candle_ts, market_id;
SQL に関する注記
-
/*+ SET (polar_csi.enable_query on) */:クエリに列ストアインデックスの実行計画を強制的に使用させます。一部のシナリオでは、オプティマイザが行ストアの方が効率的であると誤って推定することがあります。このヒントは、クエリが確実に列ストアパスを使用するようにします。 -
time_bucket(bucket_width, ts)は、時系列データベース () が提供する関数です。この関数は、タイムスタンプtsを、指定された間隔bucket_widthでグループ化します。