1. 背景
1.1. 大数据概念

1.2. 大数据管理系统
- 关系型
Oracle, DB2, MS SQL Server, Greenplum, TeraData, Vertica - 云平台
MapReduce, Apache Hadoop, MS Dryad - 云平台+SQL
Apache Hive, Yahoo Pig, MS Scope - No-SQL
Apache Hbase, Cassandra, MangoDB, Neo4j - 内存数据处理系统
MMDB, Spark, Cloudera impala - 图数据处理
Google Pregel, Apache Giraph, Graphlab 
2. 关系型数据管理系统
2.1. 关系型数据模型
Table/Relation
- 列(Column)
 - 行(Row)
 
通常是一个瘦长的表
2.1.1. 概念
2.1.1.1. Schema vs. Instance
Schema: 类型
一个表的类型是由每个列的类型决定的Instance: 具体取值
具体存储哪些记录,每个列的具体指
由具体用用决定
2.1.1.2. Key
- Primary key
 - Foreign key
是另一个表的主键 
2.2. 主要关系运算
- Selection(选择)
从一个表中提取一些行 - Projection(投影)
从一个表中提取一些列 - Join(连接)
- Equi-join(等值连接)
 
 
2.2.1. SQL Selcet

2.3. 数据库系统架构
通常的系统为典型的C/S结构:

- SQL Parser
 
- SQL语句的程序 -> 解析好的内部表达(例如:Parsing tree)
- 语法解析,语法检查,表名、列名、类型检查
 
 
- Query Optimizer
 
- SQL 内部表达 -> Query Plan(执行方案)
- 产生可行的query plan
 - 估计query plan的运行时间和空间代价
 - 在多个可行的query plans中选择最佳的query plan
 
 
- Data storage and indexing
 
- 如何在硬盘上存储数据
 - 如何高效地访问硬盘上的数据
 
Buffer Pool:在内存中缓存硬盘的数据
Execution Engine
- query plan -> SQL语句的借故偶
- 根据query plan,完成相应的运算和操作
 - 数据访问
 - 关系型运算的实现
 
 
- Transaction management
 
- 目标是实现ACID
 - 进行logging写日志,locking加锁
 - 保证并行transactions事务的正确性
 
2.4. 数据存储与访问
2.4.1. 数据表(table)

数据在硬盘上的存储:
- 硬盘最小存储访问单位为一个山区: 512B
 - 文件系统访问硬盘的单位通常为: 4KB
 - RDBMS最小的存储单位是database page size
- Data page size可以设置为1~多个文件系统的page
 - 例如, 4KB、8KB …
 
 

page内部结构:
- page header:存储page的一些信息,例如page ID
 - slot:是一个定长整数数组,从后向前增长
 - 记录:header和slot之间的空间,从前向后增长
 


数据的顺序访问:
1  | select Name, GPA  | 
- 顺序读取、student表的每个page
 - 对于每个page,顺序访问每个tuple
 - 检查条件是否成立
 - 对于成立的读取Name和GPA
 
有什么性能问题吗?假如有100个系呢?
2.4.2. 索引(index)
Selective Data Access(有选择性的访问)
- 使用Index
- Tree based index:有序,支持点查询和范围查询
 - Hash based index:无序,只支持点查询
 
 - Clustered index(主索引)与Secondary index(二级索引)
- Clustered: 记录就存在index中,记录顺序就是index顺序
 - Secondary: 记录顺序不是index顺序,index中存储page ID和in-page tuple slot ID.
 
 
- Chained Hash Table:

 
在硬盘上怎么存?
bucket = page
当chain上平均bucket数太多是,需要增大size,重新hashing。
- B+ Tree

 
- 所有的叶子结点都位于同一层
 - 每个叶子节点是一个page
 - 所有key存储在叶子节点
 - 内部节点完全是索引作用
 
叶子节点结构:
- Keys按照从小到达顺序排列: key1 < key2 < … < keyn
 - 叶子节点从左向有也是从小到达顺序排列,以sibling pointer链起来(ptr = record ID; sibling = page ID)
 
Search代价:
- 共有N个key
 - 每个节点的child/pointer个数为B
 - 总I/O次数=树高 $O(log_B N)$
 - 总比较次数
- 每个节点内部二分查找: $O(log_2 B)$
 - $O(log_B N)*O(log_2 B) = O(log_2 N)$
 
 
删除操作:
- Search 人后在节点中删除
 - node merge?
- 原设计:当节点中key个数小于一把
 - 实际实现:数据总趋势是增长的,可以只有节点为空是才node merge或者完全不进行node merge
 
 
Range Scan:
- 找到其实叶节点,包括范围起始值
 - 沿着叶的链接读下一个叶节点
 - 直到遇到范围终止值
 

