У меня есть три таблицы: 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» соответствует хронологически следующему сообщению в разговоре, следующему за системным сообщением. Таким образом, вывод будет выглядеть примерно так:
Подход 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: Здесь я застрял. Я пытаюсь переписать вышеизложенное более эффективно, используя правильное соединение, но я не могу заставить соединение выдать мне следующее в хронологическом порядке сообщение, поэтому я получаю смешанные пары, например:
Вот запрос:
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 (), иначе я получу повторяющиеся строки.)
Как лучше всего подойти к этому?
[ РЕДАКТИРОВАТЬ ]
Уточнение: сообщения всегда чередуются от системного сообщения к пользовательскому сообщению, затем к системному сообщению и так далее. Никогда не будет двух сообщений, хронологически соседних, из одного и того же источника.


Используйте 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;
CTE нигде не используется, так что это просто шум, который планировщик отбросит. Остальное — это исходный запрос OP, в котором подзапрос перенесен из коррелированного скалярного подзапроса в список select в список from как подзапрос lateral, который делает то же самое и, кажется, работает точно так же.
Я бы попробовал:
Схема (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;
«Убедиться, что сообщение «пользователь ответил Y» соответствует хронологически следующему сообщению в разговоре, следующему за системным сообщением», здесь не выполняется. Если N-е сообщение от пользователя предшествует системному сообщению, а не следует за ним, оно все равно будет сопоставлено. Ваша ссылка на скрипку пуста, поэтому я переместил ее сюда — как показано, она сохраняет только порядок отдельных сообщений, но разговор не синхронизирован. Если пользователь произнес все три свои строки вчера, они все равно будут сопоставлены с тремя сообщениями системы за сегодняшний день.
@Zegarek Я выбираю из обеих таблиц только сообщения, которые имеют общий conversation_id, и я предполагаю, что для данного conversation_id (1 в этом примере) диалог между системой и пользователем меняется так, что, например, самое низкое создание дата ответа пользователя должна находиться между самой низкой и следующей самой низкой датой создания системного сообщения (см. даты создания, которые я использовал для обеих таблиц). В этом случае преобразование даты создания в номер строки (что, по сути, я и делаю) должно позволить мне сопоставить номера строк, как я это делал. Что такое несинхронизация?
Конечно, если бы данные были ограничены таким образом, это сработало бы. Я просто указал на то, что ОП не упоминает ничего подобного, и в этом посте не упоминалось, что это работает только в том случае, если это предположение верно, на что, как я решил, стоит указать, тем более что другие запросы, включая собственные запросы OP, работают нормально. независимо от этого предположения. Проблема «рассинхронизации» показана в демо: перемещение сообщений приводит к рассинхронизации разговора по сравнению с тем, что указывают временные метки.
@Zegarek Я не уверен, что все еще слежу за тобой. Меня не волнует, каковы настоящие временные метки; Я просто использую их, чтобы упорядочить сообщения для заданного conversation_id и объединяю системные и пользовательские сообщения на основе их порядка.
Это предположение безопасно, и мне следовало упомянуть его в своем сообщении (я отредактирую его). То есть сообщения всегда поступают последовательно — сначала системное сообщение, затем сообщение пользователя, затем системное сообщение и так далее. Никогда не будет двух сообщений из одного и того же источника одно за другим. Спасибо за помощь, кстати, обоим.
Ваша идея с отличным от звучит хорошо, но есть несколько вещей, которые вы могли бы улучшить:
Distinct on(sm) проверяет всю запись FROM system_messages sm. Поскольку у него есть первичный ключ, distinct on(sm.id) достаточно.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 значительно быстрее. Но оба работают одинаково. Я попробую добавить индексы обложек (спасибо, что познакомили меня с ними), чтобы посмотреть, смогу ли я ускорить процесс еще больше.
Да, это будут все те сложные части Postgres, о которых я упоминал, о которых я не знал! Большое спасибо за это — я попробую и приму, если это сработает.