最近参与的中小学生健康档案系统,涉及到很多标签数据的存储,这里做一个梳理和整理。
说明:下面所说的存储都是指在MySQL上的存储,暂时不考虑mongodb、ES这些,毕竟引入新的数据库,会让系统更加复杂。
标签相关需求
基于标签的功能通常有几种:
- 查询某些标签关联的item有哪些;
- 统计标签的分布情况,例如统计某个范围内item对应标签的分布情况;
- item关联标签的增删。
标签通常还有两类比较不同的形态:
- 单一标签,例如视力标签,要么只能是正常/近视/远视三者中的一种;
- 多标签,例如文章的标签,一篇文章通常可以有多个标签。
这两类标签的存储和应用应该是很大不同的,对于单一标签的存储是比较简单的,通常是直接设置为字段。
多标签存储设计
基于对MySQL固有认知,标签系统设计方案通常有几种:
第一种:使用关联表对标签进行关联,这是常见的比较保险的设计
代码语言:javascript复制item表:
id, title, ...
tag表:
id, name, ...
item_tag表:
item_id, tag_id, ...
好处:支持任意多的标签;
坏处:对分析类需求不是太友好,例如进行关联分析等。
第二种:大宽表,就是把所有标签直接设置为字段:
代码语言:javascript复制item表:
id, title, tag_1, tag_2, ...
好处:只有一个表,查询可以非常高效,特别是在分析时优势明显;
坏处:不能支持太多的标签数量,增加标签的时候,可能需要更改表结构。
第三种:直接把标签拼成字符串进行存储:
代码语言:javascript复制item表:
id, title, tags...
tag表:
id, name, ...
tags字段可以将众多的标签id用逗号分隔进行存储,这里也可以不用tag表,直接把tag值存储到item表中。
好处:比较简单,不需要维护关联表;
坏处:对查询和统计非常不友好,索引基本无效(也不是完全无效,使用全文索引应该是有效果的);另外,这个对增删也很不友好。
第四种:直接使用二进制标志位:
代码语言:javascript复制item表:
id, title, bin_tags...
bin_tags定义为二进制标签字段,每个二进制位表示某个标签是否存在,通常最多只能适用于标签数量固定,且小于64(长整型)的情况(可以使用bin类型突破这个限制,不过这个类型比较少用,不知道兼容性怎么样)。
好处:相对于第三种,非常节省空间,位运算也比直接使用like进行匹配高效很多;
坏处:使用场景非常受限,标签数量稍微多点或者标签可能变化比较大的时候,就很不适合,另外索引通常也是无效的。
基于JSON类型的多标签存储方案
MySQL支持json数据已经有一段时间了,只是一直还没使用过,关于json类型的使用,看这篇文章,这里不过多介绍。简单说,就是MySQL其实直接直接存储json数据,而且还能对其进行索引,这正好可以解决标签存储及查询问题,这时表结构应该类似这样:
代码语言:javascript复制item表:
id, title, tags...
tag表:
id, name, ...
这时tags字段应该存储的是json数据,格式如:[1,2,5] 表示这个item关联的标签有id为1,2和5的三个标签,这里不直接使用标签名而是使用id,主要是为了解决提升索引的效率。
不需要维护关联表,使用简单,可读性也好,而且还能支持索引,表面上看是比较完美解决了这个问题。
不过这个设计在大数据量下性能怎么样,有待验证,网上的文章很少有涉及大数据量下的性能数据,可能兼容性还不够好,使用的人还比较少。(相对而言,可能PG数据库对json的支持会比较完善些,不过我们使用的是MySQL)
多种单一标签的存储设计
在我们系统里,有一个应用场景是这样的:每个学生会有多个类别的健康相关的标签,例如视力/营养等,每个类别下是单标签,例如视力只能是正常/近视/远视三者之一。
这种单一标签正常考虑肯定是直接放到数据表的字段上,这样查询/统计都很方便(例如统计视力的分布情况),对分析师也很友好。不过这里设计的困惑在于:
- 标签的类别很多,造成表的字段很多,这可能会对MySQL造成压力,不过对列式存储的数仓应该不成问题;
- 这些标签的类别没法保证将来不会变化,放到字段里可能会加大后续的维护工作量。
如果使用关联表:
而如果把标签独立成表,通过关联表进行关联,这样确实以后的扩展性确实可能会好些,不过查询性能和统计性能可能就会比较差一些,而这个正是这个项目重点要解决的问题。
如果使用json:
这可能是一个比较好的方案,开始也是这么想的,但是在我们的这个场景暂时却不是很适用,因为我们这些数据会被实时同步到数仓中,而合作的数仓暂时还不支持json结构。对于不需要同步到数仓的一些业务逻辑表,使用json应该是比较合适的,只要保证MySQL的版本比较高就行。
所以,在这个场景下,采用大宽表独立成字段可能暂时是目前比较合适的方案。