我有一个系统,用户可以为不同类型的贡献获得1个或多个学分。它们存储在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 :(得分: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
并沿累积分布测试任意数量的等间距点。
答案 1 :(得分: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]);
除非发布替代解决方案,否则我将接受此作为答案。