¿Está bien usar listas en una base de datos relacional?


94

Intenté diseñar una base de datos para un concepto de proyecto y me encontré con lo que parece ser un tema muy debatido. He leído algunos artículos y algunas respuestas de Stack Overflow que dicen que nunca (o casi nunca) está bien almacenar una lista de ID o similares en un campo: todos los datos deben ser relacionales, etc.

Sin embargo, el problema con el que me encuentro es que estoy tratando de hacer un asignador de tareas. Las personas crearán tareas, las asignarán a varias personas y se guardarán en la base de datos.

Por supuesto, si guardo estas tareas individualmente en "Persona", tendré que tener docenas de columnas "TaskID" ficticias y micro-administrarlas porque puede haber de 0 a 100 tareas asignadas a una persona, por ejemplo.

Por otra parte, si guardo las tareas en una tabla de "Tareas", tendré que tener docenas de columnas falsas de "PersonID" y micro-administrarlas, el mismo problema que antes.

Para un problema como este, ¿está bien guardar una lista de ID que toman una forma u otra o simplemente no estoy pensando en otra forma de lograrlo sin romper los principios?


22
Me doy cuenta de que esto está etiquetado como "base de datos relacional", así que lo dejaré como un comentario, no como una respuesta, pero en otros tipos de bases de datos tiene sentido almacenar listas. Cassandra viene a la mente ya que no tiene uniones.
Capitán Man

12
Buen trabajo en la investigación y luego preguntando aquí! De hecho, la "recomendación" de no violar nunca la primera forma normal fue realmente buena para usted, porque realmente debería encontrar otro enfoque relacional, es decir, una relación de "muchos a muchos", para la cual existe un patrón estándar en bases de datos relacionales que deben usarse.
JimmyB

66
"¿Alguna vez está bien" sí ... lo que sigue, la respuesta es sí. Siempre que tenga un motivo válido. Siempre hay un caso de uso que lo obliga a violar las mejores prácticas porque tiene sentido hacerlo. (En su caso, sin embargo, definitivamente no debería)
xyious

3
Actualmente estoy usando una matriz ( no una cadena delimitada - a VARCHAR ARRAY) para almacenar una lista de etiquetas. Probablemente no sea así como terminarán almacenándose más adelante en la línea, pero las listas pueden ser extremadamente útiles durante las etapas de creación de prototipos, cuando no tiene nada más que señalar y no desea construir todo el esquema de la base de datos antes de poder hacer cualquier otra cosa
Nic Hartley

3
@Ben " (aunque no serán indexables) ": en Postgres, varias consultas contra columnas JSON (y probablemente XML, aunque no lo haya verificado) son indexables.
Nic Hartley

Respuestas:


249

La palabra clave y el concepto clave que necesita investigar es la normalización de la base de datos .

Lo que haría, en lugar de agregar información sobre las asignaciones a las tablas de persona o tareas, es agregar una nueva tabla con esa información de asignación, con relaciones relevantes.

Ejemplo, tiene las siguientes tablas:

Personas:

+ −−−− + −−−−−−−−−−− +
El | ID | Nombre |
+ ==== + =========== +
El | 1 | Alfred |
El | 2 | Jebediah |
El | 3 | Jacob |
El | 4 | Ezequiel |
+ −−−− + −−−−−−−−−−− +

Tareas:

+ −−−− + −−−−−−−−−−−−−−−−−−--
El | ID | Nombre |
+ ==== + ==================== +
El | 1 | Alimentar a los pollos |
El | 2 | Arado |
El | 3 | Ordeñando vacas |
El | 4 | Levantar un granero |
+ −−−− + −−−−−−−−−−−−−−−−−−--

Luego crearía una tercera tabla con Asignaciones. Esta tabla modelaría la relación entre las personas y las tareas:

+ −−−− + −−−−−−−−−−− + −−−−−−−−− +
El | ID | PersonId | TaskId |
+ ==== + =========== + ========= +
El | 1 | 1 | 3 |
El | 2 | 3 | 2 |
El | 3 | 2 | 1 |
El | 4 | 1 | 4 |
+ −−−− + −−−−−−−−−−− + −−−−−−−−− +

