MySQL“ Group By” และ“ Order By”


98

ฉันต้องการที่จะสามารถเลือกแถวจำนวนมากจากตารางอีเมลและจัดกลุ่มตามผู้ส่งจาก ข้อความค้นหาของฉันมีลักษณะดังนี้:

SELECT 
    `timestamp`, `fromEmail`, `subject`
FROM `incomingEmails` 
GROUP BY LOWER(`fromEmail`) 
ORDER BY `timestamp` DESC

แบบสอบถามเกือบจะทำงานตามที่ฉันต้องการ - โดยจะเลือกระเบียนที่จัดกลุ่มตามอีเมล ปัญหาคือหัวเรื่องและการประทับเวลาไม่ตรงกับบันทึกล่าสุดสำหรับที่อยู่อีเมลหนึ่ง ๆ

ตัวอย่างเช่นอาจส่งคืน:

fromEmail: john@example.com, subject: hello
fromEmail: mark@example.com, subject: welcome

เมื่อระเบียนในฐานข้อมูลคือ:

fromEmail: john@example.com, subject: hello
fromEmail: john@example.com, subject: programming question
fromEmail: mark@example.com, subject: welcome

หากหัวข้อ "คำถามการเขียนโปรแกรม" เป็นหัวข้อล่าสุดฉันจะให้ MySQL เลือกบันทึกนั้นเมื่อจัดกลุ่มอีเมลได้อย่างไร

คำตอบ:


142

วิธีแก้ปัญหาง่ายๆคือการรวมคิวรีไว้ในส่วนที่เลือกย่อยด้วยคำสั่ง ORDER ก่อนและใช้ GROUP BY ในภายหลัง :

SELECT * FROM ( 
    SELECT `timestamp`, `fromEmail`, `subject`
    FROM `incomingEmails` 
    ORDER BY `timestamp` DESC
) AS tmp_table GROUP BY LOWER(`fromEmail`)

คล้ายกับการใช้การเข้าร่วม แต่ดูดีกว่ามาก

การใช้คอลัมน์แบบไม่รวมใน SELECT กับ GROUP BY clause นั้นไม่ได้มาตรฐาน โดยทั่วไป MySQL จะคืนค่าของแถวแรกที่พบและทิ้งส่วนที่เหลือ คำสั่ง ORDER BY จะใช้กับค่าคอลัมน์ที่ส่งคืนเท่านั้นไม่ใช่กับค่าที่ถูกทิ้ง

การอัปเดตที่สำคัญ การเลือกคอลัมน์ที่ไม่รวมที่ใช้ในการทำงานจริง แต่ไม่ควรพึ่งพา ตามเอกสารคู่มือMySQL "สิ่งนี้มีประโยชน์เป็นหลักเมื่อค่าทั้งหมดในคอลัมน์ที่ไม่ได้รวมแต่ละคอลัมน์ที่ไม่มีชื่อใน GROUP BY เหมือนกันสำหรับแต่ละกลุ่มเซิร์ฟเวอร์มีอิสระที่จะเลือกค่าใด ๆจากแต่ละกลุ่มดังนั้นค่านี้จะไม่เหมือนกัน สิ่งที่เลือกนั้นไม่แน่นอน "

ตั้งแต่5.7.5 ONLY_FULL_GROUP_BY ถูกเปิดใช้งานโดยค่าเริ่มต้นดังนั้นคอลัมน์ที่ไม่รวมทำให้เกิดข้อผิดพลาดในการสืบค้น (ER_WRONG_FIELD_WITH_GROUP)

ดังที่ @mikep ชี้ให้เห็นด้านล่างวิธีแก้ปัญหาคือการใช้ANY_VALUE ()ตั้งแต่ 5.7 ขึ้นไป

ดู http://www.cafewebmaster.com/mysql-order-sort-group https://dev.mysql.com/doc/refman/5.6/en/group-by-handling.html https://dev.mysql .com / doc / refman / 5.7 / th / group-by-Handling.html https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_any-value


7
ฉันคิดวิธีแก้ปัญหาเดียวกันเมื่อสองสามปีก่อนและเป็นวิธีแก้ปัญหาที่ยอดเยี่ยม ความรุ่งโรจน์ถึง b7kich สองประเด็นที่นี่ ... GROUP BY ไม่คำนึงถึงตัวพิมพ์เล็กและใหญ่ดังนั้น LOWER () จึงไม่จำเป็นและประการที่สอง $ userID ดูเหมือนจะเป็นตัวแปรโดยตรงจาก PHP โค้ดของคุณอาจมีช่องโหว่ในการฉีด sql หาก $ userID เป็นผู้จัดหาและไม่บังคับ เป็นจำนวนเต็ม
velcrow

