一雙鞋引發的血案:產品化數據建模淺析
這是一個發生在 2015年 的故事。中國的互聯網經濟進入高速發展的時期。各種互聯網創業公司層出不窮,人們爭先恐后地加入到這場浪潮中來。
故事的主人公小明是個有遠大理想的小朋友,有一些的開發經驗,更有敏銳的市場洞察力。 他發現以 uber 和 airbnb 為代表的共享經濟正在變成一個熱點,新加入的 “回家吃飯 “也是來勢洶洶。這些公司涵蓋了食、住、行等領域的共享,但市面上還沒有衣服共享的公司。他又想到自己有很多閑置的運動鞋,何不開一個在垂直領域共享運動鞋的創業公司。他興奮得一夜沒睡,馬上注冊了一個叫 air-sneakers 的公司。召集了幾個工程師朋友熱火朝天地干起來了。
創業艱辛
首先他們著手設計數據模型。在數據模型中最重要的表叫 ATHLETIC_SHOES 表。這個表大概長成這樣:
小明又為常用的一些品牌,材料,尺碼等重要數據建了一些關聯表。接著,小明又做好了 PRD 和 wireframe。 小明的工程師們選擇了熟悉的 java 語言來開發。每個頁面基本針對一張表的增刪改查,用 iBatis 之類的 OR mapping 開發的后端和 angular 開發的前端很快就完成了。兩個月后第一版上線了!
很快,小明注意到了一個問題。并不是很多人都有很多閑置的運動鞋,也不是每個人都喜歡每天穿不同的運動鞋。和公司 CFO (小明太太)商量后,他果斷決定推出女鞋共享,女人的閑置鞋會多一些。
小明的工程師發現原來設計的數據模型根本不夠用。女鞋可不像運動鞋那么簡單,光一個單鞋就有什么高跟,低跟,平跟,粗跟,細跟,圓頭,尖頭,真皮,假皮等等屬性,連鞋碼都不一樣。新的女鞋表大概是這樣的.
WOMENS_SHOES
當然還有 WOMENS_PUMPS 表,WOMENS_BOOTS 表,WOMENS_SANDALS 表,等等……此處略去 10000 字
原來的代碼基本上沒用了,小明還被工程師打了。
新的代碼花了很長時間才寫出來,特別復雜,到處都是 if else 之類的判斷??偹?,新版本上線了。但是用戶還是不買賬。原來,女人也不喜歡穿別人的舊鞋,也沒有那么多人喜歡把自己的鞋借給別人,過上腳氣都不知道。
小明再次調整戰略,開發出了一版包括所有服裝共享的 app 改名為 air-wardrobe。這次新的表結構就沒那么簡單了,大大小小建了上百張表。
大家天天加班,苦苦干了一年。因為屢次改需求,小明又受傷住院了。
峰回路轉
總算,新 app 上線為小明拉來了第一筆風投。投資方不希望小明只做服裝共享,應該涵蓋所有家用產品。無奈下,小明找來了一個架構師設計新的數據模型。架構師看到舊的 schema 設計撫掌大笑,指出了舊 schema 的最大問題。
傳統橫向 schema 的缺點:
- 在插入數據時,需要向許多張表里先后插入數據,并要保證數據的一致性。
- 當需要顯示來自不同表的信息時,需要連接多張表。前端在顯示產品列表時要顯示的字段常常不在一張表里。經常為了顯示一個字段而要多連接幾張表,并做各種復雜的查詢。
- 一個表的列越多,數據冗余也越多
- 不同的維度,事實和度量需要建立跟多的錯綜復雜的關系,并要維護這些一致性。
- 所有傳統關系型數據庫的缺點
新數據模型是這樣設計出來的。首先是一張 PRODUCT 表。這張表包含了世界上所有產品共有的屬性。如分類,新舊,價格,數量。
然后,那些為各類商品單獨建的表和它們的關聯表都不需要了。取代他們的是一個垂直的 schema. 首先需要的是一張表描述商品的元數據(meta data)“PROD_ATTRIBUTES”,用來存放所有產品屬性的定義。
對于每一種商品,我們只需要定義他們獨有的屬性,不同商品可以共享一些屬性,比如運動鞋和女鞋共享鞋碼的屬性。
對每樣商品的每個屬性,我們插入一條數據來保存它。這就需要一個 PROD_ATTR_VALUE 表
- 過去我們選擇一條商品數據用這樣的 SQL:Select * from athletic_shoes where id = 1001
- 現在用的 SQL 還是一樣:Select * from PROD_ATTR_VALUE where PROD_ID= 1001
區別只是在顯示方向上,過去是橫向顯示的, 現在是縱向顯示的。過去是寬的,現在是窄的。
用舊 schema,通常我們會為每張表對應一個類。方便 OR mapping。用新 schema 任何商品只需要一種數據結構來表示,就是 Map,準確地說是 Multimap,因為考慮到有一對多的屬性。 Multimap 數據結構和流行的 JSON 數據結構和 Http 請求的 query 是很相似的,很適合互聯網應用。
傳統 schema 里,一對多的關系是通過連接表和外鍵實現的。比如一雙鞋可能包括許多流行元素,假設舊 scheme 里為這些流行元素的關鍵字建了一個 SHOE_KEYWORDS 表,和 shoes 表為一對多關系。
在垂直 schema 里,只需要加一個 IDX 字段可以實現一對多關系。如下表:這個商品 101 有兩個 keywords。
如果要保存每次產品更新記錄便于存檔呢?過去,可能需要加一個類似 shoe_edit_history 的表。每次改動時把舊數據搬到這張表里,再創建一條新數據。
在新 schema 里只要加一個 ACTIVE 字段標識最新的改動,就能達到目的。
過去對于下拉框式的輸入的值,傳統上我們通常會需要其他表的輔助。
比如在舊表里可能有個 HEEL_TYPE_ID 的字段,表示不同跟高。另外有一張 VALID_HEEL_TYPES 表保存所有合法的跟高。或者像小明那樣用一個 enum 之類的 hard code 在代碼里。
在新 schema 里,我們會用到一個 CODE_LIST 的關聯表,下面這張表描述了鞋碼和跟高兩個 code list。非常適合在下拉框顯示它們。
新 schema 如何把相關的信息組合在一起?只要加一個 PROD_ATTR_GROUP 表。
事實上應該為這些數據建立一個樹狀的層級關系:
(注)PROD_ID 字段在 PROD_ITEM 表中出現是一種去范式化,為了更方便查詢。
有了這樣一個數據模型,錄入和顯示每種商品用的都是同一套代碼,基本不需要為特殊產品和特殊客戶改后端的代碼。小明的團隊做了以下分工:
- 項目經理:每開發一種新產品時,項目經理需要定義一組屬性,并為這些屬性分組,指定驗證方式,指定 code list 等等 (產品足夠多是可以開發一個工具來幫助 PM 的)。
- 前端工程師:以項目經理定義的產品屬性,用工具自動生成一個錄入數據的模板,一個展示數據的模板,和一個數據列表的模板。 必要的話可以針對每種產品貨客戶做些定制,最后把定制后的模板保存??赡苄枰獮楫a品審核,訂單等也生成一些模板。調用后端 API 把數據在模板理顯示出來。
- 后端工程師:開發一個 RESTful API 負責錄入數據,顯示數據,更改數據和,產品列表。還需要一些其他 API 來管理產品,分類,批量錄入,等等。
- 架構師:負責指定流程和標準,開發框架和工具,包括代碼生成器。
當然這樣的數據模型也是有缺點的:
- 插入數據多影響性能 - 可通過 batch 插入來改善
- 多屬性的查詢不方便 - 必須通過自連接(self-join)來查。
- 數據統計,挖掘不易 - 需要通過 ETL 等工具把豎表展開成寬表。
- 數據冗余大 - 此類數據通常具有實效性,可以定期存檔(archive)
以上數據模型只是一個簡單化例子。這類數據模型比較適合金融,電商,醫藥等行業。在設計這類模型時需要和傳統的關系型模型間找到一個折衷方案。
故事結尾
小明的公司很快拿到了更多投資,一年后上市了。小明和太太在加勒比小島上 live happily ever after。小明的工程師們也分到了期權,工作也很開心,再也不用為改需求大動干戈了。他們向小明道歉并得到了諒解。
這個故事教導我們
- 不要怪 PM 改需求,可能是代碼設計有問題。足夠靈活的設計可以做到不改或少改代碼。
- 要做產品化的應用,產品的種類和客戶的需求常常是未知的,機械地為每個產品的每組屬性添加新表和新字段是不動腦筋的設計。
- Simplicity is the ultimate sophistication。如果你的數據庫里有幾百張表,后端幾百個服務,不值得驕傲。
- 數據模型設計不要被業務牽著鼻子走,照著前端的頁面在后端做增刪改查是低級的開發方式。
- 軟件設計和架構是很重要的,它能幫助我們以最小的代價干最多的事。
- 代碼是可以用來生成代碼的,數據是可以用來描述數據的。
- 碼農和工程師的區別是:碼農是代碼的搬運工,工程師是代碼的創造者
本文作者:趙文樂,現任點融網技術 team leader,17年 軟件開發經驗,在美國工作學習 15年,從事互聯網、金融、云計算等行業。
本文由 @趙文樂 原創發布于人人都是產品經理?,未經許可,禁止轉載。
優秀的程序員和渣比程序員的故事。