如何进行数据库“状态”字段设计的思考与实践

72次阅读
没有评论

共计 9884 个字符,预计需要花费 25 分钟才能阅读完成。

本篇文章给大家分享的是有关如何进行数据库“状态”字段设计的思考与实践,丸趣 TV 小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着丸趣 TV 小编一起来看看吧。

正文

最近在做订单及支付相关的系统,在订单表的设计阶段,团队成员就“订单状态”数据库字段设计有了一些分歧,网上也有不少关于这方面的思考和探讨,结合这些资料和项目的实际情况,拟对一些共性问题进行更深一层的思考,笔耕在此,和大家一起探讨。

1. 问题综述

这里的分歧点即有团队内部的分歧点,也有网络上常见的一些分歧点,先将存在的分歧点抛出来:

1)、订单表的 lsquo; 订单状态 rsquo; 字段对应的字典值应当包含哪些状态值? 对于 lsquo; 已评论 rsquo;、lsquo; 已退货 rsquo;、rsquo; 已退款 rsquo; 这类状态是放到 lsquo; 订单状态 rsquo; 中? 还是独立一个字段标识?

2)、订单表的 lsquo; 订单状态 rsquo; 字段对应的字典值如何表示? 可选项有:使用数字标识、使用多 lsquo; 位 rsquo; 存储方式标识、使用具有明确业务含义的英文字符串标识;

3)、订单表的 lsquo; 订单状态 rsquo; 字段使用何种类型? 可选项有:number(N)、char(N)、varchar2(N);

如果嫌分析过程过于啰嗦,可以直接拉到 *** 看结论。

2. 业务分析

我们先不去看问题,先来看看和 lsquo; 订单(Order) rsquo; 实体相关的业务是怎样的。下面我们会针对可能改变订单实体状态的行为已经状态变化的可能性进行详细的分析。

订单业务实体相关的业务流程如下:下单 (create)– 买家付款(pay)–   卖家发货(deliver)– 买家收货(receive)– 退货(rereturn); 此外,还有退款(refund) 和评论(comment),这两个行为比较特殊,其前向行为可能存在多个。

首先,可以改变订单业务状态【这里的状态不是指 lsquo; 订单状态 rsquo;(OrderState)这个数据库字段,而是指实际业务状态,我们简记为 (BizState),以和 OrderState 区分开】的行为有哪些? 按照典型电商的业务流程,主要的行为(action) 有:下单、付款、发货、收货、退款 / 退货、评论; 每一种行为的发生,都会导致订单的业务状态 BizState 发生变化,比如 lsquo; 下单 rsquo; 行为会创建订单,lsquo; 付款 rsquo; 行为会使订单变为 lsquo; 已付款 rsquo;,lsquo; 发货 rsquo; 行为可以使订单状态变为 lsquo; 已发货 rsquo;,lsquo; 收货 rsquo; 行为会使订单状态变为 lsquo; 已收货 rsquo;,lsquo; 评论 rsquo; 行为会使订单状态变为 lsquo; 已评论 rsquo;。lsquo; 退款 / 退货 rsquo;action 不是所有订单都支持的,为减小复杂度,暂不考虑它们。

其次,细分下每种 action 对 BizState 带来的影响,会发现还可以细分为四种子状态 (subState):action 未开始(标记为 0)、action 进行中(标记为 1)、action 成功(标记为 2)、action 失败(标记为 3); 理论上,将所有 action 的所有 subState 进行排列得到 4 *4*4*4*4=1024(暂未考虑 lsquo; 退货 rsquo;); 实际上,很多组合是没有业务意义的,是不可能存在的,比如 lsquo; 未开始已付款 … rsquo;(***20) 这一类组合是不可能发生的,应当舍弃。用表格将上述的组合分析如下:

通过上表,我们可以发现些的规律:

lsquo; 下单 rsquo;、lsquo; 付款 rsquo;、lsquo; 发货 rsquo;、lsquo; 收货 rsquo; 前四种 action 是存在依赖关系的,亦即后一个 action 依赖于前一个 action 的完成; 所以,他们的 SubState 组合情况就会非常少;

