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