MySQL菜谱数据库实战:导入、清洗、关联查询与避坑全解
发布时间:2026/10/9 12:18:23 锦皓数字建站

简介一份包含十三万条菜谱记录的 MySQL 数据集面向美食类网站或应用开发者、数据分析学习者以及需要冷启动数据的项目团队可省去自行爬取、清洗菜谱数据的繁琐环节。压缩包共 4 个文件含 3 个数据库脚本与 1 个说明文档整体约 52.48MB数据按目录表、菜谱表、目录与菜谱关联表三张表组织导入 MySQL 后可直接支持按菜系、食材、做法等分类浏览、菜品检索、关联推荐等场景也便于二次开发。另外资源说明中附带了约 36G 配套图片素材的网盘获取地址适用于搭建图文菜谱展示或内容型应用图片库不在当前压缩包中。目前已有 941 人学习浏览适合课程设计、毕业设计、小型项目原型实践以及 SQL 查询训练对需要真实数据体量来验证功能的读者尤其有价值。1. 菜谱数据到底能不能直接用先搞清楚包里有什么再动手拿到这份号称“13万条菜谱、36G图片”的MySQL菜谱资源包第一反应别急着导入数据库。我拆过不少这类“大而全”的数据包最坑的往往不是数据量而是表结构设计得绕、字段命名不统一、图片链接跟菜谱对不上。这份资源的实际组成是三个SQL文件加一个说明文档catalog.sql是分类目录表dishes.sql是菜谱主表link.sql负责建立目录和菜谱的多对多关联另外附带一个百度网盘的图片包地址需要自己下载解压。你拿到手后要关心的只有三件事第一三个表怎么导入MySQL并建立正确关联第二13万条数据里有多少是能直接用的规范数据有多少是缺图少描述的残次品第三图片URL和本地图片文件怎么对应起来不然菜谱列表页能查出来、图片却全裂了。这篇文章按我的实际拆包顺序来写从建表、导数据、清洗、关联查询一直讲到图片资源对位和项目接入新手可以照着一步步走熟手可以直接跳到中间几章看参数和踩坑点。2. 先把三张表的关系捋清楚再导数据ER关系、SQL文件导入与字符集2.1 表结构设计逻辑为什么用三张表而不是一张大宽表先看说明.txt里对三个表的描述catalog是目录表、dishes是菜谱表、link是目录关联菜谱表。这种设计对应的是经典的“分类-条目-关联”模式菜谱和目录是多对多关系——一道菜可以同时属于“家常菜”和“快手菜”两个目录一个目录下又有大量菜谱。如果只建一张表每次改分类都要更新冗余字段而且查某个目录下所有菜谱时SQL写起来很别扭。我一般先用MySQL客户端把三个SQL文件里的建表语句拎出来看字段结构。catalog表通常有catalog_id和catalog_name两个核心字段dishes表会有dishes_id、dishes_name、原料字段、做法字段、图片URL字段等link表则只有两个外键字段分别指向两个主表的ID。关键点是确认主键类型和字符集设定如果建表语句里用的是utf8mb4导中文不会出现乱码如果建表语句里忘了指定后面数据写入后用SHOW CREATE TABLE检查一下。2.2 导入步骤从SQL文件到可查询的数据库实际操作时我习惯用命令行导入而不是图形化工具因为13万条数据的SQL文件动不动就几十MB用Navicat导入虽然可视化强但遇到超时或内存不足时不会给你有用的错误提示。在MySQL 8.0环境下先建库再指定字符集导入mysql -uroot -p --default-character-setutf8mb4 -e CREATE DATABASE IF NOT EXISTS cookbook DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p --default-character-setutf8mb4 cookbook catalog.sql mysql -uroot -p --default-character-setutf8mb4 cookbook dishes.sql mysql -uroot -p --default-character-setutf8mb4 cookbook link.sql这里的顺序不能乱因为dishes和link表如果有外键约束必须先导入被引用的主表。--default-character-setutf8mb4是防止中文乱码的关键参数MySQL 8.0默认字符集已经是utf8mb4但老版本导出的SQL文件里可能带着latin1或utf8mb3的SET NAMES语句不加这个参数的话字段内容会以错误字符集解释。导入完成后立刻做三件事看每张表的行数是否和13万这个量级吻合检查dishes表里有没有重复的主键看link表的关联行数是不是和dishes表行数接近。如果link表行数远大于dishes表行数说明一对多关系是正常的如果远小于说明大量菜谱没有挂到任何分类下后面查询时会被漏掉。mysql -uroot -p cookbook -e SELECT COUNT(*) AS catalog_cnt FROM catalog; SELECT COUNT(*) AS dishes_cnt FROM dishes; SELECT COUNT(*) AS link_cnt FROM link;参数说明catalog_cnt对应分类总数一般几百到几千不等dishes_cnt是核心数据理想情况接近13万link_cnt是关联记录数通常大于dishes_cnt是因为一道菜挂多个分类。注意导入过程如果报错ERROR 1366 (HY000): Incorrect string value说明SQL文件里混入了非法字符集字节需要检查原始文件的编码格式用Notepad或VSCode打开看右下角状态栏如果是ANSI编码需要另存为UTF-8后再导入。3. 数据质量深度排查13万条记录里藏着多少坑3.1 空字段与缺失图片URL的清洗策略把数据导进来只是第一步真正耗时间的是清洗。我用一个简单的SQL统计就能发现很多问题dishes表里dishes_name为空的记录有多少image_url字段为空的记录有多少。空名称的菜谱直接DELETE掉因为这种记录即使查出来也无法展示image_url为空的需要看是否有拼接规则能补救——有些数据源的图片URL是规律拼接的比如http://domain.com/images/菜品ID.jpg如果只有少量缺失可以按规则补全但如果大量缺失就得考虑这些菜谱先不配图或者改用本地图片路径匹配。另外要注意原料字段和做法字段的长度。有的数据源把原料和做法压在一个TEXT字段里用分隔符隔开有的拆成多个字段。这份资源我没法确定具体字段名但常见做法是dishes表里带material和practice字段material存储“猪肉, 青椒, 蒜末”这类逗号分隔的文本。清洗时可以用LENGTH()和CHAR_LENGTH()区分字符数和字节数中文字符在utf8mb4下占3字节如果发现字段长度异常考虑是否有HTML标签混入。SELECT COUNT(*) AS empty_name FROM dishes WHERE dishes_name IS NULL OR TRIM(dishes_name) ; SELECT COUNT(*) AS empty_img FROM dishes WHERE image_url IS NULL OR TRIM(image_url) ; SELECT COUNT(*) AS dup_records FROM dishes GROUP BY dishes_name HAVING COUNT(*) 1;逻辑说明第一条查无名称记录第二条查无图片记录第三条查菜名重复记录。重复记录的处理要分情况完全重复的直接删一条只保留一条部分重复的——比如同一道菜在不同分类下各有一条但内容不同——需要人工判断或者保留link表关联更全的那条。3.2 分类表的层级问题扁平目录还是树形结构catalog表如果只有两级查起来很方便但如果实际数据是树形结构——比如“川菜”下面还有“水煮系列”“干锅系列”——光靠catalog表自身可能不够用。检查方法是看catalog表里有没有parent_id这样的自关联字段。如果有说明数据源已经做了树形设计查询时要递归加载子分类如果没有所有分类都是平铺的靠link表把菜谱关联到不同父级分类上。实际查询中用户搜“川菜”时希望看到所有川菜子分类下的菜谱而不是只看到直接挂在“川菜”下的菜。如果link表只关联了最细粒度的分类查询时需要先查出该分类的所有子分类ID再IN查询。这个逻辑在SQL里可以用一条递归CTE搞定MySQL 8.0支持WITH RECURSIVEWITH RECURSIVE sub_cats AS ( SELECT catalog_id FROM catalog WHERE catalog_name 川菜 UNION ALL SELECT c.catalog_id FROM catalog c INNER JOIN sub_cats sc ON c.parent_id sc.catalog_id ) SELECT d.dishes_id, d.dishes_name FROM dishes d INNER JOIN link l ON d.dishes_id l.dishes_id WHERE l.catalog_id IN (SELECT catalog_id FROM sub_cats);参数说明catalog_name换成实际要查的目录名sub_cats是递归公共表达式先取根节点再逐层向下找子节点。如果catalog表没有parent_id字段这个CTE会报错说明数据是扁平结构直接用WHERE l.catalog_id 目标ID即可。我见过不少数据包号称有分类层级实际只是把“川菜”和“水煮肉片”平铺在同一个表里这种情况你查川菜下的所有菜谱永远只得到直接关联的那几条。4. 三个高频查询场景把菜谱数据用起来的SQL实战4.1 按分类查菜谱并关联图片URL最常见的使用场景是做一个菜谱列表页左侧显示分类点击某个分类后右侧展示该分类下的菜谱卡片卡片上要显示菜名和配图。这需要三张表联合查询但要注意联表顺序和索引使用。dishes_id和catalog_id在各自表中是主键但link表里的两个外键字段如果没有建索引联表查询会全表扫描13万数据量下性能会很差。SELECT d.dishes_id, d.dishes_name, d.image_url FROM link l INNER JOIN dishes d ON l.dishes_id d.dishes_id WHERE l.catalog_id 12 ORDER BY d.dishes_id DESC LIMIT 20;逻辑说明从link表出发先筛选出catalog_id12的所有关联记录再INNER JOIN到dishes表取菜谱详情。ORDER BY dishes_id DESC是假设新入库的菜谱ID更大这样列表默认展示最新内容。LIMIT 20是分页的第一页。如果这里卡顿大概率是link表缺索引需要执行ALTER TABLE link ADD INDEX idx_catalog (catalog_id);。4.2 按原料反向查菜谱实现“冰箱里有什么能做什么”这功能在美食类App里很受欢迎用户输入“鸡蛋 西红柿”然后返回所有同时包含这两个原料的菜谱。受限于数据源的字段设计如果原料是逗号分隔的一个字段SQL里要用LIKE配合布尔模式全文检索。13万行数据对MySQL的全文索引来说压力不大但如果你不想改表结构直接用LIKE %鸡蛋% AND LIKE %西红柿%也能跑只是每次都是全表扫描好在数据量级不算大。SELECT dishes_id, dishes_name, material FROM dishes WHERE material LIKE %鸡蛋% AND material LIKE %西红柿% AND dishes_name NOT LIKE %蛋糕% LIMIT 30;逻辑说明两个LIKE条件用AND连接表示“同时包含”如果只想匹配其中任意一个就用OR。NOT LIKE %蛋糕%是排除干扰项因为“西红柿鸡蛋”和“西红柿鸡蛋蛋糕”是两种完全不同的东西但很多数据源会把它们归到同一个原料字段里。参数说明material字段如果为NULLLIKE条件不会匹配需要在WHERE里加material IS NOT NULL确保结果集不含空值。这种查询方式的缺点是精确度一般比如搜“土豆”会匹配出“土豆泥”和“炸土豆片”业务上通常可以接受。4.3 随机推荐菜谱解决“今天吃什么”的决策场景做美食类App基本都会遇到“今天吃什么”的需求随机推荐如果直接用ORDER BY RAND()13万行数据会先创建临时表再排序每次请求都消耗不少资源。更高效的做法是先取一个随机ID范围再查。dishes_id如果是从1开始自增的连续序列可以直接生成一个随机数作为查询起点如果ID有断档先查出最大ID再在应用层生成随机数。SELECT dishes_id, dishes_name, image_url FROM dishes WHERE dishes_id FLOOR(RAND() * (SELECT MAX(dishes_id) FROM dishes)) ORDER BY dishes_id LIMIT 1;逻辑说明RAND()返回0到1之间的随机小数乘以最大ID后取整得到一个大致的随机位置然后取该位置之后的第一条记录。这比ORDER BY RAND()快得多缺点是ID稀疏时会产生偏向性——ID越大越容易被取到因为WHERE条件是“大于等于随机数”。如果想让推荐更均匀可以先用SELECT MAX(dishes_id)拿到上限在应用层用编程语言生成随机ID再精确查一次。如果数据量小到只有几千条直接ORDER BY RAND()也没问题不用过度优化。5. 避坑手册菜谱数据导入与使用的五个血泪教训5.1 SQL文件导入报错“Unknown column”现象执行catalog.sql时报错ERROR 1054 (42S22): Unknown column xxx in field list。原因数据源的建表语句和插入语句字段不一致通常是导出时用了旧版本MySQL插入语句里带的列名在表结构里不存在。这份资源如果是老版本导出的部分SQL语句可能被截断或修改过。解决先看报错的位置在哪个SQL语句用sed -n 报错行前后20行 catalog.sql把SQL文件分段提取出来检查INSERT语句里是否多了一个逗号或少了一个字段名。更简单的方案是用mysql --force参数强制继续导入跳过报错行但这样会丢失部分数据。我一般先尝试--force导入导入完成后用COUNT(*)对比行数如果缺失数据太多再回头排查。5.2 菜谱名乱码或出现问号现象导入后查询dishes_name发现中文变成“????”或者“æ··æ²”这类乱码。原因SQL文件本身是UTF-8编码但客户端连接时用了latin1字符集或者建表语句里没指定字符集导致表默认用了latin1。解决如果已经导进去且表建错不用删库重来可以改表的字符集再修复数据ALTER TABLE dishes CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;逻辑说明CONVERT TO CHARACTER SET会同时改变表默认字符集并转换已有数据但前提是数据没有在写入时被错误解释过——如果写入时已经发生乱码转换这条语句很可能无法恢复原始内容。最保险的做法是重新导入导入前确认三条命令都带了--default-character-setutf8mb4并且用SET NAMES utf8mb4;先设置会话变量。5.3 link表关联不到任何数据现象执行三表联查返回结果一直为空但单独查dishes有数据、查catalog也有数据。原因link表里的catalog_id或dishes_id跟主表的主键对不上。常见原因是数据源导出时link表用了字符串类型的ID而主表用的是INT类型导入后隐式类型转换失败导致匹配失败。解决先用一条SQL找出link表里哪些关联记录在主表中不存在SELECT l.id FROM link l LEFT JOIN dishes d ON l.dishes_id d.dishes_id WHERE d.dishes_id IS NULL LIMIT 20;逻辑说明LEFT JOIN保留link表全部记录如果dishes表没有匹配记录则d.dishes_id为NULL这些就是“孤儿关联”。参数说明如果孤儿关联数据量很少直接删掉即可如果数据量很大说明两个表本身就不是一套数据源需要检查link.sql是不是匹配错了文件。5.4 图片链接打不开或404现象菜谱列表能查出来但前端展示图片时全部裂图。原因数据源里的image_url字段是相对路径或过期链接需要拼接域名前缀或者图片域名不稳定部分图片已经被删除。解决先统计有多少条图片URL是以http://或https://开头的SELECT COUNT(*) AS full_url FROM dishes WHERE image_url LIKE http%;把这张表的查询结果跟完整数据量对比如果只有小部分是全URL说明需要拼接。常见做法是写一段Python脚本批量处理import pymysql conn pymysql.connect(hostlocalhost, userroot, passwordyourpass, databasecookbook, charsetutf8mb4) cur conn.cursor() cur.execute(SELECT dishes_id, image_url FROM dishes WHERE image_url IS NOT NULL) rows cur.fetchall() for dish_id, url in rows: if not url.startswith(http): full_url https://cdn.example.com/images/ url.lstrip(/) cur.execute(UPDATE dishes SET image_url%s WHERE dishes_id%s, (full_url, dish_id)) conn.commit() cur.close() conn.close()逻辑说明这段脚本把非http开头的URL统一加上CDN前缀如果原始字段里存的是/images/123.jpg这种相对路径url.lstrip(/)会去掉开头的斜杠再拼接。参数说明pymysql的charset参数要指定utf8mb4否则URL里带中文时写入会报错。注意这不是一个能自动解决的方案必须确认图片资源包里的目录结构——如果本地图片命名规则是菜品ID.jpg直接按ID拼路径即可。5.5 导入时内存溢出或超时现象导入dishes.sql时报ERROR 1153 (08S01): Got a packet bigger than max_allowed_packet bytes。原因MySQL服务端的max_allowed_packet参数默认值是64M或者更小如果SQL文件里某条INSERT语句包含大量TEXT字段数据比如做法字段特别长单个包超过限制就会被拒绝。解决在导入前先调大参数mysql -uroot -p -e SET GLOBAL max_allowed_packet 268435456;参数说明268435456是256MB一般足够。注意MySQL 8.0里这个值也可以在my.cnf的[mysqld]段下配置然后重启服务但SET GLOBAL的方式不需要重启当前会话和后续新建的连接都会生效。还有一个小技巧导入前关掉binlog可以减少磁盘IO开销业务库不建议这么干本地测试倒是能快不少。6. 把数据包接进真实项目从MySQL导出JSON接口与全文检索的取舍数据清洗和关联查询都跑通之后距离上线还差一步后端接口设计和检索体验。菜谱数据的特点是读多写少13万条记录既不算大也算不上小直接让App端连MySQL不现实常规做法是把数据导出成JSON或同步到专门的检索引擎。我一般用Python写个脚本把dishes表和catalog表打包成业务需要的JSON结构写进Redis或生成静态文件import json import pymysql conn pymysql.connect(hostlocalhost, userroot, passwordyourpass, databasecookbook, charsetutf8mb4) cur conn.cursor(pymysql.cursors.DictCursor) cur.execute( SELECT d.dishes_id, d.dishes_name, d.image_url, d.material, d.practice, GROUP_CONCAT(c.catalog_name SEPARATOR |) AS cat_names FROM dishes d LEFT JOIN link l ON d.dishes_id l.dishes_id LEFT JOIN catalog c ON l.catalog_id c.catalog_id GROUP BY d.dishes_id ) rows cur.fetchall() result [] for row in rows: result.append({ id: row[dishes_id], name: row[dishes_name], img: row[image_url], material: row[material], practice: row[practice], categories: row[cat_names].split(|) if row[cat_names] else [] }) with open(dishes.json, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) print(fexported {len(result)} dishes) cur.close() conn.close()这段脚本的核心是GROUP_CONCAT把一道菜的所有分类合并成一个字段避免JSON里面再嵌套一层循环查数据库对于13万量级的数据生成一个几十MB的JSON文件完全可行。如果用ES建议直接把这张JSON导入建索引查询时用match_query做中英文混合搜索。另外一个值得提的细节是全文检索的取舍。MySQL 8.0内置的全文索引支持中文需要ngram解析器建索引时指定ALTER TABLE dishes ADD FULLTEXT INDEX ft_name (dishes_name) WITH PARSER ngram;ngram默认token大小是2bigram模式对菜名这种短文本效果还行但对“西红柿炒鸡蛋”这种复合词会被切分成“西红”“红柿”“柿炒”等子串搜索精度比专业的ES差很多。如果项目只有菜谱搜索一个场景ES和一个内置全文索引选一个别两个都上维护成本翻倍。最后说一句血泪经验数据包里的图片资源36G听起来量很大但下载到本地后一定要先抽样检查图片的尺寸和格式是否统一——我遇到过图片存的全是webp格式但前端用img标签直接引用导致部分老机型不显示的问题。从那以后我每次换数据源都会强制跑一遍图片完整性校验脚本。希望这次拆包记录能帮你少走几步弯路。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。