nvarchar(max) 转换为 varchar 和表优化

Kei*_*ard 7 sql-server-2008 database-design sql-server optimization

我正在使用一个表,该表的所有字符类型都设置为nvarchar其中一些是nvarchar(max). 我们正在将所有这些转换为varchar并根据生产中的实际使用指定字符宽度。对于任何给定的列,生产数据使用 2 个字符到 900 个字符的实际使用宽度范围。我们将在适用时添加 10% 的填充。

-- Insert statements for procedure here
UPDATE Listings WITH (ROWLOCK) 
SET [SubType] = 'S' 
WHERE @idSettings = idSettings AND
    (@idRetsClass = 0 OR idRetsClass = @idRetsClass)
    AND (@idRetsSetting = 0 OR idRetsSetting = @idRetsSetting)
    AND IsNew = 1 AND ([SubType] LIKE '%Single Family Home%' OR [SubType] LIKE '%Modular%' OR [SubType] LIKE '%Mobile Home%' 
    OR [SubType] LIKE '% Story%' OR [SubType] = '' OR [SubType] = 'residential - S' OR [SubType] = '1 House on Lot' OR [SubType] = '2 Houses on Lot' 
    OR [SubType] = 'Detached' OR [SubType] LIKE '%single family%' OR [SubType] = 'ranch' OR [SubType] = 'Semi-Detached' OR [SubType] = 'single' OR [SubType] = 'one family' OR [SubType] = 'Residential' 
    OR [SubType] = 'Ranch Type' OR [SubType] = '2 or More Stories' OR [SubType] = 'Cape Cod' OR [SubType] = 'Split Level' OR [SubType] = 'Bi-Level' OR [SubType] = 'Detached Single' 
    OR [SubType] = 'Single-Family Homes' OR [SubType] = 'house' OR [SubType] = 'detached housing'  OR [SubType] = 'det')
Run Code Online (Sandbox Code Playgroud)

对这个表进行大修,它包含 140 ( nvarchar) 列,其中 11 列是 MAX。我删除了 30 个索引,然后重新创建它们。

我的问题是在什么情况下是varchar(max)首选?

仅当您希望拥有 4k 或更多字符时?

这样做时我应该学习和准备什么?

当影响聚集键的聚集索引更新必须更新所有非聚集索引时,这会提高性能吗?

我们有更新程序超时,它们使用了 75% 到 95% 的查询执行计划显示计划的聚集索引更新。

链接到实际执行计划

Sol*_*zky 9

{ 这可能有点冗长,但您的实际问题无法通过查看执行计划来解决。有两个主要问题,都是架构问题。}

分心

让我们从不是您的主要问题领域的项目开始。这些是应该考虑的事情,因为它绝对有助于提高性能以使用您需要的数据类型,而不仅仅是通用的、最适合的数据类型。存在不同数据类型有一个很好的理由,如果在其中存储 100 个字符NVARCHAR(MAX)对查询(或系统的任何其他方面)没有负面影响,那么所有内容都将存储为NVARCHAR(MAX). 但是,清理这些区域不会带来真正的可扩展性。

到 MAX,或不到 MAX

我正在使用一个表,该表的所有字符类型都设置为nvarchar其中一些是nvarchar(max).

好的。这不一定是件坏事,尽管大多数情况下至少有一个数字类型的字段作为 ID。但是,到目前为止所描述的场景肯定存在有效的案例。并且MAX字段本身并没有什么不好,因为如果数据可以容纳在那里,它们会将数据存储在数据页(即行中)上。在这种情况下,它的性能应该与相同数据类型的非 MAX 值一样好。但是,是的,一堆MAX类型字段是数据建模草率的标志,并且更有可能将大部分(或全部)MAX数据存储在需要额外查找的单独数据页(即行外)中,因此更少高效的。

VARCHAR 与 NVARCHAR

我们正在将所有这些转换为varchar...

好的,但究竟是为什么(是的,我知道此声明后面的信息和评论会增加清晰度,但我会出于某种原因保留对话方面)。每个数据类型都有它的位置。VARCHAR是每个字符 1 个字节,可以表示单个代码页上定义的 256 个字符(大多数情况下)。虽然代码页之间的字符值 0 - 127 相同,但 128 和 255 之间的字符值可以更改:

