在电商系统里,商品数据是业务运转的核心。MySQL作为最常用的关系型数据库,设计一套清晰、可扩展的商品表结构,直接影响后续搜索、下单、库存扣减等模块的开发效率。商品模型不能只看成“一个卖的东西”,它包含类目、品牌、基础信息、销售规格等多个维度。

一、商城商品模型的实体关系
从业务视角看,商城商品至少涉及四个核心实体:商品分类、品牌、商品(SPU)、规格库存单元(SKU)。商品分类用于树形归类,品牌描述生产方,SPU代表一款商品的基础描述,SKU则是具体可售卖的最小单元,比如“红色、XL码”的T恤。
如果把这些全写进一张表,会出现大量空值和重复数据。例如同一SPU下不同颜色尺码,名称和详情完全一样,仅规格与价格不同。按数据库范式拆分,既能减少冗余,也方便单独维护类目或品牌信息。实际项目中通常采用第三范式起步,再针对高频查询做适度反范式。
1.1 商品分类表
分类表一般采用邻接表或闭包表实现树形结构。中小商城用邻接表更简单,通过parent_id指向父节点。字段包括id、名称、父级id、排序值、是否显示等。
下面给出分类表的建表语句,字符集统一用utf8mb4避免表情与特殊汉字乱码:
CREATE TABLE `category` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '分类名称', `parent_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父分类id,0为顶级', `sort` INT NOT NULL DEFAULT 0 COMMENT '排序', `is_show` TINYINT NOT NULL DEFAULT 1 COMMENT '是否展示', PRIMARY KEY (`id`), KEY `idx_parent` (`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表';
1.2 品牌表与商品主表
品牌表结构较为简单,主要存品牌名与logo地址。商品主表(SPU)则关联分类与品牌,保存商品标题、副标题、详情、默认图片等通用字段,不关心具体卖哪个规格。
商品主表的设计重点是把“描述性信息”与“经营性信息”分离。详情可用长文本,但列表查询不应直接读详情字段,可通过冗余封面图与简述提升列表性能。
CREATE TABLE `brand` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '品牌名', `logo` VARCHAR(255) DEFAULT '' COMMENT '品牌logo', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='品牌表'; CREATE TABLE `product` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `category_id` INT UNSIGNED NOT NULL COMMENT '分类id', `brand_id` INT UNSIGNED NOT NULL COMMENT '品牌id', `title` VARCHAR(100) NOT NULL COMMENT '商品标题', `sub_title` VARCHAR(200) DEFAULT '' COMMENT '副标题', `cover` VARCHAR(255) DEFAULT '' COMMENT '封面图', `detail` TEXT COMMENT '商品详情', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态', PRIMARY KEY (`id`), KEY `idx_cat` (`category_id`), KEY `idx_brand` (`brand_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SPU表';
二、SKU表与规格设计
SKU是用户真正下单的对象。它必须绑定到某个SPU,并记录价格、库存、规格值。规格本身可能动态变化,如服装有颜色、尺码,手机有容量、版本,因此规格定义与SKU值需单独建模。
常见做法是建规格组表、规格值表,再用关联表把SKU与多个规格值绑定。这样新增规格不用改表结构。若业务较简单,也可在SKU表用JSON字段存规格组合,牺牲部分查询灵活性换取开发速度。
2.1 规格与SKU表示例
以下语句展示规格值表与SKU表,以及用中间表关联SKU与规格值。库存字段使用无符号整数,价格以分为单位存整数避免浮点误差。
CREATE TABLE `sku` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `product_id` INT UNSIGNED NOT NULL COMMENT '所属SPU', `price` INT UNSIGNED NOT NULL COMMENT '售价,单位分', `stock` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '库存', `sn` VARCHAR(50) DEFAULT '' COMMENT '货号', PRIMARY KEY (`id`), KEY `idx_product` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SKU表'; CREATE TABLE `spec_value` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `product_id` INT UNSIGNED NOT NULL COMMENT '所属SPU', `spec_name` VARCHAR(30) NOT NULL COMMENT '规格名,如颜色', `value` VARCHAR(30) NOT NULL COMMENT '规格值,如红色', PRIMARY KEY (`id`), KEY `idx_prod` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规格值表'; CREATE TABLE `sku_spec` ( `sku_id` INT UNSIGNED NOT NULL, `spec_value_id` INT UNSIGNED NOT NULL, PRIMARY KEY (`sku_id`,`spec_value_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SKU规格关联表';
2.2 单表宽表与多表范式对比
有些团队为图省事,用一张item表把分类名、品牌名、颜色、尺码、库存全写上。初期查询快,但同一SPU多SKU时,标题详情反复冗余;改品牌名要更新成百上千行;加新规格就得ALTER TABLE加列,线上风险高。
多表方案写操作稍复杂,插入商品要事务内写主表与SKU表,但读场景可通过视图或列表接口聚合。从长期维护看,范式化结构更稳。下表简要对比二者:
| 维度 | 单表宽表 | 多表范式 |
|---|---|---|
| 冗余度 | 高 | 低 |
| 扩展规格 | 需改表结构 | 仅插数据 |
| 联表复杂度 | 无 | 中等 |
| 维护成本 | 随业务膨胀陡增 | 平稳 |
三、索引与性能注意事项
商品检索常按分类、品牌、状态过滤,并排序或分页。上述建表已对category_id、brand_id、product_id建二级索引。若商城有搜索框,应接Elasticsearch,而非在MySQL做全文模糊查询,否则大表LIKE会拖垮库。
库存扣减是高并发重点。更新SKU库存必须用单行UPDATE配合WHERE stock>=购买数,避免超卖。如下代码展示安全扣库存写法:
UPDATE `sku` SET `stock` = `stock` - 2 WHERE `id` = 123 AND `stock` >= 2;
该语句利用数据库行锁与条件判断,在并发下保证不扣成负数。应用层检查影响行数,若为0则说明库存不足,可回滚订单。这种设计比先SELECT再UPDATE少一次交互,也规避了间隔期的超卖窗口。
四、总结与落地建议
商城商品表结构应以SPU与SKU分离为主线,配合分类、品牌、规格值等周边表。初期可省略规格组表,用spec_value加sku_spec足够覆盖大部分场景。字符集统一utf8mb4,价格存整数分,库存更新走条件UPDATE。
当商品量过百万且读多写少,可考虑将封面、标题冗余进SKU列表宽表做展示层反范式,但写入仍走规范表。保持核心模型干净,扩展表服务前端,是在MySQL上设计商城商品系统的稳妥路径。