Cómo almacenar información ordenada en una base de datos relacional

20

Estoy tratando de entender cómo almacenar correctamente la información ordenada en una base de datos relacional.

Un ejemplo:

Digamos que tengo una lista de reproducción, que consta de canciones. Dentro de mi base de datos relacional, tengo una tabla que Playlistscontiene algunos metadatos (nombre, creador, etc.). También tengo una tabla llamada Songs, que contiene un playlist_id, así como información específica de la canción (nombre, artista, duración, etc.).

Por defecto, cuando se agrega una nueva canción a una lista de reproducción, se agrega al final. Al realizar el pedido en Song-ID (ascendente), el orden será el orden de adición. Pero, ¿qué pasa si un usuario debería poder reordenar canciones en la lista de reproducción?

Se me ocurrieron algunas ideas, cada una con sus ventajas y desventajas:

  1. Una columna llamada order, que es un número entero . Cuando se mueve una canción, se cambia el orden de todas las canciones entre su posición anterior y la nueva, para reflejar el cambio. El inconveniente de esto es que se deben hacer muchas consultas cada vez que se mueve una canción, y el algoritmo de movimiento no es tan trivial como con las otras opciones.
  2. Una columna llamada order, que es un decimal ( NUMERIC). Cuando se mueve una canción, se le asigna el valor de coma flotante entre los dos números adyacentes. Inconveniente: los campos decimales ocupan más espacio y es posible que se quede sin precisión, a menos que se tenga cuidado de redistribuir el rango después de cada pocos cambios.
  3. Otra forma sería tener previousun nextcampo que haga referencia a otras canciones. (o son NULL en el caso de la primera y la última canción de la lista de reproducción en este momento; básicamente se crea una lista vinculada ). Inconveniente: las consultas como 'encontrar la X canción en la lista' ya no son de tiempo constante, sino lineal.

¿Cuál de estos procedimientos se usa con más frecuencia en la práctica? ¿Cuál de estos procedimientos es más rápido en bases de datos medianas y grandes? ¿Hay alguna otra forma de archivar esto?

EDITAR: En aras de la simplicidad, en el ejemplo, una canción solo pertenece a una lista de reproducción (una relación de muchos a uno). Por supuesto, también se podría usar una tabla de unión, por lo que la lista de reproducción de canciones es una relación de muchos a muchos (y aplicar una de las estrategias anteriores en esa tabla).

Qqwy
fuente
1
Puede usar la opción uno (ordenar como Entero) con 100 pasos. Entonces no necesita reordenar si mueve una canción, solo tome un valor entre 100. De vez en cuando, puede necesitar un nuevo renumeración para volver a tener espacios entre las canciones.
knut
44
"El inconveniente de esto es que se deben hacer muchas consultas cada vez que se mueve una canción"?! - update songorder set order = order - 1 where order >= 12 & order <= 42; update songorder set order = 42 where id = 123;Son dos actualizaciones, no treinta. Tres si quieres poner una restricción única en orden.
2
Use la opción uno a menos que sepa de hecho que necesita algo más. Un problema que enfrentan los programadores nuevos en las bases de datos es no comprender que las bases de datos son muy, muy buenas en este tipo de cosas. No tengas miedo de poner tu db a trabajar.
GrandmasterB
1
Queries like 'find the Xth Song in the list' are no longer constant-timetambién es cierto para la opción 2.
Doc Brown
2
@ MikeNakis: Parece costoso, pero todo el trabajo se está haciendo en el servidor, que (generalmente) está optimizado para este tipo de trabajo. No usaría esta técnica en una mesa con millones de filas, pero no la descartaría para una mesa con solo un par de miles.
TMN

Respuestas:

29

Las bases de datos están optimizadas para ciertas cosas. Actualizar muchas filas rápidamente es una de ellas. Esto se vuelve especialmente cierto cuando dejas que la base de datos haga su trabajo.

Considerar:

order song
1     Happy Birthday
2     Beat It
3     Never Gonna Give You Up
4     Safety Dance
5     Imperial March

Y si desea avanzar Beat Ithasta el final, tendría dos consultas:

update table 
  set order = order - 1
  where order >= 2 and order <= 5;

update table
  set order = 5
  where song = 'Beat It'

Y eso es. Esto aumenta muy bien con números muy grandes. Intente poner unos pocos miles de canciones en una hipotética lista de reproducción en su base de datos y vea cuánto tiempo lleva mover una canción de un lugar a otro. Como estos tienen formas muy estandarizadas:

update table 
  set order = order - 1
  where order >= ? and order <= ?;

update table
  set order = ?
  where song = ?

Tiene dos declaraciones preparadas que puede reutilizar de manera muy eficiente.

