Paginasi MySQL tanpa kueri ganda?

115

Saya bertanya-tanya apakah ada cara untuk mendapatkan jumlah hasil dari kueri MySQL, dan pada saat yang sama membatasi hasilnya.

Cara kerja pagination (seperti yang saya pahami), pertama-tama saya melakukan sesuatu seperti

query = SELECT COUNT(*) FROM `table` WHERE `some_condition`

Setelah saya mendapatkan num_rows (query), saya mendapatkan jumlah hasil. Tetapi kemudian untuk benar-benar membatasi hasil saya, saya harus melakukan kueri kedua seperti:

query2 = SELECT COUNT(*) FROM `table` WHERE `some_condition` LIMIT 0, 10

Pertanyaan saya: Apakah ada cara untuk mengambil jumlah total hasil yang akan diberikan, DAN membatasi hasil yang dikembalikan dalam satu kueri? Atau cara yang lebih efisien untuk melakukan ini. Terima kasih!

atp
sumber
8
Meskipun Anda tidak memiliki JUMLAH (*) di kueri2
dlofrodloh

Jawaban:

66

Tidak, berapa banyak aplikasi yang ingin melakukan paginasi harus melakukannya. Ini dapat diandalkan dan anti peluru, meskipun itu membuat kueri dua kali. Tetapi Anda dapat menyimpan hitungan ke cache selama beberapa detik dan itu akan banyak membantu.

Cara lain adalah dengan menggunakan SQL_CALC_FOUND_ROWSklausa dan kemudian memanggil SELECT FOUND_ROWS(). Terlepas dari kenyataan bahwa Anda harus melakukan FOUND_ROWS()panggilan setelahnya, ada masalah dengan ini: Ada bug di MySQL yang menggelitik ini yang memengaruhi ORDER BYkueri sehingga membuatnya jauh lebih lambat di tabel besar daripada pendekatan naif dari dua kueri.

statika
sumber
2
Namun, ini tidak cukup bukti kondisi ras, kecuali Anda melakukan dua kueri dalam transaksi. Ini biasanya bukan masalah.
NickZoic
Yang saya maksud dengan "dapat diandalkan" adalah SQL itu sendiri akan selalu mengembalikan hasil yang Anda inginkan, dan dengan "bukti peluru" yang saya maksudkan adalah tidak ada bug MySQL yang menghambat SQL yang dapat Anda gunakan. Tidak seperti menggunakan SQL_CALC_FOUND_ROWS dengan ORDER BY dan LIMIT, menurut bug yang saya sebutkan.
staticsan
5
Pada kueri kompleks, menggunakan SQL_CALC_FOUND_ROWS untuk mengambil hitungan dalam kueri yang sama akan hampir selalu lebih lambat daripada melakukan dua kueri terpisah. Ini karena ini berarti semua baris perlu diambil secara penuh, berapa pun batasnya, maka hanya yang ditentukan dalam klausa LIMIT yang dikembalikan. Lihat juga tanggapan saya yang memiliki tautan.
thomasrutter
Bergantung pada alasan Anda membutuhkannya, Anda mungkin juga ingin berpikir untuk tidak mendapatkan hasil total. Ini menjadi praktik yang lebih umum untuk menerapkan metode paging otomatis. Situs-situs seperti Facebook, Twitter, Bing, dan Google telah menggunakan metode ini sejak lama.
Thomas B
68

Saya hampir tidak pernah melakukan dua pertanyaan.

Cukup kembalikan satu baris lagi dari yang dibutuhkan, hanya tampilkan 10 pada halaman, dan jika ada lebih dari yang ditampilkan, tampilkan tombol "Next".

SELECT x, y, z FROM `table` WHERE `some_condition` LIMIT 0, 11
// iterate through and display 10 rows.

// if there were 11 rows, display a "Next" button.

Kueri Anda harus kembali dalam urutan yang paling relevan terlebih dahulu. Kemungkinannya adalah, kebanyakan orang tidak akan peduli untuk membuka halaman 236 dari 412.

Saat Anda melakukan pencarian Google, dan hasil Anda tidak ada di halaman pertama, Anda kemungkinan besar akan pergi ke halaman dua, bukan sembilan.

