Эффективный способ поиска последовательных значений

Каждый "продукт" может иметь до 10000 "сегментированных" строк. Сегменты имеют столбец сортировки, который начинается с 1 для каждого продукта (1, 2, 3, 4, 5, ...) и столбец значений, который может содержать любые значения, такие как (323.113, 5423.231, 873.42, 422.64, 763.1, ...).

Я хотел бы определить потенциальные совпадения для продуктов с учетом подмножества сегментов. Например, если у меня есть 5 значений сегмента в правильном порядке, как я могу эффективно найти все продукты, которые имеют все 5 сегментов в одном и том же порядке где-то в таблице сегментов?


Обновить

Я разместил дополнительный вопрос к этому здесь: найдите ряд данных, используя неточные измерения (нечеткая логика)

3 ответа

Решение

Предположим, что таблицы выглядят так:

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

Вероятно, наилучший способ сделать это - сохранить денормализованную версию данных.

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('')) + '%' 

Возможно, то, что я пытаюсь предложить, занимает слишком много места: как только вы добавляете новый продукт, поскольку сегменты статичны, вы можете проиндексировать их, взяв все "суффиксы" из списка сегментов.

Например, список сегментов:

34 57 67 34

будет производить:

34 57 67 34
57 67 34
67 34
34

Возможно, вам придется хранить их в файлах на жестком диске, потому что для 10000 сегментов для каждого продукта вы получите множество "суффиксов" (фактически до 10000 для каждого продукта). Хорошей новостью является то, что вы можете хранить их непрерывно, чтобы у вас не было слишком много обращений к жесткому диску. Затем вы можете просто линейно сканировать список суффиксов и сопоставлять первые k значений для запроса, который содержит k значений сегмента. Таким образом, если в приведенном выше списке вы ищете 57 67, он вернет этот продукт, потому что он соответствует второму суффиксу.

Вы также можете выполнить индексацию дерева для более быстрого соответствия, но это может оказаться слишком сложным.

Изменить: Как я уже говорил в комментарии, это адаптация соответствия подстроки. Вы также должны отсортировать суффиксы по номеру, а затем выполнить бинарный поиск по списку суффиксов, что в теории должно указывать совпадение / несоответствие в шагах журнала (10000).

Другие вопросы по тегам