【问题标题】:Efficient way to look up sequential values查找顺序值的有效方法
【发布时间】:2011-11-04 15:04:59
【问题描述】:

每个“产品”可以有多达 10000 个“细分”行。对于每个产品,段都有一个从 1 开始的排序列(1、2、3、4、5、...)和一个可以包含任何值的值列,例如(323.113、5423.231、873.42、422.64、763.1、 ...)。

我想确定给定细分子集的产品的潜在匹配项。例如,如果我有 5 个按正确顺序排列的细分值,我如何才能有效地在细分表中的某处找到所有 5 个细分都按相同顺序排列的产品?


更新

我在这里发布了一个后续问题:Find a series of data using non-exact measurements (fuzzy logic)

【问题讨论】:

  • “以相同的顺序”是指具有相同的排序列值,对吗?你能把你的架构写得更好一点吗?
  • 我还应该提到,如果需要调整数据库架构以使其工作或使其工作更快 - 这绝对是一种可能性。
  • 否,排序列值未知。一种产品可能在序列的早期具有值的子集,而另一种产品可能在稍后具有它。另一个可能有多次出现的子集。
  • 把产品想象成一首歌曲,把片段想象成歌曲中的一组音符。给定一组音符,可以在一首歌曲和多首歌曲中出现多次,匹配的歌曲是什么?
  • 那么以相同的顺序和连续的分段值?还是按相同的顺序?

标签: sql sql-server sql-server-2005 sql-server-2008 tsql


【解决方案1】:

假设表格如下:

CREATE TABLE Products
 (
   ProductId  int           not null
    constraint PK_Products
     primary key
  ,Name       varchar(100)  not null
 )

CREATE TABLE Segments
 (
   ProductId   int    not null
    constraint FK_Segments__Products
     foreign key references Products (ProductId)
  ,OrderBy     int    not null
  ,Value       float  not null
  ,constraint PK_Segments
    primary key (ProductId, OrderBy)
 )

接下来,在临时表中设置搜索数据:

CREATE TABLE #MatchThis
 (
   Position  int    not null
  ,Value     float  not null
 )

对于 N 个搜索对象,必须像这样填充它

First item   0    <value 1>
Second item  1    <value 2>
Third item   2    <value 3>
...
Nth item     N-1  <value N>

现在设置一些重要的值。 (这可能会被塞进最终查询中,但这种方式更容易阅读,并且可能会稍微提高性能。)

DECLARE
  @ItemCount   int
 ,@FirstValue  float

--  How many items to be matched ("N", above)
SELECT @ItemCount = count(*)
 from #MatchThis

--  The value of the first item in the search set
SELECT @FirstValue = Value
 from #MatchThis
 where Position = 0

然后它只是一个查询:

SELECT
   pr.Name
  ,fv.OrderBy  --  Required by the Group By, but otherwise can be ignored
 from #MatchThis mt
  cross join (--  All Segments that match the first value in the set
              select ProductId, OrderBy
               from Segment
               where Value = @FirstValue) fv
  inner join Product pr  --  Just to get the Product name
   on pr.ProductId = fv.ProductId
  inner join Segment se
   on se.ProductId = fv.ProductId
    and se.OrderBy = fv.OrderBy + mt.Position  --  Lines them up based on the first value
    and se.Value = mt.Value                    --  No join if the values don't match
 group by
   pr.Name
  ,fv.OrderBy
 having count(*) = @ItemCount  --  Only include if as many segments pulled for this product/segment.OrderBy as are required

我相信这会奏效,但我现在没有时间详细测试它。为了优化性能,除了指示的主键之外,您还可以在 Segment.Value 上添加常规索引

【讨论】:

  • +1 如果OrderBy 不包含空白,那么我认为这应该可以很好地工作。
  • 没错,我确实做出了这样的假设……但数据描述暗示了这一点。
  • 是的,同意,听起来应该是 OP 力所能及的事情。
【解决方案2】:

执行此操作的最佳执行方式可能是存储数据的非规范化版本。

ProductId, DelimitedList
1          ,323.113,5423.231,873.42,422.64,763.1,

那么你的搜索就很简单了

WHERE DelimitedList LIKE '%,323.113,5423.231,873.42,%'

您可以先执行标准的关系除法查询,以返回与所有值匹配的ProductId 值(不一定按正确顺序或连续),以减少搜索的字符串数量。

完整的演示脚本

/*Set up test tables*/
CREATE TABLE Products(
ProductId int primary key)


CREATE TABLE ProductSegments(
ProductId int REFERENCES Products,
Sort int,
Value decimal(10,3)
Primary key (ProductId,Sort))

CREATE NONCLUSTERED INDEX ix ON ProductSegments(ProductId,Value)


CREATE TABLE ProductSegmentsDenormalized
(
ProductId int REFERENCES Products,
DelimitedList varchar(max)
)

/*Insert some initial data to Products...*/
INSERT INTO Products VALUES (1),(2),(3)

/*... and for ProductSegments*/
;WITH numbers(N)
     AS (SELECT TOP 10000 ROW_NUMBER() OVER (ORDER BY (SELECT 0))
         FROM   master..spt_values v1,
                master..spt_values v2)
