2013-11-06 2 views
10

Я разрабатываю веб-приложение для витрин. Когда потенциальный клиент просматривает продукт на веб-сайте, я хотел бы предложить набор подобных продуктов из базы данных автоматически (vs требует, чтобы человек явно вводил данные/сопоставления данных о продуктах).Веб-сайт торговой марки ECommerce: Поиск похожих продуктов Программно

Фактически, когда вы думаете об этом, большинство баз данных магазинов уже имеют множество доступных данных о подобии. В моем случае Products может быть:

  • отображается на Manufacturer (ака Brand)
  • отображается на один или более Categories и
  • отображается на один или более Tags (ака Keywords).

Storefront Datamodel Snippet

Путем подсчета количества общих атрибутов между продуктом и всех остальных, можно вычислить «SimilarityScore» для сравнения других продуктов против одного просматриваемого заказчиком. Вот моя первая реализация прототипа:

;WITH ProductsRelatedByTags (ProductId, NumberOfRelations) 
AS 
(
    SELECT t2.ProductId, COUNT(t2.TagId) 
    FROM ProductTagMappings AS t1 INNER JOIN 
       ProductTagMappings AS t2 ON t1.TagId = t2.TagId AND t2.ProductId != t1.ProductId 
    WHERE t1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' 
    GROUP BY t2.ProductId 
), ProductsRelatedByCategories (ProductId, NumberOfRelations) 
AS 
(
    SELECT t2.ProductId, COUNT(t2.CategoryId) 
    FROM ProductCategoryMappings AS t1 INNER JOIN 
       ProductCategoryMappings AS t2 ON t1.CategoryId = t2.CategoryId AND t2.ProductId != t1.ProductId 
    WHERE t1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' 
    GROUP BY t2.ProductId 
) 
SELECT prbt.ProductId AS ProductId 
     ,IsNull(prbt.NumberOfRelations, 0) AS TagsInCommon 
     ,IsNull(prbc.NumberOfRelations, 0) AS CategoriesInCommon 
     ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId THEN 1 ELSE 0 END as SameManufacturer 
     ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId 
      THEN IsNull(prbt.NumberOfRelations, 0) + IsNull(prbc.NumberOfRelations, 0) + 1 
      ELSE IsNull(prbt.NumberOfRelations, 0) + IsNull(prbc.NumberOfRelations, 0) 
     END as SimilarityScore 
FROM Products AS SourceProduct, 
     Products AS SimilarProduct INNER JOIN 
     ProductsRelatedByTags prbt ON prbt.ProductId = SimilarProduct.Id FULL OUTER JOIN 
     ProductsRelatedByCategories prbc ON prbt.ProductId = prbc.ProductId 
WHERE SourceProduct.Id = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' 

который приводит данные, как это:

ProductId       TagsInCommon CategoriesInCommon SameManufacturer SimilarityScore 
------------------------------------ ------------ ------------------ ---------------- --------------- 
6416C19D-BA4F-4AE6-AB75-A25A0138B3A5 1   0     0    1 
77B2ECC0-E2EB-4C1B-A1E1-A25A0138BA19 1   0     0    1 
2D83276E-40CC-44D0-9DDF-A25A0138BE14 2   1     1    4 
E036BFE0-BBB5-450C-858C-A25A0138C21C 3   0     0    3 

Я не гуру производительности SQL, поэтому у меня есть следующие вопросы для вас SQL гуру:

  • Являются ли s (CTE) подходящими/оптимальными в этом прецеденте? (Они, похоже, облегчают чтение/выполнение SQL). Есть ли способ сохранить соединение там где-нибудь с учетом представленной выше модели?

и

  • ли это быть хорошим кандидатом для индексированного представления (для упорства) или же это добавить чрезмерные затраты на изменения исходных данных? В этом случае я сделаю это хранимой процедурой, которая обновит физическую таблицу SimilarProductMappings для любого данного продукта.
+1

Я не думаю, что вы можете использовать CTE в индексированном виде, как это. – RBarryYoung

