根据特定公式找到最小的缺失元素


8

我需要能够从具有数千万行的表中找到丢失的元素,并且具有一BINARY(64)列的主键(这是要从中计算出的输入值)。这些值主要是按顺序插入的,但有时我想重复使用先前删除的值。用IsDeleted列修改已删除的记录是不可行的,因为有时会插入一行,该行的行数比当前现有行的数百万。这意味着样本数据将类似于:

KeyCol : BINARY(64)
0x..000000000001
0x..000000000002
0x..FFFFFFFFFFFF

因此,在0x000000000002和之间插入所有缺少的值0xFFFFFFFFFFFF是不可行的,所用的时间和空间将是不希望的。本质上,当我运行算法时,我希望它返回0x000000000003,这是第一个开始。

我想出了一个用C#进行二进制搜索的算法,该算法将查询数据库中position处的每个值i,并测试是否期望该值。对于上下文,我的算法很糟糕:https : //codereview.stackexchange.com/questions/174498/binary-search-for-a-missing-or-default-value-by-a-given-formula

例如,此算法将在具有100,000,000个项目的表上运行26-27个SQL查询。(这似乎不是很多,但它的将要发生的非常频繁。)目前,这个表中有大约5000万行,并且表现越来越明显

我的第一个替代想法是将其转换为存储过程,但这有其自身的障碍。(我必须编写一个BINARY(64) + BINARY(64)算法,以及许多其他东西。)这将很痛苦,但并非不可行。我也考虑过基于实施翻译算法ROW_NUMBER,但是对此我感到很不好。(BIGINT对于这些值,A 几乎不够大。)

我想提出其他建议,因为我真的需要尽快。值得的是,C#查询选择的唯一列是KeyCol,其他与该部分无关。


同样,就其价值而言,当前获取适当记录的查询大致如下:

SELECT [KeyCol]
  FROM [Table]
  ORDER BY [KeyCol] ASC
  OFFSET <VALUE> ROWS FETCH FIRST 1 ROWS ONLY

<VALUE>算法提供的索引在哪里。我也没有这个BIGINT问题OFFSET,但是我会的。(现在只有5000万行意味着它永远不会要求该值以上的索引,但是在某个时候它将超过该BIGINT范围。)

一些其他数据:

  • 从删除来看,gap:sequential比率约为1:20;
  • 表中的最后35,000行的值> BIGINT的最大值;

寻找更多说明... 1)为什么需要“最小”可用二进制文件而不是任何可用二进制文件?2)向前看,是否有机会delete在表上放置一个触发器,该触发器会将现在可用的二进制文件转储到单独的表(例如create table available_for_reuse(id binary64))中,尤其是考虑到需要非常频繁地执行此查找?
markp-fuso

@markp最小的可用值具有“首选项”,将其视为类似于URL缩短器,您不希望下一个更长的值,因为有人可以手动指定类似的mynameisebrown内容mynameisebrowo,即您将得到,不会,如果abc有的话。
Der Kommissar'9

查询select t1.keycol+1 as aa from t as t1 where not exists (select 1 from t as t2 where t2.keycol = t1.keycol+1) order by keycol fetch first 1 rows only会给您带来什么?
Lennart's

@Lennart不是我所需要的。不得不使用SELECT TOP 1 ([T1].[KeyCol] + 1) AS [AA] FROM [SearchTestTableProper] AS [T1] WHERE NOT EXISTS (SELECT 1 FROM [SearchTestTableProper] AS [T2] WHERE [T2].[KeyCol] = [T1].[KeyCol] + 1) ORDER BY [KeyCol],它总是返回1
Der Kommissar'9

我想知道这是否是某种类型的转换错误,它不应返回1。从... return中选择t1.keycol是什么?
Lennart

Answers:


6

总结,乔已经在我花了一个小时才输入的大部分内容上很出色:

  • 非常值得怀疑的是,每个KeyCol值都会用尽< bigintmax(9.2e18),因此bigint只要将搜索范围限制为KeyCol <= 0x00..007FFFFFFFFFFFFFFF
  • 我想不出一个可以一直“高效”找到差距的查询。您可能会很幸运,并且在搜索开始前就发现了差距,或者您可以付出高昂的代价才能找到差距,这是您进行搜索的相当方法
  • 当我简短地考虑如何并行化查询时,我很快就放弃了这个想法(作为DBA,我不想发现您的进程经常使我的数据服务器的CPU利用率达到100%...尤其是如果您有多个同时运行的副本);noooo ...并行化将成为不可能

那么该怎么办?

