すべてのプロダクト
Search
ドキュメントセンター

PolarDB:列ストアインデックスを使用して時系列データ分析を高速化する

最終更新日:Nov 21, 2025

金融、物流、IoT (Internet of Things) などのビジネスシナリオでは、システムはトランザクションレコード、軌跡データ、モニタリングログなど、大量の時系列データを生成します。このテラバイト規模のデータに対してリアルタイム分析を実行すると、多くの場合、パフォーマンスの課題が生じます。PolarDB for PostgreSQL は、パーティションテーブルやホットデータとコールドデータの階層型ストレージなどの機能を備え、大量の時系列データを保存するためのコスト効率の高いソリューションを提供します。この基盤の上に、インメモリ列インデックス (IMCI) 機能を使用すると、複雑なデータ前処理なしで大量の時系列データに対してリアルタイムでパフォーマンス専有型の分析を実行でき、データの価値を効果的に引き出すのに役立ちます。

ソリューションの概要

運用フロー

  1. データ書き込み: ビジネスアプリケーションは、トランザクションレコードなどの時系列データを PolarDB for PostgreSQL クラスターに書き込みます。

  2. 列ストアインデックス: ベーステーブルに列ストアインデックスを作成します。PolarDB for PostgreSQL は、テーブル内の列ストアデータを自動的に維持します。列ストアはデータを列ごとに整理するため、圧縮率が高くなり、関連する列のみを読み取る必要があるため、集約クエリ中の I/O 消費が削減されます。

  3. クエリの高速化: ローソク足集約などの分析クエリは、オプティマイザーまたは ヒントワード によって列ストアインデックスを使用するように指示されます。その後、クエリエンジンはカラムナストレージと並列処理を使用してデータをスキャンおよび集約し、結果を返します。

ソリューションの利点

  • 使いやすさ: ビジネスの変更や複雑な抽出・変換・書き出し (ETL) プロセスは不要です。ベーステーブルに列ストアインデックスを作成するだけで、分析クエリを透過的に高速化できます。

  • 豊富な機能: パーティションテーブルをネイティブにサポートし、time_bucketfirstlast などの豊富な組み込み時系列分析関数が含まれているため、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: 環境の準備

  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 パラメーターを設定できます。このパラメーターを変更すると、クラスターが再起動します。ビジネス運用を適宜計画し、注意して進めてください。

  2. 列ストアインデックス機能を有効にします。

    列ストアインデックス機能を有効にする方法は、PolarDB for PostgreSQL クラスターのマイナーエンジンバージョンによって異なります。

    PostgreSQL 16 (2.0.16.9.8.0 以降) または PostgreSQL 14 (2.0.14.17.35.0 以降)

    これらのバージョンの PolarDB for PostgreSQL クラスターでは、2 つの方法が利用可能です。違いは次の表にまとめられています。ニーズに最も適した方法を選択できます。

    比較項目

    [推奨] 列ストアインデックス読み取り専用ノードを追加

    プリインストール済みの列ストアインデックス拡張機能を直接使用

    メソッド

    コンソールのビジュアルインターフェイスを介して、列ストアインデックスノードを手動で追加できます。

    操作は不要です。拡張機能を直接使用できます。

    リソース割り当て

    列ストアエンジンはすべてのリソースを排他的に使用し、利用可能なメモリを完全に活用できます。

    列ストアエンジンはメモリの 25% しか使用できません。残りのメモリはローストアエンジンに割り当てられます。

    ビジネスへの影響

    トランザクション処理 (TP) と分析処理 (AP) のワークロードは異なるノードで分離されており、互いに影響しません。

    TP と AP のワークロードは同じノードで実行され、互いに影響します。

    コスト

    インメモリ列インデックス (IMCI) 読み取り専用ノードには追加料金が発生し、通常の計算ノードと同じレートで請求されます。

    追加コストなし。

    列ストアインデックス読み取り専用ノードの追加

    次の 2 つの方法のいずれかで、列ストアインデックス読み取り専用ノードを追加できます。

    説明

    クラスターには少なくとも 1 つの読み取り専用ノードが含まれている必要があります。単一ノードのクラスターに列ストアインデックス読み取り専用ノードを追加することはできません。

    コンソールでの追加
    1. PolarDB コンソールにログインし、クラスターが配置されているリージョンを選択します。次のいずれかの方法で ノードの追加/削除 ウィザードを開くことができます。

      • クラスター ページで、操作 列の ノードの追加/削除 をクリックします。

        image

      • ターゲットクラスターの 概要 ページで、データベースノード セクションの ノードの追加/削除 をクリックします。

        image

    2. 列ストアインデックス読み取り専用ノードの追加 を選択し、OK をクリックします。

    3. クラスターのスペックアップ/スペックダウンページで、列ストアインデックス読み取り専用ノードを追加し、支払いを完了します。

      1. [IMCI ノードの追加] をクリックし、ノードの仕様を選択します。

      2. スイッチオーバー時間を選択します。

      3. (オプション) 製品利用規約とサービスレベル契約を確認します。

      4. 今すぐ購入 をクリックします。

      image

    4. 支払いが完了したら、クラスターの詳細ページに戻り、列ストアインデックス読み取り専用ノードが追加されるのを待ちます。ノードのステータスが 実行中 に変わると、ノードは追加されます。image

    購入時の追加

    PolarDB 購入ページで、[IMCI 読み取り専用ノード] パラメーターの ノード数 数を選択します。

    image

    PostgreSQL 16 (2.0.16.8.3.0 から 2.0.16.9.8.0) または PostgreSQL 14 (2.0.14.10.20.0 から 2.0.14.17.35.0)

    これらのバージョンの PolarDB for PostgreSQL クラスターでは、列ストアインデックスはデータベースクラスターに polar_csi 拡張機能としてデプロイされます。列ストアインデックスを使用する前に、指定されたデータベースに拡張機能を作成する必要があります。

    説明
    • polar_csi 拡張機能はデータベースレベルでスコープされます。クラスター内の複数のデータベースで列ストアインデックスを使用するには、各データベースに polar_csi 拡張機能を作成する必要があります。

    • 拡張機能をインストールするために使用されるデータベースアカウントは、特権アカウントである必要があります。

    次の 2 つの方法のいずれかで polar_csi 拡張機能をインストールできます。

    コンソールからのインストール

    1. PolarDB コンソールにログインします。左側のナビゲーションウィンドウで、クラスター をクリックします。クラスターが配置されているリージョンを選択し、クラスター ID をクリックしてクラスターの詳細ページに移動します。

    2. 左側のナビゲーションウィンドウで、設定と管理 > プラグインの管理 を選択します。プラグインの管理 タブで、プラグインがインストールされていません を選択します。

    3. ページの右上隅で、ターゲットデータベースを選択します。polar_csi 拡張機能の行で、[アクション] 列の インストール をクリックします。表示される プラグインのインストール ダイアログボックスで、ターゲットの データベースアカウント を選択し、確認 をクリックしてターゲットデータベースに拡張機能をインストールします。

      image.png

    コマンドラインからのインストール

    データベースクラスターに接続し、polar_csi 拡張機能を作成するために必要な権限を持つターゲットデータベースで次の文を実行します。

    CREATE EXTENSION polar_csi;

ステップ 2: データの準備

  1. このソリューションでは、トランザクションレコードテーブルを使用し、約 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;
  2. トランザクションレコードテーブルに列ストアインデックスを作成します。

    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) でグループ化します。