Embora o OP tenha abordado brevemente a noção de uso de uma lista vinculada para armazenar a ordem de classificação, ele possui muitas vantagens nos casos em que os itens serão reordenados com frequência.
Vi pessoas usando uma auto-referência para se referir ao valor anterior (ou próximo), mas, novamente, parece que você precisaria atualizar muitos outros itens da lista.
A questão é - você não ! Ao usar uma lista vinculada, inserção, exclusão e reordenação são O(1)operações, e a integridade referencial imposta pelo banco de dados garante que não haja referências quebradas, registros órfãos ou loops.
Aqui está um exemplo:
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;
Observe o seguinte:
- Usando uma chave primária e uma chave estrangeira compostas
FK_Sortingpara impedir que os itens se refiram acidentalmente ao item pai errado.
- O
UNIQUE INDEX UX_Sortingexecuta duas funções:
- Como permite um
NULLvalor único, cada lista pode ter apenas 1 item "principal".
- Impede que dois ou mais itens reivindiquem estar no mesmo local de classificação (impedindo
SortAftervalores duplicados ).
As principais vantagens dessa abordagem:
- Nunca requer reequilíbrio ou manutenção - como ocorre com as ordens de classificação baseadas
intou realque acabam ficando sem espaço entre os itens após reordenamentos frequentes.
- Somente itens reordenados (e seus irmãos) precisam ser atualizados.
Essa abordagem tem desvantagens, no entanto:
- Você só pode classificar essa lista no SQL usando um CTE recursivo porque não pode fazer uma tarefa direta
ORDER BY.
- Como solução alternativa, você pode criar um wrapper
VIEWou TVF que use um CTE para adicionar um derivado contendo uma ordem de classificação incremental - mas isso seria caro para uso em grandes operações.
- Você deve carregar a lista inteira no seu programa para exibi-la - você não pode operar em um subconjunto das linhas, porque a
SortAftercoluna se referirá aos itens que não foram carregados no seu programa.
- No entanto, o carregamento de todos os itens para uma lista é fácil devido à chave primária composta (ou seja, basta fazê-lo
SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad).
- A execução de qualquer operação enquanto
UX_Sortingestiver ativada requer suporte do DBMS para restrições adiadas.
- ou seja, a implementação ideal dessa abordagem não funcionará no SQL Server até que eles adicionem suporte adicional a restrições e índices adiados.
- Uma solução alternativa é tornar o Índice Único um Índice Filtrado que permita vários
NULLvalores na coluna - o que infelizmente significa que uma Lista pode ter vários itens HEAD.
- Uma solução alternativa para essa solução alternativa é adicionar uma terceira coluna,
Stateque é um sinalizador simples para declarar se um item da lista está "ativo" ou não - e o índice exclusivo ignora itens inativos.
- Isso é algo que o SQL Server usou para oferecer suporte na década de 1990 e, em seguida, eles inexplicavelmente removeram o suporte.
Solução alternativa 1: precisa da capacidade de executar uma tarefa trivial ORDER BY.
Aqui está uma VIEW usando uma CTE recursiva que adiciona uma SortOrdercoluna:
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;
Você pode usar esse VIEW em outras consultas nas quais você precisa classificar 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
Solução alternativa 2: Evitando UNIQUE INDEXrestrições de violação ao executar operações:
Adicione uma Statecoluna à WishlistItemstabela. A coluna está marcada como HIDDENa maioria das ferramentas ORM (como o Entity Framework) não a inclui ao gerar modelos, por exemplo.
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;
Operações:
Adicionando um novo item ao final da lista:
- Carregue a lista primeiro para determinar o
ItemIdúltimo item atual na lista e armazene @tailItemId- ou use SELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listId.
INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId ).
Reordenando o item 4 para ficar abaixo do item 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;
Removendo o item 4 do meio da lista:
Se um item estiver no final da lista (ou seja, onde NOT EXISTS ( SELECT 1 FROM WishlistItems WHERE SortAfter = @itemId )), você poderá fazer um único DELETE.
Se um item tiver um item classificado após ele, execute as mesmas etapas para reordenar um item, exceto você DELETEdepois em vez de definir 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;