Как объединить две таблицы истории для одного пользователя без связей
При разработке системы аудита изменений часто возникает ситуация, когда данные об изменении разных полей одной записи сохраняются в отдельные таблицы без внешних ключей и связей между ними. В вашем случае при изменении пользователя в таблице users создаются записи в двух таблицах истории: names (история изменения имени) и birthdays (история изменения даты рождения). Каждый раз добавляется новая строка в обе таблицы, даже если значение поля не изменилось. Задача - получить выборку для конкретного user_id, где количество строк равно количеству записей в любой из этих таблиц для данного пользователя, и строки сгруппированы по порядку (первая запись из names с первой записью из birthdays и так далее).
Проблема синхронизации по времени
Использовать временные метки для объединения записей не всегда надёжно, так как две записи могут быть созданы с разницей в миллисекунды, что делает точное совпадение маловероятным. Поэтому требуется другой подход - нумерация строк в порядке их добавления.
Решение: нумерация строк с помощью ROW_NUMBER
Наиболее эффективный способ - присвоить каждой записи в каждой таблице порядковый номер (ROW_NUMBER) в рамках одного user_id, отсортировав по первичному ключу (или по дате создания, если она есть). Затем объединить две таблицы по этому номеру. Ниже приведён пример SQL-запроса для PostgreSQL:
WITH numbered_names AS (
SELECT
id,
name,
user_id,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id) AS rn
FROM names
WHERE user_id = 1
),
numbered_birthdays AS (
SELECT
id,
birthday,
user_id,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id) AS rn
FROM birthdays
WHERE user_id = 1
)
SELECT
COALESCE(n.user_id, b.user_id) AS user_id,
n.id AS name_id,
n.name,
b.id AS birthday_id,
b.birthday
FROM numbered_names n
FULL OUTER JOIN numbered_birthdays b ON n.rn = b.rn
ORDER BY COALESCE(n.rn, b.rn);Как это работает
- ROW_NUMBER присваивает каждой строке уникальный номер в порядке возрастания
id(или другого поля, отражающего хронологию). - FULL OUTER JOIN гарантирует, что строки из обеих таблиц будут включены, даже если в одной таблице записей больше, чем в другой.
- COALESCE используется для выбора
user_idиз той таблицы, где он не NULL.
Альтернативные подходы
Если в вашей СУБД нет поддержки оконных функций (например, старая версия MySQL), можно использовать пользовательские переменные или нумерацию через подзапросы. Однако ROW_NUMBER - самый лаконичный и производительный вариант.
Важные замечания
- Убедитесь, что в таблицах истории есть поле, которое можно использовать для сортировки (например, автоинкрементный
idилиcreated_at). - Если записи добавляются в транзакции одновременно, порядок может быть непредсказуемым - используйте монотонно возрастающий идентификатор.
- Данный метод не требует создания дополнительных таблиц (pivot) и работает без циклов на стороне приложения.
Заключение
Использование ROW_NUMBER с последующим объединением по номеру строки - элегантное решение задачи выборки из двух несвязанных таблиц истории. Оно позволяет получить ровно столько строк, сколько записей в каждой таблице, и корректно сгруппировать их по порядку. Применяйте этот подход в своих проектах для упрощения кода и повышения производительности.