- 二级索引
假设已经建立了以Major为key的二级索引1
2
3select Name, GPA
from Student
where Major = '计算机' 
- 在二级索引中搜索 Major = ‘计算机’
 - 对于每个匹配项,访问相应的tuple
 - 读取Name和GPA
 

2.4.3. 缓冲池(buffer pool)

数据访问的局部性:
Temporal locality(时间局部性)
- 同一个数据元素可能会在一段时间内多次被访问
 - buffer pool
 
Spatial locality(空间局部性)
- 位置相近的数据元素可能会被一起访问
 - Page 为单位读写
 
访问page的过程:
检查Page a是否在buffer pool中
是:buffer pool hit
- 直接访问buffer pool中的page a
 
否: buffer pool miss
- 在buffer pool中找到一个可用的frame
 - 从硬盘读page a,放入这个frame
 
2.4.3.1 缓存替换算法
- LRU(Least Recently Used)
 - Random
 - FIFO
 - Clock
 
数据库中使用Clock算法:
引用自
时钟置换算法可以认为是一种最近未使用算法,即逐出的页面都是最近没有使用的那个。我们给每一个页面设置一个标记位u,u=1表示最近有使用u=0则表示该页面最近没有被使用,应该被逐出。
按照1-2-3-4的顺序访问页面,则缓冲池会以这样的一种顺序被填满:
此时如果要按照1-5的顺序访问,那么在访问1的时候是可以直接命中缓存返回的,但是访问5的时候,因为缓冲池已经满了,所以要进行一次逐出操作,其操作示意图如下:
每次遍历到一个节点发现u=1的,将该标记位置为0,然后遍历下一个页面,一轮遍历完后,发现没有可以被逐出的页面,则进行下一轮遍历,这次遍历之后发现原先1号页面的标记位u=0,则将该页面逐出,置换为页面5,并将指针指向下一个页面。
但是考虑一个问题,数据库里逐出的页面是要写回磁盘的,这是一个很昂贵的操作,因此我们应该优先考虑逐出那些没有被修改的页面,这样可以降低IO。  
因此在时钟置换算法的基础上可以做一个改进,就是增加一个标记为m,修改过标记为1,没有修改过则标记为0。那么u和m组成了一个元组,有四种可能,其被逐出的优先顺序也不一样:
- (u=0, m=0) 没有使用也没有修改,被逐出的优先级最高;
 - (u=1, m=0) 使用过,但是没有修改过,优先级第二;
 - (u=0, m=1) 没有使用过,但是修改过,优先级第三;
 - (u=1, m=1) 使用过也修改过,优先级第四。
 
1  | U为访问位,M为修改位。  | 
2.5. 运算的实现
2.5.1. Operator tree

Query plan 最终将表现为一棵Operator Tree
- 每个节点代表一个运算
 - 运算的输入来自孩子节点
 - 运算的输出送往父亲节点
 
实现方法
- Operator at a time
- 完全处理一个运算在处理下一个运行,会产生大量中间结果
 
 - Pull(Tuple at a time)
- 每个Operator实现Open, Close, GetNext方法
 - 父节点调用子节点的GetNext() 取得下一个子节点的输出
 
 - Push: 多线程
- 子节点吧输出放入中间结果缓冲,然后通知父节点去读
 
 
- Operator at a time
 
2.5.2. Selection & Projection
Selection: 行的过滤
- 支持多种数据类型:数值类型,字符串类型等
 - 实现比较操作、数学运算、逻辑运算
 
Projection: 列的提取
- Query plan生成时,同时产生中间结果记录的schema
 - 主要功能: 从一个记录中提取属性,生成一个结果记录
 
2.5.3. Join
实现思路:
- Nested loop
 - Hashing
 - Sorting
 
Nested loop




Hashing

如果遇到R比内存大应该怎么办?
- GRACE Hash join


 
- sort merge join
 
- 思路:
- 如果把R按照R.a的顺序排序
 - 如果把S按照S.b的顺序排序
 - 那么可以Merge(归并)找出所有的匹配
 
 

- 比较:
- 通常代价比Hash Join稍差
 - 当一个表已经有序的情况下,会被使用
 
 
2.6. Query Optimization(查询优化)

2.7. 事务处理(Transaction Processing)
大量并发用户,少量随机读写操作
- 一个事务可能包含多个操作
- select
 - insert/delete/update
 - 等
 
 - 事务中的所有操作满足ACID性质
 
事务的表现形式:
- 没有特殊设置
那么每个SQL语句被认为是一个事务 - 使用特殊的语句
 
- 开始transaction
 - 成功结束transaction
 - 异常结束transaction
 
2.7.1. ACID
- Atomicity(原子性)
- all or nothing
 - 要么完全执行,要么完全没有执行
 
 - Consistency(一致性)
