在做电商系统的这两年里,我见过不少刚入行的同学(包括当年的我自己)在SPU、SKU、ID三个词之间反复打转。后台商品列表一个字段一个字段往下填,运营跑来问"这个SPU价格是不是改错了",开发在工单里写"SKU库存扣减失败",两边说的东西好像是一回事,又明显不是一回事。其实这些概念拆开看非常直白:SPU是标准产品单元,SKU是库存保有单位,ID是唯一的标识。搞懂它们不是为了背名词,而是为了以后设计商品数据、排查库存和价格问题时不至于抓瞎。这篇就按我自己的理解,把三者什么意思、怎么区别、怎么关联一次说清楚,适合刚碰电商的产品、运营和开发同事,也适合想系统梳理商品主数据的同学。
1. 先从一个真实的翻车现场说起:把SPU和SKU混在一起会出什么事
几年前我参与一个电商后台的改版,当时商品列表页一直有个奇怪的现象:运营在Excel里批量改价,选中一行填了新价格,保存后发现同款商品下好几条不同颜色的记录价格全变了。第一次遇到时大家都以为是Excel模板的问题,后来查了数据库,才发现运营表格里的"SPU编码"列,实际填的是SKU编码,而系统批处理接口是按SPU维度去覆盖价格的。
这个事故的本质就是概念没对齐。SPU是商品的大类,比如"iPhone 15 Pro Max";SKU是这个大类下面更具体的售卖单元,比如"iPhone 15 Pro Max 256G 原色钛金属"。运营想改的是某个具体颜色和容量的价格,也就是某个SKU的价格,但她在表格里用了SPU这一层的关键字段,程序一执行,整条SPU下的所有SKU全部跟着变动,差价的损失当时算了半天才理清楚。
从那以后我就养成了一个习惯:在任何商品系统的文档、接口、表结构设计里,第一件事先明确字段到底挂在SPU上还是SKU上,宁可多写注释也不要默认别人能猜对。很多看起来像是"数据没同步"的问题,最后查下来都是这个层次没分清楚。电商系统里商品主数据是所有业务的地基,库存、价格、订单、售后全部从这里长出来,地基的层级定义错了,后面每一层都要跟着遭殃。
这个翻车经历也正好引出本文要回答的三个问题:SPU和SKU到底各自代表什么?ID在这个体系里扮演什么角色?三者之间的关系该怎么用一句话跟同事对齐?
2. SPU、SKU、ID分别是什么:从定义到生活化类比
2.1 SPU:标准化产品单元,管的是一件商品的"共同身份"
SPU全称是Standard Product Unit,标准化产品单元。它的核心特征是"标准化":只要两件商品在功能、品类、关键公共属性上是同一个东西,不管颜色、容量、尺寸怎么变,它们共享同一个SPU。
我常拿汽车来打比方:SPU相当于车型。丰田卡罗拉是一个车型,它在官网展示的时候有排量、车身结构、底盘类型这些共性信息,至于你买的是白色还是黑色、是1.2T还是1.8L,那是另一层的事情。
放到电商场景里,一个SPU通常包含这些信息:
- 商品名称(比如"iPhone 15 Pro Max")
- 类目(手机)
- 品牌(Apple)
- 主图、详情页图文
- 售后服务模板
- 公共规格参数(屏幕尺寸、电池容量、支持的网络制式)
这些信息有什么特点?它们与具体的颜色、内存版本、套餐无关。用户在商品详情页看到的大部分介绍内容,本质上都是SPU层面的内容。评价体系也很有意思,很多平台的商品评价挂的是SPU维度,因为"iPhone 15 Pro Max"的体验是一致的,不会因为换一种颜色评价就完全不同。
2.2 SKU:库存保有单位,才是真正被买走的那个东西
SKU全称是Stock Keeping Unit,库存保有单位。这个词最早来自传统零售业的库存管理,到了电商里,它被定义为"最小可售存货单位"。
还是用汽车打比方,SKU不是车型,而是某一台具体配置的车:白色车身、1.8L排量、CVT变速箱、舒适版,这一台才能被开走。电商里SKU的意思完全一样:iPhone 15 Pro Max可能衍生出3种颜色乘以3种内存共9个SKU,每个SKU有自己独立的条码、价格、库存数量。
SKU才是交易的真正载体。用户在商详页点"加入购物车",加的是一个SKU;订单里记录的商品快照,本质上也是SKU层的信息;库存系统扣减的,是某一个SKU的库存,而不是整个SPU的库存。
所以SKU上一般会挂这些字段:
- 颜色、容量、套餐等具体规格值
- 独立价格(允许和SPU的起售价不同)
- 独立库存数量
- 独立条码(常用于对接仓储系统)
- 独立图片和重量体积信息(关系到运费计算)
在代码层面,如果看到一张商品相关表,凡是行记录可以精细到"某一具体颜色某一天库存多了5件"的,那它一定是SKU维度,不是SPU维度。
2.3 ID:不是与SPU/SKU并列的东西,而是它们的"身份标签"
标题把ID和SPU、SKU并列放在一起问,这个问法很常见,但ID和它们并不是同一个维度的概念。SPU和SKU回答的是"商品分哪几层",ID回答的却是"每一层的数据到底怎么唯一识别"。
简单说,ID是一个唯一编号。它可以是数据库主键,可以是一个全局唯一标识符,也可以是业务编码。关键是要理解ID有层级:SPU ID唯一标识一个SPU,SKU ID唯一标识一个SKU。它们之间不存在"ID包含SKU"或者"SKU包含ID"这种关系,ID更像身份证号码:一群人有共同的身份定义(比如"中国公民"),但每个人有自己的身份证号。
在实际系统里,ID承担这几类职责:
- 系统内部主键:数据库表的主键,常见是自增ID或者雪花ID
- 业务查询唯一标识:通过商品ID调详情接口、拉SKU库存、做价格变更
- 对外的平台标识:比如对接菜鸟仓需要传SKU ID,投放广告需要传商品ID
- 跨系统关联键:ERP、OMS、WMS、TMS之间通过ID完成数据流转
把ID单独拎出来强调,还有一个原因:很多人做系统设计的时候只想着"反正有个主键就行",忽略了ID的生成规则和业务语义。后面我会专门讲ID设计的坑,这里先记住一句话——SPU和SKU是分层模型,ID是给每一层做唯一标注的机制,两者配合使用才有意义。
3. 它们之间的区别和联系:一张对照表加四层关系
3.1 先看一张对照表
很多文章喜欢用标题党的方式讲区别,但真正整理清楚,还是表格最直接。做一个典型的电商商品模型对照:
| 对比维度 | SPU | SKU | ID |
|---|---|---|---|
| 全称 | Standard Product Unit | Stock Keeping Unit | Identifier |
| 中文含义 | 标准化产品单元 | 库存保有单位 | 唯一标识 |
| 层级 | 商品大类层 | 具体售卖层 | 贯穿所有层 |
| 典型例子 | iPhone 15 Pro Max | iPhone 15 Pro Max 256G 原色钛金属 | SPU ID: 100123 / SKU ID: 880001 |
| 核心作用 | 信息聚合、展示、评价归集 | 价格、库存、订单履约 | 唯一识别、关联、查询 |
| 能不能直接买 | 不能 | 能 | 视标识对象而定 |
| 一个概念能对应几条记录 | 一个SPU对应多条SKU | 一个SKU只能属于一个SPU | 每个实体各有一个,不可重复 |
这张表里最容易记混的点在于:"SPU不能直接买,SKU能直接买"。以后面试或者跟同事对齐的时候,只要记住这一条,层次基本就不会搞错。
3.2 四层关系拆开讲
第一层是包含关系。一个SPU下面挂一到多个SKU。最小的时候一个SPU只挂一个SKU(比如某些无规格商品),多的时候能挂几十个。SPU是父,SKU是子,这个方向不能搞反。
第二层是从属关系。SKU的规格值组合,决定了它归属于哪个SPU。反过来看,SPU存在的价值,是把那些"除了具体售卖属性外都一样"的SKU合并成一个商品入口,避免商详页出现几十条重复的标题。
第三层是职责关系。SPU管展示、管详情、管聚合;SKU管价格、管库存、管交易。同样一条商品数据,在导航分类页和搜索结果页使用SPU信息,在购物车和订单里使用SKU信息。如果职责没分清楚,就会出现我开头说的那种改价事故。
第四层是ID的纽带关系。SPU ID和SKU ID在数据库里扮演外键和业务键,把两层的关联显式表达出来。任何一条SKU数据都应该包含spu_id字段,即便它自身有独立的sku_id。也就是说ID不是独立的第三层,而是把SPU和SKU连接起来的那根线。
4. 落到数据库里是怎么建的:商品主数据的标准设计
概念说清楚以后,还得落在表结构上才能体现出价值。这一节给出一个常见的商品主数据设计,重点解释为什么这么建。
4.1 SPU表和SKU表分开建
最核心的一条原则:SPU信息放一张表,SKU信息放另一张表,通过spu_id关联,千万不要塞进一张表。原因有两点:
- 冗余严重。如果把SPU公共属性复制到每条SKU上,一个SPU挂20个SKU,标题、主图、详情这些大字段就要存20份,数据量大了存储开销和一致性问题都很头疼。
- 扩展性差。后续如果新增一个公共属性,得把历史上所有SKU行全部刷新一遍;分开建表只需要改SPU表的字段。
两张表的简化示例:
-- SPU表:存商品公共属性 CREATE TABLE spu ( spu_id BIGINT PRIMARY KEY, spu_code VARCHAR(64) NOT NULL, -- 业务编码,如 P100123 title VARCHAR(200) NOT NULL, category_id INT NOT NULL, brand_id INT NOT NULL, main_image VARCHAR(500), detail_json JSON, -- 详情页富文本或结构化素材 status TINYINT DEFAULT 1, created_time DATETIME, updated_time DATETIME ); -- SKU表:存具体售卖单元的个性化信息 CREATE TABLE sku ( sku_id BIGINT PRIMARY KEY, sku_code VARCHAR(64) NOT NULL, spu_id BIGINT NOT NULL, -- 从属关系 title VARCHAR(200) NOT NULL, -- 完整标题,含规格描述 spec_values JSON, -- {"颜色":"原色钛金属","内存":"256G"} price DECIMAL(10,2) NOT NULL, -- 销售单价 market_price DECIMAL(10,2), stock_qty INT NOT NULL DEFAULT 0, bar_code VARCHAR(128), image_url VARCHAR(500), status TINYINT DEFAULT 1, created_time DATETIME, updated_time DATETIME, KEY idx_spu_id (spu_id) );这里有一个实践中的细节:不要只建一个外键就算完,spu_id上一定要建普通索引(MySQL里如果建了外键约束,自动会有索引,但如果为了性能选择不建外键,索引要记得手动加上),否则按SPU查所有SKU的SQL会全表扫描。
4.2 规格值到底怎么存
常见的规格属性有颜色、尺码、版本、套餐、内存容量等。这些值在页面展示时需要逐个列出,在创建SKU时又需要做组合,所以设计上要区分"规格属性定义"和"规格值定义"。
简单做法是用三张表:
- 规格属性表:定义SPU有哪些规格维度,比如颜色、内存
- 属性值表:定义每个维度的可选值,比如颜色维度有黑色、白色、原色钛金属
- SKU与值的关系:通过JSON字段或者单独的关联表,记录每个SKU在各维度上的取值
对于中小型系统,直接在SKU表里放一个spec_values JSON字段已经足够。好处是创建SKU时可以把组合结果整串写入,查询时也方便直接返回给前端渲染。坏处是如果后续要做"按规格过滤"的复杂查询,SQL会比较绕,需要用到JSON函数,性能不如关系表稳定。电商平台体量大了以后,一般会拆成schema mapping表加宽表两套并存:一套负责事务写入,一套负责查询加速。
多规格SKU的生成算法比较简单,本质是多个维度可选值的笛卡尔积。比如颜色3种、内存3种,组合数就是3乘以3等于9个SKU。方案设计的时候要注意一点:不是所有组合都必须存在,比如某些颜色和内存的组合在生产端就不生产,所以生成时不能无条件全部创建,最好支持白名单过滤。
4.3 一张图式的数据流转(这里是文字说明)
从创建商品到真正上架,顺序大概是:先有SPU基础信息,再创建SPU下的规格属性,然后根据组合生成SKU,最后针对每个SKU填写价格和库存。
系统里做商品复制的功能也要特别小心。很多后台会提供"复制商品"能力,复制维度要区分好:是复制SPU结构还是复制SKU列表。如果只复制SPU,生成新SPU ID之后,SKU必须重新生成新的SKU ID,不能沿用旧SKU ID,否则会污染历史订单和报表的关联关系。这个细节我在好几个项目里都见过翻车。
5. 从创建商品到下单发货:三者在业务链路里各管一段
概念和表设计都清楚了,再看实际业务流转过程,会更容易理解为什么SPU和SKU必须分开。
用户打开一个商品列表页,列表卡片上显示的是SPU层的标题、主图、价格区间(比如"¥5999起")。点击进入商详页,看到了商品图文介绍,这也是SPU层的东西:所有颜色共用一个详细介绍页面。页面下方出现规格选择器,用户选了"原色钛金属"和"256G",此时页面展示的"¥9999"和"有货",才开始对应SKU层的数据。
加入购物车和提交订单时,订单行记录的核心商品字段就是SKU ID。这里有个常见误解:订单里到底存SPU ID还是SKU ID?答案是至少存SKU ID,最好SPU ID也冗余存一份。原因有两方面:
- 履约必须到SKU粒度。仓库拣货要按SKU找货,如果只存SPU ID,仓库不知道具体拣哪个颜色哪个容量。
- 冗余存储SPU ID是为了减少关联查询。订单量大以后,每查一次订单都要join商品表很重,把SPU ID冗余进去能少一次join。但注意,如果商品标题或价格发生过变化,订单里还需要快照字段,不能完全依赖冗余商品表里被后面改动过的数据。
售后阶段同样要看SKU。退货时退回的库存要加到对应SKU上,如果申请退的是SPU ID,系统完全不知道是该把库存退回黑色款还是白色款。所以售后单的SKU粒度判断,是电商系统里很基础又很严肃的校验点。
从这套链路可以总结出一个核心规律:SPU管"用户看到什么",SKU管"用户买到什么"。展示层数据挂SPU,交易和库存层数据挂SKU,两个维度通过spu_id-sku_id的归属关系串联。
6. ID怎么生成才靠谱:自增、雪花、UUID和平台映射的利弊
最后专门说一下ID相关的实战问题。前面讲了ID是身份标签,但"用ID做身份标签"这件事到了工程层面,远没有想象中简单。
6.1 自增ID:最快最简单,但别用错地方
MySQL自增主键使用方便、索引紧凑、写入性能好,看SQL执行计划也直观。但它有几个明显的适用边界:
- 一旦系统需要做水平分库分表,自增ID不能保证全局唯一,分表后的id会冲突。
- 外部相对容易根据ID的间隔估算业务量,有些公司会刻意避免这种信息泄露。
- 跨环境合并数据(比如测试环境导生产数据)时,自增ID会产生主键冲突。
我的建议是:单体应用、量级不大、无跨环境合并需求的内部后台表,用自增主键没有任何问题;但商品、订单这类会走向分库分表和外部交互的核心表,最好一开始就用全局ID方案,后面对接中台时能省掉大量迁移成本。
6.2 UUID:全局唯一但别傻傻拿它当主键
UUID字符串在分布式环境里生成简单、不用中心化发号器,听起来很省事。但它有两个让数据库难受的点:长度长(36个字符),并且无序。无序插入到B+树索引里会引起大量页分裂,写入性能会明显下降。如果坚持用UUID,常见做法是把它转成binary存储,或者只取其中一段做业务标识,数据库主键还是单独用自增或雪花ID。
6.3 雪花ID:兼顾全局唯一和趋势递增的常见选型
雪花ID(Snowflake)现在几乎是电商中后台系统的默认选择。它由时间戳、机器ID、序列号拼接而成,产生的是一个64位整数,既能全局唯一,又带上了一定的时间递增特性。落到MySQL里用BIGINT存,比字符串ID省空间,索引性能也不错。
但雪花ID不是完全没有坑。我见过最多的问题是机器ID分配混乱。有的团队直接把机器IP后面几位当workerId,部署环境一变就可能重复,导致生成的ID在同一毫秒内碰撞。另一个坑是时钟回拨。如果服务器NTP时间向回拨,同一个时间戳范围内序列号可能重复,严重点的系统会有专门的发号服务做兜底。
在中小团队没有自建发号服务的条件下,最靠谱的方案其实是让数据库生成一段序列号作为机器ID来源,再用代码里独立的ID生成组件配合缓存,尽量降低碰撞概率。不要在服务里写死写代码。
6.4 平台外部ID和内部ID的映射才是电商特有的坑
很多电商公司不是只做自营商城,还要把商品分发到多个销售渠道,每个渠道有一套自己的外部商品ID。这时候内部ID和外部ID的关系必须要有一张映射表,以内部SKU ID为主键,外部渠道标识加对方商品ID为唯一索引。
实际业务中最常见的情况是:渠道侧不允许直接用对方ID做主键,而是要求传你本系统的编码,然后渠道再建立自己的ID关联。这里一定要在映射表上维护中间状态字段,比如是否已同步、最后一次同步时间,防止每次渠道对接都在为ID不存在还是没同步而扯皮。
6.5 给ID定一套命名和映射规范
最后分享一个实际项目里的做法:无论内部系统还是外部对接,所有ID字段命名必须带上前缀语义。商品层用product_id,SPU层用spu_id,SKU层用sku_id,外部渠道则用out_product_id和out_sku_id。字段一旦出现"id"这种裸命名,后期排查数据问题基本全靠猜。
同时在代码里加一道校验逻辑:SPU层的接口如果收到sku_id,直接拒绝而不是忽略。我见过很多系统把这两个ID互相兼容,导致前端传错也没有人感知,最后库存和价格乱套了才回头查接口入参。严谨的入参校验不值多少钱,但能省掉多少个通宵排查的夜晚,这笔账怎么算都是划算的。
我在实际项目中还有一个一直保留的习惯:SKU表里加一个"规格摘要"文本字段,就是简单拼出"颜色_内存_套餐"这种字符串。它的存在让运营在排查库存时完全不需要去解析JSON或关联规格表,直接看值就能定位是哪一条记录。时间久了大家可能觉得它冗余,但遇到线上问题,所有人都会感谢这个字段,这是我的一个真实体会,分享给你参考。