1. 程式人生 > >關於數據庫‘狀態’字段設計的思考與實踐

關於數據庫‘狀態’字段設計的思考與實踐

意義 數量 這樣的 裏的 業務流程 會有 有一個 標準 sql

最近在做訂單及支付相關的系統,在訂單表的設計階段,團隊成員就‘訂單狀態’數據庫字段設計有了一些分歧,網上也有不少關於這方面的思考和探討,結合這些資料和項目的實際情況,擬對一些共性問題進行更深一層的思考,筆耕在此,和大家一起探討。

問題綜述

這裏的分歧點即有團隊內部的分歧點,也有網絡上常見的一些分歧點,先將存在的分歧點拋出來:

1、訂單表的‘訂單狀態’字段對應的字典值應當包含哪些狀態值?對於‘已評論’、‘已退貨’、’已退款’這類狀態是放到‘訂單狀態’中?還是獨立一個字段標識?

2、訂單表的‘訂單狀態’字段對應的字典值如何表示?可選項有:使用數字標識、使用多‘位’存儲方式標識、使用具有明確業務含義的英文字符串標識;

3、訂單表的‘訂單狀態’字段使用何種類型?可選項有:number(N)、char(N)、varchar2(N);

如果嫌分析過程過於啰嗦,可以直接拉到最後看結論。

業務分析

我們先不去看問題,先來看看和‘訂單(Order)’實體相關的業務是怎樣的。下面我們會針對可能改變訂單實體狀態的行為已經狀態變化的可能性進行詳細的分析。

訂單業務實體相關的業務流程如下:下單(create)--> 買家付款(pay)--> 賣家發貨(deliver)-->買家收貨(receive)-->退貨(rereturn);此外,還有退款(refund)和評論(comment),這兩個行為比較特殊,其前向行為可能存在多個。

技術分享

首先,可以改變訂單業務狀態【這裏的狀態不是指‘訂單狀態’(OrderState)這個數據庫字段,而是指實際業務狀態,我們簡記為(BizState),以和OrderState區分開】的行為有哪些?按照典型電商的業務流程,主要的行為(action)有:下單、付款、發貨、收貨、退款/退貨、評論;每一種行為的發生,都會導致訂單的業務狀態BizState發生變化,比如‘下單’行為會創建訂單,‘付款’行為會使訂單變為‘已付款’,‘發貨’行為可以使訂單狀態變為‘已發貨’,‘收貨’行為會使訂單狀態變為‘已收貨’,‘評論’行為會使訂單狀態變為‘已評論’。‘退款/退貨’action不是所有訂單都支持的,為減小復雜度,暫不考慮它們。

其次,細分下每種action對BizState帶來的影響,會發現還可以細分為四種子狀態(subState):action未開始(標記為0)、action進行中(標記為1)、action成功(標記為2)、action失敗(標記為3);理論上,將所有action的所有subState進行排列得到4*4*4*4*4=1024(暫未考慮‘退貨’);實際上,很多組合是沒有業務意義的,是不可能存在的,比如‘未開始已付款...’(***20)這一類組合是不可能發生的,應當舍棄。用表格將上述的組合分析如下:

技術分享

通過上表,我們可以發現些的規律:

‘下單’、‘付款’、‘發貨’、‘收貨’前四種action是存在依賴關系的,亦即後一個action依賴於前一個action的完成;所以,他們的SubState組合情況就會非常少;

‘評論comment’這個action的SubState和其他狀態組合會有很多種可能性;除了前面了兩行是‘X’,後面是‘?’或者‘Y’,‘?’是指需求上是否允許在對應的BizState上進行評論,如果允許,則每種BizState需要多出4種可能,這樣組合的可能性就會變得很大。

沒有業務意義的SubState組合被舍棄。表中的標黑單元格,表示這個BizState是毫無意義的,因為‘未下單’的訂單對於我們來講是不存在的,這類組合需要舍棄;同樣的,還有很多其他的組合也是不存在的,被舍棄掉,未展示在上表中,如‘已下單已付款未發貨已收貨’這種。

