如何在纯 SQL 中请求随机行(或尽可能接近真正随机)?
30 回答
请参阅这篇文章:SQL to Select a random row from a database table。它通过在 MySQL、PostgreSQL、Microsoft SQL Server、IBM DB2 和 Oracle 中执行此操作的方法(以下是从该链接复制的):
使用 MySQL 选择一个随机行:
SELECT column FROM table
ORDER BY RAND()
LIMIT 1
使用 PostgreSQL 选择一个随机行:
SELECT column FROM table
ORDER BY RANDOM()
LIMIT 1
使用 Microsoft SQL Server 选择随机行:
SELECT TOP 1 column FROM table
ORDER BY NEWID()
使用 IBM DB2 随机选择一行
SELECT column, RAND() as IDX
FROM table
ORDER BY IDX FETCH FIRST 1 ROWS ONLY
使用 Oracle 选择一条随机记录:
SELECT column FROM
( SELECT column FROM table
ORDER BY dbms_random.value )
WHERE rownum = 1
像 Jeremies 这样的解决方案:
SELECT * FROM table ORDER BY RAND() LIMIT 1
工作,但他们需要对所有表进行顺序扫描(因为需要计算与每一行关联的随机值 - 以便可以确定最小的值),即使是中等大小的表也可能非常慢。我的建议是使用某种索引数字列(许多表都将这些作为主键),然后编写如下内容:
SELECT * FROM table WHERE num_value >= RAND() *
( SELECT MAX (num_value ) FROM table )
ORDER BY num_value LIMIT 1
这在对数时间内有效,无论表大小如何,如果num_value
被索引。一个警告:这假设num_value
在 range 中均匀分布0..MAX(num_value)
。如果您的数据集严重偏离此假设,您将得到偏斜的结果(某些行会比其他行更频繁地出现)。
我不知道这有多有效,但我以前用过:
SELECT TOP 1 * FROM MyTable ORDER BY newid()
因为 GUID 是非常随机的,所以排序意味着你得到一个随机的行。
ORDER BY NEWID()
需要7.4 milliseconds
WHERE num_value >= RAND() * (SELECT MAX(num_value) FROM table)
需要0.0065 milliseconds
!
我肯定会采用后一种方法。
你没有说你使用的是哪个服务器。在旧版本的 SQL Server 中,您可以使用以下命令:
select top 1 * from mytable order by newid()
在 SQL Server 2005 及更高版本中,您可以使用TABLESAMPLE
来获取可重复的随机样本:
SELECT FirstName, LastName
FROM Contact
TABLESAMPLE (1 ROWS) ;
对于 SQL 服务器
newid()/order by 会起作用,但对于大型结果集来说会非常昂贵,因为它必须为每一行生成一个 id,然后对它们进行排序。
TABLESAMPLE() 从性能的角度来看是好的,但你会得到结块的结果(将返回页面上的所有行)。
为了获得更好的真实随机样本,最好的方法是随机过滤掉行。我在 SQL Server 联机丛书文章使用 TABLESAMPLE 限制结果集中找到了以下代码示例:
如果您真的想要单个行的随机样本,请修改您的查询以随机过滤掉行,而不是使用 TABLESAMPLE。例如,以下查询使用 NEWID 函数返回 Sales.SalesOrderDetail 表中大约百分之一的行:
SELECT * FROM Sales.SalesOrderDetail WHERE 0.01 >= CAST(CHECKSUM(NEWID(),SalesOrderID) & 0x7fffffff AS float) / CAST (0x7fffffff AS int)
SalesOrderID 列包含在 CHECKSUM 表达式中,以便 NEWID() 每行计算一次,以实现逐行抽样。表达式 CAST(CHECKSUM(NEWID(), SalesOrderID) & 0x7fffffff AS float / CAST (0x7fffffff AS int) 计算为 0 到 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 应该是最后的手段。
如果可能,使用存储语句来避免 RND() 上的两个索引效率低下并创建记录号字段。
PREPARE RandomRecord FROM "SELECT * FROM table LIMIT ?,1"; SET @n=FLOOR(RAND()*(SELECT COUNT(*) FROM table)); 使用@n 执行随机记录;
最好的方法是为此目的在一个新列中放置一个随机值,并使用类似这样的东西(伪代码+ SQL):
randomNo = random()
execSql("SELECT TOP 1 * FROM MyTable WHERE MyTable.Randomness > $randomNo")
这是 MediaWiki 代码采用的解决方案。当然,对于较小的值存在一些偏差,但他们发现,当没有获取任何行时,将随机值包装为零就足够了。
newid() 解决方案可能需要全表扫描,以便可以为每一行分配一个新的 guid,这将大大降低性能。
rand() 解决方案可能根本不起作用(即使用 MSSQL),因为该函数只会被评估一次,并且每一行都将被分配相同的“随机”数字。
对于 SQL Server 2005 和 2008,如果我们想要单个行的随机样本(来自联机丛书):
SELECT * FROM Sales.SalesOrderDetail
WHERE 0.01 >= CAST(CHECKSUM(NEWID(), SalesOrderID) & 0x7fffffff AS float)
/ CAST (0x7fffffff AS int)
迟了,但通过谷歌来到这里,所以为了后代,我会添加一个替代解决方案。
另一种方法是使用 TOP 两次,顺序交替。我不知道它是否是“纯 SQL”,因为它在 TOP 中使用了一个变量,但它在 SQL Server 2008 中有效。如果我想要一个随机词,这是我对字典词表使用的示例。
SELECT TOP 1
word
FROM (
SELECT TOP(@idx)
word
FROM
dbo.DictionaryAbridged WITH(NOLOCK)
ORDER BY
word DESC
) AS D
ORDER BY
word ASC
当然,@idx 是一些随机生成的整数,范围从 1 到目标表上的 COUNT(*),包括在内。如果您的列已编入索引,您也会从中受益。另一个优点是您可以在函数中使用它,因为 NEWID() 是不允许的。
最后,上述查询在同一张表上的 NEWID() 类型查询的执行时间大约为 1/10。YYMV。
由于不鼓励使用 RAND(),您可以简单地获得最大 ID (=Max):
SELECT MAX(ID) FROM TABLE;
获得 1..Max (=My_Generated_Random) 之间的随机数
My_Generated_Random = rand_in_your_programming_lang_function(1..Max);
然后运行这个 SQL:
SELECT ID FROM TABLE WHERE ID >= My_Generated_Random ORDER BY ID LIMIT 1
请注意,它将检查 ID 等于或高于所选值的任何行。也可以在表中寻找向下的行,并获得一个等于或低于 My_Generated_Random 的 ID,然后像这样修改查询:
SELECT ID FROM TABLE WHERE ID <= My_Generated_Random ORDER BY ID DESC LIMIT 1
正如@BillKarwin 对@cnu 回答的评论中所指出的......
当与 LIMIT 结合使用时,我发现使用随机排序而不是直接排序实际行,它的性能要好得多(至少在 PostgreSQL 9.1 中):例如
SELECT * FROM tbl_post AS t
JOIN ...
JOIN ( SELECT id, CAST(-2147483648 * RANDOM() AS integer) AS rand
FROM tbl_post
WHERE create_time >= 1349928000
) r ON r.id = t.id
WHERE create_time >= 1349928000 AND ...
ORDER BY r.rand
LIMIT 100
只需确保“r”为与之连接的复杂查询中的每个可能的键值生成一个“rand”值,但仍尽可能限制“r”的行数。
CAST as Integer 对于 PostgreSQL 9.2 特别有用,它对整数和单精度浮点类型进行了特定的排序优化。
MySQL获取随机记录
SELECT name
FROM random AS r1 JOIN
(SELECT (RAND() *
(SELECT MAX(id)
FROM random)) AS id)
AS r2
WHERE r1.id >= r2.id
ORDER BY r1.id ASC
LIMIT 1
使用 SQL Server 2012+,您可以使用OFFSET FETCH 查询对单个随机行执行此操作
select * from MyTable ORDER BY id OFFSET n ROW FETCH NEXT 1 ROWS ONLY
其中 id 是标识列,n 是您想要的行 - 计算为表的 0 和 count()-1 之间的随机数(偏移量 0 毕竟是第一行)
这适用于表数据中的漏洞,只要您有用于 ORDER BY 子句的索引即可。它也非常适合随机性 - 当您自己计算出来以传递但其他方法中的琐事不存在时。此外,性能非常好,在较小的数据集上它表现得很好,尽管我没有尝试过对几百万行进行严格的性能测试。
对于 SQL Server 并且需要“单个随机行”..
如果不需要真正的采样,请生成一个随机值[0, max_rows)
并使用SQL Server 2012+ 中的 ORDER BY..OFFSET..FETCH。
如果和超过适当的索引,这是非常快的- 这样数据就已经沿着查询行“排序”了。如果涵盖了这些操作,这是一个快速请求,并且不会受到使用或类似的可怕可扩展性的影响。显然,这种方法在非索引 HEAP 表上不能很好地扩展。COUNT
ORDER BY
ORDER BY NEWID()
declare @rows int
select @rows = count(1) from t
-- Other issues if row counts in the bigint range..
-- This is also not 'true random', although such is likely not required.
declare @skip int = convert(int, @rows * rand())
select t.*
from t
order by t.id -- Make sure this is clustered PK or IX/UCL axis!
offset (@skip) rows
fetch first 1 row only
确保使用适当的事务隔离级别和/或考虑 0 结果。
对于 SQL Server 并需要“通用行示例”方法..
注意:这是对 SQL Server specific question about fetching a sample of rows的答案的改编。它是根据上下文量身定制的。
尽管此处应谨慎使用一般采样方法,但在其他答案的上下文中它仍然是潜在有用的信息(以及非缩放和/或有问题的实现的重复建议)。这种采样方法比显示的第一个代码效率低,并且如果目标是找到“单个随机行”,则容易出错。
这是对行百分比进行采样的更新和改进形式。它基于使用 CHECKSUM / BINARY_CHECKSUM 和模数的其他一些答案的相同概念。
它在庞大的数据集上相对较快,并且可以有效地用于/与派生查询一起使用。无需使用 tempdb即可在几秒钟内对数百万个预先过滤的行进行采样,如果与查询的其余部分保持一致,则开销通常很小。
不受数据运行
CHECKSUM(*)
/BINARY_CHECKSUM(*)
问题的影响。使用该CHECKSUM(*)
方法时,可以“块”而不是“随机”选择行!这是因为CHECKSUM 更喜欢速度而不是分布。产生稳定/可重复的行选择,并且可以简单地更改以在后续查询执行中生成不同的行。使用的
NEWID()
方法永远不会稳定/可重复。不使用
ORDER BY NEWID()
整个输入集,因为排序可能成为大型输入集的重要瓶颈。避免不必要的排序还可以减少内存和 tempdb 的使用。不使用
TABLESAMPLE
,因此与WHERE
预过滤器一起使用。
这是要点。有关其他详细信息和说明,请参阅此答案。
天真的尝试:
declare @sample_percent decimal(7, 4)
-- Looking at this value should be an indicator of why a
-- general sampling approach can be error-prone to select 1 row.
select @sample_percent = 100.0 / count(1) from t
-- BAD!
-- When choosing appropriate sample percent of "approximately 1 row"
-- it is very reasonable to expect 0 rows, which definitely fails the ask!
-- If choosing a larger sample size the distribution is heavily skewed forward,
-- and is very much NOT 'true random'.
select top 1
t.*
from t
where 1=1
and ( -- sample
@sample_percent = 100
or abs(
convert(bigint, hashbytes('SHA1', convert(varbinary(32), t.rowguid)))
) % (1000 * 100) < (1000 * @sample_percent)
)
这可以通过混合查询,通过混合采样和ORDER BY
从小得多的样本集中进行选择,在很大程度上得到补救。这将排序操作限制为样本大小,而不是原始表的大小。
-- Sample "approximately 1000 rows" from the table,
-- dealing with some edge-cases.
declare @rows int
select @rows = count(1) from t
declare @sample_size int = 1000
declare @sample_percent decimal(7, 4) = case
when @rows <= 1000 then 100 -- not enough rows
when (100.0 * @sample_size / @rows) < 0.0001 then 0.0001 -- min sample percent
else 100.0 * @sample_size / @rows -- everything else
end
-- There is a statistical "guarantee" of having sampled a limited-yet-non-zero number of rows.
-- The limited rows are then sorted randomly before the first is selected.
select top 1
t.*
from t
where 1=1
and ( -- sample
@sample_percent = 100
or abs(
convert(bigint, hashbytes('SHA1', convert(varbinary(32), t.rowguid)))
) % (1000 * 100) < (1000 * @sample_percent)
)
-- ONLY the sampled rows are ordered, which improves scalability.
order by newid()
这里的大多数解决方案旨在避免排序,但它们仍然需要对表进行顺序扫描。
还有一种方法可以通过切换到索引扫描来避免顺序扫描。如果您知道随机行的索引值,您几乎可以立即获得结果。问题是 - 如何猜测索引值。
以下解决方案适用于 PostgreSQL 8.4:
explain analyze select * from cms_refs where rec_id in
(select (random()*(select last_value from cms_refs_rec_id_seq))::bigint
from generate_series(1,10))
limit 1;
我在上面的解决方案中,您猜测 0 .. [id 的最后一个值] 范围内的 10 个不同的随机索引值。
数字 10 是任意的 - 您可以使用 100 或 1000,因为它(令人惊讶地)对响应时间没有太大影响。
还有一个问题 - 如果您的 id 稀疏,您可能会错过。解决方案是有一个备份计划:) 在这种情况下,通过 random() 查询的纯旧订单。当组合 id 看起来像这样:
explain analyze select * from cms_refs where rec_id in
(select (random()*(select last_value from cms_refs_rec_id_seq))::bigint
from generate_series(1,10))
union all (select * from cms_refs order by random() limit 1)
limit 1;
不是联合 ALL子句。在这种情况下,如果第一部分返回任何数据,则第二部分永远不会执行!
您也可以尝试使用new id()
函数。
只需编写您的查询并按new id()
功能使用顺序。很随意。
还没有完全看到答案中的这种变化。在给定初始种子的情况下,我需要一个额外的约束来每次选择相同的行集。
对于 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 */
如果您需要在给定种子的情况下选择相同的集合,这似乎可行。
在 MSSQL(在 11.0.5569 上测试)使用
SELECT TOP 100 * FROM employee ORDER BY CRYPT_GEN_RANDOM(10)
明显快于
SELECT TOP 100 * FROM employee ORDER BY NEWID()
对于火鸟:
Select FIRST 1 column from table ORDER BY RAND()
在 SQL Server 中,您可以将 TABLESAMPLE 与 NEWID() 结合使用以获得相当好的随机性并且仍然具有速度。如果您真的只需要 1 行或少量行,这将特别有用。
SELECT TOP 1 * FROM [table]
TABLESAMPLE (500 ROWS)
ORDER BY NEWID()
SELECT * FROM table ORDER BY RAND() LIMIT 1
我必须同意 CD-MaN:使用“ORDER BY RAND()”对于小桌子或只执行几次 SELECT 时效果很好。
我还使用“num_value >= RAND() * ...”技术,如果我真的想要随机结果,我在表中有一个特殊的“随机”列,我每天更新一次左右。单次 UPDATE 运行将需要一些时间(尤其是因为您必须在该列上有一个索引),但它比每次运行选择时为每一行创建随机数要快得多。
请小心,因为 TableSample 实际上并不返回随机的行样本。它引导您的查询查看构成您的行的 8KB 页面的随机样本。然后,您的查询将针对这些页面中包含的数据执行。由于数据在这些页面上的分组方式(插入顺序等),这可能导致数据实际上不是随机样本。
见:http ://www.mssqltips.com/tip.asp?tip=1308
TableSample 的这个 MSDN 页面包含一个如何生成实际随机数据样本的示例。
似乎列出的许多想法仍然使用排序
但是,如果您使用临时表,则可以分配一个随机索引(就像许多解决方案所建议的那样),然后获取大于 0 到 1 之间任意数字的第一个索引。
例如(对于 DB2):
WITH TEMP AS (
SELECT COMLUMN, RAND() AS IDX FROM TABLE)
SELECT COLUMN FROM TABLE WHERE IDX > .5
FETCH FIRST 1 ROW ONLY
来自http://akinas.com/pages/en/blog/mysql_random_row/的一种简单有效的方法
SET @i = (SELECT FLOOR(RAND() * COUNT(*)) FROM table); PREPARE get_stmt FROM 'SELECT * FROM table LIMIT ?, 1'; EXECUTE get_stmt USING @i;
Oracle 有更好的解决方案,而不是使用 dbms_random.value,而它需要完全扫描才能按 dbms_random.value 对行进行排序,而且对于大型表来说速度很慢。
改用这个:
SELECT *
FROM employee sample(1)
WHERE rownum=1
对于 SQL Server 2005 及更高版本,扩展 @GreyPanther 对num_value
不连续值的情况的回答。这也适用于我们没有均匀分布数据集并且num_value
不是数字而是唯一标识符的情况。
WITH CTE_Table (SelRow, num_value)
AS
(
SELECT ROW_NUMBER() OVER(ORDER BY ID) AS SelRow, num_value FROM table
)
SELECT * FROM table Where num_value = (
SELECT TOP 1 num_value FROM CTE_Table WHERE SelRow >= RAND() * (SELECT MAX(SelRow) FROM CTE_Table)
)
来自 sql 的随机函数可能会有所帮助。此外,如果您想限制为一行,只需在最后添加即可。
SELECT column FROM table
ORDER BY RAND()
LIMIT 1
select r.id, r.name from table AS r
INNER JOIN(select CEIL(RAND() * (select MAX(id) from table)) as id) as r1
ON r.id >= r1.id ORDER BY r.id ASC LIMIT 1
这将需要更少的计算时间