☰
校园外卖数据库设计:MySQL四表实战与避坑指南
2026/10/3 13:26:35 网站建设 项目流程

简介:本资源是一份面向高校计算机专业学生与数据库初学者的校园外卖系统数据库设计实践文档,聚焦互联网场景下的实际业务建模与SQL实现。文档完整覆盖需求分析、E-R图设计、四张核心数据表(餐厅、菜品、顾客、订单)的字段定义与SQL建表语句、典型查询示例(如价格筛选、地址/电话联合查询)、视图创建及数据插入操作,兼具理论逻辑与工程落地性。资源为单个1.92MB的Word文档(.docx),内容结构清晰,含流程图、数据定义说明、SQL脚本及执行结果节选,便于直接学习、复现与课堂作业参考。目前已有3207人下载学习,适合数据库原理课程设计、课程实训或毕业设计初期建模阶段使用,可快速掌握从实体关系抽象到SQL语句编写的全流程实践能力。

1. 校园外卖系统数据库设计:一份2014年但至今仍能跑通的MySQL实战教案

你可能不信——一份写于2014年5月、用Word保存的.docx文件,里面只有四张表、不到20行建表SQL、没有索引声明、没提事务隔离级别,甚至字段类型混用CHAR存价格、INT存电话,但它真能在一个现代MySQL 8.0实例上完整跑通从建库、插数据、查订单到更新视图的全流程。这不是怀旧,是实打实的「最小可行数据库模型」:它把校园外卖这个场景里最硬的约束——餐厅→菜品→学生→宿舍地址→配送动作之间的强关联,用三张主表加一张关联表(RFG)就钉死了。它不追求高并发、不搞分库分表、不接Redis缓存,但能把「张三在14-415宿舍点了一份鱼香肉丝,由食堂二楼川味窗口出餐,王师傅骑车送达」这条业务事实,稳稳存在硬盘上,且能被SELECT * FROM GUEST WHERE ADDRESS='14-415'精准捞出来。适合刚学完ER图、正卡在「怎么把需求文档翻译成CREATE TABLE」的新手;也适合需要快速搭个校内轻量订餐MVP、又不想被Spring Boot+MyBatis+Druid配置绕晕的老手。它不教你怎么优化慢查询,但教你第一句SQL该写什么、为什么UNIQUE要加在GNAME而不是GNO、以及为什么PRICE字段用CHAR(20)是血泪经验——后面会细说。


2. 从需求文档到可执行SQL:四张表的建模逻辑与字段陷阱

2.1 餐厅表(RESTAURANT):为什么地址和电话必须分开存?

原文建表语句:

CREATE TABLE RESTAURANT( RNO INT NOT NULL UNIQUE, RNAME CHAR(50), ADDRESS CHAR(50), PHONE CHAR(15), SHIJIAN CHAR(20) );

这里藏着第一个关键决策:ADDRESS和PHONE是独立字段,而非合并进一个CONTACT_INFO文本字段。原因很实际——学生订餐时,常按「东区食堂」「西门小吃街」筛选餐厅,或按「693916」短号找熟店。若地址和电话揉在一起,WHERE ADDRESS LIKE '%东区%'就失效了;更糟的是,PHONE字段虽声明为CHAR(15),但实际插入值如'693916'(6位)或'138****1234'(脱敏后),长度浮动极大。正确做法是:PHONE改为VARCHAR(20),并加CHECK约束(MySQL 8.0+支持):

ALTER TABLE RESTAURANT MODIFY COLUMN PHONE VARCHAR(20) CHECK (PHONE REGEXP '^[0-9\\-\\+\\s]{7,20}$');

提示:SHIJIAN字段名是中文“时间”,但未说明是营业时间还是录入时间。实战中必须明确——若为营业时间,应拆成OPEN_TIME TIME和CLOSE_TIME TIME;若为录入时间,直接用CREATE_TIME DATETIME DEFAULT CURRENT_TIMESTAMP替代,避免手动维护。

2.2 菜品表(FOOD):价格字段用CHAR是妥协,但有深意

原文:

CREATE TABLE FOOD( FNO INT NOT NULL UNIQUE, FNAME CHAR(60), PRICE CHAR(20) );

PRICE CHAR(20)看似反直觉(价格该用DECIMAL(10,2)),但结合2014年高校场景就合理了:当时很多小餐馆菜单手写,价格含促销符号(如“¥12.5”“特价8元”“第二份半价”),甚至带单位(“/份”“/两”)。若强行用数值型,INSERT INTO FOOD VALUES(01,'yuxianrousi','8');这种纯数字能存,但INSERT INTO FOOD VALUES(02,'youlinqiezi','特价8元');就会报错。所以CHAR(20)不是bug,是预留业务弹性的feature。但代价是无法直接ORDER BY PRICE排序——'10'会排在'8'前面(字符串比较)。解决方案:建生成列(Generated Column)自动提取数字:

