Перечислите каждое сообщение из таблицы A с самым ранним ответом в таблице B в хронологическом порядке

У меня есть три таблицы: conversations, user_messages и system_messages. Их базовая структура (ненужные столбцы удалены для краткости):

create table conversations(id int generated by default as identity primary key);

create table user_messages(
   id int generated by default as identity primary key
  ,conversation_id int references conversations(id)
  ,content text
  ,created_at timestamp);

create table system_messages(
   id int generated by default as identity primary key
  ,conversation_id int references conversations(id)
  ,content text
  ,created_at timestamp);

Я пытаюсь создать экспорт строк «система сказала X» и «пользователь ответил Y» для данного разговора. Проблема, с которой я столкнулся, заключается в том, чтобы найти способ гарантировать, что сообщение «пользователь ответил Y» соответствует хронологически следующему сообщению в разговоре, следующему за системным сообщением. Таким образом, вывод будет выглядеть примерно так:

system_said user_said Привет! Привет Как дела? Я в порядке, а ты? Я в порядке! Хороший.
  • Подход 1: Это работает, но с плохим и неэффективным запросом, использующим подзапрос, ссылающийся на внешние значения.

    SELECT
        content AS system_said, (
            SELECT content
            FROM user_messages
            WHERE conversation_id = sm.conversation_id AND created_at > sm.created_at
            ORDER BY created_at
            LIMIT 1
        ) AS user_said
    FROM system_messages sm
    WHERE sm.conversation_id = ?
    
  • Подход 2: Здесь я застрял. Я пытаюсь переписать вышеизложенное более эффективно, используя правильное соединение, но я не могу заставить соединение выдать мне следующее в хронологическом порядке сообщение, поэтому я получаю смешанные пары, например:

    system_said user_said Привет! Хороший. Как дела? Привет Я в порядке! Я в порядке, а ты?

    Вот запрос:

    SELECT
        DISTINCT ON (sm) sm.id AS message,
        sm.content AS system_said,
        um.content AS user_said
    FROM system_messages sm
    JOIN (
        SELECT conversation_id, created_at, content
        FROM user_messages
        ORDER BY created_at
    ) um ON um.conversation_id = sm.conversation_id AND um.created_at > sm.created_at
    ORDER BY sm
    WHERE c.id = ?
    

    (Я обнаружил, что мне нужно добавить DISTINCT ON (), иначе я получу повторяющиеся строки.)

Как лучше всего подойти к этому?

[ РЕДАКТИРОВАТЬ ]

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

ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
2
0
65
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Используйте CTE, чтобы ранжировать сообщения пользователя в каждом разговоре в зависимости от времени их создания, а затем используйте JOIN, чтобы связать каждое системное сообщение со следующим сообщением пользователя, используя номер строки. Попробуй это:

WITH ranked_user_messages AS (
    SELECT
        id,
        conversation_id,
        content,
        created_at,
        ROW_NUMBER() OVER (PARTITION BY conversation_id ORDER BY created_at) AS rn
    FROM user_messages
)
SELECT
    sm.content AS system_said,
    um.content AS user_said
FROM system_messages sm
LEFT JOIN LATERAL (
    SELECT content
    FROM user_messages um
    WHERE um.conversation_id = sm.conversation_id
      AND um.created_at > sm.created_at
    ORDER BY um.created_at
    LIMIT 1
) um ON true
WHERE sm.conversation_id = ?
ORDER BY sm.created_at;

Да, это будут все те сложные части Postgres, о которых я упоминал, о которых я не знал! Большое спасибо за это — я попробую и приму, если это сработает.

Mitya 31.05.2024 14:26

CTE нигде не используется, так что это просто шум, который планировщик отбросит. Остальное — это исходный запрос OP, в котором подзапрос перенесен из коррелированного скалярного подзапроса в список select в список from как подзапрос lateral, который делает то же самое и, кажется, работает точно так же.

Zegarek 31.05.2024 15:30

Я бы попробовал:

Схема (PostgreSQL v15)

create table system_messages (
  id serial primary key,
  conversation_id int,
  content text,
  created_at timestamp
);

create table user_messages (
  id serial primary key,
  conversation_id int,
  content text,
  created_at timestamp
);

insert into system_messages(conversation_id, content, created_at) values
   (1, 'Hi!', '2024-05-31 08:00:00'),
   (1, 'How''s it going?', '2024-05-31 08:01:00'),
   (1, 'I''m good!', '2024-05-31 08:02:00');

insert into user_messages(conversation_id, content, created_at) values
   (1, 'Hello', '2024-05-31 08:00:30'),
   (1, 'I''m alright, you?', '2024-05-31 08:01:30'),
   (1, 'Nice.', '2024-05-31 08:02:30');

Запрос №1

with s_m (r_n, system_said) as (
  select row_number() over (order by created_at),
         content
  from system_messages
  where conversation_id = 1
),
u_m (r_n, player_said) as (
  select row_number() over (order by created_at),
         content
  from user_messages
  where conversation_id = 1
)
select s_m.system_said, u_m.player_said from
s_m join u_m on s_m.r_n = u_m.r_n
order by s_m.r_n;
system_said player_said Привет! Привет Как дела? Я в порядке, а ты? Я в порядке! Хороший.

