Веб-сайт торговой марки ECommerce: поиск программных продуктов
Я разрабатываю веб-приложение магазина. Когда потенциальный клиент просматривает продукт на веб-сайте, я хотел бы предложить набор подобных продуктов из базы данных автоматически (vs требует, чтобы человек явно вводил данные/сопоставления данных о продуктах).
Фактически, когда вы думаете об этом, большинство баз данных магазина уже имеют много доступных данных о сходстве. В моем случае Products
может быть:
- отображается на
Manufacturer
(aka Brand
),
- сопоставленный с одним или несколькими
Categories
, и
- сопоставленный с одним или несколькими
Tags
(aka 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:
- common-table-expression (CTEs)/оптимальный в этом случае использования? (Они, похоже, облегчают чтение/выполнение SQL). Есть ли способ сохранить соединение там где-нибудь с учетом представленной выше модели?
и
- Будет ли это хорошим кандидатом для индексированного представления (for persistance) или это добавит чрезмерную стоимость изменений исходных данных? В этом случае я сделаю это хранимой процедурой, которая обновляет физическую таблицу
SimilarProductMappings
для любого данного продукта.
Ответы
Ответ 1
Вы задаете много вопросов. Я попытаюсь обратиться к каждому из них, не вдаваясь в подробности.
-
CTEs и производные таблицы - это синтаксический сахар. Это не имеет разницы в производительности. Единственное преимущество в их использовании заключается в том, что вы можете повторно использовать их вместо копирования/вставки/ввода производной таблицы. Однако вы не используете их повторно в этом случае, поэтому это зависит от вас.
-
Индексированные представления. Имейте в виду, что индексы в представлениях действуют как индексы в таблице (таблицах) с небольшим исключением. Представьте себе, что другая таблица создается для вашего конкретного запроса/представления и хранится на диске для более быстрого поиска. Когда базовые данные изменяются, эти индексы должны обновляться. Да, это может создать огромное влияние на ресурсы. В общем, я бы предпочел, чтобы кто-то написал запрос, который использует индексы в базовой таблице, и если им нужно больше индексов для определенной цели, то посмотрите на это подробно, а не на целостность в представлении с несколькими таблицами. Это намного проще в обслуживании и намного легче понять, почему ваш CRUD занимает больше времени, чем ожидалось. В индексированном представлении нет ничего неправильного. Но будьте очень осторожны, добавив это в модель базы данных приложения, подобную этой, из-за сложности таблиц, которые обновлены/вставлены/удалены. Большинство наиболее подходящих применений для индексированного представления относятся к хранилищу данных. Независимо от того, не помещайте индекс в представление, не понимая, что он будет делать с таблицами для CRUD (создавать, читать, обновлять, удалять) операции. И в CRM или приложении поддержки типа базы данных я бы держался подальше от них по большей части, если нет статической потребности, и это не влияет на производительность.
Прочтите эту статью: http://technet.microsoft.com/en-us/library/ms187864(v=sql.105).aspx
Обратите внимание на 3/4 пути вниз по странице, на которой он говорит о том, где НЕ использовать его, и я думаю, что ваш случай соответствует 4/5 сценариям, в которых вы НЕ должны его использовать.
-
Что касается спасительных объединений... имейте в виду, что FULL OUTER объединяет одно из худших нарушителей эффективности. Мне кажется, единственная причина, по которой у вас есть, заключается в том, что вы не включаете производителя в ваши CTE. Вы можете просто включить его в свои CTE, а затем объединить количество совпадений cat/tag в последнем запросе, который вытаскивает его вместе, чтобы получить ваш результат. Таким образом, у вас есть только два левых внешних соединения (по одному на каждый CTE), а затем суммируйте два счета вместе и группируйте их по одному изготовителю (case case), productId и т.д.
-
Наконец... Я бы подумал о том, чтобы разместить все это в де-нормированной таблице или, возможно, даже в кубе, где он был предварительно рассчитан. Давайте рассмотрим несколько вещей о вашем требовании:
а. Нужен ли показатель корреляции для продуктов? Если да, то почему? Это не является критическим моментом при добавлении/удалении новых продуктов. Любой, кто говорит, что он должен жить, вероятно, на самом деле не означает этого.
б. Скорость поиска. Я мог бы переписать ваш запрос, используя временные таблицы, убедиться, что индексы правильные и т.д., И придумать достаточно быстрый запрос в хранимой процедуре. Но я все еще собираю данные из БД, которые будут отображаться на каждом фронте магазина каждый раз, когда страница загружается. Если данные предварительно рассчитаны и хранятся в отдельной таблице продуктов и баллов для каждого продукта и индексируются по продукту, поиск будет очень быстрым. Вы могли бы перегружать и перезагружать таблицу в ETL почасово, ежечасно/независимо и не должны беспокоиться о поддержании индексов, которые перестраиваются каждый раз. Конечно, если ваш магазин находится на 24/7/365, вам нужно будет написать код на стороне базы данных, чтобы беспокоиться об управлении версиями, чтобы ваше приложение никогда не ожидало, если db находится в середине пересчета.
Кроме того, убедитесь, что вы по крайней мере кэшируете эту информацию на веб-сервере или сервере приложений, если ничего другого. Одно можно сказать наверняка, если вы перейдете к своему решению выше, тогда вам нужно будет что-то создать на своем сайте, чтобы он не дождался возвращения данных и вместо этого кэшировал его.
Надеюсь, что все это поможет.
Ответ 2
Как насчет несколько иного подхода?
;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'