+0

@BenSwayne. , , Сырое количество общих атрибутов - очень рудиментарная мера сходства. Вы обнаружите, что продукты, которые имеют много атрибутов, будут всплывать в верхней части списка для многих других продуктов. –

+0

@GordonLinoff - Вы правы, этот вопрос был больше о компоненте SQL моей задачи, чем до точности моего алгоритма подобия. Я предполагаю, что в игру приходят больше факторов и, возможно, также возможность добавлять весовые коэффициенты к каждому из факторов при объединении общей оценки сходства.В конце концов это несколько специфично для конкретного случая, и его необходимо будет настроить для некоторых приложений клиентов. Но это должно обеспечить хорошую «из коробки» откат реализации для 80% клиентов, которым не нужно многого. – BenSwayne

ответ

2

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

  1. CTEs - производные таблицы - это синтаксический сахар. Это не имеет разницы в производительности. Единственное преимущество в их использовании заключается в том, что вы можете повторно использовать их вместо копирования/вставки/ввода производной таблицы. Однако в этом случае вы не используете их повторно, так что это зависит от вас.

  2. Индексированные виды: Имейте в виду, что индексы на представлениях действуют как индексы на таблицах (таблицах) с небольшим исключением. Представьте себе, что другая таблица создается для вашего конкретного запроса/представления и хранится на диске для более быстрого поиска. Когда базовые данные изменяются, эти индексы должны обновляться. Да, это может создать огромное влияние на ресурсы. В общем, я бы предпочел, чтобы кто-то написал запрос, который использует индексы в базовой таблице, и если им нужно больше индексов для определенной цели, то посмотрите на это подробно, а не на целостность в представлении с несколькими таблицами. Это намного проще в обслуживании и намного легче понять, почему ваш CRUD занимает больше времени, чем ожидалось.В индексированном представлении нет ничего неправильного. Но будьте очень осторожны, добавив это в модель базы данных приложения, подобную этой, из-за сложности таблиц, которые обновляются, вставлены/удалены. Большинство наиболее подходящих применений для индексированного представления относятся к хранилищу данных. Независимо от того, не помещайте индекс в представление, не понимая, что он будет делать с таблицами для CRUD (создавать, читать, обновлять, удалять) операции. И в базе данных CRM или поддержки приложений я бы держался подальше от них, если не существует статической потребности, и это не влияет на производительность.

Прочитайте эту статью: http://technet.microsoft.com/en-us/library/ms187864(v=sql.105).aspx

Примечание о 3/4 пути вниз страницы, то это говорит о том, где не использовать один и я думаю, что ваш случай вписывается в 4/5 из сценариев, где вы НЕ должны использовать его.

  1. Что касается сохранения соединений ... имейте в виду, что соединения FULL OUTER являются одним из худших нарушителей эффективности. Мне кажется, единственная причина, по которой у вас есть, заключается в том, что вы не включаете производителя в ваши CTE. Вы можете просто включить его в свои CTE, а затем объединить количество совпадений cat/tag в последнем запросе, который вытаскивает его вместе, чтобы получить ваш результат. Таким образом, у вас есть только два левых внешних соединения (по одному на каждый CTE), а затем суммировать два счета вместе и группировать их по одному изготовителю (case statement), productId и т. Д.

  2. Окончание ... Я бы подумал о размещении всех это в де-нормированной таблице или, возможно, даже в кубе, где он предварительно рассчитан. Давайте рассмотрим несколько вещей о вашем требовании: a. Нужен ли показатель корреляции для продуктов? Если да, то почему? Это не является критическим моментом при добавлении/удалении новых продуктов. Любой, кто говорит, что он должен жить, вероятно, на самом деле не означает этого. b. Скорость поиска. Я мог бы переписать ваш запрос, используя временные таблицы, убедиться, что индексы правильные и т. Д., И придумать достаточно быстрый запрос в хранимой процедуре. Но я все еще собираю данные из БД, которые будут отображаться на каждом фронте магазина каждый раз, когда страница загружается. Если данные предварительно рассчитаны и хранятся в отдельной таблице продуктов и баллов для каждого продукта и индексируются по продукту, поиск будет очень быстрым. Вы могли бы перегружать и перезагружать таблицу в ETL почасово, ежечасно/независимо и не должны беспокоиться о поддержании индексов, которые перестраиваются каждый раз. Конечно, если ваш магазин находится на 24/7/365, вам нужно написать код на стороне базы данных, чтобы беспокоиться об управлении версиями, чтобы ваше приложение никогда не дождалось, если db находится в середине пересчета.

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

