2017-05-09 1 views
2

사용자가 다양한 유형의 기여에 대해 1 개 이상의 크레딧을 얻을 수있는 시스템이 있습니다.곱한 결과에서 임의의 고유 값을 선택하는 방법

CREATE TABLE user_contribution_types (
    type_id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, 
    title VARCHAR(255) NOT NULL, 
    credits DECIMAL(5,2) UNSIGNED NOT NULL, 
    valid TINYINT(1) UNSIGNED NOT NULL DEFAULT 1, 

    PRIMARY KEY (type_id) 
); 

CREATE TABLE user_contributions (
    user_id INTEGER UNSIGNED NOT NULL, 
    type_id INTEGER UNSIGNED NOT NULL, 
    create_date DATETIME NOT NULL, 
    valid TINYINT(1) UNSIGNED NOT NULL DEFAULT 1, 

    FOREIGN KEY (user_id) 
    REFERENCES users(user_id), 
    FOREIGN KEY (type_id) 
    REFERENCES user_contribution_types(type_id) 
); 

나는 다음과 특정 날짜 이후에 취득한 총 학점 선택할 수 있습니다 :이 2 개 테이블에 저장되어 마찬가지로

SELECT SUM(credits) AS total 
FROM user_contribution_types AS a 
JOIN user_contributions AS b ON a.type_id = b.type_id 
WHERE b.create_date >= '2017-05-01 00:00:00' 
     AND a.valid = TRUE 
     AND b.valid = TRUE 

을, 나는 총 학점을 찾을 수 b.user_id에 대한 일치를 포함 할 수있다 그 특정 사용자.

내가 뭘하고 싶은지 공짜로 항목으로 벌어 들여 각 신용을 취급하고 3 임의 (고유) user_id을 선택합니다. 따라서 한 명의 사용자가 26 크레딧을 얻으면 26 회 우승 할 수 있습니다.

이 작업을 SQL로 어떻게 수행 할 수 있습니까? 아니면 응용 프로그램 수준에서 수행하는 것이 더 합리적입니까? 나는 가능한 한 정말로 무작위적인 것에 가까운 해결책을 선호 할 것이다.

답변

0

그럼 Gordon의 코드를 오류없이 실행할 수 없으므로 응용 프로그램 논리로 되 돌린 뒤 솔루션 found here을 따라갔습니다. 예 : 나는 여러 우승자를 선택 원하는

// pick a random winner since a given date 
// optionally exclude certain users 
public function getWinner($date, array $exclude = []) { 
    if (!empty($exclude)) { 
     $in = implode(',', array_fill(0, count($exclude), '?')); 
     array_unshift($exclude, $date); 

     $sql = "SELECT b.user_id, SUM(credits) AS total 
       FROM  user_contribution_types AS a 
       JOIN  user_contributions AS b ON a.type_id = b.type_id 
       WHERE b.create_date >= ? 
         AND b.user_id NOT IN ($in) 
         AND a.valid = TRUE 
         AND b.valid = TRUE 
       GROUP BY b.user_id"; 
     $sth = $this->db->prepare($sql); 
     $sth->execute($exclude); 
    } else { 
     $sql = "SELECT b.user_id, SUM(credits) AS total 
       FROM  user_contribution_types AS a 
       JOIN  user_contributions AS b ON a.type_id = b.type_id 
       WHERE b.create_date >= :date 
         AND a.valid = TRUE 
         AND b.valid = TRUE 
       GROUP BY b.user_id"; 
     $sth = $this->db->prepare($sql); 
     $sth->execute([':date' => $date]); 
    } 

    $result = []; 
    while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { 
     $result[$row['user_id']] = floor($row['total']); 
    } 

    // cryptographically secure pseudo-random integer, otherwise fallback 
    $total = array_sum($result); 
    if (function_exists('random_int')) { 
     $rand = $total > 0 ? random_int(0, $total - 1) : 0; 
    } else { 
     // fallback, NOT cryptographically secure 
     $rand = $total > 0 ? mt_rand(0, $total - 1) : 0; 
    } 

    $running_total = 0; 
    foreach ($result as $user_id => $credits) { 
     $running_total += $credits; 
     if ($running_total > $rand) { 
      // we have a winner 
      return $user_id; 
     } 
    } 

    return false; 
} 

그래서 나는 기본적으로이 코드를 여러 번 실행할 수 있습니다

$ts = '2017-01-01 00:00:00'; 
$first_place = getWinner($ts); 
$second_place = getWinner($ts, [$first_place]); 
$third_place = getWinner($ts, [$first_place, $second_place]); 

를 대체 솔루션은 내가 대답으로 이것을 받아 게시하지 않는 한.

2

당신은 누적 분포를 계산하고 rand()를 사용하여 하나의 사용자를 선택할 수 있습니다

SELECT uc.* 
FROM (SELECT uc.user_id, (@t := @t + total) as running_total 
     FROM (SELECT uc.user_id, SUM(credits) as total 
      FROM user_contribution_types ct JOIN 
       user_contributions c 
       ON ct.type_id = c.type_id 
      WHERE c.create_date >= '2017-05-01' AND ct.valid = TRUE AND c.valid = TRUE 
      GROUP BY uc.user_id 
      ) uc CROSS JOIN 
      (SELECT @t := 0) params 
     ORDER BY rand() 
    ) uc 
WHERE rand()*@t BETWEEN (running_total - total) AND running_total; 

rand()이 경계에 정확하게 경우이 두 값을 반환하는 소문자 가능성이 있습니다. 귀하의 목적을 위해, 이것은 문제가되지 않습니다; limit 1을 추가하기 만하면됩니다.

WHERE rand()*@t BETWEEN (running_total - total) AND running_total OR 
     rand()*@t BETWEEN (running_total - total) AND running_total OR 
     rand()*@t BETWEEN (running_total - total) AND running_total 

문제는 모든 결과 값이 같은 결과가 될 수 있다는 것입니다 :

당신은 단지에 WHERE 절을 수정할 수 있습니다, 여러 행이 확장합니다.

세 개 이상의 값을 임의로 선택할 수 있습니다.

WHERE FLOOR(10*(running_total - total)/@t)) <> FLOOR(10*running_total/@t) 
ORDER BY rand() 
LIMIT 3 

이 당신이 10을 변경할 수 있기 때문에 쉽게와 함께 동일한 크기의 점의 수를 테스트 :

WHERE 0.1*@t BETWEEN (running_total - total) AND running_total OR 
     0.2*@t BETWEEN (running_total - total) AND running_total OR 
     0.3*@t BETWEEN (running_total - total) AND running_total OR 
     . . . 
ORDER BY rand() -- redundant, but why not? 
LIMIT 3 

또는 더 간단 : 내 성향은 9로, 더 큰 숫자를 선택하는 것입니다 누적 분포.

+0

정확하게 이해하면 첫 번째 예가 응용 프로그램 수준 논리와 함께 사용될 수 있습니까? 그래서 함수로 랩핑하고 그 결과를 다시 자신에게 전달할 수 있습니까? 즉,'user_id가 아닌 곳 ($ winners)'. 그리고 그게 완전히 중복 결과를 배제 할 것입니다 ... –

+0

또한이 오류가 발생합니다 : '필드 목록'에서 알 수없는 열 'uc.user_id' –

+0

@ mistermartin. . . 해당 열은 귀하의 질문에 명시되어 있습니다. 당신은'u를 잃어 버렸을지도 모른다.user_id'하지만 별칭을 수정했습니다. –

관련 문제