;WITH chars ([SampleCharacters]) AS
(
  SELECT CHAR(42) + ' '   -- *
       + CHAR(65) + ' '   -- A
       + CHAR(126) + ' '  -- 
   -------------------------------
       + CHAR(128) + ' '  -- €
       + CHAR(149) + ' '  -- •
       + CHAR(165) + ' '  -- ¥, Y, ?
       + CHAR(183) + ' '  -- ·, ?
       + CHAR(229) + ' '  -- å, a, ?
)
SELECT chr.SampleCharacters COLLATE SQL_Latin1_General_CP1_CI_AS AS [SQL_Latin1_General_CP1_CI_AS],
       chr.SampleCharacters COLLATE SQL_Latin1_General_CP1255_CI_AS AS [SQL_Latin1_General_CP1255_CI_AS],
       chr.SampleCharacters COLLATE Thai_CI_AS_KS_WS AS [Thai_CI_AS_KS_WS],
       chr.SampleCharacters COLLATE Yakut_100_CS_AS_KS AS [Yakut_100_CS_AS_KS],
       chr.SampleCharacters COLLATE Albanian_CS_AI AS [Albanian_CS_AI]
FROM   chars chr;
Run Code Online (Sandbox Code Playgroud)

请注意,VARCHAR数据可能每个字符占用 2 个字节并表示超过 256 个字符。有关双字节字符集的更多信息,请参阅以下答案:在表中存储日语字符

NVARCHAR存储为 UTF-16(Little Endian),每个字符 2 或 4 个字节,可以代表完整的 Unicode 范围。因此,如果您的数据需要存储比单个代码页可以表示的字符更多的字符,那么切换到VARCHAR将不会真正帮助您。

在转换为 之前VARCHAR,您需要确保没有存储任何 Unicode 字符。尝试以下查询以查看是否有任何行在VARCHAR不丢失数据的情况下无法转换为:

SELECT tbl.PKfield, tbl.SubType
FROM   dbo.[Listings] tbl
WHERE  tbl.SubType <> CONVERT(NVARCHAR(MAX), CONVERT(VARCHAR(MAX), tbl.SubType))
Run Code Online (Sandbox Code Playgroud)

阐明NVARCHAR工作原理:NVARCHAR字段的最大长度是2 字节字符的数量。因此,NVARCHAR(50), 将允许最多 100 个字节。这 100 个字节能容纳多少字符取决于有多少个 4 字节字符:none 将允许您容纳所有 50 个字符,所有 4 字节字符只能容纳 25 个字符,以及它们之间的许多组合。

关于VARCHARvs占用空间的另一件事要考虑NVARCHAR:从 SQL Server 2008(仅限企业版和开发者版!)开始,您可以对表、索引和索引视图启用行或页压缩。对于NVARCHAR字段中的大部分数据实际上可以容纳VARCHAR而不会丢失任何数据的情况,压缩将允许将适合的字符VARCHAR存储为 1 个字节。只有需要 2 或 4 个字节的字符才会占用该空间。这应该消除人们经常选择坚持的更大原因之一VARCHAR。有关压缩的更多信息,请参阅创建压缩表和索引的 MSDN 页面。请注意,数据在MAX 行外存储的数据类型不可压缩。

真正关注的领域

如果您希望此表真正具有可扩展性,则应解决以下方面的问题。

数字问题

...并根据生产中的实际使用情况指定字符宽度。对于任何给定的列,生产数据使用 2 个字符到 900 个字符的实际使用宽度范围。我们将在适用时添加 10% 的填充。

呃,什么?你把所有这些值加起来了吗?鉴于MAX您有多少个字段,其中 1 个或多个字段可能有 900 个字符,即使这应该等于 1800 个字节,存储在主数据页上的值也只有 24 个字节(并不总是 24 作为大小)因多种因素而异)。这可能就是为什么有这么多MAX字段的原因:它们不能放入另一个字段NVARCHAR(100)(最多占用 200 个字节),但它们确实有 24 个字节的空间。

如果目标是提高性能,那么将完整字符串转换为代码在某些层面上是朝着正确方向迈出的一步。您正在大幅减少每一行的大小,这对于缓冲池和磁盘 I/O 来说更有效。更短的字符串需要更少的时间来比较。这很好,但不是很棒。

如果目标是显着提高性能,那么转换为代码是朝着正确方向迈出的错误步骤。它仍然依赖于基于字符串的扫描(30个指数和140列,应该有大量的扫描,除非大多数字段不用于过滤),我认为这些将是区分敏感者扫描在那,这比区分大小写或二进制(即使用区分大小写或二进制排序规则)的效率低。

此外,转换为基于字符串的代码最终忽略了如何正确优化事务系统的重点。这些代码会被输入到搜索表单中吗?让人们使用'S'for[SubType]远没有搜索 on 有意义'Single Family'

