Подтвердить что ты не робот

Как выбрать аналогичные наборы в SQL

У меня есть следующие таблицы:

Order
----
ID (pk)

OrderItem
----
OrderID (fk -> Order.ID)
ItemID (fk -> Item.ID)
Quantity

Item
----
ID (pk)

Как я могу написать запрос, который может выбрать все Orders, которые по крайней мере на 85% похожи на конкретный Order?

Я рассмотрел статистику Jaccard Index, чтобы рассчитать сходство двух Orders. (Взяв пересечение каждого набора из OrderItems, деленное на объединение каждого набора из OrderItems)

Однако я не могу придумать, как это сделать, не сохраняя вычисленный индекс Jaccard для каждой возможной комбинации двух Orders. Есть ли другой способ?

Кроме того, есть ли способ включить разницу в Quantity каждого согласованного OrderItem?

Дополнительная информация:


Всего Orders: ~ 79k
Всего OrderItems: ~ 1.76m
Avg. OrderItems за Order: 21.5
Всего Items: ~ 13k

Примечание


85% -ое сходство - это просто лучшее предположение о том, что действительно нужно клиенту, оно может измениться в будущем. Решение, которое работает для любого сходства, было бы предпочтительным.

4b9b3361

Ответ 1

Вы указываете

Как я могу написать запрос, который может выбрать все заказы, которые по крайней мере 85% похоже на определенный порядок?

Это важное упрощение по сравнению с "всеми парами заказов" которые по меньшей мере на 85% похожи друг на друга ".

Мы будем использовать TDQD (Test-Driven Query Design) и некоторый анализ, чтобы помочь нам.

Отборочные

Чтобы быть удаленно похожими, два заказа должны иметь по крайней мере один элемент в общий. Этот запрос может быть использован для определения того, какие заказы имеют по крайней мере один элемент вместе с указанным порядком:

SELECT DISTINCT I1.OrderID AS ID
  FROM OrderItem AS I1
  JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID AND I2.OrderID = <specified order ID>
 WHERE I1.OrderID != <specified order ID>

Это сокращает список других заказов, которые нужно изучить довольно много, хотя если указанный заказ включает один из ваших самых популярных товаров, это вероятно, что многие другие заказы также сделали это.

Вместо DISTINCT вы можете использовать:

SELECT I1.OrderID AS ID, COUNT(*) AS Num_Common
  FROM OrderItem AS I1
  JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID AND I2.OrderID = <specified order ID>
 WHERE I1.OrderID != <specified order ID>
 GROUP BY I1.OrderID

Это дает вам количество элементов в том порядке, в котором оно имеет общее с указанным порядком. Нам также нужно количество предметов в каждом порядок:

SELECT OrderID AS ID, COUNT(*) AS Num_Total
  FROM OrderItem
 GROUP BY OrderID;

Идентичные заказы

Для 100% сходства два заказа будут иметь как можно больше элементов так как у каждого есть предметы. Это, вероятно, не найдет много пар заказов, хоть. Мы можем найти заказы с точно такими же элементами, как и заданный порядок достаточно легко:

SELECT L1.ID
  FROM (SELECT OrderID AS ID, COUNT(*) AS Num_Total
          FROM OrderItem
         GROUP BY OrderID
       ) AS L1
  JOIN (SELECT I1.OrderID AS ID, COUNT(*) AS Num_Common
          FROM OrderItem AS I1
          JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID AND I2.OrderID = <specified order ID>
         WHERE I1.OrderID != <specified order ID>
         GROUP BY I1.OrderID
       ) AS L2 ON L1.ID = L2.ID AND L1.Num_Total = L2.Num_Common;

Изменить: это оказывается недостаточно строгим; для того, чтобы заказы были идентичными, количество элементов в указанном порядке также должно быть таким же, как общее число:

SELECT L1.ID, L1.Num_Total, L2.ID, L2.Num_Common, L3.ID, L3.Num_Total
  FROM (SELECT OrderID AS ID, COUNT(*) AS Num_Total
          FROM OrderItem
         GROUP BY OrderID
       ) AS L1
  JOIN (SELECT I1.OrderID AS ID, COUNT(*) AS Num_Common
          FROM OrderItem AS I1
          JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID AND I2.OrderID = <specified order ID>
         WHERE I1.OrderID != <specified order ID>
         GROUP BY I1.OrderID
       ) AS L2 ON L1.ID = L2.ID AND L1.Num_Total = L2.Num_Common
  JOIN (SELECT OrderID AS ID, COUNT(*) AS Num_Total
          FROM OrderItem
         WHERE OrderID = <specified order ID>
         GROUP BY OrderID
       ) AS L3 ON L2.Num_Common = L3.Num_Total;

Похожие заказы - анализ формулы