lsquo; 评论 comment rsquo; 这个 action 的 SubState 和其他状态组合会有很多种可能性; 除了前面了两行是 lsquo;X rsquo;,后面是 lsquo;? rsquo; 或者 lsquo;Y rsquo;,lsquo;? rsquo; 是指需求上是否允许在对应的 BizState 上进行评论,如果允许,则每种 BizState 需要多出 4 种可能,这样组合的可能性就会变得很大。

没有业务意义的 SubState 组合被舍弃。表中的标黑单元格,表示这个 BizState 是毫无意义的,因为 lsquo; 未下单 rsquo; 的订单对于我们来讲是不存在的,这类组合需要舍弃; 同样的,还有很多其他的组合也是不存在的,被舍弃掉,未展示在上表中,如 lsquo; 已下单已付款未发货已收货 rsquo; 这种。

通常某个 action 的 SubState 为 lsquo;1 进行中 rsquo;、lsquo;3 失败 rsquo; 时,会被忽略,但也有例外; 比如 lsquo; 付款 rsquo;action 的 lsquo;3 失败 rsquo; 状态,和 lsquo; 付款 rsquo;action 的 lsquo;1 进行中 rsquo; 状态,具体分析见后面内容。

忽略所有 action 的 lsquo;0 未开始 rsquo;SubState 状态。因为这类 SubState 对于 BizState 不会带来变化。

综合下来,我们得到上表的 BizState,注意这里的 Comment  action 未进行细化处理,如果细化处理,会发现 BizState 的可能性会增大很多很多。

接下来我们就之前提出的这些问题进行逐个讨论。

3. 问题一、订单表的 lsquo; 订单状态 rsquo; 字段应当包含哪些状态值?

什么样的 lsquo; 订单业务状态 rsquo;(BizState)需要记录到系统层面的 lsquo; 订单状态 rsquo;(OrderState)字段呢? 如果记录多了,则系统处理的复杂度会增大; 记录少了,那么 lsquo; 订单状态 rsquo;(OrderState)字段就不能完整的表示出订单实体状态变化情况。

核心状态

通过上面的业务分析可知:大部分存在依赖关系的 action(create、pay、deliver、receive),他们产生的合理的 SubState 组合是非常少的,而且他们之间的依赖是单向依赖,状态机的处理也很简单,因此,我们先将这部分 BizState 纳入到 OrderState 中:

等待买家付款

买家付款成功

卖家已发货

买家已收货

目前的订单状态流转:

lsquo;action 行为 rsquo; 失败的情况

对于 action 的 SubState 是 lsquo;3 失败 rsquo; 的处理,需要针对不同的 action 进行分析。类似 lsquo; 下单 Create rsquo; 这样的 action,如果失败,则可以直接将 OrderState 置为 lsquo; 订单创建失败 rsquo;,因为 Create  action 是 *** 个 action,它的失败意味着 Order 实体出生即死,BizState 置为终态,对于这个 BizState 应当纳入到 OrderState 中记录,不过这个 OrderState 其实对于用户并无多大用处,因为用户并不会关心下单失败的订单,他更关心的是重新下单;

对于 lsquo; 支付 rsquo; 失败,则要看需求,如果需求要求用户可以继续支付,则订单需要保留,并且状态仍然为 lsquo; 等待买家付款 rsquo;,如果不允许再支付,则理论上可以将 BizState 置为 lsquo; 支付失败 rsquo; 终态,所以,lsquo; 支付失败 rsquo; 的 BizState 终态也应当记录到 OrderState 字段中。

对于 lsquo; 发货 rsquo; 失败、lsquo; 收货 rsquo; 失败的情况,通常是不会发生的,即使发生也不属于系统能够控制的范畴,系统记录并无意义,更具建设性的做法是通过线下手段尽快解决问题,重新发货等等,所以对于这些状态系统的 OrderState 字段不予记录。

这样下来我们的 OrderState 字典值增加到 6 个,加粗项为新增:

创建订单失败(终态)

等待买家付款

买家付款失败(终态,依赖需求而定)

买家付款成功

卖家已发货

买家已收货