Посмотреть на DB Fiddle

«Убедиться, что сообщение «пользователь ответил Y» соответствует хронологически следующему сообщению в разговоре, следующему за системным сообщением», здесь не выполняется. Если N-е сообщение от пользователя предшествует системному сообщению, а не следует за ним, оно все равно будет сопоставлено. Ваша ссылка на скрипку пуста, поэтому я переместил ее сюда — как показано, она сохраняет только порядок отдельных сообщений, но разговор не синхронизирован. Если пользователь произнес все три свои строки вчера, они все равно будут сопоставлены с тремя сообщениями системы за сегодняшний день.

Zegarek 31.05.2024 15:47

@Zegarek Я выбираю из обеих таблиц только сообщения, которые имеют общий conversation_id, и я предполагаю, что для данного conversation_id (1 в этом примере) диалог между системой и пользователем меняется так, что, например, самое низкое создание дата ответа пользователя должна находиться между самой низкой и следующей самой низкой датой создания системного сообщения (см. даты создания, которые я использовал для обеих таблиц). В этом случае преобразование даты создания в номер строки (что, по сути, я и делаю) должно позволить мне сопоставить номера строк, как я это делал. Что такое несинхронизация?

Booboo 31.05.2024 16:03

Конечно, если бы данные были ограничены таким образом, это сработало бы. Я просто указал на то, что ОП не упоминает ничего подобного, и в этом посте не упоминалось, что это работает только в том случае, если это предположение верно, на что, как я решил, стоит указать, тем более что другие запросы, включая собственные запросы OP, работают нормально. независимо от этого предположения. Проблема «рассинхронизации» показана в демо: перемещение сообщений приводит к рассинхронизации разговора по сравнению с тем, что указывают временные метки.

Zegarek 31.05.2024 16:30

@Zegarek Я не уверен, что все еще слежу за тобой. Меня не волнует, каковы настоящие временные метки; Я просто использую их, чтобы упорядочить сообщения для заданного conversation_id и объединяю системные и пользовательские сообщения на основе их порядка.

Booboo 31.05.2024 17:00

Это предположение безопасно, и мне следовало упомянуть его в своем сообщении (я отредактирую его). То есть сообщения всегда поступают последовательно — сначала системное сообщение, затем сообщение пользователя, затем системное сообщение и так далее. Никогда не будет двух сообщений из одного и того же источника одно за другим. Спасибо за помощь, кстати, обоим.

Mitya 31.05.2024 17:20
Ответ принят как подходящий

Ваша идея с отличным от звучит хорошо, но есть несколько вещей, которые вы могли бы улучшить:

  1. Вам не нужен подзапрос, вы можете присоединиться напрямую.
  2. Distinct on(sm) проверяет всю запись FROM system_messages sm. Поскольку у него есть первичный ключ, distinct on(sm.id) достаточно.
  3. order by, который вы используете с distinct on, может использовать не только уникальный столбец. Если вы хотите получить ответ как можно скорее, добавьте его временную метку: order by sm.id,um.created_at.

В демо-версии db<>fiddle с 20 тысячами разговоров и 200 тысячами сообщений в них требуется 40ms:

SELECT
    DISTINCT ON (sm.id) sm.id AS message,
    sm.content AS system_said,
    um.content AS user_said
FROM system_messages sm
JOIN user_messages um 
   ON um.conversation_id = sm.conversation_id 
  AND um.created_at > sm.created_at
  AND sm.conversation_id = 1
ORDER BY sm.id,um.created_at;

Вы также можете добавить покрывающие индексы, чтобы ускорить процесс сканирования только индексов, что сокращает время выполнения, как показано ниже 1ms:

create index on user_messages(conversation_id,created_at)
   include(content,id);
create index on system_messages(conversation_id,created_at)
   include(content,id);

При их наличии похоже, что ваша первоначальная идея выигрывает с точки зрения производительности, независимо от того, опускаете ли вы коррелированный скалярный подзапрос вниз для перекрестного соединения, как латеральный подзапрос , выполняющий то же самое, или нет: demo2 at дб<>рабочий пример

SELECT sm.content AS system_said,
       um.content AS user_said
FROM system_messages sm
CROSS JOIN LATERAL (
    SELECT content
    FROM   user_messages um
    WHERE um.conversation_id = sm.conversation_id 
      AND um.created_at > sm.created_at
    ORDER BY um.created_at
    LIMIT 1 ) um
WHERE sm.conversation_id = 999
ORDER BY sm.created_at;

Спасибо вам за это - очень ценно. Я включил оба ваших запроса в свой более широкий запрос (там больше объединений, столбцов и т. д.) и определенно обнаружил, что вариант DISTINCT ON значительно быстрее. Но оба работают одинаково. Я попробую добавить индексы обложек (спасибо, что познакомили меня с ними), чтобы посмотреть, смогу ли я ускорить процесс еще больше.

Mitya 31.05.2024 17:43

Другие вопросы по теме