據(jù)模型到冪等腳本的實踐)
簡介涵蓋淘寶全類目、屬性及屬性值數(shù)據(jù)的SQL文件適合電商數(shù)據(jù)分析師、后端開發(fā)人員以及需要研究商品結(jié)構(gòu)的學(xué)習(xí)者。資源以標(biāo)準(zhǔn)SQL語句組織可導(dǎo)入數(shù)據(jù)庫用于類目樹查詢、屬性篩選、商品信息關(guān)聯(lián)等場景能夠快速構(gòu)建電商基礎(chǔ)數(shù)據(jù)表減少手工整理成本。壓縮包為zip格式整體353KB包含1個sql文件結(jié)構(gòu)緊湊導(dǎo)入與遷移較為方便。數(shù)據(jù)覆蓋淘寶完整分類層級以及不同類目下的屬性和可選值開發(fā)者可基于這些信息進行商品類目導(dǎo)航、屬性篩選、市場分析或推薦系統(tǒng)標(biāo)簽設(shè)計等二次開發(fā)。雖然不包含實時更新機制但作為靜態(tài)全量數(shù)據(jù)對于理解淘寶商品結(jié)構(gòu)或進行離線分析依然實用。目前已有293人學(xué)習(xí)下載適合作為電商數(shù)據(jù)集查詢與SQL實踐的基礎(chǔ)素材尤其適合需要快速獲取類目屬性字典的初學(xué)者與項目團隊。 前陣子在做一個電商數(shù)據(jù)中臺項目被分到一個很基礎(chǔ)但又很磨人的任務(wù)把淘寶全類目和對應(yīng)的商品屬性初始化到本地數(shù)據(jù)庫還要支持后續(xù)批量加屬性。這套東西業(yè)務(wù)上就叫“淘寶全類目加屬性SQL”說白了就是把淘寶那棵龐大的類目樹、屬性字典、類目與屬性的關(guān)聯(lián)關(guān)系用一套可重復(fù)執(zhí)行的SQL腳本管起來。做完之后我最大的感受是這個需求真正難的不是某個SQL有多復(fù)雜而是數(shù)據(jù)模型設(shè)計、寫腳本的冪等性、以及上線后維護的可控性。如果你準(zhǔn)備處理類似電商類目/屬性數(shù)據(jù)或者想把接口拉下來的數(shù)據(jù)同步成結(jié)構(gòu)化表這篇內(nèi)容應(yīng)該能幫你省不少時間。1. 項目背景與核心需求拆解1.1 這到底是什么需求想象這樣一個場景你的后臺商品類目來自淘寶開放平臺一個根類目下面套了好多層子類目葉子類目可能有幾千個每個葉子類目又綁定著不同屬性比如“手機”類目有“品牌”“型號”“運行內(nèi)存”而“連衣裙”類目有“裙長”“風(fēng)格”“適用季節(jié)”。如果只把基礎(chǔ)信息入庫后面想在所有葉子類目下統(tǒng)一追加一個“是否包郵”或“上市年份”的公共屬性挨個類目操作是不可能的。于是就有了“全類目加屬性”的需求用一批SQL腳本把屬性一次性掛到所有符合條件的類目上。其實就是把重復(fù)的人工操作變成可控的數(shù)據(jù)腳本把“按類目加屬性”的復(fù)雜性交給表關(guān)系和數(shù)據(jù)運算去解決。很多剛接觸這個場景的人會誤以為“全類目”就是所有類目都一樣直接給類目表加一個字段就完事。實際不是這樣。淘寶類目是帶層級的多對多關(guān)系一個屬性可以掛在多個類目下一個類目也可以擁有多個屬性所以需要單獨維護“類目ID—屬性ID”的關(guān)聯(lián)關(guān)系。這個關(guān)系一旦建好后續(xù)不管加屬性、改屬性、查屬性都是在關(guān)聯(lián)表上操作不會污染類目主數(shù)據(jù)。1.2 為什么不用程序代碼寫這些邏輯很多團隊遇到這種情況第一反應(yīng)是寫一段Java/Python腳本for循環(huán)遍歷類目逐個調(diào)用接口或執(zhí)行SQL。我一開始也想這么干后來發(fā)現(xiàn)兩個問題一是類目和屬性數(shù)據(jù)強依賴數(shù)據(jù)庫的關(guān)聯(lián)關(guān)系程序里處理還要頻繁查庫開發(fā)效率和執(zhí)行效率都很低二是這種一次性初始化任務(wù)后續(xù)上線到不同的環(huán)境比如測試庫、預(yù)發(fā)庫、生產(chǎn)庫如果用代碼腳本環(huán)境遷移成本很高。換成純SQL腳本之后只要目標(biāo)庫結(jié)構(gòu)一致直接執(zhí)行一遍就完事而且還方便做版本管理出了問題還能在命令行里快速定位。當(dāng)然SQL方案也有自己的邊界。如果類目數(shù)據(jù)量特別大比如上百萬的SKU級屬性純SQL可能跑不動需要配合任務(wù)調(diào)度和數(shù)據(jù)同步工具。但對淘寶前臺類目這種量級幾千個類目、幾萬個屬性值MySQL完全能扛住SQL是性價比最高的選擇。2. 數(shù)據(jù)表結(jié)構(gòu)與關(guān)鍵字段解析2.1 類目表用parent_id存一棵可擴展的樹類目數(shù)據(jù)天然是樹形結(jié)構(gòu)我建議直接用平臺類目ID做主鍵而不是自增ID。因為后續(xù)從開放平臺同步數(shù)據(jù)時類目ID本身不會變?nèi)绻约涸僭煲惶譏D還要額外維護一個映射字段反而麻煩。具體表結(jié)構(gòu)可以是這樣CREATE TABLE category ( id bigint NOT NULL COMMENT 類目ID通常直接用平臺類目ID, parent_id bigint NOT NULL DEFAULT 0 COMMENT 父類目ID0表示根級, name varchar(64) NOT NULL COMMENT 類目名稱, level tinyint NOT NULL DEFAULT 1 COMMENT 層級根級為1, is_leaf tinyint NOT NULL DEFAULT 0 COMMENT 是否葉子類目1是0否, status tinyint NOT NULL DEFAULT 1 COMMENT 狀態(tài)1啟用0停用, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_parent (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT淘寶類目表;這里我用parent_id而不是左右值編碼是因為電商類目樹層級通常比較淺查詢某個父類目下的所有葉子類目用一次遞歸CTE就夠不需要維護復(fù)雜的左右值運算。只要層級不超過四五個遞歸的效率完全能接受。2.2 屬性表拆成屬性與屬性值兩張表如果屬性值直接塞在一個字段里后面做篩選和關(guān)聯(lián)會非常痛苦所以我把屬性拆成attribute和attribute_value兩張表CREATE TABLE attribute ( id bigint NOT NULL AUTO_INCREMENT, attr_name varchar(64) NOT NULL, attr_key varchar(64) NOT NULL COMMENT 屬性標(biāo)識如brand, is_sale tinyint NOT NULL DEFAULT 1 COMMENT 是否銷售屬性, is_key tinyint NOT NULL DEFAULT 0 COMMENT 是否關(guān)鍵屬性, status tinyint NOT NULL DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY uk_attr_key (attr_key) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品屬性字典; CREATE TABLE attribute_value ( id bigint NOT NULL AUTO_INCREMENT, attribute_id bigint NOT NULL, value_name varchar(64) NOT NULL, value_code varchar(64) DEFAULT NULL, sort int NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_attribute_id (attribute_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT屬性值表;把屬性值單獨拆出來是因為一個屬性下面往往有幾十個值比如“顏色”有紅黃藍綠“尺碼”有S、M、L、XL。屬性表只負責(zé)記錄屬性本身的元信息屬性值表負責(zé)維護可選值兩者通過attribute_id關(guān)聯(lián)。value_code字段用來存平臺屬性值ID這個字段不一定每個屬性都有可以留空。2.3 類目屬性關(guān)聯(lián)表中間表不只是兩個ID類目和屬性是典型的多對多關(guān)系所以必須有一張中間表。很多人建中間表只放兩個ID實際上業(yè)務(wù)需求往往更復(fù)雜比如同一個類目下某些屬性是必填某些屬性可選某些屬性排序靠前某些靠后。這些信息都應(yīng)該放在關(guān)聯(lián)表里CREATE TABLE category_attribute ( id bigint NOT NULL AUTO_INCREMENT, category_id bigint NOT NULL, attribute_id bigint NOT NULL, required tinyint NOT NULL DEFAULT 0 COMMENT 是否必填屬性, sort int NOT NULL DEFAULT 0 COMMENT 排序值, source tinyint NOT NULL DEFAULT 1 COMMENT 來源1手動2繼承, PRIMARY KEY (id), UNIQUE KEY uk_category_attribute (category_id, attribute_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT類目屬性關(guān)聯(lián)表;required字段直接決定了商品發(fā)布時這個屬性是否強制填寫sort字段決定前臺展示順序source字段用來區(qū)分這條關(guān)聯(lián)是人工配置的還是從父類目繼承下來的。這里一定要加唯一鍵uk_category_attribute否則同一對類目和屬性被重復(fù)插入后后續(xù)查詢和統(tǒng)計都會出問題。3. 核心SQL腳本全類目加屬性的落地實現(xiàn)3.1 初始化腳本從接口數(shù)據(jù)到正式表從開放平臺拿到的類目和屬性數(shù)據(jù)一般是JSON數(shù)組或臨時表。我習(xí)慣先建一張臨時清洗表把接口數(shù)據(jù)原樣導(dǎo)入再通過INSERT SELECT灌入正式表這樣可以在中間層做數(shù)據(jù)校驗和去重。全量初始化的腳本類似這樣SET FOREIGN_KEY_CHECKS 0; TRUNCATE TABLE category_attribute; TRUNCATE TABLE attribute_value; TRUNCATE TABLE attribute; TRUNCATE TABLE category; INSERT INTO category (id, parent_id, name, level, is_leaf) SELECT id, parent_id, name, level, is_leaf FROM tmp_category WHERE status 1; SET FOREIGN_KEY_CHECKS 1;這里用TRUNCATE而不是DELETE是因為全量初始化時可以接受清空重建而且TRUNCATE會重置自增ID速度更快。但如果業(yè)務(wù)上有增量同步就絕對不能TRUNCATE要用INSERT ... ON DUPLICATE KEY UPDATE做冪等更新。3.2 全類目批量加屬性的核心SQL這是標(biāo)題里最關(guān)鍵的“加屬性”。假設(shè)要給所有葉子類目統(tǒng)一添加一個“上市年份”屬性屬性ID是10086執(zhí)行下面這條SQL就夠了INSERT IGNORE INTO category_attribute (category_id, attribute_id, required, sort) SELECT c.id, 10086, 0, 0 FROM category c WHERE c.is_leaf 1;這條SQL的原理很簡單先從category表里查出所有葉子類目的ID再把這些ID和屬性ID 10086組成關(guān)聯(lián)記錄批量插入category_attribute表。因為關(guān)聯(lián)表上有唯一鍵uk_category_attributeINSERT IGNORE會跳過已經(jīng)存在的重復(fù)記錄所以這條SQL跑兩遍、三遍都不會產(chǎn)生臟數(shù)據(jù)。如果你希望重復(fù)執(zhí)行時更新sort或required就把INSERT IGNORE改成ON DUPLICATE KEY UPDATE sort VALUES(sort)。如果“全類目”的范圍只限定某個根類目下的葉子類目可以加過濾條件比如只看“手機”類目INSERT IGNORE INTO category_attribute (category_id, attribute_id, required, sort) SELECT c.id, 10086, 0, 0 FROM category c WHERE c.is_leaf 1 AND EXISTS ( SELECT 1 FROM category p WHERE p.id c.parent_id AND p.name 手機 );這種寫法雖然比普通IN子查詢可讀性好一點但性能一般。如果類目表數(shù)據(jù)量不大完全沒有問題如果數(shù)據(jù)量很大更建議先查出類目ID集合存在臨時表里再和臨時表做JOIN。3.3 屬性去重與冪等更新接口導(dǎo)入的數(shù)據(jù)經(jīng)常會有重復(fù)比如同一個“品牌”屬性在臨時表里出現(xiàn)了兩次如果直接灌入正式表會導(dǎo)致后續(xù)關(guān)聯(lián)混亂。先用這個SQL排查重復(fù)SELECT attr_name, attr_key, COUNT(*) FROM tmp_attribute GROUP BY attr_name, attr_key HAVING COUNT(*) 1;發(fā)現(xiàn)重復(fù)后保留最小ID刪除其他行DELETE a FROM tmp_attribute a JOIN ( SELECT MIN(id) AS keep_id, attr_name, attr_key FROM tmp_attribute GROUP BY attr_name, attr_key HAVING COUNT(*) 1 ) k ON a.attr_name k.attr_name AND a.attr_key k.attr_key AND a.id k.keep_id;這個DELETE JOIN是MySQL的寫法其他數(shù)據(jù)庫可能需要調(diào)整語法。去重之后再執(zhí)行正式的INSERT并且在正式表的attr_key字段上加唯一索引從根源上防止重復(fù)數(shù)據(jù)再次寫入。冪等更新的核心思路就是“唯一鍵 INSERT IGNORE/ON DUPLICATE KEY UPDATE”這條經(jīng)驗特別重要任何初始化類SQL腳本都應(yīng)該默認具備冪等性。3.4 動態(tài)SQL按條件篩選類目追加屬性有些需求會更復(fù)雜比如把所有類目名里包含“女裝”的葉子類目都加上“尺碼”屬性。如果一個個查出來再拼SQL很容易出錯還會埋下安全隱患。我實際落地時用的是存儲過程加游標(biāo)雖然有點重但邏輯清晰參數(shù)化也能做得很干凈CREATE PROCEDURE add_attr_to_categories_by_name( IN p_attr_id BIGINT, IN p_name_keyword VARCHAR(64) ) BEGIN DECLARE done INT DEFAULT 0; DECLARE v_cat_id BIGINT; DECLARE cur CURSOR FOR SELECT id FROM category WHERE is_leaf 1 AND name LIKE CONCAT(%, p_name_keyword, %); DECLARE CONTINUE HANDLER FOR NOT FOUND SET done 1; OPEN cur; read_loop: LOOP FETCH cur INTO v_cat_id; IF done THEN LEAVE read_loop; END IF; INSERT IGNORE INTO category_attribute (category_id, attribute_id, required, sort) VALUES (v_cat_id, p_attr_id, 0, 0); END LOOP; CLOSE cur; END;調(diào)用這個存儲過程只需要傳入屬性ID和關(guān)鍵字CALL add_attr_to_categories_by_name(20001, 女裝);游標(biāo)方式的好處是方便加日志、方便控制執(zhí)行批次適合做一次性的數(shù)據(jù)修正。如果數(shù)據(jù)量特別大更推薦用臨時表加集合操作但作為一次性腳本游標(biāo)完全夠用。4. 常見問題與排查技巧實錄4.1 葉子類目繼承屬性怎么處理踩過的一個坑是直接在非葉子類目上加屬性商品發(fā)布頁不一定能繼承到葉子類目。淘寶類目體系里商品只能掛在葉子類目下所以很多場景只關(guān)注葉子類目。但也有一些業(yè)務(wù)屬性比如“品牌”可能在父級類目上維護子類目默認繼承。項目里我一開始只給葉子類目加后來發(fā)現(xiàn)后臺篩選時父類目需要統(tǒng)計屬性聚合又不得不回頭給父類目補數(shù)據(jù)。建議在關(guān)聯(lián)表里加source字段標(biāo)注這條關(guān)聯(lián)是手動設(shè)置還是父級繼承。查詢時需要根據(jù)業(yè)務(wù)定義決定是否把父級繼承的屬性一并查出或者實時用遞歸CTE往上找。這個選擇要在需求階段就確認清楚寧可多花一點時間問清楚也不要寫完腳本再返工。4.2 大批量寫入的性能優(yōu)化全量給幾千個類目加屬性時如果一條條INSERT那速度會讓人崩潰。我實測過三萬條關(guān)聯(lián)數(shù)據(jù)用單條INSERT多VALUES比逐條插入快幾個量級INSERT INTO category_attribute (category_id, attribute_id, required, sort) VALUES (1, 10086, 0, 0), (2, 10086, 0, 0), (3, 10086, 0, 0);但單條INSERT多VALUES有個問題如果中間有一條違反唯一鍵整批都會失敗。所以這種方案需要先根據(jù)唯一鍵過濾好或者直接用INSERT IGNORE。另外大批量寫入時建議分批提交比如每500條一個事務(wù)既能避免長事務(wù)帶來的鎖問題也方便出錯時定位。4.3 SQL安全問題參數(shù)拼接與注入風(fēng)險這里必須多說一句。動態(tài)SQL中千萬不要直接把外部參數(shù)拼到字符串里尤其當(dāng)參數(shù)來自后臺頁面的時候很容易被構(gòu)造出惡意語句。正確做法是用預(yù)處理語句并綁定參數(shù)例如SET sql INSERT IGNORE INTO category_attribute (category_id, attribute_id) VALUES (?, ?); PREPARE stmt FROM sql; EXECUTE stmt USING cat_id, attr_id; DEALLOCATE PREPARE stmt;相比之下上面存儲過程的方式天然就避免了拼接問題。無論做數(shù)據(jù)同步還是后臺工具開發(fā)把參數(shù)化查詢當(dāng)成習(xí)慣比事后補漏洞成本低得多。4.4 常見問題速查表現(xiàn)象可能原因解決方式唯一鍵沖突導(dǎo)致腳本報錯關(guān)聯(lián)數(shù)據(jù)重復(fù)插入使用INSERT IGNORE或ON DUPLICATE KEY UPDATE中文類目名亂碼表或連接字符集不一致統(tǒng)一切到utf8mb4并執(zhí)行SET NAMES utf8mb4大批量執(zhí)行卡死關(guān)聯(lián)表缺少索引或事務(wù)過長加索引、分批提交事務(wù)腳本跑完數(shù)據(jù)對不上過濾條件沒考慮葉子類目先用SELECT和COUNT確認范圍再執(zhí)行重復(fù)執(zhí)行后屬性順序混亂未設(shè)置sort或未做冪等更新明確sort值用唯一鍵UPDATE保證一致5. 后續(xù)擴展方向這套SQL還能怎么用建好這套類目和屬性關(guān)聯(lián)結(jié)構(gòu)之后能做的事情遠不止加屬性。比如可以做商品發(fā)布模板按類目查出對應(yīng)的屬性列表自動渲染成表單運營無需理解底層表關(guān)系。也可以做數(shù)據(jù)質(zhì)量校驗凡是葉子類目缺少必填屬性的用一條SQL就能全部查出來。甚至可以把類目屬性轉(zhuǎn)成EAV模型給商品搜索篩選提供底層支持前端“按品牌篩選”“按價格區(qū)間篩選”都能復(fù)用這套數(shù)據(jù)。最后再分享一個小習(xí)慣每次跑這種批量加屬性的腳本前先把受影響類目數(shù)和關(guān)聯(lián)數(shù)用COUNT查一遍確認范圍無誤再執(zhí)行。腳本文件本身也建議入庫管理文件名標(biāo)明用途和時間比如20250115_add_pub_year_attr.sql。這些習(xí)慣看著不起眼但長期維護下來能幫你少踩很多坑。本文還有配套的精品資源點擊獲取