2. 数据库设计与建模
说实话,数据库设计这事儿,我见过太多人一上来就建表。结果呢?项目到一半,发现字段不够用,关联查不动,改起来想死的心都有。嗯,咱们今天就把这步走扎实。
配方管理系统,核心就三件事:配方是什么、用了哪些原料、怎么分类。搞明白这个,表结构就出来了。
先画ER图,再写SQL
我个人习惯,不管项目多小,先拿纸画个ER图。你想想看,表之间的关系在脑子里转,不如画出来清楚。
配方系统的核心实体就三个:
- 配方表(recipe) — 存配方的基本信息,比如名称、做法、创建时间
- 原料表(ingredient) — 存每种原料的名称、单位、库存量
- 分类表(category) — 存分类名称和层级关系
它们之间的关系:
- 一个配方属于一个分类(多对一)
- 一个配方包含多种原料(多对多)
- 一个原料可以被多个配方使用(多对多)
多对多关系需要一张中间表,叫 recipe_ingredient,记录每个配方用了多少克什么原料。
核心原则:先画清楚实体和关系,再动手建表。ER图就是你的施工蓝图。
下面是我画的ER图,你看看这个结构:
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 用不上。所以,把区分度高的列放前面。
嗯,表建好了,索引也加上了。下一步就是往里灌数据,写查询了。不过那是后面的事,先把这几张表的结构吃透,后面怎么玩都顺手。