時系列データベース選定:IoTシナリオにおけるQuestDB vs InfluxDB vs TDengine 2026
時系列データベース選定:IoTシナリオにおけるQuestDB vs InfluxDB vs TDengine
2026年のIoTの世界では、センサーが毎秒数万のデータポイントを吐き出しています——温度、湿度、振動周波数、GPS座標……従来のMySQL?書き込みボトルネックで5分後にクラッシュ。MongoDB?時系列集計クエリが遅すぎて現実を疑うレベル。
時系列データベースの選定を間違えると、クエリタイムアウト、ストレージ爆発、さらにはデータパイプライン全体の麻痺を招きます。QuestDB、InfluxDB、TDengineの三強はそれぞれ強みを持っていますが、あなたのIoTシナリオに最適なのはどれ?本記事では5つのコア実践パターンから徹底解説します。
コア概念早見表
| 概念 | 説明 | 典型的なシナリオ |
|---|---|---|
| 時系列データ(Time Series) | 時間順に並んだデータポイントのシーケンス | センサー収集、監視メトリクス |
| タグ(Tag) | データの次元/インデックスフィールド | デバイスID、リージョン、タイプ |
| フィールド(Field) | データの測定値 | 温度値、電圧値 |
| ダウンサンプリング(Downsampling) | 高頻度データを低頻度に集約 | 秒→分→時間 |
| 保持ポリシー(Retention Policy) | データの自動期限切れ・クリーンアップ | ホットデータ7日、コールドデータ1年 |
| スーパーテーブル(Super Table) | TDengine独自、マルチデバイステーブルのテンプレート | 1000台のデバイスが1つのテンプレートを共有 |
IoT時系列データの5つのペインポイント
- 書き込みスループットのボトルネック:100万デバイスの同時書き込み、従来のDBではIOPSが限界
- クエリ集計の非効率:時間窓集計、ダウンサンプリングクエリのパフォーマンスが悪く、P99レイテンシが急増
- ストレージコストの制御不能:時系列データは大量かつ増加し続け、ホット/コールド分離が困難
- マルチデバイス管理の複雑さ:千台のデバイスがそれぞれタグを持ち、Schema管理が混乱
- エコシステム統合の分断:Grafana、Kafka、Telegrafなどのツールチェーンとの統合コストが高い
パターン1:IoT時系列データの特性とモデリング
IoT時系列データには明確な特性があります:高カーディナリティタグ、追記専用書き込み、強い時間局所性、読み書き多用。これらの特性を理解することが選定の前提です。
# Python: IoT時系列データ生成シミュレーション
# 実行環境: Python 3.12+ / 追加依存なし
import time
import json
import random
from datetime import datetime, timezone
def generate_iot_sensor_data(device_count: int = 100, interval_ms: int = 1000) -> dict:
"""IoTセンサー時系列データポイントの生成
Args:
device_count: デバイス数
interval_ms: 収集間隔(ミリ秒)
Returns:
単一の時系列データポイント
"""
device_id = f"sensor-{random.randint(1, device_count):04d}"
region = random.choice(["jp-east", "jp-west", "ap-northeast", "ap-southeast"])
sensor_type = random.choice(["temperature", "humidity", "vibration", "pressure"])
base_values = {
"temperature": 25.0,
"humidity": 60.0,
"vibration": 0.5,
"pressure": 101.3,
}
value = round(base_values[sensor_type] + random.gauss(0, 2), 3)
return {
"timestamp": int(time.time() * 1_000_000), # マイクロ秒精度
"tags": {
"device_id": device_id,
"region": region,
"sensor_type": sensor_type,
"factory": f"plant-{region.split('-')[0]}",
},
"fields": {
"value": value,
"quality": random.choice(["good", "good", "good", "warning", "error"]),
"battery": round(random.uniform(10, 100), 1),
},
}
def simulate_iot_stream(duration_seconds: int = 10, devices: int = 100) -> list[dict]:
"""IoTデータストリームのシミュレーション
Args:
duration_seconds: シミュレーション時間(秒)
devices: デバイス数
Returns:
時系列データポイントのリスト
"""
data_points = []
start = time.time()
while time.time() - start < duration_seconds:
point = generate_iot_sensor_data(device_count=devices)
data_points.append(point)
time.sleep(0.01) # 100Hz収集をシミュレート
print(f"生成 {len(data_points)} データポイント, "
f"時間 {duration_seconds}s, "
f"デバイス数 {devices}")
return data_points
if __name__ == "__main__":
points = simulate_iot_stream(duration_seconds=5, devices=50)
print(json.dumps(points[0], indent=2, ensure_ascii=False))
パターン2:QuestDB SQL時系列クエリ
QuestDBは「SQLファースト」で知られ、標準SQLの時系列拡張によりクエリが極めて直感的です。ゼロ依存インストールと高性能書き込みが最大の武器です。
-- QuestDB: IoT時系列データSQLクエリパターン
-- 実行環境: QuestDB 8.x / ILP(InfluxDB Line Protocol)書き込み
-- 1. テーブル作成
CREATE TABLE IF NOT EXISTS sensor_data (
timestamp TIMESTAMP,
device_id SYMBOL,
region SYMBOL,
sensor_type SYMBOL,
factory SYMBOL,
value DOUBLE,
quality SYMBOL,
battery DOUBLE
) TIMESTAMP(timestamp) PARTITION BY DAY WAL;
-- 2. 時間窓集計 - 5分ごとの平均温度
SELECT
timestamp,
device_id,
avg(value) AS avg_temp,
min(value) AS min_temp,
max(value) AS max_temp,
count() AS sample_count
FROM sensor_data
WHERE sensor_type = 'temperature'
AND timestamp >= dateadd('h', -1, now())
SAMPLE BY 5m ALIGN TO CALENDAR;
-- 3. 最新値クエリ - LATEST ON構文
SELECT * FROM sensor_data LATEST ON timestamp PARTITION BY device_id;
-- 4. ダウンサンプリング + 多次元グループ化
SELECT
timestamp,
region,
sensor_type,
avg(value) AS avg_value,
stddev(value) AS std_value,
count() AS data_points
FROM sensor_data
WHERE timestamp >= dateadd('d', -7, now())
SAMPLE BY 1h ALIGN TO CALENDAR
GROUP BY region, sensor_type;
-- 5. 異常検知 - 3連続ポイント閾値超過アラート
WITH spike_detected AS (
SELECT
timestamp,
device_id,
value,
case when value > 35 OR value < 15 THEN 1 ELSE 0 END AS is_spike
FROM sensor_data
WHERE sensor_type = 'temperature'
)
SELECT
timestamp,
device_id,
value,
sum(is_spike) OVER (
ORDER BY timestamp
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS consecutive_spikes
FROM spike_detected
WHERE consecutive_spikes >= 3;
# Python: QuestDB ILP書き込みクライアント
# 実行環境: Python 3.12+ / pip install questdb
from questdb.ingress import IngressError, Sender, Buffer, Protocol
def write_iot_data_questdb(host: str = "localhost", port: int = 9009):
"""ILPプロトコルでQuestDBに書き込み"""
conf = f"http::addr={host}:{port};"
try:
with Sender.from_conf(conf) as sender:
for i in range(1000):
device_id = f"sensor-{i % 50:04d}"
region = ["jp-east", "jp-west", "ap-northeast"][i % 3]
sender.row(
"sensor_data",
symbols={
"device_id": device_id,
"region": region,
"sensor_type": "temperature",
"factory": f"plant-{region.split('-')[0]}",
},
columns={
"value": 25.0 + (i % 10) * 0.5,
"battery": 80.0 + (i % 20),
},
at=Sender.current_timestamp_with_nanos(),
)
sender.flush()
print(f"QuestDBに1000件のデータを正常に書き込みました")
except IngressError as e:
print(f"書き込み失敗: {e}")
if __name__ == "__main__":
write_iot_data_questdb()
パターン3:InfluxDB Fluxクエリ
InfluxDB 3.xはSQLクエリに移行しましたが、Fluxは2.xエコシステムで広く使用されています。Fluxの習得は既存システムの運用に必須です。
# Python: InfluxDB 2.x Fluxクエリパターン
# 実行環境: Python 3.12+ / pip install influxdb-client[async]
from influxdb_client import InfluxDBClient, Point, WriteOptions
from influxdb_client.client.write_api import SYNCHRONOUS
import datetime
INFLUX_URL = "http://localhost:8086"
INFLUX_TOKEN = "your-api-token"
INFLUX_ORG = "iot-org"
INFLUX_BUCKET = "sensor-bucket"
def write_iot_data_influxdb():
"""InfluxDB 2.xにIoTデータを書き込み"""
client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
write_api = client.write_api(write_options=SYNCHRONOUS)
points = []
for i in range(500):
point = (
Point("sensor_data")
.tag("device_id", f"sensor-{i % 50:04d}")
.tag("region", ["jp-east", "jp-west", "ap-northeast"][i % 3])
.tag("sensor_type", "temperature")
.field("value", 25.0 + (i % 10) * 0.5)
.field("battery", 80.0 + (i % 20))
.time(datetime.datetime.utcnow(), write_precision="ms")
)
points.append(point)
write_api.write(bucket=INFLUX_BUCKET, org=INFLUX_ORG, record=points)
print(f"InfluxDBに{len(points)}件のデータを正常に書き込みました")
client.close()
FLUX_QUERIES = """
// 1. 時間窓集計 - 5分ごとの平均温度
from(bucket: "sensor-bucket")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "sensor_data")
|> filter(fn: (r) => r.sensor_type == "temperature")
|> filter(fn: (r) => r._field == "value")
|> aggregateWindow(every: 5m, fn: mean, createEmpty: false)
|> yield(name: "avg_temp_5m")
// 2. 多次元グループ化統計
from(bucket: "sensor-bucket")
|> range(start: -7d)
|> filter(fn: (r) => r._measurement == "sensor_data")
|> filter(fn: (r) => r._field == "value")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["region", "sensor_type"])
|> yield(name: "hourly_by_region_type")
// 3. 異常検知 - 連続閾値超過
from(bucket: "sensor-bucket")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "sensor_data")
|> filter(fn: (r) => r._field == "value")
|> map(fn: (r) => ({r with is_spike: if r._value > 35.0 or r._value < 15.0 then 1 else 0}))
|> window(every: 5m)
|> sum(column: "is_spike")
|> filter(fn: (r) => r.is_spike >= 3)
|> yield(name: "spike_alert")
// 4. ダウンサンプリング連続クエリ書き込み
from(bucket: "sensor-bucket")
|> range(start: -30d)
|> filter(fn: (r) => r._measurement == "sensor_data")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> to(bucket: "sensor-downsampled", org: "iot-org")
"""
if __name__ == "__main__":
write_iot_data_influxdb()
print("\nFluxクエリ例:")
print(FLUX_QUERIES)
パターン4:TDengineスーパーテーブル
TDengineのスーパーテーブルはIoTシナリオのキラーフィーチャーです:1つのスーパーテーブルテンプレートで千台のデバイスを管理、サブテーブルが自動的にタグを継承、クエリパフォーマンスが極めて高い。
-- TDengine 3.x: IoTスーパーテーブルパターン
-- 実行環境: TDengine 3.3+ / taos CLI
-- 1. スーパーテーブル(テンプレート)作成
CREATE STABLE IF NOT EXISTS sensor_data (
ts TIMESTAMP,
value FLOAT,
battery FLOAT,
quality NCHAR(10)
) TAGS (
device_id NCHAR(20),
region NCHAR(20),
sensor_type NCHAR(20),
factory NCHAR(20)
);
-- 2. サブテーブル自動作成と書き込み
INSERT INTO d_sensor_0001 USING sensor_data TAGS ('sensor-0001', 'jp-east', 'temperature', 'plant-jp')
VALUES (NOW + 0a, 25.3, 85.2, 'good');
INSERT INTO d_sensor_0002 USING sensor_data TAGS ('sensor-0002', 'jp-west', 'humidity', 'plant-jp')
VALUES (NOW + 0a, 62.1, 91.5, 'good');
-- 3. マルチデバイスバッチ書き込み
INSERT INTO
d_sensor_0001 VALUES (NOW + 1a, 25.5, 85.0, 'good') (NOW + 2a, 25.7, 84.8, 'good')
d_sensor_0002 VALUES (NOW + 1a, 62.3, 91.2, 'good') (NOW + 2a, 62.0, 91.0, 'warning');
-- 4. スーパーテーブル集計クエリ - リージョン別統計
SELECT
region,
sensor_type,
AVG(value) AS avg_value,
STDDEV(value) AS std_value,
COUNT(*) AS data_points
FROM sensor_data
WHERE ts >= NOW - 1h
INTERVAL(5m)
GROUP BY region, sensor_type;
-- 5. 最新値クエリ - LAST_ROW
SELECT
device_id,
LAST_ROW(value) AS latest_value,
LAST_ROW(ts) AS last_update
FROM sensor_data
GROUP BY device_id;
-- 6. ダウンサンプリング + ウィンドウ関数
SELECT
_wstart AS window_start,
_wend AS window_end,
device_id,
AVG(value) AS avg_value,
MIN(value) AS min_value,
MAX(value) AS max_value
FROM sensor_data
WHERE ts >= NOW - 7d AND sensor_type = 'temperature'
INTERVAL(1h) SLIDING(30m)
GROUP BY device_id;
# Python: TDengine書き込みクライアント
# 実行環境: Python 3.12+ / pip install taos-ws-py
import taosws
def write_iot_data_tdengine(host: str = "localhost", port: int = 6041):
"""TDengine 3.xにIoTデータを書き込み"""
conn = taosws.connect(f"ws://{host}:{port}/rest/sql", user="root", password="taosdata")
conn.execute("CREATE DATABASE IF NOT EXISTS iot_db KEEP 3650")
conn.execute("USE iot_db")
conn.execute("""
CREATE STABLE IF NOT EXISTS sensor_data (
ts TIMESTAMP,
value FLOAT,
battery FLOAT,
quality NCHAR(10)
) TAGS (
device_id NCHAR(20),
region NCHAR(20),
sensor_type NCHAR(20),
factory NCHAR(20)
)
""")
for i in range(100):
device_id = f"sensor-{i:04d}"
region = ["jp-east", "jp-west", "ap-northeast"][i % 3]
table_name = f"d_sensor_{i:04d}"
sql = f"""
INSERT INTO {table_name} USING sensor_data
TAGS ('{device_id}', '{region}', 'temperature', 'plant-{region.split('-')[0]}')
VALUES (NOW + {i}a, {25.0 + i * 0.1}, {80.0 + i * 0.2}, 'good')
"""
conn.execute(sql)
print("TDengineに100件のデータを正常に書き込みました")
result = conn.execute("SELECT COUNT(*) FROM sensor_data")
for row in result:
print(f"総データ量: {row[0]}")
conn.close()
if __name__ == "__main__":
write_iot_data_tdengine()
パターン5:本番レベル選定比較
# Python: 時系列データベース選定評価フレームワーク
# 実行環境: Python 3.12+ / 追加依存なし
from dataclasses import dataclass
@dataclass
class TSDBEvaluation:
"""時系列データベース選定評価モデル"""
name: str
write_throughput_per_sec: int
query_latency_p99_ms: float
compression_ratio: float
sql_compatibility: int
ecosystem_maturity: int
cluster_scalability: int
learning_curve: int
license_type: str
best_for: str
caution: str
def evaluate_tsdb_choices() -> list[TSDBEvaluation]:
"""3つの時系列データベースの評価結果を生成"""
return [
TSDBEvaluation(
name="QuestDB",
write_throughput_per_sec=1_500_000,
query_latency_p99_ms=15.0,
compression_ratio=8.5,
sql_compatibility=9,
ecosystem_maturity=6,
cluster_scalability=5,
learning_curve=9,
license_type="Apache 2.0",
best_for="SQLチーム、高頻度書き込み、金融/IoTリアルタイム分析",
caution="クラスタ版は商用ライセンス、エコシステムプラグインが少ない",
),
TSDBEvaluation(
name="InfluxDB",
write_throughput_per_sec=500_000,
query_latency_p99_ms=45.0,
compression_ratio=5.2,
sql_compatibility=5,
ecosystem_maturity=9,
cluster_scalability=7,
learning_curve=6,
license_type="MIT / 商用",
best_for="DevOps監視、Telegrafエコシステム、中小規模IoT",
caution="Fluxの学習曲線が急、3.x移行コストが高い、クラスタ版は有料",
),
TSDBEvaluation(
name="TDengine",
write_throughput_per_sec=2_000_000,
query_latency_p99_ms=8.0,
compression_ratio=12.0,
sql_compatibility=7,
ecosystem_maturity=7,
cluster_scalability=8,
learning_curve=7,
license_type="AGPL-3.0 / 商用",
best_for="超大規模IoT、コネクテッドカー、インダストリアルIoT",
caution="AGPLライセンス制限、コミュニティ版機能制限、SQL方言の違い",
),
]
def print_comparison_table(evaluations: list[TSDBEvaluation]):
"""選定比較表を出力"""
print(f"{'指標':<30} {'QuestDB':<20} {'InfluxDB':<20} {'TDengine':<20}")
print("-" * 90)
fields = [
("書き込みスループット(点/秒)", "write_throughput_per_sec", True),
("クエリP99レイテンシ(ms)", "query_latency_p99_ms", False),
("圧縮率", "compression_ratio", True),
("SQL互換性(1-10)", "sql_compatibility", True),
("エコシステム成熟度(1-10)", "ecosystem_maturity", True),
("クラスタ拡張性(1-10)", "cluster_scalability", True),
("学習曲線(1-10)", "learning_curve", True),
("ライセンス", "license_type", None),
]
for label, attr, _ in fields:
values = [str(getattr(e, attr)) for e in evaluations]
print(f"{label:<30} {values[0]:<20} {values[1]:<20} {values[2]:<20}")
print("\n最適シナリオ:")
for e in evaluations:
print(f" {e.name}: {e.best_for}")
print("\n注意事項:")
for e in evaluations:
print(f" {e.name}: {e.caution}")
if __name__ == "__main__":
evaluations = evaluate_tsdb_choices()
print_comparison_table(evaluations)
よくある落とし穴:5つの罠
罠1:高カーディナリティタグによるOOM
❌ 間違い:device_idをInfluxDBのTagとして使用
✅ 正解:高カーディナリティフィールドはFieldに、低カーディナリティ次元はTagに
InfluxDBのTagは転置インデックスを作成します。10万のdevice_id = 10万のSeries、メモリが即座に爆発。QuestDBのSYMBOL型とTDengineのスーパーテーブルがこの問題を根本的に解決します。
罠2:タイムゾーン無視による集計ずれ
-- ❌ 間違い:タイムゾーン未指定、クロスタイムゾーン集計がずれる
SAMPLE BY 1h
-- ✅ 正解:QuestDBでタイムゾーンアライメントを指定
SAMPLE BY 1h ALIGN TO CALENDAR WITH TIME ZONE 'Asia/Tokyo'
罠3:保持ポリシー未設定によるディスク満杯
-- InfluxDB: RPの設定が必須
CREATE RETENTION POLICY "7days" ON "sensor-bucket" DURATION 7d REPLICATION 1 DEFAULT
-- TDengine: データベース作成時にKEEPを指定
CREATE DATABASE iot_db KEEP 3650
罠4:バッチ書き込みサイズの不適切な設定
# ❌ 間違い:1件ずつ書き込み、ネットワークオーバーヘッドが巨大
for point in data_points:
write_api.write(bucket=BUCKET, record=point)
# ✅ 正解:バッチ書き込み、5000-10000件/バッチ推奨
BATCH_SIZE = 5000
for i in range(0, len(data_points), BATCH_SIZE):
batch = data_points[i:i + BATCH_SIZE]
write_api.write(bucket=BUCKET, record=batch)
罠5:TDengineスーパーテーブルとサブテーブルの混同
-- ❌ 間違い:スーパーテーブルに直接INSERT(サブテーブルなし)
INSERT INTO sensor_data VALUES (NOW, 25.0, 80.0, 'good');
-- ✅ 正解:サブテーブルから書き込み、スーパーテーブルのタグを自動継承
INSERT INTO d_sensor_0001 USING sensor_data TAGS (...)
VALUES (NOW, 25.0, 80.0, 'good');
エラートラブルシューティング表
| エラーメッセージ | データベース | 原因 | 解決策 |
|---|---|---|---|
too many series |
InfluxDB | 高カーディナリティTagがSeries制限を超過 | 高カーディナリティフィールドをFieldに移動、またはInfluxDB 3.xにアップグレード |
memory limit exceeded |
QuestDB | クエリ結果セットが大きすぎる | LIMITを追加、SAMPLE BYでダウンサンプリング |
Invalid column type |
QuestDB | SYMBOL列に長すぎる文字列を書き込み | SYMBOL長を64バイト以下に、またはSTRINGを使用 |
table does not exist |
TDengine | データベースがUSEされていない | 先にUSE iot_dbを実行 |
out of memory |
TDengine | 単一クエリのメモリ制限超過 | queryMemoryパラメータを調整、時間範囲を絞る |
connection refused :9009 |
QuestDB | ILPポートが有効化されていない | tcp.enabled=trueを設定 |
authorization failed |
InfluxDB | Tokenの権限不足 | Tokenのread/write権限を確認 |
database not found |
InfluxDB | Bucketが存在しない | Bucketを先に作成、またはスペルを確認 |
invalid timestamp |
QuestDB | タイムスタンプ精度の不一致 | ILPはデフォルトでナノ秒、タイムスタンプ単位を確認 |
duplicate table name |
TDengine | サブテーブル名の重複 | サブテーブル名がグローバルに一意であることを確認 |
高度な最適化:5つの本番レベルテクニック
テクニック1:ホット/コールドデータ階層ストレージ
-- QuestDB: パーティション + O3エンジンホット/コールド分離
ALTER TABLE sensor_data ALTER PARTITION LIST
SET ATTRIBUTE 'cold' WHERE timestamp < dateadd('d', -30, now());
-- TDengine: マルチレベルストレージ
ALTER DATABASE iot_db KEEP 30,365,3650;
-- 30日ホット / 365日ウォーム / 3650日コールド
テクニック2:事前集計マテリアライズドビュー
-- QuestDB: マテリアライズドビュー自動ダウンサンプリング
CREATE MATERIALIZED VIEW sensor_hourly AS (
SELECT
timestamp,
device_id,
avg(value) AS avg_value,
min(value) AS min_value,
max(value) AS max_value,
count() AS sample_count
FROM sensor_data
SAMPLE BY 1h ALIGN TO CALENDAR
) PARTITION BY DAY;
テクニック3:書き込みバッチ処理とバックプレッシャー制御
# Python: バックプレッシャー制御付きQuestDB書き込み
# 実行環境: Python 3.12+ / pip install questdb
import time
import threading
from queue import Queue, Full
from questdb.ingress import Sender, IngressError
class BatchingWriter:
"""バックプレッシャー制御付きバッチ書き込み器"""
def __init__(self, host: str = "localhost", port: int = 9009,
batch_size: int = 5000, max_queue: int = 100_000):
self.batch_size = batch_size
self.queue = Queue(maxsize=max_queue)
self.sender = Sender.from_conf(f"http::addr={host}:{port};")
self._running = True
self._worker = threading.Thread(target=self._flush_loop, daemon=True)
self._worker.start()
def write(self, table: str, symbols: dict, columns: dict):
"""非ブロッキング書き込み、キュー満杯時はドロップ"""
try:
self.queue.put_nowait((table, symbols, columns))
except Full:
print("警告: 書き込みキューが満杯、データポイントをドロップ")
def _flush_loop(self):
"""バックグラウンドバッチフラッシュ"""
batch = []
while self._running or not self.queue.empty():
try:
item = self.queue.get(timeout=1.0)
batch.append(item)
if len(batch) >= self.batch_size:
self._flush(batch)
batch = []
except Exception:
if batch:
self._flush(batch)
batch = []
def _flush(self, batch: list):
"""バッチ書き込みを実行"""
try:
for table, symbols, columns in batch:
self.sender.row(table, symbols=symbols, columns=columns,
at=Sender.current_timestamp_with_nanos())
self.sender.flush()
print(f"{len(batch)}件のデータをフラッシュ")
except IngressError as e:
print(f"書き込み失敗: {e}")
def close(self):
self._running = False
self._worker.join(timeout=10)
self.sender.close()
テクニック4:Grafanaビジュアライゼーション統合
# docker-compose.yml: QuestDB + Grafana統合デプロイ
# 実行環境: Docker Compose v2.30+
version: "3.8"
services:
questdb:
image: questdb/questdb:8.2
ports:
- "9000:9000"
- "9009:9009"
- "9003:9003"
volumes:
- questdb_data:/root/.questdb
environment:
QDB_MALLOC_SIZE: 4G
grafana:
image: grafana/grafana:11.4
ports:
- "3000:3000"
environment:
GF_INSTALL_PLUGINS: grafana-postgresql-datasource
volumes:
- grafana_data:/var/lib/grafana
depends_on:
- questdb
volumes:
questdb_data:
grafana_data:
テクニック5:マルチアクティブ書き込みと高可用性
# TDengine 3.xクラスタデプロイ
# 実行環境: TDengine 3.3+ / 3ノードクラスタ
cluster: 1
numOfMnodes: 3
mnodeEqualVnodeNum: 4
statusInterval: 1
maxTablesPerVnode: 1000
minRowsPerBlock: 100
maxRowsPerBlock: 4096
3つの時系列データベース比較分析
| 次元 | QuestDB | InfluxDB | TDengine |
|---|---|---|---|
| クエリ言語 | SQL(時系列拡張付き) | Flux / SQL(3.x) | SQL方言 |
| 書き込みプロトコル | ILP / REST / PostgreSQL wire | ILP / REST | REST / WebSocket / JNI |
| 単一ノード書き込み | 150万点/秒 | 50万点/秒 | 200万点/秒 |
| 圧縮率 | 8-10x | 4-6x | 10-15x |
| クラスタソリューション | エンタープライズ版 | オープンソースクラスタ(3.x) | オープンソースクラスタ |
| Grafana統合 | ✅ PostgreSQLデータソース | ✅ ネイティブプラグイン | ✅ ネイティブプラグイン |
| Kafka統合 | ✅ コミュニティ | ✅ ネイティブ | ✅ ネイティブ |
| 学習コスト | 低(SQLですぐ使える) | 中高(Flux構文が独特) | 中(スーパーテーブル概念) |
| 適用規模 | 小中→大 | 小中 | 中大→超大規模 |
まとめ
IoT時系列データベースの選定に銀の弾丸はありません。シナリオとの適合性が鍵です:
- QuestDB:SQLチームの第一選択、高頻度書き込み+リアルタイム分析、学習コスト最低、ただしクラスタ版は商用ライセンスが必要
- InfluxDB:DevOps監視エコシステムが最も成熟、Telegraf+Grafanaワンストップ、ただしFluxの学習曲線が急、3.x移行コストが高い
- TDengine:超大規模IoTのキラー、スーパーテーブル+圧縮率で圧倒的、ただしAGPLライセンスに注意
選定デシジョンツリー:SQLチーム→QuestDB / DevOpsエコシステム→InfluxDB / 超大規模IoT→TDengine。
オンラインツールおすすめ
- /ja/json/format - JSONフォーマッター、時系列データAPIレスポンスの処理
- /ja/dev/curl-to-code - cURL→コード変換、時系列DB API呼び出しの迅速生成
- /ja/encode/hash - ハッシュ計算、デバイスIDの重複排除と検証
- /ja/text/diff - テキスト差分、SQLクエリバージョンのdiff
ブラウザローカルツールを無料で試す →