本章目标:写 SQL、PySpark、Linux 命令或准备面试时,能在 30 秒内查到正确写法,并知道 Hive/Spark 与本地 DuckDB 的差异。 前置知识:无,配合各章使用。 使用方式:打印出来放手边,或面试前 20 分钟快速过一遍。
1. 怎么用这张速查表
不要从头背,按场景查:写练习卡住时抄最小示例再改字段;报错时对照常见坑和方言差异;面试前遮住右列自测。速查表只解决“怎么写”,口径和建模看数仓指标口径。
2. SQL 速查
2.1 查询骨架与执行顺序
SELECT columns
FROM table
WHERE row_conditions
GROUP BY columns
HAVING group_conditions
ORDER BY columns
LIMIT n;
书写顺序是上面的顺序,逻辑执行顺序大致是:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。所以 WHERE 里不能直接用聚合函数,SELECT 的别名通常也不能在 WHERE 里用。
2.2 条件、空值与去重
| 需求 | 写法 |
|---|---|
| 等值/范围/集合 | status = 'delivered'、age BETWEEN 18 AND 30、city IN ('北京','上海') |
| 模糊匹配 | product_name LIKE '%电脑%' |
| 多条件 | status = 'delivered' AND price > 0 |
| 空值判断 | customer_id IS NULL、IS NOT NULL |
| 空值替换 | COALESCE(customer_level, '未知') |
| 空值转条件 | NULLIF(amount, 0) |
| 去重 | SELECT DISTINCT city FROM customers |
| 条件计数 | SUM(CASE WHEN status = 'canceled' THEN 1 ELSE 0 END) |
NULL = NULL 结果是未知,不是真。判断空值必须用 IS NULL。COUNT(column) 不统计空值,COUNT(*) 统计行数。
2.3 聚合与分组
| 需求 | 写法 |
|---|---|
| 计数/求和/平均 | COUNT(DISTINCT customer_id)、SUM(price * quantity)、AVG(amount) |
| 最值 | MIN(order_date)、MAX(order_date) |
| 分组 | GROUP BY category |
| 分组后过滤 | HAVING COUNT(*) > 100 |
| 条件聚合 | SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) |
| 多行合并 | STRING_AGG(city, ',')(Hive 用 collect_list) |
WHERE 过滤行,HAVING 过滤分组。SELECT 中非聚合列必须出现在 GROUP BY 里。
2.4 JOIN
| 类型 | 作用 | 保留哪边 |
|---|---|---|
INNER JOIN |
两边都匹配才保留 | 交集 |
LEFT JOIN |
左表全保留 | 左表 + 右表匹配 |
RIGHT JOIN |
右表全保留 | 右表 + 左表匹配 |
FULL OUTER JOIN |
两边都保留 | 并集 |
CROSS JOIN |
笛卡尔积 | 全部组合 |
SELECT o.order_id, c.province, SUM(i.price * i.quantity) AS gmv
FROM orders o
JOIN order_items i ON o.order_id = i.order_id
LEFT JOIN customers c ON o.customer_id = c.customer_id
WHERE o.status NOT IN ('canceled', 'unavailable')
GROUP BY o.order_id, c.province;
关联前确认右表关联键唯一,否则行数会膨胀。LEFT JOIN 后对右表字段做 WHERE 过滤,可能把左连接变成内连接;需要保留左表时把条件下移到 ON。
2.5 窗口函数
| 函数 | 作用 |
|---|---|
ROW_NUMBER() OVER (PARTITION BY x ORDER BY y) |
组内排名,不重复 |
RANK() / DENSE_RANK() |
排名,允许并列;DENSE 不留空位 |
SUM(x) OVER (PARTITION BY g ORDER BY d) |
累计求和 |
SUM(x) OVER (PARTITION BY g) |
组内总和,不排序 |
LAG(x, 1) OVER (...) / LEAD(...) |
取上一行/下一行 |
AVG(x) OVER (ORDER BY d ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) |
7 日移动平均 |
NTILE(4) OVER (ORDER BY x) |
分成 4 桶 |
窗口函数写在 SELECT 里,不能直接写在 WHERE。要过滤排名,外层再包一层子查询或 CTE。
2.6 日期与字符串
| 需求 | DuckDB 写法 | Hive/Spark 写法 |
|---|---|---|
| 当前日期 | CURRENT_DATE |
current_date(Spark 也可加括号) |
| 截到月 | DATE_TRUNC('month', d) |
trunc(d, 'MM') |
| 加一天 | d + INTERVAL 1 DAY |
date_add(d, 1) |
| 减 7 天 | d - INTERVAL 7 DAY |
date_sub(d, 7) |
| 日期差 | DATE_DIFF('day', start, end) |
datediff(end, start) |
| 格式化 | strftime(d, '%Y-%m') |
date_format(d, 'yyyy-MM') |
| 字符串转日期 | CAST(s AS DATE) |
to_date(s) |
| 拼接 | concat('a', 'b') |
concat('a', 'b')(也支持双竖线) |
| 截取 | SUBSTRING(s, 1, 3) |
substr(s, 1, 3) |
| 大小写 | LOWER(s)、UPPER(s) |
同左 |
| 去空格 | TRIM(s) |
trim(s) |
| 替换 | REPLACE(s, 'a', 'b') |
regexp_replace(s, 'a', 'b') |
日期差最容易错:Hive/Spark 的 datediff 是“结束日期在前、开始日期在后”,DuckDB 的 date_diff 把单位放在第一个参数。
2.7 CTE 与 UNION
CTE 写法:WITH t AS (SELECT ...) SELECT * FROM t;,适合把清洗步骤拆开。UNION 去重,UNION ALL 不去重;能用 UNION ALL 就不加 UNION,少一次排序去重。完整例子见第 7 节。
3. Hive / Spark SQL 差异速查
3.1 课程统一版本
| 组件 | 版本 | 说明 |
|---|---|---|
| Java | 17 | Spark 3.5 官方支持 |
| Spark / PySpark | 3.5.5 | pip install pyspark==3.5.5,Python 3.8+ |
| Hadoop | 3.3.6 | 只做 HDFS/YARN 概念演示 |
| Hive | 3.1.3 | 与 Hadoop 3.3 搭配 |
| Kafka | 3.7 | Docker 单机 KRaft 模式 |
| Flink / PyFlink | 1.18 | pip install apache-flink==1.18.1 |
| Airflow | 2.10 | 3.x 的 UI、CLI 和 provider 包有变化 |
| ClickHouse | 24.3 | Docker 镜像 clickhouse/clickhouse-server:24.3 |
正文和本章引用同一组版本。升级时先改这张表,再检查示例命令。
3.2 方言差异
| 场景 | DuckDB | Hive | Spark SQL |
|---|---|---|---|
| 查 CSV | read_csv_auto('f.csv') |
先建外部表 | spark.read.csv(...) |
| 建表 | CREATE TABLE ... |
CREATE TABLE ... STORED AS ORC |
同 Hive,支持 Delta/Iceberg |
| 覆盖写 | CREATE OR REPLACE TABLE |
INSERT OVERWRITE TABLE ... PARTITION |
同 Hive |
| 行拆多行 | UNNEST(list) |
LATERAL VIEW explode(arr) t AS x |
explode(arr) |
| 多行合并 | list(col) |
collect_list(col) / collect_set(col) |
collect_list / collect_set |
| JSON 取值 | json_extract_string(j, '$.k') |
get_json_object(j, '$.k') |
get_json_object / from_json |
| 空值替换 | COALESCE |
nvl / COALESCE |
coalesce / nvl |
| 字符串拼接 | concat(或双竖线) |
concat |
concat(或双竖线) |
| 日期差 | date_diff('day', a, b) |
datediff(b, a) |
datediff(b, a) |
| 分区裁剪 | 无分区概念 | 查询带 WHERE dt = '...' |
同 Hive |
| 分桶 | 无 | CLUSTERED BY ... INTO n BUCKETS |
支持,较少用 |
Hive 的 INSERT OVERWRITE 覆盖写天然幂等,补数常靠它。Spark 默认是批处理引擎;cache() 用于重复使用的 DataFrame,explain() 看执行计划。
4. Linux 与 Git 速查
| 场景 | 命令 |
|---|---|
| 看目录/找内容 | pwd、ls -lh、find data -name "*.csv"、grep -n "error" app.log |
| 看头尾/统计 | head -n 5 f、tail -f app.log、wc -l f |
| 排序去重/按列处理 | sort f 后接 uniq -c 和 sort -nr、awk '{print $3}' f、sed -n '1,3p' f |
| 打包/磁盘/权限 | tar -czf raw.tar.gz data/raw、du -sh data、df -h、chmod +x run.sh |
| 管道/重定向 | 管道用竖线连接两条命令;cmd > out.txt 2>&1 |
| Git 初始化/提交 | git init -b main、git status、git add f、git commit -m "msg"、git log --oneline |
| Git 分支/远程 | git switch -c feature/x、git merge feature/x、git push -u origin main、git pull --rebase |
| Git 撤销 | git restore f、git restore --staged f、git revert <commit> |
5. PySpark 常用 API
本课程统一 Java 17 + PySpark 3.5.5。
from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.master("local[*]").appName("cheatsheet").getOrCreate()
orders = spark.read.csv("data/sample/orders_demo.csv", header=True, inferSchema=True)
orders.select("order_id", "status").filter(
F.col("status") == "paid"
).groupBy("status").count().show()
orders.createOrReplaceTempView("orders")
spark.sql("SELECT status, COUNT(*) AS cnt FROM orders GROUP BY status").show()
orders.dropDuplicates(["order_id"]).orderBy("order_id").show(5)
orders.cache()
orders.explain()
orders.write.mode("overwrite").parquet("output/orders")
spark.stop()
| 需求 | DataFrame API | SQL |
|---|---|---|
| 选列/改名 | df.select("a")、df.withColumnRenamed("a", "b") |
SELECT a AS b |
| 新增列 | df.withColumn("amount", F.col("price") * F.col("quantity")) |
SELECT price * quantity AS amount |
| 筛选 | df.filter(F.col("status") == "delivered") |
WHERE status = 'delivered' |
| 分组 | df.groupBy("category").agg(F.sum("amount").alias("gmv")) |
GROUP BY category |
| 关联 | df.join(other, on="order_id", how="left") |
LEFT JOIN |
| 去重 | df.dropDuplicates(["order_id"]) |
SELECT DISTINCT |
| 排序 | df.orderBy(F.col("gmv").desc()) |
ORDER BY gmv DESC |
| 写文件 | df.write.mode("overwrite").parquet("output/x") |
INSERT OVERWRITE |
窗口函数用 from pyspark.sql import Window,例如 F.row_number().over(Window.partitionBy("user_id").orderBy("event_date"))。
6. Kafka 与 Flink 速查
6.1 Kafka 概念
| 概念 | 一句话 |
|---|---|
| topic | 消息的分类,例如 order_events |
| partition | topic 的分片,决定并行度和顺序范围 |
| offset | 每条消息在 partition 内的位置 |
| producer / consumer | 写入方 / 读取方 |
| consumer group | 同组内一个 partition 只由一个消费者处理 |
| broker | Kafka 服务节点 |
| replication | 副本数,保证节点挂了数据还在 |
acks=all |
所有副本确认后才算写入成功,可靠性高、延迟大 |
顺序性只保证单 partition 内有序。同一订单的事件要用 order_id 做 key,让它们进同一个 partition。
6.2 Kafka 本地常用命令
docker run -d --name kafka -p 9092:9092 apache/kafka:3.7.0
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh \
--create --topic order_events --bootstrap-server localhost:9092
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh \
--list --bootstrap-server localhost:9092
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh \
--topic order_events --bootstrap-server localhost:9092
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh \
--topic order_events --from-beginning --bootstrap-server localhost:9092
这是本地单机模拟,不是生产部署。生产环境要考虑副本、监控、磁盘和权限。
6.3 Flink 概念
| 概念 | 一句话 |
|---|---|
| 事件时间 / 处理时间 | 数据发生的时间 / 系统收到的时间 |
| 水位线 Watermark | 声明“晚于这个时间的数据不再等” |
| 滚动窗口 | 窗口不重叠,例如每 5 秒 |
| 滑动窗口 | 窗口有重叠,例如每 5 秒统计最近 10 秒 |
| 会话窗口 | 按活跃间隔切分 |
| 状态 State | 算子保存的中间结果 |
| Checkpoint | 定期快照,故障后恢复 |
| 精确一次 | 端到端不重不丢,靠两阶段提交 + 幂等写出 |
常用命令与入口:
| 目的 | 命令 |
|---|---|
| 安装 PyFlink | python -m pip install apache-flink==1.18.1 |
| 运行 PyFlink 作业 | python job.py(脚本内用本地执行环境) |
| 进入 Flink SQL Client | bin/sql-client.sh(已有 Flink 安装时) |
本地练习用 PyFlink 的本地环境跑一个滚动窗口统计即可。装不上不影响 SQL 主线,先做到“讲得清”。
7. 使用示例:品类 GMV 排名
直接用 DuckDB 读取 data/sample/ 下的示例 CSV,不依赖任何项目库:
WITH valid_orders AS (
SELECT order_id
FROM read_csv_auto('data/sample/orders_demo.csv', header = true)
WHERE status NOT IN ('canceled', 'unavailable')
)
SELECT i.category,
ROUND(SUM(i.price + i.freight), 2) AS gmv
FROM read_csv_auto('data/sample/order_items_demo.csv', header = true) i
JOIN valid_orders o USING (order_id)
GROUP BY i.category
ORDER BY gmv DESC;
预期结果:数码 885.00、家居 375.00、服饰 249.00,合计 1,509.00。示例只有 9 个有效订单,所以这里不写 LIMIT 5。跑出来对不上时,先检查是否排除了 canceled 和 unavailable 两种状态。
8. 常见坑
| 坑 | 正确做法 |
|---|---|
用 = NULL 判断空值 |
用 IS NULL |
WHERE 里用聚合函数 |
聚合条件放 HAVING |
| 日期差参数顺序照搬 DuckDB | Hive/Spark 是 datediff(结束, 开始) |
| 字符串用双引号 | SQL 字符串用单引号;双引号多为标识符 |
| LEFT JOIN 后在 WHERE 过滤右表 | 条件移到 ON,否则退化成 INNER JOIN |
| 关联键不唯一导致行数膨胀 | 关联前先 COUNT(*) 和 COUNT(DISTINCT key) |
| 直接把 CSV 查询语法搬到 Hive | Hive 要先建外部表,再查询 |
| 把本地 Kafka 命令当生产部署 | 明确说“本地模拟”,生产要副本和监控 |
9. 面试延伸
面试题:为什么窗口函数不能直接写在 WHERE 里?
是什么:窗口函数在 SELECT 阶段计算,而 WHERE 在它之前执行;执行到 WHERE 时窗口结果还不存在。
解决什么问题:要过滤排名或累计值,必须先用子查询/CTE 算出窗口列,再在外层 WHERE 过滤。理解执行顺序还能解释为什么 SELECT 别名不能在 WHERE 中使用。
我的实操边界:我在连续登录、品类排名、月度同比里用过 ROW_NUMBER、LAG、SUM OVER,并用 CTE 过滤排名;没有处理过超大分区下的窗口性能问题。
面试题:Kafka 如何保证同一订单的消息有序?
是什么:Kafka 只保证单 partition 内有序,不保证 topic 全局有序。
解决什么问题:生产消息时用 order_id 作为 key,Kafka 会把同一 key 的消息路由到同一 partition,消费者按 offset 顺序读取,就能保证该订单的事件有序。
我的实操边界:我在本地单机 Kafka 里跑过生产消费 demo,理解 key、partition、offset;没有在生产集群处理过 rebalance 和跨机房副本。
10. 本章验收
- 能不看资料写出
SELECT … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT的骨架和执行顺序。 - 能写出 INNER/LEFT JOIN、窗口排名、日期截断、空值替换、去重的正确语法。
- 能说出至少 5 处 Hive/Spark 与 DuckDB 的方言差异,尤其是日期差参数顺序。
- 能查表完成一次 Linux 日志统计和一次 Git 分支合并,不需要从头搜教程。
- 能在 30 秒内查到 PySpark 的读取、筛选、分组、关联、写出写法,并说清 Kafka/Flink 的核心概念。
打印建议:A4 双面,第 2–6 节放前两页,方言差异和常见坑放第三页。配合数仓指标口径使用。