1. 商品规格与销售属性到底在解决什么问题做电商系统的人绕不开一个坎同一个商品为什么能变出几十上百种卖法。一件T恤有S/M/L/XL四个尺码每个尺码又有黑/白/灰三种颜色光这两组就十二个组合每个组合还可能对应不同的库存、不同的价格、不同的图片。如果再加上“加绒版”“薄款”这种版本差异SKU数量直接爆炸。很多刚入行的开发者第一反应是给商品表加字段color、size、version一路加下去加到第十个字段的时候发现查询语句已经没法看了更别提运营那边还要随时新增一个“材质”维度。这就是商品规格与销售属性这套设计要解决的核心矛盾用一套可扩展的数据结构把“商品是什么”和“商品怎么卖”这两件事彻底分开。规格是描述商品本身的客观属性比如颜色、尺码、内存容量销售属性是决定这笔交易具体形态的参数比如你选的是“黑色XL”这个组合它对应一个独立的SKU有自己的价格和库存。把这两层拆清楚前端才能动态渲染选择器后端才能精准扣减库存运营才能灵活配置促销。这套设计适合谁看如果你正在从零搭一个电商系统或者接手了一个商品表字段已经加到失控的老系统又或者你是个前端想搞明白为什么后端返回的SKU列表长那样那这篇内容就是写给你的。我会从表结构设计一路讲到实际查询和常见坑尽量把每个决策背后的“为什么”说透而不是甩几张ER图就完事。2. 核心概念拆解SPU、SKU、规格、销售属性到底怎么分2.1 用生活场景理解SPU和SKU先把这个最容易混淆的概念理清楚。SPUStandard Product Unit是标准产品单元它描述的是“这是一款什么商品”比如“某品牌2024春季圆领纯棉T恤”。SKUStock Keeping Unit是库存量单位它描述的是“这个商品的具体哪一个可售版本”比如“某品牌2024春季圆领纯棉T恤 黑色 XL码”。打个比方SPU就像你去餐厅看菜单上面写着“宫保鸡丁”这道菜SKU就是你点单时说“宫保鸡丁微辣不要花生”。菜单上只有一道宫保鸡丁但后厨能做出很多种组合每种组合的备料和出餐流程可能都不一样。电商系统里SPU负责展示和搜索SKU负责交易和库存。很多新手会问为什么不直接只用SKU因为如果只有SKU你搜索“T恤”的时候会出来几百条几乎一样的记录用户根本没法浏览。SPU把相同款式的不同版本聚合在一起用户先看到一款商品再在里面选具体规格体验才顺畅。2.2 规格与销售属性的边界在哪里这两个词经常被混用但在数据库设计里必须分开。规格Specification是商品的固有属性它不随交易变化比如一件衣服的材质是纯棉这是客观事实。销售属性Sales Attribute是用户下单时必须选择的参数它直接决定SKU比如颜色和尺码。但边界不是绝对的。同样是“颜色”在服装类目里它是销售属性因为用户必须选一个颜色才能下单但在某些标准件类目里颜色可能只是描述性规格不影响价格和库存。所以设计时不能写死要把“哪些属性参与SKU生成”做成可配置的。我见过一个反面案例某系统把“重量”做成了销售属性结果用户买个东西还要选“500g版”还是“1000g版”其实重量只是物流计算用的规格参数。这种设计失误的根源就是没想清楚“用户下单时是否需要主动选择”这个判断标准。2.3 属性、属性值、SKU组合的三层关系完整的结构是三层属性定义层有哪些属性比如颜色、尺码、属性值层每个属性有哪些可选值比如颜色有黑/白/灰、SKU组合层具体哪几个属性值组合成一个可售单元。这三层拆开的好处是运营可以随时加属性值。比如某天要新增一个“藏青色”只需要在属性值表里插一条记录然后生成对应的SKU组合不需要改表结构。如果当初把颜色写死在商品表的字段里加一个颜色就得改表、改代码、改前端成本高得离谱。3. 数据库表结构设计从字段到索引的完整方案3.1 核心表清单与职责划分一套能跑起来的商品规格系统至少需要这几张表表名职责关键字段商品SPU表存储商品主体信息spu_id, title, category_id, brand_id属性定义表定义有哪些属性attr_id, attr_name, is_sales_attr属性值表定义每个属性的可选值value_id, attr_id, value_nameSPU属性关联表记录SPU用了哪些属性spu_id, attr_idSKU表存储可售单元sku_id, spu_id, price, stockSKU属性值关联表记录SKU对应哪些属性值sku_id, attr_id, value_id这里有个设计决策值得展开为什么SKU属性值关联表要存attr_id而不只存value_id。因为value_id本身已经能推出attr_id但存冗余的attr_id可以让查询少一次join。在SKU数量大的时候这个冗余能明显提升筛选性能。代价是要保证数据一致性插入时校验value_id确实属于attr_id。3.2 属性定义表的关键字段设计属性定义表看起来简单但有几个字段决定了系统的灵活性CREATE TABLE product_attribute ( attr_id BIGINT PRIMARY KEY AUTO_INCREMENT, attr_name VARCHAR(50) NOT NULL COMMENT 属性名如颜色, category_id BIGINT NOT NULL COMMENT 所属类目, is_sales_attr TINYINT DEFAULT 0 COMMENT 是否销售属性 1是 0否, is_required TINYINT DEFAULT 0 COMMENT 是否必填, input_type TINYINT DEFAULT 1 COMMENT 输入方式 1单选 2多选 3文本, sort_order INT DEFAULT 0 COMMENT 排序, status TINYINT DEFAULT 1 COMMENT 状态 1启用 0禁用 );is_sales_attr这个字段是整个设计的开关。当它为1时这个属性的值会参与SKU组合生成为0时它只是描述性规格不影响SKU。input_type决定了前端渲染成单选按钮、复选框还是输入框。category_id让不同类目有不同的属性集服装类目有尺码手机类目有内存容量互不干扰。注意属性名在同一类目下应该唯一但不同类目可以重名。比如“颜色”在服装和手机类目下都存在但它们的属性值完全不同。所以唯一索引要建在(category_id, attr_name)上而不是单独attr_name。3.3 SKU表与库存价格的关系SKU表是交易的核心价格和库存都挂在这一层CREATE TABLE product_sku ( sku_id BIGINT PRIMARY KEY AUTO_INCREMENT, spu_id BIGINT NOT NULL, sku_code VARCHAR(64) UNIQUE COMMENT 商家编码, price DECIMAL(10,2) NOT NULL COMMENT 售价, original_price DECIMAL(10,2) COMMENT 划线价, stock INT NOT NULL DEFAULT 0 COMMENT 可售库存, locked_stock INT NOT NULL DEFAULT 0 COMMENT 锁定库存, image_url VARCHAR(255) COMMENT SKU主图, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, INDEX idx_spu (spu_id) );这里要解释两个设计点。第一为什么库存要分可售和锁定两个字段。用户下单后库存不能立刻扣减因为可能超时未支付。常见做法是下单时把可售库存转到锁定库存支付成功后锁定库存真正扣掉超时未支付则把锁定库存还回可售。这样能避免超卖也能处理未支付订单。第二价格为什么放在SKU而不是SPU。因为不同规格的成本和定价策略不同XL码可能比S码贵加绒版比薄款贵。如果价格放在SPU层就没法做差异化定价。当然如果业务上所有SKU同价可以在SPU层放一个默认价格SKU层价格为空时取默认值这是常见的优化。3.4 索引设计与查询性能考量商品列表页和详情页的查询模式完全不同索引要分别优化。列表页通常按类目、品牌、价格区间筛选所以SPU表上要有(category_id, status)的联合索引。详情页要根据spu_id查所有SKUSKU表的idx_spu索引就是干这个的。SKU属性值关联表的索引更讲究CREATE TABLE sku_attribute_value ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id BIGINT NOT NULL, attr_id BIGINT NOT NULL, value_id BIGINT NOT NULL, UNIQUE KEY uk_sku_attr (sku_id, attr_id), INDEX idx_value (value_id) );uk_sku_attr保证一个SKU在一个属性下只能有一个值比如一个SKU不能同时是黑色又是白色。idx_value用于反向查询比如“找出所有颜色为黑色的SKU”这在筛选场景下很有用。4. SKU组合生成与前端交互的实操细节4.1 笛卡尔积生成SKU的正确姿势当运营在后台勾选了颜色黑/白/灰和尺码S/M/L系统需要自动生成3×39个SKU。这个过程叫笛卡尔积展开。实现时要注意几个细节第一不是所有组合都有效。比如灰色可能没有S码运营需要能手动删除无效组合。所以生成后要提供一个可编辑的SKU列表而不是直接落库。第二生成时要保留已有SKU的数据。如果运营只是新增了一个颜色不应该把原来的价格和库存清空。常见做法是以属性值组合为key做diff只新增缺失的组合保留已存在的。第三SKU编码的生成规则。可以用spu_id加属性值id拼接比如“1001-201-301”保证唯一且可读。但要注意属性值id可能很长拼接后超过字段长度限制所以要么用哈希要么用自增序号。4.2 前端规格选择器的联动逻辑用户在详情页选规格时前端需要做可选性判断。比如用户选了黑色那么尺码里只有有库存的才应该可点没库存的置灰。这个判断依赖后端返回的SKU列表。后端返回的数据结构通常是这样的{ spu_id: 1001, attributes: [ {attr_id: 1, attr_name: 颜色, values: [{value_id: 201, value_name: 黑色}, ...]}, {attr_id: 2, attr_name: 尺码, values: [{value_id: 301, value_name: S}, ...]} ], skus: [ {sku_id: 5001, price: 99, stock: 10, attrs: {1: 201, 2: 301}}, ... ] }前端拿到后每次用户选择变化就遍历skus找出匹配当前已选属性的组合判断剩余属性的哪些值还有对应SKU。这个逻辑用JavaScript实现大概是这样function getAvailableValues(selected, skus, attrId) { return skus.filter(sku { // 已选属性必须全部匹配 for (let [aid, vid] of Object.entries(selected)) { if (sku.attrs[aid] ! vid) return false; } return sku.stock 0; }).map(sku sku.attrs[attrId]); }提示这个计算在前端做还是后端做取决于SKU数量。如果SKU超过200个建议后端提供一个“可选值查询”接口前端每次选择后请求一次避免前端遍历大数组造成卡顿。4.3 库存扣减与超卖防护库存扣减是电商系统最容易出事故的地方。我踩过的坑是用“查询库存-判断足够-更新库存”这种三步操作在高并发下必然超卖。正确做法是用数据库的原子操作UPDATE product_sku SET stock stock - 1, locked_stock locked_stock 1 WHERE sku_id ? AND stock 1;然后判断affected rows是否为1为0说明库存不足。这条语句把判断和扣减合并成一个原子操作依赖数据库的行锁保证并发安全。如果系统规模更大数据库行锁成为瓶颈可以引入Redis做库存预扣。但要注意Redis和数据库的一致性问题常见方案是Redis扣减成功后发消息异步落库同时用定时任务对账补偿。这个方案复杂度高中小系统用数据库原子操作就够了。5. 常见问题与排查技巧实录5.1 SKU组合爆炸怎么处理一个商品如果有5个销售属性每个属性5个值笛卡尔积就是3125个SKU。这在前端渲染和后端存储上都是灾难。实际业务中大部分组合是无效的真正在售的可能只有几十个。处理思路有三个。第一限制销售属性数量一般不超过3个超过3个的建议拆成多个SPU。第二懒生成运营只维护有效组合系统不主动展开全部笛卡尔积。第三分页返回SKU详情页只返回有库存的SKU无库存的不返回给前端。5.2 属性值删除后的数据一致性运营删除了一个属性值比如“藏青色”但已经有SKU关联了这个值。直接删除会导致SKU属性值关联表出现孤儿记录。正确做法是软删除把属性值状态置为禁用同时下架关联的SKU。如果确实要物理删除必须先检查是否有SKU引用有引用则拒绝删除。5.3 商品详情页SKU价格显示错误常见原因是前端取价格时取了SPU的默认价格而不是当前选中SKU的价格。排查时先确认后端返回的skus数组里每个SKU的price是否正确再检查前端选中逻辑是否根据用户选择匹配到了正确的SKU。另一个坑是价格精度问题JavaScript的浮点数运算会导致99.9显示成99.899999所以价格在前后端传输时建议用整数分展示时再除以100。5.4 常见问题速查表问题现象可能原因排查方向规格选择器全部置灰SKU列表为空或stock都为0检查SKU是否上架、库存是否大于0选中规格后价格不更新前端未监听选择变化检查选中事件是否触发SKU匹配下单提示库存不足但页面显示有货库存缓存未同步检查Redis与数据库库存是否一致新增属性值后SKU未生成未触发组合生成逻辑检查后台保存时是否调用生成方法SKU编码重复生成规则冲突检查编码拼接逻辑是否包含唯一标识6. 从设计到落地的经验总结这套商品规格与销售属性的设计核心思想就一句话把变化的维度抽出来做成数据而不是做成字段。属性定义表、属性值表、SKU组合表这三层结构让系统能适应任意类目的商品形态运营加一个规格维度不需要开发介入。我在实际项目里最大的体会是前期多花两天把表结构设计清楚后期能省两个月改bug的时间。尤其是SKU属性值关联表一定要存attr_id冗余一定要加唯一索引一定要做软删除。这三个“一定”是用血泪换来的。另外前端规格选择器的联动逻辑建议封装成独立组件把SKU匹配算法抽成纯函数方便单元测试。我见过太多项目把这段逻辑散落在各个页面里改一处漏一处最后没人敢动。最后分享一个小技巧在SKU表里加一个attrs_hash字段把属性值id排序后拼接做哈希。这样判断两个SKU是否重复时直接比哈希值就行不用逐条比对属性值。插入前查一下哈希是否存在能有效防止运营重复生成SKU。这个字段在数据迁移和去重时特别好用成本只是一个额外的索引。