Применение сходство с Jaccard как определено в Википедии, двум заказам A и B, с | A | считая числа элементов в порядке A, сходство Jaccard J (A, B) = | A∩B | ÷ | A∪B |, где | A∩B | это количество общих для два заказа и | A∪B | это общее количество разных предметов прописал.

Чтобы выполнить критерий подобия 85% Jaccard, если количество элементов в либо порядок меньше некоторого порога, заказы должны быть одинаковыми. Например, если оба заказа A и B имеют 5 элементов, скажем, но там один элемент, отличный от двух, он дает вам 4 общих элемента (| A∩B |) и всего 6 предметов (| A∪B |), поэтому сходство Jaccard J (A, B) 66⅔%.

Для 85% подобия, когда в каждом из двух ордеров есть N элементов 1 элемент отличается, (N-1) ÷ (N + 1) ≥ 0,85, что означает N > 12 (Точнее, 12). Для доли F = J (A, B) один элемент отличается означает (N-1) ÷ (N + 1) ≥ F, который можно решить для N, что дает N ≥ (1 + F) ÷ (1 - F). Поскольку требование подобия возрастает, заказы должны быть одинаковыми для все больших значений N.

Обобщая еще больше, допустим, что мы имеем разные порядки размера с элементами N и M (без ограничения общности, N < M). Максимум значение | A∩B | теперь N и минимальное значение | A∪B | есть M (что означает все предметы в меньшем порядке отображаются в большем порядке). Давайте определите, что M = N + Δ, и что есть ∂ элементы, присутствующие в меньший порядок, который отсутствует в большем порядке. Следует, что есть Δ + ∂ элементы, присутствующие в большем порядке, которые не находятся в меньшего порядка.

По определению, тогда | A∩B | = N-∂, и | A∪B | = (N-∂) + ∂ + (N + Δ- (N-∂)), где три добавленных члена представляют (1) число общие предметы между двумя заказами, (2) количество предметов только в меньший порядок и (3) количество предметов только в большем порядке. Это упрощает: | A∪B | = N + Δ + ∂.


Уравнение ключа

Для фракции подобия F нас интересуют пары ордеров, где J (A, B) ≥ F, поэтому:

(N-∂) ÷ (N + Δ + ∂) ≥ F

F ≤ (N-∂) ÷ (N + Δ + ∂)


Мы можем использовать электронную таблицу, чтобы отобразить взаимосвязь между ними. Для заданное количество элементов в меньшем порядке (ось х) и для заданного подобия, мы можем построить максимальное значение ∂, которое дает нам сходство F. Формула:

∂ = (N (1-F) - FΔ) ÷ (1 + F)

...plot of ∂ = (N(1-F) - F∆) ÷ (1+F)...

Это линейное уравнение в N и Δ для постоянной F; он нелинейный для разных значений F. Ясно, что ∂ должно быть неотрицательным целое число.

Учитывая F = 0,85, для заказов, которые имеют одинаковый размер (Δ = 0), для 1 ≤ N < 13, ∂ = 0; для 13≤N < 25, ≤ ≤ 1; для 25 ≤ N < 37, ≤ ≤ 2, для 37 ≤ N < 50, ∂ ≤ 3.

Для заказов, которые отличаются на 1 (Δ = 1), для 1 ≤ N < 18, ∂ = 0; за 18 ≤ N < 31, ∂ ≤ 1; для 31 ≤ N < 43, ≤ ≤ 2; и т.д. Если Δ = 6, вы нужно N = 47 до того, как заказы по-прежнему остаются на 85% похожими на ∂ = 1. Что означает, что у небольшого заказа есть 47 предметов, из которых 46 являются общими с большой порядок из 53 предметов.

Аналогичные заказы - применение анализа

До сих пор так хорошо. Как мы можем применить эту теорию к выбору заказов аналогично указанному порядку?

Сначала отметим, что указанный порядок может быть того же размера, что и аналогичный порядок, или больше, или меньше. Это немного усложняет ситуацию.

Параметры вышеуказанного уравнения:

  • N - количество элементов в меньшем порядке
  • Δ - разница между количеством элементов в большем порядке и N
  • F - исправлено
  • ∂ - количество элементов в меньшем порядке, не соответствующих более крупному заказу

Значения, доступные с использованием незначительных вариаций в запросах, разработанных в верхней части:

  • NC - количество общих элементов
  • NA - количество элементов в указанном порядке
  • NB - количество элементов в сравниваемом порядке

Соответствующие запросы:

SELECT OrderID AS ID, COUNT(*) AS NA
  FROM OrderItem
 WHERE OrderID = <specified order ID>
 GROUP BY OrderID;

SELECT OrderID AS ID, COUNT(*) AS NB
  FROM OrderItem
 WHERE OrderID != <specified order ID>
 GROUP BY OrderID;

