Article
第一章:QuestDB 概述与核心特性
1.1 什么是 QuestDB
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| QuestDB 定义 | QuestDB 是一个开源的高性能时序数据库(Time-Series Database),专为快速摄取和查询时间序列数据而设计,采用列式存储与向量化执行引擎。 | 虽支持 SQL,但并非传统 OLTP 数据库,不适用于高并发事务处理。 |
| 开源协议 | 基于 Apache 2.0 许可证开源,允许商业使用与修改。 | 社区版功能完整,企业版提供额外运维工具(如 Web Console 增强、监控集成等)。 |
| 核心语言与架构 | 使用 Java 和 C++ 编写,底层基于 SIMD(单指令多数据)优化和零拷贝内存管理。 | 高性能依赖现代 CPU 架构(如 AVX2),老旧硬件可能无法发挥全部性能。 |
| 主要接口 | 支持 InfluxDB Line Protocol (ILP)、PostgreSQL Wire Protocol、REST API、Web Console。 | 所有写入最终统一为内部表结构,不同协议仅是入口方式不同。 |
1.2 QuestDB 的核心优势与适用场景
| 优势/场景名称 | 说明 | 注意事项 |
|---|---|---|
| 极致写入性能 | 单节点可实现每秒数百万点的写入吞吐(如 400万+ points/sec),得益于无锁架构和列式追加写入。 | 写入性能高度依赖时间戳有序性;乱序写入会触发重排,影响性能。 |
| 低延迟查询 | 向量化执行引擎 + 列存 + 时间分区,使聚合查询响应在毫秒级。 | 查询复杂度仍受限于数据量和过滤条件;不支持复杂 JOIN。 |
| 简化数据模型 | 自动识别时间戳列,符号(Symbol)类型高效压缩高基数字符串(如设备ID、股票代码)。 | Symbol 类型需预先声明,不能动态变更;默认最大 distinct 值为 256(可配置)。 |
| 适用场景 - IoT | 适合海量传感器数据实时采集、存储与分析(如智能电表、车联网)。 | 需配合消息队列(如 Kafka)做缓冲,避免突发流量压垮服务。 |
| 适用场景 - 金融 | 适用于高频交易日志、行情 Tick 数据存储与回测。 | 不支持 ACID 事务,不可用于账户余额等强一致性场景。 |
| 适用场景 - DevOps 监控 | 可替代 Prometheus 远程存储,长期保存指标数据并支持 SQL 分析。 | 需通过 Telegraf 或自定义 exporter 推送数据。 |
1.3 QuestDB 与其他时序数据库对比(InfluxDB、TimescaleDB 等)
| 对比维度 | QuestDB | InfluxDB (OSS 2.x) | TimescaleDB |
|---|---|---|---|
| 存储模型 | 列式存储,原生时序优化 | 自研 TSM 引擎(列式) | 基于 PostgreSQL 的行存 + 自动分块(hypertable) |
| 查询语言 | 标准 SQL(扩展 LATEST BY、SAMPLE BY) | Flux(函数式)或 InfluxQL(类 SQL) | 完全兼容 PostgreSQL SQL |
| 写入协议 | ILP、PostgreSQL Wire、REST | Line Protocol、HTTP API | PostgreSQL 协议 |
| 性能特点 | 写入极快,查询低延迟 | 写入快,但 Flux 学习曲线陡峭 | 写入中等,复杂查询能力强(继承 PG 生态) |
| 高可用 | 社区版仅单机;企业版支持主从复制(实验性) | 2.x 版本无内置集群(需企业版或自建) | 支持流复制、Patroni 集群,成熟 HA 方案 |
| 扩展性 | 插件生态较弱,依赖外部系统集成 | 有丰富 Telegraf 插件生态 | 可直接使用 PostgreSQL 扩展(如 PostGIS、pg_cron) |
| 适用人群 | 追求极致性能、简单部署的开发者 | 已使用 TICK Stack 的用户 | 需要 SQL 兼容性与关系模型的团队 |
| 注意事项 | 不支持 UPDATE / DELETE(仅 TTL 删除) | 2.x 资源占用高,社区活跃度下降 | 写入性能低于专用 TSDB,需合理配置 chunk |
第二章:安装与部署
2.1 支持的平台与系统要求
| 项目 | 说明 | 注意事项 |
|---|---|---|
| 操作系统 | Linux(x86_64, aarch64)、macOS(Intel & Apple Silicon)、Windows(通过 WSL2 或 Docker) | 官方不提供原生 Windows 二进制包;生产环境推荐 Linux。 |
| CPU 架构 | x86_64(支持 AVX2 指令集)、ARM64(如 AWS Graviton) | 若 CPU 不支持 AVX2,需使用兼容模式(性能下降约 30%)。 |
| 内存要求 | 最低 2 GB RAM,推荐 4 GB 以上 | 内存用于缓存符号字典和 WAL 缓冲区;数据量大时建议 ≥8 GB。 |
| 磁盘空间 | 至少 1 GB 可用空间(不含数据) | 数据存储路径需高性能 SSD;HDD 会显著降低写入吞吐。 |
| Java 运行时 | 自带嵌入式 JRE(无需单独安装) | 不依赖系统 Java;但若自行编译源码则需 JDK 17+。 |
| 网络端口 | 默认使用 9000(HTTP/REST)、8812(PostgreSQL Wire)、9009(ILP TCP)、9003(Web Console) | 需确保防火墙开放对应端口;可修改配置文件自定义。 |
2.2 本地安装方式(Docker / Binary / Homebrew)
| 安装方式 | 操作细节 | 注意事项 |
|---|---|---|
| Docker 安装 | 执行命令:docker run -p 9000:9000 -p 8812:8812 -p 9009:9009 questdb/questdb | 首次运行会自动拉取镜像;数据默认存储在容器内(非持久化),建议挂载卷:-v "$(pwd)/qdb-data:/var/lib/questdb" |
| 二进制安装(Linux/macOS) | 1. 下载 tar.gz 包(https://questdb.io/download) 2. 解压: tar -xzf questdb-*.tar.gz3. 进入目录执行 ./questdb.sh start | 启动脚本位于 bin/ 目录;需赋予执行权限(chmod +x);适用于无 Docker 环境。 |
| Homebrew 安装(macOS) | 执行命令:brew install questdb,然后启动:questdb start | 仅支持 Intel 和 Apple Silicon;版本可能略滞后于官方发布。 |
| Windows 安装 | 推荐使用 Docker Desktop 或 WSL2 + 二进制包 | 不支持直接运行 .exe;WSL2 需启用 systemd 或手动启动脚本。 |
| 验证安装 | 访问 http://localhost:9000 或执行 curl http://localhost:9000/status | 若返回 JSON 格式的服务器状态信息,表示服务正常。 |
2.3 服务启动与基本配置
| 操作名称 | 操作细节 | 注意事项 |
|---|---|---|
| 启动服务 | 在二进制目录执行:./questdb.sh start,或 Docker 中已自动启动 | 启动后进程在后台运行;日志输出到 logs/ 目录。 |
| 停止服务 | 执行:./questdb.sh stop | 强制 kill 可能导致 WAL 未刷盘,建议优雅停止。 |
| 配置文件位置 | conf/server.conf(二进制包)或挂载卷中的 conf/ 目录(Docker) | 修改配置后需重启服务生效。 |
| 常用配置项 | http.bind.to=0.0.0.0:9000、pg.enabled=true、line.tcp.enabled=true、http.auth.enabled=false | 默认仅绑定 localhost;生产环境需修改 bind 地址并启用认证。 |
| 数据目录 | 默认为 rootDir/db/(相对路径)或通过 -d 参数指定 | 可在启动时指定:./questdb.sh -d /data/questdb start |
| 日志级别设置 | 在 conf/log.properties 中修改:io.questdb.level=INFO | 调试时可设为 DEBUG,但会显著增加 I/O。 |
2.4 Web 控制台与 REST API 访问
| 访问方式 | 用途 | 代码示例 / 操作说明 | 注意事项 |
|---|---|---|---|
| Web Console | 图形化 SQL 查询界面,支持结果可视化 | 浏览器访问 http://localhost:9000,默认无需登录(若未启用 auth) | 仅支持 SELECT 查询;不可执行 DDL/DML(如 CREATE、INSERT)。 |
| REST API - 查询 | 通过 HTTP 执行 SQL 查询 | curl -G --data-urlencode "query=SELECT * FROM trades LIMIT 5" http://localhost:9000/exec | 返回 JSON 格式;需 URL 编码查询语句;支持分页(count、cursor)。 |
| REST API - 导入 CSV | 批量导入结构化数据 | curl -F data=@trades.csv -F schema="timestamp, symbol, price" -F name=trades http://localhost:9000/import | CSV 首行为列名;时间戳列需为 ISO8601 或 Unix 微秒;表会自动创建。 |
| REST API - 状态检查 | 获取服务运行状态 | curl http://localhost:9000/status | 返回字段包括 version、uptime、memory usage 等。 |
| PostgreSQL Wire 协议 | 使用 psql 或任意 PG 客户端连接 | psql -h localhost -p 8812 -U admin -d qdb,密码为空(默认) | 仅支持有限 SQL 子集;不支持事务、存储过程等 PG 高级特性。 |
| ILP TCP 写入测试 | 快速测试写入 | `echo “trades,sym=ETH price=2500 1700000000000000” | nc localhost 9009` |
第三章:数据模型与表结构
3.1 表类型(普通表 vs 分区表)
| 表类型 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| 普通表(Non-partitioned Table) | 无时间分区,所有数据存储在一个目录中 | CREATE TABLE events (ts TIMESTAMP, msg STRING); | 适用于小规模或非时序数据;不支持 TTL 自动清理。 |
| 分区表(Partitioned Table) | 按时间戳列自动按天/月/年等粒度分区存储 | CREATE TABLE trades (ts TIMESTAMP, symbol SYMBOL, price DOUBLE) TIMESTAMP(ts) PARTITION BY DAY; | 必须指定 TIMESTAMP 列;PARTITION BY 支持 DAY、MONTH、YEAR;分区提升查询剪枝效率。 |
| 自动分区行为 | 写入时根据时间戳值自动归入对应分区目录 | — | 分区粒度一旦设定不可更改;需在 CREATE TABLE 时指定。 |
| 分区目录结构 | 按 /// 存储(如 trades/2024-01-01/) | — | 手动删除分区目录可快速清理历史数据(需先停止写入)。 |
| 性能影响 | 分区表在时间范围查询中显著减少 I/O | SELECT * FROM trades WHERE ts > '2024-01-01'; | 过细分区(如 HOUR)会增加文件句柄开销;推荐 DAY 或 MONTH。 |
3.2 时间戳列与分区策略
| 概念/操作 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| 时间戳列定义 | 必须使用 TIMESTAMP 类型,并通过 TIMESTAMP(ts) 指定为时间列 | CREATE TABLE logs (ts TIMESTAMP, level STRING) TIMESTAMP(ts); | 一张表只能有一个时间戳列;未显式指定则无法使用时间函数和分区。 |
| 时间戳单位 | 支持微秒(默认)、纳秒(需显式声明) | CREATE TABLE ticks (ts TIMESTAMP NANOS, px DOUBLE) TIMESTAMP(ts); | ILP 写入时时间戳单位为纳秒;REST/SQL 默认为微秒。 |
| 分区策略选项 | PARTITION BY 支持:NONE、HOUR、DAY、MONTH、YEAR | CREATE TABLE sensor (ts TIMESTAMP, val DOUBLE) TIMESTAMP(ts) PARTITION BY MONTH; | NONE 等价于普通表;HOUR 仅建议用于极高频写入场景。 |
| 分区对 TTL 的作用 | TTL(Time-To-Live)基于分区粒度自动删除过期数据 | ALTER TABLE sensor SET ttl = '7d'; | TTL 值必须是分区粒度的整数倍(如 DAY 分区不能设 12h TTL)。 |
| 乱序写入处理 | 允许有限乱序(由 cairo.o3.max.lag 控制) | — | 超出 O3 最大延迟窗口的乱序数据会被拒绝或触发重排,影响性能。 |
3.3 符号(Symbol)类型详解
| 概念/参数 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| Symbol 类型定义 | 高效存储低基数字符串(如设备ID、股票代码),内部映射为整数 | CREATE TABLE trades (ts TIMESTAMP, sym SYMBOL, price DOUBLE) TIMESTAMP(ts); | 不是普通字符串;不能直接 LIKE 或正则匹配(需 CAST(sym AS STRING))。 |
| 最大不同值限制 | 默认最多 256 个 distinct Symbol 值 | CREATE TABLE events (cat SYMBOL CAPACITY 1024) ... | 可通过 CAPACITY 指定(如 64, 128, 256, 512, …, 1073741824);必须是 2 的幂。 |
| 缓存行为 | Symbol 字典常驻内存,加速 JOIN 和 GROUP BY | SELECT sym, avg(price) FROM trades GROUP BY sym; | 高基数 Symbol(如用户ID)会导致内存爆炸,应改用 STRING。 |
| 写入性能优势 | 比 STRING 快 2–5 倍,磁盘占用减少 50%+ | — | 仅适用于已知枚举值或有限集合的字段。 |
| 与 ILP 兼容性 | ILP 中 tag 自动转为 Symbol | trades,sym=ETH price=2500 1700000000000000 | 若同一 tag 出现超 CAPACITY 个不同值,后续值将被丢弃或报错(取决于配置)。 |
3.4 列约束与数据类型支持
| 数据类型 | 说明 | 有效取值范围 / 格式 | 注意事项 |
|---|---|---|---|
| TIMESTAMP | 时间戳,微秒或纳秒精度 | '2024-01-01T12:00:00.000000Z' 或 Unix 微秒 | 必须指定一列为时间列才能启用时序特性。 |
| SYMBOL | 枚举式字符串(见 3.3) | 任意字符串(受 CAPACITY 限制) | 不支持 NULL;空字符串视为有效值。 |
| STRING | 可变长 UTF-8 字符串 | 最大长度约 2GB(理论) | 支持 NULL;适合高基数或全文内容。 |
| DOUBLE | 64 位浮点数 | IEEE 754 双精度 | 默认数值类型;支持 NaN、Infinity。 |
| FLOAT | 32 位浮点数 | IEEE 754 单精度 | 精度较低,节省空间;不推荐用于金融计算。 |
| LONG | 64 位有符号整数 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 适合计数器、ID 等。 |
| INT | 32 位有符号整数 | -2,147,483,648 到 2,147,483,647 | 节省内存;超出范围会溢出。 |
| SHORT | 16 位有符号整数 | -32,768 到 32,767 | 极少使用。 |
| BYTE | 8 位有符号整数 | -128 到 127 | 极少使用。 |
| BOOLEAN | 布尔值 | true / false | 存储为单字节。 |
| BINARY | 二进制数据(实验性) | — | 社区版支持有限;不推荐生产使用。 |
| 列约束 | QuestDB 不支持 PRIMARY KEY、FOREIGN KEY、NOT NULL(除 Symbol 外) | — | 所有列默认允许 NULL(Symbol 除外);无索引概念(靠分区和列存优化)。 |
| 自动类型推断 | REST / CSV 导入时自动推断类型 | — | 时间戳需符合 ISO8601 或 Unix 微秒;否则可能被识别为 STRING。 |
第四章:数据写入
4.1 使用 ILP(Influx Line Protocol)写入
| 方法/要素 | 语法 / 格式 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| ILP 基本格式 | <measurement>,<tag_key>=<tag_value> <field_key>=<field_value> <timestamp> | 高性能写入时序数据点 | trades,sym=ETH price=2500.5,qty=10i 1700000000000000 | 字段值默认为 DOUBLE;整数需加后缀 i;时间戳单位为纳秒。 |
| 时间戳省略 | 省略时间戳部分 | 使用服务器当前时间 | sensors,device=dev01 temp=23.5 | 仅适用于实时流写入;不推荐用于回填历史数据。 |
| 多字段写入 | 单行包含多个 field | 一次写入多个指标 | cpu,host=server01 usage_idle=90.0,usage_user=8.5,usage_system=1.5 1700000000000000000 | 字段间用逗号分隔,无空格;顺序无关。 |
| Tag 与 Symbol 映射 | 所有 tag 自动转为 SYMBOL 列 | 高效存储分类维度 | logs,level=error msg="disk full" → level 列为 SYMBOL | 同一 tag 超过 CAPACITY 限制将被拒绝或丢弃(见 3.3)。 |
| TCP 写入方式 | 通过端口 9009 发送纯文本 | 低延迟、高吞吐 | echo "trades,sym=BTC price=45000 1700000000000000" | nc localhost 9009 | 支持批量多行(每行一个点);连接可复用。 |
| HTTP 写入方式 | POST 到 /write,Content-Type: text/plain | 兼容 InfluxDB 客户端 | curl -i -X POST 'http://localhost:9000/write?precision=n' --data-raw 'trades,sym=ETH price=2500 1700000000000000000' | precision 参数:n=纳秒(默认),u=微秒,ms=毫秒等。 |
| 表自动创建 | 若表不存在,ILP 写入自动建表 | 快速原型开发 | — | 表结构由首次写入的 tag/field 推断;后续 schema 不可变。 |
4.2 使用 PostgreSQL Wire Protocol 写入
| 方法/要素 | 语法 / 格式 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 连接参数 | host=localhost, port=8812, database=qdb, user=admin | 使用标准 PG 客户端连接 QuestDB | psql -h localhost -p 8812 -U admin -d qdb | 密码默认为空;支持任意语言的 PostgreSQL 驱动(如 psycopg2、pgx)。 |
| CREATE TABLE | 标准 SQL DDL | 显式定义表结构(推荐) | CREATE TABLE ticks (ts TIMESTAMP, symbol SYMBOL, bid DOUBLE) TIMESTAMP(ts) PARTITION BY DAY; | 必须指定 TIMESTAMP 列才能启用时序特性。 |
| INSERT 语句 | 标准 SQL INSERT | 单条或批量插入 | INSERT INTO ticks VALUES (now(), 'BTC', 45000.0); INSERT INTO ticks SELECT * FROM another_table; | 支持 now() 函数;不支持 ON CONFLICT、RETURNING 等 PG 特性。 |
| 批量插入(COPY) | 使用 COPY FROM STDIN | 高吞吐批量导入 | COPY ticks FROM STDIN WITH (FORMAT CSV); 随后发送 CSV 数据流 | 需客户端支持;比逐条 INSERT 快 5–10 倍。 |
| 参数化查询 | 使用预编译语句 | 防止注入、提升性能 | PREPARE ins AS INSERT INTO ticks VALUES ($1, $2, $3); EXECUTE ins(now(), 'ETH', 2500.0); | 并非所有驱动都支持;建议使用绑定变量。 |
| 事务支持 | 不支持 BEGIN/COMMIT | — | — | 所有写入立即生效;无法回滚。 |
4.3 使用 REST API 批量导入 CSV/JSON
| 方法/要素 | 语法 / 格式 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| CSV 导入端点 | POST /import,multipart/form-data | 从文件批量导入结构化数据 | curl -F data=@trades.csv -F name=trades http://localhost:9000/import | CSV 首行为列名;时间戳需为 ISO8601 或 Unix 微秒。 |
| 指定 Schema | 通过 schema 参数显式定义列类型 | 避免类型推断错误 | curl -F data=@data.csv -F name=sensor -F schema="ts TIMESTAMP, dev SYMBOL, val DOUBLE" http://localhost:9000/import | 列顺序必须与 CSV 一致;类型名区分大小写。 |
| 自动建表 | 若表不存在,根据 CSV 或 schema 自动创建 | 快速导入 | — | 表一旦创建,后续导入必须兼容 schema。 |
| JSON 导入 | 不直接支持 JSON;需转为 CSV 或用 ILP | — | — | 官方推荐:JSON 数据先转换为 ILP 或 CSV 再导入。 |
| 导入响应 | 返回 JSON 格式结果 | 获取导入统计 | {"rowCount":1000,"errorCount":0} | errorCount > 0 表示部分行失败(如类型不匹配)。 |
| 大文件处理 | 支持 GB 级 CSV 流式解析 | 高效批量加载 | — | 内存占用低;但单次请求不宜超过 10GB(受 HTTP 超时限制)。 |
4.4 写入性能调优建议
| 调优项 | 操作细节 | 代码/配置示例 | 注意事项 |
|---|---|---|---|
| 保证时间戳有序 | 按时间升序写入 | — | 乱序写入触发 O3 重排,性能下降 50%+;若无法避免,增大 cairo.o3.max.lag。 |
| 使用 ILP TCP 而非 HTTP | 减少协议开销 | nc localhost 9009 < data.ilp | TCP 连接可复用;HTTP 每次请求有额外解析成本。 |
| 批量写入 | 单次发送多行(ILP)或多行 INSERT(PG) | ILP: 每次发送 5k–50k 行;PG: 使用 COPY 或批量 INSERT | 小包频繁写入会触发过多 I/O;建议每批 ≥1000 点。 |
| 调整 WAL 设置 | 控制写前日志刷盘频率 | 在 server.conf 中:wal.enabled=true、wal.sync.on.write=false | 关闭 sync 可提升吞吐,但宕机可能丢最近数据;生产环境建议保留 WAL。 |
| 分区粒度合理 | 避免过细分区 | PARTITION BY DAY(而非 HOUR) | 每个分区对应目录和文件句柄;HOUR 分区在高基数下易耗尽 fd。 |
| Symbol CAPACITY 预估 | 避免运行时扩容 | CREATE TABLE ... sym SYMBOL CAPACITY 4096 | Symbol 字典扩容需重建,影响写入;建议预留 2–5 倍余量。 |
| 磁盘与 CPU 优化 | 使用 NVMe SSD + AVX2 CPU | — | HDD 写入延迟高;无 AVX2 的 CPU 性能损失显著。 |
| 监控写入瓶颈 | 查看 /status 和日志 | curl http://localhost:9000/status | 关注 o3.partition.active、wal.queue.size 等指标。 |
第五章:数据查询
5.1 SQL 查询语法基础
| 语法要素 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| SELECT 基本查询 | 支持标准 SELECT 语句 | SELECT ts, symbol, price FROM trades LIMIT 10; | 所有列名区分大小写;默认按物理存储顺序返回(非时间序)。 |
| WHERE 过滤 | 支持等值、范围、IN、IS NULL 等 | SELECT * FROM logs WHERE level = 'error'; SELECT * FROM trades WHERE price > 1000; | 不支持正则或 LIKE(除非 CAST SYMBOL AS STRING);字符串需用单引号。 |
| ORDER BY | 可按任意列排序 | SELECT * FROM trades ORDER BY ts DESC LIMIT 5; | 排序会显著增加内存和延迟;建议配合 LIMIT 使用。 |
| LIMIT / OFFSET | 分页控制 | SELECT * FROM trades LIMIT 100 OFFSET 200; | OFFSET 性能差,不适用于深度分页;推荐基于时间戳游标分页。 |
| 列别名 | 支持 AS 重命名 | SELECT price AS px, ts FROM trades; | 别名可在 ORDER BY 中使用,但不能在 WHERE 中使用(SQL 标准行为)。 |
| 注释 | 支持 -- 和 /* */ | -- This is a comment /* multi-line */ | Web Console 和 REST API 均支持。 |
| 不支持特性 | 不支持子查询(部分场景除外)、窗口函数、CTE、HAVING(需用嵌套替代) | — | 复杂分析建议在应用层处理或导出到分析引擎。 |
5.2 时间范围过滤与时间函数
| 时间函数/操作 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 时间字面量 | ISO8601 或 RFC3339 格式 | 指定绝对时间 | WHERE ts > '2024-01-01T00:00:00.000000Z' | 必须带时区(Z 表示 UTC);微秒精度。 |
| now() 函数 | now() | 获取服务器当前时间 | WHERE ts > now() - 1h | 返回 TIMESTAMP 类型;常用于实时窗口查询。 |
| 时间间隔运算 | + / - + 时间单位 | 计算相对时间 | ts > now() - 5m ts < '2024-01-01' + 1d | 支持单位:s(秒)、m(分)、h(小时)、d(天)、w(周)、M(月)、y(年)。 |
| date_trunc() | date_trunc('unit', ts) | 截断时间到指定粒度 | SELECT date_trunc('hour', ts), avg(price) FROM trades GROUP BY 1; | unit 支持:second、minute、hour、day、week、month、year。 |
| to_timestamp() | to_timestamp(str, format) | 字符串转时间戳 | to_timestamp('2024-01-01 12:00', 'yyyy-MM-dd HH:mm') | format 遵循 Java SimpleDateFormat;慎用,性能较低。 |
| 时间分区剪枝 | 自动优化 | WHERE ts BETWEEN '2024-01-01' AND '2024-01-02' | 查询仅扫描相关分区;仅对分区表生效;WHERE 条件需直接作用于时间列。 |
5.3 聚合查询与 downsample
| 聚合方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 标准聚合函数 | COUNT, SUM, AVG, MIN, MAX | 基础统计 | SELECT symbol, avg(price) FROM trades GROUP BY symbol; | 支持所有数值类型;STRING/SYMBOL 仅支持 COUNT。 |
| SAMPLE BY | SAMPLE BY interval [FILL(...)] | 按固定时间窗口降采样 | SELECT ts, avg(price) FROM trades SAMPLE BY 1h FILL(linear); | 必须与时间列一起使用;是 QuestDB 特有语法(非标准 SQL)。 |
| FILL 策略 | FILL(prev), FILL(linear), FILL(null), FILL(constant) | 处理空窗口 | SAMPLE BY 1m FILL(prev) | prev = 前向填充;linear = 线性插值;constant 需指定值(如 FILL(0))。 |
| 分组与时间对齐 | GROUP BY + date_trunc | 手动降采样 | SELECT date_trunc('minute', ts), avg(price) FROM trades GROUP BY 1; | 不保证连续时间轴;空窗口不会输出(与 SAMPLE BY 不同)。 |
| 多维聚合 | GROUP BY col1, col2 | 多维度统计 | SELECT symbol, date_trunc('hour', ts), avg(price) FROM trades GROUP BY 1, 2; | Symbol 列聚合效率高(得益于字典编码)。 |
| 性能提示 | 避免 SELECT * 在聚合中 | — | 聚合时只选择必要列可减少内存和 I/O。 |
5.4 最新值查询(LATEST BY)
| 语法要素 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| LATEST BY 基本用法 | 获取每个分组的最新记录 | SELECT * FROM trades LATEST BY symbol; | 按指定列分组,返回每组时间戳最大的一行。 |
| 多列 LATEST BY | 支持多列组合分组 | SELECT * FROM events LATEST BY source, category; | 等价于”按 (source, category) 联合去重取最新”。 |
| 与 WHERE 结合 | 先过滤再取最新 | SELECT * FROM trades WHERE ts > now() - 1d LATEST BY symbol; | 过滤在 LATEST BY 之前执行,提升性能。 |
| 与时间范围结合 | 限定时间窗口内最新 | SELECT * FROM trades WHERE ts BETWEEN '2024-01-01' AND '2024-01-02' LATEST BY symbol; | 常用于每日快照场景。 |
| 性能优势 | 利用时间序和索引跳转 | — | 比 ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ts DESC) 快 10–100 倍(后者不支持)。 |
| 限制 | 不能与 ORDER BY、LIMIT 混用(除隐式外) | — | LATEST BY 自身已定义输出顺序;额外排序无效。 |
5.5 JOIN 与子查询支持情况
| 特性 | 支持情况 | 代码示例 | 注意事项 |
|---|---|---|---|
| INNER JOIN | ✅ 支持(有限) | SELECT t1.ts, t1.price, t2.name FROM trades t1 JOIN assets t2 ON t1.symbol = t2.symbol; | 仅支持等值 JOIN;右表必须是小表(广播 JOIN);大表 JOIN 性能极差。 |
| LEFT JOIN | ✅ 支持 | SELECT ... FROM trades LEFT JOIN meta ON trades.sym = meta.id; | 同上,右表需小;不支持 RIGHT/FULL OUTER JOIN。 |
| JOIN 条件限制 | 仅支持单列等值 | ON a.x = b.y | 不支持复合条件(如 a.x = b.y AND a.z = b.w)或表达式。 |
| 子查询(FROM) | ✅ 支持派生表 | SELECT * FROM (SELECT symbol, avg(price) p FROM trades GROUP BY symbol) t WHERE p > 1000; | 可用于封装聚合结果;但不能嵌套过深。 |
| 子查询(WHERE) | ❌ 不支持 IN/EXISTS 子查询 | — | 无法写 WHERE id IN (SELECT ...);需改用 JOIN 或应用层处理。 |
| 自连接 | ⚠️ 理论支持,但不推荐 | SELECT a.ts, b.ts FROM trades a JOIN trades b ON a.symbol = b.symbol WHERE a.ts < b.ts; | 极易导致 OOM;QuestDB 非为复杂关系查询设计。 |
| 替代方案建议 | 应用层关联或预聚合 | — | 对于复杂分析,建议导出到 DuckDB、Pandas 或 ClickHouse。 |
第六章:性能优化与运维
6.1 分区管理与 TTL 设置
| 操作/配置项 | 操作细节 | 代码示例 / 配置 | 注意事项 |
|---|---|---|---|
| 查看表分区 | 列出所有分区目录或通过系统表 | SELECT * FROM tables();(查看 partitionBy 字段)或直接检查 db/<table>/ 目录 | 分区信息不直接暴露为 SQL 表;需结合文件系统或元数据推断。 |
| 手动删除分区 | 删除对应分区目录(需停写) | rm -rf db/trades/2023-12-01/ | 必须先停止对该表的写入,否则可能损坏 WAL 或导致查询异常。 |
| 设置 TTL(Time-To-Live) | 自动删除过期分区 | ALTER TABLE trades SET ttl = '30d'; | TTL 值必须是分区粒度的整数倍(如 DAY 分区不能设 12h);最小单位为秒(s)、分(m)、小时(h)、天(d)。 |
| 查看 TTL 设置 | 查询表元数据 | SELECT name, ttl FROM tables() WHERE name = 'trades'; | 返回值为微秒(如 30d = 2592000000000 µs)。 |
| 禁用 TTL | 清除自动清理策略 | ALTER TABLE trades SET ttl = null; | 原有已设置的 TTL 规则立即失效。 |
| TTL 执行时机 | 后台定时任务扫描 | — | 默认每 10 分钟检查一次;可通过 cairo.check.partition.read.only.delay 调整。 |
| 分区对查询影响 | 查询自动跳过无关分区 | SELECT * FROM trades WHERE ts > '2024-01-01'; | 仅当 WHERE 条件直接作用于时间列时生效;函数包裹(如 date_trunc)可能失效。 |
6.2 WAL(Write-Ahead Log)机制与恢复
| 概念/操作 | 说明 | 配置/命令示例 | 注意事项 |
|---|---|---|---|
| WAL 作用 | 保证写入持久性,支持崩溃恢复 | 默认启用:wal.enabled=true(server.conf) | 所有 ILP 和 PG 写入先写 WAL,再异步刷入主存储。 |
| WAL 文件位置 | 存储在 <root>/wal/<table>/ 目录 | — | 每个表独立 WAL;按 segment 分片(默认 32MB)。 |
| 强制刷盘 | 控制 WAL 同步策略 | wal.sync.on.write=true(默认 false) | 设为 true 可防宕机丢数据,但显著降低吞吐(每次写触发 fsync)。 |
| WAL 回放(恢复) | 服务重启时自动应用未刷盘日志 | — | 启动日志显示 replaying WAL for table…;恢复期间表只读。 |
| 手动清理 WAL | 正常运行时自动归档/删除 | — | 不要手动删除 wal/ 目录;可能导致数据丢失或启动失败。 |
| WAL 状态监控 | 查看队列积压 | curl http://localhost:9000/status → wal.queue.size | 若持续增长,说明磁盘 I/O 瓶颈或 CPU 不足。 |
| 禁用 WAL | 提升写入性能(不推荐生产) | wal.enabled=false | 宕机可能丢失最近数秒至分钟级数据;仅用于测试或可容忍丢失场景。 |
6.3 内存与磁盘配置调优
| 配置项 | 用途 | 配置示例(server.conf) | 注意事项 |
|---|---|---|---|
| cairo.sql.map.page.size | 控制内存映射页大小 | cairo.sql.map.page.size=4194304(4MB) | 默认 2MB;增大可减少 page fault,但增加内存占用。 |
| cairo.o3.max.lag | 允许的最大乱序时间窗口 | cairo.o3.max.lag=120000000(120 秒,单位微秒) | 乱序写入超过此值将被拒绝;调大可容忍更多乱序,但增加内存缓冲。 |
| cairo.cache.column.indexes | 是否缓存列索引 | cairo.cache.column.indexes=true | 对 Symbol 列聚合有帮助;默认 true。 |
| cairo.cache.symbol.keys | 缓存 Symbol 字典键 | cairo.cache.symbol.keys=true | 减少重复解析开销;建议保持开启。 |
| 磁盘 I/O 优化 | 使用 SSD + ext4/xfs | — | 避免使用 NFS 或网络存储;QuestDB 重度依赖本地低延迟 I/O。 |
| 内存分配 | JVM 堆外内存为主 | 启动脚本中 -XX:MaxDirectMemorySize=8G | 主要内存消耗在堆外(列缓存、WAL buffer);JVM 堆可保持默认(1–2GB)。 |
| 文件描述符限制 | 高分区/高并发需调高 | ulimit -n 65536 | 每个分区、WAL segment、连接均消耗 fd;默认 1024 不足。 |
6.4 监控指标与日志分析
| 监控方式 | 说明 | 示例 / 路径 | 注意事项 |
|---|---|---|---|
| REST /status 接口 | 获取核心运行指标 | curl http://localhost:9000/status | 返回 JSON,含 version、uptime、memory、WAL queue、O3 active partitions 等。 |
| 日志文件位置 | 记录启动、错误、查询等 | logs/questdb.log | 默认 INFO 级别;调试时可改 log.properties 为 DEBUG。 |
| 慢查询日志 | 自动记录超时查询 | 需启用:pg.select.timeout=30000(毫秒) | 超时查询会中断并记录到日志;无专用 slowlog 文件。 |
| 关键日志关键词 | 用于告警或分析 | O3 rollback, WAL replay, out of memory, partition closed | 出现 O3 rollback 表示乱序严重;WAL replay 表示恢复过程。 |
| Prometheus 指标导出 | 通过第三方 exporter | 社区项目:questdb-prometheus-exporter | 官方暂未内置 /metrics 端点;需额外部署 exporter。 |
| Web Console 监控 | 实时执行 EXPLAIN | EXPLAIN SELECT * FROM trades WHERE ts > now() - 1h; | 显示查询计划,包括是否使用分区剪枝、全表扫描等。 |
| 系统资源监控 | 结合 top / iostat / vmstat | — | 关注 iowait(磁盘瓶颈)、CPU user%(向量化计算负载)、direct memory usage。 |
第七章:高可用与扩展
7.1 单机 vs 集群部署模式
| 部署模式 | 说明 | 适用场景 | 注意事项 |
|---|---|---|---|
| 单机模式(Standalone) | 默认部署方式,所有数据存储于单节点 | 开发测试、中小规模生产(如 ≤10 亿点/天) | 官方社区版仅支持此模式;简单、低运维成本、性能极致。 |
| 集群模式(QuestDB Enterprise) | 支持主从复制、读写分离(实验性) | 高可用要求场景(如金融、核心监控) | 仅企业版提供;需额外许可证;社区版无法启用。 |
| 无共享架构(Shared-Nothing) | 每个节点独立存储,无分布式协调 | — | QuestDB 不支持自动分片(sharding);无法水平扩展写入吞吐。 |
| 读扩展方案 | 应用层路由只读查询到副本 | 通过外部工具实现(如 pgpool-II) | 需手动同步数据(如 rsync + WAL replay),非原生支持。 |
| 写扩展限制 | 无法跨节点分片写入 | — | 所有写入必须指向单一主节点;高吞吐依赖单机垂直扩展(CPU/SSD)。 |
| 推荐架构 | 单机 + 备份 + 监控 | 绝大多数用户场景 | 通过定期快照(snapshot)和 WAL 归档实现灾难恢复。 |
7.2 数据复制与故障转移(当前限制说明)
| 功能项 | 支持情况 | 说明 | 注意事项 |
|---|---|---|---|
| 主从复制(Replication) | ⚠️ 企业版实验性支持 | 基于 WAL 日志异步复制到只读副本 | 社区版完全不支持;副本不可写;延迟取决于网络与负载。 |
| 自动故障转移(Failover) | ❌ 不支持 | 无内置 leader election 或 VIP 切换 | 需依赖外部工具(如 HAProxy + 自定义健康检查脚本)。 |
| 手动故障恢复 | ✅ 可行(社区版) | 1. 停止主节点;2. 将备份数据复制到新节点;3. 启动服务 | 恢复时间取决于数据量;WAL 可用于重放未刷盘数据。 |
| 数据一致性 | 最终一致(企业版) | 异步复制,可能丢失最近写入 | RPO > 0;不满足强一致性要求。 |
| 备份策略 | ✅ 支持物理快照 | cp -r db/ backup_$(date +%s)/(需停写或冻结文件系统) | 推荐使用 LVM snapshot 或 ZFS send/receive 实现在线备份。 |
| PITR(时间点恢复) | ⚠️ 有限支持 | 结合 WAL + 快照可恢复到最近 checkpoint | 无内置工具;需手动解析 WAL segment(高级操作,风险高)。 |
| 社区版高可用建议 | 应用层重试 + 多活写入队列 | 写入前经 Kafka/Pulsar 缓冲,主库宕机时切换消费者 | 数据仍写入单实例,但避免直接丢弃。 |
7.3 与 Kafka / Telegraf / Prometheus 集成
| 集成组件 | 集成方式 | 配置/代码示例 | 注意事项 |
|---|---|---|---|
| Kafka → QuestDB | 使用 Kafka Connect 或自定义消费者 | 1. 启用 ILP TCP(端口 9009);2. 编写消费者将 JSON 转为 ILP 并发送:"trades,sym=BTC price=45000 1700000000000000" | 推荐使用 QuestDB 官方 Kafka Connector(GitHub 社区项目);确保时间戳有序。 |
| Telegraf → QuestDB | 配置输出插件为 InfluxDB | 在 telegraf.conf 中:[[outputs.influxdb_v2]]、urls = ["http://questdb:9000"]、precision = "ns" | 必须使用 InfluxDB v2 输出插件并指向 QuestDB 的 /write 端点;precision 设为 ns。 |
| Prometheus → QuestDB | 作为远程存储(Remote Write) | 1. 启用 QuestDB ILP HTTP;2. 在 prometheus.yml 中:remote_write: - url: "http://questdb:9000/write?precision=ms" | Prometheus 时间戳单位为毫秒,需加 ?precision=ms;标签自动转为 Symbol。 |
| QuestDB → Grafana | 通过 PostgreSQL 数据源连接 | 在 Grafana 添加 PG 数据源:Host: questdb:8812、Database: qdb、User: admin | 支持 SQL 查询面板;不支持变量自动补全(因无 information_schema 完整支持)。 |
| 日志采集(Filebeat/Fluentd) | 转为 ILP 后写入 | 自定义 pipeline 将日志字段映射为 tag/field | 时间戳字段需提取为纳秒时间戳;避免高基数字段设为 Symbol。 |
| 批量回填历史数据 | 使用 REST /import + CSV | curl -F data=@history.csv -F name=metrics http://questdb:9000/import | 确保 CSV 时间戳列格式正确;大文件分批次导入防超时。 |
第八章:实战案例
8.1 IoT 设备数据采集与可视化
| 步骤/组件 | 操作细节 | 代码/配置示例 | 注意事项 |
|---|---|---|---|
| 表结构设计 | 按设备 ID 分组,存储传感器值 | CREATE TABLE sensors (ts TIMESTAMP, device_id SYMBOL CAPACITY 1024, metric SYMBOL, value DOUBLE) TIMESTAMP(ts) PARTITION BY DAY; | device_id 和 metric 使用 SYMBOL 提升压缩率与查询速度;避免将设备 ID 存为 STRING。 |
| 数据写入(边缘设备) | 通过 HTTP ILP 推送 | POST /write?precision=ms,Body: sensors,device_id=sensor_001,metric=temp value=23.5 1700000000000 | 时间戳单位为毫秒(因多数 IoT SDK 输出 ms);需加 ?precision=ms。 |
| 批量写入(网关聚合) | 边缘网关缓存后批量发送 | 每 5 秒拼接多行 ILP 通过 TCP 发往 9009 端口 | 减少连接开销;确保时间戳有序(按采集时间排序)。 |
| 实时查询最新状态 | 获取每个设备最新读数 | SELECT * FROM sensors LATEST BY device_id, metric; | 利用 LATEST BY 高效实现”设备状态快照”。 |
| 降采样趋势分析 | 按小时聚合平均值 | SELECT ts, device_id, avg(value) FROM sensors SAMPLE BY 1h FILL(prev) WHERE metric = 'temp'; | FILL(prev) 避免图表断点;适用于 Grafana 可视化。 |
| 可视化(Grafana) | 添加 PostgreSQL 数据源,写 SQL 面板 | SELECT ts as time, value FROM sensors WHERE device_id = '$device' AND metric = 'temp' ORDER BY ts; | Grafana 变量 $device 需手动定义;time 列必须别名为 time。 |
| 异常阈值告警 | 应用层轮询或规则引擎 | 查询:SELECT device_id, max(value) FROM sensors WHERE ts > now() - 5m GROUP BY device_id; | QuestDB 无内置告警;需结合 Prometheus Alertmanager 或自定义脚本。 |
8.2 金融行情 Tick 数据存储与回测
| 步骤/组件 | 操作细节 | 代码/配置示例 | 注意事项 |
|---|---|---|---|
| 表结构设计 | 存储逐笔成交(Tick) | CREATE TABLE ticks (ts TIMESTAMP NANOS, symbol SYMBOL, side SYMBOL, price DOUBLE, size LONG) TIMESTAMP(ts) PARTITION BY DAY; | 时间戳精度设为 NANOS(纳秒);symbol 和 side 用 SYMBOL 节省空间。 |
| 高频写入优化 | 使用 ILP TCP 直连 | 客户端持续发送:ticks,symbol=AAPL,side=BUY price=190.25,size=100i 1700000000000000000 | 单连接复用;每批 ≥1000 行;确保时间戳严格递增。 |
| VWAP 计算(成交量加权均价) | 按分钟聚合 | SELECT ts, sum(price * size) / sum(size) AS vwap FROM ticks SAMPLE BY 1m WHERE symbol = 'BTC'; | 利用 SAMPLE BY 保证时间轴连续;避免使用 GROUP BY + date_trunc(可能漏空窗)。 |
| 回测数据提取 | 导出指定时间段全量 Tick | COPY (SELECT * FROM ticks WHERE symbol = 'ETH' AND ts BETWEEN '2024-01-01' AND '2024-01-02') TO '/data/eth_20240101.csv'; | COPY TO 需启用(默认关闭);路径需在 server.conf 中配置 http.min.enabled=true 并授权。 |
| 最新买卖价(模拟 Level1) | 假设 side 区分买卖 | SELECT side, price FROM ticks LATEST BY symbol, side WHERE symbol = 'BTC'; | 实际 Level2 需订单簿重建,QuestDB 不直接支持;此为简化模型。 |
| 性能保障 | 分区 + SSD + AVX2 CPU | — | 日均 10 亿 Tick 需 ≥32GB RAM、NVMe SSD;避免乱序写入。 |
| 数据生命周期 | 设置 90 天 TTL | ALTER TABLE ticks SET ttl = '90d'; | 符合法规与成本控制;自动清理旧分区。 |
8.3 日志监控与异常检测
| 步骤/组件 | 操作细节 | 代码/配置示例 | 注意事项 |
|---|---|---|---|
| 表结构设计 | 存储结构化日志 | CREATE TABLE app_logs (ts TIMESTAMP, service SYMBOL, level SYMBOL, message STRING, trace_id STRING) TIMESTAMP(ts) PARTITION BY HOUR; | 按 HOUR 分区适应高吞吐日志;service/level 用 SYMBOL 加速过滤。 |
| 日志采集(Telegraf) | 配置 tail 输入 + InfluxDB 输出 | [[inputs.tail]] files = ["/var/log/app/*.log"] data_format = "json" [[outputs.influxdb_v2]] urls = ["http://questdb:9000"] | JSON 日志需含 ts、service、level 字段;Telegraf 自动转为 ILP。 |
| 错误日志统计 | 每分钟 error/warn 计数 | SELECT ts, count(*) FROM app_logs WHERE level IN ('error', 'warn') SAMPLE BY 1m FILL(0); | FILL(0) 确保无错误时显示 0,便于告警基线计算。 |
| 异常突增检测 | 应用层对比滑动窗口 | 查询最近 5 分钟 vs 前 5 分钟:SELECT count(*) FROM app_logs WHERE level='error' AND ts > now() - 5m; | QuestDB 无内置 anomaly detection;需外部服务(如 Python 脚本)调用 REST API 分析。 |
| 关联追踪 | 按 trace_id 查询全链路 | SELECT ts, service, message FROM app_logs WHERE trace_id = 'abc123' ORDER BY ts; | trace_id 为 STRING(高基数),不建议设为 SYMBOL。 |
| 日志保留策略 | 7 天 TTL | ALTER TABLE app_logs SET ttl = '7d'; | 日志数据量大,短 TTL 控制成本。 |
| 可视化(Grafana) | 构建服务健康仪表盘 | 面板 1:错误率(error / total);面板 2:各服务日志量热力图 | 使用变量 $service 动态过滤;避免 SELECT * 导致内存溢出。 |