MySQL'in ORDER BY RAND () işlevini nasıl optimize edebilirim?


90

Araştırmak için sorgularımı optimize etmek istiyorum mysql-slow.log.

Yavaş sorgularımın çoğu içeriyor ORDER BY RAND(). Bu sorunu çözmek için gerçek bir çözüm bulamıyorum. MySQLPerformanceBlog'da olası bir çözüm var ama bunun yeterli olduğunu düşünmüyorum. Yetersiz optimize edilmiş (veya sık güncellenen, kullanıcı tarafından yönetilen) tablolarda çalışmıyor veya oluşturduğum PHPrastgele satırı seçmeden önce iki veya daha fazla sorgu çalıştırmam gerekiyor .

Bu sorun için herhangi bir çözüm var mı?

Sahte bir örnek:

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

Yanıtlar:


67

Bunu dene:

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

Bu özellikle etkilidir MyISAM(çünkü COUNT(*)anlık), ama bile InnoDB's 10daha verimlidir zamanlarda ORDER BY RAND().

Buradaki ana fikir, sıralama yapmıyoruz, bunun yerine iki değişkeni tutmamız running probabilityve mevcut adımda seçilecek bir satırın hesaplanmasıdır .

Daha fazla ayrıntı için blogumdaki bu makaleye bakın:

Güncelleme:

Tek bir rastgele kayıt seçmeniz gerekiyorsa, şunu deneyin:

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
        )
        )

Bu, sizin ac_idadreslerinizin aşağı yukarı eşit olarak dağıtıldığını varsayar .


Merhaba Quassnoi! Öncelikle hızlı yanıtınız için teşekkürler! Belki bu benim hatam ama çözümünüz hala belirsiz. Orijinal gönderimi somut bir örnekle güncelleyeceğim ve çözümünüzü bu örnekte açıklarsanız mutlu olacağım.
fabrik

aco.id gerçekten aco.ac_id olduğu "JOIN accomodation aco ON aco.id =" konumunda bir yazım hatası vardı. Öte yandan, düzeltilmiş sorgu benim için işe yaramadı çünkü bir hata veriyor # 1241 - İşlenen beşinci SELECT'te (dördüncü alt seçim) 1 sütun içermelidir. Problemi parantez ile bulmaya çalıştım (yanılmıyorsam) ama problemi henüz bulamıyorum.
fabrik

@fabrik: şimdi dene. Tablo komut dosyalarını göndermeniz gerçekten yararlı olacaktır, böylece yayınlamadan önce onları kontrol edebilirim.
Quassnoi

Teşekkürler, işe yarıyor! :) JOIN ... ON aco.id bölümünü aco.ac_id ÜZERİNDE JOIN olarak düzenleyebilir misiniz, böylece çözümünüzü kabul edebilirim. Tekrar teşekkürler! Bir soru: Mümkün olup olmadığını merak ediyorum, bu RANDA SİPARİŞ () gibi daha kötü bir rastgele mi? Sırf bu sorgu bazı sonuçları birçok kez tekrar ediyor.
fabrik

1
@Adam: hayır, bu kasıtlı, böylece sonuçları yeniden üretebilirsiniz.
Quassnoi

12

Ne kadar rastgele olman gerektiğine bağlı. Bağladığınız çözüm oldukça iyi çalışıyor IMO. Kimlik alanında büyük boşluklar olmadıkça, hala oldukça rastgeledir.

Ancak, bunu kullanarak bunu tek bir sorguda yapabilmelisiniz (tek bir değer seçmek için):

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

Diğer çözümler:

  • Tabloya kalıcı bir kayan alan ekleyin randomve rastgele sayılarla doldurun. Daha sonra PHP'de rastgele bir sayı oluşturabilir ve"SELECT ... WHERE rnd > $random"
  • Tüm kimlik listesini alın ve bunları bir metin dosyasında önbelleğe alın. Dosyayı okuyun ve ondan rastgele bir kimlik seçin.
  • Sorgunun sonuçlarını HTML olarak önbelleğe alın ve birkaç saat saklayın.

8
Sadece ben mi yoksa bu sorgu çalışmıyor mu? Birkaç varyasyonla denedim ve hepsi "Grup işlevinin geçersiz kullanımı"
hatası veriyor

Bunu bir alt sorgu ile yapabilirsiniz, SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1ancak bu, son kaydı asla döndürmediği için düzgün çalışmıyor gibi görünüyor
Mark

11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1Görünüşe göre benim için numara yapıyor
Mark

1

İşte bunu nasıl yapacağım:

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;


masam sürekli değil çünkü sık sık düzenleniyor. örneğin şu anda ilk kimlik
121'dir