SELECT I1.OrderID AS ID, COUNT(*) AS NC
  FROM OrderItem AS I1
  JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID AND I2.OrderID = <specified order ID>
 WHERE I1.OrderID != <specified order ID>
 GROUP BY I1.OrderID

Для удобства мы хотим получить значения N и N + Δ (и, следовательно, Δ), поэтому мы можем использовать СОЮЗ, чтобы упорядочить вещи соответствующим образом:

  • NS = N - количество элементов в меньшем порядке
  • NL = N + Δ - количество элементов в большем порядке

а во второй версии запроса UNION:

  • NC = N - ∂ - общее количество элементов

Оба запроса содержат два идентификатора заказа, чтобы вы могли отслеживать остальная часть информации о заказе позже.

SELECT v1.ID AS OrderID_1, v1.NA AS NS, v2.ID AS OrderID_2, v2.NB AS NL
  FROM (SELECT OrderID AS ID, COUNT(*) AS NA
          FROM OrderItem
         WHERE OrderID = <specified order ID>
         GROUP BY OrderID
       ) AS v1
  JOIN (SELECT OrderID AS ID, COUNT(*) AS NB
          FROM OrderItem
         WHERE OrderID != <specified order ID>
         GROUP BY OrderID
       ) AS v2
    ON v1.NA <= v2.NB
UNION
SELECT v2.ID AS OrderID_1, v2.NB AS NS, v1.ID AS OrderID_2, v1.NA AS NL
  FROM (SELECT OrderID AS ID, COUNT(*) AS NA
          FROM OrderItem
         WHERE OrderID = <specified order ID>
         GROUP BY OrderID
       ) AS v1
  JOIN (SELECT OrderID AS ID, COUNT(*) AS NB
          FROM OrderItem
         WHERE OrderID != <specified order ID>
         GROUP BY OrderID
       ) AS v2
    ON v1.NA > v2.NB

Это дает нам табличное выражение с столбцами OrderID_1, NS, OrderID_2, NL, где NS - количество элементов в "меньшем порядке", а NL - количество элементов в большем порядке. Поскольку в номера заказов, генерируемые табличными выражениями v1 и v2, нет нужно беспокоиться о "рефлексивных" записях, где значения OrderID являются одна и та же. Добавление NC к этому наиболее легко обрабатывается в запросе UNION:

SELECT v1.ID AS OrderID_1, v1.NA AS NS, v2.ID AS OrderID_2, v2.NB AS NL, v3.NC AS NC
  FROM (SELECT OrderID AS ID, COUNT(*) AS NA
          FROM OrderItem
         WHERE OrderID = <specified order ID>
         GROUP BY OrderID
       ) AS v1
  JOIN (SELECT OrderID AS ID, COUNT(*) AS NB
          FROM OrderItem
         WHERE OrderID != <specified order ID>
         GROUP BY OrderID
       ) AS v2
    ON v1.NA <= v2.NB
  JOIN (SELECT I1.OrderID AS ID, COUNT(*) AS NC
          FROM OrderItem AS I1
          JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID AND I2.OrderID = <specified order ID>
         WHERE I1.OrderID != <specified order ID>
         GROUP BY I1.OrderID
       ) AS v3
    ON v3.ID = v2.ID
UNION
SELECT v2.ID AS OrderID_1, v2.NB AS NS, v1.ID AS OrderID_2, v1.NA AS NL, v3.NC AS NC
  FROM (SELECT OrderID AS ID, COUNT(*) AS NA
          FROM OrderItem
         WHERE OrderID = <specified order ID>
         GROUP BY OrderID
       ) AS v1
  JOIN (SELECT OrderID AS ID, COUNT(*) AS NB
          FROM OrderItem
         WHERE OrderID != <specified order ID>
         GROUP BY OrderID
       ) AS v2
    ON v1.NA > v2.NB
  JOIN (SELECT I1.OrderID AS ID, COUNT(*) AS NC
          FROM OrderItem AS I1
          JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID AND I2.OrderID = <specified order ID>
         WHERE I1.OrderID != <specified order ID>
         GROUP BY I1.OrderID
       ) AS v3
    ON v3.ID = v1.ID

Это дает нам табличное выражение с столбцами OrderID_1, NS, OrderID_2, NL, NC, где NS - количество элементов в "меньшем порядке", а NL - количество элементов в более крупном заказе, а NC - количество элементов в общем.

Учитывая NS, NL, NC, мы ищем заказы, которые удовлетворяют:

(N-∂) ÷ (N + Δ + ∂) ≥ F.

  • N - количество элементов в меньшем порядке
  • Δ - разница между количеством элементов в большем порядке и N
  • F - исправлено
  • ∂ - количество элементов в меньшем порядке, не соответствующих более крупному заказу

  • NS = N - количество элементов в меньшем порядке

  • NL = N + Δ - количество элементов в большем порядке
  • NC = N - ∂ - общее количество элементов

