哈喽,掘金的各位全栈练习生们!欢迎来到 AI全栈项目实战的第三天。
在前两天的课程中,也许你已经搞定了前端的页面,或者搭建了简单的 Node.js 服务。但今天,我们要进入一个更加深邃、更加迷人,也绝对是"后端工程师"分水岭的领域 ------ 数据库。
如果你觉得数据库只是用来"存个数据"的,那你就太小看它了。在 AI 全栈的架构中,数据库不仅是仓库,它是数据的堡垒 ,是业务逻辑的最终防线 。今天我们的主角,就是被誉为"世界上最先进的开源关系型数据库" ------ PostgreSQL(简称 Postgres 或 PG)。
准备好了吗?我们要开始"硬核"但"愉快"的旅程了!
🧐 为什么是关系型数据库 (RDBMS)?
在 NoSQL(如 MongoDB)大行其道的今天,为什么我们依然首选关系型数据库?
想象一下,你正在通过 Excel 管理一个学校。
- 你有一张表叫"学生表"(Users)。
- 你有一张表叫"文章表"(Posts)。
- 你有一张表叫"评论表"(Comments)。
这些表不是孤立的。学生写文章,文章有评论,评论属于某个学生。这种数据与数据之间存在严密逻辑关联 的结构,就是关系型数据库的灵魂。
PostgreSQL 就是这样一个以二维表格(行 Row / 列 Column) 为基础,通过主键 (Primary Key) 和 外键 (Foreign Key) 编织数据网络,并严格遵循 ACID 特性来保证数据"即使天塌下来也不会乱"的神器。
🛡️ 每一个全栈都必须烂熟于心的 ACID
在面试中,ACID 是必考题;在开发中,ACID 是保命符。我们结合一个经典的转账场景(A 转给 B 100元)来彻底搞懂它。
1. A - Atomicity(原子性)
"要么全部成功,要么玉石俱焚。" 事务(Transaction)是数据库操作的最小单位。
- 场景 :
- 从 A 账户扣除 100 元。
- (突然停电了/报错了)
- 向 B 账户增加 100 元。
- 如果没有原子性:A 的钱没了,B 也没收到,这 100 元凭空消失了!
- PG 的做法 :如果第 2 步失败了,第 1 步的操作会立刻回滚(Rollback),仿佛一切从未发生过。
2. C - Consistency(一致性)
"能量守恒定律。" 数据库必须从一个"正确状态"变更为另一个"正确状态"。
- 场景:转账前后,A 和 B 的账户总金额必须保持不变(比如一共 1000 元)。
- PG 的做法:通过约束(Constraints)和触发器,确保数据不会违反你设定的规则(比如余额不能为负数)。
3. I - Isolation(隔离性)
"你干你的,我干我的。" 并发执行的事务之间互不干扰。
- 场景:A 正在给 B 转 100 元,同时 C 也在给 B 转 200 元。
- PG 的做法:数据库会通过锁机制或 MVCC(多版本并发控制),确保这两笔交易不会因为同时发生而导致 B 的余额算错。
4. D - Durability(持久性)
"落子无悔。" 事务一旦提交(Commit),对数据的改变就是永久的。
- 场景:提示"转账成功"后,哪怕下一秒数据库服务器爆炸了,只要硬盘还在,数据就在。
🛠️ 实战:设计一个 AI 博客系统的数据库
Talk is cheap, show me the code. 我们来为博客系统设计两张最核心的表:用户表 (Users) 和 文章表 (Posts)。
第一步:用户表 (Users) ------ 身份的基石
我们需要存储用户的 ID、用户名、密码以及注册时间。
看看这段由资深 PG 工程师编写的 SQL:
sql
CREATE TABLE users (
-- 【重点 1】主键与自增
-- id: 用户的唯一标识。
-- BIGINT: 为什么不用 INT?INT 最大约 21 亿。对于大型互联网应用,BIGINT (8字节) 更安全,避免 ID 溢出。
-- GENERATED BY DEFAULT AS IDENTITY: 这是 SQL 标准推荐的自增写法,比旧版的 SERIAL 更规范。
-- PRIMARY KEY: 主键约束,意味着这个字段 1. 唯一 2. 不为空 3. 自带索引。
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
-- 【重点 2】唯一性与非空
-- VARCHAR(255): 变长字符串,节省空间。
-- NOT NULL: 必填项,防止出现"无名氏"。
-- UNIQUE: 唯一约束。数据库会自动为 name 字段建立索引,保证查询速度,同时拒绝重复的用户名注册。
name VARCHAR(255) NOT NULL UNIQUE,
-- 【重点 3】密码安全
-- 永远不要明文存储密码!
-- 预留 255 长度是为了适配 bcrypt 等哈希加密算法生成的长字符串(如 $2b$10$...)。
password VARCHAR(255) NOT NULL,
-- 【重点 4】审计字段(高级工程师的素养)
-- 记录这条数据是什么时候创建的,什么时候最后修改的。
-- TIMESTAMPTZ: 带时区的时间戳 (Timestamp with time zone)。全球化应用必备,它会帮你处理夏令时和跨国时区问题。
-- DEFAULT CURRENT_TIMESTAMP: 如果你不传值,默认填入当前时间。
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
🔍 深度解析:
- 主键 (Primary Key):就像身份证号。它是词典的索引目录,查询速度极快。
- 唯一索引 (Unique) :给
name加上这个,不仅是为了不重名,更是为了在登录时通过用户名快速查找到用户(数据库不需要全表扫描,而是直接走索引树)。
第二步:文章表 (Posts) ------ 关系的建立
文章属于用户。这里涉及到了一对多关系(一个用户可以写多篇文章)。
sql
CREATE TABLE posts (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
-- 标题不能为空,长度限制适中
title VARCHAR(255) NOT NULL,
-- 内容是长文本,使用 TEXT 类型,没有长度限制,适合存 Markdown 源码
content TEXT,
-- 【重点 5】外键关联
-- 这里存储的是 users 表里的 id。
-- 注意加了双引号 "userId",因为 PG 默认会将未加引号的字段转为小写。如果你想保留驼峰命名,必须加双引号!
"userId" BigInt NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
-- 【重点 6】外键约束 (Foreign Key Constraint)
-- CONSTRAINT fk_posts_user: 给这个约束起个名字,方便报错时定位。
-- FOREIGN KEY ("userId") REFERENCES users(id): 声明 posts 表的 userId 引用自 users 表的 id。
-- ON DELETE CASCADE: 级联删除。这非常关键!
-- 意思是:如果"李白"这个用户被删除了,那么"李白"写的所有文章也会被自动删除。
-- 如果没有这句,删除用户时数据库会报错,提示还有文章依赖于该用户。
CONSTRAINT fk_posts_user FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE
);
🔍 深度解析:
- 外键 (Foreign Key) :这是连接两张表的桥梁。
posts.userId <==> users.id。 - 普通外键 vs 乱建 :外键不能乱建,虽然它保证了数据完整性(你不能给一个不存在的用户添加文章),但过多的外键会影响写入性能(每次插入都要去检查父表)。但在核心业务中,数据的一致性 > 极致的写入性能。
🥗 数据填充 (Seeds)
表建好了,空荡荡的怎么办?我们需要"种子数据"来初始化数据库,方便测试。
1. 插入用户 (Users)
注意这里密码已经是加密后的乱码(模拟真实场景)。
sql
INSERT INTO "users" ("id", "name", "password") VALUES
('1', '王皓', '$2b$10$CsO/ykedPpuxqUETBZTYm.F2U4TXDdo01rLmoRPwjKBv3pIL5pnWq'),
('2', '小雪', '$2b$10$CsO/ykedPpuxqUETBZTYm.F2U4TXDdo01rLmoRPwjKBv3pIL5pnWq'),
('3', '李白', '$2b$10$CsO/ykedPpuxqUETBZTYm.F2U4TXDdo01rLmoRPwjKBv3pIL5pnWq'),
-- ... 更多数据
('6', '张三', '$2b$10$CsO/ykedPpuxqUETBZTYm.F2U4TXDdo01rLmoRPwjKBv3pIL5pnWq');
2. 插入文章 (Posts)
这里我们用诗句来充当内容,充满诗意的数据库!
sql
INSERT INTO "posts" ("title", "content", "userId") VALUES
('黄鹤楼送孟浩然之广陵', '故人西辞黄鹤楼,烟花三月下扬州', 3), -- 李白写的
('春夜喜雨', '好雨知时节,当春乃发生', 4), -- 杜甫写的
('琵琶行', '浔阳江头夜送客,枫叶荻花秋瑟瑟', 5), -- 白居易写的
('静夜思', '床前明月光,疑是地上霜', 3); -- 李白写的
注意:userId 必须是 users 表里已经存在的 id(1-6),否则外键约束会直接报错!这就是 PG 的严谨之处。
🤝 玩转连接查询 (Joins)
数据分表存了,怎么把它们"连"起来看?比如我想看"所有文章以及作者的名字"。这就需要用到 Join。
1. 左连接 (Left Join) ------ "以我为主"
sql
SELECT posts.title, users.name
FROM posts
LEFT JOIN users ON posts."userId" = users.id;
- 含义 :
posts是左表。显示所有 文章。如果某篇文章没有作者(虽然我们的约束不允许,但假设允许),作者栏会显示NULL。 - 口诀:左表全显示,右表配不上的补空。
2. 右连接 (Right Join) ------ "客随主便"
sql
SELECT posts.title, users.name
FROM posts
RIGHT JOIN users ON posts."userId" = users.id;
- 含义 :
users是右表。显示所有 用户。如果"张三"没写过文章,他的名字也会出现,但文章标题栏是NULL。
3. 内连接 (Inner Join) ------ "两情相悦"
sql
SELECT posts.title, users.name
FROM posts
INNER JOIN users ON posts."userId" = users.id;
- 含义 :只显示既有文章又有作者的记录。如果张三没写文章,张三不会出现;如果有一篇无头文章,也不会出现。这是最常用的连接方式。
⚡️ 常用 PSQL 命令行指令
作为全栈开发者,不要只依赖图形化工具,命令行才是装 X...哦不,效率的体现!
\list或\l: 列出所有数据库(看一眼你的战利品)。\c xuebi: 连接到名为xuebi的数据库(进入战场)。\dt: 列出当前数据库下的所有表(查看兵力)。\d users: 查看users表的详细结构(查看兵种属性)。
🎓 总结
今天我们不仅学习了 SQL 语法,更重要的是理解了数据设计的哲学。
- ACID 给了我们安全感。
- 主外键 给了我们逻辑关联。
- 数据类型 的选择体现了工程师的远见(BIGINT, TIMESTAMPTZ)。
数据库设计没有绝对的"完美",只有最适合业务的"权衡"。希望通过今天的学习,你在敲下 CREATE TABLE 时,脑海里能浮现出数据在磁盘上井井有条流动的画面。
下节课,我们将拿起 Node.js 这个强大的武器,去连接并操作我们刚刚建立的"数据堡垒"。敬请期待!
如果你喜欢这篇文章,别忘了点赞、收藏、关注三连哦!这对我持续输出高质量技术内容非常重要! 🚀