关系数据库入门:表、主键外键与 SQL Join 实战(Data-Science-For-Beginners 第 5 课)
【免费下载链接】Data-Science-For-Beginners10 Weeks, 20 Lessons, Data Science for All!项目地址: https://gitcode.com/GitHub_Trending/da/Data-Science-For-Beginners
本篇指南以 Data-Science-For-Beginners 课程「Working with Data: Relational Databases」为核心,系统讲解关系数据库的核心概念——表、行与列、主键(PK)与外键(FK),并手把手演示如何使用 SQL 的SELECT、WHERE与INNER JOIN从多张表中检索和合并数据。读完本文,你将能够理解"为什么单表会带来数据冗余",掌握用主外键拆表建模的思路,并能在 VS Code 中直接操作仓库自带、真实可查询的 airports.db 完成实战练习。
一切从表开始:关系数据库的最小单元
关系数据库(Relational Database)最核心的组成单元是表(Table)。如果你用过 Excel 等电子表格软件,就已经熟悉它的基本形态:一张表由若干行(Row)和列(Column)构成。行中存放的是我们真正关心的数据(如一座城市的名字、某年的降水量),而列则描述这些数据的含义——这类"描述数据的数据"有时也被称为元数据(Metadata)。
以课程文档中的示例为例,我们用一张表存放城市信息:
| City | Country |
|---|---|
| Tokyo | Japan |
| Atlanta | United States |
| Auckland | New Zealand |
这里的列名City、Country就是元数据,它说明每一行里存储的分别是什么;而每一行则是对一座城市的完整描述。关系数据库正是建立在这个"列 + 行"的朴素原理之上,它的强大之处在于:允许把信息分散到多张表中,从而支持更复杂的数据、避免重复,并让数据探索方式更加灵活。
单表方案的短板:冗余与僵化
当我们只有三座城市时,单表看起来无可挑剔。可一旦数据量增长,问题就会暴露出来。课程文档以"年度降水量"为例展开了两个典型反例。
反例一:纵向重复行。如果我们在同一张表里按年份追加降水数据,为了保存 Tokio 三年(2018、2019、2020)的数据,城市名和国家名就必须重复出现三遍:
| City | Country | Year | Amount |
|---|---|---|---|
| Tokyo | Japan | 2020 | 1690 |
| Tokyo | Japan | 2019 | 1874 |
| Tokyo | Japan | 2018 | 1445 |
这种重复既浪费存储空间,也会带来更新隐患——比如城市名拼写变更时,你必须同步修改所有重复行,否则数据就会不一致。
反例二:横向扩展列。换个思路,把年份变成列:
| City | Country | 2018 | 2019 | 2020 |
|---|---|---|---|---|
| Tokyo | Japan | 1445 | 1874 | 1690 |
| Atlanta | United States | 1779 | 1111 | 1683 |
| Auckland | New Zealand | 1386 | 942 | 1176 |
虽然避免了行的重复,却引入了新的麻烦:每新增一个年份,就必须修改表结构添加新列;而且当数据量变大后,把年份作为列会让"检索某一年、按年份求和"这类计算变得非常别扭。
两个反例指向同一个结论:我们需要多张表,以及表与表之间的"关系(Relationship)"。通过把数据拆分,才能避免重复并获得操作上的灵活性。
关系的两个支点:主键与外键
拆分数据之前,必须先解决一个关键问题:如何唯一标识一张表中的一行?答案是主键(Primary Key,缩写PK)。
✅ 课程文档特别提醒:本课中"id"与"primary key"两个词会交替使用;这一概念同样适用于你稍后接触的 DataFrame——DataFrame 虽然不用"主键"这个术语,但行为上高度相似。
主键是用于标识表中某一行唯一记录的值。虽然理论上可以拿业务值当主键(例如直接用城市名),但实践中主键几乎总是数字或其它与业务无关的标识符——因为我们不希望主键发生改变,一旦主键变化,所有依赖它的关系都会被破坏。大多数数据库会自动生成自增主键。
于是课程把城市信息整理成一张带city_id主键的cities表:
| city_id | City | Country |
|---|---|---|
| 1 | Tokyo | Japan |
| 2 | Atlanta | United States |
| 3 | Auckland | New Zealand |
存放降水量的rainfall表则不再重复城市名与国家,而是只保存城市的引用:
| rainfall_id | city_id | Year | Amount |
|---|---|---|---|
| 1 | 1 | 2018 | 1445 |
| 2 | 1 | 2019 | 1874 |
| 3 | 1 | 2020 | 1690 |
| 4 | 2 | 2018 | 1779 |
| 5 | 2 | 2019 | 1111 |
| 6 | 2 | 2020 | 1683 |
| 7 | 3 | 2018 | 1386 |
| 8 | 3 | 2019 | 942 |
| 9 | 3 | 2020 | 1176 |
注意rainfall表中的city_id列:它存放的值指向cities表的主键。在关系数据库术语中,这种"引用另一张表主键"的列被称为外键(Foreign Key,缩写 FK)。你可以把它理解成一个引用或指针——例如city_id为 1 的记录,就指向城市 Tokio。同时,新表也保留了自己的主键rainfall_id,因为每一张表都应该有自己的主键。
[!NOTE] 外键常被缩写为FK;主键常被缩写为PK。
用 SQL 检索数据:SELECT 与 WHERE
数据拆到两张表后,如何把需要的信息取回来?答案是 SQL——结构化查询语言(Structured Query Language)。像 MySQL、SQL Server、Oracle 这类关系数据库都支持 SQL,它是检索和修改关系数据库中数据的标准语言(SQL 常被读作 "sequel")。
检索数据的基础命令是SELECT:在SELECT后列出你想看的列,在FROM后列出这些列所在的表。如果只想显示所有城市名称:
SELECT city FROM cities; -- Output: -- Tokyo -- Atlanta -- Auckland[!NOTE] SQL 语法不区分大小写,
select与SELECT等价;但列名、表名在某些数据库上可能区分大小写。因此编程中最好的习惯是"默认一切都区分大小写"。写 SQL 时,业内惯例是把关键字统一写成大写。
上面的查询会返回所有城市。如果我们只关心新西兰的城市呢?这时需要过滤条件,SQL 的关键字是WHERE——即"在条件为真的地方":
SELECT city FROM cities WHERE country = 'New Zealand'; -- Output: -- AucklandWHERE接受一个布尔表达式,只有满足条件的行才会出现在结果中。
合并两张表:INNER JOIN
到目前为止我们都是从单表取数。现在要把cities和rainfall的数据合并展示,这就要用到Join(连接)。Join 的本质是在两张表之间"缝合"起来:把一张表的某列值与另一张表某列的值一一匹配。
本课的例子是用rainfall表的city_id与cities表的city_id做匹配,从而把每条降水记录关联回它的城市。这里使用的连接类型是INNER JOIN(内连接):如果某行在另一张表中找不到匹配,则不会出现在结果里。本例中每座城市都有降水数据,所以全部行都会显示。
先看第一步——仅指定连接列,把两表缝合:
SELECT cities.city, rainfall.amount FROM cities INNER JOIN rainfall ON cities.city_id = rainfall.city_id说明:
SELECT后以"表名.列名"的方式限定列,避免两张表出现同名列时产生歧义;ON后面给出两表连接的"缝线"条件。原文档示例中两列之间需以逗号分隔,这是 SQL 的合法写法。
在此基础上追加WHERE,只保留 2019 年的降水数据:
SELECT cities.city, rainfall.amount FROM cities INNER JOIN rainfall ON cities.city_id = rainfall.city_id WHERE rainfall.year = 2019 -- Output -- city | amount -- -------- | ------ -- Tokyo | 1874 -- Atlanta | 1111 -- Auckland | 942可以看到,INNER JOIN负责"横向拼表",WHERE负责"纵向筛行",两者组合即可完成"按城市关联、按年份过滤"的典型分析需求。这正是关系数据库的威力所在:数据虽然分散在多张表,但通过连接可以随时按需重组,进行展示、计算与各种灵活操作。
实战:用 airports.db 练习连接查询
理论讲完,现在进入可运行的实战环节。课程为这一课配套了真实数据库 airports.db(基于 SQLite 构建),其中包含英国与爱尔兰的机场数据。完整练习说明见 作业文档,下面给出从零开始的操作路径与参考答案。
准备环境:VS Code + SQLite 扩展
- 安装 Visual Studio Code;
- 在其扩展市场安装SQLite 扩展(
SQLiteby alexcvzz),该扩展提供了图形化的数据库浏览与查询入口。
[!NOTE] 更详细的扩展使用说明请查阅扩展自身的文档页面。
打开数据库并建立查询窗口
- 打开 Visual Studio Code;
- 按Ctrl-Shift-P(Mac 上为Cmd-Shift-P),输入并执行
SQLite: Open database; - 选择Choose database from file,打开上文提到的 airports.db;
- 打开数据库后界面可能没有明显变化,此时再按Ctrl-Shift-P(Mac 上为Cmd-Shift-P),输入并执行
SQLite: New query新建查询窗口; - 在查询窗口中编写 SQL,用Ctrl-Shift-Q(Mac 上为Cmd-Shift-Q)执行。
[!NOTE] 数据库的 schema(结构设计)决定了你能查询什么。
airports库包含两张表:Cities存放英国与爱尔兰的城市列表,Airports存放机场列表。由于一座城市可能拥有多个机场,所以拆成两张表,通过外键关联——这正是本文前半部分讲解的建模思路。
数据库结构速览
对照数据库实际定义(可通过PRAGMA table_info查看),两张表的结构如下:
| Cities |
|---|
| id (PK, integer) |
| city (text) |
| country (text) |
| Airports |
|---|
| id (PK, integer) |
| name (text) |
| code (text) |
| city_id (FK → Cities.id) |
从源码结构看(airports.db 实际表定义),Airports.city_id即为指向Cities.id的外键;Airports表中目前存有 181 条机场记录。
四道练习与参考答案
以下查询结果均基于仓库中的 airports.db 实际执行验证,可直接在 VS Code 查询窗口复现。
练习 1:列出Cities表中的全部城市名
SELECT city FROM Cities;练习 2:列出Cities表中位于爱尔兰(Ireland)的全部城市
SELECT city FROM Cities WHERE country = 'Ireland';执行后共返回 16 行,包括 Cork、Galway、Dublin、Shannon、Kerry 等爱尔兰城市。
练习 3:列出所有机场的名称及其所在城市和国家
SELECT a.name, c.city, c.country FROM Airports a INNER JOIN Cities c ON a.city_id = c.id;这里用a、c作为表的别名(Alias),并以外键a.city_id = c.id作为连接条件。结果中的部分行例如:
Belfast International Airport | Belfast | United Kingdom George Best Belfast City Airport | Belfast | United Kingdom Birmingham International Airport | Birmingham | United Kingdom练习 4:找出伦敦(London, United Kingdom)的全部机场
SELECT a.name, a.code FROM Airports a INNER JOIN Cities c ON a.city_id = c.id WHERE c.city = 'London' AND c.country = 'United Kingdom';这一步同时用到了本文的全部知识点:INNER JOIN关联两张表,WHERE过滤目标城市,最终返回伦敦的 6 个机场及其 ICAO 代码:
| name | code |
|---|---|
| London Luton Airport | EGGW |
| London Gatwick Airport | EGKK |
| London City Airport | EGLC |
| London Heathrow Airport | EGLL |
| London Stansted Airport | EGSS |
| London Heliport | EGLW |
可以看到,练习 4 与前文"cities + rainfall 按 2019 年过滤"的 JOIN 思路完全一致:先连接、后过滤。掌握了SELECT、FROM、WHERE、INNER JOIN、ON这五个要素,你就已经掌握了关系数据库最核心的日常操作。
总结
关系数据库的核心思想,是把信息拆分到多张表中,再在需要展示与分析时通过 Join重组起来。这种"拆-合"的循环带来了极高的灵活性,使计算和数据操作不再受限于单表结构。通过本课,你已经理解了:
- 表、行、列与元数据的关系;
- 单表方案的两种典型缺陷(行重复、列扩展导致的冗余与僵化);
- 用主键(PK)唯一标识行、用外键(FK)跨表引用,从而建立表间关系;
- 用
SELECT ... FROM检索、用WHERE过滤、用INNER JOIN ... ON合并两张表; - 在真实数据库 airports.db 上完成从环境搭建到四道连接查询练习的完整流程。
下一步,你可以进入 第 6 课:非关系型数据,对比 NoSQL 与关系模型的差异;也可以在仓库中继续学习 用 Python 处理数据,把 SQL 思维迁移到 DataFrame 中。本文对应的英文原版文档见 05-relational-databases 英文 README,德语译本即 当前文档。
【免费下载链接】Data-Science-For-Beginners10 Weeks, 20 Lessons, Data Science for All!项目地址: https://gitcode.com/GitHub_Trending/da/Data-Science-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考