Дизайн базы данных - разные объекты с общими тегами

8

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

Ситуация: у меня есть отчеты в одной таблице и рекомендации в другой таблице. Каждый отчет может иметь много рекомендаций. У меня также есть отдельная таблица для ключевых слов (для реализации тегов). Однако я хочу иметь только один набор ключевых слов, который будет применяться как к отчетам, так и к рекомендациям, чтобы поиск по ключевым словам давал вам отчеты и рекомендации в качестве результатов.

Вот структура, с которой я начал:

Reports
----------
ReportID
ReportName


Recommendations
----------
RecommendationID
RecommendationName
ReportID (foreign key)


Keywords
----------
KeywordID
KeywordName


ObjectKeywords
----------
KeywordID (foreign key)
ReportID (foreign key)
RecommendationID (foreign key)

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

BaseObjects
----------
ObjectID (primary key)
ObjectType


Reports
----------
ObjectID_Report (foreign key)
ReportName


Recommendations
----------
ObjectID_Recommendation (foreign key)
RecommendationName
ObjectID_Report (foreign key)


Keywords
----------
KeywordID (primary key)
KeywordName


ObjectKeywords
----------
ObjectID (foreign key)
KeywordID (foreign key)

Должен ли я пойти с этой второй структурой? Я пропускаю какие-то важные проблемы здесь? Кроме того, если я пойду со вторым, что я должен использовать в качестве неуниверсального имени вместо «Объект»?

Обновить:

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

matikin9
источник
Похоже, у вас нет ответа на самый важный вопрос - как будут доступны данные? - Для каких запросов / утверждений вы хотите «настроить» свою модель? - Как вы планируете расширить функционал? Я думаю, что нет общей передовой практики - решение зависит от ответов на эти вопросы. И это начинает иметь значение даже в таких простых моделях, как эта. Или вы можете получить модель, которая следует некоторым более высоким принципам, но в действительности учитывает самые важные сценарии - те, которые видят пользователи системы.
Штефан Оравец
Хорошая точка зрения! Мне придется потратить некоторое время на размышления об этом!
matikin9

Ответы:

6

Проблема с вашим первым примером - таблица с тремя связями. Будет ли требоваться, чтобы один из внешних ключей в отчете или рекомендациях всегда был равен NULL, чтобы ключевые слова связывались только так или иначе?

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

Учитывая это, почему бы просто не сделать это явным и исключить все значения NULL и LEFT JOIN?

Reports
----------
ReportID
ReportName


Recommendations
----------
RecommendationID
RecommendationName
ReportID (foreign key)


Keywords
----------
KeywordID
KeywordName


ReportKeywords
----------
KeywordID (foreign key)
ReportID (foreign key)

RecommendationKeywords
----------
KeywordID (foreign key)
RecommendationID (foreign key)

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

Тогда ваши результаты поиска будут выглядеть следующим образом (смотрите, что все еще происходит выбор типов и превращение их в универсальные на уровне результатов объекта, если вам нужен единый список результатов):

SELECT CAST('REPORT' AS VARCHAR(15)) AS ResultType
    ,Reports.ReportID AS ObjectID
    ,Reports.ReportName AS ObjectName
FROM Keywords
INNER JOIN ReportKeywords
    ON ReportKeywords.KeywordID = Keywords.KeywordID
INNER JOIN Reports
    ON Reports.ReportID = ReportKeywords.ReportID
WHERE Keywords.KeywordName LIKE '%' + @SearchCriteria + '%'
UNION ALL
SELECT 'RECOMMENDATION' AS ResultType
    ,Recommendations.RecommendationID AS ObjectID
    ,Recommendations.RecommendationName AS ObjectName
FROM Keywords
INNER JOIN RecommendationKeywords
    ON RecommendationKeywords.KeywordID = Keywords.KeywordID
INNER JOIN Recommendations
    ON Recommendations.RecommendationID = RecommendationKeywords.ReportID