Kerekan
sumber
42
Sebenarnya, jika saya tidak menemukannya di halaman pertama kueri Google, biasanya saya melompat ke halaman sembilan.
Phil
3
@Phil Saya mendengar ini sebelumnya tetapi mengapa melakukan itu?
TK123
5
Sedikit terlambat, tapi inilah alasan saya. Beberapa pencarian didominasi oleh kumpulan tautan yang dioptimalkan mesin pencari. Jadi beberapa halaman pertama adalah peternakan berbeda yang memperebutkan posisi nomor 1, hasil yang berguna kemungkinan masih terkait dengan kueri, hanya saja tidak di atas.
Phil
4
COUNTadalah fungsi agregat. Bagaimana Anda mengembalikan hitungan dan semua hasil dalam satu kueri? Kueri di atas hanya akan mengembalikan 1 baris, apa pun yang LIMITdisetel. Jika Anda menambahkan GROUP BY, itu akan mengembalikan semua hasil tetapi COUNTakan tidak akurat
pixelfreak
2
Ini adalah salah satu pendekatan yang direkomendasikan oleh Percona: percona.com/blog/2008/09/24/…
techdude
27

Pendekatan lain untuk menghindari pembuatan kueri ganda adalah dengan mengambil semua baris untuk halaman saat ini menggunakan klausa LIMIT terlebih dahulu, kemudian hanya melakukan kueri COUNT (*) kedua jika jumlah baris maksimum telah diambil.

Dalam banyak aplikasi, hasil yang paling mungkin adalah semua hasil muat di satu halaman, dan harus melakukan penomoran halaman adalah pengecualian daripada norma. Dalam kasus ini, kueri pertama tidak akan mengambil jumlah hasil maksimum.

Misalnya, jawaban atas pertanyaan stackoverflow jarang muncul di halaman kedua. Komentar pada jawaban jarang melebihi batas 5 atau lebih yang diperlukan untuk menampilkan semuanya.

Jadi dalam aplikasi ini Anda cukup melakukan kueri dengan LIMIT terlebih dahulu, dan kemudian selama batas itu tidak tercapai, Anda tahu persis berapa banyak baris yang ada tanpa perlu melakukan kueri COUNT (*) kedua - yang seharusnya mencakup sebagian besar situasi.

thomasrutter
sumber
1
@thomasrutter Saya memiliki pendekatan yang sama, namun menemukan kekurangannya hari ini. Halaman terakhir hasil tidak akan memiliki data penomoran halaman. misalkan, setiap halaman harus memiliki 25 hasil, halaman terakhir kemungkinan tidak akan memiliki banyak, katakanlah memiliki 7 ... itu berarti hitungan (*) tidak akan pernah dijalankan, sehingga tidak ada penomoran halaman yang akan ditampilkan ke pengguna.
duellsy
2
Tidak - jika Anda mengatakan, 200 hasil masuk, Anda menanyakan 25 berikutnya dan Anda hanya mendapatkan 7 kembali, yang memberi tahu Anda bahwa jumlah total hasil adalah 207 dan oleh karena itu Anda tidak perlu melakukan kueri lain dengan COUNT (*) karena Anda sudah tahu apa yang akan dikatakannya. Anda memiliki semua informasi yang Anda butuhkan untuk menampilkan pagination. Jika Anda mengalami masalah dengan pagination yang tidak ditampilkan kepada pengguna, maka Anda memiliki bug di tempat lain.
thomasrutter
15

Dalam kebanyakan situasi, jauh lebih cepat dan lebih sedikit sumber daya untuk melakukannya dalam dua kueri terpisah daripada melakukannya dalam satu kueri, meskipun hal itu tampaknya kontra-intuitif.

Jika Anda menggunakan SQL_CALC_FOUND_ROWS, maka untuk tabel besar itu membuat kueri Anda jauh lebih lambat, jauh lebih lambat bahkan daripada menjalankan dua kueri, yang pertama dengan COUNT (*) dan yang kedua dengan LIMIT. Alasannya adalah karena SQL_CALC_FOUND_ROWS menyebabkan klausa LIMIT diterapkan setelah mengambil baris, bukan sebelumnya, sehingga mengambil seluruh baris untuk semua kemungkinan hasil sebelum menerapkan batas. Ini tidak dapat dipenuhi oleh indeks karena itu benar-benar mengambil data.

