TimescaleDBを用いた工場のセンサー時系列データ収集とGrafana可視化ダッシュボードの構築
はじめに
こんにちは、NKKTech Global 技術チームです。
製造現場のIoT化において、エンジニアが直面するデータベース選定の課題は以下の2点に集約されます。
- 秒間数万件の時系列データを高速にINSERTでき、数か月分のデータをストレージを圧迫せずに保持したい
- センサー値だけでなく、「設備型番」「担当ライン」「閾値マスタ」などの業務データと標準SQLでJOINしたい
InfluxDBなどのNoSQL時系列データベースは時系列の書き込みには強いものの、マスタデータとのリレーショナル結合が困難です。一方、PostgreSQLの拡張機能である TimescaleDB は、「完全なPostgreSQL互換(SQL/JOIN可能)」を保ちながら、時系列データの書き込み・集計速度を10〜100倍に高め、最大90%以上のデータ圧縮を実現します。
本記事では、工場センサーデータをTimescaleDBに蓄積し、Grafanaで瞬時に描画するための設計と実装を完全解説します。
1. システム全体アーキテクチャ
工場現場(OT)からクラウド/オンプレミス可視化基盤(IT)までのパイプライン構成です。
2. TimescaleDBのセットアップとHypertable設計
① テーブル作成とHypertableへの変換
まずは通常のPostgreSQLテーブルを作成し、TimescaleDBの核となる Hypertable(ハイパーテーブル) へ変換します。Hypertable化することで、指定した時間間隔(デフォルト7日)ごとに内部でテーブルが自動パーティショニング(チャンク分割)され、B-Treeインデックスの肥大化を防ぎます。
-- 1. TimescaleDB拡張の有効化
CREATE EXTENSION IF NOT EXISTS timescaledb;
-- 2. 設備マスタテーブル(通常のPostgreSQLリレーショナルテーブル)
CREATE TABLE IF NOT EXISTS equipment_master (
equipment_id VARCHAR(50) PRIMARY KEY,
line_name VARCHAR(50) NOT NULL,
model_name VARCHAR(100),
install_date DATE,
critical_level INT DEFAULT 1
);
-- 3. センサー時系列テーブル
CREATE TABLE IF NOT EXISTS sensor_metrics (
recorded_at TIMESTAMPTZ NOT NULL,
equipment_id VARCHAR(50) NOT NULL,
temperature NUMERIC(5,2),
vibration_rms NUMERIC(6,3),
current_ampere NUMERIC(5,2),
CONSTRAINT fk_equipment FOREIGN KEY (equipment_id) REFERENCES equipment_master (equipment_id)
);
-- 4. Hypertableへの変換 (時間列: recorded_at, チャンク期間: 1日)
SELECT create_hypertable(
'sensor_metrics',
by_range('recorded_at', INTERVAL '1 day')
);
-- 5. 高速検索用複合インデックス
CREATE INDEX IF NOT EXISTS idx_sensor_equipment_time
ON sensor_metrics (equipment_id, recorded_at DESC);
3. 本番運用の必須機能:圧縮とContinuous Aggregates
数か月〜数年分のデータを運用する場合、ストレージの枯渇と描画遅延を防ぐために以下の2つの設定が必須です。
① ネイティブ・カラムナー圧縮(Compression Policy)
時系列データは類似した値が連続するため、カラムナ形式に変換することで90%以上のストレージ削減が可能です。
-- 1. 圧縮設定の有効化 (設備IDごとにソートして圧縮)
ALTER TABLE sensor_metrics SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'equipment_id',
timescaledb.compress_orderby = 'recorded_at DESC'
);
-- 2. 7日以上前のチャンクを自動圧縮するポリシーを追加
SELECT add_compression_policy('sensor_metrics', INTERVAL '7 days');
② 継続的集計(Continuous Aggregates)
Grafanaで「過去1か月間の温度推移」を表示する際、数億レコードの生データをスキャンすると数十秒かかります。バックグラウンドで自動集計されるマテリアライズドビューを定義します。
-- 5分単位の平均・最大・最小値を保持する集計ビュー
CREATE MATERIALIZED VIEW sensor_metrics_5min
WITH (timescaledb.continuous) AS
SELECT
time_bucket('5 minutes', recorded_at) AS bucket,
equipment_id,
AVG(temperature) AS avg_temp,
MAX(temperature) AS max_temp,
AVG(vibration_rms) AS avg_vibration,
MAX(vibration_rms) AS max_vibration
FROM sensor_metrics
GROUP BY bucket, equipment_id
WITH NO DATA;
-- 毎分自動更新するポリシーの追加 (直近1時間〜過去1か月の範囲を更新)
SELECT add_continuous_aggregate_policy('sensor_metrics_5min',
start_offset => INTERVAL '1 month',
end_offset => INTERVAL '5 minutes',
schedule_interval => INTERVAL '1 minute');
4. 高速データ取り込みパイプライン(Python)
エッジから送られてくる大量のJSONデータを、単一INSERTではなく**バッチ実行(copy または execute_values)**で秒間数万レコード規模まで引き上げます。
import asyncio
import asyncpg
from datetime import datetime, timezone
DATABASE_URL = "postgresql://postgres:password@localhost:5432/factory_iot"
async def bulk_insert_metrics(records):
"""
records: List of tuples (recorded_at, equipment_id, temp, vib, amp)
"""
conn = await asyncpg.connect(DATABASE_URL)
try:
# asyncpgのcopy_records_to_tableによる超高速バイナリインポート
await conn.copy_records_to_table(
'sensor_metrics',
records=records,
columns=['recorded_at', 'equipment_id', 'temperature', 'vibration_rms', 'current_ampere']
)
finally:
await conn.close()
# 実行シミュレーション
async def main():
now = datetime.now(timezone.utc)
# サンプルバッチデータ作成
batch_data = [
(now, f"LINE_A_MC_0{i%5+1}", 65.5 + i*0.1, 1.25, 14.8)
for i in range(1000)
]
await bulk_insert_metrics(batch_data)
print("1,000 records successfully ingested via COPY.")
if __name__ == "__main__":
asyncio.run(main())
5. Grafanaダッシュボードの構築とSQLクエリ設計
Grafanaのデータソースに「PostgreSQL」を選択し、TimescaleDBオプションを有効にします。
① マスタ結合を用いたリアルタイム時系列グラフ
Grafanaの標準マクロ $__timeFilter と TimescaleDB固有の time_bucket を組み合わせます。
SELECT
$__timeFilter(bucket) AS "time",
CONCAT(m.line_name, ' - ', m.model_name, ' (', s.equipment_id, ')') AS metric,
s.avg_temp AS "平均温度 [℃]"
FROM sensor_metrics_5min s
JOIN equipment_master m ON s.equipment_id = m.equipment_id
WHERE
$__timeFilter(s.bucket)
AND m.line_name = '$SelectedLine' -- Grafanaダッシュボード変数
ORDER BY 1;
② 異常予兆の警告パネル(Stat Panel)
振動値が警戒値を超えている設備の最新ステータスを表示します。
SELECT
m.equipment_id AS "設備ID",
m.line_name AS "ライン",
s.vibration_rms AS "最新振動RMS",
CASE
WHEN s.vibration_rms > 4.5 THEN '危険 (Critical)'
WHEN s.vibration_rms > 2.8 THEN '注意 (Warning)'
ELSE '正常 (Normal)'
END AS "ステータス"
FROM (
SELECT DISTINCT ON (equipment_id) equipment_id, vibration_rms, recorded_at
FROM sensor_metrics
ORDER BY equipment_id, recorded_at DESC
) s
JOIN equipment_master m ON s.equipment_id = m.equipment_id
ORDER BY s.vibration_rms DESC;
6. 本番運用のベストプラクティス
-
チャンクサイズの調整基準:
1つのチャンク(テーブルパーティション)のインデックスとデータが、サーバーのRAM容量の25%程度に収まるように時間範囲(chunk_time_interval)を設定します(例: メモリ32GB環境で1日あたり数GBのデータ量なら1日単位が最適)。 -
自動保持ポリシー(Data Retention):
「生データは3か月で削除し、集計ビュー(Continuous Aggregate)は2年間保持する」設定を行うことで、ストレージコストを一定に保ちます。SELECT add_retention_policy('sensor_metrics', INTERVAL '90 days'); -
PgBouncerによるコネクションプール:
多数のエッジゲートウェイやワーカープロセスから同時接続される場合、PostgreSQLのプロセス枯渇を防ぐため、必ず手前にPgBouncerを配置してください。
まとめ:OTデータの真価は「SQLで語れること」にある
工場のIoT化を成功に導く最大のポイントは、「時系列センサー値」と「工場の生産管理コンテキスト」をシームレスに結合できるアーキテクチャを選択することです。
- TimescaleDBのHypertableで、秒間数万件のインジェストに耐えうる時系列性能を確保する。
- Continuous Aggregates & Columnar Compressionで、Grafanaの超高速描画とストレージの省力化を実現する。
- PostgreSQL互換の標準SQLを活用し、設備マスタや保全履歴と瞬時に紐付ける。
NKKTech Globalでは、製造業に向けたエッジゲートウェイの設計から、TimescaleDBを用いた大規模IoT基盤の構築、Grafanaダッシュボードの導入支援までを一気通貫で支援しています。
お問い合わせ先
工場IoT・センサーデータ可視化基盤の構築、TimescaleDBの設計・性能チューニング、スマートファクトリー化のご相談は、下記よりお気軽にお問い合わせください。
- Webサイト: https://nkktech.com/
- メール: contact@nkk.com.vn
- LinkedIn: https://www.linkedin.com/company/nkktech
著者:NKKTech Global 技術チーム
私たちは、強固なデータ基盤と確かなIoTエンジニアリングで、製造業のデータドリブン経営をグローバルに支援します。
