Як отримати поточне та наступне більші значення за один вибір?


18

У мене є таблиця "idtimes" (MySQL 5.0.22-журнал) InnoDB зі стовпцями

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

зі складним унікальним ключем

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

тож може бути декілька часових позначок на id та кілька ідентифікаторів на часову марку.

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

+-----+------------+------------+
| 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

Зробити приєднання - це одне, що може знадобитися.

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;

Ви хочете лише р. рядок, який має найменший (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 ніколи не буде задоволений, і "найвищий час" для кожного ідентифікатора, ніколи не може відображатися в наборі результатів.

Ви вирішуєте це, усуваючи ПРИЄДНАЙТЕСЬ та переміщуючи скалярний підзапит у список ВИБІР:

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між idtimes столом з самими собою, стримуючим об'єднанням з тим же ідентифікатором і час більше , ніж час поточного рядка.

Ви повинні використовувати, 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 не тільки більше , але next_time

Кращий спосіб для фільтрації всіх цих непотрібних рядків , щоб з'ясувати, чи є час між часом (більше) і 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, у нас ще є помилка next_time !

+-----+------------+--------------+---------------+
| 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 роки!


Це розумно. Я не впевнений, що це легше зрозуміти. Я думаю, що якби ми замінили is nulli і приєдналися до i3 where not exists (select 1 from itimes i3 where [same clause]), код буде більш точно відображати те, що ми хочемо висловити.
Ендрю Спенсер

thx чувак, ти врятував мій (наступний) день!
Якоб

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

Пояснення:

  • Таке ж приєднання, як і ваше: приєднайтеся до двох таблиць, правильний отримує лише більші часи
  • Згрупуйте за обома стовпцями з лівої таблиці: це гарантує отримання всіх (id, time) комбінацій (які, як відомо, є унікальними).
  • Для кожного (l.id, l.time)отримайте перше, r.time яке більше, ніж l.time. Це відбувається з першим замовленням r.times via GROUP_CONCAT(r.time ORDER BY r.time), нарізанням першого маркера viaSUBSTRING_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-х хвилин виконання на ~ 24 к. Набір результатів)
Nathan Lafferty

Ендрю здається, що ви програли ставку :-)
Ервін Смоут

Цікаво, адже має бути загальним випадком, що підзапит, який приєднується до зовнішньої таблиці запитів одним із стовпців PK, є тим самим, що і група. Цікаво, чи оптимізували б це будь-які інші бази даних. (Я дуже мало знаю про оптимізатори баз даних BTW; мені просто цікаво.)
Ендрю Спенсер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.