从SQL Server表中随机选择n行

370
我有一个包含约50,000行的SQL Server表格。我想随机选择其中的大约5,000行。我曾考虑过一种复杂的方法,创建一个带有“随机数”列的临时表,将我的表格复制到其中,循环遍历临时表并使用RAND()更新每行,然后从该表格中选择随机数列<0.1的行。如果可能的话,我正在寻找更简单的方法,在单个语句中完成。

这篇文章建议使用NEWID()函数。这看起来很有前途,但我不知道如何可靠地选择一定比例的行。

有人以前做过这件事吗?有什么想法吗?


4
MSDN有一篇很好的文章涵盖了很多这些问题:从大表中随机选择行 - KyleMit
可能是如何在SQL中请求随机行?的重复问题。 - Moslem Ben Dhaou
18个回答

458
select top 10 percent * from [yourtable] order by newid()

回应有关大型表格的“纯垃圾”评论:您可以像这样改进性能。

select  * from [yourtable] where [yourPk] in 
(select top 10 percent [yourPk] from [yourtable] order by newid())
这将会消耗掉键值扫描和连接的成本,但在选择百分比较小的大表上应该是合理的。

21
请记住,newid()不是一个真正好的伪随机数生成器,至少不像rand()那样好。但是,如果你只需要一些模糊的随机样本,并且不在意其数学质量等方面,那么它就足够了。否则你需要查看:https://dev59.com/s3VC5IYBdhLWcg3wliGe - user12861
5
newid - guid被设计为唯一而不是随机的.. 错误的方法 - Brans Ds
3
如果数据行数很大,例如超过一百万条,并且使用了newid()进行排序,那么预估的I/O成本将会非常高,并影响性能。 - aadi1295
2
关于在大表上使用NEWID()的成本的评论不是“纯粹的垃圾”。它甚至在官方的Microsoft Doc中提到https://learn.microsoft.com/en-us/previous-versions/software-testing/cc441928(v=msdn.10)?redirectedfrom=MSDN。 “ORDER BY子句导致将表中的所有行复制到tempdb数据库中,然后在其中进行排序。” RJardines发布的答案对此进行了扩展。 - pedram bashiri
1
@BransDs GUID作为一个整体被设计成唯一的,但NEWID()实际上是一个随机数。它是一个v4 UUID,你可以通过第三组的开头始终是4来判断。在v4 UUID中,除了第三组的开头(始终为4)和第四组的开头(始终为8、9、A或B,因为这是版本变体)外,所有其他位都是完全随机的数字。v4 UUID只是花哨的 ~122 位随机数。就是这样。SQL Server文档表示NEWID()符合RFC4122标准,并且RFC4122明确说明了其随机性质:https://tools.ietf.org/html/rfc4122#section-4.4 - Bacon Bits
显示剩余2条评论

106
根据您的需求,TABLESAMPLE 可以帮助您获得几乎随机且性能更好的数据。该功能适用于 MS SQL Server 2005 及以上版本。 TABLESAMPLE 将从随机页面返回数据,而不是从随机行返回数据,因此它甚至不会检索它不会返回的数据。
我在一个非常大的表上进行了测试。
select top 1 percent * from [tablename] order by newid()

花费了超过20分钟的时间。

select * from [tablename] tablesample(1 percent)

花了2分钟。

TABLESAMPLE对较小的样本表现也会有所提高,而使用newid()则不会。

请记住,这种方法的随机性不如newid(),但可以给你一个不错的样本。

请参阅MSDN页面


7
正如Rob Boek所指出的,表格抽样会使结果聚集,因此不是获取少量随机结果的好方法。 - Oskar Austegard
你是否在关心这个问题:select top 1 percent * from [tablename] order by newid(),因为newid()不是[tablename]中的列。SQL Server是否会在每一行内部附加newid()列,然后进行排序? - FrenkyB
1
表抽样对我来说是最好的答案,因为我在一个非常大的表上进行了复杂的查询。毫无疑问,它的速度非常快。我运行多次后返回的记录数有所变化,但所有结果都在可接受的误差范围内。 - jessier3
@FrenkyB 是的,基本上是这样。SQL Server将为整个表中的每一行生成一个GUID,然后对结果集进行排序。它可能具有优化的排序算法,在达到1%阈值时进行短路,但在开始排序之前仍必须为表中的每一行生成GUID。否则,将得到偏倚的样本。对于非常大的表,这意味着SQL Server将使用临时表来进行排序。 - Bacon Bits

49

使用newid() / order by可以工作,但对于大结果集来说会非常昂贵,因为它必须为每一行生成一个id,然后对它们进行排序。

