零基础入门Python15|关联、聚合、索引与事务:订单数据库

零基础入门Python15|关联、聚合、索引与事务:订单数据库

本篇图解:数据从哪里来、到哪里去

图中最重要的边界是事务:只有成功路径才提交数据,失败路径不能留下半条业务记录。

一、上一篇课后练习讲解

多对多关系需要中间表:

sql 复制代码
CREATE TABLE authors (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE
);

CREATE TABLE book_authors (
    book_id INTEGER NOT NULL,
    author_id INTEGER NOT NULL,
    PRIMARY KEY (book_id, author_id),
    FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
    FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE
);

参数化关联函数:

python 复制代码
def add_author_to_book(book_id, author_id):
    with connect() as connection:
        connection.execute(
            "INSERT INTO book_authors (book_id, author_id) VALUES (?, ?)",
            (book_id, author_id),
        )

联合主键禁止重复关联,外键保证两个编号真实存在。

可执行验收答案

把下面 SQL 保存为 check_book_tags.sql,SQLite 中执行:

sql 复制代码
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS books(id INTEGER PRIMARY KEY, title TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS authors(id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE);
CREATE TABLE IF NOT EXISTS book_authors(
    book_id INTEGER NOT NULL REFERENCES books(id) ON DELETE CASCADE,
    author_id INTEGER NOT NULL REFERENCES authors(id) ON DELETE CASCADE,
    PRIMARY KEY(book_id, author_id)
);
INSERT INTO books VALUES (1, 'Python入门');
INSERT INTO authors VALUES (1, '小李');
INSERT INTO book_authors VALUES (1, 1);
-- 下面一行重复执行时应失败:UNIQUE constraint failed
-- INSERT INTO book_authors VALUES (1, 1);
SELECT b.title, a.name FROM books b
JOIN book_authors ba ON ba.book_id=b.id
JOIN authors a ON a.id=ba.author_id;

预期查询返回一行 Python入门 | 小李。若重复关联没有失败,检查是否漏写复合主键;若删除 book 后关联仍在,确认开启 PRAGMA foreign_keys=ON,SQLite 默认可能关闭外键检查。

二、本篇成果

建立用户、商品、订单和订单项四张表,使用JOIN读取关联数据,GROUP BY统计消费,索引优化查询,并用事务保证"创建订单、写明细、扣库存"全部成功或全部失败。

三、订单项为什么是独立实体

sql 复制代码
CREATE TABLE order_items (
    id INTEGER PRIMARY KEY,
    order_id INTEGER NOT NULL,
    product_id INTEGER NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    price NUMERIC NOT NULL CHECK (price > 0),
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

成交价格必须复制到订单项,不能以后总读取商品当前价格,否则商品涨价会改变历史订单。

四、JOIN和聚合

sql 复制代码
SELECT o.id AS order_id, u.name, p.name AS product_name,
       oi.quantity, oi.price, oi.quantity * oi.price AS line_total
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.id = ?;

SELECT u.id, u.name, COUNT(o.id) AS order_count,
       COALESCE(SUM(o.total_amount), 0) AS spent
FROM users u LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name
ORDER BY spent DESC;

INNER JOIN只保留匹配行;LEFT JOIN保留左表,即使没有订单。COUNT计数,SUM求和,COALESCE把NULL转为0。

五、索引

sql 复制代码
CREATE INDEX idx_orders_user_created
ON orders (user_id, created_at DESC);

索引加快读取,但增加写入成本和存储空间。先观察真实查询,再创建索引。

六、完整下单事务

python 复制代码
from datetime import datetime, timezone

def create_order(connection, user_id, items):
    try:
        connection.execute("BEGIN")
        cursor = connection.execute(
            "INSERT INTO orders (user_id,total_amount,created_at) VALUES (?,0,?)",
            (user_id, datetime.now(timezone.utc).isoformat()),
        )
        order_id = cursor.lastrowid
        total = 0

        for product_id, quantity in items:
            product = connection.execute(
                "SELECT name,price,stock FROM products WHERE id = ?",
                (product_id,),
            ).fetchone()

            if product is None:
                raise ValueError("商品不存在")
            if product["stock"] < quantity:
                raise ValueError(f"{product['name']}库存不足")

            connection.execute(
                "INSERT INTO order_items(order_id,product_id,quantity,price) VALUES (?,?,?,?)",
                (order_id, product_id, quantity, product["price"]),
            )
            connection.execute(
                "UPDATE products SET stock=stock-? WHERE id=?",
                (quantity, product_id),
            )
            total += product["price"] * quantity

        connection.execute(
            "UPDATE orders SET total_amount=? WHERE id=?",
            (total, order_id),
        )
        connection.commit()
        return order_id
    except Exception:
        connection.rollback()
        raise

任何一步失败都rollback。捕获后重新raise,让调用者知道失败原因;不能回滚后假装成功。

七、验证回滚

准备一个库存充足和一个库存不足的商品,同时购买。预期orders、order_items没有新增,充足商品库存也没有减少。只检查异常不够,还要查询三张表证明回滚完整。

八、本篇验收

  • 能判断一对多和多对多;
  • 订单项保存成交价格快照;
  • 会写JOIN、GROUP BY和聚合;
  • 理解索引读写权衡;
  • 下单失败后所有相关写操作回滚。

九、课后练习

实现cancel_order(connection, order_id):只能取消未取消订单;把订单项数量加回库存,再修改状态;任一步失败全部回滚。下一篇会把SQLite环境迁移到MySQL。

实战补充:用 SQL 解释更新结果

UPDATE 和 DELETE 都要检查影响行数;0 行可能表示资源不存在,不能盲目返回成功。

python 复制代码
cursor = db.execute('UPDATE tasks SET done = ? WHERE id = ?', (1, task_id))
if cursor.rowcount == 0:
    raise NotFound('任务不存在')
db.commit()

课后练习:把完成任务和审计放进同一事务,并写成功、不存在和审计失败三组测试;下一篇比较数据库类型和连接配置。

本篇结束:完整模块文件

本节不是代码片段,而是本篇结束时该模块的完整版本。请先备份旧文件,再整体替换;替换后重新运行本篇命令和测试。阅读时重点看本篇新增的函数、事务边界和错误处理,未涉及的代码先不要自行删减。

本篇完整示例

sql 复制代码
SELECT u.name, COUNT(o.id) AS order_count
FROM users u LEFT JOIN orders o ON u.id=o.user_id
GROUP BY u.id, u.name
ORDER BY order_count DESC;
相关推荐
江屿风1 小时前
【STM32基础篇】【嵌入式生态问题及历史追溯】流食般投喂
大数据·开发语言·人工智能·笔记·stm32·嵌入式硬件
charlie1145141912 小时前
深探std::vector:三指针、扩容与迭代器失效
开发语言·c++·开源项目
asyxchenchong8882 小时前
R语言混合效应(多水平/层次/嵌套)模型及贝叶斯实现技术应用
开发语言·r语言
Javatutouhouduan2 小时前
Java初学者如何高效学习JVM?
java·jvm·java虚拟机·java面试·后端开发·java程序员·java八股文
熊猫钓鱼>_>2 小时前
从“串数据“焦虑到 Space 自由,我用 Agent Bucket 智能体桶管游戏素材
开发语言·人工智能·游戏·agent·bucket·workbuddy·space
qq_513728042 小时前
Alert 原生弹窗处理
python
李可以量化2 小时前
Redis 从了解到精通(三)上:量化交易场景下的数据备份与安全配置
redis·python·安全·qmt·ptrade
卷无止境3 小时前
FastAPI查询参数模型:把散落的参数收拢成一个整齐的盒子
后端·python