目前的订单状态流转:

lsquo;action 行为 rsquo; 进行中的情况

对于 action 的 SubState 是 lsquo;1 进行中 rsquo; 的处理,同样需要具体场景具体分析。lsquo; 付款 rsquo; 行为是用户发起的,但是并不是和订单系统之间的交互,涉及到支付系统的处理,这个领域也不是订单系统可控的,但关系到钱,用户比较关系,所以对于这样一个中间态,我们需要记录,以便用户通过订单系统查询订单状态,为便于用户理解,将此状态在 OrderState 中记为 lsquo; 付款确认中 rsquo;; lsquo; 发货 rsquo; lsquo; 收货 rsquo; 进行中的情况,不是订单系统可以控制的领域,我们可以把他们当着行为 lsquo; 未开始 rsquo; 处理,比如 lsquo; 发货进行中 rsquo;,订单系统的 OrderState 值为 lsquo; 买家已付款 rsquo;,但给用户看到的提示信息是 lsquo; 买家已付款,等待卖家发货 rsquo;,实际上这时候卖家可能正在发货中,但是用户不会去关心到底有没有打包好货物什么的,所以这类 lsquo; 进行中 rsquo; 状态可以舍弃。这样下来订单系统的 OrderState 字段又多了一个字典值:lsquo; 付款确认中 rsquo;:

创建订单失败(终态)

等待买家付款

付款确认中

买家付款失败(终态,依赖需求而定)

买家付款成功

卖家已发货

买家已收货

目前的订单状态流转:

lsquo;action 行为 rsquo; 未开始的情况

忽略所有 action 的 lsquo;0 未开始 rsquo;SubState 状态。因为这类 SubState 对于 BizState 不会带来变化。

lsquo; 评论 comment rsquo; 的处理

***,再来看看 lsquo; 评论 comment rsquo; 这个 action。如果需求上要求:只有买家收货后才能发起 lsquo; 评论 rsquo; 操作,则可以任务 lsquo; 评论 comment rsquo; 单向依赖于 lsquo;receive 收货 rsquo; 行为,那么可以将这个 action 的 subState 对应的少量 BizState(应当只有 lsquo; 买家已评论 rsquo;、lsquo; 卖家已评论 rsquo; 状态)纳入 OrderState 字段统一记录; 但是如果需求是:买家在下单后就可以开始评论,比如如果卖家发货慢了,买家可以上去吐槽,那么 lsquo; 评论 comment rsquo; 就不是单向依赖于 lsquo;receive 收货 rsquo; 行为了,而是多向依赖于 lsquo;pay 付款 rsquo;、lsquo;deliver 发货 rsquo;、lsquo;receive 收货 rsquo;,那么这些 actions 的 subState 组合可能性就暴增,BizState 的字典取值也会暴增,显然,不应当将这么多的 BizState 交给 OrderState 来记录,而应当由一个独立的数据库字段负责记录 lsquo; 评论 comment rsquo; 的 SubState,我们可以将这个字段取名

为 lsquo;CommentState rsquo;(评论状态),它的字典值不多,只有:lsquo; 未评论 rsquo;、lsquo; 买家已评论 rsquo;、lsquo; 卖家已评论 rsquo;; 其实,对于前一种需求,也可以不讲 lsquo; 评论 comment rsquo; 对应的 SubState 产生的 BizState 纳入 OrderState,因为用户对于评论与否其实并不是那么关心的,也就是说 lsquo; 评论 comment rsquo; 并不是核心业务流程,为了降低核心业务流程的系统处理复杂度,将其从核心业务流程中剥离出来较好。

综上,我们应当将 lsquo; 评论 comment rsquo; 对应的 BizState 独立到一个字段中记录。

lsquo; 退货 rereturn rsquo; 的处理

