跳至正文

数据库从 13 张表缩到 5 张,查询慢了 3 倍,我反而觉得赚了

做项目最怕什么?不是需求变,是数据库设计拍脑门。

上个月接了个二手交易小程序的后端,老板扔过来一份需求文档,说“猫哥,两周能上线吧”。我扫了一眼,用户、商品、订单、支付、物流、评价、收藏、积分——这不就是个电商吗,13 张表一画,开干。

结果第三天就翻车了。

用户下单买了个相机,订单表要查商品信息,商品表要查卖家信息,卖家信息又关联店铺表,店铺表还得查地址表。一个订单详情页,MySQL 里 JOIN 了 7 张表,查一次 380 毫秒。页面上还有个“猜你喜欢”推荐模块,又要从收藏表和浏览记录表里捞数据。整个接口响应时间 1.2 秒,老板拿着手机在群里艾特我:“猫哥,这玩意儿咋这么卡?”

我当时的表情就跟那个捂脸猫表情包一样。

硬着头皮查慢查询日志,发现最坑的一条 SQL 长这样:

“`sql
SELECT o.*, u.nickname, g.title, g.images, s.shop_name, a.province, a.city
FROM orders o
LEFT JOIN users u ON o.buyer_id = u.id
LEFT JOIN goods g ON o.goods_id = g.id
LEFT JOIN shops s ON g.shop_id = s.id
LEFT JOIN addresses a ON s.address_id = a.id
WHERE o.id = ?
“`

7 张表,7 个 LEFT JOIN,MySQL 执行计划里 Extra 列全是 Using temporary; Using filesort。索引不是没建,是建了也没用——关联字段类型不统一。订单表里的 buyer_id 是 varchar(32),用户表里的 id 是 int(11)。MySQL 隐式类型转换,索引原地失效。

这事怪谁?怪我建表的时候没对齐类型。但更深层的问题是:我一开始就没想清楚数据到底怎么用。

后来花了 3 天重构,做了 3 件事。

第一件:把高频查询的字段直接冗余到主表。订单详情页要展示商品标题和封面图,那好,订单表里直接加 goods_title 和 goods_cover 字段,写入订单时从商品表同步过来。查询订单详情不再 JOIN 商品表,单表查询 3 毫秒搞定。

第二件:按业务边界拆分数据库。原来的 13 张表全在一个库里,读写都在一个连接上。现在拆成 3 个库:核心交易库(订单、支付流水)、用户库(用户、地址)、商品库(商品、店铺、分类)。每个库独立连接池,互不影响。

第三件:引入缓存兜底。商品详情和店铺信息这种变更频率低的,查出来就扔 Redis 里,过期时间设 30 分钟。订单状态这种频繁变动的,只缓存用户最近 20 条订单的 ID 列表,具体数据还是查库。

重构完,表结构缩到了 5 张核心表,但加了 4 张冗余字段表。总表数 9 张,比原来还少 4 张。接口响应时间从 1.2 秒降到了 80 毫秒。老板在群里发了三个大拇指表情。

但说实话,查询慢了 3 倍是真的——原来一个复杂的聚合查询跑 200 毫秒,现在因为数据分散在多个库,要分多次查再组装,整体反而慢了。不过业务场景里这种聚合查询一天跑不到 10 次,完全能接受。

总结几个踩过的坑:

数据库设计别追求范式化到底。第三范式是教科书里的,实际业务里冗余字段该加就加。我见过最离谱的设计是用户表里不存手机号,每次查手机号都要去账户表 JOIN,理由是“手机号可能变更,冗余了不好维护”。兄弟,手机号变更频率一年一次,订单详情页每秒被访问 100 次,哪个代价大?

字段类型一定要统一。关联字段的类型、字符集、排序规则必须一模一样。varchar 对 int、utf8 对 utf8mb4,MySQL 都会让你付出惨痛代价。我那次 buyer_id 和 id 的类型不一致,就是建表时没统一规范,两个人各建各的。

索引不是越多越好。我见过一张表 20 个索引的,写入性能惨不忍睹。索引要按查询模式来建,最左前缀原则得记住。联合索引 (status, create_time) 能覆盖大部分按状态排序的场景,就别单独建 status 索引和 create_time 索引了。

最后,别信“先上线再说”这种鬼话。数据库设计一开始没想好,后面改起来比重新写一遍还痛苦。我这一周重构,光数据迁移脚本就写了 300 行,还要兼容线上正在跑的订单,生怕漏掉一笔。凌晨 3 点盯着屏幕跑数据,那种酸爽,一次就够了。


📎 延伸阅读

看完这篇,如果你想:

  • 直接拿工具 → 回复”13“,我把跨境获客工具包发给你
  • 系统学习 → 点击菜单”AI训练营”,从0开始跑通AI变现

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注