จะต้องปรากฏในข้อ GROUP BY หรือจะใช้ในฟังก์ชั่นรวม


276

ฉันมีตารางที่ดูเหมือนผู้โทรรายนี้ 'makerar'

 cname  | wmname |          avg           
--------+-------------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | luffy  | 1.00000000000000000000
 spain  | usopp  |     5.0000000000000000

และฉันต้องการเลือกเฉลี่ยสูงสุดสำหรับแต่ละชื่อ

SELECT cname, wmname, MAX(avg)  FROM makerar GROUP BY cname;

แต่ฉันจะได้รับข้อผิดพลาด

ERROR:  column "makerar.wmname" must appear in the GROUP BY clause or be used in an   aggregate function 
LINE 1: SELECT cname, wmname, MAX(avg)  FROM makerar GROUP BY cname;

ดังนั้นฉันทำสิ่งนี้

SELECT cname, wmname, MAX(avg)  FROM makerar GROUP BY cname, wmname;

อย่างไรก็ตามสิ่งนี้จะไม่ให้ผลลัพธ์ที่ต้องการและแสดงผลลัพธ์ที่ไม่ถูกต้องด้านล่าง

 cname  | wmname |          max           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | luffy  | 1.00000000000000000000
 spain  | usopp  |     5.0000000000000000

ผลลัพธ์ที่แท้จริงควรเป็น

 cname  | wmname |          max           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | usopp  |     5.0000000000000000

ฉันจะแก้ไขปัญหานี้ได้อย่างไร

หมายเหตุ: ตารางนี้เป็นมุมมองที่สร้างขึ้นจากการดำเนินการก่อนหน้า


2
ที่เกี่ยวข้อง: stackoverflow.com/q/18061285/398670
Craig Ringer

ฉันไม่เข้าใจ ทำไมwmname="usopp"คาดหวังและไม่ได้ตัวอย่างเช่นwmname="luffy"?
AndreKR

คำตอบ:


226

ใช่นี่เป็นปัญหาการรวมตัวโดยทั่วไป ก่อนSQL3 (1999)ฟิลด์ที่เลือกจะต้องปรากฏในGROUP BYข้อ [*]

หากต้องการแก้ไขปัญหานี้คุณต้องคำนวณการรวมในแบบสอบถามย่อยและเข้าร่วมกับตัวเองเพื่อรับคอลัมน์เพิ่มเติมที่คุณต้องการแสดง:

SELECT m.cname, m.wmname, t.mx
FROM (
    SELECT cname, MAX(avg) AS mx
    FROM makerar
    GROUP BY cname
    ) t JOIN makerar m ON m.cname = t.cname AND t.mx = m.avg
;

 cname  | wmname |          mx           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | usopp  |     5.0000000000000000

แต่คุณอาจใช้ฟังก์ชั่นหน้าต่างซึ่งดูง่ายกว่า:

SELECT cname, wmname, MAX(avg) OVER (PARTITION BY cname) AS mx
FROM makerar
;

สิ่งเดียวที่มีวิธีนี้คือมันจะแสดงระเบียนทั้งหมด (ฟังก์ชั่นหน้าต่างไม่ได้จัดกลุ่ม) แต่มันจะแสดงให้ถูกต้อง (เช่น maxed ที่cnameระดับ) MAXสำหรับประเทศในแต่ละแถวดังนั้นมันขึ้นอยู่กับคุณ:

 cname  | wmname |          mx           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | luffy  |     5.0000000000000000
 spain  | usopp  |     5.0000000000000000

วิธีแก้ปัญหาที่มีความสง่างามน้อยกว่าเพื่อแสดงสิ่ง(cname, wmname)อันดับเดียวที่ตรงกับค่าสูงสุดคือ:

SELECT DISTINCT /* distinct here matters, because maybe there are various tuples for the same max value */
    m.cname, m.wmname, t.avg AS mx
FROM (
    SELECT cname, wmname, avg, ROW_NUMBER() OVER (PARTITION BY avg DESC) AS rn 
    FROM makerar
) t JOIN makerar m ON m.cname = t.cname AND m.wmname = t.wmname AND t.rn = 1
;


 cname  | wmname |          mx           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | usopp  |     5.0000000000000000

[*]: น่าสนใจพอแม้ว่าประเภทข้อมูลจำเพาะจะอนุญาตให้เลือกเขตข้อมูลที่ไม่ได้จัดกลุ่มได้ แต่เครื่องมือสำคัญดูเหมือนจะไม่ชอบ Oracle และ SQLServer ไม่อนุญาตสิ่งนี้เลย Mysql ใช้เพื่ออนุญาตตามค่าเริ่มต้น แต่ตอนนี้ตั้งแต่ 5.7 ผู้ดูแลระบบต้องเปิดใช้งานตัวเลือกนี้ ( ONLY_FULL_GROUP_BY) ด้วยตนเองในการกำหนดค่าเซิร์ฟเวอร์เพื่อให้รองรับคุณสมบัตินี้ ...


1
ขอบคุณไวยากรณ์คือสิ่งที่ถูกต้อง แต่คุณต้องเปรียบเทียบค่าของ mx และ avg เมื่อเข้าร่วม
RandomGuy

