2. 数据库设计与建模

说实话,数据库设计这事儿,我见过太多人一上来就建表。结果呢?项目到一半,发现字段不够用,关联查不动,改起来想死的心都有。嗯,咱们今天就把这步走扎实。

配方管理系统,核心就三件事:配方是什么、用了哪些原料、怎么分类。搞明白这个,表结构就出来了。

先画ER图,再写SQL

我个人习惯,不管项目多小,先拿纸画个ER图。你想想看,表之间的关系在脑子里转,不如画出来清楚。

配方系统的核心实体就三个:

  • 配方表(recipe) — 存配方的基本信息,比如名称、做法、创建时间
  • 原料表(ingredient) — 存每种原料的名称、单位、库存量
  • 分类表(category) — 存分类名称和层级关系

它们之间的关系:

  • 一个配方属于一个分类(多对一)
  • 一个配方包含多种原料(多对多)
  • 一个原料可以被多个配方使用(多对多)

多对多关系需要一张中间表,叫 recipe_ingredient,记录每个配方用了多少克什么原料。

核心原则:先画清楚实体和关系,再动手建表。ER图就是你的施工蓝图。

下面是我画的ER图,你看看这个结构:

配方管理系统 ER 图 分类表 category_id (PK) name parent_id (FK) 配方表 recipe_id (PK) name category_id (FK) instructions 原料表 ingredient_id (PK) name unit 配方-原料关联表 recipe_id (FK) ingredient_id (FK) quantity 1 N 1 N 1 N 说明: 分类与配方是 1:N 关系;配方与原料是 M:N 关系,通过中间表解耦。

MySQL安装与配置

我建议你用 MySQL 8.0+,字符集直接上 utf8mb4。为啥?因为 utf8mb3 存不了 emoji,万一用户配方名字里带个 🍳,你就等着报错吧。

安装步骤我就不啰嗦了,说几个关键配置:

配置项 推荐值 说明
character_set_server utf8mb4 全局字符集
collation_server utf8mb4_unicode_ci 排序规则,支持更多语言
innodb_buffer_pool_size 物理内存的 70% InnoDB 缓存,影响查询速度
max_connections 200 并发连接数,根据业务调整

小技巧:安装完后,记得跑一下 mysql_secure_installation,把匿名用户和测试库删掉。我见过太多生产库被人扫到弱口令,那叫一个惨。

建表实战:字段类型选择

好,图有了,库配好了,咱们开始建表。字段类型选不对,后面全是坑。

分类表(category)

CREATE TABLE category (
    category_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    parent_id INT UNSIGNED DEFAULT NULL,
    sort_order TINYINT UNSIGNED DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_parent (parent_id),
    INDEX idx_sort (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

这里我用了 INT UNSIGNED,因为分类 ID 不会为负数。你想想看,用 INT 浪费一半空间,何必呢?

parent_id 支持无限级分类,比如「川菜 → 火锅 → 麻辣火锅」。我曾经在一个项目中用 VARCHAR 存路径,结果查询时用 LIKE 匹配,慢得想砸电脑。还是 parent_id 递归查靠谱。

配方表(recipe)

CREATE TABLE recipe (
    recipe_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    category_id INT UNSIGNED NOT NULL,
    difficulty ENUM('简单','中等','困难') DEFAULT '简单',
    prep_time SMALLINT UNSIGNED DEFAULT 0 COMMENT '准备时间(分钟)',
    cook_time SMALLINT UNSIGNED DEFAULT 0 COMMENT '烹饪时间(分钟)',
    instructions TEXT,
    image_url VARCHAR(255) DEFAULT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_category (category_id),
    INDEX idx_name (name),
    INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

注意 difficulty 字段,我用了 ENUM。为啥不用 VARCHAR?因为 ENUM 在 MySQL 内部存的是整数,查询更快,而且能限制输入值。不过要小心,ENUM 加新值要改表结构,所以只适合稳定的枚举。

避坑:TEXT 字段不能有默认值,也不能直接加索引。如果要做全文搜索,建议用 VARCHAR 或上 Elasticsearch。我曾经在 TEXT 上建索引,MySQL 直接报错,查了半天文档才发现。

原料表(ingredient)

CREATE TABLE ingredient (
    ingredient_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    unit VARCHAR(20) NOT NULL COMMENT '单位:克、毫升、个等',
    stock DECIMAL(10,2) DEFAULT 0.00,
    min_stock DECIMAL(10,2) DEFAULT 0.00 COMMENT '最低库存预警',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE INDEX idx_name (name),
    INDEX idx_stock (stock)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

stock 用 DECIMAL(10,2),别用 FLOAT。为啥?FLOAT 是近似值,0.1 + 0.2 可能等于 0.30000000000000004。做库存管理,一分一毫都不能差。

配方-原料关联表(recipe_ingredient)

CREATE TABLE recipe_ingredient (
    recipe_id INT UNSIGNED NOT NULL,
    ingredient_id INT UNSIGNED NOT NULL,
    quantity DECIMAL(10,2) NOT NULL COMMENT '用量',
    unit VARCHAR(20) DEFAULT NULL COMMENT '单位,可覆盖原料默认单位',
    PRIMARY KEY (recipe_id, ingredient_id),
    INDEX idx_ingredient (ingredient_id),
    FOREIGN KEY (recipe_id) REFERENCES recipe(recipe_id) ON DELETE CASCADE,
    FOREIGN KEY (ingredient_id) REFERENCES ingredient(ingredient_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

这里用了联合主键 (recipe_id, ingredient_id),保证同一个配方不会重复添加同一种原料。外键的 ON DELETE CASCADE 表示删配方时自动删关联,ON DELETE RESTRICT 表示有配方在用某原料时,不能删该原料。

索引优化:别让查询慢成狗

索引这东西,加多了写慢,加少了查慢。怎么平衡?我一般按这几个原则来:

场景 索引策略 举例
经常作为 WHERE 条件 加单列索引 category_id
经常作为 JOIN 关联 加外键索引 recipe_ingredient.recipe_id
经常作为 ORDER BY 加排序字段索引 created_at
经常作为 GROUP BY 加分组字段索引 category_id
经常作为多条件查询 加联合索引 (category_id, created_at)

核心原则:索引不是越多越好。每个表建议不超过 5 个索引,写频繁的表更要克制。我见过一张表 12 个索引,插入一条数据要半秒,那还玩个啥?

还有一个容易忽略的点:索引列的顺序。联合索引 (a, b, c) 能加速 WHERE a=1、WHERE a=1 AND b=2、WHERE a=1 AND b=2 AND c=3,但 WHERE b=2 用不上。所以,把区分度高的列放前面。

嗯,表建好了,索引也加上了。下一步就是往里灌数据,写查询了。不过那是后面的事,先把这几张表的结构吃透,后面怎么玩都顺手。