探秘空值位图掩码(NULL bitmap mask)

这篇文章我想谈下空值位图掩码(NULL bitmap mask) ,并揭开它的神秘面纱。空值位图掩码是在存储引擎层为列是否存储NULL值进行编码。它是一个简单的位图掩码,如果值是1的话,表示这列有NULL值,如果是0的话,表示这列有具体的值(非NULL值)。

这样的解释听起来非常直接,但当我们进一步分析时,用这个方法还是有启发性的。首先我们来看看数据行的组合结构,这是存储引擎用来在磁盘上存储记录的结构。结构如下图所示:

这个格式被称为定长变量格式(FixedVar format),因为SQL Server总是先存储定长列(像INT,CHAR),再存储变长列(像VARCHAR)。从图中我们可以看到,SQL Server以存储2 bytes的状态位开始,接着用2 bytes存储由行头到定长列结尾长度(包含所有定长列数据)。然后用2 bytes存储列个数,紧随其他的就是真正的空值位图掩码(NULL bitmap mask)

所有的一切第一眼看起来都很合理,但我们再仔细看下的话,我们就开始思考,并且你可能会问:为什么SQL Server在每条数据行里存储具体的列数?对于每条数据行,列数都不是一样的么?为什么SQL Server要存储这些冗余的数据?

第1个答案是非常简单和有逻辑性的:SQL Server需要列数来计算用作实际空值位图掩码(NULL bitmap mask)的字节数。小于等于8列的表需要1 byte,9到16列需要 2 bytes,17到24列需要3 bytes,以此类推。明白了么?但在表里每条记录的列数必须是一样的

我们来看第2个用实例分析的技术性正确答案:首先,你要知道空值位图掩码(NULL bitmap mask)是用在数据引擎级别,即当前记录中的列数。这就是说SQL Server在物理行可以存储不同数量的列。额,好像说的有点含糊不清…………物理数据行列数和表元数据层(sys.dolumns)里列数并不一致。这些也是SQL Server内部的真正不同层级。

因此在什么情况下这些层级间会彼此不相等呢?很简单:当你往表里增加列的时候!如果你加的列是NULL还是NOT NULL,SQL Server会作出完全不同的区别。当你增加一个新的NULL列到表时,SQL Server只更新表元数据层,一点也不接触到存储引擎层。也就是说当你增加一个NULL列时,所有的记录物理存储上不发生任何改变。另一方面,当你增加一个NOT NULL列时,SQL Server会更新表元数据层,同时也会更新存储引擎层,这就是说,SQL Server会接触并重写表里的每一条记录,在那里你增加了一个NOT NULL列。这会带来性能上的巨大区别!因此SQL Server需要在每条数据记录里存储具体的列数,因为这里的列数不能和表元数据层的列数同步。

我们来拿具体的例子来详细分析下。这个例子我们创建了一个简单的含8列的表,SQL Server需要使用1 bytes来作为空值位图掩码(NULL bitmap mask)

 1 CREATE TABLE TestTable
 2 (
 3     Column1 INT IDENTITY(1, 1) NOT NULL,
 4     Column2 CHAR(600) NOT NULL,
 5     Column3 CHAR(600) NOT NULL,
 6     Column4 CHAR(600) NOT NULL,
 7     Column5 CHAR(600) NOT NULL,
 8     Column6 VARCHAR(600) NOT NULL,
 9     Column7 VARCHAR(600) NOT NULL,
10     Column8 VARCHAR(600) NOT NULL
11 )
12 GO

然后,我们往表里插入2条记录:

 1 INSERT INTO TestTable VALUES
 2 (
 3     REPLICATE(‘2‘, 600),
 4     REPLICATE(‘3‘, 600),
 5     REPLICATE(‘4‘, 600),
 6     REPLICATE(‘5‘, 600),
 7     REPLICATE(‘6‘, 600),
 8     REPLICATE(‘7‘, 600),
 9     REPLICATE(‘8‘, 600)
10 ),
11 (
12     REPLICATE(‘2‘, 600),
13     REPLICATE(‘3‘, 600),
14     REPLICATE(‘4‘, 600),
15     REPLICATE(‘5‘, 600),
16     REPLICATE(‘6‘, 600),
17     REPLICATE(‘7‘, 600),
18     REPLICATE(‘8‘, 600)
19 )
20 GO

我们通过DBCC PAGE命令查看下具体的数据页:

1 DBCC IND(ALLOCATIONDB, TestTable, -1)
2 GO

1 DBCC TRACEON(3604)
2 GO
3 DBCC PAGE(ALLOCATIONDB, 1, 24993, 1)
4 GO
5
6 DBCC TRACEON(3604)
7 GO
8 DBCC PAGE(ALLOCATIONDB, 1, 24995, 1)
9 GO

