1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PostgreSQL時足データ取得を6.6倍高速化 - B-tree二分木探索の威力

Posted at

はじめに

株式取引システムで、リアルタイムデータから時足データを取得する処理がボトルネックになっていました。PostgreSQLのB-treeインデックス(二分木探索)を活用したクエリ最適化により、約6.6倍(14.9ms → 2.3ms)の高速化を実現したので、その手法を紹介します。

課題: 時足データの取得が遅い

株価エンジン起動時に、過去250時間分の時足データ(各時間帯の最終価格)を取得する必要がありました。

データベース構造

stock_prices テーブル(株価リアルタイムデータ)

┌────────┬─────────────────────┬──────────┬────────┐
│ symbol │ timestamp           │ price    │ volume │
├────────┼─────────────────────┼──────────┼────────┤
│ 1234   │ 2025-01-15 09:00:12 │ 1500.0   │ 1000   │
│ 1234   │ 2025-01-15 09:00:58 │ 1501.0   │ 500    │
│ 1234   │ 2025-01-15 09:01:23 │ 1502.0   │ 800    │
│ 1234   │ 2025-01-15 10:00:05 │ 1505.0   │ 1200   │
│ 1234   │ 2025-01-15 10:00:47 │ 1506.0   │ 700    │
│ ...    │ ...                 │ ...      │ ...    │
└────────┴─────────────────────┴──────────┴────────┘

インデックス: idx_symbol_time (symbol, timestamp)
データ量: 約1億レコード(全銘柄・全時刻の分足レベルデータ)

business_days テーブル(営業日カレンダー)

┌────────────┬──────────────────┐
│ date       │ is_business_day  │
├────────────┼──────────────────┤
│ 2025-01-15 │ true             │
│ 2025-01-16 │ true             │
│ 2025-01-17 │ false (土曜日)   │
│ 2025-01-18 │ false (日曜日)   │
│ 2025-01-19 │ false (祝日)     │
│ 2025-01-20 │ true             │
│ ...        │ ...              │
└────────────┴──────────────────┘

取得したいデータ

各時間帯(9:00, 10:00, 11:00...15:00)の最終価格を250時間分

2025-01-15 09:00台 → 1502.0 (09:01:23の価格)
2025-01-15 10:00台 → 1506.0 (10:00:47の価格)
2025-01-15 11:00台 → 1510.0 (11:00:55の価格)
...

最適化の核心: B-tree二分木探索で目的データに直接アクセス

❌ 旧方式: 集計テーブルを参照

事前に時足データを集計したテーブルhourly_aggregatedを使用していました。

SELECT symbol, timestamp, price, 
       open_price, high_price, low_price, volume,
       ma_5, ma_25, ma_50, ma_75, ma_250,
       -- 以下、多数のテクニカル指標カラム
FROM hourly_aggregated
WHERE symbol = '1234'
ORDER BY timestamp DESC
LIMIT 250

DB側の処理イメージ:

[PostgreSQL の処理]

1. hourly_aggregatedテーブル全体をスキャン
   ↓
2. WHERE symbol = '1234' で絞り込み
   ↓ (多数のカラムを含む大きなレコードを走査)
3. ORDER BY timestamp DESC でソート
   ↓
4. LIMIT 250 で上位250件を取得

【問題点】
- テーブルサイズが大きい(多数のカラム × 大量レコード)
- 全レコードスキャン → メモリ上でソート → 切り出し
- ヒープアクセスが多い(ディスクI/Oが多発)
- 集計テーブルの事前計算・維持コストが高い

✅ 新方式: LATERAL JOIN + 営業日カレンダー

ポイント: 複合インデックス(symbol, timestamp)のB-tree二分木探索を最大限活用

WITH business_hours AS (
    -- 営業日の営業時間のみを生成(9時〜15時)
    SELECT bd.date + (h.hour || ' hours')::INTERVAL as hour_start
    FROM business_days bd
    CROSS JOIN (SELECT generate_series(9, 15) as hour) h
    WHERE bd.is_business_day = true
      AND bd.date <= CURRENT_DATE
    ORDER BY bd.date DESC, h.hour DESC
    LIMIT 300
)
SELECT sp.timestamp, sp.price, sp.volume
FROM business_hours bh
CROSS JOIN LATERAL (
    -- 各時間帯の最終レコードを二分木探索で直接取得
    SELECT timestamp, price, volume
    FROM stock_prices sp
    WHERE symbol = '1234'
      AND timestamp >= bh.hour_start
      AND timestamp < bh.hour_start + INTERVAL '1 hour'
    ORDER BY timestamp DESC
    LIMIT 1
) sp
ORDER BY sp.timestamp DESC
LIMIT 250

