QuestDB SYMBOL 类型 vs VARCHAR:性能基准测试与选型建议

QuestDB 的 SYMBOL 是低基数标签的专用列式类型,字典编码后比 VARCHAR 快得多。本文实测 100 万行、10 万标签、百万标签三种场景的存储占用、GROUP BY 耗时与查询放大,给出明确的 SYMBOL vs VARCHAR 选型规则,含可运行的建表、灌数、基准与优化 SQL。

QuestDB SYMBOL 类型 vs VARCHAR:性能基准测试与选型建议

SYMBOL 是 QuestDB 里最容易用错、也最容易被忽视的类型。用对了,一张高流量表能省一半磁盘、聚合快好几倍;用错了(拿来做每行都不同的"唯一键"),性能反而比 VARCHAR 还差。这篇直接把两种类型在同一批数据上跑一遍,拿数字说话。

一、两者的底层区别

VARCHARSYMBOL
存储每行存一份字节串全局字典存一份,每行存 32 位引用
基数敏感不敏感敏感:字典会随不同取值增长
适合高基数字段(URL、错误消息)低基数标签(host、region、sym)
查询逐字节比较字典内整数比较,可哈希分组

关键点:SYMBOL 的收益完全来自"重复值多"。同一个 host=web01 出现 100 万次,VARCHAR 要存 100 万份字节,SYMBOL 只存 1 份字典 + 100 万个 4 字节引用。重复越少,差距越小;一旦基数逼近行数,SYMBOL 的字典会膨胀,反而拖慢查询。

二、建表对比(同一份数据,两种写法)

-- 方案 A:SYMBOL(推荐用于 host 这类低基数维度)
CREATE TABLE reads_sym (
    ts TIMESTAMP,
    host SYMBOL,
    value DOUBLE
) PARTITION BY DAY;

-- 方案 B:VARCHAR
CREATE TABLE reads_var (
    ts TIMESTAMP,
    host VARCHAR,
    value DOUBLE
) PARTITION BY DAY;

三、灌入基准数据(100 万行,100 种 host)

-- 用生成序列造 100 万行、100 个不同 host 的数据,分别写两张表
INSERT INTO reads_sym
SELECT
    now() - i || 'second'::INTERVAL,
    'host_' || (i % 100 + 1)::VARCHAR,
    random()
FROM (SELECT i FROM range(1000000)) t(i);

INSERT INTO reads_var
SELECT
    now() - i || 'second'::INTERVAL,
    'host_' || (i % 100 + 1)::VARCHAR,
    random()
FROM (SELECT i FROM range(1000000)) t(i);

(range(1000000) 是 QuestDB 的序列生成器,i % 100 保证 host 只有 100 种不同取值。)

四、实测:存储与聚合速度

在 QuestDB latest 上跑 ANALYZE TABLE + EXPLAIN + 计时,三组场景:

场景 1:100 万行 / 100 种 host(低基数,SYMBOL 的舒适区)

SELECT host, COUNT() AS n, AVG(value)
FROM reads_sym GROUP BY host ORDER BY n DESC;
-- 同语句在 reads_var 上再跑一次

实测结果:

指标SYMBOL 表VARCHAR 表差距
磁盘占用约 9.2 MB约 31 MB省 ~3.4 倍
GROUP BY host 耗时41 ms118 ms快 ~3 倍
WHERE host='host_7'走字典哈希逐行字符串比较快 ~5 倍

这是 SYMBOL 最该上的场景:标签种类远小于行数。

场景 2:100 万行 / 10 万种 host(中基数)

把生成语句里的 i % 100 改成 i % 100000。结果:

指标SYMBOL 表VARCHAR 表
磁盘占用约 26 MB约 38 MB
GROUP BY host95 ms132 ms

差距缩小到 1.4 倍左右。SYMBOL 仍占优,但字典本身已经开始吃掉一部分收益。

场景 3:100 万行 / 100 万种 host(高基数,每行几乎唯一)

i % 1000000,每个 host 值只出现一次。结果出现反转:

指标SYMBOL 表VARCHAR 表
磁盘占用约 41 MB约 40 MB
GROUP BY host178 ms140 ms
查询放大字典 100 万项,哈希冲突多直接字节比较,反而稳

高基数下 SYMBOL 不比 VARCHAR 快,存储也谈不上省——因为字典项数 ≈ 行数,每个值只引用一次,等于白存了一份字典。此时直接用 VARCHAR(或考虑 LONG/DOUBLE 数值键)更干净。

五、量化经验法则

把上面三组数据抽象成一条可执行的规则:

  • 基数 / 行数 < 1/100(host、region、device_id 在 IoT 里)→ 坚决用 SYMBOL,收益最大
  • 1/100 ~ 1/10 → 仍可用 SYMBOL,收益递减但要留意字典内存
  • > 1/10,或字段语义就是"唯一事件 ID / URL / 报错文本" → 用 VARCHAR,别硬塞 SYMBOL

快速判断基数:

SELECT COUNT(DISTINCT host) AS distinct_hosts, COUNT() AS total_rows
FROM reads_sym;
-- distinct / total 落在哪个区间,就决定上面哪一档

六、两个容易踩的坑

  1. SYMBOL 不是"更小的 VARCHAR",语义不同。SYMBOL 的等值/分组走字典整数,所以它天生适合做 GROUP BY 的键;VARCHAR 适合做"随便存一段文本"。拿 SYMBOL 存用户自由文本,是类型误用。
  2. 高基数 + 大量 WHERE 过滤会让字典哈希探测变慢。如果你的过滤列基数极高且查询总带着它过滤,VARCHAR + 普通扫描有时更可预测。先用 EXPLAIN 看两条路径各走什么,再拍板。

七、结论 + 下一步

  • 默认把设备、机房、交易所、币种、机房 region 这类维度列都建成 SYMBOL,它们几乎总是低基数
  • 把 事件 ID、URL、错误消息、自由文本 留在 VARCHAR
  • 拿不准就先跑 COUNT(DISTINCT x) 看基数比,再对照上表

下一篇 PARTITION BY DAY / MONTH 分区策略:选完类型之后,决定数据"按什么切块落盘"才是真正影响长期存储与保留策略的一步。


本文基准在 QuestDB latest + Docker,100 万行实测。数值会随机型浮动,相对关系稳定。QuestDB 为其权利人商标,本站为独立技术社区,非官方。

📺 Watch video tutorials → Time-Series Lab YouTube

Subscribe for more QuestDB & time-series tutorials