Diseño de base de datos: diferentes objetos con etiquetado compartido

8

Mi experiencia es más en programación web que en administración de bases de datos, así que corríjame si estoy usando la terminología incorrecta aquí. Estoy tratando de encontrar la mejor manera de diseñar la base de datos para una aplicación que codificaré.

La situación: tengo Informes en una tabla y Recomendaciones en otra tabla. Cada informe puede tener muchas recomendaciones. También tengo una tabla separada para palabras clave (para implementar el etiquetado). Sin embargo, quiero tener solo un conjunto de palabras clave que se apliquen tanto a los Informes como a las Recomendaciones para que la búsqueda de palabras clave le brinde informes y recomendaciones como resultados.

Aquí está la estructura con la que comencé:

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


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


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


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

Instintivamente, siento que esto no es óptimo y que debería hacer que mis objetos etiquetables hereden de un padre común, y que ese comentario sea etiquetado, lo que daría la siguiente estructura:

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)

¿Debo ir con esta segunda estructura? ¿Me estoy perdiendo alguna preocupación importante aquí? Además, si voy con el segundo, ¿qué debo usar como nombre no genérico para reemplazar "Objeto"?

Actualizar:

Estoy usando SQL Server para este proyecto. Es una aplicación interna con una pequeña cantidad de usuarios no concurrentes, por lo que no preveo una gran carga. En términos de uso, las palabras clave probablemente se usarán con moderación. Es prácticamente solo para fines de informes estadísticos. En ese sentido, cualquier solución que elija probablemente solo afectará a los desarrolladores que necesiten mantener este sistema en el futuro ... pero pensé que es bueno implementar buenas prácticas siempre que pueda. ¡Gracias por toda la perspectiva!

matikin9
fuente
Parece que no tiene la pregunta más importante respondida: ¿cómo se accederá a los datos? - ¿Para qué consultas / afirmaciones desea "ajustar" su modelo? - ¿Cómo planea expandir la funcionalidad? Creo que no existe una mejor práctica general: la solución depende de las respuestas a estas preguntas. Y comienza a importar incluso en los modelos simples como este. O puede terminar con un modelo que sigue algunos principios superiores pero que realmente apesta a los escenarios más importantes: los que ven los usuarios del sistema.
Štefan Oravec
¡Buen punto! ¡Tendré que pasar un tiempo pensando en esto!
matikin9

Respuestas:

6

El problema con su primer ejemplo es la tabla de tres enlaces. ¿Eso requerirá que una de las claves foráneas en el informe o las recomendaciones sea siempre NULL para que las palabras clave se vinculen solo de una manera u otra?

En el caso de su segundo ejemplo, la unión desde la base a las tablas derivadas ahora puede requerir el uso del selector de tipo o uniones IZQUIERDA dependiendo de cómo lo haga.

Dado eso, ¿por qué no hacerlo explícito y eliminar todos los NULL y 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)

En este escenario, cuando agrega algo más que necesita ser etiquetado, simplemente agrega la tabla de entidades y la tabla de enlaces.

Luego, los resultados de su búsqueda se verán así (vea si todavía hay una selección de tipo en curso y los convierte en genéricos en el nivel de resultados del objeto si desea una sola 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 + '%'

No importa qué, en algún lugar habrá selección de tipo y algún tipo de ramificación.

Si observa cómo haría esto en su opción 1, es similar pero con una declaración CASE o uniones izquierdas y una COALESCE. A medida que expande su opción 2 con más elementos vinculados, debe seguir agregando más IZQUIERDAS IZQUIERDAS donde normalmente no se encuentran elementos (un objeto vinculado solo puede tener una tabla derivada que sea válida).

No creo que haya algo fundamentalmente incorrecto con su opción 2, y en realidad podría hacer que parezca esta propuesta con un uso de vistas.

En su opción 1, tengo algunas dificultades para ver por qué optó por la tabla de tres enlaces.

Cade Roux
fuente
La tabla de tres enlaces que mencionas probablemente fue el resultado de mi pereza mental ...: P Después de leer las diversas respuestas, creo que ninguna de mis opciones iniciales tiene sentido. Tener tablas separadas de ReportKeywords y RecomendaciónKeywords tiene más sentido práctico. Estaba considerando la escalabilidad, en términos de tener potencialmente más objetos que necesitaban palabras clave aplicadas, pero en realidad probablemente solo haya un tipo de objeto más que pueda necesitar palabras clave.
matikin9
4

Primero, tenga en cuenta que la solución ideal depende en cierta medida del RDBMS que utilice. Voy a dar entonces la respuesta estándar y la respuesta específica de PostgreSQL.

Respuesta normalizada, estándar

La respuesta estándar es tener dos tablas de unión.

Supongamos que tenemos nuestras tablas:

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

Este enfoque sigue todas las reglas de normalización estándar y no rompe los principios tradicionales de normalización de la base de datos. Debería funcionar en cualquier RDBMS.

Respuesta específica de PostgreSQL, diseño N1NF

Primero, una palabra sobre por qué PostgreSQL es diferente. PostgreSQL admite varias formas muy útiles de usar índices sobre matrices, sobre todo usando lo que se conoce como índices GIN. Estos pueden beneficiar bastante el rendimiento si se usan correctamente aquí. Debido a que PostgreSQL puede "alcanzar" los tipos de datos de esta manera, el supuesto básico de atomicidad y normalización es algo problemático de aplicar rígidamente aquí. Por esta razón, mi recomendación sería romper la regla de atomicidad de la primera forma normal y confiar en los índices GIN para un mejor rendimiento.

Una segunda nota aquí es que, si bien esto proporciona un mejor rendimiento, agrega algunos dolores de cabeza porque tendrá que realizar un trabajo manual para lograr que la integridad referencial funcione correctamente. Entonces, la compensación aquí es el rendimiento para el trabajo 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[]  
);

