☰
13万条菜谱数据高效导入MySQL:从建表到索引优化全指南
2026/10/12 5:41:36 网站建设 项目流程

简介:这份菜谱食谱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,先回答三个问题:每条菜谱有哪些字段?图片是二进制文件还是已有 URL?13 万行要不要拆表?很多教程喜欢把食材、步骤、图片全塞进主表,比如用逗号把食材串成一个字符串。这种做法写入简单,但查询“哪些菜同时有番茄和鸡蛋”会变成正则或 LIKE,性能立刻崩。所以即使源数据是一行一个菜谱,我也会在导入前把它拆开。我一般会拆成四张表:recipe 主表、recipe_ingredient 食材表、recipe_step 步骤表、recipe_image 图片表。这样后面做“番茄炒蛋怎么做”“哪些菜用到了鸡蛋”这类查询,逻辑会清晰很多。

2.1 菜谱表设计:用 MySQL 8.0 建一个能承载 13 万行的最小四表结构

先建库和主表。我用 utf8mb4,不用默认的 utf8mb3,因为数据里可能有特殊符号、空格、甚至 emoji,utf8mb4 才是真正完整的字符集。

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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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_id=12345的图片路径就是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-infile=1 -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_INFILE=1打开。

3.2 导入前临时调整 4 个关键会话参数,避免中途失败

LOAD DATA 在默认配置下也会因为 binlog、唯一索引检查、自动提交太频繁而变慢。我的习惯是导入前开一个会话,把这些参数临时调一下:

SET SESSION autocommit=0; SET SESSION unique_checks=0; SET SESSION foreign_key_checks=0; SET SESSION sql_mode='NO_AUTO_VALUE_ON_ZERO';

autocommit=0是让所有写入在一个事务里,最后统一 COMMIT,减少磁盘刷页次数;unique_checks=0是告诉 MySQL 唯一索引先不逐行检查,配合 LOAD DATA 能快不少,但前提是你已经确认源数据没有重复主键;foreign_key_checks=0在有外键的场景导入子表时必须关掉,否则先插食材表会报外键失败;sql_mode改成NO_AUTO_VALUE_ON_ZERO是为了防止 CSV 里出现 id=0 时被 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', encoding='utf-8-sig') as rf, \ out_main.open('w', newline='', encoding='utf-8') as mf, \ out_ingredients.open('w', newline='', encoding='utf-8') as igf, \ out_steps.open('w', newline='', encoding='utf-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 [], start=1): step_writer.writerow([ rid, idx, step.get('text', '') or '' ])

这段脚本最值得注意的不是循环,而是三个细节。第一,逐行读文件,13 万行不会把内存打爆,不要把 JSON 一次性json.load进来。第二,encoding='utf-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 会告诉你type=ALL,rows 接近 13 万,还会出现Using filesort。13 万行 filesort 不至于卡死,但并发一高就明显变慢。解决方法是建一个复合索引:

ALTER TABLE recipe ADD INDEX idx_cuisine_time (cuisine_type, cook_time_min);

建完再 EXPLAIN,type 变成ref,rows 从 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 parser:

ALTER TABLE recipe ADD FULLTEXT INDEX ft_name_description (name, description) WITH PARSER ngram;

搜索时用 MATCH AGAINST:

SELECT 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 EXISTS:

SELECT 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 BY:

SELECT 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-set=utf8mb4连接,并在 LOAD DATA 前执行SET NAMES utf8mb4。SET NAMES utf8mb4是让客户端、连接、返回结果三层都统一,加上 LOAD DATA 的CHARACTER SET utf8mb4才完整。如果是 Java 程序,JDBC URL 写characterEncoding=utf8mb4而不是utf8,MySQL 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-mode=DISABLED

JDBC 连接串加:

jdbc:mysql://127.0.0.1:3306/recipes?sslMode=DISABLED

服务端也可以在 my.cnf 里加skip_ssl,然后重启 MySQL。线上如果有公网访问需求,别关 SSL,而是把正确证书配好,并让客户端sslMode=VERIFY_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 是常态。我会把连接池的defaultAutoCommit=true设成显式值,防止隐式事务变成黑匣子。

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 是垃圾数据检查。我的习惯是把这些数写进导入脚本的输出日志,存成一份校验文件,以后排查问题不用靠记忆。每次数据更新后自动跑一遍,确认行数不跳水、图片不丢失,再更新线上版本。希望这份流程对你有用,也祝你的菜谱库一次导入不翻车。

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

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

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

立即咨询