数据依赖与数据库范式——以术力口数据库为例

数据依赖、数据库范式这些东西挺绕的。这些概念需要慢慢说清。本文我会以术力口数据库为例,尽量清晰地介绍一下这些内容。

背景资料:术力口数据库

术力口数据库是统计 bilibili 的虚拟歌手外语歌曲的数据库,其数据包括但不限于:

  • 歌曲的(不止一个)作者、歌手、引擎
  • 歌曲在B站的(不止一个)视频
  • 视频的标题、BV号
  • 歌曲每天记录的播放、收藏、硬币、点赞总量
  • 歌曲在排行榜的排名、得分

数据依赖

数据依赖是通过属性之间值是否相等体现的数据之间的相互关系。之所以强调“值是否相等”,是因为“数值大于某个值”这种不确定的约束不是数据依赖。

简单来说,数据依赖就是属性和属性之间的对应关系。比如术力口数据库中,歌曲和视频之间的对应关系,视频和BV号之间的对应关系等。

数据依赖主要分为函数依赖和多值依赖。

函数依赖

函数依赖是“一对一”或“多对一”的关系。

对于函数 y=f(x),一个 x 就只对应一个 y,这就是一个 x → y 的映射。

同样, A → B 的函数依赖,就是说数据库中有一个属性 A 和一个属性 B,属性 A 的一个值就只对应属性 B 的一个值。

当然,A 并不一定是“一个属性”,也可以是多个属性组合而成的“属性组”。A 也可以同时决定许多的属性 B、C、D,不过 B、C、D 是互相独立的,我们可以把它们写在一起,但是它们其实是不同的函数依赖。

以术力口数据库为例,数据库中存在以下的函数依赖:

  • (单个属性)视频BV号 → 标题、UP主、投稿时间……
  • (单个属性)歌手 → 歌手代表色
  • (属性组)排行榜名称、排行榜期数、排名 → 歌曲名、视频BV号、得分、播放量……

多值依赖

多值依赖是“一对多”或“多对多”的关系。

多值依赖,意味着属性/属性组 A 并不能直接确定属性 B 的值,而是给出了多个值。

以术力口数据库为例,数据库中存在以下的多值依赖:

  • 歌曲名 →→ 作者、歌手、引擎、视频BV号、UP主
  • 歌手 →→ 歌曲名

我们可以看到,歌手与歌曲之间这种“多对多”的关系,其实从两个方向来看都是多值依赖。如果把这两个属性单独拿出来建立一个“多对多”的表,那么这个表中的多值依赖就是一种“平凡的多值依赖”。

第一范式

第一范式很简单。它要求数据库中的每一个属性都必须是不可分的原子属性。

术力口数据库在本地记录和制作视频的过程中,为了方便,使用直接用顿号分隔的方法记录一些多值字段,比如:

songauthorvocalsynthesizer
Intergalactic Bound雄之助、CircusP初音未来、KAITOVOCALOID

这种做法就违反了第一范式,因为这种多值属性需要拆分。我们应该让 author、vocal 等字段只储存一个名字。像这样:

songauthorvocalsynthesizer
Intergalactic Bound雄之助初音未来VOCALOID
Intergalactic Bound雄之助KAITOVOCALOID
Intergalactic BoundCircusP初音未来VOCALOID
Intergalactic BoundCircusPKAITOVOCALOID

当然,这个设计也不好。我们后面还会继续修改。

第二范式

第二范式要求没有部分依赖。也就是说,如果主键是由多个属性组成的联合主键,我们不应该让数据表中的属性依赖于主键中的部分属性。

术力口数据库的总量数据记录,里面有如下内容:

bviddateuploaderview
BV1EK4y1z7St2025-08-23初音未来_Crypton5,152,646

这个表的主键是 bvid 和 date,因为视频的 id 和日期对应一条数据记录。但是,uploader 仅仅依赖于 bvid,出现在这里是不合理的。这种存储方式意味着,如果我们想要修改一个视频的 uploader,就必须修改每一项的 uploader。所以应该拆表:

  • bviddate、view 属于数据记录表
  • bvid、uploader 属于视频信息表

第三范式

以下进入了较为复杂的领域。在讲下面的这些范式之前,我们先介绍一些重要但容易混淆的概念。

  • 超键:在数据表中能够唯一确定一行数据的属性/属性组。
  • 候选键:不包含多余属性的超键。候选键可能有多组,主键是用户从候选键中选择的其中一组。

第三范式的定义是,对于数据表中的任何一组候选键,它以外的属性只能被它决定。

所谓传递依赖,就是说数据依赖 A→B 和数据依赖 B→C 出现在了一张表中。这个时候,正确的做法是拆表,把A和B放在一张表,B和C放在一张表。

在术力口数据库的排名表中,存在这样的内容:

boardpartissuerankbvidsongviewfavoritecoinlikepoint
vocaloid-dailymain48765BV1EK4y1z7StIntergalactic Bound4,34025814431013,888

这个表有两处违反第三范式。

第一,这个排名表的联合主键是 (board, part, issue, rank)。这个主键对应一个唯一的 bvid,而 bvid 对应一个唯一的 song。这就导致了传递依赖。在这种情况下,如果我们想要改变一个 bvid 对应的 song,就必须修改这里每一行的 song。因此,我们可以在这张表中去掉 song 这个字段,在每次查询排名的时候联合查询一下歌曲与视频的关系表。

第二,这个排名表中的 point 是根据 view、favorite、coin、like 四项数据,再加上由 board 和 issue 唯一确定的算法决定的。也就是说,在这张表里存储 point 其实是多余的。我们可以把 point 属性删掉,改成在查询的时候动态计算。

BC 范式

BC 范式是第三范式的增强版。

BC 范式规定,对于数据表中的任何一组候选键,数据表中的每一个键都只能被它决定。与第三范式相比,它规定了“候选键自身不能被其他键决定”。

依然以术力口数据库的排名表为例。刚刚我们说过,这个表的联合主键是 (board, part, issue, rank)。然而这里面的 rank,不就是根据 (board, part, issue, point) 计算出来的吗?这就违反了 BC 范式。我们应该把这个表的主键改为 (board, part, issue, bvid),然后在查询的时候动态计算 point 和 rank。

所以,正确的排名表其实是这样的:

boardpartissuebvidviewfavoritecoinlike
vocaloid-dailymain487BV1EK4y1z7St4,340258144310

第四范式

第四范式的定义是,对于任意一个非平凡的多值依赖 X →→ Y,X 都是这个数据表的超键。这里的“非平凡”意味着,Y 不属于 X,而且 X 和 Y 合起来没有组成整个表。

刚才我们在第一范式那里写了一个拆分多值属性的表。之前说过那个表也不好,是因为它违反了第四范式。这个表中存在多值依赖 song → author。我们可以从它这里拆分出一个多对多的表格,只有 song 和 author 两个字段。

songauthor
Intergalactic Bound雄之助
Intergalactic BoundCircusP

由于 song 和 author 合起来就是整个表,所以它就是一个“平凡的多值依赖”,是可以使用的。同样,song 和 vocal,song 和 synthesizer 也要拆分成多对多的表格。

然而……

虽然遵守范式看起来是很好的,但是因为术力口数据库以前已经积累了一些历史遗留问题,所以现在很难建立满足所有范式的数据表了。

现实中的情况也很复杂,我们要考虑各种需求。比如,拆表太多,会导致查询数据时需要联合查询很多表,减慢查表的速度。所以,我们在设计数据库时其实并不是非要遵守所有的数据库范式。

好了,以上就是对数据依赖和数据库范式的介绍。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