Ahora tenemos que agregar desencadenantes para garantizar que las palabras clave se administren correctamente.

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

En segundo lugar, tenemos que decidir qué hacer cuando se elimina una palabra clave. Tal como está ahora, una palabra clave eliminada de la tabla de palabras clave no se aplicará en cascada a los campos de palabras clave. Tal vez esto sea deseable y tal vez no. Lo más simple es restringir la eliminación siempre y esperar que maneje manualmente este caso si aparece (use un disparador para seguridad aquí). Otra opción puede ser reescribir cada valor de palabras clave donde exista la palabra clave para eliminarla. Una vez más, un disparador sería la forma de hacerlo también.

La gran ventaja de esta solución es que puede indexar búsquedas muy rápidas por palabra clave, y puede extraer todas las etiquetas sin una unión. La desventaja es que eliminar una palabra clave es una molestia y no funcionará bien incluso en un buen día. Esto puede ser aceptable porque es un evento raro y podría ser enviado a un proceso en segundo plano, pero es una compensación que vale la pena entender.

Criticando su primera solución

El verdadero problema con su primera solución es que no tiene una clave posible en ObjectKeywords. En consecuencia, tiene un problema en el que no puede garantizar que cada palabra clave se aplique a cada objeto solo una vez.

Su segunda solución es un poco mejor. Si no le gustan las otras soluciones ofrecidas, le sugiero que lo haga. Sin embargo, sugeriría deshacerse de keyword_id y simplemente unirse al texto de la palabra clave. Eso elimina una unión sin desnormalizar.

Chris Travers
fuente
Estoy usando MS SQL Server para este proyecto, pero gracias por la información sobre PostgreSQL. Los otros puntos que mencionó sobre eliminar y asegurarse de que los pares de objeto-palabra clave ocurran solo una vez. Sin embargo, incluso si tuviera claves para cada par de objeto-palabra clave, ¿no tendría que verificar antes de insertar? En cuanto a tener una identificación de palabra clave separada ... Leí que para SQL Server, tener una cadena larga podría reducir el rendimiento, y probablemente voy a tener que permitir que los usuarios ingresen "frases clave" en lugar de solo "palabras clave". ".
matikin9
0

Sugeriría dos estructuras separadas:

report_keywords
---------------
  ID de informe
  ID de palabra clave

recomendación_palabras clave
-----------------------
  ID de recomendación
  palabra clave_id

De esta forma, no tiene todos los identificadores de entidad posibles en la misma tabla (que no es muy escalable, y podría ser confuso), y no tiene una tabla con un "ID de objeto" genérico que deba desambiguar en otro lugar usando la base_objecttabla, que funcionará, pero creo que complica el diseño.

FrustratedWithFormsDesigner
fuente
No estoy en desacuerdo con que lo que sugiere es una opción viable, pero ¿por qué no se puede aplicar RI con el diseño B de OP? (Supongo que eso es lo que estás diciendo).
ypercubeᵀᴹ
@ypercube: Creo que me perdí la BaseObjectstabla en mi primera lectura y pensé que estaba viendo una descripción de una tabla donde object_idpuede apuntar a una ID en cualquier tabla.
FrustratedWithFormsDesigner
-1

En mi experiencia, esto es lo que puedes hacer.

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

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

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

Y para la relación entre palabras clave, informes y recomendaciones, puede hacer una de dos opciones: Opción A:

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

Esto permite una relación directa de Informes a Recomendaciones, a Palabras clave y finalmente a Palabras clave. Opcion B:

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

La opción A es la más fácil de aplicar y administrar, ya que tendrá las restricciones de la base de datos para manejar la integridad de los datos y no permitirá la inserción de datos no válidos.

La opción B, aunque requiere un poco más de trabajo, ya que tendrá que codificar la identificación de la relación. Es más flexible a largo plazo, si por casualidad en algún momento en el futuro necesita agregar palabras clave a otro elemento que no sea el informe o la recomendación, solo necesita agregar la identificación y usar directamente la tabla.

Erxgli
fuente
Permítanme explicar por qué voté en contra: 1. No está claro si está a favor de la opción A, B o un tercer enfoque. Parece (para mí) que usted dice que ambos están más o menos bien (con lo que no estoy de acuerdo porque A tiene varios problemas que otros han descrito con sus respuestas. 2. ¿Está sugiriendo hacer mejoras en el diseño de A (o B) "No está claro tampoco. También sería bueno tener los FK definidos claramente, no es del todo obvio lo que estás sugiriendo. En total me gustan las respuestas que aclaran las cosas y las opciones para cualquier visitante futuro. Por favor, trata de editar tu respuesta y Revertiré mi voto.
ypercubeᵀᴹ