ฉันจะปรับฟังก์ชัน ORDER BY RAND () ของ MySQL ให้เหมาะสมได้อย่างไร


90

mysql-slow.logฉันต้องการที่จะเพิ่มประสิทธิภาพการค้นหาของฉันดังนั้นฉันมองเข้าไปใน

ORDER BY RAND()ส่วนใหญ่เป็นคำสั่งของฉันช้ามี ฉันไม่พบวิธีแก้ปัญหาที่แท้จริงในการแก้ไขปัญหานี้ Theres เป็นทางออกที่เป็นไปได้ที่MySQLPerformanceBlogแต่ฉันคิดว่ามันไม่เพียงพอ บนตารางที่ปรับให้เหมาะสมไม่ดี (หรืออัปเดตบ่อยครั้งมีการจัดการโดยผู้ใช้) มันใช้งานไม่ได้หรือฉันต้องเรียกใช้การสืบค้นสองครั้งขึ้นไปก่อนจึงจะสามารถเลือกPHPแถวสุ่มที่สร้างขึ้นได้

มีวิธีแก้ไขปัญหานี้หรือไม่?

ตัวอย่างจำลอง:

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

คำตอบ:


67

ลองสิ่งนี้:

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

สิ่งนี้มีประสิทธิภาพโดยเฉพาะอย่างยิ่งในMyISAM(เนื่องจากCOUNT(*)เป็นแบบทันที) แต่ถึงแม้InnoDBจะ10มีประสิทธิภาพมากกว่าORDER BY RAND()ไฟล์.

แนวคิดหลักที่นี่คือเราไม่ได้เรียงลำดับ แต่เก็บตัวแปรสองตัวไว้แทนและคำนวณrunning probabilityแถวที่จะเลือกในขั้นตอนปัจจุบัน

ดูบทความนี้ในบล็อกของฉันสำหรับรายละเอียดเพิ่มเติม:

อัปเดต:

หากคุณต้องการเลือก แต่สุ่มระเบียนเดียวให้ลองทำดังนี้:

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

สิ่งนี้จะถือว่าของคุณac_idมีการกระจายอย่างเท่าเทียมกันมากหรือน้อย


สวัสดีคุณ Quassnoi! ก่อนอื่นขอขอบคุณสำหรับการตอบกลับที่รวดเร็วของคุณ! อาจจะเป็นความผิดของฉัน แต่ก็ยังไม่ชัดเจนในการแก้ปัญหาของคุณ ฉันจะอัปเดตโพสต์ต้นฉบับของฉันด้วยตัวอย่างที่เป็นรูปธรรมและยินดีหากคุณอธิบายวิธีแก้ปัญหาของคุณในตัวอย่างนี้
fabrik

มีการพิมพ์ผิดที่ "JOIN Accommodation aco ON aco.id =" โดยที่ aco.id คือ aco.ac_id ในทางกลับกันแบบสอบถามที่แก้ไขไม่ได้ผลสำหรับฉันเพราะมีข้อผิดพลาด # 1241 - ตัวดำเนินการควรมี 1 คอลัมน์ที่ SELECT ที่ห้า (การเลือกย่อยที่สี่) ฉันพยายามค้นหาปัญหาด้วยวงเล็บ (ถ้าฉันไม่ผิด) แต่ฉันยังไม่พบปัญหา
fabrik

@fabrik: ลองตอนนี้. มันจะมีประโยชน์มากถ้าคุณโพสต์สคริปต์ตารางเพื่อให้ฉันได้ตรวจสอบก่อนโพสต์
Quassnoi

ขอบคุณมันได้ผล! :) คุณสามารถแก้ไขส่วน JOIN ... ON aco.id เป็น JOIN ... ON aco.ac_id เพื่อให้ฉันสามารถยอมรับโซลูชันของคุณได้ ขอบคุณอีกครั้ง! คำถาม: ฉันสงสัยว่าถ้าเป็นไปได้นี่เป็นการสุ่มที่แย่กว่าเช่น ORDER BY RAND () หรือไม่? เพียงเพราะคำค้นหานี้ทำซ้ำผลลัพธ์บางอย่างหลายครั้ง
fabrik

1
@Adam: ไม่นั่นเป็นความตั้งใจเพื่อให้คุณสามารถสร้างผลลัพธ์ได้
Quassnoi