Entonces tendríamos una restricción de clave externa, de modo que la base de datos imponga que PersonId y TaskIds tengan que ser ID válidos para esos elementos externos. Para la primera fila, podemos ver PersonId is 1, por lo que Alfred , se le asigna a TaskId 3, vacas de ordeño .

Lo que debería poder ver aquí es que podría tener la menor cantidad de tareas por tarea o por persona que desee. En este ejemplo, a Ezequiel no se le asigna ninguna tarea, y a Alfred se le asigna 2. Si tiene una tarea con 100 personas, al realizarla SELECT PersonId from Assignments WHERE TaskId=<whatever>;obtendrá 100 filas, con una variedad de personas asignadas diferentes. Puede WHEREen el PersonId encontrar todas las tareas asignadas a esa persona.

Si desea devolver consultas reemplazando los ID con los nombres y las tareas, puede aprender a UNIR tablas.


86
La palabra clave que desea buscar para obtener más información es "relación de muchos a muchos "
BlueRaja - Danny Pflughoeft

34
Para elaborar un poco sobre el comentario de Thierrys: puede pensar que no necesita normalizar porque solo necesito X y es muy simple almacenar la lista de ID , pero para cualquier sistema que pueda ampliarse más tarde, lamentará no haberlo normalizado. más temprano. Siempre normalizar ; la única pregunta es qué forma normal
Jan Doggen

8
De acuerdo con @Jan: en contra de mi mejor criterio, permití que mi equipo tomara un atajo de diseño hace un tiempo, almacenando JSON en su lugar para algo que "no necesitará ser extendido". Eso duró como seis meses FML. Nuestro actualizador tuvo una lucha desagradable en sus manos para migrar el JSON al esquema con el que deberíamos haber comenzado. Debería de saberlo mejor.
Carreras de ligereza en órbita el

13
@Deduplicator: es solo una representación de una columna de clave primaria de entero de incremento automático con variedad de jardín. Cosas bastante típicas.
cuál es el

8
@whatsisname En la tabla de Personas o Tareas, estoy de acuerdo con usted. ¿En una mesa de bridge donde el único propósito es representar la relación de muchos a muchos entre otras dos tablas que ya tienen claves sustitutas? No agregaría uno sin una buena razón. Es solo gastos generales, ya que nunca se utilizará en consultas o relaciones.
jpmc26

35

Estás haciendo dos preguntas aquí.

Primero, pregunta si está bien almacenar listas serializadas en una columna. Si esta bien. Si su proyecto lo requiere. Un ejemplo podría ser los ingredientes del producto para una página de catálogo, donde no desea tratar de rastrear cada ingrediente individualmente.

Lamentablemente, su segunda pregunta describe un escenario en el que debe optar por un enfoque más relacional. Necesitarás 3 mesas. Uno para las personas, uno para las tareas y otro que mantiene la lista de qué tarea se asigna a qué personas. El último sería vertical, una fila por combinación de persona / tarea, con columnas para su clave principal, ID de tarea e ID de persona.


99
El ejemplo de ingrediente al que hace referencia es correcto en la superficie; pero sería texto sin formato en ese caso. No es una lista en el sentido de la programación (a menos que quiera decir que la cadena es una lista de caracteres que obviamente no). OP que describe sus datos como "una lista de ID" (o incluso simplemente "una lista de [..]") implica que en algún momento están manejando estos datos como objetos individuales.
Flater

10
@Flater: Pero es una lista. Debe poder volver a formatearlo (de manera variada) como una lista HTML, una lista de Markdown, una lista JSON, etc., para asegurarse de que los elementos se muestran correctamente (de manera diversa) en una página web, un documento de texto sin formato, un dispositivo móvil aplicación ... y realmente no se puede hacer eso con texto sin formato.
Kevin

12
@Kevin Si ese es tu objetivo, entonces se logra mucho más fácil y fácilmente almacenando los ingredientes en una tabla. Sin mencionar si, más tarde, la gente ... oh, no sé, por ejemplo, desearía sustitutos recomendados , o algo tonto como buscar todas las recetas sin maní, ni gluten, ni proteínas animales ...
Dan Bron

