数据库的范式设计(来自:sql必知必会---陈旸)

2023-11-04 10:20

本文主要是介绍数据库的范式设计(来自:sql必知必会---陈旸),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

数据库的范式设计

  • 范式设计
    • 数据库的设计范式都包括哪些
    • 数据表中的那些键
    • 从 1NF 到 3NF
  • 反范式设计
    • BCNF(巴斯范式)
    • 反范式设计
    • 反范式优化实验对比
    • 反范式优化实验对比

范式设计

数据库的设计范式都包括哪些

我们在设计关系型数据库模型的时候,需要对关系内部各个属性之间联系的合理化程度进行定义,这就有了不同等级的规范要求,这些规范要求被称为范式(NF)。你可以把范式理解为,一张数据表的设计结构需要满足的某种设计标准的级别。
目前关系型数据库一共有 6 种范式,按照范式级别,从低到高分别是:1NF(第一范式)、2NF(第二范式)、3NF(第三范式)、BCNF(巴斯 - 科德范式)、4NF(第四范式)和 5NF(第五范式,又叫做完美范式)。
数据库的范式设计越高阶,冗余度就越低,同时高阶的范式一定符合低阶范式的要求,比如满足 2NF 的一定满足 1NF,满足 3NF 的一定满足 2NF,依次类推。
一般来说数据表的设计应尽量满足 3NF。但也不绝对,有时候为了提高某些查询性能,我们还需要破坏范式规则,也就是反规范化。
在这里插入图片描述

数据表中的那些键

范式的定义会使用到主键和候选键(因为主键和候选键可以唯一标识元组),数据库中的键(Key)由一个或者多个属性组成。我总结了下数据表中常用的几种键和属性的定义:

  • 超键:能唯一标识元组的属性集叫做超键。
  • 候选键:如果超键不包括多余的属性,那么这个超键就是候选键。
  • 主键:用户可以从候选键中选择一个作为主键。
  • 外键:如果数据表 R1 中的某属性集不是 R1 的主键,而是另一个数据表 R2 的主键,那么这个属性集就是数据表 R1 的外键。
  • 主属性:包含在任一候选键中的属性称为主属性。
  • 非主属性:与主属性相对,指的是不包含在任何一个候选键中的属性。

通常,我们也将候选键称之为“码”,把主键也称为“主码”。因为键可能是由多个属性组成的,针对单个属性,我们还可以用主属性和非主属性来进行区分。

从 1NF 到 3NF

1NF 指的是数据库表中的任何属性都是原子性的,不可再分。
2NF 指的数据表里的非主属性都要和这个数据表的候选键有完全依赖关系。所谓完全依赖不同于部分依赖,也就是不能仅依赖候选键的一部分属性,而必须依赖全部属性。
3NF 在满足 2NF 的同时,对任何非主属性都不传递依赖于候选键。也就是说不能存在非主属性 A 依赖于非主属性 B,非主属性 B 依赖于候选键的情况。

有一点需要注意的是,这些范式只是提出了设计的标准,实际上设计数据表时,未必要符合这些原则。一方面是因为这些范式本身存在一些问题,可能会带来插入,更新,删除等的异常情况,另一方面,它们也可能降低会查询的效率。这是为什么呢?因为范式等级越高,设计出来的数据表就越多,进行数据查询的时候就可能需要关联多张表,从而影响查询效率。

在这里插入图片描述

反范式设计

BCNF(巴斯范式)

如果数据表的关系模式符合 3NF 的要求,就不存在问题了吗?我们来看下这张仓库管理关系表:
在这里插入图片描述

在这个数据表中,一个仓库只有一个管理员,同时一个管理员也只管理一个仓库。我们先来梳理下这些属性之间的依赖关系。

仓库名决定了管理员,管理员也决定了仓库名,同时(仓库名,物品名)的属性集合可以决定数量这个属性。

这样,我们就可以找到数据表的候选键是(管理员,物品名)和(仓库名,物品名),然后我们从候选键中选择一个作为主键,比如(仓库名,物品名)。
在这里,主属性是包含在任一候选键中的属性,也就是仓库名,管理员和物品名。非主属性是数量这个属性。

如何判断一张表的范式呢?我们需要根据范式的等级,从低到高来进行判断。

