一雙鞋引發(fā)的血案:產(chǎn)品化數(shù)據(jù)建模淺析

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