来自Sql数据库的简单随机样本


93

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

“显而易见”的答案是:

SELECT * FROM table ORDER BY RAND() LIMIT 10000

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

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

编辑:5年后

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

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

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

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


3
这甚至在SQL Server中也不起作用,因为RAND()在每个后续调用中都返回相同的值。
安德鲁·毛

1
好点-我将添加一条注释,即SQL Server用户应改用ORDER BY NEWID()。
ojrac 2012年

它仍然非常低效,因为它必须对所有数据进行排序。某种程度上采用随机抽样技术会更好,但是即使阅读了此处的大量文章,我仍然没有找到足够随机的可接受解决方案。
安德鲁·毛

如果您读了这个问题,我是在特别问,因为ORDER BY RAND()是O(n lg n)。
ojrac 2012年

如果您对RAND()的统计随机性不太痴迷,那么muposat的答案很好。
乔什·格里弗

Answers:


25

这里有一个关于此类问题的非常有趣的讨论: http://www.titov.net/2005/09/21/do-not-use-order-by-rand-or-how-to-get-random-rows-from-table/

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

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

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

  2. 您要选择的随机行数(m)远小于表中的行数(n)

  3. 唯一主键是一个介于1到n之间且没有空格的整数

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

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


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) < 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以外,几乎所有其他方法都可能在所需的循环和随机数生成方面更好。


我建议在随机键选择上添加一个唯一索引,或者忽略插入内容上的重复项,这样您就可以摆脱不同的东西,并且连接会更快。
山姆番红花

我认为随机数算法可能会进行一些调整-如所述的UNIQUE约束,或者仅生成2 * m个数字,然后选择DISTINCT,ORDER BY id(先到先服务,因此这减少为UNIQUE约束)LIMIT m。我喜欢。
ojrac

至于在随机键选择中添加唯一索引,然后忽略插入时的重复项,我认为这可以使您回到O(m ^ 2)的行为,而不是O(m lg m)。不确定一次插入一个随机行时服务器维护索引的效率如何。
2008年

至于生成2 * m数的建议,我想要一种无论如何都能保证工作的算法。您的2 * m个随机数总是(微小)机会具有m个重复项,因此您的查询将不够用。
2008年

1
如何获得表中的行数?
Awesome-o

53

我认为最快的解决方案是

select * from table where rand() <= .3

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

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

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

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

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

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

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


1
首先,您有一个问题,那就是它不能真正回答问题,因为它返回的结果是半随机数,接近所需的数目,但不一定精确地等于该数目,而不是确切的所需数目。
user12861 2013年

1
接下来,关于效率,您的效率为O(n),其中n是表中的行数。这几乎不如O(m log m)好,其中m是您想要的结果数,而m << n。您仍然可能在实践中会更快,这是正确的,因为正如您所说的那样,生成rand()并将它们与常量进行比较会非常快。您必须对其进行测试才能找到答案。如果桌子较小,您可能会赢。巨大的表和更少的预期结果我对此表示怀疑。
user12861 2013年

1
尽管@ user12861正确地获得了正确的数字是正确的,但这是将数据集缩减为正确的粗略大小的一种好方法。
ojrac

1
数据库如何为以下查询服务SELECT * FROM table ORDER BY RAND() LIMIT 10000 ?它必须首先为每行创建一个随机数(与我描述的解决方案相同),然后对其进行排序。排序非常昂贵!这就是为什么此解决方案将比我描述的解决方案慢的原因,因为不需要任何排序。您可以为我描述的解决方案添加一个限制,它不会给您更多的行数。正如某人正确指出的那样,它不会给您确切的样本量,但是对于随机样本,EXACT通常不是严格的要求。
无知

有没有办法指定最小行数?
CMCDragonkai 2014年


4

比RAND()更快

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

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

非MSSQL版本-我没有测试

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

MSSQL版本:

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秒


3

只需使用

WHERE RAND() < 0.1 

获得10%的记录或

WHERE RAND() < 0.01 

获得1%的记录,等等。


1
这将为每一行调用RAND,使其变为O(n)。海报正在寻找比这更好的东西。
user12861

1
不仅如此,而且RAND()为后续调用返回相同的值(至少在MSSQL上),这意味着您将获得整个表,也可能不会获得整个表。
安德鲁·毛

1

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

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


0

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

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

我们可以得出这样的结果:如果我们可以生成字符串"(4, 1, 2, 5, 3)",那么我们将有比RAND()

例如,在Java中:

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之间有间隔,则初始arraylistindices是对id进行sql查询的结果。


0

如果m确实需要行,那么实际上,您将在SQL之外生成ID的子集。大多数方法都需要选择“ nth”条目,而SQL表实际上根本不是数组。假设键是连续的以便仅将1和计数之间的随机整数连接起来,这也很难满足–例如,MySQL本机不支持它,并且锁定条件非常棘手。

这是一个仅使用简单BTREE密钥的O(max(n, m lg n))-time,O(n)-space解决方案:

  1. 以您喜欢的脚本语言以任何顺序将数据表的键列的所有值提取到数组中 O(n)
  2. 执行费雪耶茨洗牌,停药后m互换,并提取子阵[0:m-1]ϴ(m)
  3. 将子数组与原始数据集(例如SELECT ... WHERE id IN (<subarray>))“连接”到O(m lg n)

任何在SQL外部生成随机子集的方法都必须至少具有这种复杂性。O(m lg n)联接的速度不会比BTREE快(因此,O(m)对于大多数引擎而言都是幻想),并且改组限制在下面nm lg n并且不影响渐近行为。

在Pythonic伪代码中:

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

0

在Netezza中选择3000条随机记录:

WITH IDS AS (
     SELECT ID
     FROM MYTABLE;
)

SELECT ID FROM IDS ORDER BY mt_random() LIMIT 3000

除了添加一些特定于SQL方言的注释外,我认为这没有回答如何在没有'ORDER BY rand()LIMIT $ 1'的情况下查询随机行样本的问题。
ojrac

0

尝试

SELECT TOP 10000 * FROM table ORDER BY NEWID()

这样做不会太复杂吗?


请注意,这NEWID()特定于T-SQL。
彼得·O。

我很抱歉。它是。谢谢然而,了解是否有人像我一样以一种更好的方式来到这里看起来很有用,并且正在使用T-SQL
Northernlad

ORDER BY NEWID()在功能上与ORDER BY RAND()-调用RAND()集合中的每一行-O(n)-然后对整个对象进行排序-O(n lg n)。换句话说,这是该问题希望改进的最坏情况的解决方案。
ojrac

0

在某些方言中,例如Microsoft SQL Server,PostgreSQL和Oracle(但不是MySQL或SQLite),您可以执行以下操作

select distinct top 10000 customer_id from nielsen.dbo.customer TABLESAMPLE (20000 rows) REPEATABLE (123);

不只是做的原因 (10000 rows)没有的top在于TABLESAMPLE逻辑给你,所以你要过采样和选择的确切数字所需的行极不精确的数字(即,有时1.25%倍,像有时75%)。的REPEATABLE (123)是,用于提供一个随机种子。


-4

也许你可以做

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

1
看起来这将选择我的数据的随机切片;我正在寻找更复杂的东西-10,000个随机分布的行。
ojrac

然后,如果要在数据库中执行此操作,则唯一的选择是ORDER BY rand()。
staticsan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.