人人都是 DBA(VIII)SQL Server 页存储结构

原文:人人都是 DBA(VIII)SQL Server 页存储结构

当在 SQL Server 数据库中创建一张表时,会在多张系统基础表中插入所创建表的信息,用于管理该表。通过目录视图 sys.tables, sys.columns, sys.indexes 可以查看新建的表的元数据信息。

下面使用创建 Customer 表的过程作为示例。

USE [TEST]
GO

DROP TABLE [dbo].[Customer]
GO

CREATE TABLE [dbo].[Customer](
    [ID] [int] NOT NULL,
    [Name] [varchar](50) NOT NULL,
    [Address] [varchar](100) NULL,
    [Phone] [varchar](100) NULL
)
GO
select * from sys.tables where [name] = 'Customer';
select * from sys.columns where object_id = (select object_id from sys.tables where [name] = 'Customer');
select * from sys.indexes where object_id = (select object_id from sys.tables where [name] = 'Customer');

如果一张表没有聚集索引(Clustered Index),则数据本身没有结构,这样的表称为堆(Heap)。sys.indexes 中堆的 index_id 为 0,可以看到 Customer 表没有建立任何索引,所以 index_id 为 0,并且 type_desc 为 HEAP。

堆中的数据没有任何结构,当有新的数据行插入时,SQL Server 会寻找空闲的可用空间直接插入。存储数据的数据页之间也没有连接关系,不会形成一个链表。

如果为表建立上非聚集索引(Non-Clustered Index),则 index_id 从 2 开始增长。1 的位置始终留给聚集索引(Clustered Index)。 

CREATE NONCLUSTERED INDEX [IX_Customer_Name] ON [dbo].[Customer] ([Name]);

ALTER TABLE [dbo].[Customer] ADD CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED ([ID]);

SQL Server 在构成 Primary Key 约束的列上创建一个唯一索引,如果不明确指定,则默认该索引类型是唯一聚集索引。建立 Primary Key 后,可以在 sys.key_constraints 视图中查看主键信息。

select * from sys.check_constraints
select * from sys.default_constraints
select * from sys.key_constraints
select * from sys.foreign_keys

当为表建立聚集索引后,表内的数据会重构成树状结构,也就是使用 B 树形式进行存储。

SQL Server 数据库的每张表和索引都可以存储在多个分区上,sys.partitions 视图为堆或索引的每个分区包含一行数据。每个堆或索引都至少有一个分区,即使没有专门进行分区。

select * from sys.partitions where object_id = (select object_id from sys.tables where [name] = 'Customer');

SQL Server 用于描述某个分区上某张表或索引子集的术语是 hobt,代表 Heap Or B-Tree,即堆或 B 树,发音读作 "hobbit"。所以上图中可以看到存在一列为 hobt_id,使得 partition_id 和 hobt_id 形成 一对一的关系,实际上这两列总是具有相同的值。

分区上可以存储 3 种类型的数据页:

  • 行内数据页(In-Row Data Pages)
  • 行溢出页(Row-Overflow Data Pages)
  • LOB 数据页(LOB Data Pages)

一个分区下的一种数据页类型的一组页面称为分配单元(Allocation Unit),可以使用 sys.allocation_units 视图来查看。

SELECT object_name(object_id) AS [name]
    ,partition_id
    ,partition_number AS pnum
    ,rows
    ,allocation_unit_id AS au_id
    ,type_desc AS page_type_desc
    ,total_pages AS pages
FROM sys.partitions p
JOIN sys.allocation_units a ON p.partition_id = a.container_id
WHERE object_id = OBJECT_ID('dbo.Customer');

如果行的长度超过了最大值 8060 字节,那么行的数据会被存放到行溢出页中。如果行中存在 LOB 类型的字段,则会使用 LOB 数据页存储。

ALTER TABLE [dbo].[Customer] ADD [OrderDescription] varchar(8000);
ALTER TABLE [dbo].[Customer] ADD [OrderDetails] text;