- 从一个正确状态转换到另一个正确状态(正确指:constraints, triggers等)
 
 - Isolation(隔离性)
- 每个事务与其它并发事务互不影响
 
 - Durability(持久性)
- Transaction commit后,结果持久有效,crash也不消失
 
 
2.7.2. Concurrency Control(并发控制后)
2.7.2.1. 数据冲突引起的问题:
- Read uncommitted data (读脏数据) (写读)
- 在T2 commit之前,T1读了T2已经修改了的数据
 
 - Unrepeatable reads(不可重复读) (读写)
- 在T2 commit之前,T1写了T2已经读的数据
 - 如果T2再次读同一个数据,那么将发现不同的值
 
 - Overwrite uncommitted data (更新丢失) (写写)
- 在T2 commit之前,T1重写了T2已经修改了的数据
 
 
两大类解决方案:
Pessimistic (悲观)
- 假设:数据竞争可能经常出现
 - 防止:采用某种机制保证数据竞争不会出现 – 如果一个Transaction T1可能和正在运行的其它Transaction有冲突,那么就让这个T1等待,一直等到有冲突的其它所有Transaction都完成为止,才开始执行。
 
Optimistic (乐观)
- 假设:数据竞争很少见
 - 检查:先执行,在提交前检查是否没有数据竞争 
- 允许所有Transaction都直接执行
 - 但是Transaction不直接修改数据,而是把修改保留起来
 - 当Transaction结束时,检查这些修改是否有数据竞争
- 没有竞争,成功结束,真正修改数据
 - 有竞争,丢弃结果,重新计算
 
 
 
2.7.2.2. 悲观解决方案
Pessimistic: 加锁
- 使用加锁协议来实现
 - 对于每个事务中的SQL语句,数据库系统自动检测其中的读、写的数据
 - 对事务中的读写数据进行加锁
 - 通常采用两阶段加锁(2 Phase Locking)
 
2 Phase Locking
- Pessimistic concurrency control
 - 对每个访问的数据都要加锁后才能访问
 - 算法如下
- 在Transaction开始时,对每个需要访问的数据加锁 – 如果不能加锁,就等待,直到加锁成功
 - 执行Transaction的内容
 - 在Transaction commit前,集中进行解锁
 - Commit
 
 - 有一个集中的加锁阶段和一个集中的解锁阶段
- 由此得名
 
 

实现细节2: Lock Granularity
- 锁的粒度是不同的
- Table?
 - Record?
 - Index?
 - Leaf node?
 
 - Intent locks
- IS(a):将对a下面更细粒度的数据元素进行读
 - IX(a):将对a下面更细粒度的数据元素进行写
 
 - 为了得到S,IS: 所有祖先必须为IS或IX
 - 为了得到X,IX: 所有祖先必须为IX
 

实现细节3: deadlock
如何解决deadlock问题?
死锁避免
- 规定lock对象的顺序
 - 按照顺序请求lock
 - 适用于lock对象少的情况
 
数据库的lock对象很多,不适合死锁避免
死锁检测
- 周期地对长期等待的Transactions检查是否有circular wait
 - 如果有,那么就选择环上其中一Transaction abort
 
2.7.2.3. 乐观的并发控制:不采用加锁
- 事务执行分为三个阶段
- 读:事务开始执行,读数据到私有工作区,并在私有工作区上完成事务的处理请求,完成修改操作
 - 验证:如果事务决定提交,检查事务是否与其它事务冲突 – 如果存在冲突,那么终止事务,清空私有工作区 – 重试事务
 - 写:验证通过,没有发现冲突,那么把私有工作区的修改
复制到数据库公共数据中 
 - 优点:当冲突很少时,没有加锁的开销
 - 缺点:当冲突很多时,可能不断地重试,浪费大量资源,甚至无法前进
 
另一种并发控制方法:Snapshot Isolation
- 一种Optimistic concurrency control
 - Snapshot: 一个时点的数据库数据状态
 - Transaction
- 在起始时点的snapshot
 - 读:这个snapshot的数据
 - 写:先临时保存起来,在commit时检查有无冲突,有冲突就abort
- First writer wins
 
 
 


2.7.3. Crash Recovery(崩溃恢复)
2.7.3.1. Durability (持久性) 如何实现?
- Transaction commit后,结果持久有效,crash不消失
 - 想法一
- 在transaction commit时,把所有的修改都写回硬盘
 - 只有当写硬盘完成后,才commit
 
 - 有什么问题?
- 正确性问题:如果写多个page,中间掉电,怎么办?
Atomicity被破坏了! - 性能问题:随机写硬盘,等待写完成
 
 - 正确性问题:如果写多个page,中间掉电,怎么办?
 
解决方案:WAL (Write Ahead Logging)
- 什么是Logging
 - 什么是Write-Ahead