WHERE Keywords.KeywordName LIKE '%' + @SearchCriteria + '%'

Неважно, что где-то там будет выбор типа и какое-то ветвление.

Если вы посмотрите на то, как вы это сделаете, в своем варианте 1, он похож, но с оператором CASE или LEFT JOINs и COALESCE. Поскольку вы расширяете свой вариант 2, добавляя больше связанных вещей, вы должны продолжать добавлять больше ЛЕВЫХ СОЕДИНЕНИЙ, где вещи обычно НЕ обнаруживаются (связанный объект может иметь только одну производную таблицу, которая является действительной).

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

В вашем варианте 1 мне трудно понять, почему вы выбрали таблицу с тремя ссылками.

Кейд Ру
источник
Таблица трех ссылок, о которой вы упомянули, вероятно, была результатом моей психической лени ...: P После прочтения различных ответов, я думаю, что ни один из моих первоначальных вариантов не имеет смысла. Наличие отдельных таблиц ReportKeywords и РекомендацииKeywords имеет больший практический смысл. Я рассматривал масштабируемость с точки зрения возможного применения большего количества объектов, для которых требовались ключевые слова, но на самом деле, возможно, существует только еще один тип объекта, которому могут понадобиться ключевые слова.
matikin9
4

Во-первых, обратите внимание, что идеальное решение в некоторой степени зависит от того, какую СУБД вы используете. Я собираюсь дать как стандартный, так и специфичный для PostgreSQL ответ.

Нормализованный стандартный ответ

Стандартный ответ - две таблицы соединений.

Предположим, у нас есть наши таблицы:

CREATE TABLE keywords (
     kword text
);

CREATE TABLE reports (
     id serial not null unique,
     ...
);

CREATE TABLE recommendations (
     id serial not null unique,
     ...
);

CREATE TABLE report_keywords (
     report_id int not null references reports(id),
     keyword text not null references keyword(kword),
     primary key (report_id, keyword)
);

CREATE TABLE recommendation_keywords (
     recommendation_id int not null references recommendation(id),
     keyword text not null references keyword(kword),
     primary key (recommendation_id, keyword)
);

Этот подход следует всем стандартным правилам нормализации и не нарушает традиционные принципы нормализации базы данных. Это должно работать на любой RDBMS.

PostgreSQL-специфичный ответ, дизайн N1NF

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

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

CREATE TABLE keyword (
    kword text primary key
);

CREATE FUNCTION check_keywords(in_kwords text[]) RETURNS BOOL LANGUAGE SQL AS $$

WITH kwords AS ( SELECT array_agg(kword) as kwords FROM keyword),
     empty AS (SELECT count(*) = 0 AS test FROM unnest($1)) 
SELECT bool_and(val = ANY(kwords.kwords))
  FROM unnest($1) val
 UNION
SELECT test FROM empty WHERE test;
$$;

CREATE TABLE reports (
     id serial not null unique,
     ...
     keywords text[]   
);

CREATE TABLE recommendations (
     id serial not null unique,
     ...
     keywords text[]  
);

Теперь нам нужно добавить триггеры, чтобы обеспечить правильное управление ключевыми словами.

CREATE OR REPLACE FUNCTION trigger_keyword_check() RETURNS TRIGGER
LANGUAGE PLPGSQL AS
$$
BEGIN
    IF check_keywords(new.keywords) THEN RETURN NEW
    ELSE RAISE EXCEPTION 'unknown keyword entered'
    END IF;
END;
$$;

CREATE CONSTRAINT TRIGGER check_keywords AFTER INSERT OR UPDATE TO reports
WHEN (old.keywords <> new.keywords)
FOR EACH ROW EXECUTE PROCEDURE trigger_keyword_check();

CREATE CONSTRAINT TRIGGER check_keywords AFTER INSERT OR UPDATE 
TO recommendations
WHEN (old.keywords <> new.keywords)
FOR EACH ROW EXECUTE PROCEDURE trigger_keyword_check();

