简介:这是一份包含淘宝全类目、属性及属性值数据的SQL文件,面向需要处理电商商品结构化数据的数据库开发、数据分析或后端工程师,也适合用于类目树设计、商品筛选与统计分析等场景。压缩包仅1个文件,为单个SQL脚本,整体大小353KB,导入数据库后即可直接查询、关联和扩展,轻量实用。目前已有293人学习下载,可作为商品类目字典或初始化基础数据使用。借助该SQL可快速了解淘宝多层级的类目体系,理解属性与属性值如何关联商品,并在此基础上开展类目导航、商品推荐、市场分析或数据同步等开发工作;也可用于学习SQL表结构设计与复杂查询写法,帮助熟悉电商平台核心数据模型。文件体量精简、结构明确,能有效降低自行采集整理类目数据的成本,是一份实用型数据资产。 聊到“淘宝全类目加属性SQL”,不少做电商数据分析、商品管理后台、或者爬虫数据清洗的朋友应该都有共鸣——淘宝的类目体系庞大且层级复杂,每个类目下面又挂了不同的销售属性、关键属性,如果靠手工一条条去整理,基本是灾难。这个需求本质上不是“要不要用SQL”,而是“怎么把一套层级化、动态化的电商类目数据结构化地落进数据库”,并保证后续业务能高效查询和维护。
这篇文章我就围绕“如何设计并生成一份可用的淘宝全类目加属性SQL脚本”展开,把我在实际项目里踩过的坑、用过的方案、优化过的执行细节都写出来。无论是你只想把类目和属性导入 Mysql 做数据分析,还是想给运营后台做联动筛选,这篇内容都能直接落地参考。
1. 项目背景与核心需求拆解
1.1 这个需求到底在解决什么问题
先说清楚,这不是一个“从淘宝官方接口拉数据”的教程,因为官方开放平台的类目权限申请门槛不低,很多中小型团队根本拿不到全量类目数据。现实情况往往是:你手上有一份通过采集工具或历史项目积累下来的类目 JSON / Excel,里面有类目 ID、类目层级、父级 ID、属性名、属性值列表,但它是嵌套结构,没法直接用于数据库查询。
所以“淘宝全类目加属性 SQL”这句话,翻译成实际需求就是三件事:
- 把嵌套的类目树拍平,变成一张父子关系清晰的类目表。
- 把类目下挂的属性解析出来,拆成属性主表和属性值子表。
- 生成一套可重复执行的 SQL 脚本,建表、导数据、加索引一气呵成。
1.2 这套 SQL 能做哪些事、适合谁用
这套 SQL 一旦落地,直接能支撑的场景非常多。比如电商后台的商品发布页,需要根据前台类目联动展示对应的品牌、尺码、颜色等属性;再比如数据分析团队需要统计某个一级类目下所有叶子类目的商品数,如果没有一张完整的类目树表,这个统计基本没法写。
适合的人群大概是三类:
- 数据开发 / 后端工程师:需要把外部采集的类目数据快速入库,给业务方提供查询接口。
- 数据分析师:做类目维度拆解分析,需要一个稳定且更新的维度表。
- 爬虫方向的技术爱好者:采集到数据后不知道怎么标准化存储,可以参考这套表结构和 SQL 设计。
1.3 技术方案的选型思路
市面上有人直接用 JSON 字段把属性塞进一张表,这样做也不是不行,但查询属性时就要用 LIKE 去捞,数据量一大必然卡死。合理的方案是关系型建模 + 汇总冗余字段。
用 MySQL 作为载体,类目表、属性表、属性值表三张表分开存,这是比较通用的做法。类目表用自增主键和 parent_id 构建层级,属性表用类目 ID 做外键关联,属性值表再关联属性 ID。查询时如果需要一次性拿全路径,可以通过冗余一个category_path字段来避免递归查询。
2. 数据模型设计:类目树与属性的存储结构
2.1 类目表设计要点
淘宝类目体系最高有三级,部分类目甚至到第四级。建表时最核心的字段是parent_id和level。parent_id指向上级类目,顶级类目默认为 0;level从 1 开始计数。
我当时设计的类目表结构大致是这样:
CREATE TABLE `tb_category` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `cat_id` bigint(20) NOT NULL COMMENT '淘宝类目ID', `cat_name` varchar(100) NOT NULL COMMENT '类目名称', `parent_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '父级类目ID', `level` tinyint(4) NOT NULL DEFAULT '1' COMMENT '层级,1=一级类目', `is_leaf` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否叶子类目', `category_path` varchar(500) DEFAULT NULL COMMENT '冗余路径,如:女装/连衣裙/碎花裙', `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_cat_id` (`cat_id`), KEY `idx_parent_id` (`parent_id`), KEY `idx_level` (`level`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='淘宝类目表';这里有两个细节值得注意:
第一,cat_id必须加唯一索引。因为淘宝类目 ID 在源数据里就是唯一的,后续如果做增量更新,直接用INSERT ... ON DUPLICATE KEY UPDATE就能幂等执行。
第二,category_path字段是典型的空间换时间。虽然第三范式要求路径通过关联查询算出来,但实际业务中 90% 的场景都是直接展示全路径,与其每次递归查,不如在导入时就拼好。
2.2 属性表与属性值表的设计
属性这块比类目表稍微复杂一点。一个类目下面可能挂多个属性,一个属性又可能有多个属性值。比如“连衣裙”这个类目,属性有“尺码”、“颜色”、“裙长”,其中“尺码”的值可能是“S、M、L、XL”。这种一对多的嵌套结构,必须拆两张表。
属性主表:
CREATE TABLE `tb_category_attr` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `cat_id` bigint(20) NOT NULL COMMENT '类目ID', `attr_id` bigint(20) NOT NULL COMMENT '属性ID', `attr_name` varchar(100) NOT NULL COMMENT '属性名称', `attr_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '属性类型:1=关键属性,2=销售属性,3=非关键属性', `is_required` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否必填', `sort_order` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `uk_cat_attr` (`cat_id`, `attr_id`), KEY `idx_attr_name` (`attr_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='类目属性表';属性值表:
CREATE TABLE `tb_category_attr_value` ( `id` int(11) NOT NULL AUTO_INCREMENT, `attr_id` bigint(20) NOT NULL COMMENT '属性ID', `value_id` bigint(20) NOT NULL COMMENT '属性值ID', `value_name` varchar(200) NOT NULL COMMENT '属性值名称', `sort_order` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `uk_attr_value` (`attr_id`, `value_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='属性值表';之所以单独拆属性值表,是因为同一个属性在不同类目下会有不同的值集合。比如“尺码”在女装下有“S、M、L”,在童装下却有“80、90、100、110”。如果只用一个字段存值,后续做属性筛选时没法用索引。
2.3 为啥不直接用 JSON 字段一把梭
我知道很多人会图省事,把属性全部塞进一个 JSON 字段,或者用一个attr_name和attr_value的纵表存。这两种方案在数据量小的时候看着方便,但一旦涉及联合查询和统计就非常痛苦。
纵表虽然扩展性强,但同一行数据要取出多个属性时,就只能做大量的行转列操作,SQL 写起来又长又容易出错。JSON 字段的问题则是没法对内部属性建索引,一旦某个类目下商品属性需要作为筛选条件,查询性能会很差。
拆表的代价也不高,无非是导入时多两次INSERT,但查询和维护的体验提升是实打实的。
3. 数据采集与清洗:从原始数据到可用 SQL
3.1 源数据从哪来、长什么样
通常拿到的原始数据会有四种形态:
- 淘宝开放平台接口返回的 JSON(全量权限,一般团队拿不到)。
- 采集工具导出的类目页 HTML 或 JSON 文件。
- 第三方数据服务商提供的 Excel 表格。
- 网上各种开源项目里已经抓好的 SQL / CSV。
不管哪种形态,内部逻辑都是一致的:类目是树形结构,属性是挂在叶子类目或非叶子类目下的键值对。我以最常见的 JSON 为例说明处理思路,下面是简化后的结构:
[ { "cat_id": 16, "cat_name": "女装/女士精品", "parent_id": 0, "level": 1, "children": [ { "cat_id": 1621, "cat_name": "连衣裙", "parent_id": 16, "level": 2, "leaf": true, "attrs": [ { "attr_id": 122216001, "attr_name": "尺码", "attr_type": "销售属性", "is_required": true, "values": [ {"value_id": 1, "value_name": "S"}, {"value_id": 2, "value_name": "M"} ] } ] } ] } ]拿到这个 JSON 后,最关键的一步是把嵌套数据拍平。很多人会手动去重、手写 INSERT,结果数据一多就乱了。正确做法是先写一个简单的解析脚本,把类目、属性、属性值拆成三个文件或三张临时表,再统一导入。
3.2 清洗时的三个关键检查点
检查点一:类目 ID 重复。淘宝偶尔会出现同一个类目 ID 在文件里出现多次,但名称略有差异的情况。这种一般以最新出现的为准,或者以叶子类目优先级更高的那条为准。
检查点二:属性的父子继承。有些属性挂在二级类目上,但它的子类目实际上也继承了这些属性。导入时如果只处理叶子类目,会导致部分数据缺失。我的做法是:先导入所有类目,再为每个非叶子类目生成一张“继承关系临时表”,把父级属性和子级类目做笛卡尔积关联,再统一导入属性表。
检查点三:属性值去重。同一个 value_name 在不同属性下可能对应不同的 value_id。所以属性值表唯一索引必须建立在(attr_id, value_id)上,不能只对 value_id 建唯一。
3.3 脚本生成策略:直接生成 SQL 文件
清洗完成后,可以直接生成一份 .sql 文件,也可以生成 CSV 再用LOAD DATA导入。这里我更推荐直接生成 SQL 脚本,理由有两点:
- SQL 脚本自带建表语句,可以直接在目标环境跑,不需要额外准备导入工具。
- 只要加上
INSERT IGNORE或ON DUPLICATE KEY UPDATE,就能保证脚本可重复执行。
生成时注意控制单条 INSERT 的规模,比如每条 INSERT 包含 500 行数据,避免单条 SQL 过长导致max_allowed_packet报错。
4. SQL 脚本的完整实操:从建表到数据入库
4.1 建表脚本的执行顺序
如果你的环境还没有这三张表,建议按以下顺序执行:
tb_categorytb_category_attrtb_category_attr_value
执行顺序不能乱,因为属性表依赖类目表的cat_id,属性值表依赖属性表的attr_id。如果是重复执行,要么先DROP TABLE再重建,要么基于唯一索引做幂等插入。生产环境我一般用后者,因为直接 DROP 会丢失业务运行期间手工补充的数据。
-- 如果只是临时测试,可以这样清理重建 DROP TABLE IF EXISTS tb_category_attr_value; DROP TABLE IF EXISTS tb_category_attr; DROP TABLE IF EXISTS tb_category; SOURCE /path/to/schema.sql;4.2 批量导入套路示范
类目表批量导入的 SQL 长这样:
INSERT INTO `tb_category` (`cat_id`, `cat_name`, `parent_id`, `level`, `is_leaf`, `category_path`, `sort_order`) VALUES (16, '女装/女士精品', 0, 1, 0, '女装/女士精品', 1), (1621, '连衣裙', 16, 2, 1, '女装/女士精品/连衣裙', 2), ... ON DUPLICATE KEY UPDATE `cat_name` = VALUES(`cat_name`), `parent_id` = VALUES(`parent_id`), `level` = VALUES(`level`), `is_leaf` = VALUES(`is_leaf`), `category_path` = VALUES(`category_path`);属性表批量导入类似,只不过需要在ON DUPLICATE KEY UPDATE里更新属性名称和是否必填字段。属性值表更新频次较低,但一旦源数据修正,同样用UPDATE覆盖。
这里有个优化技巧:如果是千万级以上的数据量,先用ALTER TABLE ... DISABLE KEYS关闭非唯一索引,导入完成后再ENABLE KEYS,速度能快一个量级。唯一索引不受这个开关控制,所以不用担心重复数据混入。
4.3 查询实践:如何用一条 SQL 拿到完整属性列表
这是很多朋友实际最关心的一点。属性表拆开后,要一次性查出某个类目下挂的所有属性和值,用 LEFT JOIN 就能搞定:
SELECT c.cat_id, c.cat_name, a.attr_id, a.attr_name, a.attr_type, a.is_required, v.value_id, v.value_name FROM tb_category c LEFT JOIN tb_category_attr a ON c.cat_id = a.cat_id LEFT JOIN tb_category_attr_value v ON a.attr_id = v.attr_id WHERE c.cat_id = 1621 ORDER BY a.sort_order, v.sort_order;如果还想同时把父级类目的属性也带出来,就在 WHERE 条件里加上c.category_path LIKE CONCAT('%', '女装', '%'),或者用c.parent_id IN (...)把父子类目一起捞出来,再去重展示。
4.4 WITH RECURSIVE 实现任意层级子类目查询
MySQL 8.0 以上支持递归 CTE,这个功能用来查类目树非常合适。比如我要查“女装”下所有层级的叶子类目 ID,可以直接写:
WITH RECURSIVE cat_tree AS ( SELECT cat_id, cat_name, parent_id, level FROM tb_category WHERE cat_id = 16 UNION ALL SELECT c.cat_id, c.cat_name, c.parent_id, c.level FROM tb_category c INNER JOIN cat_tree ct ON c.parent_id = ct.cat_id ) SELECT cat_id, cat_name FROM cat_tree WHERE is_leaf = 1;这套写法在老版本的 MySQL 里只能用多次 JOIN 或临时表替代,但不管是哪种方式,核心思路都是利用 parent_id 顺着树往下走。
5. 常见问题与排查技巧实录
5.1 中文乱码
这是最常踩的坑。导入后发现表中中文全是问号,99% 的原因是连接字符集和表字符集不一致。建表时已经用了 utf8mb4,但客户端连接时如果设的是 latin1,照样会乱。
解决办法是在执行 SQL 文件前先执行:
SET NAMES utf8mb4;在命令行下导入时,还可以显式指定:
mysql -uroot -p --default-character-set=utf8mb4 ecommerce < tb_category.sql这个细节看着小,但一旦数据灌进去了再返工,清洗脚本要重跑一遍,浪费半个小时都是少的。
5.2 数据量太大导致导入中断
类目表可能几千行,属性值表则是几十万行起步。如果一次性执行一个巨大的 SQL 文件,很容易卡死或报错。解决思路有三种:
- 脚本里批量 INSERT,每条 500 行左右,避免单条 SQL 过长。
- 用
mysql客户端的source命令取代在 Navicat 里复制粘贴执行。 - 分文件导入,比如类目一个文件、属性一个文件、属性值一个文件,哪个环节失败就重跑哪个。
另外别忘了调大max_allowed_packet,否则稍微大一点的批量 INSERT 直接报 “packet too large”。
5.3 属性表数据重复
重复来源主要是源数据里同一个属性在多个类目下重复出现,且属性 ID 一样。此时如果唯一索引没建好,很容易插两遍。解决方式:
- 唯一索引必须落在
(cat_id, attr_id),这样即使源数据重复,MySQL 也会静默拒绝。 - 用
INSERT IGNORE代替普通INSERT,可以明确跳过已有数据。 - 如果是数据修复场景,用临时表导入新数据后再
UPDATE JOIN更新正式表。
5.4 如何做增量更新
类目和属性不是一成不变的,淘宝每年都会调整几次。增量更新方案建议用“全量比对 + 增量写入”:
- 把最新采集的数据导到临时表。
- 用
LEFT JOIN找出正式表不存在的 cat_id / attr_id / value_id,作为新增写入。 - 对于已存在的记录,只更新
cat_name、attr_name、sort_order等可变字段。
这样既保证了数据最新,又不会误删业务侧已经配置的扩展数据。
5.5 排查 SQL 慢查询的定位思路
如果你发现按属性筛选商品时很慢,先不要急着加索引,用EXPLAIN看一下执行计划:
EXPLAIN SELECT * FROM tb_category_attr_value WHERE attr_id = 122216001;正常情况下应该走uk_attr_value唯一索引,Extra 列出现Using index condition说明索引生效。如果出现Using where; Using filesort,就要考虑是否排序字段没有索引,或者类型转换导致索引失效。
属性值表的value_name如果需要模糊查询,建议加普通索引;但如果查询模式是LIKE '%S%',普通索引也帮不上忙,这种场景就该考虑接入 Elasticsearch 或其他的搜索组件了。
这套类目加属性的 SQL 设计,我在两个电商项目里完整用过一遍。坦白说,第一次做的时候只在类目表上建了唯一索引,属性值表漏了(attr_id, value_id)联合唯一索引,结果就是同样的数据出现在多个类目下,后续做商品发布筛选项时总是多出重复值。后来重新清洗数据、补上索引才逐步稳定下来。所以如果你准备按这个方案落地,强烈建议在建表阶段就把唯一索引和字符集都定好,后面会省掉大量返工的麻烦。
如果你手头的数据量不大,也可以先用这套表结构顶着,后续等采集源稳定了再做增量同步。总之,类目数据这种“看起来简单、做起来繁琐”的东西,最重要的就是从一开始就得把结构设计对,尤其是索引和层级关系这两块,千万别图省事塞 JSON 了。
本文还有配套的精品资源,点击获取