让我们暂时搁置(重复的,CPU密集型的,蛮力的)搜索想法,然后看一看大图。

  • 平均而言,此搜索的一个实例将需要扫描数百万个索引键(并且需要大量的cpu,数据库高速缓存的崩溃以及用户正在观察旋转的沙漏),以查找单个值
  • 将cpu-usage / cache-thrashing / spinning-hour-glass乘以...,您一天预计要进行多少次搜索?
  • 请记住,一般而言,此搜索的每个实例都需要扫描同一组(数百万个)索引键;如此多次的好处,很多重复的活动

我想提出的是对数据模型的一些补充...

  • 一个新表,该表跟踪一组“可用” KeyCol值,例如:available_for_use(KeyCol binary(64) not null primary key)
  • 您可以决定在此表中维护多少条记录,例如,足以满足一个月的活动量?
  • 该表可以定期(每周一次)用新的一批KeyCol值“填充” (也许创建一个“填充”存储过程?)[例如,更新Joe的select/top/row_number()查询以执行top 100000]
  • 您可以设置一个监视过程,以跟踪可用条目的数量,available_for_use 以防万一您开始用尽所有值
  • > main_table <上的新的(或修改的)DELETE触发器,每当从主表中删除一行时,该触发器就会将已删除的KeyCol值放入新表available_for_use
  • 如果您允许更新KeyCol列,则> main_table <上的新的/修改的UPDATE触发器也可以使我们的新表保持available_for_use更新
  • 当需要“搜索”新KeyCol值时select min(KeyCol) from available_for_use(显然,这还有很多,因为a)您将需要为并发问题编写代码-不想让您的过程的2个副本抓住相同的值,min(KeyCol)并且b)您'需要min(KeyCol)从表中删除;这应该相对容易编码,也许可以作为存储的proc,并在必要时可以在另一个问答中解决)
  • 在最坏的情况下,如果您的select min(KeyCol)进程找不到可用的行,则可以启动“自上而下”的过程以生成一批新的行

对数据模型提出了以下建议的更改:

  • 你消除LOT过多的CPU周期的[你的DBA会感谢你]
  • 您消除了所有这些重复索引扫描并消除了缓存混乱[您的DBA会感谢您]
  • 您的用户不再需要观看旋转的沙漏了(尽管他们可能不喜欢失去借口离开办公桌的借口)
  • 有很多方法可以监视available_for_use表的大小,以确保您永远不会用完新值

是的,建议的available_for_use表只是预先生成的“下一个键”值的表;是的,在获取“下一个”值时可能会发生某些争用,但是任何争用a)都可以通过适当的表/索引/查询设计轻松解决,并且b)与开销/当前的重复搜索,蛮力搜索,索引搜索的想法有所延迟。


这实际上与我最终在聊天中所想的相似,我想大概每15-20分钟运行一次,因为Joe的查询运行相对较快(在人为拥有测试数据的实时服务器上,最坏的情况是4.5s,最好的情况是0.25秒),我可以拉入一天的密钥,并且不少于n密钥(可能是10或20,以强制它搜索可能更低,更理想的值)。虽然真的很欣赏这里的答案,但您还是把想法写下来了!:)
Der Kommissar

嗯,如果您有一个应用程序/中间件服务器可以提供可用KeyCol值的中间高速缓存...是的,它也可以工作:-)并且显然消除了数据模型更改的必要性eh
markp-fuso

确切地说,我正在考虑甚至在Web应用程序本身上构建静态缓存,唯一的问题是它是分布式的(因此我需要在服务器之间同步缓存),这意味着SQL或中间件实现会很多首选。:)
Der Kommissar

hmmmm ...分布式KeyCol管理,而且需要代码为潜在的PK违反如果应用程序尝试的2(或以上)的并发情况下使用相同的KeyCol价值...... ......一个单一的中间件服务器或肯定更容易以数据库为中心的解决方案
markp-fuso

8

这个问题有一些挑战。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之外进行计算或更改数据模型可能是最好的选择。


即使最好的答案是“这在SQL中不能很好地工作”,至少它告诉我下一步要去哪里。:)
Der Kommissar

1

这是一个可能对您不起作用的答案,但是我还是会添加它。

即使BINARY(64)可以枚举,也很难确定项目的后继项目。由于BIGINT对于您的域来说似乎太小,您可以考虑使用DECIMAL(38,0),它似乎是SQL服务器中最大的NUMBER类型。

CREATE TABLE SearchTestTableProper
( keycol decimal(38,0) not null primary key );

INSERT INTO SearchTestTableProper (keycol)
VALUES (1),(2),(3),(12);

找到第一个缺口很容易,因为我们可以构建所需的数字:

select top 1 t1.keycol+1 
from SearchTestTableProper t1 
where not exists (
    select 1 
    from SearchTestTableProper t2 
    where t2.keycol = t1.keycol + 1
)
order by t1.keycol;

pk索引上的嵌套循环联接应足以找到第一个可用项目。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.