有一种方法可以保留完整的描述性文本,同时减少使用的空间并大大加快查询速度:创建查找表。您应该有一个名为的表[SubType],该表清楚地存储每个描述性术语,并且[SubTypeID]每个都有一个。如果数据系统(即的一部分enum),那么[SubTypeID]现场应该不会是一个IDENTITY字段作为数据应通过释放脚本得到填充。如果这些值是由最终用户输入的,则该[SubTypeID]字段为 IDENTITY。在这两种情况下:

  • [SubTypeID] 是主键。
  • 最有可能INT用于[SubTypeID].
  • 如果数据是内部/系统数据,并且您知道不同值的最大数量将始终低于 40k,那么您可以使用SMALLINT. 如果您从 1 开始编号(手动或通过 IDENTITY 种子),那么您最多可以获得 32,768。但是,如果您从最低值 -32,768 开始,那么您将获得完整的 65,535 个值以供使用。
  • 如果您使用的是企业版,则启用行或页面压缩
  • 可以调用描述性文本字段[SubType](与表名相同),或者[SubTypeDescription]
  • UNIQUE INDEX[SubTypeDescription]。请记住,索引的最大大小为 900字节。如果 Production 中此数据的最大长度为 900 个字符,并且您确实需要NVARCHAR,那么这可能会在启用压缩的情况下工作,或者VARCHAR仅在您绝对不需要存储 Unicode 字符时才使用,否则通过AFTER INSERT, UPDATE触发器强制唯一性。
  • [Listings]表有[SubTypeID]字段。
  • [SubTypeID][Listings]表中的字段是外键,引用[SubType].[SubTypeID].
  • 查询现在可以JOIN[SubType][Listings]表上搜索全文[SubTypeDescription](不区分大小写,甚至,与当前功能相同),同时使用该 ID 对[Listings].

这种方法可以(并且应该)应用于此表(和其他表)中行为类似的其他字段。

问题数

对这个表进行了大修,它包含 140 (nvarchar) 列,其中 11 列是 MAX。我删除了 30 个索引,然后重新创建它们。

如果这是一个事务系统而不是数据仓库,那么我会说(同样,通常),140 列太多而无法有效处理。我非常怀疑所有 140 个字段是否同时使用和/或具有相同的用例。MAX如果它们需要包含超过 4000 个字符,则 11是无关紧要的。但是,在事务表上拥有 30 个索引又有点笨拙(如您所见)。

表格需要包含所有 140 个字段是否存在技术原因?这些领域可以分成几个更小的组吗?考虑以下:

  • 找到“核心”(最重要/最常用)字段并将它们放入“主”表中,命名[Listing](我更喜欢使用单数词,以便 ID 字段可以很容易地只是TableName + "ID")。
  • [Listing] 表有这个PK: [ListingID] INT IDENTITY(1, 1) NOT NULL CONSTRAINT [PK_Listing] PRIMARY KEY
  • “次要”表被命名为[Listing{GroupName}](例如[ListingPropertyAttribute]——“属性”如:NumberOfBedrooms、NumberOfBathrooms 等)。
  • [ListingPropertyAttribute] 表有这个PK: [ListingID] INT NOT NULL CONSTRAINT [PK_ListingPropertyAttribute] PRIMARY KEY, CONSTRAINT [FK_ListingPropertyAttribute_Listing] FOREIGN KEY REFERENCES([Listing].[ListingID])
    • 注意IDENTITY这里没有
    • 注意“核心”和“次要”表之间的PK名称相同
    • 注意PK和FK到“core”表是同一个字段
  • “核心”[Listing]表同时获取[CreatedDate][LastModifiedDate]字段
  • “次要”表只获取[LastModifiedDate]字段。假设是所有辅助表的行与“核心”表同时填充(即所有行应始终在所有“辅助”表中表示)。因此,[CreatedDate]“核心”[Listing]表中的值在所有“辅助”表中始终相同,以每行为基础,因此无需在“辅助”表中复制它。但是它们每个都可以在不同的时间更新。

这种结构确实增加了许多查询所需的 JOIN 数量,但为了编码方便,可以创建一个或多个视图来封装更常用的 JOIN。但从好的方面来说:

  • 当涉及到 DML 语句时,争用应该少得多,因为“核心”表应该获得大部分更新。
  • 大多数更新将花费更少的时间,因为它们正在修改较小的行。
  • 每个新表(“核心”和“辅助”表)的索引维护应该更快,至少在每个表的基础上。

回顾

当前模型的设计效率低下,而且似乎正在实现该设计目标(即速度很慢)。如果您希望系统运行速度快,那么数据模型需要设计为高效的,而不仅仅是低效的。