Files
ray-project--ray/release/nightly_tests/dataset/tpch/tpch_q2.py
T
2026-07-13 13:17:40 +08:00

209 lines
7.5 KiB
Python

import ray
from ray.data.aggregate import Min
from ray.data.expressions import col
from common import parse_tpch_args, load_table, to_f64, run_tpch_benchmark
def main(args):
def benchmark_fn():
# Q2: Minimum Cost Supplier Query
# Find the cheapest supplier in a given region for parts of a given size and type.
#
# Equivalent SQL:
# SELECT s_acctbal, s_name, n_name, p_partkey, p_mfgr,
# s_address, s_phone, s_comment
# FROM part, supplier, partsupp, nation, region
# WHERE p_partkey = ps_partkey
# AND s_suppkey = ps_suppkey
# AND p_size = 15
# AND p_type LIKE '%BRASS'
# AND s_nationkey = n_nationkey
# AND n_regionkey = r_regionkey
# AND r_name = 'EUROPE'
# AND ps_supplycost = (
# SELECT MIN(ps_supplycost)
# FROM partsupp, supplier, nation, region
# WHERE p_partkey = ps_partkey
# AND s_suppkey = ps_suppkey
# AND s_nationkey = n_nationkey
# AND n_regionkey = r_regionkey
# AND r_name = 'EUROPE'
# )
# ORDER BY s_acctbal DESC, n_name, s_name, p_partkey
# LIMIT 100;
#
# Note:
# The correlated subquery is decorrelated into two branches sharing
# a "regional suppliers" chain:
# Branch A: region -> nation -> supplier (regional_suppliers)
# Branch B: partsupp join regional_suppliers -> groupby Min(supplycost)
# The main pipeline joins part -> partsupp -> regional_suppliers -> min_cost.
# Load all required tables with early column pruning to reduce
# intermediate data size (projection pushes down to Parquet reader)
# TODO: Remove manual projection once we support proper projection derivation
region = load_table("region", args.sf).select_columns(["r_regionkey", "r_name"])
nation = load_table("nation", args.sf).select_columns(
["n_nationkey", "n_name", "n_regionkey"]
)
supplier = load_table("supplier", args.sf).select_columns(
[
"s_suppkey",
"s_name",
"s_address",
"s_nationkey",
"s_phone",
"s_acctbal",
"s_comment",
]
)
part = load_table("part", args.sf).select_columns(
["p_partkey", "p_mfgr", "p_type", "p_size"]
)
partsupp = load_table("partsupp", args.sf).select_columns(
["ps_partkey", "ps_suppkey", "ps_supplycost"]
)
# Q2 parameters
region_name = "EUROPE"
part_size = 15
part_type_suffix = "BRASS"
# ── Branch A: build regional suppliers ──────────────────────────
region_filtered = region.filter(expr=col("r_name") == region_name)
nation_region = region_filtered.join(
nation,
num_partitions=16,
join_type="inner",
on=("r_regionkey",),
right_on=("n_regionkey",),
).select_columns(["n_nationkey", "n_name"])
# Materialize to avoid recomputing the region->nation->supplier chain
# in both Branch B and the main pipeline (Ray Data has no CSE).
regional_suppliers = nation_region.join(
supplier,
num_partitions=16,
join_type="inner",
on=("n_nationkey",),
right_on=("s_nationkey",),
).materialize()
# ── Branch B: min supply cost per part from regional suppliers ──
regional_partsupp = partsupp.join(
regional_suppliers.select_columns(["s_suppkey"]),
num_partitions=16,
join_type="inner",
on=("ps_suppkey",),
right_on=("s_suppkey",),
)
regional_partsupp = regional_partsupp.with_column(
"ps_supplycost_f", to_f64(col("ps_supplycost"))
).select_columns(["ps_partkey", "ps_supplycost_f"])
min_cost = regional_partsupp.groupby("ps_partkey").aggregate(
Min(on="ps_supplycost_f", alias_name="min_supplycost")
)
# ── Main pipeline ───────────────────────────────────────────────
# Filter part by size (pushes down to Parquet) and type suffix.
# Keep ends_with() filter after load_table to avoid pushing a UDF
# expression into parquet read predicate conversion.
part_filtered = part.filter(expr=col("p_size") == part_size)
part_filtered = part_filtered.filter(
expr=col("p_type").str.ends_with(part_type_suffix)
)
# Join part with partsupp
part_partsupp = part_filtered.join(
partsupp,
num_partitions=16,
join_type="inner",
on=("p_partkey",),
right_on=("ps_partkey",),
)
part_partsupp = part_partsupp.with_column(
"ps_supplycost_f", to_f64(col("ps_supplycost"))
).select_columns(["p_partkey", "p_mfgr", "ps_suppkey", "ps_supplycost_f"])
# Join with regional suppliers to enforce region constraint and get details
part_regional = part_partsupp.join(
regional_suppliers.select_columns(
[
"s_suppkey",
"s_name",
"s_address",
"s_phone",
"s_acctbal",
"s_comment",
"n_name",
]
),
num_partitions=16,
join_type="inner",
on=("ps_suppkey",),
right_on=("s_suppkey",),
).select_columns(
[
"p_partkey",
"p_mfgr",
"ps_supplycost_f",
"s_acctbal",
"s_name",
"s_address",
"s_phone",
"s_comment",
"n_name",
]
)
# Join with min cost and filter to keep only minimum-cost suppliers.
# Float equality is safe: both sides are cast from the same Decimal
# source via to_f64, and Min preserves the exact float64 value.
ds = part_regional.join(
min_cost,
num_partitions=16,
join_type="inner",
on=("p_partkey",),
right_on=("ps_partkey",),
)
ds = ds.filter(expr=col("ps_supplycost_f") == col("min_supplycost"))
# Cast Decimal s_acctbal to float64 for sort compatibility
ds = ds.with_column("s_acctbal", to_f64(col("s_acctbal")))
# Select output columns, sort, and limit
_ = (
ds.select_columns(
[
"s_acctbal",
"s_name",
"n_name",
"p_partkey",
"p_mfgr",
"s_address",
"s_phone",
"s_comment",
]
)
.sort(
key=["s_acctbal", "n_name", "s_name", "p_partkey"],
descending=[True, False, False, False],
)
.limit(100)
.materialize()
)
# Report arguments for the benchmark.
return vars(args)
run_tpch_benchmark("tpch_q2", benchmark_fn)
if __name__ == "__main__":
ray.init()
args = parse_tpch_args()
main(args)