Следовательно, условие должно быть:

NC / (NL + (NS - NC)) ≥ F

Термин LHS должен оцениваться как число с плавающей запятой, а не как целочисленное выражение. Применив это к запросу UNION выше, вы получите:

SELECT OrderID_1, NS, OrderID_2, NL, NC,
        CAST(NC AS NUMERIC) / CAST(NL + NS - NC AS NUMERIC) AS Similarity
  FROM (SELECT v1.ID AS OrderID_1, v1.NA AS NS, v2.ID AS OrderID_2, v2.NB AS NL, v3.NC AS NC
          FROM (SELECT OrderID AS ID, COUNT(*) AS NA
                  FROM OrderItem
                 WHERE OrderID = <specified order ID>
                 GROUP BY OrderID
               ) AS v1
          JOIN (SELECT OrderID AS ID, COUNT(*) AS NB
                  FROM OrderItem
                 WHERE OrderID != <specified order ID>
                 GROUP BY OrderID
               ) AS v2
            ON v1.NA <= v2.NB
          JOIN (SELECT I1.OrderID AS ID, COUNT(*) AS NC
                  FROM OrderItem AS I1
                  JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID AND I2.OrderID = <specified order ID>
                 WHERE I1.OrderID != <specified order ID>
                 GROUP BY I1.OrderID
               ) AS v3
            ON v3.ID = v2.ID
        UNION
        SELECT v2.ID AS OrderID_1, v2.NB AS NS, v1.ID AS OrderID_2, v1.NA AS NL, v3.NC AS NC
          FROM (SELECT OrderID AS ID, COUNT(*) AS NA
                  FROM OrderItem
                 WHERE OrderID = <specified order ID>
                 GROUP BY OrderID
               ) AS v1
          JOIN (SELECT OrderID AS ID, COUNT(*) AS NB
                  FROM OrderItem
                 WHERE OrderID != <specified order ID>
                 GROUP BY OrderID
               ) AS v2
            ON v1.NA > v2.NB
          JOIN (SELECT I1.OrderID AS ID, COUNT(*) AS NC
                  FROM OrderItem AS I1
                  JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID AND I2.OrderID = <specified order ID>
                 WHERE I1.OrderID != <specified order ID>
                 GROUP BY I1.OrderID
               ) AS v3
            ON v3.ID = v1.ID
       ) AS u
 WHERE CAST(NC AS NUMERIC) / CAST(NL + NS - NC AS NUMERIC) >= 0.85 -- F

Вы можете заметить, что в этом запросе используется таблица OrderItem; Таблицы заказа и позиции не нужны.


Предупреждение: частично протестированный SQL (caveat lector). Вышеупомянутый SQL, похоже, дает правдоподобные ответы на миниатюрные наборы данных. Я скорректировал требование подобия (0,25, затем 0,55) и получил правдоподобные значения и соответствующую селективность. Однако мои тестовые данные имели только 8 пунктов в самом большом порядке и, разумеется, не охватывали весь объем описанных данных. Поскольку СУБД, который я использую наиболее часто, не поддерживает CTE, SQL ниже не тестируется. Тем не менее, я уверенно уверен, что, если я не допустил большую ошибку, код CTE в версии 1 (с большим количеством повторений указанного идентификатора заказа) должен быть чистым. Я думаю, что версия 2 тоже может быть ОК, но... она не проверена.

Возможно, существуют более компактные способы выражения запроса, возможно, используя функции OLAP.

Если бы я собирался проверить это, я бы создал таблицу с несколькими представительные наборы элементов заказа, проверяя, что мера подобия вернулся был разумным. Я работал бы более или менее, как показано, постепенно создавая сложный запрос. Если одно из выражений показанный как ошибочный, тогда я бы сделал соответствующие корректировки в этом запросе пока дефект не будет исправлен.

Очевидно, что производительность будет проблемой. Самые внутренние запросы не ужасно сложный, но они не тривиальны. Однако измерение покажет, является ли это драматической проблемой или просто неприятностью. изучение могут помочь планы запросов. Кажется весьма вероятным, что индекс на OrderItem.OrderID; запросы вряд ли будут хорошо работать если такого индекса нет. Это вряд ли будет проблемой, поскольку является столбцом внешнего ключа.

Вы можете извлечь выгоду из использования предложений WITH WITH (Общие выражения таблицы). Они будут делать явное повторение, которое подразумевается в двух половинах подзапроса UNION.


Использование общих выражений таблицы

Использование общих табличных выражений разъясняет оптимизатору, когда выражения одинаковы и могут помочь ему работать лучше. Они также помогите людям, читающим ваш запрос. Приведенный выше запрос скорее использование CTE.

Версия 1: Повторение указанного номера заказа

