Pilih baris dengan tanggal terbaru per pengguna

125

Saya memiliki tabel ("lms_attendance") waktu masuk dan keluar pengguna yang terlihat seperti ini:

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

Saya mencoba membuat tampilan tabel ini yang hanya akan menampilkan rekaman terbaru per id pengguna, sambil memberi saya nilai "masuk" atau "keluar", jadi seperti ini:

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

Sejauh ini saya cukup dekat, tetapi saya menyadari bahwa penayangan tidak akan menerima subquerys, yang membuatnya jauh lebih sulit. Pertanyaan terdekat yang saya dapatkan adalah:

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`

Tapi yang saya dapatkan adalah:

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

Yang mendekati, tapi tidak sempurna. Saya tahu bahwa grup terakhir seharusnya tidak ada di sana, tetapi tanpanya, grup ini mengembalikan waktu terakhir, tetapi tidak dengan nilai IO relatifnya.

Ada ide? Terima kasih!

Keith
sumber
Kembali ke manual. Anda akan melihat bahwa ia menawarkan solusi untuk masalah ini dengan dan tanpa subkueri (berkorelasi dan tidak berkorelasi).
Strawberry
@Barmar, secara teknis, seperti yang saya tunjukkan dalam jawaban saya, ini adalah duplikat dari 700 pertanyaan dengan tag -n-per-grup terbesar .
TMS
@Prodikl, apa itu 'io (enum)'?
Monica Heddneck
Saya memiliki kolom yang disebut "IO" yang berarti "masuk atau keluar", itu adalah jenis enum dengan kemungkinan nilai "masuk" atau "keluar". Ini digunakan untuk melacak ketika orang check in dan out dari kelas.
Keith

Jawaban:

199

Pertanyaan:

SQLFIDDLEExample

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

Hasil:

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

Solusi yang akan bekerja setiap saat:

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)
Justin
sumber
2
Wow! tidak hanya melakukan pekerjaan ini, saya diizinkan untuk membuat tampilan dengan kueri ini meskipun berisi subkueri. sebelumnya, ketika saya mencoba membuat tampilan yang berisi subkueri, saya tidak diizinkan. apakah ada aturan tentang mengapa hal ini diperbolehkan tetapi yang lain tidak?
Keith
sangat aneh. Terima kasih banyak! mungkin karena subquery saya adalah tabel semu yang saya pilih DARI, di mana dalam contoh ini digunakan dalam klausa WHERE.
Keith
4
Tidak perlu subkueri! Selain itu, solusi ini tidak berfungsi jika ada dua record dengan waktu yang sama persis . Tidak perlu mencoba menemukan kembali roda setiap saat, karena ini adalah masalah umum - sebagai gantinya, carilah solusi yang sudah teruji dan dioptimalkan - @Prodikl lihat jawaban saya.
TMS
ah, terima kasih atas wawasannya! saya akan mencoba kode baru ketika saya di kantor besok.
Keith
3
@TMS Solusi ini berfungsi jika rekaman memiliki waktu yang sama persis, karena kueri menemukan rekaman dengan id terbesar. Ini menyiratkan bahwa waktu dalam tabel adalah waktu penyisipan, yang mungkin bukan asumsi yang baik. Solusi Anda malah membandingkan stempel waktu dan, ketika dua stempel waktu identik, Anda juga mengembalikan baris dengan id terbesar. Karenanya, solusi Anda juga mengasumsikan bahwa stempel waktu dalam tabel ini terkait dengan urutan penyisipan, yang merupakan kelemahan terbesar pada kedua kueri Anda.
WebWanderer
73

Tidak perlu mencoba menemukan kembali roda, karena ini biasa terjadi terbesar per grup . Solusi yang sangat bagus disajikan .

Saya lebih suka solusi yang paling sederhana ( lihat SQLFiddle, memperbarui Justin ) tanpa subkueri (sehingga mudah digunakan dalam tampilan):

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

Ini juga berfungsi dalam kasus di mana ada dua rekaman berbeda dengan nilai terbesar yang sama dalam grup yang sama - berkat trik dengan (t1.time = t2.time AND t1.Id < t2.Id). Semua yang saya lakukan di sini adalah untuk memastikan bahwa jika dua rekaman dari pengguna yang sama memiliki waktu yang sama, hanya satu yang dipilih. Tidak masalah jika kriteria itu Idatau sesuatu yang lain - pada dasarnya kriteria apa pun yang dijamin unik akan membuat pekerjaan di sini.

TMS
sumber
1
Penggunaan maksimal t1.time < t2.timedan jumlah minimum t1.time > t2.timeadalah kebalikan dari intuisi awal saya.
Tidak ada
1
@ J.Money karena ada negasi implisit yang disembunyikan: Anda memilih semua catatan dari t1 yang tidak memiliki catatan terkait dari t2 di mana t1.time < t2.timekondisi berlaku :-)
TMS
4
WHERE t2.user IS NULLagak aneh. Peran apa yang dimainkan baris ini?
tumultous_rooster
1
Jawaban yang diterima, diposting oleh Justin, mungkin lebih optimal. Jawaban yang diterima menggunakan pemindaian indeks mundur pada kunci utama tabel, diikuti dengan batas, diikuti dengan pemindaian urutan tabel. Oleh karena itu, jawaban yang diterima dapat sangat dioptimalkan dengan indeks tambahan. Kueri ini juga dapat dioptimalkan dengan indeks, karena melakukan pemindaian dua urutan, namun juga menyertakan hash dan "hash-anti-join" dari hasil pemindaian urutan dan hash dari pemindaian urutan lainnya. Saya akan tertarik dengan penjelasan tentang pendekatan mana yang benar-benar lebih optimal.
WebWanderer
@TMS bisakah Anda menjelaskan OR (t1.time = t2.time AND t1.Id < t2.Id))bagian?
Oleg Kuts
6

Berdasarkan jawaban @TMS, saya suka karena tidak perlu subkueri tetapi menurut saya menghilangkan 'OR'bagian itu sudah cukup dan lebih sederhana untuk dipahami dan dibaca.

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

jika Anda tidak tertarik dengan baris dengan waktu nol, Anda dapat memfilternya di WHEREklausa:

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
pengguna1792210
sumber
Menghilangkan ORbagian tersebut adalah ide yang sangat buruk jika dua rekaman dapat memiliki kesamaan time.
TMS
Saya akan menghindari solusi ini demi kinerja. Seperti yang disebutkan @OlegKuts, ini menjadi sangat lambat pada kumpulan data menengah hingga besar.
Peter Meadley
4

Sudah terpecahkan, tetapi hanya sebagai catatan, pendekatan lain adalah membuat dua tampilan ...

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;

Klik di sini untuk melihatnya beraksi di SQL Fiddle

davmos
sumber
1
terima kasih atas tindak lanjutnya! ya, saya akan membuat banyak tampilan jika tidak ada cara yang lebih mudah. terima kasih lagi
Keith
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
chetan
sumber
Terima kasih. saya tahu saya bisa melakukannya menggunakan subquery, tapi saya berharap untuk mengubahnya menjadi view, dan itu tidak akan mengizinkan subquery dalam views AFAIK. apakah saya harus mengubah setiap sub query menjadi view, dll.?
Keith
join (select * from lms_attendance ) b= join lms_attendance b
azerafati
0
 select result from (
     select vorsteuerid as result, count(*) as anzahl from kreditorenrechnung where kundeid = 7148
     group by vorsteuerid
 ) a order by anzahl desc limit 0,1
Konstantin XFlash Stratigenas
sumber
0

Jika Anda menggunakan MySQL 8.0 atau lebih tinggi, Anda dapat menggunakan fungsi Window :

Pertanyaan:

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;

Hasil:

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

Keuntungan yang saya lihat dibandingkan menggunakan solusi yang diusulkan oleh Justin adalah memungkinkan Anda memilih baris dengan data terbaru per pengguna (atau per id, atau per apa pun) bahkan dari subkueri tanpa memerlukan tampilan atau tabel perantara.

Dan jika Anda menjalankan HANA, ini juga ~ 7 kali lebih cepat: D

Nicolas Brauer
sumber
-1

Oke, ini mungkin hack atau rawan kesalahan, tetapi entah bagaimana ini berfungsi juga-

SELECT id, MAX(user) as user, MAX(time) as time, MAX(io) as io FROM lms_attendance GROUP BY id;
kev
sumber
-2

Coba kueri ini:

  select id,user, max(time), io 
  FROM lms_attendance group by user;
Sugan
sumber
Cobalah membuat SQLFiddle ini. Anda mungkin akan menemukan itu iddan iomerupakan kolom nonagregasi, yang tidak dapat digunakan dalam file group by.
Dewi Morgan
1
tidak ada jaminan id akan menjadi id dengan max (waktu), bisa jadi salah satu id dalam grup. ini adalah masalah yang saya datang ke sini untuk menyelesaikan, masih mencari
robisrob
-3

Mungkin Anda dapat melakukan kelompok berdasarkan pengguna dan kemudian memesan berdasarkan waktu desc. Sesuatu seperti di bawah ini

  SELECT * FROM lms_attendance group by user order by time desc;
pengguna2365199
sumber
-3

Ini berhasil untuk saya:

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
Alvaro Sifuentes
sumber