Во-вторых, мы должны решить, что делать, когда ключевое слово удалено. В настоящее время ключевое слово, удаленное из таблицы ключевых слов, не будет каскадно добавляться к полям ключевых слов. Может быть, это желательно, а может и нет. Самое простое, что нужно сделать, это просто ограничить удаление всегда и ожидать, что вы вручную обработаете этот случай, если он возникнет (используйте триггер для безопасности здесь). Другой вариант может состоять в том, чтобы переписать каждое значение ключевого слова, в котором существует ключевое слово, для его удаления. Опять же, триггер был бы способом сделать это.

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

Критикуя ваше первое решение

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

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

Крис Траверс
источник
Я использую MS SQL Server для этого проекта, но спасибо за информацию о PostgreSQL. Остальные пункты, которые вы упомянули об удалении и проверке, что пары объект-ключевое слово встречаются только один раз. Даже если бы у меня были ключи для каждой пары объект-ключевое слово, мне все равно пришлось бы проверять перед вставкой? Что касается отдельного идентификатора ключевого слова ... Я читал, что для SQL Server наличие длинной строки может снизить производительность, и мне, вероятно, придется разрешить пользователям вводить "ключевые фразы", а не просто "ключевые слова". ».
matikin9
0

Я бы предложил две отдельные структуры:

report_keywords
---------------
  ID отчета
  Идентификатор ключевого слова

recommendation_keywords
-----------------------
  recommendation_id
  keyword_id

Таким образом, у вас не будет всех возможных идентификаторов сущностей в одной и той же таблице (что не очень масштабируемо и может привести к путанице), и у вас нет таблицы с общим «идентификатором объекта», которую вы должны устранить в другом месте используя base_objectтаблицу, которая будет работать, но я думаю, что усложняет дизайн.

FrustratedWithFormsDesigner
источник
Я не согласен с тем, что то, что вы предлагаете, является жизнеспособным вариантом, но почему RI не может быть реализовано с помощью конструкции B B? (Я полагаю, это то, что вы говорите).
ypercubeᵀᴹ
@ypercube: я думаю, что пропустил BaseObjectsтаблицу при первом прочтении и подумал, что вижу описание таблицы, в которой object_idможно указать идентификатор в любой таблице.
FrustratedWithFormsDesigner
-1

По моему опыту, это то, что вы можете сделать.

Reports
----------
Report_id (primary_key)
Report_name

Recommendations
----------------
Recommendation_id (primary key)
Recommendation_name
Report_id (foreign key)

Keywords
----------
Keyword_id (primary key)
Keyword

А для связи между ключевыми словами, отчетами и рекомендациями вы можете сделать один из двух вариантов: Вариант A:

Recommendation_keywords
------------------------
Recommendation_id(foreign_key)
keyword_id (foreign_key)

Это позволяет установить прямую связь между отчетами, рекомендациями, ключевыми словами и, наконец, ключевыми словами. Вариант Б:

object_keywords
---------------
Object_id
Object_type
Keyword_id(foreign_key)

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

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

Erxgli
источник
Позвольте мне объяснить, почему я отказался от голосования: 1. Не ясно, поддерживаете ли вы вариант A, B или третий подход. Мне кажется (вы), что вы говорите, что оба более или менее в порядке (с чем я не согласен, потому что у А есть несколько проблем, которые другие обрисовали в своих ответах. 2. Вы предлагаете улучшить дизайн А (или В)? «Это также не ясно. Было бы также хорошо, чтобы FK были четко определены, совсем не очевидно, что вы предлагаете. В целом мне нравятся ответы, которые проясняют вопросы и варианты для любого будущего посетителя. Пожалуйста, попробуйте отредактировать ваш ответ и Я переверну свой голос.
ypercubeᵀᴹ