2023春数据库开发复习
T1 视图可以用的几个场景?
-
不同表字段聚合、信息重组:当某个查询涉及多表连接、次数频繁时,可以创建视图隐藏底层表的复杂性,简化查询。
-
控制权限:根据不同用户的权限,可以建立不同的视图,让用户查看部分数据(如只公开非敏感数据)。
-
安全性需要:用户无法对视图进行随意的修改和删除,只能在只读视图中检索数据,增加了安全性。
-
更新/重构数据库,同时满足原来原有应用的访问:当数据库需要重构(如删除了一些旧表,创建了一些新表),但不希望这些更改影响之前的应用程序时,可以使用与已删除、修改的旧表相同的表结构创建数据库视图。因为视图保留了原有的表架构,应用程序可以访问视图来完成此前功能而无需修改。
-
计算列需要:数据库设计范式要求减少冗余字段,所以数据库表大多没有计算列。但报表中通常需要总销售额等计算列字段,所以可以创建一个含有计算列字段的视图。
T2 不能把数据库当“黑盒”使用,为什么?
-
因为每个数据库都是不相同的。在一个数据库上取得的经验也许可以部分用于另一个数据库,但必须有心理准备,二者可能存在一些基本差别,可能还有一些细微差别。
-
细微差别(如对null的处理)与基本差别(并发控制机制)可能有同样显著的影响
-
所以我们应当了解数据库,知道它如何工作,其特性如何实现,这是解决这些问题的唯一途径
不同数据库在性能、安全、连接、锁机制、空值等方面有不同,下面以空值处理和锁机制为例:
空值处理
不同的数据库系统可能会对空值的处理有所不同,有四种场景:
- unknown:几乎所有的关系型数据库都认为空值为unknown,如MySQL、Oracle 和 SQL Server,它们遵循SQL标准。
- null:SQLite 数据库则将空值视为 NULL,并支持将 NULL 视为一个特殊的值来进行处理
- 不包括:一些 NoSQL 数据库不包括空值(或者叫做缺失值),因为它们的数据模型可能不需要将所有属性都定义为必填的,此时数据项缺少某些属性时,将不会存储空值。
- 实数:一些数据库可能允许在对实数列进行聚合操作时将空值视为0,这样可以避免在计算总和或平均值时出现错误。这取决于数据库的实现和特性,而不是标准的行为。
锁机制
锁机制是实现并发控制机制的一种主要手段。不同的数据库,实现锁机制是不一样的。
比如Oracle采取多版本控制(MVVC),读一致性的并发模型,支持读一致查询和非阻塞查询,有以下特性:
- 只有修改才加行级锁
- Read绝对不会对数据加锁
- Writer不会阻塞Reader
- 读写器绝对不会阻塞写入器
这种机制让Oracle存在有时读不到正确数据的现象,因为在读取时可能会读到已提交的旧版本数据,而不像其他数据库一样选择检查时间戳和退回。
此外,读不阻塞写可以极大提高数据库的吞吐能力,所以Oracle拥有散回特性。但其他数据库可能不是这样的,比如SQLite不支持非阻塞查询。
此外,因为锁粒度、锁类型、事务隔离级别等不同,Oracle与其他数据库的锁机制还有很多不同。
总之,我们不能简单把数据库当黑盒,否则可能会出错,比如Oracle的无阻塞设计有一个副作用,若想保证一次最多只有一个用户访问一行数据,就需要开发人员自己一些做工作。
T3 索引
MySQL为什么不用BST,而是用B+树?
已知在数据库系统中,通常采用磁盘来存储索引数据,以支持对大量数据的高效查询操作。
因为索引是顺序结构,可以用二分搜索树构建索引。但是当N比较大的时候,树的深度比较高。数据查询的时间主要依赖于磁盘IO的次数,二叉树深度越大,查找的次数越多,性能越差。最坏的情况是退化成了链表。即使做Tree Balancing,也比较耗时。其存储上的不连续性,也会导致存储空间的浪费。
而B+树有高扇出和低高度的特性,更适合磁盘实现。
- 高扇出:指在数据库索引中,每个父节点下包含的子节点数量较大,可以改善临近键的数据局限性。
- 低高度:遍历深度小,可以减少遍历期间的寻道次数。
B+树结构
![img](https://i-blog.csdnimg.cn/blog_migrate/0c79855435be1753493c3592b6a410e1.png)
-
基本结构:
- 一个结点是一个page
- B+树中的所有数据均保存在叶子结点,且根结点和内部结点均只是充当控制查找记录的媒介,并不代表数据本身,所有的内部结点元素都同时存在于子结点中。
- 叶子节点之间通过指针链接,可以实现连续范围查找,适合范围查询和分页查找等场景。
-
使用方式:
- 从根节点开始,根据该节点的关键字和待查找数据键值的大小比较,决定应该搜索该节点的哪个子节点。如果关键字都相等,则可以直接在叶子节点返回查询结果。如果比较较大,则到右子树查找;如果比较较小,则到左子树查找。
- 在选中的子节点中,重复执行第一步,直到搜索到一个叶子节点,即数据所在的节点。
- 在叶子节点中,按照存储顺序直接查找,或在叶子节点之间的指针链接进行有序的连续范围扫描,直到找到满足条件的数据记录。
- 前者是排序后直接定位需要查找的记录所在的位置,后者适合连续场景。
-
使用场景:
- 全键值 Where x = 123 (depth + 1次的固定次数)
- 键值范围 Where 45 < x < 123 (先进行x=45,然后顺序读取直到x>=123)
- 键前缀查找 where x LIKE J%’
结点分裂
B+树自下而上构建,随着叶结点的数量增加,内部节点的数量和树的高度也增加。
分裂涉及到两层:下面分裂,上面增加。移动的是中间值。
叶结点分裂和非叶结点分裂的区别:因为B+树中的所有数据均保存在叶子结点,13移动到内部结点后,也同时存在于叶子结点。而非叶结点分裂时,13上升后不会出现在原有层。
索引访问基本表
先访问索引再访问基本表,索引只是查询工作的第⼀步,读取基本表中的数据才是查询的结束。
同样的索引,但不同的物理结构,可能会引起查询效率的千差万别:
- 磁盘访问的速率
- 物理I/O很可能是内存访问
- 记录存储
主键索引
主键索引肯定存在,和基本表有对应关系:如文件偏移量对应,记录的是该记录在文件中偏移的字节数
二级索引
二级索引是非主键的其他键值构建的索引,有两种构建方法:
- 直接指向主数据文件
- 优点:访问快,直接
- 缺点:物理组织更新时两边都要处理(2次写)
- 指向主键索引,间接访问主数据文件(可以用文件偏移量对应)
- 优点:物理组织更新只需要改主键索引,二级索引会随着主键索引的更新而更新,因为不直接对应偏移量
- 缺点:多了一次I/O,因为每次读都要经过主键索引
不同数据库选择不同,如MySQL选第二种。
T4 物理组织形式
原始
在数据库中,通常将存储空间划分为多个page,每个page包含多个数据块,数据块中包含多个三元组,其中每个三元组对应一个数据项:
-
关键字(Key):唯一标识每个数据项;
-
相关值(Value):与关键字对应的具体数据信息;
-
指针(Pointer):指向与当前数据项相关的数据块或其他地址信息。
在数据的读写过程中,通过k找到对应的v和p,从而实现数据的定位和操作。
优点:
- 记录定长数据
- 实现灵活存储和快速检索数据,提高数据管理的效率。
缺点:
- 除非最右侧插入数据,否则需要移动前面的数据
- 无法有效管理变长的字段
分槽页
- header:包括页码、记录的数量和页的其他一些元信息
- pointer:指向下一个空闲槽的指针
- cells:存储实际记录的槽
需要满足的需求
- 最小开销的变长存储需求
- 回收已删除记录的空间
- 引用页中的记录,无论它在哪
好处
-
可以根据指针快速查找到记录的位置;
-
两边可以伸缩
-
最小开销:唯一额外开销是一个指针数组,用于保存记录实际所在位置偏移量
-
空间回收:通过对页进行碎片整理和重写,可以回收空间
-
动态布局:从页外部只能通过槽ID引用槽,确切位置由页内部决定
更新情况
- 只重排指针
- 记录从后往前放
迁移情况:溢出、两边迁移怎么办
当第一次数据溢出迁移时,系统会将数据存储到其他的块或页中,但是由于系
统自身的机制,可能会出现数据不平均地分布在多个块或页中的情况。
后面再需要进行迁移时,系统会将两端的无效数据都删除掉,只保留有效数
据,然后将数据存储到新的块或页(溢出页)中。
区别
行迁移是原来的页中还会存一部分的数据和指针,而行溢出是原来的页只存指
针。(优先进行行迁移)
为什么同一个分槽页中行迁移只会发生一次
由于数据页中的数据是按照主键值排序的,因此当某个数据行被迁移时,会导致该数据页中的其他数据行的位置发生改变。
因此多次数据迁移操作可能会导致数据页中的数据行位置变得十分混乱,甚至可能会影响查询性能。
所以如果之后的超额数据都将存储在溢出页中,可以减少数据行位置变化的次数,提高查询性能。
freeblock
- 为什么要有freeblock?避免更多可能出现的行迁移。留一块空白,update就可以在本页中直接操作
- 遵循70%/30%原则,即留出30%空白
管理版本
- 在文件名中加入版本前缀
- 版本使用专门文件存储,PostgreSQL将版本存在PG_VERSION
- 直接存在每⼀个具体文件(索引)的头部,头部按照不变格式编码
映射校验
映射校验通常用于大规模数据的验证。
映射校验将数据划分为多个任务,然后在每个任务内部通过计算生成本地校验
和(Local Checksum)。最后,在一个Reduce节点上将所有本地校验和进行
合并计算,以生成最终的全局校验和。
分槽页一定要有校验,一页存一个校验和。这样可以尽早识别磁盘文件问题,避免传播到其他子系统和节点。
XOR和CRC是常见的计算校验和方法:
- XOR:简单快速,对每个字节做异或运算
- CRC循环冗余校验:检测连续比特位的损坏
T5/6 sql
船只租赁
设有一个船员租赁船只系统,表结构如下:
有sailors
表,
+--------------+---------+
| Column Name | Type |
+--------------+---------+
| sid | int |
| sname | varchar |
| rating | int |
| age | int |
+--------------+---------+
sid为该表主键。
该表包含船员的编号,姓名,等级和年龄
有boats
表,
+--------------+---------+
| Column Name | Type |
+--------------+---------+
| bid | int |
| bname | varchar |
| color | varchar |
+--------------+---------+
bid为该表主键。
该表包含船只编号,船只名称和船只颜色
有reserves
表,
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| sid | int |
| bid | int |
| reserve_date | date |
+---------------+---------+
(sid,bid)为该表主键。
该表包含船员编号,船员预定的船只编号,船员预定船只的日期
-
编写一个sql语句,找出年龄在35以上的并且在2020-09-01至2020-09-30期间没有预定红色(RED)船只的水手,结果返回水手姓名sname。
select sailors.sname from sailors where sailors.age > 35 and sailors.sid not in ( select reserves.sid from reserves, boats where reserves.bid = boats.bid and boats.color = 'RED' and reserves.reserve_date between '2020-09-01' and '2020-09-30' )
-
编写一个sql语句,找出预定了所有船的水手,结果返回水手姓名sname。
select s.sname from sailors s where not exists ( select * from boats b where not exists( select * from reserves r where b.bid = r.bid and s.sid = r.sid ) )
-
编写一个sql语句,找出2020-05-01至2020-05-31期间预定过绿色船(GREEN)的等级最高的水手,结果返回水手姓名sname。
select sailors.sname from sailors where sailors.sid in ( select reserves.sid from reserves, boats where reserves.bid = boats.bid and boats.color = 'GREEN' and reserves.reserve_date between '2020-05-01' and '2020-05-31' ) and sailors.rating >= ALL( select sailors.rating from sailors where sailors.sid in ( select reserves.sid from reserves, boats where reserves.bid = boats.bid and boats.color = 'GREEN' and reserves.reserve_date between '2020-05-01' and '2020-05-31' ) )
-
编写一个sql语句,找出年龄在35岁以上,并且在2020-08-01至2020-08-31期间同时预定了红色船(RED)和绿色船(GREEN)的水手,结果返回水手姓名sname。
select sailors.sname from sailors where sailors.age > 35 and sailors.sid in ( select r.sid from reserves r, boats b where r.bid = b.bid and b.color = 'GREEN' and r.reserve_date between '2020-08-01' and '2020-08-31' ) and sailors.sid in ( select r.sid from reserves r, boats b where r.bid = b.bid and b.color = 'RED' and r.reserve_date between '2020-08-01' and '2020-08-31' )
比昨天温度高
表 Weather
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| id | int |
| recordDate | date |
| temperature | int |
+---------------+---------+
id 是这个表的主键
SELECT tod.`Id` AS Id
FROM Weather yes
JOIN Weather tod
ON DATEDIFF(tod.`RecordDate`, yes.`RecordDate`) = 1
WHERE tod.`Temperature` > yes.`Temperature`
平均值、中位数、replace
- 删除不想要的字符:replace
UPDATE tbl_name
SET
field_name = REPLACE(field_name, string_to_find, string_to_replace)
WHERE
conditions;
UPDATE products --表名
SET
productDescription = REPLACE(productDescription, 'abuot', 'about'); --列名
查询查找所有出现的拼写错误词:abuot
,并通过products
表的productDescription
列中使用正确单词将其替换
- 计算平均值,最大最小值
coalesce(num,0) // 去除null
- 计算中位数
- 计算众数
count(*)包括了所有的列,相当于行数,不会忽略列值为NULL
count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空
T7 BOM问题
存储简单树
简单树:单一结点只有一个父节点
- 邻接模型:父节点id作为子节点id的一个属性
- 物化路径:把树中的每个节点和它在树中的描述数据相结合(描述数据是从根节点到直接父节点的串联序列,类似1 ->1.1->1.1.1)
- 嵌套集合模型:每个节点被赋予一对数字,其中父节点的两个数字包含其所有节点的左右数字,然后一层一层这样包含下去。
- 嵌套间隔模型:以两个数字为特定节点的路径编码,这个数字被解释成有理数的分子和分母(不常用,可以忽略)
存储多父节点的树
BOM:单一结点可能有多个父节点。
一定通过多表,一张表存不了。一个表存内容,一个表存关系。
如下图Components存内容,Composition存关系。
以邻接表模型为例:使用两个表来存储多父节点的树:
- 存储节点的信息的表:记录节点的各种属性。
- 存储父子关系的表:记录节点间的父子关系。
- 增加一个额外的字段,用于存储每个父子关系在多父树中的顺序。
Node表:
| id | value | … |
Relationship表:
| id | parent_id | child_id | order |
计算百分比
--先查询初始成分的占比,再对其他部分计算实际占比
with recursive_composition(actual_pct, component_id)
as (select a.pct, a.component_id
from composition a, components b
where b.component_id = a.recipe_id
and b.component_name = 'Philter #5'
union all
select parent.pct * child.pct, child.component_id
from recursive_composition parent, composition child
where child.recipe_id = parent.component_id)
--主查询,去重
select x.component_name, sum(y.actual_pct)
from recursive_composition y, components x
where x.component_id = y.component_id
and x.component_type = 'I'
group by x.component_name
附加题 回忆一下上课时听到的感兴趣的知识点?
我对函数索引中where f(indexed_col)='some value'
这样的检索条件会使索引无法发挥作用感兴趣。
老师上课以日期函数为例,说第一种写法会导致 MySQL 无法使用 order_date
上的索引,因为需要对每一行记录执行 DATE_FORMAT
函数,这个过程无法利用索引。
SELECT COUNT(*) FROM orders WHERE DATE_FORMAT(order_date, '%Y-%m-%d') = '2023-03-08';
SELECT COUNT(*) FROM orders WHERE order_date >= '2023-03-08 00:00:00' AND order_date <= '2023-03-08 23:59:59';
隐式类型转换因为触发了CAST函数,也无法发挥作用。因此应该尽量避免在查询条件或者查询字段中使用函数,以便能够充分利用索引提高查询性能。
我好奇背后的原因,通过课下学习,发现B+ 树提供的这个快速定位能力,来源于同一层兄弟节点的有序性。而运行函数后的值会被修改或者变形,可能与实际存储在B+树中的原始值不同。比如假设树的第一层数据是2018-1-1,2019-9-1,2020-7-1
,对每个值执行month()
函数,得到的值(1,9,7)
,破坏了有序的前提。
mysql中规定只要对索引字段使用函数操作(无论是否破坏了了有序性),就放弃走树搜索功能,所以效率降低。