45395 - シコウサクゴ -

PostgreSQL + TimescaleDBで時系列データを管理する:個人開発のDB設計

2026-04-03
AI駆動開発
AI駆動開発
PostgreSQL
TimescaleDB
データベース
時系列データ
Last updated:2026-04-05
8 Minutes
1510 Words

自動化プロジェクトで扱うデータは、メトリクスデータ・分析結果・パフォーマンス記録など、全てが時系列データです。当初はSQLiteやCSVファイルで管理していましたが、以下の限界に直面しました。

  • 並行アクセス: 複数のlaunchdジョブが同時にデータを書き込む → SQLiteのロック競合
  • クエリ性能: 数百万行の時系列データに対する範囲クエリが遅い
  • 型安全性: CSVでは型が曖昧、SQLiteでは型制約が弱い

本記事では、PostgreSQL 17 + TimescaleDB を採用した経緯と、個人開発での設計判断を記録します。


なぜPostgreSQLか

SQLiteの限界

SQLiteは「1プロセスからの読み書き」には優れていますが、本番システムでは複数のジョブが同時にDBにアクセスします。

1
09:00 ジョブA: メトリクスデータ書き込み
2
09:00 ジョブB: 分析結果書き込み(ジョブAと同時実行)
3
09:00 ジョブC: パフォーマンス読み取り(↑と同時実行)
4
→ SQLite: "database is locked" エラー

WALモードを有効にしても、書き込みの同時実行は1つに制限されます。90ジョブをlaunchdで運用する環境では、この制約は致命的でした。

PostgreSQLを選んだ3つの理由

観点SQLitePostgreSQL
並行書き込み1プロセスのみMVCC対応(複数同時書き込み可)
型安全性型アフィニティ(弱い型)厳密な型制約
時系列拡張なしTimescaleDB対応
セットアップファイル1つHomebrewでインストール

個人開発で「PostgreSQLはオーバースペック」と思われがちですが、並行アクセスと型安全性が必要な時点で、SQLiteの方がむしろリスクが高いです。


macOSでのセットアップ

Homebrew インストール

Terminal window
1
# PostgreSQL 17 インストール
2
brew install postgresql@17
3
4
# サービス起動
5
brew services start postgresql@17
6
7
# TimescaleDB インストール
8
brew install timescaledb
9
10
# TimescaleDB 有効化
11
timescaledb-tune --yes
12
brew services restart postgresql@17

データベース作成

Terminal window
1
# データベース作成
2
createdb metrics_analysis
3
4
# TimescaleDB拡張を有効化
5
psql metrics_analysis -c "CREATE EXTENSION IF NOT EXISTS timescaledb;"

テーブル設計

daily_metrics: 日次メトリクスデータ

1
CREATE TABLE daily_metrics (
2
id BIGSERIAL PRIMARY KEY,
3
datetime TIMESTAMPTZ NOT NULL,
4
source VARCHAR(20) NOT NULL,
5
timeframe VARCHAR(10) NOT NULL,
6
value DOUBLE PRECISION NOT NULL,
7
min_value DOUBLE PRECISION NOT NULL,
8
max_value DOUBLE PRECISION NOT NULL,
9
avg_value DOUBLE PRECISION NOT NULL,
10
sample_count DOUBLE PRECISION,
11
created_at TIMESTAMPTZ DEFAULT NOW(),
12
UNIQUE (datetime, source, timeframe)
13
);
14
15
-- インデックス
2 collapsed lines
16
CREATE INDEX idx_metrics_source_time
17
ON daily_metrics (source, datetime DESC);

必須カラムはDateTime, Value, MinValue, MaxValue, AvgValue, SampleCountの6つです。これは時系列メトリクスの標準的なフォーマットです。

analysis_results: 分析結果

1
CREATE TABLE analysis_results (
2
id BIGSERIAL PRIMARY KEY,
3
datetime TIMESTAMPTZ NOT NULL,
4
source VARCHAR(20) NOT NULL,
5
timeframe VARCHAR(10) NOT NULL,
6
metric_name VARCHAR(50) NOT NULL,
7
metric_value DOUBLE PRECISION,
8
reference_date DATE NOT NULL,
9
composite_id VARCHAR(100) NOT NULL,
10
created_at TIMESTAMPTZ DEFAULT NOW()
11
);
12
13
-- BIツール対応: 複合IDインデックス
14
CREATE INDEX idx_analysis_composite
15
ON analysis_results (composite_id);

performance_snapshots: パフォーマンス記録(TimescaleDB hypertable)

1
CREATE TABLE performance_snapshots (
2
datetime TIMESTAMPTZ NOT NULL,
3
metric_name VARCHAR(50) NOT NULL,
4
metric_value DOUBLE PRECISION NOT NULL,
5
source VARCHAR(20),
6
timeframe VARCHAR(10),
7
metadata JSONB
8
);
9
10
-- TimescaleDB hypertable に変換(自動パーティション)
11
SELECT create_hypertable(
12
'performance_snapshots',
13
'datetime',
14
chunk_time_interval => INTERVAL '1 month'
15
);

