某 沪深 300 私募 的 交易员 提单:『上 一 季 在 ticks 表 上 15 秒 出结果 的 1 分钟 VWAP-per-symbol 查询,今天 跑 了 11 分钟。Postgres 仓库 正在 维持 每秒 9 万 写入 来自 沪深 行情 网关,EXPLAIN 在 一条 仅 触 三日 数据 的 查询 上 报 1.8 亿 缓冲 读取』。L2 的 卫生 清单——ANALYZE、正确 索引、覆盖 复合——都 已 应用。约束 现在 在 存储 引擎。装着 L2 schema 与 索引 的 单 机 Postgres 处理 日终 bar 表 与 几 十 亿 历史 tick 都 还 happy;但 当 持续 tick 摄入 突破 每秒 几十万 行、或者 GROUP BY symbol, time_bucket(...) 查询 在 上百 亿 行 表 上 即便 索引 正确 也 不肯 毫秒 级 返回 时,它 就 开始 喘 了。时序 数据库 是 为 这种 场景 专门 设计 的 存储。
区分 TSDB 与 行存 RDBMS 的 三 条 存储 属性
TSDB 之 所以 赢 tick 聚合 负载,是 因为 三 条 存储 属性,按 这个 顺序:columnar storage、time partitioning、time-bucket aggregation。列式 存储 把 一 列 的 值 在 磁盘 上 连续 摆放;触 三 列 的 四十 列 表,列存 大约 读 行存 的 3/40 字节,并且 同 一 列 的 邻接 值(微秒 时间戳、聚簇 在 近期 价位 附近 的 价格)熵 低、压缩 比 好。时间 分区 把 表 物理 按 ts 范围 切 成 chunk——常见 是 每日 一 chunk 或 每周 一 chunk——于是 WHERE ts BETWEEN ... 只 扫 与 窗口 重叠 的 chunk,而 DROP CHUNKS OLDER THAN '90 days' 这类 保留策略 用 一次 元数据 操作 回收 旧 数据,不必 跑 上亿 行 DELETE + VACUUM。时间 分桶 聚合 把 把 时间 按 N 分钟 / N 秒 / N 微秒 分桶 暴露 为 一等 算子(time_bucket、SAMPLE BY、xbar),规划器 可以 直接 从 列存 满足,不 走 显式 Sort。
同 一条 VWAP 查询,四 种 写法
对 ticks(symbol, ts, price, size, side) 计算 昨日 交易 时段 每标的 1 分钟 VWAP。普通 Postgres + SQL(L1 / L2 基线):
SELECT symbol, date_trunc('minute', ts) AS bucket, sum(price * size) / nullif(sum(size), 0) AS vwap FROM ticks WHERE ts >= date_trunc('day', now() - INTERVAL '1 day') AND ts < date_trunc('day', now()) GROUP BY symbol, bucket ORDER BY bucket, symbol;
TimescaleDB,同样 的 SQL 加 time_bucket:
SELECT create_hypertable('ticks', 'ts', chunk_time_interval => INTERVAL '7 days');
SELECT symbol, time_bucket(INTERVAL '1 minute', ts) AS bucket, sum(price * size) / nullif(sum(size), 0) AS vwap FROM ticks WHERE ts >= now() - INTERVAL '1 day' GROUP BY symbol, bucket ORDER BY bucket, symbol;
QuestDB,一行 用 SAMPLE BY 加 原生 VWAP 函数:
SELECT symbol, vwap(price, size) AS vwap FROM ticks WHERE ts IN yesterday() SAMPLE BY 1m;
kdb+ / q,四 种 里 最 致密——每个 token 都 有 含义:
select vwap:size wavg price by symbol, 1 xbar ts.minute from ticks where date=.z.d-1
读 这条 q:vwap:size wavg price 是 加权 平均 聚合,输出 列 命名 为 vwap;by symbol, 1 xbar ts.minute 按 标的 与 ts.minute 投影 的 1 分钟 桶 分组;where date=.z.d-1 是 在 昨日 日期 目录 上 的 分区 谓词。
TimescaleDB:从 Postgres 出来 的 平缓 坡道
Postgres 扩展。一个 hypertable 是 一个 逻辑 父表,Timescale 在 底下 按 ts 自动 切 成 N 日 chunk。SQL 表面 就 是 普通 Postgres SQL——L1 与 L2 的 所有 东西 原 样 沿用。新 原语:SELECT create_hypertable('bars_1m', 'ts', chunk_time_interval => INTERVAL '7 days'); 一句 话 把 L2 事实 表 转 成 hypertable;time_bucket(INTERVAL '1 minute', ts) 作为 GROUP BY 表达式;CREATE MATERIALIZED VIEW bars_1m_5m_vwap WITH (timescaledb.continuous) AS SELECT symbol, time_bucket('5 minutes', ts) AS bucket, sum(close * volume) / nullif(sum(volume), 0) AS vwap FROM bars_1m GROUP BY symbol, bucket; 这类 连续 聚合 用于 增量 物化 视图;add_retention_policy('bars_1m', INTERVAL '5 years'); 配 保留 策略。卖点:迁移 就是 一句 CREATE EXTENSION timescaledb; 加 一句 SELECT create_hypertable(...);,L1 / L2 的 知识 全部 原 样 沿用。A 股 量化 团队 上 PostgreSQL / PolarDB-O 的 默认 升级 路径 是 TimescaleDB。
QuestDB:为 tick 摄入 而 生
专门 设计 的 列式 TSDB,讲 Postgres 线 协议,所以 psql 与 Python 的 psycopg 直接 能 用。SQL 表面 是 SQL 加 量化 倾斜 的 扩展:SAMPLE BY 1m FILL(LINEAR) 做 带 缺口 填补 的 时间 分桶,LATEST ON ts PARTITION BY symbol 做 每 标的 最新 值 查询,以及 设计器(yesterday()、today()、'2026-05-23' 这类 日期 字面 直接 当 一整 天 数据)。磁盘 布局 是 列式 加 按日 分区 加 显式 指定 时间戳 列;商品 硬件 单 节点 摄入 吞吐 触 每秒 几十万 行。四 个 里 试 用 门槛 最低——单 二进制 文件、零 配置、SAMPLE BY 语法 一行 就 写完。社区 翻译 的 中文 README 已 覆盖 关键 章节。
InfluxDB:另一套 心智 模型
写入 是 line-protocol 记录——measurement,tag1=v1,tag2=v2 field1=1.0,field2=2.5 timestamp_ns——读取 用 Flux(现代 InfluxDB 2.x / 3.x 查询 语言)或 InfluxQL(遗留 1.x 语言)。写时 schema 模型 区分 tag(带索引 的 字符串 维度)与 field(无 索引 的 数值)——专为 监控 与 IoT 数据 优化,那里 维度 是 有限 集合(主机、地域、传感器 类型)而 值 是 数值。在 中国 量化 圈 多 用 作 监控 系统 的 后端,作 一线 tick 仓库 较少;这里 点名 是 为 完整性,也 因为 tag-vs-field 模型 是 标准 的 『没 SQL 的 TSDB』 反例。
kdb+ / q:对冲 基金 旧主
全球 头部 对冲 基金 与 国内 头部 私募 / 少数 券商 自营 桌 做 tick 分析 的 经典 答案——点名 仅 抽象 描述,不 作 推荐。q 是 一种 从 APL / K 派生 的 向量 语言,第一 眼 看 上去 奇怪,但 致密 又 快。分区 表 在 磁盘 上 按 每日 一 目录 摆放——/db/2026.05.23/trade/、/db/2026.05.23/quote/、…,每个 日期 目录 内 每 列 一 文件。杀手 特性 是 aj(asof-join:对 trades 每行 找 ts ≤ trade.ts 的 同 标的 最近 一条 quote——标准 的 『把 成交 匹配 到 当时 的 顶档 行情』)与 wj(窗口 join:在 每笔 成交 ts 附近 的 窗口 内 聚合 行情 统计)。卖点:当 你的 负载 是 两 张 各 上百 亿 行 tick 表 之间 的 asof-join、答案 必须 一 秒 出 而 不 是 一 小时 出 时,kdb+ 就是 答案。代价 是 商业 license 费、q 语言 学习 曲线,以及 维护 一 个 厂商 专用 存储 带来 的 运维 习惯。
决策 规则,明 说
| TSDB | 查询 语言 | 磁盘 布局 | 主要 用 场 |
|---|---|---|---|
| TimescaleDB | Postgres SQL | Postgres rows + hypertable chunks by ts | gentle on-ramp from Postgres |
| QuestDB | Postgres-wire-compatible SQL with SAMPLE BY / LATEST ON | columnar with designated timestamp | high-throughput tick ingest |
| InfluxDB | Flux / InfluxQL (not SQL) | tags-and-fields columnar | metrics / IoT pipelines |
| kdb+ / q | q language with q-SQL | partitioned-on-disk per-date directories with one column file per column | asof-joins on multi-billion-row tick tables |
留 在 Postgres 上,直到 (a) 持续 摄入 突破 约 10 万 行/秒、写入 开始 积压,或 (b) EXPLAIN (ANALYZE, BUFFERS) 报 上 亿 缓冲 读取 与 分钟 级 墙钟 在 跨 多 十亿 行 tick 表 的 GROUP BY symbol, date_trunc('minute', ts) 查询 上 出现。这时 把 tick 表 迁 到 TimescaleDB(迁移 摩擦 最低)或 QuestDB(写入 吞吐 最高)。kdb+ 用 在 你 团队 已 经 在 付 license、并 且 asof-join 工作(把 成交 匹配 到 当时 顶档 行情)是 瓶颈 时。InfluxDB 仅当 『你的 生产 端 已经 在 用 line protocol』 已经 是 事实 时 选。ClickHouse 处 在 TimescaleDB 与 QuestDB 之间——列式 SQL 也 快、但 没 QuestDB 那种 专 为 tick 摄入 调过——本课 点名 不 讲。DuckDB 是 笔记本 上 Parquet 文件 分析 的 故事;团队 仓库 故事 是 上面 四 个 之一。一 条 提醒:大型 私募 与 部分 头部 券商 自营 维护 kdb+ 集群;公募 与 中小 私募 更 多 落 在 TimescaleDB 上。
操作 前指:TSDB ticker plant
TSDB 上 实时 tick 摄入 通常 由 ticker plant 前置:feed-handler 进程 消费 实时 行情(multicast、Kafka、ZeroMQ——3.6.4 消息 与 流 模块 教),把 行 追加 进 内存 中 的 实时 数据库(RDB),并 在 日 终 落 到 磁盘 分区 数据库。kdb+ tickerplant 是 标准 例子。这个 模式 这里 点名、不 实现——3.6.4 拥有 消息 故事。本课 停 在 『每 个 TSDB 怎么 存 和 查 数据』这 一层;『摄入 怎么 实时 喂』 留 给 下 一 模块。
纪律 总结
TSDB 是 专用 工具。中位 量化 负载 的 正确 答案 仍 是 普通 Postgres;上亿 行 tick 上 慢 VWAP 的 正确 答案 是 TimescaleDB 或 QuestDB;asof-join 主导 的 负载 答案 是 kdb+。挑 能 满足 负载 的 最小 存储;从 Postgres 迁 出去 的 不 可 逆 程度 足以 让 你 用 一份 写下来 的 决策 文档 做 一次。第 4 课 把 L1 + L2 + L3 缝 成 一个 可 跑 的 日终 管道。
本课构件清单。Inline-code:三 条 TSDB-vs-RDBMS 存储 属性(columnar storage、time partitioning、time-bucket aggregation);三 个 时间 分桶 原语(time_bucket、SAMPLE BY、xbar)。Fenced sql 代码 块:Postgres 基线 VWAP;TimescaleDB 变种 `create_hypertable` + `time_bucket`;QuestDB 变种 `SAMPLE BY 1m`。Fenced q 代码 块:kdb+ VWAP。Inline-code 表 把 四 个 参考 TSDB 映射 到 查询 语言 / 磁盘 布局 / 主要 用 场。一 个 Exercise。Two Hints。市场 数据 锚 是 A 股 沪深 300 的 '510050'、'510500'、'510300' 在 上证 / 深证 / CFFEX 受 监管 的 私募 / 量化 私募 体系,T+1 结算、涨跌停 制度、SSE / SZSE 流通 的 50ETF 与 300ETF。国内 主流 仓库 厂商 与 部署:PolarDB、TDSQL、OceanBase、GoldenDB、TiDB、StarRocks、Doris、TDengine、CnosDB、Tushare 行情 接口、WindPy、聚宽、米筐、JoinQuant、Choice 终端 是 国内 量化 团队 常见 的 行情 与 仓库 接入 锚。
练习
Exercise
你 的 Postgres 仓库 有 一张 ticks(symbol TEXT NOT NULL, ts TIMESTAMPTZ NOT NULL, price NUMERIC(18,6) NOT NULL, size BIGINT NOT NULL, side CHAR(1) NOT NULL) 表,已 长 到 ~3 billion rows,覆盖 200 symbols;每 日 1 分钟 VWAP 汇总 查询(SELECT symbol, date_trunc('minute', ts) AS bucket, sum(price * size) / nullif(sum(size), 0) FROM ticks WHERE ts >= now() - INTERVAL '1 day' GROUP BY symbol, bucket;)现在 要 8 minutes,EXPLAIN (ANALYZE, BUFFERS) 报 180M buffer reads。(a) 套 L2 卫生 清单(ANALYZE、复合 索引 (symbol, ts) INCLUDE (price, size)、ts 上 的 BRIN)后 重 跑 EXPLAIN;若 仍 > 60 seconds,(b) 用 两 句 话 说 为 什么 迁 到 TimescaleDB 或 QuestDB 是 下 一步、并 在 给定 工作量 100k rows/s sustained ingest + 1-minute time-bucket queries on the latest day 下 你 会 选 哪 一 个。(c) 写 出 TimescaleDB 版本:先 SELECT create_hypertable('ticks', 'ts', chunk_time_interval => INTERVAL '7 days');,再 用 time_bucket(INTERVAL '1 minute', ts) 写 同 一条 查询;并 写 出 等价 的 QuestDB 查询,用 SAMPLE BY 1m。(d) 用 一句话 说 何 时 你 反而 会 选 kdb+ / q。
提示
Seq Scan 应该 消失,但 几 十 亿 行 上 的 time_bucket 查询 正是 TSDB 为 之 设计 的 工作 量。提示
create_hypertable,SQL 完全 一样);QuestDB 在 持续 tick 摄入 吞吐 上 胜出。题目 工作 量 同时 提到 两 者——若 Postgres 工具 链 重要 挑 TimescaleDB,若 写入 吞吐 是 紧 约束 挑 QuestDB。