Как эффективно самостоятельно присоединиться к одной и той же таблице несколько раз с порядком сортировки по возрастанию и убыванию?

Определение данных

У меня есть такая таблица статусов, атрибуты, не относящиеся к вопросу, опущены:

идентификатор созданный ценить 1 2024-06-24T13:01:00 ошибка 2 2024-06-24T13:02:00 хорошо 3 2024-06-24T13:03:00 предупреждение 4 2024-06-24T13:04:00 ошибка 5 2024-06-24T13:05:00 ошибка 6 2024-06-24T13:05:30 ошибка 7 2024-06-24T13:06:00 хорошо 8 2024-06-24T13:07:00 ошибка 9 2024-06-24T13:07:30 ошибка 10 2024-06-24T13:08:00 предупреждение 11 2024-06-24T13:09:00 ошибка

Задача под рукой

Я хотел бы свернуть таблицу в блочное представление, где блоки «ошибок» (1, 4-6, 8-9, 11) свернуты в одну строку, но с соответствующими состояниями до и после, а также временными метками, например так:

error_first_occurance значение_перед timestamp_before значение_после timestamp_after 2024-06-24T13:01:00 НУЛЕВОЙ НУЛЕВОЙ хорошо 2024-06-24T13:02:00 2024-06-24T13:04:00 предупреждение 2024-06-24T13:03:00 хорошо 2024-06-24T13:06:00 2024-06-24T13:07:00 хорошо 2024-06-24T13:06:00 предупреждение 2024-06-24T13:08:00 2024-06-24T13:09:00 предупреждение 2024-06-24T13:08:00 НУЛЕВОЙ НУЛЕВОЙ

Варианты решения

Насколько я знаю, у меня есть следующие варианты:

1. Создайте подзапросы

SELECT 
  value AS "value_before"
  -- created AS "timestamp_before"
FROM t AS t1 
WHERE t1.value != 'error' AND t1.created < t.created 
ORDER BY t1.created DESC 
LIMIT 1
SELECT 
  value AS "value_after"
  -- created AS "timestamp_after"
FROM t AS t2 
WHERE t2.value != 'error' AND t2.created > t.created 
ORDER BY t2.created ASC
LIMIT 1

2. БОКОВОЕ СОЕДИНЕНИЕ

Использование латерального JOIN предположительно сэкономило бы половину запросов, поскольку я мог бы извлечь два поля одновременно (value, created), что невозможно с подзапросами.

Однако механизму, скорее всего, придется снова выполнить сортировку для каждой строки, создаваемой основным запросом. Поэтому я не стал заниматься этим дальше.

3. САМОСТОЯТЕЛЬНЫЕ ПРИСОЕДИНЕНИЯ

Здесь я бы создал две производные таблицы t1 и t2 (CTE в данном случае будет одним и тем же) с сортировкой по createdDESC (сначала самые новые) и ASC (сначала самые старые) и объединил бы их по t1.value != 'error' AND t1.created < t.created для «самых новых, которые есть». не ошибка и старше, чем t.created», и на t2.value != 'error' AND t2.created > t.created для «самого старого, что не является ошибкой, и младше, чем t.created».

SELECT
  t.created  "error_first_occurance",
  t1.value   "value_before",
  t1.created "timestamp_before",
  t2.value   "value_after",
  t2.created "timestamp_after"
FROM t LEFT
JOIN (
  SELECT value, created
  FROM t WHERE value != 'error'
  ORDER BY created DESC
) t1 ON t1.created < t.created LEFT
JOIN (
  SELECT value, created
  FROM t WHERE value != 'error'
  ORDER BY created ASC
) t2 ON t2.created > t.created
WHERE 
  t.value = 'error'
ORDER BY 
  t.created

Это уже дает правильный «супернабор», но поскольку я не могу полагаться на то, что t1.value или t2.value имеют только одинаковые значения между блоками (см. строку 2-3), я не знаю, как сообщить СУБД, что мне нужен MAX()/MIN() значение из временной метки created и то же самое value из этой записи.

Вопрос

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

Следовательно, в предполагаемой реальной таблице со многими сотнями записей:

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

ORDER BY без LIMIT/TOP, которого нет в SELECT или UNION, который является самым внешним запросом, не имеет никакого эффекта.

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

Ответы 3

Для каждого блока ошибок требуется информация из трех строк, поэтому наиболее простой способ — использовать два шага, каждый из которых объединяет по два. Вы можете сделать это либо с помощью самосоединений, либо с помощью оконных функций, таких как lag(). Остальное — просто правильно расположить строки. Вот реализация с самообъединениями, которая мне кажется предпочтительнее (более кратко):

WITH a AS (
    SELECT 
        ROW_NUMBER() OVER (ORDER BY COALESCE(t1.id, -1)),
        t1.value AS value_before,
        t1.created AS timestamp_before,
        t2.created AS error_first_occurrence,
        t2.value,
        t2.created
    FROM
        t t1
        FULL JOIN t t2 ON t1.id+1 = t2.id
    WHERE
        (t1.value = 'error' AND t2.value IS DISTINCT FROM 'error')
        OR (t1.value IS DISTINCT FROM 'error' AND t2.value = 'error')
)
SELECT
    a1.error_first_occurrence,
    a1.value_before,
    a1.timestamp_before,
    a2.value AS value_after,
    a2.created AS timestamp_after
FROM
    a a1
    JOIN a a2 ON a1.row_number+1 = a2.row_number
