Saya tidak percaya GROUP BY akan memberikan hasil yang Anda inginkan. Dan sayangnya, MySQL tidak mendukung fungsi analitik (begitulah cara kami memecahkan masalah ini di Oracle atau SQL Server.)
Ada kemungkinan untuk meniru beberapa fungsi analitik yang belum sempurna, dengan memanfaatkan variabel yang ditentukan pengguna.
Dalam hal ini, kami ingin meniru:
ROW_NUMBER() OVER(PARTITION BY doctor_id ORDER BY distance ASC) AS seq
Jadi, dimulai dengan kueri asli, saya mengubah ORDER BY sehingga mengurutkan doctor_id
pertama, dan kemudian pada distance
yang dihitung . (Sampai kita mengetahui jarak itu, kita tidak tahu mana yang "paling dekat".)
Dengan hasil yang diurutkan ini, pada dasarnya kita "menomori" baris untuk setiap doctor_id, yang paling dekat dengan 1, yang kedua paling dekat dengan 2, dan seterusnya. Saat kita mendapatkan doctor_id baru, kita mulai lagi dengan yang terdekat seperti 1.
Untuk mencapai ini, kami menggunakan variabel yang ditentukan pengguna. Kami menggunakan satu untuk menetapkan nomor baris (nama variabel adalah @i, dan kolom yang dikembalikan memiliki alias seq). Variabel lain yang kami gunakan untuk "mengingat" doctor_id dari baris sebelumnya, sehingga kami dapat mendeteksi "break" di doctor_id, sehingga kami dapat mengetahui kapan harus memulai kembali penomoran baris pada 1 lagi.
Berikut pertanyaannya:
SELECT z.*
, @i := CASE WHEN z.doctor_id = @prev_doctor_id THEN @i + 1 ELSE 1 END AS seq
, @prev_doctor_id := z.doctor_id AS prev_doctor_id
FROM
(
/* original query, ordered by doctor_id and then by distance */
SELECT zip,
( 3959 * acos( cos( radians(34.12520) ) * cos( radians( zip_info.latitude ) ) * cos(radians( zip_info.longitude ) - radians(-118.29200) ) + sin( radians(34.12520) ) * sin( radians( zip_info.latitude ) ) ) ) AS distance,
user_info.*, office_locations.*
FROM zip_info
RIGHT JOIN office_locations ON office_locations.zipcode = zip_info.zip
RIGHT JOIN user_info ON office_locations.doctor_id = user_info.id
WHERE user_info.status='yes'
ORDER BY user_info.doctor_id ASC, distance ASC
) z JOIN (SELECT @i := 0, @prev_doctor_id := NULL) i
HAVING seq = 1 ORDER BY z.distance
Saya membuat asumsi bahwa kueri asli mengembalikan set hasil yang Anda butuhkan, itu hanya memiliki terlalu banyak baris, dan Anda ingin menghilangkan semua kecuali "terdekat" (baris dengan nilai jarak minimum) untuk setiap doctor_id.
Saya telah membungkus kueri asli Anda dengan kueri lain; satu-satunya perubahan yang saya buat pada kueri asli adalah mengurutkan hasil berdasarkan doctor_id dan kemudian berdasarkan jarak, dan untuk menghapus HAVING distance < 50
ayat. (Jika Anda hanya ingin mengembalikan jarak kurang dari 50, lanjutkan dan tinggalkan klausa itu di sana. Tidak jelas apakah itu maksud Anda, atau apakah itu ditentukan dalam upaya membatasi baris menjadi satu per doctor_id.)
Beberapa masalah yang perlu diperhatikan:
Permintaan penggantian mengembalikan dua kolom tambahan; ini tidak benar-benar diperlukan dalam kumpulan hasil, kecuali sebagai sarana untuk menghasilkan kumpulan hasil. (Dimungkinkan untuk membungkus seluruh SELECT ini lagi di SELECT lain untuk menghilangkan kolom-kolom itu, tetapi itu benar-benar lebih berantakan daripada nilainya. Saya hanya akan mengambil kolom, dan tahu bahwa saya dapat mengabaikannya.)
Masalah lainnya adalah penggunaan .*
dalam kueri dalam agak berbahaya, karena kami benar-benar perlu menjamin bahwa nama kolom yang dikembalikan oleh kueri itu unik. (Bahkan jika nama kolom saat ini berbeda, penambahan kolom ke salah satu tabel tersebut dapat menimbulkan pengecualian kolom "ambigu" dalam kueri. Sebaiknya hindari hal itu, dan hal itu mudah diatasi dengan mengganti .*
dengan daftar kolom yang akan dikembalikan, dan menentukan alias untuk nama kolom "duplikat". (Penggunaan z.*
di kueri luar tidak menjadi masalah, selama kita mengendalikan kolom yang dikembalikan oleh z
.)
Tambahan:
Saya perhatikan bahwa GROUP BY tidak akan memberikan hasil yang Anda butuhkan. Meskipun dimungkinkan untuk mendapatkan hasil yang ditetapkan dengan kueri menggunakan GROUP BY, pernyataan yang mengembalikan kumpulan hasil yang BENAR akan membosankan. Anda dapat menentukan MIN(distance) ... GROUP BY doctor_id
, dan itu akan memberi Anda jarak terkecil, TETAPI tidak ada jaminan bahwa ekspresi non-agregat lainnya dalam daftar SELECT akan berasal dari baris dengan jarak minimum, dan bukan baris lain. (MySQL sangat liberal dalam hal GROUP BY dan agregat. Untuk membuat mesin MySQL lebih berhati-hati (dan sejalan dengan mesin basis data relasional lainnya), SET sql_mode = ONLY_FULL_GROUP_BY
Tambahan 2:
Masalah Performa yang dilaporkan oleh Darious "beberapa kueri membutuhkan waktu 7 detik."
Untuk mempercepat, Anda mungkin ingin men-cache hasil fungsi. Pada dasarnya, buat tabel pencarian. misalnya
CREATE TABLE office_location_distance
( office_location_id INT UNSIGNED NOT NULL COMMENT 'PK, FK to office_location.id'
, zipcode_id INT UNSIGNED NOT NULL COMMENT 'PK, FK to zipcode.id'
, gc_distance DECIMAL(18,2) COMMENT 'calculated gc distance, in miles'
, PRIMARY KEY (office_location_id, zipcode_id)
, KEY (zipcode_id, gc_distance, office_location_id)
, CONSTRAINT distance_lookup_office_FK
FOREIGN KEY (office_location_id) REFERENCES office_location(id)
ON UPDATE CASCADE ON DELETE CASCADE
, CONSTRAINT distance_lookup_zipcode_FK
FOREIGN KEY (zipcode_id) REFERENCES zipcode(id)
ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB
Itu hanya ide. (Saya berharap Anda mencari jarak office_location dari kode pos tertentu, jadi indeks pada (kode pos, gc_distance, office_location_id) adalah indeks penutup yang diperlukan kueri Anda. (Saya akan menghindari menyimpan jarak yang dihitung sebagai FLOAT, karena buruk kinerja kueri dengan tipe data FLOAT)
INSERT INTO office_location_distance (office_location_id, zipcode_id, gc_distance)
SELECT d.office_location_id
, d.zipcode_id
, d.gc_distance
FROM (
SELECT l.id AS office_location_id
, z.id AS zipcode_id
, ROUND( <glorious_great_circle_calculation> ,2) AS gc_distance
FROM office_location l
CROSS
JOIN zipcode z
ORDER BY 1,3
) d
ON DUPLICATE KEY UPDATE gc_distance = VALUES(gc_distance)
Dengan hasil fungsi yang di-cache dan diindeks, kueri Anda akan jauh lebih cepat.
SELECT d.gc_distance, o.*
FROM office_location o
JOIN office_location_distance d ON d.office_location_id = o.id
WHERE d.zipcode_id = 63101
AND d.gc_distance <= 100.00
ORDER BY d.zipcode_id, d.gc_distance
Saya ragu untuk menambahkan predikat HAVING pada INSERT/UPDATE ke tabel cache; (jika Anda memiliki garis lintang/bujur yang salah, dan telah menghitung jarak yang salah di bawah 100 mil; lari berikutnya setelah garis lintang/bujur ditetapkan dan jaraknya mencapai 1000 mil... jika baris dikeluarkan dari kueri, maka baris yang ada di tabel cache tidak akan diperbarui. (Anda dapat menghapus tabel cache, tetapi itu tidak terlalu diperlukan, itu hanya banyak pekerjaan ekstra untuk database dan log. Jika kumpulan hasil dari kueri pemeliharaan terlalu besar, dapat dipecah untuk dijalankan secara berulang untuk setiap kode pos, atau setiap lokasi_kantor.)
Di sisi lain, jika Anda tidak tertarik dengan jarak pada nilai tertentu, Anda dapat menambahkan HAVING gc_distance <
predikat, dan mengurangi ukuran tabel cache secara signifikan.