harus muncul dalam klausa GROUP BY atau digunakan dalam fungsi agregat

276

Saya punya meja yang mirip penelepon ini 'pembuat'

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

Dan saya ingin memilih rata-rata maksimum untuk setiap cname.

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

tapi saya akan mendapatkan kesalahan,

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;

jadi saya melakukan ini

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

namun ini tidak akan memberikan hasil yang diinginkan, dan keluaran yang salah di bawah ini ditampilkan

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

Hasil yang sebenarnya seharusnya

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

Bagaimana saya bisa memperbaiki masalah ini?

Catatan: Tabel ini adalah LIHAT yang dibuat dari operasi sebelumnya.

RandomGuy
sumber
2
Terkait: stackoverflow.com/q/18061285/398670
Craig Ringer
Saya tidak mengerti. Mengapa wmname="usopp"diharapkan dan bukan misalnya wmname="luffy"?
AndreKR

Jawaban:

226

Ya, ini adalah masalah agregasi yang umum. Sebelum SQL3 (1999) , bidang yang dipilih harus muncul dalam GROUP BYklausa [*].

Untuk mengatasi masalah ini, Anda harus menghitung agregat dalam sub-kueri dan kemudian bergabung dengan itu sendiri untuk mendapatkan kolom tambahan yang Anda perlu tampilkan:

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

Tetapi Anda juga dapat menggunakan fungsi jendela, yang terlihat lebih sederhana:

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

Satu-satunya hal dengan metode ini adalah ia akan menampilkan semua catatan (fungsi jendela tidak mengelompokkan). Tapi itu akan menunjukkan yang benar (yaitu maks. Di cnamelevel) MAXuntuk negara di setiap baris, jadi terserah Anda:

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

Solusinya, bisa dibilang kurang elegan, untuk menunjukkan satu-satunya (cname, wmname)tupel yang cocok dengan nilai maks, adalah:

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

[*]: Cukup menarik, meskipun jenis spek memungkinkan untuk memilih bidang yang tidak dikelompokkan, mesin utama tampaknya tidak terlalu menyukainya. Oracle dan SQLServer tidak mengizinkan ini sama sekali. Mysql dulu mengizinkannya secara default, tetapi sekarang sejak 5.7 administrator perlu mengaktifkan opsi ini ( ONLY_FULL_GROUP_BY) secara manual dalam konfigurasi server agar fitur ini didukung ...

Sebas
sumber
1
Terima kasih sintaksnya benar, tetapi, Anda harus membandingkan nilai mx dan rata-rata saat bergabung
RandomGuy
1
Ya sintaks Anda sudah benar dan menghilangkan duplikat namun Anda perlu m.avg = t.mx pada akhirnya (setelah Anda menulis JOING) untuk mendapatkan hasil yang diinginkan
RandomGuy
1
@Sebas Ini dapat dilakukan tanpa bergabung MAX(lihat jawaban oleh @ ypercube, ada juga solusi lain dalam jawaban saya) tetapi tidak dengan cara Anda melakukannya. Periksa output yang diharapkan.
zero323
1
@Sebas Solusi Anda hanya menambahkan kolom (MAX avgper cname) tetapi tidak membatasi baris hasil (seperti yang diinginkan OP). Lihat Hasil Sebenarnya harus paragraf dalam pertanyaan.
ypercubeᵀᴹ
1
Menghidupkan off ONLY_FULL_GROUP_BY di MySQL 5.7 tidak mengaktifkan cara SQL menspesifikasikan standar ketika kolom dapat dihilangkan dari group by(atau membuat MySQL berperilaku seperti Postgres). Itu hanya kembali ke perilaku lama di mana MySQL mengembalikan hasil acak (= "tak tentu") sebagai gantinya.
a_horse_with_no_name
126

Di Postgres, Anda juga dapat menggunakan DISTINCT ON (expression)sintaks khusus :

SELECT DISTINCT ON (cname) 
    cname, wmname, avg
FROM 
    makerar 
ORDER BY 
    cname, avg DESC ;