1
ใช่ไวยากรณ์ของคุณถูกต้องและกำจัดสิ่งที่ซ้ำกัน แต่คุณต้อง m.avg = t.mx ในตอนท้าย (หลังจากที่คุณเขียน JOING) เพื่อให้ได้ผลลัพธ์ที่ต้องการ
RandomGuy

1
@Sebas สามารถทำได้โดยไม่ต้องเข้าร่วมMAX(ดูคำตอบโดย @ypercube ยังมีวิธีแก้ไขปัญหาอื่นในคำตอบของฉัน) แต่ไม่ใช่วิธีที่คุณทำ ตรวจสอบผลลัพธ์ที่คาดหวัง
zero323

1
@Sebas โซลูชันของคุณเพิ่มคอลัมน์ (MAX avgต่อcname) เท่านั้น แต่ไม่ได้ จำกัด แถวของผลลัพธ์ (ตามที่ OP ต้องการ) ดูผลลัพธ์ที่แท้จริงควรเป็นย่อหน้าในคำถาม
ypercubeᵀᴹ

1
การปิด ONLY_FULL_GROUP_BYใน MySQL 5.7 ไม่เปิดใช้งานวิธีที่มาตรฐาน SQL ระบุเมื่อคอลัมน์สามารถละเว้นได้จากgroup by(หรือทำให้ MySQL ทำงานเหมือน Postgres) มันจะเปลี่ยนเป็นพฤติกรรมเดิมที่ MySQL ให้ผลลัพธ์แบบสุ่ม (= "ไม่ทราบแน่ชัด") แทน
a_horse_with_no_name

126

ใน Postgres คุณยังสามารถใช้DISTINCT ON (expression)ไวยากรณ์พิเศษ:

SELECT DISTINCT ON (cname) 
    cname, wmname, avg
FROM 
    makerar 
ORDER BY 
    cname, avg DESC ;

5
มันจะไม่ทำงานอย่างที่คาดไว้ถ้าใครต้องการเรียงคอลัมน์เช่น avg
amenzhinsky

@amenzhinsky คุณหมายถึงอะไร หากต้องการที่จะมีผลตั้งเรียงด้วยลำดับที่แตกต่างกว่าBY cname?
ypercubeᵀᴹ

@ypercube จริง ๆ แล้วเรียงลำดับ psql ก่อนจากนั้นจึงใช้ DISTINCT ในกรณีของการเรียงลำดับโดย avg เราจะได้ผลลัพธ์ที่แตกต่างกันสำหรับค่าต่ำสุดและสูงสุดทุกแถวขึ้นอยู่กับทิศทางการเรียงลำดับ
amenzhinsky

3
แน่นอน. หากคุณไม่เรียกใช้แบบสอบถามที่ฉันโพสต์คุณจะได้รับผลลัพธ์ที่แตกต่างกัน! นั่นไม่เหมือนกับ "มันใช้งานไม่ได้ตามที่คาดหวัง" ...
ypercubeᵀᴹ

1
@ Batfan ขอบคุณ โปรดทราบว่าแม้ว่าจะค่อนข้างเล็กกะทัดรัดและเขียนได้ง่าย แต่ก็ไม่ใช่วิธีที่มีประสิทธิภาพมากที่สุดสำหรับการสืบค้นประเภทนี้
ypercubeᵀᴹ

27

ปัญหาในการระบุเขตข้อมูลที่ไม่ได้จัดกลุ่มและไม่รวมอยู่ในการgroup byเลือกคือเอ็นจิ้นไม่มีวิธีที่จะทราบว่าเขตข้อมูลของระเบียนใดควรกลับมาในกรณีนี้ ก่อนไหม มันเป็นครั้งสุดท้ายหรือไม่ โดยปกติจะไม่มีบันทึกที่สอดคล้องกับผลลัพธ์โดยรวม ( minและmaxเป็นข้อยกเว้น) ตามธรรมชาติ

อย่างไรก็ตามมีวิธีแก้ปัญหาคือ: ทำให้ฟิลด์ที่ต้องการรวมเข้าด้วยกัน ในขั้นตอนนี้ควรทำงาน:

SELECT cname, (array_agg(wmname ORDER BY avg DESC))[1], MAX(avg)
FROM makerar GROUP BY cname;

โปรดทราบว่าสิ่งนี้จะสร้างอาร์เรย์ของชื่อทั้งหมดเรียงลำดับโดย avg และส่งกลับองค์ประกอบแรก (อาร์เรย์ใน postgres เป็นแบบ 1)


จุดดี. แม้ว่ามันอาจเป็นไปได้ที่ฐานข้อมูลสามารถทำการรวมภายนอกเพื่อเชื่อมโยงเขตข้อมูลที่ไม่รวมจากแต่ละแถวไปยังผลลัพธ์ที่สรุปรวมซึ่งแถวนั้นมีส่วน ฉันมักจะสงสัยว่าทำไมพวกเขาไม่มีตัวเลือกสำหรับสิ่งนั้น แม้ว่าฉันก็อาจจะไม่รู้ตัวเลือกนี้ :)
เบนซิมมอนส์

