这个问题有一些挑战。SQL Server中的索引可以很有效地执行以下操作,每个索引只需读取几个逻辑即可:
- 检查是否存在一行
- 检查行是否不存在
- 查找从某一点开始的下一行
- 找到上一点开始的行
但是,它们不能用于在索引中找到第N行。这样做需要滚动存储为表的索引或扫描索引的前N行。您的C#代码在很大程度上取决于您可以有效地找到数组的第N个元素的事实,但是您不能在此处这样做。我认为,如果不更改数据模型,该算法不适用于T-SQL。
第二个挑战涉及对BINARY
数据类型的限制。据我所知,您无法以通常的方式执行加,减或除。您可以将转换BINARY(64)
为BIGINT
,并且不会引发转换错误,但是行为未定义:
在任何版本的SQL Server之间,任何数据类型和二进制数据类型之间的转换都不能保证相同。
另外,缺少转换错误在这里有些问题。您可以转换大于最大可能BIGINT
值的任何值,但会给您错误的结果。
的确,您现在拥有的值大于9223372036836854775807。但是,如果您始终从1开始并搜索最小值,那么除非表中的行数超过9223372036854775807,否则这些大值将不相关。这似乎不太可能,因为此时的表大约为2000艾字节,因此,为了回答您的问题,我将假定不需要搜索非常大的值。我还将进行数据类型转换,因为它们似乎不可避免。
对于测试数据,我将等效的5000万个连续整数插入表中,再插入5000万个整数,每20个值之间有一个单一值差距。我还插入了一个不适合带符号的单个值BIGINT
:
CREATE TABLE dbo.BINARY_PROBLEMS (
KeyCol BINARY(64) NOT NULL
);
INSERT INTO dbo.BINARY_PROBLEMS WITH (TABLOCK)
SELECT CAST(SUM(OFFSET) OVER (ORDER BY (SELECT NULL) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BINARY(64))
FROM
(
SELECT 1 + CASE WHEN t.RN > 50000000 THEN
CASE WHEN ABS(CHECKSUM(NewId()) % 20) = 10 THEN 1 ELSE 0 END
ELSE 0 END OFFSET
FROM
(
SELECT TOP (100000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
) t
) tt
OPTION (MAXDOP 1);
CREATE UNIQUE CLUSTERED INDEX CI_BINARY_PROBLEMS ON dbo.BINARY_PROBLEMS (KeyCol);
-- add a value too large for BIGINT
INSERT INTO dbo.BINARY_PROBLEMS
SELECT CAST(0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000 AS BINARY(64));
该代码花了几分钟在我的机器上运行。我使表的前半部分没有任何间隙来代表某种性能下降的情况。我用来解决问题的代码按顺序扫描索引,因此如果表中的第一个间隙早于索引,索引将很快完成。在开始之前,让我们验证数据是否正确:
SELECT TOP (2) KeyColBigInt
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
FROM dbo.BINARY_PROBLEMS
) t
ORDER By KeyCol DESC;
结果表明,我们转换为的最大值BIGINT
为102500672:
╔══════════════════════╗
║ KeyColBigInt ║
╠══════════════════════╣
║ -9223372036854775808 ║
║ 102500672 ║
╚══════════════════════╝
有1亿行的值符合预期的BIGINT:
SELECT COUNT(*)
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF;
解决此问题的一种方法是按顺序扫描索引,并在行的值与预期ROW_NUMBER()
值不匹配时立即退出。无需扫描整个表即可获得第一行:只需扫描直到第一个间隔的行。这是一种编写可能获得该查询计划的代码的方法:
SELECT TOP (1) KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF
) t
WHERE KeyColBigInt <> RN
ORDER BY KeyCol;
由于不适合该答案的原因,该查询通常由SQL Server串行运行,并且SQL Server通常会低估找到第一个匹配项之前需要扫描的行数。在我的机器上,SQL Server在找到第一个匹配项之前从索引扫描50000022行。该查询需要11秒钟才能运行。请注意,这将返回经过间隙的第一个值。目前尚不清楚您确切需要哪一行,但是您应该能够更改查询以适合您的需求,而不会带来很多麻烦。该计划如下所示:
我唯一的其他想法是欺负SQL Server使用并行性进行查询。我有四个CPU,因此我将数据分成四个范围,并在这些范围内进行搜索。将为每个CPU分配一个范围。为了计算范围,我只是获取了最大值,并假设数据是均匀分布的。如果您想变得更聪明,可以查看样本统计直方图以获取列值,并以此方式建立范围。下面的代码依赖于许多未记录的不安全生产的技巧,包括跟踪标志8649:
SELECT TOP 1 ca.KeyCol
FROM (
SELECT 1 bucket_min_value, 25625168 bucket_max_value
UNION ALL
SELECT 25625169, 51250336
UNION ALL
SELECT 51250337, 76875504
UNION ALL
SELECT 76875505, 102500672
) buckets
CROSS APPLY (
SELECT TOP 1 t.KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, buckets.bucket_min_value - 1 + ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol >= CAST(buckets.bucket_min_value AS BINARY(64)) AND KeyCol <= CAST(buckets.bucket_max_value AS BINARY(64))
) t
WHERE t.KeyColBigInt <> t.RN
ORDER BY t.KeyCol
) ca
ORDER BY ca.KeyCol
OPTION (QUERYTRACEON 8649);
这是并行嵌套循环模式的样子:
总体而言,查询将比以前执行更多的工作,因为它将扫描表中的更多行。但是,它现在可以在我的桌面上运行7秒钟。它可以在真实服务器上更好地并行化。这是实际计划的链接。
我真的想不出解决此问题的好方法。在SQL之外进行计算或更改数据模型可能是最好的选择。
delete
在表上放置一个触发器,该触发器会将现在可用的二进制文件转储到单独的表(例如create table available_for_reuse(id binary64)
)中,尤其是考虑到需要非常频繁地执行此查找?