Je travaille sur un système de liste de souhaits, où les utilisateurs peuvent ajouter des articles à leurs différentes listes de souhaits, et je prévois de permettre aux utilisateurs de réorganiser les articles plus tard. Je ne suis pas vraiment sûr de la meilleure façon de stocker cela dans une base de données tout en restant rapide et en évitant les dégâts (cette application sera utilisée par une base d'utilisateurs assez importante, donc je ne veux pas qu'elle descende pour nettoyer des trucs).
J'ai d'abord essayé une colonne position
, mais il semble que ce serait assez inefficace de devoir changer la valeur de position de chaque autre élément lorsque vous les déplacez.
J'ai vu des gens utiliser une auto-référence pour se référer à la valeur précédente (ou suivante), mais encore une fois, il semble que vous deviez mettre à jour beaucoup d'autres éléments de la liste.
Une autre solution que j'ai vue consiste à utiliser des nombres décimaux et à simplement coller des éléments dans les espaces entre eux, ce qui semble être la meilleure solution jusqu'à présent, mais je suis sûr qu'il doit y avoir un meilleur moyen.
Je dirais qu'une liste typique contiendrait jusqu'à une vingtaine d'articles environ, et je la limiterai probablement à 50. La réorganisation serait par glisser-déposer et se fera probablement par lots pour éviter les conditions de course et autres du demandes ajax. J'utilise des postgres (sur heroku) si cela est important.
Quelqu'un a-t-il une idée?
Bravo pour toute aide!
Tout d'abord, n'essayez pas de faire quelque chose d'intelligent avec des nombres décimaux, car ils vous contrarieront. REAL
et DOUBLE PRECISION
sont inexacts et peuvent ne pas représenter correctement ce que vous y mettez. NUMERIC
est exact, mais la bonne séquence de mouvements vous fera manquer de précision et votre implémentation se cassera mal.
Limiter les mouvements à des montées et descentes simples rend toute l'opération très facile. Pour une liste d'éléments numérotés séquentiellement, vous pouvez déplacer un élément vers le haut en décrémentant sa position et en incrémentant le numéro de position de ce que le décrément précédent a proposé. (En d'autres termes, l'élément 5
deviendrait 4
et quel était l'élément 4
devient 5
, en fait un échange comme Morons l'a décrit dans sa réponse.) Le déplacer vers le bas serait le contraire. Indexez votre table en identifiant de manière unique une liste et une position et vous pouvez le faire avec deux UPDATE
dans une transaction qui s'exécutera très rapidement. À moins que vos utilisateurs ne réorganisent leurs listes à des vitesses surhumaines, cela ne causera pas beaucoup de charge.
Déplacements par glisser-déposer (par exemple, déplacer l'élément 6
pour s'asseoir entre les articles 9
et 10
) sont un peu plus délicats et doivent être effectués différemment selon que la nouvelle position est au-dessus ou en dessous de l'ancienne. Dans l'exemple ci-dessus, vous devez ouvrir un trou en incrémentant toutes les positions supérieures à 9
, mise à jour de l'élément 6
est le nouveau 10
puis décrémenter la position de tout ce qui est supérieur à 6
pour remplir la place vacante. Avec la même indexation que j'ai décrite précédemment, ce sera rapide. Vous pouvez réellement accélérer cela un peu plus vite que je l'ai décrit en minimisant le nombre de lignes que la transaction touche, mais c'est une microoptimisation dont vous n'avez pas besoin jusqu'à ce que vous puissiez prouver qu'il y a un goulot d'étranglement.
Quoi qu'il en soit, essayer de surpasser la base de données avec une solution maison, trop intelligente par moitié ne mène généralement pas au succès. Des bases de données dignes de ce nom ont été soigneusement rédigées pour effectuer ces opérations très, très rapidement par des gens qui sont très, très bons dans ce domaine.
Même réponse d'ici https://stackoverflow.com/a/49956113/10608
Solution: faites de index
une chaîne (car les chaînes ont, par essence, une "précision arbitraire" infinie). Ou si vous utilisez un entier, incrémentez index
de 100 au lieu de 1.
Le problème (performances/complexité) étant résolu par cette solution: il n'y a pas de valeurs "intermédiaires" entre deux éléments triés.
item index
-----------------
gizmo 1
<<------ Oh no! no room between 1 and 2.
This requires incrementing _every_ item after it
gadget 2
gear 3
toolkit 4
box 5
Au lieu de cela, faites quelque chose comme ça (avec une meilleure solution ci-dessous):
item index
-----------------
gizmo 100
<<------ Sweet :). I can re-order 99 (!) items here
without having to change anything else
gadget 200
gear 300
toolkit 400
box 500
Encore mieux: voici comment Jira résout ce problème. Leur "rang" (ce que vous appelez index) est une valeur de chaîne qui permet une tonne de marge de manœuvre entre les éléments classés.
Voici un exemple réel d'une base de données jira avec laquelle je travaille
id | jira_rank
---------+------------
AP-2405 | 0|hzztxk:
ES-213 | 0|hzztxs:
AP-2660 | 0|hzztzc:
AP-2688 | 0|hzztzk:
AP-2643 | 0|hzztzs:
AP-2208 | 0|hzztzw:
AP-2700 | 0|hzztzy:
AP-2702 | 0|hzztzz:
AP-2411 | 0|hzztzz:i
AP-2440 | 0|hzztzz:r
Notez cet exemple hzztzz:i
. L'avantage d'un rang de chaîne est que vous manquez d'espace entre deux éléments, vous toujours n'avez pas à re-classer autre chose. Vous commencez simplement à ajouter plus de caractères à la chaîne pour affiner le focus.
"mais il semble que ce serait assez inefficace"
Avez-vous mesure cela? Ou est-ce juste une supposition? Ne faites pas de telles hypothèses sans aucune preuve.
"20 à 50 articles par liste"
Honnêtement, ce n'est pas "beaucoup d'articles", pour moi, cela semble très peu.
Je vous suggère de vous en tenir à l'approche de la "colonne de position" (si c'est la mise en œuvre la plus simple pour vous). Pour ces petites tailles de liste, ne commencez pas l'optimisation inutile avant de rencontrer de vrais problèmes de performances
J'ai vu des gens utiliser une auto-référence pour se référer à la valeur précédente (ou suivante), mais encore une fois, il semble que vous deviez mettre à jour beaucoup d'autres éléments de la liste.
Pourquoi? Supposons que vous adoptiez une approche de table de liste liée avec des colonnes (listID, itemID, nextItemID).
L'insertion d'un nouvel élément dans une liste coûte une insertion et une ligne modifiée.
Le repositionnement d'un élément coûte trois modifications de ligne (l'élément en cours de déplacement, l'élément avant lui et l'élément avant son nouvel emplacement).
La suppression d'un élément coûte une suppression et une ligne modifiée.
Ces coûts restent les mêmes, que la liste comporte 10 articles ou 10 000 articles. Dans les trois cas, il y a une modification de moins si la ligne cible est le premier élément de la liste. Si vous utilisez plus souvent l'élément de liste last, il peut être avantageux de stocker prevItemID plutôt que next.
C'est vraiment une question d'échelle et de cas d'utilisation ..
Combien d'articles attendez-vous dans une liste? Si des millions, je pense que la voie décimale est la plus évidente.
Si 6, la renumérotation des nombres entiers est le choix évident. s Aussi, les questions sont comment les listes ou réorganisées. Si vous utilisez des flèches vers le haut et vers le bas (vous déplacez vers le haut ou vers le bas d'un emplacement à la fois), le i utilise des entiers, puis échange avec le précédent (ou le suivant) en mouvement.
De plus, à quelle fréquence vous engagez-vous, si l'utilisateur peut apporter 250 modifications, puis validez à la fois, alors je dis des entiers avec une nouvelle numérotation ...
tl; dr: Besoin de plus d'informations.
Edit: "Listes de souhaits" ressemble à beaucoup de petites listes (hypothèse, cela peut être faux) .. Je dis donc Entier avec renumérotation. (Chaque liste contient sa propre position)
OK, j'ai récemment fait face à ce problème délicat, et toutes les réponses dans ce post de questions et réponses m'ont beaucoup inspiré. Selon moi, chaque solution a ses avantages et ses inconvénients.
Si le champ position
doit être séquentiel sans lacunes, vous devrez essentiellement réorganiser la liste entière. Il s'agit d'une opération O(N). L'avantage est que le côté client n'aurait pas besoin de logique particulière pour obtenir la commande.
Si nous voulons éviter l'opération O(N) MAIS TOUJOURS maintenir une séquence précise, l'une des approches consiste à utiliser "auto-référence pour faire référence à la valeur précédente (ou suivante)". Il s'agit d'un scénario de liste liée de manuels scolaires. De par sa conception, il n'engendre PAS "beaucoup d'autres éléments de la liste". Cependant, cela nécessite que le côté client (un service Web ou peut-être une application mobile) implémente le lien liste la logique de travesal pour dériver l'ordre.
Certaines variantes n'utilisent pas la référence, c'est-à-dire la liste chaînée. Ils choisissent de représenter la totalité de la commande comme un blob autonome, tel qu'un tableau JSON-dans-une-chaîne [5,2,1,3,...]
; cette commande sera ensuite stockée dans un endroit séparé. Cette approche a également pour effet secondaire d'exiger que le code côté client maintienne ce blob d'ordre séparé.
Dans de nombreux cas, nous n'avons pas vraiment besoin de stocker l'ordre exact, nous avons juste besoin de maintenir un rang relatif parmi chaque enregistrement. Par conséquent, nous pouvons permettre des écarts entre les enregistrements séquentiels. Les variations comprennent: (1) l'utilisation d'un entier avec des lacunes telles que 100, 200, 300 ... mais vous manquerez rapidement de lacunes et aurez alors besoin du processus de récupération; (2) en utilisant décimal qui vient avec des lacunes naturelles, mais vous devrez décider si vous pouvez vivre avec la limitation de précision éventuelle; (3) en utilisant un classement basé sur des chaînes comme décrit dans cette réponse mais attention aux pièges d'implémentation délicats .
La vraie réponse peut être "ça dépend". Revoyez vos besoins commerciaux. Par exemple, s'il s'agit d'un système de liste de souhaits, personnellement, j'utiliserais volontiers un système organisé par seulement quelques rangs en tant que "must-have", "good-to-have", "peut-être plus tard", puis présenter des éléments sans particulier ordre à l'intérieur de chaque rang. S'il s'agit d'un système de livraison, vous pouvez très bien utiliser le délai de livraison comme un rang approximatif qui vient avec un écart naturel (et la prévention des conflits naturels car aucune livraison ne se produirait en même temps). Votre kilométrage peut varier.
Si l'objectif est de minimiser le nombre d'opérations de base de données par opération de réorganisation:
En admettant que
Stockez la liste de souhaits triée de l'utilisateur sous la forme d'une séquence compactée d'entiers (tableaux d'entiers) dans une colonne. Chaque fois que la liste de souhaits est réorganisée, l'ensemble du tableau (une seule ligne; une seule colonne) est mis à jour - ce qui doit être effectué avec une seule mise à jour SQL.
https://www.postgresql.org/docs/current/static/arrays.html
Si l'objectif est différent, respectez l'approche de la "colonne de position".
En ce qui concerne la "vitesse", assurez-vous de comparer l'approche de procédure stockée. Bien que l'émission de 20+ mises à jour distinctes pour un mélange de liste de souhaits puisse être lente, il peut y avoir un moyen rapide d'utiliser la procédure stockée.
Utilisez un nombre à virgule flottante pour la colonne de position.
Vous pouvez ensuite réorganiser la liste en modifiant uniquement la colonne de position dans la ligne "déplacée".
Fondamentalement, si votre utilisateur veut positionner "rouge" après "bleu" mais avant "jaune"
Ensuite, il vous suffit de calculer
red.position = ((yellow.position - blue.position) / 2) + blue.position
Après quelques millions de repositionnements, vous pouvez obtenir des nombres à virgule flottante si petits qu'il n'y a pas "entre" - mais c'est à peu près aussi probable que de voir une licorne.
Vous pouvez implémenter cela en utilisant un champ entier avec un écart initial de disons 1000. Ainsi, votre oredring initial serait 1000-> bleu, 2000-> jaune, 3000-> rouge. Après avoir "déplacé" le rouge après le bleu, vous auriez 1000-> bleu, 1500-> rouge, 2000-> jaune.
Le problème est qu'avec un écart initial apparemment grand de 1000, aussi peu que 10 coups vous mettront dans une situation comme 1000-> bleu, 1001-puce, 1004-> biege ...... où vous ne pourrez plus pour insérer quoi que ce soit après "bleu" sans renuméroter toute la liste. En utilisant des nombres à virgule flottante, il y aura toujours un point "à mi-chemin" entre les deux positions.
Oui, la question est plutôt ancienne et contient déjà quelques réponses. Pourtant, toutes les solutions proposées ici sont assez complexes. Et des plus simples?
La question initiale concerne la liste de souhaits - quelque chose qui a probablement un nombre d'articles en dizaines, peut-être en centaines - mais pas en milliers, généralement. Pourquoi ne pas alors stocker l'ordre de tri dans un seul champ de texte sous la forme tableau sérialisé? Toute insertion, mise à jour et suppression n'affecterait qu'un seul enregistrement supplémentaire de cette façon.
Si un tableau sérialisé n'est pas assez sophistiqué, vous pouvez toujours en faire un champ JSON, mais ce ne sera toujours qu'une cellule en cours de modification dans la base de données.
Bien que l'OP ait brièvement abordé la notion d'utilisation d'une liste liée pour stocker l'ordre de tri, il présente de nombreux avantages pour les cas où les articles seront réorganisés fréquemment.
J'ai vu des gens utiliser une auto-référence pour se référer à la valeur précédente (ou suivante), mais encore une fois, il semble que vous deviez mettre à jour beaucoup d'autres éléments de la liste.
La chose est - vous ne le faites pas! Lorsque vous utilisez une liste chaînée, l'insertion, la suppression et le réordonnancement sont des opérations O(1)
et l'intégrité référentielle imposée par la base de données garantit qu'il n'y a pas 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:
FK_Sorting
Pour empêcher les éléments de se référer accidentellement au mauvais élément parent.UNIQUE INDEX UX_Sorting
Joue deux rôles: NULL
, chaque liste ne peut avoir qu'un seul élément "head".SortAfter
en double).Les principaux avantages de cette approche:
int
ou real
qui finissent par manquer d'espace entre les éléments après de fréquentes réorganisations.Cette approche présente cependant des inconvénients:
ORDER BY
. VIEW
ou TVF qui utilise un CTE pour ajouter un dérivé contenant un ordre de tri incrémenté - mais cela coûterait cher à utiliser dans de grandes opérations.SortAfter
fera référence aux éléments qui ne sont pas chargés dans votre programme. SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad
).UX_Sorting
Est activé nécessite la prise en charge du SGBD pour les contraintes différées. NULL
dans la colonne - ce qui signifie malheureusement qu'une liste pourrait avoir plusieurs HEAD items. State
qui est un simple indicateur pour déclarer si un élément de liste est "actif" ou non - et l'index unique ignore les éléments inactifs.ORDER BY
Trivial.Voici une VUE utilisant un CTE récursif qui ajoute une colonne SortOrder
:
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
UNIQUE INDEX
Lors de l'exécution des opérations:Ajoutez une colonne State
à la table WishlistItems
. La colonne est marquée comme HIDDEN
donc la plupart des outils ORM (comme Entity Framework) ne l'incluent 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;
ItemId
du dernier élément en cours dans la liste et enregistrez-le dans @tailItemId
- ou utilisez SELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listId
.INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId )
.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;
Si un élément se trouve à la fin de la liste (c'est-à-dire où NOT EXISTS ( SELECT 1 FROM WishlistItems WHERE SortAfter = @itemId )
), vous pouvez faire un seul DELETE
.
Si un élément est trié après un élément, vous effectuez les mêmes étapes que la réorganisation d'un élément, sauf que vous le DELETE
après au lieu de définir 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;