Jika Anda mengambil pendekatan dua kueri, yang pertama hanya mengambil JUMLAH (*) dan tidak benar-benar mengambil dan data aktual, ini dapat dipenuhi jauh lebih cepat karena biasanya dapat menggunakan indeks dan tidak harus mengambil data baris sebenarnya untuk setiap baris yang dilihatnya. Lalu, kueri kedua hanya perlu melihat baris $ offset + $ limit pertama lalu kembali.

Posting dari blog kinerja MySQL ini menjelaskan lebih lanjut:

http://www.mysqlperformanceblog.com/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/

Untuk informasi lebih lanjut tentang mengoptimalkan pagination, periksa posting ini dan posting ini .

thomasrutter
sumber
2

Jawaban saya mungkin terlambat, tetapi Anda dapat melewati kueri kedua (dengan batasnya) dan hanya memfilter info melalui skrip back end Anda. Dalam PHP misalnya, Anda dapat melakukan sesuatu seperti:

if($queryResult > 0) {
   $counter = 0;
   foreach($queryResult AS $result) {
       if($counter >= $startAt AND $counter < $numOfRows) {
            //do what you want here
       }
   $counter++;
   }
}

Tetapi tentu saja, ketika Anda memiliki ribuan catatan untuk dipertimbangkan, itu menjadi tidak efisien dengan sangat cepat. Hitungan yang sudah dihitung sebelumnya mungkin ide yang bagus untuk dilihat.

Berikut bacaan yang bagus tentang subjek: http://www.percona.com/ppc2009/PPC2009_mysql_pagination.pdf

Kama
sumber
Link sudah mati, saya rasa ini yang benar: percona.com/files/presentations/ppc2009/… . Tidak akan mengedit karena tidak yakin apakah itu.
hectorg87
1
query = SELECT col, col2, (SELECT COUNT(*) FROM `table`) AS total FROM `table` WHERE `some_condition` LIMIT 0, 10
Cris McLaughlin
sumber
16
Kueri ini hanya mengembalikan jumlah total record dalam tabel; bukan jumlah record yang sesuai dengan kondisi.
Lawrence Barsanti
1
Jumlah total record yang dibutuhkan untuk pagination (@Lawrence).
Imme
Oh, baiklah, tambahkan saja whereklausa ke kueri bagian dalam dan Anda mendapatkan "total" yang benar bersama dengan hasil halaman (halaman dipilih dengan limitklausa
Erenor Paz
jumlah sub-query (*) akan membutuhkan yang sama di mana klausa atau jika tidak itu tidak akan mengembalikan jumlah hasil yang benar
AKrush95
1

Untuk siapapun yang mencari jawaban di tahun 2020. Sesuai dokumentasi MySQL:

"Pengubah kueri SQL_CALC_FOUND_ROWS dan fungsi FOUND_ROWS () yang menyertai tidak digunakan lagi mulai MySQL 8.0.17 dan akan dihapus di versi MySQL yang akan datang. Sebagai gantinya, pertimbangkan untuk mengeksekusi kueri Anda dengan LIMIT, lalu kueri kedua dengan COUNT (*) dan tanpa LIMIT untuk menentukan apakah ada baris tambahan. "

Saya rasa itu sudah cukup.

https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_found-rows

Igor K
sumber
0

Anda dapat menggunakan kembali sebagian besar kueri dalam subkueri dan menyetelnya ke pengenal. Misalnya, permintaan film yang menemukan film yang berisi urutan surat berdasarkan runtime akan terlihat seperti ini di situs saya.

SELECT Movie.*, (
    SELECT Count(1) FROM Movie
        INNER JOIN MovieGenre 
        ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
    WHERE Title LIKE '%s%'
) AS Count FROM Movie 
    INNER JOIN MovieGenre 
    ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
WHERE Title LIKE '%s%' LIMIT 8;

Perhatikan bahwa saya bukan ahli database, dan saya berharap seseorang dapat mengoptimalkannya sedikit lebih baik. Karena berdiri menjalankannya langsung dari antarmuka baris perintah SQL, mereka berdua membutuhkan ~ 0,02 detik di laptop saya.

Philip Rollins
sumber
-14
SELECT * 
FROM table 
WHERE some_condition 
ORDER BY RAND()
LIMIT 0, 10
John
sumber
3
Ini tidak menjawab pertanyaan itu, dan perintah melalui rand adalah ide yang sangat buruk.
Dan Walmsley