关于mysql:来自Sql数据库的简单随机样本

Simple Random Samples from a Sql database

如何在SQL中获取有效的简单随机样本?有关的数据库正在运行MySQL。我的表至少有200,000行,我想要一个大约10,000的简单随机样本。

"显而易见"的答案是:

对于大型表,这太慢了:它对每一行调用RAND()(已经将其放在O(n)),并对它们进行排序,使其充其量为O(n lg n)。有没有办法比O(n)更快地做到这一点?

注意:正如Andrew Mao在注释中指出的那样,如果在SQL Server上使用这种方法,则应使用T-SQL函数NEWID(),因为RAND()可能对所有行返回相同的值。

编辑:5年后

我再次遇到了一个更大的表的问题,最终使用了@ignorant解决方案的一个版本,进行了两次调整:

  • 将行采样到我所需样本大小的2-5倍,以便宜的价格订购RAND()
  • 在每次插入/更新时,将RAND()的结果保存到索引列中。 (如果您的数据集不是很重更新,则可能需要寻找另一种方法来使该列保持最新状态。)

要获取一个表的1000个项目的样本,我对数据行进行计数,并使用Frozen_rand列对结果进行平均采样,平均减少到10,000行:

1
2
3
4
5
6
SELECT COUNT(*) FROM table; -- Use this to determine rand_low and rand_high

  SELECT *
    FROM table
   WHERE frozen_rand BETWEEN %(rand_low)s AND %(rand_high)s
ORDER BY RAND() LIMIT 1000

(我的实际实现涉及更多工作,以确保我不会采样不足,并手动将rand_high包起来,但是基本思想是"将N随机减少到几千。")

尽管这样做有所牺牲,但它允许我使用索引扫描对数据库进行采样,直到足够小以再次进行ORDER BY RAND()。


我认为最快的解决方案是

这就是为什么我认为这应该做的原因。

  • 它将为每一行创建一个随机数。数字介于0和1之间
  • 如果生成的数字在0到.3(30%)之间,它将评估是否显示该行。

假设rand()正在生成均匀分布的数字。这是最快的方法。

我看到有人推荐了该解决方案,但他们却被拒绝,没有证据..这就是我要说的-

  • 这是O(n),但不需要排序,因此它比O(nlg n)快
  • mysql非常有能力为每一行生成随机数。尝试这个 -

    从INFORMATION_SCHEMA.TABLES限制10中选择rand();

由于所讨论的数据库是mySQL,因此这是正确的解决方案。


这里有关于这种类型问题的非常有趣的讨论:http://www.titov.net/2005/09/21/do-not-use-order-by-rand-or-how-to-get-random-行从 - 表/

我认为在没有任何假设的情况下,您的O(n lg n)解决方案是最好的。尽管实际上使用好的优化程序或稍有不同的技术,但您列出的查询可能会更好一些,O(m * n)其中m是所需的随机行数,因为它不必对整个大型数组进行排序,它可能只搜索最小的m次。但是对于您发布的那种数字,无论如何,m大于lg n。

