<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>后端开发 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Wed, 26 Oct 2022 09:30:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Spark 从入门到精通：架构原理、RDD/DataFrame、调优踩坑与电商实战全指南</title>
      <link>https://ftmao.com/blog/posts/spark-beginner-to-mastery/</link>
      <pubDate>Wed, 26 Oct 2022 09:30:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/spark-beginner-to-mastery/</guid>
      <description>&lt;h1 id=&#34;spark-从入门到精通架构原理rdddataframe调优踩坑与电商实战全指南&#34;&gt;Spark 从入门到精通：架构原理、RDD/DataFrame、调优踩坑与电商实战全指南&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：写过一两年后端（Java/Scala/Python 皆可）、第一次要碰大数据的同学；或者用 PySpark 写过一些脚本、但一遇到性能问题就抓瞎的同学。
写这篇文章时（2022 年 10 月），Spark 3.3 刚发布，DataFrame 早已是绝对主流，Flink 在实时方向上攻城略地。但离线批处理、数仓 ETL、特征工程这些重活，Spark 依然是绕不开的那座山。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;全文六篇，顺序就是学习的顺序：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;一 为什么是 Spark&amp;lt;br/&amp;gt;从单机到分布式&amp;#34;] --&amp;gt; B[&amp;#34;二 架构与原理&amp;lt;br/&amp;gt;Driver Executor DAG&amp;#34;]
    B --&amp;gt; C[&amp;#34;三 RDD 与 DataFrame&amp;lt;br/&amp;gt;两代 API 的分水岭&amp;#34;]
    C --&amp;gt; D[&amp;#34;四 电商实战&amp;lt;br/&amp;gt;宽表 漏斗 特征&amp;#34;]
    D --&amp;gt; E[&amp;#34;五 调优与踩坑&amp;lt;br/&amp;gt;倾斜 OOM 小文件&amp;#34;]
    E --&amp;gt; F[&amp;#34;六 AI 时代&amp;lt;br/&amp;gt;少写代码 多写提示词&amp;#34;]
    style F fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;为什么是这个顺序？第一篇解决「&lt;strong&gt;为什么要用它&lt;/strong&gt;」；第二篇解决「&lt;strong&gt;它在集群里怎么跑&lt;/strong&gt;」——不理解这个，写出来的代码就是玄学；第三篇解决「&lt;strong&gt;代码怎么写&lt;/strong&gt;」；第四篇用真实业务把知识串起来；第五篇是血泪史，生产环境和本地 Demo 是两个物种；第六篇聊怎么让 AI 替你干活。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第一篇-为什么是-spark一台机器装不下的时代&#34;&gt;第一篇 为什么是 Spark：一台机器装不下的时代&lt;/h2&gt;
&lt;h3 id=&#34;11-一个电商场景把问题引出来&#34;&gt;1.1 一个电商场景把问题引出来&lt;/h3&gt;
&lt;p&gt;假设你在一家中型电商公司做后端。双十一刚过，老板要看一份报表：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;昨天所有下单用户，按城市分组，统计成交额、下单次数、客单价，再看这批用户里有多少是 30 天内的新客。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;订单表 8 亿行，用户表 4000 万行，行为日志（埋点）一天 20 亿条。MySQL 里跑这个 SQL？&lt;code&gt;GROUP BY&lt;/code&gt; 一个 8 亿行的表，跑一晚上 MySQL 先把你杀了——不，是 DBA 先把你杀了。&lt;/p&gt;
&lt;p&gt;你想到的办法是「把数据导出来，写个 Python 脚本跑」。8 亿行订单数据，CSV 导出大概 400GB。你的开发机内存 32GB。&lt;strong&gt;装不下，这就是大数据的全部问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解决思路只有两个方向：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph 思路一[&amp;#34;思路一：垂直扩展（Stack Overflow 式的天真）&amp;#34;]
        A1[&amp;#34;换个 1TB 内存的机器&amp;#34;] --&amp;gt; A2[&amp;#34;贵得离谱&amp;lt;br/&amp;gt;且有物理上限&amp;#34;]
    end
    subgraph 思路二[&amp;#34;思路二：水平扩展（唯一出路）&amp;#34;]
        B1[&amp;#34;把 400GB 切成 1000 份&amp;#34;] --&amp;gt; B2[&amp;#34;1000 台机器&amp;lt;br/&amp;gt;每台处理 400MB&amp;#34;]
        B2 --&amp;gt; B3[&amp;#34;各自算局部结果&amp;lt;br/&amp;gt;最后合并&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;思路二就是分布式计算。但自己写 1000 台机器的协调代码？分发任务、处理机器挂掉、合并结果、处理网络超时……你会写出第二个 Hadoop。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spark 就是替你把这些脏活全包了&lt;/strong&gt;：你只写「对这批数据做什么变换」，它负责切分、调度、容错、合并。&lt;/p&gt;
&lt;h3 id=&#34;12-spark-在生态里的位置&#34;&gt;1.2 Spark 在生态里的位置&lt;/h3&gt;
&lt;p&gt;新人最容易被一堆名词搞晕：Hadoop、HDFS、YARN、Hive、Spark、Flink。一张图说清楚：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph 存储层
        HDFS[&amp;#34;HDFS&amp;lt;br/&amp;gt;分布式文件系统&amp;lt;br/&amp;gt;（把文件切块存到多台机器）&amp;#34;]
        S3[&amp;#34;S3 / OSS&amp;lt;br/&amp;gt;对象存储&amp;#34;]
    end
    subgraph 资源层
        YARN[&amp;#34;YARN / K8s&amp;lt;br/&amp;gt;集群资源调度&amp;lt;br/&amp;gt;（给任务分配 CPU 和内存）&amp;#34;]
    end
    subgraph 计算层
        MR[&amp;#34;MapReduce&amp;lt;br/&amp;gt;老一辈 磁盘IO 慢&amp;#34;]
        SPARK[&amp;#34;Spark&amp;lt;br/&amp;gt;内存计算 快&amp;lt;br/&amp;gt;本篇主角&amp;#34;]
        FLINK[&amp;#34;Flink&amp;lt;br/&amp;gt;流处理专精&amp;#34;]
    end
    subgraph 应用层
        HIVE[&amp;#34;Hive/Spark SQL&amp;lt;br/&amp;gt;SQL 写报表&amp;#34;]
        DS[&amp;#34;数据科学&amp;lt;br/&amp;gt;MLlib 特征工程&amp;#34;]
    end
    HDFS --&amp;gt; YARN
    S3 --&amp;gt; YARN
    YARN --&amp;gt; MR
    YARN --&amp;gt; SPARK
    YARN --&amp;gt; FLINK
    SPARK --&amp;gt; HIVE
    SPARK --&amp;gt; DS
    style SPARK fill:#ffd699&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个关键认知：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spark 不是存储系统&lt;/strong&gt;，它没有自己的存储，数据放在 HDFS、Hive 表、S3 甚至 MySQL 里，它读进来算完再写回去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spark 也不是资源调度器&lt;/strong&gt;，它跑在 YARN 或 Kubernetes 上，向它们「申请」机器。&lt;/li&gt;
&lt;li&gt;Spark 相对 MapReduce 的快，核心就一句话：&lt;strong&gt;中间结果放内存，不落地磁盘&lt;/strong&gt;。MapReduce 每步都要写磁盘，迭代式计算（比如机器学习跑 50 轮迭代）就慢到不可用。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑预告&lt;/strong&gt;：很多人学 Spark 的第一步是去装 Hadoop 全家桶，装了一周还没跑起来 Hello World。其实本地学习完全不需要——Spark 有 &lt;code&gt;local&lt;/code&gt; 模式， pip 装个 &lt;code&gt;pyspark&lt;/code&gt; 就能跑。下一篇的 Demo 全是单机可运行的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第二篇-架构与原理代码提交之后到底发生了什么&#34;&gt;第二篇 架构与原理：代码提交之后到底发生了什么&lt;/h2&gt;
&lt;p&gt;这是最重要的一篇。Spark 调优 80% 靠对这套架构的直觉，剩下 20% 才是参数。&lt;/p&gt;
&lt;h3 id=&#34;21-一次-spark-任务的全貌&#34;&gt;2.1 一次 Spark 任务的全貌&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    CODE[&amp;#34;你的代码&amp;lt;br/&amp;gt;spark.read... .groupBy... .write...&amp;#34;] --&amp;gt; DRV
    subgraph Cluster[&amp;#34;集群（YARN / K8s）&amp;#34;]
        subgraph DRV[&amp;#34;Driver（司机/总指挥）&amp;#34;]
            DAG[&amp;#34;DAG Scheduler&amp;lt;br/&amp;gt;把代码翻译成执行计划&amp;lt;br/&amp;gt;切成若干 Stage&amp;#34;]
            TS[&amp;#34;Task Scheduler&amp;lt;br/&amp;gt;把 Stage 拆成 Task&amp;lt;br/&amp;gt;派发给 Executor&amp;#34;]
        end
        subgraph EXE[&amp;#34;Executor × N（干活的工人）&amp;#34;]
            T1[&amp;#34;Task 1&amp;lt;br/&amp;gt;处理 Partition 1&amp;#34;]
            T2[&amp;#34;Task 2&amp;lt;br/&amp;gt;处理 Partition 2&amp;#34;]
            T3[&amp;#34;Task 3&amp;lt;br/&amp;gt;处理 Partition 3&amp;#34;]
            CACHE[&amp;#34;内存/磁盘&amp;lt;br/&amp;gt;缓存中间数据&amp;#34;]
        end
    end
    TS --&amp;gt; T1
    TS --&amp;gt; T2
    TS --&amp;gt; T3
    T1 -.shuffle 阶段互相交换数据.-&amp;gt; T2&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键角色就三个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Driver&lt;/strong&gt;：你的 &lt;code&gt;main&lt;/code&gt; 函数（或 PySpark 脚本）运行的地方。它不干活，它&lt;strong&gt;发号施令&lt;/strong&gt;——分析你的代码、生成执行计划、把任务派发出去、回收结果。Driver 挂了，整个任务就完了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Executor&lt;/strong&gt;：集群里真正干活的 JVM 进程，每个 Executor 有若干核，每个核同时跑一个 Task。数据是**分区（Partition）**存储的，一个 Task 处理一个 Partition。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shuffle&lt;/strong&gt;：当操作需要跨机器汇总数据（&lt;code&gt;groupBy&lt;/code&gt;、&lt;code&gt;join&lt;/code&gt;、&lt;code&gt;distinct&lt;/code&gt;），数据就要在网络上重新分发——这叫 Shuffle。&lt;strong&gt;Shuffle 是 Spark 任务慢的第一大原因&lt;/strong&gt;，后文反复出现。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-懒执行lazy-evaluation为什么你的代码没报错却在运行时炸了&#34;&gt;2.2 懒执行（Lazy Evaluation）：为什么你的代码&amp;quot;没报错&amp;quot;却在运行时炸了&lt;/h3&gt;
&lt;p&gt;新手最常踩的认知坑：&lt;strong&gt;Spark 代码写下去并不会立刻执行&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;read&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;csv(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders.csv&amp;#34;&lt;/span&gt;)          &lt;span style=&#34;color:#75715e&#34;&gt;# 没执行！只是记录&amp;#34;要读个文件&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df2 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;amount &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;)           &lt;span style=&#34;color:#75715e&#34;&gt;# 没执行！只是记录&amp;#34;要过滤&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df3 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; df2&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;)    &lt;span style=&#34;color:#75715e&#34;&gt;# 没执行！只是记录&amp;#34;要分组聚合&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df3&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;save(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&amp;#34;&lt;/span&gt;)                   &lt;span style=&#34;color:#75715e&#34;&gt;# 现在才真正开始跑！&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;读数据、过滤、分组这些叫 &lt;strong&gt;Transformation（变换）&lt;/strong&gt;，只是往计划里加节点；&lt;code&gt;write&lt;/code&gt;、&lt;code&gt;count&lt;/code&gt;、&lt;code&gt;collect&lt;/code&gt;、&lt;code&gt;show&lt;/code&gt; 这些叫 &lt;strong&gt;Action（行动）&lt;/strong&gt;，碰到 Action，Driver 才把整条链路翻译成 DAG（有向无环图），切成 Stage，真正派发执行。&lt;/p&gt;
&lt;p&gt;这个设计的好处是 &lt;strong&gt;优化&lt;/strong&gt;：Spark 的 Catalyst 优化器能看到整条链路，比如发现你过滤条件写晚了，它可以自动把 &lt;code&gt;filter&lt;/code&gt; 下推到读数据时就做（Predicate Pushdown），少读一半数据。&lt;/p&gt;
&lt;p&gt;但坑也随之而来：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 1：错误行号的谎言&lt;/strong&gt;
刚转 Spark 时我排查一个任务失败，日志说第 87 行 &lt;code&gt;df.write.save()&lt;/code&gt; 报错 &amp;ldquo;文件不存在&amp;rdquo;。我盯着 87 行看了半小时——write 语法没问题啊！后来才明白：&lt;strong&gt;真正的错误发生在第 12 行的 &lt;code&gt;spark.read&lt;/code&gt;（路径写错了），但因为是懒执行，直到 87 行触发 Action 才爆出来&lt;/strong&gt;。看 Spark 报错，永远要从日志里的执行计划往上找 Transformation 的源头，而不是看报错行号。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;23-stage-是怎么切出来的宽依赖与窄依赖&#34;&gt;2.3 Stage 是怎么切出来的：宽依赖与窄依赖&lt;/h3&gt;
&lt;p&gt;理解 Shuffle 就理解了 Stage 的切分：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph Stage1[&amp;#34;Stage 1（窄依赖，流水线执行）&amp;#34;]
        P1[&amp;#34;Partition 1&amp;lt;br/&amp;gt;读文件&amp;#34;] --&amp;gt; F1[&amp;#34;filter&amp;lt;br/&amp;gt;过滤&amp;#34;]
        P2[&amp;#34;Partition 2&amp;lt;br/&amp;gt;读文件&amp;#34;] --&amp;gt; F2[&amp;#34;filter&amp;lt;br/&amp;gt;过滤&amp;#34;]
    end
    subgraph SHUFFLE[&amp;#34;⚡ Shuffle（跨网络搬数据）&amp;#34;]
        direction TB
        S[&amp;#34;按 city 重新分区&amp;lt;br/&amp;gt;同 city 的数据&amp;lt;br/&amp;gt;汇聚到同一 Partition&amp;#34;]
    end
    subgraph Stage2[&amp;#34;Stage 2&amp;#34;]
        G1[&amp;#34;Partition A&amp;lt;br/&amp;gt;北京的订单&amp;lt;br/&amp;gt;sum(amount)&amp;#34;]
        G2[&amp;#34;Partition B&amp;lt;br/&amp;gt;上海的订单&amp;lt;br/&amp;gt;sum(amount)&amp;#34;]
    end
    F1 --&amp;gt; SHUFFLE
    F2 --&amp;gt; SHUFFLE
    SHUFFLE --&amp;gt; G1
    SHUFFLE --&amp;gt; G2
    style SHUFFLE fill:#ffcccc&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;窄依赖&lt;/strong&gt;（filter、map）：子 Partition 只依赖父的一个 Partition，可以像流水线一样在同一台机器一口气算完，不分 Stage。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宽依赖&lt;/strong&gt;（groupBy、join、distinct）：子 Partition 需要父的&lt;strong&gt;所有&lt;/strong&gt; Partition 的数据，必须 Shuffle，Stage 在这里断开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个 Stage 内的失败重试只重跑这个 Stage&lt;/strong&gt;。所以 Spark 的容错单位是 Stage——这也是为什么 Spark 会把 Shuffle 的中间结果落盘（对，嘴上说自己内存计算，Shuffle 还是要写磁盘的），挂了能从这里恢复，而不是从头再来。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第三篇-rdd-与-dataframe两代-api-的分水岭&#34;&gt;第三篇 RDD 与 DataFrame：两代 API 的分水岭&lt;/h2&gt;
&lt;h3 id=&#34;31-rdd能看清每个零件但要自己造发动机&#34;&gt;3.1 RDD：能看清每个零件，但要自己造发动机&lt;/h3&gt;
&lt;p&gt;RDD（弹性分布式数据集）是 Spark 1.x 的核心抽象。用它写上面的需求：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-scala&#34; data-lang=&#34;scala&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// RDD 风格：你告诉 Spark 每一步&amp;#34;怎么做&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;).&lt;/span&gt;toDouble &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;     &lt;span style=&#34;color:#75715e&#34;&gt;// 自己解析 CSV，自己转类型
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;map&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;line &lt;span style=&#34;color:#66d9ef&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;line&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;),&lt;/span&gt;           &lt;span style=&#34;color:#75715e&#34;&gt;// 自己取城市字段做 key
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                line&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;).&lt;/span&gt;toDouble&lt;span style=&#34;color:#f92672&#34;&gt;))&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// 自己取金额做 value
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;reduceByKey&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;                          &lt;span style=&#34;color:#75715e&#34;&gt;// 按 key 聚合
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;saveAsTextFile&lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;RDD 的问题是：&lt;strong&gt;Spark 看不懂你在干什么&lt;/strong&gt;。&lt;code&gt;filter(_.split(&amp;quot;,&amp;quot;)(5).toDouble &amp;gt; 100)&lt;/code&gt; 对优化器来说就是一个黑盒 lambda，它不知道你在过滤哪列，没法做谓词下推，没法选更好的 join 策略。而且 JVM 里每一行都是一个 Java 对象，序列化开销大、GC 压力巨大。&lt;/p&gt;
&lt;h3 id=&#34;32-dataframe你只说要什么优化器替你想怎么做&#34;&gt;3.2 DataFrame：你只说&amp;quot;要什么&amp;quot;，优化器替你想&amp;quot;怎么做&amp;quot;&lt;/h3&gt;
&lt;p&gt;DataFrame（Spark 1.3 引入，2.0 后与 Dataset 合并）给数据加了&lt;strong&gt;Schema（表结构）&lt;/strong&gt;，从此 Spark 能看懂你的意图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# DataFrame 风格：声明式&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;read
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;schema(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id STRING, city STRING, amount DOUBLE&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;csv(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders.csv&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount &amp;gt; 100&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&amp;#34;&lt;/span&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;两者差距有多大？同样的逻辑，DataFrame 通常比 RDD &lt;strong&gt;快 2~10 倍&lt;/strong&gt;，原因：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph RDD路径[&amp;#34;RDD：信任程序员&amp;#34;]
        R1[&amp;#34;你的 lambda&amp;#34;] --&amp;gt; R2[&amp;#34;逐行 Java 对象&amp;lt;br/&amp;gt;序列化/GC 开销大&amp;lt;br/&amp;gt;优化器爱莫能助&amp;#34;]
    end
    subgraph DF路径[&amp;#34;DataFrame：信任优化器&amp;#34;]
        D1[&amp;#34;声明式表达式&amp;#34;] --&amp;gt; D2[&amp;#34;Catalyst 优化器&amp;lt;br/&amp;gt;逻辑计划优化&amp;lt;br/&amp;gt;谓词下推/列裁剪&amp;#34;]
        D2 --&amp;gt; D3[&amp;#34;Tungsten 执行引擎&amp;lt;br/&amp;gt;二进制内存布局&amp;lt;br/&amp;gt;代码生成 绕开 GC&amp;#34;]
    end
    style DF路径 fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话结论（2022 年视角）&lt;/strong&gt;：新业务&lt;strong&gt;永远用 DataFrame/Spark SQL 写&lt;/strong&gt;，RDD 只在两种情况下出现——操作的是非结构化数据（比如解析一堆不规则日志），或者需要逐分区精细控制（&lt;code&gt;mapPartitions&lt;/code&gt; 里建数据库连接池这种）。面试让你手写 RDD wordcount 是考察理解，不是让你生产上这么写。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;33-本地就能跑的完整-demo&#34;&gt;3.3 本地就能跑的完整 Demo&lt;/h3&gt;
&lt;p&gt;别急着上集群。装个 Python 环境就能跑：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install pyspark&lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt;3.3.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;造一份模拟电商订单数据，跑通「读 → 变换 → 聚合 → 写」全流程：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo01_basic.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; pyspark.sql &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; SparkSession
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; pyspark.sql &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; functions &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; F
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (SparkSession&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;builder
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;appName(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;demo01&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;master(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;local[*]&amp;#34;&lt;/span&gt;)      &lt;span style=&#34;color:#75715e&#34;&gt;# 本地模式，用满 CPU 核&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;getOrCreate())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 造数据：100 万行模拟订单&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;range(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1_000_000&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;select(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;concat(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ORD&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;50000&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;element_at(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;array([F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(c) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; c &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                           [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;北京&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;上海&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;广州&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;深圳&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;杭州&amp;#34;&lt;/span&gt;]]),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cast(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;int&amp;#34;&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;rand() &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2000&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;date_add(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                         &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cast(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;int&amp;#34;&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_date&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (orders
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;city&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_cnt&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;round(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;gmv&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;round(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;avg(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;avg_price&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;orderBy(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;desc(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;gmv&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;result&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;show()          &lt;span style=&#34;color:#75715e&#34;&gt;# Action：真正触发执行&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;result&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mode(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overwrite&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output/city_gmv&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;stop()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;运行：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python demo01_basic.py&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;输出：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;+----+---------+-----------+---------+
|city|order_cnt|        gmv|avg_price|
+----+---------+-----------+---------+
|  深圳|   190231|...        |  1050.12|
|  北京|   190087|...        |  1049.87|
...
+----+---------+-----------+---------+&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个 Demo 麻雀虽小，五脏俱全：&lt;code&gt;spark.range&lt;/code&gt; 造数、&lt;code&gt;element_at&lt;/code&gt; 数组取值、&lt;code&gt;rand&lt;/code&gt; 随机数、聚合多指标、写 Parquet。本地玩熟了，语法迁移到集群上&lt;strong&gt;一行都不用改&lt;/strong&gt;——这就是 Spark 的优雅之处。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 2：Windows 上的 winutils.exe&lt;/strong&gt;
我在 Windows 笔记本上第一次跑 PySpark，直接 &lt;code&gt;winutils.exe&lt;/code&gt; 缺失报错 + 一堆权限异常。解决办法是下载对应 Hadoop 版本的 &lt;code&gt;winutils.exe&lt;/code&gt; 放到 &lt;code&gt;%HADOOP_HOME%\bin&lt;/code&gt; 并设置环境变量。后来学乖了：&lt;strong&gt;学习期直接 WSL2 或者 Docker 起个 Linux 环境&lt;/strong&gt;，少生一肚子气。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第四篇-电商实战把知识串成业务&#34;&gt;第四篇 电商实战：把知识串成业务&lt;/h2&gt;
&lt;p&gt;这一篇用三个电商真实场景，把前面的知识点全部用起来。这三个场景也是面试里最高频的&amp;quot;你做过什么项目&amp;quot;素材。&lt;/p&gt;
&lt;h3 id=&#34;41-场景一订单宽表每日-etl&#34;&gt;4.1 场景一：订单宽表（每日 ETL）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;业务&lt;/strong&gt;：订单数据分散在三张表——订单主表（order_id、user_id、amount、pay_time）、订单明细表（order_id、sku_id、quantity）、商品维表（sku_id、category、brand）。数据仓库要求每天凌晨产出一张&amp;quot;订单明细宽表&amp;quot;，下游 BI 直接查它。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo02_dwd_wide_table.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ods.orders&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;items  &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ods.order_items&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;skus   &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dim.sku&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;wide &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (items
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;join(orders, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;join(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;broadcast(skus), &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;# ★ 维表小，广播它！&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;select(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;category&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;brand&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pay_time&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;withColumn(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(wide&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mode(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overwrite&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;partitionBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;warehouse/dwd/order_detail_wide&amp;#34;&lt;/span&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;知识点：Broadcast Join&lt;/strong&gt;。普通 join 两张表都要 Shuffle；但如果一张表很小（商品维表几十万行，几十 MB），Spark 可以把它&lt;strong&gt;整个复制到每个 Executor 的内存里&lt;/strong&gt;，大表原地不动就能完成 join——Shuffle 直接消失，速度提升一个数量级。&lt;/p&gt;
&lt;p&gt;判断标准：小表 &amp;lt; &lt;code&gt;spark.sql.autoBroadcastJoinThreshold&lt;/code&gt;（默认 10MB，生产常调到 100MB~500MB）。写 &lt;code&gt;F.broadcast(skus)&lt;/code&gt; 是显式强制，防止优化器估算失误。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 普通Join[&amp;#34;普通 Sort-Merge Join（两张表都 Shuffle）&amp;#34;]
        A1[&amp;#34;大表 Partition&amp;#34;] -.网络搬家.-&amp;gt; J1[&amp;#34;Join 节点&amp;#34;]
        A2[&amp;#34;维表 Partition&amp;#34;] -.网络搬家.-&amp;gt; J1
    end
    subgraph 广播Join[&amp;#34;Broadcast Join（只有小表复制）&amp;#34;]
        B1[&amp;#34;大表 Partition 1&amp;lt;br/&amp;gt;原地不动&amp;#34;] --&amp;gt; C1[&amp;#34;本地 Join&amp;lt;br/&amp;gt;查内存里的小表&amp;#34;]
        B2[&amp;#34;大表 Partition 2&amp;lt;br/&amp;gt;原地不动&amp;#34;] --&amp;gt; C2[&amp;#34;本地 Join&amp;lt;br/&amp;gt;查内存里的小表&amp;#34;]
        SM[&amp;#34;小表整表复制&amp;lt;br/&amp;gt;→ 每个 Executor&amp;#34;] --&amp;gt; C1
        SM --&amp;gt; C2
    end
    style 广播Join fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-场景二用户行为漏斗转化率分析&#34;&gt;4.2 场景二：用户行为漏斗（转化率分析）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;业务&lt;/strong&gt;：运营想知道双十一预热期的转化漏斗：曝光 → 点击 → 加购 → 下单 → 支付，每一步流失多少人。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo03_funnel.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;events &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ods.user_events&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# events 字段：user_id, event_type(view/click/cart/order/pay), ts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 每个用户在每一步的最早时间&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;first_ts &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (events
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;view&amp;#34;&lt;/span&gt;,  F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_view&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;click&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_click&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cart&amp;#34;&lt;/span&gt;,  F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_cart&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_order&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;min(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pay&amp;#34;&lt;/span&gt;,   F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ts&amp;#34;&lt;/span&gt;)))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_pay&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 严格漏斗：必须按时间顺序走到该步才算转化&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;funnel &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; first_ts&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;agg(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_view&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;曝光&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_click&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_view&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;点击&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_cart&amp;#34;&lt;/span&gt;)  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_click&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;加购&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_order&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_cart&amp;#34;&lt;/span&gt;),  &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;下单&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_pay&amp;#34;&lt;/span&gt;)   &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;t_order&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;支付&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;funnel&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;show()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;知识点：&lt;code&gt;groupBy&lt;/code&gt; + 条件聚合（&lt;code&gt;F.when&lt;/code&gt; inside &lt;code&gt;agg&lt;/code&gt;）是漏斗分析的标准套路&lt;/strong&gt;——一行 SQL 出五步漏斗，不用写五次 count 再 join。这也是经典面试题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 3：漏斗口径的战争&lt;/strong&gt;
我第一版漏斗上线后，运营说数据和他们对不上。查了半天，发现分歧在口径：我写的是&amp;quot;严格顺序漏斗&amp;quot;（必须 view 之后才 click 才算），运营期望的是&amp;quot;存在性漏斗&amp;quot;（只要当天两个行为都发生过就算）。&lt;strong&gt;大数据开发一半的返工不是技术问题，是口径没对齐&lt;/strong&gt;。现在的习惯：开工前把每一步的定义写成一行白话发群里，确认了再写代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;43-场景三推荐特征工程用户画像&#34;&gt;4.3 场景三：推荐特征工程（用户画像）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;业务&lt;/strong&gt;：算法团队要用户特征——每个用户近 30 天的购买次数、客单价、最偏爱类目、最近一次购买距今天数（RFM 模型的雏形）。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# demo04_user_features.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; pyspark.sql.window &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Window
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders_30d &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;table(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dwd.order_detail_wide&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;between(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-09-26&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-25&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 基础聚合特征&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agg_feat &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (orders_30d
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;countDistinct(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_cnt_30d&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;round(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;avg(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;avg_amount_30d&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;max(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pay_time&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last_pay_time&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 最偏爱类目：窗口函数按类目的购买金额排序取第一&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;w &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Window&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;partitionBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;orderBy(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;desc(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cat_amount&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;top_cat &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (orders_30d
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;groupBy(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;category&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;agg(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cat_amount&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;withColumn(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rn&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;row_number()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;over(w))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rn&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;select(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;, F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;category&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;alias(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;top_category&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;features &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (agg_feat
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;join(top_cat, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;withColumn(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;days_since_last_pay&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;datediff(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2022-10-26&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last_pay_time&amp;#34;&lt;/span&gt;))))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;features&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mode(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overwrite&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;features/user_rfm/dt=2022-10-25&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;知识点：窗口函数&lt;/strong&gt;（&lt;code&gt;Window.partitionBy(...).orderBy(...)&lt;/code&gt; + &lt;code&gt;row_number&lt;/code&gt;）是&amp;quot;每组取 TopN&amp;quot;的标准解法，Hive SQL 里也是同一句，大数据面试必考。&lt;/p&gt;
&lt;p&gt;三个场景串起来，就是一条标准的离线数仓链路：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 数据源
        DB[&amp;#34;业务 MySQL&amp;lt;br/&amp;gt;订单/明细/商品&amp;#34;]
        LOG[&amp;#34;埋点日志&amp;lt;br/&amp;gt;曝光/点击/加购&amp;#34;]
    end
    DB --&amp;gt;|Sqoop/DataX 同步| ODS
    LOG --&amp;gt;|Flume/直接落盘| ODS
    subgraph 数仓[&amp;#34;数仓分层（Hive on Spark）&amp;#34;]
        ODS[&amp;#34;ODS 贴源层&amp;lt;br/&amp;gt;原样搬运&amp;#34;] --&amp;gt; DWD[&amp;#34;DWD 明细层&amp;lt;br/&amp;gt;清洗 关联 宽表&amp;lt;br/&amp;gt;★ 场景一&amp;#34;]
        DWD --&amp;gt; DWS[&amp;#34;DWS 汇总层&amp;lt;br/&amp;gt;按主题聚合&amp;lt;br/&amp;gt;★ 场景二漏斗&amp;#34;]
        DWS --&amp;gt; ADS[&amp;#34;ADS 应用层&amp;lt;br/&amp;gt;直接服务业务&amp;lt;br/&amp;gt;★ 场景三特征&amp;#34;]
    end
    ADS --&amp;gt; BI[&amp;#34;BI 报表&amp;#34;]
    ADS --&amp;gt; ALG[&amp;#34;推荐算法&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;第五篇-调优与踩坑生产环境才是真的战场&#34;&gt;第五篇 调优与踩坑：生产环境才是真的战场&lt;/h2&gt;
&lt;p&gt;本地 Demo 岁月静好，上了生产环境才知道什么叫残酷。下面三个坑，每一个我都在凌晨两点处理过。&lt;/p&gt;
&lt;h3 id=&#34;51-天坑一数据倾斜一个-task-跑三小时其他三分钟&#34;&gt;5.1 天坑一：数据倾斜（一个 Task 跑三小时，其他三分钟）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：任务 99% 的 Stage 秒过，最后一个 Stage 的 200 个 Task 里，199 个 5 分钟跑完，&lt;strong&gt;1 个跑了 3 小时还在爬&lt;/strong&gt;。Spark UI 上看，那个 Task 的 Shuffle Read 是别人的几百倍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：某个 key 的数据量碾压其他 key。电商里最典型的就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大促时某个爆款商品（iPhone 发售当天）的订单明细占全站 30%；&lt;/li&gt;
&lt;li&gt;维表 join 时空值/默认值 key——脏数据的 &lt;code&gt;user_id = null&lt;/code&gt; 或 &lt;code&gt;&#39;-1&#39;&lt;/code&gt; 有上千万条，全挤到一个 Partition。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;排查&lt;/strong&gt;：Spark UI → Stages → 看 Task 的 Summary Metrics，Max 时间远大于 Median（中位数），就是倾斜。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法三板斧&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 招式1：过滤异常 key（脏数据导致的倾斜，90% 的情况）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;isNotNull() &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt; (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;-1&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 招式2：热点 key 加盐打散（真热点，比如爆款商品）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 给大表的热点 key 加随机后缀 0~9，小表对应 key 复制 10 份&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;salted &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; big&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;withColumn(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;join_key&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;爆款SKU&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;concat(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_&amp;#34;&lt;/span&gt;), (F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;rand()&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cast(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;int&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;otherwise(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;small_exploded &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; small&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;withColumn(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;join_key&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;when(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;爆款SKU&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;concat(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;), F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;explode(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;array([F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lit(i) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;)]))))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;otherwise(F&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sku_id&amp;#34;&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 招式3：开启 AQE 自动处理（Spark 3.x 的救星）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;conf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;spark.sql.adaptive.enabled&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;conf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;spark.sql.adaptive.skewJoin.enabled&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 4：AQE 救了我一个通宵&lt;/strong&gt;
2022 年上半年迁移 Spark 3.x 后开了 AQE（Adaptive Query Execution），它能&lt;strong&gt;在运行时&lt;/strong&gt;发现倾斜的 Partition 并自动拆分。以前要手工调参数 &lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt; 反复试的任务，AQE 直接自己搞定了。所以第一建议永远是：&lt;strong&gt;先升级到 Spark 3.x 并开启 AQE，再用脑子去调&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph 倾斜前[&amp;#34;倾斜：一个 Partition 吞掉 40% 数据&amp;#34;]
        K1[&amp;#34;Partition 1: 50MB&amp;#34;] 
        K2[&amp;#34;Partition 2: 8GB 😱&amp;#34;] 
        K3[&amp;#34;Partition 3: 50MB&amp;#34;]
    end
    subgraph 倾斜后[&amp;#34;AQE 拆分后：化整为零&amp;#34;]
        L1[&amp;#34;Partition 1: 50MB&amp;#34;]
        L2a[&amp;#34;Partition 2a: 200MB&amp;#34;]
        L2b[&amp;#34;Partition 2b: 200MB&amp;#34;]
        L2c[&amp;#34;Partition 2c: 200MB&amp;lt;br/&amp;gt;...共拆 40 份&amp;#34;]
        L3[&amp;#34;Partition 3: 50MB&amp;#34;]
    end
    倾斜前 --&amp;gt;|&amp;#34;AQE 运行时检测拆分&amp;#34;| 倾斜后
    style K2 fill:#ffcccc&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-天坑二oom内存不够但到底谁不够&#34;&gt;5.2 天坑二：OOM（内存不够，但到底谁不够）&lt;/h3&gt;
&lt;p&gt;Spark 的 OOM 分两种，报错长得像，病因完全不同：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;报错关键字&lt;/th&gt;
					&lt;th&gt;病因&lt;/th&gt;
					&lt;th&gt;解法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;ExecutorLostFailure&lt;/code&gt; / 容器被 kill / &lt;code&gt;java.lang.OutOfMemoryError&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Executor 内存不够&lt;/strong&gt;：数据太多、缓存太大&lt;/td&gt;
					&lt;td&gt;加 &lt;code&gt;executor-memory&lt;/code&gt;、增加分区数摊薄数据、减少 &lt;code&gt;cache&lt;/code&gt; 滥用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Driver 端 OOM&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Driver 内存不够&lt;/strong&gt;：几乎永远是 &lt;code&gt;collect()&lt;/code&gt; / &lt;code&gt;toPandas()&lt;/code&gt; 把全量数据拉回 Driver&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;不要用 collect 收大数据！&lt;/strong&gt; 用 &lt;code&gt;take(100)&lt;/code&gt; 抽样看，用 &lt;code&gt;write&lt;/code&gt; 落盘&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事 5：一个 collect 拖垮了整个集群调度&lt;/strong&gt;
同事为了&amp;quot;快速验证&amp;quot;，在生产任务里写了 &lt;code&gt;df.collect()&lt;/code&gt; 然后 len() 一下——那张表 2 亿行。Driver 内存从 4G 一路撑爆，任务挂掉不说，还因为是调度系统的重要任务，报警电话打到了总监那里。&lt;strong&gt;从此组里 Code Review 第一条红线：搜索代码里有没有 &lt;code&gt;collect()&lt;/code&gt;、&lt;code&gt;toPandas()&lt;/code&gt;，有就必须说明数据量上限&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;53-天坑三小文件灾难hdfs-的慢性病&#34;&gt;5.3 天坑三：小文件灾难（HDFS 的慢性病）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：任务很快跑完，但写出的结果目录里是 &lt;strong&gt;20000 个 100KB 的小文件&lt;/strong&gt;。然后：Hive 查这张表慢得离谱，NameNode 内存报警，下游任务读它要启动几万个 Task。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：200 个分区 × 每次写 100 个文件 = 每天积累，几个月后就是几百万小文件。HDFS 的 NameNode 把每个文件的元数据放内存，小文件多是集群的头号慢性病。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 写出前合并分区（注意：coalesce 不 Shuffle，repartition 会 Shuffle）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;coalesce(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;)        &lt;span style=&#34;color:#75715e&#34;&gt;# 知道大概要几个文件时用它&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;df&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;repartition(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parquet(&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;)  &lt;span style=&#34;color:#75715e&#34;&gt;# 需要按列重新分区时用它&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 根治：开启 AQE 的 Shuffle 分区自动合并，从源头减少小文件&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;conf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;spark.sql.adaptive.coalescePartitions.enabled&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;54-一张排障决策图&#34;&gt;5.4 一张排障决策图&lt;/h3&gt;
&lt;p&gt;任务慢了/挂了，按这个顺序查：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q1{&amp;#34;任务挂了还是慢？&amp;#34;}
    Q1 --&amp;gt;|挂了| Q2{&amp;#34;Driver 日志里&amp;lt;br/&amp;gt;OOM 关键字？&amp;#34;}
    Q2 --&amp;gt;|Executor 端| F1[&amp;#34;加 executor-memory&amp;lt;br/&amp;gt;或增大分区数&amp;#34;]
    Q2 --&amp;gt;|Driver 端| F2[&amp;#34;查 collect/toPandas&amp;lt;br/&amp;gt;换成 write 或 take&amp;#34;]
    Q2 --&amp;gt;|文件不存在等| F3[&amp;#34;懒执行报错错位&amp;lt;br/&amp;gt;从执行计划往上找源头&amp;#34;]
    Q1 --&amp;gt;|慢| Q3{&amp;#34;Spark UI 看 Stage：&amp;lt;br/&amp;gt;个别 Task 特别慢？&amp;#34;}
    Q3 --&amp;gt;|是 Max远大于Median| F4[&amp;#34;数据倾斜&amp;lt;br/&amp;gt;过滤脏key/加盐/AQE&amp;#34;]
    Q3 --&amp;gt;|否 全都慢| Q4{&amp;#34;Shuffle Read/Write&amp;lt;br/&amp;gt;数据量巨大？&amp;#34;}
    Q4 --&amp;gt;|是| F5[&amp;#34;能广播的小表广播&amp;lt;br/&amp;gt;列裁剪 过滤下推&amp;lt;br/&amp;gt;提前聚合减少数据量&amp;#34;]
    Q4 --&amp;gt;|否| F6[&amp;#34;查上游：小文件太多？&amp;lt;br/&amp;gt;查集群：资源不够排队？&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;第六篇-ai-时代少写代码完成大数据开发&#34;&gt;第六篇 AI 时代：少写代码，完成大数据开发&lt;/h2&gt;
&lt;p&gt;时间快进到 AI 编程工具普及之后。回头看，Spark 开发是&lt;strong&gt;最适合被 AI 改造&lt;/strong&gt;的领域之一，因为它的代码模式极其规整：读、变换、聚合、写，套路就那么多。&lt;/p&gt;
&lt;h3 id=&#34;61-ai-能替你做什么不能替你做什么&#34;&gt;6.1 AI 能替你做什么，不能替你做什么&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph AI擅长[&amp;#34;AI 做得比人好&amp;#34;]
        A1[&amp;#34;自然语言 → PySpark/SQL 代码&amp;lt;br/&amp;gt;按用户30天行为算RFM特征 一句话出码&amp;#34;]
        A2[&amp;#34;样板代码生成&amp;lt;br/&amp;gt;Schema 定义 造数脚本 单元测试&amp;#34;]
        A3[&amp;#34;报错诊断&amp;lt;br/&amp;gt;贴堆栈 直接定位到懒执行错位/倾斜&amp;#34;]
        A4[&amp;#34;口径文档化&amp;lt;br/&amp;gt;把代码翻译成业务能看懂的白话&amp;#34;]
    end
    subgraph AI不擅长[&amp;#34;人必须自己把关&amp;#34;]
        B1[&amp;#34;业务口径&amp;lt;br/&amp;gt;漏斗是严格顺序还是存在性&amp;lt;br/&amp;gt;AI 不知道你老板要什么&amp;#34;]
        B2[&amp;#34;性能直觉&amp;lt;br/&amp;gt;哪里该广播 哪里会倾斜&amp;lt;br/&amp;gt;要看真实数据分布&amp;#34;]
        B3[&amp;#34;生产决策&amp;lt;br/&amp;gt;资源参数 重跑策略 数据回补&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;我的实际工作流变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;以前&lt;/strong&gt;：写「用户漏斗分析」= 查文档 30 分钟 + 写代码 40 分钟 + 调试 30 分钟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现在&lt;/strong&gt;：给 AI 描述清楚——表名、字段、漏斗定义（严格按时间顺序）、输出格式——10 秒出代码，我花 20 分钟 review 口径和边界条件。&lt;strong&gt;总耗时砍到三分之一&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但注意：&lt;strong&gt;省下来的时间全部应该砸在口径确认和数据校验上&lt;/strong&gt;。AI 写的代码语法几乎不会错，但它不知道你们的 &lt;code&gt;user_id = &#39;-1&#39;&lt;/code&gt; 是脏数据、不知道运营要的是存在性漏斗。这正是第五篇那些坑的价值——&lt;strong&gt;AI 时代，踩坑经验不是贬值了，而是成了你最贵的资产&lt;/strong&gt;，因为代码人人都能生成，能判断代码对不对的人变稀缺了。&lt;/p&gt;
&lt;h3 id=&#34;62-具体怎么干三个提效姿势&#34;&gt;6.2 具体怎么干：三个提效姿势&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;姿势一：需求描述模板化。&lt;/strong&gt; 给 AI 下指令时固定四段式：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;背景：表 ods.user_events，字段 user_id/event_type/ts，按天分区 dt
任务：计算 2022-10-25 的转化漏斗 曝光→点击→加购→下单→支付
口径：严格顺序漏斗，后一步的最早时间必须大于前一步
输出：一行五列，列名用中文&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;第四篇场景三的完整代码，用这样的提示词一次就能生成得八九不离十。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;姿势二：报错即提示词。&lt;/strong&gt; Spark 报错堆栈又长又唬人，直接整段贴给 AI，附上你的 Transformation 代码，它对「懒执行错位」「ClassNotFound（依赖没打包）」「分区越界」这类经典错误的诊断准确率很高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;姿势三：让 AI 写测试和校验脚本。&lt;/strong&gt; 大数据开发最缺的就是测试——数据对不对以前靠人肉抽查。现在让 AI 顺手生成校验 SQL（行数对比、NULL 比例、金额总和与上游对账），质量反而比手工时代更好了。&lt;/p&gt;
&lt;h3 id=&#34;63-冷静的一句话&#34;&gt;6.3 冷静的一句话&lt;/h3&gt;
&lt;p&gt;AI 不会让你&amp;quot;少学 Spark&amp;quot;，它让&lt;strong&gt;学习曲线的重心变了&lt;/strong&gt;：以前背 API 占 60% 的时间，现在这部分 AI 补上了；剩下 40%——架构直觉、业务口径、排障经验——变成了 100% 的核心竞争力。&lt;strong&gt;入门变容易了，精通的标准反而更高了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;思考与感悟&#34;&gt;思考与感悟&lt;/h2&gt;
&lt;p&gt;写完这篇文章，有几句话想单独说：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 学 Spark 的本质是学&amp;quot;分布式的世界观&amp;quot;。&lt;/strong&gt; 单机时代的直觉——数据随手可得、循环遍历随意、内存无限——全都要扔掉。你要习惯一种新的思维方式：数据是流动的、分区是平行的、Shuffle 是昂贵的、失败是必然的（所以容错设计是一等公民）。这套世界观建立起来之后，学 Flink、学 Kafka、学 ClickHouse 都会很快，因为它们要解决的是同一族问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 调优经验 = 疼痛的记忆。&lt;/strong&gt; 没有人看文档能学会处理数据倾斜，都是被凌晨两点的报警电话教会的。所以别怕踩坑，踩了记下来——我这篇第五篇的每个坑，当年都值一个通宵。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 离业务近一点，离技术远一点。&lt;/strong&gt; 做了几年大数据，最大的感悟是：把 ETL 跑得快不算本事，&lt;strong&gt;知道这张表给谁用、口径对不对、错了谁会亏钱&lt;/strong&gt;才是本事。电商场景里，GMV 算错 1% 可能就是一个部门的年终奖。技术只是手段，数据是业务的镜像。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. AI 时代最值得押注的能力是&amp;quot;判断力&amp;quot;。&lt;/strong&gt; 生成代码会越来越便宜，判断&amp;quot;这段代码对不对、这个口径该不该这么定、这个架构扛不扛得住双十一&amp;quot;会越来越贵。把 AI 当成一个能力极强但没有常识的实习生：活让它干，锅你来背，所以你必须看得懂它干的活。&lt;/p&gt;
&lt;p&gt;与君共勉。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
