Estoy trabajando en un sistema de lista de deseos, donde los usuarios pueden agregar elementos a sus diversas listas de deseos, y planeo permitir que los usuarios vuelvan a ordenar los elementos más adelante. No estoy realmente seguro de cuál es la mejor manera de almacenar esto en una base de datos mientras permanezco rápido y no se convierte en un desastre (esta aplicación será utilizada por una base de usuarios bastante grande, por lo que no quiero que se caiga para limpiar cosas).
Inicialmente probé una position
columna, pero parece que sería bastante ineficiente tener que cambiar el valor de posición de cada otro elemento cuando los mueves.
He visto a personas que usan una autorreferencia para referirse al valor anterior (o siguiente), pero nuevamente, parece que tendría que actualizar una gran cantidad de otros elementos en la lista.
Otra solución que he visto es usar números decimales y simplemente pegar elementos en los espacios entre ellos, lo que parece ser la mejor solución hasta ahora, pero estoy seguro de que tiene que haber una mejor manera.
Yo diría que una lista típica contendría hasta unos 20 elementos, y probablemente la limitaré a 50. El reordenamiento sería arrastrar y soltar y probablemente se realice en lotes para evitar condiciones de carrera y demás. solicitudes ajax. Estoy usando postgres (en heroku) si es importante.
¿Alguien tiene alguna idea?
Saludos por cualquier ayuda!
fuente
Respuestas:
Primero, no intentes hacer nada inteligente con los números decimales, porque te molestarán.
REAL
yDOUBLE PRECISION
son inexactos y pueden no representar adecuadamente lo que pones en ellos.NUMERIC
es exacto, pero la secuencia correcta de movimientos te hará perder precisión y tu implementación se romperá gravemente.Limitar los movimientos a altibajos simples hace que toda la operación sea muy fácil. Para obtener una lista de elementos numerados secuencialmente, puede mover un elemento hacia arriba disminuyendo su posición e incrementando el número de posición de cualquier resultado anterior. (En otras palabras, el tema
5
se convertiría4
y lo que era tema4
se convierte5
, efectivamente un intercambio como Imbéciles describe en su respuesta.) Mover hacia abajo sería lo contrario. Indice su tabla por lo que identifique de forma única una lista y una posición, y puede hacerlo con dosUPDATE
s dentro de una transacción que se ejecutará muy rápidamente. A menos que sus usuarios reorganicen sus listas a velocidades sobrehumanas, esto no causará mucha carga.Los movimientos de arrastrar y soltar (p. Ej., Mover elementos
6
para sentarse entre los elementos9
y10
) son un poco más complicados y tienen que hacerse de manera diferente dependiendo de si la nueva posición está por encima o por debajo de la anterior. En el ejemplo anterior, debe abrir un agujero incrementando todas las posiciones más que9
, actualizando6
la posición del elemento para que sea el nuevo10
y luego disminuyendo la posición de todo lo que sea mayor que6
llenar el lugar desocupado. Con la misma indexación que describí antes, esto será rápido. En realidad, puede hacer que esto vaya un poco más rápido de lo que describí minimizando el número de filas que toca la transacción, pero esa es una microoptimización que no necesita hasta que pueda demostrar que hay un cuello de botella.De cualquier manera, tratar de superar la base de datos con una solución casera, demasiado inteligente a la mitad, generalmente no conduce al éxito. Las bases de datos que valen la pena han sido escritas cuidadosamente para hacer estas operaciones muy, muy rápidamente por personas que son muy, muy buenas en eso.
fuente
Misma respuesta desde aquí https://stackoverflow.com/a/49956113/10608
Solución: haga
index
una cadena (porque las cadenas, en esencia, tienen una "precisión arbitraria" infinita). O si usa un int, incrementeindex
en 100 en lugar de 1.El problema de rendimiento es este: no hay valores "intermedios" entre dos elementos ordenados.
En cambio, haz esto (mejor solución a continuación):
Aún mejor: así es como Jira resuelve este problema. Su "rango" (lo que llama índice) es un valor de cadena que permite un montón de espacio para respirar entre los elementos clasificados.
Aquí hay un ejemplo real de una base de datos jira con la que trabajo
Note este ejemplo
hzztzz:i
. La ventaja de un rango de cadena es que te quedas sin espacio entre dos elementos, todavía no tienes que volver a clasificar nada más. Simplemente comienza a agregar más caracteres a la cadena para reducir el enfoque.fuente
¿Por qué? Supongamos que adopta un enfoque de tabla de lista vinculada con columnas (listID, itemID, nextItemID).
Insertar un nuevo elemento en una lista cuesta una inserción y una fila modificada.
Reposicionar un artículo cuesta tres modificaciones de fila (el artículo que se mueve, el artículo anterior y el artículo anterior a su nueva ubicación).
Eliminar un artículo cuesta una eliminación y una fila modificada.
Estos costos siguen siendo los mismos independientemente de si la lista tiene 10 artículos o 10,000 artículos. En los tres casos, hay una modificación menos si la fila de destino es el primer elemento de la lista. Si opera con más frecuencia en el último elemento de la lista, puede ser beneficioso almacenar prevItemID en lugar de siguiente.
fuente
¿ Mediste eso? ¿O es solo una suposición? No haga tales suposiciones sin ninguna prueba.
Honestamente, eso no es "una gran cantidad de elementos", para mí eso suena muy pocos.
Le sugiero que se adhiera al enfoque de "columna de posición" (si esa es la implementación más simple para usted). Para tamaños de lista tan pequeños, no comience una optimización innecesaria antes de experimentar problemas de rendimiento reales
fuente
Esto es realmente una cuestión de escala y caso de uso.
¿Cuántos artículos esperas en una lista? Si son millones, creo que la ruta decimal es la obvia.
Si 6, entonces la numeración de enteros es la opción obvia. s También la pregunta es cómo se reorganizaron las listas. Si usa flechas hacia arriba y hacia abajo (moviéndose hacia arriba o hacia abajo una ranura a la vez), usaría enteros y luego cambiaría con el anterior (o siguiente) en movimiento.
Además, ¿con qué frecuencia se compromete? Si el usuario puede realizar 250 cambios, confirme de una vez, de lo que digo números enteros con una nueva numeración ...
tl; dr: Necesito más información.
Editar: "Listas de deseos" suena como muchas listas pequeñas (suposición, esto puede ser falso) .. Entonces digo Integer con renumeración. (Cada lista contiene su propia posición)
fuente
Si el objetivo es minimizar el número de operaciones de la base de datos por operación de reordenamiento:
Asumiendo que
Almacene la lista de deseos ordenada del usuario como una secuencia empaquetada de enteros (matrices de enteros) en una columna. Cada vez que se reordena la lista de deseos, se actualiza toda la matriz (una sola fila; una sola columna), que se realizará con una única actualización de SQL.
https://www.postgresql.org/docs/current/static/arrays.html
Si el objetivo es diferente, quédese con el enfoque de "columna de posición".
Con respecto a la "velocidad", asegúrese de comparar el enfoque del procedimiento almacenado. Si bien la emisión de más de 20 actualizaciones separadas para una lista de deseos aleatoria puede ser lenta, puede haber una forma rápida de usar el procedimiento almacenado.
fuente
OK, recientemente me enfrenté a este complicado problema, y todas las respuestas en esta publicación de preguntas y respuestas me dieron mucha inspiración. A mi modo de ver, cada solución tiene sus pros y sus contras.
Si el
position
campo tiene que ser secuencial sin espacios, básicamente necesitará reordenar la lista completa. Esta es una operación O (N). La ventaja es que el lado del cliente no necesitaría ninguna lógica especial para obtener el pedido.Si queremos evitar la operación O (N) PERO TODAVÍA mantenemos una secuencia precisa, uno de los enfoques es usar "autorreferencia para referirnos al valor anterior (o siguiente)". Este es un escenario de lista enlazada de libros de texto. Por diseño, NO incurrirá en "una gran cantidad de otros elementos en la lista". Sin embargo, esto requiere que el lado del cliente (un servicio web o tal vez una aplicación móvil) implemente la lógica de recorrido de la lista vinculada para derivar el orden.
Algunas variaciones no usan referencia, es decir, lista vinculada. Eligen representar todo el orden como un blob autocontenido, como un JSON-array-in-a-string
[5,2,1,3,...]
; dicho pedido se almacenará en un lugar separado. Este enfoque también tiene el efecto secundario de requerir que el código del lado del cliente mantenga ese blob de orden separado.En muchos casos, realmente no necesitamos almacenar el orden exacto, solo necesitamos mantener un rango relativo entre cada registro. Por lo tanto, podemos permitir brechas entre registros secuenciales. Las variaciones incluyen: (1) usar un número entero con espacios como 100, 200, 300 ... pero rápidamente se quedará sin espacios y luego necesitará el proceso de recuperación; (2) usar el decimal que viene con espacios naturales, pero deberá decidir si puede vivir con la limitación de precisión eventual; (3) usar el rango basado en cadenas como se describe en esta respuesta, pero tenga cuidado con las trampas de implementación difíciles .
La verdadera respuesta puede ser "depende". Revise sus requisitos comerciales. Por ejemplo, si se trata de un sistema de lista de deseos, personalmente usaría un sistema organizado por unos pocos rangos como "must-have", "good-to-have", "maybe-later", y luego presentar elementos sin particular orden dentro de cada rango. Si se trata de un sistema de entrega, puede utilizar el tiempo de entrega como un rango aproximado que viene con una brecha natural (y prevención de conflictos naturales ya que no ocurriría ninguna entrega al mismo tiempo). Su experiencia puede ser diferente.
fuente
Use un número de coma flotante para la columna de posición.
Luego puede reordenar la lista cambiando solo la columna de posición en la fila "movida".
Básicamente, si su usuario quiere posicionar "rojo" después de "azul" pero antes de "amarillo"
Entonces solo necesitas calcular
Después de algunos millones de reubicaciones, puede obtener números de coma flotante tan pequeños que no haya "entre", pero esto es casi tan probable como ver un unicornio.
Puede implementar esto usando un campo entero con un espacio inicial de, digamos, 1000. Entonces, su orientación inicial sería 1000-> azul, 2000-> Amarillo, 3000-> Rojo. Después de "mover" el rojo después del azul, tendría 1000-> azul, 1500-> rojo, 2000-> amarillo.
El problema es que con una brecha inicial aparentemente grande de 1000, tan solo 10 movimientos lo llevarán a una situación como 1000-> azul, 1001-puce, 1004-> biege ...... donde ya no podrá para insertar cualquier cosa después de "azul" sin volver a numerar la lista completa. Usando números de coma flotante siempre habrá un punto "intermedio" entre las dos posiciones.
fuente
"pos": 1310719, + "pos": 638975.5
. Para ser justos, la mayoría de las personas no hacen listas de trello con 4 millones de entradas, pero el tamaño y el caso de uso de la lista de Trello es bastante común para el contenido clasificable por el usuario. Y cualquier cosa ordenable por el usuario no tiene casi nada que ver con el alto rendimiento, la velocidad de clasificación int vs flotante es discutible para eso, especialmente teniendo en cuenta que las bases de datos están limitadas principalmente por el rendimiento de IO.Si bien el OP se refirió brevemente a la noción de usar una Lista Vinculada para almacenar el orden de clasificación, tiene muchas ventajas para los casos en que los artículos se reordenarán con frecuencia.
La cosa es que no . Cuando se utiliza una lista enlazada, la inserción, la eliminación y el reordenamiento son
O(1)
operaciones, y la integridad referencial impuesta por la base de datos garantiza que no haya referencias rotas, registros huérfanos o bucles.Aquí hay un ejemplo:
Tenga en cuenta lo siguiente:
FK_Sorting
para evitar que los elementos se refieran accidentalmente al elemento primario incorrectoUNIQUE INDEX UX_Sorting
realiza dos roles:NULL
valor, cada lista puede tener solo 1 elemento "principal".SortAfter
valores duplicados ).Las principales ventajas de este enfoque:
int
oreal
que eventualmente se quedan sin espacio entre los artículos después de un pedido frecuente.Sin embargo, este enfoque tiene desventajas:
ORDER BY
.VIEW
o TVF que use un CTE para agregar un derivado que contenga un orden de clasificación incremental, pero sería costoso usarlo en operaciones grandes.SortAfter
columna se referirá a elementos que no están cargados en su programa.SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad
).UX_Sorting
está habilitada requiere soporte DBMS para restricciones diferidas.NULL
valores en la columna, lo que desafortunadamente significa que una lista podría tener múltiples elementos HEAD.State
que es un indicador simple para declarar si un elemento de la lista está "activo" o no, y el índice exclusivo ignora los elementos inactivos.Solución 1: necesita capacidad para realizar un trivial
ORDER BY
.Aquí hay una VISIÓN utilizando un CTE recursivo que agrega una
SortOrder
columna:Puede usar esta VISTA en otras consultas en las que necesite ordenar valores usando
ORDER BY
:Solución 2: Prevención de
UNIQUE INDEX
restricciones de violación al realizar operaciones:Agregue una
State
columna a laWishlistItems
tabla. La columna está marcada comoHIDDEN
así que la mayoría de las herramientas ORM (como Entity Framework) no la incluirán al generar modelos, por ejemplo.Operaciones:
Agregar un nuevo elemento al final de la lista:
ItemId
último elemento actual de la lista y guárdelo@tailItemId
, o úseloSELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listId
.INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId )
.Reordenar el elemento 4 por debajo del elemento 7
Eliminando el elemento 4 del medio de la lista:
Si un elemento está al final de la lista (es decir, dónde
NOT EXISTS ( SELECT 1 FROM WishlistItems WHERE SortAfter = @itemId )
), puede hacer uno soloDELETE
.Si un elemento tiene un elemento ordenado después de él, realiza los mismos pasos que reordenar un elemento, excepto que
DELETE
lo haga después en lugar de configurarloState = 1;
.fuente