下载工作台
Python 数据库实战

DDL、约束与索引设计

试读上半部分 · 解锁后可读全文

第 5 章 · 建表、约束与索引设计

本章目标:掌握 CREATE TABLE 完整语法;PRIMARY KEY / FOREIGN KEY / UNIQUE / CHECK / NOT NULL / DEFAULT;理解 InnoDB B+Tree 索引单列与复合索引最左前缀原则;用 EXPLAIN 初判执行计划;为 shop_db 编写生产级 DDL(schema/shop_db.sql);对照 django-web Migration 与 fastapi-web SQLAlchemy Index()

学时建议:5~6 小时(含 DDL 重写与 EXPLAIN 实验)

前置python-database ch01~ch04;理解 JOIN 与事务。


5.1 场景说明:从练习表到规范 shop-db Schema

ch02 的样本表无外键,便于入门。本章在 ~/python-learn/db-demo/schema/shop_db.sql 定义可对接 django-web / fastapi-web 的正式结构,字段对齐全站约定:

关键约束
usersemail UNIQUE,password_hash NOT NULL
productsslug UNIQUE,price INT 分,is_published
ordersFK → users,statustotal_amount
order_linesFK → orders + products,unit_price 快照

虚构 API GET /api/v1/products/:slug 依赖 products(slug) UNIQUE 索引 实现 O(log n) 查找。


5.2 CREATE TABLE 基础语法

CREATE TABLE table_name (
  col_name data_type [NOT NULL | NULL] [DEFAULT expr]
    [COMMENT '注释'],
  ...
  [表级约束],
  [表选项]
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

常用数据类型(shop_db)

类型用途
BIGINT主键 id(自增)
VARCHAR(n)slug、email、title
INTprice、total_amount(分)
TINYINT(1)is_published 布尔
DATETIME(6)created_at 微秒
DECIMAL(12,2)选修:报表金额元(本课统一用分 INT)

5.3 shop_db 完整 DDL

schema/shop_db.sql(执行前会 DROP,仅 dev):

USE shop_db;

SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS order_lines;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS users;
SET FOREIGN_KEY_CHECKS = 1;

CREATE TABLE users (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  email VARCHAR(255) NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
    ON UPDATE CURRENT_TIMESTAMP(6),
  PRIMARY KEY (id),
  UNIQUE KEY uk_users_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  COMMENT='商城用户';

CREATE TABLE products (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  slug VARCHAR(64) NOT NULL,
  title VARCHAR(200) NOT NULL,
  category VARCHAR(50) NOT NULL,
  price INT UNSIGNED NOT NULL COMMENT '分',
  is_published TINYINT(1) NOT NULL DEFAULT 0,
  created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
    ON UPDATE CURRENT_TIMESTAMP(6),
  PRIMARY KEY (id),
  UNIQUE KEY uk_products_slug (slug),
  KEY idx_products_published_category (is_published, category),
  CONSTRAINT chk_products_price CHECK (price > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  COMMENT='商品';

CREATE TABLE orders (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id BIGINT UNSIGNED NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'pending',
  total_amount INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '分',
  created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
    ON UPDATE CURRENT_TIMESTAMP(6),
  PRIMARY KEY (id),
  KEY idx_orders_user_created (user_id, created_at),
  KEY idx_orders_status (status),
  CONSTRAINT fk_orders_user
    FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT chk_orders_status CHECK (status IN ('pending','paid','cancelled','refunded'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  COMMENT='订单';

CREATE TABLE order_lines (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  order_id BIGINT UNSIGNED NOT NULL,
  product_id BIGINT UNSIGNED NOT NULL,
  quantity INT UNSIGNED NOT NULL DEFAULT 1,
  unit_price INT UNSIGNED NOT NULL COMMENT '下单快照,分',
  PRIMARY KEY (id),
  KEY idx_order_lines_order (order_id),
  KEY idx_order_lines_product (product_id),
  CONSTRAINT fk_ol_order
    FOREIGN KEY (order_id) REFERENCES orders(id)
    ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT fk_ol_product
    FOREIGN KEY (product_id) REFERENCES products(id)
    ON DELETE RESTRICT ON UPDATE CASCADE,
  CONSTRAINT chk_ol_qty CHECK (quantity > 0),
  CONSTRAINT chk_ol_price CHECK (unit_price > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  COMMENT='订单明细';

执行:

docker exec -i shop-db-mysql mysql -ushop_user -pdev_shop_pass_change_me shop_db \
  < schema/shop_db.sql

5.4 PRIMARY KEY 与 AUTO_INCREMENT

概念说明
PRIMARY KEY唯一 + 非 NULL;每表一个
AUTO_INCREMENTInnoDB 聚簇索引,顺序插入友好
BIGINT UNSIGNED避免 id 用尽;与 ORM BigAutoField 对齐

为何不用 slug 作主键? slug 可变、较长;代理键 id + UNIQUE(slug) 是电商惯例。

django-webmodels.BigAutoField(primary_key=True)gin-web:GORM uint / bigint


5.5 FOREIGN KEY 与引用动作

FOREIGN KEY (user_id) REFERENCES users(id)
  ON DELETE RESTRICT   -- 有订单的用户不可删
  ON UPDATE CASCADE;  -- users.id 变更时同步(极少发生)

FOREIGN KEY (order_id) REFERENCES orders(id)
  ON DELETE CASCADE;   -- 删订单级联删明细
动作含义
RESTRICT / NO ACTION有引用则拒绝删除父行
CASCADE父删/改,子同步
SET NULL子外键置 NULL(列须可 NULL)

应用层 vs 数据库层:ORM 可省略 FK,但 shop-db 教学要求 DB 层 enforce,防止脏数据。


5.6 UNIQUE、CHECK、NOT NULL

-- UNIQUE:slug、email
UNIQUE KEY uk_products_slug (slug)

-- CHECK(MySQL 8.0.16+ 强制)
CONSTRAINT chk_products_price CHECK (price > 0)

-- 插入违反 CHECK
INSERT INTO products (slug, title, category, price, is_published)
VALUES ('bad-price', 'x', 'books', 0, 1);  -- ERROR 3819
约束django MigrationSQLAlchemy
UNIQUEunique=Trueunique=True
CHECKCheckConstraintCheckConstraint
NOT NULL默认非空nullable=False

5.7 索引原理:B+Tree 简述

InnoDB 聚簇索引 = 主键 B+Tree,叶子存整行;二级索引叶子存 主键值,回表查聚簇索引。

                    [10 | 20 | 30]          ← 非叶子:键值 + 指针
                   /    |     \
          [1..9]      [10..19]    [20..29]  ← 叶子:有序,链表相连
          行数据       行数据        行数据
术语说明
回表二级索引查完再查 PK
覆盖索引SELECT 列全在索引中,无需回表
最左前缀复合索引 (a,b,c) 可用 a;a,b;a,b,c

api.example.com 商品详情WHERE slug = ? AND is_published = 1

  • uk_products_slug (slug)唯一索引定位一行;
  • 若常带 is_published,可建 (slug, is_published)(is_published, category) 列表索引。

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。