再来看看 lsquo; 退货 rereturn rsquo; 行为对应的 BizState 的处理。lsquo; 退货 rereturn rsquo; 并不是所有订单都会经历的,但是一旦涉及,则 lsquo; 退货 rereturn rsquo; 在业务流程上必定是单向依赖于单向依赖于 lsquo;receive 收货 rsquo;,所以应当将 lsquo; 退货 rereturn rsquo; 产生的 BizState(lsquo; 退货中 rsquo;、lsquo; 退货成功 rsquo;,lsquo; 退款失败 rsquo; 和 lsquo; 未退货 rsquo; 被忽略,见上面解释)纳入 OrderState 一并记录; 这样我们的 OrderState 有多了两种字典值,这里我们不考虑一个订单中有多种商品的情况,故把 lsquo; 退货成功 当着终态处理,如果是一个订单多种货物的情况,需要重新仔细分析。加粗项为新增:

创建订单失败(终态)

等待买家付款

付款确认中

买家付款失败(终态,依赖需求而定)

买家付款成功

卖家已发货

买家已收货

退货中

退货成功(终态)

目前的订单状态流转:

lsquo; 退款 refund rsquo; 的处理

*** 来看下 lsquo; 退款 refund rsquo; 行为对应的 BizState 的处理。首先,我们需要知道 lsquo; 退货 rsquo; 和 lsquo; 退款 rsquo; 是两种不同的业务行为,他们的关系是:通常意义上,lsquo; 退货 rsquo; 必然导致 lsquo; 退款 rsquo;,但是 lsquo; 退款 rsquo; 可以没有 lsquo; 退货 rsquo; 的参与(这里不讨论特殊情况,比如对于虚拟货物来讲,付款成功通常以为着收货成功,这时候就只能是在由 lsquo; 退货 rsquo; 导致 lsquo; 退款 rsquo;),比如电商允许用户付款成功后收到货物前发起 lsquo; 退款 rsquo;。也就是说 lsquo; 退款 refund rsquo; 并不单向依赖于 lsquo; 退货 rereturn rsquo;,和 lsquo; 评论 comment rsquo; 一样是多项依赖,所以,我们可以参考 lsquo; 评论 comment rsquo; 的处理方式,单独建立一个字段 lsquo;RefundState 退款状态 rsquo; 记录 lsquo; 退款 refund rsquo; 产生的 BizState,这个状态字段的字典值有:退款中,退款成功。

其他情况考虑

另外,可能还有一些增强型需求,让客户体验更好,比如用户可以创建订单之后付款之前,将订单取消,或者由系统跑批将用户长时间未支付的订单关闭,这会产生一种新的 action mdash; mdash; lsquo;close 关闭 rsquo;,对应的会产生一种新的有意义的 BizState mdash; mdash; lsquo; 订单关闭 / 取消 rsquo;,这个不属于核心流程中的,且并无纠结之处,不予详细讨论,罗列如下:

创建订单失败(终态)

等待买家付款

付款确认中

买家付款失败(终态,依赖需求而定)

买家付款成功

卖家已发货

买家已收货

退货中

退货成功(终态)

订单关闭(终态)

结论

综上,我们可以得出放入数据库 rsquo; 订单状态 lsquo; 字段的标准:核心业务流程,向前单向依赖。扩展到其他业务实体是一样的,这里说的 rsquo; 订单状态 lsquo; 字段实际是指该业务实体对应的数据表的主业务状态字段。我们把结论扩展一下:

如果某个 action 属于业务实体对应的核心业务流程,且该 action 单向依赖于其前向的 action,则需要将这个 action 产生的 BizState 放入到业务实体对应的数据库表的主状态字段中记录。

OrderState 字段记录的 BizState 业务状态有 10 种,其中 4 种是终态,其余状态为中间态。这些状态的流转关系为:

4. 问题二、订单表的 lsquo; 订单状态 rsquo; 字段的字典值的表示形式?

先列出可选项:使用数字标识、使用多 lsquo; 位 rsquo; 存储方式标识、使用具有明确业务含义的英文字符串标识; 对可选项做逐一解释:

a、使用数字标识 mdash; mdash; 使用一个数字标识一种状态,并未要求是 sequence 的; 如 lsquo; 等待买家付款 rsquo; 表示为 lsquo;0 rsquo;;

