Làm cách nào để tối ưu hóa hàm ORDER BY RAND () của MySQL?


90

Tôi muốn tối ưu hóa các truy vấn của mình để tôi xem xét mysql-slow.log.

Hầu hết các truy vấn chậm của tôi đều 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 khả thi 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ậ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 có thể chọn PHPhàng ngẫu nhiên do tôi tạo.

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

Câu trả lời:


67

Thử cái 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ả khi bật MyISAM(vì COUNT(*)tức thì), nhưng thậm chí InnoDB10còn hiệu quả hơn gấp nhiều lần ORDER BY RAND().

Ý tưởng chính ở đây là chúng tôi không sắp xếp, nhưng thay vào đó giữ hai biến và tính toán running probabilityhàng sẽ được chọn ở bước hiện tại.

Xem bài viết này trong blog của tôi để biết thêm chi tiết:

Cập nhật:

Nếu bạn cần chọn trừ một bản ghi ngẫu nhiên, hãy thử cách 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 rằng của bạn ac_idđược phân phối nhiều hơn hoặc ít hơn đồng đều.


Xin chào, Quassnoi! Trước hết, cảm ơn vì phản hồi nhanh chóng của bạn! Có thể đó 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 bằng 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

có lỗi đánh máy tại "JOIN accomodation aco ON aco.id =" trong đó aco.id thực sự là aco.ac_id. mặt khác, truy vấn đã sửa không hoạt động với tôi vì nó gây ra lỗi # 1241 - Toán hạng phải chứa (các) cột ở SELECT thứ năm (lựa chọn con thứ tư). Tôi đã cố gắng tìm vấn đề với dấu ngoặc (nếu tôi không sai) nhưng tôi chưa thể tìm ra vấn đề.
fabrik

@fabrik: thử ngay bây giờ. 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

Cảm ơn, nó hoạt động! :) Bạn có thể chỉnh sửa phần JOIN ... ON aco.id thành JOIN ... ON aco.ac_id để tôi có thể chấp nhận giải pháp của bạn. Cảm ơn một lần nữa! Một câu hỏi: Tôi tự hỏi nếu có thể đây là một ngẫu nhiên tồi tệ hơn như ORDER BY RAND ()? Chỉ vì truy vấn này lặp lại một số kết quả nhiều lần.
fabrik

1
@Adam: không, đó là cố ý, để bạn có thể tái tạo kết quả.
Quassnoi

12

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

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

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

Các giải pháp khác:

  • Thêm một trường float vĩnh viễn được gọi randomvào bảng và điền vào nó bằng các 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 vào bộ nhớ cache trong một tệp văn bản. Đọc tệp và chọn một ID ngẫu nhiên từ nó.
  • Lưu vào bộ nhớ cache kết quả của truy vấn dưới dạng HTML và giữ nó trong vài giờ.

8
Là chỉ 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ả họ đều ném "sử dụng không hợp lệ của chức năng nhóm" ..
Sophivorus

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

11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1Có vẻ như đang làm trò lừa cho tôi
Mark

1

Đây là cách tôi sẽ 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;


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 id đầu tiên là 121.
Fabrik

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

1
Việc sử dụng OFFSET(dùng để làm gì @r) không tránh được quá trình quét - lên đến việc quét toàn bộ bảng.
Rick James

@RickJames, đúng vậy. Nếu tôi phải trả lời câu hỏi này hôm nay, tôi sẽ thực hiện truy vấn bằng khóa chính. Sử dụng độ lệch với LIMIT sẽ quét rất nhiều hàng. Truy vấn bằng khóa chính, mặc dù nhanh hơn nhiều, nhưng không đảm bảo cơ hội chọn từng hàng đều - nó ưu tiên các hàng theo sau khoảng trống.
Bill Karwin

1

(Đúng vậy, tôi sẽ phát mệt vì không có đủ thịt ở đây, nhưng bạn không thể ăn chay trong một ngày sao?)

Trường hợp: AUTO_INCREMENT liên tiếp không có khoảng trống, 1 hàng được trả về
Trường hợp: AUTO_INCREMENT liên tiếp không có khoảng trống, 10 hàng
Trường hợp: AUTO_INCREMENT có khoảng trống, 1 hàng được trả về
Trường hợp: Cột FLOAT bổ sung để tạo ngẫu nhiên
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 blog của tôi để biết chi tiết.


0

Điều này sẽ cung cấp cho bạn một truy vấn phụ duy nhất sẽ sử dụng chỉ mục để lấy một id ngẫu nhiên sau đó truy vấn khác sẽ kích hoạt việc lấy 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 ví dụ giả của 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ề các lựa chọn thay thế ORDER BY RAND(), bạn nên đọc bài viết này .


0

Tôi đang tối ưu hóa rất 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 thật khó để 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 một giải pháp ít tối ưu hơn. Về cơ bản, nó hoạt động giống 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]tính ra xác suất 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 xác suất. Điều này thực hiện một cách hiệu quả 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ả về í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 ta nhân $ size với $ factor (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 làmlimit $size

Sự cố hiện đang giải quyết số lượng 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 kích thước bảng. Điều này sẽ chạy nhanh nhất, nhưng rõ ràng điều nà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ả. Vì tôi đang sử dụng innodb nên tôi chỉ thực hiện một phép đếm + lựa chọn đơn giản. Trong trường hợp của bạn, nó sẽ giố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à tìm ra xác suất đúng. Như bạn có thể thấy đoạn mã sau thực tế chỉ tính toán kích thước bảng tạm thời thô (Trên 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 để đưa ra giá trị 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 QUÁ hơn là chọn dưới các hàng. 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 so với giải pháp của Quassnoi vì chúng ta cần tính toán lại kích thước bảng. Tuy nhiên, tôi thấy việc viết mã này dễ quản lý hơn rất nhiều. Đây là sự đánh đổi giữa độ chính xác + hiệu suấtđộ phức tạp của mã hóa . Phải nói rằng, trên các bảng lớn, điều này vẫ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 bất kỳ hoạt động nối nào.


-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; 
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.