【问题标题】:SQL Server Index performance - long columnSQL Server 索引性能 - 长列
【发布时间】:2010-11-08 12:16:33
【问题描述】:

在 SQL Server (2005+) 中,我需要为 nvarchar(2000+) 的列(仅完全匹配)建立索引。解决此问题的最可扩展、最高效的方法是什么?

在 SQL Server (2005+) 中,对具有以下类型的列进行索引的实际区别是什么:

  • nvarchar(2000)
  • char(40)
  • binary(16)

例如查找索引的binary(16) 列会比查找索引的nvarchar(2000) 快得多吗?如果有,多少钱?

在某些方面显然越小越好,但我对 SQL Server 如何优化其索引以了解它如何处理长度还不够熟悉。

【问题讨论】:

  • 您需要搜索还是强制执行唯一性?
  • @Alex 我需要强制唯一性,但只会进行完全匹配。
  • 另一个想法是将你的 nvarchar 压缩成一个较小的二进制值,并在其上建立索引,但你能保证每个值总是压缩到 900 字节或更少吗?

标签: sql sql-server performance indexing


【解决方案1】:

当然二进制(16)会快得多 - 只需进行最快的计算:

  • SQL Server 页面始终为 8K
  • 如果每个条目有 16 个字节,则可以在一个页面上存储 500 个条目
  • 每个条目有 4000 个字节 (nvarchar),您最终会得到每页 2 个条目(最坏的情况,如果您的 NVARCHAR(2000) 已完全填充)

如果您有一个包含 100'000 个条目的表,您将需要 200 页用于使用 binary(16) 键的索引,而对于使用 nvarchar(2000) 的同一索引,您将需要 50'000 页)

即使只是增加了用于读取和扫描所有这些页面的 I/O 也会扼杀您可能拥有的任何性能........

马克

更新:
对于我常用的索引,我尽量避免使用复合索引 - 从其他表中引用它们会变得相当混乱(带有多个相等比较的 WHERE 子句)。

此外,定期检查和维护您的索引 - 如果碎片超过 30%,请重建 - 如果碎片有 5-30%,请重新组织。在http://sqlfool.com/2009/06/index-defrag-script-v30/ 上查看一个经过良好测试的自动数据库索引维护脚本

对于 SQL Server 表上的聚集键,尽量避免使用 GUID,因为它们本质上是随机的,因此可能会导致大量索引碎片,从而损害性能。此外,虽然不是硬性要求,但请尝试确保您的群集键是唯一的 - 如果不是,SQL Server 将为其添加一个四字节的唯一符。此外,聚集键被添加到每个非聚集索引中的每个条目中 - 因此在聚集键中,拥有一个小的、唯一的、稳定的(不变的)列(最好是不断增加的)非常重要,为您提供最佳特性和性能 --> INT IDENTITY 是完美的)。

【讨论】:

  • 除了纯空间考虑之外还有什么?如果其他几个列与索引一起存储,那么您的页面数比较不会那么激烈,还有什么其他差异?
【解决方案2】:

每个索引条目最多可以有 900 个字节,因此您的 nvarchar(2000) 不会飞。最大的区别是索引深度——从根页到叶页要遍历的页数。所以,如果你需要搜索,你可以在 CHECKSUM 上做索引,像这样:

alter table recipe add text_checksum as checksum(recipe_text)
create index text_checksum_ind on recipe(text_checksum)

(此处的示例Indexes on Computed Columns: Speed Up Queries, Add Business Rules) 这不会给你一个精确的匹配,只会很好地缩小你的搜索范围。

当然,如果您需要强制唯一性,则必须使用触发器。

另一个想法是将你的 nvarchar 压缩到一个较小的二进制值,并在其上建立索引,但你能保证每个值总是压缩到 900 字节或更少吗?

【讨论】:

  • +1 好点,是的 - 900 字节是索引条目的最大值。
  • 您需要比 32 位校验和更大的散列。 CHECKSUM 返回 int,在 最佳 的情况下,只有 64k 条记录后,它会有 50% 的概率发生冲突,这是一个非常非常小的表。 rusanu.com/2009/05/29/…
  • Remus,使用更大的散列,你得到误报的机会就会更少,但你仍然会有一些。仅在这种情况下触发。
  • 是的,如果您决定使用触发器强制执行它,那么快速、小哈希就可以了,因为无论如何您都将“手动”解决冲突。另一方面,足够大的散列允许您仅依靠机会并且不允许重复(如果冲突不太可能发生,即使在中间相遇),然后您可以依靠索引唯一性,效率更高比触发。当然,这是一种权衡,正确的道路取决于具体情况。
【解决方案3】:

你从错误的方向思考这个问题:

  • 创建满足性能目标所需的索引
  • 不要创建不需要的索引

列是binary(16) 还是nvarchar(2000) 在那里没有什么区别,因为您不会随便添加索引。

不要让索引选择决定您的列类型。如果您需要为 nvarchar(2000) 建立索引,请考虑使用全文索引或为列添加哈希值并为其编制索引。


