小白python入门 - 25. SQL 与表设计入门

1. 先看一张「像 Excel」的表

上一课说了「为什么需要数据库」。本课不讲空道理,先看数据长什么样

假设班级要记同学信息,在 Excel 里可能是这样:

学号 姓名 手机号
1 小明 13800001111
2 小红 13900002222
3 小刚 13700003333

在数据库里,这整张格子就叫一张 表(table)。可以把它想成:

  • ≈ 一张有表头的 Excel 工作表(比如叫 students
  • 列(column) ≈ 表头:学号姓名手机号(规定每一列存什么)
  • 行(row) ≈ 表头下面的每一条记录:小明一行、小红一行

再记三个中文说法,后面会反复用到:

说法 是什么 上面例子
一类东西的清单 学生表
行 / 记录 清单里的一条 小明那一整行
列 / 字段 清单里的一种信息 「姓名」这一列

小练习(心里想) :若再加一列「生日」,是加了一 还是一

答案:加的是一(多了一种信息);小明、小红各多一个生日格子,行数仍是 3。


2. 为什么要拆成多张表?(看坏例子)

Day24 的记账如果全塞进一张大表,可能变成:

流水号 用户姓名 用户邮箱 金额 备注
1 alice alice@example.com 12.5 lunch
2 bob bob@example.com 30.0 taxi
3 alice alice@example.com -5.0 refund

问题一眼能看出来:

  1. alice 的邮箱写了两遍。她以后改邮箱,你要改两行(实际业务可能改几百行),漏改一行就乱了。
  2. 「人」和「一笔账」混在同一行,表又宽又难维护。

更好的做法(本课主线):

表 1:users(只存「人」)

id name email
1 alice alice@example.com
2 bob bob@example.com

表 2:ledger(只存「账」)

id user_id amount note
1 1 12.5 lunch
2 2 30.0 taxi
3 1 -5.0 refund

注意 ledger 里没有再写姓名、邮箱,只写了 user_id = 1 表示「属于 id 为 1 的用户」

1 就是指向 users 表里 alice 那一行的「门牌号」。

这样:

  • 改 alice 邮箱 → 只改 users 里一行;
  • 流水再多,也不用复制姓名邮箱。

3. 键、主键、外键------用「身份证 / 门牌号」理解

3.1 什么是「键」?

不是键盘上的键,而是:用来认出某一行 、或把两张表对上号的那一列(或几列)。

最常见两种:

名称 白话 在本课例子里
主键(PRIMARY KEY) 这一行的「身份证号」,全表唯一 users.idledger.id
外键(FOREIGN KEY) 写在 A 表里、指向 B 表主键的「门牌号」 ledger.user_idusers.id
3.2 主键长什么样?

再看 users 表,把主键圈出来:

text 复制代码
users
+----+-------+---------------------+
| id | name  | email               |   ← 列名
+----+-------+---------------------+
| 1  | alice | alice@example.com   |   ← 第 1 行,主键是 1
| 2  | bob   | bob@example.com     |   ← 第 2 行,主键是 2
+----+-------+---------------------+
     ↑
   主键列:每人一个号,不重复、不能空

规则记三句就够:

  1. 主键用来唯一认出一行(不能两个人都是 id=1)。
  2. 主键不能为空(没有身份证号就无法认人)。
  3. 常用自增整数:插入时不写 id,数据库自动给 1、2、3...

SQL 里常见写法:

sql 复制代码
id INTEGER PRIMARY KEY AUTOINCREMENT
3.3 外键长什么样?

外键写在「多」的那一侧。一笔账属于一个用户 → 外键在流水表:

text 复制代码
users(被指向的表)              ledger(带着门牌号的表)
+----+-------+                 +----+---------+--------+--------+
| id | name  |                 | id | user_id | amount | note   |
+----+-------+                 +----+---------+--------+--------+
| 1  | alice |  <----+         | 1  |    1    |  12.5  | lunch  |
| 2  | bob   |  <----|----+    | 2  |    2    |  30.0  | taxi   |
+----+-------+       |    |    | 3  |    1    |  -5.0  | refund |
                     |    |    +----+---------+--------+--------+
                     |    |              ↑
                     |    +---- user_id=2 表示「bob 的账」
                     +--------- user_id=1 表示「alice 的账」

对照读一遍:

  • 流水第 1 行:user_id = 1 → 去 usersid=1 → alice,午餐 12.5
  • 流水第 2 行:user_id = 2 → bob,打车 30
  • 流水第 3 行:又是 user_id = 1 → 还是 alice(一人可以有多笔账)

如果有人强行插入 user_id = 999,但 users 里没有 id=999 的人,这就是脏数据

打开外键约束后,数据库会直接拒绝,后面实践里你会看到报错:

IntegrityError: FOREIGN KEY constraint failed

3.4 和 Excel「VLOOKUP」的感觉类似

你在 Excel 里用学号去另一张表查姓名,就是「用编号把两张表对上」。

数据库的外键 + 查询,就是把这件事变成规则(不能指到不存在的学号),而不只靠人工小心。


4. SQL 是什么?(先建立地图)

SQL 是和数据库说话的专用语言。你写出「建什么表、查什么数据」,由数据库去执行,而不是自己用 Python 一行行扫文件。

本课只要分清四大类(后三类下节细讲):

类别 人话 例子
DDL 定规矩:桌子长什么样 CREATE TABLE users (...)
DML 改数据:增删改格子里的值 INSERT / UPDATE / DELETE
DQL 查数据:把符合条件的行找出来 SELECT ... FROM users
DCL 谁能看、谁能改(权限) 后续课

本课工具仍是 SQLite (Python 自带 sqlite3,不用先装 MySQL)。


5. 建表语句怎么读?(对照真实例子)

下面这段 SQL 就是在「画表头 + 定规则」:

sql 复制代码
CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,  -- 主键,自动 1,2,3...
    name TEXT NOT NULL,                    -- 姓名,不能空
    email TEXT NOT NULL UNIQUE             -- 邮箱,不能空,且不能重复
);