通常某個action的SubState為‘1進行中’、‘3失敗’時,會被忽略,但也有例外;比如‘付款’action的‘3失敗’狀態,和‘付款’action的‘1進行中’狀態,具體分析見後面內容。

忽略所有action的‘0未開始’SubState狀態。因為這類SubState對於BizState不會帶來變化。

綜合下來,我們得到上表的BizState,註意這裏的Comment action未進行細化處理,如果細化處理,會發現BizState的可能性會增大很多很多。

接下來我們就之前提出的這些問題進行逐個討論。

問題一、訂單表的‘訂單狀態’字段應當包含哪些狀態值?

什麽樣的‘訂單業務狀態’(BizState)需要記錄到系統層面的‘訂單狀態’(OrderState)字段呢?如果記錄多了,則系統處理的復雜度會增大;記錄少了,那麽‘訂單狀態’(OrderState)字段就不能完整的表示出訂單實體狀態變化情況。

核心狀態

通過上面的業務分析可知:大部分存在依賴關系的action(create、pay、deliver、receive),他們產生的合理的SubState組合是非常少的,而且他們之間的依賴是單向依賴,狀態機的處理也很簡單,因此,我們先將這部分BizState納入到OrderState中:

  • 等待買家付款
  • 買家付款成功
  • 賣家已發貨
  • 買家已收貨

目前的訂單狀態流轉:

技術分享

‘action行為’失敗的情況

對於action的SubState是‘3失敗’的處理,需要針對不同的action進行分析。類似‘下單Create’這樣的action,如果失敗,則可以直接將OrderState置為‘訂單創建失敗’,因為Create action是第一個action,它的失敗意味著Order實體出生即死,BizState置為終態,對於這個BizState應當納入到OrderState中記錄,不過這個OrderState其實對於用戶並無多大用處,因為用戶並不會關心下單失敗的訂單,他更關心的是重新下單;

對於‘支付’失敗,則要看需求,如果需求要求用戶可以繼續支付,則訂單需要保留,並且狀態仍然為‘等待買家付款’,如果不允許再支付,則理論上可以將BizState置為‘支付失敗’終態,所以,‘支付失敗’的BizState終態也應當記錄到OrderState字段中。

對於‘發貨’失敗、‘收貨’失敗的情況,通常是不會發生的,即使發生也不屬於系統能夠控制的範疇,系統記錄並無意義,更具建設性的做法是通過線下手段盡快解決問題,重新發貨等等,所以對於這些狀態系統的OrderState字段不予記錄。

這樣下來我們的OrderState字典值增加到6個,加粗項為新增:

  • 創建訂單失敗(終態)
  • 等待買家付款
  • 買家付款失敗(終態,依賴需求而定)
  • 買家付款成功
  • 賣家已發貨
  • 買家已收貨

目前的訂單狀態流轉:

技術分享

‘action行為’進行中的情況

對於action的SubState是‘1進行中’的處理,同樣需要具體場景具體分析。‘付款’行為是用戶發起的,但是並不是和訂單系統之間的交互,涉及到支付系統的處理,這個領域也不是訂單系統可控的,但關系到錢,用戶比較關系,所以對於這樣一個中間態,我們需要記錄,以便用戶通過訂單系統查詢訂單狀態,為便於用戶理解,將此狀態在OrderState中記為‘付款確認中’;‘發貨’‘收貨’進行中的情況,不是訂單系統可以控制的領域,我們可以把他們當著行為‘未開始’處理,比如‘發貨進行中’,訂單系統的OrderState值為‘買家已付款’,但給用戶看到的提示信息是‘買家已付款,等待賣家發貨’,實際上這時候賣家可能正在發貨中,但是用戶不會去關心到底有沒有打包好貨物什麽的,所以這類‘進行中’狀態可以舍棄。這樣下來訂單系統的OrderState字段又多了一個字典值:‘付款確認中’:

  • 創建訂單失敗(終態)
  • 等待買家付款
  • 付款確認中
  • 買家付款失敗(終態,依賴需求而定)
  • 買家付款成功
  • 賣家已發貨
  • 買家已收貨