- Logging 总是先于实际的操作
 - Logging 相当于意向,先记录意向,然后再实际操作
 
 - 怎样保证Durability
 - 怎么实现Write-Ahead Logging
 - Crash Recovery
 
Checkpoint:
- 为什么要用checkpoint?
- 为了使崩溃恢复的时间可控
 - 如果没有checkpoint,可能需要读整个日志,redo/undo很多工作
 
 - 定期执行checkpoint
 - checkpoint的内容
- 当前活动的事务表:包括事务的最新日志的LSN
 - 当前脏页表:每个页最早的尚未写回硬盘的LSN
 
 
2.7.3.2. 崩溃恢复
- 分析阶段
 
- 找到最后一个检查点
- 检查点的位置记录在硬盘上一个特定文件中
 - 读这个文件,可以得知最后一个检查点的位置
 
 - 找到日志崩溃点
- 如果是掉电等故障,必须找到日志的崩溃点
 - 当日志是循环写时,需要从检查点扫描日志,检查每个日志页的校验码,发现校验码出错的位置,或者LSN变小的位置
 
 - 确定崩溃时的活跃事务和脏页
- 最后一个检查点时的活跃事务表和脏页表
 - 正向扫描日志,遇到commit, rollback, begin更新事务表 – 同时记录每个活动事务的最新LSN
 - 遇到写更新脏页表 – 同时记录每个页的最早尚未写回硬盘的LSN
 
 
- Redo阶段
 
- 目标:把系统恢复到崩溃前瞬间的状态
 - 找到所有脏页的最早的LSN
 - 从这个LSN向日志尾正向读日志
- Redo每个日志修改记录
 
 - 对于一个日志记录
- 如果其涉及的页不在脏页表中,那么跳过
 - 如果数据页的LSN>=日志的LSN,那么跳过 – 数据页已经包含了这个修改
 - 其它情况,修改数据页
 
 
- Undo阶段
 
- 目标:清除未提交的事务的修改
 - 对于所有在崩溃时活跃的事务
- 找到这个事务最新的LSN
 - 通过反向链表,读这个事务的所有日志记录
 
 - undo所有未提交事务的修改
- Undo时,比较数据页的LSN和日志的LSN
 - if (数据页LSN>=日志LSN) 时,才进行undo
 
 
2.8. 数据仓库
2.8.1. OLAP
2.8.2. 行式与列式数据库
列式存储:
- 数据仓库的分析查询
- 大部分情况只涉及一个表的少数几列
 - 会读一大部分记录
 
 - 在这种情况下,行式存储需要读很多无用的数据
 - 采用列式存储可以降低读的数据量
 
列式存储的压缩
- 每个文件存储相同数据类型的值
 - 数据更容易被压缩
 - 比行式存储有更高的压缩比
 
列式存储的问题
- 如果用到了一个表的多个列
 - 太多列拼装在一起,付出拼装代价很大
 
2.9. 分布式数据库
2.9.1. 系统架构
三种架构
- Shared memory
- 多芯片、多核
 - 或Distributed shared memory
 
 - Shared disk
- 多机连接相同的数据存储设备
 
 - Shared nothing
- 普通意义上的机群系统
 - 由以太网连接多台服务器

 
 
关键技术
Partitioning(划分)
- 把数据分布在多台服务器上
 - 通常采用Horizontal partitioning – 把不同的记录分布在不同的服务器上
 
Replication(备份)
- 为了提高可靠性
 - 对性能的影响 
- 读?可能提高并行性
 - 写?额外代价
 
 
Hash partitioning
- 类似GRACE: machine ID = hash(key) % MachineNumber
 
Range partitioning
- 每台服务器负责一个key的区间,所有区间都不重叠
 
2.9.2. 分布式查询处理
2.9.3. 分布式事务处理
2.9.3.1. 2 Phase Commit
- Participant: 完成分布式事务的部分读写操作
 - Coordinator: 协调分布式事务的进行
 
- phase 1 (voting)

 
- Coordinator向每个participant发送query to commit消息
 - 每个participant根据本地情况回答yes 或 no
 
- phase 2 (completion)

 
当所有的回答都是yes, transaction 将commit
Coordinator向每个participant发送commit消息
Participant 回答acknowledgment
当至少一个的回答是no, transaction 将abort
Coordinator向每个participant发送abort消息
Participant 回答acknowledgment
2.9.3.2. 崩溃恢复
- 恢复时日志中可能有下述情况
- 有commit或abort记录:那么分布式事务处理结果已经收到,进行相应的本地commit或abort
 - 有prepare,而没有commit/abort:那么分布式事务的处理结果未知,需要和prepare记录中的coordinator进行联系
 - 没有prepare/commit/abort:那么本地abort
 
 