การอัปเดตที่สำคัญยังใช้กับ MariaDB: mariadb.com/kb/en/mariadb/…
Arthur Shipkowski

1
As of 5.7.5 ONLY_FULL_GROUP_BY is enabled by default, i.e. it's impossible to use non-aggregate columns.โหมด SQL สามารถเปลี่ยนได้ระหว่างรันไทม์โดยไม่มีสิทธิ์ของผู้ดูแลระบบดังนั้นจึงง่ายมากที่จะปิดใช้งาน ONLY_FULL_GROUP_BY ตัวอย่างเช่น: SET SESSION sql_mode = '';. Demo: db-fiddle.com/f/esww483qFQXbXzJmkHZ8VT/3
mikep

1
หรือทางเลือกอื่นในการบายพาสที่เปิดใช้ ONLY_FULL_GROUP_BY คือการใช้ ANY_VALUE () ดูเพิ่มเติมdev.mysql.com/doc/refman/8.0/en/…
mikep

คำตอบนี้ไม่ถูกต้อง
มาระโก

44

นี่คือแนวทางเดียว:

SELECT cur.textID, cur.fromEmail, cur.subject, 
     cur.timestamp, cur.read
FROM incomingEmails cur
LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.timestamp < next.timestamp
WHERE next.timestamp is null
and cur.toUserID = '$userID' 
ORDER BY LOWER(cur.fromEmail)

โดยทั่วไปคุณจะเข้าร่วมตารางด้วยตัวมันเองโดยค้นหาแถวในภายหลัง ในประโยคที่คุณระบุว่าไม่สามารถมีแถวในภายหลังได้ ซึ่งจะให้เฉพาะแถวล่าสุด

หากอาจมีอีเมลหลายฉบับที่มีการประทับเวลาเดียวกันคำค้นหานี้จะต้องมีการปรับแต่ง หากมีคอลัมน์รหัสส่วนเพิ่มในตารางอีเมลให้เปลี่ยน JOIN เช่น:

LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.id < next.id

บอกว่าtextIDคลุมเครือ = /
John Kurlak

1
จากนั้นลบ ambuigity และนำหน้าด้วยชื่อตารางเช่น cur.textID เปลี่ยนแปลงในคำตอบเช่นกัน
Andomar

นี่เป็นทางออกเดียวที่สามารถทำได้กับ Doctrine DQL
VisioN

วิธีนี้ใช้ไม่ได้เมื่อคุณพยายามรวมคอลัมน์หลายคอลัมน์ด้วยตนเอง IE เมื่อคุณพยายามค้นหาอีเมลล่าสุดและชื่อผู้ใช้ล่าสุดและคุณต้องใช้การรวมซ้ายด้วยตนเองหลายตัวเพื่อดำเนินการนี้ในแบบสอบถามเดียว
Loveen Dyall

เมื่อทำงานกับการประทับเวลา / วันที่ในอดีตและอนาคตหากต้องการ จำกัด ชุดผลลัพธ์เป็นวันที่ที่ไม่ใช่วันที่ในอนาคตคุณต้องเพิ่มเงื่อนไขอื่นในLEFT JOINเกณฑ์AND next.timestamp <= UNIX_TIMESTAMP()
Will B.

34

ตามที่ได้ระบุไว้ในการตอบกลับคำตอบปัจจุบันไม่ถูกต้องเนื่องจาก GROUP BY เลือกบันทึกจากหน้าต่างโดยพลการ

หากมีใครใช้ MySQL 5.6 หรือ MySQL 5.7 ด้วยONLY_FULL_GROUP_BYแบบสอบถาม (กำหนด) ที่ถูกต้องคือ:

SELECT incomingEmails.*
  FROM (
    SELECT fromEmail, MAX(timestamp) `timestamp`
    FROM incomingEmails
    GROUP BY fromEmail
  ) filtered_incomingEmails
  JOIN incomingEmails USING (fromEmail, timestamp)
GROUP BY fromEmail, timestamp

เพื่อให้การสืบค้นทำงานได้อย่างมีประสิทธิภาพจำเป็นต้องมีการจัดทำดัชนีที่เหมาะสม

โปรดทราบว่าเพื่อจุดประสงค์ในการทำให้เข้าใจง่ายฉันได้ลบสิ่งนี้ออกLOWER()ซึ่งโดยส่วนใหญ่จะไม่ถูกนำมาใช้


