金融、物流、IoT (Internet of Things) などのビジネスシナリオでは、システムはトランザクションレコード、軌跡データ、モニタリングログなど、大量の時系列データを生成します。このテラバイト規模のデータに対してリアルタイム分析を実行すると、多くの場合、パフォーマンスの課題が生じます。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。
所要時間は次のとおりです (秒):
シナリオ
秒レベルのローソク足集約
分レベルのローソク足集約
時間単位の K ライン集約
日レベルのローソク足集約
全データ集約 (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 時間のデータ集約 (約 600 万エントリ)
0.41
0.39
0.37
N/A
実装手順
ステップ 1: 環境の準備
クラスターのバージョンと構成が次の要件を満たしていることを確認します。
クラスターのバージョン:
PostgreSQL 16 (マイナーエンジンバージョン 2.0.16.8.3.0 以降)
PostgreSQL 14 (マイナーエンジンバージョン 2.0.14.10.20.0 以降)
説明コンソールで、または
SHOW polardb_version;文を実行して、マイナーエンジンバージョン番号を表示できます。マイナーエンジンバージョンが要件を満たしていない場合は、マイナーエンジンバージョンをアップグレードする必要があります。ソーステーブルにはプライマリキーが必要です。列ストアインデックスを作成するときに、プライマリキー列を含める必要があります。
wal_levelパラメーターはlogicalに設定する必要があります。これにより、論理レプリケーションをサポートするために必要な情報が先行書き込みログ (WAL) に追加されます。説明コンソールで wal_level パラメーターを設定できます。このパラメーターを変更すると、クラスターが再起動します。ビジネス運用を適宜計画し、注意して進めてください。
列ストアインデックス機能を有効にします。
列ストアインデックス機能を有効にする方法は、PolarDB for PostgreSQL クラスターのマイナーエンジンバージョンによって異なります。
ステップ 2: データの準備
このソリューションでは、トランザクションレコードテーブルを使用し、約 2 日間にわたる 1 億件のトランザクションデータエントリの生成をシミュレートします。この期間中、トランザクションは毎日 8: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: ローソク足集約クエリの実行
シナリオ: 各固定タイムウィンドウのローソク足データを計算します。
例: 各秒の最高価格、最低価格、始値、終値、および総トランザクションボリュームを計算します。
以下の例では、秒単位、分単位、時間単位、日単位でローソク足データを計算します。
第 2 レベルのローソク足集約
-- 秒レベルのローソク足集約
/*+ 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) でグループ化します。





