เลือกแถวที่มีวันที่ล่าสุดต่อผู้ใช้


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 ผู้ใช้ในขณะที่ให้ค่า "in" หรือ "out" ให้ฉันทำดังนี้

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

ฉันค่อนข้างใกล้แล้ว แต่ฉันตระหนักว่าการดูจะไม่ยอมรับ subquerys ซึ่งทำให้ยากขึ้นมาก คำถามที่ใกล้เคียงที่สุดที่ฉันได้รับคือ:

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

ซึ่งใกล้ แต่ไม่สมบูรณ์. ฉันรู้ว่ากลุ่มสุดท้ายโดยไม่ควรอยู่ที่นั่น แต่ถ้าไม่มีมันจะส่งคืนเวลาล่าสุด แต่ไม่ใช่กับค่า IO สัมพัทธ์

ความคิดใด ๆ ? ขอบคุณ!



กลับไปที่คู่มือ คุณจะเห็นว่ามีวิธีแก้ปัญหานี้ทั้งที่มีและไม่มี (ที่เกี่ยวข้องและไม่มีการตรวจสอบ)
สตรอเบอร์รี่

@Barmar ในทางเทคนิคตามที่ฉันได้ชี้ให้เห็นในคำตอบของฉันนี่เป็นคำถามที่ซ้ำกันทั้งหมด 700 ข้อพร้อมแท็ก-n-ต่อกลุ่มที่ยิ่งใหญ่ที่สุด
TMS

@Prodikl 'io (enum)' คืออะไร?
Monica Heddneck

ฉันมีคอลัมน์ชื่อ "IO" ซึ่งย่อมาจาก "in or out" เป็นประเภท enum ที่มีค่าเป็นไปได้ "in" หรือ "out" ข้อมูลนี้ใช้เพื่อติดตามเวลาที่ผู้คนเช็คอินและออกจากชั้นเรียน
Keith

คำตอบ:


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
ว้าว! ไม่เพียง แต่ทำงานนี้ฉันได้รับอนุญาตให้สร้างมุมมองด้วยแบบสอบถามนี้แม้ว่าจะมีแบบสอบถามย่อยก็ตาม ก่อนหน้านี้เมื่อฉันพยายามสร้างมุมมองที่มีเคียวรีย่อยมันไม่ยอมให้ฉัน มีกฎว่าทำไมจึงได้รับอนุญาต แต่อีกข้อหนึ่งไม่ได้?
Keith

แปลกมาก. ขอบคุณมาก! อาจเป็นเพราะเคียวรีย่อยของฉันเป็นตารางหลอกที่ฉันเลือกจากซึ่งในตัวอย่างนี้ใช้ในคำสั่ง WHERE
Keith

4
ไม่จำเป็นต้องมีแบบสอบถามย่อย! นอกจากนี้การแก้ปัญหานี้ไม่ทำงานถ้ามีสองระเบียนที่มีตรงเวลาเดียวกัน ไม่จำเป็นต้องพยายามสร้างวงล้อใหม่ทุกครั้งเนื่องจากนี่เป็นปัญหาทั่วไป แต่ให้ไปหาโซลูชันที่ผ่านการทดสอบและปรับแต่งมาแล้ว - @Prodikl ดูคำตอบของฉัน
TMS

อาขอบคุณสำหรับข้อมูลเชิงลึก! ฉันจะลองรหัสใหม่เมื่อฉันอยู่ในสำนักงานพรุ่งนี้
Keith

