17

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

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)这样做是否有助于保持表大小更小?或者我只是想使数据库变得聪明而一无所获?

4

6 回答 6

30

保持内联。自 SQL 2005 以来,SQL Server 已经将 MAX 列存储在单独的“分配单元”中。请参阅表和索引组织。这实际上与将 MAX 列保留在其自己的表中完全相同,但没有显式这样做的任何缺点。

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

替代文字 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 分配单元的文本指针:

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'应该使用。请注意,这不会影响现有行,在我的测试中甚至不会影响 index rebuild,因此必须尽早打开该选项。

对于大多数 OLTP 类型加载,尽管 MAX 类型尽可能内联存储这一事实实际上是一个优势,因为 OLTP 访问模式是寻找并且行宽对其几乎没有影响。

尽管如此,关于原始问题:不需要单独的表格。打开该large value types out of row选项可免费获得相同的开发/测试结果。

于 2009-11-09T16:32:43.610 回答
11

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

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

话虽如此,通常您的长度组合范围很广,因此我会将其移至自己的表格中。这使您可以灵活地将此表移动到不同的文件组等。

请注意,您还可以指定它以使用sp_tableoption将数据强制移出行。varchar(max) 基本上类似于 TEXT 数据类型,它默认为行中的数据(对于 varchar(max))而不是默认为行外的数据(对于 TEXT)。

于 2009-11-09T16:09:27.270 回答
2

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

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

于 2009-11-09T16:14:23.263 回答
0

保持内联。的重点varchar是,如果它是空的,它占用 0 个字节,'Hello' 占用 4 个字节,依此类推。

于 2009-11-09T15:41:09.337 回答
0

我会通过创建 Detail 表对其进行规范化。我假设 Log 中的某些条目将具有相同的详细信息?因此,如果您对其进行规范化,如果您将文本存储在 Detail 表中,那么您将只存储 FK id INTEGER 而不是每次出现的文本。如果您有理由去规范化,请执行此操作,但从您的问题来看,我认为情况并非如此。

于 2009-11-09T15:45:53.580 回答
0

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

于 2009-11-09T21:10:13.620 回答