论文《Architecture of a Database System》小结
我正在参加「掘金·启航计划」
原文: https://dsf.berkeley.edu/papers/fntdb07-architecture.pdf
本文主要讨论DBMS的体系结构,包括进程模型、并行架构、存储系统设计、事物系统实现、查询处理器和优化器架构,以及常见的共享组件和工具。
1 介绍
数据库系统是最早广泛部署的在线服务器系统之一,因此,它开创了不仅跨越数据管理,而且跨越应用程序、操作系统和网络服务的设计解决方案。早期的dbms是计算机科学中最具影响力的软件系统之一,为dbms开创的思想和实现问题被广泛复制和重新发明。
1.0 DBMS的架构——DBMS的主要组件
DBMS的主要组件
DBMS的5大组件:
- 客户端通信管理器(Client Communication Manager)
- 进程管理器(Process Manger)
- 查询处理器(Relation Query Processor)
- 事物性存储管理器(Transanctional Stroage Manager)
- 共享组件和工具(Shared Components and Utilities)
1.1 关系型系统:一条查询的执行过程
-
建立连接。调用者调用客户端API,客户端通过网络与服务端(的Client Communications Manager)通信.
-
Client Communication Manager
的主要功能:- 为调用者建立连接并维护调用者的状态;
- 响应调用者的SQL命令,然后返回正确的数据和控制信息;
-
-
分配计算线程。在接收到客户端的第一个SQL命令之后,DBMS必须分配一个“计算线程”来执行SQL命令。还需要保证线程的数据和控制输出能够通过 Communication Manger 发送给客户端。
-
Process Manager
的主要功能:- 为收到的SQL指令分配计算线程。DBMS在此阶段要做的最重要的决定是关于准入控制( admission control)的——即系统是否应该立即处理查询,或延迟执行直到有足够的系统资源可以用于此查询。
-
-
执行查询。一旦确定并分配了一个计算线程,就可以执行查询了。
-
Relation Query Processor
:-
检查用户是否有权限执行查询,
-
然后将用户的SQL编译为内部的 查询计划(internal query plan) 。
-
一旦编译完成,**计划执行器(plan executor)**会执行该计划。
计划执行器由一组
operators
组成,它可以执行任何查询。通常operators
实现的关系型查询处理任务包括joins,selection,projection,aggregation,sorting等,以及从系统的较低层请求数据记录的调用。
-
-
-
从数据库中获取数据。需要通过 Transaction Storage Manager 才能获取数据
-
Transanctional Storage Manager
:-
它管理所有的**数据访问(read) 和操控(create,update,delete)**调用。operators从Transactional Storage Manager获取数据。
存储系统包括组织和访问磁盘上数据(access methods)的算法和数据结构,
- 包括像表和索引这样的基础数据结构
- 它还包括缓存管理模块( buffer manager)。
在访问数据之前,需要先从 lock manager 获取锁,以保证并发查询时的正确执行。
如果查询需要更新数据库,还需要 log manger 交互,以保证事物在提交后是持久的,在取消后是可撤销的。
-
-
-
返回数据,事务结束,关闭连接。访问数据记录,计算最终结果并返回给客户端。
- access methods 将控制返回给查询执行器的 operators,它编排数据库数据的计算;当结果生成后,会被放到 client communications manager 缓存中,它会将结果返回给调用者。
上面的例子涉及了RDBMS的多个核心组件,但不是所有。catalog 和 memory managers 会在事物中被调用。catalog在认证、解析和查询优化时会被查询处理器使用。memory manager 只要在DBMS需要动态分配或取消分配内存时就会使用。
1.2 讨论范围和概览
本文主要关注的是支持数据库核心功能的基础架构:
- 进程架构
- DBMS特定领域的组件
- 存储架构和事物存储架构设计
- 常见DBMS中的共享组件和工具
2 进程模型
当设计任何一个多用户服务时,早期要决定 如何执行并发用户请求以及 如何将这些请求映射到操作系统进程或线程。
我们先从一个简单的框架开始,假设操作系统对于线程的支持很好,且只关注单处理器系统。接下来的讨论基于下面的定义:
- 一个操作系统进程由多个操作系统线程和进程私有地址空间组成。为进程维护的状态包括操作系统资源和安全的上下文。线程由操作系统内核调度,每个进程有其唯一的地址空间。
- 操作系统线程是操作系统程序的执行单元,没有私有操作系统上下文和私有地址空间。操作系统线程由内核调度。
- Lightweight Thread (轻量级线程,LWT)支持在单个进程中存在多个线程。不像操作系统线程由内核调度,这些线程由应用程序调度。LWT在用户空间中调用,操作系统线程在内核空间中调度。
- DBMS Client是一个软件,实现了用于应用程序与DBMS通信的API。
- DBMS worker 是DBMS中执行的线程,它代表DBMS客户端工作。DBMS worker和DBMS 客户端之间是一一对应的:DBMS worker处理所有来自单个DBMS 客户端的SQL请求。
2.1 单处理器(Uniprocessors)和轻量级线程(LWT)
我们以两个简单的假设开始(后面会放宽假设):
- 操作系统线程支持:假设操作系统对内核线程提供了高效的支持,且一个进程可以有大量线程。我们还假设每个线程的内存开销很小,且上下文切换和便宜。
- 单处理器硬件: 假设我们是为只有单CPU的单个机器设计的。
在此简化的环境下,DBMS有三个自然而然的进程模型可选。总最简单到最复杂,依次是:
- 每个(DBMS)worker一个进程
- 每个(DBMS) worker一个线程
- 进程池
尽管这些模型都被简化了,它们三个都在商业DBMS中有应用。
2.1.1 每个worker一个进程(Process per DBMS Worker)
Fig. 2.1 Process per DBMS worker model: each DBMS worker is implemented as an OS process.
由操作系统对DBMS workers进行管理,DBMS程序员可以依靠操作系统的保护设施来隔离标准错误,如内存超限。
优点:
- 易于实现。workers 直接映射到操作系统进程。
- 各种编程工具可以使用。如调试器、内存检查器。
缺点:
- 多个DBMS连接之间需要 共享内存数据结构。包括lock table 和 buffer pool。共享的数据结构必须由操作系统显示的分配,需要操纵系统支持和一些特殊DBMS编码。在实践中,共享内存降低了地址空间分离的优势。
- 对于大量并发连接的 扩展性不好。进程需要维护的信息很多,因此大量进程需要消耗更多的内存。
2.1.2 每个worker一个线程
Fig. 2.2 Thread per DBMS worker model: each DBMS worker is implemented as an OS thread
单个多线程进程管理所有的DBMS worker 活动。调度器线程监听新的DBMS client连接。每个连接分配一个新的线程。
优点:
- 对于并发的扩展性好。
缺点:
- 多线程编程的缺点它都有。难于调试,竞争条件;各个操作系统的线程接口不同,跨平台有问题。
2.1.3 进程池
此模型是“每个worke一个进程”的一个变体。有了进程池,不是每个worker分配一个进程,进程由进程池管理。每个client分配一个进程。当SQL执行完成后,客户端收到结果,进程会回收到进程池,等待分配给下一次请求。如果一个请求来了,但是进程池空了,那么该请求需要等待有进程可用。
进程池的大小通常是动态的,通常其大小与并发请求数有关。
优点:
- “每个worker一个进程” 有的它都有
- 需要的内存更小,更高效
2.1.4 共享数据和进程边界
以上介绍的模型的目的都是 尽可能独立地执行并发请求。然而, DBMS worker完全的独立和隔离是不可能的,因为它们需要操作同一个数据库。在这三个模型中,数据需要从DBMS移动到客户端。这暗指,所有的SQL请求需要移动到服务端进行,且所有的返回的结果需要从服务端移回客户端。如何移动?简单来说就是使用各种 缓冲。两种重要的缓冲类型是:
-
* 磁盘I/O缓冲(disk I/O buffers) :*最常见的跨worker数据依赖是对共享数据存储的读取和写入。于是,worker之间的I/O交互很常见。有两种独立的磁盘I/O场景需要考虑:
-
数据库I/O请求(Database I/O Requests): 缓冲池(The Buffer Pool)。 所有持久化的数据库数据都需要通过 DBMS buffer pool暂存。
- 在 “每个worker一个线程 ” 模型中,缓冲池分配在堆上,DBMS地址空间中的所有的线程都可以访问
- 在另外两种模型中,缓冲池分配在所有进程可以访问的共享内存中。
所有模型中的最终结果是,缓冲池是一个大的共享数据结构,所有数据库线程或进程都可以访问。
缓冲“读”
-
日志请求(Log I/O Requests):
The Log Tail
。数据库日志是存在一个或多个磁盘上的一组条目。所有日志条目都是在事物处理过程中生成的,它们暂存在内存队列中,被周期性的按FIFO顺序写入到日志磁盘中。这个队列通常叫做log tail
。在很多系统中,有一个分离的进程或线程负责周期性地将log tail
写入到磁盘中。-
在 “每个worker一个线程” 模型中,
log tail
分配在堆上 -
在另外两个模型中,有两种常见的设计方法:
-
使用一个独立的进程管理日志。日志记录通过共享内存或其他高效的进程间通信协议与日志管理器通信。
-
与上面处理缓冲池的方式类似,
log tail
在共享内存中分配。关键的一点是,所有执行数据库客户端请求的线程和/或进程都需要能够请求写入日志记录并刷新log tail
。一种重要的 log flush 类型是
commnit transaction flush
。事务只有在提交日志记录被写入到日志设备之后才能被报告为成功提交。
-
缓冲“写”
-
-
-
* 客户端通信缓冲(client communication buffers) *:SQL通常以“pull”模式使用:客户通过反复发出SQL FETCH请求,从查询游标中消费结果记录,每次请求检索一个或多个记录。大多数DBMS试图在FETCH请求流之前工作,以便在客户端请求之前排定结果。
为了支持这种预取行为,DBMS worker可以使用客户端通信socket作为结果集队列。更复杂的方法是实现客户端游标缓存,并使用DBMS客户端来存储可能在不久的将来被获取的结果,而不是依赖操作系统的通信缓冲区。
其他共享的数据:
- 锁定表(Lock table) 。锁定表由所有DBMS worker 共享,并由**锁定管理器(Lock Manager)**用来实现数据库的锁定语义。共享锁表的技术与缓冲池的技术相同,这些技术也可以用来支持DBMS实现所需的任何其他共享数据结构。
2.2 DBMS 线程
由于遗留、可移植性和可扩展性的原因,大多数DBMS并不基于操作系统线程实现。那些使用“thread per DBMS worker” 模型的,需要一个不使用操作系统线程的解决方案。其中一个方案是:自己实现轻量级的线程。
2.3 标准实践
以上介绍的三种架构(及变体)在现实的DBMS中都有使用。如 IBM DB2支持4种进程模型:
- 如果OS对线程的可扩展性支持的很好,DB2默认使用 thread per DBMS worker 模型,也可以选择 thread pool 模型
- 如果OS对线程的可扩展性支持的不好,DB2默认使用 process per DBMS worker 模型,也可以选择 process pool 模型。
现在来总结一下IBM DB2,MySQL,Oracle , PostgreSQL 和 Microsoft SQL Server 支持的进模型:
-
Process per DBMS worker:
这是最直接的模型,依然被很多数据库使用。
- DB2在对线程扩展性支持不好的OS上的默认模型
- Oracle的默认模型
- PostgreSQL支持此模型
-
Thread per DBMS worker:
此模型的两个主要变体是:
-
OS thread per DBMS worker:
- DB2在对线程扩展性支持很好的OS上的默认模型
- MySQL使用的模型
-
DBMS thread per DBMS worker:
在此模型中,DBMS worker 由调度器调度,要么调度到OS processes上,要么调度到OS threads上。
此模型有两个主要的子类:
-
DBMS threads 被调度到 OS process 上:
- Syhbase 支持此模型
-
DBMS threads 被调度到 OS threads 上:
- Microsoft SQL Server支持此模型
-
-
-
Process/thread pool
-
process pool
- Oracle 支持此模型
-
thread pool
- Microsoft SQL Server在大多数机器上的默认模型
-
大多数现代商用DBMS都支持内部查询并行(intra-query parallelism):将单个查询分成多个部分,在多个处理器上并行执行。内部查询并行就是将单个SQL查询分配给多个DBMS worker去执行。底层的进程模型不会因此受到影响。
2.4 准入控制(Admission Control)
随着吞吐量的增加,DBMS由于内存压力会出现抖动:无法将数据库页的“工作集”保留在缓冲池中,并且所有时间都花在了页替换上。
什么情况下会造成抖动?
- sorting 和 hash joins 消耗大量内存
- 锁争用: 事物之间相互死锁,需要回滚和重新启动
如何防止抖动?
准入控制机制——只有在DBMS资源足够时才接收新的工作。有了一个好的准入控制器,系统在过载的情况下会优雅的退化:事物延迟将与到达率(arrival rate)成比例地增加,但吞吐量将保持在峰值。
DBMS 中的准入控制可以在两层中进行:
-
第一层:dispathcer process 保证客户端连接数在阈值之下。这样可以防止过度消耗网络连接资源。
-
第二层:在DBMS
relational query procecssor
中实现。准入控制在查询被解析和优化后执行,并决定查询是被推迟,或以少量资源执行,还是无约束地执行。优化器会估计查询需要的资源和当前系统中的可用资源,这些信息为提供给准入控制器。
3. 并行架构: 进程和内存协调(Memory Coordination)
本章讨论每个并行架构的中进程模型和内存协调问题
3.1 共享内存(Shared Memory)
共享内存架构:所有处理器可以以大致相同的性能访问同一个RAM和磁盘。
第2章中的3中模型都可以在共享内存架构上很好地运行。在共享内存机器上,OS对于跨处理器分配worker(processes 或 thread)是透明的。
此架构的主要的挑战是修改查询层,使其能够利用多个CPU对单个查询并行执行。
3.2 无共享(Shared-Nothing)
无共享并行系统由一组独立的机器组成,它们之间通过网络进行通信。各个机器之间无法直接访问内存或磁盘。
无共享系统没有提供硬件共享抽象,需要DBMS来调度各个机器。DBMS采用的最常见的技术是在每个机器或节点上运行各自的标准进程模型。每个节点都能够接收客户端的SQL请求,访问必要的元数据,编译SQL请求,并进行数据访问,就像在单个机器上一样。主要的不同的是:集群中的每个系统只存储部分数据。查询请求不只查询本地数据,还会被发送到集群中的其他相关节点上,所有相关节点都需要执行查询。这些表使用水平数据分区分布在集群中的多个系统上,每个处理器可以独立于其他处理器执行。
有哪些数据分区方案?
- hash-based
- range-based
- round-robin
- range-based + hash-based
无共享架构的问题:
-
部分失效:一个处理器失效会导致整个机器失效,因此DBMS也会失效。虽然单个节点失效不会影响其他节点,但会对整体DBMS的行为产生影响。
3种解决方法:
-
一个节点失效,就关闭所有节点。(本质上模仿的是共享内存架构)
-
“Data skip” ——跳过故障节点上的数据
- 适用于: 数据的可用性 > 结果的完整性
-
冗余
-
无共享架构的优点:
- 扩展性好
- 便宜
应用场景:
- 决策支持系统
- 数据仓库
3.3 共享磁盘(Shared Disk)
共享磁盘架构:所有的处理器可以以大致相同的性能访问同一个磁盘,但不能访问各自的RAM。
优点:
- 管理成本低。不用考虑对数据进行分区
- 单个节点失效不会影响其他节点访问数据库
缺点:
-
单点故障。
-
跨机器的数据共享需要显示调度
- 基于分布式锁管理器
- 需要缓存一致性协议来管理分布式缓冲池
这些都是很复杂的组件,易产生竞争,成为系统的瓶颈。
3.4 NUMA
NUMA(* Non-Uniform Memory Access (NUMA,非统一内存访问)*
)在内存独立的集群上提供了一个共享的内存编程模型(把多个独立的机器上的内存看成一个内存)。集群中每个节点可以快速访问本地内存,而远程内存访问需要通过集群中的高速互联通道(存在一些延迟)。此架构名称的来源就是 内存访问时间不统一。
NUMA架构介于无共享和共享内存之间。
NUAM集群已经消失了。
3.5 DBMS线程和多处理器
在“thread per DBMS worker”模型中,所有线程在单个进中运行,单个进程一次只能在单个处理器上执行。因此,在多处理器系统中,DBMS只会使用单个处理器,其他处理器会闲置。
当在多个进程中运行DBMS线程时,有时会出现一个进程承担了大部分工作,而其他进程(也就是处理器)处于空闲状态。为了使这种模式在这些情况下能很好地工作,DBMS必须在进程之间实现 线程迁移(migration) 。从6.0版本开始,Informix在这方面做得很好。
当把DBMS线程映射到多个操作系统进程时,需要决定采用多少个操作系统进程,如何把DBMS线程分配给操作系统线程,以及如何在多个操作系统进程中分配。一个好的经验法则是 每个物理处理器分配一个进程。这可以最大限度地提高硬件中固有的物理并行性,同时最大限度地减少每个进程的内存开销。
3.6 标准实践
关于对并行的支持,流行的方式与上一张类似:大多数主要的DBMS支持多种并行模型。由于共享内存系统(SMP、多核系统和两者的组合)在商业上的流行,所有主要的DBMS供应商都对共享内存的并行性提供了良好的支持。我们看到支持的分歧是在多节点集群并行中,广泛的设计选择是共享磁盘和无共享。
- 共享内存:大多数主要的DBMS都支持,包括:IBM DB2,Oracle 和 Microsoft SQL Server
- 无共享:IBM DB2, Informix, Tandem, 和 NCR Teradata
- 共享磁盘:Oracle RAC, RDB , 和 IBM DB2
4. 关系型查询处理器
查询处理器(query processor)的作用:接收一个SQL语句,验证SQL,将SQL优化为查询计划,然后执行该查询计划。客户端获取(pull/fetch)查询的结果,通常是一次获取或一批一批地获取。
以下讨论的是常见的SQL:DML语句(增删改查)。而不是DDL语句。查询优化器不处理DDL; DDL通常是静态的DBMS逻辑,通过显示调用存储引擎和catalog manager。
查询处理器的主要组件:
- 解析器:解析查询语句、验证执行权限
- 重写器:简化和标准化查询
- 优化器:生成查询计划
- 执行器:执行查询计划
4.1 查询解析和授权
对于一个SQL语句,SQL**解析器(parser)**通常需要执行以下处理:
-
检查表名是否正确
-
解析名称和引用
-
将查询转换为优化器使用的内部格式
-
验证用户是否有权限执行查询
- 验证用户是否对表、用户自定义函数、或查询中引用的其他对象有权限。
- 有些系统会将权限检查放到查询计划执行的时候做。如支持行级安全的系统,因为只有在执行时才能基于值做安全检查。
在解析和验证通过之后,生成查询的内部格式。
4.2 查询重写
查询重写模块(重写器,rewriter) 负责简化和标准化查询,而不改变其语义。大多数重写实际上是对查询的内部表示的操作,而不是对原始SQL语句的。查询重写模块的输出通常是查询的内部表示。
在大多商用数据库中,查询重写是一个逻辑组件,一般在查询解析后,或查询优化前执行。不论如何,将查询重写与其他模块分离是很有用的。
重写器的主要职责有:
-
扩展视图(View expansion): 将视图替换为视图引用的表、谓词或列。
-
简化常量数学表达式。如,将
R.x < 10+2+R.y
重写为R.x < 12+R.y
-
逻辑重写谓词(predicates): 基于WHERE语句中的谓词和常量进行逻辑重写。
如,将 NOT
Emp.Salary > 1000000
重写为Emp.Salary <= 1000000
。将
Emp.salary < 75000 AND Emp.salary > 1000000
重写为FALSE
;甚至还可以从一个谓词转换为另一个谓词,
R.x < 10 AND R.x = S.y
转换为AND S.y < 10
。目的:
- 提升优化器的性能,使其选择更好的查询计划
-
语义优化: 当表上的限制与查询谓词不兼容时,语义优化可以避免执行查询。
例子,消除冗余的join。
SELECT Emp.name, Emp.salary FROM Emp, Dept WHERE Emp.deptno = Dept.dno
重写为:
SELECT Emp.name, Emp.salary FROM Emp
-
子查询展开和其他启发式重写:
优化器是DBMS中最复杂的组件。为了保持这种复杂性的有限,大多数优化器只在单个SELECT-FROM-WHERE 查询块上运行,而不是跨块进行优化。因此,许多系统不会使优化器复杂化,而不是重写查询,使其更符合优化器。这种形式转换有时叫做 查询规范化(query normalization) 。
- 一类例子就是将查询重写为语义上相等的规范形式,语义相等的查询会被优化生成相同的查询计划
- 另一个重要的启发式方式是尽可能地展开嵌套查询,以最大限度地为查询优化器的单块优化提供计划。
4.3 查询优化器
**查询优化器(query optimizer) 的主要任务是将内部的查询表示转换为一个高效的查询计划(query plan)。**一个查询计划可以被认为是一个查询运算符(query operator)组成的图,表数据会流过该图。
查询计划有多种表示方式:
-
机器码
-
可解释(interpretable)的数据结构 (为了跨平台)
- 轻量的对象
- 低级的“操作码”语言。与Java的字节码的思想类似
- 类代数(algebra-like)
所有的DBMS都为Selinger的论文中提到的查询计划做了扩展,主要的扩展有:
- 计划空间(Plan space)
- 选择性估计(Selectivity estimation)
- 搜索算法(Search Algorithms)
- 并行(Parallelism)
- 自动调优(Auto-Tuning)
4.4 查询执行器
**查询执行器(query executor)**用于执行查询计划。查询计划通常是一个数据流的有向图,其中节点是operators(包含要访问的表和各种查询算法)。
在某些系统中,这个图被优化器编译为操作码,这时执行器的作用就是一个运行时 解释器。
在其他系统中,执行器的输入是一个图,它会 递归的调用程序来执行operators。
大多数现代查询执行器采用的是迭代器模型。
执行器的运行模型:
-
迭代器模型(iterator model):迭代器的输入就是数据流图中的边。查询计划中的每个operators都是迭代器的子类。
-
特点
-
面向对象模式
-
迭代器的逻辑与其父亲和孩子相互独立
-
数据流和控制流耦合在一起
-
单线程架构。只需要使用单个线程来执行这个查询图。
- 实现简洁、易于调试
- 在单系统(非集群)中查询效率高
-
-
4.5 访问方法
访问方法(Access Methods)是一个管理程序,管理各种基于磁盘的数据结构的访问。这些通常包括无序的文件("堆"),以及各种索引。所有主要的商业系统都实现了堆和B+树索引。
Access Methods提供的基础API:
- init():接收一个“搜索参数”——SARG,SARG为空表示全表扫描,SARG为列可能会使用索引。
- next():用于获取数据,如果next() 返回NULL,则说明没有满足条件的数据了。
Q: 为什么要向 access method 层传递SARGs?
- 像B+树这样的index access methods需要SARGs来高效执行
- 性能问题:它更适合堆扫描和索引扫描
- 所有查询逻辑都在access methods 层中完成,使存储引擎与关系引擎之间的边界清晰,性能更好
Q: 索引如何与数据表(base 表)中的行关联?
-
RID(row ID):直接指向base表中数据的物理磁盘地址。
-
优点
- 块
-
缺点
- base表的行移动很麻烦,因为需要更新所有的二级索引。查询和更新的成本都很高
-
Q: 如何解决行移动的问题?
- DB2: forwarding pointer:需要多一次IO找到移动后的page,但不用更新二级索引。
- 使用B+树作为主存储:使用主键代替物理地址。损失了二级索引访问base表的性能,但避免了行移动导致的问题。
4.6 数据仓库
数据仓库——用于决策支持的大型历史数据库,定期加载新的数据——需要专门的查询处理支持。
此主题之所以重要的两个原因:
- 数据仓库是DBMS技术的一个重要应用。
- 传统查询优化和执行引擎在数据仓库上效果不佳。因此,需要扩展或修改以提高性能。
Q: 为什么需要数据仓库?
- “商业分析”的需求出现。系型数据库主要是用于解决商业数据处理的需求,而数据仓库主要用于解决“商业分析”需求
Q: 数据仓库与OLTP的区别?
- 数据的本质不同。数据仓库处理的是的历史数据,而OLTP处理的是“现在”的数据
- 数据的schema不同。需要进行数据转换
Q: 数据仓库中的数据从哪里来?
- 从OLTP系统中获取数据,并将这些数据放到数据仓库中。可以使用ELT(extract,transform and load)系统。流行的ELT产品有:Data Stage 和 PowerCenter。
4.6.1 Bitmap Indexes
B+tree对快速插入、删除和更新记录做了优化。相反,数据仓库中存储的都是静态数据,只用加载一次数据即可。此外,数据仓库通常具有包含少量值的列。
Bitmap相对B+树的优点:
- 节省空间。方便存储列值数量较少的数据,如用户的性别,只有两个值。
- 数据过滤。多个bitmap取交集,可以很快地对数据进行过滤
缺点:
- 更新成本高。因此只在数据仓库中使用
在现在的产品中,bitmap一般作为B+树的一个补充存在。
4.6.2 Fast Load
通常数据仓库会在夜里加载白天的交易数据,原因有:
- 白天交易数据,晚上加载是一个很自然的策略
- 避免在用户交互的时候更新数据
Q: 为什么数据仓库的数据不能被并发加载(查询和加载同时存在)?
- 因为数据分析师分析数据时,通常要使用很多查询,这些 查询应该基于同一个数据集,如果允许查询的同时加载最近新生成的数据,那么可能会产生问题。
Q: 如何快速地批量加载数据?
-
批量加载器(bulk loader)。将大量数据以流的形式传到存储中,不会对SQL层造成压力,并且利用像B+中的那样的特殊的批量加载方法来获取数据。
这种方式比SQL插入快一个数量级,所有主要的厂商都提供了高性能的批量加载器。
Q: “实时的”数据仓库(应用: 电商和24小时零售)有哪些问题?
-
插入(来自批量加载器或事物)必须要设置写锁,这些锁会与读锁冲突,并且可能导致数据参仓库“冻结”。
-
跨查询集的兼容性问题。解决方法有:
- 避免就地更新并提供历史查询
- 提供快照隔离这样的MVCC隔离级别
4.6.3 物化视图(Materialized Views)
数据仓库通常非常大,多个大表的join查询很耗时。为了加速常用的查询,大多数厂商提供了物化视图。
与逻辑视图不同,物化视图采取的是可以查询的实际表,但它对应于真正的 "基础 "数据表上的逻辑视图表达。对物化视图的查询可以避免在运行时执行视图表达式中的join。而在数据更新时,物化视图必须保持最新状态。
物化视图使用的三个方面:
-
选择要物化的视图
-
维护视图的“新鲜”。两种方式:
- 更新表的时候更新物化视图
- 周期性地删除并重建物化视图
-
考虑在特别的查询中使用物化视图
需要在运行时开销和物化视图的数据一致性之间进行权衡
4.6.4 OLAP和特殊查询的支持
一些数据仓库有一些可预测的查询。例如,每个月底汇总一下各个部分的销售额。除了这些常规查询外,就是一些特殊的查询了,由业务分析师临时制定。
对于可预测的查询,可以构建物化视图来加速。更一般地说,由于大多数商业分析查询都要求汇总,我们可以计算出一个物化视图,它是每个商店的部门的总销售额。然后,如果指定了上述的区域查询,它可以通过 "滚动 "每个区域的各个商店来满足。
这种聚合通常被称为数据立方(data cubes),是一类有趣的物化视图。在20世纪90年代初,Essbase等产品提供了定制的工具,用于以优先立方体格式存储数据,同时提供基于立方体的用户界面来浏览数据,这种能力被称为在线分析处理(OLAP,联机分析处理)。随着时间的推移,数据立方体的支持已经被添加到全功能的关系型数据库系统中,并且通常被称为关系型OLAP(ROLAP)。许多提供ROLAP的DBMS已经发展到在内部实现一些特殊情况下的早期OLAP式存储方案,因此有时被称为HOLAP(混合OLAP)方案。
4.6.5 雪花(Snowflake)模式查询的优化
fact表中存储的数据通常是: “customer X bought product Y from store Z at time T.”。dimensions通常包括 customer, product, store , time 等。
- Star模式: fact表中的一条记录,包含N个dimension表中的外键。
- Snowflake模式(多层Star模式): 与Star模式类似,不过这里的dimension是分层的,如时间(日/月/年),管理层级等。
基本上所有的数据仓库查询都需要在雪花模式中对这些表中的一些属性进行过滤,然后将结果连接到中央facts表,通过facts表或dimensions表中的一些属性进行分组,然后计算SQL聚合。随着时间的推移,供应商在他们的优化器中对这一类查询进行了特殊处理,因为它非常流行,而且为这种长时间运行的命令选择一个好的计划是非常关键的。
4.6.6. 数据仓库总结
数据仓库需要的能力与OLTP有很大差别。除了B+树,数据仓库还需要bitmap索引。数据仓库需要特别关注雪花模式上的聚合查询,而不是通用的优化器。数据仓库需要物化视图,而不是常规视图。数据仓库需要快速批量加载,而不是快速事物更新;等等。
主要的关系型厂商都是从基于OLTP的架构开始的,然后添加了基于数据仓库的架构。此外,有大量的小厂商也提供DBMS的解决方案。包括Teradata 和 Netezza,它们无共享的专有硬件,可以运行在它们的DBMS上。
最后,列存储在数据仓库领域与传统的存储引擎相比有巨大的优势,因为传统的存储单位是表行。当表很"宽"的时候,单独存储每一列是特别有效的,而且访问往往只在几列上。列存储还能实现简单有效的磁盘压缩,因为列中的所有数据都来自同一类型。**列式存储的挑战在于,表内行的位置需要在所有存储的列中保持一致,否则就需要额外的机制来连接列。**这对OLTP来说是个大问题,但对像仓库或系统日志库这样的主要应用数据库来说不是个大问题。提供列存储的供应商包括Sybase、Vertica、Sand、Vhayu和KX。
4.7 数据库的可扩展性
关系型数据库有哪些扩展数据类型,为了可以扩展它做了哪些努力?
4.7.1 抽象数据类型
最初的关系型数据库系统只支持一组静态的字母数字列类型,这种限制与关系型模型本身相关联。
DBMS可以在运行时扩展到新的抽象数据类型。为了实现这一点,DBMS的类型系统——以及解析器——必须由系统目录(system catalog)驱动,该目录维护着系统已知的类型列表,以及用于操作这些类型的 "方法"(代码)的指针。在这种方法中,DBMS不解释类型,它只是在表达式计算中适当地调用它们的方法;因此被称为 "抽象数据类型"。典型的例子是“存储过程”,让你可以在SQL中执行自定义的函数。
4.7.2 结构化类型和XML
多年来,有许多建议对数据库进行积极的改变,以支持非关系结构化类型:即嵌套的集合类型,如数组、集合、树,以及嵌套的图元和/或关系。也许今天这些建议中最相关的是通过像XPath和XQuery这样的语言对XML的支持。
处理像XML这样的结构化类型的方法大致有三种:
- 建立一个定制的数据库系统,对具有结构化类型的数据进行操作
- 把XML类型当作一个ADT
- DBMS在插入时将嵌套结构 "规范化 "为一组关系,用外键将子对象连接到它们的父对象
4.7.3 全文搜索
传统上,在处理丰富的文本数据和通常与之相关的关键词搜索方面,关系型数据库是出了名的差。
然而,今天的大多数DBMS要么包含一个用于文本索引的子系统,要么可以与一个单独的引擎捆绑在一起来完成这项工作。文本索引设施通常既可用于全文文档,也可用于图元中的简短文本属性。在大多数情况下,全文索引是异步更新的("抓取"),而不是事务性维护。在一些系统中,全文索引被存储在DBMS之外,因此需要单独的工具进行备份和恢复。
5. 存储管理
DBMS存储管理器(storage manager)的两种访问模式:
- 原始模式:直接与磁盘的低级的块设备驱动交互
- OS文件系统:使用标准的OS文件系统工具
访问模式会影响DBMS在空间和时间上控制存储的能力。
5.1 空间控制(Spatial Control)
磁盘顺序访问比随机访问的速度快10-100倍,且这个比例还在增长。因此,DBMS存储管理器的关键是: 合理地存放数据块,让大数据量的查询能够顺序访问磁盘。由于DBMS比底层的OS更清楚如何访问其存储的数据,因此由DBMS控制磁盘上数据块的访问是有意义的。
控制空间局部性最好的方式有:
-
原始模式:完全绕过文件系统直接将数据存储到“原始”磁盘上。
-
优点:局部性完全可控
-
缺点
-
DBA需要为DBMS进行磁盘分区
-
"原始磁盘"访问接口通常是特定于操作系统的,难以移植
- 大多数DBMS已经克服了此障碍
-
“虚拟”磁盘的流行,导致原始模式的优点被淡化。RAID、Storage Area Networks(SAN) 和 logical volume managers。如今,大多数场景下,“虚拟”磁盘已经成为了标准。
-
-
-
大文件+偏移量:在文件系统中创建一个大文件,然后通过文件中的偏移量来管理数据的位置。
-
优点
- 避免直接访问块设备
- 性能好
-
有的DBMS还支持自定义数据页的大小:
-
好处
- 适应不同的负载
-
缺点
- 管理复杂度提高
-
案例
- DB2,Oracle
5.2 临时空间控制:缓存
大多数操作系统提供了内置的I/O缓存机制,决定何时读写文件块。如果DBMS使用标准文件接口来写入数据,OS缓冲可能会通过静默推迟或重排写入混淆DBMS的逻辑。这可能会导致DBMS出现重大问题。因此,除了控制数据在磁盘上的位置之外,DBMS还必须控制何时将数据写入磁盘。
不控制何时将数据写入磁盘可能会对DBMS造成的影响:
-
第一类问题:操作系统 I/O缓冲对ACID事物正确性影响。如果无法明确控制磁盘写入的时间和顺序,DBMS无法保证在软件或硬件故障后进行原子恢复。WAL要求在写入到数据库设备之前,需要先写入到日志设备,且在日志被可靠地写入日志设备之前,提交请求不会返回。
-
第二类问题:操作系统 I/O缓冲问题会影响性能(但不影响正确性)。 文件系统内置的预读(read-ahead)和后写(write-behind)机制不适合于DBMS的访问模式。
文件系统逻辑依赖于文件中物理字节 偏移的连续性来做出预读决定。DBMS基于未来的(在SQL查询处理层知道,但在文件系统中不容易辨别的) 读取请求,支持逻辑上可预测的I/O决策,这些请求在SQL查询处理级别已知,但在文件系统级别不容易识别。例如,在扫描不一定是连续的B+-树的叶子(行存储在B+-树的叶子中)时,可以请求逻辑dbms级别的预读。逻辑预读在DBMS逻辑中很容易实现,方法是让DBMS提前发出I/O请求。
-
第三个性能问题:“双缓冲”和内存拷贝的高CPU开销。如果DBMS自己需要做缓冲,那么操作系统做的额外缓冲就是冗余的。这种冗余会导致两个成本:
-
浪费了系统内存——可以进行有效工作的内存减少了。
-
浪费时间和处理资源。多了一个额外的拷贝步骤。
内存中的拷贝可能是一个严重的瓶颈。拷贝会导致延迟,消耗CPU执行周期,且填满CPU数据缓存。 由于CPU和RAM之间的速度差异,内存拷贝成为了计算机架构中的主要瓶颈。
大多数现在系统都提供了钩子,可以让数据库这样的程序能够规避双重缓冲文件缓存且能够控制页的替换策略。
-
5.3 缓冲管理
-
共享缓冲池:高效地访问数据库页
- 缓冲池分配:静态分配 → 动态分配
- 缓冲池的结构:缓冲池由一组frame组成,每个frame是一个内存区域,大小相当于数据库磁盘块。区块从磁盘拷贝到内存中不需要格式转换,在内存中操作的是其原生格式,稍后会写回。 这种不用转换的方式避了“编码”和 “解码”到/从磁盘中的CPU瓶瓶颈;更重要的是,固定大小的frame避免了外部碎片和通用的压缩技术导致的内存管理复度。
- 页替换: LRU算法
6. 事务:并发控制和恢复
实践中,数据库系统并非是单体的、整体的软件系统,而是被分成多个独立的组件。而DBMS中真正单体的部分是 transactional storage manager,它通常包含4个深度交织在一起的组件:
- 用于并发控制的锁管理器(lock manager)
- 用于恢复的日志管理器(log manager)
- 用于暂存数据库I/O的缓冲池(buffer pool)
- 用于组织磁盘上的数据的Access methods。
6.1 ACID 注意事项
回顾一下ACID:
-
原子性是对事务的 "全有或全无 "的保证——要么一个事务的所有行为都提交,要么都不提交。
-
一致性是一种特定于应用的保证;SQL完整性约束通常被用来在DBMS中捕获这些保证。考虑到由一组约束条件提供的一致性定义,一个事务只有在离开数据库时处于一致性状态才能提交。
一致性严格来说是凑数的,它通常需要应用程序来维护。
-
隔离是对应用程序编写者的一种保证,即两个并发的事务不会看到对方的飞行中的(尚未提交的)更新。因此,应用程序不需要进行 "防御性 "编码以担心其他并发事务的 "脏数据";它们可以被编码为程序员对数据库的唯一访问。
-
持久性是一种保证,已提交事务的更新在数据库中对后续事务是可见的,不受后续硬件或软件错误的影响,直到它们被另一个已提交事务覆盖。
粗略地说,现代DBMS系统通过 锁协议实现隔离性。通过 日志和恢复实现持久性。隔离性和原子性通过**锁(防止暂时数据可见) + 日志(保证可见数据的正确性)**保证。一致性由查询执行器在运行时检查:如果一个事物的操作会违反SQL一致性限制,该事物会被取消并返回一个错误代码。
6.2 可串行化简要回顾
可串行化由DBMS并发控制模型强制执行。存在三种广泛的并发控制技术:
-
严格的两阶段锁(2PL): 事物在读取数据之前获得共享锁,在写入数据之前获得互斥锁。所有的锁在事物结束后自动释放。事物在等待获取锁时阻塞在等待队列中。
事物开始前获取锁,结束后释放锁
-
多版本并发控制(MVCC):事务不持有锁,而是保证在过去的某个时间点上对数据库状态的一致视图,即使在那个固定的时间点之后,行已经改变了。
-
乐观并发控制(Optimistic concurrency Control, OCC):多个事务被允许在不阻塞的情况下读取和更新一个项目。相反,事务维护其读和写的历史,在提交事务之前,检查历史上可能发生的隔离冲突;如果发现任何冲突,其中一个冲突的事务将被回滚。
冲突检测 + 回滚
大多数商用DBMS使用 2PL实现完全的串行化。锁管理器是负责为2PL提供设施的代码模块。
为了减少锁和锁冲突,一些DBMS支持MVCC或OCC,通常作为2PL的插件存在。在MVCC中,没有读锁,但这是通常以无法提供完全串行化为代价实现的。
- 在商用MVCC实现中,一致视图要么是读事物开始时的值,要么是事物的最近的SQL语句开始时的值。
- 虽然OCC避免了锁上的等待,但在事务之间的真正冲突中,它可能会导致更高的惩罚。在处理跨事务的冲突时,OCC就像2PL一样,只是它将2PL中的锁等待转换为事务回滚。
6.3 锁和闩锁(Latch)
锁有不同的锁 "模式",这些模式与锁—模式兼容表相关,一般的锁模式有:共享模式、独占(排他)模式。
锁管理器支持两个基本调用:lock(lockname, transactionID, mode) 调用,和 remove transaction(transactionID) 调用。
对于2PL协议,不应该有单独的调用来单独解锁资源——remove transaction()调用将解锁与一个事务相关的所有资源。
对于较低程度的隔离级别,需要一个unlock(lockname, transactionID)调用。还有一个upgrade(lockname, transactionID, newmode)调用,允许事务以两阶段的方式 "升级 "到更高的锁模式(例如,从共享模式到独占模式),而不需要放弃和重新获取锁。
此外,一些系统还支持conditional lock(lockname, transactionID, mode)调用。带条件的lock()调用总是立即返回,并指出它是否成功地获取了锁。如果没有成功,调用的DBMS线程不会排队等待获取到锁。
为了支持这些调用,锁管理器维护两个数据结构:
-
全局锁表。保存锁名及其相关信息。锁表是一个动态的哈希表,key是锁名的哈希值,value是一个锁模式标志,以及一个锁请求(事物ID,模式)的等待队列。
-
事物表。key是事物ID,value包含每个事物T中都有的两个属性:
-
- 一个指针,指向T的DBMS线程状态,当T获取到锁时,其对应的DBMS线程可以被重新调度。
-
- 一个指针列表,指向全局锁表中T请求的所有锁,以便删除与事物T相关的所有锁(例如,在事物提交或中止时)。
-
作为数据库锁的辅助手段,较轻量的闩锁(latch)也有互斥的功能。闩锁更类似于监控器或信号量,而不是锁;它们被用于实现对DBMS内部数据结构的互斥访问。例如,缓冲池页表有一个与每个帧相关的锁,以保证在任何时候只有一个DBMS线程在替换一个给定的帧。闩锁被用于实现锁和可能被并发修改的、短暂稳定的内部数据结构。闩锁在很多方面与锁都有不同:
- 锁保存在锁表中,并通过哈希表定位;闩锁位于它们所保护的资源附近的内存中,并通过直接寻址进行访问。
- 在严格的2PL实现中,锁需要在事物开始前获取,事物结束后释放。闩锁可以在事物执行期间获取或释放。
- 锁的获取完全由数据访问驱动,因此获取锁的顺序和声明周期受应用程序和查询优化器的控制。闩锁通过DBMS内部的特殊代码获取,DBMS会根据策略请求和释放闩锁。
- 对于锁来说,死锁是允许的,通常会检测死锁并通过重启事物来解决死锁问题。对于闩锁来说,必须避免死锁;闩锁的死锁是DBMS代码bug。
- 闩锁使用原子的硬件指令或OS内核中的互斥指令来实现。
- 闩锁请求最多消耗几十个CPU周期,而锁请求需要几百个CPU周期。
- 锁管理器会追踪一个事物持有的所有锁,并在事物异常后自动释放锁;但在DBMS内部程序中,异常处理时需要手动清理闩锁。
- 闩锁无法被追踪,因此如果出错也无法被自动释放
6.3.1 事物隔离级别
在事务概念发展的早期,人们试图通过提供比可序列化更 "弱 "的语义来提高并发性。挑战在于如何在这些情况下为语义提供强有力的定义。ANSI SQL标准定义了四个 "隔离级别":
-
读未提交: 事物可以读取任何已提交或未提交的数据,。这在锁的实现中是通过读取请求的进行而不获取任何锁来实现的。
-
读已提交:事物能够读取任何已提交的数据。重复读取对象可能或导致读到不同版本的提交数据。这是通过读取请求在访问对象前获取一个读锁,然后在访问该对象后立即释放锁实现的。
-
可重复读: 事物只能够读取某个版本的已提交的数据;一旦事物读取了一个对象,它总是会读取该对象的相同版本。这是通过读取请求在访问对象前获取一个读锁,且直到事物结束一致持有该锁实现的。
乍一看,可重复读似乎提供了完全的可序列化,但事实并非如此。可重复读存在幻读问题。
幻读(phantom read): 在同一个事务中用相同的谓词多次访问一个关系,但是在再次访问时看到了第一次访问时没有看到的新的 "幻影 "记录。幻读产生的原因是行级粒度的两阶段锁不能防止向表中插入新的记录。表的两阶段锁可以防止幻行, 但在事物只能通过索引访问几个元祖的情况下,表级锁定可能会受到限制。 -
串行化: 保证完全串行化的访问。
商业系统通过基于锁的并发控制实现来提供上述四个隔离级别。不幸的是,早期的ANSI标准没有提供真正明确的定义。它依赖于一个假设,即锁定方案被用于并发控制,而不是乐观的或多版本并发方案。除了标准的ANSI SQL隔离级别之外,各种供应商还提供了额外的级别,这些级别在特定的情况下被证明是受欢迎的。
- 游标稳定性( CURSOR STABILITY):这个级别是为了解决READ COMMITTED的 "丢失更新(lost update) "问题。假设两个事务T1和T2。T1在READ COMMITTED模式下运行,读取一个对象X(例如一个银行账户的价值),记住它的价值,随后根据记住的价值写入对象X(例如在原始账户价值上增加100美元)。T2也读和写X(例如从账户中减去300美元)。如果T2的操作发生在T1的读和T1的写之间,那么T2的更新效果就会丢失——在我们的例子中,账户的最终价值将增加100美元,而不是像预期的那样减少200美元。在CURSOR STABILITY模式下的事务对查询游标上最近读到的记录持有一个锁;当游标被移动(例如,通过另一个FETCH)或者事务终止时,这个锁会自动丢弃。CURSOR STABILITY允许事务在单个记录上进行读—想—写的操作序列,而阻止其他事务的介入更新。
- 快照隔离( SNAPSHOT ISOLATION):在SNAPSHOT ISOLATION模式下运行的事务是在事务开始时存在的数据库版本上运行的;其他事务的后续更新对该事务来说是不可见的。这是MVCC在生产数据库系统中的主要用途之一。当事务开始时,它从一个单调增长的计数器中获得一个唯一的开始时间戳;当它提交时,它从计数器中获得一个唯一的结束时间戳。只有当没有其他重叠的事物(开始/结束交易时间重叠)写了相同的数据时,该事物才会提交。这种隔离模式依赖于多版本并发来实现,而不是锁。然而,在支持SNAPSHOT ISOLATION的系统中,这些方案通常是共存的。
- 一致读( READ CONSISTENCY): 这是由Oracle定义的MVCC方案;它与SNAPSHOT ISOLATION有细微的不同。在Oracle方案中,每个SQL语句(在一个事务中可能有很多)看到的都是该语句开始时的最新提交的值。对于从游标中获取的语句,游标集是基于它被打开时的值。这是通过维护单个记录的多个逻辑版本来实现的,一个事务可能引用一个记录的多个版本。与其存储可能需要的每个版本,Oracle只存储最新的版本。如果需要一个较早的版本,它通过采取当前版本和 "回滚",根据需要应用撤销日志记录来产生较早版本。修改是通过长期的写锁来维护的,所以当两个事务想写同一个对象时,第一个写者 "赢 "了,第二个写者必须等待第一个写者的事务完成后才能继续写。相比之下,在SNAPSHOT ISOLATION中,第一个提交者 "赢",而不是第一个写入者。
弱隔离方案可以提供比完全串行化更高的并发性。因此,一些系统甚至将弱一致性作为默认值。例如,Microsoft SQL Server默认为READ COMMITTED。缺点是,隔离(ACID意义上的隔离)没有保证。因此,应用程序的编写者需要理解这些方案的微妙之处,以确保他们的事务能够正确运行。考虑到这些方案在操作上定义的语义,这可能很棘手,并可能导致应用程序更难在DBMS之间移动。
6.4 日志管理器
日志管理器(log manager) 负责维护已提交事物的持久性、促进中止事物的 回滚以保证原子性,以及负责从系统故障或无序关机中 恢复。
日志管理器在磁盘上维护了一个日志记录序列,并在内存维护了一组数据结构。为了支持在崩溃后恢复,内存中的数据结构需要能够从日志和数据库中的持久化数据重建。
数据库恢复的标准主题是预写日志(WAL)协议。预写日志协议由三个很简单的规则组成:
- 每次对数据的更改都应该生成一个日志记录,且该日志记录必须在数据库页被刷 之前被刷到日志设备。
- 数据库日志必须被 按序刷入;直到所有在 r 之前的日志记录被刷入后,日志记录r才能被输入。
- 一旦事物提交请求, 提交日志记录必须在提交请求成功返回 之前被刷到日志设备。
第1条保证原子性——未完成的事物的动作可以被撤销。第2、3条保证持久性——已提交事物的动作可以在系统崩溃后被重做。
鉴于这些简单的原则,高效的数据库日志是令人惊讶的,因为它是如此微妙和详细。然而,在实践中,上述简单的故事由于需要极端的性能而变得复杂了。面临的挑战是如何保证提交的事务在 "fast path "上的效率,同时也为中止的事务提供高性能的回滚,并在崩溃后快速恢复。
这些原则虽然简单,但效率却很高。然而,在实践中,这些简单的原则由于需要极端的性能而变得复杂。面临的挑战是如何保证提交的事务在 "快速路径(fast path)"上的效率,同时为中止的事务提供高性能的回滚,以及崩溃后的快速恢复。当添加特定的应用优化时,日志变得更加复杂,例如,支持提高只能增量或减量的字段的性能("托管事务")。
为了最大限度地提高快速路径的速度,大多数商业数据库系统的运行模式被Haerder和Reuter称为 "直接(DIRECT)、偷窃(STEAL)/不强迫(NO-FORCE)":
- 数据就地更新
- 缓冲池中未固定的frame可以被“偷”,即使它们可能包含未提交的数据
- 在提交请求返回到用户之前,缓冲池中的页不需要被“强制”刷到数据库。
这些策略将数据保留在DBA选择的位置,它们给了缓冲区管理器和磁盘调度器充分的自由来决定内存管理和I/O策略,而不考虑事务的正确性。这些功能可以带来很大的性能优势,但是需要日志管理器有效地处理所有的微妙问题,即撤销从中止的事务中偷来的页面的冲刷,以及重做对已提交的事务中因崩溃而丢失的非强制页面的修改。一些DBMS使用的一种优化方法是将DIRECT、STEAL/NOT-FORCE系统的可扩展性优势与DIRECT NOT-STEAL/NOT-FORCE系统的性能相结合。在这些系统中,除非缓冲池中没有干净的页面,否则页面不会被盗,在这种情况下,系统会退化到STEAL策略,并产生上述的额外开销。
日志中的另一个快速路径挑战是保持尽可能小的日志记录,以提高日志I/O的吞吐量。一个自然的优化是记录逻辑操作(例如,"将(Bob, $25000)插入EMP"),而不是物理操作(例如,通过插入元组修改的所有字节范围的后映像,包括堆文件和索引块上的字节)。这样做的权衡是,重做和撤销逻辑操作的逻辑变得相当复杂。在实践中,使用的是物理和逻辑日志的混合物(所谓的 "生理 "日志)。在ARIES中,物理日志通常被用来支持REDO,而逻辑日志被用来支持UNDO。这是ARIES规则的一部分,即在恢复过程中 "重复历史 "以达到崩溃状态,然后从该点回滚事务。
崩溃恢复是需要在系统故障或非有序关闭后将数据库恢复到一个一致的状态。如上所述,恢复在理论上是通过重放历史,从第一条一直到最近的记录,一步步通过日志记录实现的。这种技术是正确的,但效率不高,因为日志可能是任意长的。与其从第一条日志记录开始,不如从下面的这两条日志记录中最古老的一条开始恢复,就能得到正确的结果:
- 描述缓冲池中最古老的脏页的最早变化的日志记录;
- 代表系统中最古老的事务开始的日志记录。
这条日志的序列号被称为 恢复日志序列号(recovery LSN) 。由于计算和保存恢复日志LSN会产生开销,而且我们知道恢复日志LSN是单调增长的,所以我们不需要一直保持它的最新状态。相反,我们在称为**检查点(checkpoints)**的周期性间隔中计算它。
一个简单的检查点机制是强制所有的脏页刷盘,然后计算和存储恢复日志LSN。对于一个大的缓冲池,完成脏页刷盘可能需要几秒钟的时间。因此,需要一个更有效的 "模糊 "检查点方案,以及通过处理尽可能少的日志将检查点正确带到最近的一致状态的逻辑。ARIES使用了一个非常聪明的方案,其中实际的检查点记录非常小,只包含足够的信息来启动日志分析过程,并能够重新创建崩溃时丢失的主内存数据结构。在ARIES模糊检查点期间,恢复日志LSN被计算出来,但不需要同步写入缓冲池页面。采用一个单独的策略来决定何时异步刷盘。
请注意,回滚需要写入日志记录。这可能会导致一种情况——即由于日志空间耗尽,运行中的事务不能继续进行,但它们也不能回滚。这种情况通常是通过**空间预留方案(space reservation schemes)**来避免的,然而,这些方案很难在多版本系统中实现并保持正确。
最后,由于数据库不仅仅是磁盘页上的一组用户数据记录,它还包括各种 "物理 "信息,使其能够管理其内部基于磁盘的数据结构,这使得日志和恢复的任务更加复杂。我们在下一节的索引日志中讨论这个问题。
6.5 索引中的锁和日志
索引是用于访问数据库中的数据的物理存储结构。索引本身对于数据库应用程序的开发者来说是不可见的,除非它们能提高性能或执行唯一性约束。开发人员和应用程序不能直接观察或操作索引中的记录。这使得索引可以通过更有效的(和复杂的)事务性方案来管理。索引并发和恢复需要保留的唯一不变因素是——索引总是从数据库中返回事务上一致的记录。
6.5.1 B+树中的闩锁
B+树由数据库磁盘页组成,通过缓冲池访问,就像数据页一样。因此,索引并发控制的一个方案是对索引页使用两阶段锁。这意味着每一个需要访问索引的事务都需要锁定B+树的根,直到事物提交——这种方案的并发性有限。为了解决这个问题,人们开发了各种基于闩锁的方案,而不在索引页上设置任何事务锁。这些方案的关键是对树的物理结构的修改(例如,拆分页面)可以以非事物的方式进行,只要所有并发的事务依旧可以在叶子上找到正确的数据。这方面大致有三种方法:
- 保守方案: 多个事物可以访问同一个页中的不同内容。一个可能的冲突是,一个读事务想要遍历树的一个完全打包的内部页面,而一个并发的插入事务正在该页面下面操作,可能需要分割该页面。这些保守的方案与下面较新的想法相比,牺牲了太多的并发性。
- 闩与锁联合的方案。在每个树节点被访问之前都会锁住它,只有当下一个要访问的节点被成功锁住时才会解除锁住的节点。这种方案有时被称为闩锁 "抓取",因为在树上 "抓 "住一个节点,"抓 "住它的子节点,释放父节点,然后重复这种动作。闩与锁耦合在一些商业系统中使用,如IBM的ARIES—IM版本。
- 右链接方案。向B+树中额外添加简单的结构,以尽量减少对闩锁和重新遍历的要求。特别是,在每个节点到右邻居之间增加了一个链接。在遍历过程中,右链方案不做闩与锁的耦合——每个节点都被闩、读取和解除闩。右链方案的主要思想是,如果一个遍历事务跟踪一个指向节点n的指针,发现n在中间被分割了,遍历事务可以检测到这个事实,并通过右链 "右移",找到树中新的正确位置。一些系统也支持使用反向链接进行反向遍历。
6.5.2 日志的物理结构
除了特殊的并发逻辑外,索引还使用特殊的记录日志逻辑。这种逻辑使得日志的记录和恢复更加有效,但代价是增加代码的复杂性。主要的想法是,当相关的事务被中止时,对于索引结构的更改不需要在相关事务取消时被撤销;这样的(索引结构)变化通常不会对其他事务所看到的数据库记录产生影响。例如,如果一个B+树页面在一个插入事务中被分裂,而该事务随后又中止了,那么在中止处理过程中就没必要撤销分裂。
这带来的挑战就是——将一些日志记录标记为只重做(redo-only)。在对日志的任何撤销处理过程中,只重做的修改可以保留(不必撤销)。ARIES为这些情况提供了一种优雅的机制,称为嵌套顶层行动(nested top actions),它允许恢复进程在恢复期间 "跳过 "物理结构修改的日志记录,而不需要任何处理特殊情况的代码。
6.5.3 Next-Key锁定:
当一个事务通过索引访问记录时,就会出现幻行问题。在这种情况下,事务通常不会锁定整个表,只是锁定表中通过索引访问的记录(例如,"Name BETWEEN 'Bob' AND 'Bobby' ")。在没有表级锁的情况下,其他事务可以自由地向表中插入新的记录(例如,"Name='Bobbie'")。当这些新插入的数据落在查询谓词的值范围内时,它们将出现在随后通过该谓词的访问中。请注意,幻行问题与数据库记录的可见性有关,因此是一个锁的问题,而不仅仅是锁的问题。原则上,我们需要的是以某种方式锁定原始查询的搜索谓词所代表的逻辑空间的能力,例如,按照词典顺序介于 "Bob "和 "Bobby "之间的所有可能字符串的范围。不幸的是,谓词锁定是昂贵的,因为它需要一种方法来比较任意的谓词是否重叠。这不能用基于散列的锁表来完成。
在B+树中,解决幻行问题的一种常见方法被称为下一个键锁定(next-key locking)。在下一个键锁定中,索引插入代码被修改,这样,一个索引键为k的记录的插入必须对索引中存在的下一个键记录分配一个独占锁,其中下一个键记录的最低键大于k。它还确保记录不能被插入到之前返回的最低键的记录下面。例如,如果在第一次访问中没有发现 "Bob "键,那么在同一事务中的后续访问中也不应该发现 "Bob "键。还有一种情况:插入刚刚超过先前返回的最高键位元组的元组。为了防止这种情况,下一个键锁定协议要求读事务在索引中的下一个键元组上也获得一个共享锁。在这种情况下,下一个键元组是不满足查询前提条件的最小键元组。更新在逻辑上表现为先删除后插入,尽管优化是可能的和常见的。
下一个键的锁定,虽然有效,但确实存在过度锁定的问题,这在某些工作负载中可能会出现问题。例如,如果我们扫描从键1到键10的记录,但是被索引的列只存储了键1、5和100,那么从1到100的整个范围都会被读取锁定,因为100是10之后的下一个键。
下一个键锁定并不仅仅是一个聪明的骇客方法。它是一个使用物理对象(当前存储的元组)作为逻辑概念(谓词)的代名词的例子。这样做的好处是,简单的系统基础设施,如基于哈希的锁表,可以用于更复杂的目的,只需修改锁协议。当这种语义信息可用时,复杂软件系统的设计者应该将这种逻辑代理的一般方法放在他们的 "工具箱 "中。
6.6 事物存储的内部依赖
事务性存储系统的三个主要方面之间的一些相互依赖关系:并发控制、恢复管理和访问方法。在一个更幸福的世界里,有可能在这些模块之间确定狭窄的API,从而使这些API背后的实现可以被交换。我们在本节的例子表明,这并不容易做到。我们并不打算在此提供一份详尽的相互依赖关系的清单;生成并证明这样一份清单的完整性将是一项非常具有挑战性的工作。然而,我们确实希望能够说明事物存储的一些曲折逻辑,从而证明商业DBMS中的单体实现是合理的。
我们首先只考虑并发控制和恢复,而不考虑访问方法的进一步复杂化。即使是这样的简化,各部分也是深深交织在一起的。并发和恢复之间关系的一个表现是,预写日志对锁定协议进行了隐含的假设。写入日志需要严格的两阶段锁定,而在非严格的两阶段锁定下,将无法正确操作。要看到这一点,请考虑在一个中止的事务的回滚过程中会发生什么。恢复代码开始处理中止的事务的日志记录,撤销其修改。一般来说,这需要改变事务之前修改的页面或图元。为了进行这些修改,该事务需要在这些页面或图元上拥有锁。在一个非严格的2PL方案中,如果事务在中止前放弃了任何锁,它可能无法重新获得完成回滚过程所需的锁。
访问方法使事情进一步复杂化。将教科书上的访问方法算法(例如线性散列或R-树),在事物系统中实现一个正确的、高并发的、可恢复的版本,这是一个重大的智力和工程挑战。由于这个原因,大多数领先的DBMS仍然只实现堆文件和B+树作为事物保护的访问方法;PostgreSQL的GiST实现是一个明显的例外。正如我们在上面为B+树所说明的那样,事务性索引的高性能实现包括复杂的闩锁、锁定和记录的协议。DBMS中的B+树充满了对并发和恢复代码的调用。即使是像堆文件这样简单的访问方法,也有一些围绕描述其内容的数据结构的棘手的并发和恢复问题。这种逻辑对所有的访问方法来说都不是通用的——它在很大程度上是根据访问方法的具体逻辑和它的特定实现而定制的。
访问方法的并发控制只在面向锁的方案中得到了很好的发展。其他的并发方案(例如,乐观的或多版本的并发控制)通常根本不考虑访问方法,或者只在不经意间提到它们,而且不切实际。因此,对于一个给定的访问方法实现,混合和匹配不同的并发机制是很困难的。
访问方法中的恢复逻辑是特别针对系统的:访问方法日志记录的时间和内容取决于恢复协议的细节,包括对结构修改的处理(例如,它们是否在事务回滚时被撤销,如果不是,如何避免),以及物理和逻辑日志的使用。即使是像B+树这样的特定访问方法,恢复和并发逻辑也是相互交织的。在一个方向上,恢复逻辑取决于并发协议:如果恢复管理器必须恢复树的物理一致状态,那么它需要知道哪些不一致的状态可能会出现,以便用日志记录适当地括住这些状态以实现原子性(例如,通过嵌套的顶部动作)。在相反的方向,一个访问方法的并发协议可能依赖于恢复逻辑。例如,B+树的右链方案假设树中的页在分裂后永远不会 "重新合并"。这个假设要求恢复方案使用一种机制,如嵌套的顶部行动,以避免撤销由中止的事务产生的分裂。
这里的一个亮点是,缓冲区管理与存储管理器的其他组件相对隔离得很好。只要页面被正确地缓冲,缓冲区管理器就可以自由地封装它的其余逻辑,并根据需要重新实现它。例如,缓冲区管理器可以自由地选择要替换的页面(因为有STEAL属性),以及页面刷新的调度(由于有NOT FORCE属性)。当然,实现这种隔离是造成并发和恢复方面的许多复杂性的直接原因。因此,这个地方也许没有想象中那么清晰。
7. 共享组件
在本节中,我们将介绍一些共享组件和实用程序,它们几乎存在于所有商业DBMS中,但在文献中很少讨论。
7.1 Catalog Manager
数据库目录(catalog )持有系统中的数据信息,是元数据的一种形式。目录记录了系统中基本实体的名称(用户、模式、表、列、索引等)以及它们之间的关系,它本身作为一组表存储在数据库中。通过将元数据保持在与数据相同的格式中,系统变得更加紧凑,使用起来也更加简单:用户可以使用与其他数据相同的语言和工具来调查元数据,而管理元数据的内部系统代码也与管理其他表的代码基本相同。这种代码和语言的重用是一个重要的教训,在早期阶段的实施中经常被忽视,通常会让后来的开发者感到非常遗憾。
由于效率的原因,基本目录数据的处理方式与普通表有些不同。目录中的高流量部分通常根据需要在主内存中被具体化,典型的数据结构是将目录的平面关系结构 "去规范化 "为一个主内存的对象网络。这种在内存中缺乏数据独立性的情况是可以接受的,因为内存中的数据结构只被查询分析器和优化器以一种风格化的方式使用。额外的目录数据在解析时被缓存在查询计划中,而且通常是以适合查询的非规范化的形式。此外,目录表经常受制于特殊情况下的事务处理技巧,以减少事务处理中的 "热点"。
在商业应用中,目录可以变得非常大。例如,一个主要的企业资源规划应用有超过60,000个表,每个表有4到8列,每个表通常有两到三个索引。
7.2 内存分配器
教科书中对DBMS内存管理的介绍往往完全集中在缓冲池上。在实践中,数据库系统也会为其他任务分配大量的内存。对这些内存的正确管理既是一个编程负担,也是一个性能问题。塞林格式的查询优化可以使用大量的内存,例如,在动态编程期间建立状态。像哈希连接和排序这样的查询操作者在运行时分配大量的内存。商业系统中的内存分配通过使用基于上下文的内存分配器而变得更加有效和容易调试。
内存上下文是一个内存数据结构,它维护着一个连续的虚拟内存区域的列表,通常称为内存池。每个区域都可以有一个小头,包含一个上下文标签或一个指向上下文头结构的指针。内存上下文的基本API包括对以下内容的调用:
- 创建一个具有给定名称或类型的上下文。上下文类型可能会建议分配器如何有效地处理内存分配。例如,查询优化器的上下文通过小的增量增长,而哈希连接的上下文则以几个大的批次分配它们的内存。基于这些知识,分配器可以选择一次分配更大或更小的区域。
- 在一个上下文中分配一大块内存。这个分配将返回一个指向内存的指针(很像传统的malloc()调用)。该内存可能来自上下文中的一个现有区域。或者,如果在任何区域中没有这样的空间,分配器将要求操作系统提供一个新的内存区域,给它贴上标签,并将其链接到上下文中。
- 在一个上下文中删除一个内存块。这可能会也可能不会导致该上下文删除相应的区域。从内存上下文中删除是有点不寻常的。一个更典型的行为是删除整个上下文。
- 删除一个上下文。这首先释放了与该上下文相关的所有区域,然后删除了上下文头。
- 重置一个上下文。这将保留该上下文,但将其返回到最初创建时的状态,通常是通过取消所有先前分配的内存区域。
内存上下文提供了重要的软件工程优势。最重要的是,它们可以作为垃圾收集的低级别的、程序员可控制的替代品。例如,编写优化器的开发人员可以在优化器上下文中为一个特定的查询分配内存,而不用担心以后如何释放内存的问题。当优化器选择了最佳计划后,它可以从查询的单独执行器上下文中将计划复制到内存中,然后简单地删除查询的优化器上下文。这就省去了写代码的麻烦,可以仔细浏览所有的优化器数据结构并删除它们的组件。它也避免了棘手的内存泄漏,因为这种代码中的错误可能会导致内存泄漏。这个功能对于查询执行的自然 "分阶段 "行为非常有用,控制从解析器到优化器再到执行器,每个上下文中都有一些分配,然后是删除上下文。
请注意,内存上下文实际上比大多数垃圾收集器提供了更多的控制,因为开发者可以控制去分配的空间和时间位置。上下文机制本身提供了空间控制,使程序员能够将内存分成逻辑单元。时间上的控制来自于允许程序员在适当的时候发布上下文删除。相比之下,垃圾收集器通常在程序的所有内存上工作,并对何时运行做出自己的决定。这是试图用Java编写服务器质量代码的挫折之一。
在malloc()和free()的开销相对较高的平台上,内存上下文也具有性能优势。特别是,内存上下文可以使用语义知识(通过上下文类型),了解内存将如何被分配和取消,并可以相应地调用malloc()和free(),以尽量减少操作系统的开销。数据库系统的一些组件(例如解析器和优化器)会分配大量的小对象,然后通过上下文删除一次性释放它们。在大多数平台上,调用free()许多小对象是相当昂贵的。内存分配器可以调用malloc()来分配大的区域,并将得到的内存分配给其调用者。相对来说,缺乏内存的取消分配意味着不需要malloc()和free()使用的压缩逻辑。当上下文被删除时,只需要调用几个free()来删除大区域。
有兴趣的读者可能想浏览一下开源的PostgreSQL代码。这利用了一个相当复杂的内存分配器。
7.3 磁盘管理子系统
DBMS的教科书倾向于把磁盘当作同质的对象。对象。在实践中,磁盘驱动器是复杂的、异质的(异质)硬件,在容量和带宽上差别很大。在容量和带宽方面差别很大的硬件。因此,每个DBMS都有一个磁盘管理子系统来处理这些问题,并管理表和其他存储单元在原始设备、逻辑卷或文件上的分配。
这个子系统的一个职责是将表映射到设备和/或文件。表与文件的一对一映射听起来很自然,但在早期的文件系统中引起了重大问题。首先,操作系统文件传统上不能大于一个磁盘,而数据库表可能需要跨越多个磁盘。其次,分配太多的操作系统文件被认为是不好的,因为操作系统通常只允许几个开放的文件描述符,而且许多用于目录管理和备份的操作系统实用程序不能扩展到非常多的文件。最后,许多早期的文件系统将文件大小限制在2GB。这显然是一个不可接受的小表限制。许多DBMS供应商使用原始IO完全规避了操作系统的文件系统,而其他供应商则选择绕过这些限制。因此,所有领先的商业DBMS都可以将一个表分散到多个文件中,或者在一个数据库文件中存储多个表。随着时间的推移,大多数操作系统文件系统的发展已经超越了这些限制。但是遗留的影响仍然存在,现代DBMS仍然通常将操作系统文件作为抽象的存储单元,任意地映射到数据库表。
更复杂的是处理设备特定细节的代码,以维持第四节所述的时间和空间控制。今天有一个庞大而充满活力的产业,它基于复杂的存储设备,"假装 "是磁盘驱动器,但实际上是大型硬件/软件系统,其API是一个传统的磁盘驱动器接口,如SCSI。这些系统包括RAID系统和存储区域网络(SAN)设备,往往具有非常大的容量和复杂的性能特征。管理员喜欢这些系统,因为它们易于安装,而且通常提供易于管理、具有快速故障转移的位级可靠性。这些特点为客户提供了一种重要的舒适感,超过了DBMS恢复子系统的承诺。例如,大型DBMS安装通常使用SANs。
不幸的是,这些系统使DBMS的实现变得复杂。例如,RAID系统在发生故障后的表现与所有磁盘正常工作时的表现非常不同。这有可能使DBMS的I/O成本模型复杂化。一些磁盘可以在写缓存模式下运行,但这可能导致硬件故障期间的数据损坏。先进的SANs实现了大型电池支持的缓存,在某些情况下接近一兆字节,但这些系统带来了超过一百万行的微代码和相当的复杂性。复杂性带来了新的故障模式,这些问题可能非常难以检测和正确诊断。
RAID系统也因为在数据库任务上表现不佳而使数据库设计者感到沮丧。RAID是为面向数据流的存储(如UNIX文件)而设计的,而不是数据库系统使用的面向页面的存储。因此,与数据库特定的解决方案相比,RAID设备在多个物理设备上分割和复制数据时,往往表现不佳。例如,Gamma[43]的链式去lustering方案,与RAID的发明时间大致吻合,在DBMS环境中表现得更好。此外,大多数数据库提供了DBA命令来控制跨多个设备的数据分区,但是RAID设备通过将多个设备隐藏在一个界面后面来颠覆这些命令。
许多用户配置他们的RAID设备,以尽量减少空间开销("RAID级别5"),而数据库通过更简单的方案如磁盘镜像("RAID级别1")会表现得更好。RAID级别5的一个特别令人不快的特点是,写性能很差。这可能会给用户带来令人惊讶的瓶颈,而DBMS供应商往往要对这些瓶颈进行解释或提供解决方法。无论好坏,使用(和滥用)RAID设备是商业DBMS必须考虑的一个事实。因此,大多数供应商花费了大量的精力来调整他们的DBMS,使其在领先的RAID设备上运行良好。
在过去的十年中,大多数客户的部署将数据库存储分配给文件,而不是直接分配给逻辑卷或原始设备。但是大多数DBMS仍然支持原始设备访问,并且在运行大规模事务处理基准时经常使用这种存储映射。而且,尽管有上面概述的一些缺点,今天大多数企业DBMS的存储是SAN托管的。
7.4 复制服务
通过定期更新在网络上复制数据库通常是可取的。这通常是为了获得额外的可靠性:复制的数据库作为一个稍微过时的 "热备用",以防主系统发生故障。在物理上不同的地方保持热备用是有利的,可以在火灾或其他灾难发生后继续运行。复制也经常被用来为大型的、地理上分布的企业提供一种实用的分布式数据库功能的形式。大多数这样的企业将他们的数据库划分为大的地理区域(如国家或大陆),并在数据的主副本上运行所有本地更新。查询也在本地执行,但可以在来自本地操作的新鲜数据和从远程区域复制的稍微过时的数据的混合上运行。
忽略硬件技术(如EMC SRDF),使用了三种典型的复制方案,但只有第三种方案提供了高端设置所需的性能和可扩展性。当然,它也是最难实现的。
-
物理复制。最简单的方案是在每个复制期对整个数据库进行物理复制。由于运输数据的带宽和在远程站点重新安装数据的成本,这种方案不能扩展到大型数据库。此外,保证数据库的交易一致性快照是很困难的。因此,物理复制只被用作低端的客户端解决方法。大多数供应商并不鼓励通过任何软件支持来实现这一方案。
-
基于触发器的复制。在这个方案中,触发器被放置在数据库表上,这样在对表进行任何插入、删除或更新时,一个 "差异 "记录会被安装在一个特殊的复制表中。这个复制表被运送到远程站点,并在那里 "重放 "修改内容。这个方案解决了上面提到的物理复制的问题,但对某些工作负载来说,带来了不可接受的性能损失。
-
基于日志的复制。在可行的情况下,基于日志的复制是首选的复制方案。在基于日志的复制中,一个日志嗅探器进程拦截日志写入,并将其传递给远程系统。基于日志的复制使用两种广泛的技术来实现:(1)读取日志并建立SQL语句来对目标系统进行重放,或者(2)读取日志记录并将其发送到目标系统,目标系统处于永久(永久的)的 处于永久(永久的)恢复模式,在日志记录到达时进行重放。这两种机制都有价值,所以Microsoft SQL Server、DB2和Oracle都实现了这两种机制。SQL Server称第一种为日志运输,第二种为数据库镜像。
这个方案克服了之前的所有问题:它是低开销的,对运行中的系统产生最小或看不见的性能开销;它提供增量更新,因此可以随着数据库的大小和更新率的变化而优雅地扩展;它重用了DBMS的内置机制,没有大量的额外逻辑;最后,它通过日志的内置逻辑自然地提供事务性的一致复制。
大多数主要的供应商为他们自己的系统提供基于日志的复制。提供跨供应商的基于日志的复制要困难得多,因为在远程端驱动供应商的重放逻辑需要了解该供应商的日志格式。
7.5 管理员、监控和工具
每个DBMS都提供了一套用于管理其系统的实用程序。这些实用程序很少被作为基准,但往往决定了(主宰)系统的可管理性。系统的可管理性。一个在技术上具有挑战性和特别重要的功能是使这些实用程序在线运行,即在用户查询和交易正在进行时。这对于近年来由于电子商务的全球影响而变得更加普遍的24×7的操作是很重要的。传统的 "重组窗口 "在凌晨(一小会儿)的时候 凌晨时分的传统 "重组窗口 "通常已不复存在。因此,大多数供应商近年来都在提供在线服务方面投入了大量精力。我们在这里介绍一下这些工具的情况:
- 优化器统计数据的收集。每一个主要的DBMS都有一些手段来扫描表并建立这样或那样的优化器统计数据。一些统计数据,如直方图,在不占用内存的情况下一次性建立是不难的。
- 物理重组和索引构建。随着时间的推移,由于插入和删除的模式留下了未使用的空间,访问方法可能变得低效。另外,用户可能偶尔会要求在后台重组表,例如,在不同的列上重新分组(排序),或者在多个磁盘上重新分区。文件和索引的在线重组可能很棘手,因为在保持物理一致性的同时,必须避免保持任何时间的锁。在这个意义上,它与第5.4节中描述的用于索引的记录和锁定协议有一些类似之处。
- 备份/导出。所有的DBMS都支持将数据库物理转储到备份存储器的能力。同样,由于这是一个长期运行的过程,它不能天真地设置锁。相反,大多数系统执行某种 "模糊 "转储,并通过日志逻辑来确保交易的一致性。类似的方案也可以用来将数据库导出为交换格式。
- 批量加载:在许多情况下,大量的数据需要被快速带入数据库。与其一次插入每一行,供应商提供了一个为高速数据导入而优化的批量加载工具。通常,这些工具由存储管理程序中的自定义代码支持。例如,B+树的特殊批量加载代码可以比重复调用树的插入代码快得多。
- 监控、调优和资源治理器。即使是在管理环境中,查询所消耗的资源超过预期的情况也是很常见的。因此,大多数DBMS提供工具来帮助管理员识别和防止这类问题的发生。典型的做法是通过 "虚拟表 "为DBMS的性能计数器提供一个基于SQL的接口,它可以显示按查询或按锁、内存、临时存储等资源细分的系统状态。在一些系统中,还可以查询这些数据的历史日志。许多系统允许在查询超过某些性能限制时注册警报,包括运行时间、内存或锁的获取;在某些情况下,警报的触发会导致查询被中止。. 最后,像IBM的预测资源治理器这样的工具试图阻止资源密集型的查询被运行。
8. 结论
现代商业数据库系统既基于学术研究,也基于为高端客户开发工业强度产品的经验。从头开始编写和维护一个高性能、全功能的关系型DBMS的任务是对时间和精力的巨大投入。然而,关系型数据库管理系统的许多经验都可以转化为新的领域。网络服务、网络附加存储、文本和电子邮件库、通知服务和网络监视器都可以从DBMS的研究和经验中受益。数据密集型服务是当今计算的核心,而数据库系统设计的知识是一种广泛适用的技能,在主要数据库商店的大厅内外都是如此。这些新的方向也提出了一些数据库管理方面的研究问题,并为数据库社区和其他计算领域之间的新的互动指明了方向。