b、使用多 lsquo; 位 rsquo; 存储方式标识 mdash; mdash; 将某种行为是否发生对应的状态对应到一个位上,比如 lsquo; 是否付款 rsquo; 定义在 *** 位,lsquo; 是否发货 rsquo; 定义在第二位,lsquo; 是否收货 rsquo; 定义在第三位,lsquo; 是否评论 rsquo; 定义在第四位,则状态 lsquo; 卖家已收货未评论 rsquo; 可以表示为:0111; 而 lsquo; 等待买家付款 rsquo; 则表示为 lsquo;0000 rsquo;; 当然这里的 lsquo; 位 rsquo; 可能是二进制的也可能是 N 进制,后面我们详细讨论。

c、使用具有明确业务含义的英文字符串标识 mdash; mdash; 该方案和方案 a 类似,不过字典值变为具有明确业务含义的英文支付串,如 lsquo; 等待买家付款 rsquo; 表示为 lsquo;WAIT_BUYER_PAY rsquo;;

方案 a 是数据库字段字典的惯用方式,简单直观,但是有一个坏处在于:当字典值较多时,数据库表的使用者记不住字典的含义,需要反复查找资料确认; 有人会说将字典值写到字段的注释里,这个在实践中不是很靠谱,通常表建立后,如果字段增加了字典值,通常开发人员都会忽略更改字典值; 而且在使用工具 (如 pl/sql) 查询数据库时,并不会将所有字典值展示出来;

通过问题一的分析,可知:方案 b 使用多 lsquo; 位 rsquo; 存储方式会增加复杂度,并没有必要,可以通过将 lsquo; 是否评论 rsquo; 状态独立成一个字段进行表示。

方案 c 和方案 a 类似,好处在于通过字典值直接知道业务含义,坏处在于会给编码和手工查询时带来复杂度,通常人们也记不住 lsquo; 等待买家付款 rsquo; 的英文字典

是 lsquo;WAIT_BUYER_PAY rsquo;,那么手动写 sql 查询 lsquo; 等待买家付款 rsquo; 时就犯迷糊了。

折中之后,我们组合方案 a 和方案 c,得到方案 d:另外建立一张字典表,存储:数字形式的字典值、字典英文名称、字典中文简称、字典解释; 订单实体表的 OrderState 字段使用数字作为字典值。

对于方案 d,看到 OrderState 的数字形式状态时,可以先看看字段注释是否有此字典的定义,如果没有就取查下字典表,得到字典值和含义; 在编码和手动 sql 查询时也会变得比较容易,数字的位数毕竟要少些; 建立字典表的其他好处还有:字典的解释可以写的很详细,在报表中要求展示字典中文名时,也能直接从数据库联表查询得到,而不必额外做一次映射。(有参考:数据库表设计(状态字段))

那么对于字典数量很少的状态字段是否有必要额外新建一张字典表呢? 这个根据实际情况考虑,通常可以先不建,如果后续有业务场景需要再行创建也不迟。

而对于非业务实体表的系统日志 / 跑批记录表等的状态,则完全可以使用数字形式的字典,因为通常不会有业务场景使用到这些字典值,而且这些字典值域应当会比较小,所以没有必要为他们创建单独的字典表。

综上得出结论:

1)、字典值域较多、变化较多、报表等业务场景会使用到的业务实体表的业务状态字段,使用 lsquo; 方案 d:新建字典表 rsquo; 的方案处理; 如 lsquo; 订单业务实体表 rsquo; 中的 lsquo; 订单状态 rsquo; 字段。

2)、字典值域较少、变化较少、报表等业务场景不会使用到的业务实体表的业务状态字段,使用 lsquo; 方案 a:使用数字标识字典 rsquo; 的方案处理; 如 lsquo; 支付宝的支付流水表 rsquo; 的 lsquo; 支付流水状态 rsquo; 字段。

3)、系统日志 / 跑批记录表的状态字段,使用 lsquo; 方案 a:使用数字标识字典 rsquo; 的方案处理; 如 lsquo; 待收货记录表 rsquo; 的 lsquo; 跑批状态 rsquo; 字段。

5. 问题三、数据库表的 lsquo; 状态 rsquo; 字段使用何种类型

