无需索引即可快速搜索

问题



当某人带着真正不可能的要求来到我们身边时,我们所有人都在工作,而要实现这一要求则需要奇迹。我的案例发生在一位营销同事向我提出一个看似简单的问题时:几年前我是否可以从某个表中获取某个月的数据。您不能说错,但我隐约记得桌子很大。该表具有带有创建时间的datetime字段,但是此字段上有索引吗?



当然,他们也想快速获取数据。像往常一样,我说:“我会看看我能做些什么。”,然后仔细研究了正在讨论的表格。祝您好运永远不会离开我们,索引确实不存在,并且表格很大。没问题,我们可以扫描桌子,对吧?错误。如果我从使用数据库的那几年中学到了什么,那就重要了。一个包含数亿条记录(由几个整数列组成)的表将非常强大。然后添加各种varchar和datetime列。现在这是一个真正的挑战,不是吗?



实际上,我正在查看的表有十亿条记录。一个简单的表扫描可能很容易花费一天的时间,因此我也需要处理提取的数据。另外,扫描这种大小的表可能不像乍看起来那样有利于系统的一般状态。应该通过将所有扫描的页面从磁盘中提取到sql服务器内存中。反过来,这将从缓存中卸载数据页,这些数据页可用于当前的操作任务。当服务器从磁盘重新读取数据时,当前用户的请求可能会等待更长的时间,而不是快速重新使用内存中的数据页。性能可能会下降,在最坏的情况下,服务器可能会完全屈服。从而,扫描表格时,应该使用一种特殊的技术-小批量运行它,将当前位置和中间结果保留在某个位置。这种方法允许服务器重新配置并喘口气,而不会影响性能。



因此,当我想到创建时间的datetime值与表标识符相关联时,我考虑了场景并估计了启动和执行脚本所需的时间。标识符(ID)是一列不断增加的值和基于它的主键。按标识符获取时,创建日期和时间值自然会按照与标识符值相同的方式进行预排序。等等,我想,我可以实现比表扫描快得多的东西,在最坏的情况下,它只需30步,而不是500,000,000!二进制搜索!



搜索



对于像我一样早就完成学业的每个人,理论上的再训练都应该按顺序进行。二进制搜索是一种在排序数组中(在我们的情况下是在表列中)搜索值的简单但极其有效的方法。数组必须是预排序且有限的。通过将数组的中间元素与目标进行比较来完成搜索。如果它们相等,则搜索结束。如果不是,则删除明显缺少所需元素的那一半,然后重复上一步,直到只剩下一个。找到中间,与目标进行比较,如果目标不相等,则再去除一半,依此类推。在最坏情况下找到目标所需的总步数为log(N),其中N是元素数。将此与N / 2进行比较当您仅遍历数组时。一般而言,这将是N,但是如果我们可以猜测目标是否接近终点,我们可以返回,从而减少所需步骤的总数。



在我的情况下,N = 1,000,000,000,这就是我得出上述两个数字的方式:30和500,000,000。该ID将充当数组枚举器的角色,而创建日期时间将是该数组元素的值。尽管有一个警告。就其定义而言,数组枚举器是连续的整数顺序序列。由于删除记录或重新填充标识符,表标识符中很容易出现空格。通过将标识符的范围除以2来简单地定义中间,您不应期望会有这样的标识符的记录。而不是直接搜索,我不得不使用top()函数。像这样:



Select top(1) * from Table where id <= @id order by id desc


我使用“ <=”和“ desc”是因为我想找到一个等于或紧邻目标值的值。假设@l和@r分别是左边界和右边界,ID 是中间的,@dt是创建日期时间, tdt 目标是 IDr实表标识符(ID),算法可能如下所示:




while @l <@r

begin

    --  

    @id = @l +floor((@r-@l)/2)

    --    

    select top(1) @idr = id, @dt = creation_datetime from Table where id <= @id order by id desc

    --  

    if(@dt<@tdt)

        @l = @id +1

    else

        @r = @id

end 


最后发现 IDr将是条目的标识符,后跟所需的条目。该算法具有一种“左”偏移,即倾向于选择所有值中的最左值。由于我希望记录的值尽可能地接近目标,因此我还检查了表中最接近的左右邻居,看看其中一个是否更好。请注意,我没有将表中的真实ID用于迭代过程,因为在这种情况下,ID值中有空格,并且在某些情况下,该算法可能会陷入无限循环。



我花了大约一个小时来编写和测试脚本。使用它,我找到了具有特定创建日期和时间的第一条记录。之后,我使用了一个简单的select语句,其中包含两个条件的where子句:id> = @和creation_datetime> = @ dt1和creation_datetime <@ dt2。我只需要确保优化程序会选择使用主键而不是索引或表扫描。总而言之,我不到2小时就做到了!再次令人惊讶地发现,这些算法不是深深地埋在sql服务器内部的东西,而是可以在日常工作中轻松使用的东西。



下面是我编写的整个脚本。请查看内部评论以了解如何使用它。




/* 
         
      ,     . 
     ,   datetime  3 
*/
--  ,  ,       
declare @debug bit = 0
-- @Table -  ,     .
declare @Table varchar(128) = 'TableX' 
-- @Id -    (id)   
declare @ID varchar(128) = 'Id' 
-- @DateTimeColumn -   datetime      
declare @DateTimeColumn varchar(128) = 'created_dt'
--      
declare @TargetDateTime datetime = '2020-01-03 18:23:03'
declare @Sql varchar(max)
set @sql = '
--    
declare @debug bit = <debug>
--    
declare @tdt datetime = ''<TargetDateTime>''
--        ( ) 
declare @id bigint 
--       
declare @l bigint, @r bigint
--  ,        , datetime    
declare @dt datetime, @idr bigint
--   «»  «» 
select @l = min(<ID>), @r = max(<ID>) from <Table>
while @l < @r
begin
    --  
    set @id = @l +floor((@r-@l)/2)
    --     
    select top(1) @dt = <DateTimeColumn>, @idr = <ID> from <Table> where <ID> <= @id order by <ID> desc
    --   ,   
    if( @debug = 1 )
    begin
        select ''@id'' = @id, ''@l'' = @l, ''@r'' = @r, ''@dt'' = @dt, ''@idr'' = @idr
    end
    if(@dt < @tdt)
        set @l = @id +1
    else
        set @r = @id
end
-- ,       
declare @t table(id bigint, dt datetime, dlt float)
insert @t(id, dt, dlt)
select top(1) <ID>, <DateTimeColumn>, abs(cast(<DateTimeColumn> as float) -cast(@tdt as float)) 
  from <Table> 
 where <ID> < @idr 
order by <ID> desc
insert @t(id, dt, dlt) 
select top(1) <ID>, <DateTimeColumn>, abs(cast(<DateTimeColumn> as float) -cast(@tdt as float)) 
  from <Table> 
 where <ID> > @idr 
order by <ID>
insert @t(id, dt, dlt) 
select @idr, @dt, abs(cast(@dt as float) -cast(@tdt as float))
select top(1) @dt = dt, @idr = id from @t order by dlt, id 
select ''Found Value'' = @dt, ''Record Id'' = @idr
'
set @sql = replace(
            replace(
             replace(
              replace(
               replace(@sql, '<ID>', @ID), 
              '<Table>', @Table), 
             '<TargetDateTime>', convert(varchar(50), @TargetDateTime, 121)),
            '<debug>', @debug),
           '<DateTimeColumn>', @DateTimeColumn)
exec (@sql)



All Articles