Виберіть рядок із останньою датою на користувача


125

У мене є таблиця ("lms_attendance") періодів реєстрації та виїзду користувачів, яка виглядає приблизно так:

id  user    time    io (enum)
1   9   1370931202  out
2   9   1370931664  out
3   6   1370932128  out
4   12  1370932128  out
5   12  1370933037  in

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

id  user    time    io
2   9   1370931664  out
3   6   1370932128  out
5   12  1370933037  in

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

select 
    `lms_attendance`.`id` AS `id`,
    `lms_attendance`.`user` AS `user`,
    max(`lms_attendance`.`time`) AS `time`,
    `lms_attendance`.`io` AS `io` 
from `lms_attendance` 
group by 
    `lms_attendance`.`user`, 
    `lms_attendance`.`io`

Але я отримую:

id  user    time    io
3   6   1370932128  out
1   9   1370931664  out
5   12  1370933037  in
4   12  1370932128  out

Що близько, але не ідеально. Я знаю, що останньої групи by не повинно бути там, але без неї вона повертає останній час, але не з відносним значенням IO.

Будь-які ідеї? Дякую!



Поверніться до посібника. Ви побачите, що він пропонує рішення цієї проблеми як із підзапросами, так і без них (корельовані та некорельовані).
Полуниця

@Barmar, технічно, як я зазначив у своїй відповіді, це дублікат усіх 700 питань з найбільшою темою n-per-group .
TMS

@Prodikl, що таке 'io (enum)'?
Моніка Геднек

У мене був стовпчик під назвою "IO", який означає "увійти чи вийти", це був тип перерахунку з можливими значеннями "в" чи "поза". Це було використано для відстеження того, коли люди заїжджали та виходили з уроку.
Кіт

Відповіді:


199

Запит:

Приклад SQLFIDDLEExample

SELECT t1.*
FROM lms_attendance t1
WHERE t1.time = (SELECT MAX(t2.time)
                 FROM lms_attendance t2
                 WHERE t2.user = t1.user)

Результат:

| ID | USER |       TIME |  IO |
--------------------------------
|  2 |    9 | 1370931664 | out |
|  3 |    6 | 1370932128 | out |
|  5 |   12 | 1370933037 |  in |

Рішення, яке буде працювати щоразу:

Приклад SQLFIDDLEExample

SELECT t1.*
FROM lms_attendance t1
WHERE t1.id = (SELECT t2.id
                 FROM lms_attendance t2
                 WHERE t2.user = t1.user            
                 ORDER BY t2.id DESC
                 LIMIT 1)

2
Оце Так! це не тільки зробило цю роботу, і мені було дозволено створити подання з цим запитом, хоча він містить підзапити. раніше, коли я намагався створити подання, що містить підзапити, це мені не дозволило. чи є правила, чому це дозволено, але інше - ні?
Кіт

дуже дивно. спасибі тонну! Можливо, це було тому, що мій підзапит був псевдо таблицею, яку я вибирав ВІД, де в цьому прикладі його використовується в пункті WHERE.
Кіт

4
Не потрібно підпитів! Більше того, це рішення не працює, якщо є два записи з точно однаковим часом . Не потрібно намагатися кожен раз винаходити колесо, оскільки це звичайна проблема - натомість перейдіть на вже перевірені та оптимізовані рішення - @Prodikl дивіться мою відповідь.
TMS

ах, дякую за розуміння! Я спробую новий код, коли завтра я в офісі.
Кіт

3
@TMS Це рішення спрацьовує, якщо записи мають точно той же час, оскільки запит розміщує запис із найбільшим ідентифікатором. Це означає, що час у таблиці - це час вставки, що може бути не гарним припущенням. Натомість ваше рішення порівнює часові позначки, і коли дві часові позначки однакові, ви також повертаєте рядок із найбільшим ідентифікатором. Отже, ваше рішення також передбачає, що часова позначка в цій таблиці пов'язана з порядком вставки, який є найбільшим недоліком для обох ваших запитів.
WebWanderer

73

Не потрібно намагатися винаходити колесо, оскільки це загальна проблема, яка найбільше не стосується групи . Представлено дуже приємне рішення .

Я віддаю перевагу найбільш спрощеному рішенню ( див. SQLFiddle, оновлений Джастін ) без підзапитів (таким чином, простий у використанні у видах):

SELECT t1.*
FROM lms_attendance AS t1
LEFT OUTER JOIN lms_attendance AS t2
  ON t1.user = t2.user 
        AND (t1.time < t2.time 
         OR (t1.time = t2.time AND t1.Id < t2.Id))
WHERE t2.user IS NULL

Це також працює у випадку, коли в одній групі є два різні записи з однаковим найбільшим значенням - завдяки трюку з (t1.time = t2.time AND t1.Id < t2.Id). Все, що я роблю тут, - це запевнити, що у випадку, коли дві записи одного і того ж користувача мають один і той же час, вибирається лише одна. Насправді не має значення, чи є критерії Idчи щось інше - в основному будь-які критерії, які гарантовано є унікальними, зробили б роботу тут.


1
Максимум використання t1.time < t2.timeта мінімум було б, t1.time > t2.timeщо є протилежним моїй початковій інтуїції.
Жодної

1
@ J.Money, оскільки приховано заперечне заперечення: ви вибираєте всі записи з t1, які не мають відповідного запису з t2, де t1.time < t2.timeзастосовується умова :-)
TMS

4
WHERE t2.user IS NULLтрохи дивно. Яку роль відіграє ця лінія?
tumultous_rooster