WITH SO AS (SELECT OrderID AS ID, COUNT(*) AS NA       -- Specified Order (SO)
              FROM OrderItem
             WHERE OrderID = <specified order ID>
             GROUP BY OrderID
           ),
     OO AS (SELECT OrderID AS ID, COUNT(*) AS NB       -- Other orders (OO)
              FROM OrderItem
             WHERE OrderID != <specified order ID>
             GROUP BY OrderID
           ),
     CI AS (SELECT I1.OrderID AS ID, COUNT(*) AS NC    -- Common Items (CI)
              FROM OrderItem AS I1
              JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID AND I2.OrderID = <specified order ID>
             WHERE I1.OrderID != <specified order ID>
             GROUP BY I1.OrderID
           )
SELECT OrderID_1, NS, OrderID_2, NL, NC,
        CAST(NC AS NUMERIC) / CAST(NL + NS - NC AS NUMERIC) AS Similarity
  FROM (SELECT v1.ID AS OrderID_1, v1.NA AS NS, v2.ID AS OrderID_2, v2.NB AS NL, v3.NC AS NC
          FROM SO AS v1
          JOIN OO AS v2 ON v1.NA <= v2.NB
          JOIN CI AS v3 ON v3.ID  = v2.ID
        UNION
        SELECT v2.ID AS OrderID_1, v2.NB AS NS, v1.ID AS OrderID_2, v1.NA AS NL, v3.NC AS NC
          FROM SO AS v1
          JOIN OO AS v2 ON v1.NA  > v2.NB
          JOIN CI AS v3 ON v3.ID  = v1.ID
       ) AS u
 WHERE CAST(NC AS NUMERIC) / CAST(NL + NS - NC AS NUMERIC) >= 0.85 -- F

Версия 2: Избегание повторения указанного номера заказа

WITH SO AS (SELECT OrderID AS ID, COUNT(*) AS NA       -- Specified Order (SO)
              FROM OrderItem
             WHERE OrderID = <specified order ID>
             GROUP BY OrderID
           ),
     OO AS (SELECT OI.OrderID AS ID, COUNT(*) AS NB    -- Other orders (OO)
              FROM OrderItem AS OI
              JOIN SO ON OI.OrderID != SO.ID
             GROUP BY OI.OrderID
           ),
     CI AS (SELECT I1.OrderID AS ID, COUNT(*) AS NC    -- Common Items (CI)
              FROM OrderItem AS I1
              JOIN SO AS S1 ON I1.OrderID != S1.ID
              JOIN OrderItem AS I2 ON I2.ItemID = I1.ItemID
              JOIN SO AS S2 ON I2.OrderID  = S2.ID
             GROUP BY I1.OrderID
           )
SELECT OrderID_1, NS, OrderID_2, NL, NC,
        CAST(NC AS NUMERIC) / CAST(NL + NS - NC AS NUMERIC) AS Similarity
  FROM (SELECT v1.ID AS OrderID_1, v1.NA AS NS, v2.ID AS OrderID_2, v2.NB AS NL, v3.NC AS NC
          FROM SO AS v1
          JOIN OO AS v2 ON v1.NA <= v2.NB
          JOIN CI AS v3 ON v3.ID  = v2.ID
        UNION
        SELECT v2.ID AS OrderID_1, v2.NB AS NS, v1.ID AS OrderID_2, v1.NA AS NL, v3.NC AS NC
          FROM SO AS v1
          JOIN OO AS v2 ON v1.NA  > v2.NB
          JOIN CI AS v3 ON v3.ID  = v1.ID
       ) AS u
 WHERE CAST(NC AS NUMERIC) / CAST(NL + NS - NC AS NUMERIC) >= 0.85 -- F

Ни одно из них не читается легко; оба они легче, чем большой SELECT с выписанными CTE.


Минимальные тестовые данные

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

CREATE TABLE Order (ID SERIAL NOT NULL PRIMARY KEY);
CREATE TABLE Item  (ID SERIAL NOT NULL PRIMARY KEY);
CREATE TABLE OrderItem
(
    OrderID INTEGER NOT NULL REFERENCES Order,
    ItemID INTEGER NOT NULL REFERENCES Item,
    Quantity DECIMAL(8,2) NOT NULL
);

INSERT INTO Order VALUES(1);
INSERT INTO Order VALUES(2);
INSERT INTO Order VALUES(3);
INSERT INTO Order VALUES(4);
INSERT INTO Order VALUES(5);
INSERT INTO Order VALUES(6);
INSERT INTO Order VALUES(7);

INSERT INTO Item VALUES(111);
INSERT INTO Item VALUES(222);
INSERT INTO Item VALUES(333);
INSERT INTO Item VALUES(444);
INSERT INTO Item VALUES(555);
INSERT INTO Item VALUES(666);
INSERT INTO Item VALUES(777);
INSERT INTO Item VALUES(888);
INSERT INTO Item VALUES(999);

