MySQL "Group By" dan "Order By"

96

Saya ingin dapat memilih sekelompok baris dari tabel email dan mengelompokkannya berdasarkan pengirim dari. Kueri saya terlihat seperti ini:

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

Kueri hampir berfungsi seperti yang saya inginkan - ini memilih catatan yang dikelompokkan berdasarkan email. Masalahnya adalah subjek dan stempel waktu tidak sesuai dengan catatan terbaru untuk alamat email tertentu.

Misalnya, mungkin mengembalikan:

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

Ketika catatan dalam database adalah:

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

Jika subjek "pertanyaan pemrograman" adalah yang terbaru, bagaimana saya bisa meminta MySQL untuk memilih rekaman itu saat mengelompokkan email?

John Kurlak
sumber

Jawaban:

140

Solusi sederhana adalah membungkus kueri ke dalam subselect dengan pernyataan ORDER terlebih dahulu dan menerapkan GROUP BY nanti :

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

Ini mirip dengan menggunakan gabungan tetapi terlihat jauh lebih bagus.

Menggunakan kolom non-agregat di SELECT dengan klausa GROUP BY tidak standar. MySQL biasanya akan mengembalikan nilai dari baris pertama yang ditemukannya dan membuang sisanya. Klausa ORDER BY apa pun hanya akan berlaku untuk nilai kolom yang dikembalikan, bukan ke yang dibuang.

PEMBARUAN PENTING Memilih kolom non-agregat yang digunakan untuk bekerja dalam praktik tetapi tidak boleh diandalkan. Menurut dokumentasi MySQL "ini berguna terutama ketika semua nilai di setiap kolom nonagregasi yang tidak dinamai di GROUP BY adalah sama untuk setiap grup. Server bebas memilih nilai apa pun dari setiap grup, jadi kecuali nilainya sama, nilainya dipilih tidak pasti . "

Mulai 5.7.5, ONLY_FULL_GROUP_BY diaktifkan secara default sehingga kolom non-agregat menyebabkan kesalahan kueri (ER_WRONG_FIELD_WITH_GROUP)

Seperti yang ditunjukkan @mikep di bawah, solusinya adalah dengan menggunakan ANY_VALUE () dari 5.7 ke atas

Lihat 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 / id / group-by-handling.html https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_any-value

b7kich.dll
sumber
7
Saya datang dengan solusi yang sama beberapa tahun yang lalu, dan itu adalah solusi yang bagus. pujian untuk b7kich. Dua masalah di sini ... GROUP BY tidak peka huruf besar / kecil sehingga LOWER () tidak diperlukan, dan kedua, $ userID tampaknya merupakan variabel langsung dari PHP, kode Anda mungkin rentan injeksi sql jika $ userID disediakan oleh pengguna dan tidak dipaksa menjadi integer.
velcrow
PEMBARUAN PENTING juga berlaku untuk 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.Mode SQL dapat diubah selama runtime tanpa hak admin, jadi sangat mudah untuk menonaktifkan ONLY_FULL_GROUP_BY. Sebagai contoh: SET SESSION sql_mode = '';. Demo: db-fiddle.com/f/esww483qFQXbXzJmkHZ8VT/3
mikep
1
Atau alternatif lain untuk mengabaikan ONLY_FULL_GROUP_BY yang diaktifkan adalah menggunakan ANY_VALUE (). Lihat lebih banyak dev.mysql.com/doc/refman/8.0/en/…
mikep
42

Inilah satu pendekatan:

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)

Pada dasarnya, Anda bergabung dengan tabel itu sendiri, mencari baris selanjutnya. Di klausa where Anda menyatakan bahwa tidak dapat ada baris selanjutnya. Ini hanya memberi Anda baris terbaru.

Jika ada beberapa email dengan stempel waktu yang sama, kueri ini perlu diperbaiki. Jika ada kolom ID tambahan di tabel email, ubah JOIN seperti:

LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.id < next.id
Andomar
sumber
Mengatakan textIDitu ambigu = /
John Kurlak
1
Kemudian hapus ambuigity dan awali dengan nama tabel, seperti cur.textID. Berubah dalam jawabannya juga.
Andomar
Ini adalah satu-satunya solusi yang mungkin dilakukan dengan Doctrine DQL.
VisioN
Ini tidak berfungsi ketika Anda mencoba menggabungkan diri untuk beberapa kolom dengan baik. IE saat Anda mencoba mencari email terbaru dan nama pengguna terbaru dan Anda memerlukan beberapa gabungan kiri sendiri untuk melakukan operasi ini dalam satu kueri.
Loveen Dyall
Saat bekerja dengan cap waktu / tanggal yang lalu dan yang akan datang, untuk membatasi kumpulan hasil ke tanggal yang tidak akan datang, Anda perlu menambahkan ketentuan lain ke LEFT JOINkriteriaAND next.timestamp <= UNIX_TIMESTAMP()
fyrye
32