列出可选项:number(N)、char(N)、varchar2(N),其中 N 是一个长度值。

这个问题主要需要考虑使用场景、扩展性、性能、存储。

lsquo; 状态 rsquo; 字段主要使用在查询场景,且通常是 lsquo;= rsquo; 或者 lsquo;in rsquo; 的查询,并没有区间类的查询,故三者差别不大;

对于性能,参考 [原创] 在 Oracle 10g,Number、Char 和 Varchar2 类型作为主键,查询效率分析  char(N)、varchar2(N)性能优于 number(N),故舍弃 number(N)。

考虑到扩展性,char(N)、varchar2(N)差不多;

考虑到存储,varchar2 更加占用空间更小,故选择 varchar2(N)。

综上:选择 varchar2(N)作为数据库 lsquo; 状态 rsquo; 字段的类型。

6. 问题结论汇总

1)、订单表的 lsquo; 订单状态 rsquo; 字段对应的字典值应当包含哪些状态值? 对于 lsquo; 已评论 rsquo;、lsquo; 已退货 rsquo; 这类状态是放到 lsquo; 订单状态 rsquo; 中? 还是独立一个字段标识?

如果某个 action(行为,如支付)属于业务实体对应的核心业务流程,且该 action 单向依赖于其前向的 action,则需要将这个 action 产生的业务状态放入到业务实体对应的数据库表的主状态字段中记录。

问题中的 lsquo; 已评论 rsquo; 由 lsquo; 评论 rsquo; 行为产生,而 lsquo; 评论 rsquo; 这个 action 并不是订单业务实体的核心业务流程,且可能存在多个前向依赖 action(支付、发货、收货等),所以应当独立到一个字段标识。

问题中的 lsquo; 已退货 rsquo; 由 lsquo; 退货 rsquo; 行为产生,而 lsquo; 退货 rsquo; 这个 action 是订单业务实体的核心业务流程,用户非常关心,且只单向依赖于 lsquo; 收货 rsquo;action,所以应当记录到订单业务实体表的 lsquo; 订单状态 rsquo; 字段中。

问题中的 lsquo; 已退款 rsquo; 由 lsquo; 退款 rsquo; 行为产生,而 lsquo; 退款 rsquo; 这个 action 是订单业务实体的核心业务流程,用户非常关心,但是这个 action 存在多个前向依赖 action(支付、发货、收货等),所以应当独立到一个字段标识。

2)、订单表的 lsquo; 订单状态 rsquo; 字段对应的字典值如何表示? 可选项有:使用数字标识、使用多 lsquo; 位 rsquo; 存储方式标识、使用具有明确业务含义的英文字符串标识;

i、字典值域较多、变化较多、报表等业务场景会使用到的业务实体表的业务状态字段,使用 lsquo; 方案 d:新建字典表 rsquo; 的方案处理; 如 lsquo; 订单业务实体表 rsquo; 中的 lsquo; 订单状态 rsquo; 字段。

j、字典值域较少、变化较少、报表等业务场景不会使用到的业务实体表的业务状态字段,使用 lsquo; 方案 a:使用数字标识字典 rsquo; 的方案处理; 如 lsquo; 支付宝的支付流水表 rsquo; 的 lsquo; 支付流水状态 rsquo; 字段。

k、系统日志 / 跑批记录表的状态字段,使用 lsquo; 方案 a:使用数字标识字典 rsquo; 的方案处理; 如 lsquo; 待收货记录表 rsquo; 的 lsquo; 跑批状态 rsquo; 字段。

3)、订单表的 lsquo; 订单状态 rsquo; 字段使用何种类型? 可选项有:number(N)、char(N)、varchar2(N);

varchar2(N)占用存储更少,且具有同等的性能、扩展性,选择 varchar2(N)作为数据库 lsquo; 状态 rsquo; 字段的类型。

7. 参考资料

数据库表设计(状态字段)

[原创]在 Oracle 10g,Number、Char 和 Varchar2 类型作为主键,查询效率分析

以上就是如何进行数据库“状态”字段设计的思考与实践,丸趣 TV 小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-19发表,共计9884字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)