对表添加聚集索引不会修改 sys.allocation_units 中的行数,但会修改 partition_id,因为创建聚集索引会导致表在系统内重建。添加非聚集索引则会至少添加一行记录以跟踪该索引。

ALTER TABLE [dbo].[Customer] ADD CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED ([ID]);
CREATE NONCLUSTERED INDEX [IX_Customer_Name] ON [dbo].[Customer] ([Name]);
SELECT convert(CHAR(8), object_name(i.object_id)) AS table_name
    ,i.NAME AS index_name
    ,i.index_id
    ,i.type_desc AS index_type
    ,partition_id
    ,partition_number AS pnum
    ,rows
    ,allocation_unit_id AS au_id
    ,a.type_desc AS page_type_desc
    ,total_pages AS pages
FROM sys.indexes i
JOIN sys.partitions p ON i.object_id = p.object_id
    AND i.index_id = p.index_id
JOIN sys.allocation_units a ON p.partition_id = a.container_id
WHERE i.object_id = object_id('dbo.Customer');

可以通过 DMV 视图查询索引状况。

SELECT *
FROM sys.dm_db_index_physical_stats(DB_ID('TEST'), OBJECT_ID(N'dbo.Customer'), NULL, NULL, 'DETAILED');

SQL Server 中的数据页具有 8KB(8192 Bytes)的固定长度,每个数据页由 3 部分组成:

  • 页头(Page Header)
  • 数据行(Data Rows)
  • 行偏移数组(Row Offset Array)

页头(Page Header)占了 96 字节长度。向 Customer 表插入条数据看看。

INSERT INTO [dbo].[Customer] (
    [ID]
    ,[Name]
    ,[Address]
    ,[Phone]
    )
VALUES (
    1
    ,'Dennis Gao'
    ,'Beijing Haidian'
    ,'18866667777'
    )
GO

使用 sys.fn_dblog 查询事务日志,获取新插入的 Page 相关信息。

SELECT [Current LSN]
    ,[Operation]
    ,[Context]
    ,[Transaction ID]
    ,[Log Record Length]
    ,[Previous LSN]
    ,[AllocUnitId]
    ,[AllocUnitName]
    ,[Page ID]
    ,[Slot ID]
    ,[Xact ID]
FROM sys.fn_dblog(NULL, NULL)

发现 LOP_INSERT_ROWS 操作对应的 Page ID 为 0001:0000008e,翻译成 10 进制为 1:142。也就是文件号为 1,页编号为 142。实际上,每个页都会通过一个 6 Bytes 的值来表示,其中包含 2 Bytes 的文件 ID 和 4 Bytes 的页编号。

由于我们只插入了 1 条数据,可以使用 sys.system_internals_allocation_units 目录视图来查看首页地址。以 sys.system_internals_ 开头的目录视图是还未公开的目录视图。sys.system_internals_allocation_units 比 sys.allocation_units 视图多 3 个字段:first_page, root_page, first_iam_page。

SELECT object_name(object_id) AS [name]
    ,rows
    ,type_desc AS page_type_desc
    ,total_pages AS pages
    ,first_page
    ,root_page
    ,first_iam_page
FROM sys.partitions p
JOIN sys.system_internals_allocation_units a ON p.partition_id = a.container_id
WHERE object_id = object_id('dbo.Customer');

这里的 first_page = 0x8E0000000100 十六进制结果可以翻译为 00 01 共 2 字节的文件号,和 00 00 00 8E 共 4 字节的页编号。与上面的 0001:0000008e 正好匹配。

不过,这么计算基本上乐趣全无了,可以使用如下的 SQL 进行直接翻译。

DECLARE @page_num BINARY (6) = 0x8E0000000100;

SELECT (convert(VARCHAR(2), (
        convert(INT, substring(@page_num, 6, 1)) * power(2, 8))
        + (convert(INT, substring(@page_num, 5, 1))))
    + ':'
    + convert(VARCHAR(11), (
        convert(INT, substring(@page_num, 4, 1)) * power(2, 24))
        + (convert(INT, substring(@page_num, 3, 1)) * power(2, 16))
        + (convert(INT, substring(@page_num, 2, 1)) * power(2, 8))
        + (convert(INT, substring(@page_num, 1, 1))
        ))) AS page_number;