3
Yukarıdaki teknik, id değerlerinin sürekli olmasına dayanmaz. Diğer çözümlerde olduğu gibi 1 ve MAX (id) değil, 1 ile COUNT (*) arasında rastgele bir sayı seçer.
Bill Karwin

1
OFFSET(Bunun @riçin) kullanmak taramayı engellemez - tam bir tablo taramasına kadar.
Rick James

@RickJames, bu doğru. Bugün bu soruyu yanıtlasaydım, sorguyu birincil anahtarla yapardım. LIMIT ile bir ofset kullanmak çok sayıda satırı tarar. Birincil anahtarla sorgulama, çok daha hızlı olmasına rağmen, her satırı seçme şansını eşit şekilde garanti etmez - boşlukları takip eden satırları tercih eder.
Bill Karwin

1

(Evet, burada yeterince et yemediğim için dingin olacağım, ama bir günlüğüne vegan olamaz mısın?)

Durum: Boşluksuz ardışık AUTO_INCREMENT, 1 satır döndürüldü
Durum: Ardışık AUTO_INCREMENT, boşluksuz, 10 satır
Durum: AUTO_INCREMENT, boşluklu, 1 satır döndürüldü
Durum: Rastgele dağıtmak için Ekstra FLOAT sütunu
Durum: UUID veya MD5 sütunu

Bu 5 durum, büyük masalar için çok verimli hale getirilebilir. Ayrıntılar için bloguma bakın.


0

Bu size rastgele bir kimlik almak için dizini kullanacak tek bir alt sorgu verecektir, ardından diğer sorgu birleştirilmiş tablonuzu almak için ateşleyecektir.

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

Sahte örneğiniz için çözüm şöyle olacaktır:

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

Alternatifleri hakkında daha fazla bilgi edinmek için bu makaleyiORDER BY RAND() okumalısınız .


0

Projemdeki birçok mevcut sorguyu optimize ediyorum. Quassnoi'nin çözümü sorguları çok hızlandırmama yardımcı oldu! Bununla birlikte, özellikle birden çok büyük tablodaki birçok alt sorguyu içeren karmaşık sorgular için söz konusu çözümü tüm sorgulara dahil etmeyi zor buluyorum.

Bu yüzden daha az optimize edilmiş bir çözüm kullanıyorum. Temelde Quassnoi'nin çözümüyle aynı şekilde çalışır.

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]rastgele bir sıra seçme olasılığını hesaplar. Rand () rastgele bir sayı oluşturacaktır. Rand () daha küçükse veya olasılığa eşitse satır seçilecektir. Bu, tablo boyutunu sınırlamak için etkili bir şekilde rastgele bir seçim gerçekleştirir. Tanımlanan limit sayısından daha az geri dönme şansı olduğundan, yeterli sayıda satır seçtiğimizden emin olmak için olasılığı artırmamız gerekir. Bu nedenle, $ size'ı bir $ faktör ile çarpıyoruz (genellikle $ faktör = 2 olarak belirledim, çoğu durumda işe yarıyor). Sonunda yapıyoruzlimit $size

Şimdi sorun, accomodation_table_row_count üzerinde çalışmaktır . Masa boyutunu bilirsek, masa boyutunu sabit kodlayabiliriz. Bu en hızlı koşacaktı, ancak açıkçası bu ideal değil. Myisam kullanıyorsanız, masa sayısını almak çok etkilidir. Innodb kullandığım için, sadece basit bir sayım + seçim yapıyorum. Sizin durumunuzda şöyle görünecektir:

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

İşin zor kısmı doğru olasılığı bulmaktır. Gördüğünüz gibi, aşağıdaki kod aslında sadece kaba geçici tablo boyutunu hesaplar (Aslında, çok kaba!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))Ancak daha yakın bir tablo boyutu yaklaşımı vermek için bu mantığı iyileştirebilirsiniz. Satırları eksik seçmektense OVER seçiminin daha iyi olduğunu unutmayın. yani, olasılık çok düşük ayarlanmışsa, yeterince satır seçmeme riskiniz vardır.

Tablo boyutunu yeniden hesaplamamız gerektiğinden, bu çözüm Quassnoi'nin çözümünden daha yavaş çalışıyor. Ancak bu kodlamayı çok daha yönetilebilir buluyorum. Bu, doğruluk + performans ile kodlama karmaşıklığı arasında bir değiş tokuş . Büyük tablolarda bu hala Order by Rand () 'dan çok daha hızlıdır.

Not: Sorgu mantığı izin veriyorsa, rastgele seçimi herhangi bir birleştirme işleminden önce olabildiğince erken gerçekleştirin.


-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; 
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.