INSERT INTO ProductSegments
            (ProductId,
             Sort,
             Value)
SELECT ProductId AS Product,
       n1.N      Sort,
       ( ABS(CHECKSUM(NEWID()))% 1000000000 ) / 1000.00
FROM   numbers n1,
       Products  



/*Set up table for search data*/

DECLARE @SearchValues TABLE
(
Sequence int primary key,
Value decimal(10,3)
)
INSERT INTO @SearchValues
VALUES (1,323.113),(2,5423.231),(3,873.420),(4,422.640),(5,763.100)



/*Fiddle the test data so we have some guaranteed matches*/
UPDATE ps 
SET ps.Value = sv.Value
FROM ProductSegments ps
JOIN @SearchValues sv ON ProductId = 1 AND Sort = 100 + Sequence

UPDATE ps 
SET ps.Value = sv.Value
FROM ProductSegments ps
JOIN @SearchValues sv ON ProductId = 3 AND Sort = 987 + Sequence


/*Create the denormalised data*/
INSERT INTO ProductSegmentsDenormalized
SELECT ProductId, '|' + DelimitedList
FROM Products p
CROSS APPLY ( SELECT CAST(Value as varchar) + '|'
                 FROM ProductSegments ps
                 WHERE ps.ProductId = p.ProductId
                 ORDER BY Sort
                 FOR XML PATH('') )  D ( DelimitedList )
                 


/*Do the search*/
SELECT ProductId
FROM   ProductSegmentsDenormalized psd
WHERE  psd.ProductId IN (SELECT p.ProductId
                         FROM   Products p
                         WHERE  NOT EXISTS (SELECT *
                                            FROM   @SearchValues sv
                                            WHERE  NOT EXISTS
                                                   (SELECT *
                                                    FROM ProductSegments ps
                                                    WHERE ps.ProductId = p.ProductId
                                                    AND sv.Value = ps.Value)))
AND DelimitedList LIKE '%|' + (SELECT CAST(Value AS VARCHAR) + '|'
                                FROM   @SearchValues sv
                                ORDER  BY Sequence
                                FOR XML PATH('')) + '%' 

【讨论】:

  • 我认为这不是一个好主意。即使排序正确,分隔列表也可能有一个他没有搜索的额外元素,并且此数据结构会崩溃。此外,如果不强制存储数据的排序,除非存储排序与搜索字符串排序匹配,否则他仍然会丢失。
  • @Strommy - 很明显,需要以正确的顺序生成分隔列表才能使该方案工作(使用“正常”XML PATH 模拟group_concat 的方法很容易完成)。我真的不明白你的意思。额外的元素位无关紧要,因为 OP 澄清他们正在寻找 contiguous 段。 (即使它们不是,也可以通过额外的通配符轻松调整)
  • 同意订购位。在以后的 cmets 中进行了澄清。也同意连续。我对您的解决方案的不满是您提出的架构更改提供了有问题的本地优化,同时提供了一个次优的全局解决方案。你能澄清一下这将是一个高效的解决方案吗?
  • @MS:我想当您处理大量有序元素时确实如此,但您是否需要编辑该列?此外,搜索具有 1 个段的所有产品会强制进行表扫描。每个解决方案都有一个权衡,但魔鬼在细节中。我想需要更多信息,对吗?
  • @Strommy - OP 还在 cmets 中阐明了这些段是静态的,因此每个列表的创建都是一次性的。
【解决方案3】:

可能我要建议的内容占用了太多空间:添加新产品后,由于细分是静态的,因此您可以通过获取细分列表的所有“后缀”来对它们进行索引。

例如分段列表:

34 57 67 34

将产生:

34 57 67 34
57 67 34
67 34
34

您可能需要将它们存储在硬盘文件中,因为对于每个产品的 10000 个段,您会得到很多“后缀”(实际上每个产品最多 10000 个)。好消息是您可以将它们连续存储,这样您就不会进行太多的硬盘寻道。 然后,您可以简单地线性扫描后缀列表并匹配包含 k 个段值的查询的前 k 个值。因此,如果您在上面的列表中搜索 57 67,它将返回该产品,因为它与第二个后缀匹配。

您还可以进行树索引以加快匹配速度,但这可能会变得过于复杂。

编辑:正如我在评论中所说,这是对子字符串匹配的改编。您还必须按数字对后缀编号进行排序,然后您可以对后缀列表进行二进制搜索,这在理论上应该指示 log(10000) 步骤中的匹配/不匹配。

【讨论】:

  • “太复杂”不是我担心的事情。这必须以一种或另一种方式发生。我没有想过在数据库之外这样做。如果我要重新问这个问题,所以它不是特定于 SQL Server 的,我可以包括哪些标签来让合适的人回答更多这样的解决方案?我不知道这类算法的术语。
  • 这个算法实际上是用于文本搜索中的子串匹配。
猜你喜欢
  • 2019-06-12
  • 2022-01-27
  • 1970-01-01
  • 2012-08-08
  • 2011-06-15
  • 2013-06-24
  • 2021-07-16
  • 2012-04-18
  • 2017-05-26
相关资源
最近更新 更多