另一种确认页编号的方式是使用 DBCC IND 命令,用于查找表中的所有的页。

DBCC IND([TEST], 'dbo.Customer', -1)
GO

实际上,还有一种查询页编号的方式是使用内部函数 sys.fn_PhysLocFormatter,不过很遗憾,这个函数的返回结果存在问题,不准确。

SELECT %%physloc%%, sys.fn_PhysLocFormatter (%%physloc%%) AS RID, * FROM dbo.Customer;
DBCC TRACEON(3604, -1)
GO

DBCC PAGE([TEST], 1, 142, 1)
GO

DBCC PAGE 的输出结果分成 4 个部分:BUFFER,PAGE HEADER,DATA,OFFSET TABLE。

  • BUFFER 部分显示给定页面的缓冲区信息。
  • PAGE HEADER 部分显示 Page 页头信息。
  • DATA 部分显示 Page 中存放的记录信息。
  • OFFSET TABLE 部分显示行偏移数组信息。 
PAGE: (1:142)

BUFFER:

BUF @0x000000027D0048C0

bpage = 0x0000000272292000          bhash = 0x0000000000000000          bpageno = (1:142)
bdbid = 7                           breferences = 0                     bcputicks = 0
bsampleCount = 0                    bUse1 = 17332                       bstat = 0x10b
blog = 0x15acc                      bnext = 0x0000000000000000          

PAGE HEADER:

Page @0x0000000272292000

m_pageId = (1:142)                  m_headerVersion = 1                 m_type = 1
m_typeFlagBits = 0x0                m_level = 0                         m_flagBits = 0x8000
m_objId (AllocUnitId.idObj) = 110   m_indexId (AllocUnitId.idInd) = 256
Metadata: AllocUnitId = 72057594045136896
Metadata: PartitionId = 72057594040287232                                Metadata: IndexId = 0
Metadata: ObjectId = 1045578763     m_prevPage = (0:0)                  m_nextPage = (0:0)
pminlen = 8                         m_slotCnt = 1                       m_freeCnt = 8039
m_freeData = 151                    m_reservedCnt = 0                   m_lsn = (34:369:91)
m_xactReserved = 0                  m_xdesId = (0:0)                    m_ghostRecCnt = 0
m_tornBits = 0                      DB Frag ID = 1                      

Allocation Status

GAM (1:2) = ALLOCATED               SGAM (1:3) = ALLOCATED
PFS (1:1) = 0x61 MIXED_EXT ALLOCATED  50_PCT_FULL                        DIFF (1:6) = CHANGED
ML (1:7) = NOT MIN_LOGGED           

DATA:

Slot 0, Offset 0x60, Length 55, DumpStyle BYTE

Record Type = PRIMARY_RECORD        Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
Record Size = 55
Memory Dump @0x000000000B7DA060

0000000000000000:   30000800 01000000 04000003 001d002c 00370044  0..............,.7.D
0000000000000014:   656e6e69 73204761 6f426569 6a696e67 20486169  ennis GaoBeijing Hai
0000000000000028:   6469616e 31383836 36363637 373737             dian18866667777

OFFSET TABLE:

Row - Offset
0 (0x0) - 96 (0x60)  
 字段名 
 描述 


 m_pageId 


 页在文件中的位置序号。(1:142) 表示文件 1 中第 142 页。


 m_headerVersion 


 Page Header 的定义版本。目前一直为 1。


 m_type


 页类型。

 1 - 数据页,存放堆或聚集索引的页节点数据等。

 2 - 索引页,存放聚集索引的内部节点数据,或者所有的非聚集索引数据。

 3 - Text Mix Page

 4 - Text Tree Page

 7 - Sort Page,存放排序操作的中间结果。

 8 - GAM Page

 9 - SGAM Page

 10 - IAM Page,IAM = Index Allocation Map。

 11 - PFS Page

 13 - Boot Page

 15 - File Header Page

 16 - Diff Map Page

 17 - ML Map Page

 18 - A page is deallocated by DBCC CHECKDB during a repair.

 19 - Temporary page for DBCC INDEXDEFRAG

 20 - Page pre-allocated as part of a bulk load operation.