12

ขึ้นอยู่กับว่าคุณต้องสุ่มอย่างไร โซลูชันที่คุณเชื่อมโยงใช้งานได้ดี IMO หากคุณมีช่องว่างขนาดใหญ่ในช่อง ID แต่ก็ยังค่อนข้างสุ่ม

อย่างไรก็ตามคุณควรจะทำได้ในแบบสอบถามเดียวโดยใช้สิ่งนี้ (สำหรับการเลือกค่าเดียว):

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

โซลูชันอื่น ๆ :

  • เพิ่มฟิลด์ลอยถาวรที่เรียกrandomลงในตารางและเติมด้วยตัวเลขสุ่ม จากนั้นคุณสามารถสร้างตัวเลขสุ่มใน PHP และทำ"SELECT ... WHERE rnd > $random"
  • คว้ารายการ ID ทั้งหมดและแคชไว้ในไฟล์ข้อความ อ่านไฟล์และเลือกรหัสสุ่มจากไฟล์
  • แคชผลลัพธ์ของแบบสอบถามเป็น HTML และเก็บไว้สองสามชั่วโมง

8
เป็นแค่ฉันหรือแบบสอบถามนี้ใช้ไม่ได้? ฉันลองใช้หลายรูปแบบและพวกเขาก็โยน "Invalid use of group function" ..
Sophivorus

คุณสามารถทำได้ด้วยการสืบค้นย่อยSELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1แต่ดูเหมือนจะทำงานไม่ถูกต้องเนื่องจากไม่เคยส่งคืนระเบียนล่าสุด
ทำเครื่องหมาย

11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1ดูเหมือนว่าจะหลอกล่อฉัน
Mark

1

นี่คือวิธีที่ฉันจะทำ:

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;


ตารางของฉันไม่ต่อเนื่องเพราะมีการแก้ไขบ่อยครั้ง เช่นปัจจุบัน id แรกคือ 121
fabrik

3
เทคนิคข้างต้นไม่ได้ขึ้นอยู่กับว่าค่า id เป็นแบบต่อเนื่อง จะเลือกตัวเลขสุ่มระหว่าง 1 ถึง COUNT (*) ไม่ใช่ 1 และ MAX (id) เหมือนกับโซลูชันอื่น ๆ
Bill Karwin

1
การใช้OFFSET(ซึ่งมี@rไว้สำหรับ) ไม่ได้หลีกเลี่ยงการสแกน - จนถึงการสแกนแบบเต็มตาราง
Rick James

@RickJames ใช่เลย ถ้าฉันจะตอบคำถามนี้ในวันนี้ฉันจะค้นหาด้วยคีย์หลัก การใช้ออฟเซ็ตกับ LIMIT จะสแกนแถวจำนวนมาก การค้นหาด้วยคีย์หลักแม้ว่าจะเร็วกว่ามาก แต่ก็ไม่ได้รับประกันว่าจะมีโอกาสที่จะเลือกแต่ละแถวได้เท่า ๆ กัน แต่จะช่วยให้แถวที่อยู่ตามช่องว่าง
Bill Karwin

1

(ใช่ฉันจะรู้สึกเบื่อหน่ายที่มีเนื้อไม่เพียงพอที่นี่ แต่คุณไม่สามารถเป็นมังสวิรัติได้หรือไม่?)

กรณี: AUTO_INCREMENT ต่อเนื่องโดยไม่มีช่องว่าง 1 แถวส่งคืน
กรณี: AUTO_INCREMENT ต่อเนื่องโดยไม่มีช่องว่าง 10 แถว
กรณี: AUTO_INCREMENT ที่มีช่องว่าง 1 แถวส่งคืน
กรณี: คอลัมน์ FLOAT เสริมสำหรับการสุ่ม
กรณี: คอลัมน์ UUID หรือ MD5

ทั้ง 5 กรณีสามารถทำให้มีประสิทธิภาพมากสำหรับตารางขนาดใหญ่ ดูรายละเอียดในบล็อกของฉัน


0

สิ่งนี้จะทำให้คุณมีแบบสอบถามย่อยเดียวที่จะใช้ดัชนีเพื่อรับรหัสแบบสุ่มจากนั้นแบบสอบถามอื่น ๆ จะเริ่มเรียกใช้ตารางที่เข้าร่วมของคุณ

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