ypercubeᵀᴹ
sumber
5
Ini tidak akan berfungsi seperti yang diharapkan jika seseorang ingin mengurutkan kolom seperti avg
amenzhinsky
@amenzhinsky Apa maksudmu? Jika seseorang ingin agar set hasil diurutkan dengan urutan yang berbeda dari BY cname?
ypercubeᵀᴹ
@ ypercube, Sebenarnya psql mengurutkan pertama dan kemudian menerapkan DISTINCT. Dalam hal pengurutan berdasarkan rata-rata, kami akan mendapatkan hasil yang berbeda untuk setiap baris, nilai minimum dan maksimal, bergantung pada arah pengurutan
amenzhinsky
3
Tentu saja. Jika Anda tidak menjalankan kueri yang saya poskan, Anda akan mendapatkan hasil yang berbeda! Itu tidak sama dengan "itu tidak akan berfungsi seperti yang diharapkan" ...
ypercubeᵀᴹ
1
@Batfan thnx. Perhatikan bahwa meskipun ini cukup keren, ringkas, dan mudah ditulis, ini bukan cara yang paling efisien untuk pertanyaan seperti ini.
ypercubeᵀᴹ
27

Masalah dengan menentukan bidang non-kelompok dan non-agregat dalam group bypemilihan adalah bahwa mesin tidak memiliki cara untuk mengetahui bidang rekaman mana yang harus dikembalikan dalam kasus ini. Apakah ini yang pertama? Apakah ini yang terakhir? Biasanya tidak ada catatan yang secara alami sesuai dengan hasil agregat ( mindan maxmerupakan pengecualian).

Namun, ada solusinya: buat bidang yang dibutuhkan juga teragregasi. Dalam posgres, ini harus bekerja:

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

Perhatikan bahwa ini menciptakan larik semua nama, dipesan oleh rata-rata, dan mengembalikan elemen pertama (array di postgres adalah berbasis 1).

e-neko
sumber
Poin yang bagus. Meskipun tampaknya mungkin bahwa DB dapat melakukan join luar untuk menautkan bidang non-agregat dari setiap baris ke hasil agregat yang berkontribusi pada baris tersebut. Saya sering penasaran mengapa mereka tidak memiliki pilihan untuk itu. Meskipun saya bisa saja mengabaikan opsi ini :)
Ben Simmons
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;

Menggunakan rank() fungsi jendela :

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

Catatan

Salah satu dari mereka akan mempertahankan beberapa nilai maks per grup. Jika Anda hanya ingin catatan tunggal per grup walaupun ada lebih dari satu catatan dengan rata-rata sama dengan maksimal Anda harus memeriksa jawaban @ ypercube.

nol323
sumber
16

Bagi saya, ini bukan tentang "masalah agregasi umum", tetapi hanya tentang permintaan SQL yang salah. Satu jawaban yang benar untuk "pilih rata-rata maksimum untuk setiap nama ..." adalah

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

Hasilnya adalah:

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

Hasil ini secara umum menjawab pertanyaan "Apa hasil terbaik untuk setiap kelompok?" . Kita melihat bahwa hasil terbaik untuk Spanyol adalah 5 dan untuk Kanada hasil terbaik adalah 2. Benar, dan tidak ada kesalahan. Jika kita perlu menampilkan wmname juga, kita harus menjawab pertanyaan: "Apa ATURAN untuk memilih wmname dari set yang dihasilkan?" Mari kita ubah sedikit data input untuk mengklarifikasi kesalahan:

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

Yang mengakibatkan yang Anda harapkan pada runnig query ini: SELECT cname, wmname, MAX(avg) FROM makerar GROUP BY cname;? Harus itu spain+luffyatau spain+usopp? Mengapa? Tidak ditentukan dalam kueri cara memilih wmname "lebih baik" jika beberapa cocok, sehingga hasilnya juga tidak ditentukan. Itu sebabnya penerjemah SQL mengembalikan kesalahan - kueri tidak benar.

Dengan kata lain, tidak ada jawaban yang benar untuk pertanyaan "Siapa yang terbaik dalam spainkelompok?" . Luffy tidak lebih baik dari usopp, karena usopp memiliki "skor" yang sama.

ox160d05d
sumber
Solusi ini juga bekerja untuk saya. Saya memiliki masalah kueri karena ORM saya juga menyertakan kunci utama terkait, menghasilkan kueri salah berikut :,SELECT cname, id, MAX(avg) FROM makerar GROUP BY cname; yang memang memberikan kesalahan menyesatkan ini.
Roberto
1

Ini sepertinya berhasil juga

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

Saya baru-baru ini mengalami masalah ini, ketika mencoba menghitung menggunakan case when, dan menemukan bahwa mengubah urutan whichdan countpernyataan memperbaiki masalah:

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

Alih-alih menggunakan - di yang terakhir, di mana saya mendapat kesalahan bahwa apel dan jeruk harus muncul dalam fungsi agregat

CASE WHEN ((apples = 'TRUE' OR oranges 'TRUE') THEN COUNT(*) END) END AS fruit_counter
Rachel Windzberg
sumber
1
The whichpernyataan?
Hillary Sanders