2009-08-07 35 views
86

Tôi muốn tối ưu hóa truy vấn của mình để tôi xem xét mysql-slow.log.Làm thế nào tôi có thể tối ưu hóa chức năng ORDER BY RAND() của MySQL?

Hầu hết các truy vấn chậm của tôi chứa ORDER BY RAND(). Tôi không thể tìm thấy một giải pháp thực sự để giải quyết vấn đề này. Theres là một giải pháp có thể tại MySQLPerformanceBlog nhưng tôi không nghĩ rằng điều này là đủ. Trên các bảng được tối ưu hóa kém (hoặc được cập nhật thường xuyên, do người dùng quản lý), nó không hoạt động hoặc tôi cần chạy hai hoặc nhiều truy vấn trước khi tôi có thể chọn hàng ngẫu nhiên PHP -generated của mình.

Có giải pháp nào cho vấn đề này không?

Một ví dụ giả:

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 
+0

Có thể trùng lặp [MySQL chọn 10 hàng ngẫu nhiên từ hàng 600K nhanh] (http://stackoverflow.com/questions/4329396/mysql-select-10-random-rows-from-600k-rows-fast) –

Trả lời

62

Hãy thử điều này:

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 

Điều này đặc biệt hiệu quả trên MyISAM (kể từ khi COUNT(*) là ngay lập tức), nhưng ngay cả trong InnoDB nó hiệu quả hơn 10 lần so với ORDER BY RAND() .

Ý tưởng chính ở đây là chúng tôi không sắp xếp, nhưng thay vào đó, hãy giữ hai biến và tính running probability của hàng được chọn trên bước hiện tại.

Xem bài viết này trong blog của tôi để xem chi tiết hơn:

Cập nhật:

Nếu bạn cần phải lựa chọn nhưng mức kỷ lục ngẫu nhiên duy nhất, cố gắng này:

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

Điều này giả định của bạn ac_id 's được phân phối nhiều hơn hoặc ít hơn đồng đều.

+0

Xin chào, Quassnoi! Trước hết, cảm ơn phản hồi nhanh của bạn! Có lẽ đó là lỗi của tôi nhưng vẫn chưa rõ giải pháp của bạn. Tôi sẽ cập nhật bài đăng gốc của mình với một ví dụ cụ thể và tôi sẽ rất vui nếu bạn giải thích giải pháp của mình trên ví dụ này. – fabrik

+0

có lỗi đánh máy tại "THAM GIA chỗ ở aco TRÊN aco.id =" nơi aco.id thực sự là aco.ac_id. mặt khác truy vấn đã sửa không hoạt động vì tôi ném lỗi # 1241 - Toán tử phải chứa 1 cột ở lệnh SELECT thứ năm (lựa chọn thứ tư). Tôi đã cố gắng để tìm ra vấn đề với dấu ngoặc đơn (nếu tôi không sai) nhưng tôi không thể tìm thấy vấn đề được nêu ra. – fabrik

+0

'@ fabrik': thử ngay bây giờ. Nó sẽ thực sự hữu ích nếu bạn đăng các kịch bản bảng để tôi có thể kiểm tra chúng trước khi đăng. – Quassnoi

1

Đây là cách tôi muốn làm điều đó:

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; 
+0

Xem thêm http://stackoverflow.com/questions/211329/quick-selection-of-a-random-row-from-a-large-table-in-mysql/213242#213242 –

+0

bảng của tôi không liên tục vì nó thường được chỉnh sửa. ví dụ hiện tại id đầu tiên là 121. – fabrik

+3

Kỹ thuật trên không dựa vào các giá trị id đang được liên tục. Nó chọn một số ngẫu nhiên giữa 1 và COUNT (*), không phải 1 và MAX (id) giống như một số giải pháp khác. –

13

Nó phụ thuộc vào cách ngẫu nhiên bạn cần phải. Giải pháp bạn đã liên kết hoạt động khá tốt với IMO. Trừ khi bạn có khoảng trống lớn trong lĩnh vực ID, nó vẫn còn khá ngẫu nhiên.

Tuy nhiên, bạn sẽ có thể làm điều đó trong một truy vấn sử dụng này (để chọn một giá trị duy nhất):

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

giải pháp khác:

  • Thêm một lĩnh vực phao vĩnh viễn gọi random để bảng và điền nó với số ngẫu nhiên. Sau đó, bạn có thể tạo một số ngẫu nhiên trong PHP và làm "SELECT ... WHERE rnd > $random"
  • Lấy toàn bộ danh sách ID và lưu chúng trong một tệp văn bản. Đọc tệp và chọn một ID ngẫu nhiên từ tệp đó.
  • Cache kết quả truy vấn dưới dạng HTML và giữ nó trong vài giờ.
+5

Chỉ là tôi hoặc truy vấn này không hoạt động? Tôi đã thử nó với một số biến thể và tất cả đều ném "Sử dụng chức năng nhóm không hợp lệ" .. – Sophivorus

+0

Bạn có thể làm điều đó với truy vấn con 'SELECT [trường] TỪ [bảng] WHERE id> = FLOOR (RAND() * (SELECT MAX (id) FROM [table])) LIMIT 1' nhưng điều này dường như không hoạt động đúng vì nó không bao giờ trả về bản ghi cuối cùng – Mark

+7

'SELECT [fields] FROM [table] WHERE id> = FLOOR (1 + RAND() * (CHỌN MAX (id) TỪ [bảng])) GIỚI HẠN 1' Dường như đang làm thủ thuật cho tôi – Mark

0

Điều này sẽ cung cấp cho bạn truy vấn phụ duy nhất sẽ sử dụng chỉ mục để nhận id ngẫu nhiên khi đó truy vấn khác sẽ kích hoạt bảng đã tham gia của bạn.

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

Giải pháp cho giả-dụ bạn sẽ là:

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 

Để đọc thêm về lựa chọn thay thế để ORDER BY RAND(), bạn nên đọc this article.

0

Tôi đang tối ưu hóa nhiều truy vấn hiện có trong dự án của mình. Giải pháp của Quassnoi đã giúp tôi tăng tốc các truy vấn rất nhiều! Tuy nhiên, tôi thấy khó có thể kết hợp giải pháp đã nói trong tất cả các truy vấn, đặc biệt là đối với các truy vấn phức tạp liên quan đến nhiều truy vấn con trên nhiều bảng lớn.

Vì vậy, tôi đang sử dụng giải pháp ít được tối ưu hóa hơn. Về cơ bản nó hoạt động theo cách tương tự như giải pháp của 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] hoạt động ra khả năng chọn một hàng ngẫu nhiên. Rand() sẽ tạo ra một số ngẫu nhiên. Hàng sẽ được chọn nếu rand() nhỏ hơn hoặc bằng với xác suất. Điều này có hiệu quả thực hiện một lựa chọn ngẫu nhiên để giới hạn kích thước bảng. Vì có khả năng nó sẽ trả lại ít hơn số lượng giới hạn đã xác định, chúng tôi cần tăng xác suất để đảm bảo chúng tôi đang chọn đủ hàng. Do đó chúng tôi nhân kích thước $ với một yếu tố $ (tôi thường đặt $ factor = 2, hoạt động trong hầu hết các trường hợp). Cuối cùng, chúng tôi thực hiện limit $size

