Это уже шестой раз, когда я пытаюсь задать этот вопрос, и это тоже самый короткий вопрос. Все предыдущие попытки привели к чему-то более похожему на сообщение в блоге, а не на сам вопрос, но я уверяю вас, что моя проблема реальна, просто она касается одной большой темы, и без всех этих подробностей, содержащихся в этом вопросе, она будет не понятно в чем моя проблема. Так что здесь идет ...
Аннотация
У меня есть база данных, она позволяет довольно необычно хранить данные и предоставляет несколько нестандартных функций, которые требуются моему бизнес-процессу. Особенности следующие:
- Неразрушающие и неблокирующие обновления / удаления, реализованные с использованием подхода только для вставки, который позволяет восстанавливать данные и автоматически регистрировать данные (каждое изменение связано с пользователем, который внес это изменение)
- Мультиверсионные данные (может быть несколько версий одних и тех же данных)
- Разрешения на уровне базы данных
- Возможная согласованность со спецификацией ACID и безопасностью транзакций создает / обновляет / удаляет
- Возможность перемотки назад или быстрой перемотки вашего текущего представления данных в любой момент времени.
Могут быть и другие функции, которые я забыл упомянуть.
Структура базы данных
Все пользовательские данные хранятся в Items
таблице в виде строки в кодировке JSON ( ntext
). Все операции с базой данных выполняются с помощью двух хранимых процедур, GetLatest
и InsertSnashot
они позволяют работать с данными подобно тому, как GIT работает с исходными файлами.
Полученные данные связаны (JOINed) на внешнем интерфейсе в полностью связанный граф, поэтому в большинстве случаев нет необходимости выполнять запросы к базе данных.
Также возможно хранить данные в обычных столбцах SQL вместо того, чтобы хранить их в закодированной форме Json. Однако это увеличивает общую сложность.
Чтение данных
GetLatest
Результаты с данными в виде инструкций, рассмотрите следующую диаграмму для объяснения:
Диаграмма показывает эволюцию изменений, которые когда-либо были внесены в одну запись. Стрелки на диаграмме показывают версию, в соответствии с которой имело место редактирование (представьте, что пользователь обновляет некоторые данные в автономном режиме, параллельно с обновлениями, которые были сделаны онлайн-пользователем, в таком случае может возникнуть конфликт, который в основном представляет собой две версии данных вместо одного).
Таким образом, вызов в GetLatest
течение следующих временных интервалов ввода приведет к следующим версиям записи:
GetLatest 0, 15 => 1 <= The data is created upon it's first occurance
GetLatest 0, 25 => 2 <= Inserting another version on top of first one overwrites the existing version
GetLatest 0, 30 => 3 <= The overwrite takes place as soon as the data is inserted
GetLatest 0, 45 => 3, 4 <= This is where the conflict is introduced in the system
GetLatest 0, 55 => 4, 5 <= You can still edit all the versions
GetLatest 0, 65 => 4, 6 <= You can still edit all the versions
GetLatest 0, 75 => 4, 6, 7 <= You can also create additional conflicts
GetLatest 0, 85 => 4, 7, 8 <= You can still edit records
GetLatest 0, 95 => 7, 8, 9 <= You can still edit records
GetLatest 0, 105 => 7, 8 <= Inserting a record with `Json` equal to `NULL` means that the record is deleted
GetLatest 0, 115 => 8 <= Deleting the conflicting versions is the only conflict-resolution scenario
GetLatest 0, 125 => 8, X <= The conflict can be based on the version that was already deleted.
GetLatest 0, 135 => 8, Y <= You can delete such version too and both undelete another version on parallel within one Snapshot (or in several Snapshots).
GetLatest 0, 145 => 8 <= You can delete the undeleted versions by inserting NULL.
GetLatest 0, 155 => 8, Z <= You can again undelete twice-deleted versions
GetLatest 0, 165 => 8 <= You can again delete three-times deleted versions
GetLatest 0, 10000 => 8 <= This means that in order to fast-forward view from moment 0 to moment `10000` you just have to expose record 8 to the user.
GetLatest 55, 115 => 8, [Remove 4], [Remove 5] <= At moment 55 there were two versions [4, 5] so in order to fast-forward to moment 115 the user has to delete versions 4 and 5 and introduce version 8. Please note that version 7 is not present in results since at moment 110 it got deleted.
Для того , GetLatest
чтобы поддерживать такой эффективный интерфейс каждая запись должна содержать специальные атрибуты службы BranchId
, RecoveredOn
, CreatedOn
, UpdatedOnPrev
, UpdatedOnCurr
, UpdatedOnNext
, UpdatedOnNextId
которые используются , GetLatest
чтобы выяснить , относится ли запись адекватно в отрезок времени , предусмотренного GetLatest
аргументов
Вставка данных
Для обеспечения возможной согласованности, безопасности и производительности транзакций данные вставляются в базу данных с помощью специальной многоступенчатой процедуры.
Данные просто вставляются в базу данных без возможности запроса
GetLatest
хранимой процедуры.Данные становятся доступными для
GetLatest
хранимой процедуры, данные становятся доступными в нормализованном (то естьdenormalized = 0
) состоянии. В то время как данные в нормализованном состоянии, поля службыBranchId
,RecoveredOn
,CreatedOn
,UpdatedOnPrev
,UpdatedOnCurr
,UpdatedOnNext
, вUpdatedOnNextId
настоящее время вычисляются , которые очень медленно.Чтобы ускорить процесс, данные денормализуются, как только они становятся доступными для
GetLatest
хранимой процедуры.- Поскольку шаги 1,2,3 проводятся в разных транзакциях, возможно, что аппаратный сбой может произойти в середине каждой операции. Оставление данных в промежуточном состоянии. Такая ситуация нормальная, и даже если это произойдет, данные будут исцелены в течение следующего
InsertSnapshot
вызова. Код для этой части можно найти между шагами 2 и 3InsertSnapshot
хранимой процедуры.
- Поскольку шаги 1,2,3 проводятся в разных транзакциях, возможно, что аппаратный сбой может произойти в середине каждой операции. Оставление данных в промежуточном состоянии. Такая ситуация нормальная, и даже если это произойдет, данные будут исцелены в течение следующего
Проблема
Новые функции (обязательные для бизнеса) заставили меня изменить специальный Denormalizer
вид, который связывает все функции вместе и используется как для, так GetLatest
и для других InsertSnapshot
. После этого у меня начались проблемы с производительностью. Если изначально он SELECT * FROM Denormalizer
выполнялся всего за доли секунды, то теперь для обработки 10000 записей требуется около 5 минут.
Я не профессионал в области БД, и мне потребовалось почти шесть месяцев, чтобы представить текущую структуру базы данных. И я потратил две недели сначала на проведение рефакторинга, а затем пытался выяснить, в чем причина моей проблемы с производительностью. Я просто не могу найти это. Я предоставляю резервную копию базы данных (которую вы можете найти здесь), потому что схема (со всеми индексами) достаточно велика, чтобы поместиться в SqlFiddle, база данных также содержит устаревшие данные (более 10000 записей), которые я использую для целей тестирования , Также я предоставляю текст для Denormalizer
просмотра, который подвергся рефакторингу и стал мучительно медленным:
ALTER VIEW [dbo].[Denormalizer]
AS
WITH Computed AS
(
SELECT currItem.Id,
nextOperation.id AS NextId,
prevOperation.FinishedOn AS PrevComputed,
currOperation.FinishedOn AS CurrComputed,
nextOperation.FinishedOn AS NextComputed
FROM Items currItem
INNER JOIN dbo.Operations AS currOperation ON currItem.OperationId = currOperation.Id
LEFT OUTER JOIN dbo.Items AS prevItem ON currItem.PreviousId = prevItem.Id
LEFT OUTER JOIN dbo.Operations AS prevOperation ON prevItem.OperationId = prevOperation.Id
LEFT OUTER JOIN
(
SELECT MIN(I.id) as id, S.PreviousId, S.FinishedOn
FROM Items I
INNER JOIN
(
SELECT I.PreviousId, MIN(nxt.FinishedOn) AS FinishedOn
FROM dbo.Items I
LEFT OUTER JOIN dbo.Operations AS nxt ON I.OperationId = nxt.Id
GROUP BY I.PreviousId
) AS S ON I.PreviousId = S.PreviousId
GROUP BY S.PreviousId, S.FinishedOn
) AS nextOperation ON nextOperation.PreviousId = currItem.Id
WHERE currOperation.Finished = 1 AND currItem.Denormalized = 0
),
RecursionInitialization AS
(
SELECT currItem.Id,
currItem.PreviousId,
currItem.UUID,
currItem.Json,
currItem.TableName,
currItem.OperationId,
currItem.PermissionId,
currItem.Denormalized,
currItem.Id AS BranchID,
COALESCE (C.PrevComputed, C.CurrComputed) AS CreatedOn,
COALESCE (C.PrevComputed, CAST(0 AS BIGINT)) AS RecoveredOn,
COALESCE (C.PrevComputed, CAST(0 AS BIGINT)) AS UpdatedOnPrev,
C.CurrComputed AS UpdatedOnCurr,
COALESCE (C.NextComputed, CAST(8640000000000000 AS BIGINT)) AS UpdatedOnNext,
C.NextId AS UpdatedOnNextId,
0 AS RecursionLevel
FROM Items AS currItem
INNER JOIN Computed AS C ON currItem.Id = C.Id
WHERE currItem.Denormalized = 0
UNION ALL
SELECT currItem.Id,
currItem.PreviousId,
currItem.UUID,
currItem.Json,
currItem.TableName,
currItem.OperationId,
currItem.PermissionId,
currItem.Denormalized,
currItem.BranchId,
currItem.CreatedOn,
currItem.RecoveredOn,
currItem.UpdatedOnPrev,
currItem.UpdatedOnCurr,
currItem.UpdatedOnNext,
currItem.UpdatedOnNextId,
0 AS RecursionLevel
FROM Items AS currItem
WHERE currItem.Denormalized = 1
),
Recursion AS
(
SELECT *
FROM RecursionInitialization AS currItem
UNION ALL
SELECT currItem.Id,
currItem.PreviousId,
currItem.UUID,
currItem.Json,
currItem.TableName,
currItem.OperationId,
currItem.PermissionId,
currItem.Denormalized,
CASE
WHEN prevItem.UpdatedOnNextId = currItem.Id
THEN prevItem.BranchID
ELSE currItem.Id
END AS BranchID,
prevItem.CreatedOn AS CreatedOn,
CASE
WHEN prevItem.Json IS NULL
THEN CASE
WHEN currItem.Json IS NULL
THEN prevItem.RecoveredOn
ELSE C.CurrComputed
END
ELSE prevItem.RecoveredOn
END AS RecoveredOn,
prevItem.UpdatedOnCurr AS UpdatedOnPrev,
C.CurrComputed AS UpdatedOnCurr,
COALESCE (C.NextComputed, CAST(8640000000000000 AS BIGINT)) AS UpdatedOnNext,
C.NextId,
prevItem.RecursionLevel + 1 AS RecursionLevel
FROM Items currItem
INNER JOIN Computed C ON currItem.Id = C.Id
INNER JOIN Recursion AS prevItem ON currItem.PreviousId = prevItem.Id
WHERE currItem.Denormalized = 0
)
SELECT item.Id,
item.PreviousId,
item.UUID,
item.Json,
item.TableName,
item.OperationId,
item.PermissionId,
item.Denormalized,
item.BranchID,
item.CreatedOn,
item.RecoveredOn,
item.UpdatedOnPrev,
item.UpdatedOnCurr,
item.UpdatedOnNext,
item.UpdatedOnNextId
FROM Recursion AS item
INNER JOIN
(
SELECT Id, MAX(RecursionLevel) AS Recursion
FROM Recursion AS item
GROUP BY Id
) AS nested ON item.Id = nested.Id AND item.RecursionLevel = nested.Recursion
GO
Вопросы)
Есть два сценария, которые принимаются во внимание, денормализованные и нормализованные случаи:
Глядя на исходное резервное копирование, что делает
SELECT * FROM Denormalizer
это мучительно медленным, я чувствую, что есть проблема с рекурсивной частью представления Denormalizer, я пытался ограничить,denormalized = 1
но ни одно из моих действий не повлияло на производительность.После запуска
UPDATE Items SET Denormalized = 0
он будет делатьGetLatest
иSELECT * FROM Denormalizer
бежать в (первоначально думали , что будет) медленный сценарий, есть способ ускорить процесс, когда мы вычислительными служебных полейBranchId
,RecoveredOn
,CreatedOn
,UpdatedOnPrev
,UpdatedOnCurr
,UpdatedOnNext
,UpdatedOnNextId
заранее спасибо
PS
Я пытаюсь придерживаться стандартного SQL, чтобы сделать запрос легко переносимым на другие базы данных, такие как MySQL / Oracle / SQLite, на будущее, но если нет стандартного SQL, который мог бы помочь, я в порядке, придерживаясь конструкций, специфичных для базы данных.