可以看到,每条记录的长度是 4129 bytes(4204 bytes 数据+ 7 bytes 行开销+ 2 bytes 变长列个数 + 3 * 2 bytes 每个变长列结束位置的偏移量)。

现在我们往表里加一个新的NULL列:

1 ALTER TABLE TestTable ADD Column9 CHAR(600) NULL
2 GO

这是表里的第9列,也就是说SQL Server对于这个列数需要2 bytes。但是 SQL Server并不在存储引擎层改变物理数据行,因为我们只加了一个NULL列。SQL Server不需要在存储引擎层做任何处理。我们可以通过查看数据页来验证下:

记录还是同样4219 bytes的长度,但是我们逻辑上已经在表上加了1列。现在我们来更新表的1条记录,这样的话,新加列就有具体值了:

1 UPDATE TestTable SET Column9 = REPLICATE(‘9‘, 600)
2 WHERE Column1 = 1
3 GO

当你查看表里第2条记录的数据页时,记录大小还是原来的4219 bytes。

1 DBCC TRACEON(3604)
2 GO
3 DBCC PAGE(ALLOCATIONDB, 1, 24995, 1)
4 GO

你现在创建了一个场景:SQL Server在数据行内部存储了不同长度的空值位图掩码(NULL bitmap mask)。这就是说你有定长列的表,在存储引擎级别,却有不同长度的行大小!很有趣,是不是?

现在我们删除表并重建,继续往表里插入2条记录:

 1 DROP TABLE dbo.TestTable
 2
 3 CREATE TABLE TestTable
 4 (
 5     Column1 INT IDENTITY(1, 1) NOT NULL,
 6     Column2 CHAR(600) NOT NULL,
 7     Column3 CHAR(600) NOT NULL,
 8     Column4 CHAR(600) NOT NULL,
 9     Column5 CHAR(600) NOT NULL,
10     Column6 VARCHAR(600) NOT NULL,
11     Column7 VARCHAR(600) NOT NULL,
12     Column8 VARCHAR(600) NOT NULL
13 )
14 GO
15
16 INSERT INTO TestTable VALUES
17 (
18     REPLICATE(‘2‘, 600),
19     REPLICATE(‘3‘, 600),
20     REPLICATE(‘4‘, 600),
21     REPLICATE(‘5‘, 600),
22     REPLICATE(‘6‘, 600),
23     REPLICATE(‘7‘, 600),
24     REPLICATE(‘8‘, 600)
25 ),
26 (
27     REPLICATE(‘2‘, 600),
28     REPLICATE(‘3‘, 600),
29     REPLICATE(‘4‘, 600),
30     REPLICATE(‘5‘, 600),
31     REPLICATE(‘6‘, 600),
32     REPLICATE(‘7‘, 600),
33     REPLICATE(‘8‘, 600)
34 )
35 GO

现在我们往表里增加一个NOT NULL列:

1 ALTER TABLE TestTable ADD Column9 CHAR(600) NOT NULL
2 DEFAULT REPLICATE(‘9‘, 600)
3 GO

现在,SQL Server需要在存储引擎层改变每条记录,因为新列的默认值必须被增加(当你表里已经有记录存储时,新加列必须要定义一个默认值),而且SQL Server需要扩展空值位图掩码(NULL bitmap mask)

 1 DBCC IND(ALLOCATIONDB, TestTable, -1)
 2 GO
 3
 4 DBCC TRACEON(3604)
 5 GO
 6 DBCC PAGE(ALLOCATIONDB, 1, 24993, 1)
 7 GO
 8
 9 DBCC TRACEON(3604)
10 GO
11 DBCC PAGE(ALLOCATIONDB, 1, 24995, 1)
12 GO

当你处理大表,给表增加NOT NULL列时,这个现象会导致严重的性能问题。想象下我们往表里插入100万条记录。当我们增加NULL列时,SQL Server只需要几毫秒,因为只进行元数据修改操作。但当我们往表里增加NOT NULL列时,SQL Server待ALTER TABLE操作完成需要花费40秒!在处理大表,往表里增加NOT NULL列,这的确是个非常严重的性能降级!!

希望你现在已经理解了为什么SQL Server在存储引擎层对每条记录存储具体的列数,还有在SQL Server里,当你往大表里增加NOT NULL列,会出现严重的性能问题。

时间: 2024-11-25 07:05:26

探秘空值位图掩码(NULL bitmap mask)的相关文章

位图索引(Bitmap Index)——索引共用

位图索引区别于传统B*树索引有两个结构特点:其一是叶子节点上是一个可能的索引列取值对应一个叶子节点.另一个就是叶子节点上通过一个位图向量表示对应行是否取定这个索引值. 使用位图向量记录对应行的取值情况不仅可以带来存储空间上的节省,而且可以借助计算机位图运算的快速特性来提高索引结果利用率.下面我们通过模拟情况来进行分析. Bitmap Index模拟说明 假设存在数据表T,有两个数据列A和B,取值如下. 序号 A B 1 L 1 2 T 2 3 L 2 4 M 1 对两个数据列A.B分别建立位图索

