今天方才就以财务系统的科目余额相关需求为例,给大家展示下在实际的企业级项目中,如何应用该方法论进行数据库表结构设计。
通过这个示例,我相信大家会发现业务需求分析和技术方案的设计才是表结构设计的关键,最终关于表结构和索引的设计是可以通过参考人家的经验贴快速掌握的,但业务分析能力和技术方案设计能力,需要长期的刻意练习以及在业务领域的深耕才能有所成就。
image-本文目录导航
方才想再次强调一下:技术是为业务服务的,所以对业务需求的分析是最最最重要的,只有理解了需求,才有可能设计出合适的技术方案,从而设计出相对最优的表结构。
先看方法论中关于需求分析的可选维度:
image-20250120162524223
接下来参考上图,我们开始对财务系统的科目余额相关需求进行分析(考虑大家可能没有接触过财务系统,以下分析更多会用大白话解释,而不是专业的业务概念去解释):
相关的概念如下:
和会计科目余额相关的业务行为(其他无关的,方才就省略掉了):
业务模型如下图(ps:业务概念的分析,是为了后续的技术方案的设计):
image-20250122172207204
理解了业务概念和业务行为后,就需要对相关业务进行数据量预估,这样在设计技术方案时,才能建立有效的评估指标,在本次分析的财务系统的会计科目余额相关的业务,核心的业务数据量情况如下:
ps:数据预估基础指标来源是商务和业务资料的综合分析得出,这里就不细说了。
考虑整体篇幅,后续方才就仅讨论会计期间维度的科目余额的表结构设计。所以只需要关注 会计科目数 10亿 和 凭证分录数 40亿 这两个指标了。
前提说明:在这个量级的系统设计中,会先有业务架构设计-》应用架构设计&数据架构设计-》部署架构设计,此处讲述的技术方案属于应用架构设计中代码级别的设计。 本文的重点是讲解表结构设计,所以这里先明确一个前提:应用架构已经设计完成,数据库选型为分布式数据库TiDB,不需要分库分表(大家若对海量数据下分布式数据库TiDB的实战经验感兴趣的,可以在评论区告诉方才哟)。
基于上面的前提条件,我们就直接开始拟定关于会计科目余额计算相关的技术方案了。整个思路如下:
image-20250120164205451
结合之前的业务分析,会计科目余额相关的依赖如下:
所以整个方案的关键就是:
经过头脑风暴,拟定3个可选方案(ps:这里更多是感知整个分析的过程,对于方案的具体的内容和逻辑,方才这里做了省略):
方案的指标对比:
评估指标 | 方案1 | 方案2 | 方案3 |
---|---|---|---|
实现复杂度 | 本质都一样,均是根据 科目初始化数据+会计凭证&分录数据计算,区别就是中间结果是否落库 | 本质都一样 | 本质都一样 |
可能存在的性能点 | 跨期间查询时,需要查询对应期间的所有凭证分录数据,按平均值计算,需要查询4000条数据,但考虑峰值,可能会涉及查询到10万级别的数据在内存中计算的情况,可能会导致应用内存溢出或数据库压力过大拖垮整个系统。 | 科目余额数据更新频繁,每次有凭证更新,均需要更新对应层级树的所有的指标;同时会计科目余额表数据量会达到每年 36亿级别,对数据库资源的需求更大,且数据量上去后查询压力较大。 | 若所有科目在每个会计期间均有凭证发生,会和方案2存在同样的问题,但这几乎不可能。相比方案2,会计科目余额表数据量能缩减5-10倍,每年在 7亿级别,同时更新的数据量也会更小,无性能风险。 |
可扩展性:以指标公式更新为场景 | 仅需要更新代码,无侵入 | 更新代码,极端情况需要重算 36亿级别的数据,同时因为记录是全量指标,发生概率比较大 | 更新代码,极端了情况需要重算7亿级别的数据,相对而言指标很少,发生概率是比较小的。可通过大数据组件重算回写。 |
线上数据分析 | 数据未入库,若用户反馈数据异常,分析难度较大。需提供单独的逻辑将数据临时落库,便于分析。 | 所见即所得,用户看到的数据,数据库都有,可以快速分析出是什么指标的问题 | 指标落库不全,若用户反馈数据异常,分析难度较大。需提供单独的逻辑将数据临时落库,便于分析。 |
通过多维度对方案的对比,最终确定使用方案3进行落地实施(通过对比,可以感知到不同的方案,会计科目余额表的结构是不一样的,甚至都不需要有)。
有了确定的技术方案后,就进入到了完整的表结构设计阶段。
主要思路是参考数据库范式&反范式设计,结合阿里巴巴规约,以及历史经验的总结,完成从表名、字段名、字段类型的定义。
image-20250120163525047
DDL
unsetunset先简单看下完整的表结构,然后我们再完整讲解表结构设计一些技巧。
CREATE TABLE`kjkm_fse` (
`id`bigint(20) UNSIGNEDNOTNULL AUTO_INCREMENT COMMENT'主键',
`zt_id`bigint(20) UNSIGNEDNOTNULLCOMMENT'账套id',
`kjkm_id`bigint(20) UNSIGNEDNOTNULLCOMMENT'会计科目id',
`kjkm_bm`varchar(64) NOTNULLCOMMENT'会计科目编码-冗余字段,便于查询',
`kjqj_id`bigint(20) UNSIGNEDNOTNULLCOMMENT'会计期间id',
`ljjf_je`decimal(22,2) NOTNULLDEFAULT'0.00'COMMENT'本期借方发生额',
`ljdf_je`decimal(22,2) NOTNULLDEFAULT'0.00'COMMENT'本期贷方发生额',
`bqs_je`decimal(22,2) NOTNULLDEFAULT'0.00'COMMENT'本期数-金额(借-贷,结合科目方向计算得到)',
`bqs_sl`decimal(22,2) NOTNULLDEFAULT'0.00'COMMENT'本期数-数量',
`created_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',
`updated_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',
`create_user_id`varchar(32) NOTNULLCOMMENT'创建人用户id',
`update_user_id`varchar(32) NOTNULLCOMMENT'更新人用户id',
PRIMARY KEY (`id`,`zt_id`),
UNIQUEKEY`uk_zt_kjqj_kjkm_id` (`zt_id`,`kjqj_id`,`kjkm_id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=1COMMENT='会计科目-发生额'
PARTITIONBYHASH (`zt_id`) PARTITIONS3;
ljjf_je
(本期借方发生额)和ljdf_je
(本期贷方发生额)分别独立存储,每一列都是不可分割的原子数据项。zt_id
(账套ID)、kjqj_id
(会计期间ID)和kjkm_id
(会计科目ID)组合成唯一键,确保了表中的数据与这些关键字段的强关联性。 kjkm_id
(会计科目Id) 和 kjkm_bm
(会计科目编码)。这种设计虽然增加了存储空间,但减少了查询时的连表操作或计算成本,能提高查询性能。bqs_je= ljjf_je - ljdf_je
是通过预计算得到的扩展字段,也是为了满足查询需求。包括表名和字段名,参考以下几点:
uk_/ idx_
**:唯一索引名为 uk_
字段名;普通索引名则为 idx_
字段名;命名规范还是很好理解的,大家日常应该是使用英语单词更多点,财务这块名词太长,所以方才使用的是中文的首字母缩写(只要整个库保持一个风格,可读性也是很高的)。
参考阿里规约,建议表的必备字段有3个:id, create_time, update_time。
说明:其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。create_time, update_time 的类型均为 datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。
这个可以根据实际情况,自己去约定,比如方才建的表就有5个必备的字段:
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`created_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',
`updated_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',
`create_user_id`varchar(32) NOTNULLCOMMENT'创建人用户id',
`update_user_id`varchar(32) NOTNULLCOMMENT'更新人用户id',
注意:
updated_time
字段要有 ON UPDATE CURRENT_TIMESTAMP
属性,该属性用于指定当表中的记录被更新时,该字段的值会自动更新为当前的时间戳(CURRENT_TIMESTAMP
),这个特性通常用于记录数据最后被修改的时间。create_user_id
是varchar(32)
类型,在此处是为了兼容历史数据,一般情况也应该是 bigint(20) UNSIGNED
。字段类型要尽量和实际类型保持一致,然后优先选择最小的存储长度类型即可。
参考MySQL官网:https://dev.mysql.com/doc/refman/8.4/en/integer-types.html 。
不同整数类型的存储大小和取值范围:
Type | Storage (Bytes) | Minimum Value Signed | Minimum Value Unsigned | Maximum Value Signed | Maximum Value Unsigned |
---|---|---|---|---|---|
TINYINT | 1 | -128 | 0 | 127 | 255 |
SMALLINT | 2 | -32768 | 0 | 32767 | 65535 |
MEDIUMINT | 3 | -8388608 | 0 | 8388607 | 16777215 |
INT | 4 | -2147483648 | 0 | 2147483647 | 4294967295 |
BIGINT | 8 | -2^63 | 0 | 2^63-1 | 2^64-1 |
按最小存储长度原则:
TINYINT
;BIGINT
(上次抖音出现点赞数为负数,就是因为 int
溢出了);BIGINT
;若是字典维护表这种内部使用的,主键就可以用 INT
。补充:关于整数类型在DDL
中定义时括号中的数字的含义是显示宽度:比如 int(11) 中的 (11)
是一个 显示宽度(主要用于 ZEROFILL
选项时,指定数字显示时前面填充的零的数量),而不是数据类型的实际存储长度或精度,整数类型的存储长度是固定了的,int
类型的储空间始终是 4 字节。
对于小数类型,建议遵循阿里巴巴规约,使用decimal
类型(如decimal(22,2)
),禁止使用float
和double
,以确保数据的精确性。
注意: DECIMAL(5,2)
表示能存储任何具有 5 位数字和 2 位小数的值,值范围为 -999.99
到 999.99
。
当然对于金额字段的存储,一般有两种方案,一种是使用 decimal
类型 保留2位小数,单位一般为元及以上;另一种就是使用BIGINT
类型,将单位转为分,进行存储。
MySQL中字符串类型,包括 CHAR
、VARCHAR
、BINARY
、VARBINARY
、BLOB
、TEXT
、ENUM
以及 SET
。这里方才就只讲几个重点了:
CHAR(M)
是定长字符串。CHAR
列的长度固定为创建表时声明的长度。M 表示列长度(是字符的个数,不是字节的个数),当存储字符长度不足M时,会用空格右填充到指定长度。适用于存储枚举code等场景。VARCHAR
是变长字符串。M 表示最大列长度(字符的最大个数)。VARCHAR
的空间占用大小不得超过 65535 字节。在选择 VARCHAR
长度时,应当根据最长的行的大小和使用的字符集确定。是最常用的字符串类型。VARCHAR
列长度的取值范围(关于字符集后续会讲):字符集 | 单个字符字节数 | VARCHAR 最大列长度的取值范围 |
---|---|---|
ascii | 1 | (0, 65535] |
latin1 | 1 | (0, 65535] |
binary | 1 | (0, 65535] |
utf8 | 3 | (0, 21845] |
utf8mb4 | 4 | (0, 16383] |
也就是说字符集为utf8mb4
的 varchar类型的最大长度只能是 16383,若超过,你建表时会报错:
image-20250122151028028
类型,最大列长度为 4,294,967,295 字节;或者 使用二进制大文件
LONGBLOB` 类型,最大列长度为 4,294,967,295 字节。text
类型,且独立出来一张表,用主键来对应,避免影响其它字段索引效率(后续讲解了聚簇索引就可以理解为什么了)。在我们当前这个示例中,使用varchar
即可:
`kjkm_bm` varchar(64) NOT NULL COMMENT '会计科目编码-冗余字段,便于查询',
MySQL 的日期和时间类型,包括 DATE
、TIME
、DATETIME
、TIMESTAMP
以及 YEAR
。
DATE
类型只包含日期部分,不包含时间部分。DATE
类型的格式为 YYYY-MM-DD
,支持的范围是 0000-01-01
到 9999-12-31
。TIME
类型的格式为 HH:MM:SS[.fraction]
,支持的范围是 -838:59:59.000000
到 838:59:59.000000
。DATETIME
类型是日期和时间的组合,格式为 YYYY-MM-DD HH:MM:SS[.fraction]
。支持的范围是 0000-01-01 00:00:00.000000
到 9999-12-31 23:59:59.999999
。TIMESTAMP
类型是日期和时间的组合,支持的范围是 UTC 时间从 1970-01-01 00:00:01.000000
到 2038-01-19 03:14:07.999999
。注意:TIMESTAMP
数据类型受 2038 年问题的影响。如果存储的值大于 2038,需使用 DATETIME
类型。TIMESTAMP
时,MySQL 会将当前时区的 TIMESTAMP
值转换为 UTC 时区。当读取 TIMESTAMP
时,MySQL 将存储的 TIMESTAMP
值从 UTC 时区转换为当前时区(DATETIME` 不会这样处理)。YEAR
类型的格式为 YYYY
,支持的值范围是 1901
到 2155
,也支持零值 0000
。所以,方才建议,日期和时间类型字段,优先选用DATETIME
,同时要合理利用其自动初始化或更新为当前时间的特性,比如说创建时间和更新时间:
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
什么是物理删除?什么是逻辑删除?
is_deleted
,以标记该数据已经逻辑删除。根据方才的经验,建议是优先采用逻辑删除。更方便去最终一些问题或者手动回滚数据等。
但若满足以下场景,是更适合使用物理删除的:
这个场景下,无用的数据太多,会影响到查询和更新的效率了。
而刚好,会计科目余额表就符合这个场景,数据量超10亿,凭证更新会导致数据的频繁覆盖写入,同时本身数据就是可以通过期初数据+凭证数据计算得来,所以可以看到kjkm_fse
这个表是没有is_deleted
字段的。
个人建议表中字段尽量不超过20个,最多不超过50个。
理由是:因为MySQL
的聚簇索引特征,过多的字段会导致回表操作成本过高,影响查询性能。
字符集影响的是字符串类型的存储,包括能否存储以及一个字符对应的字节长度。
MySQL
支持的字符集有utf8、utf8mb4、GBK、latin1
等。
方才推荐字符集优先选择utf8mb4
,支持更广泛的字符集范围,通过建表语句 CHARSET=utf8mb4
可以指定。
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=1 COMMENT='会计科目-发生额'
PARTITION BY HASH (`zt_id`) PARTITIONS 3;
排序规则会影响对字符类型的排序以及查询,是非常重要的,一不小心就容易出现bug,且很难定位。
一个字符集可以有多种排序规则。排序规则的命名格式为 <character_set>_<collation_properties>
。例如,utf8mb4
字符集有一个名为 utf8mb4_bin
的排序规则,它是 utf8mb4
字符集的二进制排序规则。下表是常见字符集和排序规则的后缀和含义:
后缀 | 含义 |
---|---|
_bin | 二进制排序规则,区分大小写 |
_ci | 不区分大小写 |
_ai_ci | 不区分重音和大小写 |
_0900_bin | Unicode UCA 9.0.0,二进制排序规则 |
_unicode_ci | (较旧的)Unicode UCA 排序规则,不区分大小写 |
_general_ci | 较宽松的 Unicode 排序规则,不区分大小写 |
简单看两个示例就会理解深刻了。
下面这个示例,通过COLLATE=utf8mb4_general_ci
和COLLATE=utf8mb4_bin
分别设置不同的排序规则:
CREATE TABLE`fc_test` (
`id`bigint(20) UNSIGNEDNOTNULL AUTO_INCREMENT COMMENT'主键',
`mc`varchar(64) NOTNULLCOMMENT'名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='不区分大小的测试';
INSERTINTO fc_test( mc) VALUES('fangcaicoding'),('Fangcaicoding'),('cdoing');
CREATETABLE`fc_test2` (
`id`bigint(20) UNSIGNEDNOTNULL AUTO_INCREMENT COMMENT'主键',
`mc`varchar(64) NOTNULLCOMMENT'名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='区分大小的测试';
INSERTINTO fc_test2
( mc)
VALUES('fangcaicoding'),('Fangcaicoding'),('cdoing');
查询和排序下:
-- 不区分大小写
select * from fc_test where mc = "Fangcaicoding" order by mc;
select * from fc_test order by mc desc;
-- 区分大小写
select * from fc_test2 where mc = "Fangcaicoding" order by mc;
select * from fc_test order by mc desc;
运行截图如下:
通过这个示例,我想大家都一定理解了字符集的排序规则的影响点了。
一般情况,方才推荐排序规则优先选择utf8mb4_bin
,通过COLLATE=utf8mb4_bin
语句设置,区分大小写,确保数据的准确性和一致性。
关于索引,方才发现很多初中级程序员都没有形成一个方法论。很容易走两个极端,要么是除了主键没有其他任何索引,要么就是索引一大堆。
方才结合自己针对数十亿表的索引优化经验,总结如下:
image-20250120165626152
结合上面的脑图,针对kjkm_fse
这个表,我们来一一分析下。
方才建议所有的表均应该有主键,优先为数字类型,且保持自增性(若是辅助表,主键可以直接使用主表的)。
常用的主键生成机制有:
在kjkm_fse
表是有自增id
的,但因为数据量较大,使用了分区表(关于TiDB的分区表,大家若有兴趣,可以在评论区告诉方才哟),分区字段按规范需要作为主键的组合字段之一,所以该表的主键如下:
-- 省略了无效内容
CREATE TABLE `kjkm_fse` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键'
PRIMARY KEY (`id`,`zt_id`)
) PARTITION BY HASH (`zt_id`) PARTITIONS 3;
补充:如果一个表没有显示定义主键:
NOT NULL
和 UNIQUE INDEX
)作为聚簇索引。如果没有合适的唯一索引,InnoDB 会自动生成一个隐藏的 ROW_ID
列作为聚簇索引,这个隐藏列是递增的。_tidb_rowid
列作为行 ID。这个列的值是单调递增的。除了主键建议均有,其他索引的创建,是需要跟进实际情况进行判断的,方才总结规则如下:
方才建议,业务上有唯一特性的字段,必须建唯一索引或组合唯一索引:
理由:唯一键对 insert、update的性能损耗较小,对查询速度的提升是很明显的;同时根据墨菲定律,比如会产生脏数据
一定要注意:组合唯一键的所有字段均不能为空,否则可能导致唯一键约束失效。
uk_typeId_mc2
是没有锁住的,数据1-3-8在逻辑上都是重复的:image-20250122164336346
回到kjkm_fse
这个表,在业务上,就要求一个账套下,一个会计期间,同一个科目的余额一定是只能有一条数据,所以就创建了组合唯一索引:
UNIQUE KEY `uk_zt_kjqj_kjkm_id` (`zt_id`,`kjqj_id`,`kjkm_id`)
ps:业务上是有根据 kjkm_bm
会计科目编码查询需求的,但这里方才并没有针对该字段创建索引,是因为什么呢?可以参考索引的创建依据说明,业务上明确所有的查询,一定都会携带zt_id
账套id参数,根据该参数,已经可以将数据量过滤至5000以内了,就没有必要再创建了。
关于组合索引,方才就提两个技巧:
关于普通索引的创建,就优先参考上面的内容,有必要再创建。
需要注意的是,参考阿里规约,在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。(防止字段太长,索引内容过大,导致其他问题)。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。
我相信通过这个示例,大家对如何进行表结构设计,都有了自己的理解,也欢迎在评论区交流哟。
原创不易,如果对你有所帮助,记得帮方才点个赞、点个爱心,来个评论,这对方才很重要。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有