Как получить текущее и следующее большее значение за один выбор?

18

У меня есть InnoDB таблицы «idtimes» (MySQL 5.0.22-log) со столбцами

`id` int(11) NOT NULL,
`time` int(20) NOT NULL, [...]

с составным уникальным ключом

UNIQUE KEY `id_time` (`id`,`time`)

поэтому может быть несколько временных меток на один идентификатор и несколько идентификаторов на одну временную метку.

Я пытаюсь настроить запрос, в котором я получаю все записи плюс следующее большее время для каждой записи, если она существует, поэтому она должна возвращать, например:

+-----+------------+------------+
| id  | time       | nexttime   |
+-----+------------+------------+
| 155 | 1300000000 | 1311111111 |
| 155 | 1311111111 | 1322222222 |
| 155 | 1322222222 |       NULL |
| 156 | 1312345678 | 1318765432 |
| 156 | 1318765432 |       NULL |
+-----+------------+------------+

Прямо сейчас я так далеко

SELECT l.id, l.time, r.time FROM 
    idtimes AS l LEFT JOIN idtimes AS r ON l.id = r.id
    WHERE l.time < r.time ORDER BY l.id ASC, l.time ASC;

но, конечно, это возвращает все строки с r.time> l.time, а не только первую ...

Я думаю, мне понадобится подобрать

SELECT outer.id, outer.time, 
    (SELECT time FROM idtimes WHERE id = outer.id AND time > outer.time 
        ORDER BY time ASC LIMIT 1)
    FROM idtimes AS outer ORDER BY outer.id ASC, outer.time ASC;

но я не знаю, как ссылаться на текущее время (я знаю, что выше не является допустимым SQL).

Как мне сделать это с одним запросом (и я бы предпочел не использовать @variables, которые зависят от перехода по таблице по одной строке за раз и запоминания последнего значения)?

Мартин Хеннингс
источник

Ответы:

20

Вступление в JOIN - это то, что вам может понадобиться.

SELECT l.id, l.time, r.time FROM 
    idtimes AS l LEFT JOIN idtimes AS r ON l.id = r.id

Я полагаю, что внешнее соединение является преднамеренным, и вы хотите получить нулевые значения. Подробнее об этом позже.

WHERE l.time < r.time ORDER BY l.id ASC, l.time ASC;

Вы хотите только r. строка с наименьшим (MIN) временем, которое больше, чем l.time. Это место, где вам нужно подзапрос.

WHERE r.time = (SELECT MIN(time) FROM idtimes r2 where r2.id = l.id AND r2.time > l.time)

Теперь к нулям. Если «нет следующего более высокого времени», тогда SELECT MIN () будет иметь нулевое (или худшее) значение, и само по себе никогда не будет сравниваться ни с чем, поэтому ваше предложение WHERE никогда не будет выполнено, и «самое высокое время» для каждого идентификатора, никогда не может появиться в наборе результатов.

Вы решаете это, удаляя JOIN и перемещая скалярный подзапрос в список SELECT:

SELECT id, time, 
    (SELECT MIN(time) FROM idtimes sub 
        WHERE sub.id = main.id AND sub.time > main.time) as nxttime
  FROM idtimes AS main 
Эрвин Смут
источник
4

Я всегда избегаю использовать подзапросы в SELECTблоке или в FROMблоке, потому что это делает код «грязнее», а иногда и менее эффективным.

Я думаю, что более элегантный способ сделать это:

1. Найдите время больше, чем время строки

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

Вы должны использовать, LEFT JOINчтобы избежать исключения строк, в которых нет времени больше, чем текущая строка.

SELECT
    i1.id,
    i1.time AS time,
    i2.time AS greater_time
FROM
    idtimes AS i1
    LEFT JOIN idtimes AS i2 ON i1.id = i2.id AND i2.time > i1.time

Проблема, как вы упомянули, состоит в том, что у вас есть несколько строк, где next_time больше времени .

+-----+------------+--------------+
| id  | time       | greater_time |
+-----+------------+--------------+
| 155 | 1300000000 | 1311111111   |
| 155 | 1300000000 | 1322222222   |
| 155 | 1311111111 | 1322222222   |
| 155 | 1322222222 |       NULL   |
| 156 | 1312345678 | 1318765432   |
| 156 | 1318765432 |       NULL   |
+-----+------------+--------------+

2. Найдите строки, где большее_время не только больше, но в следующий раз

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