Sự cố hiện đang làm việc trên accomodation_table_row_count. Nếu chúng ta biết kích thước bảng, chúng tôi có thể mã hóa cứng kích thước bảng. Điều này sẽ chạy nhanh nhất, nhưng rõ ràng đây không phải là lý tưởng. Nếu bạn đang sử dụng Myisam, việc đếm bảng rất hiệu quả. Kể từ khi tôi đang sử dụng innodb, tôi chỉ làm một số đơn giản + lựa chọn. Trong trường hợp của bạn, nó sẽ trông như sau:

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 

Phần khó khăn là xác định đúng xác suất. Như bạn có thể thấy mã sau đây thực sự chỉ tính toán kích thước bảng tạm thời thô (Trong thực tế, quá thô!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category)) Nhưng bạn có thể tinh chỉnh logic này để cung cấp cho một xấp xỉ kích thước bảng gần đúng hơn. Lưu ý rằng tốt hơn là chọn OVER hơn so với các hàng không chọn. tức là nếu xác suất được đặt quá thấp, bạn có nguy cơ không chọn đủ hàng.

Giải pháp này chạy chậm hơn giải pháp của Quassnoi vì chúng tôi cần phải tính toán lại kích thước bảng. Tuy nhiên, tôi thấy mã này dễ quản lý hơn nhiều. Đây là giao dịch giữa độ chính xác + hiệu suất so với độ phức tạp mã hóa. Có nói rằng, trên bảng lớn này vẫn còn nhanh hơn nhiều so với Order by Rand().

Lưu ý: Nếu logic truy vấn cho phép, hãy thực hiện lựa chọn ngẫu nhiên càng sớm càng tốt trước mọi hoạt động nối.

0

(Ừ, tôi sẽ cảm thấy không có đủ thịt ở đây, nhưng bạn không thể ăn thuần chay trong một ngày?)

Trường hợp: AUTO_INCREMENT liên tiếp mà không cần những khoảng trống, 1 hàng trở
Trường hợp: AUTO_INCREMENT liên tiếp mà không cần những khoảng trống, 10 dòng
Trường hợp: AUTO_INCREMENT với những khoảng trống, 1 hàng trở
Trường hợp: FLOAT cột tắm cho randomizing
Trường hợp: Cột UUID hoặc MD5

5 trường hợp đó có thể được thực hiện rất hiệu quả cho các bảng lớn. Xem my blog để biết chi tiết.

-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; 
} 
Các vấn đề liên quan