QuestDB SYMBOL 类型 vs VARCHAR:性能基准测试与选型建议
SYMBOL 是 QuestDB 里最容易用错、也最容易被忽视的类型。用对了,一张高流量表能省一半磁盘、聚合快好几倍;用错了(拿来做每行都不同的"唯一键"),性能反而比 VARCHAR 还差。这篇直接把两种类型在同一批数据上跑一遍,拿数字说话。
一、两者的底层区别
VARCHAR | SYMBOL | |
|---|---|---|
| 存储 | 每行存一份字节串 | 全局字典存一份,每行存 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 ms | 118 ms | 快 ~3 倍 |
WHERE host='host_7' | 走字典哈希 | 逐行字符串比较 | 快 ~5 倍 |
这是 SYMBOL 最该上的场景:标签种类远小于行数。
场景 2:100 万行 / 10 万种 host(中基数)
把生成语句里的 i % 100 改成 i % 100000。结果:
| 指标 | SYMBOL 表 | VARCHAR 表 |
|---|---|---|
| 磁盘占用 | 约 26 MB | 约 38 MB |
GROUP BY host | 95 ms | 132 ms |
差距缩小到 1.4 倍左右。SYMBOL 仍占优,但字典本身已经开始吃掉一部分收益。
场景 3:100 万行 / 100 万种 host(高基数,每行几乎唯一)
i % 1000000,每个 host 值只出现一次。结果出现反转:
| 指标 | SYMBOL 表 | VARCHAR 表 |
|---|---|---|
| 磁盘占用 | 约 41 MB | 约 40 MB |
GROUP BY host | 178 ms | 140 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 落在哪个区间,就决定上面哪一档
六、两个容易踩的坑
- SYMBOL 不是"更小的 VARCHAR",语义不同。
SYMBOL的等值/分组走字典整数,所以它天生适合做GROUP BY的键;VARCHAR适合做"随便存一段文本"。拿SYMBOL存用户自由文本,是类型误用。 - 高基数 + 大量
WHERE过滤会让字典哈希探测变慢。如果你的过滤列基数极高且查询总带着它过滤,VARCHAR+ 普通扫描有时更可预测。先用EXPLAIN看两条路径各走什么,再拍板。
七、结论 + 下一步
- 默认把设备、机房、交易所、币种、机房 region 这类维度列都建成
SYMBOL,它们几乎总是低基数 - 把 事件 ID、URL、错误消息、自由文本 留在
VARCHAR - 拿不准就先跑
COUNT(DISTINCT x)看基数比,再对照上表
下一篇 PARTITION BY DAY / MONTH 分区策略:选完类型之后,决定数据"按什么切块落盘"才是真正影响长期存储与保留策略的一步。
本文基准在 QuestDB latest + Docker,100 万行实测。数值会随机型浮动,相对关系稳定。QuestDB 为其权利人商标,本站为独立技术社区,非官方。