1
Прийнята відповідь, опублікована Джастіном, може бути більш оптимальною. У прийнятій відповіді використовується зворотне сканування індексу на первинному ключі таблиці з подальшим обмеженням, після чого послідовність сканування таблиці. Тому прийняту відповідь можна значно оптимізувати додатковим індексом. Цей запит також може бути оптимізований за допомогою індексу, оскільки він виконує два сканування послідовності, але також включає хеш та "хеш-анти-приєднання" результатів сканування послідовності та хеш-сканування іншої послідовності. Мені було б цікаво пояснити, який підхід є справді більш оптимальним.
WebWanderer

@TMS, чи можете ви проясніть OR (t1.time = t2.time AND t1.Id < t2.Id))розділ?
Олег Куць

6

Виходячи з відповіді @TMS, мені це подобається, тому що немає необхідності в підзапитах, але я думаю, що пропускання 'OR'частини буде достатньою і набагато простішою для розуміння та читання.

SELECT t1.*
FROM lms_attendance AS t1
LEFT JOIN lms_attendance AS t2
  ON t1.user = t2.user 
        AND t1.time < t2.time
WHERE t2.user IS NULL

якщо вас не цікавлять рядки з нульовим часом, ви можете відфільтрувати їх у WHEREпункті:

SELECT t1.*
FROM lms_attendance AS t1
LEFT JOIN lms_attendance AS t2
  ON t1.user = t2.user 
        AND t1.time < t2.time
WHERE t2.user IS NULL and t1.time IS NOT NULL

Опущення ORчастини - це дуже погана ідея, якщо два записи можуть мати однакові time.
TMS

Я б уникнув цього рішення заради ефективності. Як зазначав @OlegKuts, це стає дуже повільним у великих наборах даних від середнього до великого.
Пітер Мідлі

4

Вже вирішено, але для запису ще одним підходом було б створення двох поглядів ...

CREATE TABLE lms_attendance
(id int, user int, time int, io varchar(3));

CREATE VIEW latest_all AS
SELECT la.user, max(la.time) time
FROM lms_attendance la 
GROUP BY la.user;

CREATE VIEW latest_io AS
SELECT la.* 
FROM lms_attendance la
JOIN latest_all lall 
    ON lall.user = la.user
    AND lall.time = la.time;

INSERT INTO lms_attendance 
VALUES
(1, 9, 1370931202, 'out'),
(2, 9, 1370931664, 'out'),
(3, 6, 1370932128, 'out'),
(4, 12, 1370932128, 'out'),
(5, 12, 1370933037, 'in');

SELECT * FROM latest_io;

Клацніть тут, щоб побачити це в дії на SQL Fiddle


1
дякую за подальші дії! так, я збирався створити кілька переглядів, якби не було більш простого способу. ще раз дякую
Кіт

0
select b.* from 

    (select 
        `lms_attendance`.`user` AS `user`,
        max(`lms_attendance`.`time`) AS `time`
    from `lms_attendance` 
    group by 
        `lms_attendance`.`user`) a

join

    (select * 
    from `lms_attendance` ) b

on a.user = b.user
and a.time = b.time

Дякую. Я знаю, що я можу це зробити за допомогою підзапиту, але я сподівався перетворити це на перегляд, і це не дозволить підзапроси у видах AFAIK. Чи повинен я перетворити кожен підзапит на перегляд тощо?
Кіт

join (select * from lms_attendance ) b= join lms_attendance b
azerafati


0

Якщо ваш MySQL 8.0 або новіший, ви можете використовувати функції Window :

Запит:

DBFiddleExample

SELECT DISTINCT
FIRST_VALUE(ID) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS ID,
FIRST_VALUE(USER) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS USER,
FIRST_VALUE(TIME) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS TIME,
FIRST_VALUE(IO) OVER (PARTITION BY lms_attendance.USER ORDER BY lms_attendance.TIME DESC) AS IO
FROM lms_attendance;

Результат:

| ID | USER |       TIME |  IO |
--------------------------------
|  2 |    9 | 1370931664 | out |
|  3 |    6 | 1370932128 | out |
|  5 |   12 | 1370933037 |  in |

Перевага, яку я бачу над використанням рішення, запропонованого Джастіном, полягає в тому, що воно дозволяє вибирати рядок з останніми даними на кожного користувача (або за ідентифікатором, або за будь-яким іншим) навіть із підзапитів без необхідності проміжного перегляду чи таблиці.

І якщо ваш HANA працює, це також ~ 7 разів швидше: D


-1

Гаразд, це може бути або хак, або схильний до помилок, але якимось чином це працює також

SELECT id, MAX(user) as user, MAX(time) as time, MAX(io) as io FROM lms_attendance GROUP BY id;

-2

Спробуйте цей запит:

  select id,user, max(time), io 
  FROM lms_attendance group by user;

Спробуйте зробити SQLFiddle з цього. Ви, ймовірно, виявите це idі ioє неагрегованими стовпцями, які не можна використовувати в group by.
Деві Морган

1
немає гарантії, що ідентифікатор буде ідентифікатором з max (час), це може бути будь-який ідентифікатор групи. це проблема, яку я прийшов сюди вирішити, все ще шукаю
robisrob

-3

Можливо, ви можете зробити групу за користувачем, а потім замовити за часом опис. Щось на зразок нижче

  SELECT * FROM lms_attendance group by user order by time desc;

-3

Це працювало для мене:

SELECT user, time FROM 
(
    SELECT user, time FROM lms_attendance --where clause
) AS T 
WHERE (SELECT COUNT(0) FROM table WHERE user = T.user AND time > T.time) = 0
ORDER BY user ASC, time DESC
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.