在次线性时间从MySQL表中选择加权随机项

在次线性时间从MySQL表中选择加权随机项,mysql,optimization,random,Mysql,Optimization,Random,我见过提倡使用类似于SELECT*fromtblorderby-LOG(RAND())/weights LIMIT 1的东西从表中选择一个随机条目。然而,这在我看来效率非常低,因为我们必须在排序之前遍历整个表并为每个表生成随机数。这似乎是朝着正确方向迈出的一步:假设预先计算了总的总和,现在我们有了一个简单的线性搜索 尽管如此,确实有可能做得更好,但脑海中浮现的方法似乎都需要粗略的操作 我们可以存储累积权重分布,生成一个介于0和最大权重之间的随机数,并使用权重上的索引以及之间的来查找帖子。然而,在

我见过提倡使用类似于
SELECT*fromtblorderby-LOG(RAND())/weights LIMIT 1的东西
从表中选择一个随机条目。然而,这在我看来效率非常低,因为我们必须在排序之前遍历整个表并为每个表生成随机数。这似乎是朝着正确方向迈出的一步:假设预先计算了总的总和,现在我们有了一个简单的线性搜索

尽管如此,确实有可能做得更好,但脑海中浮现的方法似乎都需要粗略的操作

  • 我们可以存储累积权重分布,生成一个介于0和最大
    权重
    之间的随机数,并使用
    权重
    上的索引以及
    之间的
    来查找帖子。然而,在中间删除或移动条目需要大量的工作来更新它之后的权重。
  • 我们可以将表分割成
    sqrt(n)
    较小的表,并计算其中的权重之和。我们首先搜索这些范围,直到找到包含所选随机数的范围,然后对该表执行线性搜索。然而,为大型
    n
    拥有如此多的表似乎是糟糕的数据库设计,理想情况下,我希望将其降到对数时间,而不是
    O(sqrt(n))

  • 我已经为这个问题挣扎了一段时间,这是我想出的最好的办法。还有其他想法吗?

    让我试着大声思考

  • 最好的方法应该为范围(0,1)中的每个值确定地选择特定条目。对于权重为零的条目,应保持零

  • 为了使项目1成为可能,每个条目都应该知道其在0到1之间的下限和上限。这正是你在可能的解决方案中所说的。你还说这将意味着在一个入口重量变化上需要做大量的工作。但无论如何,您需要重新调整权重,使其总和为1,或者更新并缓存所有权重的总和,以便计算每个权重的百分比。嗯,你是对的,如果你在第一个条目和最后一个条目之间重新调整权重,你将不得不更新中间每个条目的限制

  • 这就引出了一个解决方案,您可以在运行时计算限制(下限或上限,您实际上只需要一个),将权重相加到条目。好消息是,您可以使用权重覆盖索引,即从内存中读取权重值

  • 该方法的缺点是,查询的执行时间将取决于随机值:值越多,查询所需时间越长。但是,较大权重的范围将更频繁地匹配,因此我们可能会以某种方式利用以下事实:权重在btree索引中排序,强制从末尾进行索引查找,并在找到值时退出处理(但目前还不确定,这对于累积值的查询是可能的)

  • 我应该再考虑一下

    Upd1。我刚刚意识到,我所描述的与链接答案中所写的完全相同。考虑到查询将从索引中获取权重,解决方案应该足够快。当然,有一个更快的选择方案,但需要更多的空间和准备。这可能看起来很疯狂,但在某些情况下可能会奏效。您可以将权重分布描述为一个固定的int值范围,并维护一个从该范围内的任何int值到特定加权项的映射(内存)表。然后,查询将一个随机值取整为整数,整数值(内存表中的主键)将指向某个条目id。显然,表中的行数将取决于权重的粒度,并且在任何权重更新之后,您必须更新整个表。但如果体重更新很少,这可能是一种选择

    Upd2。我决定展示一些SQL。下面是两个可行的解决方案。我们有一张桌子:

    CREATE TABLE entries (
      entry_id int(10) unsigned NOT NULL AUTO_INCREMENT,
      weight float NOT NULL DEFAULT 0.,
      data varchar(50),
      PRIMARY KEY (entry_id) USING BTREE,
      KEY weights (weight) USING BTREE
    ) ENGINE=InnoDB;
    
    INSERT INTO entries (weight) VALUES (0.), (0.3), (0.1), (0.3), (0.0), (0.2), (0.1);
    
    我们所能想到的最佳查询将有一个从rand()值到特定条目id的现成映射。在这种情况下,我们只需要通过主键找到一个条目。正如我所说,用于此类查询的表将占用一些空间,但假设我们已经准备好了。我们可能希望将映射保留在内存中,因此可以使用内存引擎,它使用散列索引作为主键(这也很好,因为我们需要将一个值映射到特定的值)

    让我们看看我们的表格:

    mysql> SELECT entry_id, weight FROM entries ORDER BY weight;
    +----------+--------+
    | entry_id | weight |
    +----------+--------+
    |        1 |      0 |
    |        5 |      0 |
    |        3 |    0.1 |
    |        7 |    0.1 |
    |        6 |    0.2 |
    |        2 |    0.3 |
    |        4 |    0.3 |
    +----------+--------+
    
    让我们创建另一个表并用值填充它:

    CREATE table int2entry (
      an_int int(10) unsigned NOT NULL AUTO_INCREMENT,
      entry_id int(10) unsigned NOT NULL,
      PRIMARY KEY (an_int)
    ) ENGINE=Memory;
    TRUNCATE int2entry;
    INSERT INTO int2entry (entry_id)
    VALUES (3), (7), (6), (6), (2), (2), (2), (4), (4), (4);
    
    其思想是,特定条目id的引用数量与权重成正比。仅使用SQL可能很难更新该表,并且在每次权重更改后都必须截断并更新它,但是,正如我所说的,当更新很少时,这仍然是一个选项。以下是获取条目\u id的查询,您可以将其加入到entries表中(您应该知道映射表中的行数):

    另一种解决方案是使用累积权重并利用索引中的顺序

    当我们选择数据时,数据以某种索引顺序被选择(对于主键顺序中的select*)。
    权重
    索引是从权重到条目ID的有序映射。如果我们只选择权重和条目ID,则可以直接从
    权重
    索引中获取值,也就是说,数据将按索引顺序读取。我们可以使用ORDERBY以相反的索引顺序强制迭代(最后存储的权重越大,但匹配的频率越高)。为什么它很重要?因为我们要在WHERE子句中添加一些骇人的魔法,并依赖于处理行的特定顺序:

    SET @rand:= RAND(), @cum_weight:=0.;
    SELECT entry_id, weight, @cum_weight, @rand 
    FROM entries
    WHERE @rand < @cum_weight:=@cum_weight+weight
    ORDER BY weight DESC
    LIMIT 1;
    
    +----------+--------+----------------------------------+--------------------+
    | entry_id | weight | @cum_weight                      | @rand              |
    +----------+--------+----------------------------------+--------------------+
    |        6 |    0.2 | 0.800000026822090100000000000000 | 0.6957228003961247 |
    +----------+--------+----------------------------------+--------------------+
    
    SET
    
    SET @rand:= RAND(), @cum_weight:=0.;
    SELECT entry_id, weight, @cum_weight, @rand 
    FROM entries
    WHERE @rand < @cum_weight:=@cum_weight+weight
    ORDER BY weight DESC
    LIMIT 1;
    
    +----------+--------+----------------------------------+--------------------+
    | entry_id | weight | @cum_weight                      | @rand              |
    +----------+--------+----------------------------------+--------------------+
    |        6 |    0.2 | 0.800000026822090100000000000000 | 0.6957228003961247 |
    +----------+--------+----------------------------------+--------------------+
    
    SELECT *
    FROM entries
    JOIN (
      SELECT entry_id
      FROM entries
      JOIN (SELECT @rand:= RAND(), @cum_weight:=0.) as init
      WHERE @rand < @cum_weight:=@cum_weight+weight
      ORDER BY weight DESC
      LIMIT 1) as rand_entry USING (entry_id);
    
    TABLE intervals:
    
    id  int_start  int_size 
    1   0          95
    7   95         95
    9   190        190
    
    SELECT int_start + int_size AS total_interval 
    FROM intervals 
    WHERE int_start = 
        SELECT MAX(int_start)
        FROM intervals
    
    SELECT id from intervals 
    WHERE int_start = 
        (SELECT MAX(int_start)
        FROM intervals
        WHERE int_start <= :rand_n * :total_interval)
    
    UPDATE intervals 
    SET int_start = int_start - :inst_size_for_deleted_row 
    WHERE int_start >= :int_start_for_deleted_row