目前的訂單狀態流轉:

技術分享

‘action行為’未開始的情況

忽略所有action的‘0未開始’SubState狀態。因為這類SubState對於BizState不會帶來變化。

‘評論comment’的處理

最後,再來看看‘評論comment’這個action。如果需求上要求:只有買家收貨後才能發起‘評論’操作,則可以任務‘評論comment’單向依賴於‘receive收貨’行為,那麽可以將這個action的subState對應的少量BizState(應當只有‘買家已評論’、‘賣家已評論’狀態)納入OrderState字段統一記錄;但是如果需求是:買家在下單後就可以開始評論,比如如果賣家發貨慢了,買家可以上去吐槽,那麽‘評論comment’就不是單向依賴於‘receive收貨’行為了,而是多向依賴於‘pay付款’、‘deliver發貨’、‘receive收貨’,那麽這些actions的subState組合可能性就暴增,BizState的字典取值也會暴增,顯然,不應當將這麽多的BizState交給OrderState來記錄,而應當由一個獨立的數據庫字段負責記錄‘評論comment’的SubState,我們可以將這個字段取名為‘CommentState’(評論狀態),它的字典值不多,只有:‘未評論’、‘買家已評論’、‘賣家已評論’;其實,對於前一種需求,也可以不講‘評論comment’對應的SubState產生的BizState納入OrderState,因為用戶對於評論與否其實並不是那麽關心的,也就是說‘評論comment’並不是核心業務流程,為了降低核心業務流程的系統處理復雜度,將其從核心業務流程中剝離出來較好。

綜上,我們應當將‘評論comment’對應的BizState獨立到一個字段中記錄。

‘退貨rereturn’的處理

再來看看‘退貨rereturn’行為對應的BizState的處理。‘退貨rereturn’並不是所有訂單都會經歷的,但是一旦涉及,則‘退貨rereturn’在業務流程上必定是單向依賴於單向依賴於‘receive收貨’,所以應當將‘退貨rereturn’產生的BizState(‘退貨中’、‘退貨成功’,‘退款失敗’和‘未退貨’被忽略,見上面解釋)納入OrderState一並記錄;這樣我們的OrderState有多了兩種字典值,這裏我們不考慮一個訂單中有多種商品的情況,故把‘退貨成功‘當著終態處理,如果是一個訂單多種貨物的情況,需要重新仔細分析。加粗項為新增:

  • 創建訂單失敗(終態)
  • 等待買家付款
  • 付款確認中
  • 買家付款失敗(終態,依賴需求而定)
  • 買家付款成功
  • 賣家已發貨
  • 買家已收貨
  • 退貨中
  • 退貨成功(終態)

目前的訂單狀態流轉:

技術分享

‘退款refund’的處理

最後來看下‘退款refund’行為對應的BizState的處理。首先,我們需要知道‘退貨’和‘退款’是兩種不同的業務行為,他們的關系是:通常意義上,‘退貨’必然導致‘退款’,但是‘退款’可以沒有‘退貨’的參與(這裏不討論特殊情況,比如對於虛擬貨物來講,付款成功通常以為著收貨成功,這時候就只能是在由‘退貨’導致‘退款’),比如電商允許用戶付款成功後收到貨物前發起‘退款’。也就是說‘退款refund’並不單向依賴於‘退貨rereturn’,和‘評論comment’一樣是多項依賴,所以,我們可以參考‘評論comment’的處理方式,單獨建立一個字段‘RefundState退款狀態’記錄‘退款refund’產生的BizState,這個狀態字段的字典值有:退款中,退款成功。

其他情況考慮

另外,可能還有一些增強型需求,讓客戶體驗更好,比如用戶可以創建訂單之後付款之前,將訂單取消,或者由系統跑批將用戶長時間未支付的訂單關閉,這會產生一種新的action——‘close關閉’,對應的會產生一種新的有意義的BizState——‘訂單關閉/取消’,這個不屬於核心流程中的,且並無糾結之處,不予詳細討論,羅列如下:

  • 創建訂單失敗(終態)
  • 等待買家付款
  • 付款確認中
  • 買家付款失敗(終態,依賴需求而定)
  • 買家付款成功
  • 賣家已發貨
  • 買家已收貨
  • 退貨中
  • 退貨成功(終態)
  • 訂單關閉(終態)

