数据库系统期末复习I:数据库存储与索引技术

主要是期末复习,结合了学关系代数的课程加上重新整理了一下之前的 midterm 总结,重新聚焦一些要点,添加了详细的解释和重新理解知识点。 整理了一下方便自己复习。

DBMS 模式结构

  • 外模式是模式的访问权限控制视图
  • 模式是库表关系
  • 内模式包括物理存储:heap file,以什么做 clustering,索引类型,压缩与否,加密数据等

Relational model SQL

  • 关系代数和关系演算:区别是,一个通过算子(函数操作)来表述,关系演算是通过逻辑谓词。对于编程来说,用 relational algebra 多。
  • 对于关系的理论研究,用 calculus 多。calculus 是 declarative 的,而 algebra 是 procedural 的。
  • 声明式和程序式:calculus 不说明怎么实现,而 relational algebra 可以表示完整的操作流程,也就可以表示为树了。
  • SQL 一个综合的语言,用到 join 和 select 确实是提供了实现的说明对应着 relational algebra 的 join 和 projection 操作,而用到 any all exist not exist in not in 这些谓词又属于 relational calculus 的范畴。
  • SQL 有两部分组成,DDL,DML,一个是定义一个是操纵。
  • 经典的 nested not exist 相关子查询如何理解?其实这个东西就是一个相关子查询的条件判断,如果满足了就是真,实际实现不过就是 nested loop,然后里面根据条件来判断是返回一个结果还是直接 continue 循环而已。这种题这么适合考试,408竟然不考数据库。

数据库系统期末复习I:数据库存储与索引技术_第1张图片

  • 不过(这里忘记想说什么了)
  • 学校有一个 自己学生写的 SQL 软件 作为编译原理课程的大作业的一种实现,数据库系统编程实验为编写笛卡尔积的递归实现。学习了怎么通过递归实现多表联和查询求笛卡尔积。
  • 主要记住先 join 再查询会快很多。而不是求笛卡尔积。(马上就就能看到 join 的实现比如 hash join)

Storage I

首先是对于一系列的 Attribute 怎么存储的问题,这些既可能是同一个 字段 也可能是不同的字段

  • fixed-len,顺序存储+链表记录删除:对于 fixed length attribute,可以直接 seq 存储,但是对于删除了的 record, 要维护两个链表信息。
  • variable-lenslotted-page对于variable-length attribute 一种是每个 attribute 都用 len + 值 来记录。第二种是通过一个 block header 然后维护(类似堆分配,用指针方法,或者类似 ext2/3 的磁盘格式),称为  slotted-page。slotted-page 的要点是从两头向中间靠拢。

