TPC-DS 这套基准,跑通不难,跑出能拿去说服人的结果很难。我见过太多人拿着一张 Excel 表格说"分布式数据库比单机 MySQL 快 5 倍",细问下去发现两边数据规模不一样、索引建得不一样、一边跑了热缓存一边跑了冷缓存,甚至查询语句都不是同一份方言生成的。标题里提到的"tpc ds mysql"和"tpc-ds 测试 tidb 结果",本质上是把单机 MySQL 和分布式 TiDB 放在同一套 TPC-DS 负载下做横向对比,这件事本身很有价值——它能同时暴露单机数据库的 IO 天花板和分布式数据库的网络与调度开销,也能让你真正理解"什么时候该上分布式"这个绕不开的选型问题。
问题在于,TPC-DS 是 24 张表、99 条查询、7 张事实表加 17 张维度表组成的复杂分析型负载,它对数据生成、DDL 方言、统计信息、缓存状态、并发模型的敏感度极高。任何一个环节没对齐,最后的数字就是废的。所以这篇内容不讲空泛的"分布式更快",而是把这套测试从环境设计到结果解读的完整链路拆开:怎么定数据规模、怎么改 DDL、怎么导入、怎么生成 99 条 SQL、怎么统一计时口径、结果出来之后哪些数字能看哪些不能看。适合手里有机器、想自己做一轮靠谱对比的读者,也适合只是想知道"TPC-DS 结果该怎么读"的人。
1. 先弄明白 TPC-DS 到底在测什么
1.1 24 张表和 99 条查询背后的设计意图
TPC-DS 模拟的是一套零售企业的决策支持系统,数据模型里有三条销售渠道——门店、目录、网络,每条渠道都配了销售事实表和退货事实表,再加一张库存表,构成 7 张事实表。17 张维度表负责描述客户、商品、门店、时间、促销、地址这些分析维度。这个模型的关键特征是"雪花"结构而不是规整的星型,维度表之间还有层级关联,比如客户要经过 customer_address、customer_demographics、household_demographics 才能拼出完整画像,商品要经过 item 关联到 brand 和 category。这种设计直接导致查询里出现大量多表 join,而不是简单的单表聚合。
99 条查询覆盖的场景也很刻意:有只扫一张事实表的报表型查询,有跨三条渠道做对比的复杂关联,有带窗口函数和比率计算的排名查询,也有嵌套子查询堆了四五层的"地狱查询"。SF=100 的时候,store_sales 大约 2.88 亿行,catalog_sales 约 1.44 亿行,web_sales 约 7200 万行,inventory 大约一亿出头,而最大的维度表 customer 也就一千万行左右。这个数据分布意味着:事实表决定扫描成本,维度表决定 join 的选择顺序,两者叠加会出现极强的长尾效应。
提示:很多人只跑 99 条里的一部分,比如挑 20 条"有代表性"的。这样做省时间,但会丢掉最关键的结论。TPC-DS 的价值恰恰在于长尾——那些单条跑十几秒甚至几分钟的查询,才是区分两类架构的真正分水岭。
所以你在设计测试之前,先明确自己要看的是什么。如果是看"报表响应时间",那就该关注单查询延迟和 P95;如果是看"批量跑批能力",那要看 99 条串行跑完的总时长和吞吐;如果是看"并发支撑",那要走多流模型,同时跑 N 个查询流。这三种测法对硬件和参数的要求完全不同,混在一起谈就是耍流氓。
1.2 为什么单机 MySQL 和分布式 TiDB 放一起比容易失真
第一个失真点是数据规模。TPC-DS 的 SF 从 1 到 100000 都有,SF=1 的时候全部数据也就 1GB 左右,这么小的量级塞进 MySQL 的 buffer pool 里,所有查询几乎都是纯内存计算,分布式架构反而因为多了一层网络 RPC 而变慢。这时候得出的"MySQL 更快"没有任何工程意义,因为你真正上生产的时候数据量不可能是 1GB。反过来,如果你直接上 SF=1000(约 1TB),单机 MySQL 光是导入就要跑很久,查询里随便一条全表扫描都能把磁盘打满。
第二个失真点是索引和表结构。TPC-DS 官方给的是通用 DDL,没有业务索引,只有主键和少量外键声明。但实际做测试的时候,有人会给 MySQL 加一堆二级索引让查询变快,却忘了给 TiDB 加同样的索引,或者给 TiDB 加了索引但因为优化器选择不同根本没走。这种做法出来的对比毫无意义。
第三个失真点是缓存状态。MySQL 的 InnoDB buffer pool 是有状态的,第一次跑和第二次跑的差距可以有数倍。TiDB 的 TiKV block cache 和 coprocessor cache 同样有状态,而且 region 的分布还会随着访问模式变化。你要么统一跑冷数据(每次清 page cache),要么统一跑热数据(预热两轮取第三轮),绝不能一边冷一边热。
第四个失真点是执行引擎差异。MySQL 8.0 之后有了 hash join,但默认可能还是走 nested loop;TiDB 支持 index join、hash join、merge join,还能下推聚合和谓词到 TiKV 层,如果开了 TiFlash 还能走列存的 MPP 模式。这些开关的不同组合,结果能差出好几倍。所以对比报告里必须写清楚:什么版本、什么参数、有没有列存副本、优化器版本是什么。
2. 环境与参数设计:结果差异的八成在这一步决定
2.1 硬件、版本与部署形态怎么定
做横向对比,硬件对齐是第一原则。单机 MySQL 用的机器,CPU 核数、内存、磁盘类型要和 TiDB 集群的单节点保持同一档次,否则结论会被硬件差异污染。举一个我实际用过的配置形态:MySQL 侧一台 16 核 64GB 内存、NVMe SSD 的物理机;TiDB 侧三台同规格机器,分别部署 TiDB、TiKV、PD(小规模测试里混部也能接受,但要在报告里注明)。这样 TiDB 的总资源是 MySQL 的三倍,这是分布式架构的天然属性,你在解读结果时必须把这个因素考虑进去——否则"TiDB 快 3 倍"可能只是"资源多 3 倍"。
版本选择上,MySQL 建议 8.0.30 以上,主要是 hash join 和直方图统计信息的成熟度;TiDB 建议 7.x 系列,这个版本的优化器对复杂 join 的处理明显比 5.x 稳定,MPP 模式也成熟了。两边的字符集统一用 utf8mb4,排序规则统一用 utf8mb4_bin 或 utf8mb4_general_ci,不要一边用默认一边用自定义,否则字符串比较的开销会不一致。
存储层面有个容易被忽略的细节:TiKV 对磁盘的随机写非常敏感,如果用的是机械盘或者网络存储,Raft 日志写入会成为瓶颈,导入阶段和写多的场景会被严重拖慢。测 TPC-DS 虽然以读为主,但导入阶段的表现会直接影响你的总耗时统计,所以 NVMe 是基本要求。MySQL 侧同理,InnoDB 的 redo log 和 doublewrite 都需要低延迟磁盘。
2.2 数据规模的选择逻辑
选 SF 是个权衡题。我的经验是分三档看:
| 规模 | 数据量 | 适用场景 | 主要观察点 |
|---|---|---|---|
| SF=100 | 约 100GB | 快速验证、参数调优 | 优化器行为、SQL 改写是否正确 |
| SF=300 | 约 300GB | 中等规模对比 | 缓存命中率下降后的 IO 表现 |
| SF=1000 | 约 1TB | 严肃对比 | 分布式并行扫描的真实收益 |
SF=100 是最实用的起点。100GB 的数据量,MySQL 单机 64GB 内存装不下全部热数据,但能装下大部分维度表和一部分事实表,这样既能测出内存计算的能力,又能测出 IO 回落的拐点。TiDB 三节点加起来内存更多,但数据是按 region 分散的,单节点缓存命中率未必比 MySQL 高——这个对比本身很有意思。
如果你只想验证"分布式在什么规模下开始赢",SF=100 和 SF=1000 各跑一遍就够了。中间的 300 更多是用来观察曲线的连续性,时间成本高,除非你要写正式报告,否则可以跳过。
注意:不要用 SF=1 或 SF=10 做架构对比。那个量级下所有数据库都跑在内存里,测的是 CPU 单核性能和优化器的常数开销,跟"分布式 vs 单机"这个命题没关系。
2.3 关键参数对齐清单
这一块是最容易被跳过、也最容易导致结果不可比的。下面这张表是我每次做对比都会过一遍的清单:
| 类别 | MySQL 侧设置 | TiDB 侧设置 | 对齐理由 |
|---|---|---|---|
| 内存 | innodb_buffer_pool_size 设为物理内存 70%~75% | TiKV block cache 设为单节点内存 45% 左右 | 都是读缓存的主力,比例要合理 |
| 并发 | 单查询串行执行 | 单查询串行执行,关闭并行算子 | 避免并行度不同造成的不公平 |
| 统计信息 | ANALYZE TABLE 全表 | ANALYZE TABLE 全表,调大采样 | 优化器依赖统计信息选 join 顺序 |
| 索引 | 只保留主键,不建二级索引 | 同样只保留主键 | 保证扫描路径一致 |
| 字符集 | utf8mb4 | utf8mb4 | 避免比较开销差异 |
| 日志 | 关闭 general log 和 slow log 的采样开销 | 关闭 expensive query 日志的额外开销 | 减少测量干扰 |
关于 TiDB 的并行度,这里有个取舍。TiDB 默认会开启 coprocessor 的并发扫描,这个特性在分布式下是优势,但它让"单查询延迟"这个指标变得不可比——MySQL 是单线程扫,TiDB 是多线程扫,快是应该的。我的做法是分两组跑:一组关闭并行(tidb_distsql_scan_concurrency 设为 1,tidb_executor_concurrency 设为 1),测纯单线程能力;一组保持默认并行,测实际生产形态。两组数据都记录,报告里分开写。
3. 数据生成与导入的完整实操
3.1 用 dsdgen 生成 100GB 数据
TPC-DS 官方工具包里有 dsdgen(数据生成)和 dsqgen(查询生成)两个程序,源码需要在 Linux 下编译。拿到源码包之后:
cd tools make OS=LINUX编译完会在当前目录生成 dsdgen 和 dsqgen。生成 SF=100 的数据,注意文件会按表拆成 24 个 .dat 文件:
mkdir -p /data/tpcds/100g ./dsdgen -scale 100 \ -dir /data/tpcds/100g \ -TERMINATE N \ -FORCE Y-TERMINATE N的作用是让每行末尾带一个分隔符|,这是 TPC-DS 的标准格式,方便后续用 LOAD DATA 时把行尾的竖线当作行结束符处理。-FORCE Y是覆盖已有文件。生成 100GB 大概需要十几分钟到半小时,取决于磁盘写入速度,瓶颈主要在 store_sales 和 inventory 这两张大表上。
生成完之后建议立刻做两件事:一是统计每个文件的行数,作为后续导入校验的基准;二是把数据放在和数据库不同的磁盘上,避免导入时读和写抢同一个设备的 IO。
wc -l /data/tpcds/100g/*.dat > /data/tpcds/100g/linecount.txt提示:如果你打算用 TiDB Lightning 的 local backend 导入,数据文件需要能被 Lightning 所在节点访问。分布式部署时要么把数据放在共享存储,要么在每个 TiKV 节点都放一份,然后用 Lightning 的分布式导入模式。
3.2 建表 DDL 的方言改造
官方给的 DDL 是通用 ANSI 风格,直接拿到 MySQL 或 TiDB 上跑通常会报错。常见问题有这么几类:with default子句、not null后面跟的默认值写法、decimal精度声明、日期类型。改造思路是把它简化成 MySQL 兼容的语法,因为 TiDB 本身高度兼容 MySQL 协议和语法,改一份两边都能用。
具体改动包括:去掉所有with default,去掉外键约束(TPC-DS 的外键只是声明,实际数据里不一定严格满足,加上反而会让导入变慢),把char(n)统一成varchar(n),日期字段统一用date,金额字段保持decimal(7,2)到decimal(15,2)不等。主键保留,因为 TPC-DS 的查询里很少用到主键过滤,保留主键主要是为了 TiDB 能有聚簇索引的分布依据。
TiDB 这边还有一个额外考虑:聚簇索引还是非聚簇索引。对于 store_sales 这种有复合主键的大表,用聚簇索引可以让主键相关的范围扫描更快,但如果查询模式是随机点查,聚簇索引会导致热点。TPC-DS 以扫描为主,聚簇索引是更合适的选择,建表时不加NONCLUSTERED关键字即可走默认的聚簇模式。
两边建表完成后,务必核对表数量和字段数量:
SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'tpcds';24 张表,一个不多一个不少。
3.3 MySQL 侧导入:LOAD DATA 的细节坑
MySQL 导入 .dat 文件最快的办法是 LOAD DATA INFILE,但 TPC-DS 的文件格式有两个坑。第一,行尾多一个|,需要用LINES TERMINATED BY '|\n'来处理;第二,空字符串和 NULL 在文件里都表现为连续两个竖线,无法区分。对于性能测试来说,把空值统一当空字符串导入是可接受的,只要两边处理方式一致就行。
LOAD DATA INFILE '/data/tpcds/100g/store_sales.dat' INTO TABLE store_sales FIELDS TERMINATED BY '|' LINES TERMINATED BY '|\n';导入前必须确认secure_file_priv和local_infile两个参数,前者限制 LOAD DATA 能读的目录,后者控制是否允许客户端本地文件导入。为了速度,导入期间可以临时把innodb_flush_log_at_trx_commit设为 2,sync_binlog设为 0,导入完再改回来。这两个参数对导入速度的影响是数量级的,从每小时几十 GB 提升到几百 GB 很常见。
100GB 数据在单机 MySQL 上导入,用上面这套优化,大概一小时左右能完成,前提是磁盘不是瓶颈。如果超过两小时,先去看是不是 redo log 太小导致频繁 checkpoint,或者是不是 autocommit 逐行提交导致事务开销过大——LOAD DATA 本身是批量提交的,但如果表上有触发器或者唯一索引冲突,会退化成逐行处理。
3.4 TiDB 侧导入:为什么必须用 Lightning
TiDB 用普通的 INSERT 或者 LOAD DATA 导入 100GB 数据会非常慢,因为每一行都要走一次 Raft 复制,网络往返和日志写入的开销叠加起来,可能要跑一整天。正确做法是用 TiDB Lightning,尤其是 local backend 模式,它会把数据先在本地排序成 SST 文件,然后直接 ingest 到 TiKV,绕过 SQL 层和 Raft 复制,速度能提升一个数量级以上。
配置文件大概长这样:
[lightning] level = "info" file = "/data/tidb-lightning.log" [tikv-importer] backend = "local" sorted-kv-dir = "/data/nvme/import" [mydumper]>SELECT COUNT(*) FROM store_sales;如果不一致,大概率是 CSV 解析配置有问题,比如某个字段里出现了分隔符,或者 terminator 没匹配上。TiDB Lightning 的日志里会有详细的错误行号和字段内容,照着改就行。
4. 查询生成与执行:从 dsqgen 到计时脚本
4.1 生成 99 条查询并做方言修正
TPC-DS 的查询是用模板加上参数替换生成的,dsqgen 支持多种方言,包括 db2、oracle、sqlserver、netezza 和 ansi。MySQL 和 TiDB 最接近的是 netezza 和 ansi,但还是需要后处理。
./dsqgen -DIRECTORY ../query_templates \ -INPUT ../query_templates/templates.lst \ -VERBOSE Y \ -QUALIFY Y \ -SCALE 100 \ -DIALECT netezza \ -OUTPUT_DIR /data/tpcds/query生成之后会得到 query1.sql 到 query99.sql,但其中有几条在 MySQL 上会报语法错。典型问题包括:substring(x from 1 for 4)这种 ANSI 写法 MySQL 不认,要改成substr(x,1,4);fetch first n rows only要改成limit n;少数查询里的cast(... as decimal)精度声明不一致会报错。批量替换可以用 sed:
sed -i 's/substring(\([^ ]*\) from \([0-9]*\) for \([0-9]*\))/substr(\1,\2,\3)/g' query*.sql sed -i 's/fetch first \([0-9]*\) rows only/limit \1/g' query*.sql注意:替换完一定要逐条试跑一遍,确认没有语法错误再开始正式计时。带报错的查询如果被计入耗时统计,会让整体结果偏低,看起来很美好,实际上是无效数据。
TiDB 对 netezza 方言的兼容性比 MySQL 好一些,因为它的语法解析器本身就是从 MySQL 扩展来的,同时又支持不少标准 SQL 特性。但为了两边可比,我建议用同一份修正后的 SQL 在两边跑,而不是各用各的方言——这样才能保证测的是数据库能力,不是方言适配能力。
4.2 统计信息与预热策略
导入完成后第一件事是收集统计信息。MySQL 用ANALYZE TABLE,TiDB 也是ANALYZE TABLE,但 TiDB 的采样率和自动分析策略需要额外调整。对于大表,TiDB 默认的采样量可能不足以让优化器准确估算选择率,可以把tidb_analyze_version设为 2,并对大表手动指定采样:
SET @@tidb_analyze_version = 2; ANALYZE TABLE store_sales WITH 50000 BUCKETS;不过桶数设太大也会拖慢分析时间,一般 256 到 1024 就够了,50000 这个量级是给特别大的表用的,实际测试里没必要。
预热策略有两种。热数据跑法是在正式计时前把 99 条查询完整跑两遍,第三遍才开始记录;冷数据跑法是在每一条查询前清空 page cache:
echo 3 > /proc/sys/vm/drop_caches后者在分布式环境里很难做彻底,因为 TiKV 的 block cache 是进程内的,清 page cache 只能清掉操作系统层面的缓存。所以更推荐的做法是"统一热数据 + 记录首次加载时间":热数据结果反映稳定状态下的能力,首次加载时间单独记录,反映冷启动成本。两种结果都有意义,但不要混在一起取平均。
4.3 计时口径统一
计时这件事,细节决定可信度。最外层用客户端 wall time 测网络加执行的总时间,这个最直观但包含了网络往返和结果集传输;服务端耗时则能剔掉网络因素。我的做法是两个都记,用客户端时间做对比基准,用服务端时间做归因分析。
统一脚本跑 99 条查询的大致思路:
#!/bin/bash OUT=/data/tpcds/result mkdir -p $OUT for i in $(seq 1 99); do start=$(date +%s.%N) mysql -h127.0.0.1 -P4000 -uroot tpcds < /data/tpcds/query/query$i.sql > /dev/null 2>&1 end=$(date +%s.%N) echo "query$i $(echo "$end - $start" | bc)" >> $OUT/timing.txt done这个脚本很粗糙,但够用。要注意的是每次连接都会引入额外的握手开销,如果查询本身只有几十毫秒,这个开销会显著拉偏结果。更严谨的做法是一个连接里跑完 99 条,用 SQL 文件拼接的方式执行,或者在客户端里用程序控制连接复用。
提示:把每条查询的结果行数也记录下来。如果某条查询在一边返回 1000 行、另一边返回 0 行,说明真的有问题,去查 WHERE 条件里的日期边界或者 NULL 处理,别急着相信那个"快 10 倍"的数字。
5. 结果怎么读:把数字翻译成结论
5.1 单查询耗时分布和长尾
99 条查询跑完,最常见的结果形态是:大部分查询在几百毫秒到两秒之间,少数几条超过十秒甚至一分钟,最慢的那条通常占了总耗时的很大比例。这种分布下,平均值几乎没用,你要看的是中位数、P90、P99 和总耗时。
在我见过的多次不同规模的对比里,SF=100 量级下,单机 MySQL 在很多轻量查询上并不输,甚至更快,因为它的执行路径短、没有网络 RPC、没有分布式事务开销。但一旦碰到需要扫描大事实表并做多层聚合的查询,TiDB 的并行扫描就开始体现优势,尤其是三节点同时读,理论上扫描带宽是单机的两倍以上。真正拉开差距的是那几条涉及多个事实表 join 的复杂查询,TiDB 可以把 join 的一侧下推到 TiKV 并行处理,MySQL 只能在单进程里做 hash join。
下面这张表是个示意性的归因,具体数值会随硬件和参数浮动,重点看"为什么"那一列:
| 查询类型 | 相对表现 | 主要原因 |
|---|---|---|
| 单事实表 + 维度表 join | 两者接近,MySQL 略快 | 执行路径短,无 RPC 开销 |
| 大事实表全扫描聚合 | TiDB 明显快 | 多节点并行扫描,聚合下推 |
| 多事实表交叉 join | TiDB 明显快 | 分布式 join,内存池更大 |
| 子查询嵌套极深 | 两者都慢,差异大 | 优化器能力差异,TiDB 更能重写 |
| 带窗口函数的排名查询 | 取决于数据量 | 内存排序成本和溢写策略不同 |
5.2 冷热缓存和并发流的影响
冷热差异能有多大?在一张大表上,冷缓存第一次访问要走磁盘,热缓存直接命中内存,单条查询差距可以是十倍以上。所以任何没有说明缓存状态的对比结果,都要打问号。
并发流的影响更微妙。TPC-DS 的官方测试模型里有一个 throughput test,同时跑多个查询流,看单位时间完成多少个查询。这个指标和单查询延迟经常是矛盾的:单机 MySQL 在低并发下延迟低,但并发一上来,CPU 和 IO 很快饱和,吞吐增长停滞甚至下降;TiDB 单条查询可能慢一点,但增加并发后能靠更多节点分摊,吞吐曲线更平滑。如果你关心的是"多人同时用"的场景,那吞吐比延迟更重要。
我的建议是至少跑三档并发:1、4、8,记录每档的吞吐(每秒完成查询数)和 P95 延迟。这三个点连成的线,比任何单点数据都更能说明问题。
5.3 典型查询的逐条归因
想真正理解结果,别只看总分,挑几条有代表性的查询单独分析。比如那些扫描 store_sales 和 catalog_sales 做对比的查询,可以看执行计划里扫描的行数、下推的谓词比例、聚合发生在哪一层。TiDB 用EXPLAIN ANALYZE能看到每个算子的实际耗时和行数,MySQL 用EXPLAIN ANALYZE也能看到类似的树形结构,两边对照着看,差异的来源就很清楚了。
常见的归因结论有这几类:一是扫描量差异,TiDB 的 coprocessor 把过滤条件下推到 TiKV,实际传输的数据量远小于逻辑扫描量;二是 join 策略差异,MySQL 可能选了 nested loop 而 TiDB 选了 hash join;三是统计信息差异导致的行数估算偏差,这个会直接反映在执行计划的选择上。把这几类原因标到具体查询上,你的报告才不是一堆数字的堆砌。
6. 常见问题与排查技巧实录
6.1 导入阶段的典型故障
导入慢是最常见的问题。MySQL 这边先看三个参数:innodb_flush_log_at_trx_commit、sync_binlog、innodb_buffer_pool_size。前两个控制日志写入频率,导入期间放宽能快好几倍;第三个决定脏页能缓存多少,太小会频繁刷盘。如果这些都调了还慢,去看是不是表上建了太多二级索引,每插一行都要维护一棵 B+ 树,索引越多导入越慢。
TiDB 这边导入失败的典型原因是 sorted-kv-dir 空间不够。这个目录的峰值占用可能是原始数据的两倍多,100GB 的数据要预留 250GB 以上空间。另一个常见原因是 region 分裂和调度没跟上,导入完成后如果立刻跑查询,会发现很多请求打到一个 region 上形成热点,等几分钟让 PD 调度完成就好了。
6.2 执行阶段的问题排查
查询报错集中在方言和类型转换上。日期比较是最容易翻车的地方,TPC-DS 的查询里大量使用date_dim的日期范围过滤,如果数据导入时日期格式没对上,过滤条件会静默失效,查询返回 0 行但你不会注意到。排查办法是在查询前后加计数校验,或者直接对比两边的结果行数。
执行计划不稳定是另一个大坑。TiDB 在统计信息刚收集完的短时间内,plan cache 可能命中旧计划,导致同一条 SQL 时快时慢。可以在测试前执行SET @@tidb_enable_prepared_plan_cache = 0关掉计划缓存,保证每次都重新生成计划,代价是多了编译时间,但一致性更好。MySQL 的 plan cache 概念不一样,主要是 prepared statement 的缓存,普通查询每次都要解析,影响不大。
6.3 结果不一致的排查顺序
如果两边跑同一条查询结果行数不一样,按这个顺序查:
| 排查顺序 | 检查项 | 常见原因 |
|---|---|---|
| 1 | 数据量是否一致 | 导入时某张表少导或重复导 |
| 2 | 日期边界处理 | 时区、日期格式、边界包含关系 |
| 3 | NULL 值语义 | 空字符串被当成 NULL 或反之 |
| 4 | 数值精度截断 | decimal 精度声明不同导致舍入差异 |
| 5 | 排序和 limit 组合 | 没有确定性排序时 limit 结果随机 |
| 6 | 关联条件类型隐式转换 | 字符串和数字比较的隐式转换规则不同 |
这张表里最隐蔽的是第 5 项。TPC-DS 有些查询带order by ... limit N,如果排序键有重复值,两边的返回顺序可能不同,行数一样但内容对不上。这种情况不算错误,但如果你拿来做结果校验就会误判。
7. 几个容易被忽略但影响很大的细节
第一个细节是客户端的开销。用命令行客户端跑查询,结果集要序列化成文本再输出,如果查询返回几十万行,序列化时间可能比查询本身还长。测性能时应该把结果集重定向到 /dev/null,或者用-e加--quick参数减少缓冲,再或者在程序里用游标逐行读但不做任何处理。这一项没做好,轻量查询的耗时会被整体抬高,掩盖真实差异。
第二个细节是 TiDB 的 region 数量。小规模测试时如果 region 数量太少,并行扫描的粒度不够,多节点优势发挥不出来。SF=100 的数据导入后 region 数量应该能达到几千个,如果只有几百个,说明 region 切分没做够,可以手动 split 或者调小 region 大小再导一遍。这个细节在对比报告里经常被忽略,但它直接影响 TiDB 的扫描并行度。
第三个细节是操作系统层面的 CPU 调度。TiKV 是 CPU 密集型的,NUMA 架构下如果线程和内存跨了节点,访问延迟会明显增加。绑定 CPU 亲和性虽然麻烦,但在做精确对比时值得做一遍。MySQL 侧同理,InnoDB 的后台线程和查询线程如果抢同一个核,波动会很大。
第四个细节是结果校验的成本。完整的 99 条查询结果集可能几十 GB,全量 diff 不现实。实用的做法是挑几条关键查询做 checksum,或者对比聚合后的行数和几个数值字段的和。校验的目的是排除"算错了所以看起来快"这种情况,不需要做到逐行一致。
最后分享一个我在做这类对比时的习惯:每次测试开跑之前,把机器上所有无关进程关掉,包括监控 agent、日志采集、定时任务,然后用vmstat 1和iostat -x 1挂一个后台采集,跑完之后看 CPU 空闲率和磁盘利用率曲线。如果发现某段时间 CPU 空闲率掉到 0 或者磁盘 await 飙到几百毫秒,那段时间的查询耗时就要标记出来,单独解释。这套动作多做几轮,你会对自己机器的脾气摸得很清楚,也就能分辨出哪些数字是数据库的能力,哪些只是机器在喘气。