简介这份菜谱食谱MySQL数据集适合餐饮类网站开发、美食App后端练习及数据分析学习。数据量约13万条按目录表、菜谱表、目录-菜谱关联表三张核心表组织结构清晰便于实现按分类检索、查看菜谱详情及维护多级目录。资源为RAR压缩包共4个文件主要为3个SQL脚本与1份说明文档压缩包整体仅52.48MB导入MySQL后即可使用另配套约36G图片素材网盘形式可结合数据表完成图文展示。已有941人浏览学习适合具备SQL基础、希望用真实规模数据练习建表、联表查询、索引优化或搭建菜谱类网站的开发者。借助三张表的关联设计与说明文档可快速理解餐饮数据建模思路也能基于数据继续开发推荐、搜索、分类统计等功能是一个实用且超值的练习数据集。1. 拿到 13 万条菜谱食谱数据第一件事不是“怎么做”而是想清楚它怎么进 MySQL手里有一套 13 万条菜谱食谱数据36G 图片准备落到 MySQL 里很多人第一反应是“这么多导到什么时候”第二反应是“菜名和图片怎么关联”。这套数据值不值得买不是重点重点是怎么把它变成 MySQL 里能查、能搜、能支撑后端的东西。如果只是把 CSV 导入一次它只是一堆文件但拆成主表、食材表、步骤表、图片表再配上索引和全文检索它就能变成按菜系、按食材、按名称搜索的食谱库。适合正在做中文搜索、推荐系统、内容站后端的人练手。下面这份落地流程我会从头到尾讲清楚怎么做、参数怎么调、坑在哪。2. 规划数据模型菜谱表、图片路径和 36G 存储的放置方案拿到数据别急着写 INSERT先回答三个问题每条菜谱有哪些字段图片是二进制文件还是已有 URL13 万行要不要拆表很多教程喜欢把食材、步骤、图片全塞进主表比如用逗号把食材串成一个字符串。这种做法写入简单但查询“哪些菜同时有番茄和鸡蛋”会变成正则或 LIKE性能立刻崩。所以即使源数据是一行一个菜谱我也会在导入前把它拆开。我一般会拆成四张表recipe 主表、recipe_ingredient 食材表、recipe_step 步骤表、recipe_image 图片表。这样后面做“番茄炒蛋怎么做”“哪些菜用到了鸡蛋”这类查询逻辑会清晰很多。2.1 菜谱表设计用 MySQL 8.0 建一个能承载 13 万行的最小四表结构先建库和主表。我用 utf8mb4不用默认的 utf8mb3因为数据里可能有特殊符号、空格、甚至 emojiutf8mb4 才是真正完整的字符集。CREATE DATABASE IF NOT EXISTS recipes DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE recipes; CREATE TABLE recipe ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 菜谱唯一 ID, name VARCHAR(200) NOT NULL COMMENT 菜谱名称, cuisine_type VARCHAR(30) DEFAULT NULL COMMENT 菜系如川菜、粤菜, category VARCHAR(30) DEFAULT NULL COMMENT 分类如热菜、凉菜、主食, difficulty TINYINT UNSIGNED DEFAULT 1 COMMENT 难度1 简单2 中等3 困难, cook_time_min SMALLINT UNSIGNED DEFAULT 0 COMMENT 制作时长分钟, description TEXT COMMENT 做法描述或简介, weight INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 人工置顶权重越大越靠前, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜谱主表;id 用 BIGINT而不是 INT主要考虑以后可能会合并多个来源的数据一旦超过 40 亿INT 就撑不住了。name 字段虽然一般只有十几个字VARCHAR(200) 看起来浪费但能给“名称 描述”的全文索引留空间。cuisine_type 和 category 拆成两个字段因为“菜系”和“类型”是两个不同的筛选维度。description 用 TEXT真实菜谱的描述动不动几百字VARCHAR(1000) 也未必够但要注意 TEXT 字段不能直接建普通索引后面我会用全文索引解决。接着建食材、步骤、图片三张子表CREATE TABLE recipe_ingredient ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, recipe_id BIGINT UNSIGNED NOT NULL COMMENT 关联 recipe.id, ingredient_name VARCHAR(100) NOT NULL COMMENT 食材或调料名称, amount VARCHAR(100) DEFAULT NULL COMMENT 用量如“500克”“适量”, sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 在食材列表中的顺序, PRIMARY KEY (id), KEY idx_ingredient_name (ingredient_name), KEY idx_ingredient_recipe (recipe_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT食材配料表; CREATE TABLE recipe_step ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, recipe_id BIGINT UNSIGNED NOT NULL, step_no SMALLINT UNSIGNED NOT NULL DEFAULT 1 COMMENT 第几步, content TEXT NOT NULL COMMENT 步骤文字, image_path VARCHAR(255) DEFAULT NULL COMMENT 步骤图路径可有可无, PRIMARY KEY (id), KEY idx_step_recipe (recipe_id, step_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT制作步骤表; CREATE TABLE recipe_image ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, recipe_id BIGINT UNSIGNED NOT NULL, image_path VARCHAR(255) NOT NULL COMMENT 相对路径或对象存储 Key, sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 第几张图0 为主图, PRIMARY KEY (id), KEY idx_image_recipe (recipe_id, sort_order) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜谱图片表;食材、步骤、图片都通过 recipe_id 关联主表。这里我故意不建 FOREIGN KEY原因很实在导入这几张表有先后顺序外键约束会让 LOAD DATA 的顺序更苛刻而且 13 万行用索引查询完全够外键的强一致性对菜谱场景不是必需品。如果你坚持要外键就等数据导入完成之后再补别一开始就挂上。2.2 36G 图片为什么不该塞进 MySQL文件系统存放 路径入库血泪经验先说结论36G 图片不要用 BLOB 存进 MySQL。原因不是 MySQL 存不了而是 BLOB 会让 binlog、备份、临时表全部变大。mysqldump 一次导出几十 G传输和恢复都是灾难InnoDB buffer pool 还要分内存给这些二进制内容菜谱表明明可以用 1G 内存盖住结果被图片拖慢。常见做法是把图片文件放到独立目录或对象存储数据库里只存相对路径。推荐下面这样分目录避免一个文件夹下几万个文件data/ recipe_images/ 00/ 01/ 02/ ... ff/ 45/12345.jpg目录可以用 recipe_id 的散列值或 ID 取模生成。以 ID 后两位为例recipe_id12345的图片路径就是recipe_images/45/12345.jpg这样 256 个目录均摊 36G单目录最多几百个文件。写入数据库时INSERT INTO recipe_image (recipe_id, image_path, sort_order) VALUES (12345, recipe_images/45/12345.jpg, 0);存相对路径而不是C:\data\...或/home/user/...的好处是以后换服务器只要把整个 recipe_images 目录平移到新机器数据库不用改要上 CDN 或 OSS也只用把前缀拼上。如果原始数据里给的是完整 URL导入时我会先统一截成相对路径或换成一个新域名前缀而不是把 URL 原样存进去。提示决定图片放 CDN 时只需要在应用层拼https://cdn.example.com/image_path不要回头去 UPDATE 数据库里的几千条路径。2.3 字段取舍VARCHAR、TEXT、BIGINT 和重量级字段怎么选13 万条在 MySQL 里是中小型表但字段设计不合理照样会翻车。我的优先级是能用数值不用字符串能用短字符串不用长字符串。难度、时长、排序权重都用整数因为排序和过滤走索引最方便菜系、分类用 VARCHAR(30) 而不是 VARCHAR(255)一方面省空间另一方面也避免索引太长。amount 这类“适量、500克”混着的字段用 VARCHAR(100) 就好不要因为看到“克”就以为是数字。步骤内容、做法描述这类长文本用 TEXT 没问题但不要让它进入 GROUP BY 或 ORDER BY。如果要做中文搜索后面会在 TEXT 字段上建全文索引如果只做等值匹配比如“按菜系筛选”就在 cuisine_type 上加普通索引。TEXT 字段不能设置默认值所以导入时如果为空我一般写成空字符串而不是让 MySQL 报 1067 错误。另一个容易忽略的点是主键。有些人习惯从源数据里复制 id 过来但多个数据源合并时会出现 ID 冲突。我通常保留自己的自增主键把源数据里的 ID 单独存成一个字段source_id用来做原图路径回查。这样既能对上原始文件又不影响 InnoDB 主键的单调性。3. 一次性导入 13 万条菜谱LOAD DATA、预处理脚本和 MySQL 导入调优数据表建好接下来是把数据从原始文件灌进 MySQL。13 万条用 INSERT 循环也能导但速度可能慢到你想删库。我更推荐先用 Python 把原始 JSON 或 Excel 整理成 CSV再用 MySQL 的 LOAD DATA 批量导入。LOAD DATA 不是银弹CSV 里如果字段内换行没处理它会把一行截断所以预处理脚本里要把描述里的\r和\n替换掉。下面这组操作按顺序做下来基本不会卡。3.1 用 LOAD DATA LOCAL INFILE 替代逐条 INSERT导入快一个量级LOAD DATA 是 MySQL 自带的高速导入命令它能直接读取本机 CSV 文件把数据按行写入表。先确认 MySQL 允许 local_infile再执行mysql --local-infile1 -uroot -p recipes看到 MySQL 提示符后执行LOAD DATA LOCAL INFILE /data/csv/recipe.csv INTO TABLE recipe CHARACTER SET utf8mb4 FIELDS TERMINATED BY , OPTIONALLY ENCLOSED BY LINES TERMINATED BY \n IGNORE 1 LINES (id, name, cuisine_type, category, difficulty, cook_time_min, description, weight);逐项说明。CHARACTER SET utf8mb4告诉 MySQL 文件里的字符串是 utf8mb4不是客户端默认字符集。FIELDS TERMINATED BY ,表示字段之间用逗号分隔OPTIONALLY ENCLOSED BY 表示字段可以用双引号包起来包起来之后的内部逗号不会被当成分隔符。IGNORE 1 LINES跳过 CSV 表头。最后那行列的是 CSV 每列对应表里的字段顺序不能错如果 CSV 里没有 id就让 id 为 NULL 自动生成但要注意导入后 AUTO_INCREMENT 会自动靠最大行更新一般不用手动补。如果你用 Workbench 或 Navicat 的导入向导底层原理也是 LOAD DATA但很多图形工具会把 local_infile 关掉报“The used command is not allowed with this MySQL version”。这时候要么按上面命令手动导入要么在客户端连接参数里把OPT_LOCAL_INFILE1打开。3.2 导入前临时调整 4 个关键会话参数避免中途失败LOAD DATA 在默认配置下也会因为 binlog、唯一索引检查、自动提交太频繁而变慢。我的习惯是导入前开一个会话把这些参数临时调一下SET SESSION autocommit0; SET SESSION unique_checks0; SET SESSION foreign_key_checks0; SET SESSION sql_modeNO_AUTO_VALUE_ON_ZERO;autocommit0是让所有写入在一个事务里最后统一 COMMIT减少磁盘刷页次数unique_checks0是告诉 MySQL 唯一索引先不逐行检查配合 LOAD DATA 能快不少但前提是你已经确认源数据没有重复主键foreign_key_checks0在有外键的场景导入子表时必须关掉否则先插食材表会报外键失败sql_mode改成NO_AUTO_VALUE_ON_ZERO是为了防止 CSV 里出现 id0 时被 MySQL 当成自增值重置。这三个参数只在当前会话生效不会污染其他连接。导入完成后执行COMMIT;如果导入过程中报“Row size too large”或者“Data truncated for column”不要急着加字段长度先打开错误日志看是哪一行通常是有空值、引号没闭合或类型写错和 MySQL 配置关系不大。这个量级的数据完全不需要改max_allowed_packet或重开一个大 buffer pool默认配置就能跑完。3.3 Python 把 JSON 拆成 CSV逐行处理 13 万数据不爆内存原始数据经常是 JSON Lines每一行一个菜谱对象里面有食材数组、步骤数组。我一般先用 Python 把它拆成主表 CSV 和子表 CSV而不是直接读成 Python 对象再 INSERT。import csv import json from pathlib import Path src Path(recipes.json) out_main Path(recipe.csv) out_ingredients Path(recipe_ingredient.csv) out_steps Path(recipe_step.csv) with src.open(r, encodingutf-8-sig) as rf, \ out_main.open(w, newline, encodingutf-8) as mf, \ out_ingredients.open(w, newline, encodingutf-8) as igf, \ out_steps.open(w, newline, encodingutf-8) as stf: main_writer csv.writer(mf) ing_writer csv.writer(igf) step_writer csv.writer(stf) main_writer.writerow([id, name, cuisine_type, category, difficulty, cook_time_min, description, weight]) ing_writer.writerow([recipe_id, ingredient_name, amount, sort_order]) step_writer.writerow([recipe_id, step_no, content]) for line in rf: item json.loads(line) rid item.get(id) or 0 main_writer.writerow([ rid, item.get(name, ).strip(), item.get(cuisine_type, ) or , item.get(category, ) or , int(item.get(difficulty, 1) or 1), int(item.get(cook_time_min, 0) or 0), (item.get(description) or ).replace(\r, ).replace(\n, ), 0 ]) for idx, ing in enumerate(item.get(ingredients) or []): ing_writer.writerow([ rid, str(ing.get(name, )).strip(), ing.get(amount, ) or , idx ]) for idx, step in enumerate(item.get(steps) or [], start1): step_writer.writerow([ rid, idx, step.get(text, ) or ])这段脚本最值得注意的不是循环而是三个细节。第一逐行读文件13 万行不会把内存打爆不要把 JSON 一次性json.load进来。第二encodingutf-8-sig可以处理带 BOM 的文件遇到菜名开头有一个不可见字符导致搜索不到十有八九就是它。第三写主表和写子表在同一个with里完成食材和步骤都带着 recipe_id导入子表时需要主表的 id如果你的源数据没有主键编号就要靠 name 唯一键回填。4. 索引、排序和深分页13 万条菜谱查询如何保持流畅数据导完之后查询慢的坑才开始显形。13 万行不算多但如果每个查询都是全表扫描服务器照样会 CPU 飙高、页面转圈。下面三个优化是我做菜谱库时一定会做的。4.1 给常用过滤条件加复合索引从全表扫到命中 20 行先看这条最简单的查询SELECT id, name, cuisine_type, cook_time_min FROM recipe WHERE cuisine_type 川菜 ORDER BY cook_time_min LIMIT 20;在没索引的情况下EXPLAIN 会告诉你typeALLrows 接近 13 万还会出现Using filesort。13 万行 filesort 不至于卡死但并发一高就明显变慢。解决方法是建一个复合索引ALTER TABLE recipe ADD INDEX idx_cuisine_time (cuisine_type, cook_time_min);建完再 EXPLAINtype 变成refrows 从 13 万变成几十行ORDER BY 也可以直接走索引不再 filesort。这里有个顺序问题等值条件cuisine_type放在索引最左边排序字段cook_time_min放右边。如果你把范围条件放在前面排序索引就失效了所以“食材属于哪一类”和“按什么排序”要分清。如果你以后要加“主料”筛选比如同时查菜系和主料就再建一个(cuisine_type, main_ingredient)索引。普通索引别超过五个否则写入变慢。4.2 中文菜谱搜索别用 LIKE %词%全文索引配 ngram排序再加权重菜谱站最常用的功能是搜索菜名。新手容易直接写SELECT id, name FROM recipe WHERE name LIKE %红烧肉% ORDER BY weight DESC, id DESC LIMIT 20;这条 SQL 会让 MySQL 全表扫一遍因为它没法用普通 B-Tree 索引来加速%词%这种前导通配。13 万行勉强能跑但页面多了就会慢。MySQL 从 5.7 开始支持内置全文索引中文需要 ngram parserALTER TABLE recipe ADD FULLTEXT INDEX ft_name_description (name, description) WITH PARSER ngram;搜索时用 MATCH AGAINSTSELECT id, name, MATCH(name, description) AGAINST(红烧肉) AS score FROM recipe WHERE MATCH(name, description) AGAINST(红烧肉 IN NATURAL LANGUAGE MODE) ORDER BY weight DESC, score DESC LIMIT 20;注意两点。第一ngram_token_size默认是 2能覆盖绝大多数中文菜名如果你要搜“鱼”这种单字就得把 ngram_token_size 改成 1改完要重启 MySQL。第二score是相关性分数但不能完全代表用户偏好所以我通常在 ORDER BY 里把 weight人工置顶权重排在 score 前面热门菜优先。这个搜索方案在 13 万条这个量级完全够用如果数据到几百万条再考虑第三方检索引擎但那是另一个话题了。4.3 图片关联统计与 COUNT 的正确姿势经常有运营要“找出没有主图的菜谱”。用 JOIN 和 COUNT 也能做但写法不对会生成临时表SELECT r.id, r.name FROM recipe r LEFT JOIN recipe_image i ON i.recipe_id r.id GROUP BY r.id, r.name HAVING COUNT(i.id) 0 LIMIT 20;这种写法在 13 万行上其实也能跑但 GROUP BY 会做临时表而且COUNT(i.id)遇到 NULL 会跳过。更稳妥的写法是 NOT EXISTSSELECT r.id, r.name FROM recipe r WHERE NOT EXISTS ( SELECT 1 FROM recipe_image i WHERE i.recipe_id r.id ) LIMIT 20;NOT EXISTS 在 recipe_image 的 recipe_id 索引命中后对每一行主表做一次快速探测性能通常比 LEFT JOIN GROUP BY 好。统计每个菜有几张图反而建议用 GROUP BYSELECT recipe_id, COUNT(*) AS image_count FROM recipe_image GROUP BY recipe_id ORDER BY image_count DESC LIMIT 10;图片表用索引在 recipe_id 上扫描很快。不要在这条查询里 JOIN recipe 主表拿菜名先分组统计拿 ID 再回表逻辑更清楚。5. MySQL 避坑现场乱码、重复导入、SSL 连接和锁表排查前面流程看起来顺实际导入和上线会遇到几个高频问题。我把踩过的坑按“现象→原因→解决”整理在下面你大概率会遇到其中一两个。5.1 导入乱码菜名全是“”十有八九是连接字符集现象LOAD DATA 完成后SELECT * FROM recipe LIMIT 10看到菜名全是中文问号。很多人第一反应是重建表、改 COLLATE结果越改越乱。原因文件本身是 GBK 或 GB18030而导入时指定了 utf8mb4或者文件是 UTF-8但客户端连接默认character_set_client不是 utf8mb4。MySQL 只按连接指定字符集去解释 CSV 字节和表字段字符集是两回事。解决先确认文件真实编码再决定转码file -bi recipe.csv iconv -f GBK -t UTF-8 recipe.csv recipe_utf8.csv然后用--default-character-setutf8mb4连接并在 LOAD DATA 前执行SET NAMES utf8mb4。SET NAMES utf8mb4是让客户端、连接、返回结果三层都统一加上 LOAD DATA 的CHARACTER SET utf8mb4才完整。如果是 Java 程序JDBC URL 写characterEncodingutf8mb4而不是utf8MySQL Connector/J 8.0 对 “utf8” 有历史兼容问题。5.2 中断重导出现重复数据唯一索引是唯一后悔药现象LOAD DATA 跑到一半客户端断线你重新执行同一条命令完成后发现 recipe 表行数超过 13 万出现了重复菜名。原因LOAD DATA 不具备“已存在则跳过”的语义重复执行就是重复插入。源数据没有主键可控MySQL 也无法判断。解决给业务自然键加上唯一索引再用LOAD DATA ... IGNORE。比如菜名理论上不应该重复ALTER TABLE recipe ADD UNIQUE KEY uk_recipe_name (name);然后LOAD DATA LOCAL INFILE /data/csv/recipe.csv IGNORE INTO TABLE recipe ...IGNORE会让遇到重复 name 的行跳过而不是报错中断。这个做法适合“首次建库”和“增量补充”两个场景。如果你已经插入了重复数据先执行SELECT name, COUNT(*) FROM recipe GROUP BY name HAVING COUNT(*) 1 LIMIT 20;确认重复范围后保留最小 id、删掉其余行。没有唯一索引删到一半又来一批重复仍会翻车。所以我在导入前先做两件事看源数据里 name 是否唯一建唯一索引再加 IGNORE。5.3 MySQL 8.0 的 SSL 连接报错别一上来就卸载重装现象应用连数据库报[...][08001] SSL connection error或者Unable to connect to any of the specified MySQL hosts。搜索教程很容易走到“卸载重装”这条路实际上重装解决不了。原因MySQL 8.0 默认启用 SSL客户端连接时服务端会下发证书。如果你用旧版本驱动、或者服务端证书目录被清理过SSL 握手就会失败。这个问题和密码无关也和你数据是否敏感无关。解决内网环境直接关闭 SSL或客户端禁用mysql -h127.0.0.1 -uroot -p --ssl-modeDISABLEDJDBC 连接串加jdbc:mysql://127.0.0.1:3306/recipes?sslModeDISABLED服务端也可以在 my.cnf 里加skip_ssl然后重启 MySQL。线上如果有公网访问需求别关 SSL而是把正确证书配好并让客户端sslModeVERIFY_CA。很多云数据库控制台默认给的是内网地址没必要为这个 SSL 报错折腾半天。5.4 数据量不大但长事务锁表先查行锁和 MDL 锁再决定要不要 kill现象网站某个页面突然卡住SHOW PROCESSLIST里连接没有在运行 SQL但状态是Waiting for table metadata lock或者报Lock wait timeout exceeded。明明只有 13 万行为什么会锁死原因有人打开一个事务更新了几行然后一直没提交。后续对这个表的任何查询可能被 MDL 锁卡住InnoDB 行锁也可能让其他 UPDATE 等待。MySQL 的锁分三种行锁、表锁、MDL 元数据锁。菜谱这种并发不高的表最常见的就是事务没提交。解决先看事务状态SELECT p.ID, p.TIME, p.STATE, p.INFO FROM performance_schema.processlist p WHERE p.COMMAND Sleep AND p.TIME 10 OR p.STATE LIKE %metadata lock%;找到持锁连接后确认是应用未提交再KILL id。不要见到 Waiting 就 kill有时候是 DDL 在等查询结束kill 掉只读连接会影响正在跑的查询。更好的做法是在应用层强制短事务所有 UPDATE 后立即 COMMIT避免在代码里“先查出来改半天再写”。MySQL 的 autocommit 默认是 1但连接池如果开启了手动提交忘了 commit 是常态。我会把连接池的defaultAutoCommittrue设成显式值防止隐式事务变成黑匣子。6. 先用查询验证数据再做第一个实用功能按食材组合找菜谱数据导入完成先别急着写接口。我会用两条 SQL 验证数据和实现第一个功能顺便把 13 万条、36G 图片这个预期对齐到库上。6.1 用 HAVING 实现“同时包含番茄和鸡蛋”的菜谱筛选食材子表拆好之后按食材找菜谱就是 JOIN GROUP BY HAVING 的组合SELECT r.id, r.name, GROUP_CONCAT(i.ingredient_name ORDER BY i.sort_order SEPARATOR 、) AS ingredients FROM recipe r JOIN recipe_ingredient i ON i.recipe_id r.id WHERE i.ingredient_name IN (番茄, 鸡蛋) GROUP BY r.id, r.name HAVING COUNT(DISTINCT i.ingredient_name) 2 ORDER BY r.weight DESC, r.id DESC LIMIT 20;HAVING COUNT(DISTINCT i.ingredient_name) 2表示两个食材都出现了改成1就是“包含任意一个”。这个功能对 13 万条完全够用前提是 recipe_ingredient.ingredient_name 建了普通索引。如果以后要支持“不要某种食材”再加一个 NOT IN 条件但要处理好 NULL。6.2 用 wc、du 和 COUNT 校验 13 万条与 36G 图片校验数据不是可有可无它能暴露导入时的截断、缺图、重复。我一般会这样对账wc -l recipe.csv du -sh recipe_images/ mysql -h127.0.0.1 -uroot -p -e SELECT COUNT(*) AS total_recipes FROM recipes.recipe; SELECT COUNT(DISTINCT recipe_id) AS recipe_with_images FROM recipes.recipe_image; SELECT COUNT(*) FROM recipes.recipe WHERE name OR name IS NULL; wc -l是 CSV 的总行数要减去表头一行才等于数据行数du -sh是图片目录实际大小和 36G 对比能发现缺图第三条 SQL 是垃圾数据检查。我的习惯是把这些数写进导入脚本的输出日志存成一份校验文件以后排查问题不用靠记忆。每次数据更新后自动跑一遍确认行数不跳水、图片不丢失再更新线上版本。希望这份流程对你有用也祝你的菜谱库一次导入不翻车。本文还有配套的精品资源点击获取