Выберите строку с самой последней датой для каждого пользователя


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

Я пытаюсь создать представление этой таблицы, которое выводило бы только самую последнюю запись для каждого идентификатора пользователя, давая мне значение «in» или «out», например:

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

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

Любые идеи? Спасибо!



Вернитесь к руководству. Вы увидите, что он предлагает решения этой проблемы как с подзапросами, так и без (коррелированными и некоррелированными).
Strawberry

@Barmar, технически, как я указал в своем ответе, это дубликат всех 700 вопросов с тегом наибольшее число n на группу .
TMS

@Prodikl, что такое io (enum)?
Моника Хедднек

У меня был столбец с названием «IO», что означает «in or out», это был тип перечисления с возможными значениями «in» или «out». Это использовалось для отслеживания того, когда люди приходили в класс и выходили из него.
Кейт

Ответы:


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
Вот Это Да! не только выполнил эту работу, мне было разрешено создать представление с этим запросом, даже если оно содержит подзапросы. раньше, когда я пытался создать представление, содержащее подзапросы, мне это не позволяло. есть ли правила, почему это разрешено, а другое - нет?
Кейт

очень странно. благодаря тонну! возможно, это произошло потому, что мой подзапрос был псевдотаблицей, которую я выбирал FROM, где в этом примере он использовался в предложении WHERE.
Кейт

4
Нет необходимости в подзапросах! Более того, это решение не работает, если есть две записи с одинаковым временем . Нет необходимости каждый раз изобретать велосипед, это обычная проблема - вместо этого воспользуйтесь уже проверенными и оптимизированными решениями - @Prodikl см. Мой ответ.
TMS

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

3
@TMS Это решение работает, если записи имеют одно и то же время, поскольку запрос находит запись с наибольшим идентификатором. Это означает, что время в таблице - это время вставки, что может быть плохим предположением. Вместо этого ваше решение сравнивает метки времени, и, когда две метки времени идентичны, вы также возвращаете строку с наибольшим идентификатором. Следовательно, ваше решение также предполагает, что метка времени в этой таблице связана с порядком вставки, что является самым большим недостатком в обоих ваших запросах.
WebWanderer 08

73

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

Я предпочитаю наиболее упрощенное решение ( см. SQLFiddle, обновленный Justin's ) без подзапросов (что упрощает использование в представлениях):

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 08

@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 или выше, вы можете использовать оконные функции :

Запрос:

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
нет гарантии, что id будет идентификатором с max (время), это может быть любой из идентификаторов в группе. это проблема, которую я пришел решить, но все еще ищу
robisrob 05

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