用于前缀搜索的最佳DB查询

时间:2012-06-15 15:16:14

标签: mysql algorithm mongodb tree prefix

我有一个数据集,它是一个前缀范围列表,前缀的大小不一样。以下是一些例子:

low: 54661601   high: 54661679   "bin": a
low: 526219100  high: 526219199  "bin": b
low: 4305870404 high: 4305870404 "bin": c

我想查找哪个“bin”对应于具有相应前缀的特定值。例如,值5466160179125211将对应于“bin”a。在重叠(其中很少)的情况下,我们可以返回最长的前缀或所有前缀。

最佳算法显然是可以插入bin对象的某种树,其中树的每个连续级别代表越来越多的前缀。

问题是:我们如何在数据库中实现这个(在一个查询中)?允许更改/添加数据集。什么是最好的数据&这个查询设计?使用mongo或MySQL的答案最好。

4 个答案:

答案 0 :(得分:4)

如果您对前缀范围中的重叠次数做出温和的假设,则可以使用MongoDB或MySQL以最佳方式执行您想要的操作。在下面的回答中,我将用MongoDB进行说明,但是将这个答案移植到MySQL应该很容易。

首先,让我们重新解释一下这个问题。当你谈到匹配“前缀范围”时,我相信你实际谈论的是在词典排序下找到正确的范围(直观地说,这只是字符串的自然字母顺序)。例如,前缀匹配54661601到54661679的数字集恰好是一组数字,当写为字符串时,按字典顺序大于或等于“54661601”,但按字典顺序小于“54661680”。因此,您应该做的第一件事是在所有边界添加1,这样您就可以用这种方式表达您的查询。在mongo中,您的文档看起来像

{low: "54661601", high: "54661680", bin: "a"}
{low: "526219100",  high: "526219200",  bin: "b"}
{low: "4305870404", high: "4305870405", bin: "c"}

现在问题变成:给定一组[)形式的一维间隔,我们如何快速找到包含哪个区间给定点?最简单的方法是使用字段的索引。我们使用字段。在mongo shell中:

db.coll.ensureIndex({high : 1})

现在,让我们假设间隔根本不重叠。如果是这种情况,那么对于给定的查询点“x”,包含“x”的唯一可能区间是值大于“x”的区间。因此,我们可以查询该文档并检查其值是否也小于“x”。例如,这将打印出匹配间隔(如果有):

db.coll.find({high : {'$gt' : "5466160179125211"}}).sort({high : 1}).limit(1).forEach(
       function(doc){ if (doc.low <= "5466160179125211") printjson(doc) } 
  )

现在假设不是假设间隔根本不重叠,而是假设每个间隔与小于 k 相邻间隔重叠(我不知道 k的值是多少) 会让你真实,但希望它是一个小的)。在这种情况下,你可以在上面的“限制”中用 k 替换1,即

 db.coll.find({high : {'$gt' : "5466160179125211"}}).sort({high : 1}).limit(k).forEach(
       function(doc){ if (doc.low <= "5466160179125211") printjson(doc) } 
  )

此算法的运行时间是多少?索引使用B树存储,因此如果数据集中有 n 间隔,则需要O(log n )时间来查找第一个匹配的文档< strong>高值,然后O( k )时间迭代下一个 k 文档,总计为O(log n + k )时间。如果 k 是常数,或者事实上任何小于O(log n ),那么这是渐近最优的(这是在标准的计算模型中;我不是计算外部存储器传输的数量或任何花哨的东西)。

这种情况发生故障的唯一情况是 k 很大,例如,如果某个大间隔包含几乎所有其他间隔。在这种情况下,运行时间为O( n )。如果您的数据结构如下,那么您可能希望使用不同的方法。一种方法是使用mongo的“2d”索引,使用值来编码 x y 坐标。然后,您的查询将对应于查询 x - y 平面的给定区域中的点。这在实践中可能做得很好,尽管当前实现2d索引,最坏的情况仍然是O(n)。

有许多理论结果可以为 k 的所有值实现O(log n )性能。它们使用优先搜索树,段树,间隔树等名称。但是,这些是您必须自己实现的专用数据结构。据我所知,目前还没有流行的数据库实现它们。

答案 1 :(得分:0)

使用MySQL,您可能必须使用存储过程,您可以调用该存储过程将值映射到bin。所述过程将查询每行的桶列表,并执行算术或字符串操作以查找匹配的桶。您可以使用固定长度的前缀来改进此设计,前缀以固定数量的层排列。您可以为树指定固定深度,每个图层都有一个表。使用这两种方法都不会得到树状表现。

如果你想做更复杂的事情,我怀疑你必须使用不同的平台。

Sql Server具有层次结构数据类型:   http://technet.microsoft.com/en-us/library/bb677173.aspx

PostgreSQL有一个cidr数据类型。我不熟悉它具有的查询支持级别,但从理论上讲,您可以在数据库内部构建路由表并使用它来分配存储区:   http://www.postgresql.org/docs/7.4/static/datatype-net-types.html#DATATYPE-CIDR

答案 2 :(得分:0)

“最佳”对不同的人来说意味着不同的东西。看起来你可以做一些事情,比如将你的低值和高值保存为varchars。那么你所要做的就是

select bin from datatable where '5466160179125211' between low and high

或者,如果您有理由将值保持为表中的整数,则可以在查询中执行CASTing。

我不知道这是否会让你在使用大型数据集时表现糟糕。我希望我明白你想做什么。

答案 3 :(得分:0)

佩顿! :)

如果你需要将所有内容保持为整数,并希望它能够使用单个查询,那么这应该有效:

select bin from datatable where 5466160179125211 between 
      low*pow(10, floor(log10(5466160179125211))-floor(log10(low))) 
   and ((high+1)*pow(10, floor(log10(5466160179125211))-floor(log10(high)))-1);

在这种情况下,它将在数字5466160100000000(具有低前缀的最低数字和与要查找的数字相同的位数)和546616799999999(具有高前缀和相同数字的最高数字)之间进行搜索数字作为要查找的数字)。这应该仍然适用于高前缀比低前缀更多的数字。如果数字短于前缀的长度,前一个解决方案中的varchar代码可能会给出不正确的结果,它也应该起作用(我认为)。

您需要尝试比较在查询中使用大量内联数学的性能(如此解决方案中)与使用varchars的性能。

编辑:即使在没有索引的大表上,性能似乎也非常好;如果你可以使用varchars,那么你可以通过索引低和高列来进一步提高性能。请注意,如果任何前缀具有初始零,您肯定希望使用varchars。这是一个修复,允许使用varchars时数字短于前缀的情况:

select * from datatable2 where '5466' between low and high
    and length('5466') >= length(high);