TimescaleDBのhypertableは、時系列データを月単位で自動パーティションします。古いデータの削除やアーカイブが、パーティション単位で高速に行えます。


Pythonからの接続

共有モジュール: mgmtDB

DB接続は共有モジュールで抽象化します。各スクリプトが個別に接続情報を持つと、接続先の変更時に全スクリプトの修正が必要になります。

1
from myModules.mgmtDB import get_db_connection
2
3
4
def save_analysis_results(df: pd.DataFrame, table_name: str) -> None:
5
"""分析結果をDBに保存する。
6
7
Args:
8
df: 保存するDataFrame
9
table_name: 保存先テーブル名
10
"""
11
with get_db_connection() as conn:
12
df.to_sql(table_name, conn, if_exists="append", index=False)

時系列クエリの例

1
import pandas as pd
2
from myModules.mgmtDB import get_db_connection
3
4
5
def get_recent_performance(
6
days: int = 30,
7
source: str | None = None,
8
) -> pd.DataFrame:
9
"""直近N日のパフォーマンスデータを取得する。"""
10
query = """
11
SELECT datetime, metric_name, metric_value, source
12
FROM performance_snapshots
13
WHERE datetime >= NOW() - INTERVAL '%s days'
14
"""
15
params = [days]
9 collapsed lines
16
17
if source is not None:
18
query += " AND source = %s"
19
params.append(source)
20
21
query += " ORDER BY datetime DESC"
22
23
with get_db_connection() as conn:
24
return pd.read_sql(query, conn, params=params)

TimescaleDB hypertable の利点

自動パーティション

通常のPostgreSQLでは、テーブルのパーティショニングを手動で設定する必要があります。TimescaleDBはcreate_hypertable()を呼ぶだけで、指定した時間間隔で自動パーティションされます。

1
-- 月単位の自動パーティション
2
SELECT create_hypertable(
3
'performance_snapshots',
4
'datetime',
5
chunk_time_interval => INTERVAL '1 month'
6
);
7
8
-- 古いデータの一括削除(パーティション単位で高速)
9
SELECT drop_chunks('performance_snapshots', older_than => INTERVAL '1 year');

時系列専用の集約関数

1
-- 直近30日の日次平均を取得
2
SELECT time_bucket('1 day', datetime) AS day,
3
avg(metric_value) AS avg_value
4
FROM performance_snapshots
5
WHERE metric_name = 'daily_throughput'
6
AND datetime >= NOW() - INTERVAL '30 days'
7
GROUP BY day
8
ORDER BY day;

time_bucket()はTimescaleDB固有の関数で、任意の時間間隔でのグルーピングが簡潔に書けます。


BIツール対応:複合IDカラム

BIツール(Metabase等)との連携のため、各レコードに複合IDを付与しました。

1
from datetime import datetime
2
3
4
def generate_composite_id(
5
source: str,
6
timeframe: str,
7
sequence: int,
8
) -> str:
9
"""複合IDを生成する。
10
11
形式: {YYYYMMDD}_{Source}_{TimeFrame}_{YYYYMMDDHHMM}_{00001-99999}
12
例: 20260327_server01_4H_202603271500_00001
13
"""
14
today = datetime.now().strftime("%Y%m%d")
15
now = datetime.now().strftime("%Y%m%d%H%M")
1 collapsed line
16
return f"{today}_{source}_{timeframe}_{now}_{sequence:05d}"

この複合IDにより、BIツールから「特定日・特定データソース・特定タイムフレーム」のデータを一意に参照できます。


学んだこと

1. 個人開発でもPostgreSQLを選ぶべき場面がある

「個人開発ならSQLite」という定石は、並行アクセスと型安全性が必要になった時点で見直すべきです。Homebrewで簡単にインストールでき、運用コストも大きくありません。

2. TimescaleDBの自動パーティションは時系列データの必須機能

手動パーティションは設定ミスの温床になります。create_hypertable()一行で自動パーティションが有効になるTimescaleDBは、時系列データを扱うなら必須の拡張です。

3. 共有モジュール(mgmtDB)でDB接続を抽象化すべき

各スクリプトが個別に接続情報を持つと、接続先変更時に全スクリプトを修正する羽目になります。get_db_connection()で接続を一元管理することで、環境切り替えも1箇所の変更で済みます。


まとめ

個人開発でのDB設計で重要なのは以下の3点です。

  1. 並行アクセスが必要ならPostgreSQL: SQLiteのロック競合は90ジョブ運用で致命的です。PostgreSQLのMVCCが解決します
  2. TimescaleDBで自動パーティション: create_hypertable()一行で月単位のパーティションが有効になります。古いデータの削除も高速です
  3. DB接続は共有モジュールで抽象化: mgmtDB.get_db_connection()で接続を一元管理し、環境依存を排除します
Article title:PostgreSQL + TimescaleDBで時系列データを管理する:個人開発のDB設計
Article author:45395
Release time:2026-04-03

記事へのご質問・ご感想をお聞かせください

フィードバックを送る