16
SELECT t1.cname, t1.wmname, t2.max
FROM makerar t1 JOIN (
    SELECT cname, MAX(avg) max
    FROM makerar
    GROUP BY cname ) t2
ON t1.cname = t2.cname AND t1.avg = t2.max;

ใช้rank() ฟังก์ชั่นหน้าต่าง :

SELECT cname, wmname, avg
FROM (
    SELECT cname, wmname, avg, rank() 
    OVER (PARTITION BY cname ORDER BY avg DESC)
    FROM makerar) t
WHERE rank = 1;

บันทึก

คนใดคนหนึ่งจะรักษาค่าสูงสุดหลายต่อกลุ่ม หากคุณต้องการบันทึกเดียวเท่านั้นต่อกลุ่มแม้ว่าจะมีมากกว่าหนึ่งระเบียนที่มีเฉลี่ยเท่ากับค่าสูงสุดคุณควรตรวจสอบคำตอบของ @ ypercube


16

สำหรับฉันมันไม่เกี่ยวกับ "ปัญหาการรวมทั่วไป" แต่เกี่ยวกับแบบสอบถาม SQL ที่ไม่ถูกต้อง คำตอบที่ถูกต้องเพียงคำเดียวสำหรับ "เลือกค่าเฉลี่ยสูงสุดสำหรับแต่ละชื่อ ... " คือ

SELECT cname, MAX(avg) FROM makerar GROUP BY cname;

ผลลัพธ์จะเป็น:

 cname  |      MAX(avg)
--------+---------------------
 canada | 2.0000000000000000
 spain  | 5.0000000000000000

ผลลัพธ์โดยทั่วไปตอบคำถามว่า "ผลลัพธ์ที่ดีที่สุดสำหรับแต่ละกลุ่มคืออะไร" . เราเห็นว่าผลลัพธ์ที่ดีที่สุดสำหรับสเปนคือ 5 และสำหรับแคนาดาผลลัพธ์ที่ดีที่สุดคือ 2 มันเป็นความจริงและไม่มีข้อผิดพลาด หากเราต้องการแสดงwmnameด้วยเราต้องตอบคำถาม: " RULEคืออะไรในการเลือก wmname จากชุดผลลัพธ์" ลองเปลี่ยนข้อมูลอินพุตเล็กน้อยเพื่อชี้แจงข้อผิดพลาด:

  cname | wmname |        avg           
--------+--------+-----------------------
 spain  | zoro   |  1.0000000000000000
 spain  | luffy  |  5.0000000000000000
 spain  | usopp  |  5.0000000000000000

ส่งผลให้ผู้ที่ทำคุณคาดหวังใน runnig แบบสอบถามนี้: SELECT cname, wmname, MAX(avg) FROM makerar GROUP BY cname;? มันควรจะเป็นspain+luffyหรือspain+usopp? ทำไม? มันไม่ได้ถูกกำหนดในแบบสอบถามวิธีการเลือก "ดีกว่า" wmnameหากมีความเหมาะสมดังนั้นจึงไม่ได้รับผลลัพธ์ นั่นเป็นสาเหตุที่ SQL interpreter ส่งคืนข้อผิดพลาด - การค้นหาไม่ถูกต้อง

ในคำอื่น ๆ ไม่มีคำตอบที่ถูกต้องสำหรับคำถาม"ใครดีที่สุดในspainกลุ่ม" . ลูฟี่ไม่ได้ดีกว่า usopp เพราะ usopp มี "คะแนน" เท่ากัน


วิธีนี้ใช้ได้สำหรับฉันเช่นกัน ฉันมีปัญหาการสืบค้นเพราะ ORM ของฉันยังรวมคีย์หลักที่เกี่ยวข้องซึ่งทำให้มีการค้นหาที่ไม่ถูกต้องต่อไปนี้: SELECT cname, id, MAX(avg) FROM makerar GROUP BY cname;ซึ่งทำให้เกิดข้อผิดพลาดที่ทำให้เข้าใจผิดนี้
Roberto

1

ดูเหมือนว่าจะทำงานเช่นกัน

SELECT *
FROM makerar m1
WHERE m1.avg = (SELECT MAX(avg)
                FROM makerar m2
                WHERE m1.cname = m2.cname
               )

0

ฉันเพิ่งพบปัญหานี้เมื่อพยายามนับการใช้case whenและพบว่าการเปลี่ยนลำดับของคำสั่งwhichและcountคำสั่งแก้ไขปัญหา:

SELECT date(dateday) as pick_day,
COUNT(CASE WHEN (apples = 'TRUE' OR oranges 'TRUE') THEN fruit END)  AS fruit_counter

FROM pickings

GROUP BY 1

แทนที่จะใช้ - ในระยะหลังที่ฉันมีข้อผิดพลาดที่แอปเปิ้ลและส้มควรปรากฏในฟังก์ชั่นรวม

CASE WHEN ((apples = 'TRUE' OR oranges 'TRUE') THEN COUNT(*) END) END AS fruit_counter

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