INSERT INTO OrderItem VALUES(1, 111, 1);
INSERT INTO OrderItem VALUES(1, 222, 1);
INSERT INTO OrderItem VALUES(1, 333, 1);
INSERT INTO OrderItem VALUES(1, 555, 1);

INSERT INTO OrderItem VALUES(2, 111, 1);
INSERT INTO OrderItem VALUES(2, 222, 1);
INSERT INTO OrderItem VALUES(2, 333, 1);
INSERT INTO OrderItem VALUES(2, 555, 1);

INSERT INTO OrderItem VALUES(3, 111, 1);
INSERT INTO OrderItem VALUES(3, 222, 1);
INSERT INTO OrderItem VALUES(3, 333, 1);
INSERT INTO OrderItem VALUES(3, 444, 1);
INSERT INTO OrderItem VALUES(3, 555, 1);
INSERT INTO OrderItem VALUES(3, 666, 1);

INSERT INTO OrderItem VALUES(4, 111, 1);
INSERT INTO OrderItem VALUES(4, 222, 1);
INSERT INTO OrderItem VALUES(4, 333, 1);
INSERT INTO OrderItem VALUES(4, 444, 1);
INSERT INTO OrderItem VALUES(4, 555, 1);
INSERT INTO OrderItem VALUES(4, 777, 1);

INSERT INTO OrderItem VALUES(5, 111, 1);
INSERT INTO OrderItem VALUES(5, 222, 1);
INSERT INTO OrderItem VALUES(5, 333, 1);
INSERT INTO OrderItem VALUES(5, 444, 1);
INSERT INTO OrderItem VALUES(5, 555, 1);
INSERT INTO OrderItem VALUES(5, 777, 1);
INSERT INTO OrderItem VALUES(5, 999, 1);

INSERT INTO OrderItem VALUES(6, 111, 1);
INSERT INTO OrderItem VALUES(6, 222, 1);
INSERT INTO OrderItem VALUES(6, 333, 1);
INSERT INTO OrderItem VALUES(6, 444, 1);
INSERT INTO OrderItem VALUES(6, 555, 1);
INSERT INTO OrderItem VALUES(6, 777, 1);
INSERT INTO OrderItem VALUES(6, 888, 1);
INSERT INTO OrderItem VALUES(6, 999, 1);

INSERT INTO OrderItem VALUES(7, 111, 1);
INSERT INTO OrderItem VALUES(7, 222, 1);
INSERT INTO OrderItem VALUES(7, 333, 1);
INSERT INTO OrderItem VALUES(7, 444, 1);
INSERT INTO OrderItem VALUES(7, 555, 1);
INSERT INTO OrderItem VALUES(7, 777, 1);
INSERT INTO OrderItem VALUES(7, 888, 1);
INSERT INTO OrderItem VALUES(7, 999, 1);
INSERT INTO OrderItem VALUES(7, 666, 1);

Ответ 2

На самом деле нет простого ответа на этот вопрос. Конечно, вы можете хранить индекс Jaccard (на самом деле я бы просто сохранил те, которые отвечают критериям, и выбрасывать остальное), но реальная проблема заключается в его вычислении (фактически, нужно сканировать весь ваш существующий заказ каждый раз, когда новый заказ был введен в систему для расчета нового индекса).

Это может быть довольно дорогостоящим в зависимости от вашего объема заказов, которые вы поддерживаете. Может быть, вы сравните его только с прошлым годом заказов или что-то в этом роде.

Если вы делаете это "на лету", это становится более интересным, но все же дорогостоящим.

Вы можете легко получить список всех заказов, имеющих одинаковые элементы продукта. Один список для каждого элемента. На самом деле это не обязательно много данных (если у вас много заказов для одного популярного элемента, то это может быть длинный список). Индивидуальные запросы не особенно безумны (опять же в зависимости от ваших данных). Если у вас обширный объем данных, запрос может легко отображаться/уменьшаться и даже работать с запертыми хранилищами данных. Индексы растровых изображений (если ваша БД поддерживает это) особенно хороши для получения списков, подобных этому довольно быстро.

Затем вы можете просто подсчитать время, когда номер заказа встречается во всех списках, а затем отбросить те, которые не соответствуют порогу. Это операция прямого слияния.

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

Таким образом, это действительно сводится к тому, для чего вам нужна информация, как часто вам это нужно, распределение ваших товаров ↔ , сколько времени вы можете дождаться и т.д.

Addenda:

Размышляя об этом немного, это простой запрос, но для запуска может потребоваться некоторое время. Наверное, не так много с современным оборудованием, у вас на самом деле не так много данных. Для одного экрана, просматривающего заказ, вы его не заметите. Если вы выполняли отчет по всем заказам, то вы определенно заметили бы его - и ему нужен другой подход.