結論

綜上,我們可以得出放入數據庫’訂單狀態‘字段的標準:核心業務流程,向前單向依賴。擴展到其他業務實體是一樣的,這裏說的’訂單狀態‘字段實際是指該業務實體對應的數據表的主業務狀態字段。我們把結論擴展一下:

如果某個action屬於業務實體對應的核心業務流程,且該action單向依賴於其前向的action,則需要將這個action產生的BizState放入到業務實體對應的數據庫表的主狀態字段中記錄。

OrderState字段記錄的BizState業務狀態有10種,其中4種是終態,其余狀態為中間態。這些狀態的流轉關系為:

技術分享

問題二、訂單表的‘訂單狀態’字段的字典值的表示形式?

先列出可選項:使用數字標識、使用多‘位’存儲方式標識、使用具有明確業務含義的英文字符串標識;對可選項做逐一解釋:

a、使用數字標識——使用一個數字標識一種狀態,並未要求是sequence的;如‘等待買家付款’表示為‘0’;

b、使用多‘位’存儲方式標識——將某種行為是否發生對應的狀態對應到一個位上,比如‘是否付款’定義在第一位,‘是否發貨’定義在第二位,‘是否收貨’定義在第三位,‘是否評論’定義在第四位,則狀態‘賣家已收貨未評論’可以表示為:0111;而‘等待買家付款’則表示為‘0000’;當然這裏的‘位’可能是二進制的也可能是N進制,後面我們詳細討論。

c、使用具有明確業務含義的英文字符串標識——該方案和方案a類似,不過字典值變為具有明確業務含義的英文支付串,如‘等待買家付款’表示為‘WAIT_BUYER_PAY’;

方案a是數據庫字段字典的慣用方式,簡單直觀,但是有一個壞處在於:當字典值較多時,數據庫表的使用者記不住字典的含義,需要反復查找資料確認;有人會說將字典值寫到字段的註釋裏,這個在實踐中不是很靠譜,通常表建立後,如果字段增加了字典值,通常開發人員都會忽略更改字典值;而且在使用工具(如pl/sql)查詢數據庫時,並不會將所有字典值展示出來;

通過問題一的分析,可知:方案b使用多‘位’存儲方式會增加復雜度,並沒有必要,可以通過將‘是否評論’狀態獨立成一個字段進行表示。

方案c和方案a類似,好處在於通過字典值直接知道業務含義,壞處在於會給編碼和手工查詢時帶來復雜度,通常人們也記不住‘等待買家付款’的英文字典是‘WAIT_BUYER_PAY’,那麽手動寫sql查詢‘等待買家付款’時就犯迷糊了。

折中之後,我們組合方案a和方案c,得到方案d:另外建立一張字典表,存儲:數字形式的字典值、字典英文名稱、字典中文簡稱、字典解釋;訂單實體表的OrderState字段使用數字作為字典值。

對於方案d,看到OrderState的數字形式狀態時,可以先看看字段註釋是否有此字典的定義,如果沒有就取查下字典表,得到字典值和含義;在編碼和手動sql查詢時也會變得比較容易,數字的位數畢竟要少些;建立字典表的其他好處還有:字典的解釋可以寫的很詳細,在報表中要求展示字典中文名時,也能直接從數據庫聯表查詢得到,而不必額外做一次映射。(有參考:數據庫表設計(狀態字段))

那麽對於字典數量很少的狀態字段是否有必要額外新建一張字典表呢?這個根據實際情況考慮,通常可以先不建,如果後續有業務場景需要再行創建也不遲。

而對於非業務實體表的系統日誌/跑批記錄表等的狀態,則完全可以使用數字形式的字典,因為通常不會有業務場景使用到這些字典值,而且這些字典值域應當會比較小,所以沒有必要為他們創建單獨的字典表。

