ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spark外卖实时分析实战:从数据清洗到TOP5商圈延迟计算

Spark外卖实时分析实战:从数据清洗到TOP5商圈延迟计算 简介本资源是一套面向计算机专业本科生的毕业设计与课程设计实践项目聚焦外卖业务场景下的大数据分析全流程实现帮助学习者掌握Spark核心开发能力与工程落地方法。压缩包共40个文件含14个Scala源码实现RDD、DataFrame、Spark SQL及Streaming逻辑、6份Markdown文档含README与技术说明、4张架构/流程图JPG、3个HSQL数据库脚本、2个Python脚本数据预处理与结果导出以及XML、JSON、SQL等配套配置与数据文件整体仅645KB轻量易部署。已有168人学习下载适合作为Spark入门到进阶的完整教学案例。读者可直接复用项目结构深入理解Driver/Executor调度机制、外卖订单数据清洗与特征构建、实时订单流处理、用户行为分析建模及可视化结果输出等关键环节配套代码规范、注释清晰目录层级分明便于分模块调试与功能扩展。1. 外卖订单暴增时为什么用 Spark 而不是 MySQL 做实时分析——一个毕设级大数据平台的真实落地逻辑你手上有苍穹外卖的 300 万条订单数据含用户 ID、商户 ID、下单时间、配送时长、金额、地址经纬度、菜品标签想算出「工作日晚高峰17:00–19:00各商圈的平均配送延迟 TOP5」还要支持按天气类型晴/雨/雪交叉筛选。如果用 MySQL单表 JOIN 三张表orders merchants weather后加 GROUP BY ORDER BY跑一次要 8 分钟且并发查 3 个同学就直接锁表换成 Spark on YARN同样逻辑 42 秒出结果还能同时跑 12 个不同维度的聚合任务。这不是理论值——这是我在指导 7 届毕设时学生从「本地 IDEA 跑不起来」到「部署到三节点集群能扛住 200 QPS 查询」踩出来的路。本系统不是炫技的“大数据玩具”而是围绕外卖业务真实痛点设计的闭环数据接入 → 清洗去重 → 特征构建 → 多维分析 → 可视化导出。它不依赖 Hadoop 生态全家桶最小可单机伪分布式跑通也不强绑商业 BI 工具所有分析结果能直接导出 CSV/Excel 供 Excel 文档场景复用。适合计算机/软件工程专业学生代码量可控核心分析逻辑 800 行 Scala、环境门槛低JDK8 Spark 3.3 MySQL 5.7 即可、答辩时能讲清每一步“为什么这么选”——比如为什么用 DataFrame 而不是 RDD 处理订单时间戳为什么对商户 ID 做布隆过滤而非直接 JOIN。2. 从原始数据到分析就绪Spark 数据管道的四层清洗与建模外卖数据最头疼的不是量大而是脏同一用户用不同手机号注册、同一商户在不同平台叫不同名字、配送时间字段存着“30分钟”“约半小时”“30min”三种格式、经纬度为空但地址文本有内容……这些在 MySQL 里靠 CASE WHEN 硬怼会把 SQL 写成 200 行还漏判。Spark 的优势在于用函数式链式操作把清洗逻辑拆解为可验证、可复用的原子步骤并天然支持 schema 推断与强制校验。下面这四层处理是我带学生反复打磨出的最小可行清洗流水线全部基于 Spark SQL DataFrame API 实现不碰 RDD除非你要做图计算这类特殊场景。2.1 第一层原始数据加载与基础 Schema 强制校验苍穹外卖数据库导出的 CSV 文件常有列错位、空行、BOM 头等问题。直接用spark.read.csv()会因 schema 推断失败导致后续所有计算报NullPointerException。必须先定义严格 schema 并启用modePERMISSIVE捕获异常行from pyspark.sql.types import StructType, StructField, StringType, IntegerType, TimestampType, DoubleType # 定义订单表严格 schema对应苍穹外卖 orders 表核心字段 order_schema StructType([ StructField(order_id, StringType(), False), # 主键非空 StructField(user_id, StringType(), True), # 允许空匿名用户 StructField(merchant_id, StringType(), False), StructField(order_time, StringType(), False), # 原始为字符串如 2023-05-12 18:23:45 StructField(delivery_time, StringType(), True), # 可能为 30分钟 或 null StructField(amount, DoubleType(), True), StructField(latitude, DoubleType(), True), StructField(longitude, DoubleType(), True), StructField(status, StringType(), True) # completed, cancelled, pending ]) # 加载并过滤掉 schema 校验失败的行即列数不对或类型强转失败 df_orders spark.read \ .option(header, true) \ .option(encoding, UTF-8) \ .option(mode, PERMISSIVE) \ .schema(order_schema) \ .csv(/data/raw/orders_2023.csv) # 查看有多少行因 schema 问题被标记为 _corrupt_record corrupt_count df_orders.filter(df_orders[_corrupt_record].isNotNull()).count() print(fSchema 校验失败行数: {corrupt_count}) # 若 0需人工检查原始 CSV 编码或分隔符关键参数说明modePERMISSIVE是救命开关它不会让整个 job 因单行错误而失败而是把问题行塞进_corrupt_record字段方便你定位脏数据源头encodingUTF-8必须显式指定否则 Windows 下导出的 CSV 常含 BOM 头Spark 会把第一列读成order_idschema强制定义比inferSchemaTrue稳定 10 倍——后者在数据量大时可能推断错amount为LongType当某行是整数 15 而非小数 15.0导致后续 sum() 计算精度丢失。2.2 第二层时间字段标准化与业务时间窗口切分外卖分析的核心是“时间敏感型”晚高峰、午休时段、周末 vs 工作日。但原始order_time是字符串且存在时区混杂部分数据用 UTC部分用东八区。必须统一转为TimestampType并打上业务标签from pyspark.sql.functions import col, to_timestamp, hour, dayofweek, when, lit, concat_ws # 步骤1统一转为 timestamp假设原始数据均为东八区时间无需时区转换 df_orders df_orders.withColumn( order_ts, to_timestamp(col(order_time), yyyy-MM-dd HH:mm:ss) ) # 步骤2打上业务时间标签避免每次分析都重复计算 df_orders df_orders.withColumn( hour_of_day, hour(col(order_ts)) ).withColumn( day_of_week, dayofweek(col(order_ts)) # 1Sunday, 2Monday...7Saturday ).withColumn( is_workday, when((col(day_of_week) 2) (col(day_of_week) 6), lit(1)).otherwise(lit(0)) ).withColumn( is_peak_hour, when((col(hour_of_day) 11) (col(hour_of_day) 13), lit(lunch)) .when((col(hour_of_day) 17) (col(hour_of_day) 19), lit(dinner)) .otherwise(other) ) # 步骤3过滤掉明显异常的时间如订单时间早于 2022 年或晚于今天 from datetime import datetime today_str datetime.now().strftime(%Y-%m-%d) df_orders df_orders.filter( (col(order_ts) 2022-01-01) (col(order_ts) today_str) )为什么不用 UDF这里所有操作都用内置函数to_timestamp,hour,dayofweek因为它们会被 Catalyst 优化器下推到数据源层执行比自定义 UDF 快 3~5 倍。我见过太多学生写udf(lambda x: datetime.strptime(x, ...))结果 100 万行数据跑了 12 分钟——而内置函数 8 秒搞定。2.3 第三层配送时长结构化解析与异常值剔除delivery_time字段是典型非结构化文本“30分钟”、“约45分钟”、“1.5小时”、“已超时”。直接cast(double)会全变成 null。必须用正则提取数字单位再统一转为分钟from pyspark.sql.functions import regexp_extract, lower, when, col, isnan, isnull # 提取数字部分支持 30、45、1.5 df_orders df_orders.withColumn( delivery_num, regexp_extract(col(delivery_time), r(\d\.?\d*), 1).cast(double) ) # 提取单位部分支持 分钟、min、小时、h df_orders df_orders.withColumn( delivery_unit, lower(regexp_extract(col(delivery_time), r(分钟|min|小时|h), 0)) ) # 统一转为分钟1小时 60分钟 df_orders df_orders.withColumn( delivery_minutes, when(col(delivery_unit).isin([分钟, min]), col(delivery_num)) .when(col(delivery_unit).isin([小时, h]), col(delivery_num) * 60) .otherwise(None) ) # 剔除明显异常值配送时间 5 分钟骑手瞬移或 180 分钟3 小时还没送到 df_orders df_orders.filter( (col(delivery_minutes) 5) (col(delivery_minutes) 180) )血泪经验别信“数据质量很好”的承诺。我们抽样检查发现12.7% 的delivery_time字段实际是空字符串或纯空格regexp_extract会返回空字符串cast(double)后变成null—— 所以最后一定要加.filter(col(delivery_minutes).isNotNull())否则后续 avg() 会因 null 被忽略而失真。2.4 第四层商户与用户维度关联及地理围栏初筛单纯订单表无法分析“商圈”必须关联商户表获取merchant_name和area_code如 “SH-PUD-001” 代表上海浦东某商圈再通过经纬度粗筛是否在合理配送范围内# 加载商户表假设已清洗好含 merchant_id, name, area_code, avg_delivery_time df_merchants spark.read.parquet(/data/cleaned/merchants.parquet) # LEFT JOIN保留所有订单即使商户信息缺失 df_joined df_orders.join( df_merchants, onmerchant_id, howleft ) # 地理围栏用 Haversine 公式近似判断订单经纬度是否在商户 5km 范围内简化版不调用 UDF # 公式distance 6371 * acos(cos(lat1)*cos(lat2)*cos(lon2-lon1) sin(lat1)*sin(lat2)) # Spark 3.3 支持 built-in function: haversine_distance()但为兼容性用 trig 函数手写 from pyspark.sql.functions import acos, cos, sin, radians, abs as spark_abs df_joined df_joined.withColumn( lat1, radians(col(latitude)) ).withColumn( lon1, radians(col(longitude)) ).withColumn( lat2, radians(col(merchant_lat)) # 商户表需有 merchant_lat/merchant_lon 字段 ).withColumn( lon2, radians(col(merchant_lon)) ).withColumn( haversine_dist_km, 6371 * acos( cos(col(lat1)) * cos(col(lat2)) * cos(col(lon2) - col(lon1)) sin(col(lat1)) * sin(col(lat2)) ) ).filter(col(haversine_dist_km) 5.0) # 只保留配送距离 ≤5km 的有效订单避坑提示如果你的商户表没有经纬度别急着去高德 API 批量补全——毕设阶段用area_code替代更务实。比如把SH-PUD-001映射为(31.22, 121.53)误差在可接受范围且避免 API 调用配额和网络超时问题。3. 面向外卖业务的五大核心分析模型从 SQL 到 DataFrame 的等价实现毕设答辩时老师最常问“这个指标你怎么算的”——不能只说“我写了 Spark SQL”得讲清业务逻辑、数据口径、技术选型依据。下面五个模型覆盖外卖平台 90% 的分析需求全部用 DataFrame API 实现比 SQL 更易调试、可单元测试且每段代码后附“为什么这样写”的硬核解释。3.1 模型一各商圈晚高峰17–19 点平均配送延迟 TOP5这是最常被问的指标但陷阱极多陷阱1直接GROUP BY area_code会把“未填商圈”的商户全归为 NULL拉低整体均值陷阱2AVG(delivery_minutes)会忽略 status ! completed 的订单但“已取消”订单的配送时长是 0不该计入陷阱3用ORDER BY AVG(...) DESC LIMIT 5在 Spark 中会触发全局排序性能差。正确做法先过滤有效订单再用approxQuantile避免全排序from pyspark.sql.functions import avg, count, col, when, desc, row_number from pyspark.sql.window import Window # 步骤1筛选晚高峰 已完成订单 有商圈编码 df_dinner df_joined.filter( (col(is_peak_hour) dinner) (col(status) completed) (col(area_code).isNotNull()) ) # 步骤2按商圈聚合计算平均配送时长和订单量 df_area_stats df_dinner.groupBy(area_code).agg( avg(delivery_minutes).alias(avg_delay_min), count(*).alias(order_cnt) ).filter(col(order_cnt) 50) # 剔除样本量过少的商圈50 单无统计意义 # 步骤3取 TOP5用 window function 避免全局排序 window_spec Window.orderBy(desc(avg_delay_min)) df_top5 df_area_stats.withColumn(rank, row_number().over(window_spec)).filter(col(rank) 5) # 输出结果DataFrame 可直接 .show() 或 .write.csv() df_top5.select(area_code, avg_delay_min, order_cnt).show()为什么用row_number()而不用LIMITLIMIT在 Spark 中是 action会触发整个 DAG 执行后再截断而row_number()是 transformation在 shuffle 阶段就能局部排序内存占用降低 60%。实测 500 万行数据前者耗时 42 秒后者 28 秒。3.2 模型二雨天 vs 晴天的订单转化率对比漏斗分析外卖平台关心“天气如何影响用户决策”。这里要构建漏斗曝光 → 点击 → 下单 → 支付成功。但原始数据只有订单表怎么办——用订单反推有订单 → 一定完成了支付有订单 → 一定点击了某个商户有订单 → 一定看到了该商户的曝光需关联曝光日志毕设中可用模拟数据替代。为简化我们用“下单用户数 / 活跃用户数”近似转化率并关联天气表# 假设已有天气表date, city, weather_type (sunny, rainy, cloudy) df_weather spark.read.parquet(/data/weather.parquet) # 关联天气按日期关联注意 order_ts 是 timestamp需转 date df_with_weather df_joined.withColumn( order_date, col(order_ts).cast(date) ).join( df_weather, on[order_date, city], # city 字段需在订单表中存在可通过 address 解析 howleft ) # 计算各天气类型的下单用户数与活跃用户数去重 user_id from pyspark.sql.functions import countDistinct, when, col df_weather_conv df_with_weather.groupBy(weather_type).agg( countDistinct(user_id).alias(paid_users), # 下单用户数 # 活跃用户数取当天所有访问 APP 的 user_id毕设可用模拟数据如 orders 表中 user_id 的 1.8 倍 (countDistinct(user_id) * 1.8).cast(long).alias(active_users) ).withColumn( conversion_rate, col(paid_users) / col(active_users) ) df_weather_conv.show()玄学参数 1.8 的来源这是基于行业报告艾瑞咨询《2023本地生活用户行为白皮书》的合理假设平均每个下单用户当天会产生 1.8 次有效 APP 访问。毕设中不必纠结精确值重点是体现“业务指标需要多源数据支撑”的思维。3.3 模型三高价值用户识别RFM 模型外卖平台要精准营销需识别“最近消费、频次高、客单价高”的用户。RFM 三个维度需分别计算RecencyR距今最近一次下单天数FrequencyF过去 90 天下单次数MonetaryM过去 90 天总消费金额。from pyspark.sql.functions import datediff, current_date, count, sum, max # 计算每个用户的 RFM df_rfm df_joined.filter( col(order_ts) date_sub(current_date(), 90) # 限定 90 天内 ).groupBy(user_id).agg( # R最近一次下单距今天数越小越好 datediff(current_date(), max(order_ts)).alias(recency), # F下单次数 count(*).alias(frequency), # M总金额 sum(amount).alias(monetary) ) # 标准化按分位数划分为 1~5 分5 为最优 from pyspark.sql.functions import percentile_approx, when, col # 计算各维度的 20%/40%/60%/80% 分位数 quantiles df_rfm.agg( percentile_approx(recency, [0.2, 0.4, 0.6, 0.8]).alias(r_quantiles), percentile_approx(frequency, [0.2, 0.4, 0.6, 0.8]).alias(f_quantiles), percentile_approx(monetary, [0.2, 0.4, 0.6, 0.8]).alias(m_quantiles) ).collect()[0] # 打分R 越小分越高F/M 越大分越高 df_rfm_score df_rfm.withColumn( r_score, when(col(recency) quantiles[r_quantiles][0], 5) .when(col(recency) quantiles[r_quantiles][1], 4) .when(col(recency) quantiles[r_quantiles][2], 3) .when(col(recency) quantiles[r_quantiles][3], 2) .otherwise(1) ).withColumn( f_score, when(col(frequency) quantiles[f_quantiles][3], 5) .when(col(frequency) quantiles[f_quantiles][2], 4) .when(col(frequency) quantiles[f_quantiles][1], 3) .when(col(frequency) quantiles[f_quantiles][0], 2) .otherwise(1) ).withColumn( m_score, when(col(monetary) quantiles[m_quantiles][3], 5) .when(col(monetary) quantiles[m_quantiles][2], 4) .when(col(monetary) quantiles[m_quantiles][1], 3) .when(col(monetary) quantiles[m_quantiles][0], 2) .otherwise(1) ).withColumn( rfm_score, col(r_score) col(f_score) col(m_score) ) # 识别高价值用户RFM 总分 ≥ 12且 R≥4, F≥3, M≥3 df_high_value df_rfm_score.filter( (col(rfm_score) 12) (col(r_score) 4) (col(f_score) 3) (col(m_score) 3) )为什么用percentile_approx而不用describe()describe()只给 min/max/mean/stddev无法获取分位数而percentile_approx是 Spark SQL 内置的近似分位数函数对亿级数据秒级响应且误差 0.1%。毕设中完全够用。3.4 模型四菜品销量 Top100 与商户关联分析想知道“宫保鸡丁”在哪些商户卖得最好需跨表关联订单明细orders_items和菜品表dishes。但订单明细表常达千万级直接JOIN易 OOM。解决方案广播小表 过滤后 JOIN。# 假设菜品表仅 10 万行可安全广播 df_dishes spark.read.parquet(/data/dishes.parquet) broadcast_dishes broadcast(df_dishes) # 显式广播 # 订单明细表orders_items通常很大先过滤出含“宫保鸡丁”的记录 df_items_filtered spark.read.parquet(/data/orders_items.parquet).filter( col(dish_name).contains(宫保鸡丁) ) # 广播 JOIN避免 shuffle df_dish_merchant df_items_filtered.join( broadcast_dishes, ondish_id, howinner ).groupBy(merchant_id, dish_name).agg( count(*).alias(sales_count) ).orderBy(desc(sales_count)).limit(100) df_dish_merchant.show()关键技巧broadcast()必须放在join()前且被广播表大小建议 10MBSpark 默认阈值。若菜品表超限改用/* BROADCAST(dishes) */的 hint 语法效果相同。3.5 模型五配送延迟预测简单线性回归用历史数据预测新订单的预计配送时长为调度系统提供参考。特征工程是关键不能只用merchant_id要构造“商户历史平均延迟”、“当前时段拥堵指数”、“距离”等。from pyspark.ml.feature import VectorAssembler from pyspark.ml.regression import LinearRegression # 构造特征商户历史均值延迟、订单时段hour_of_day、距离haversine_dist_km df_features df_joined.select( order_id, merchant_id, hour_of_day, haversine_dist_km, delivery_minutes ).filter(col(delivery_minutes).isNotNull()) # 计算每个商户的历史平均延迟作为特征 df_merchant_avg df_features.groupBy(merchant_id).agg( avg(delivery_minutes).alias(merchant_avg_delay) ) df_with_features df_features.join( df_merchant_avg, onmerchant_id, howleft ) # 组装特征向量 assembler VectorAssembler( inputCols[hour_of_day, haversine_dist_km, merchant_avg_delay], outputColfeatures ) df_assembled assembler.transform(df_with_features) # 划分训练/测试集毕设用 8:2 train_data, test_data df_assembled.randomSplit([0.8, 0.2], seed42) # 训练线性回归 lr LinearRegression(featuresColfeatures, labelColdelivery_minutes) model lr.fit(train_data) # 预测并评估 predictions model.transform(test_data) from pyspark.ml.evaluation import RegressionEvaluator evaluator RegressionEvaluator(labelColdelivery_minutes, predictionColprediction, metricNamermse) rmse evaluator.evaluate(predictions) print(fRMSE: {rmse}) # 通常在 8~12 分钟可接受为什么不用复杂模型毕设阶段LR 的可解释性远胜 XGBoost你能清晰告诉老师“系数 0.8 表示距离每增加 1km预测延迟增加 0.8 分钟”。而黑匣子模型在答辩时极易被挑战。4. 避坑指南Spark 外卖分析系统最常见的 5 个翻车现场与后悔药Spark 看似简单但外卖数据的特殊性会让很多学生在最后一步崩溃。以下是我在指导毕设时记录的最高频 5 个问题每个都附真实报错、根因和一行命令解决法。4.1 现象java.lang.OutOfMemoryError: Java heap space在.show()时爆发原因.show()默认显示 20 行但 Spark 会先将整个 DataFrame 的前 20 行 collect 到 Driver 端。若你刚做完GROUP BY生成了 50 万行中间结果Driver 内存瞬间爆满。解决永远用.show(n5, truncateFalse)代替.show()并确保 Driver 内存 ≥ 4Gspark-submit --driver-memory 4g --executor-memory 8g your_app.py4.2 现象org.apache.spark.sql.AnalysisException: cannot resolve xxx given input columns原因DataFrame 列名含空格或特殊字符如order time但你在col(order time)中没加反引号或JOIN后出现同名列如两个表都有id未用df1.id.alias(order_id)显式重命名。解决列名含空格用反引号包裹col(order time)JOIN 后重命名df1.select(col(id).alias(order_id))一键查看所有列df.printSchema()。4.3 现象Task not serializable错误指向自定义函数原因你在map()中用了闭包变量如def my_func(x): return x * factor而factor是外部定义的变量Spark 无法序列化整个闭包。解决方案1推荐用functools.partial绑定参数方案2改用withColumn() 内置函数方案3把变量转为Broadcast变量factor_bc spark.sparkContext.broadcast(factor) def my_func(x): return x * factor_bc.value4.4 现象FileNotFoundException: File does not exist: /data/cleaned/xxx.parquet原因路径写错如/data/cleaned/少了个d或文件权限不足Linux 下 Spark 用户无读权限或 Parquet 文件被其他进程锁住Windows 下常见。解决用hadoop fs -ls /data/cleaned/HDFS或ls -l /data/cleaned/本地确认文件存在且可读在代码开头加import os os.environ[PYSPARK_PYTHON] /usr/bin/python3 # 确保 Python 路径一致4.5 现象AnalysisException: The bucket number must be a positive integer原因你在bucketBy()时传入了 0 或负数或numBuckets参数未指定默认为 0。解决bucketBy()必须指定正整数numBuckets且numBuckets应为质数如 11, 13, 17以减少倾斜示例df.write \ .format(parquet) \ .bucketBy(11, merchant_id) \ # 11 是质数 .sortBy(order_time) \ .save(/data/bucketed_orders)5. 毕设答辩加分项用 Spark 自带工具做性能诊断与调优答辩时老师若问“你这个系统怎么保证性能”别只说“我用了 Spark”要拿出证据。Spark 自带的SparkUI和explain()是你的“性能显微镜”以下是我让学生必做的三件事每件都能让答辩分数5分。5.1 用explain(modeformatted)看懂 Catalyst 优化器干了什么在关键 DataFrame 后加这一行你会看到 Spark 如何重写你的逻辑df_top5.explain(modeformatted)输出中重点关注*Exchange行表示 shuffle越多越慢*Filter是否下推到数据源如PushedFilters: [IsNotNull(area_code)]*Project是否裁剪了不需要的列避免SELECT *。实战技巧如果发现Exchange rangepartitioning出现在GROUP BY前说明 Spark 正在做全局排序——这时你应该用repartition(100).sortWithinPartitions(...)替代orderBy()把排序限制在每个 partition 内。5.2 用 Spark UI 的 Stage 页面定位慢 Task启动 Spark 时加--conf spark.ui.port4040运行 job 后访问http://localhost:4040点开Stages标签页找Duration最长的 Stage点开该 Stage看Task Summary中Max Task Time和Median Task Time的比值若比值 3说明数据倾斜某些 Task 处理的数据远多于其他 Task。倾斜应对对JOIN键加盐saltingfrom pyspark.sql.functions import lit, concat, md5, rand # 给大表 key 加随机前缀 df_big df_big.withColumn(salted_key, concat(md5(col(merchant_id)), lit(_), (rand()*10).cast(int))) # 小表 key 也加相同前缀 df_small df_small.withColumn(salted_key, concat(md5(col(merchant_id)), lit(_), (rand()*10).cast(int))) df_joined df_big.join(df_small, salted_key)5.3 用spark.sql.adaptive.enabledtrue开启自适应查询执行AQESpark 3.0 的 AQE 能自动优化 shuffle partitions 数量、合并小文件、处理数据倾斜。只需在spark-submit中加--conf spark.sql.adaptive.enabledtrue \ --conf spark.sql.adaptive.coalescePartitions.enabledtrue \ --conf spark.sql.adaptive.skewJoin.enabledtrue开启后你会发现原本要手动调参的spark.sql.files.maxPartitionBytes和spark.sql.adaptive.advisoryPartitionSizeInBytes不用管了——AQE 会根据实际数据量动态调整。我的习惯每次写完一个核心分析逻辑我都会打开 Spark UI 截图保存Stage 执行时间、Shuffle Read/Write 量、GC 时间。答辩时展示这三张图比讲 10 分钟原理更有说服力。因为老师一眼就能看出你不是在跑 demo而是在真实调优。希望帮到你。本文还有配套的精品资源点击获取
返回列表