m_level


 代表页在 B 树上的层。

 B 树中叶节点 Level 为 0,向上累加。


m_objId


 关联的对象 ID


m_indexId


 关联的索引 ID


m_prevPage


 指向前一页的指针。


m_nextPage


 指向后一页的指针。


pminlen


 行的固定长度部分的字节数。


m_slotCnt


 该页上存放的记录行数。


m_freeCnt


 页上的可以用空间字节数。


m_freeData

 该页上第一个可用空间的字节偏移位置。

m_reservedCnt

 为活动事务保留的可用空间字节数量。

m_lsn

 最后一个更改页信息的操作的 Log Sequence Number 事务日志编号。

m_xactReserved

 最后更新 m_reservedCnt 字段的事务保留的可用空间的字节数。

m_xdesId

 最近更新 m_reservedCnt 字段的事务 ID。

m_ghostRecCnt

 Ghost 记录的数量。

m_tornBits

 Page Checksum

Metadata 为前缀的字段不是 Page Header 中的内容,它们是 DBCC PAGE 的一些查询结果。

  • Metadata: AllocUnitId = 72057594045136896
  • Metadata: PartitionId = 72057594040287232
  • Metadata: IndexId = 0
  • Metadata: ObjectId = 1045578763

这与使用目录视图查询的结果一致。

SELECT convert(CHAR(8), object_name(i.object_id)) AS table_name
    ,i.object_id
    ,i.index_id
    ,partition_id
    ,partition_number AS pnum
    ,rows
    ,allocation_unit_id AS au_id
    ,total_pages AS pages
FROM sys.indexes i
JOIN sys.partitions p ON i.object_id = p.object_id
    AND i.index_id = p.index_id
JOIN sys.allocation_units a ON p.partition_id = a.container_id
WHERE i.object_id = object_id('dbo.Customer');

我们发现 m_objId 和 m_indexId 两个字段的值没有在之前的查询中出现过:

  • m_objId (AllocUnitId.idObj) = 110 
  • m_indexId (AllocUnitId.idInd) = 256 

实际上这两个字段的值是计算出来的结果,计算规则:

  • 将 m_indexId 向左移 48 位,记录为值 A;
  • 将 m_objId 向左移 16 位,记录为值 B;
  • AllocUnitId = A|B;(逻辑或)

例如,使用上面的页中的值,

  • A = 256 << 48 = 72057594037927936
  • B = 110 << 16 = 7208960
  • A|B = 72057594045136896

可以使用如下 SQL 进行计算。

SELECT 256 * CONVERT(BIGINT, POWER(2.0, 48));
SELECT 110 * CONVERT(BIGINT, POWER(2.0, 16));
SELECT 256 * CONVERT(BIGINT, POWER(2.0, 48)) | 110 * CONVERT(BIGINT, POWER(2.0, 16));

那么,反向根据 AllocUnitId 求 m_objId 和 m_indexId 的方式和上面的规则正好相反:

  • m_indexId = AllocUnitId >> 48
  • m_objId = (AllocUnitId - (m_indexId << 48)) >> 16

可以使用下面的 SQL 语句进行计算。

DECLARE @alloc BIGINT = 72057594045136896;
DECLARE @index BIGINT;

SELECT @index = CONVERT(BIGINT, CONVERT(FLOAT, @alloc) * (1 / POWER(2.0, 48))
    );

SELECT CONVERT(BIGINT, CONVERT(FLOAT, @alloc - (@index * CONVERT(BIGINT, POWER(2.0, 48)))) * (1 / POWER(2.0, 16))
    ) AS [m_objId]
    ,@index AS [m_indexId];
GO