SQL Server :理解数据记录结构

在SQL Server :理解数据页结构我们提到每条记录都有7 bytes的系统行开销,那这个7 bytes行开销到底是一个什么样的结构,我们一起来看下. 数据记录存储我们具体的数据,换句话说,它存在堆表里,或者存在聚集索引的叶子节点.数据记录结构是为了让SQL Server更高效的管理数据.我们来看下数据记录结构示意图: 上图中蓝色部分是所有数据记录部分,绿色部分是表结构里取决于定长/变长列的数据记录部分. 行头系统数据: 用做状态位1的第1字节(8位)是用来定义记录的属性: 第0位:版本信息

[Android开源]一个非常简单易用用来花式展示二维码样式生成的库QRCodeStyle

类库说明 一个非常简单易用用来花式展示二维码样式生成的库 自由组合二维码样式 使用范例 设置带圆边圈的logo Bitmap logo = BitmapFactory.decodeResource(getResources(), R.mipmap.logo); ImageView logo_iv = (ImageView) findViewById(R.id.logo_circle_space_iv); Bitmap targetBitmap = QRCodeStyle.Builder.buil

bitmap位图法

位图法定义 位图法就是bitmap的缩写,所谓bitmap,是用每一位来存放某种状态,适用于大规模数据,但数据状态又不是很多的情况.通常是用来判断某个数据存不存在的. 例如,要判断一千万个人的状态,每个人只有两种状态:男人,女人,可以用0,1表示.那么就可以开一个int数组,一个int有32个位,就可以表示32个人.操作的时候可以使用位操作. 本文地址:http://www.cnblogs.com/archimedes/p/bitmap.html,转载请注明源地址. 数据结构 unsigned

Android探秘:SimpleAdapter与Bitmap的结合

首先我们知道,在Android中,Adapter本身是一个接口,他 派生了很多子接口,这些子接口又被很多具体的类实现,来实现具体的显示效果.本次我们主要介绍的是SimpleAdapter实现类. SimpleAdapter类:实际上SimpleAdapter并不简单,而且他的功能非常强大,可以将List集合的多个对象封装成列表项. 这就是我们经常需要用到的功能.例如:我们在手机上显示东西的时候,每一个Item都会有 id.title.description.image之类的attribute,显

位图索引

     位图索引主要针对大量相同值的列而创建的索引.(例如:性别), 位图索引相对于传统的B*树索引,在叶子节点上采用了完全不同的结构组织方式.传统B*树索引将每一行记录保存为一个叶子节点,上面记录对应的索引列取值和行rowid信息.而位图索引将每个可能的索引取值组织为一个叶子节点.每个位图索引的叶子节点上,记录着索引键值.该索引键值的起始截止rowid和一个位图向量串.从本质上将,位图索引通过一个bit位来记录一个数据行是否存在对应键值.这种方式存储数据,相对于B*Tree索引,占用的空间非

如何将Icon转成Bitmap

个最近工作中有个需求是将Icon转成带Alpha通道的Bitmap, 虽然网上有不少这方面的文章,但很多都是错的, 这里记录下,或许对后来人有用. 要实现这个功能,我们首先需要理解Icon的格式,我们可以看到Icon的结构如下: typedef struct _ICONINFO {    BOOL fIcon;    DWORD xHotspot;    DWORD yHotspot;    HBITMAP hbmMask;    HBITMAP hbmColor;} ICONINFO;type

如何将Icon转成Bitmap(对ICON的内部格式讲的比较清楚)

最近工作中有个需求是将Icon转成带Alpha通道的Bitmap, 虽然网上有不少这方面的文章,但很多都是错的, 这里记录下,或许对后来人有用. 要实现这个功能,我们首先需要理解Icon的格式,我们可以看到Icon的结构如下: typedef struct _ICONINFO {    BOOL fIcon;    DWORD xHotspot;    DWORD yHotspot;    HBITMAP hbmMask;    HBITMAP hbmColor;} ICONINFO;typed

数据库使用-oracle位图索引

我们目前大量使用的索引一般主要是B*Tree索引,在索引结构中存储着键值和键值的RowID,并且是一一对应的.而位图索引主要针对大量相同值的列而创建(例如:类别,操作员,部门ID,库房ID等),索引块的一个索引行中存储键值和起止Rowid,以及这些键值的位置编码,位置编码中的每一位表示键值对应的数据行的有无.一个块可能指向的是几十甚至成百上千行数据的位置.这种方式存储数据,相对于B*Tree索引,占用的空间非常小,创建和使用非常快. 位图索引的目标是为用户提供指向包含特定键值(key value