Быстрое обновление наборов данных

Под быстрым обновлением наборов данных здесь подразумевается изменение набора, приводящее к синхронизации его содержимого с текущим содержимым БД без повторной выборки всех данных набора. Предполагается, что с БД идет параллельная работа нескольких клиентов, каждый из которых может как выбирать данные из БД в локальные наборы при помощи SQL запросов, так и изменять их. Подход будет рассматриваться в контексте конкретных решений внутри БД.

Изменения данных

Очевидно, что после того, как мы выбрали данные из БД в набор, данные хранимые в наборе, могут рассинхронизироваться с содержимым БД (если, конечно, мы не не имеем монопольного доступа к БД). Если мы говорим о простой таблице БД, то могут произойти вставки, изменения и удаления записей. При объединении (join) таблиц, изменение данных может приводить к как появлению, так и исчезновению данных из выборки на основании измененных записей - такие случаи здесь не будут рассматриваться. Мы ограничимся рассмотрением случаев, когда связи между записями таблиц устойчивы во времени.

Для синхронизации изменений и удалений понадобится однозначная идентификация записей. Так, имея основной набор данных, и набор изменений (дельта-набор) для него, мы должны будем сопоставить каждую запись дельта-набора с существующей записью основного набора данных для того, чтобы правильно применить изменения к записям. Для такой идентификации необходимо иметь в наборе уникальный (первичный) ключ. Для обновления записей нам понадобятся обновленные записи со всеми полями, включая ключевые. Для удаления достаточно иметь только набор удаленных ключей (последовательное удаление, а затем создание записи с одним и тем же ключом мы будем интерпретировать как изменение записи).

Для выделения измененных и вставленных записей нам необходим параметр, который четко определяет как изменение данных записи, так и набора данных целиком во времени. Таким параметром может быть дата изменения (создания) записи, либо некоторый глобальный номер изменений (ревизия в терминах версионных хранилищ), приписанный записи. Если говорить только о вставленных записях, то таким параметром может быть и значение первичного ключа, если его генератор гарантирует последовательное получение ключей (рост значения ключа). Что касается набора данных, то при его открытии или изменении, ему должна быть приписана адекватная дата (ревизия), соответствующая моменту выборки или синхронизации данных. Это значение должно позволить сформировать запрос для следующего обновления набора, т. е. выбрать вставленные или измененные записи. При определенных условиях, это значение может быть сформировано на основе текущих данных набора (например, как максимум из ревизий записей набора).

Для выделения удаленных записей понадобится дополнительная структура, которая бы позволяла сохранять ключи удаляемых записей. Логично в данные этой структуры так же включить дату (ревизию), соответствующую операции удаления. Таким образом можно будет получить ключи записей, удаленные с момента последнего обращения к БД.

Реализация

Предполагаем, что каждая выборка из БД будет содержать уникальный ключ, который мы знаем заранее. В качестве параметра, определяющего изменения, используем дату. Такой выбор, несмотря на "естественность", обладает недостатками. Точность представления даты может меняться в зависимости от СУБД. Машинное время дискретно, поэтому, даже на практике, двум соседним операциям может быть приписано одно и то же время. Поэтому при формировании запросов нужно учесть возможные погрешности.

Добавим в каждую интересующую нас таблицу поле UpdateDate типа ДатаВремя. С использование триггеров на вставку и изменение записи безусловно заполним это поле текущий датой и временем - это будет являться гарантией фиксации изменений и вставок. После этого можем сформировать запрос на выборки изменений. Имея основной запрос

select Key, f1, f2...fn from t1, t2... where ...

сформируем запрос изменений данных

select Key, f1, f2...fn from t1, t2... where (...) and ((t1.UpdateDate >= :pDate) or (t2.UpdateDate >= :pDate)...)

