2017-05-09 67 views
2

我有一個系統,用戶可以爲不同類型的貢獻獲得一個或多個積分。這些存儲2下表中:如何從相乘結果中選擇隨機唯一值

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

我可以選擇,因爲一個特定的日期與以下賺取的總學分:

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

嗯,我無法讓戈登的代碼無誤地運行,所以我最終恢復爲應用程序邏輯,並遵循解決方案found here。例如:除非一個替代的解決方案張貼,我會接受這個作爲答案

$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]); 

// 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; 
} 

所以我基本上是我要選擇多個獲獎者執行該代碼多次。

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子句:

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 

的問題是,所有的結果值可能是相同的結果。

您可以隨機選擇三個以上的值。我的傾向是選擇一個更大的數字,如9:

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 

或者更簡單地說:

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

這是更容易,因爲你可以改變10和沿測試任何數量的等距點累積分配。

+0

如果我理解正確,您的第一個示例可以與應用程序級邏輯結合使用?所以我可以將它包裝到一個函數中並將結果傳回給它自己?即,'WHERE user_id不在($ winners)'中。這將完全排除重複的結果... –

+0

此外,我收到此錯誤:'字段列表'中的未知列'uc.user_id' –

+0

@mistermartin。 。 。該列在您的問題中指定。你可能已經錯過了你。user_id',但我修復了別名。 –