Проектирование базы данных: как справиться с проблемой «архива»?


18

Я уверен, что многие приложения, критические приложения, банки и так далее делают это ежедневно.

Идея, стоящая за всем этим:

  • все строки должны иметь историю
  • все ссылки должны оставаться связными
  • должно быть легко делать запросы, чтобы получить «текущие» столбцы
  • клиенты, которые купили устаревшие вещи, все равно должны видеть то, что они купили, хотя этот продукт больше не является частью каталога

и так далее.

Вот что я хочу сделать, и я объясню проблемы, с которыми я сталкиваюсь.

Все мои таблицы будут иметь эти столбцы:

  • id
  • id_origin
  • date of creation
  • start date of validity
  • start end of validity

И вот идеи для операций CRUD:

  • создать = вставить новую строку с id_origin= id, date of creation= сейчас, start date of validity= сейчас, end date of validity= ноль (= означает, что это текущая активная запись)
  • обновление =
    • читать = читать все записи с end date of validity== ноль
    • обновить "текущую" запись end date of validity= null с end date of validity= сейчас
    • создайте новый с новыми значениями, и end date of validity= null (= означает, что это текущая активная запись)
  • delete = обновить "текущую" запись end date of validity= null с end date of validity= сейчас

Итак, вот моя проблема: со многими ко многим ассоциациям. Давайте рассмотрим пример со значениями:

  • Таблица A (id = 1, id_origin = 1, начало = сейчас, конец = ноль)
  • Таблица A_B (начало = сейчас, конец = ноль, id_A = 1, id_B = 48)
  • Таблица B (id = 48, id_origin = 48, начало = сейчас, конец = ноль)

Теперь я хочу обновить таблицу A, id записи = 1

  • Я отмечаю запись id = 1 с конца = сейчас
  • Я вставляю новое значение в таблицу A и ... черт, я потерял свое отношение A_B, если я тоже не дублирую отношение ... это закончится таблицей:

  • Таблица A (id = 1, id_origin = 1, начало = сейчас, конец = сейчас + 8мн)

  • Таблица A (id = 2, id_origin = 1, начало = сейчас + 8 мин, конец = ноль)
  • Таблица A_B (начало = сейчас, конец = ноль, id_A = 1, id_B = 48)
  • Таблица A_B (начало = сейчас, конец = ноль, id_A = 2, id_B = 48)
  • Таблица B (id = 48, id_origin = 48, начало = сейчас, конец = ноль)

И ... ну, у меня есть еще одна проблема: отношение A_B: я должен пометить (id_A = 1, id_B = 48) как устаревшее или нет (A - id = 1 устарело, но не B - 48)?

Как с этим бороться?

Я должен разработать это в больших масштабах: продукты, партнеры и так далее.

Каков ваш опыт в этом? Как бы вы сделали (как вы сделали)?

-- Редактировать

Я нашел эту очень интересную статью , но она не имеет дело с "каскадным устареванием" (= то, что я спрашиваю на самом деле)


Как насчет копирования данных записи обновления до ее обновления на новую с новым идентификатором, хранящим связанный список истории с полем id_hist_prev. Таким образом, идентификатор текущей записи никогда не меняется

Вместо того, чтобы заново изобретать колесо, рассматривали ли вы, например, использование Flashback Data Archive в Oracle?
Джек Дуглас

Ответы:


4

Мне не ясно, предназначены ли эти требования для целей аудита или просто для простого исторического справочника, такого как CRM и корзины покупок.

В любом случае, рассмотрите наличие таблиц main и main_archive для каждой основной области, где это требуется. «Main» будет иметь только текущие / активные записи, тогда как «main_archive» будет иметь копию всего, что когда-либо попадало в main. Вставка / обновление в main_archive может быть триггером из вставки / обновления в main. Удаление против main_archive может затем выполняться в течение более длительного периода времени, если вообще когда-либо.

Для проблем со ссылками, таких как Cust X купил продукт Y, самый простой способ решить вашу ссылочную проблему cust_archive -> product_archive - никогда не удалять записи из product_archive. Как правило, отток должен быть намного ниже в этой таблице, поэтому размер не должен быть слишком плохим.

НТН.


2
Отличный ответ, но я хотел бы добавить, что еще одним преимуществом наличия архивной таблицы является то, что они имеют тенденцию к денормализации, что делает отчетность по таким данным намного более эффективной. Рассмотрите потребности вашего приложения в отчетности и с этим подходом.
maple_shaft

1
В большинстве баз данных, которые я проектирую, все «основные» таблицы имеют префикс имени продукта, например LP_, и каждая важная таблица имеет эквивалент LH_, с триггерами, вставляющими исторические строки при вставке, обновлении, удалении. Это не работает для всех случаев, но это было хорошей моделью для того, что я делаю.