Seperti yang sudah ditunjukkan dalam balasan, jawaban saat ini salah, karena GROUP BY secara sewenang-wenang memilih rekaman dari jendela.

Jika seseorang menggunakan MySQL 5.6, atau MySQL 5.7 dengan ONLY_FULL_GROUP_BY, kueri yang benar (deterministik) adalah:

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

Agar kueri dapat berjalan secara efisien, diperlukan pengindeksan yang tepat.

Perhatikan bahwa untuk tujuan penyederhanaan, saya telah menghapus LOWER(), yang dalam banyak kasus, tidak akan digunakan.

Marcus
sumber
2
Ini harus menjadi jawaban yang benar. Saya baru saja menemukan bug di situs saya yang terkait dengan ini. Di order bysubpilihan di jawaban lain, tidak berpengaruh sama sekali.
Jette
1
OMG, jadikan ini jawaban yang diterima. Yang diterima menyia-nyiakan 5 jam waktuku :(
Richard Kersey
29

Lakukan GROUP BY setelah ORDER BY dengan membungkus kueri Anda dengan GROUP BY seperti ini:

SELECT t.* FROM (SELECT * FROM table ORDER BY time DESC) t GROUP BY t.from
11101101b
sumber
1
Jadi GROUP BY` secara otomatis memilih yang terbaru time, atau terbaru time, atau acak?
xrDDDD
1
Ini memilih waktu terbaru karena kami memesan oleh time DESCdan kemudian grup dengan mengambil yang pertama (terbaru).
11101101b
Sekarang kalau saja saya bisa melakukan GABUNG pada sub-pilih di VIEWS, di mysql 5.1. Mungkin fitur itu hadir dalam rilis yang lebih baru.
IcarusNM
21

Menurut standar SQL Anda tidak dapat menggunakan kolom non-agregat dalam daftar pilih. MySQL mengizinkan penggunaan seperti itu (mode ONLY_FULL_GROUP_BY digunakan) tetapi hasilnya tidak dapat diprediksi.

ONLY_FULL_GROUP_BY

Anda harus terlebih dahulu memilih fromEmail, MIN (read), dan kemudian, dengan kueri kedua (atau subkueri) - Subjek.

noonex
sumber
MIN (baca) akan mengembalikan nilai minimal "baca". Dia mungkin mencari tanda "baca" pada email terbaru.
Andomar
2

Saya berjuang dengan kedua pendekatan ini untuk kueri yang lebih kompleks daripada yang ditampilkan, karena pendekatan subkueri sangat tidak efisien tidak peduli indeks apa yang saya gunakan, dan karena saya tidak bisa mendapatkan penggabungan mandiri luar melalui Hibernate.

Cara terbaik (dan termudah) untuk melakukan ini adalah dengan mengelompokkan berdasarkan sesuatu yang dibangun untuk memuat rangkaian bidang yang Anda perlukan dan kemudian menariknya keluar menggunakan ekspresi dalam klausa SELECT. Jika Anda perlu melakukan MAX () pastikan bahwa bidang yang ingin Anda MAX () lewati selalu di ujung paling signifikan dari entitas gabungan.

Kunci untuk memahami hal ini adalah bahwa kueri hanya bisa masuk akal jika bidang lain ini invarian untuk setiap entitas yang memenuhi Max (), jadi dalam hal pengurutan, bagian lain dari penggabungan dapat diabaikan. Ini menjelaskan bagaimana melakukan ini di bagian paling bawah tautan ini. http://dev.mysql.com/doc/refman/5.0/en/group-by-hidden-columns.html

Jika Anda bisa mendapatkan acara sisipkan / perbarui (seperti pemicu) untuk menghitung sebelumnya rangkaian bidang, Anda dapat mengindeksnya dan kueri akan secepat jika grup tersebut berada di atas bidang yang sebenarnya Anda inginkan untuk MAX ( ). Anda bahkan dapat menggunakannya untuk mendapatkan beberapa bidang secara maksimal. Saya menggunakannya untuk melakukan kueri terhadap pohon multi-dimensi yang diekspresikan sebagai kumpulan bersarang.

Mike N
sumber