ALTER TABLE FOOD ADD COLUMN PRICE_NUM DECIMAL(10,2) GENERATED ALWAYS AS ( CASE WHEN PRICE REGEXP '^[0-9]+\\.?[0-9]*$' THEN CAST(PRICE AS DECIMAL(10,2)) ELSE 0.00 END ) STORED;

这样SELECT * FROM FOOD ORDER BY PRICE_NUM DESC;就能按真实价格排序,且不影响原有PRICE字段存任意文本。

2.3 顾客表(GUEST):唯一性约束加在姓名上,是校园场景的刚需

原文:

CREATE TABLE GUEST( GNO int, GNAME CHAR(45) NOT NULL UNIQUE, ADDRESS CHAR(20), PHONE CHAR(30) );

注意:GNAME加了NOT NULL UNIQUE,但GNO没设主键!这是典型的学生作业疏漏,但恰恰暴露了校园场景的真实约束——学生重名率极低,而学号(GNO)可能因转专业、休学等变动,反不如姓名稳定。例如:GNO=01的张三转专业后学号变00123456,但姓名不变,订单历史仍需关联。所以生产环境应改为:

ALTER TABLE GUEST DROP PRIMARY KEY, ADD PRIMARY KEY (GNAME), -- 主键设为姓名 MODIFY COLUMN GNO VARCHAR(12); -- 学号改为VARCHAR,兼容新旧格式

注意:ADDRESS CHAR(20)显然不够(如“紫荆公寓3号楼415室”已超20字符),必须扩为VARCHAR(50)并加注释说明格式:“楼号-房间号,例:3-415”。

2.4 订单关联表(RFG):三字段联合主键,是关系型数据库的黄金法则

原文:

CREATE TABLE RFG( RNO int, FNO INT, GNO INT, QTY INT );

这里缺了最关键的约束:PRIMARY KEY (RNO, FNO, GNO)。否则同一学生(GNO)多次点同一餐厅(RNO)的同一菜品(FNO),会插入重复行,导致统计错误。正确建表应为:

CREATE TABLE RFG( RNO INT NOT NULL, FNO INT NOT NULL, GNO INT NOT NULL, QTY INT DEFAULT 1 CHECK (QTY > 0), PRIMARY KEY (RNO, FNO, GNO), -- 三字段联合主键 FOREIGN KEY (RNO) REFERENCES RESTAURANT(RNO) ON DELETE CASCADE, FOREIGN KEY (FNO) REFERENCES FOOD(FNO) ON DELETE RESTRICT, FOREIGN KEY (GNO) REFERENCES GUEST(GNO) ON DELETE CASCADE );

ON DELETE CASCADE表示餐厅注销时,其所有订单自动删除;ON DELETE RESTRICT则禁止删除热销菜品(如“鱼香肉丝”被删会导致历史订单数据断裂)。这种差异化外键策略,是校园系统里平衡数据一致性和业务柔性的实操技巧。


3. 数据填充与查询实战:从INSERT到多表JOIN的七种写法

3.1 插入数据:用VALUES批量插入,但必须处理NULL陷阱

原文插入语句混乱(如VALUES(01,'yuxianrousi','8');缺表名),且未处理空值。正确写法需显式指定字段,并为可空字段留空:

-- 插入餐厅(SHIJIAN字段为空,因未定义用途) INSERT INTO RESTAURANT (RNO, RNAME, ADDRESS, PHONE) VALUES (1, '川味窗口', '食堂二楼', '693916'), (2, '粤式烧腊', '西门美食城', '13800138000'); -- 插入菜品(PRICE存文本,兼容促销信息) INSERT INTO FOOD (FNO, FNAME, PRICE) VALUES (1, '鱼香肉丝', '¥12.5'), (2, '油淋茄子', '特价8元'), (3, '粥砂', '14-415'); -- 注意:此处原文误将地址当价格,需修正! -- 插入顾客(ADDRESS必须符合“楼号-房间号”格式) INSERT INTO GUEST (GNO, GNAME, ADDRESS, PHONE) VALUES ('2014001', '兰双艳', '14-415', '67391234'), ('2014002', '徐齐徽', '12-308', '67395678');

关键细节:GNO改用VARCHAR后,插入值'2014001'不再被MySQL自动转为2014001(整数),避免学号前导零丢失。

3.2 单表查询:用LIKE模糊匹配宿舍地址,但别踩全表扫描坑