DB側の処理イメージ:

[PostgreSQL の処理]

1. business_hours CTE: 営業日×営業時間を生成
   例: 2025-01-15 09:00, 2025-01-15 10:00, ...
   ↓
2. 各時間帯ごとにLATERAL JOIN実行:
   
   【時間帯: 2025-01-15 09:00】
   B-treeインデックス (symbol, timestamp) で二分木探索
   ↓
   log₂(N) の計算量で (symbol='1234', timestamp>='2025-01-15 09:00:00') 
   の範囲開始点を特定
   ↓
   インデックスから該当データブロックのアドレスを取得
   ↓
   データブロックに直接アクセス(ピンポイントI/O)
   ↓
   timestamp DESC でスキャンして LIMIT 1 → 最終レコード取得
   ↓
   次の時間帯へ...

【改善点】
✅ B-tree二分木探索で目的データのアドレスに直接ジャンプ
✅ 各時間帯で1レコードのみ取得(早期終了)
✅ 必要なカラムのみSELECT(price, volumeのみ)
✅ 営業日のみ検索(土日祝をスキップ)

なぜ速いのか?

1. B-tree二分木探索による効率的なアクセス

PostgreSQLのB-treeインデックスは二分木構造で、log₂(N)の計算量で目的データを特定できます。

B-treeインデックスの二分木探索イメージ:

                    [ルートノード]
                   /              \
         [symbol='1000'台]    [symbol='5000'台]
            /        \              /         \
    ['1000'-'2000'] ['2000'-'3000'] ...     ...
         /    \
   ['1234']  ['1500']
      ↓
   [timestamp範囲]
      ↓
   データブロックアドレス: 0x1A2B3C

探索回数: log₂(100,000,000) ≒ 27回の比較で特定

線形探索(フルスキャン)との違い:

  • ❌ 線形探索: 1億レコードを順番にチェック → O(N)
  • ✅ B-tree二分木探索: 約27回の比較で到達 → O(log N)

2. 営業日・営業時間に絞り込み

  • 土日祝日をスキップ(約30%削減)
  • 営業時間外(16時〜翌8時)をスキップ(約71%削減)
  • 実質的な検索対象を約80%削減

3. LATERAL JOINによる早期終了

各時間帯でLIMIT 1により、最終レコードを見つけた瞬間にスキャン終了。

結果

項目 旧方式 新方式 改善率
実行時間 14.9ms 2.3ms 6.6倍高速化
データソース 集計テーブル リアルタイムテーブル リアルタイム性向上
I/O効率 多数のカラム取得 必要なカラムのみ メモリ効率向上
スキャン方式 線形スキャン B-tree二分木探索 計算量O(N)→O(log N)

まとめ

PostgreSQLのB-treeインデックスによる二分木探索を活用することで、大量データから目的のレコードに効率的にアクセスできます。

成功のポイント:

  1. ✅ 適切な複合インデックス設計: (symbol, timestamp)
  2. ✅ インデックスを活用できるクエリ構造: LATERAL JOIN
  3. ✅ 不要な検索範囲の排除: 営業日カレンダーで絞り込み

時系列データを扱う金融システムでは、この手法が非常に効果的です。同じアプローチで日足データも**3,530倍高速化(28.6秒 → 8.1ms)**を実現しました。

参考: クエリ全体

完全なSQLクエリ(クリックで展開)
WITH business_hours AS (
    SELECT bd.date + (h.hour || ' hours')::INTERVAL as hour_start
    FROM business_days bd
    CROSS JOIN (
        SELECT generate_series(9, 15) as hour
    ) h
    WHERE bd.is_business_day = true
      AND bd.date <= CURRENT_DATE
    ORDER BY bd.date DESC, h.hour DESC
    LIMIT 300
)
SELECT sp.timestamp, sp.price, sp.volume
FROM business_hours bh
CROSS JOIN LATERAL (
    SELECT timestamp, price, volume
    FROM stock_prices
    WHERE symbol = %s
      AND timestamp >= bh.hour_start
      AND timestamp < bh.hour_start + INTERVAL '1 hour'
    ORDER BY timestamp DESC
    LIMIT 1
) sp
ORDER BY sp.timestamp DESC
LIMIT 250
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?