然后是怎么存储一整个 record/tuple 的问题,一个 tuple 会有多个不同的字段(column

  • 唯一访问标识符:tuple 的访问需要 id,简单的做法是通过 page_id + offset or slot 来做,但是这些 id 只是用来管理的,不暴露给 application (和物理存储结构有关)。
  • 反范式 prejoinDenormalized Tuple Data,对于简单的两个表然后有一个 primary key 是另一个的外键,可以 prejoin ,但是 prejoin 就会反范式并且有冗余,但是保持在同一个 page 能够加速访问(IO 是瓶颈)我们说的 page 一般就是不同表的数据不会同在一个 page 上的。

Storage II

大量数据之后怎么存储

磁盘背景,为什么是页

  • 块设备:4KB page 是磁盘调度的基本单位,对 hdd 和 ssd 都是这样,SATA 和 NVMe 都是是块设备驱动来的,区别主要是对块的管理算法/数据结构不同。
  • HDD盘片与顺序读写:HDD 就是简单的扇区,读写是一次读一个扇区512B,可以一次读8块(比如有多个盘片,一个盘片最多两个记录面,如果有8个记录面就能组装一个 4Kb 了)组装成一个块送给驱动。他顺序读写快是因为他一直在旋转悬空磁力读写,所以实际可以预读写,随机访问需要频繁控制电磁的启停以及寻道。
  • SSD无法覆盖写:SSD 用的是NAND,读写单位一个 Page 可能是 16KB,然后擦除单位是一个 Block 一千个 Page 可能,写之前必须要先擦除,所以就有问题就是他没办法覆盖写,这时就必须做一个垃圾回收机制,所以更加不能写满 SSD,必须提供一个缓存或者一些空间来做垃圾回收的。而且NAND这个东西很容易坏,所以肯定会有坏块。这就需要建立一个映射表屏蔽掉一些区块和完成映射关系。当然为了负载均衡和速度问题每次都是并发读写多个存储体(芯片)。SLC MLC TLC QLC 分别是每个Nor运算的位数,一般 DRAM 缓存用来存映射表,SLC 缓存是把 MLC~QLC 的部分通过模拟 SLC(一个bit)的方法来存数据(比如让 MLC 00 10 11 01 里面两个归为 0 两个归为1,一次只需要改变一个 bit,这样读写快一些)但是需要限制存储不超过1/3容量。

页式存储

  • Heap file: 对于一系列数据,基本方法是 heap file 存储,heap file 可以用 page directory 或者 linked list 实现,但是 linked list 就是要连续访问很麻烦,所以做一个 index,就是基于 Page Directory 的访问。比如这个实验的 bustub,通过 page_id 来定位一个页面,最简单的做法是做一个 auto increment 字段做 id,但是自增对于并发是不好的,所以 page manager (比如生成页面)可以只用一个线程。
  • Why not OS: 我们永远比 OS 知道地更多,所以不要用 OS 的 mmap 和缓存。
  • Buffer Pool : 几个替换 Policy,LRU 需要用双向链表和 map 实现。LFU 经典版本使用小顶堆和 hashmap 每次需要 lgn 的查询。LRU 直接提升等级就行了,直接添加到链表头就行了,LFU 由于更新了一个页面的使用频度,就会连带影响其他页面的使用频度,没有办法。根据某个论文的思路空间换时间可以做到 O(1), 使用多重表(横纵双链表+hashmap)。但是实际没有意义,因为瓶颈不在 on memory data strucute 访问,而是在 disk IO。
  • 特殊数据:尽量不要用 numeric 。不要在关系数据库里面存 blob,必要可以存 URL 而不是 blob。

OLAP vs OLTP

  • 页内部的内容存储的不同,分为 cloumn base 和 row base 存储
  • olap 适合用 column based(Decomposition Storage Model),因为这里会涉及很多单独的 select 和各种 join,DSM 加速。对点查询不友好,因为查询之后要组装。
  • oltp : 适合用 row based (N-ary Storage Model),方便 CRUD (create retrieve update delete)。
  • not only sql: 包括图,文档(如 json,xml),KV(KV主要做缓存多一些),bigtable
  • hlap: 这个应该是大方向,对于某些 txn 数据来说,他也能用来搞数据挖掘的属于。比如经典的购物数据好像是 oltp,但是为了进行商品推荐,我们可以搞关联规则分析和频繁模式识别的。

查询、索引相关术语:

  • system catalog数据库自己的数据,一般可以自己存成表,但是需要 bootstrap 代码,因为操作表本身需要数据库自己的数据。最简单的理解方法就是把他当成递归终止条件的某个 if 特殊处理。
  • 点查询 point Query,意思是基于某个 single equality relation 来查询一系列 row/col。
  • 范围查询 range Query,意思是基于某个范围(不等式)来查询一系列 row/col。
  • selective :有区分度的查询
  • full table scan: 没有索引的时候走的 query plan
  • indexed scan有索引的时候走的 query plan
  • primary indexclustering index基于 index 排序在物理存储上有序,是最快的访问,非 clustering 的也可以可以统筹为序列化访问,延迟一步而已(这种一般就是先打 log,之后根据 log schedule 序列访问)。MySQL 使用 InnoDB 存储引擎的时候,主键自动成为聚簇索引,首先,叶子节点是紧凑有序存储的,聚簇索引则 B+ 树在上面进行构造。传统的 B+ 树是通过不断地插入,分裂然后构造的。实际 B+ 树可以自底向上构建。不过瓶颈在 io 不在这里
  • bitmap index scan: 类似 log structure merge trees 的原理那样,最后要把一系列的 random access 转为 seq 的一个 technique,之后的内容也会非常常见 。即在 index 叶子的时候(叶子一般是记录 tuple id)用 bitmap 把需要去访问的 tuple 给记录下来,之后再用磁盘访问算法 seq 访问。 一般 OS 都实现了批处理的电梯算法XX算法。直接送批处理读写请求过去就行了。
  • secondary index这个对 range query 会慢一点,因为他不是 clustering。对于 innodb 这个引擎,因为 clustering 的时候用的是 primary key,所以实际寻找一个 tuple 的时候可能是最后要在 B+树的叶子节点上找到 primary key 然后再查一次。(不然更新依赖就很复杂,前面说了可以用 page_id + 排列 offset/第几条来唯一标识一个 tuple,但是这样每次更新数据及存储位置的时候,都会引发依赖,所以这里也是一个 2NF 的设计)。
  • sparse index dense index一般都是 dense index。这个在 RDBMS 和 NoSQL 里面不一样。在 RDBMS 里面,对于 clustering 的 index,实际可以减少数据量,中间再用二分查找做查询(即索引只索引一部分)就是 sparse。对于 nosql 来说,sparse 可能是说对于没有该字段的(比如 json 存储,一个 document 的 entry 可能没有某个字段)就不建立索引,所以查找不到他。看厂商定义了,我们主要掌握的是 RDBMs 的。
  • Multi-arribute index,这个对 OLTP 来说可能用 hash index 的效果好一些,适合 point query 而不是 range query。B+ 树是 logn 但是既支持 point 也支持 range。对于 on memory 的不是瓶颈,但是 B+ 树对于每个节点都 on disk 的话,logn 和 log1 的意味就涉及多次 IO 了。
  • 联合索引 II 与最左匹配原则:对于需要 range query 的联合索引一般是 B+ 树(mysql 有限采用 b+ 树,因为 hash 只适合 oltp),最左匹配原则是说以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配。原因是必须在前一个字段确定的情况下后一个字段才是局部有序的。而第一个范围查询就已经开始遍历全部前缀满足的条件的,范围查询会得到第一个范围,这个范围的所有前缀都是等值查询,这个范围里面的后面的关键字已经是乱序的了,无法利用索引。
  • Covering Index根据某个 key 创建索引,但是索引里面直接保护了不仅那个 key,还包含一部分 column,这样直接访问 index 就返回结果,超级加速,对于 OLAP 很常用。
  • Fitered indexpartial index就是说只对满足某些条件的记录建立索引,利用了 hot data 和 cold data 的特性。比如我经常要再某个性别的数据里面查询,就可以只建立那个性别的数据的索引,节约空间和搜索时间。
  • full text index这个其实就是搜索技术里面的倒排索引,所以对于中文要用到特定的分词技术,比如 ngram。索引会比 grep on the fly 快得多了。GNU grep 用的是 boyer-moore 算法,超快,这个算法广泛应用再文本编辑器的文本匹配里面。bm 算法是 kmp 算法的优化情况,我在考数据机构考试之前可以默写优化为 nextval 的 kmp 算法的 PMT 写法和 DFA 写法,现在默写不了了。BM 的表获取了更加多的信息。我不期望能几句话让我提起 BM 和 KMP 的默写思路,因为这两个算法都是大神级别的论文。机械式的思路是我不是在这里做原创性工作的,也没必要尝试重新发明一辆汽车才开车,我只要知道有这个东西,并且能找到实现他的实现方法或者直接能用的代码实现需求。
  • functionalexpression index这个就是说创建一些 derived 出来的索引,当然,对日期这种做不了,因为 volatile 数据的索引没有意义,除非短时间内大量 重复查询。

数据结构和算法概述

  • B+Tree支持 range query 和 point query,复杂度 O(lgn),做 clustering 很方便。每次读页进来,结合 buffer pool 和 page id。对于 Range query 不一定走 b+ 树,因为有时候求的是大于某个值的,这种情况(别忘了我们的 B+Tree 实际叶子是连起来的链表,从 B linked Tree 学来的)。这里为了面试问题,也要补充一下 b Tree, 两者的区别在于一个所有内容都在叶子,非叶子是叶子的采样,没有意义。一个是非叶子本身也存储数据。B+Tree 肯定更好因为他中间不用存数据,更加矮,而且 B+树的叶子可以遍历,B树不行。所以b树一点用都没有(节约的那点空间微不足道,除非 key 比 value 还大)。
  • Hashing支持 point query,复杂度 O(1) ,实际做不到,一个是对于非 unique 关键字的索引来说,数据量大了仍然需要多个页面存储。第二个是哈希冲突频发,毕竟数据量非常大,可能一个 bucket 的冲突超过一个 page。(当然页内的冲突根本不算事,因为 in memory 的非常快)。
  • Partition我们学过 group by 和 partition by 的原理,原因是 partition by 或者 group by  的关键字很有可能是多重复的(which means 不是很 selective),这种情况用 hashing 就能很容易 hash 到同一个 bucket 里面了,只要没有 order by,就能轻松做 aggregation 或者 window query。
  • Partition II 更强劲的优化是,在内模式里面直接进行 partition,基本的有 range ,list ,hash ,key (都有对应的 mysql 语句)。partition 技术的好处是,一个 scalable,一个提升检索性能,特别是 aggregation 查询,如果本身 partition 就是满足的,就能直接在分区内进行计算了。提升并发性能,回想做 concurrent B+Tree index 的时候,对于上面的节点的锁,持有的时间会长很多,直到确定不会 split 或者 merge 了才能放开,而 partition 了之后,index 也多了起来(可能本质也是空间换时间)。
  • bitmap索引好像有了 B+ Tree 和 hash 就够了,其实不是的,他们比较适合 unique 的,对于大量重复的会很浪费。我们 projects 里面的 B+ Tree 也没有处理 not unique 的情况。对于 not unique 的情况,基本方案是要么用链表,变长列表什么的。hashing 也差不多不过和哈希冲突的情况冲突了。前面讲了 partition 和 group by 常常是对于键字很有可能是多重复的,这种情况可以建立 bitmap index,而且这种情况还不少见呢!这样查询的时候可以直接从 bitmap 构建结果。不过这个功能和 partition 提供的功能差不多重合了,而 partition 还能提供更多的功能。一开始 Oracle 支持,mysql 到了 8.0 都没有这个。所以实际这个只是一个知识点了吧,毕竟 partition 技术里这个不过是个 bonus。
  • radix tree这个实际也没用,比较适合 long int。对于 linux 内核来说,用来做缓存的索引(指针就是 long 嘛)。对于 int 来说,hash 冲突很容易发生。他是基于 trie 的二叉树(或者四叉树、八叉树,256 span树),不过实际是二进制也可以哦,利用结构本身来存数据。主要又是在于重复的位串,比如连续10个0,只需要做 vertical compaction 就行了。

HASHING

  • Cache spike 对齐的时候更容易 cache conflict。最好用的是 xxhash3.
  • linear probe: 静态哈希是指哈希函数和 val 在桶里面的位置都是确定性的,最简单的是 linear probe hashing,这个东西复杂度最坏退化 On,存储的时候必须存储 key + value (实际好像都要)。linear probe 的删除很麻烦,因为你 hash 过来不能认为空了就不找了,这样要判断在不在就必须  On 了,根本用不了。
  • robin hood 劫富济贫,要记录冲突的顺序移动次数(如果一个元素瞬移了10次,在瞬移的过程中如果有人顺移到他那个位置只需要顺移更少的次数,平均来说均贫富了)。空间换时间,实际性能不好。没人用的。
  • cuckoo 就是 bucket 或者 double buffer(google hashing),两个 hash 函数随机选或者看有没有冲突。位置有人了就踢他出来。查询删除都总是 O1,插入最坏无穷此时进行扩容。但是很少可能。实际也是均摊复杂度的。
  • 静态哈希和动态哈希:上面说的都是静态哈希,必须知道要存的数量,有限个哈希函数的模数也是恒定的。
  • Chained hashing: in memory 的 Java 的 hashmap 采用的是 ,直接是用 linked list 来处理冲突问题。这里顺便把 Java 的 hashmap 原理给复习了,Java 1.8 以后引入了红黑树,8个 的时候转为红黑树,不直接用红黑树是因为 TreeNode 是 Node 的两倍。一般是不会出现红黑树的。除非你 override 了一个垃圾 hashCode 函数。而且 Java 的 Object 的 HashCode 本身就定制过的,结合 hash 函数,实际效果还不错的。6个才变回链表这个和滞回比较器的感觉是差不多的。unordered_map 和 unordred_set 都使用哈希表实现,内部也是链的方法解决哈希冲突,到 8 的时候转为 map(红黑树)。扩容 rehash 和 reindex 的麻烦,stop the world,不是均摊的。Java 解决方案是用 2 的幂,全都不用 rehash,一半不用 reindex (高位不变),一半因为只有高位变了,就 oldindex + oldsize 就行了。注意要合并链表。stop the world 是不可接受的。
  • extendible hashing避免 STW,保证每次分裂都最多修改一个桶,没有全员重新核酸检测。通过 radix 树(tries)或者哈夫曼前缀码差不多的东西控制哪些前缀用哪些 bucket。只有挤爆的那个桶要重新核酸检测。
  • 数据库系统期末复习I:数据库存储与索引技术_第2张图片
  • linear hashing :上面这个实现其实是很复杂的,然后对于删除来说合并很麻烦,因为有多对一的关系。然后这个指针数组每次都扩展2倍,十分浪费。线性哈希既可以高效扩展也可以高效收缩。然后他还不用真正地扩容两倍(ghost bucket)。实际 partition 就用这个。用 cursor 来 track split 是为了保证 fine grained 的 lock。不管哪个 bucket overflow,都只 split split pointer 的地方。区分是原来的位置还是新位置(bucket)是用老 hash functin hash 之后的值是在 split point 上面还是下面,如果在下面,就不用 probe 第二个函数了。但是这个的 8 就算 mod 第二个函数也在 0 即上面(类似 java 的2倍扩展)。但是我感觉他每次 split 都必须 rehash split pointer 前面的所有 bucket 里面的元素啊??这不还是 stop the world 了?只不过是避免了 directory 的 2倍扩展而已?
  • 数据库系统期末复习I:数据库存储与索引技术_第3张图片

  • 数据库系统期末复习I:数据库存储与索引技术_第4张图片
  • Hashing indexashing 做 index,其实他没什么用(只能做等值查询,适合 OLTP)。不过因为他既能处理等值,又能处理大量重复(比如 bitmap 做 partition),所以实际他和 B+ 树两个合起来就平分数据库索引了。

RWLOCK实现

  • 关键一是 spinlock(mutex) 用来保护 meta(count 之类的)
  • 然后 wait lock 睡觉前会释放 mutex,醒来重新获取 mutex。
  • 读者锁睡觉的情况是写者排队或者读者最大。
  • 写者所睡觉的情况是已经有写者排队了,不然他就排队,然后睡觉等最后读者释放。

你可能感兴趣的:(操作系统/数据库,笔记,数据库,database)