時系列データベース選定: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つのペインポイント

  1. 書き込みスループットのボトルネック:100万デバイスの同時書き込み、従来のDBではIOPSが限界
  2. クエリ集計の非効率:時間窓集計、ダウンサンプリングクエリのパフォーマンスが悪く、P99レイテンシが急増
  3. ストレージコストの制御不能:時系列データは大量かつ増加し続け、ホット/コールド分離が困難
  4. マルチデバイス管理の複雑さ:千台のデバイスがそれぞれタグを持ち、Schema管理が混乱
  5. エコシステム統合の分断: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

ブラウザローカルツールを無料で試す →

#时序数据库#IoT数据#QuestDB#InfluxDB#TDengine#2026#数据库