Data Engineering Zoomcamp SQL 复习:在 pgAdmin 中实战 NYC Taxi 数据的 JOIN 与聚合查询

Data Engineering Zoomcamp SQL 复习:在 pgAdmin 中实战 NYC Taxi 数据的 JOIN 与聚合查询 Data Engineering Zoomcamp SQL 复习在 pgAdmin 中实战 NYC Taxi 数据的 JOIN 与聚合查询【免费下载链接】data-engineering-zoomcampData Engineering Zoomcamp is a free 9-week course on building production-ready data pipelines. Join the course here 项目地址: https://gitcode.com/GitHub_Trending/da/data-engineering-zoomcamp本指南是 Data Engineering Zoomcamp 第一模块「Docker 与 PostgreSQL」的 SQL 复习章节在已经通过 Docker Compose 搭建好的 Postgres 数据库上围绕 NYC Yellow Taxi 行程数据表与 Zones 区域查询表系统演练隐式/显式 INNER JOIN、数据质量检查、LEFT/RIGHT/OUTER JOIN、GROUP BY、ORDER BY 与多字段聚合查询。读完本篇你将掌握一套可直接复制到 pgAdmin 查询编辑器运行的真实 SQL 实战语句并理解每类连接与聚合操作在数据工程场景中的典型用途。前置条件让 Postgres 与 pgAdmin 跑起来本节是上一篇 09-docker-compose.md 的延续。如果按课程顺序完成此时 Docker Compose 应该已经在运行pgdatabasePostgreSQL与pgadminpgAdmin两个服务对应配置见 pipeline/docker-compose.yamlservices: pgdatabase: image: postgres:18 environment: POSTGRES_USER: root POSTGRES_PASSWORD: root POSTGRES_DB: ny_taxi volumes: - ny_taxi_postgres_data:/var/lib/postgresql ports: - 5432:5432 pgadmin: image: dpage/pgadmin4 environment: PGADMIN_DEFAULT_EMAIL: adminadmin.com PGADMIN_DEFAULT_PASSWORD: root volumes: - pgadmin_data:/var/lib/pgadmin ports: - 8085:80 volumes: ny_taxi_postgres_data: pgadmin_data:确认服务运行后在浏览器打开 http://localhost:8085/browser/ 即可进入 pgAdminpgAdmin 容器内部端口为 80映射到宿主机 8085端口细节见 07-pgadmin.md。登录后如果看不到新表记得右键点击服务器或数据库并刷新——数据是后续通过摄取脚本写入的pgAdmin 不会自动感知新出现的表。进入 pgAdmin 的 Query Tool查询工具就可以开始下面的练习了。数据表速览行程表与区域查询表本节的查询围绕两张表展开yellow_taxi_trips行程表NYC Yellow Taxi 行程明细核心字段包括tpep_pickup_datetime上车时间、tpep_dropoff_datetime下车时间、total_amount总金额、passenger_count乘客数、PULocationID上车区域 ID、DOLocationID下车区域 ID。zones区域查询表包含LocationID、Borough行政区如 Manhattan、Queens、Zone区域名等字段用于把行程表中的数值 ID 翻译成可读的地理名称。表名可以结合摄取脚本理解pipeline/ingest_data.py 中通过--target-table参数指定目标表名默认yellow_taxi_data课程实践中常以yellow_taxi_trips为名建表同时脚本用dtype显式声明了PULocationID、DOLocationID为Int64整数类型、两个时间字段为日期类型parse_dates这与 05-data-ingestion.md 中展示的建表 DDL 一致——两列在 Postgres 中落地为BIGINT时间列为TIMESTAMP WITHOUT TIME ZONE。这正是zones.LocationID能与行程表两列进行等值连接的前提。INNER JOIN隐式写法与显式写法INNER JOIN 返回两表中满足连接条件的行。这里把行程表同时连接两次zones一次取上车区域zpu一次取下车区域zdo并用CONCAT拼接出Borough | Zone的友好文本。隐式 INNER JOIN逗号 WHERESELECT tpep_pickup_datetime, tpep_dropoff_datetime, total_amount, CONCAT(zpu.Borough, | , zpu.Zone) AS pickup_loc, CONCAT(zdo.Borough, | , zdo.Zone) AS dropoff_loc FROM yellow_taxi_trips t, zones zpu, zones zdo WHERE t.PULocationID zpu.LocationID AND t.DOLocationID zdo.LocationID LIMIT 100;隐式写法本质是「逗号交叉连接 WHERE 过滤」FROM中列出三张表行程表一次、zones 两次再用WHERE写连接条件。它虽然可用但连接条件与过滤条件混在一起复杂查询中可读性差不推荐在生产中长期使用。显式 INNER JOINJOIN ... ONSELECT tpep_pickup_datetime, tpep_dropoff_datetime, total_amount, CONCAT(zpu.Borough, | , zpu.Zone) AS pickup_loc, CONCAT(zdo.Borough, | , zdo.Zone) AS dropoff_loc FROM yellow_taxi_trips t JOIN zones zpu ON t.PULocationID zpu.LocationID JOIN zones zdo ON t.DOLocationID zdo.LocationID LIMIT 100;在 PostgreSQL 中JOIN默认就是INNER JOININNER JOIN是更显式、更少使用的完整写法二者语义完全相同。显式写法把连接条件放在ON子句中与过滤条件分离是推荐的方式。注意表/列名中带大写字母的如Borough、Zone、PULocationID必须使用双引号因为它们在建表时以带引号的标识符形式存在不加引号会被 Postgres 自动转为小写从而导致列不存在。数据质量检查从 JOIN 前的 NULL 与悬空 ID 说起在做连接之前先回答一个数据工程中的常见问题连接条件两边的键是否真的对得上以下两种检查是实践中最高频的探针。检查 NULL 的 Location IDSELECT tpep_pickup_datetime, tpep_dropoff_datetime, total_amount, PULocationID, DOLocationID FROM yellow_taxi_trips WHERE PULocationID IS NULL OR DOLocationID IS NULL LIMIT 100;IS NULL用于判断列值是否为空。注意不能写成 NULL比较运算对 NULL 的结果永远是不确定的这是 SQL 新手最常见的坑之一。若查询返回了行说明源数据存在缺失的 Location ID这些记录在 INNER JOIN 中会被静默丢弃。检查不在 zones 表中的 Location ID悬空外键SELECT tpep_pickup_datetime, tpep_dropoff_datetime, total_amount, PULocationID, DOLocationID FROM yellow_taxi_trips WHERE DOLocationID NOT IN (SELECT LocationID from zones) OR PULocationID NOT IN (SELECT LocationID from zones) LIMIT 100;子查询(SELECT LocationID from zones)先取出所有合法区域 ID再检查行程表中的上/下车 ID 是否落在集合之外。这条查询能暴露「ID 非 NULL 但查不到对应区域」的脏数据——例如区域表更新滞后或 ID 映射错误。一个工程上的注意事项如果zones.LocationID存在 NULLNOT IN子查询的结果会不符合直觉NULL 参与比较时结果不确定需要时可改用NOT EXISTS或先清洗区域表。LEFT、RIGHT 与 OUTER JOIN处理连接不上的行为了直观演示三类外连接的差异文档做了一个「破坏性实验」先删除zones表中LocationID 142的区域。DELETE FROM zones WHERE LocationID 142;之后行程表中凡是PULocationID或DOLocationID等于 142 的行在连接zones时都找不到匹配项于是CONCAT(zpu.Borough, | , zpu.Zone)的结果变为 NULL。LEFT JOINSELECT tpep_pickup_datetime, tpep_dropoff_datetime, total_amount, CONCAT(zpu.Borough, | , zpu.Zone) AS pickup_loc, CONCAT(zdo.Borough, | , zdo.Zone) AS dropoff_loc FROM yellow_taxi_trips t LEFT JOIN zones zpu ON t.PULocationID zpu.LocationID JOIN zones zdo ON t.DOLocationID zdo.LocationID LIMIT 100;LEFT JOIN保留左表yellow_taxi_trips t的所有行即使上车区域 142 已不存在行程记录仍然出现只是pickup_loc为 NULL。注意示例中右侧仍是普通JOIN因此下车区域仍要求严格匹配。RIGHT JOINSELECT tpep_pickup_datetime, tpep_dropoff_datetime, total_amount, CONCAT(zpu.Borough, | , zpu.Zone) AS pickup_loc, CONCAT(zdo.Borough, | , zdo.Zone) AS dropoff_loc FROM yellow_taxi_trips t RIGHT JOIN zones zpu ON t.PULocationID zpu.LocationID JOIN zones zdo ON t.DOLocationID zdo.LocationID LIMIT 100;RIGHT JOIN保留右表zones zpu的所有行。因为这里是区域表结果中会出现没有行程与之对应的区域行行程相关字段为 NULL——这可以用来发现「定义了区域但从无车次」的空置区域。工程上RIGHT JOIN用得较少通常可以用LEFT JOIN调换表顺序替代。OUTER JOINFULL OUTER JOINSELECT tpep_pickup_datetime, tpep_dropoff_datetime, total_amount, CONCAT(zpu.Borough, | , zpu.Zone) AS pickup_loc, CONCAT(zdo.Borough, | , zdo.Zone) AS dropoff_loc FROM yellow_taxi_trips t OUTER JOIN zones zpu ON t.PULocationID zpu.LocationID JOIN zones zdo ON t.DOLocationID zdo.LocationID LIMIT 100;OUTER JOIN即FULL OUTER JOIN同时保留左右两侧表中不匹配的行既包含「无对应区域的行程」也包含「无行程的区域」。它是核对两表键值完整性最强的工具常用于表对账与数据一致性审计。小结练习结束后如果需要把删掉的区域恢复可以重新执行区域表的数据摄取例如用 pandas/SQLAlchemy 重新写入zones表这是数据回滚的常见操作。GROUP BY计算每天的行程数GROUP BY把行按指定列或表达式分组再对每组执行聚合函数。下面按「下车日期」分组统计每日行程数SELECT CAST(tpep_dropoff_datetime AS DATE) AS day, COUNT(1) FROM yellow_taxi_trips GROUP BY CAST(tpep_dropoff_datetime AS DATE) LIMIT 100;CAST(tpep_dropoff_datetime AS DATE)把时间戳截断为日期丢弃时分秒得到day列COUNT(1)统计每组内的行数——它等价于COUNT(*)比COUNT(列名)更常用于「数行数」后者会跳过 NULLGROUP BY必须包含SELECT中所有非聚合列这里是CAST(...)表达式这是 SQL 的硬性语法约束。数据摄取脚本中两个时间字段被解析为TIMESTAMP见 05-data-ingestion.md 的建表 DDL所以CAST ... AS DATE才能按天切分。ORDER BY让结果有序可读GROUP BY的输出顺序是不保证的需要用ORDER BY明确排序。按日期升序SELECT CAST(tpep_dropoff_datetime AS DATE) AS day, COUNT(1) FROM yellow_taxi_trips GROUP BY CAST(tpep_dropoff_datetime AS DATE) ORDER BY day ASC LIMIT 100;ORDER BY day ASC按日期从早到晚排列ASC为默认可省略。按计数降序SELECT CAST(tpep_dropoff_datetime AS DATE) AS day, COUNT(1) AS count FROM yellow_taxi_trips GROUP BY CAST(tpep_dropoff_datetime AS DATE) ORDER BY count DESC LIMIT 100;这里给COUNT(1)起了别名countORDER BY count DESC直接引用别名排序一眼就能看出哪几天打车量最大。更多聚合COUNT、MAX 的配合使用聚合函数不限于计数。在同一个GROUP BY中可叠加多个聚合例如统计每天的行程数、单笔最高金额与最高乘客数SELECT CAST(tpep_dropoff_datetime AS DATE) AS day, COUNT(1) AS count, MAX(total_amount) AS total_amount, MAX(passenger_count) AS passenger_count FROM yellow_taxi_trips GROUP BY CAST(tpep_dropoff_datetime AS DATE) ORDER BY count DESC LIMIT 100;MAX(total_amount)取当天单笔最高金额注意这里是「最大单笔」不是求和若想看总营收应改用SUM(total_amount)MAX(passenger_count)取当天单车最多载客数ORDER BY count DESC让行程数最多的日子排在最前配合LIMIT 100只看头部结果。多字段分组GROUP BY 1, 2分组维度也可以有多个。下面的查询同时按「下车日期」与「下车区域 ID」分组得到每天每个下车区域的统计SELECT CAST(tpep_dropoff_datetime AS DATE) AS day, DOLocationID, COUNT(1) AS count, MAX(total_amount) AS total_amount, MAX(passenger_count) AS passenger_count FROM yellow_taxi_trips GROUP BY 1, 2 ORDER BY day ASC, DOLocationID ASC LIMIT 100;这里引入了一个 PostgreSQL 支持的便捷写法GROUP BY 1, 2按 SELECT 列表中的第 1、2 列即day与DOLocationID分组等价于写出完整的表达式。ORDER BY同样支持按day ASC, DOLocationID ASC组合排序。位置序号写法在列表达式很长时能大幅减少重复但可读性略差——团队协作时建议以「写全表达式」为主、位置序号为辅。小结与下一步至此你已经在真实数据集上完成了 SQL 复习的核心闭环连接隐式 vs 显式 INNER JOIN以及 LEFT / RIGHT / OUTER JOIN 在「连接不上」时的行为差异数据质量用IS NULL与NOT IN子查询探测脏数据这是任何 JOIN 前都值得先做的检查聚合与排序GROUP BY配合COUNT/MAX、CAST按天分组、ORDER BY排序、位置序号GROUP BY 1, 2多字段分组。这些查询都运行在本模块用 Docker 搭起的 Postgres 上建表与数据摄入细节可回看 05-data-ingestion.md 与 06-ingestion-script.md。数据工程中 JOIN 与聚合是后续一切分析如 03-data-warehouse 中的 BigQuery 查询、04-analytics-engineering 中的 dbt 模型的地基熟练掌握本节语法将直接受益于后续模块。实验完成后可按 11-cleanup.md 清理 Docker 资源或回到 docker-sql/README.md 总览整个模块。【免费下载链接】data-engineering-zoomcampData Engineering Zoomcamp is a free 9-week course on building production-ready data pipelines. Join the course here 项目地址: https://gitcode.com/GitHub_Trending/da/data-engineering-zoomcamp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考