首先,数据表每个属性都是原子性的,符合 1NF 的要求;其次,数据表中非主属性”数量“都与候选键全部依赖,(仓库名,物品名)决定数量,(管理员,物品名)决定数量,因此,数据表符合 2NF 的要求;最后,数据表中的非主属性,不传递依赖于候选键。因此符合 3NF 的要求。

既然数据表已经符合了 3NF 的要求,是不是就不存在问题了呢?我们来看下下面的情况:

	1.增加一个仓库,但是还没有存放任何物品。根据数据表实体完整性的要求,主键不能有空值,因此会出现插入异常;2.如果仓库更换了管理员,我们就可能会修改数据表中的多条记录3.如果仓库里的商品都卖空了,那么此时仓库名称和相应的管理员名称也会随之被删除。

你能看到,即便数据表符合 3NF 的要求,同样可能存在插入,更新和删除数据的异常情况。

首先我们需要确认造成异常的原因:主属性仓库名对于候选键(管理员,物品名)是部分依赖的关系,这样就有可能导致上面的异常情况。人们在 3NF 的基础上进行了改进,提出了BCNF,也叫做巴斯 - 科德范式,它在 3NF 的基础上消除了主属性对候选键的部分依赖或者传递依赖关系

根据 BCNF 的要求,我们需要把仓库管理关系表拆分成下面这样:

	仓库表:(仓库名,管理员)库存表:(仓库名,物品名,数量)

这样就不存在主属性对于候选键的部分依赖或传递依赖,上面数据表的设计就符合 BCNF。

反范式设计

尽管围绕着数据表的设计有很多范式,但事实上,我们在设计数据表的时候却不一定要参照这些标准。

我们在之前已经了解了越高阶的范式得到的数据表越多,数据冗余度越低。但有时候,我们在设计数据表的时候,还需要为了性能和读取效率违反范式化的原则。反范式就是相对范式化而言的,换句话说,就是允许少量的冗余,通过空间来换时间。

如果我们想对查询效率进行优化,有时候反范式优化也是一种优化思路。

比如我们想要查询某个商品的前 1000 条评论,会涉及到两张表。

商品评论表,对应的字段名称及含义如下:
在这里插入图片描述

用户表,对应的字段名称及含义如下:
在这里插入图片描述

下面,我们就用这两张表模拟一下反范式优化。

实验数据:模拟两张百万量级的数据表

为了更好地进行 SQL 优化实验,我们需要给用户表和商品评论表随机模拟出百万量级的数据。我们可以通过存储过程来实现模拟数据。

下面是给用户表随机生成 100 万用户的代码:

	DELIMITER $$CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_many_user`(IN start INT(10), IN max_num INT(10))BEGINDECLARE i INT DEFAULT 0;DECLARE date_start DATETIME DEFAULT ('2017-01-01 00:00:00');DECLARE date_temp DATETIME;SET date_temp = date_start;SET autocommit=0;REPEATSET i=i+1;SET date_temp = date_add(date_temp, interval RAND()*60 second);INSERT INTO user(user_id, user_name, create_time)VALUES((start+i), CONCAT('user_',i), date_temp);UNTIL i = max_numEND REPEAT;COMMIT;END$$DELIMITER ;

用 date_start 变量来定义初始的注册时间,时间为 2017 年 1 月 1 日 0 点 0 分 0 秒,然后用 date_temp 变量计算每个用户的注册时间,新的注册用户与上一个用户注册的时间间隔为 60 秒内的随机值。然后使用 REPEAT … UNTIL … END REPEAT 循环,对 max_num 个用户的数据进行计算。在循环前,我们将 autocommit 设置为 0,这样等计算完成再统一插入,执行效率更高。

然后来运行 call insert_many_user(10000, 1000000); 调用存储过程。这里需要通过 start 和 max_num 两个参数对初始的 user_id 和要创建的用户数量进行设置。运行结果:
在这里插入图片描述
能看到在 MySQL 里,创建 100 万的用户数据用时 1 分 37 秒。

接着我们再来给商品评论表 product_comment 随机生成 100 万条商品评论。这里我们设置为给某一款商品评论,比如 product_id=10001。评论的内容为随机的 20 个字母。以下是创建随机的 100 万条商品评论的存储过程:

	DELIMITER $$CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_many_product_comments`(IN START INT(10), IN max_num INT(10))BEGINDECLARE i INT DEFAULT 0;DECLARE date_start DATETIME DEFAULT ('2018-01-01 00:00:00');DECLARE date_temp DATETIME;DECLARE comment_text VARCHAR(25);DECLARE user_id INT;SET date_temp = date_start;SET autocommit=0;REPEATSET i=i+1;SET date_temp = date_add(date_temp, INTERVAL RAND()*60 SECOND);SET comment_text = substr(MD5(RAND()),1, 20);SET user_id = FLOOR(RAND()*1000000);INSERT INTO product_comment(comment_id, product_id, comment_text, comment_time, user_id)VALUES((START+i), 10001, comment_text, date_temp, user_id);UNTIL i = max_numEND REPEAT;COMMIT;END $$DELIMITER ;

