我应该使用内联varchar(max)列还是将其存储在单独的表中?

时间:2009-11-09 15:38:03

标签: sql-server normalization varchar

我想在MS SQL Server 2005中创建一个表来记录某些系统操作的详细信息。从下面的表格设计中可以看出,除Details之外的每一列都是不可为空的。

CREATE TABLE [Log]
(
[LogID] [int] IDENTITY(1,1) NOT NULL,
[ActionID] [int] NOT NULL,
[SystemID] [int] NOT NULL,
[UserID] [int] NOT NULL,
[LoggedOn] [datetime] NOT NULL,
[Details] [varchar](max) NULL
)

因为Details列并不总是包含数据。将此列存储在单独的表中并提供指向它的链接是否更有效?

CREATE TABLE [Log]
(
[LogID] [int] IDENTITY(1,1) NOT NULL,
[ActionID] [int] NOT NULL,
[SystemID] [int] NOT NULL,
[UserID] [int] NOT NULL,
[LoggedOn] [datetime] NOT NULL,
[DetailID] [int] NULL
)       

CREATE TABLE [Detail]
(
[DetailID] [int] IDENTITY(1,1) NOT NULL,
[Details] [varchar](max) NOT NULL
)

对于较小的数据类型,我不会真正考虑它,但对于varchar(max)这样做有助于保持表格大小更小?或者我只想尝试智能数据库而不实现任何目标?

6 个答案:

答案 0 :(得分:27)

保持内联。自SQL 2005以来,SQL Server已将MAX列存储在单独的“分配单元”中。请参阅Table and Index Organization。这实际上与将MAX列保留在自己的表中完全相同,但没有明确这样做的任何缺点。

拥有一个显式表实际上是(因为外键约束)和消耗更多空间(因为DetaiID重复)。更不用说它需要更多代码,并且......编写代码会引入错误。

alt text http://i.msdn.microsoft.com/ms189051.3be61595-d405-4b30-9794-755842d7db7e(en-us,SQL.100).gif

<强>更新

要检查数据的实际位置,可以使用简单的测试显示:

use tempdb;
go

create table a (
  id int identity(1,1) not null primary key,
  v_a varchar(8000),
  nv_a nvarchar(4000),
  m_a varchar(max),
  nm_a nvarchar(max),
  t text,
  nt ntext);
go

insert into a (v_a, nv_a, m_a, nm_a, t, nt)
values ('v_a', N'nv_a', 'm_a', N'nm_a', 't', N'nt');
go

select %%physloc%%,* from a
go

%%physloc%%伪列将显示该行的实际物理位置,在我的情况下是第200页:

dbcc traceon(3604)
dbcc page(2,1, 200, 3)

Slot 0 Column 2 Offset 0x19 Length 3 Length (physical) 3
v_a = v_a                            
Slot 0 Column 3 Offset 0x1c Length 8 Length (physical) 8
nv_a = nv_a                          
m_a = [BLOB Inline Data] Slot 0 Column 4 Offset 0x24 Length 3 Length (physical) 3
m_a = 0x6d5f61                       
nm_a = [BLOB Inline Data] Slot 0 Column 5 Offset 0x27 Length 8 Length (physical) 8
nm_a = 0x6e006d005f006100            
t = [Textpointer] Slot 0 Column 6 Offset 0x2f Length 16 Length (physical) 16
TextTimeStamp = 131137536            RowId = (1:182:0)                    
nt = [Textpointer] Slot 0 Column 7 Offset 0x3f Length 16 Length (physical) 16
TextTimeStamp = 131203072            RowId = (1:182:1)   

所有列值但TEXT和NTEXT都是内联存储的,包括MAX类型 更改表选项并插入新行(sp_tableoption不影响现有行)后,MAX类型被逐出到自己的存储中:

sp_tableoption 'a' , 'large value types out of row', '1';
insert into a (v_a, nv_a, m_a, nm_a, t, nt)
values ('2v_a', N'2nv_a', '2m_a', N'2nm_a', '2t', N'2nt');    
dbcc page(2,1, 200, 3);