DBCC PAGE 输出的 OFFSET TABLE 部分是一块 2 字节的条目块,其中每个条目指向着行数据页的起始位置。也就是,每一行数据都有一个 2 字节的条目在行偏移数组中。行偏移数组指向着页内数据的逻辑顺序。例如,对于一个聚集索引的表,SQL Server 会按照聚集索引的键的顺序对数据进行存储,这并不是说物理顺序就是按照键的顺序排列,而是在行偏移数组中的 slot 0 存放索引的第 1 个数据引用,slot 1 存放第 2 个数据引用,以此类推。这样,物理上数据就可以存放在任意位置了。

对于数据行存储的结构,可以用下图所示,包括存储固定长度字段数据和变长字段数据。

可以与 DBCC PAGE 输出的 DATA 数据段落进行数据对比分析,具体的内容较为复杂,这里就不详细展开了。

Slot 0, Offset 0x60, Length 55, DumpStyle BYTE

Record Type = PRIMARY_RECORD        Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
Record Size = 55
Memory Dump @0x000000000B1DA060

0000000000000000:   30000800 01000000 04000003 001d002c 00370044  0..............,.7.D
0000000000000014:   656e6e69 73204761 6f426569 6a696e67 20486169  ennis GaoBeijing Hai
0000000000000028:   6469616e 31383836 36363637 373737             dian18866667777

由于每页有固定的 8K 字节的大小。每一个数据页中都包含多条数据记录,一页中能存储多少条记录则直接依赖于记录本身的大小。由于从磁盘读取数据时也是从磁道和柱面一次读取多个数据页,让后将数据页存放到内存缓冲区,所以如果每数据页可以存放更多的数据记录,则可以直接减少物理读的次数,而通过逻辑读来提高性能。

 

《人人都是 DBA》系列文章索引:

 序号 
 名称 


1


 人人都是 DBA(I)SQL Server 体系结构


2


 人人都是 DBA(II)SQL Server 元数据


3


 人人都是 DBA(III)SQL Server 调度器


4


 人人都是 DBA(IV)SQL Server 内存管理


5


 人人都是 DBA(V)SQL Server 数据库文件


6


 人人都是 DBA(VI)SQL Server 事务日志


7


 人人都是 DBA(VII)B 树和 B+ 树


8


 人人都是 DBA(VIII)SQL Server 页存储结构


9


 人人都是 DBA(IX)服务器信息收集脚本汇编


10


 人人都是 DBA(X)资源信息收集脚本汇编


11


 人人都是 DBA(XI)I/O 信息收集脚本汇编


12


 人人都是 DBA(XII)查询信息收集脚本汇编


13


 人人都是 DBA(XIII)索引信息收集脚本汇编


14


 人人都是 DBA(XIV)存储过程信息收集脚本汇编 


15


 人人都是 DBA(XV)锁信息收集脚本汇编

本系列文章《人人都是 DBA》由 Dennis Gao 发表自博客园个人技术博客,未经作者本人同意禁止任何形式的转载,任何自动或人为的爬虫转载或抄袭行为均为耍流氓。

时间: 2024-10-22 21:13:51

人人都是 DBA(VIII)SQL Server 页存储结构的相关文章

SQL Server 2008存储结构之非聚集索引

非聚集索引与聚集索引具有相同的 B 树结构,它们之间的显著差别在于以下两点: 基础表的数据行不按非聚集键的顺序排序和存储. 非聚集索引的叶层是由索引页而不是由数据页组成. 非聚集索引既可以建在堆表结构上也可以建在聚集索引表上:非聚集索引中的每个索引行都包含非聚集键值和行定位符.此定位符指向聚集索引或堆中包含该键值的数据行. 如果表是堆则行定位器是指向行的指针.该指针由文件标识符 (ID).页码和页上的行数生成.整个指针称为行 ID (RID). 如果表包含有聚集索引,则行定位器是行的聚集索引键.

SQL Server 2008存储结构之GAM、SGAM介绍_mssql2008