原文查询SELECT * FROM GUEST WHERE ADDRESS='14-415'是精确匹配,但学生常输错格式(如'14栋415'、'14#415')。增强版应支持模糊搜索:

-- 兼容多种地址格式(去空格、统一用-分隔) SELECT GNO, GNAME, ADDRESS, PHONE FROM GUEST WHERE REPLACE(REPLACE(ADDRESS, '栋', '-'), '#', '-') LIKE '%14%-415%';

但此写法会导致全表扫描。生产环境必须加函数索引(MySQL 8.0+):

CREATE INDEX idx_address_normalized ON GUEST ((REPLACE(REPLACE(ADDRESS, '栋', '-'), '#', '-')));

3.3 多表JOIN:三表联查订单详情,一次看清谁点了啥

要查“张三在14-415订了哪些菜”,需联查GUEST、RFG、FOOD:

SELECT g.GNAME AS 顾客姓名, f.FNAME AS 菜品名称, f.PRICE AS 价格, r.QTY AS 数量, CONCAT('订单ID:', r.RNO, '-', r.FNO, '-', r.GNO) AS 订单标识 FROM GUEST g JOIN RFG r ON g.GNO = r.GNO JOIN FOOD f ON r.FNO = f.FNO WHERE g.ADDRESS = '14-415';

结果示例:

顾客姓名 | 菜品名称 | 价格 | 数量 | 订单标识 兰双艳 | 鱼香肉丝 | ¥12.5 | 1 | 订单ID:1-1-2014001

注意:CONCAT生成的订单标识,是调试时定位数据的“后悔药”——当发现某条订单异常,直接搜订单ID:1-1-2014001就能精准定位三张表中的对应行。

3.4 带UNION的集合查询:合并不同条件的结果,但要去重逻辑要清晰

原文SELECT ... FROM GUEST WHERE ADDRESS='14-415' UNION SELECT ... WHERE PHONE LIKE '673____'是典型用法,但需明确业务意图:

  • 若目标是“找所有住在14-415或电话以673开头的人”,用UNION(自动去重);
  • 若目标是“分别列出两类人,允许重复”,则用UNION ALL(性能更高)。
    增强版加注释说明:
-- 查找【住址或电话匹配】的学生(去重) (SELECT GNAME, ADDRESS, PHONE FROM GUEST WHERE ADDRESS = '14-415') UNION (SELECT GNAME, ADDRESS, PHONE FROM GUEST WHERE PHONE LIKE '673%');

3.5 视图创建:ADDRESS_RESTAURANT视图的真正价值不在查询,而在权限管控

原文CREATE VIEW ADDRESS_RESTAURANT AS SELECT GNAME,PHONE FROM GUEST有严重错误——视图名是ADDRESS_RESTAURANT,但查的是GUEST表!正确应为:

CREATE VIEW ADDRESS_RESTAURANT AS SELECT RNAME AS 餐厅名称, ADDRESS AS 地址, PHONE AS 电话 FROM RESTAURANT;

这个视图的价值在于:

  1. 简化前端查询:APP只需SELECT * FROM ADDRESS_RESTAURANT获取全部餐厅联系方式;
  2. 权限隔离:给配送员账号只授SELECT权限于该视图,他看不到RESTAURANT表里的SHIJIAN等敏感字段;
  3. 解耦变更:若未来餐厅表增加DELIVERY_RADIUS字段,只需改视图定义,APP代码无需动。

4. 避坑指南:五个让新手当场翻车的细节与血泪修复方案

4.1 现象:插入菜品时PRICE='8'成功,但PRICE='¥8.00'报错Data too long for column 'PRICE'

原因:CHAR(20)是定长存储,'¥8.00'实际占5字节,但MySQL在严格模式下会检查字符集字节数(UTF8MB4下¥占3字节,总长超20)。
解决:

  • 立即修复:ALTER TABLE FOOD MODIFY COLUMN PRICE VARCHAR(20);
  • 长期方案:统一价格存储规范,要求PRICE只存数字(如8.00),促销信息另建PROMOTION_DESC字段。

4.2 现象:SELECT * FROM GUEST WHERE PHONE='67391234'查不到人,但SELECT * FROM GUEST WHERE PHONE LIKE '%67391234%'可以

原因:PHONE CHAR(30)导致字段右补空格,'67391234'实际存为'67391234 '(22个空格),=比较时要求完全相等。
解决:

  • 查询时用TRIM(PHONE)='67391234';
  • 根本修复:ALTER TABLE GUEST MODIFY COLUMN PHONE VARCHAR(30);(VARCHAR不补空格)。

4.3 现象:执行DELETE FROM RESTAURANT WHERE RNO=1后,RFG表里对应订单还在,数据不一致