3
@TMS โซลูชันนี้จะใช้งานได้หากเร็กคอร์ดมีเวลาเดียวกันเนื่องจากคิวรีกำลังค้นหาเร็กคอร์ดที่มี id มากที่สุด นี่หมายความว่าเวลาในตารางเป็นเวลาแทรกซึ่งอาจไม่ใช่ข้อสันนิษฐานที่ดีนัก โซลูชันของคุณจะเปรียบเทียบการประทับเวลาแทนและเมื่อการประทับเวลาสองรายการเหมือนกันคุณจะส่งคืนแถวที่มีรหัสที่มากที่สุดเช่นกัน ดังนั้นวิธีการแก้ปัญหาของคุณยังถือว่าการประทับเวลาในตารางนี้เกี่ยวข้องกับลำดับการแทรกซึ่งเป็นข้อบกพร่องที่ใหญ่ที่สุดสำหรับข้อความค้นหาของคุณทั้งสอง
WebWanderer

73

ไม่จำเป็นต้องพยายามบูรณาการล้อเช่นนี้เป็นเรื่องธรรมดาที่ยิ่งใหญ่ที่สุด-N-ต่อการแก้ไขปัญหากลุ่ม ดีมากวิธีการแก้ปัญหาที่จะนำเสนอ

ฉันชอบโซลูชันที่เรียบง่ายที่สุด ( ดู 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
คำตอบที่ได้รับการยอมรับซึ่งโพสต์โดย Justin อาจเหมาะสมกว่า คำตอบที่ยอมรับจะใช้การสแกนดัชนีย้อนหลังบนคีย์หลักของตารางตามด้วยขีด จำกัด ตามด้วยการสแกนตามลำดับของตาราง ดังนั้นคำตอบที่ยอมรับสามารถปรับให้เหมาะสมได้อย่างมากด้วยดัชนีเพิ่มเติม แบบสอบถามนี้สามารถปรับให้เหมาะสมโดยดัชนีได้เช่นกันเนื่องจากทำการสแกนสองลำดับ แต่ยังมีแฮชและ "แฮชป้องกันการเข้าร่วม" ของผลลัพธ์ของการสแกนลำดับและแฮชของการสแกนลำดับอื่น ๆ ฉันสนใจคำอธิบายว่าแนวทางใดเหมาะสมกว่ากัน
WebWanderer

@TMS คุณช่วยชี้แจงOR (t1.time = t2.time AND t1.Id < t2.Id))ส่วนได้ไหม
Oleg Kuts

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 กล่าวไว้สิ่งนี้จะช้ามากในชุดข้อมูลขนาดกลางถึงใหญ่
Peter Meadley

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
ขอบคุณสำหรับการติดตาม! ใช่ฉันจะสร้างหลายมุมมองถ้าไม่มีวิธีที่ง่ายกว่านี้ ขอบคุณอีกครั้ง
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

ขอบคุณ ฉันรู้ว่าฉันสามารถทำได้โดยใช้แบบสอบถามย่อย แต่ฉันหวังว่าจะเปลี่ยนเป็นมุมมองและจะไม่อนุญาตให้มีการสืบค้นย่อยในมุมมอง AFAIK ฉันจะต้องเปลี่ยนแบบสอบถามย่อยแต่ละรายการให้เป็นมุมมองหรือไม่?
Keith

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 |

ข้อได้เปรียบที่ฉันเห็นในการใช้โซลูชันที่ Justin เสนอคือช่วยให้คุณสามารถเลือกแถวที่มีข้อมูลล่าสุดต่อผู้ใช้ (หรือต่อ id หรือต่อสิ่งใดก็ตาม) แม้กระทั่งจากแบบสอบถามย่อยโดยไม่ต้องใช้มุมมองหรือตารางระดับกลาง

และในกรณีที่คุณใช้งาน 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.
Dewi Morgan

1
ไม่มีรหัสรับประกันว่าจะเป็นรหัสที่มี max (เวลา) ซึ่งอาจเป็นรหัสใดก็ได้ภายในกลุ่ม นี่คือปัญหาที่ฉันมาที่นี่เพื่อแก้ไขยังคงมองหา
robisrob

-3

เป็นไปได้ว่าคุณสามารถจัดกลุ่มตามผู้ใช้แล้วเรียงลำดับตามเวลา desc สิ่งที่ต้องการดังต่อไปนี้

  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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.