一、前言
在很多应用场景中,通常需要给数据加上一些标识,以表明这条数据的某个特性,如:标识用户的性别、标识订单支付的渠道、标识商品的类型等等。在数据库设计时,通常我们会单独用一个字段来存储这些标识,如:可用gender字段来标识用户的性别,其值为“男”、“女”、“未知”这3种值中的一个;对于普通的具有有限固定的几个值的标识,这样自然没有什么问题,但是,对于一些同时具有多个属性且变化较大的就有些不合适了,比方说,标识智能手机的商品的类型时,其类型可标识为电子商品、娱乐商品、数码商品等多种类型,其具有的可分类属性众多且不固定;这些,就是本文需要讨论的:关于数据库设计时,给数据设计标识字段时的一些思考。
二、常见场景、问题与解决方法
场景一:
问题与分析:
用户是任何系统中最为重要的组成部分之一,在设计存储用户信息时,性别是用户信息的重要组成部分,应该如何存储呢?性别有 “男”、“女”2种情况,如果某个用户没有具体性别数据的话,可以标识为“未知”,因此,每一个用户,性别有“男”、“女”、“未知”共3种情况,我们需要在数据表字段中需要存储这3种情况的一种(实际应用可能需要更多情况以满足具体业务,这里姑且以常见的3种为例)。
思考与方法:
方法一:在用户信息表中,添加一个性别字段,其数据类型为char,其值为“未知”、“男”、“女”中的一个,当需要使用该值时(前端展示、分析统计等),直接取出使用即可。
方法二:在用户信息表中,添加一个字段,其数据类型为tinyint(一个字节),其值为 “0”、“1”、“2”,分别对应“未知”、“男”、“女”这3种情况,其中0对应的未知为默认值。当需要使用该值时,可针对不同的使用场景进行简单的转换,如前端需展示时可由数字转换成相应含义文字后再展示。
方法一的优点是语义明确、便于前端展示,不足之处有相对占用存储空间多、数据传输更耗流量等;方法二的优点是相对占用存储空间少、数据传输更省流量、一定程度上有利于数据统计分析,不足之处有展示时需要转换、数字语义需要约定等。笔者个人推荐使用方法二。(换个角度看,需要转换也带来了一定的灵活性,如:若某个业务需要展示性别为“保密”、“帅哥”、“靓女”,这样我们只需修改转换的地方即可,而不需修改数据库数据,这也有利于一个用户中心为不同的具体业务线提供服务)
场景二:
问题与分析:
电商平台通常会划分商品品类,如服饰类、食品类、数码类、书籍类等等,而有的商品可能具有多个商品品类属性,如智能手机,其既可划为数码类、又可划分为手机类、智能设备类,常见的场景是显示商品所属品类、修改商品的所属品类、查询某个品类下有哪些商品等,在这种情况下,该如何存储呢?
思考与方法:
假如平台的商品品类共有n种,那么,理论上某个商品的所属品类可能的组合就有[2^n-1]种情况,当然,实际上不会有这么多组合,但像用户选择兴趣爱好之类的场景可能的组合就多了,因此,上文场景一中的2种方法已不再适用。
可行的一个方法是:再引入一个品类关系表,用于存储商品品类与各商品的关系。这样的话,商品信息表本身用一个字符字段直接存储其所属品类信息,以满足基本需求,而操作某个品类下的商品的相关业务,则可通过品类关系表去做;一旦发生数据的变化,则同时维护这2个地方的数据。(事实上,通常的做法是,一件商品最多只能划分为有限的商品品类,一个人最多只能选择有限个兴趣爱好,如:一件商品最多只能所属3种品类,一个人最多只能选5个兴趣爱好)
场景三:
问题与分析:
在电商系统中,通常会通过各种优惠的方式来促销,如给用户发各种优惠券、积分抵扣等,用户提交订单时可以使用满足条件的各种优惠;那么,如何存储该订单具体使用了哪些优惠信息呢?
思考与方法:
与以上两种情况有所不同,以上的两种场景更多的业务场景是前端的展示与业务查询,而这种场景更多是标识优惠以计算用户实际所需支付金额,以及为后续业绩统计、制定促销计划、提高用户活跃度等提供数据依据。
这里我们举一个具体例子来逐步分析。
实例分析:
假设某平台为A平台,其平台当前可使用的优惠方式有以下几种:
序号 优惠内容 使用条件 是否长期有效 备注
1 用户账户余额 直接抵扣现金 是 用户充值所得(平台奖励吸引的充值,如:充100送10元)
2 平台积分 100积分抵扣1元 是 通过参与平台活动、购物行为积累获取
3 平台币(A币) 直接抵扣,1个币抵扣1元 是 平台奖励(类似Q币之类的概念,姑且叫A币)
4 满减卷5元 满100减5元 否 平台活动促销发放
5 免邮费 订单总金额符合条件即可 是 平台单笔订单总金额满199元免邮费
用户下单时,只要是满足各优惠的使用条件,就可以叠加使用各种优惠,那么,数据库如何存储用户具体使用了哪些优惠呢?(实际各种促销优惠可能更多,尤其是各种优惠券,这里姑且以5种举例)
分析:
在这个业务场景中,用户下单时最多可以同时使用5种优惠抵扣方式,用户可能使用的优惠组合共有 2^5-1=31 种,在最终计算用户的订单实际需要支付的金额时,如何标识并存储用户到底使用了哪种优惠组合呢?
如果单独用一个普通标识字段来标识存储,实现起来是比较简单,但是其需要标识的组合种类实在有点多,不太利于编码与后续扩展,试想,如果新加了一种优惠类型,其需要添加多少种组合标识啊,且呈指数式爆长,这种方式显然不太合理。如果采用另外引入一张关联表的方式,专门用一张关联表来存储订单使用的优惠组合信息,每使用一种优惠就添加一条关联记录,相比单独使用普通字段标识,这在一定程度上减少了设置标识的繁琐性,增加了灵活性(每多使用一种优惠就添加一条关联记录),但是,同时也带来了另一些问题,其中主要问题是:新增一张关联表后,数据维护起来麻烦。在互联网场景下,数据量通常是非常大的,像订单数据一般都需要进行数据库sharding,以应对数据量暴涨后数据库的读写性能瓶颈,增加系统的水平扩展能力。因此,另外增加一张数据量是订单数据本身数据量几倍的关联表也显然不太合适。
那么,有没有一种方式既方便标识存储又方便扩展呢?
我们试着以一种“特殊标识位”的方式来实现,具体思路如下:
a、定义一个标识位 mask 用于标识存储优惠信息;
b、mask存储的值并不是直接存1、2、3之类的十进制数字,而是存储一个二进制数转化后的十进制数,这些1、2、3之类的优惠数字表示占二进制数的第几位(从右至左数);
c、具体数据的存储、读取判断通过工具类转换进行。
例:
int 数据类型4个字节,共32位,除去符号位,可用于标识的位数有31位,即最多可以标识31种优惠情况,而如果是long数据类型的话,能标识的种类就更多了。
在以上共5种优惠方式场景中,可按如下标识存储:
说明:若用户使用了优惠1,则使用二进制数 00000001 标识,若使用了优惠2,则使用二进制数 00000010 标识,存储到DB时,转换成对应十进制数分别对应1、2;若同时使用了优惠1、优惠2,则使用二进制数 00000011 标识,最终存储到DB的对应十进制数是3。其它优惠项,所占的二进制位依次类推。
代码示例如下:
DiscountEnum.java
作者:对门山上
来源:CSDN
原文:https://blog.csdn.net/cndmss/article/details/54232738