Design de banco de dados - objetos diferentes com marcação compartilhada

8

Minha formação é mais em programação da Web do que em administração de banco de dados; portanto, corrija-me se estiver usando a terminologia errada aqui. Estou tentando descobrir a melhor maneira de projetar o banco de dados para um aplicativo que codificarei.

A situação: eu tenho relatórios em uma tabela e recomendações em outra tabela. Cada relatório pode ter muitas recomendações. Eu também tenho uma tabela separada para palavras-chave (para implementar a marcação). No entanto, quero ter apenas um conjunto de palavras-chave que sejam aplicadas aos Relatórios e às Recomendações, para que a pesquisa em palavras-chave forneça Relatórios e Recomendações como resultados.

Aqui está a estrutura com a qual comecei:

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


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


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


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

Instintivamente, acho que isso não é o ideal e que meus objetos taggable sejam herdados de um pai comum e que esse pai de comentário seja marcado, o que daria a seguinte estrutura:

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)

Devo ir com esta segunda estrutura? Estou perdendo alguma preocupação importante aqui? Além disso, se eu for com o segundo, o que devo usar como um nome não genérico para substituir "Objeto"?

Atualizar:

Estou usando o SQL Server para este projeto. É um aplicativo interno com um pequeno número de usuários não concorrentes, por isso não prevejo uma carga alta. Em termos de uso, as palavras-chave provavelmente serão usadas com moderação. É basicamente apenas para fins de relatórios estatísticos. Nesse sentido, qualquer solução que eu escolher provavelmente afetará apenas os desenvolvedores que precisarão manter esse sistema abaixo da linha ... mas achei que seria bom implementar boas práticas sempre que possível. Obrigado por toda a visão!

matikin9
fonte
Parece que você não tem a pergunta mais importante respondida - como os dados serão acessados? - Para quais consultas / declarações você deseja "ajustar" seu modelo? - Como você planeja expandir a funcionalidade? Penso que não há boas práticas gerais - a solução depende das respostas a estas perguntas. E isso começa a importar mesmo em modelos simples como este. Ou você pode acabar com um modelo que segue alguns princípios mais altos, mas realmente é péssimo nos cenários mais importantes - aqueles vistos pelos usuários do sistema.
Štefan Oravec 02/03
Bom ponto! Vou ter que gastar algum tempo pensando sobre isso!
precisa saber é o seguinte

Respostas:

6

O problema com o seu primeiro exemplo é a tabela de três links. Isso exigirá que uma das chaves estrangeiras no relatório ou nas recomendações seja sempre NULL, para que as palavras-chave vinculem apenas uma maneira ou de outra?

No caso do seu segundo exemplo, a união da base às tabelas derivadas agora pode exigir o uso do seletor de tipo ou LEFT JOINs, dependendo de como você o faz.

Dado isso, por que não explicitar e eliminar todos os NULLs e LEFT JOINs?

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)

Nesse cenário, quando você adiciona outra coisa que precisa ser marcada, basta adicionar a tabela de entidades e a tabela de ligação.

Em seguida, seus resultados de pesquisa terão a seguinte aparência (veja se ainda existe uma seleção de tipos e os transforma em genéricos no nível de resultados do objeto, se você deseja uma única lista de resultados):

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

Não importa o que aconteça, em algum lugar haverá seleção de tipo e algum tipo de ramificação acontecendo.

Se você observar como faria isso em sua opção 1, é semelhante, mas com uma instrução CASE ou LEFT JOINs e um COALESCE. À medida que você expande sua opção 2 com mais itens vinculados, é necessário continuar adicionando mais LEFT JOINs onde normalmente não são encontrados itens (um objeto vinculado pode ter apenas uma tabela derivada válida).

Não acho que exista algo fundamentalmente errado com a sua opção 2, e você pode fazer com que pareça com esta proposta com o uso de visualizações.

Na sua opção 1, tenho algumas dificuldades para entender por que você optou pela tabela de três links.

Cade Roux
fonte
A tabela de três links que você mencionou provavelmente foi o resultado de eu ser mentalmente preguiçoso ...: P Depois de ler as várias respostas, acho que nenhuma das minhas opções iniciais faz sentido. Ter tabelas separadas ReportKeywords e RecommendKeywords separadas faz mais sentido prático. Eu estava pensando em escalabilidade, em termos de potencialmente ter mais objetos que precisassem de palavras-chave aplicadas, mas, realisticamente, provavelmente há apenas mais um tipo de objeto que poderia precisar de palavras-chave.
precisa saber é o seguinte
4

Primeiro, observe que a solução ideal depende, em certa medida, de qual RDBMS você usa. Vou dar então a resposta padrão e a específica do PostgreSQL.

Resposta normalizada e padrão

A resposta padrão é ter duas tabelas de junção.

Suponha que tenhamos nossas tabelas:

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)
);

Essa abordagem segue todas as regras de normalização padrão e não quebra os princípios tradicionais de normalização de banco de dados. Deve funcionar em qualquer RDBMS.

Resposta específica do PostgreSQL, design N1NF