Надеюсь, что это поможет.

+0

Я мог бы добавить ... к пункту ответа майазо выше: количество атрибутов, которые вы в настоящее время отслеживаете, чтобы найти похожие продукты, может измениться/расшириться в будущем. Таким образом, наличие полностью отдельного процесса и таблицы, которая отслеживает результаты для «похожих» продуктов, даст возможность пересмотреть ее и изменить ее, не затрагивая остальную часть вашей структуры базы данных. – maplemale

+0

Благодарим Maplemale, я согласен с вашими предложениями здесь и продолжу разработку с использованием таблицы хранения и хранимой процедуры, которая может предлагать предложения через административный интерфейс или напрямую писать в таблицу. Это был интересный академический вопрос, мне понравилось! – BenSwayne

+0

Добро пожаловать. Мне понравился вопрос и документация/диаграмма и т. Д. Было очень ясно, что вы спрашивали, и увлекательная передовая практика. :) – maplemale

1

Как насчет несколько иного подхода?

;WITH ProductFindings (ProductId, NbrTags, NbrCategories) 
AS 
(
    SELECT t2.ProductId, COUNT(t2.TagId), 0 
    FROM ProductTagMappings AS t1 
    INNER JOIN 
      ProductTagMappings AS t2 ON t1.TagId  = t2.TagId 
            AND t1.ProductId != t2.ProductId 
    WHERE t1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' 
    GROUP BY t2.ProductId 

    UNION ALL 

    SELECT c2.ProductId, 0, COUNT(c2.CategoryId) 
    FROM ProductCategoryMappings AS c1 
    INNER JOIN 
      ProductCategoryMappings AS c2 ON c1.CategoryId = c2.CategoryId 
              AND c1.ProductId != c2.ProductId 
    WHERE c1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' 
    GROUP BY c2.ProductId 

), ProductTally (ProductId, TotTags, TotCategories) as 
(
    SELECT ProductID, sum(NbrTags), sum(NbrCategories) 
    FROM  ProductFindings 
    GROUP BY ProductID 
) 
SELECT Tot.ProductId  AS ProductId 
     ,Tot.TotTags  AS TagsInCommon 
     ,Tot.TotCategories AS CategoriesInCommon 
     ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId 
       THEN 1 
       ELSE 0 
     END    as SameManufacturer 
     ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId 
       THEN 1 
       ELSE 0 
     END + Tot.TotTags + Tot.TotCategories 
          as SimilarityScore 
FROM ProductTally as Tot 
INNER JOIN Products  AS SimilarProduct ON Tot.ProductID = SimilarProduct.Id 
INNER JOIN Products  AS SourceProduct ON SourceProduct.Id = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' 
+0

Спасибо за ваш вклад. Является ли это предположение превосходным по сравнению с моим SQL или просто альтернативным подходом для рассмотрения? Может быть, небольшое объяснение вашего мыслительного процесса может быть полезным. Я полагаю, что это сводилось бы к тому, будет ли ваш 'UNION ALL' с последующим' SUM() 'более эффективным, чем мой« FULL OUTER JOIN »? Одна вещь, которую я не знал до вашего сообщения, заключается в том, что вы можете ссылаться на ранее определенный CTE в последующем CTE в том же запросе. Это может быть полезно! – BenSwayne

+0

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

Смежные вопросы