同样的,用 date_start 变量来定义初始的评论时间。这里新的评论时间与上一个评论的时间间隔还是 60 秒内的随机值,商品评论表中的 user_id 为随机值。我们使用 REPEAT … UNTIL … END REPEAT 循环,来对 max_num 个商品评论的数据进行计算。

然后调用存储过程,运行结果如下:
在这里插入图片描述MySQL 一共花了 2 分 7 秒完成了商品评论数据的创建。

反范式优化实验对比

如果我们想要查询某个商品 ID,比如 10001 的前 1000 条评论,需要写成下面这样:

	SELECT p.comment_text, p.comment_time, u.user_name FROM product_comment AS p LEFT JOIN user AS u ON p.user_id = u.user_id WHERE p.product_id = 10001 ORDER BY p.comment_id DESC LIMIT 1000

运行结果(1000 条数据行):
在这里插入图片描述
运行时长为 0.395 秒,查询效率并不高。

这是因为在实际生活中,我们在显示商品评论的时候,通常会显示这个用户的昵称,而不是用户 ID,因此我们还需要关联 product_comment 和 user 这两张表来进行查询。当表数据量不大的时候,查询效率还好,但如果表数据量都超过了百万量级,查询效率就会变低。这是因为查询会在 product_comment 表和 user 表这两个表上进行聚集索引扫描,然后再嵌套循环,这样一来查询所耗费的时间就有几百毫秒甚至更多。对于网站的响应来说,这已经很慢了,用户体验会非常差。

如果我们想要提升查询的效率,可以允许适当的数据冗余,也就是在商品评论表中增加用户昵称字段,在评论表的基础上增加 user_name 字段,就得到了评论表2 数据表。

这样一来,只需单表查询就可以得到数据集结果:

	SELECT comment_text, comment_time, user_name FROM product_comment2 WHERE product_id = 10001 ORDER BY comment_id DESC LIMIT 1000

运行结果(1000 条数据):
在这里插入图片描述

优化之后只需要扫描一次聚集索引即可,运行时间为 0.039 秒,查询时间是之前的 1/10。 你能看到,在数据量大的情况下,查询效率会有显著的提升。

反范式优化实验对比

从上面的例子中可以看出,反范式可以通过空间换时间,提升查询的效率,但是反范式也会带来一些新问题。

在数据量小的情况下,反范式不能体现性能的优势,可能还会让数据库的设计更加复杂。比如采用存储过程来支持数据的更新、删除等额外操作,很容易增加系统的维护成本。

比如用户每次更改昵称的时候,都需要执行存储过程来更新,如果昵称更改频繁,会非常消耗系统资源。

那么反范式优化适用于哪些场景呢?

在现实生活中,我们经常需要一些冗余信息,比如订单中的收货人信息,包括姓名、电话和地址等。每次发生的订单收货信息都属于历史快照,需要进行保存,但用户可以随时修改自己的信息,这时保存这些冗余信息是非常有必要的。

当冗余信息有价值或者能大幅度提高查询效率的时候,我们就可以采取反范式的优化。
此外反范式优化也常用在数据仓库的设计中,因为数据仓库通常存储历史数据,对增删改的实时性要求不强,对历史数据的分析需求强。这时适当允许数据的冗余度,更方便进行数据分析。

我简单总结下数据仓库和数据库在使用上的区别:

	1.数据库设计的目的在于捕获数据,而数据仓库设计的目的在于分析数据;2.数据库对数据的增删改实时性要求强,需要存储在线的用户数据,而数据仓库存储的一般是历史数据;3.数据库设计需要尽量避免冗余,但为了提高查询效率也允许一定的冗余度,而数据仓库在设计上更偏向采用反范式设计。

