阅读更多
1 history
1960s - Integrated Data Store, IDS
Network data model
:见下图Tuple-at-a-time
1960s - Information Management System, IMS
Hierarchical data model
:见下图Programmer-defined physical storage format
Tuple-at-a-time
1970s - Relational Model
Store database in simple data structures
Access data through high-level language
Physical storage left up to implementation
- 早期的实现包括
System R
INGRES
Oracle
1980s - Relational Model
Relation Model
在角逐中胜出,SEQUEL
演变成为SQL
Oracle
在商业角逐中胜出Stonebraker
创立了Postgre
1980s - Object-Oriented Databases
- 大多数这一阶段产生的
DBMS
在今天都不存在了,但是这些技术以另一种方式存在,比如JSON/XML
等
1990s - Boring Days
- 这十年中,数据库系统没有重大进步
- 微软借鉴了
Sybase
,创立了SQL Server
Mysql
出现,作为mSQL
的一种替代方案Postgres
支持SQL
SQLite
在2000年早期出现
2000s - Internet Boom
- 网络大发展,分布式兴起
- 原有的数据库都是重量级且及其昂贵的,在分布式的场景中不再有优势
- 各大公司都独立开发中间件,用以支持
DBMS
的水平伸缩
2000s - Data Warehouses
OLAP
兴起Relational / SQL
- 分布式、
Shared-Noting
架构 - 列存大放异彩
2000s - NoSQL Systems
- 专注于高可用和高可扩展
Non-relational data model
,例如键值对- 无
ACID
事务 API
取代了SQL
2010s - NewSQL
- 在支持
ACID
事务的同时,提供与NoSQL
相当的性能 Relational / SQL
- 分布式
2010s - Hybrid Systems
Hybrid Transactional-Analytical Processing, HTAP
- 同时提供
OLTP
和OLAP
的功能和性能 - 分布式、
Shared-Noting
架构 Relational / SQL
2010s - Cloud Systems
DBaaS, Database-as-a-service
2010s - Shared-Disk Engines
- 存储计算分离
- 通常用于数据湖(
Data Lake
)
2010s - Graph Systems
- 提供了针对图形的API
- 研究表明,尚不清楚使用以图形为中心的执行引擎和存储管理器是否有任何好处
2010s - Timeseries Systems
- 时序数据库,主要存储时序相关的数据
Andy's Thoughts
- 随着专用系统扩展其领域范围,
DBMS
类别的分界线将随着时间的推移而继续模糊 - 我相信关系模型和声明式查询语言促进了更好的数据工程
2 inmemory
2.1 Disk-Oriented DBMSs
Buffer Pool
Steal + No-Force
示意图参考课件中的7 ~ 13
页
2.2 In-Memory DBMSs
首批商用的In-Memory DBMS
在1990s
发布,包括:
TimesTen
DataBlitz
Altibase
索引:
1980s
提出了专门的主存索引,当时高速缓存和内存访问速度大致相当- 但后来高速缓存的速度远远大于内存的访问速度时,内存优化索引的性能比
B+
树差,因为它们不支持缓存(为啥会不支持缓存)
执行查询计划:
- 由于数据都在内存中,顺序访问不再比随机访问快
- 传统的
tuple-at-a-time
的访问方式会因为函数调用的开销而变得很慢。这一情况在OLAP
中更加突出
Logging & Recovery
In-Memory DBMS
也需要将WAL
写入非易失性存储上,因为系统可能随时崩溃- 由于不存在
Dirty Page
,因此无需追踪整个系统中的LSN
性能瓶颈:对于In-Memory DBMS
来说,I/O
不再是性能瓶颈,同时其他开销也会被放大:
Locking/Latching
Cache-line misses
Pointer chasing
Predicate evaluations
Data movement & copying
Networking
2.3 Concurrency Control Bottlenecks
对于In-Memory DBMS
而言,事务获取锁的开销和访问数据的开销相当
DBMS
可以将Lock Information
与数据存储在一起,提高CPU Cache Locality
- 需要用
CAS
替代Mutex
Concurrency Control Schemes
Two-Phase Locking, 2PL
DeadLock Detection
DeadLock Prevention
- 示意图参考课件中的
30 ~ 37
页
Timestamp Ordering, T/O
Basic T/O
Optimistic Concurrency Control, OCC
- 示意图参考课件中的
40 ~ 63
页
仿真结果参考课件中的71 ~ 75
页
Schemes
DL_DETECT
:2PL w/ DeadLock Detection
NO_WAIT
:2PL w/ Non-waiting Prevention
WAIT_DIE``2PL w/ Wait-and-Die Prevention
TIMESTAMP
:Basic T/O Algorithm
MVCC
:Multi-Version T/O
OCC
:Optimistic Concurrency Control
Bottlenecks
Lock Thrashing
:DL_DETECT
、WAIT_DIE
- 按照
primary key
的顺序来获取锁,彻底消除死锁
- 按照
Timestamp Allocation
:WAIT_DIE
、All T/O Algorithm
Mutex
Atomic Addition
Batched Atomic Addition
Hardware Clock
Hardware Counter
Memory Allocations
:OCC
、MVCC
- 不要使用默认的
malloc
- 不要使用默认的
3 mvcc1
DBMS
对每个逻辑对象维护了多个物理版本
- 当事务写某个对象时,会创建该对象的一个新的物理版本
- 当事务读某个对象时,会读取事物开始时该对象的最新版本。用时间戳来判断可见性
- 写操作不阻塞读操作
- 读操作不阻塞写操作
Snapshot Isolation, SI
:
- 若两个事务同时更新同一个对象,那么时间上较早写入的事务获胜
- 会产生
Write Skew Anomaly
,示意图参考课件中的5 ~ 9
页
MVCC
主要设计点:
Concurrency Control Protocol
Timestamp Ordering
read-ts
:用于记录最近一次读取的时间- 若
Latch
未被其他事务持有,且Tid
介于begin-ts
和end-ts
之间,那么该记录对事务T
可见 - 若
Latch
未被其他事务持有,且Tid > read-ts
,那么事务T
可以创建当前记录的一个新版本
Optimistic Concurrency Control
Two-Phase Locking
- 使用
read-cnt
作为Shared Lock
;使用txn-id
以及read-cnt
作为Exclusive Lock
- 若
txn-id = 0
,那么递增read-cnt
字段来表示获取Shared Lock
- 若
txn-id = 0 && read-cnt == 0
,那么将txn-id
设置成当前事务的Tid
,且递增read-cnt
字段来表示获取Exclusive Lock
- 使用
- 示意图参考课件中的
15 ~ 40
页
Version Storage
Append-Only Storage
- 新版本会追加到
table
所在的同一个存储空间 Version Chain Ordering
Oldest-to-Newest(O2N)
- 新版本追加到链的尾部
- 查找时,需要遍历整个链
Newest-to-Oldest(N2O)
- 新版本插入到链的首部,有额外的更新指针的操作
- 查找时,无需遍历整个链
- 新版本会追加到
Time-Travel Storage
- 维护两个数据表,一个叫做
Main Table
,另一个叫做Time-Travel Table
- 每次更新时,都会将当前记录移动到
Time-Travel Table
- 同一记录的所有版本以
Newest-to-Oldest
的方式关联起来
- 维护两个数据表,一个叫做
Delta Storage
- 维护两个数据表,一个叫做
Main Table
,另一个叫做Delta Storage Segment
- 每次更新时,将数值的变化表达式记录到
Delta Storage Segment
中 - 可以通过重放
Delta Storage Segment
来重建老的版本
- 维护两个数据表,一个叫做
Non-Inline Attributes
- 维护两个数据表,一个叫做
Main Table
,另一个叫做Variable-Length Data
- 通过指针复用那些在多个版本间没有变化的属性
- 需要额外维护计数器
- 内存分配复杂
- 维护两个数据表,一个叫做
Garbage Collection
DBMS
需要持续移除那些可回收的版本- 对任何事务都不可见的版本
- 终止的事务创建的版本
- 主要设计要点包括
- 如何查找过期版本
- 如何判断版本是否可回收
- 到哪查找过期版本
- 实现方式
Tuple-Level
- 直接检查每个元组
Background Vacuuming
:周期性地扫描数据表,由额外的线程池完成Cooperative Cleaning
:在事务查找最新的可见版本时,进行清理。由事务线程完成,只能用于O2N
Transaction-Level
- 事务记录了数据修改前的版本,因此可以通过事务查找这些过期版本
Index Management
Primary Key Indexes
:总是指向Version Chain Header
Secondary Indexes
:更加复杂Logical Pointers
- 每个
Tuple
需要维护一个固定的标识符(不同版本中,该标识符保持一致) - 需要一个中间层,来做标识符到物理地址的转换(物理地址指向
Version Chain Header
) - 标识符可以是
Primary Key
或者Tuple Id
- 每个
Physical Pointers
- 维护一个指向
Version Chain Header
的指针
- 维护一个指向
MVCC Indexes
MVCC DBMS
通常不在索引上直接存储版本信息- 索引需要支持
Duplicate Key
,同一个Key
可能指向不同的Logical Tuple Snapshot
- 示意图参考课件中的
87 ~ 93
页
4 mvcc2
4.1 Microsoft Hekaton (SQL Server)
Hekaton MVCC
:
- 记录的每个版本都维护了两个时间戳
BEGIN-TS
:活跃事务的BeginTS
,或者是已提交事务的CommitTS
END-TS
:Infinity
,或者是已提交事务的CommitTS
- 示意图参考课件中的
6 ~ 24
页
- 维护了一个全局的
Transaction state map
ACTIVE
:事务进行中VALIDATING
:事务触发了Commit
,且DBMS
正在校验合法性COMMITTED
:事务已经结束,但是尚未修改由该事务创建的所有版本的时间戳TERMINATED
:事务已经结束,且已经修改由该事务创建的所有版本的时间戳
- 只使用
Lock-Free
的数据结构- 唯一的串行点是时间戳的分配
4.2 TUM HyPer
HyPer MVCC
:
Delta Storage
以及Column Storage
- 非索引字段可以原地更新
- 插入、删除操作会更新索引
N2O Version Chain
No Predicate Locks
、No Scan Checks
- 通过直接终止那些试图修改未提交记录的事务,来避免写冲突
- 示意图参考课件中的
33 ~ 37
页(完全没看懂)
4.3 SAP HANA
SAP HANA MVCC
:
Time-Travel Storage
(N2O
)Main Data Table
中存储的是最老的版本- 每个
Tuple
维护一个标识位,用于表示Version Space
中是否有新版本 - 维护一个
Hash Table
,用于映射Record Identifier
和Version Chain Header
4.4 CMU Cicada
CMU Cicada MVCC
:
In-Memory DBMS
Append-Only-Storage
(N2O
)Best-effort Inlining
4.5 Summary
MVCC Limitations
:
Computation & Storage Overhead
- 大多数
MVCC
方案都会使用间接的方式来搜索Version Chain
,这会增加CPU Cache Miss
- 需要频繁的垃圾回收,来减小版本搜索的开销
- 大多数
Shared Memory Writes
- 大多数
MVCC
方案将版本信息存储在全局的内存中,而没有考虑局部性(CPU Cache Miss
)
- 大多数
Timestamp Allocation
- 所有线程访问同一个
Counter
- 所有线程访问同一个
OCC LIMITATIONS
:
Frequent Aborts
- 频繁地终止事务,尤其是高并发场景下
Extra Reads & Writes
- 事务需要将记录拷贝到私有的空间,来保证可重复度
- 同时,提交时需要检查读是否满足一致性
Index Contention
5 mvcc3
5.1 MVCC Deletes
当一个Tuple
的所有版本都逻辑删除后,DBMS
才会对其进行物理删除
如何表示逻辑删除?有如下两种方式:
Deleted Flag
- 在最新的版本后面增加一个标志位,用于表示逻辑删除
Tombstone Tuple
- 创建一个空的物理版本来表示逻辑删除
- 用一个独立的
Pool
来存储这些Tombstone Tuple
,每个Tombstone Tuple
只需要1-bit
的存储空间
5.2 Garbage Collection
设计要点:
Index Clean-up
Version Tracking Level
Tuple-Level
Background Vacuuming
Cooperative Cleaning
Transaction-Level
Epochs
Frequency
:Trade-off
,过于频繁,浪费CPU资源,且降低事务的效率;过于不频繁,浪费存储空间,降低版本查找的效率Periodically
:定期触发,或者某些指标达到阈值后触发Continuously
:将清理过程作为事务处理的一部分
Granularity
Single Version
- 单独追踪每个版本的可见性,单独回收
- 控制粒度更细,但是开销大
Group Version
- 以分组的方式管理版本,且以分组为单位进行回收。每个分组包含多个版本
- 开销较低,但是会延迟回收(分组中的所有版本都可以回收时,才能回收整个分组)
Table
(P29,没懂)
Comparison Unit
Timestamp
- 用一个全局最小的时间戳来判断版本是否可以回收
- 实现简单
Interval
- 用区间来判断可见性
- 实现复杂
5.3 Block Compaction
DBMS
需要将那些未满的块合并成更少的块,以减少内存使用量
6 oltpindexes1
6.1 In-Memory T-Tree
B+ Tree
是为了提高在速度较低的存储介质上的访问速度。而T-Tree
是针对内存数据库的一种替代方案,它基于AVL Tree
T-Tree
的节点包含如下属性
Data Pointers
:指向数据的指针Parent Pointer
:指向父节点的指针Left Child Pointer
:指向左孩子的指针Right Child Pointer
:指向右孩子的指针Max-K
:当前节点指向的数据中的最大值。若Key > Max-K
,那么Key
可能存在于右孩子中Min-K
:当前节点指向的数据中的最小值。若Key < Min-K
,那么Key
可能存在于左孩子中
优势:
- 由于其不直接存储节点数据,因此占用内存更少
劣势:
- 难以平衡
- 难以实现并发安全
示意图参考课件中的7 ~ 22
页
6.2 Latch-Free Bw-Tree
A new form of B tree, Bw-tree achieves its very high performance via a latch-free approach that effectively exploits the processor caches of modern multi-core chips.
6.2.1 Delta Updates
更新时,生成对应的Delta
,Delta
指向原链表头,然后通过CAS
操作替换Mapping Table
中的指针,使其指向自己。若失败,则终止或重试
示意图参考课件中的23 ~ 40
页
6.2.2 Garbage Collection
我们需要知道何时可以安全地为Latch-Free Index
中的已删除节点回收内存
Reference Counting
- 计数为
0
时,可以删除 - 在多核CPU上,并发性能较差。因为递增递减计数器,会导致大量的缓存一致性流量
- 我们其实并不关心计数器的大小是多少,而只是希望在计数值为
0
的时候可以进行回收(允许延迟,不必立即回收)
- 计数为
Epoch-based Reclamation
(没懂)- 维护一个全局的
Epoch Counter
,并对其进行周期性地更新- 跟踪在一个
Epoch
期间哪些线程进入索引以及它们何时离开
- 跟踪在一个
- 标记删除节点时标记节点的当前
Epoch
- 一旦所有线程都离开该
Epoch
,就可以回收该节点
- 一旦所有线程都离开该
- 操作用一个
Epoch
进行标记- 每个
Epoch
都会跟踪属于它的线程以及可以回收的对象 - 线程在每个操作之前加入一个
Epoch
,并发布可以为当前Epoch
回收的对象(不一定是它加入的那个)
- 每个
- 示意图参考课件中的
49 ~ 57
页
- 维护一个全局的
Hazard Pointers
6.2.3 Structure Modifications
Split Delta Record
:
- 标记
Page
的某个Key Range
现在位于另一个Page
- 使用
Logical Pointer
指向New Page
Separator Delta Record
:
- 在修改
Page
的Parent
中记录New Page
的搜索范围
示意图参考课件中的59 ~ 70
页
6.3 B+ Tree Optimistic Latching
优化1:Pre-Allocated Delta Records
- 在
Page
中预先分配空间,用于存放Delta
优化2:Mapping Table Expansion
- 最高效的内存数据结构就是数组。但是为每个索引都分配一个完整的数组会比较浪费
- 使用虚拟内存分配整个数组。仅在访问对应偏移量的时候,才分配对应的物理内存(如何实现???)
7 oltpindexes2
7.1 Latches
Latch
方案的目标:
- 更小的内存占用
- 执行效率更高(无冲突)
- 当前线程等待时间过长时,取消调度线程
Latch
的实现方案:
Test-and-Set Spinlock
- 高效(指令简单)
- 扩展性差,
Cache
不友好,OS
不优化(在最坏的情况下,可能浪费大量CPU资源) std::atomic<T>
Blocking OS Mutex
- 使用简单
- 扩展性差(每个
lock
/unlock
大约花费25ns) std::mutex
Adaptive Spinlock
- 在用户态自旋一段时间
- 若在自旋期间无法获取锁,就会让出
CPU
,并记录到Parking Lot
中 - 线程在进入自旋期间,看下是否有其他线程位于
Parking Lot
中,若是,择直接将自己挂起,避免无效自旋 - 苹果的
WTF::ParkingLot
Queue-based Spinlock
- 比
Mutex
更高效,且具有更好的Cache Locality
std::atomic<Latch*>
- 比
Reader-Writer Locks
- 允许并发读
- 需要分别管理
Read Queue
、Write Queue
,以防止饿死 - 可以基于
Spinlock
实现
7.2 B+ Tree
B+ Tree
是一种平衡树,其访问、插入、删除的复杂度都是O(log n)
在访问一颗B+ Tree
的过程中,需要获取或者释放Latch
- 当某个节点的孩子节点都被认为是安全时,可以释放该节点的
Latch
- 安全意味着,不会发生
Split
以及Merge
操作
- 安全意味着,不会发生
Search
:从根节点开始,向下重复执行如下过程- 获取孩子节点的
Read Latch
- 释放父节点的
Read Latch
- 获取孩子节点的
Insert/Delete
:从根节点开始,向下重复执行如下过程- 获取孩子节点的
Write Latch
- 若孩子是安全的,那么释放该孩子节点的所有祖先节点的
Write Latch
- 获取孩子节点的
- 示意图参考课件中的
35 ~ 49
页
对于Insert/Delete
的访问流程,由于直接沿路径加了Write Latch
,这样很容易导致整个流程串行化,一个优化方式是:
- 假设目标的
Leaf Node
是安全的,先用Read Latch
访问,如果在访问路径中发现任何不安全的节点,再退回到上面这个版本 - 示意图参考课件中的
51 ~ 54
页
Versioned Latch
- 每个节点维护一个版本信息(
Counter
) - 写操作获取
Latch
时,会递增该版本信息(释放不会递减) - 读操作查看
Latch
是否可用,但是不获取它,只是记录当时的版本信息。在读操作找到目标叶子节点后,再检查版本信息是否发生过变化。若发生过变化,那么终止或者重试读操作 -
- 示意图参考课件中的
56 ~ 69
页(没看懂)
- 示意图参考课件中的
7.3 Trie Index
Trie Index
又称为字典树或者前缀树
- 其形状只依赖于
Key Space
以及其长度 - 不需要平衡操作
- 所有操作的复杂度是
O(k)
,其中k
是Key
的长度 Key
是隐式存储的,从根到叶节点的整条路径,表示了某个Key
- 示意图参考课件中的
75 ~ 83
页
Radix Tree
- 作为唯一孩子节点的每个节点都与其父节点合并
- 又称为
Patricia Tree
- 会产生假阳性(
False Positive
),所以DBMS
需要再次校验Key
是否匹配
7.3.1 Judy Array
Judy Array
是256-Way Radix Tree
的变体,是第一种已知的能够实现自适应节点表示的Radix Tree
:
- 支持下三种类型:
Judy1
:将Integer Key
映射成单个bit, true or false
JudyL
:将Integer Key
映射成Integer Value
JudySL
:将Variable-Length Key
映射成Integer Value
- 节点的元数据被打包成一个
128-bit
的Judy Pointers
,存储在父节点中Node Type
Linear Node
:稀疏- 分为左右两部分,左边存储排序后的
Key
,右边存储对应于Key
的指向孩子的指针
- 分为左右两部分,左边存储排序后的
Bitmap Node
:常规(好像理解的不太对)- 包含一个长度为256的
Bitmap
,用于表示某个Key
是否存在(Key
大小为1-byte
,范围是0-255,正好作为Bitmap
数组的下标) - 每
8-bit
一组,总共分为32组,可以隐式存储32个Key
- 包含一个长度为256的
Uncompressed Node
:密集- 类似于下面要介绍的
ART-Node256
- 类似于下面要介绍的
Population Count
Child Key Prefix / Value
64-bit Child Pointer
7.3.2 ART
Adapative Radix Tree, ART
- ART-Paper
- 被应用于
TUM HyPer DBMS
256-Way Radix Tree
,基于分布,支持不同的节点类型- 每个节点的元数据存储在其
Header
中
ART vs. Judy
Judy
有3中不同的节点类型。ART
有4种不同的的节点类型(基于孩子的数量)Judy
是一个通用的关联数组。它拥有键和值。ART
是表索引,不需要覆盖完整的键,其值是指向元组的指针
ART
的节点类型:
Node4
Node4
包含两个最大长度为4的数组,一个数组用于存储Key
(1-byte
),另一个用于存储Pointer
(8-byte
)。Key
和Pointer
一一对应。总大小是1byte * 4 + 8byte * 4 = 36 byte
Node16
Node16
,其结构与Node4
类似,包含两个最大长度为8的数组,一个数组用于存储Key
(1-byte
),另一个用于存储Pointer
(8-byte
)。Key
和Pointer
一一对应。总大小是1byte * 16 + 8byte * 16 = 144 byte
- 搜索时,可以采用二分查找,或者直接利用
SIMD
指令
Node48
Node48
结构上和Node4
、Node16
有所不同,包含两个长度不同的数组,一个数组长度是256,每个元素1-byte
,另一个数组长度48,每个元素8-byte
,用于存储Pointer
。总大小是1byte * 256 + 8byte * 48 = 640 byte
- 该结构隐式存储了48个
Key
。由于Key
的大小是1-byte
,其数值刚好是0-255
,正好作为第一个数组的下标,第一个数组中存储的值,同时作为第二个数组的下标(由于第二个数组的长度是48,因此1-byte
完全可以表示)
- 该结构隐式存储了48个
Node256
Node256
只有一个长度为256的数组,用于存储Pointer
(8-byte
)。总大小是8byte * 256 = 2048 byte
- 该结构同样隐式存储了256个
Key
,由于Key
的大小是1-byte
,其数值刚好是0-255
,正好作为数组的下标 - 正因为
Node48
的Pointer
数组只有48个元素,无法用Key Byte
直接索引,于是才引入了一个长度为256
,大小为1-byte
的数组,充当第一级索引
- 该结构同样隐式存储了256个
7.3.3 Masstree
Masstree
的每个节点都是一颗B+ Tree
8 storage
8.1 Type Representation
int/bigint/smallint/tinyint
:直接用C++
基本类型表示float/real/numeric
:IEEE-754 Standard- 可变精度,存在精度损失
- 计算效率高
decimal
:Fixed-point Decimalstime/date/timestamp
:32/64-bit int
varchar/varbinary/text/blob
- 指针
- 包含当前长度和下一个位置的指针的
Header
8.2 Data Layout / Alignment
以字对齐(Word-Aligned
)的方式存储,能够显著提高吞吐率
No Alignment
Alignment With Padding
Alignment With Padding + Sorting
8.3 Storage Models
N-ary Storage Model, NSM
:
- 连续存储单个
Tuple
的所有属性 OLTP
的理想存储模型,因为OLTP
通常读写单个Tuple
,且经常会有批量插入操作- 使用
tuple-at-a-time
的迭代模型 - 优势:
- 插入、删除、更新操作快
- 对于需要获取
Tuple
所有属性的查询更友好 - 可以使用面向索引的存储
- 劣势:
- 对于扫描全表的操作不友好
- 对于需要获取
Tuple
部分属性的查询不友好
Decomposition Storage Model, DSM
:
- 单独存储
Tuple
的每个属性 OLAP
的理想存储模型,因为OLTP
通常会对某几个属性进行大范围或全表扫描- 优势:
- 无需读取不需要的属性
- 更好的压缩性能
- 劣势:
- 插入、删除、更新、点查较慢
Tuple Identification
Fixed-length Offsets
:Tuple
中的每个属性的偏移量都是相同的Embedded Tuple Ids
:Tuple
中的每个属性额外存储其Tuple Id
Data Organization
Insertion Order
Sorted Order
Partitioned
Hybrid Storage Model
:
Hot Data vs. Cold Data
- 数据插入后,有很大概率会被再次更新
- 经过一段时间后,数据大概率只会被读取
Execution Engine
Separate Execution Engines
:针对NSM
以及DSM
两种存储模型,使用两个独立的Execution Engine
- 需要合并来自两个引擎的结果
- 需要事务跨两个引擎,那需要使用
2PC
- 实现方式有2种
Fractured Mirrors
:Oracle, IBMDelta Store
:SAP HANA
Single, Flexible Architecture
:使用一个能够兼容处理NSM
以及DSM
两种存储模型的Execution Engine
- 不需要存储数据库的两个副本
- 不需要同步
Database Segments
- 实现方式
Peloton Ataptive Storage
8.4 System Catalogs
几乎所有的DBMS
以存储普通数据的方式存储Cagalogs
。但由于Catalogs
的特殊性,需要有专门的引导代码。DDL
同样需要保证ACID
Schema Change
:
Add Columns
NSM
:将元组复制到新区域DSM
:只需创建新的列
Drop Columns
NSM
:- 将元组复制到新区域
- 标记为删除,后面再清理
DSM
:只需删除列
Change Columns
Indexes
:
Create Index
- 扫描全表,并填充索引
- 记录在扫描期间,由其他事务引起的变更
- 扫描结束后,锁表,将扫描期间记录的变更更新到索引上
Drop Index
- 逻辑删除索引
- 仅当删除索引的事务提交时,它才会变得不可见。在此之前,所有活跃的事务仍然需要更新它
8.5 小结
Hybrid Storage Model
已被抛弃,原因如下:
- 工程实现的难度太大
Delta Version Storage + Column Store
与之等价Catalog
实现难度大
9 compression
9.1 Compression Background
I/O
是DBMS
的瓶颈之一,它需要权衡速度与压缩率- 在现实场景中,数据倾斜普遍存在
- 同一个数据表中的不同列,存在很大的数据相关性
Compression
的目标:
- 产生定长的输出
- 尽可能地推迟解压缩的时机,这样可以使得处理的数据尽可能的小。也称为延迟物化(
Late Meterialization
) - 无损压缩
Data Skipping
:
Approximate Queries
:仅查询采样数据级,来产生近似的结果Zone Map
:提前计算每个数据块的元数据,比如最大值、最小值等。这样在查询的时候,可以检查这些元数据来决定是否跳过当前块
Compression Granularity
:
Block-Level
Tuple-Level
- 仅适用于
NSM
- 仅适用于
Attribute-Level
Column-Level
- 仅适用于
DSM
- 仅适用于
9.2 Naive Compression
Naive Compression
即采用一种通用算法对数据进行压缩,常用的算法包括:
- LZO(1996)
- LZ4(2011)
- Snappy(2011)
- Brotli(2013)
- Oracle OZIP(2014)
- Zstd(2015)
在这种方案下:
DBMS
必须先对数据进行解压缩,然后才能对其进行读写操作- 不考虑数据的高级含义以及语义
- 相等性比较可以在压缩的状态下进行(前提是,数据以相同的方式压缩)
9.3 OLAP Columnar Compression
常用的Columnar Compression
算法如下:
Null Supression
- 对连续的零值、空值或者空白进行压缩,将其替换成出现位置以及数量的描述信息(类似于游程编码)
- 适用于数据稀疏的场景
Run-length Encoding
- 将连续相同的值编码成一个三元组,即值,起始偏移量,长度
Bitmap Encoding
- 额外存储一个
Bitmap
用于映射属性的每个值 - 仅适用于基数较低的场景
- 上述
Bitmap
存在两种压缩方式General Purpose Compression
- 例如
LZ4
、Snappy
- 必须解码后才能进行数据处理
- 对内存数据库不友好
- 例如
Oracle Byte-Aligned Bitmap Codes
- 对
Bitmap
进行分类Gap Byte
:所有bit
都是0
Tail Byte
:部分bit
是1
- 对包含
Gap Bytes
以及Tail Bytes
的数据进行压缩Gap Byte
使用RLE
进行压缩Tail Bytes
- 若仅包含
1
,或者只有一个1
的情况,进行压缩 - 否则不压缩,直接存储
- 若仅包含
- 示意图参考课件中的
31 ~ 40
页 - 这种方式已被淘汰。虽然它提供了很好的压缩性能,但是效率较低。
Word-Aligned Hybrid, WAH
作为一种替代方案,提供了更好的性能- 这两种方式都不支持随机访问
- 对
- 额外存储一个
Delta Encoding
- 存储的不是值本身,而是变化
- 配合
RLE
可以达到更好的压缩效果
Incremental Encoding
- 类似于
Delta Encoding
的思路,Incremental Encoding
避免存储相同的前缀 - 在数据有序时,压缩效率更高
- 类似于
Mostly Encoding
- 当大部分数据小于该类型的最大值时,可以用更小的类型来存储。剩下无法用更小类型存储的数据还是保留原先的类型
Dictionary Encoding
- 何时构建字典
All At Once
- 扫描所有数据,构建字典
- 对于新插入的数据,使用额外的字典,或者重新计算
Incremental
- 将新插入的数据合并到已有的字典中
Scope
Block-Level
- 仅包含一个数据表中的部分数据
- 压缩率较低,更新效率高
Table-Level
- 压缩率较高,更新效率低
Multi-Level
- 可能包含多个表的部分数据或者全量数据
- 有时用于
Join
操作
Multi-Aggribute Encoding
- 字典可以横跨多个属性
Order-Preserving Encoding
- 编码后的值要保持与原有值相同的顺序
Dictionary Data Structures
Array
- 两个数组,一个数组包含变长的数据,另一个数组存储数据的偏移量
- 更新成本高
Hash Table
- 快速,紧凑
- 不支持范围、前缀查找
B+ Tree
- 比
Hash Table
慢,且需要更多内存 - 支持范围、前缀查找
- 比
- 何时构建字典
9.4 OLTP Index Compression
OLTP
无法使用OLAP
的压缩技术,因为它需要支持Tuple
的快速随机访问。即时进行压缩、解压缩操作会大大降低性能。同时,OLTP
中的索引会占用很多内存资源
10 recovery
Recovery
用于在发生故障时,确保数据库的原子性、一致性和持久性。它包含两个部分:
- 在事务正常进行时所执行的一系列操作(写
WAL
日志),来确保DBMS
能从故障中恢复 - 在
DBMS
进入异常状态时锁执行的一系列恢复操作,用于保证数据库的原子性、一致性和持久性
异常在数据库中是非常常见的,它不光指机器崩溃,还可能是如下场景:
OS
更新- 硬件更新
DBMS
更新
10.1 Logging Schemes
日志方案:
Physical Logging
- 直接记录更新的值
Logical Logging
- 记录的是高层级的操作,比如
UPDATE
、DELETE
、INSERT
等
- 记录的是高层级的操作,比如
刷新方式:
All-at-Once Flushing
- 直到事务提交之前,才将日志刷到磁盘
Incremental Flushing
- 允许在事务提交之前,将日志刷到磁盘
Early Lock Release
:在事务的Commit Log
刷入磁盘后,事务的锁便可以释放了,无需等到将结果传回客户端。因为从DBMS
的视角来看,此时事务已经结束了
MSSQL Constant Time Recovery
:属于Physical Logging
,且直接将MVCC
的Time-Travel
表作为Recovery Log
。因此无需再打印WAL
- 详细内容参考课件中的
12 ~ 21
页
SILO
,详细内容参考课件中的22 ~ 40
页
10.2 Checkpoint Protocols
理想的Checkpoint
应该具备以下特性:
- 不能影响正常事务的处理效率
- 不能引入较大的延迟
- 内存开销要小
Consistent vs. Fuzzy Checkpoints
Consistent Checkpoints
- 表示数据库在某个时间点的一致性快照。不包含未提交的更改
- 恢复时,无其他额外操作
Fuzzy Checkpoints
- 会包含
Checkpoint
启动后才提交的事务的更改 - 需要确保
Checkpoint
包含上述已提交事务的更改
- 会包含
Checkpoint Mechanism
Do It Yourself
- 同一个进程完成,可以利用
MVCC
来确定快照
- 同一个进程完成,可以利用
OS Fork Snapshots
Fork
一个子进程,拷贝父进程的内存,因此子进程的内存中包含快照所需的所有内容- 需要额外的操作来除去那些未提交的记录(利用Undo Log)
Checkpoint Contents
Complete Checkpoint
- 包含所有数据,包括从上个
Checkpoint
以来未发生改变的Tuple
- 包含所有数据,包括从上个
Delta Checkpoint
- 仅包含从上个
Checkpoint
以来发生改变的Tuple
- 在后台可以对多个
Checkpoint
进行合并
- 仅包含从上个
Frequency
Time-based
Log File Size Threshold
On Shutdown
10.3 Restart Protocols
Shared Memory Restarts
:
Shared Memory Heaps
Copy On Shutdown
11 networking
11.1 Database Access APIs
常见的API
有如下几种:
Direct Access, DBMS-Specific
Open Database Connectivity, ODBC
- 与
OS
和具体的DBMS
解耦,几乎每种主流的DBMS
都会有ODBC
的实现 ODBC
的设计采用了Device Driver
的模型,其中Driver
封装了将标准Standard-API
转换成DBMS-Specific-API
的逻辑
- 与
Java Database Connectivity, JDBC
- 可以理解成是
ODBC
的一个版本,只不过针对的是Java
语言而不是C
语言
- 可以理解成是
11.2 Database Network Protocols
几乎所有主流的DBMS
都基于TCP/IP
实现了一套通信协议,标准的C/S
交互流程如下:
Client
连接到DBMS
,进行鉴权流程Client
发送一个Query
DMBS
解析并执行Query
,然后将结果序列化后返回给Client
协议设计要点:
Row vs. Column Layout
ODBC
和JDBC
是典型的Row-Oriented
Compression
Naive Compression
Columnar-Specific Encoding
Data Serialization
Binary Encoding
- 客户端需要进行额外的字节序列的转换
- 序列化后体积小
- 可以基于现有的框架来实现,比如
ProtoBuffers
、Thrift
、FlatBuffers
等
Text Encoding
- 将所有类型都转换成
String
- 序列化后体积大
- 客户端反序列化简单
- 将所有类型都转换成
String handling
Null Termination
:在末尾加\0
,表示String
的结束Length-Prefixes
:在Header
中增加长度信息Fixed Width
:所有String
长度固定,不足需要补白
11.3 Replication Protocols
DBMS
会通过网络同步各个节点之间的数据,用以提升集群的可用性
设计要点:
Replica Configuration
Master-Replica
- 所有更新必须经过
Master
节点 Master
节点会将更新同步给其他Replica
节点Read-Only
的事务可以访问Replica
节点Master
如果宕机了,那么会重新选举出一个Master
- 所有更新必须经过
Multi-Master
- 所有节点均支持数据更新
- 节点之间的数据同步需要使用
Atomic Commit Protocol
Propagation Scheme
:当事务提交时,DBMS
需要决定是否需要等到该事务的变更都同步到其他节点后,才通知客户端事务已完成Synchronous
:强一致性Asynchronous
:最终一致性
11.4 Kernel Bypass Methods
网络通信协议的实现并不是降低系统效率的唯一原因。TCP/IP
协议栈本身也是原因之一,比如:
- 上下文切换、中断的开销
- 数据拷贝(内核空间和内核空间)
- 内核中大量的
Latches
Kernel Bypass Methods
允许DBMS
直接从Network Interface Card, NIC
中获取数据
- 没有数据拷贝
- 没有
TCP/IP
协议栈
Kernel Bypass Methods
的实现方式有如下两种:
Data Plane Development Kit
Remote Direct Memory Access
- 直接读写远程主机的内存,必须知道正确的地址
DBMS
本身不感知这个过程
12 scheduling
对于每个查询计划,DBMS
必须决定何时、何地、以及如何执行
12.1 Process Models
Process Models
有以下三种
Process per DBMS Worker
- 每个
Worker
都是一个进程,且依赖操作系统的进程调度 - 使用共享内存来共享一些全局资源
- 一个进程挂了不影响整个系统
IBM DB2
、Postgres
、Oracle
- 每个
Process Pool
- 维护一个进程池,当有查询时,随机挑选一个空闲的进程来进行计算
- 仍然依赖操作系统调度以及共享内存
CPU Cache
不友好IBM DB2
、Postgres(2015)
Thread per DBMS Worker
- 维护一个线程池,
DBMS
需要管理调度 - 线程挂了会导致整个系统崩溃
IBM DB2
、MSSQL
、MySQL
、Oracle(2014)
- 维护一个线程池,
12.2 Data Placement
调度器必须感知硬件的内存布局
Uniform Memory Access, UMA
Non-Uniform Memory Access, NUMA
DBMS
可以将内存分区,并将每个分区分配给一个CPU
。通过控制和跟踪分区的位置,将算子调度到最近的CPU
上执行
Memory Allocation Location
Interleaving
:跨CPU均匀分布内存First-Touch
:在CPU第一次访问该内存时(Page Fault
)- 操作系统可以根据其观测到的访问模式,将内存移动到另一个
NUMA Region
12.3 Scheduling
Static Scheduling
- 在生成查询计划时就决定使用多少个线程,且在执行过程中,不会发生改变
Morsel-Driven Scheduling
:Task
中数据对应的内存,分为多个区域(称为Morsel
),分布在不同核上- 每个
Worker
一个核(如何绑核) Pull-Based Task Assignment
Round-Robin Data Placement
- 每个
Hyper-Architecture
:Worker
使用队列来协助调度Worker
会尽量选择那些本地的Task
来执行(Cache Locality
,尽量保证Task
在同一个核上执行)- 当没有本地的
Task
时,尝试从全局的队列中获取Task
来执行(stealing
)
SAP HANA - NUMA-Aware Scheduler
- 线程组(
Group
)- 每个
Group
包含多个线程池(Pools
)Working
:正在执行Task
Inactive
:阻塞中Free
:短暂地休眠后会查看是否可以获取新的Task
Parked
:休眠,等待操作系统唤醒
- 每个
- 每个
CPU
可以拥有多个Group
- 每个
Group
需要有优先级队列(Hard
、Soft
)- 线程可以从其他
Group
中的Sort
队列中窃取任务
- 线程可以从其他
- 一个额外的
Watchdog Thread
,用于监测Group
的饱和度,并且可以动态分配任务
当请求到达的速度比DBMS
执行的速度更快时,系统就超载了(Overload
)
CPU Bound
:执行非常慢Memory Bound
:OOM
如何解决超载问题
Admission Control
:终止新的请求Throttling
:放慢响应速度
13 execution
- Query Plan Processing
- Scan Sharing
- Materialized Views
- Query Compilation
- Vectorized Operators
- Parallel Algorithms
- Application Logic Executions
13.1 System Architecture
CPU
以流水线的方式执行指令,目的是充分利用CPU的每个时钟周期。部分CPU
甚至可以包含多条流水线,只要指令之间无相互依赖,就可以并行执行。部分情况会导致流水线停顿,比如:
Dependencies
:指令之间相互依赖,指令A
依赖指令B
的结果Branch Prediction
:分支预测,当预测错误时,就需要回退,并重新执行另一个分支
对于分支预测来说,有两个典型的场景
-
Selection Scan
1
2
3SELECT * FROM table
WHERE key >= $(low)
AND key <= $(high) -
Multi-Data Types
:DBMS
需要支持多种类型,因此在执行操作前必须对每个类型进行校验,这就会引入非常巨大的switch
语句,进一步降低CPU
对分支预测的准确性
13.2 Processing Models
13.2.1 Iterator Model
特征:每个算子都实现了next
方法
- 每次迭代,算子都返回一个
Tuple
或者空值 - 算子会先通过循环调用孩子算子的
next
获取输入
13.2.2 Materialization Model
特征:all-at-once
,即算子一次性处理所有的输入,并一次性返回所有的结果
- 函数调用次数少
- 输出可以是完整的
Tuple
(NSM
),或者仅包含部分列(DSM
) - 对
OLTP
友好,对OLAP
不友好
13.2.3 Vectorized / Batch Model
类似于Iterator Model
,每个算子都实现了next
方法,但区别是每次返回的是一批Tuple
而不是一个Tuple
- 算子需要在内部实现一个循环遍历每批
Tuple
- 批的大小取决于硬件(
Cache
) - 对
OLAP
友好,可以大幅降低函数调用次数,且可以使用SIMD
指令加速处理
13.3 Parallel Execution
执行方向有两种:
Top-to-Bottom
:从根节点开始,依次从孩子节点pull
数据Bottom-to-Top
:从叶节点开始,依次向父节点push
数据- Allows for tighter control of caches/registers in pipelines.
通常,我们可以通过并发执行来提高整体的执行效率。且并行执行的实现难易程度与Process Model
相关性不大,即比较独立
Intra-Query-Parallelism
Intra-Operator
- 算子实例化多份,每个实例分别处理不同的数据集
- 有时需要插入
Exchange
算子来合并多路输入
Inter-Operator
并行度的设置与CPU核数、数据大小、算子实现都相关
One Worker per Core
- 工作线程与
Core
绑定 - 线程不可阻塞,因为无其他线程可用
- 工作线程与
Multiple Worders per Core
- 每个核关联一个线程池
- 线程可以阻塞
14 compilation
14.1 Background
如果仅考虑内存数据库,那么提升数据库的性能的唯一方式就是减少执行的指令数量
一种可以显著减少指令数量的方法叫做:Code Specialization
,比如为某个查询生成特定的高效代码。绝大部分的代码都必须首先保证易读,其次再考虑性能
14.2 Gode Generation
Code Generation
的两种方式
Transpilation
:将查询计划翻译成某种语言的源码,然后使用该语言的编译器编译成本地代码JIT Compilation
:生成代码的中间表示Intermediate Representation, IR
,例如Java的.class
文件、LLVM IR
,然后在运行时生成本地代码
14.3 Real-Word Implementations
15 vectorization1
15.1 Background
向量化指的是一次处理一组数据,需要借助Single Instruction Multiple Data, SIMD
指令
x86
:MMX
/SSE
/SSE2
/SSE3
/SSE4
/AVX
/AVX2
/AVX512
PowerPC
:Altivec
ARM
:NEON
/SVE
- 数据可以绕过
CPU Cache
直接从SIMD Registers
写入内存
SIMD
的优劣势:
- 优势:
- 成倍提升程序性能
- 劣势:
- 编写
SIMD
程序比较繁琐(现在编译器可以帮你完成这个工作) - 要求数据严格对齐
- 将数据放入
SIMD Registers
或者将数据从SIMD Registers
放回对应的内存地址中这一过程比较tricky
或者低效
- 编写
15.2 Vectorization
向量化的方式有如下三种:
Automatic Vectorization
- 编译器可以自动判断位于一个循环内的指令是否能替换成
SIMD
指令 - 无法对存在
Pointer Aliasing
的代码进行向量化
- 编译器可以自动判断位于一个循环内的指令是否能替换成
Compiler Hints
- 提供额外的信息让编译器进行向量化
- 比如,用
__restrict
关键词或者#pragma ivdep
可以消除Pointer Aliasing
Explicit Vectorization
- 手动编写向量化代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void add(int32_t* a, int32_t* b, int32_t* res, int32_t size) {
__m128i* vec_a = (__m128i*)a;
__m128i* vec_b = (__m128i*)b;
__m128i* vec_res = (__m128i*)res;
for (int i = 0; i < size / 4; i++) {
_mm_store_si128(vec_res++, _mm_add_epi32(*vec_a++, *vec_b++));
}
}
constexpr int32_t SIZE = 100;
int main() {
int32_t a[SIZE];
int32_t b[SIZE];
int32_t res[SIZE];
for (int i = 0; i < SIZE; i++) {
a[i] = i;
b[i] = SIZE - i;
}
add(a, b, res, SIZE);
for (auto i = 0; i < SIZE; i++) {
std::cout << i << ": " << res[i] << std::endl;
}
return 0;
}
Vectorization Direction
:
Horizontal
Vertical
15.3 Vectorized Algorithms
向量化算法是指更好地利用向量化操作来实现高级功能的方法和原则
Vertical Vectorization
,要能处理不同的输入数据Maximum Lane utilization
,向量寄存器中尽量填放更多的数据
Fundamental Operations
Selective Load
Selective Store
Selective Gather
Selective Scatter
- 注意,在当前的体系结构中,
Selective Gather
和Selective Scatter
可能无法实现并行,因为L1 Cache
每个周期只允许一个或两个不同的访问。也许在新的CPU中能解决这个问题
Vectorized Operators
:
Selection Scans
Hash Tables
Partitioning
/Histograms
Joins
Sorting
Bloom filters
16 vectorization2
16.1 Vectorization vs. Compilation
16.2 Relaxed Operator Fusion
17 hash joins
17.1 Background
为了提高效率,我们需要对Join
进行并行化,实现并行Join
的方法有如下两种:
Hash Join
Sort-Merge Join
设计时,考虑的因素包括:
Minimize Synchronization
- 避免加锁
Minimize Memory Access Cost
- 尽量保证操作的数据都在缓存中
相关学术论文:
- Sort vs. Hash Revisited Fast Join Implementation on Modern Multi-Core CPUs
- Design and Evaluation of Main Memory Hash Join Algorithms for Multi-core CPUs
- Massively Parallel Sort-Merge Joins in Main Memory Multi-Core Database Systems
- Massively Parallel NUMA-aware Hash Joins
- Main-Memory Hash Joins on Multi-Core CPUs Tuning to the Underlying Hardware
- An Experimental Comparison of Thirteen Relational Equi-Joins in Main Memory
- Efficient Implementation of Sorting on Multi-Core SIMD CPU Architecture
- Parallel Sort-Merge Join
对于OLTP
来说,一般不会实现Hash Join
,因为对于少量数据来说,Nested-Loop Join
的效率与Hash Join
的效率相当。对于OLAP
来说,Hash Join
是最重要的工作负载
17.2 Parallel Hash Join
Hash Join (R ⨝ S)
的主要流程如下:
Partition
:将R
和S
中的元组按照Join Key
进行分区Non-Blocking Partition
Shared Partitions
Private Partitions
Blocking Partition(Radix)
Radix
这里特指Hash Value
某个位置(第i
位)上的整数值Step1
:扫描R
,计算每个Hash Value
对应的Tuple
数量的直方图Step2
:利用直方图,通过Prefix Sum
计算输出的偏移量Step3
:再次扫描R
,利用Hash Value
以及上一步计算出来的偏移量进行分区- 可以重复上述过程(用第
i+1
位),在原有分区的基础上进行进一步分区 - 示意图参考课件中的
21 ~ 34
页
Build
:对每个分区中的R
进行扫描,并创建一个Hash Table
- 确保
Hash Table
的每个分桶足够小,一个Cache Line
能够容纳
- 确保
Probe
:对每个分区中的S
,利用上一步创建的Hash Table
判断是否存在- 在
Build
阶段创建Bloom Filter
,可以利用Bloom Filter
来加速Probe
过程。能起到加速的原因是整个Bloom Filter
都可以放在CPU Cache
中
- 在
Hash Table
的主要设计点
Hash Function
- 如何将一个大的值空间映射到一个更小的值空间
- 需要在效率和冲突之间权衡
Hashing Scheme
- 如何解决冲突
- 如何高效实现插入、查找
17.3 Hash Functions
MHasher是一个测试套件,旨在测试非加密哈希函数的分布、冲突和性能属性
常用的Hash Functions
如下:
CRC-64 (1975)
- 常用于网络异常校验
MurmurHash (2008)
- 高效、通用的哈希函数
Google CityHash (2011)
- 针对短键(<64 bytes)的哈希函数
Facebook XXHash (2012)
- 由
zstd
压缩的作者提出并实现
- 由
Google FarmHash (2014)
CityHash
的升级版,冲突概率更小
17.4 Hashing Schemes
Chained Hashing
- 为哈希表中的每个槽维护一个桶的链表
Linear Probe Hashing
- 开放寻址法
- 每个槽位存储一个元素
- 当槽位已被占用时,按规则寻找下一个可用槽位
- 示意图参考课件中的
52 ~ 59
页
Robin Hood Hashing
Linear Probe Hashing
的变体- 会将元素从
Rich Key
的槽位迁移到Poor Key
对应的槽位 - 需要额外存储原始槽位与当前槽位的
Offset
- 示意图参考课件中的
62 ~ 72
页
Hopscotch Hashing
Linear Probe Hashing
的变体,也可看做是对Robin Hood Hashing
的进一步限制,限制了迁移的距离- 元素会在其
Netighborhood
之前进行迁移,Netighborhood Size
通过配置指定 - 存在约束:如果
Key
不存在于Netighborhood
之中,那么Key
不存在 - 示意图参考课件中的
74 ~ 94
页
Cuckoo Hashing
- 使用多个哈希表,每个哈希表用不同的哈希函数
- 插入时,检查每个哈希表,将元素插入到有空闲槽位的哈希表。若无法找到空闲槽位,则将对应槽位中的元素驱逐,并重新插入
- 需要确保上述流程不会进入死循环
- 哈希表的数量是2,那么哈希表利用率的上限是50%
- 哈希表的数量是3,那么哈希表利用率的上限是90%
- 示意图参考课件中的
96 ~ 106
页
18 sort merge joins
18.1 Parallel Sort-Merge Join
由于排序的开销比较大,因此并行化能够显著的提升性能
- 尽可能地利用更多CPU
NUMA
SIMD
Sort-Merge Join (R ⨝ S)
的主要流程如下:
Partitioning
- 将
R
和S
中的元组按照Join Key
进行分区 - 分区方式有如下两种
Implicit Partitioning
- 数据存储时就已分区,因此无需额外的分区操作便可获得分区的数据
Explicit Partitioning
- 需要对左表进行显式的分区操作
- 可以使用之前介绍过的
Radix Partitioning
- 将
Sort
- 在每个分区中,对
R
和S
进行排序 - 一般使用
QuickSort
- 可以探索其他能够更好地利用
NUMA
以及并发的排序算法 Cache-Conscious Sorting
In-Register Sorting
In-Cache Sorting
Out-of-Cache Sorting
- 在每个分区中,对
Merge
Multi-Way Sort-Merge (M-WAY)
- 左表:
- 在每个核上对本地数据进行排序(
Level 1/Level 2
) - 使用多路合并
Multi-Way Merge
- 在每个核上对本地数据进行排序(
- 右表:
- 与左表相同的方式
- 左表:
Multi-Pass Sort-Merge (M-PASS)
- 左表:
- 在每个核上对本地数据进行排序(
Level 1/Level 2
) - 不对排序后的数据进行重新分布
- 在每个核上对本地数据进行排序(
- 右表:
- 与左表相同的方式
- 左表:
Massively Parallel Sort-Merge (MPSM)
- 左表:
- 按照范围进行分区,每个分区独立排序
- 右表:
- 不进行分区,每个核扫描本地的数据
- 左表:
18.2 Evaluation
19 optimizer1
优化器用于在Plan
搜索空间中找出执行开销最小的Plan
,该问题已被证明是NP-Complete
问题
目前来说,已知的优化器选出的不一定是最优解
- 利用基数估计来预估实际的执行开销
- 利用启发式的方法来裁剪搜索空间的大小
19.1 Background
Logical vs. Physical Plans
Relational Algebra Equivalences
(A ⨝ (B ⨝ C)) = (B ⨝ (A ⨝ C))
Cost Estimation
- 中间结果的大小
- 算法、访问方式
- 资源利用率
- 数据属性,包括倾斜、顺序、位置等
19.2 Implementation Design Decisions
设计点:
Optimization Granularity
Single Query
- 搜索空间小
- 需要考虑资源竞争
Multiple Queries
- 搜索空间大
- 数据、中间结果的共享能够带来显著增益
Optimization Timing
Static Optimization
- 在执行前选出最优
Plan
Plan
的质量取决于Cardinality Estimation
以及Cost Model
的准确性- How-Good-Are-Query-Optimizers指出
Cardinality Estimation
的准确性更重要
- How-Good-Are-Query-Optimizers指出
- 借助于
Prepared Statements
,可以将优化开销均摊到多次不同的实现中
- 在执行前选出最优
Dynamic Optimization
- 执行时实时调整
Plan
- 在多次执行时,有机会可以调整
Plan
- 难以实现
- 执行时实时调整
Adaptive Optimization
Prepared Statements
Reuse Last Plan
:直接复用前一次生成的Plan
Re-Optimize
:每次都重新进行优化Multiple Plans
:用不同的参数,生成多个Plan
Average Plan
:用参数的均值来生成Plan
Plan Stability
Hints
Fixed Optimizer Versions
Backwards-Compatible Plans
Search Termination
Wall-clock Time
:优化超时Cost Threshold
:通过Cost Boundary
来裁剪搜索空间Exhaustion
:穷尽枚举
19.3 Optimizer Search Strategies
搜索策略如下:
Heuristics Only
- 优化手段包括
- 尽早执行最严格的选择
- 在
Join
之前执行选择 - 谓词下推
Limit
下推Projection
下推Join Ordering
- 优点:
- 易于实现和调试
- 对于简单查询能够有效地工作
- 缺点:
- Relies on magic constants that predict the efficacy of a planning decision
- 在算子间存在相关性的情况下,很难产生较好的
Plan
- 优化手段包括
Heuristics + Cost-based Join Order Search
Top-Down vs. Bottom-Up
Top-Down
Volcano
、Cascades
Bottom-up
System R
、Starburst
- 优点:
- 通常可以选出一个较好的
Plan
,而无需穷举搜索空间
- 通常可以选出一个较好的
- 缺点:
- 包含
Heuristics Only
的所有缺点 - 只考虑左深树,但最优解并不一定是左深树
- 需要额外考虑数据的物理属性(例如倾斜、排序等)
- 包含
Randomized Algorithms
- 在搜索空间中随机搜索
Plan
,直至找到Cost
低于指定Threshold
的Plan
或者超时 - 示例:
Postgre's genetic algorithm
- 优点:
- 在搜索空间中随机跳跃可以让优化器摆脱局部最小值
- 内存开销小
- 缺点:
- 很难解释如何选出某个
Plan
- 不稳定性,必须做额外的工作以确保
Plan
是稳定的
- 很难解释如何选出某个
- 在搜索空间中随机搜索
Stratified Search
- 第一步:应用
Transformation Rules
,对原Logical Plan
进行重写 - 第二步:基于
Cost Model
,将Logical Plan
转换成Physical Plan
- 第一步:应用
Unified Search
- 将
Logical Plan
到Logical Plan
以及Logical Plan
到Physical Plan
都视为Transformation
,因此,整个优化过程就一个阶段
- 将
Optimizer Generators
- 用面向过程的语言来写
Transformation Rule
会比较难,且容易出错,需要写大量的模糊测试来保证正确性 - 另一种途径:通过
DSL
来描述规则;通过Optimizer Generator
来生成对应的代码 - 例子:
Starburst
、Exodus
、Volcano
、Cascades
、OPT++
涉及到的优化器包括:
Ingres Optimizer
System R Optimizer
Postgres Genetic Optimizer
Starburst Optimizer
Volcano Optimizer
20 optimizer2
20.1 Logical Query Optimization
该阶段的目的主要包括:
- 扩充搜索空间,枚举可能的
Plan
- 简化
Plan
- 表达式的重写和简化
- 列裁剪
- 谓词拆分
- 谓词下推
- Limit下推
- Projection下推
- 笛卡尔积替换为Join(有等值条件的情况下)
- 等价谓词推导(常量传播)
- 常量折叠
- 公共表达式复用
- 子查询重写
- 消除不必要的
Cast
20.2 Physical Query Optimization
该阶段的目的主要包括:
- 将
Logical Plan
中的Logical Operator
转换成Physical Operator
- 需要知道更多的执行相关的信息
- 索引、访问方式
- 实现方式
- 何时物化
- 需要使用
Cost Model
来对每个Physical Plan
进行代价评估
20.3 Cascades / Columbia
概念:
Expression
:由一个算子以及零或多个输入构成,每个输入也是Expression
Operator
区分逻辑与物理,因此Expression
也有逻辑和物理之分
Group
:由一组等价的Logical Expression
以及Physical Expression
构成- 等价意味着,这些
Expression
都能产生相同的输出
- 等价意味着,这些
Rule
:将一个Expression
转换成另一个等价的Expression
Transformation Rule
:Logical to Logical
Implementation Rule
:Logical to Physical
Rule
的构成Pattern
:匹配当前Rule
的模式Substitute
:应用当前Rule
会产生的输出
Memo Table
:存储所有已探索的Plan
,即搜索空间- 重复检测
Property
、Cost
管理
示意图参考课件中的35 ~ 52
页
Cascades
的实现包括:
Standalone
Wisconsin OPT++ (1990s)
Portland State Columbia (1990s)
Pivotal Orca (2010s)
Apache Calcite (2010s)
Integrated
Microsoft SQL Server (1990s)
Tandem NonStop SQL (1990s)
Clustrix (2000s)
CMU Peloton (2010s – RIP)
20.4 Dynamic Programming
20.5 Other Implementations
Pivotal Orca
Apache Calcite
Query Language
、Cost Model
、Rule
支持插件化- 不区分
Logical Operator
以及Physical Operator
MemSQL Optimizer
Rewriter
- 利用
Cost Model
进行Logical to Logical
的转换
- 利用
Enumerator
- 进行
Logical to Physical
的转换
- 进行
Planner
- 将
Physical Plan
转换成SQL
- 将
20.6 一些思考
优化器是否能选出最优的Plan
取决于Cost Model
,而Cost Model
依赖于合理且准确的统计信息
21 optimizer3
优化器依赖Cardinality Estimation
以及Cost Model
,若偏差较大的话,容易生成Bad Plan
优化器基于一些静态的统计信息以及执行上下文来进行代价估计
- 静态模型/直方图/采样
- 硬件性能(CPU/IO)
- 当前算子的复杂度
21.1 Adaptive Query Optimization
Modify Future Invocations
- 监控查询执行,并采集相关信息,用于优化后续的查询
- 方法包括
Plan Correction
Feedback Loop
Identifying Equivalent Subplans
Logical Expression
相同且Physical Property
相同的Sub Plan
就被认为是等价的
Replan Current Invocation
- 如果在执行时,发现实际开销远大于估计值,那么终止当前执行,并重新生成
Plan
- 方法包括
Start-Over from Scratch
Keep Intermediate Results
- 如果在执行时,发现实际开销远大于估计值,那么终止当前执行,并重新生成
Plan Pivot Points
- 在物化节点,提供多个
Sub Plan
,当执行到该节点时,依据当前执行所获取的统计信息,选择使用哪个Sub Plan
继续执行 - 方法包括
Parametric Optimization
Proactive Reoptimization
- 在物化节点,提供多个
22 cost models
22.1 Cost Models
Cost Model Components
:
Physical Costs
Instruction cycless
Cache misses
RAM
I/O
- 与硬件强相关
Logical Costs
- 算子输出数据的大小
- 与具体算法无关
Algorithmic Costs
- 算法的开销
Disk-Based DBMS Cost Model
:
CPU
成本对于磁盘I/O
而言,可以忽略不计- 若整个
DB
都可以cache在内存中之后,那么此时就与CPU
成本相关了
- 若整个
- 需要考虑顺序或者随机
I/O
Postgres Cost Model
:
- 分别给
CPU
和I/O
一个乘因子,用于调整它们的占比 - 默认配置值假设不存在大量内存(无法全部cache)
- 读内存的性能大约是读磁盘的400倍
- 顺序读的性能大约是随机读的4倍
In-Memory DBMS Cost Model
:
- 由于没有磁盘
I/O
的开销,因此需要考虑CPU
和内存的开销 - 内存开销的估计比较困难,因为
MESI
协议对于DBMS
而言完全是透明的 CPU
开销可以用算子处理的数据量来代替
22.2 Cost Estimation
Selectivity
:估计谓词的选择度
- 估计时会用到的信息:
Domaon Constraints
:领域限制,比如温度,不可能存在低于-274℃
的温度值Precomputed Statistics(Zone Map)
Histograms / Approximations
Sampling
- 此外,还与如下因素相关
- 访问数据的方式
- 数据分布
- 谓词
Approximations
:给估值加上一个错误边界,不至于错的太离谱
Count Distinct
Quantiles
Frequent Items
Tuple Sketch
Sampling
:采样
Result Cardinality
:对于ScanOperator
来说,结果基数取决于选择度以及输入的数据量。且存在如下假设:
Uniform Data
:数据均匀分布Indenpendent Predicates
:谓词之间相互独立Inclusion Principle
:包容性原则
Column Group Statistics
:
- 列的相关性会导致谓词之间也有相关信息
- 可以将一组相关性较强的列放入一个分组中,然后对分组进行统计分析
- 需要
DBA
手动指定分组
- 需要
一些观点:
- 一个好的优化器要比一个快的执行器更重要
- 基数估计存在误差
Hash Join
以及Seq Scan
通常是一个较好的选择- 研究准确的模型是浪费时间
- 结合
Sampling
与Sketch
,也许可以得到一个较为准确的估计
23 larger than memory
23.1 Implementation Issues
OLTP Issues
:
Runtime Operations
:Cold Data Identification
On-line
Off-line
Eviction Policies
:Timing
Threshold
On Demand
Evicted Metadata
Tuple Tombstones
Bloom Filters
DBMS Managed Pages
OS Virtual Memory
Data Retrieval Policies
:Granularity
All Tuples in Block
Only Tuples Needed
Retrieval Mechanism
Abort-and-Restart
Synchronous Retrieval
Merging
Always Merge
Merge Only on Update
Selective Merge
24 udfs
我们可以将一些业务逻辑相关的代码嵌入在DBMS
中,形式可以是:
User-Defined Functions, UDFs
Stored Procedures
Triggers
User-Defined Types, UDTs
User-Defined Aggregates, UDAs
Microsoft SQL Server UDF History
:
- 2001 – Microsoft adds TSQL Scalar UDFs.
- 2008 – People realize that UDFs are “evil”.
- 2010 – Microsoft acknowledges that UDFs are evil.
- 2014 – UDF decorrelation research @ IIT-B.
- 2015 – Froid project begins @ MSFT Gray Lab.
- 2018 – Froid added to SQL Server 2019.
Froid Overview
:
- Transform Statements
- Break UDF into Regions
- Merge Expressions
- Inline UDF Expression into Query
- Run Through Query Optimizer
25 hardware
25.1 Persistent Memory
Fundamental Elements of Circuits
:
Capacitor
:电容器Resistor
:电阻器Inductor
:电感器Memristor
:忆阻器
Technologies
:
Phase-Change Memory, PRAM
- 存储单元由两个由电阻加热器和相变材料(硫属化物)隔开的金属电极组成
Resistive RAM, ReRAM
- 两个金属层,中间有两个
TiO2
层。 向一个方向运行电流会将电子从顶部TiO2
层移动到底部,从而改变电阻
- 两个金属层,中间有两个
Magnetoresistive RAM, MRAM
- 使用磁存储元件而不是电荷或电流来存储数据
25.2 GPU Acceleration
25.3 Hardware Transactional Memory
26 课件
- 01-history
- 02-inmemory
- 03-mvcc1
- 04-mvcc2
- 05-mvcc3
- 06-oltpindexes1
- 07-oltpindexes2
- 08-storage
- 09-compression
- 10-recovery
- 11-networking
- 12-scheduling
- 13-execution
- 14-compilation
- 15-vectorization1
- 16-vectorization2
- 17-hash-joins
- 18-sort-merge-joins
- 19-optimizer1
- 20-optimizer2
- 21-optimizer3
- 22-cost-models
- 23-larger-than-memory
- 24-udfs
- 25-hardware
27 Summary
28 Else
- Apache Arrow format
- Delta: always refers to what changed
- RCU, Read-Copy-Update
- Latch vs. Lock
- 本质上都是锁,锁的对象不同。Lock锁的对象是事务;而
Latch
锁的对象是一些内存资源(数据结构)
- 本质上都是锁,锁的对象不同。Lock锁的对象是事务;而