注意m_a和nm_a列现在是LOB分配单元的Textpointer:

Slot 1 Column 2 Offset 0x19 Length 4 Length (physical) 4
v_a = 2v_a                           
Slot 1 Column 3 Offset 0x1d Length 10 Length (physical) 10
nv_a = 2nv_a                         
m_a = [Textpointer] Slot 1 Column 4 Offset 0x27 Length 16 Length (physical) 16
TextTimeStamp = 131268608            RowId = (1:182:2)                    
nm_a = [Textpointer] Slot 1 Column 5 Offset 0x37 Length 16 Length (physical) 16
TextTimeStamp = 131334144            RowId = (1:182:3)                    
t = [Textpointer] Slot 1 Column 6 Offset 0x47 Length 16 Length (physical) 16
TextTimeStamp = 131399680            RowId = (1:182:4)                    
nt = [Textpointer] Slot 1 Column 7 Offset 0x57 Length 16 Length (physical) 16
TextTimeStamp = 131465216            RowId = (1:182:5)                    

为了完成起见,我们还可以强制其中一个非最大字段不在行中:

update a set v_a = replicate('X', 8000);
dbcc page(2,1, 200, 3);

请注意v_a列如何存储在Row-Overflow存储中:

Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4
v_a = [BLOB Inline Root] Slot 0 Column 2 Offset 0x19 Length 24 Length (physical) 24
Level = 0                            Unused = 99                          UpdateSeq = 1
TimeStamp = 1098383360               
Link 0
Size = 8000                          RowId = (1:176:0) 

因此,正如其他人已经评论过的那样,MAX类型默认存储在内联中,如果它们适合的话。对于许多DW项目而言,这是不可接受的,因为典型的DW加载必须扫描或至少扫描范围,因此应使用sp_tableoption ..., 'large value types out of row', '1'。请注意,这不会影响现有行,在我的测试中甚至不在索引重建上,因此必须尽早启用该选项。

对于大多数OLTP类型的加载,尽管如果可能的话,MAX类型以内联方式存储这一事实实际上是一个优点,因为要搜索OLTP访问模式并且行宽对它几乎没有影响。

尽管如此,关于原始问题:没有必要使用单独的表格。启用large value types out of row选项可以免费获得开发/测试,从而获得相同的结果。

答案 1 :(得分:10)

矛盾的是,如果你的数据通常少于8000个字符,我会将它存储在一个单独的表中,而如果数据大于8000个字符,我会将它保存在同一个表中。

这是因为如果SQL允许行位于单页中,SQL Server会将数据保留在页面中,但是当数据变大时,它会像TEXT数据类型一样将其移出,只留下一个指针在行中。因此,对于一堆3000个字符的行,每页的行数较少,这实际上是低效的,但对于一堆12000个字符的行,数据不在行中,因此它实际上更有效。

话虽如此,通常你会有各种各样的长度混合,因此我会把它移到自己的桌子上。这使您可以灵活地将此表移动到其他文件组等。

请注意,can also specify it to force the data out of the row使用sp_tableoption。 varchar(max)基本上类似于TEXT数据类型,它默认为行中的数据(对于varchar(max)),而不是默认为行外的数据(对于TEXT)。

答案 2 :(得分:2)

您应该将数据结构化为最合乎逻辑的结构,并允许SQL Server对如何物理存储数据执行优化。

如果通过性能分析发现您的结构存在性能问题,请考虑对结构或存储设置进行更改。

答案 3 :(得分:0)

保持内联。 varchar的全部意义在于,如果它为空则占用0个字节,为“Hello”占用4个字节,依此类推。

答案 4 :(得分:0)

我会通过创建Detail表来规范化它。我假设Log中的一些条目具有相同的细节?因此,如果将其标准化,则只有在将详细信息存储在详细信息表上时,才会存储FK id INTEGER而不是每次出现的文本。如果你有理由去标准化那么做,但从你的问题我不认为是这种情况。

答案 5 :(得分:0)

拥有一个可为空的列,每16个就需要2个字节。如果这是表中唯一(或第17或第33等)可空列,则每行将花费2个字节,否则无效。