10
@DanBron: YAGNI. En este momento solo estamos usando una lista porque facilita la lógica de la interfaz de usuario. Si necesitamos o necesitaremos un comportamiento similar a una lista en la capa de lógica de negocios, entonces debería normalizarse en una tabla separada. Las tablas y las uniones no son necesariamente caras, pero no son gratuitas y plantean preguntas sobre el orden de los elementos ("¿Nos importa el orden de los ingredientes?") Y una mayor normalización ("¿Va a convertir '3 huevos'? into ('eggs', 3)? ¿Qué pasa con 'Salt, al gusto', es eso ('salt', NULL)? ").
Kevin

77
@ Kevin: YAGNI está bastante equivocado aquí. Usted mismo argumentó la necesidad de poder transformar la lista de muchas maneras (HTML, markdown, JSON) y, por lo tanto, argumenta que necesita los elementos individuales de la lista . A menos que el almacenamiento de datos y las aplicaciones de "manejo de listas" sean dos aplicaciones que se desarrollan de forma independiente (¡y tenga en cuenta que las capas de aplicaciones separadas! = Aplicaciones separadas), la estructura de la base de datos siempre debe crearse para almacenar los datos en un formato que los deje fácilmente disponibles - mientras se evita la lógica de análisis / conversión adicional.
Flater

22

Lo que estás describiendo se conoce como una relación de "muchos a muchos", en tu caso entre Persony Task. Por lo general, se implementa usando una tercera tabla, a veces llamada tabla de "enlace" o "referencia cruzada". Por ejemplo:

create table person (
    person_id integer primary key,
    ...
);

create table task (
    task_id integer primary key,
    ...
);

create table person_task_xref (
    person_id integer not null,
    task_id integer not null,
    primary key (person_id, task_id),
    foreign key (person_id) references person (person_id),
    foreign key (task_id) references task (task_id)
);

2
También es posible que desee agregar un índice task_idprimero, si está haciendo consultas filtradas por tarea.
jpmc26

1
También se conoce como una mesa de bridge. Además, desearía poder darle una ventaja adicional por no tener una columna de identidad, aunque recomendaría un índice en cada columna.
jmoreno

13

... nunca (o casi nunca) está bien almacenar una lista de ID o similares en un campo

El único momento en que puede almacenar más de un elemento de datos en un solo campo es cuando ese campo solo se usa como una entidad única y nunca se considera que está compuesto por esos elementos más pequeños. Un ejemplo podría ser una imagen, almacenada en un campo BLOB. Está compuesto por muchos y muchos elementos más pequeños (bytes), pero estos no significan nada para la base de datos y solo se pueden usar todos juntos (y se ven bonitos para un Usuario final).

Dado que una "lista" está, por definición, compuesta de elementos más pequeños (elementos), este no es el caso aquí y debe normalizar los datos.

... si guardo estas tareas individualmente en "Persona", tendré que tener docenas de columnas falsas "TaskID" ...

No. Tendrá algunas filas en una tabla de intersección (también conocida como entidad débil) entre persona y tarea. Las bases de datos son realmente buenas para trabajar con muchas filas; en realidad son bastante basura trabajando con muchas columnas [repetidas].

Buen ejemplo claro dado por whatsisname.


44
Al crear sistemas de la vida real, "nunca digas nunca" es una muy buena regla para vivir.
l0b0

1
En muchos casos, el costo por elemento de mantener o recuperar una lista en forma normalizada puede exceder ampliamente el costo de mantener los elementos como una gota, ya que cada elemento de la lista tendría que tener la identidad del elemento maestro con el que está asociado y su ubicación dentro de la lista además de los datos reales. Incluso en los casos en que el código podría beneficiarse de poder actualizar algunos elementos de la lista sin actualizar la lista completa, podría ser más barato almacenar todo como un blob y reescribir todo cuando sea necesario reescribir algo.
supercat

4

Puede ser legítimo en ciertos campos calculados previamente.

Si algunas de sus consultas son costosas y decide ir con los campos precalculados que se actualizan automáticamente mediante los activadores de la base de datos, entonces puede ser legítimo mantener las listas dentro de una columna.

