
第一次把 TPC-DS 的 99 条查询分别在 MySQL 和 TiDB 上跑完对着两份耗时表看了很久同一台机器、同一份数据、同一批 SQL整体差了一个数量级以上但其中十几条查询 MySQL 反而更快还有几条 TiDB 慢得反常。如果只截一张总耗时柱状图就下结论那张图大概率是错的。TPC-DS 这套基准真正难的不是“跑起来”而是让两边的结果具备可比性造数怎么切、建表怎么对齐、方言怎么改、计时口径怎么定、失败重试算不算任何一环处理不当出来的数字就只能内部自嗨。这篇内容围绕 TPC-DS 在 MySQL 与 TiDB 上的对比测试展开把整个链路上真正影响结论的环节拆开讲造数与规模因子怎么选、建表和加载怎么把两边的起跑线拉平、查询方言改写的坑在哪、计时怎么定才服人、结果该按什么维度分组去读最后是我踩过的几个印象最深的坑。适合正在做数据库选型评估、准备跑一遍 OLAP 基准、或者已经跑完但对结果心里没底的同学参考。不涉及具体厂商结论只讲方法论和实操。1. TPC-DS 的测试语义先明确你要证明的到底是什么动手之前得先把一件事想清楚这次测试要回答的问题是什么。是“MySQL 能不能扛住分析型负载”还是“同等硬件下 TiDB 的 OLAP 能力比 MySQL 强多少”又或者是“我们现有这套 MySQL 报表系统迁移到 TiDB 能省多少时间”。三个问题对应三套完全不同的测试设计混在一起做出来的结论谁也说服不了。我见过最常见的错误是把 TPC-DS 当成一个“跑分软件”——装好、造数、跑完、看总耗时结束。这种做法的结果往往经不起追问为什么这条查询慢改一个参数会不会翻盘换一个规模因子结论还成立吗答不上来报告就白写了。1.1 99 条查询不是 99 个独立考题TPC-DS 的查询集不是手写死的 99 条 SQL而是从一组查询模板加上参数替换生成的。同一个模板换一组日期区间、换一个州、换一个类别生成的 SQL 结构一样但过滤条件完全不同执行计划可能天差地别。这意味着“我把 99 条都跑了一遍”这句话本身信息量很低——你跑的是哪一组参数变体这组变体对数据分布友好还是苛刻官方把查询大致分成几类报表类大量分组聚合、结果集较小、迭代类带WITH或临时结果的反复扫描、分析类多维聚合、GROUPING SETS风格、数据挖掘类复杂关联与窗口计算。这个分类很有用因为它对应完全不同的数据库能力报表类主要考察聚合和排序效率考验内存和临时表迭代类考验优化器对重复子表达式的处理以及中间结果的落盘策略分析类考验多表 join 的执行顺序选择数据挖掘类考验窗口函数和复杂子查询的展开能力。我建议在测试设计阶段就把 99 条按这四类打上标签后面出结果时按类看而不是只看总数。单看总数会被几条极端慢的查询带偏按类看才能看出“到底是并行能力差还是优化器差还是内存不够”。1.2 七个事实表与三条渠道决定了负载的重心TPC-DS 的数据模型是典型的雪花结构24 张表里 7 张事实表、17 张维度表。三条销售渠道——门店、目录、网络——各自对应销售表和退货表再加一张库存表。这些表的数据量分布极不均匀门店销售事实表通常是最大的目录和网络渠道小一些库存表行数介于中间但更新语义特殊。这个分布直接决定了查询的重心。绝大多数查询都会命中至少一张大的销售事实表而且经常是多张大表互相关联。所以真正吃资源的是“大表扫 大表 join”而不是维度表的过滤。理解了这一点你就知道为什么单机数据库在这类负载上先天吃亏一条查询要扫几十 GB 的事实表单线程扫描就是硬伤跟优化器聪明不聪明关系不大。反过来这也解释了为什么有些查询在 MySQL 上表现不差——那些只涉及小维度表或过滤后结果集很小的查询压根没触及扫描瓶颈比的就是常量级开销和索引命中的效率这时候轻量单机反而有优势。1.3 “跑完”和“跑对”是两件完全不同的事这是我最想强调的一点。TPC-DS 的查询里有大量窗口函数、CTE、多表外连接、INTERSECT/EXCEPT这类集合运算。MySQL 8.0 之前的版本连窗口函数和 CTE 都不支持8.0 早期版本也不支持INTERSECT/EXCEPTFULL OUTER JOIN至今没有原生语法。这意味着如果你不做方言改写MySQL 侧会有相当一部分查询直接语法报错。报错了怎么办呢很多人的处理方式是“跳过”。跳过之后跑出来的“平均耗时”是残缺样本的平均值跟 TiDB 那边完整 99 条的统计根本不可比。更隐蔽的情况是改写之后语义悄悄变了——本来该是外连接的地方写成了内连接结果集少了一批行查询变快了但快得毫无意义。所以一个负责任的 TPC-DS 测试至少要回答三个问题MySQL 侧有多少条查询需要改写、改写了哪些部分、改写后怎么验证语义等价。这三件事不做后面的所有数字都站不住。2. 造数环节规模因子与并行切分决定后续一切造数看起来是最没技术含量的一步实际上它埋的雷最多。数据生成不完整、文件切分不均匀、行尾格式没对齐这些问题在加载阶段不一定暴露等查询跑出来结果对不上才发现回头重来一遍就是几个小时。2.1 规模因子怎么选才不是自欺欺人规模因子直接决定数据量。经验换算关系大致是规模因子 1 对应约 1 GB 的原始文本数据因子 100 对应约 100 GB 文本。导入数据库之后因为索引、行开销、页填充率等因素实际占用通常是文本体积的 1.2 到 1.5 倍。所以选因子 100MySQL 侧 InnoDB 表空间大概要预留 130 GB 以上主键索引和二级索引另算。这里有个很现实的取舍数据量必须显著大于内存否则整个数据集都缓存在缓冲池里测出来的就是纯 CPU 性能跟磁盘 IO 能力毫无关系而真实生产环境里的分析查询几乎不可能全内存命中。我的经验是让数据集达到可用内存的 2 到 4 倍这样既能测出缓存命中率的影响又不至于让测试跑一整天。还有一点容易被忽略规模因子不同查询的执行计划可能不同。小规模下优化器可能选择全表扫描规模上去之后才走索引或者改变 join 顺序。所以如果你打算用因子 1 跑一遍就外推到因子 100结论很可能是错的。要么直接在目标规模上测要么至少在两个不同规模上各跑一遍确认趋势一致。2.2 dsdgen 的并行切分与文件碎片数据生成工具支持并行切分通过指定并行度和子进程编号多个进程各生成一部分数据文件。这里要注意两点。第一并行度不要超过物理核数的一半。造数本身是 CPU 密集型的字符串拼接开太满会跟系统其他进程抢资源反而变慢。而且每个进程都要写文件磁盘顺序写带宽会成为瓶颈。第二并行生成会产生大量小文件每个子进程对每张表输出一个独立文件。表多、并行度高的时候文件数量会膨胀到几百个。加载阶段如果按文件逐个导入光是打开关闭文件的开销就很可观更麻烦的是某些加载工具对文件数量敏感容易出现句柄耗尽。我一般会在生成完之后先把同一张表的分片文件按顺序合并成一个大文件再统一加载。合并的时候要严格保证顺序否则自增主键的连续性会被破坏虽然不影响正确性但会让后续的聚簇索引局部性变差。生成命令的典型形态是这样参数含义需要按自己的环境调整./dsdgen -SCALE 100 \ -DIR /data/tpcds/sf100 \ -TERMINATE N \ -FORCE Y \ -PARALLEL 8 \ -CHILD 0-TERMINATE N表示字段之间用分隔符结尾而不是换行-FORCE Y表示覆盖已有文件-PARALLEL和-CHILD配合做切分。这几项配错了后面加载阶段会莫名其妙地少行或者多列。2.3 加载前的数据完整性自检我的习惯是加载之前先做一轮轻量自检成本很低但能省下大量返工时间。具体做法是统计每张表的数据文件行数跟理论行数对照。TPC-DS 各表的行数跟规模因子基本是线性关系偏差超过 1% 就说明生成过程有问题通常是并行切分时子进程编号没覆盖全或者某个进程中途失败没被发现。另一个必查项是行尾格式。文本文件每行末尾带一个分隔符如果用标准的行终止符去加载最后一列会多出一个空字段导致列数不匹配。这个问题在加载时报错还算好的怕的是某些宽松模式下被静默截断数据少了一列你还不知道。解决办法后面加载章节会讲。3. 建表与加载把两种引擎的起跑线拉平这一段是全场最影响结论可信度的部分也是最容易被敷衍过去的部分。很多对比测试之所以结论离谱就是因为两边建表方式、索引策略、加载顺序完全不对等跑出来的差异里有一大半是配置差异而不是引擎能力差异。3.1 MySQL 侧字符集、聚簇索引与 sql_modeMySQL 的 InnoDB 是索引组织表主键即聚簇索引所有二级索引的叶子节点都存主键值。这个特性意味着主键的选择对性能影响巨大。TPC-DS 的原始表定义里主键大多是一个自增的代理键用它做聚簇主键插入是顺序的页分裂少这是比较好的选择。但也有人为了“优化”把主键改成业务组合键结果插入随机化页分裂严重加载时间翻几倍查询也因为二级索引变大而变慢。字符集建议统一用utf8mb4排序规则用通用的那个。不要用utf8mb3图省空间一旦后续要关联的字段排序规则不一致join 时会触发隐式转换索引直接失效这种问题排查起来非常痛苦。建表时显式写上字符集和排序规则别依赖服务端默认值因为不同环境的默认值可能不一样。sql_mode必须提前统一。严格模式下非法日期、超长字段会直接报错非严格模式下会被截断成警告。TPC-DS 数据里有些字段长度是压着上限设计的宽松模式下静默截断会导致后续 join 匹配不上结果集少行。我的做法是在会话级别强制严格模式让问题在加载阶段就暴露出来。另外一个细节是ONLY_FULL_GROUP_BY。TPC-DS 查询里有些聚合语句的SELECT列表包含非聚合列标准 SQL 下这是非法的但某些数据库容忍。MySQL 开启ONLY_FULL_GROUP_BY会直接拒绝这类查询关掉又会让结果不确定。这里的处理方式是改写查询而不是改sql_mode理由后面方言改写章节细说。3.2 TiDB 侧主键选择、TiFlash 副本与 region 分布TiDB 这边最关键的决策是TPC-DS 这类分析负载到底走行存还是列存。默认的 TiKV 是行存擅长点查和短事务跑 99 条大聚合查询会很吃力——不是因为算不动而是因为行存要做全表扫描再聚合IO 放大严重。真正的 OLAP 场景要给表加列存副本。建表之后对每张表执行加副本操作然后等副本同步完成ALTER TABLE store_sales SET TIFLASH REPLICA 1; SELECT TABLE_NAME, AVAILABLE, PROGRESS FROM information_schema.tiflash_replica WHERE TABLE_SCHEMA tpcds;AVAILABLE变成 1 才算同步完成PROGRESS是进度百分比。这一步不能省很多人加完副本立刻就跑查询结果优化器根本没选到列存测出来的还是行存性能然后得出“TiDB 跑 OLAP 不行”的结论实际上是自己配错了。副本数方面测试环境 1 个就够生产环境考虑高可用才需要 2 个。副本数越多写入放大越严重加载时间越长。主键选择上TiDB 支持聚簇索引和非聚簇索引。自增主键做聚簇索引时写入是顺序追加的region 切分均匀如果主键是随机值比如 UUID 或者哈希值写入会打散到所有 region虽然并发度高但容易出现热点和 region 分裂频繁。TPC-DS 的代理键是递增的用聚簇索引比较合适但要注意递增主键在写入时可能集中打到最后一个 region 形成写热点——这是加载阶段的瓶颈不是查询阶段的。3.3 批量加载的参数取舍与统计信息初始化MySQL 侧加载的核心思路是减少每次提交的开销。关掉唯一性检查、外键检查把日志刷盘策略调宽松事务批量提交。这几个参数只在加载期间临时改加载完必须改回来否则后面的查询测试会受持久化策略影响。SET GLOBAL local_infile 1; SET GLOBAL innodb_flush_log_at_trx_commit 2; SET SESSION unique_checks 0; SET SESSION foreign_key_checks 0; LOAD DATA LOCAL INFILE /data/tpcds/sf100/store_sales.dat INTO TABLE store_sales FIELDS TERMINATED BY | LINES TERMINATED BY |\n;注意LINES TERMINATED BY |\n这个写法它把行尾那个多余的分隔符一起吃掉了避免最后一列多出空值。这是处理 TPC-DS 数据文件最省事的方式比建一个占位列再丢弃干净得多。如果没有本地文件权限也可以先把文件放到服务器上再加载但LOCAL方式在客户端侧读取省去了一次上传。TiDB 侧大表加载建议用物理导入方式走批量写 SST 的路径比逐条INSERT快一到两个数量级。关键配置是导入模式选local后端、关闭表结构版本检查、适当调大并发和区域大小。导入完成之后一定要手动触发一次全量统计信息收集因为刚导入的数据在优化器眼里还是“未知分布”默认估算会严重偏离实际导致执行计划全部走偏。统计信息收集完成后建议抽查几张关键大表的行数估算误差。误差超过 20% 就要考虑调大采样比例或者调高统计信息的桶数。这一步很多人跳过结果所有查询都因为行数估算错误选了错误的 join 顺序白白背了性能差的锅。4. 查询生成与方言改写最耗时间也最容易出错的一环查询生成这一步的复杂度被严重低估。很多人以为跑个生成命令就完事了实际上从模板生成到能真正在两个引擎上执行中间要做的事相当多。4.1 dsqgen 的方言选项与现实妥协查询生成工具支持若干种方言但这些方言里没有专门针对 MySQL 的。通常的做法是选一个相对保守的通用方言生成标准 SQL再手工适配目标引擎。./dsqgen -DIRECTORY ../query_templates \ -INPUT ../query_templates/templates.lst \ -VERBOSE Y \ -QUALIFY Y \ -SCALE 100 \ -DIALECT ansi \ -OUTPUT_DIR ./queries_ansi选方言的时候要注意不同方言生成的 SQL 差异主要在于日期处理、字符串函数、分页语法和类型转换。选错方言会导致大量函数名不兼容改写工作量翻倍。通用方言生成的 SQL 相对干净但也会有部分查询生成标准外连接或者集合运算这些在 MySQL 上得单独处理。还有一个隐藏问题生成查询时用的规模因子会影响某些查询里日期区间的取值。如果造数用因子 100 而生成查询用因子 1某些查询的过滤条件会落在数据范围之外导致返回空结果——查询秒回看起来“性能极好”实际是什么都没算。这种坑非常隐蔽一定要保证生成查询和造数用同一个规模因子。4.2 MySQL 侧绕不开的语法改写清单我把实际遇到的改写需求整理成一张表这是最花时间的部分语法特性TPC-DS 中出现的形态MySQL 8.0 的处理方式窗口函数row_number() over (partition by ... order by ...)8.0 原生支持无需改写但要注意排序缓冲开销公共表表达式WITH x AS (SELECT ...)8.0 原生支持非递归 CTE集合运算INTERSECT/EXCEPT需用JOINNOT EXISTS或INNOT IN改写全外连接FULL OUTER JOIN无原生语法需拆成左连接与反连接UNION ALL日期字面量cast(2000-01-01 as date)兼容但日期加减建议统一改成DATE_ADD字符串截取substring(x from 1 for 4)需改成SUBSTRING(x, 1, 4)空值排序ORDER BY x NULLS FIRST无原生语法需用ISNULL(x)或COALESCE辅助排序分组集GROUPING SETS/ROLLUPROLLUP支持GROUPING SETS需拆成多次聚合再UNION ALL表里每一项改写都有代价。改写FULL OUTER JOIN会让一条查询变成两条扫描再合并耗时自然上去改写GROUPING SETS会让原本一次扫描完成的多维聚合变成多次扫描。这些成本是语法差异带来的不是引擎能力差异解读结果时必须心里有数否则会把改写成本错算成引擎劣势。还有一个更隐蔽的问题某些改写会让优化器失去走索引的机会。比如把EXCEPT改写成NOT IN子查询如果子查询返回大量行且含空值MySQL 会退化成逐行比较性能断崖式下跌。这种情况下改成LEFT JOIN ... IS NULL往往更稳。具体选哪种得看子查询的基数和空值情况没有万能答案。4.3 改写后怎么确认语义没跑偏这是最容易被跳过的一步。改写完 SQL语法通过了跑出结果了就默认它跟原版等价——这个假设非常危险。我的做法是分层验证。第一层是结构验证把改写前后的 SQL 都做一次语法解析对比表引用集合、过滤条件集合是否一致。这一步能抓出漏写WHERE条件、多写连接条件这类低级错误。第二层是小数据集对拍。挑一个很小的规模因子比如因子 1 或者更小在 MySQL 和 TiDB 上分别跑改写后和原始的查询把结果集按所有输出列排序后逐行比对。这一步能抓出聚合逻辑错误、连接类型写反、空值处理不一致这类语义问题。小数据集跑得快99 条全对拍也就几分钟。第三层是聚合校验。对大数据集不逐行比对了改成对每条查询的结果算几个统计量——行数、若干数值列的求和与计数。改写前后这些统计量必须完全一致。这个方法的盲区是行内容变了但统计量恰好一样概率很低但存在所以只适合作为大数据集上的补充验证。这三层都过了我才会认为改写是可信的。跳过验证直接跑最后结果差一倍你都不知道该信谁。5. 执行与计时让两边的数字具备可比性查询能跑通之后怎么跑、怎么计时直接决定结论的可信度。这块的争议最多因为不同的计时方式能得出完全相反的排名。5.1 冷跑、热跑与并发档位怎么定首先要区分冷跑和热跑。冷跑是指清空缓存后执行反映的是磁盘 IO 能力和冷启动开销热跑是缓存预热后执行反映的是内存计算能力和 CPU 效率。两者的结论经常不一致某个引擎冷跑慢但热跑快说明它初次加载数据效率低但计算效率高。我的做法是两个都测分开报告。冷跑的做法是先重启数据库服务或者把数据文件挪走再挪回来更彻底但更慢确保缓冲池是空的。热跑的做法是每条查询先跑一遍丢弃结果再连续跑三遍取后两遍的平均。这里有个坑某些引擎有查询结果缓存或者计划缓存连着跑同一条查询第二遍可能压根没真正执行。要确认缓存机制是否开启如果开了要么关掉要么在查询里加一个不影响结果的随机条件破坏缓存命中但这会改变执行计划慎用。最稳妥的是在测试前明确记录缓存相关参数的取值别让它成为隐藏变量。并发档位也要想清楚。单并发测的是单条查询的极致性能反映引擎的并行度和优化器能力多并发测的是吞吐和资源竞争反映真实混合负载下的表现。TPC-DS 官方有吞吐量测试的定义但那套流程很重。做选型评估的话我一般测三档单并发、四个并发、八个并发看吞吐量曲线随并发增长是否线性拐点在哪。5.2 计时口径客户端 wall time 与服务端耗时这两个口径经常差很多。客户端计时包含网络往返、结果集传输、客户端解析服务端耗时只算引擎内部执行。做引擎能力对比应该用服务端耗时做端到端体验评估应该用客户端时间。问题在于不同引擎的服务端耗时统计范围不一样。有的只算执行阶段有的把解析、优化、执行分开统计。如果直接拿两个引擎的“执行耗时”对比可能一个含了优化时间一个没含结论就歪了。我的处理方式是统一用客户端 wall time 作为主口径因为它定义清晰、两边一致、可复现。同时在结果里附上服务端耗时作为辅助参考用于定位慢在哪一段。这样即使服务端统计口径有差异主结论也是站得住的。另外必须明确失败和超时的处理规则。一条查询跑了半小时没出结果算超时还是继续等超时之后算 1800 秒还是直接剔除我的规则是设一个明确的超时阈值比如 30 分钟超时就记为该阈值并单独标注。绝不剔除——剔除会让平均值变好看但那是欺骗自己。同时报告里要写清楚有几条超时读者自己判断。5.3 参数清单要克制调参这件事很容易失控。为了让某一方表现好不断加参数、改配置最后测出来的不是引擎的默认能力而是你调参水平的对比。这种结果对选型毫无参考价值因为生产环境没人能保证调到同样程度。我的原则是只调那些“不调就没法正常工作”的参数比如内存上限、临时空间位置、连接数上限。性能相关的优化参数除非两方都能对应调整否则一律用默认值并在报告里声明。比如给 TiDB 开了列存副本那 MySQL 侧就不能只给一个裸库得相应地把缓冲池调到合理大小、把临时表引擎配好这样才算对等。还有一个常被忽略的点两边的硬件资源上限要明确限制在相同水平。如果 TiDB 是分布式部署占了 3 台机器MySQL 是 1 台机器那对比的就不是引擎而是集群规模。要么把 TiDB 也压到单机上跑要么把 MySQL 部署到同等总资源的机器上。这一点不说清楚所有数字都失去意义。6. 结果分段解读差异到底出在哪些查询形态上拿到两份耗时表之后直接看总和是最偷懒的读法。正确的读法是先分组再找组内的异常点最后才看整体。6.1 大事实表扫描与聚合并行度是分水岭这一类查询的特征是扫描亿级行的事实表做过滤、聚合、分组结果集通常只有几十行。耗时几乎完全由扫描吞吐决定。MySQL 在这类查询上的瓶颈非常明确单条查询基本是单线程执行。虽然有并行读取的相关参数但它的适用范围很窄主要针对无过滤条件的全表计数和表检查普通查询用不上。所以一条要扫 50 GB 数据的聚合查询MySQL 就是一个线程硬啃耗时跟数据量线性相关。TiDB 在这类查询上的优势来自分布式的扫描并行一条查询会被拆成多个扫描任务分发给多个存储节点再汇总。加了列存副本之后列式存储只读需要的列IO 量还能再降一个量级。这两项叠加同样是扫 50 GB实际读的数据量可能只有行存的几分之一而且并行执行。这是一边倒的差距也是最容易得出“某某引擎碾压”结论的地方。但要注意这个差距反映的是架构差异单机单线程 vs 分布式并行 列存而不是优化器水平的差异。如果你的真实负载里这类全表聚合查询占比很低那这个差距对你意义不大。6.2 多表 join优化器选路的分水岭多表 join 的查询最能体现优化器水平。TPC-DS 里不少查询涉及五六张表关联join 顺序的选择空间很大选错了中间结果会膨胀几个数量级。这里的观察点是执行计划而不是耗时。我会把两边的执行计划都拉出来对比三件事join 顺序是否一致、每步的估算行数与实际行数偏差多大、有没有出现意外的全表扫描或者笛卡尔积。实际测下来MySQL 在星型模型上的表现比想象中好。因为维度表通常很小可以直接放进 join buffer 做哈希连接事实表只扫一次。MySQL 8.0 引入哈希连接之后这类查询的改善很明显。真正出问题的是那些多张大表互相关联、维度表也要参与过滤的场景——这种时候 join 顺序一旦选错中间结果会大到需要落盘耗时直接失控。TiDB 这边因为统计信息更细、有代价模型的持续优化加上可以选择广播小表或者重分布大表join 顺序的选择通常更优。但也不是没有翻车的时候统计信息过期、或者列存副本还没同步完优化器会基于错误的行数估算做出糟糕决策这时候表现可能还不如 MySQL。6.3 窗口函数、CTE 与子查询展开这一类的差异比较微妙。两边的窗口函数实现都要先按分区键排序再计算排序开销是主要成本。区别在于排序能不能利用并行以及内存不够时怎么落盘。我在实测中遇到的典型现象是窗口函数的分区键如果跟表的排序键或者索引顺序一致两边都快如果不一致都要做一次全量排序这时候数据量大的那一方会明显吃亏因为排序缓冲区不够就要落盘落盘之后性能断崖。CTE 的处理差异更值得注意。有的引擎会把 CTE 物化成临时结果复用有的会直接内联展开成子查询。物化省了重复计算但多了写临时表的开销内联省了写临时表但可能重复扫描。对于被引用多次的 CTE物化通常更好只引用一次的内联更好。实际表现取决于引擎的默认策略而这个策略往往不能通过参数控制属于需要实测确认的黑盒行为。子查询展开是另一个坑。关联子查询在某些引擎上会被改写成半连接性能提升一个量级改写不了就退化成逐行执行慢得离谱。TPC-DS 里有几条查询故意用了这种写法如果某一方没有做展开优化那几条的耗时会异常突出把整体平均值拉高。6.4 几个反直觉的观察第一个反直觉小结果集的查询MySQL 有时更快。原因是它的执行路径短没有分布式调度和网络传输开销。一条查询在 TiDB 上要经过 SQL 层解析、优化、下发到存储层、多个节点并行执行、结果汇总这套流程的固定开销对于毫秒级的查询来说是纯负担。而 MySQL 一次函数调用就进去了。所以如果负载里短查询占比高整体平均下来差距会明显缩小。第二个反直觉中等选择率的查询TiDB 不一定比 MySQL 快。列存适合高选择率的大扫描但如果过滤条件能筛掉 99% 的数据行存的索引反而更高效——直接定位到少数几行而列存要做块级扫描再过滤。这种场景下加了列存副本反而可能变慢因为优化器可能误判选择了列存路径。第三个反直觉同一份数据、同样的查询跑第二遍和第一遍的排名可能反过来。这就是缓存和统计信息在起作用。所以单次测试的结论非常脆弱至少要在不同时间点跑三轮看排名是否稳定。不稳定的话说明有未受控的变量得先把它找出来。7. 踩坑清单统计信息、内存与结果一致性这一段是我自己踩过的、也是最容易让别人重复踩的坑。每个坑单拎出来都不复杂但在测试过程中撞上往往要花半天才能定位。7.1 统计信息缓存的幻觉MySQL 8.0 有个默认行为information_schema里的表统计信息会被缓存默认缓存时间长达一天。也就是说你手动执行了统计信息收集再去查表的行数看到的还是旧的缓存值。这会带来一个极其误导人的现象你以为统计信息已经更新了实际上优化器看到的还是老数据执行计划一直不对。-- 查看当前缓存有效期 SHOW VARIABLES LIKE information_schema_stats_expiry; -- 会话级别关掉缓存确保读到最新统计 SET SESSION information_schema_stats_expiry 0;我第一次遇到这个问题时反复执行统计信息收集、反复查执行计划怎么都不对最后才发现是缓存。这个坑的隐蔽之处在于它不影响数据正确性只影响你对“统计信息是否更新”的判断进而影响你对执行计划的归因。TiDB 这边的问题是另一个方向自动统计信息收集有触发阈值表的数据变更量没到阈值就不会自动收集。大批量导入之后如果没手动触发统计信息可能还停留在导入前的状态空表或者极少数行优化器估算出来的行数是几十行而不是几亿行join 顺序完全乱套。所以大批量导入之后手动跑一次全量收集是必须的动作别指望自动机制。7.2 内存水位与落盘分析型查询的内存消耗很容易失控。一条带大分组聚合或者大排序的查询峰值内存可能是数据量的几倍。MySQL 侧的内存相关参数很多容易配错。排序缓冲、连接缓冲、临时表内存上限、内存临时表引擎的容量上限这几个参数相互作用。特别是内存临时表超限之后的降级路径有的版本会转成磁盘临时表有的会转成内存映射文件。转成磁盘临时表意味着走 InnoDB每次读写都要过缓冲池性能下降非常明显转成内存映射文件则依赖操作系统的页缓存表现介于两者之间。测试时要把这个降级行为搞清楚否则同一条查询在两次执行中的耗时差异会让你怀疑人生。TiDB 侧有单条查询的内存配额限制超出后可以选择落盘或者直接报错终止。测试环境下我倾向于允许落盘因为报错终止会让你丢失这条查询的耗时数据生产环境则要谨慎落盘可能把磁盘打满。落盘目录要单独配置到大容量盘上别跟数据目录混在一起否则落盘 IO 会跟正常读写抢带宽。还有一个细节内存配额不是越大越好。配额调得很大多条查询并发时可能把物理内存吃光触发系统的内存回收整个实例都会卡住。比较稳妥的做法是配额乘以最大并发数不超过物理内存的七成。7.3 结果比对时的排序与类型陷阱最后说结果比对这是判断“跑对了没有”的唯一手段但坑不少。第一个坑是排序不稳定。如果查询的ORDER BY列存在重复值相同值之间的相对顺序是不确定的两次执行结果的物理顺序可能不同。逐行比对就会误判为不一致。解决办法是比对前按所有输出列重新排序或者只比对聚合后的统计量。第二个坑是空值处理。不同引擎在聚合函数里对空值的处理、在排序里对空值位置的处理、在连接条件里对空值匹配的处理都有差异。这些差异有的是标准规定有的是实现选择。比对时如果某一列大量为空很容易出现“行数一样但值对不上”的情况根源往往是空值参与聚合的方式不同。第三个坑是数值类型精度。金额类字段有的是定点数有的是浮点数浮点数在不同引擎上的舍入行为可能有微小差异累加之后偏差会被放大。比对时不要用精确相等用相对误差阈值判断比如百万分之一以内算一致。但阈值不能随便放宽放得太宽会把真实的计算错误也放过。第四个坑是时间字段的时区。如果表里有时间戳字段而两边的会话时区设置不同同一份数据读出来会差几个小时。这种差异在结果比对时会表现为整列偏移很好发现但如果没意识到时区这个概念会往数据类型错误的方向去查浪费大量时间。8. 一点个人体会跑这类对比测试最耗时间的从来不是执行阶段而是前面的对齐工作。造数、建表、方言改写、参数校准这四件事占了我八成的精力真正跑查询可能就几个小时。但这个投入是值得的——前面不对齐后面的数字就是废纸。我现在做这类测试有个固定习惯先把 MySQL 侧的改写清单和验证记录整理成文档把每条改写的理由、验证方式、验证结果都写清楚。这份文档比最终的耗时表重要得多因为过两个月有人质疑某个数字你能立刻翻出依据而耗时表本身换个硬件、换个版本就作废了。另外提醒一句别迷信任何公开的基准测试报告包括看起来很权威的那种。硬件配置、版本号、参数设置、方言改写方式任何一个不一样结论都可能反过来。自己跑一遍哪怕规模小一点、配置糙一点得到的是你自己能解释清楚的数据这比引用别人的数字有价值得多。真要引用也只引用方法和趋势别引用具体倍数。