綜上得出結論:

1、字典值域較多、變化較多、報表等業務場景會使用到的業務實體表的業務狀態字段,使用‘方案d:新建字典表’的方案處理;如‘訂單業務實體表’中的‘訂單狀態’字段。

2、字典值域較少、變化較少、報表等業務場景不會使用到的業務實體表的業務狀態字段,使用‘方案a:使用數字標識字典’的方案處理;如‘支付寶的支付流水表’的‘支付流水狀態’字段。

3、系統日誌/跑批記錄表的狀態字段,使用‘方案a:使用數字標識字典’的方案處理;如‘待收貨記錄表’的‘跑批狀態’字段。

問題三、數據庫表的‘狀態’字段使用何種類型

列出可選項:number(N)、char(N)、varchar2(N),其中N是一個長度值。

這個問題主要需要考慮使用場景、擴展性、性能、存儲。

‘狀態’字段主要使用在查詢場景,且通常是‘=’或者‘in’的查詢,並沒有區間類的查詢,故三者差別不大;

對於性能,參考[原創]在Oracle 10g,Number、Char和Varchar2類型作為主鍵,查詢效率分析 char(N)、varchar2(N)性能優於number(N),故舍棄number(N)。

考慮到擴展性,char(N)、varchar2(N)差不多;

考慮到存儲,varchar2更加占用空間更小,故選擇varchar2(N)。

綜上:選擇varchar2(N)作為數據庫‘狀態’字段的類型。

問題結論匯總

1、訂單表的‘訂單狀態’字段對應的字典值應當包含哪些狀態值?對於‘已評論’、‘已退貨’這類狀態是放到‘訂單狀態’中?還是獨立一個字段標識?

如果某個action(行為,如支付)屬於業務實體對應的核心業務流程,且該action單向依賴於其前向的action,則需要將這個action產生的業務狀態放入到業務實體對應的數據庫表的主狀態字段中記錄。

問題中的‘已評論’由‘評論’行為產生,而‘評論’這個action並不是訂單業務實體的核心業務流程,且可能存在多個前向依賴action(支付、發貨、收貨等),所以應當獨立到一個字段標識。

問題中的‘已退貨’由‘退貨’行為產生,而‘退貨’這個action是訂單業務實體的核心業務流程,用戶非常關心,且只單向依賴於‘收貨’action,所以應當記錄到訂單業務實體表的‘訂單狀態’字段中。

問題中的‘已退款’由‘退款’行為產生,而‘退款’這個action是訂單業務實體的核心業務流程,用戶非常關心,但是這個action存在多個前向依賴action(支付、發貨、收貨等),所以應當獨立到一個字段標識。

2、訂單表的‘訂單狀態’字段對應的字典值如何表示?可選項有:使用數字標識、使用多‘位’存儲方式標識、使用具有明確業務含義的英文字符串標識;

i、字典值域較多、變化較多、報表等業務場景會使用到的業務實體表的業務狀態字段,使用‘方案d:新建字典表’的方案處理;如‘訂單業務實體表’中的‘訂單狀態’字段。

j、字典值域較少、變化較少、報表等業務場景不會使用到的業務實體表的業務狀態字段,使用‘方案a:使用數字標識字典’的方案處理;如‘支付寶的支付流水表’的‘支付流水狀態’字段。

k、系統日誌/跑批記錄表的狀態字段,使用‘方案a:使用數字標識字典’的方案處理;如‘待收貨記錄表’的‘跑批狀態’字段。

3、訂單表的‘訂單狀態’字段使用何種類型?可選項有:number(N)、char(N)、varchar2(N);

varchar2(N)占用存儲更少,且具有同等的性能、擴展性,選擇varchar2(N)作為數據庫‘狀態’字段的類型。

參考資料

數據庫表設計(狀態字段)

[原創]在Oracle 10g,Number、Char和Varchar2類型作為主鍵,查詢效率分析

關於數據庫‘狀態’字段設計的思考與實踐