Por ejemplo, en la interfaz de usuario, desea mostrar esta lista utilizando la vista de cuadrícula, donde cada fila puede abrir detalles completos (con listas completas) después de hacer doble clic:

REGISTERED USER LIST
+------------------+----------------------------------------------------+
|Name              |Top 3 most visited tags                             |
+==================+====================================================+
|Peter             |Design, Fitness, Gifts                              |
+------------------+----------------------------------------------------+
|Lucy              |Fashion, Gifts, Lifestyle                           |
+------------------+----------------------------------------------------+

Mantiene la segunda columna actualizada por disparador cuando el cliente visita un nuevo artículo o una tarea programada.

Puede hacer que dicho campo esté disponible incluso para búsquedas (como texto normal).

Para tales casos, mantener listas es legítimo. Solo necesita considerar el caso de que posiblemente exceda la longitud máxima del campo.


Además, si está utilizando Microsoft Access, los campos multivalor ofrecidos son otro caso de uso especial. Manejan tus listas en un campo automáticamente.

Pero siempre puede recurrir a la forma normalizada estándar que se muestra en otras respuestas.


Resumen: Las formas normales de base de datos son modelos teóricos necesarios para comprender aspectos importantes del modelado de datos. Pero, por supuesto, la normalización no tiene en cuenta el rendimiento u otro costo de recuperar los datos. Está fuera del alcance de ese modelo teórico. Pero la implementación práctica a menudo requiere el almacenamiento de listas u otros duplicados precalculados (y controlados).

A la luz de lo anterior, en una implementación práctica, ¿preferiríamos que la consulta se base en una forma normal perfecta y se ejecute 20 segundos o una consulta equivalente que se base en valores precalculados que requieren 0.08 s? A nadie le gusta que su producto de software sea acusado de lentitud.


1
Puede ser legítimo incluso sin cosas precalculadas. Lo he hecho un par de veces donde los datos se almacenan correctamente, pero por razones de rendimiento es útil guardar algunos resultados en caché en los registros principales.
Loren Pechtel

@LorenPechtel - Sí, gracias, en mi uso del término precalculado también incluyo casos de valores almacenados en caché almacenados donde sea necesario. En sistemas con dependencias complejas, son la forma de mantener el rendimiento normal. Y si se programa con los conocimientos adecuados, estos valores son confiables y están siempre sincronizados. Simplemente no quería agregar un caso de almacenamiento en caché en la respuesta para mantener la respuesta simple y segura. Fue votado de todos modos. :)
miroxlav

@LorenPechtel En realidad, eso todavía sería una mala razón ... los datos de caché deben mantenerse en un almacén intermedio de caché, y aunque el caché sigue siendo válido, esa consulta nunca debe llegar a la base de datos principal.
Tezra

1
@Tezra No, digo que a veces se necesita un dato de una tabla secundaria con la frecuencia suficiente para que tenga sentido colocar una copia en el registro principal. (Ejemplo que he hecho: la tabla de empleados incluye la última vez que entró y la última vez que salió. Se usan solo con fines de visualización, cualquier cálculo real proviene de la tabla con los registros de entrada / salida)
Loren Pechtel

0

Dadas dos tablas; los llamaremos Persona y Tarea, cada uno con su propia ID (PersonID, TaskID) ... la idea básica es crear una tercera tabla para unirlos. Llamaremos a esta tabla PersonToTask. Como mínimo, debe tener su propia identificación, así como las otras dos. Entonces, cuando se trata de asignar a alguien a una tarea; ya no necesitará ACTUALIZAR la tabla Persona, solo tiene que INSERTAR una nueva línea en la Tabla PersonToTask. Y el mantenimiento se vuelve más fácil: la necesidad de eliminar una tarea simplemente se convierte en DELETE según el TaskID, ya no se actualiza la tabla Person y su análisis asociado

CREATE TABLE dbo.PersonToTask (
    pttID INT IDENTITY(1,1) NOT NULL,
    PersonID INT NULL,
    TaskID   INT NULL
)

CREATE PROCEDURE dbo.Task_Assigned (@PersonID INT, @TaskID INT)
AS
BEGIN
    INSERT PersonToTask (PersonID, TaskID)
    VALUES (@PersonID, @TaskID)
