简介:TPC-H V3.0.1 是面向数据仓库与商业智能方向的标准化基准测试套件,适合数据库内核研发、数仓性能调优及数据分析学习者使用,用于在统一模型下评估复杂查询与并发分析场景下的系统表现。压缩包共约2000个文件,整体24.07MB,包含27个SQL查询脚本、大量以数字命名的数据分片文件、若干subparam参数文件,以及sh、h、c等辅助脚本与txt、pdf、docx说明文档,覆盖建表、加载、查询与参数化执行的完整链路。资源同时提供lineitem、customer、partsupp、supplier等表的tbl数据片段,便于按需拼装不同规模数据集。目前已有335人学习下载,可帮助读者快速搭建可复现的测试环境,理解TPC-H查询语义、数据分布与性能对比方法,为数据库选型、SQL优化和数仓压测提供参考依据。
1. TPC-H V 3.0.1:为什么数据库选型总绕不开这套基准
做过数据仓库选型的人大概都有过这种经历:三款引擎摆在面前,厂商 PPT 上的性能数字一个比一个漂亮,真到自己的业务查询上跑起来,排名却完全反过来。问题出在测试口径不统一——有人测的是单表扫描,有人测的是高并发点查,有人干脆只跑了一条预热到极致的 SQL。TPC-H V 3.0.1 就是用来终结这种扯皮的:它用一套固定的 22 条查询、固定的 8 张表结构和固定的数据生成规则,把「谁的查询引擎更强」这件事拉到一个可复现的平面上。
它解决的不是「哪个数据库最快」这种笼统问题,而是「在决策支持类负载下,谁的查询规划、连接算法、聚合与排序综合表现更稳」。适合谁?做数仓选型的技术负责人、要给引擎做性能回归的数据库开发者、以及需要向业务方证明「这套架构扛得住分析型查询」的架构师。这套基准不测事务、不测写入吞吐,只盯分析查询,所以别拿它去评价 OLTP 场景,那是另一个基准的活。
2. 把 TPC-H V 3.0.1 的 22 条查询拆开看:它到底在考什么
很多人第一次跑 TPC-H,看到 22 条 SQL 就头大,觉得无非是些多表连接的堆砌。真跑过几轮才会发现,这 22 条查询是精心设计过的「能力矩阵」,每一条都在压榨查询引擎的某个特定环节。理解这个矩阵,比盲目调参有用得多。
2.1 八张表的关系模型与数据量级
TPC-H 的 schema 是固定的八张表:lineitem、orders、customer、part、partsupp、supplier、nation、region。其中lineitem是绝对的事实表,在 SF=1(约 1GB 原始数据)时大约 600 万行,orders约 150 万行,customer15 万行,part20 万行,partsupp80 万行,supplier1 万行,nation25 行,region5 行。
这个量级分布很关键:nation和region是小到可以广播的维表,lineitem和orders是必须走分区或列存扫描的大表。查询里频繁出现lineitem与orders、part、supplier的多路连接,考的就是优化器能不能选对连接顺序、能不能把广播小表这件事做对。
数据生成用dbgen工具,SF 参数控制规模。SF=1 是入门,SF=100 就是 100GB 级别,SF=1000 是 TB 级。我一般建议先用 SF=10 做功能验证,确认 22 条查询都能跑通且结果正确,再上大 SF 做性能对比。
2.2 22 条查询的四种能力分组
把 22 条查询按考察点分组,比逐条背 SQL 高效得多:
| 分组 | 查询编号 | 主要考察点 |
|---|---|---|
| 聚合与分组 | Q1, Q6, Q12, Q14, Q15, Q17 | 大表扫描后的聚合、条件过滤、视图复用 |
| 多表连接 | Q3, Q4, Q5, Q7, Q8, Q9, Q10, Q11 | 连接顺序选择、连接算法(Hash/Merge/Nested Loop) |
| 子查询与半连接 | Q2, Q16, Q18, Q20, Q21, Q22 | 相关子查询、EXISTS/IN 的改写能力 |
| 排序与 Top-N | Q13, Q19 | 外排序、Limit 下推、内存管理 |
Q1 是典型的全表聚合,几乎就是lineitem的列扫描加sum/avg/count,考的是列存和向量化执行。Q5 和 Q9 是六表连接,考的是优化器会不会把nation、region这种小表做广播。Q13 的left outer join加group by再排序,是很多引擎翻车的地方——外连接后的聚合基数估算一错,整个计划就崩了。
2.3 用 dbgen 生成数据的完整命令
数据生成这一步看着简单,但参数设错会导致后续所有对比失效。下面是我常用的生成流程:
# 下载并编译 dbgen(以常见发行版为例,实际路径按环境调整) # 假设源码已解压到当前目录的 dbgen 文件夹 cd dbgen cp makefile.suite makefile # 编辑 makefile,设置 CC、DATABASE、MACHINE、WORKLOAD 四个变量 # DATABASE=ORACLE 只是占位,实际生成的是文本文件,与数据库无关 make # 生成 SF=10 的数据,输出到 /data/tpch/sf10 mkdir -p /data/tpch/sf10 ./dbgen -s 10 -f -C 8 -S 1 -o /data/tpch/sf10 # -s 10 规模因子,约 10GB # -f 覆盖已有文件 # -C 8 分成 8 个 chunk,便于并行加载 # -S 1 生成第 1 个 chunk # -o 输出目录 # 生成更新数据(refresh 用,做增量测试时才需要) ./dbgen -s 10 -U 1 -o /data/tpch/sf10-C和-S配合使用是为了并行生成,8 个 chunk 可以同时跑 8 个进程,每个进程负责一部分。生成出来的.tbl文件是管道符分隔的文本,加载时注意lineitem的日期字段格式是YYYY-MM-DD,有些引擎的LOAD DATA需要显式指定日期格式,否则会静默转成 NULL,这个坑后面还会提。
2.4 查询执行前的三个必要准备
数据加载完不能直接跑查询,有三件事必须先做:
第一,收集统计信息。绝大多数优化器依赖统计信息做基数估算,lineitem和orders的统计信息不准,连接顺序就会选错。常见做法是ANALYZE TABLE或引擎对应的统计收集命令,大表上这一步可能跑几分钟,别省。
第二,确认查询变体。TPC-H 官方提供的是参数化查询模板,q1.sql到q22.sql里的:1、:2是替换参数。跑之前要用qgen生成具体参数,或者手动替换。直接跑带:的模板会报语法错误。
第三,设置合理的并发度。单条查询跑通和并发跑是两回事。TPC-H 的吞吐量测试要求多流并发,但如果你只是做引擎对比,先单流跑通 22 条,记录每条的执行时间,再考虑并发。
3. 从零跑通一轮 TPC-H:加载、执行与结果校验
这一章是给要动手的人看的。我会按「建表 → 加载 → 跑查询 → 校验」的顺序走一遍,命令和参数都落到具体值。不同引擎的语法有差异,但流程是通用的,遇到不兼容的地方按引擎文档调整即可。
3.1 建表 DDL 的关键约束
TPC-H 官方提供dss.ddl,但直接拿来用经常出问题,因为不同引擎对数据类型和约束的支持不一样。下面是我一般会调整的几个点:
-- 以 lineitem 为例,展示关键字段和约束 CREATE TABLE lineitem ( l_orderkey BIGINT NOT NULL, l_partkey BIGINT NOT NULL, l_suppkey BIGINT NOT NULL, l_linenumber INTEGER NOT NULL, l_quantity DECIMAL(15,2) NOT NULL, l_extendedprice DECIMAL(15,2) NOT NULL, l_discount DECIMAL(15,2) NOT NULL, l_tax DECIMAL(15,2) NOT NULL, l_returnflag CHAR(1) NOT NULL, l_linestatus CHAR(1) NOT NULL, l_shipdate DATE NOT NULL, l_commitdate DATE NOT NULL, l_receiptdate DATE NOT NULL, l_shipinstruct CHAR(25) NOT NULL, l_shipmode CHAR(10) NOT NULL, l_comment VARCHAR(44) NOT NULL ); -- 主键在官方 DDL 里是 (l_orderkey, l_linenumber) -- 但很多列存引擎不支持主键约束,建表时去掉,靠数据生成保证唯一性DECIMAL(15,2)这个精度不能随便改。l_extendedprice最大可能到千万级别,DECIMAL(10,2)会溢出。CHAR类型在部分引擎里会自动补空格,做字符串比较时要注意TRIM,否则l_returnflag = 'R'可能匹配不上。
3.2 数据加载的并行与格式处理
加载 10GB 数据,单线程可能要几十分钟,并行加载是必须的。以常见的LOAD DATA为例:
# 假设已生成 8 个 chunk,分别加载 for i in $(seq 1 8); do # 每个 chunk 对应一个 lineitem.tbl.N 文件 # 实际加载命令按引擎调整,这里展示参数结构 load_data --table lineitem \ --file /data/tpch/sf10/lineitem.tbl.$i \ --delimiter '|' \ --line_terminator '\n' \ --parallel 4 & done wait三个参数最容易出错:--delimiter '|'必须和 dbgen 输出一致;--line_terminator在 Windows 生成的文件里是\r\n,Linux 下是\n,混用会导致最后一行多一个\r;日期字段如果引擎默认格式不是YYYY-MM-DD,要加--date_format参数,否则l_shipdate会变成 NULL,Q1 的结果直接错。
加载完成后做一次行数校验:lineitem在 SF=10 时应该是 59,986,052 行左右(不同版本 dbgen 可能有微小差异),orders是 15,000,000 行。行数对不上,后面所有对比都没意义。
3.3 执行 22 条查询的脚本化方法
手动跑 22 条查询不现实,写个脚本批量执行并记录时间:
#!/bin/bash # run_tpch.sh - 批量执行 TPC-H 查询并记录耗时 QUERY_DIR=/data/tpch/queries RESULT_DIR=/data/tpch/results mkdir -p $RESULT_DIR for i in $(seq 1 22); do qfile=$QUERY_DIR/q$i.sql # 用 qgen 生成参数化查询,或直接用已替换参数的版本 start=$(date +%s.%N) # 执行查询,输出到结果文件 execute_query --file $qfile --output $RESULT_DIR/q$i.out end=$(date +%s.%N) elapsed=$(echo "$end - $start" | bc) echo "Q$i: $elapsed seconds" >> $RESULT_DIR/timing.log doneexecute_query是占位,实际用引擎的客户端命令替换,比如psql -f、mysql <、clickhouse-client --queries-file。计时用date +%s.%N拿纳秒精度,bc做浮点减法。注意第一次执行会受缓存影响,正式对比时每条查询至少跑 3 次,取后两次的平均值,或者先跑一轮预热。
3.4 结果正确性校验:别只看跑得快
性能对比的前提是结果正确。TPC-H 官方提供answers目录,里面有每条查询在 SF=1 时的标准答案。校验方法:
# 对比查询结果与标准答案(注意排序和浮点精度) # 标准答案在 SF=1 下生成,SF=10 的结果行数会不同,但聚合逻辑一致 # 对于 Q1 这种聚合查询,可以对比各分组的行数和关键聚合值 diff <(sort $RESULT_DIR/q1.out) <(sort $ANSWER_DIR/q1.out)浮点聚合结果会有精度差异,sum和avg的末位可能不同,对比时用round到小数点后两位再比。如果某条查询结果行数差很多,先查连接条件是不是写错了,再查统计信息是不是没收集。
4. 参数调优与常见翻车点:那些让结果不可信的细节
跑通不等于跑对,跑对不等于跑得公平。这一章讲的是实操里最容易让对比结果失真的地方,每一条都是踩过的坑。
4.1 内存参数设错,Q13 和 Q19 直接崩
现象:Q13 和 Q19 执行时报内存不足,或者执行时间比其他查询高一个数量级。
原因:这两条查询涉及外连接后的聚合和排序,中间结果集可能膨胀到原始数据的数倍。如果排序内存或哈希表内存设得太小,会溢写到磁盘,性能断崖式下跌。
解决:把排序内存调到可用内存的 50% 到 70%,哈希连接内存单独设一个上限。以某引擎为例,sort_mem和hash_mem两个参数要一起调,只调一个没用。调完后重跑 Q13 和 Q19,确认执行计划里没有external sort或spill字样。
4.2 统计信息过期,连接顺序全乱
现象:同一条查询,昨天跑 3 秒,今天跑 30 秒,数据没变。
原因:统计信息没更新,或者更新后采样率太低。优化器拿到的行数估算偏差大,选了 Nested Loop 而不是 Hash Join。
解决:加载完数据立即收集统计信息,大表用全量采样。如果引擎支持直方图,对l_shipdate、l_discount这种过滤条件多的列建直方图。收集完用EXPLAIN看连接顺序,确认小表在广播侧。
4.3 查询参数替换错误,结果静默偏差
现象:查询能跑通,但结果和标准答案对不上,或者行数明显偏少。
原因:qgen生成的参数里,日期范围、IN列表的取值和标准答案不一致。比如 Q6 的l_shipdate范围,标准答案是1994-01-01到1995-01-01,手动替换时写成了1995-01-01到1996-01-01,结果自然不同。
解决:用qgen按固定 seed 生成参数,保证每次对比用的是同一组参数。qgen -s 1 -d可以指定 seed,seed 固定后参数就固定了。
4.4 缓存干扰,第二轮比第一轮快一倍
现象:同一查询连续跑两次,第二次快很多,误以为引擎有缓存优化。
原因:操作系统页缓存、引擎自身缓存、甚至 CPU 缓存都在起作用。第一轮是冷启动,第二轮是热数据。
解决:正式对比时,每轮测试前清空缓存。Linux 下用echo 3 > /proc/sys/vm/drop_caches,引擎自身的缓存用对应命令清理。如果清不了,就统一用第二轮之后的数据做对比,保证所有引擎都是热缓存状态。
4.5 并发度不一致,吞吐量对比没意义
现象:A 引擎单流跑得快,B 引擎单流慢,但并发时 B 反超。
原因:单流性能看的是单查询延迟,并发性能看的是吞吐量和资源争用。两个指标不能混为一谈。
解决:明确测试目标。如果比单查询能力,就单流跑,记录每条查询的延迟。如果比吞吐,就按 TPC-H 的流数要求并发跑,记录单位时间内完成的查询数。别拿 A 的单流数据去比 B 的并发数据。
5. 进阶:用 TPC-H 做引擎回归与容量规划
跑通一轮 TPC-H 只是起点。真正有价值的是把它变成持续回归的一部分,以及在选型时用数据说话。
5.1 把 22 条查询变成回归基线
每次引擎升级或参数调整后,重跑 22 条查询,和基线对比。基线数据包括:每条查询的执行时间、执行计划的关键节点、结果行数。执行时间波动超过 20% 就告警,执行计划变了就人工审查。
# 回归对比脚本框架 # 读取 baseline.json 和 current.json,逐条对比 python3 compare.py --baseline baseline.json --current current.json --threshold 0.2compare.py的逻辑很简单:对每条查询,计算时间变化率,超过阈值就打印警告;同时对比执行计划里的连接算法和扫描方式,有变化就标记。这个脚本我一般放在 CI 里,每次引擎镜像更新自动触发。
5.2 用 SF 梯度做容量规划
单看 SF=10 的性能不够,业务数据是会涨的。用 SF=1、10、100 三个梯度跑同一组查询,画出「数据量-执行时间」曲线。如果某条查询的时间随数据量超线性增长,说明它的算法复杂度有问题,比如本该 O(n log n) 的排序退化成了 O(n²)。
| SF | lineitem 行数 | 典型用途 |
|---|---|---|
| 1 | 600 万 | 功能验证、CI 回归 |
| 10 | 6000 万 | 单机性能对比 |
| 100 | 6 亿 | 分布式引擎压测 |
| 1000 | 60 亿 | 集群容量规划 |
从 SF=10 到 SF=100,如果 Q5 的执行时间涨了 15 倍而不是 10 倍,就要查是不是数据倾斜导致某个节点成了瓶颈。这种非线性增长在分布式引擎里尤其常见,早发现早调整分区策略。
5.3 一个容易被忽略的技巧:固定查询参数做 A/B 对比
最后说一个我踩过坑才养成的习惯。做引擎 A/B 对比时,一定要用同一组查询参数。qgen默认每次生成随机参数,如果 A 引擎跑的是 seed=1 的参数,B 引擎跑的是 seed=2 的参数,那对比的就是两组不同难度的查询,结论完全不可信。
我的做法是:先用qgen -s 1 -d生成一套固定参数,存成params.sql,所有引擎都用这套参数。这样即使换了引擎,查询的过滤条件、连接键分布都是一样的,对比才有意义。这个习惯帮我避免过好几次「看起来 B 快很多,其实是 B 拿到的参数更简单」的误判。
TPC-H V 3.0.1 这套基准的价值不在于它有多新,而在于它足够稳定、足够透明,让不同引擎能在同一个平面上比。把数据生成、加载、查询执行、结果校验、参数固定这几步做扎实,得到的结论才敢拿去支撑选型决策。希望帮到你。
本文还有配套的精品资源,点击获取