当我们创建一个数据库的时候,例如以缺省的方式CREATE DATABASE TESTDB,SQLServer自动帮我们创建好如下两个数据库文件. 这两个数据文件是实实在在的操作系统文件,其中一个是叫行数据文件,用来存储数据库的各种对象,另外一个是日志文件,从来记录数据变化的过程. 从逻辑角度而言,数据库的最小存储单位为页即8kb. 数据库被分成若干逻辑页面(每个页面8KB),并且在每个文件中,所有页面都被连续地从0到x编号,其中x是由文件的大小决定的.我们可以通过指定一个数据库ID.一个文件ID

SQL Server 2008存储结构之IAM结构

索引分配映射(Index Allocation Map,IAM)页面在4  GB的区间中跟踪被一个分配单元所使用的区.一个分配单元就是一组页面,这些页面属于一个数据表或索引的单个分区.它由下面三种类型页面中的一种组成:含 有常规的行内数据的页面.含有大型对象(Large Object,LOB)数据的页面和含有行溢出数据的页面. 其实SQL  Server的数据页面类型与Oracle的段的概念有些类似,一个对象包含若干段,而一个段只能属于一个对象. 假如一张在四个分区上 的含有所有三种类型的数据(

SQL Server 基础存储结构 B-tree和Heap特性

聚集索引架构 B-tree 如图1-1 a.B-tree的结构,叶子节点为数据.数据按照聚集索引键有序排列. b.每个表只能有一个聚集索引. c.创建时如果未声明Unique,索引字段有重复值会内部添加唯一标识符(4字节)额外维护 非聚集索引架构 B-tree 如图1-2 a 索引树为B-tree,叶子节点包含索引行内容,并包含指向数据页的书签.当表为堆表时书签为RID(文件号,页号,槽号),用以指向具体数据页进行书签查找.当表为聚集表时书签为聚集索引键,用以指向具体数据页进行键查找. b 非聚

人人都是 DBA(V)SQL Server 数据库文件

原文:人人都是 DBA(V)SQL Server 数据库文件 SQL Server 数据库安装后会包含 4 个默认系统数据库:master, model, msdb, tempdb. SELECT [name] ,database_id ,suser_sname(owner_sid) AS [owner] ,create_date ,user_access_desc ,state_desc FROM sys.databases WHERE database_id <= 4; master mas

人人都是 DBA(IV)SQL Server 内存管理

原文:人人都是 DBA(IV)SQL Server 内存管理 SQL Server 的内存管理是一个庞大的主题,涉及特别多的概念和技术,例如常见的 Plan Cache.Buffer Pool.Memory Clerks 等.本文仅是管中窥豹,描述常见的内存管理相关概念. 在了解内存管理之前,通过 sys.dm_os_memory_clerks 视图可以查询内存的使用职责(Memory Clerks),也就是内存的消耗者. SELECT [type], SUM(pages_kb) AS tota

人人都是 DBA(VI)SQL Server 事务日志

原文:人人都是 DBA(VI)SQL Server 事务日志 SQL Server 的数据库引擎通过事务服务(Transaction Services)提供事务的 ACID 属性支持.ACID 属性包括: 原子性(Atomicity) 一致性(Consistency) 隔离性(Isolation) 持久性(Durability) 事务日志(Transaction Log) 事务日志(Transaction Log)存储的是对数据库所做的更改信息,让 SQL Server 有机会恢复数据库.而恢复

人人都是 DBA(X)资源信息收集脚本汇编

原文:人人都是 DBA(X)资源信息收集脚本汇编 什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA". 索引 获取数据库的 CPU 使用率 过去一段时间里 CPU 利用率的历史情况 谁用 CPU 工作的时间最长 服务器上安装了多大的 Memory SQL Server 进程用了多少 Memory 是否申请新的 Memory 无法得到 SQL Ser

人人都是 DBA(XI)I/O 信息收集脚本汇编

原文:人人都是 DBA(XI)I/O 信息收集脚本汇编 什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA". 索引 数据文件和日志文件位置和大小 查看指定数据库文件的大小和可用空间 服务器 Disk 容量和挂载信息 查看 Disk 剩余空间 查询数据库设置的 Recovery Model 查看最近的 Full Backup 信息 获取所有数据库的