Как правильно объединить данные из двух таблиц, чтобы записи соединялись по их количественным номерам?

При изменении записи в таблице users создается история изменений, и эти изменения записываются в две различные таблицы, между которыми нет прямой связи:  

Исходная таблица: users (id, name, birthay)  
Таблицы истории изменений: names (id, name, user_id) и birthays (id, birthay, user_id)  

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

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

Необходимо избежать синхронизации по дате, так как записи могут быть добавлены с разницей в миллисекунды. Требуется, чтобы записи просто группировались последовательно.  

Как можно объединить запросы к этим таблицам без использования циклов? Не могу вспомнить, как это сделать. Возможно, стоит создать виртуальную таблицу для каждой из таблиц и назначить динамический id, по которому будет выполняться группировка. Как еще можно решить эту задачу?  

Использование таблицы Pivot рассматривается как крайний случай, и хотелось бы найти решение без ее применения.
  • 7 октября 2024 г. 17:44
Ответы на вопрос 2
Чтобы объединить данные из двух таблиц `names` и `birthays`, так чтобы каждая запись была сопоставлена с соответствующей записью пользователя, можно использовать конструкцию SQL, которая позволяет объединить результаты, добавляя дополнительные строки, чтобы сделать набор значений одинаковым по количеству. В данном случае можно использовать `FULL OUTER JOIN` для соединения двух таблиц, а затем использовать нумерацию строк, чтобы связать их.

Мы можем добавить уникальные идентификаторы к записям в каждой таблице и затем использовать их для соединения.

Вот пример запроса, который позволяет это сделать:

```sql
WITH RankedNames AS (
    SELECT id, name, user_id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id) AS rn
    FROM names
    WHERE user_id = @user_id  -- Замените @user_id на нужный вам user_id
),
RankedBirthays AS (
    SELECT id, birthay, user_id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id) AS rn
    FROM birthays
    WHERE user_id = @user_id  -- Замените @user_id на нужный вам user_id
)

SELECT 
    COALESCE(n.id, b.id) AS id,
    n.name,
    b.birthay
FROM 
    RankedNames n
FULL OUTER JOIN 
    RankedBirthays b ON n.rn = b.rn
ORDER BY COALESCE(n.rn, b.rn);
```

### Описание запроса:
1. **CTE (Common Table Expression)**: Используем два CTE, `RankedNames` и `RankedBirthays`, чтобы создать нумерацию строк для каждой записи, разделяя их по `user_id`.
2. **ROW_NUMBER()**: Функция `ROW_NUMBER()` используется для присвоения уникального номера каждой строке в каждой из таблиц.
3. **FULL OUTER JOIN**: Соединяем две таблицы по нумерации строк (rn). Это обеспечит наличие всех записей из обеих историй изменений, даже если у одной из таблиц записей больше, чем у другой.
4. **COALESCE**: Используется для выбора ненулевых значений `id` из двух таблиц, чтобы уникальный идентификатор был из одной из таблиц.
5. **ORDER BY**: Упорядочиваем результаты по нумерации строк, чтобы сохранить последовательность.

Такой подход позволит вам получить все изменения для конкретного пользователя в одной таблице, не теряя информации и ей не требуя дополнительных синхронизаций по времени изменений.
Чтобы любую таблицу обложить историей изменения, нужно создать следующую обвязку: 
1. Создать таблицу <название исходной таблицы>__history
Поля таблицы:
Первичный ключ - history_id,
Копируем названия полей исходной таблицы как есть (всем полям даете возможность быть null).
Делаем внешние ключи таблицы истории к исходной таблице (обычно id- исходной таблицы нужно привязать) и к другим таблицам, такие же как в исходной таблице, но опускаем ограничение уникальности ключей. Не используем каскадное обновление значение ключей.
Добавляете поле - вид операции над строкой исходной таблицы (вставка, обновление, удаление).
И самое главное, в таблице истории должно быть поле next_history_id - делаете по нему связанный список истории изменения одного поля. Как это поле работает? - см. пункт 2.
2. Создаете триггер/ы на события вставки, изменения, удаления для исходной таблицы в ней пишите тело со следующими действиями:
Читаете id тронутой событием записи из исходной таблицы.
Определяете history_id по данной id, где next_history_id is null - запоминаете в локальной переменной как before_history_id.
Если действие не удаление, то создаете запись в таблице истории из new-полей, которые пришли в insert/update для исходной таблицы, добавляете значение поля вида действия, next_history_id делаете null, получаете history_id этой вставленной записи - записываете в переменную new_history_id.
Если действие - удаление, то new поля вычитывать не нужно, нужно записать значение поля вида действия, next_history_id делаете null, получаете history_id этой вставленной записи - записываете в переменную new_history_id.
Делаете обновление поля next_history_id = new_history_id у записи истории со значением ключа before_history_id.
Тело триггера готово.
На любое действие в исходной таблице будет создаваться запись в истории, с указанием действия, а также будет формироваться цепочка изменений конкретной записи через атрибут next_history_id.
Чтобы вычитать последнее изменение по строчке исходной таблицы, нужно обратиться в таблицу истории по id от исходной таблицы и next_history_id is null.
Похожие вопросы