根据您的更新,我可能会使用 HashBytes() 函数创建校验和列或计算列并为其编制索引。请注意,校验和与加密哈希不同,因此您更有可能发生冲突,但您也可以匹配文本的全部内容,它将首先使用索引进行过滤。 HashBytes() 发生冲突的可能性较小,但仍有可能发生,因此您仍然需要比较实际列。 HashBytes 计算每个查询和每个更改的哈希的成本也更高。

【讨论】:

  • 实际上,这是我问这个问题的原因之一 - 一个大字段的短二进制哈希会更好地索引吗?
  • 哈希列只能寻找完全匹配。如果您不需要部分匹配(LIKE 'foo%')或范围(BETWEEN 'A' AND 'B'),那么您可以使用哈希。
  • 好的:现在我们正在研究一个不同的问题:“我需要索引一个 nvarchar(2000) 列。目标是使这种类型的查询运行得更快:______。我应该怎么做那个?”
  • @Joel 谢谢,我已经把问题的范围缩小到了。
  • 如果(加密)哈希结果足够大,那么发生冲突的机会就非常低,您不需要比较实际的列值。我怀疑 128 位足以满足几乎所有目的:您需要插入 2^64 个值(相当无法实现的数据量)才能有相当大的几率观察到一对值发生冲突。如果您出于某种原因不相信这一点 - 只需使用更长的哈希;随着比特数的增加,冲突的概率呈指数下降。 Sha512 很常见,也不算太慢...
【解决方案4】:

In index max length is 900 bytes anyway,因此您无法索引 NVARCHAR(2000)。

更大的索引键意味着更少的键适合索引页面,因此它会创建更大的树,使用更多的磁盘,更多的 I/O,更多的缓冲区拉取,更少的缓存。对于聚集键,这要糟糕得多,因为聚集键值被用作所有其他非聚集索引的查找值,因此它增加了所有索引的大小。

最终,查询中最普遍的性能驱动指标是扫描/搜索的页面数。这转化为物理读取(=I/O 等待时间)或逻辑读取(=缓存污染)。

除了空间考虑之外,数据类型对查询行为几乎没有影响。 char/varchar/nchar/nvarchar 有比较需要考虑的排序规则,但排序规则查找的成本通常不是决定因素。

最后但同样重要的是,可能是最重要的因素是您的应用程序访问模式。索引使查询 SARGable 的列,必须维护优化器不使用的索引绝对没有好处。

有时你必须考虑并发问题,比如你必须消除deadlocks caused by distinct update access path to the same record

编辑后更新

使用持久化的 MD5 哈希列:

create table foo (
    bar nvarchar(2000) not null, 
    [hash] as hashbytes('MD5', bar) persisted not null,
    constraint pk_hash unique ([hash]));
go


insert into foo (bar) values (N'Some text');
insert into foo (bar) values (N'Other text');
go

select * from foo
    where [hash] = hashbytes('MD5', N'Some text');
go

你必须非常小心你的搜索,哈希值会因输入的任何差异而大不相同,即。如果您寻求 Ascii 参数而不是 Unicode 参数...

如果您的桌子变大,您将拥有decent collision chance

【讨论】:

    【解决方案5】:

    实际上,最好自己进行基准测试并查看。例如,以下脚本比较了通过 4 字节整数的索引搜索与通过 50 字节字符的搜索。对于一个 int(建立在 INT 列上的 B 树的深度)读取 3 次,而对于一个 char 读取 4 次(建立在 CHAR 列上的 B 树的深度)。

    CREATE TABLE dbo.NarrowKey(n INT NOT NULL PRIMARY KEY, m INT NOT NULL)
    GO
    DECLARE @i INT;
    SET @i = 1;
    INSERT INTO dbo.NarrowKey(n,m) SELECT 1,1;
    WHILE @i<1024000 BEGIN
      INSERT INTO dbo.NarrowKey(n,m)
        SELECT n + @i, n + @i FROM dbo.NarrowKey;
      SET @i = @i * 2;
    END;
    GO
    DROP TABLE dbo.WideKey
    GO
    CREATE TABLE dbo.WideKey(n CHAR(50) NOT NULL PRIMARY KEY, m INT NOT NULL)
    GO
    DECLARE @i INT;
    SET @i = 1;
    INSERT INTO dbo.WideKey(n,m) SELECT '1',1;
    WHILE @i<1024000 BEGIN
      INSERT INTO dbo.WideKey(n,m)
        SELECT CAST((m + @i) AS CHAR(50)), n + @i FROM dbo.WideKey;
      SET @i = @i * 2;
    END;
    GO
    SET STATISTICS IO ON;
    SET STATISTICS TIME ON;
    GO
    SELECT * FROM dbo.NarrowKey WHERE n=123456
    SELECT * FROM dbo.WideKey WHERE n='123456'
    

    对于更宽的键,索引查找速度要慢 33%,但表要大 4 倍:

    EXEC sp_spaceused 'dbo.NarrowKey';
    -- 32K
    EXEC sp_spaceused 'dbo.WideKey';
    -- 136K
    

    【讨论】:

      猜你喜欢
      • 2010-12-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-20
      • 1970-01-01
      • 2013-04-25
      • 2021-04-19
      • 2011-06-25
      相关资源
      最近更新 更多