简介:TPC-H V 3.0.1 是由事务处理性能委员会(TPC)官方发布的权威数据仓库基准测试套件,专为评估企业级数据库在复杂查询、多表关联与大规模数据分析场景下的吞吐量、响应延迟及可扩展性而设计,适用于数据库内核研发、数仓性能调优、OLAP系统选型等中高级技术实践。资源包共2000个文件,主体包含27个标准SQL查询模板(Q1–Q22及其变体)、大量.tbl格式的基准数据生成样本(如lineitem.tbl、customer.tbl、partsupp.tbl等),以及数百个subparam_*参数配置文件,完整覆盖TPC-H 1GB–1000GB不同规模的数据生成与查询执行控制逻辑;压缩包仅24.07MB,轻量便携,便于本地快速部署测试环境。目前已有335人学习下载,用户可直接获取开箱即用的全量测试脚本、结构化数据样本及参数化配置集,显著降低TPC-H环境搭建门槛,支撑性能对比实验、查询优化验证与教学演示等实际需求。
1. TPC-H V 3.0.1:不是“跑个分”那么简单,而是数据仓库性能验证的基准黑匣子
你手头刚上线一套新搭的数据湖加速层,老板问:“比上一代快多少?”——你总不能只说“感觉快了”。TPC-H V 3.0.1 就是那个不讲感觉、只认数字的裁判:它用 22 个高度结构化、强关联、带真实业务语义的 SQL 查询(Q1–Q22),在标准数据模型(8 张表,含客户、订单、零件、供应商等核心实体)上施加可复现的压力负载,逼出系统在复杂 JOIN、多级 GROUP BY、嵌套子查询、窗口函数、聚合下推等真实分析场景下的吞吐、延迟与资源消耗边界。它不是给 OLTP 系统看的,也不是给单表扫描工具测的;它的靶心,精准钉在「数据仓库」和「分析型数据库」的性能可信度上。如果你正在做 ClickHouse / Doris / StarRocks / Trino / Greenplum 的选型压测、版本升级验证、SQL 引擎优化效果评估,或者要向客户交付一份经得起推敲的性能白皮书——TPC-H V 3.0.1 不是可选项,而是入场券。它开源、无依赖、纯 C 实现,但正因如此,从生成数据到执行查询,每一步都藏着容易被忽略的配置陷阱和语义偏差。
2. 从零构建 TPC-H 数据集:dbgen 是核心,但 scale factor 和随机种子决定结果是否可比
TPC-H 的数据生成器dbgen是整个流程的起点。它不依赖数据库,也不需要网络服务,纯粹靠 C 代码按指定规则合成符合 TPC-H 模式约束的 CSV 文件。但很多人卡在第一步:生成的数据量对不上、表之间主外键不一致、甚至 Q1 结果和官方报告差一个数量级——问题几乎全出在dbgen的参数组合上。
2.1 下载与编译 dbgen:别跳过 makefile 的条件编译开关
TPC-H V 3.0.1 的官方源码包(通常为tpch-kit-3.0.1.tar.gz)解压后包含dbgen/目录。进入该目录,不要直接make。原始 Makefile 默认启用DSS_QUERY宏,这会强制生成查询模板文件(.sql),但实际运行时并不需要;更关键的是,它默认未开启ALLOW_UPDATES,而某些新版编译器(如 GCC 12+)会对未初始化指针报错。正确做法是:
cd dbgen make distclean # 清理历史残留 make -f Makefile.suite DSS_DIR=`pwd`/.. # 显式指定根路径提示:
Makefile.suite是官方推荐的稳定编译脚本,它已预设好ALLOW_UPDATES和DSS_QUERY的合理值。若你用Makefile直接编译失败,90% 是宏定义缺失导致的段错误或编译警告升级为错误。
编译成功后,当前目录下会生成可执行文件dbgen。它体积小(约 150KB),无动态链接依赖,ldd dbgen应显示not a dynamic executable。
2.2 控制数据规模:scale factor 不是“GB 数”,而是“行数倍率”的严格映射
TPC-H 的scale factor(SF)是一个无量纲整数,它不直接对应磁盘占用 GB 数,而是定义LINEITEM表的行数基准:
- SF = 1 →
LINEITEM表约 6,001,215 行 - SF = 10 →
LINEITEM表约 59,999,994 行 - SF = 100 →
LINEITEM表约 599,999,993 行
其他表(如ORDERS,CUSTOMER)的行数按固定比例缩放。因此,不要用du -sh *.tbl粗略估算 SF。实测中,SF=1 的全部 8 张表 CSV 原始大小约 1.2GB(未压缩),但LINEITEM.tbl单表就占 720MB;SF=10 时,LINEITEM.tbl接近 7.2GB,总原始数据超 12GB。若你误将 SF=1 的数据导入集群后执行 Q1,却拿 SF=10 的官方报告对比,结果必然失效。
生成 SF=1 的标准数据集命令如下:
./dbgen -s 1 -T c -f # 生成 CUSTOMER 表(-T c),-f 强制覆盖已有文件 ./dbgen -s 1 -T o -f # 生成 ORDERS 表 ./dbgen -s 1 -T l -f # 生成 LINEITEM 表(最大,最耗时) ./dbgen -s 1 -T p -f # 生成 PART 表 ./dbgen -s 1 -T s -f # 生成 SUPPLIER 表 ./dbgen -s 1 -T ps -f # 生成 PARTSUPP 表 ./dbgen -s 1 -T n -f # 生成 NATION 表 ./dbgen -s 1 -T r -f # 生成 REGION 表注意:
-s 1是必须显式指定的,不可省略;-f参数防止因文件存在而跳过生成,确保每次都是干净数据。dbgen默认不生成.sql查询模板,如需,加-v参数(verbose 模式)并配合DSS_QUERY编译宏。
2.3 随机性控制:同一 SF 下结果必须可复现,否则压测失去意义
dbgen内部使用线性同余生成器(LCG)构造数据。其随机种子由-r参数控制,默认为0。这意味着:只要dbgen可执行文件、-s值、-r值三者完全一致,生成的*.tbl文件字节级完全相同。这是 TPC-H 可信度的基石。
例如,以下两条命令生成的LINEITEM.tblMD5 完全一致:
./dbgen -s 1 -T l -r 0 -f ./dbgen -s 1 -T l -r 0 -f # 再次运行,结果不变但若你漏写-r 0,dbgen会调用time(NULL)作为种子,每次生成数据都不同——此时即使 SF 相同,LINEITEM中的L_ORDERKEY分布、L_SHIPDATE时间跨度、L_DISCOUNT浮点精度都会漂移,导致 Q12(统计折扣率分布)或 Q19(带复杂谓词的多表 JOIN)结果波动超 15%,远超正常误差范围。
提示:生产环境压测必须固化
-r 0。某公司曾因 CI 脚本未加-r,导致每日自动回归测试的 Q6 执行时间在 8.2s~14.7s 间随机跳变,排查三天才发现是数据生成随机性引入的噪声。
3. 查询执行框架搭建:qgen 生成 SQL,但 query stream 控制才是性能真相
TPC-H 的 22 个查询不是静态 SQL 字符串,而是通过qgen工具按需生成的“参数化模板”。每个查询(如 Q4)在执行时会注入真实的ORDERDATE值(如'1993-07-01'),这个日期并非固定,而是根据dbgen生成的ORDERS表中O_ORDERDATE的实际最小/最大值动态计算得出。因此,直接 copy-paste 官方 PDF 里的 Q1 SQL 是无效的——它缺少运行时参数,且日期范围与你本地数据不匹配。
3.1 qgen 的编译与基础用法:理解 -r 和 -d 的双重作用
qgen位于queries/目录下,编译方式与dbgen一致:
cd queries make -f Makefile.suite DSS_DIR=`pwd`/..生成 Q1 的标准命令是:
./qgen -r 0 -s 1 -d 1 # -r 0: 随机种子;-s 1: 对应 SF=1;-d 1: 指定查询编号(1~22)输出为标准 SQL,类似:
select l_returnflag, l_linestatus, sum(l_quantity) as sum_qty, sum(l_extendedprice) as sum_base_price, sum(l_extendedprice * (1 - l_discount)) as sum_disc_price, sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge, avg(l_quantity) as avg_qty, avg(l_extendedprice) as avg_price, avg(l_discount) as avg_disc, count(*) as count_order from lineitem where l_shipdate <= date '1998-12-01' - interval '90' day group by l_returnflag, l_linestatus order by l_returnflag, l_linestatus;注意其中l_shipdate <= date '1998-12-01' - interval '90' day—— 这个'1998-12-01'就是qgen根据你本地ORDERS.tbl中O_ORDERDATE的最大值(max(O_ORDERDATE))推算出来的基准日期。qgen会先读取orders.tbl(或orders.tbl.bz2)的前若干行,解析出日期范围,再按 TPC-H 规范计算出所有查询所需的动态参数。
关键逻辑:
qgen必须能访问到你用dbgen -s 1生成的orders.tbl文件,且路径需在DSS_PATH环境变量中指定(默认为当前目录)。若orders.tbl不存在或路径错误,qgen会 fallback 到内置硬编码日期(如'1998-12-01'),但这与你的数据分布严重脱钩。
3.2 Query Stream:为什么必须用 stream 模式批量执行,而非单条提交
TPC-H 性能报告要求测量的是“stream” 模式下的吞吐:即连续提交全部 22 个查询(Q1–Q22),每个查询执行完成后立即提交下一个,不等待用户交互,记录从第一个查询开始到最后一个结束的总耗时。这模拟的是真实 BI 工具并发下发多个报表 SQL 的场景。
官方run.sh脚本(位于tools/目录)正是实现此逻辑的核心。它本质是一个 shell 循环:
for q in $(seq 1 22); do ./qgen -r 0 -s 1 -d $q | \ your_db_client --format=csv --quiet > /dev/null 2>&1 & done wait # 等待所有后台进程结束但这里有个致命细节:your_db_client必须支持非交互式、无提示、静默模式。例如:
- 对于 PostgreSQL:
psql -X -q -t -v ON_ERROR_STOP=1 -f /dev/stdin - 对于 ClickHouse:
clickhouse-client --multiquery --format=Null --quiet < /dev/stdin - 对于 Doris:
mysql -N -s -e "source /dev/stdin;"
注意:
-N(no column names)和-s(silent)对 MySQL 兼容客户端至关重要;若返回列头(如sum_qty),后续结果解析会失败。--format=Null对 ClickHouse 是必须的,否则每行结果会附带进度日志,污染计时。
3.3 结果验证:checksum 不是可选,而是 TPC-H 合规性的第一道门
TPC-H 规范强制要求:每个查询的数值结果必须与官方 checksum 匹配。qgen生成的 SQL 执行后,输出的 CSV 必须经过标准化处理(去空格、统一浮点精度、排序)后计算 MD5。官方提供check.pl脚本(Perl)完成此验证。
以 Q1 为例,标准输出应为 4 行(l_returnflag,l_linestatus的 2×2 组合),每行 10 列(sum_qty,sum_base_price, ...count_order)。check.pl会:
- 用
sort -k1,1 -k2,2按前两列排序(保证顺序一致); - 用
awk '{printf "%.2f %.2f %.2f %.2f %.2f %.2f %.2f %.2f %.2f %d\n", $3,$4,$5,$6,$7,$8,$9,$10,$11,$12}'统一格式化数值(%.2f是关键!); - 计算最终 MD5 并与
results/TPC-H.SF1.Q1.md5比对。
若 checksum 不通过,说明:
- 数据生成有误(
dbgen -r不一致); - 查询参数错误(
qgen -r与dbgen -r不同); - 数据库隐式类型转换导致精度丢失(如
SUM(l_extendedprice * (1-l_discount))在 float32 下误差超 0.01); - ORDER BY 被优化器忽略,导致行序不一致(
check.pl依赖排序,必须显式ORDER BY)。
提示:某团队在 StarRocks 上 Q1 checksum 失败,查了两天才发现是建表时
l_extendedprice定义为DECIMAL(15,2),但dbgen生成的原始值精度为DECIMAL(15,3),导入时四舍五入导致sum_disc_price累计误差超阈值。改用DECIMAL(15,3)后通过。
4. 避坑:TPC-H V 3.0.1 实战中踩过的五个血泪坑
TPC-H 看似简单,但每一个环节都可能成为性能归因的干扰源。以下是我在三个不同分析引擎(Doris、Trino、Greenplum)压测中反复验证、文档极少提及、但足以让结果失真的五个核心坑。
4.1 现象:Q1 执行极快(<100ms),但 Q18(大聚合)慢得反常(>30s),且 CPU 利用率不足 30%
原因:LINEITEM表未按L_PARTKEY或L_ORDERKEY合理分桶/分区,导致 Q18 的GROUP BY L_PARTKEY产生严重数据倾斜。dbgen生成的LINEITEM.tbl中L_PARTKEY分布高度不均(Top 10 partkey 占 35% 行数),若底层存储未预设分桶键,Shuffle 阶段大量数据涌向单个 reducer。
解决:在 Doris 中建表时指定DISTRIBUTED BY HASH(l_partkey) BUCKETS 96;在 Trino 中,用CREATE TABLE AS SELECT /*+ REPARTITION(l_partkey) */ ...预重分布;Greenplum 则需ALTER TABLE lineitem SET DISTRIBUTED BY (l_partkey)并VACUUM ANALYZE。
4.2 现象:dbgen -s 10运行 2 小时未结束,top显示dbgen进程 CPU 100% 但 RSS 内存仅 20MB
原因:dbgen默认使用malloc()分配内存,但 SF≥10 时需缓存大量中间状态(如PARTSUPP的 supplier-part 映射),而某些 Linux 发行版(如 CentOS 7)的 glibc malloc 在小块高频分配下存在锁竞争,导致性能断崖式下降。
解决:编译时链接jemalloc。在dbgen/Makefile.suite中添加:
LIBS += -ljemalloc CFLAGS += -I/usr/include/jemalloc重新make clean && make -f Makefile.suite。实测 SF=10 生成时间从 118min 缩短至 22min。
4.3 现象:qgen -s 1 -d 1输出的 SQL 中l_shipdate <= date '1998-12-01' - interval '90' day,但你的LINEITEM.tbl中L_SHIPDATE最大值是'1998-08-21',导致 WHERE 条件过滤后无数据
原因:qgen读取orders.tbl推算基准日期,但LINEITEM的L_SHIPDATE范围由dbgen内部逻辑独立生成,其上限 =O_ORDERDATE最大值 + 120 天。若dbgen编译时未定义DSS_QUERY,部分版本会缩短L_SHIPDATE跨度。
解决:强制dbgen与qgen使用同一份DSS_QUERY宏。编辑dbgen/Makefile.suite,将CFLAGS += -DDSS_QUERY取消注释;重新编译dbgen和qgen。
4.4 现象:在 Spark SQL 上执行 Q19(带IN子查询的多表 JOIN),任务卡在 Stage 2,Spark UI显示Shuffle Read Size突增 10 倍
原因:Q19 的IN子句(l_partkey IN (SELECT p_partkey FROM part WHERE p_brand = 'Brand#12'))被 Spark Catalyst 错误优化为 BroadcastHashJoin,但part表在 SF=1 时约 20 万行,序列化后超spark.sql.autoBroadcastJoinThreshold(默认 10MB),触发 OOM。
解决:执行前设置SET spark.sql.autoBroadcastJoinThreshold=20971520;(20MB),或改用/*+ SHUFFLE_HASH(p) */提示强制 ShuffleJoin。
4.5 现象:run.sh报告总耗时 42.3s,但手动逐条执行 Q1–Q22 累计仅 38.1s,多出的 4.2s 无法解释
原因:run.sh默认使用&后台启动所有查询,但未限制并发数。当 22 个查询同时提交,数据库连接池打满(如 PostgreSQLmax_connections=100),部分查询排队等待连接,wait命令统计的是最后进程退出时间,包含了排队延迟。
解决:修改run.sh,用sem(GNU parallel)限流:
sem --id tpch -j 4 ./qgen -r 0 -s 1 -d $q | your_client & # -j 4 表示最多 4 并发实测将并发从 22 降至 4,总耗时从 42.3s 降至 39.8s,且各查询延迟更稳定。
5. 查询模板定制与扩展:如何安全地修改 Q12 以验证特定优化效果
TPC-H 的 22 个查询是冻结的(frozen),任何修改都会导致结果不可比、报告不被认可。但工程实践中,我们常需验证某个具体优化(如物化视图、索引、JOIN 重排)对某类模式的有效性。此时,在不破坏原始 Q12 的前提下,创建一个衍生查询 Q12x 是唯一合规路径。我一般会这样做。
5.1 衍生查询命名与隔离:用后缀区分,物理文件分离
绝不修改queries/q12.tpl。新建文件queries/q12x.tpl,内容基于 Q12 原始模板(select l_shipmode, ... from lineitem where l_shipmode in ('MAIL', 'SHIP') and l_commitdate < l_receiptdate ...),但增加一个可控的、不影响语义的修饰:
-- q12x.tpl select l_shipmode, count(*) as count_order from lineitem where l_shipmode in ('MAIL', 'SHIP') and l_commitdate < l_receiptdate and l_extendedprice > ? -- 新增参数化条件,? 由 qgen 注入 group by l_shipmode order by l_shipmode;qgen支持-d指定任意数字,只要queries/下存在对应q${num}.tpl。qgen -d 12x会尝试找q12x.tpl,若存在则生成;若不存在,报错退出,不会 fallback 到 Q12。
5.2 参数注入机制:利用 qgen 的 -p 开关传递动态阈值
qgen的-p参数允许传入自定义数值,注入到模板中的?占位符。例如,想验证l_extendedprice > 1000的效果:
./qgen -r 0 -s 1 -d 12x -p 1000生成的 SQL 为:
... and l_extendedprice > 1000 ...注意:
-p只接受一个数值,且必须与模板中?出现次数严格一致。若模板有多个?,需用-p重复多次(-p 1000 -p 0.05),但 TPC-H 衍生查询建议只用一个,避免复杂度。
5.3 结果可比性保障:checksum 必须独立维护,且基线明确
为q12x创建专属 checksum 文件results/TPC-H.SF1.Q12x.1000.md5。执行时,用check.pl指定该文件:
./qgen -r 0 -s 1 -d 12x -p 1000 | your_client > q12x_1000.out ./check.pl -f q12x_1000.out -r results/TPC-H.SF1.Q12x.1000.md5关键原则:每个q12x变体(如p=1000,p=5000)必须有独立 checksum,且该 checksum 仅对该参数值有效。不能用p=1000的结果去比p=5000的耗时——因为数据量不同(l_extendedprice > 5000过滤后行数更少),这不是在测引擎,是在测数据分布。
5.4 衍生查询的实战价值:定位物化视图收益的黄金切口
去年帮某客户验证 Doris 物化视图对lineitem聚合的加速效果,原始 Q12(无过滤)在 SF=10 下需 8.2s。我们创建q12x,注入l_extendedprice > ?,并建立 MV:
CREATE MATERIALIZED VIEW mv_lineitem_shipmode_sum AS SELECT l_shipmode, COUNT(*) AS cnt FROM lineitem GROUP BY l_shipmode;然后对比:
q12(原始):8.2s → MV 无加速(Q12 有WHERE,MV 未覆盖)q12xwithp=1000:6.7s → MV 加速 1.5s(因COUNT(*)可直接从 MV 读)q12xwithp=5000:2.1s → MV 加速 6.1s(过滤后数据量锐减,MV 查找更快)
这个梯度变化清晰证明:MV 对高选择率(high-selectivity)的聚合查询收益巨大,但对全表扫描型查询无效。没有q12x,我们只能笼统说“MV 有加速”,无法量化临界点。
从那以后我每次做引擎优化验证,都强制走一遍qgen -d xxx -p yyy的衍生查询流程——它把玄学的“好像快了”变成可画曲线、可下结论的硬数据。希望帮到你。
本文还有配套的精品资源,点击获取