数据库与 SQL 入门:The Odin Project 课程中的关系数据库核心概念与实战
【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum
数据库是 Web 应用的底层基石,负责替你记住一切用户数据——从登录密码到文章内容。本文以 databases/databases/databases.md 课程为主体,系统讲解数据库是什么、关系数据库如何用表组织数据、SQL(结构化查询语言)如何增删改查数据,并结合本仓库后续课程(databases_and_sql.md)与真实工程实践(PostgreSQL、ORM、迁移)展开。读完本文,你将掌握数据库的基本心智模型,能够理解并写出SELECT、INSERT等核心 SQL 语句,为后续学习 ORM 与后端开发打下基础。
数据库:Web 应用最底层的数据基石
你可能好奇过:网站是如何记住所有用户数据的?是谁记得你的登录密码,好让你顺利登录?答案是——任何 Web 应用的最底层都是数据库(database),它负责完成所有的"记忆"工作(缓存是更后面的话题)。
数据库既可以简单得像一张 Excel 电子表格,也可以复杂得像 Facebook 那样被拆分成许多巨大的部分。它们通常藏在 Web 应用的"幕后",因此初学者往往对其既好奇又敬畏。但事实上,数据库并不需要让人望而生畏——随着学习的深入,你会与它成为好朋友(至少也是"相爱相杀"的伙伴),最终能够熟练地与之交互,甚至比一些在职开发人员做得更好。
从整个 Web 开发的视角看,本仓库的 后端导论 明确指出:后端负责"幕后"的一切,而数据库正是后端存储数据的地方。前端只用 HTML、CSS、JavaScript 与用户交互,而后端则要考虑数据如何持久化——这就是数据库登场的时机。
关系数据库:用表来组织数据
大多数主流数据库(本课程聚焦的那类)都是关系数据库(relational database)。关系数据库使用大量**表(table)**来存储不同类型的数据,例如users表和posts表。
可以把表想象成电子表格:每一**行(row)是一条记录(record,即一个对象,比如一个具体的用户),每一列(column)**是该记录的一个属性(比如姓名、邮箱)。以下是本课程中反复出现的经典示例模型:
| users 表 | posts 表 | |||||
|---|---|---|---|---|---|---|
| id | name | created_at | id | user_id | title | |
| 1 | foobar | foo@bar.com | ... | 1 | 1 | Hello |
主键(Primary Key)
所有表都包含一列ID,为每一行提供唯一的编号,这一列被称为记录的主键(primary key)。主键保证了每条记录可以被唯一标识——这也是后续删除、更新单条记录时的安全依据。
外键(Foreign Key)
关系数据库的威力在于"关联"。你可以通过让一张表的某一列指向另一张表的 ID 来"链接"两张表。例如,posts表中的一行可能包含作者 ID,存在user_id列中。因为posts表持有另一张表的 ID,所以该列被称为外键(foreign key)。
本仓库 Rails 关联基础课程 是这样总结的:关系数据库(如 SQLite3 或 PostgreSQL)允许你通过主键把两张表链接起来,而引用其他表的键就是外键;这正是关系数据库的真正力量所在——利用这些"关系"。例如在 Rails 中,声明has_many :posts/belongs_to :user之后,数据库表层面就是posts表的每一行都有一个user_id列来标明它属于哪个用户。
关系数据库与 XML 的区别
课程大纲中特别提出了一个问题:关系数据库与 XML 有何不同?
简而言之:XML 是一种文档格式,用嵌套标签(如<users><user>...</user></users>)以层级结构描述数据,数据保存在文档里,适合数据交换与配置表达;而关系数据库以表和行的扁平结构存储数据,通过主键/外键关系把不同实体关联起来,并提供了标准化的查询语言(SQL)进行检索、聚合与更新。关系数据库强项在于:大规模数据的 CRUD 操作、条件查询、跨表关联和事务一致性——这些都是 XML 文档难以高效胜任的场景。这也是为什么课程后续会把大量篇幅放在 SQL 与关系数据库上。
SQL:与关系数据库对话的语言
**SQL(Structured Query Language,结构化查询语言)**是用于查询数据库的语言。与已经学过的普通编程语言相比,SQL 的语法非常简短——只需掌握一小撮动词(statement),例如SELECT、INSERT、UPDATE、DELETE、CREATE等。
真正让人犯难的是:你需要学会在脑海中可视化一条 SQL 语句到底在做什么——它选中了哪些表、筛选了哪些行、聚合了哪些列。SQL 并不是一门大语言,更重要的是理解其背后的概念。
SQL 的两个书写习惯值得一开始就养成:
- 语句以**分号(
;)**结尾; - 字符串使用**单引号(
')**而非双引号(")。
SQL 用来做什么
SQL 的核心用途就是"向数据库提问"——查询数据,偶尔也新增或修改数据。本仓库的 SQL 进阶课程 给出了生动的例子:
- 简单场景:显示所有通过促销码 "FREESTUFF" 在 12 月注册的用户;
- 中等场景:显示当前用户创建的所有评论,并按主题和创建日期排序;
- 复杂场景:列出所有发往用户数超过 1000 的州的订单,按数量和订单总价排序;
- 内部运营场景:统计哪些推广渠道带来的用户达到了"每工作日阅读 5 篇文章"的活跃标准。
幸运的是,我们要聚焦的数据库大多会说 SQL,而 SQL 常用的动词只有十几个。学会 SQL,你就能理解 ORM(对象关系映射,如 Rails 的 Active Record 或 Node.js 的 Prisma)在幕后做了什么,也能更自在地向数据提出更复杂的问题。
Schema 与数据库搭建:从建库到建表
SQL 可以做所有事。第一类命令用于搭建环境:创建数据库(CREATE DATABASE)、创建表(CREATE TABLE),以及修改或销毁它们的命令。
什么是 Schema
数据库的设置信息保存在一个特殊的地方,叫做Schema(模式)。每当你修改数据库的结构,Schema 都会被更新。可以把 Schema 理解为一份"数据库说明书":
这是我们的数据库,它有几张表。第一张表叫
users,包含ID(整数)、name(一串字符)、
Rails 课程 迁移(Migrations) 也印证了这一点:Schema 就是数据库的结构——创建和删除表、增删列都属于 Schema 变更,而增删行不算。Rails 中的"迁移"本质上是 Schema 迁移:在不移动数据位置的前提下修改数据库结构。
用索引加速查询
除了建表,你还可以让数据库限制某列只能有唯一值(比如用户名),或者用CREATE INDEX为某列建立索引,以便日后更快地搜索。索引本质上是在建表时提前做了"排序"工作——对将来要频繁用于搜索的列(如用户名)建立索引,会让数据库快得多。
一个真实的建库建表例子
本仓库的 PostgreSQL 实战课程 展示了在 PostgreSQL shell 中完整的建库建表流程。先创建数据库并连接:
CREATE DATABASE top_users; \c top_users再创建一张存储用户名的表(using_postgresql.md):
CREATE TABLE usernames ( id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, username VARCHAR ( 255 ) );这里用到了几个关键概念:id列被声明为主键,GENERATED ALWAYS AS IDENTITY使其成为标识列(identity column)——PostgreSQL 会自动为每一行生成自增的值(默认从 1 开始、每次加 1),并隐式创建usernames_id_seq序列对象来跟踪下一个值。这正是"主键保证每行唯一"在生产数据库中的具体落地方式。
用 CRUD 语句操纵数据
数据库搭建好、有了空表之后,就要开始填充数据了。核心操作就是我们熟悉的CRUD:Create(创建)、Read(读取)、Update(更新)、Destroy(销毁)。大部分命令属于"读取"类别,因为你会花大量时间向数据提问并展示结果。
每条 CRUD 命令都包含几个部分:动作(statement)、它作用的表、以及条件(clause)。如果对整张表执行动作而不指定条件,就会作用于整张表——你很可能搞砸某些东西。
Create:用 INSERT INTO 插入记录
"创建"查询使用INSERT INTO,需要指定要插入值的列,后面跟上对应的值(databases_and_sql.md):
INSERT INTO users (name, email) VALUES ('foobar', 'foo@bar.com');注意:技术上可以省略列名,但这属于不良实践,通常不推荐。
这是少数几种不需要小心"选中了哪些行"的查询,因为你只是在向表里新增记录。PostgreSQL 实战中插入多条记录的形式(using_postgresql.md):
INSERT INTO usernames (username) VALUES ('Mao'), ('nevz'), ('Lofty');Read:用 SELECT 获取所有记录
"读取"查询使用SELECT,是最常用的语句。**如何获取一张表的所有记录?**答案是:
SELECT * FROM users;这里的*表示"所有列"。课程还给出了带条件的读取示例:SELECT * FROM users WHERE created_at < '2013-12-11 15:35:59 -0800'。
推荐始终用"表名.列名"的方式指定列:单表查询时只写列名可以蒙混过关,但只要涉及多张表,SQL 就会报错,所以最好养成习惯:
SELECT users.id, users.name FROM users;SELECT的近亲是SELECT DISTINCT,用于只取某列的不重复值。例如想列出所有用户的不同名字、去掉重复项:
SELECT DISTINCT users.name FROM users;Update:用 UPDATE 修改记录
"更新"查询使用UPDATE,需要告诉它要SET什么数据(键值对形式)以及更新哪些行。小心:如果WHERE子句匹配到多行(比如按常见名字搜索),它们都会被更新。更新用户邮箱的典型查询如下(现实中应按始终唯一的 ID 搜索):
UPDATE users SET name='barfoo', email='bar@foo.com' WHERE email='foo@bar.com';Destroy:用 DELETE 删除记录
"销毁"查询的经典错误是:只写DELETE FROM users而没有WHERE子句——这会清空表中所有用户。正确做法是基于某个(希望是唯一的)属性指定条件,例如:
DELETE FROM users WHERE users.id = 1;你可以运用各种常识性写法:用比较运算符(>、<、<=等)指定要操作的行组,或用逻辑运算符(AND、OR、NOT等)串联多个条件:
DELETE FROM users WHERE id > 12 AND name = 'foo';用 JOIN 把表"缝合"在一起
如果想获取某个用户创建的所有帖子,你需要告诉 SQL 用哪几列把两张表"拉链"起来——这就是ON子句;执行"拉链"动作的是JOIN命令。
但问题来了:如果把两张数据不完全匹配的表拼在一起(比如一个用户有多条帖子),该保留哪些行?有四种可能:
| JOIN 类型 | 行为 |
|---|---|
INNER JOIN(即JOIN) | 只保留两张表中互相匹配的行。这是你最常用的类型,大约覆盖 95% 的需求。 |
LEFT OUTER JOIN | 保留左表所有行,并把右表中匹配的行附加进来;没有匹配的单元格置为NULL。 |
RIGHT OUTER JOIN | 与 LEFT 相反,保留右表所有行。 |
FULL OUTER JOIN | 保留所有表中的所有行,即使不匹配;不匹配的单元格置为NULL。 |
澄清:JOIN 中的"左表"指的是
FROM子句后面的原始表(即ON之前的表),例如下面示例中的users。
例如,获取所有写了帖子的用户及其帖子:
SELECT * FROM users JOIN posts ON users.id = posts.user_id;如果某位作者写了多篇帖子,会返回多行(其中用户数据列会重复出现)。JOIN 也可以自然地带条件,比如只要 ID 为 42 的用户的帖子:
SELECT * FROM users JOIN posts ON users.id = posts.user_id WHERE users.id = 42;聚合函数、GROUP BY 与 HAVING
普通的 SQL 查询通常返回一堆行。但有时你只想要一个能聚合某列的单一值,比如统计某个用户写了多少帖子。这时就用 SQL 提供的聚合函数——SUM、MIN、MAX、AVG、COUNT等,都是符合直觉的函数。
聚合函数作为SELECT语句的一部分使用(databases_and_sql.md):
SELECT MAX(users.age) FROM users;聚合函数只作用于单列,除非你指定*——*只对部分函数有意义,比如COUNT(*)统计所有行,而MAX(*)没有意义("对一切取最大值"是什么?)。
用 AS 起别名
通常会用别名(AS)重命名列或聚合函数,便于之后引用:
SELECT MAX(users.age) AS highest_age FROM users;这条语句会返回一个名为highest_age的列,其中是最大年龄。
用 GROUP BY 分组聚合
聚合函数真正的乐趣在于:对数据中特定的"块"分组后再聚合。例如,要显示每个用户的帖子数(而不是所有用户的总帖子数):
SELECT users.id, users.name, COUNT(posts.id) AS posts_written FROM users JOIN posts ON users.id = posts.user_id GROUP BY users.id, users.name;建议:除了
users.id,再显式把users.name也加入GROUP BY。把所有选中的非聚合列都放进GROUP BY是清晰且符合最佳实践的做法,虽然对多数数据库来说并非严格必需。
HAVING vs WHERE:聚合后的条件过滤
最后一个技巧:如果只想显示数据的子集,平时用WHERE缩小范围。但一旦用了COUNT这样的聚合函数(比如上面的"每个用户的帖子数"),WHERE就失效了。要基于聚合函数的结果条件化地取记录,需要使用HAVING子句——HAVING就是针对聚合的WHERE。
例如,只显示写了 10 篇以上帖子的用户:
SELECT users.id, users.name, COUNT(posts.id) AS posts_written FROM users JOIN posts ON users.id = posts.user_id GROUP BY users.id, users.name HAVING COUNT(posts.id) >= 10;WHERE 与 HAVING 的区别小结
WHERE:在聚合之前筛选行,作用于原始表数据;HAVING:在聚合之后筛选分组,作用于聚合结果(如COUNT(...)的值)。
为什么用 SQL 而不是应用程序代码处理数据
这部分知识尤其重要,因为巧妙地用 SQL 构建查询,比把一大堆数据拉出来再用编程语言(Ruby 或 JavaScript)处理要快得多。
举例:想要所有用户的不重复名字。你可以用SELECT users.name FROM users拉出全列表,再用 JS/Ruby 方法去重——但这就需要把数据从数据库取出、放进内存、再遍历处理。改用SELECT DISTINCT users.name FROM users,让 SQL 一步完成。
原因在于 SQL 天生为速度而设计:它内置查询优化器(query optimizer),会通盘审视即将执行的查询,计算出需要连接哪些表、如何最快执行。SELECT与SELECT DISTINCT之间的性能差异,相比你在代码里手动处理的耗时几乎可以忽略。学好 SQL,写出能做更多事的更优查询,会让你的应用快得多。
课程配套练习与后续学习路径
SQL Zoo 实战项目
本课知识需要在实践中固化。仓库的 SQL Zoo 项目 是少有的能让你对着现成表实际构建并运行查询的在线资源:每个教程展示一张表,然后让你运行查询回答具体问题。项目要求完成 Tutorials 0-9(包括带 +/- 标记的)及每节末尾的测验:
- 确保主页面右上角 "Engine" 下拉框选择 "MySQL"(默认值);
- 注意大结果会被截断,并非所有行和列都会显示,因此"答案"可能看起来不完全正确,这属正常现象。
下一课:SQL 进阶
本课只是"预告片"。紧接着的 databases_and_sql.md 会深入讲解主键/外键、Schema、WHERE/LIKE/DISTINCT子句、AVG/COUNT/SUM函数、索引、WHERE与HAVING的区别,并提供两个交互式 SQL 教程作为作业,是巩固本课概念的自然延伸。
从 SQL 到 ORM:让数据库进入代码库
学会 SQL 后,你会接触 ORM(对象关系映射)。本仓库 Prisma ORM 课程 总结了裸写 SQL 的痛点:代码量大、数据库结构难以在代码库中体现、迁移难以管理。ORM 通过把数据库定义(Schema)引入代码库、提供类型安全的查询客户端(如prisma.message.findMany())和标准化的迁移机制来解决这些问题。Rails 侧的 Active Record 关联 则把外键关系抽象为has_many/belongs_to,例如用User.first.posts就能拿到第一个用户的全部帖子。
迁移:Schema 的版本管理
在实际工程中,数据库结构会随需求演进。Rails 迁移课程 指出:迁移是"以可逆、可重复应用的方式设置或修改数据库 Schema 的脚本",rails db:migrate执行未运行的迁移,rails db:rollback撤销最近一批迁移——这都建立在对"Schema 是数据库结构"的清晰理解之上,也就是本课的核心概念。
知识检查
课程末尾的知识检查问题及本文给出的答案速览:
- 什么是数据库?数据库是 Web 应用最底层的存储层,负责持久化并记住所有用户数据,小到电子表格,大到 Facebook 级别的分布式系统。
- 什么是关系数据库?使用多张表(行=记录、列=属性)存储数据,并通过主键/外键在表之间建立关联的数据库。
- 什么是主键?每张表都有的
ID列,为每一行提供唯一编号,用于唯一标识记录。 - 什么是 SQL?结构化查询语言,用于与关系数据库对话,语法简短、只有少量常用动词。
- 如何获取一张表的所有记录?使用
SELECT * FROM table_name;。 - 如何插入一条记录?使用
INSERT INTO table_name (col1, col2) VALUES (val1, val2);。
结语
数据库与 SQL 是任何 Web 应用的地基。本课的目标不是让你立刻精通,而是建立起"数据库如何工作"的心智模型:数据以表的形式组织,主键与外键把实体关联起来,SQL 用少量动词完成从建库建表到增删改查的全部操作。在后续课程中,你会在 SQL Zoo 等实战项目中反复练习这些概念,进而理解 ORM 如何在幕后替你生成 SQL——到那时,你与数据库的关系将真正从"敬畏"变成"专业"。
【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考