我们可以尝试以下三种假设:

  • 表中有一个唯一的,已索引的主键

  • 您要选择的随机行数(m)比表中的行数(n)小得多

  • 唯一主键是一个整数,范围是1到n,没有空格

  • 仅假设1和2,我认为这可以在O(n)中完成,尽管您需要向表中写入一个完整的索引以匹配假设3,因此不一定需要快速的O(n)。如果我们可以另外假设该表有其他优点,则可以在O(m log m)中执行任务。假设3是一个易于使用的好属性。有了一个很好的随机数生成器,它可以保证在连续生成m个数时不会重复,因此O(m)解决方案是可能的。

    给定这三个假设,基本思想是生成介于1和n之间的m个唯一的随机数,然后从表中选择具有这些键的行。我现在没有mysql或任何更新,所以在伪代码中看起来像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    create table RandomKeys (RandomKey int)
    create table RandomKeysAttempt (RandomKey int)

    -- generate m random keys between 1 and n
    for i = 1 to m
      insert RandomKeysAttempt select rand()*n + 1

    -- eliminate duplicates
    insert RandomKeys select distinct RandomKey from RandomKeysAttempt

    -- as long as we don't have enough, keep generating new keys,
    -- with luck (and m much less than n), this won't be necessary
    while count(RandomKeys) &lt m
      NextAttempt = rand()*n + 1
      if not exists (select * from RandomKeys where RandomKey = NextAttempt)
        insert RandomKeys select NextAttempt

    -- get our random rows
    select *
    from RandomKeys r
    join table t ON r.RandomKey = t.UniqueKey

    如果您真的担心效率,则可以考虑使用某种过程语言来生成随机密钥,并将结果插入数据库中,因为除SQL以外,几乎任何其他方法都可能在所需的循环和随机数生成方面更好。


    比RAND()更快

    我测试了该方法,它比ORDER BY RAND()快得多,因此它运行时间为O(n),而且执行速度如此之快。

    从http://technet.microsoft.com/zh-cn/library/ms189108%28v=sql.105%29.aspx:

    非MSSQL版本-我没有测试

    1
    2
    SELECT * FROM Sales.SalesOrderDetail
    WHERE 0.01 >= RAND()

    MSSQL版本:

    1
    2
    SELECT * FROM Sales.SalesOrderDetail
    WHERE 0.01 >= CAST(CHECKSUM(NEWID(), SalesOrderID) & 0x7fffffff AS float) / CAST (0x7fffffff AS int)

    这将选择?1%的记录。因此,如果需要选择确切的百分比或记录数,请以一定的安全余量估算百分比,然后使用更昂贵的ORDER BY RAND()方法从结果集中随机抽取多余的记录。

    甚至更快

    因为我有一个众所周知的索引列值范围,所以我能够进一步改进此方法。

    例如,如果您的索引列具有均匀分布的整数[0..max],则可以使用该列随机选择N个小间隔。在程序中动态执行此操作,以为每次查询运行获取不同的集合。该子集选择将是O(N),它可以比整个数据集小几个数量级。

    在我的测试中,我将使用ORDER BY RAND()从3分钟获得20条(超过2000万条)样本记录所需的时间减少到0.0秒!


    显然,在某些版本的SQL中,有一个TABLESAMPLE命令,但并非在所有SQL实现中(尤其是Redshift)。

    http://technet.microsoft.com/en-us/library/ms189108(v=sql.105).aspx


    只需使用

    1
    WHERE RAND() < 0.1

    获得10%的记录或

    1
    WHERE RAND() < 0.01

    获得1%的记录,等等。


    我想指出的是,所有这些解决方案似乎都可以提供样品,无需更换。从随机排序中选择前K行,或以随机顺序连接到包含唯一键的表,将生成一个随机样本,无需替换。

    如果要使样品独立,则需要更换样品。有关如何以类似于user12861解决方案的方式使用JOIN进行此操作的示例,请参见问题25451034。该解决方案是为T-SQL编写的,但该概念可在任何SQL数据库中使用。


    如果确实需要m行,实际上,您将在SQL之外生成ID的子集。大多数方法在某些时候都需要选择" nth"条目,而SQL表实际上根本不是数组。假设键是连续的以便仅加入1和2之间的随机整数,这也很难满足MySQL,例如,它本身不支持它,并且锁定条件很棘手。

    这是一个O(max(n, m lg n))-时间,O(n)-空间解决方案,假定仅使用简单的BTREE密钥:

  • 以您喜欢的脚本语言(O(n))以任何顺序将数据表的键列的所有值提取到数组中。
  • 执行Fisher-Yates随机播放,在m交换后停止,并在?(m)中提取子数组[0:m-1]
  • O(m lg n)中的原始数据集(例如SELECT ... WHERE id IN ())"加入"子数组
  • 任何在SQL外部生成随机子集的方法都必须至少具有这种复杂性。联接不能比使用BTREE的O(m lg n)快(因此O(m)声明对于大多数引擎来说都是幻想),并且混洗限制在nm lg n以下,并且不影响渐近行为。

    在Pythonic伪代码中:

    1
    2
    3
    4
    5
    6
    ids = sql.query('SELECT id FROM t')
    for i in range(m):
      r = int(random() * (len(ids) - i))
      ids[i], ids[i + r] = ids[i + r], ids[i]

    results = sql.query('SELECT * FROM t WHERE id IN (%s)' % ', '.join(ids[0:m-1])

    从观察到我们可以基于一个集合检索表的ID(例如计数5)开始:

    1
    2
    3
    select *
    from table_name
    where _id in (4, 1, 2, 5, 3)

    我们可以得出的结果是,如果我们可以生成字符串"(4, 1, 2, 5, 3)",那么我们将拥有比RAND()更有效的方式。

    例如,在Java中:

    1
    2
    3
    4
    5
    6
    ArrayList<Integer> indices = new ArrayList<Integer>(rowsCount);
    for (int i = 0; i < rowsCount; i++) {
        indices.add(i);
    }
    Collections.shuffle(indices);
    String inClause = indices.toString().replace('[', '(').replace(']', ')');

    如果id之间有间隔,则初始arraylist indices是对id进行sql查询的结果。


    也许你可以做

    1
    SELECT * FROM table LIMIT 10000 OFFSET FLOOR(RAND() * 190000)