Я согласен - если большинство запросов относятся к «текущим» строкам, вы, вероятно, получите превосходное преимущество, разделив текущие данные из истории на две таблицы. Вид может объединить их вместе, для удобства. Таким образом, страницы данных с текущими строками все вместе и, вероятно, лучше сохраняются в кэше, и вам не нужно постоянно проверять запросы на текущие данные с помощью логики даты.
onupdatecascade

1
@onupdatecascade: обратите внимание, что (по крайней мере, в некоторых RDBMS) вы можете поместить индексы в это UNIONпредставление, что позволяет вам делать интересные вещи, такие как принудительное применение уникального ограничения для текущих и исторических записей.
Джон на все руки

Спустя 5 лет я сделал кучу вещей и все время возвращал тебе твою идею. Единственное, что я изменил, так это то, что в таблицах истории у меня есть столбец " id" и " id_ref". id_refэто ссылка на актуальную идею таблицы. Пример: personа person_h. у person_hменя есть " id", и " id_ref" где id_refсвязано с ' person.id', поэтому у меня может быть много строк с одинаковыми person.id(= когда строка personмодифицируется), и idвсе мои таблицы имеют автоматический ввод.
Оливье Понс

2

Это имеет некоторое совпадение с функциональным программированием; в частности, концепция неизменности.

Вы называете одну таблицу, PRODUCTа другую - PRODUCTVERSIONили похожую. Когда вы меняете продукт, вы не обновляете его, а просто вставляете новую PRODUCTVERSIONстроку. Чтобы получить последнюю версию, вы можете индексировать таблицу по номеру версии (desc), метке времени (desc) или иметь флаг ( LatestVersion).

Теперь, если у вас есть что-то, что ссылается на продукт, вы можете решить, на какую таблицу он указывает. Указывает ли это на PRODUCTсущность (всегда относится к этому продукту) или на PRODUCTVERSIONсущность (относится только к этой версии продукта)?

Это становится сложным. Что делать, если у вас есть фотографии продукта? Они должны указывать на таблицу версий, потому что они могут быть изменены, но во многих случаях этого не произойдет, и вы не захотите дублировать данные без необходимости. Это означает, что вам нужен PICTUREстол и отношения « PRODUCTVERSIONPICTUREмногие ко многим».


1

Я реализовал все вещи из здесь с 4 полями , которые находятся на все мои таблицах:

  • Я бы
  • date_creation
  • date_validity_start
  • date_validity_end

Каждый раз, когда необходимо изменить запись, я дублирую ее, отмечаю дублированную запись как «старую» =, date_validity_end=NOW()а текущую - как хорошую date_validity_start=NOW()и date_validity_end=NULL.

Хитрость заключается в отношениях «многие ко многим» и «один ко многим»: она работает, не касаясь их! Это все о запросах, которые являются более сложными: чтобы запросить запись в точную дату (= не сейчас), я должен для каждого объединения и для основной таблицы добавить эти ограничения:

WHERE (
  (date_validity_start<=:dateparam AND date_validity_end IS NULL)
  OR
  (date_validity_start<=:dateparam AND date_validity_start>=:dateparam)
)

Так же с продуктами и атрибутами (многие ко многим относятся):

SELECT p.*,a.*

FROM products p

JOIN products_attributes pa
ON pa.id_product = p.id
AND (
  (pa.date_validity_start<=:dateparam AND pa.date_validity_end IS NULL)
  OR
  (pa.date_validity_start<=:dateparam AND pa.date_validity_start>=:dateparam)
)

JOIN attributes a
ON a.id = pa.id_attribute
AND (
  (a.date_validity_start<=:dateparam AND a.date_validity_end IS NULL)
  OR
  (a.date_validity_start<=:dateparam AND a.date_validity_start>=:dateparam)
)

WHERE (
  (p.date_validity_start<=:dateparam AND p.date_validity_end IS NULL)
  OR
  (p.date_validity_start<=:dateparam AND p.date_validity_start>=:dateparam)
)

0

Как насчет этого? Это кажется простым и довольно эффективным для того, что я сделал в прошлом. В вашей таблице «истории» используйте другой ПК. Итак, ваше поле «CustomerID» - это PK в вашей таблице «Customer», а в таблице «history» ваш PK - «NewCustomerID». «CustomerID» становится просто другим полем только для чтения. Это оставляет «CustomerID» неизменным в истории, и все ваши отношения остаются неизменными.


Очень хорошая идея То, что я сделал, очень похоже: я дублирую запись и отмечаю новую как "устаревшую", чтобы текущая запись оставалась прежней. Примечание. Я хотел создать триггер для каждой таблицы, но mysql запрещает модификацию таблицы, когда вы находитесь в триггере этой таблицы. PostGRESQL сделать это. SQL-сервер делает это. Oracle делает это. Короче говоря, MySQL еще предстоит пройти долгий путь, и в следующий раз я дважды подумаю, когда выберу свой сервер базы данных.
Оливье Понс
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.