上篇文章《SQL语句处理》介绍了数据库执行的四个阶段,每个阶段都做了什么工作。今天这篇文章继续探讨数据库是如何获取到用户所需的数据。
如下所探讨的内容仅适用于Oracle单机环境,RAC环境稍有不同,不在本篇讨论范围内。
数据库的所有操作增、删、改、查都是在内存中完成的,为了处理数据,数据库首先需要将数据从磁盘读取到内存中,然后进行相应的操作。但是内存和磁盘的读写速度有着天壤之别。DDR4内存读写速度大概50G每秒(50000M),固态硬盘速度是300M每秒,是内存的二百分之一,机械硬盘的速度是100M每秒,是内存的五百分之一。为了解决两者之间的速度差的问题,诞生了缓存的概念。缓存的作用就是避免每次获取数据时都从缓慢的磁盘读取,而是将之前访问过的数据缓存在内存中,后续操作如果需要相同数据时,直接从内存获取,大大提升读取速度。
Oracle内存结构中有一个很重要的结构叫做DB Buffer Cache,DB Buffer Cache位于SGA中,正常系统中此部分内存占整个Oracle内存结构大绝大部分。如果DB Buffer Cache很大,几十G,甚至是几百G,每次在其中搜索所需的块是否存在,也需要很长的时间,因此,为了提高效率,Oracle将DB Buffer Cache划分为多个区域,每个区域称为一个工作集(workset),每个工作集又被划分为多个hash buckets,一个hash buckets管理着一个或多个数据块。为了保证数据的一致性,hash buckets访问是串行的,由相应的latch保护,只有获取到相应的latch的会话才能到hash buckets上搜索数据块。
1、查询语句的执行
假设有如下查询语句:
SELECT * FROM employees WHERE employee_id=199;
表employees表结构如下,employee_id是表的主键。
先看下上述sql语句的执行计划
首先根据索引EMP_EMP_ID_PK查找employee_id=199键,获取到键所对应的值ROWID,ROWID指向数据块的真实地址。通过ROWID获取数据块,获取到数据块后,再从数据块中获取符合条件的数据。在此过程中至少需要访问两个数据块(为了简化过程,忽略其他数据块),一个是键为199索引块,假设为100号块;另一个是索引块所指向的数据块,假设为1000号块。
前面缓存介绍中提到,为了提升数据的访问速度,Oracle会将访问过的数据块缓存到DB Buffer cache中,以备重复使用。因此上面获取100号索引块的时候,需要先到DB Buffer cache中查找100号索引块是否已存在,以减少昂贵的磁盘读。在搜索DB Buffer cache前,必须获取相应的latch后才能进行搜索。如果未获取到latch,此时会进入等待。待获取到latch后便可以对DB Buffer cache搜索。
如果100号索引块已存在于内存中,且没有会话正在修改数据块内容,直接访问内存中的数据块,获取键为199的数据。如果有会话正在修改此数据块内容,则需要通过undo数据和当前的数据块构造一个一致性读版本的数据块,来读取会话修改前的历史版本数据。
如果100号索引块不在内存中,首先判断是否有其他会话正在将100号索引块读入内存,如果是,则等待。否则通知后台进程将100号索引块从磁盘读入内存,在将100号索引块读入内存前,需要在内存中找到一块合适的空闲块,以便保存即将读入的100号索引块。搜索空闲块时同样需要获得latch。
如果内存中没有空闲的空间,则通知后台刷新进程刷新脏页,以腾出空闲空间。
如果内存中有空闲的空间,直接将100号索引块读入内存。获取到ROWID后,再去读取数据块,读取数据块的过程同读取索引块,读取到数据块后再从数据块中获取符合条件的行,返回给客户端。
2、DML语句执行
假设有如下更新语句:
UPDATE employees SET first_name='Hello',last_name='Kitty' WHERE employee_id=199;
update语句执行计划如下:
DML语句都存在相应的查询过程,因为只有查询到了所需要修改的数据,才能进行相应的操作。查询的过程大致与查询语句执行过程相同。但也有如下不同点:
获取latch时,如果是只读操作,多个会话可以同时获取相同的latch,如果是DML操作,则不可以同时获得。
只读操作多个会话可以同时读取同一个数据块,DML操作则不可以多个会话同时修改同一个数据块。
获取到数据块后进行相应的操作,并记录对应的undo信息和redo log信息,完成update操作。