商品基础字段设计应优先满足高频查询路径,用联合索引替代fulltext;价格必须用decimal(10,2)保证精度;库存需拆分为stock_total和stock_available并加行锁校验;分类、品牌、规格应独立建表或用生成列索引,外键约束不可省略。

商品基础字段怎么选,别一上来就加 fulltext
大多数人在设计商品表时,第一反应是把所有能想到的字段全塞进去:名称、描述、价格、库存、分类、品牌……结果导出 SQL 一看,description 用 TEXT,还顺手加了 FULLTEXT 索引。但实际业务里,90% 的搜索走的是关键词模糊匹配(LIKE '%xxx%')或前端传来的分类+价格区间筛选——FULLTEXT 不仅写入变慢,还可能因停用词、分词规则导致查不到预期结果。
建议先锁定高频查询路径:WHERE category_id = ? AND status = 'on_sale' ORDER BY created_at DESC。据此优先建联合索引:INDEX (category_id, status, created_at);描述字段保持 TEXT 即可,真要搜内容再单独上 Elasticsearch 或 MySQL 5.7+ 的 JSON_CONTAINS 配合生成列。
price 字段必须用 DECIMAL,别信 FLOAT
用 FLOAT 存价格是线上事故高发区。比如 99.99 + 0.01 在 FLOAT 下可能算出 100.00000000000001,前端展示异常,支付对账时直接报错。MySQL 的 DECIMAL(10,2) 才是标准解法——10 位总长,2 位小数,既能覆盖万元级商品,又保证精度零丢失。
额外注意两点:
- 所有价格类字段(
price、cost_price、market_price)统一用DECIMAL(10,2),别混用DECIMAL(8,2)或DECIMAL(12,2) - 应用层写入前不做四舍五入,让数据库自己处理;比如 PHP 用
bcadd('99.99', '0.01', 2),Python 用decimal.Decimal
库存字段要不要拆成 stock_total 和 stock_available
单存一个 stock 字段看似简单,但订单创建、支付成功、退款、超时释放这些流程全靠它,极易出现超卖。真实场景中,必须分离「总库存」和「可用库存」:
-
stock_total:只在商品上架/调拨时更新,业务侧人工干预才动 -
stock_available:随订单生命周期变更(下单减、支付失败回滚、发货扣减、退货加回) - 关键约束:
stock_available ,且所有扣减操作必须加行锁:<code>UPDATE products SET stock_available = stock_available - 1 WHERE id = ? AND stock_available >= 1
漏掉 AND stock_available >= 1 条件,或者没用 FOR UPDATE(在事务里),高并发下库存就穿底。
分类、品牌、规格这些关联数据,别全堆在商品表里
看到「分类名称」「品牌 logo URL」「规格 JSON」全放在 products 表里,就知道表快废了。分类和品牌是典型多对一关系,硬编码进商品表会导致:
- 改个分类名要批量
UPDATE,锁表风险高 - 品牌 logo 变更,所有商品记录都要更新,IO 浪费严重
- 规格差异大(手机有颜色/内存,衣服有尺码/颜色),用
JSON存会丧失查询能力(比如“查所有 128G 黑色 iPhone”)
正确做法:
- 独立
categories和brands表,商品表只存category_id、brand_id外键 - 规格走 EAV 或专用规格表(如
product_skus),每个 SKU 一行,带独立库存、价格、条码 - 如果真要用 JSON 存扩展属性,至少加生成列:
CREATE COLUMN color VARCHAR(32) AS (JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color'))),再建索引
字段越多越容易忽略数据一致性——比如删掉一个品牌,却忘了清空关联商品的 brand_id,后续查出来全是 NULL 品牌。外键约束不是可选项,是必选项。