CREATE TABLE ledger (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,              -- 存的是 users.id 的号码
    amount REAL NOT NULL,                  -- 金额
    note TEXT,                             -- 备注,允许空
    FOREIGN KEY (user_id) REFERENCES users(id)  -- 声明:user_id 必须在 users 里存在
);

逐项对照:

写法 意思 生活例子
TEXT / INTEGER / REAL 这列存文字 / 整数 / 小数 姓名、学号、金额
NOT NULL 这格不能空着 报名表「姓名」必填
UNIQUE 整列不能有重复值 邮箱不能两人相同
PRIMARY KEY 本表主键 每人唯一学号
FOREIGN KEY ... REFERENCES 外键指向哪张表的哪一列 成绩单上的学号必须在学生花名册里

类型入门记三样即可:

存什么 声明 例子
整数、编号 INTEGER id、user_id、数量
小数 REAL 12.5 元
文字 TEXT 姓名、备注、邮箱

删表(表结构 + 里面所有数据都没):

sql 复制代码
DROP TABLE IF EXISTS ledger;

改表结构用 ALTER TABLE,本课知道有这回事即可。


6. 表设计:三个「好习惯」(仍用例子)

习惯 1:一格只放一种信息

不好:爱好 一格写 "篮球,钢琴,编程",以后很难查「所有喜欢编程的人」。

更好:爱好单独一张表,或先接受「本课不查爱好」------至少别在一格里塞一堆又当多个值用。

习惯 2:少复制会变的信息

不好:每笔流水都抄一遍用户邮箱。

更好:流水只存 user_id,邮箱只在 users 里存一份。

习惯 3:历史可以「拍快照」

订单打印小票时的「商品名称」可以抄一份存在订单里,这样商品以后改名,旧订单标题不变。

这是有意冗余,和「无害地到处复制邮箱」不同。

命名建议:表名、列名用小写英文 + 下划线,如 user_idcreated_at,团队统一一种风格。


7. 画关系图(ER):动笔前先想清楚

ER 草图 = 用方框和连线回答:有哪些「东西」?它们谁连谁?

三种常见关系(只记形状):