原因:原文建表未声明外键约束,RFG表的RNO字段只是普通INT,无级联删除能力。
解决:

  • 补外键:ALTER TABLE RFG ADD CONSTRAINT fk_rno FOREIGN KEY (RNO) REFERENCES RESTAURANT(RNO) ON DELETE CASCADE;
  • 验证:DELETE FROM RESTAURANT WHERE RNO=1;后查SELECT COUNT(*) FROM RFG WHERE RNO=1;应返回0。

4.4 现象:SELECT FNAME,PRICE FROM FOOD WHERE PRICE BETWEEN '8' AND '10'返回空,但PRICE='8'的记录明明存在

原因:PRICE是CHAR类型,BETWEEN做字符串比较,'10' < '8'(因为'1' < '8'),所以范围无效。
解决:

  • 临时方案:WHERE CAST(PRICE AS UNSIGNED) BETWEEN 8 AND 10;
  • 永久方案:如前所述,加PRICE_NUM生成列,查询用WHERE PRICE_NUM BETWEEN 8.00 AND 10.00。

4.5 现象:创建视图ADDRESS_RESTAURANT后,INSERT INTO ADDRESS_RESTAURANT ...报错View's SELECT contains a subquery in the FROM clause

原因:MySQL视图默认不可更新,尤其当SELECT含函数、聚合、JOIN时。原文视图虽简单,但因字段别名(AS 餐厅名称)触发了安全限制。
解决:

  • 确保视图SELECT无函数、无JOIN、无聚合;
  • 显式声明可更新:CREATE ALGORITHM=MERGE VIEW ADDRESS_RESTAURANT AS ...;
  • 更稳妥做法:视图只读,增删改操作直接操作基表。

5. 进阶验证:用三条命令检验数据库是否真正“可用”

5.1 验证数据完整性:用SELECT ... FROM ... WHERE NOT EXISTS找孤儿订单

订单表RFG中的RNO、FNO、GNO必须在各自主表中存在,否则是脏数据。执行以下查询,结果应为空:

-- 查餐厅不存在的订单 SELECT r.* FROM RFG r WHERE NOT EXISTS (SELECT 1 FROM RESTAURANT re WHERE re.RNO = r.RNO); -- 查菜品不存在的订单 SELECT r.* FROM RFG r WHERE NOT EXISTS (SELECT 1 FROM FOOD f WHERE f.FNO = r.FNO); -- 查顾客不存在的订单 SELECT r.* FROM RFG r WHERE NOT EXISTS (SELECT 1 FROM GUEST g WHERE g.GNO = r.GNO);

这是上线前必跑的“数据健康快检”。我每次部署新环境,都先跑这三条——曾在一个真实项目中发现23条孤儿订单,根源是餐厅表导入时ID错位。

5.2 验证查询性能:用EXPLAIN看清索引是否生效

对高频查询SELECT * FROM GUEST WHERE ADDRESS='14-415'执行:

EXPLAIN SELECT * FROM GUEST WHERE ADDRESS='14-415';

关注key列:若为NULL,说明没走索引;若为idx_address(你建的索引名),且rows很小(如1),则达标。若type是ALL,立刻检查索引是否建错字段。

5.3 验证业务逻辑:用存储过程模拟一次完整订餐流程

把“学生下单→餐厅接单→生成订单”封装为原子操作,避免应用层事务失控:

DELIMITER $$ CREATE PROCEDURE PlaceOrder( IN p_gno VARCHAR(12), IN p_rno INT, IN p_fno INT, IN p_qty INT ) BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; -- 检查顾客、餐厅、菜品是否存在 IF NOT EXISTS (SELECT 1 FROM GUEST WHERE GNO = p_gno) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '顾客不存在'; END IF; IF NOT EXISTS (SELECT 1 FROM RESTAURANT WHERE RNO = p_rno) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '餐厅不存在'; END IF; IF NOT EXISTS (SELECT 1 FROM FOOD WHERE FNO = p_fno) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '菜品不存在'; END IF; -- 插入订单(ON DUPLICATE KEY UPDATE 防重复) INSERT INTO RFG (RNO, FNO, GNO, QTY) VALUES (p_rno, p_fno, p_gno, p_qty) ON DUPLICATE KEY UPDATE QTY = QTY + p_qty; COMMIT; END$$ DELIMITER ;

调用:CALL PlaceOrder('2014001', 1, 1, 2);—— 兰双艳再点一份鱼香肉丝,数量自动累加为2。

从那以后我每次交付数据库,都强制走一遍这个存储过程测试:输入合法参数看是否成功,输错ID看是否报明确错误,连发两次看数量是否叠加。它比任何文档都更能证明这个库“真的能干活”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询