Esto proporciona algunas ventajas significativas: el orden de la tabla es algo sobre lo que puede razonar. La tercera canción tiene un orderde 3, siempre. La única forma de garantizar esto es usar enteros consecutivos como orden. El uso de listas seudoenlazadas o números decimales o enteros con espacios no le permitirá garantizar esta propiedad; en estos casos, la única forma de obtener la enésima canción es ordenar toda la tabla y obtener el enésimo registro.

Y realmente, esto es mucho más fácil de lo que piensas. Es simple descubrir lo que quiere hacer, generar las dos declaraciones de actualización y que otras personas vean esas dos declaraciones de actualización y se den cuenta de lo que se está haciendo.

vedant
fuente
2
Me está empezando a gustar este enfoque.
Mike Nakis
2
@ MikeNakis funciona bien. También hay un árbol binario que se basa en una idea similar: el árbol de pedidos modificado . Se necesita un poco más para entenderlo, pero le permite hacer algunas consultas muy agradables para obtener datos jerárquicos. Nunca he tenido problemas de rendimiento, incluso en árboles grandes. Poder razonar sobre el código es algo en lo que pongo gran énfasis hasta que se demuestre que el código simple carece del rendimiento necesario (y eso solo ha sido en situaciones extremas).
¿Habrá algún problema con el uso orderya que order byes una palabra clave?
kojow7
@ kojow7, si sus campos tienen nombres en conflicto con las palabras clave, debe incluirlos en las marcas de verificación "` ".
Andri
Este enfoque tiene sentido, pero cuál es la mejor manera de obtener el ordervalor al agregar una nueva canción a una lista de reproducción. Digamos que es la novena canción, ¿hay alguna forma mejor de insertar 9 en orderhacer una COUNTantes de agregar el registro?
Delashum
3

En primer lugar, no está claro en su descripción de lo que ha hecho, pero necesita una PlaylistSongstabla que contenga a PlaylistIdy a SongId, que describa qué canciones pertenecen a qué listas de reproducción.

Es en esta tabla donde debe agregar la información de pedido.

Mi mecanismo favorito es con números reales. Lo implementé recientemente y funcionó de maravilla. Cuando desee mover una canción a una posición específica, calcule su nuevo Orderingvalor como el promedio de los Orderingvalores de la canción anterior y la siguiente. Si usa un número real de 64 bits, se quedará sin precisión aproximadamente al mismo tiempo que el infierno se congelará, pero si realmente está escribiendo su software para la posteridad, considere reasignar Orderingvalores enteros redondeados a todas las canciones de cada canción. lista de reproducción de vez en cuando.

Como una ventaja adicional, aquí está el código que he escrito que implementa esto. Por supuesto, no puede usarlo como está, y sería demasiado trabajo para mí en este momento desinfectarlo para usted, por lo que solo lo estoy publicando para que obtenga ideas de él.

La clase es ParameterTemplate(¡lo que sea, no pregunte!) El método obtiene la lista de plantillas de parámetros a las que pertenece esta plantilla de su padre ActivityTemplate. (¡Lo que sea, no preguntes!) El código contiene algo de protección contra la falta de precisión. El divisor se usa para las pruebas: la prueba de la unidad usa un divisor grande para quedarse sin precisión rápidamente, y así activar el código de protección de precisión. El segundo método es público y "solo para uso interno; no invocar" para que el código de prueba pueda invocarlo. (No podría ser un paquete privado porque mi código de prueba no está en el mismo paquete que el código que prueba). El campo que controla el pedido se llama Ordering, se accede a través de getOrdering()y setOrdering(). No ve ningún SQL porque estoy usando el mapeo relacional de objetos a través de Hibernate.

/**
 * Moves this {@link ParameterTemplate} to the given index in the list of {@link ParameterTemplate}s of the parent {@link ActivityTemplate}.
 *
 * The index must be greater than or equal to zero, and less than or equal to the number of entries in the list.  Specifying an index of zero will move this item to the top of
 * the list. Specifying an index which is equal to the number of entries will move this item to the end of the list.  Any other index will move this item to the position
 * specified, also moving other items in the list as necessary. The given index cannot be equal to the current index of the item, nor can it be equal to the current index plus
 * one.  If the given index is below the current index of the item, then the item will be moved so that its new index will be equal to the given index.  If the given index is
 * above the current index, then the new index of the item will be the given index minus one.
 *
 * NOTE: this method flushes the persistor and refreshes the parent node so as to guarantee that the changes will be immediately visible in the list of {@link
 * ParameterTemplate}s of the parent {@link ActivityTemplate}.
 *
 * @param toIndex the desired new index of this {@link ParameterTemplate} in the list of {@link ParameterTemplate}s of the parent {@link ActivityTemplate}.
 */