SELECT
    i1.id,
    i1.time AS time,
    i2.time AS next_time,
    i3.time AS intrudor_time
FROM
    idtimes AS i1
    LEFT JOIN idtimes AS i2 ON i1.id = i2.id AND i2.time > i1.time
    LEFT JOIN idtimes AS i3 ON i2.id = i3.id AND i3.time > i1.time AND i3.time < i2.time

ops, у нас все еще есть ложное следующее время !

+-----+------------+--------------+---------------+
| id  | time       | next_time    | intrudor_time |
+-----+------------+--------------+---------------+
| 155 | 1300000000 | 1311111111   |         NULL  |
| 155 | 1300000000 | 1322222222   |    1311111111 |
| 155 | 1311111111 | 1322222222   |         NULL  |
| 155 | 1322222222 |       NULL   |         NULL  |
| 156 | 1312345678 | 1318765432   |         NULL  |
| 156 | 1318765432 |       NULL   |         NULL  |
+-----+------------+--------------+---------------+

Просто отфильтруйте строки, где происходит это событие, добавив WHEREограничение ниже

WHERE
    i3.time IS NULL

Вуаля, у нас есть то, что нам нужно!

+-----+------------+--------------+---------------+
| id  | time       | next_time    | intrudor_time |
+-----+------------+--------------+---------------+
| 155 | 1300000000 | 1311111111   |         NULL  |
| 155 | 1311111111 | 1322222222   |         NULL  |
| 155 | 1322222222 |       NULL   |         NULL  |
| 156 | 1312345678 | 1318765432   |         NULL  |
| 156 | 1318765432 |       NULL   |         NULL  |
+-----+------------+--------------+---------------+

Я надеюсь, что вам все еще нужен ответ через 4 года!

luisfsns
источник
Это умно. Я не уверен, что это легче понять, хотя Я думаю, что если мы is nullзаменим i3 и join на i3 where not exists (select 1 from itimes i3 where [same clause]), то код будет более точно отражать то, что мы хотим выразить.
Эндрю Спенсер
спасибо чувак ты спас мой (следующий) день!
Якоб
2

Прежде чем представить решение, я должен отметить, что это не красиво. Было бы намного проще, если бы у вас AUTO_INCREMENTна столе была какая-то колонка (правда?)

SELECT 
  l.id, l.time, 
  SUBSTRING_INDEX(GROUP_CONCAT(r.time ORDER BY r.time), ',', 1)
FROM 
  idtimes AS l 
  LEFT JOIN idtimes AS r ON (l.id = r.id)
WHERE 
  l.time < r.time
GROUP BY
  l.id, l.time

Объяснение:

  • Соединение такое же, как и у вас: объедините две таблицы, правая получит только большее время
  • GROUP BY оба столбца из левой таблицы: это гарантирует, что мы получим все (id, time)комбинации (которые также известны как уникальные).
  • Для каждого (l.id, l.time)получите первое, r.time которое больше чем l.time. Это происходит при первом заказе r.times via GROUP_CONCAT(r.time ORDER BY r.time), путем разрезания первого токена via SUBSTRING_INDEX.

Удачи, и не ожидайте хорошей производительности, если этот стол большой.

Шломи Ноах
источник
2

Вы также можете получить то, что вы хотите от min()и GROUP BYбез внутреннего выбора:

SELECT l.id, l.time, min(r.time) 
FROM idtimes l 
LEFT JOIN idtimes r on (r.id = l.id and r.time > l.time)
GROUP BY l.id, l.time;

Я бы почти ставка большой суммы денег , которую Оптимизатор превращает это в одно и то же , как ответ Эрвин Смает в любом случае, и это спорно , является ли это яснее, но там для полноты ...

Эндрю Спенсер
источник
1
При всей своей ценности SSMS & SQLServer 2016 понравился ваш запрос намного больше, чем запрос Эрвина (время выполнения 2 с и время выполнения 24 с при наборе результатов ~ 24k)
Натан Лафферти,
Эндрю, кажется, вы проиграли :-)
Эрвин Смут
Интересно, потому что в общем случае подзапрос, который присоединяется к внешней таблице запросов одним из столбцов PK, должен совпадать с подгруппой. Интересно, лучше ли оптимизировать какие-либо другие базы данных? (Кстати, я очень мало знаю об оптимизаторах баз данных; просто любопытно.)
Эндрю Спенсер,