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.
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.
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:
CREATE TABLE Wishlists (
WishlistId int NOT NULL IDENTITY(1,1) PRIMARY KEY,
[Name] nvarchar(200) NOT NULL
);
CREATE TABLE WishlistItems (
ItemId int NOT NULL IDENTITY(1,1),
WishlistId int NOT NULL,
Text nvarchar(200) NOT NULL,
SortAfter int NULL,
CONSTRAINT PK_WishlistItem PRIMARY KEY ( ItemId, WishlistId ),
CONSTRAINT FK_Wishlist_WishlistItem FOREIGN KEY ( WishlistId ) REFERENCES Wishlists ( WishlistId ),
CONSTRAINT FK_Sorting FOREIGN KEY ( SortAfter, WishlistId ) REFERENCES WishlistItems ( ItemId, WishlistId )
);
CREATE UNIQUE INDEX UX_Sorting ON WishlistItems ( SortAfter, WishlistId );
-----
SET IDENTITY_INSERT Wishlists ON;
INSERT INTO Wishlists ( WishlistId, [Name] ) VALUES
( 1, 'Wishlist 1' ),
( 2, 'Wishlist 2' );
SET IDENTITY_INSERT Wishlists OFF;
SET IDENTITY_INSERT WishlistItems ON;
INSERT INTO WishlistItems ( ItemId, WishlistId, [Text], SortAfter ) VALUES
( 1, 1, 'One', NULL ),
( 2, 1, 'Two', 1 ),
( 3, 1, 'Three', 2 ),
( 4, 1, 'Four', 3 ),
( 5, 1, 'Five', 4 ),
( 6, 1, 'Six', 5 ),
( 7, 1, 'Seven', 6 ),
( 8, 1, 'Eight', 7 );
SET IDENTITY_INSERT WishlistItems OFF;
Tenga en cuenta lo siguiente:
- Usar una clave principal compuesta y una clave externa
FK_Sorting
para evitar que los elementos se refieran accidentalmente al elemento primario incorrecto
- El
UNIQUE INDEX UX_Sorting
realiza dos roles:
- Como permite un solo
NULL
valor, cada lista puede tener solo 1 elemento "principal".
- Impide que dos o más elementos afirmen estar en el mismo lugar de clasificación (al evitar
SortAfter
valores duplicados ).
Las principales ventajas de este enfoque:
- Nunca requiere reequilibrio o mantenimiento, como ocurre con las órdenes de clasificación basadas en
int
o real
que eventualmente se quedan sin espacio entre los artículos después de un pedido frecuente.
- Solo los artículos que se reordenan (y sus hermanos) deben actualizarse.
Sin embargo, este enfoque tiene desventajas:
- Solo puede ordenar esta lista en SQL utilizando un CTE recursivo porque no puede hacer una tarea sencilla
ORDER BY
.
- Como solución alternativa, puede crear un contenedor
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.
- Debe cargar la lista completa en su programa para poder visualizarla; no puede operar en un subconjunto de filas porque la
SortAfter
columna se referirá a elementos que no están cargados en su programa.
- Sin embargo, cargar todos los elementos para una lista es fácil debido a la clave primaria compuesta (es decir, simplemente hacer
SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad
).
- La realización de cualquier operación mientras
UX_Sorting
está habilitada requiere soporte DBMS para restricciones diferidas.
- es decir, la implementación ideal de este enfoque no funcionará en SQL Server hasta que agreguen soporte para restricciones e índices diferibles.
- Una solución alternativa es hacer que el índice único sea un índice filtrado que permita múltiples
NULL
valores en la columna, lo que desafortunadamente significa que una lista podría tener múltiples elementos HEAD.
- Una solución alternativa para esta solución consiste en agregar una tercera columna,
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.
- Esto es algo que SQL Server solía admitir en la década de 1990 y luego eliminaron inexplicablemente el soporte para ello.
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:
CREATE VIEW OrderableWishlistItems AS
WITH c ( ItemId, WishlistId, [Text], SortAfter, SortOrder )
AS
(
SELECT
ItemId, WishlistId, [Text], SortAfter, 1 AS SortOrder
FROM
WishlistItems
WHERE
SortAfter IS NULL
UNION ALL
SELECT
i.ItemId, i.WishlistId, i.[Text], i.SortAfter, c.SortOrder + 1
FROM
WishlistItems AS i
INNER JOIN c ON
i.WishlistId = c.WishlistId
AND
i.SortAfter = c.ItemId
)
SELECT
ItemId, WishlistId, [Text], SortAfter, SortOrder
FROM
c;
Puede usar esta VISTA en otras consultas en las que necesite ordenar valores usando ORDER BY
:
Query:
SELECT * FROM OrderableWishlistItems
Results:
ItemId WishlistId Text SortAfter SortOrder
1 1 One (null) 1
2 1 Two 1 2
3 1 Three 2 3
4 1 Four 3 4
5 1 Five 4 5
6 1 Six 5 6
7 1 Seven 6 7
8 1 Eight 7 8
Solución 2: Prevención de UNIQUE INDEX
restricciones de violación al realizar operaciones:
Agregue una State
columna a la WishlistItems
tabla. La columna está marcada como HIDDEN
así que la mayoría de las herramientas ORM (como Entity Framework) no la incluirán al generar modelos, por ejemplo.
CREATE TABLE WishlistItems (
ItemId int NOT NULL IDENTITY(1,1),
WishlistId int NOT NULL,
Text nvarchar(200) NOT NULL,
SortAfter int NULL,
[State] bit NOT NULL HIDDEN,
CONSTRAINT PK_WishlistItem PRIMARY KEY ( ItemId, WishlistId ),
CONSTRAINT FK_Wishlist_WishlistItem FOREIGN KEY ( WishlistId ) REFERENCES Wishlists ( WishlistId ),
CONSTRAINT FK_Sorting FOREIGN KEY ( SortAfter, WishlistId ) REFERENCES WishlistItems ( ItemId, WishlistId )
);
CREATE UNIQUE INDEX UX_Sorting ON WishlistItems ( SortAfter, WishlistId ) WHERE [State] = 1;
Operaciones:
Agregar un nuevo elemento al final de la lista:
- Cargue la lista primero para determinar el
ItemId
último elemento actual de la lista y guárdelo @tailItemId
, o úselo SELECT 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
BEGIN TRANSACTION
DECLARE @itemIdToMove int = 4
DECLARE @itemIdToMoveAfter int = 7
DECLARE @prev int = ( SELECT SortAfter FROM WishlistItems WHERE ItemId = @itemIdToMove )
UPDATE WishlistItems SET [State] = 0 WHERE ItemId IN ( @itemIdToMove , @itemIdToMoveAfter )
UPDATE WishlistItems SET [SortAfter] = @itemIdToMove WHERE ItemId = @itemIdToMoveAfter
UPDATE WishlistItems SET [SortAfter] = @prev WHERE SortAfter = @itemIdToMove
UPDATE WishlistItems SET [State] = 1 WHERE ItemId IN ( @itemIdToMove, @itemIdToMoveAfter )
COMMIT;
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 solo DELETE
.
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 configurarlo State = 1;
.
BEGIN TRANSACTION
DECLARE @itemIdToRemove int = 4
DECLARE @prev int = ( SELECT SortAfter FROM WishlistItems WHERE ItemId = @itemIdToRemove )
UPDATE WishlistItems SET [State] = 0 WHERE ItemId = @itemIdToRemove
UPDATE WishlistItems SET [SortAfter] = @prev WHERE SortAfter = @itemIdToRemove
DELETE FROM WishlistItems WHERE ItemId = @itemIdToRemove
COMMIT;