在这里插入图片描述

这篇关于数据库的范式设计(来自:sql必知必会---陈旸)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/345113

相关文章

mysqld_multi在Linux服务器上运行多个MySQL实例

《mysqld_multi在Linux服务器上运行多个MySQL实例》在Linux系统上使用mysqld_multi来启动和管理多个MySQL实例是一种常见的做法,这种方式允许你在同一台机器上运行多个... 目录1. 安装mysql2. 配置文件示例配置文件3. 创建数据目录4. 启动和管理实例启动所有实例

将sqlserver数据迁移到mysql的详细步骤记录

《将sqlserver数据迁移到mysql的详细步骤记录》:本文主要介绍将SQLServer数据迁移到MySQL的步骤,包括导出数据、转换数据格式和导入数据,通过示例和工具说明,帮助大家顺利完成... 目录前言一、导出SQL Server 数据二、转换数据格式为mysql兼容格式三、导入数据到MySQL数据

MySQL分表自动化创建的实现方案

《MySQL分表自动化创建的实现方案》在数据库应用场景中,随着数据量的不断增长,单表存储数据可能会面临性能瓶颈,例如查询、插入、更新等操作的效率会逐渐降低,分表是一种有效的优化策略,它将数据分散存储在... 目录一、项目目的二、实现过程(一)mysql 事件调度器结合存储过程方式1. 开启事件调度器2. 创

SQL Server使用SELECT INTO实现表备份的代码示例

《SQLServer使用SELECTINTO实现表备份的代码示例》在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误,在SQLServer中,可以使用SELECTINT... 在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误。在 SQL Server 中,可以使用 SE

mysql外键创建不成功/失效如何处理

《mysql外键创建不成功/失效如何处理》文章介绍了在MySQL5.5.40版本中,创建带有外键约束的`stu`和`grade`表时遇到的问题,发现`grade`表的`id`字段没有随着`studen... 当前mysql版本:SELECT VERSION();结果为:5.5.40。在复习mysql外键约

SQL注入漏洞扫描之sqlmap详解

《SQL注入漏洞扫描之sqlmap详解》SQLMap是一款自动执行SQL注入的审计工具,支持多种SQL注入技术,包括布尔型盲注、时间型盲注、报错型注入、联合查询注入和堆叠查询注入... 目录what支持类型how---less-1为例1.检测网站是否存在sql注入漏洞的注入点2.列举可用数据库3.列举数据库

数据库oracle用户密码过期查询及解决方案

《数据库oracle用户密码过期查询及解决方案》:本文主要介绍如何处理ORACLE数据库用户密码过期和修改密码期限的问题,包括创建用户、赋予权限、修改密码、解锁用户和设置密码期限,文中通过代码介绍... 目录前言一、创建用户、赋予权限、修改密码、解锁用户和设置期限二、查询用户密码期限和过期后的修改1.查询用

Mysql虚拟列的使用场景

《Mysql虚拟列的使用场景》MySQL虚拟列是一种在查询时动态生成的特殊列,它不占用存储空间,可以提高查询效率和数据处理便利性,本文给大家介绍Mysql虚拟列的相关知识,感兴趣的朋友一起看看吧... 目录1. 介绍mysql虚拟列1.1 定义和作用1.2 虚拟列与普通列的区别2. MySQL虚拟列的类型2

mysql数据库分区的使用

《mysql数据库分区的使用》MySQL分区技术通过将大表分割成多个较小片段,提高查询性能、管理效率和数据存储效率,本文就来介绍一下mysql数据库分区的使用,感兴趣的可以了解一下... 目录【一】分区的基本概念【1】物理存储与逻辑分割【2】查询性能提升【3】数据管理与维护【4】扩展性与并行处理【二】分区的

MySQL中时区参数time_zone解读

《MySQL中时区参数time_zone解读》MySQL时区参数time_zone用于控制系统函数和字段的DEFAULTCURRENT_TIMESTAMP属性,修改时区可能会影响timestamp类型... 目录前言1.时区参数影响2.如何设置3.字段类型选择总结前言mysql 时区参数 time_zon