关系 人话例子 外键放哪
1 对 1 一人一本护照 外键放任一侧,或两表合并
1 对 多 一用户有多笔流水 外键放在「多」的那张表(流水表的 user_id
多 对 多 学生选多门课,一门课很多学生 再加一张中间表,两头各一个外键

本课:用户 1 ------ 多 流水

text 复制代码
  users                     ledger
+--------+                +----------+
| id (主键) | ←------门牌号------ | user_id(外键) |
| name     |                | amount     |
| email    |                | note       |
+--------+                +----------+
   1 人                        多 笔账

扩展:图书借阅(多对多)

一个读者可借多本书,一本书可被多人借过 → 中间表 loans

text 复制代码
readers          loans                    books
+--------+     +-------------+          +--------+
| id     |←----| reader_id   |          | id     |
| name   |     | book_id     |----→     | title  |
+--------+     | borrowed_at |          +--------+
               +-------------+

中间表每一行的意思很具体:「某读者在某天借了某本书」


8. 实践:亲手建「用户表 + 流水表」

8.0 运行环境说明
项目 要求
Python 3.10+(推荐 3.12);标准库 sqlite3
工作目录 自拟,下文以 ~/python-lab/src/day25 为例
Shell Bash 兼容(支持 mkdircat <<'EOF'

各系统建议:

系统 建议
Linux / macOS 系统终端 + python3
Windows 推荐 Cygwin (或 WSL)。不推荐在纯 CMD / 未配置 heredoc 的 PowerShell 中硬套 cat <<
bash 复制代码
mkdir -p ~/python-lab/src/day25
cd ~/python-lab/src/day25
python3 --version
8.1 一键生成并运行

脚本会:建库文件 shop.db → 建两张表 → 插入样例 → 打印表结构 → 按 user_id 对上姓名 → 故意插入不存在的用户看外键是否拦住。

bash 复制代码
cat > schema_demo.py << 'EOF'
# Day25: users + ledger schema demo
import sqlite3
from pathlib import Path

DB = Path(__file__).with_name("shop.db")


def main():
    if DB.exists():
        DB.unlink()
    conn = sqlite3.connect(DB)
    conn.execute("PRAGMA foreign_keys = ON")

    conn.executescript(
        """
        CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            email TEXT NOT NULL UNIQUE
        );

        CREATE TABLE ledger (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL,
            amount REAL NOT NULL,
            note TEXT,
            FOREIGN KEY (user_id) REFERENCES users(id)
        );
        """
    )

    conn.execute(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        ("alice", "alice@example.com"),
    )
    conn.execute(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        ("bob", "bob@example.com"),
    )
    conn.execute(
        "INSERT INTO ledger (user_id, amount, note) VALUES (?, ?, ?)",
        (1, 12.5, "lunch"),
    )
    conn.execute(
        "INSERT INTO ledger (user_id, amount, note) VALUES (?, ?, ?)",
        (2, 30.0, "taxi"),
    )
    conn.execute(
        "INSERT INTO ledger (user_id, amount, note) VALUES (?, ?, ?)",
        (1, -5.0, "refund"),
    )
    conn.commit()

    print("--- tables ---")
    for row in conn.execute(
        "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
    ):
        print(row[0])

    print("--- users columns ---")
    for row in conn.execute("PRAGMA table_info(users)"):
        print(row)

    print("--- ledger columns ---")
    for row in conn.execute("PRAGMA table_info(ledger)"):
        print(row)

    print("--- sample join ---")
    sql = (
        "SELECT u.name, l.amount, l.note "
        "FROM ledger l JOIN users u ON u.id = l.user_id "
        "ORDER BY l.id"
    )
    for row in conn.execute(sql):
        print(row)

    print("--- bad FK insert ---")
    try:
        conn.execute(
            "INSERT INTO ledger (user_id, amount, note) VALUES (?, ?, ?)",
            (999, 1.0, "ghost"),
        )
        conn.commit()
        print("unexpected success")
    except sqlite3.IntegrityError as e:
        print("IntegrityError:", e)

    conn.close()


if __name__ == "__main__":
    main()
EOF

python3 schema_demo.py
8.2 实测输出
text 复制代码
--- tables ---
ledger
sqlite_sequence
users
--- users columns ---
(0, 'id', 'INTEGER', 0, None, 1)
(1, 'name', 'TEXT', 1, None, 0)
(2, 'email', 'TEXT', 1, None, 0)
--- ledger columns ---
(0, 'id', 'INTEGER', 0, None, 1)
(1, 'user_id', 'INTEGER', 1, None, 0)
(2, 'amount', 'REAL', 1, None, 0)
(3, 'note', 'TEXT', 0, None, 0)
--- sample join ---
('alice', 12.5, 'lunch')
('bob', 30.0, 'taxi')
('alice', -5.0, 'refund')
--- bad FK insert ---
IntegrityError: FOREIGN KEY constraint failed

对照着读输出:

输出 说明
users / ledger 两张表建成功了
sqlite_sequence 自增主键用的内部表,可先忽略
PRAGMA table_info 打印每一列:名字、类型等;最后一项为 1 表示该列是主键
('alice', 12.5, 'lunch') user_id 对上了姓名,说明两表通过「门牌号」连起来了
FOREIGN KEY constraint failed user_id=999 在用户表不存在,被拦住了------这就是外键的作用

SQLite 默认不强制外键,所以脚本里有一句:

python 复制代码
conn.execute("PRAGMA foreign_keys = ON")

每个新连接都要开一次,外键才会真正生效。


总结

  • 像 Excel 工作表, 是表头,是一条记录。
  • 主键 是这一行的唯一编号(身份证);外键是写在另一张表上的门牌号,指向主键。
  • 拆表是为了少复制、好维护;user_id 把「人」和「账」连起来。
  • 建表用 DDL(CREATE TABLE + 类型 + NOT NULL / UNIQUE + 主键外键)。
  • SQLite 使用外键前先 PRAGMA foreign_keys = ON
  • 下一课:增删改查与过滤(系统学 INSERT / UPDATE / DELETE / SELECT)。

小练笔

题 1

看表:

id title
1 Python 入门
2 SQL 基础

这里的 主键 最可能是哪一列?为什么?

题 2

流水表有一行:user_id = 2, amount = 30。用户表如下:

id name
1 alice
2 bob

这笔 30 元是谁花的?user_id 在这里扮演什么角色(主键还是外键)?

题 3

判断:可以把主键设为 NULL,反正很少用到空值。

(对 / 错)

题 4

设计题:学校要记录「哪个学生选了哪门课」。学生可多选,一门课也有很多学生。

请写出建议的表(至少表名 + 主键/外键列),可用文字列表。

题 5(可选实践)

在本课 day25 目录(已有 shop.db)下列出所有表名。写出命令与输出。


小练笔参考答案

题 1

id

它能唯一区分每一本书;title 理论上可能重名,不适合单独当主键。

题 2

bob 花的(users 里 id=2)。

user_id 是流水表上的 外键 ,指向用户表主键 id

题 3

主键必须能认出这一行,不能为空,也不能重复。

题 4(参考)

text 复制代码
students(id 主键, name, ...)
courses(id 主键, title, ...)
enrollments(id 主键,
            student_id 外键→students.id,
            course_id 外键→courses.id)

中间表 enrollments 每一行表示「某学生选了某课」。

题 5(示例)

bash 复制代码
python3 - <<'PY'
import sqlite3
conn = sqlite3.connect('shop.db')
for (name,) in conn.execute(
    "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
):
    print(name)
conn.close()
PY

典型输出:

text 复制代码
ledger
sqlite_sequence
users
相关推荐
ttwuai1 小时前
AI 生成后台改数据后,操作日志别只记按钮:Go + MySQL 怎么验
数据库·人工智能·mysql·golang
KaMeidebaby2 小时前
卡梅德生物技术快报 | 核酸适配体文库测序:核酸适配体文库测序的技术原理、实验流程与数据解析
前端·网络·数据库·人工智能·算法
ClouGence2 小时前
除了 DBeaver,2026 年还有哪些好用的开源数据库管理工具?
数据库·开源
RSTJ_16252 小时前
PYTHON+AI LLM DAY ONE HUNDRED AND EIGHTEEN
python
景芯SoC芯片实战3 小时前
12nm工艺下,6T-Turbo Cell的Filler到底该怎么插?
数据库·数字ic·dft·soc后端
韩楚风3 小时前
【参天引擎】一次宕机后的数据恢复,让我把 Cantian 持久化与恢复的六大机制全搞明白了
服务器·网络·数据库·分布式·mysql·架构·cantian
寒月小酒3 小时前
第五章 生成集成 和第六章 RAG评估(all-in-rag学习)
数据库·学习
AI人工智能集结号3 小时前
AI回答采集:基于PostgreSQL JSONB的原始数据存储与可追溯性方案
数据库·人工智能·postgresql
tkevinjd4 小时前
MiniCode 项目详解7:Memory 记忆系统
大数据·python·搜索引擎·llm·agent