Bagaimana cara mengoptimalkan fungsi ORDER BY RAND () MySQL?


90

Saya ingin mengoptimalkan kueri saya, jadi saya memeriksanya mysql-slow.log.

Sebagian besar kueri lambat saya berisi ORDER BY RAND(). Saya tidak dapat menemukan solusi nyata untuk menyelesaikan masalah ini. Ada solusi yang mungkin di MySQLPerformanceBlog tetapi saya rasa ini tidak cukup. Pada tabel yang dioptimalkan dengan buruk (atau sering diperbarui, dikelola pengguna), itu tidak berfungsi atau saya perlu menjalankan dua atau lebih kueri sebelum saya dapat memilih PHPbaris acak yang saya buat.

Apakah ada solusi untuk masalah ini?

Contoh dummy:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
ORDER BY
        RAND()
LIMIT 1

Jawaban:


67

Coba ini:

SELECT  *
FROM    (
        SELECT  @cnt := COUNT(*) + 1,
                @lim := 10
        FROM    t_random
        ) vars
STRAIGHT_JOIN
        (
        SELECT  r.*,
                @lim := @lim - 1
        FROM    t_random r
        WHERE   (@cnt := @cnt - 1)
                AND RAND(20090301) < @lim / @cnt
        ) i

Hal ini terutama efisien pada MyISAM(sejak COUNT(*)instan), tetapi bahkan di InnoDBitu 10kali lebih efisien daripada ORDER BY RAND().

Ide utamanya di sini adalah kita tidak mengurutkan, tetapi menyimpan dua variabel dan menghitung running probabilitybaris yang akan dipilih pada langkah saat ini.

Lihat artikel ini di blog saya untuk detail lebih lanjut:

Memperbarui:

Jika Anda perlu memilih satu catatan acak, coba ini:

SELECT  aco.*
FROM    (
        SELECT  minid + FLOOR((maxid - minid) * RAND()) AS randid
        FROM    (
                SELECT  MAX(ac_id) AS maxid, MIN(ac_id) AS minid
                FROM    accomodation
                ) q
        ) q2
JOIN    accomodation aco
ON      aco.ac_id =
        COALESCE
        (
        (
        SELECT  accomodation.ac_id
        FROM    accomodation
        WHERE   ac_id > randid
                AND ac_status != 'draft'
                AND ac_images != 'b:0;'
                AND NOT EXISTS
                (
                SELECT  NULL
                FROM    accomodation_category
                WHERE   acat_id = ac_category
                        AND acat_slug = 'vendeglatohely'
                )
        ORDER BY
                ac_id
        LIMIT   1
        ),
        (
        SELECT  accomodation.ac_id
        FROM    accomodation
        WHERE   ac_status != 'draft'
                AND ac_images != 'b:0;'
                AND NOT EXISTS
                (
                SELECT  NULL
                FROM    accomodation_category
                WHERE   acat_id = ac_category
                        AND acat_slug = 'vendeglatohely'
                )
        ORDER BY
                ac_id
        LIMIT   1
        )
        )

Ini mengasumsikan bahwa Anda ac_iddidistribusikan lebih atau kurang merata.


Halo, Quassnoi! Pertama-tama, terima kasih atas tanggapan Anda yang cepat! Mungkin ini salah saya tapi masih belum jelas solusinya. Saya akan memperbarui posting asli saya dengan contoh konkret dan saya akan senang jika Anda menjelaskan solusi Anda pada contoh ini.
fabrik

ada kesalahan ketik pada "JOIN accomodation aco ON aco.id =" di mana aco.id sebenarnya adalah aco.ac_id. di sisi lain kueri yang dikoreksi tidak berfungsi untuk saya karena memunculkan kesalahan # 1241 - Operand harus berisi 1 kolom di SELECT kelima (sub-pilih keempat). Saya mencoba menemukan masalah dengan tanda kurung (jika saya tidak salah) tetapi saya belum bisa menemukan masalahnya.
fabrik

@fabrik: coba sekarang. Akan sangat membantu jika Anda memposting skrip tabel sehingga saya dapat memeriksanya sebelum memposting.
Quassnoi