2
นี่น่าจะเป็นคำตอบที่ถูกต้อง ฉันเพิ่งค้นพบข้อบกพร่องบนเว็บไซต์ของฉันที่เกี่ยวข้องกับเรื่องนี้ order byใน subselect ในคำตอบอื่น ๆ ที่มีผลที่ทุกคน
Jette

1
OMG โปรดทำให้คำตอบนี้เป็นที่ยอมรับ คนที่ยอมรับเสียเวลาไป 5 ชั่วโมง :(
Richard Kersey

29

ทำ GROUP BY หลัง ORDER BY โดยการตัดคำค้นหาของคุณด้วย GROUP BY ดังนี้:

SELECT t.* FROM (SELECT * FROM table ORDER BY time DESC) t GROUP BY t.from

1
ดังนั้น GROUP BY` จะเลือกล่าสุดโดยอัตโนมัติtimeหรือใหม่ที่สุดtimeหรือสุ่ม?
xrDDDD

1
เลือกเวลาใหม่ล่าสุดเนื่องจากเราเรียงลำดับตามtime DESCแล้วจัดกลุ่มโดยรับเวลาแรก (ล่าสุด)
11101101b

ตอนนี้ถ้าฉันสามารถเข้าร่วมในการเลือกย่อยใน VIEWS ได้ใน mysql 5.1 เท่านั้น คุณสมบัติดังกล่าวอาจเป็นรุ่นที่ใหม่กว่า
IcarusNM

22

ตามมาตรฐาน SQL คุณไม่สามารถใช้คอลัมน์ที่ไม่ใช่แบบรวมในรายการที่เลือกได้ MySQL อนุญาตการใช้งานดังกล่าว (ไม่ใช้โหมด ONLY_FULL_GROUP_BY) แต่ผลลัพธ์ไม่สามารถคาดเดาได้

ONLY_FULL_GROUP_BY

ก่อนอื่นคุณควรเลือก fromEmail, MIN (read) จากนั้นด้วยแบบสอบถามที่สอง (หรือแบบสอบถามย่อย) - Subject


MIN (อ่าน) จะส่งกลับค่าต่ำสุดของ "อ่าน" เขาอาจมองหาธง "อ่าน" ของอีเมลล่าสุดแทน
Andomar

2

ฉันพยายามดิ้นรนกับทั้งสองวิธีนี้เพื่อให้ได้แบบสอบถามที่ซับซ้อนกว่าที่แสดงไว้เนื่องจากวิธีการสืบค้นย่อยนั้นไม่แน่นอนอย่างมากไม่ว่าฉันจะใส่ดัชนีใดก็ตามและเนื่องจากฉันไม่สามารถเข้าร่วมด้วยตนเองภายนอกผ่านไฮเบอร์เนตได้

วิธีที่ดีที่สุด (และง่ายที่สุด) ในการทำเช่นนี้คือการจัดกลุ่มตามสิ่งที่สร้างขึ้นเพื่อให้มีการต่อเขตข้อมูลที่คุณต้องการจากนั้นดึงออกมาโดยใช้นิพจน์ในส่วนคำสั่ง SELECT หากคุณต้องการทำ MAX () ตรวจสอบให้แน่ใจว่าฟิลด์ที่คุณต้องการให้ MAX () over อยู่ที่ส่วนท้ายที่สำคัญที่สุดของเอนทิตีที่ต่อกัน

กุญแจสำคัญในการทำความเข้าใจสิ่งนี้คือการสืบค้นจะมีเหตุผลก็ต่อเมื่อฟิลด์อื่น ๆ เหล่านี้ไม่แปรผันสำหรับเอนทิตีใด ๆ ที่ตรงตามค่าสูงสุด () ดังนั้นในแง่ของการจัดเรียงส่วนอื่น ๆ ของการเรียงต่อกันจึงสามารถละเว้น โดยจะอธิบายวิธีดำเนินการที่ด้านล่างสุดของลิงก์นี้ http://dev.mysql.com/doc/refman/5.0/en/group-by-hidden-columns.html

หากคุณสามารถรับเหตุการณ์แทรก / อัปเดต (เช่นทริกเกอร์) เพื่อคำนวณการเชื่อมต่อของฟิลด์ล่วงหน้าคุณสามารถจัดทำดัชนีได้และแบบสอบถามจะเร็วราวกับว่ากลุ่มอยู่เหนือฟิลด์ที่คุณต้องการให้ MAX ( ). คุณยังสามารถใช้เพื่อรับสูงสุดของหลายฟิลด์ ฉันใช้มันเพื่อทำแบบสอบถามกับต้นไม้หลายมิติที่แสดงเป็นชุดที่ซ้อนกัน

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