Primeiro, uma palavra sobre por que o PostgreSQL é diferente. O PostgreSQL suporta várias maneiras muito úteis de usar índices sobre matrizes, principalmente usando o que é conhecido como índices GIN. Isso pode beneficiar bastante o desempenho se usado corretamente aqui. Como o PostgreSQL pode "acessar" os tipos de dados dessa maneira, a suposição básica de atomicidade e normalização é um tanto problemática para aplicar rigidamente aqui. Portanto, por esse motivo, minha recomendação seria quebrar a primeira regra de atomicidade da forma normal e confiar nos índices GIN para obter melhor desempenho.

Uma segunda observação aqui é que, embora isso ofereça melhor desempenho, ele adiciona algumas dores de cabeça, porque você terá algum trabalho manual para fazer para que a integridade referencial funcione corretamente. Portanto, a desvantagem aqui é o desempenho do trabalho manual.

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[]  
);

Agora temos que adicionar gatilhos para garantir que as palavras-chave sejam gerenciadas corretamente.

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();

Em segundo lugar, temos que decidir o que fazer quando uma palavra-chave é removida. Como está agora, uma palavra-chave removida da tabela de palavras-chave não entrará em cascata nos campos de palavras-chave. Talvez isso seja desejável e talvez não. A coisa mais simples a fazer é restringir sempre a exclusão e esperar que você lide manualmente com esse caso, se ele aparecer (use um gatilho para segurança aqui). Outra opção pode ser reescrever todos os valores de palavras-chave onde a palavra-chave existe para removê-la. Novamente, um gatilho seria a maneira de fazer isso também.

A grande vantagem desta solução é que você pode indexar pesquisas muito rápidas por palavra-chave e pode puxar todas as tags sem uma associação. A desvantagem é que remover uma palavra-chave é uma dor e não terá um bom desempenho, mesmo em um bom dia. Isso pode ser aceitável porque é um evento raro e pode ser enviado para um processo em segundo plano, mas é uma troca que vale a pena entender.

Criticando sua primeira solução

O verdadeiro problema com sua primeira solução é que você não possui uma chave possível no ObjectKeywords. Consequentemente, você tem um problema no qual não pode garantir que cada palavra-chave seja aplicada a cada objeto apenas uma vez.

Sua segunda solução é um pouco melhor. Se você não gostar das outras soluções oferecidas, sugiro que siga com ela. No entanto, eu sugeriria se livrar do keyword_id e apenas se juntar ao texto da palavra-chave. Isso elimina uma junção sem desnormalização.

Chris Travers
fonte
Estou usando o MS SQL Server para este projeto, mas obrigado pelas informações sobre o PostgreSQL. Os outros pontos que você mencionou sobre como excluir e garantir que os pares objeto-palavra-chave ocorram apenas uma vez. Mesmo que eu tivesse chaves para cada par objeto-palavra-chave, ainda não precisaria verificar antes de inserir? Quanto a ter uma identificação de palavra-chave separada ... Li que, para o SQL Server, ter uma string longa pode reduzir o desempenho e provavelmente terei que permitir que os usuários insiram "frases-chave" em vez de apenas "palavras-chave" "
precisa saber é o seguinte
0

Eu sugeriria duas estruturas separadas:

report_keywords
---------------
  ID do relatório
  ID da palavra-chave

recommend_keywords
-----------------------
  recomendação_id
  keyword_id

Dessa forma, você não possui todos os IDs de entidade possíveis na mesma tabela (o que não é muito escalável e pode ser confuso) e não possui uma tabela com um "id de objeto" genérico que você precisa desambiguar em outro lugar usando a base_objecttabela, que funcionará, mas acho que complica demais o design.

FrustratedWithFormsDesigner
fonte
Não discordo de que o que você está sugerindo seja uma opção viável, mas por que o RI não pode ser aplicado com o design B do OP? (Presumo que é isso que você está dizendo).
precisa saber é o seguinte
@ypercube: Eu acho que perdi a BaseObjectstabela na minha primeira leitura e pensei que estava vendo uma descrição para uma tabela onde object_idpode apontar para um ID em qualquer tabela.
FrustratedWithFormsDesigner
-1

Na minha experiência, é isso que você pode fazer.

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

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

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

E para a relação entre palavras-chave, relatórios e recomendações, você pode executar uma das duas opções: Opção A:

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

Isso permite uma relação direta entre Relatórios e Recomendações, Palavras-chave e, finalmente, Palavras-chave. Opção B:

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

A opção A é mais fácil de aplicar e gerenciar, uma vez que terá as constratints do banco de dados para lidar com a integridade dos dados e não permitirá a inserção de dados inválidos.

A opção B ainda requer um pouco mais de trabalho, pois você precisará codificar a identificação do relacionamento. É mais flexível a longo prazo, se por acaso, em algum momento no futuro, você precisar adicionar palavras-chave a outro item que não seja o relatório ou a recomendação, basta adicionar a identificação e usar diretamente a tabela.

Erxgli
fonte
Deixe-me explicar por que eu diminuí a votação: 1. Não está claro se você é a favor da opção A, B ou de uma terceira abordagem. Parece (para mim) que você diz que ambos estão mais ou menos bem (com o qual discordo, porque A tem vários problemas que outros descreveram com suas respostas. 2. Você está sugerindo fazer melhorias no design de A (ou B) "Também não está claro. Também seria bom definir os FKs claramente, não é óbvio o que você está sugerindo. No total, eu gosto de respostas que esclarecem coisas e opções para qualquer visitante futuro. Tente editar sua resposta e Eu vou reverter o meu voto.
ypercubeᵀᴹ