Terima kasih, ini berhasil! :) Dapatkah Anda mengedit bagian JOIN ... ON aco.id menjadi JOIN ... ON aco.ac_id sehingga saya dapat menerima solusi Anda. Terima kasih lagi! Sebuah pertanyaan: saya ingin tahu apakah mungkin ini adalah acak yang lebih buruk seperti ORDER BY RAND ()? Hanya karena kueri ini mengulangi beberapa hasil berkali-kali.
fabrik

1
@Adam: tidak, itu disengaja, supaya bisa memperbanyak hasilnya.
Quassnoi

12

Itu tergantung pada seberapa acak Anda perlu. Solusi yang Anda tautkan berfungsi dengan cukup baik IMO. Kecuali Anda memiliki celah besar di bidang ID, itu masih cukup acak.

Namun, Anda harus dapat melakukannya dalam satu kueri menggunakan ini (untuk memilih satu nilai):

SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*MAX(id)) LIMIT 1

Solusi lain:

  • Tambahkan bidang float permanen yang dipanggil randomke tabel dan isi dengan angka acak. Anda kemudian dapat membuat nomor acak di PHP dan lakukan"SELECT ... WHERE rnd > $random"
  • Ambil seluruh daftar ID dan simpan dalam cache dalam file teks. Baca file dan pilih ID acak darinya.
  • Simpan hasil kueri dalam cache sebagai HTML dan simpan selama beberapa jam.

8
Apakah hanya saya atau kueri ini tidak berfungsi? Saya mencobanya dengan beberapa variasi dan semuanya membuang "Penggunaan fungsi grup yang tidak valid" ..
Sophivorus

Anda dapat melakukannya dengan subkueri SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1tetapi ini tampaknya tidak berfungsi dengan baik karena tidak pernah mengembalikan rekor terakhir
Tandai

11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1Tampaknya melakukan trik untuk saya
Markus

1

Inilah cara saya melakukannya:

SET @r := (SELECT ROUND(RAND() * (SELECT COUNT(*)
  FROM    accomodation a
  JOIN    accomodation_category c
    ON (a.ac_category = c.acat_id)
  WHERE   a.ac_status != 'draft'
        AND c.acat_slug != 'vendeglatohely'
        AND a.ac_images != 'b:0;';

SET @sql := CONCAT('
  SELECT  a.ac_id,
        a.ac_status,
        a.ac_name,
        a.ac_status,
        a.ac_images
  FROM    accomodation a
  JOIN    accomodation_category c
    ON (a.ac_category = c.acat_id)
  WHERE   a.ac_status != ''draft''
        AND c.acat_slug != ''vendeglatohely''
        AND a.ac_images != ''b:0;''
  LIMIT ', @r, ', 1');

PREPARE stmt1 FROM @sql;

EXECUTE stmt1;


tabel saya tidak kontinu karena sering diedit. misalnya saat ini id pertama adalah 121.
fabrik

3
Teknik di atas tidak bergantung pada nilai id yang berkelanjutan. Ini memilih nomor acak antara 1 dan COUNT (*), bukan 1 dan MAX (id) seperti beberapa solusi lainnya.
Bill Karwin

1
Menggunakan OFFSET(untuk apa @r) tidak menghindari pemindaian - hingga pemindaian tabel penuh.
Rick James

@RickJames, itu benar. Jika saya harus menjawab pertanyaan ini hari ini, saya akan melakukan kueri dengan kunci utama. Menggunakan offset dengan LIMIT tidak memindai banyak baris. Mengkueri dengan kunci utama, meskipun jauh lebih cepat, tidak menjamin peluang genap untuk memilih setiap baris - ini mendukung baris yang mengikuti celah.
Bill Karwin

1

(Ya, saya akan dihukum karena tidak memiliki cukup daging di sini, tetapi tidak bisakah Anda menjadi vegan untuk satu hari?)

Kasus: AUTO_INCREMENT berturut-turut tanpa celah, 1 baris dikembalikan
Kasus: AUTO_INCREMENT berurutan tanpa celah, 10 baris
Kasus: AUTO_INCREMENT dengan celah, 1 baris dikembalikan
Kasus: Kolom FLOAT ekstra untuk pengacakan
Kasus: Kolom UUID atau MD5

Kelima casing tersebut dapat dibuat sangat efisien untuk tabel besar. Lihat blog saya untuk detailnya.


0

Ini akan memberi Anda satu sub kueri yang akan menggunakan indeks untuk mendapatkan id acak, lalu kueri lain akan mengaktifkan tabel gabungan Anda.

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
AND accomodation.ac_id IS IN (
        SELECT accomodation.ac_id FROM accomodation ORDER BY RAND() LIMIT 1
)

0

Solusi untuk contoh dummy Anda adalah:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation,
        JOIN 
            accomodation_category 
            ON accomodation.ac_category = accomodation_category.acat_id
        JOIN 
            ( 
               SELECT CEIL(RAND()*(SELECT MAX(ac_id) FROM accomodation)) AS ac_id
            ) AS Choices 
            USING (ac_id)
WHERE   accomodation.ac_id >= Choices.ac_id 
        AND accomodation.ac_status != 'draft'
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
LIMIT 1

Untuk membaca lebih lanjut tentang alternatif ORDER BY RAND(), Anda harus membaca artikel ini .


0

Saya mengoptimalkan banyak kueri yang ada dalam proyek saya. Solusi Quassnoi telah membantu saya mempercepat banyak kueri! Namun, saya merasa sulit untuk memasukkan solusi tersebut di semua kueri, terutama untuk kueri rumit yang melibatkan banyak subkueri di beberapa tabel besar.

Jadi saya menggunakan solusi yang kurang dioptimalkan. Pada dasarnya ini bekerja dengan cara yang sama seperti solusi Quassnoi.

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
        AND rand() <= $size * $factor / [accomodation_table_row_count]
LIMIT $size

$size * $factor / [accomodation_table_row_count]menghitung probabilitas untuk memilih baris acak. Rand () akan menghasilkan nomor acak. Baris akan dipilih jika rand () lebih kecil atau sama dengan probabilitas. Ini secara efektif melakukan pemilihan acak untuk membatasi ukuran tabel. Karena ada kemungkinan itu akan kembali kurang dari jumlah batas yang ditentukan, kita perlu meningkatkan kemungkinan untuk memastikan kita memilih baris yang cukup. Karenanya kami mengalikan $ ukuran dengan $ faktor (saya biasanya menetapkan $ faktor = 2, berfungsi dalam banyak kasus). Akhirnya kami melakukanlimit $size

Masalahnya sekarang adalah mengerjakan accomodation_table_row_count . Jika kita mengetahui ukuran tabel, kita BISA kode keras ukuran tabel. Ini akan berjalan paling cepat, tetapi jelas ini tidak ideal. Jika Anda menggunakan Myisam, mendapatkan hitungan tabel sangat efisien. Karena saya menggunakan innodb, saya hanya melakukan hitungan + pemilihan sederhana. Dalam kasus Anda, akan terlihat seperti ini:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
        AND rand() <= $size * $factor / (select (SELECT count(*) FROM `accomodation`) * (SELECT count(*) FROM `accomodation_category`))
LIMIT $size

Bagian yang sulit adalah mencari kemungkinan yang benar. Seperti yang Anda lihat, kode berikut sebenarnya hanya menghitung ukuran tabel temp kasar (Sebenarnya, terlalu kasar!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))Tetapi Anda dapat memperbaiki logika ini untuk memberikan perkiraan ukuran tabel yang lebih dekat. Perhatikan bahwa lebih baik memilih OVER daripada memilih baris yang kurang. yaitu jika probabilitas disetel terlalu rendah, Anda berisiko tidak memilih cukup baris.

Solusi ini berjalan lebih lambat daripada solusi Quassnoi karena kita perlu menghitung ulang ukuran tabel. Namun, menurut saya pengkodean ini jauh lebih mudah dikelola. Ini adalah trade off antara akurasi + kinerja vs kompleksitas pengkodean . Karena itu, pada tabel besar ini masih jauh lebih cepat daripada Order by Rand ().

Catatan: Jika logika kueri mengizinkan, lakukan pemilihan acak sedini mungkin sebelum operasi gabungan apa pun.


-1
function getRandomRow(){
    $id = rand(0,NUM_OF_ROWS_OR_CLOSE_TO_IT);
    $res = getRowById($id);
    if(!empty($res))
    return $res;
    return getRandomRow();
}

//rowid is a key on table
function getRowById($rowid=false){

   return db select from table where rowid = $rowid; 
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.