T-SQL 中 varbinary 比较的性能

Performance of varbinary comparison in T-SQL

table 列中的一个是 VARBINARY 类型,需要提出查询来评估特定字节模式的行,以下方法会显着降低性能:

declare @pattern varbinary

// 19 bytes constant
set @pattern = 0x00.....

-- r.payload is of VARBINARY type as well
SELECT .... FROM ...
WHERE substring(r.payload, 0, 19) <> @pattern

对于给定的方法是否有任何替代方法?

是否可以创建一个包含前 19 个字节的附加二进制列?

如果是这样,您可以将其创建为持久计算列或 'real' 列,并创建一个 INSERT 触发器,在添加记录时填充它(以及 UPDATE 如果记录更新也会触发)。

然后您可以为新列编制索引,然后重写查询以与该列进行比较,这样您将失去昂贵的子字符串操作。

给你,使用 calculated column 存储前 19 个字节,PERSISTED 这样你就可以对其进行索引。

A demonstration can be found here on SQLFiddle.

当我执行链接测试时,持久化和索引方法大约快 5 倍。如果平均值 [Payload] 非常大,这可能会显着增加。

CREATE TABLE [dbo].[YourTable]
(
    [Id] INT CONSTRAINT [PK_YourTable] PRIMARY KEY,
    [Payload] VARBINARY(MAX),
    [Prefix] AS CAST([Payload] AS BINARY(19)) PERSISTED
);

CREATE NONCLUSTERED INDEX [IX_YourTable_Prefix] ON [YourTable]([Prefix]);

允许,

SELECT
            [Id]
    FROM
            [YourTable]
    WHERE
            [Prefix] <> @pattern 

或类似的。


我不完全清楚您要实现的目标,但使用 HASHBYTES 可能会有用。

严格来说不是替代品...更多的是增强...

在您的有效负载字段上创建列存储索引

CREATE COLUMNSTORE INDEX payloadcsindex 
    ON yourdb.yourschema.yourtable ( payload )

您最终会得到一个内存中的列存储索引扫描,它可以使现有查询的性能提高 10 倍。

听起来你想在专栏上做一个 LIKE。但是你不能,因为 LIKE 只适用于 varcharnvarchar 列。

让我们假装它是一个 nvarchar(4000) 列,您想要查找以这个 19 字节模式开头的任何内容。以 % 结尾的点赞被计划优化为中间查询。

例如考虑这个查询:

select data FROM #a
where data like N'006%'

查询计划优化器已将其变为以下情况:

Start: [tempdb].[dbo].[#a].data >= Scalar Operator(N'006'), End: [tempdb].[dbo].[#a].data < Scalar Operator(N'007')

因此我们可以将您的搜索替换为:

select data 
from my_table
where data >= @searchPattern and data < (convert(varbinary,1)+@searchPattern)

如果对 varbinary 列进行了索引,则此搜索应该得到很好的优化。

我有几个observations/suggestions:

  1. 由于 SQL 服务器优化,您可能会针对不同的查询获得类似的基准测试结果。要正确比较查询的性能,您需要避免这些优化。 但是,在您这样做之前,请确保您只在测试环境中尝试此操作。 我再说一遍,不要在生产环境中这样做!
  2. 查询前缀列肯定会提高查​​询的性能。
  3. 由于您要查询的前缀是固定长度 (19),因此您应该为该列使用 BINARY(19) 数据类型。这里的一些其他建议将创建一个 VARBINARY 列。
  4. 我个人会尽可能避免将触发器放在 table 上。请参阅 C# 示例以了解一种可能的替代方法。
  5. 在我的测试中,使用 Equality 查询比 SUBSTRING 快,后者又比 LEFT

我是如何得出这个结论的,方法是将大约 15000 个文件加载到测试数据库并执行不同类型的查询(多次),结果如下:

  1. 使用前缀列的相等性检查 -> 191 毫秒
  2. SUBSTRING -> 20448 毫秒 = 慢 107 倍
  3. 左 -> 34091 毫秒 = 慢 178 倍

这是一个 SQL 脚本来创建测试 table:

CREATE TABLE [dbo].[Files]
(
    [Id]            [int] IDENTITY(1,1) NOT NULL,
    [Path]          [varchar](260)      NOT NULL,
    [Prefix]        [binary](19)        NOT NULL,
    [AllBytes]      [varbinary](MAX)    NOT NULL,
 CONSTRAINT [PK_Files] PRIMARY KEY CLUSTERED ([Id] ASC)
)

现在查询(运行 这些在不同的选项卡中):

1.

--1.    Equality check using a prefix  column -> 191 ms
--WARNING!! Do not run this on a production server
--Clear SQL optimizations
CHECKPOINT;
GO
DBCC DROPCLEANBUFFERS;
GO

DECLARE @pattern BINARY(19) --NOTE use BINARY not VARBINARY to match the column type exactly 
SET @pattern = 0x4D5A90000300000004000000FFFF0000B80000 --Start of EXE / DLL files

SELECT  [Path]
FROM    [dbo].[Files]
WHERE   Prefix <> @pattern

2.

--2.    SUBSTRING  -> 20448 ms  = 107x slower
--WARNING!! Do not run this on a production server
--Clear Cache
CHECKPOINT;
GO
DBCC DROPCLEANBUFFERS;
GO


DECLARE @pattern BINARY(19) 
SET @pattern = 0x4D5A90000300000004000000FFFF0000B80000 --Start of EXE / DLL files
SELECT  [Path]
FROM    [dbo].[Files]
WHERE   SUBSTRING(AllBytes, 0, 19) <> @pattern 

3.

-- 3.   LEFT  ->  34091 ms   = 178x slower
--WARNING!! Do not run this on a production server
--Clear Cache
CHECKPOINT;
GO
DBCC DROPCLEANBUFFERS;
GO


DECLARE @pattern VARBINARY(19)
SET @pattern = 0x4D5A90000300000004000000FFFF0000B80000 --Start of EXE / DLL files
SELECT  [Path]
FROM    [dbo].[Files]
WHERE   LEFT(AllBytes, 19) <> @pattern

现在用一些代码来创建测试数据:

using System;
using System.Data;
using System.Data.SqlClient;
using System.IO;

namespace SO
{
    class Program
    {
        static void Main(string[] args)
        {
            //!! To keep this EXAMPLE as simple as possible I'm leaving off all error handling and taking other shortcuts I wouldn't normal advise.

            const string sqlConnectionString = @"Data Source=yourServer;Initial Catalog=databaseName;Integrated Security=True";
            const string folderToProcess = @"C:\Program Files";  //choose folder with your test files
            const int prefixLength = 19;

            using (var connection = new SqlConnection(sqlConnectionString))
            {
                connection.Open();

                foreach (var filePath in Directory.GetFiles(folderToProcess, "*.*", SearchOption.AllDirectories))
                {
                    Console.WriteLine(filePath);

                    //I'm only using ReadAllBytes here to keep this example simple. 
                    //This isn't appropriate for large files because it'll load the entire file into memory
                    byte[] allBytes = File.ReadAllBytes(filePath);

                    //To keep things simple I'm assuming we're only working with binary values that are >= prefix length
                    if (allBytes.Length < prefixLength) { continue; }

                    //This can also be accomplished using SQL 
                    byte[] prefix = new byte[prefixLength];
                    Array.Copy(allBytes, prefix, prefixLength);

                    //Rather use a stored procedure here
                    using (var command = new SqlCommand("INSERT INTO dbo.Files (Path, Prefix, AllBytes) Values(@path, @prefix, @allBytes)", connection))
                    {
                        command.Parameters.Add("@path", SqlDbType.NVarChar, 260).Value = filePath;
                        command.Parameters.Add("@prefix", SqlDbType.Binary, prefixLength).Value = prefix;             //DataType is Binary!!
                        command.Parameters.Add("@allBytes", SqlDbType.VarBinary, allBytes.Length).Value = allBytes;   //DataType is VarBinary
                        command.ExecuteNonQuery();
                    }
                }

                connection.Close();
            }

            Console.WriteLine("All Done !");
            Console.ReadLine();
        }
    }
}

如果计算列不是一个选项,这也是一个选项。

DECLARE @t TABLE (
    val VARBINARY(MAX)
)

INSERT INTO @t SELECT 0x00000100000000000000000000000000000000000000000000000000
INSERT INTO @t SELECT 0x00001000000000000000000000000000000000000000000000000000
INSERT INTO @t SELECT 0x00010000000000000000000000000000000000000000000000000000
INSERT INTO @t SELECT 0x00100000000000000000000000000000000000000000000000000000
INSERT INTO @t SELECT 0x00000f00000000000000000000000000000000000000000000000000

declare @pattern varbinary(19)
declare @pattern2 varbinary(19)
set @pattern = 0x0001
set @pattern2 = @pattern+0xFF

select @pattern,@pattern2

SELECT
    *
FROM @t
WHERE val<@pattern
OR val>@pattern2

这里提出的问题很可能在查找查询速度减慢的真正原因方面具有误导性。这里有一些很好的建议,但测试太少。我已经做了相当彻底的测试,在这里发布给那些想自己尝试的人:

http://pastebin.com/0RFUL7W5

我测试了 50k 行,而不是 OP 规定的 5k 行。测试包括 tables,包括和不包括索引的、持久的计算列,以及这两个 tables 的变体,Payload 字段为 VARBINARY(100)VARBINARY(MAX). VARBINARY(MAX) 字段测试首先测试数据类型的差异,因为初始数据集与 VARBINARY(100) tables 中的相同(本例中的数据仍在数据页面上),然后更新 MAX tables 以将数据大小推至每行 14k 字节(这种情况下的数据现在移至 lob 页面。

这些测试需要 运行 单独进行,这样一个测试就不会影响另一个测试的结果。而且每个测试应该运行多次。

运行 在我的笔记本电脑上进行的这些测试表明,在 VARBINARY(100)、<= 100 字节的 VARBINARY(MAX) 和 14k 字节的 VARBINARY(MAX) 的变体之间:

  • SUBSTRING、CONVERT 和“< @Pattern OR > @Pattern + 0xFF”的方法受数据类型和数据大小的影响很大,因此根据实际模式和数据,每个方法都有可能成为更好的选择
  • 索引持久化计算列通常优于上述 3 种方法,但绝不会超过 110 毫秒左右。

这一切意味着什么?意思是:

  • 如果不先从 OP 获取更多信息,特别是完整的 table 模式(包括所有索引)和一些样本数据,就无法回答这个问题。问题是假设的当前情况和任何可能的修复之间的时间差异是如此之小(50k 行上 110 毫秒,而 OP 只有 5k 行)以至于怀疑 VARBINARY 上的过滤是任何查询缓慢的真正原因。即使是最慢的方法,在 5000 行上也会 return 太快,甚至不会引起注意。
  • 虽然在所述场景中有一些改进性能的好建议,但如果没有适当的测试,一些看起来不错的建议可能根本不会显示任何改进。不应假设 SQL 服务器将如何反应,也不需要做出假设,因为可以测试任何情况(尽管正确构建测试可能有点棘手。

奖励课程:

在复制和粘贴问题(甚至是相关问题的答案)中的代码时,非常 小心。为什么不?因为您可能正在复制错误。一个错误,例如使用 0 作为 SQL 中 SUBSTRING 函数的起始位置。这对大多数语言来说都很好,但是 SQL 中字符串的起始索引是 1。为什么这很重要?因为你指定的起始位置1以下的字符数是从长度中扣除的。意思是,使用 SUBSTRING([Payload], 0, 19) 实际上执行 SUBSTRING([Payload], 1, 18)。同样,使用 SUBSTRING(field, -1, 19) 实际上会执行 SUBSTRING([Payload], 1, 17).

此外,问题中显示的代码还使用了 DECLARE @Pattern VARBINARY,它实际上创建了一个 VARBINARY(1) 变量,而不是我假设的 VARBINARY(30)。对于 VARCHAR / NVARCHAR / VARBINARY,默认长度在某些情况下(例如 table 列)为 30,在其他情况下(例如局部变量)为 1。

我尝试了以下方法,在我的测试中,对于 500K 行,它工作得非常快!

这里重要的是:您需要有一个非常好的聚集索引!聚簇索引应始终为: NUSE

  • N箭头尽可能多,根据它存储的字节数
  • Unique – 避免 SQL 服务器需要添加一个 "uniqueifier" 来复制键值
  • S静态 – 理想情况下,从未更新
  • Ever-increasing – 避免碎片并提高写入性能

我这样创建了 table:

CREATE TABLE [dbo].[YourTable]
(
    [Id] INT IDENTITY(1,1),
    [Payload] VARBINARY(MAX),
    Prefix AS CAST(LEFT([Payload],19) AS BINARY(19)) PERSISTED 
)

CREATE UNIQUE CLUSTERED INDEX idx1 ON dbo.YourTable(Id)
GO
CREATE NONCLUSTERED INDEX idex ON dbo.YourTable (Prefix)
GO

因此 Prefix 计算列是 BINARY(19),并且始终是前 19 个字节。 SQL 服务器必须执行的唯一操作是聚簇索引扫描。

请多多输入,否则就是教科书问题

请提供一些示例数据和 让我们知道您的申请。有时会有应用程序行为具有更广泛的解决方案。

我注意到您将其归类为 SQL Server 2008,因此您应该:

打开页面压缩, 禁用自动统计生成, 看看你 sys.dm_os_wait_stats,然后 检查高碎片。

您正在阅读哪种记录需要 <> 比较?
是否有任何东西被分配给第二个内存流来处理?
您可以进行任何更改以防止将数据写入磁盘吗?

处理这个 table 需要多长时间?