Ключ Key позволит нам сопоставить измененные записи с существующими и отделить изменения от вставок (вставленные записи не будут найдены по ключу в основном наборе). Параметр pDate - дата, приписанная набору при его предыдущем открытии или обновлении. Используя сравнение на равно (>=) мы будем избегать проблем с дискретностью времени - даже если время операций совпадет, мы выберем данные. Однако при таком подходе могут быть выбраны лишние данные - например, при формировании параметра на основе данных набора (выбранных ранее значений UpdateDate).  Поэтому, предлагается выбирать время из БД непосредственно перед открытием/изменением отдельным запросом, а приписать по окончании операции. Так мы гарантируем, что приписанное значение будет точно не позже, чем последующая выборка данных, но достаточно близко к ней по времени. Кроме того нам нужно время СУБД, т. к. локальное время может быть не синхронно. Для этого можно использовать запросы (в зависимости от СУБД: Interbase, FireBird, Oracle):

select cast ('now' as Date) from RDB$DATABASE
select cast ('now' as TimeStamp) from RDB$DATABASE
select SYSDATE from DUAL

Для хранения удаленных записей нам понадобится дополнительная таблица. Если ключи всех таблиц имеют один тип, то достаточно одной таблицы на БД.

create table DELRECINDEXES(
TABLEID Int,
RECORDID KeyType,
DELETEDATE TimeStamp)

Далее добавим для каждой интересующей нас таблицы БД триггер на удаление записи, где зафиксируем ключ удаляемой записи и время удаления в новой записи DELRECINDEXES. Следует учесть, что для таблиц, где ключ не является суррогатным необходима дополнительная обработка ситуации с удалением-вставкой одного и того же ключа. При вставке новой записи необходимо исключать ключ, т. к. он больше не является удаленным. При изменении поля-ключа, необходимо добавить в удаленные старое значение, а новое значение исключить из удаленных.

Для "наката" удаленных записей используем запрос вида:

select RecordId from delrecindexes where (TableId = :pTableID) and (DeleteDate >= :pDate)

где pTableID - идентификатор таблицы (для разделения значений в delrecindexes), pDate - как и ранее, дата, приписанная набору при его предыдущем открытии или обновлении. Таким образом мы получим набор ключей, которые были удалены в БД за прошедшее время.

Общая схема работы

Условия:

  1. Наличие уникального ключа в наборе для идентификации записей.
  2. Наличие полей с датой-временем создания/изменения записи.
  3. Наличие таблицы для хранения ключей удаленных записей (при необходимости контроля удалений).

Первичное открытие набора

  1. Выбираем из БД текущее время на сервере.
  2. Выполняем запрос набора.
  3. Приписываем время п.1 набору для последующего наката изменений.

Синхронизация изменений

  1. Выбираем из БД текущее время на сервере. Это будет временем текущей синхронизации и мы его используем после выполнения всех операций.
  2. Запрашиваем изменения (дельта-набор) из БД, используя модифицированный запрос с условием по UpdateDate. В качестве параметра используем время предыдущего открытия/синхронизации.
  3. Обрабатываем измененные и вставленные записи
  4. Запрашиваем удаленные записи, используя выборку ключей из спец. таблицы. В качестве параметра используем время предыдущего открытия/синхронизации и фиксированный идентификатор таблицы.
  5. Обрабатываем удаленные ключи
  6. Приписываем время п.1 набору для последующего наката изменений.

Таким образом, для обновления нам понадобится 3 запроса к БД, однако предполагается, что результат возврата этих наборов будет скромными по объему. Кроме того, если нет необходимости контролировать удаления, то запрос и обработку удаленных ключей можно исключить.

Недостатки

Условие по дате вставки/изменения может быть не всегда эффективным. Если такое поле в наборе одно, то его можно проиндексировать и этот индекс, как правило, будет эффективен. Однако, если набор основан на выборке из объединения (join) таблиц, то может потребоваться контроль нескольких полей-дат (например, при выборке объединенного мастер-детального отношения). Такой контроль идет с использованием оператора OR, что, как правило, снижает эффективность обработки на сервере.

Если задачи синхронизации включает только синхронизацию новых записей и имеется суррогатный, строго растущий ключ, то можно построить очень простую выборку изменений:

select Key, f1, f2...fn from t1, t2... where (...) and (Key > :pOldKey)

где pOldKey - максимальное текущее значение ключа в наборе. Эта выборка самодостаточна, если конечно ключ выбирается: максимальное значение ключа можно всегда определить по набору, все выбранные записи достаточно добавить в набор без проверок - при этом можно легко и быстро обновить максимальное значение ключа.