วิธีแก้ปัญหาสำหรับตัวอย่างจำลองของคุณคือ:

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

อ่านเพิ่มเติมเกี่ยวกับทางเลือกในการORDER BY RAND()คุณควรอ่านบทความนี้


0

ฉันกำลังเพิ่มประสิทธิภาพการสืบค้นที่มีอยู่จำนวนมากในโครงการของฉัน โซลูชันของ Quassnoi ช่วยให้ฉันเร่งการค้นหาได้มาก! อย่างไรก็ตามฉันพบว่าเป็นการยากที่จะรวมโซลูชันดังกล่าวไว้ในแบบสอบถามทั้งหมดโดยเฉพาะอย่างยิ่งสำหรับการสืบค้นที่ซับซ้อนซึ่งเกี่ยวข้องกับการสืบค้นย่อยจำนวนมากในตารางขนาดใหญ่หลาย ๆ

ดังนั้นฉันจึงใช้โซลูชันที่เหมาะสมน้อยกว่า โดยพื้นฐานแล้วจะทำงานในลักษณะเดียวกับโซลูชันของ 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]คำนวณความน่าจะเป็นของการเลือกแถวสุ่ม Rand () จะสร้างตัวเลขสุ่ม แถวนี้จะถูกเลือกถ้า rand () เล็กกว่าหรือเท่ากับความน่าจะเป็น วิธีนี้ดำเนินการเลือกแบบสุ่มอย่างมีประสิทธิภาพเพื่อ จำกัด ขนาดตาราง เนื่องจากมีโอกาสที่จะคืนค่าน้อยกว่าจำนวน จำกัด ที่กำหนดไว้เราจึงต้องเพิ่มความน่าจะเป็นเพื่อให้แน่ใจว่าเราเลือกแถวได้เพียงพอ ดังนั้นเราจึงคูณ $ size ด้วย $ factor (โดยปกติฉันจะตั้งค่า $ factor = 2 ซึ่งใช้ได้ในกรณีส่วนใหญ่) ในที่สุดเราก็ทำlimit $size

ตอนนี้ปัญหากำลังดำเนินการในการแก้ไขค่าfacodation_table_row_count accomodation_table_row_countถ้าเราทราบขนาดตารางเราสามารถกำหนดขนาดตารางได้ยาก สิ่งนี้จะทำงานได้เร็วที่สุด แต่เห็นได้ชัดว่านี่ไม่เหมาะ หากคุณใช้ Myisam การนับตารางจะมีประสิทธิภาพมาก เนื่องจากฉันใช้ innodb ฉันแค่ทำการนับ + การเลือกแบบธรรมดา ในกรณีของคุณจะมีลักษณะดังนี้:

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

ส่วนที่ยุ่งยากคือการหาค่าความน่าจะเป็นที่เหมาะสม ดังที่คุณเห็นโค้ดต่อไปนี้จะคำนวณขนาดตารางอุณหภูมิคร่าวๆเท่านั้น (อันที่จริงแล้วหยาบเกินไป!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))แต่คุณสามารถปรับแต่งตรรกะนี้เพื่อให้ได้ค่าประมาณขนาดตารางที่ใกล้ขึ้นโปรดทราบว่าการเลือก OVER จะดีกว่าการเลือกแถวใต้ กล่าวคือหากตั้งค่าความน่าจะเป็นต่ำเกินไปคุณมีความเสี่ยงที่จะเลือกแถวไม่เพียงพอ

โซลูชันนี้ทำงานช้ากว่าโซลูชันของ Quassnoi เนื่องจากเราต้องคำนวณขนาดตารางใหม่ อย่างไรก็ตามฉันพบว่าการเข้ารหัสนี้จัดการได้ง่ายกว่ามาก นี่เป็นการแลกเปลี่ยนระหว่างความแม่นยำ + ประสิทธิภาพและความซับซ้อนในการเข้ารหัสการเข้ารหัสความซับซ้อนต้องบอกว่าบนโต๊ะขนาดใหญ่ยังเร็วกว่า Order by Rand () มาก

หมายเหตุ: ถ้าตรรกะของแบบสอบถามอนุญาตให้ทำการเลือกแบบสุ่มโดยเร็วที่สุดก่อนที่จะดำเนินการรวมใด ๆ


-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; 
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.