WHERE
    a1.value = 'error';

Обратите внимание на использование IS DISTINCT и COALESCE для обозначения NULL.

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

Другим подходом было бы использование рекурсивного запроса, но я не думаю, что в данном случае это вам что-нибудь даст (кроме еще более непонятного синтаксиса). Однако они являются наиболее общим и мощным инструментом для решения подобных проблем (агрегирование по группам последовательных строк), поэтому, если вы продолжаете сталкиваться с чем-то подобным, возможно, вам захочется взглянуть.

Что касается индексов, я предполагаю, что id — это первичный ключ, поэтому самый важный индекс уже есть. Помимо этого, многостолбцовый индекс на id и value может помочь ускорить первоначальное самосоединение, но только если ваши блоки с ошибками и без ошибок достаточно велики, поскольку в противном случае большинство строк таблицы все равно придется сканировать. .

Ответ принят как подходящий

Это проблема пробелов и островов . Как уже отмечал @Knoep , вы можете использовать оконные функции:

select distinct on(island_n)
       first_value(created)         over w2 as error_first_occurance
      ,first_value(value_before)    over w2 as value_before
      ,first_value(timestamp_before)over w2 as timestamp_before
      ,last_value(value_after)      over w2 as value_after
      ,last_value(timestamp_after)  over w2 as timestamp_after
from(select*,count(*)filter(where value<>'error')over w1 as island_n 
            ,value='error'
             and value is distinct from lag(value)over w1 as is_starting_island
            ,lag(value)   over w1 as value_before
            ,lag(created) over w1 as timestamp_before
            ,lead(value)  over w1 as value_after
            ,lead(created)over w1 as timestamp_after
     from t window w1 as(order by created) )_
where value='error'
window w2 as(partition by island_n order by created
             rows between unbounded preceding and unbounded following)
order by island_n,1;

Он проходит по вашей таблице, находя острова (непрерывные последовательности error) и собирает значения из предшествующих и последующих строк. После этого для каждого острова требуется один набор первых/последних значений, используя , отличающийся от .

Это примерно так же быстро , как и четверное само-join, но не обязательно полагаться на два тихих предположения о том, что в столбце id нет пробелов и что он следует тому же порядку, что и created - что один из них терпит неудачу если ни одно из двух не выполняется. Режим «окно над окном» также не требует дополнительных усилий по поддержанию предварительно отсортированного содержимого без зазоров id.

Я склонен принять этот ответ, потому что вы правы, в столбце id будут пробелы в реальной таблице, а также из-за ссылки на общее описание проблемы «пробел и остров». Я бы попросил только отформатировать SQL, потому что неопытные люди (например, я) могут быть сбиты с толку несколько смешанными отступами...

user1931751 26.06.2024 15:43

@user1931751 user1931751 Я не против согласиться, если вы нажмете «Редактировать» в сообщении, вставите версию, пропущенную через ваш линтер/IDE/форматер, и переставите ее по своему вкусу, но я должен признать, что впервые вижу, чтобы кто-то делал это - за исключением случаев, когда определенное форматирование заменяет отсутствие форматирования.

Zegarek 26.06.2024 16:11

Альтернативный подход был бы именно тем, что вы сделали, просто свернутым так, как вы хотели:

  1. Берите все error.
  2. Найдите их непосредственно предыдущую строку.
  3. Оставьте только те, перед которыми стоит знак без error, чтобы сохранить только начало острова.
  4. Найдите самый ранний не-error после них.

order by..limit 1 — это то, как боковым подзапросам удается легко найти одну строку, которая предшествует или следует за целевой строкой.

демо на db<>fiddle

select*from t as t1 
left join lateral 
(  select value   as value_before
         ,created as timestamp_before 
   from t as t2 
   where t2.created<=t1.created
     and t2.id<>t1.id
   order by created desc 
   limit 1)as s1 on true
left join lateral 
(  select value   as value_after
         ,created as timestamp_after
   from t as t2 
   where t2.created>=t1.created
     and t2.value<>'error'
   order by created asc
   limit 1)as s2 on true
where t1.value='error'
  and s1.value_before is distinct from 'error'

Лучше всего использовать индексы покрытия:

create index idx1 on t(created)include(id,value);
create index idx2 on t(value)  include(id,created);

Это позволяет ему выполнить 3 быстрых сканирования только индекса.

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

Похожие вопросы

Запретить SQL сопоставлять конечные пробелы и вместо этого явно сопоставлять условие
Как вы можете вычислить, является ли результат десятичным или целым числом в SQL?
ПОЛНОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ НА ДВУХ ТАБЛИЦАХ MYSQL С 3 СВЯЗАННЫМИ КОЛОННАМИ
Подзапрос INSERT INTO SELECT, который использует udf и возвращает 1 значение
Файл содержит значения даты как «01.09.2015 14:30:00». Но после импорта в таблицу oracle все значения даты отображаются как «20-01-19 15:12:15.00000 PM»
Как поднять ошибку, если условие не соответствует
Как вставить много строк тегов на основе входного массива?
UNSUPPORTED_FEATURE.SET_OPERATION_ON_MAP_TYPE Эта функция не поддерживается: не может быть столбцов типа MAP, которые вызывают операции набора (INTERSECT, E
SQL SELECT * FROM table WHERE Столбец NOT LIKE '%str%' не работает, я чувствую себя глупо
MySQL – получение неправильного результата при попытке получить случайный результат из большой таблицы