Bien que le PO ait brièvement évoqué la possibilité d’utiliser une liste liée pour stocker un ordre de tri, il présente de nombreux avantages pour les cas où les articles seront fréquemment réorganisés.
J'ai vu des personnes utiliser une référence propre pour faire référence à la valeur précédente (ou suivante), mais encore une fois, il semble que vous deviez mettre à jour de nombreux autres éléments de la liste.
La chose est - vous ne faites pas ! Lorsque vous utilisez une liste chaînée, l'insertion, la suppression et la réorganisation sont des O(1)
opérations. L'intégrité référentielle imposée par la base de données garantit l'absence de références cassées, d'enregistrements orphelins ou de boucles.
Voici un exemple:
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;
Notez les points suivants:
- Utilisation d'une clé primaire composite et d'une clé étrangère
FK_Sorting
pour éviter que les éléments ne se réfèrent accidentellement au mauvais élément parent.
- Le
UNIQUE INDEX UX_Sorting
remplit deux rôles:
- Comme cela permet une seule
NULL
valeur, chaque liste ne peut avoir qu'un seul "élément".
- Il empêche deux ou plusieurs éléments de prétendre se trouver au même endroit de tri (en évitant les doublons
SortAfter
).
Les principaux avantages de cette approche:
- Jamais ne nécessite de rééquilibrage ou de maintenance - comme les ordres de tri basés sur
int
ou real
basés sur le disque qui finissent par manquer d’espace entre les éléments après des réorganisations fréquentes.
- Seuls les articles réorganisés (et leurs frères et sœurs) doivent être mis à jour.
Cette approche présente toutefois des inconvénients:
- Vous pouvez uniquement trier cette liste en SQL à l'aide d'un CTE récursif, car vous ne pouvez pas le faire directement
ORDER BY
.
- Pour contourner le problème, vous pouvez créer un wrapper
VIEW
ou un fichier TVF utilisant un CTE pour ajouter un dérivé contenant un ordre de tri incrémenté, mais son utilisation serait coûteuse dans les grandes opérations.
- Vous devez charger la liste complète dans votre programme pour pouvoir l'afficher. Vous ne pouvez pas utiliser un sous-ensemble de lignes car la
SortAfter
colonne fera alors référence aux éléments qui ne sont pas chargés dans votre programme.
- Cependant, le chargement de tous les éléments d’une liste est facile grâce à la clé primaire composite (c’est-à-dire qu’il suffit de le faire
SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad
).
- L'exécution de toute opération pendant qu'elle
UX_Sorting
est activée nécessite la prise en charge par le SGBD des contraintes différées.
- C'est-à - dire que l'implémentation idéale de cette approche ne fonctionnera pas dans SQL Server jusqu'à ce qu'ils ajoutent une prise en charge des contraintes et index pouvant être différés.
- Une solution de contournement consiste à transformer l'index unique en un index filtré qui autorise plusieurs
NULL
valeurs dans la colonne, ce qui signifie malheureusement qu'une liste peut comporter plusieurs éléments HEAD.
- Une solution de contournement pour cette solution consiste à ajouter une troisième colonne
State
qui est un simple indicateur permettant de déclarer si un élément de la liste est "actif" ou non - et l'index unique ignore les éléments inactifs.
- C’est quelque chose que SQL Server avait l'habitude de supporter dans les années 90, puis ils ont inexplicablement supprimé le support.
Solution 1: vous devez être capable d'effectuer une tâche triviale ORDER BY
.
Voici une vue utilisant un CTE récursif qui ajoute une SortOrder
colonne:
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;
Vous pouvez utiliser cette vue dans d'autres requêtes où vous devez trier les valeurs à l'aide de 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
Solution 2: prévention UNIQUE INDEX
des contraintes de violation lors de l'exécution d'opérations:
Ajouter une State
colonne à la WishlistItems
table. La colonne est marquée comme HIDDEN
si la plupart des outils ORM (comme Entity Framework) ne l'incluraient pas lors de la génération de modèles, par exemple.
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;
Opérations:
Ajouter un nouvel élément à la fin de la liste:
- Chargez d'abord la liste pour déterminer le
ItemId
dernier élément actuel de la liste et la stocker @tailItemId
ou l'utiliser SELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listId
.
INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId )
.
Remettre en ordre le point 4 en dessous du point 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;
Enlever l'item 4 du milieu de la liste:
Si un élément se trouve à la fin de la liste (c.-à-d. Où NOT EXISTS ( SELECT 1 FROM WishlistItems WHERE SortAfter = @itemId )
), vous pouvez en faire un seul DELETE
.
Si un élément est trié après un élément, vous procédez de la même manière que pour réorganiser un élément, sauf que vous le DELETE
modifiez par la suite 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;