TABLESAMPLE()在性能方面很好,但您将获得结果的聚类(页面上的所有行都将返回)。

对于更好的随机抽样性能,最好的方法是随机筛选行。我在SQL Server Books Online文章使用TABLESAMPLE限制结果集中找到了以下代码示例:

If you really want a random sample of individual rows, modify your query to filter out rows randomly, instead of using TABLESAMPLE. For example, the following query uses the NEWID function to return approximately one percent of the rows of the Sales.SalesOrderDetail table:

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

The SalesOrderID column is included in the CHECKSUM expression so that NEWID() evaluates once per row to achieve sampling on a per-row basis. The expression CAST(CHECKSUM(NEWID(), SalesOrderID) & 0x7fffffff AS float / CAST (0x7fffffff AS int) evaluates to a random float value between 0 and 1.

当对包含1,000,000行的表进行运行时,以下是我的结果:

SET STATISTICS TIME ON
SET STATISTICS IO ON

/* newid()
   rows returned: 10000
   logical reads: 3359
   CPU time: 3312 ms
   elapsed time = 3359 ms
*/
SELECT TOP 1 PERCENT Number
FROM Numbers
ORDER BY newid()

/* TABLESAMPLE
   rows returned: 9269 (varies)
   logical reads: 32
   CPU time: 0 ms
   elapsed time: 5 ms
*/
SELECT Number
FROM Numbers
TABLESAMPLE (1 PERCENT)

/* Filter
   rows returned: 9994 (varies)
   logical reads: 3359
   CPU time: 641 ms
   elapsed time: 627 ms
*/    
SELECT Number
FROM Numbers
WHERE 0.01 >= CAST(CHECKSUM(NEWID(), Number) & 0x7fffffff AS float) 
              / CAST (0x7fffffff AS int)

SET STATISTICS IO OFF
SET STATISTICS TIME OFF

如果可以使用TABLESAMPLE,它将提供最佳性能。否则,请使用newid() / filter方法。如果结果集很大,则应该将newid() / order by作为最后一种选择。

我也看到了那篇文章,并在我的代码中尝试了一下,似乎NewID()只被评估了一次,而不是每一行都评估,这让我不太喜欢... - Andrew Mao

34

在MSDN上,选择大表的随机行有一个简单、清晰的解决方案,能够解决大规模性能问题。

  SELECT * FROM Table1
  WHERE (ABS(CAST(
  (BINARY_CHECKSUM(*) *
  RAND()) as int)) % 100) < 10

1
非常有趣。阅读完这篇文章后,我并不真正理解为什么 RAND() 不会为每一行返回相同的值(这将破坏 BINARY_CHECKSUM() 的逻辑)。是因为它被调用在另一个函数内而不是作为 SELECT 子句的一部分吗? - John M Gant
3
我在一个有35个记录的表格上运行了这个查询,结果集中经常会出现其中两个。这可能是由于 rand() 或上述两者的组合造成的问题,但我因此放弃了这个解决方案。此外,结果数量从1到5不等,因此在某些情况下可能也不可接受。 - Oliver
RAND()函数不会为每一行返回相同的值吗? - Sarsaparilla
2
RAND() 返回每一行相同的值(这就是为什么这个解决方案很快的原因)。然而,二进制校验和非常接近的行存在生成类似校验和结果的高风险,当 RAND() 很小时会导致聚集。例如,(ABS(CAST((BINARY_CHECKSUM(111,null,null) * 0.1) as int))) % 100 == SELECT (ABS(CAST((BINARY_CHECKSUM(113,null,null) * 0.1) as int))) % 100。如果您的数据遇到此问题,请将 BINARY_CHECKSUM 乘以 9923。 - Brian
1
我随意选择了9923。然而,我希望它是质数(虽然与100互质可能已经足够)。此外,只要RAND()不是非常小,9923就足够大以分散聚集。 - Brian
显示剩余2条评论

14

这个链接提供了一个有趣的比较,比较了使用Orderby(NEWID())和其他方法在包含100万、700万和1300万行的表中进行随机选取行的效果。

常常在讨论组中有人问如何选择随机行,而NEWID查询通常会被提出来;它非常简单且对于小表格来说效果非常好。

SELECT TOP 10 PERCENT *
  FROM Table1
  ORDER BY NEWID()

然而,当你在大表中使用NEWID查询时,它有一个很大的缺点。ORDER BY子句会导致表中所有行被复制到tempdb数据库中进行排序。这会带来两个问题:

  1. 排序操作通常会产生高昂的成本。排序可能会使用大量磁盘I/O,并且可能运行很长时间。
  2. 在最坏的情况下,tempdb可能会耗尽空间。在最好的情况下,tempdb可能会占用大量磁盘空间,而不使用手动收缩命令永远无法回收。

你需要的是一种选择随机行的方法,它不会使用tempdb,并且随着表变得越来越大,速度也不会变慢。以下是如何做到这一点的新想法:

SELECT * FROM Table1
  WHERE (ABS(CAST(
  (BINARY_CHECKSUM(*) *
  RAND()) as int)) % 100) < 10
这个查询的基本思想是针对表中的每一行生成 0 到 99 之间的随机数,然后选择所有随机数小于指定百分比值的行。在这个例子中,我们希望随机选取约 10% 的行;因此,我们选择所有随机数小于 10 的行。
请在MSDN中阅读完整的文章。

12

通过随机数对表进行排序,并使用TOP获取前5000行。

SELECT TOP 5000 * FROM [Table] ORDER BY newid();

更新

刚试了一下,调用newid()即可 - 不需要进行所有强制类型转换和数学运算。


13
使用“所有角色和所有算法”的原因是为了更好的性能表现。 - hkf

12

如果你(不同于提问者)需要特定数量的记录(这使得使用CHECKSUM方法很困难),并且希望比TABLESAMPLE本身提供的更随机的样本,并且想要比CHECKSUM更快的速度,那么可以将TABLESAMPLE和NEWID()方法结合起来,例如:

DECLARE @sampleCount int = 50
SET STATISTICS TIME ON

SELECT TOP (@sampleCount) * 
FROM [yourtable] TABLESAMPLE(10 PERCENT)
ORDER BY NEWID()

SET STATISTICS TIME OFF
在我的情况下,这是最简单的平衡随机性(我知道实际上并不是)和速度的方法。根据需要变化 TABLESAMPLE 的百分比(或行数)- 百分比越高,样本就越随机,但速度会线性下降。(请注意,TABLESAMPLE 不接受变量)。

11
这是一个由初始种子想法和校验和组合而成的方法,看起来可以给出适当的随机结果,而不需要使用NEWID()带来的开销:
SELECT TOP [number] 
FROM table_name
ORDER BY RAND(CHECKSUM(*) * RAND())

请注意,如果任何列的类型为'xml',则CHECKSUM(*)无效。 - undefined

6

在MySQL中,您可以这样做:

SELECT `PRIMARY_KEY`, rand() FROM table ORDER BY rand() LIMIT 5000;

3
这不会起作用。由于选择语句是原子的,它只获取一个随机数并为每行重复它。你必须在每一行上重新播种它才能强制它改变。 - Tom H
4
嗯...喜欢供应商的差异。在MySQL中,SELECT是原子操作,但我想方式可能不同。这将适用于MySQL。 - Jeff Ferland
ORDER BY rand() 对我有用。 - Shuhad zaman

4

我还没有看到类似这样的答案变化。我有一个额外的限制,需要在给定初始种子的情况下,每次选择相同的一组行。

对于 MS SQL:

最小示例:

select top 10 percent *
from table_name
order by rand(checksum(*))

执行时间归一化: 1.00

NewId()示例:

select top 10 percent *
from table_name
order by newid()

标准化执行时间: 1.02

NewId() 的执行速度比 rand(checksum(*)) 稍慢,因此可能不适用于大型记录集。

初始种子选择:

declare @seed int
set @seed = Year(getdate()) * month(getdate()) /* any other initial seed here */

select top 10 percent *
from table_name
order by rand(checksum(*) % @seed) /* any other math function here */

如果您需要根据一个种子选择相同的集合,似乎可以使用这个方法。

使用特殊的@seed与RAND()相比,有什么优势吗? - QMaster
绝对,您使用了种子参数并通过日期参数填充它,RAND()函数执行相同的操作,只是使用完整的时间值,我想知道使用像上面的手动创建的参数(如种子)是否有任何优势,而不是使用RAND()函数? - QMaster
啊!好的,这是项目的要求。我需要以确定性的方式生成n个随机行的列表。基本上领导想知道我们在几天前选择和处理行时会选择哪些“随机”行。通过根据年/月建立种子值,我可以保证该年对查询的任何调用都将返回相同的“随机”列表。我知道,这很奇怪,可能有更好的方法,但它起作用了... - klyd
哈哈 :) 我明白了,但我认为随机选择记录的一般意义并不是在不同运行查询中相同的记录。 - QMaster
请注意,如果任何列的类型为'xml',则CHECKSUM(*)无效。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接