【问题标题】:How to select random unique values from multiplied result如何从相乘结果中选择随机唯一值
【发布时间】:2017-05-09 10:28:16
【问题描述】:

我有一个系统,用户可以通过不同类型的贡献获得 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_ids。因此,如果一位用户获得了 26 个积分,他们将有 26 次获胜的机会。

如何使用 SQL 完成此操作,或者在应用程序级别执行此操作是否更有意义?我更喜欢尽可能接近真正随机的解决方案。

【问题讨论】:

    标签: mysql sql database


    【解决方案1】:

    您可以通过计算累积分布并使用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 并沿累积分布测试任意数量的等距点。

    【讨论】:

    • 如果我理解正确,您的第一个示例可以与应用程序级逻辑结合使用吗?所以我可以把它包装成一个函数并将结果传回给它自己?即WHERE user_id NOT IN ($winners)。这将完全排除重复的结果......
    • 我也收到此错误:“字段列表”中的未知列“uc.user_id”
    • @mistermartin 。 . .该列在您的问题中指定。你可能错过了u.user_id,但我修复了别名。
    • 是的,我正在更改您的示例 userIduserid 以匹配我的示例 user_id。它仍然产生同样的错误。
    • 我很乐意接受这个答案,但我无法克服提到的错误。我在 phpmyadmin (4.0.10deb1) 中执行此操作:“字段列表”中的未知列“uc.user_id”,即使该列确实存在。
    【解决方案2】:

    好吧,我无法让 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]);
    

    除非发布替代解决方案,否则我将接受此作为答案。

    【讨论】:

      猜你喜欢
      • 2023-03-15
      • 2012-03-27
      • 1970-01-01
      • 1970-01-01
      • 2010-10-18
      • 1970-01-01
      • 2021-10-01
      • 2016-05-09
      • 2019-07-30
      相关资源
      最近更新 更多