
简介本资源是一份面向高校计算机专业学生与数据库初学者的校园外卖系统数据库设计实践文档聚焦互联网场景下的实际业务建模与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 ADDRESS14-415精准捞出来。适合刚学完ER图、正卡在「怎么把需求文档翻译成CREATE TABLE」的新手也适合需要快速搭个校内轻量订餐MVP、又不想被Spring BootMyBatisDruid配置绕晕的老手。它不教你怎么优化慢查询但教你第一句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)但实际插入值如6939166位或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可能因转专业、休学等变动反不如姓名稳定。例如GNO01的张三转专业后学号变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 ADDRESS14-415是精确匹配但学生常输错格式如14栋415、14#415。增强版应支持模糊搜索-- 兼容多种地址格式去空格、统一用-分隔 SELECT GNO, GNAME, ADDRESS, PHONE FROM GUEST WHERE REPLACE(REPLACE(ADDRESS, 栋, -), #, -) LIKE %14%-415%;但此写法会导致全表扫描。生产环境必须加函数索引MySQL 8.0CREATE INDEX idx_address_normalized ON GUEST ((REPLACE(REPLACE(ADDRESS, 栋, -), #, -)));3.3 多表JOIN三表联查订单详情一次看清谁点了啥要查“张三在14-415订了哪些菜”需联查GUEST、RFG、FOODSELECT 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 ADDRESS14-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;这个视图的价值在于简化前端查询APP只需SELECT * FROM ADDRESS_RESTAURANT获取全部餐厅联系方式权限隔离给配送员账号只授SELECT权限于该视图他看不到RESTAURANT表里的SHIJIAN等敏感字段解耦变更若未来餐厅表增加DELIVERY_RADIUS字段只需改视图定义APP代码无需动。4. 避坑指南五个让新手当场翻车的细节与血泪修复方案4.1 现象插入菜品时PRICE8成功但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 PHONE67391234查不到人但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 RNO1后RFG表里对应订单还在数据不一致原因原文建表未声明外键约束RFG表的RNO字段只是普通INT无级联删除能力。解决补外键ALTER TABLE RFG ADD CONSTRAINT fk_rno FOREIGN KEY (RNO) REFERENCES RESTAURANT(RNO) ON DELETE CASCADE;验证DELETE FROM RESTAURANT WHERE RNO1;后查SELECT COUNT(*) FROM RFG WHERE RNO1;应返回0。4.4 现象SELECT FNAME,PRICE FROM FOOD WHERE PRICE BETWEEN 8 AND 10返回空但PRICE8的记录明明存在原因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 ...报错Views SELECT contains a subquery in the FROM clause原因MySQL视图默认不可更新尤其当SELECT含函数、聚合、JOIN时。原文视图虽简单但因字段别名AS 餐厅名称触发了安全限制。解决确保视图SELECT无函数、无JOIN、无聚合显式声明可更新CREATE ALGORITHMMERGE 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 ADDRESS14-415执行EXPLAIN SELECT * FROM GUEST WHERE ADDRESS14-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看是否报明确错误连发两次看数量是否叠加。它比任何文档都更能证明这个库“真的能干活”。希望帮到你。本文还有配套的精品资源点击获取