END

CREATE PROCEDURE dbo.Task_Deleted (@TaskID INT)
AS
BEGIN
    DELETE PersonToTask  WHERE TaskID = @TaskID
    DELETE Task          WHERE TaskID = @TaskID
END

¿Qué tal un informe simple o quién está asignado a una tarea?

CREATE PROCEDURE dbo.Task_CurrentAssigned (@TaskID INT)
AS
BEGIN
    SELECT PersonName
    FROM   dbo.Person
    WHERE  PersonID IN (SELECT PersonID FROM dbo.PersonToTask WHERE TaskID = @TaskID)
END

Por supuesto, podrías hacer mucho más; se podría hacer un Informe de tiempo si agrega campos de fecha y hora para TaskAssigned y TaskCompleted. Todo depende de usted


0

Puede funcionar si dice que tiene claves primarias legibles por humanos y desea una lista de números de tarea sin tener que lidiar con la naturaleza vertical de una estructura de tabla. es decir, mucho más fácil de leer la primera tabla.

------------------------  
Employee Name | Task 
Jack          |  1,2,5
Jill          |  4,6,7
------------------------

------------------------  
Employee Name | Task 
Jack          |  1
Jack          |  2
Jack          |  5
Jill          |  4
Jill          |  6
Jill          |  7
------------------------

La pregunta sería: si la lista de tareas debe almacenarse o generarse a pedido, lo que dependería en gran medida de requisitos tales como: con qué frecuencia se necesita la lista, cuán precisa es la cantidad de filas de datos, cómo se utilizarán los datos, etc. .. después de lo cual se debe analizar las compensaciones a la experiencia del usuario y cumplir con los requisitos.

Por ejemplo, comparar el tiempo que llevaría recuperar las 2 filas frente a ejecutar una consulta que generaría las 2 filas. Si lleva mucho tiempo y el usuario no necesita la lista más actualizada (* esperando menos de 1 cambio por día), entonces podría almacenarse.

O si el usuario necesita un registro histórico de las tareas que se le asignaron, también tendría sentido si la lista estuviera almacenada. Así que realmente depende de lo que estés haciendo, nunca digas nunca.


Como usted dice, todo depende de cómo se vayan a recuperar los datos. Si usted / solo / alguna vez consulta esta tabla por Nombre de usuario, entonces el campo "lista" es perfectamente adecuado. Sin embargo, ¿cómo puede consultar una tabla de este tipo para saber quién está trabajando en la Tarea # 1234567 y aún así mantener su rendimiento? Casi todos los tipos de función de cadena "find-X-anywhere-in-the-field" provocarán una consulta de este tipo en / Table Scan /, lo que ralentizará las cosas. Con datos correctamente normalizados e indexados correctamente, eso simplemente no sucede.
Phill W.

0

Estás tomando lo que debería ser otra mesa, girándola 90 grados y calzándola en otra mesa.

Es como tener una tabla de pedidos donde tiene itemProdcode1, itemQuantity1, itemPrice1 ... itemProdcode37, itemQuantity37, itemPrice37. Además de ser incómodo de manejar mediante programación, puede garantizar que mañana alguien querrá ordenar 38 cosas.

Solo lo haría a su manera si la 'lista' no es realmente una lista, es decir, dónde se encuentra en su conjunto y cada línea de pedido individual no se refiere a alguna entidad clara e independiente. En ese caso, solo tiene que incluir todo en algún tipo de datos lo suficientemente grande.

Por lo tanto, un pedido es una lista, una lista de materiales es una lista (o una lista de listas, lo que sería aún más una pesadilla para implementar "de lado"). Pero una nota / comentario y un poema no lo son.


0

Si "no está bien", entonces es bastante malo que cada sitio de Wordpress tenga una lista en wp_usermeta con wp_capabilities en una fila, lista de despedidos_wp_pointers en una fila, y otras ...

De hecho, en casos como este, podría ser mejor para la velocidad, ya que casi siempre querrá la lista . Pero Wordpress no es conocido por ser el ejemplo perfecto de las mejores prácticas.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.