Рассмотрим порядок с 20 позициями.

И вы хотите совпадение 85%. Это означает, что у нас есть 17 или более предметов.

Вот запрос, который даст вам заказы, которые вас интересуют:

SELECT orderId, count(*) FROM OrderItem
WHERE itemId in ('list', 'of', 'items', 'in', 'order', 123, 456, 789)
GROUP BY orderId
HAVING count(*) >= 17

Итак, это дает вам коллекцию всех позиций с теми же элементами, что и ваш заказ. Затем вы просто суммируете их по порядку, а те, которые равны или превышают ваш порог (17 в этом случае), являются кандидатами.

Теперь вы не говорите, сколько у вас предметов в вашем каталоге. Если у вас есть 1000 предметов, отлично распределенных, этот запрос будет пережевывать 1600 строк данных - что немаловажно. При правильных индексах это должно идти довольно быстро. Однако, если у вас есть элементы, которые "действительно популярны", тогда вы собираетесь пережевывать гораздо больше строк данных.

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

Итак, попробуйте и посмотрите, как это происходит для вас.

Ответ 3

В этом подходе учитывается количество, использующее коэффициент расширенной зарплаты или сходство Танимото. Он вычисляет сходство по всем ордерам, используя вектор общих ItemIDs величины Quantity. Это стоит сканирование таблицы, но не требует вычисления N ^ 2 всех возможных сходств.

SELECT
    OrderID,
    SUM(v1.Quantity * v2.Quantity) /
    (SUM(v1.Quantity * v1.Quantity) +
     SUM(v2.Quantity * v2.Quantity) -
     SUM(v1.Quantity * v2.Quantity) ) AS coef
FROM
    OrderItem v1 FULL OUTER JOIN OrderItem v2
    ON v1.ItemID = v2.ItemID
    AND v2.OrderID = ?
GROUP BY OrderID
HAVING coef > 0.85;

Формула для расширенного коэффициента Jaccard:

Tanimoto Similarity

Ответ 4

Это не такой ответ, как расширенный комментарий. Я удалю его, если он не будет иметь смысла.

Если вы пытаетесь найти "похожие" элементы "на лету", проблема в том, что у вас есть много (~ 79 тыс.) ордеров, на которые нужно смотреть. Поэтому, если вы пытаетесь это сделать, вам нужны способы сократить количество заказов, которые вы рассматриваете, прежде чем выполнять дорогостоящее сравнение.