public void moveAt( int toIndex )
{
    moveAt( toIndex, 2.0 );
}

/**
 * For internal use only; do not invoke.
 */
public boolean moveAt( int toIndex, double divisor )
{
    MutableList<ParameterTemplate<?>> parameterTemplates = getLogicDomain().getMutableCollections().newArrayList();
    parameterTemplates.addAll( getParentActivityTemplate().getParameterTemplates() );
    assert parameterTemplates.getLength() >= 1; //guaranteed since at the very least, this parameter template must be in the list.
    int fromIndex = parameterTemplates.indexOf( this );
    assert 0 <= toIndex;
    assert toIndex <= parameterTemplates.getLength();
    assert 0 <= fromIndex;
    assert fromIndex < parameterTemplates.getLength();
    assert fromIndex != toIndex;
    assert fromIndex != toIndex - 1;

    double order;
    if( toIndex == 0 )
    {
        order = parameterTemplates.fetchFirstElement().getOrdering() - 1.0;
    }
    else if( toIndex == parameterTemplates.getLength() )
    {
        order = parameterTemplates.fetchLastElement().getOrdering() + 1.0;
    }
    else
    {
        double prevOrder = parameterTemplates.get( toIndex - 1 ).getOrdering();
        parameterTemplates.moveAt( fromIndex, toIndex );
        double nextOrder = parameterTemplates.get( toIndex + (toIndex > fromIndex ? 0 : 1) ).getOrdering();
        assert prevOrder <= nextOrder;
        order = (prevOrder + nextOrder) / divisor;
        if( order <= prevOrder || order >= nextOrder ) //if the accuracy of the double has been exceeded
        {
            parameterTemplates.clear();
            parameterTemplates.addAll( getParentActivityTemplate().getParameterTemplates() );
            for( int i = 0; i < parameterTemplates.getLength(); i++ )
                parameterTemplates.get( i ).setOrdering( i * 1.0 );
            rocs3dDomain.getPersistor().flush();
            rocs3dDomain.getPersistor().refresh( getParentActivityTemplate() );
            moveAt( toIndex );
            return true;
        }
    }
    setOrdering( order );
    rocs3dDomain.getPersistor().flush();
    rocs3dDomain.getPersistor().refresh( getParentActivityTemplate() );
    assert getParentActivityTemplate().getParameterTemplates().indexOf( this ) == (toIndex > fromIndex ? toIndex - 1 : toIndex);
    return false;
}
Mike Nakis
fuente
Usaría un pedido de enteros y si sintiera que reordenar era demasiado costoso, simplemente reduciría el número de reordenamientos, haciendo que cada uno salte por X, donde X es la cantidad que necesito para reducir el reordenamiento, digamos 20, que debería estar bien como entrante.
Warren P
1
@WarrenP sí, lo sé, también se puede hacer de esta manera, por eso acabo de llamar a este enfoque "mi favorito" en lugar de "el mejor" o "el único".
Mike Nakis
0

Lo que funcionó para mí, para una pequeña lista del orden de 100 artículos, fue adoptar un enfoque híbrido:

  1. Columna SortOrder decimal, pero con la precisión suficiente para almacenar una diferencia de 0,5 (es decir, decimal (8,2) o algo así).
  2. Al ordenar, tome las PK de la fila arriba y abajo donde se movió la fila actual, si existen. (No tendrá una fila arriba si mueve el elemento a la primera posición, por ejemplo)
  3. Publique las PK de la fila actual, anterior y siguiente en el servidor para realizar la ordenación.
  4. Si tiene una fila anterior, establezca la posición de la fila actual en prev + 0.5. Si solo tiene un siguiente, establezca la posición de la fila actual en siguiente: 0.5.
  5. A continuación, tengo un proceso almacenado que actualiza todas las posiciones utilizando la función Row_Number de SQL Server, ordenando por el nuevo orden de clasificación. Esto transformará el orden de 1,1.5,2,3,4,6 a 1,2,3,4,5,6, ya que la función row_number le da ordinales enteros.

Entonces terminas con un orden entero sin espacios, almacenado en una columna decimal. Es bastante limpio, creo. Pero es posible que no se amplíe extremadamente bien una vez que tenga cientos de miles de filas que necesita actualizar, todo a la vez. Pero si lo hace, ¿por qué está utilizando un tipo definido por el usuario en primer lugar? (Nota: si tiene una tabla grande con millones de usuarios pero cada usuario solo tiene unos cientos de elementos para ordenar, puede usar el enfoque anterior muy bien ya que de todos modos usará una cláusula where para limitar los cambios a un solo usuario )

John
fuente