MySQL의 ORDER BY RAND () 함수를 어떻게 최적화 할 수 있습니까?


90

쿼리를 최적화하고 싶습니다. mysql-slow.log .

내 느린 쿼리의 대부분에는 ORDER BY RAND(). 이 문제를 해결할 실제 해결책을 찾을 수 없습니다. 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 2009-08-07

"JOIN accomodation aco ON aco.id ="에 오타가 있습니다. 여기서 aco.id는 실제로 aco.ac_id입니다. 반면에 수정 된 쿼리는 오류 # 1241이 발생하기 때문에 작동하지 않았습니다. 피연산자는 다섯 번째 SELECT (네 번째 하위 선택)에서 1 개의 열을 포함해야합니다. 나는 괄호로 문제를 찾으려고 노력했지만 (내가 틀리지 않은 경우) 아직 문제를 찾을 수 없습니다.
fabrik 2009-08-10

@fabrik: 지금 시도하십시오. 게시하기 전에 확인할 수 있도록 테이블 스크립트를 게시하면 정말 도움이 될 것입니다.
Quassnoi

감사합니다, 작동합니다! :) JOIN ... ON aco.id 부분을 JOIN ... ON aco.ac_id로 편집하여 솔루션을 수락 할 수 있습니다. 다시 한 번 감사드립니다! 질문 : 가능하다면 이것이 ORDER BY RAND ()와 같은 더 나쁜 무작위인지 궁금합니다. 이 쿼리가 일부 결과를 여러 번 반복하기 때문입니다.
fabrik

1
@Adam : 아니요, 의도적이므로 결과를 재현 할 수 있습니다.
Quassnoi 2011

12

그것은 당신이 얼마나 무작위 적이어야 하는가에 달려 있습니다. 연결 한 솔루션은 IMO에서 잘 작동합니다. ID 필드에 큰 간격이 있지 않는 한 여전히 무작위입니다.

그러나 다음을 사용하여 하나의 쿼리에서 수행 할 수 있어야합니다 (단일 값 선택 용).

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

기타 솔루션 :

  • random테이블에 호출 된 영구 부동 필드를 추가하고 난수로 채 웁니다. 그런 다음 PHP에서 임의의 숫자를 생성하고"SELECT ... WHERE rnd > $random"
  • 전체 ID 목록을 가져 와서 텍스트 파일에 캐시합니다. 파일을 읽고 임의의 ID를 선택하십시오.
  • 쿼리 결과를 HTML로 캐시하고 몇 시간 동안 보관합니다.

8
나뿐입니까 아니면이 쿼리가 작동하지 않습니까? 나는 몇 가지 변화와 함께 그것을 시도하고 그들은 모두 던져 "그룹 기능의 사용이 잘못되었습니다"..
Sophivorus

하위 쿼리로 수행 할 수 SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1있지만 마지막 레코드를 반환하지 않기 때문에 제대로 작동하지 않는 것 같습니다
Mark

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과 MAX (id)가 아닌 1과 COUNT (*) 사이의 임의의 숫자를 선택합니다.
Bill Karwin

1
사용 OFFSET하는 것은 @r전체 테이블 스캔까지 스캔을 피하지 않습니다.
릭 제임스

@RickJames, 맞습니다. 오늘이 질문에 답한다면 기본 키로 쿼리를 수행 할 것입니다. LIMIT와 함께 오프셋을 사용하면 많은 행을 스캔합니다. 기본 키로 쿼리하는 것은 훨씬 빠르지 만 각 행을 선택할 수있는 균등 한 기회를 보장하지는 않습니다. 간격을 따르는 행을 선호합니다.
Bill Karwin

1

(예, 여기 고기가 충분하지 않다는 이유로 물을 찌르 겠지만 하루 동안 비건 채식을 할 수 없나요?)

케이스 : 간격없는 연속 AUTO_INCREMENT, 1 행 리턴
케이스 : 간격없는 연속 AUTO_INCREMENT, 10 행
케이스 : 간격이있는 AUTO_INCREMENT, 리턴 1 행
케이스 : 랜덤 화를위한 추가 FLOAT 열
케이스 : UUID 또는 MD5 열

이 5 가지 경우는 큰 테이블에 대해 매우 효율적으로 만들 수 있습니다. 보다 내 블로그 를하십시오.


0

이것은 인덱스를 사용하여 임의의 ID를 얻는 단일 하위 쿼리를 제공하고 다른 쿼리는 조인 된 테이블을 가져옵니다.

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

이제 문제는 accomodation_table_row_count를 해결하는 것 입니다. 테이블 크기를 알고 있다면 테이블 크기를 하드 코딩 할 수 있습니다. 이것은 가장 빠르게 실행되지만 분명히 이상적이지 않습니다. Myisam을 사용하는 경우 테이블 수를 얻는 것이 매우 효율적입니다. innodb를 사용하고 있기 때문에 간단한 count + selection을하고 있습니다. 귀하의 경우에는 다음과 같습니다.

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))그러나이 논리를 구체화하여 테이블 크기 근사치를 제공 할 수 있습니다. 행을 과소 선택하는 것보다 과도하게 선택하는 것이 좋습니다. 즉, 확률이 너무 낮게 설정되면 충분한 행을 선택하지 않을 위험이 있습니다.

이 솔루션은 테이블 크기를 다시 계산해야하므로 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.