Один из способов, обозначенный @Will, заключается в учете количества элементов в Заказе. Поэтому, если у вашего целевого заказа есть 20 элементов, вам нужно только рассмотреть Заказы с 17-23 OrderItems (или что-то в этом роде, в зависимости от точного расчета для '85% сходства). Я предполагаю, что эти числа могут быть вычислены с помощью триггера, всякий раз, когда заказ создается или изменяется, и сохраняется в столбцах в таблице Order.

Но если вы можете сохранить размер набора, вы также можете сохранить другие номера. Например, вы можете сохранить количество нечетных значений первичного ключа OrderItem в каждом порядке. Тогда Заказы, которые вы рассматриваете, должны были бы быть близки к тому, чтобы иметь такое количество чисел нечетного порядка (я мог бы выполнить математику в какой-то момент, чтобы заполнить "надлежащим образом закрыть" ).

И если вы думаете о разбиении значений на "нечетные" числа на разбиение на полосы размером 1, вы можете легко разбивать полосы разного размера с помощью оператора модуля. Например. ItemID% 4 < 2 будет иметь размер-2 полосы. Затем вы можете записать для каждого заказа количество первичных ключей OrderItem в этих полосах. Ваши кандидаты должны быть надлежащим образом близки к вашему целевому заказу по каждому пути разбиения значений.

Итак, в итоге вы получите большой подзапрос, который пытается ограничить размер кандидатов в таблице Orders, просмотрев целую кучу метрик, которые хранятся и индексируются в этой таблице.

Ответ 5

Я бы попробовал что-то вроде этого для скорости, перечисляя заказы по подобию Order @OrderId. Объединенный INTS должен быть пересечением, а значение подобия - это моя попытка вычислить индекс Jaccard.

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

SELECT 
    OI.OrderId,
    1.0*COUNT(INTS.ItemId) / 
    (COUNT(*)
    + (SELECT COUNT(*) FROM OrderItem WHERE OrderID = @OrderId) 
    - COUNT(INTS.ItemId)) AS Similarity
FROM    
    OrderItem OI        
JOIN
    OrderItem INTS ON INTS.ItemID = OI.ItemID AND [email protected]
GROUP BY 
    OI.OrderId
HAVING  
    1.0*COUNT(INTS.ItemId) / 
    (COUNT(*)
    + (SELECT COUNT(*) FROM OrderItem WHERE OrderID = @OrderId) 
    - COUNT(INTS.ItemId)) > 0.85
ORDER BY
    Similarity DESC

Он также предполагает, что комбинации OrderId/ItemId уникальны в OrderItem. Я понимаю, что это может быть не так, и его можно было бы использовать, используя представление.

Я уверен, что есть лучшие способы, но один из способов взвешивания в количественной разнице заключается в том, чтобы заменить номинатор COUNT (INTS.ItemId) чем-то вроде этого (предположим, что все величины будут положительными), что медленно уменьшает удар до 0 когда величины различаются.

    1/(ABS(LOG(OI.quantity)-LOG(INTS.quantity))+1)  

Добавлено: Это более читаемое решение, использующее сходство Tanimoto, предложенное JRideout

DECLARE 
    @ItemCount INT,
    @OrderId int 
SELECT     
    @OrderId  = 1
SELECT     
    @ItemCount = COUNT(*)
FROM 
    OrderItem
WHERE 
    OrderID = @OrderId 


SELECT 
    OI.OrderId,
    SUM(1.0* OI.Quantity*INTS.Quantity/(OI.Quantity*OI.Quantity+INTS.Quantity*INTS.Quantity-OI.Quantity*INTS.Quantity )) /
    (COUNT(*) + @ItemCount - COUNT(INTS.ItemId)) AS Similarity
FROM    
    OrderItem OI        
LEFT JOIN
    OrderItem INTS ON INTS.ItemID = OI.ItemID AND [email protected]
GROUP BY 
    OI.OrderId
HAVING      
    SUM(1.0* OI.Quantity*INTS.Quantity/(OI.Quantity*OI.Quantity+INTS.Quantity*INTS.Quantity-OI.Quantity*INTS.Quantity )) /
    (COUNT(*) + @ItemCount - COUNT(INTS.ItemId)) > 0.85
ORDER BY
    Similarity DESC

Ответ 6

Хм, смешно, я сейчас работаю над чем-то похожим. Почему бы вам просто не присоединиться к образцу заказа (т.е. Их предметов) со всеми другими заказами (их элементами) и перечислить все заказы, которые имеют совпадение не менее 85%, группируя количество совпадений за заказ?

-- let @SampleorderID be the ID of a sample

declare @totalOrders int, @ThresholdOrderCount int
select @totalOrders = count(*) from OrderItems where [email protected]

set @ThresholdOrderCount = 85*@totalOrders/100 -- 85% of the item amount of the sample

-- Now match the contents of the sample order with the contents of all other orders
-- count the #matches and show only those orders with at least 85% identical items
Select AllOrder.OrderID, count(*)
from   OrderItems sample 
       join OrderItems AllOrder 
         on sample.ItemID = AllOrder.ItemID
where sample.OrderID = @SampleOrderID 
  and sample.OrderID<>AllOrder.OrderID
group by AllOrder.OrderID 
having count(*)>@ThresholdOrderCount

Это должно сработать. Однако он также возвращает заказы, которые содержат больше элементов, чем образец. Если это нормально, то указанный выше запрос также должен быть довольно быстрым.

Ответ 7

Подход, который я хотел бы предпринять, заключался бы прежде всего в том, чтобы найти все порядокты, которые на 85% похожи на порядковые элементы для выбранного порядка, затем подсчитать количество этих порядковых номеров для каждого заказа и проверить, является ли количество элементов равным 85 %, подобный выбранному порядку, используя следующий запрос:

DECLARE @OrderId int = 2
SET @OrderId = 6

/*
Retrieve orderitems that match 85% with required @orderId
*/
;WITH SelectedOrderItemCount
AS
(
    SELECT COUNT(*) * 0.85 AS LowerBoundary, COUNT(*) * 1.15 AS UpperBoundary
    FROM OrderItem
    WHERE OrderId = @OrderId
)
SELECT OtherOrders.OrderId, COUNT(*) as NumberOfOrderItems
FROM OrderItem SpecificOrder
INNER JOIN OrderItem OtherOrders
    ON OtherOrders.ItemId = SpecificOrder.ItemId
WHERE SpecificOrder.OrderId = @OrderId AND
      OtherOrders.OrderId <> @OrderId AND
    OtherOrders.Quantity BETWEEN SpecificOrder.Quantity * 0.85 AND SpecificOrder.Quantity * 1.15
GROUP BY OtherOrders.OrderId
HAVING COUNT(*) BETWEEN (SELECT LowerBoundary FROM SelectedOrderItemCount) 
                    AND (SELECT UpperBoundary FROM SelectedOrderItemCount)

Полное демо SQLFiddle здесь

Ответ 8

Это один из видов проблемы с поиском данных. Поэтому вместо использования sql вы можете использовать алгоритм Apriori с поддержкой 85%. Реализация этого алгоритма свободно доступна во многих инструментах.