복붙노트

[SQL] 가중 행 확률로 PostgreSQL의 테이블에서 임의의 행을 선택

SQL

가중 행 확률로 PostgreSQL의 테이블에서 임의의 행을 선택

예 입력 :

SELECT * FROM test;
 id | percent   
----+----------
  1 | 50 
  2 | 35   
  3 | 15   
(3 rows)

어떻게 시간이 평균 50 %에 내가 ID = 1 행을 얻을 수 있다는 것을, 같은 쿼리를 작성합니다, ID = 2 시간 행의 35 %, 그리고 ID = 3 시간 행의 15 %?

나는 * 무작위 () DESC LIMIT 1 P로 시험 순서와 SELECT ID로 뭔가를 시도하지만 잘못된 결과를 제공합니다. 10,000 실행 후 나는 같은 분포를 얻을 : {= 6293 1, 3 = 405 = 3302 2},하지만 난 분포가 될 것으로 예상 거의 : {= 5000 1 = 3500 2, 3 = 1500}.

어떤 아이디어?

해결법

  1. ==============================

    1.이 트릭을 수행해야합니다

    이 트릭을 수행해야합니다

    WITH CTE AS (
        SELECT random() * (SELECT SUM(percent) FROM YOUR_TABLE) R
    )
    SELECT *
    FROM (
        SELECT id, SUM(percent) OVER (ORDER BY id) S, R
        FROM YOUR_TABLE CROSS JOIN CTE
    ) Q
    WHERE S >= R
    ORDER BY id
    LIMIT 1;
    

    서브 질의 Q는 다음의 결과를 제공한다 :

    1  50
    2  85
    3  100
    

    우리는 단순히 범위 [0, 100에서 임의의 번호를 생성) 및 또는 그 수 (절) 넘어 첫 번째 행을 선택합니다. 우리는 임의의 숫자는 한 번만 계산하기 위해 공통 테이블 표현식 (WITH)를 사용합니다.

    BTW, YOUR_TABLE의 FROM SELECT SUM (퍼센트)를 사용하면 퍼센트의 모든 무게를 가지고 있습니다 - 그들은 엄격 비율 될 필요가 없습니다 (즉, 추가 최대 100).

    [SQL 바이올린]

  2. ==============================

    2.알고리즘에서 Efraimidis 및 Spirakis 설명.

    알고리즘에서 Efraimidis 및 Spirakis 설명.

  3. ==============================

    3.귀하의 제안 쿼리 작업에 나타납니다; 이 SQLFiddle 데모를 참조하십시오. 그것은 잘못된 유통 불구을 생성; 아래를 참조하십시오.

    귀하의 제안 쿼리 작업에 나타납니다; 이 SQLFiddle 데모를 참조하십시오. 그것은 잘못된 유통 불구을 생성; 아래를 참조하십시오.

    나는 휘발성 SQL 기능에 싸서 한 하위 쿼리를 최적화에서 PostgreSQL을 방지합니다. PostgreSQL은 당신이 휘발성을 강제하지 않는 경우 그래서 그냥 한 번 실행하는 것입니다, 당신은 외부 쿼리의 모든 행에 대해 한 번 실행하는 하위 쿼리를하려한다는 것을 알 수있는 방법이 없습니다. http://sqlfiddle.com/# : 또 다른 가능성은 - - 일하지만 쿼리 계획이 미래에서 최적화 할 수 있다는 것이이 같은이 항상 사실 where 절을 사용하여이 해킹과 같은 상관 하위 쿼리로 표시하는 것입니다 ! 12 / 3039b / 9

    (당신이 작동하지 않는 이유를 설명하기 위해 업데이트 전) 추측에 당신의 테스트 방법론은 잘못했다, 또는 어디 PostgreSQL의 그것은 상관 하위 쿼리 아니다 몰래 그것을 실행하는 외부 쿼리에서 하위 쿼리로 이것을 사용하고 한 번만,이 예에서 좋아한다. .

    UPDATE : 생산 유통 당신이 기대하는 것이 아니다. 여기에서 문제는 당신이 여러 샘플을 채취하여 분포를 기울어 야한다는 것입니다 () 임의; 당신은 하나의 샘플이 필요합니다.

    이 쿼리는 정확한 분포 (SQLFiddle)를 생성합니다

    WITH random_weight(rw) AS (SELECT random() * (SELECT sum(percent) FROM test))
     SELECT id
    FROM (                   
      SELECT 
        id,
        sum(percent) OVER (ORDER BY id),
        coalesce(sum(prev_percent) OVER (ORDER BY id),0) FROM (
          SELECT 
            id,
            percent,
            lag(percent) OVER () AS prev_percent
          FROM test
        ) x
    ) weighted_ids(id, weight_upper, weight_lower)
    CROSS JOIN random_weight
    WHERE rw BETWEEN weight_lower AND weight_upper;
    

    성능, 끔찍한, 말할 필요도 없다. 이 창을 두 개의 중첩 세트를 사용하고. 내가하고 있어요 것은 :

  4. ==============================

    4.여기에 당신이 연주 할 일이 있습니다 :

    여기에 당신이 연주 할 일이 있습니다 :

    select t1.id as id1
      , case when t2.id is null then 0 else t2.id end as id2
      , t1.percent as percent1
      , case when t2.percent is null then 0 else t2.percent end as percent2 
    from "Test1" t1 
      left outer join "Test1" t2 on t1.id = t2.id + 1
    where random() * 100 between t1.percent and 
      case when t2.percent is null then 0 else t2.percent end;
    

    기본적으로 왼쪽 외부 당신이 절 사이에 적용하는 두 개의 열이 너무 조인 수행합니다.

    그것은 당신이 당신의 표는 올바른 방법으로 만 작업 지시 얻을 것이다 경우합니다.

  5. ==============================

    5.브란 코 Dimitrijevic의 답변에 따라, 나는 나 (안 ROLLUP과 달리) 계층 윈도 기능을 사용하여 퍼센트의 총합을 사용하여 빠르게하지 않을 수도 있습니다 쿼리를 썼다.

    브란 코 Dimitrijevic의 답변에 따라, 나는 나 (안 ROLLUP과 달리) 계층 윈도 기능을 사용하여 퍼센트의 총합을 사용하여 빠르게하지 않을 수도 있습니다 쿼리를 썼다.

    WITH random AS (SELECT random() AS random)
    SELECT id FROM (
        SELECT id, percent,
        SUM(percent) OVER (ORDER BY id) AS rank,
        SUM(percent) OVER () * random AS roll
        FROM test CROSS JOIN random
    ) t WHERE roll <= rank LIMIT 1
    

    순서가 중요하지 않은 경우 먼저 데이터를 정렬하는 것을 피할 수 있기 때문에, 순위 AS OVER SUM (%)은 (ROWS UNBOUNDED이 PRECEDING), 바람직 할 수있다.

    매우 성능면에서 유망한 것 같다, (분명히이 논문에 기술 된 바와 같이) 또한 기계공 웨이의 답변을했지만, 몇 가지 테스트 후, 분포는 꺼져있는 것처럼 :

    SELECT id
    FROM test
    ORDER BY random() ^ (1.0/percent)
    LIMIT 1
    
  6. ==============================

    6.브랑코의 허용 솔루션은 큰 (감사합니다!)입니다. 그러나, 나는 단지 확대됨에로 (내 테스트에 따라) 인 대안을 기여하고 시각화 아마도 쉽게 싶습니다.

    브랑코의 허용 솔루션은 큰 (감사합니다!)입니다. 그러나, 나는 단지 확대됨에로 (내 테스트에 따라) 인 대안을 기여하고 시각화 아마도 쉽게 싶습니다.

    하자의 요점을 되풀이하다. 다음과 같이 원래의 질문에 아마도 일반화 될 수 있습니다 :

    상대적 무게,하지 퍼센트에 중점을합니다. 로 브랑코는 백분율을 포함하여 무엇이든을 위해 작동 할 상대 가중치를 사용하여, 그의 대답에 지적한다.

    이제, 우리는 임시 테이블에 올려 놓을 게요 테스트 데이터를 고려 :

    CREATE TEMP TABLE test AS
    SELECT * FROM (VALUES
        (1, 25),
        (2, 10),
        (3, 10),
        (4, 05)
    ) AS test(id, weight);
    

    나는 그것이 편리하게 100까지 추가하지 않는다는 점에서, 원래의 질문에보다 더 복잡한 예제를 사용하고, 같은 무게 (20)이 한 번 이상 사용되는 것을있어주의 IDS (2, 3), 나중에 살펴 보 겠지만 이는 고려하는 것이 중요하다.

    우리가해야 할 첫 번째 일은 간단한 정상화 (무게 / 합계 (무게))보다 더 아무것도없는 0에서 1로 확률에 가중치를 설정입니다 :

    WITH p AS ( -- probability
        SELECT *,
            weight::NUMERIC / sum(weight) OVER () AS probability
        FROM test
    ),
    cp AS ( -- cumulative probability
        SELECT *,
            sum(p.probability) OVER (
                ORDER BY probability DESC
                ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
            ) AS cumprobability
        FROM p
    )
    SELECT
        cp.id,
        cp.weight,
        cp.probability,
        cp.cumprobability - cp.probability AS startprobability,
        cp.cumprobability AS endprobability
    FROM cp
    ;
    

    이것은 다음과 같은 결과가 발생합니다 :

     id | weight | probability | startprobability | endprobability
    ----+--------+-------------+------------------+----------------
      1 |     25 |         0.5 |              0.0 |            0.5
      2 |     10 |         0.2 |              0.5 |            0.7
      3 |     10 |         0.2 |              0.7 |            0.9
      4 |      5 |         0.1 |              0.9 |            1.0
    

    위의 쿼리는 일반적으로 인정 하듯이 우리의 요구에 꼭 필요한 것보다 더 많은 일을하고있다,하지만 난 그게 도움이 상대적 확률이 방법을 시각화 발견, 그것은 사소한 ID를 선택하는 마지막 단계를 만들 않습니다 :

    SELECT id FROM (queryabove)
    WHERE random() BETWEEN startprobability AND endprobability;
    

    자,하자가 쿼리가 예상 분포 데이터를 반환 보장하는 테스트로 모두 함께 넣어. 우리는 임의의 숫자를 만 번을 생성하는 generate_series ()를 사용합니다 :

    WITH p AS ( -- probability
        SELECT *,
            weight::NUMERIC / sum(weight) OVER () AS probability
        FROM test
    ),
    cp AS ( -- cumulative probability
        SELECT *,
            sum(p.probability) OVER (
                ORDER BY probability DESC
                ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
            ) AS cumprobability
        FROM p
    ),
    fp AS ( -- final probability
        SELECT
            cp.id,
            cp.weight,
            cp.probability,
            cp.cumprobability - cp.probability AS startprobability,
            cp.cumprobability AS endprobability
        FROM cp
    )
    SELECT *
    FROM fp
    CROSS JOIN (SELECT random() FROM generate_series(1, 1000000)) AS random(val)
    WHERE random.val BETWEEN fp.startprobability AND fp.endprobability
    ;
    
    

    이것은 다음과 유사한 출력을 발생합니다 :

     id | count  
    ----+--------
     1  | 499679 
     3  | 200652 
     2  | 199334 
     4  | 100335 
    

    어느, 당신이 볼 수있는, 완벽 트랙 예상 분포를.

    위의 쿼리는 매우 성능이 좋은 것입니다. 심지어 내 평균 기계, PostgreSQL을가 WSL1 인스턴스에 (! 공포)를 실행하여 실행이 상대적으로 빠르다 :

         count | time (ms)
    -----------+----------
         1,000 |         7
        10,000 |        25
       100,000 |       210
     1,000,000 |      1950 
    

    I 종종 단위 / 통합 테스트를위한 테스트 데이터를 생성 할 때, 상기 질의의 변형을 사용한다. 아이디어는 현실을 추적하는 확률 분포를 근사 임의의 데이터를 생성하는 것입니다.

    그 상황에서 나는 그것이 유용 시작과 끝 분포를 계산하기 위해 찾아 번 테이블에 결과를 저장 :

    CREATE TEMP TABLE test AS
    WITH test(id, weight) AS (VALUES
        (1, 25),
        (2, 10),
        (3, 10),
        (4, 05)
    ),
    p AS ( -- probability
        SELECT *, (weight::NUMERIC / sum(weight) OVER ()) AS probability
        FROM test
    ),
    cp AS ( -- cumulative probability
        SELECT *,
            sum(p.probability) OVER (
                ORDER BY probability DESC
                ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
            ) cumprobability
        FROM p
    )
    SELECT
        cp.id,
        cp.weight,
        cp.probability,
        cp.cumprobability - cp.probability AS startprobability,
        cp.cumprobability AS endprobability
    FROM cp
    ;
    

    나는 그 여분의 성능과 간단한 사용 된 결과, 반복이 미리 계산 확률을 사용할 수 있습니다.

    난 내가 임의의 ID를 얻기 위해 원하는 시간을 호출 할 수있는 함수에 모든 것을 포장 할 수 있습니다 :

    CREATE OR REPLACE FUNCTION getrandomid(p_random FLOAT8 = random())
    RETURNS INT AS
    $$
        SELECT id
        FROM test
        WHERE p_random BETWEEN startprobability AND endprobability
        ;
    $$
    LANGUAGE SQL STABLE STRICT
    

    이 기술은 위의 PRECEDING UNBOUNDED과 현재 행 사이의 비 표준 프레임 행 창 기능을 사용하고 있음을 지적 그것의 가치. 이것은 내가 처음부터 반복 무게 테스트 데이터를 선택한 이유입니다 약간 무게가 반복 될 수 있다는 사실에 대처하는 것이 필요하다!

  7. from https://stackoverflow.com/questions/13040246/select-random-row-from-a-postgresql-table-with-weighted-row-probabilities by cc-by-sa and MIT license