Хотя ОП кратко затронул идею использования связанного списка для хранения порядка сортировки, он имеет много преимуществ в тех случаях, когда элементы будут часто переупорядочиваться.
Я видел людей, использующих собственную ссылку для ссылки на предыдущее (или следующее) значение, но, опять же, кажется, что вам придется обновить множество других элементов в списке.
Дело в том , что нет ! При использовании связанного списка вставка, удаление и переупорядочение являются O(1)
операциями, а навязанная базой данных ссылочная целостность гарантирует, что не будет битых ссылок, потерянных записей или циклов.
Вот пример:
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;
Обратите внимание на следующее:
- Использование составного первичного ключа и внешнего ключа
FK_Sorting
для предотвращения случайного обращения элементов к неправильному родительскому элементу.
UNIQUE INDEX UX_Sorting
Выполняет две роли:
- Поскольку он допускает одно
NULL
значение, каждый список может иметь только 1 «головной» элемент.
- Это предотвращает утверждение о том, что два или более элементов находятся в одном и том же месте сортировки (за счет предотвращения дублирования
SortAfter
значений).
Основные преимущества этого подхода:
- Никогда не требуется перебалансировка или обслуживание - как в случае заказов сортировки на основе
int
или real
, в которых между частями после частого переупорядочения в конечном итоге заканчивается свободное пространство.
- Только элементы, которые переупорядочены (и их братья и сестры) должны быть обновлены.
Этот подход имеет недостатки, однако:
- Вы можете отсортировать этот список в SQL только с помощью Recursive CTE, потому что вы не можете сделать это просто
ORDER BY
.
- В качестве обходного пути вы можете создать оболочку
VIEW
или TVF, которые используют CTE для добавления производного, содержащего возрастающий порядок сортировки, но это будет дорого использовать в больших операциях.
- Вы должны загрузить весь список в свою программу, чтобы отобразить его - вы не можете работать с подмножеством строк, потому что тогда
SortAfter
столбец будет ссылаться на элементы, которые не загружены в вашу программу.
- Однако загрузка всех элементов для списка проста из-за составного первичного ключа (то есть просто сделать
SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad
).
- Выполнение любой операции, пока
UX_Sorting
она включена, требует поддержки СУБД для отложенных ограничений.
- т.е. идеальная реализация этого подхода не будет работать в SQL Server до тех пор, пока не будет добавлена поддержка отложенных ограничений и индексов.
- Обходной путь - сделать уникальный индекс фильтрованным индексом, который допускает несколько
NULL
значений в столбце, что, к сожалению, означает, что список может иметь несколько элементов HEAD.
- Обходной путь для этого обходного пути заключается в добавлении третьего столбца,
State
который представляет собой простой флаг для объявления, является ли элемент списка «активным» или нет - и уникальный индекс игнорирует неактивные элементы.
- Это то, что SQL Server использовал для поддержки еще в 1990-х годах, а затем они необъяснимым образом прекратили поддержку.
Обходной путь 1: Нужна способность выполнять тривиально ORDER BY
.
Вот ВИД, использующий рекурсивный CTE, который добавляет 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;
Вы можете использовать этот VIEW в других запросах, где вам нужно отсортировать значения, используя 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
Обходной путь 2: Предотвращение UNIQUE INDEX
нарушений ограничений при выполнении операций:
Добавьте State
столбец в WishlistItems
таблицу. Столбец помечен HIDDEN
так, что большинство инструментов ORM (например, Entity Framework) не включают его, например, при создании моделей.
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
текущий последний элемент в списке, и сохраните его @tailItemId
или используйте SELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listId
.
INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId )
,
Изменение порядка позиций 4 ниже 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;
Удаление пункта 4 из середины списка:
Если элемент находится в конце списка (т.е. где NOT EXISTS ( SELECT 1 FROM WishlistItems WHERE SortAfter = @itemId )
), то вы можете сделать один DELETE
.
Если у элемента есть элемент, отсортированный после него, вы выполняете те же шаги, что и при переупорядочении элемента, за исключением того, DELETE
что вы выполняете его впоследствии, вместо настройки 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;