获得随机排序的最佳方法是什么?

gor*_*ric 32 sql-server

我有一个查询,我希望随机排序结果记录。它使用聚集索引,所以如果我不包括order by它,它可能会按照该索引的顺序返回记录。如何确保随机行顺序?

我知道它可能不是“真正的”随机,伪随机足以满足我的需要。

小智 25

ORDER BY NEWID() 将随机排序记录。这里有一个例子

SELECT *
FROM Northwind..Orders 
ORDER BY NEWID()
Run Code Online (Sandbox Code Playgroud)


EBa*_*arr 20

这是一个老问题,但我认为缺少讨论的一个方面——性能。 ORDER BY NewId()是一般的答案。当别人那里的幻想,他们补充说,你真的应该换NewID()CheckSum(),你知道的,性能!

这种方法的问题在于,您仍然可以保证完整的索引扫描,然后是完整的数据排序。如果您处理过任何严重的数据量,这可能会迅速变得昂贵。看看这个典型的执行计划,并注意排序如何占用你 96% 的时间......

在此处输入图片说明

为了让您了解这是如何扩展的,我将从我使用的数据库中提供两个示例。

  • TableA - 在 2500 个数据页上有 50,000 行。随机查询在 42 毫秒内生成 145 次读取。
  • 表 B - 在 114,000 个数据页上有 120 万行。Order By newid()在此表上运行会生成 53,700 次读取,耗时 16 秒。

这个故事的寓意是,如果您有大表(想想数十亿行)或需要频繁运行此查询,则该newid()方法会崩溃。那么男孩该怎么办呢?

认识 TABLESAMPLE()

在 SQL 2005TABLESAMPLE中创建了一个名为的新功能。我只看过一篇文章讨论它的用途……应该还有更多。MSDN文档在这里。先举个例子:

SELECT Top (20) *
FROM Northwind..Orders TABLESAMPLE(20 PERCENT)
ORDER BY NEWID()
Run Code Online (Sandbox Code Playgroud)

表样本背后的想法是为您提供大约您要求的子集大小。SQL 为每个数据页编号并选择这些页的 X%。您返回的实际行数可能因所选页面中存在的内容而异。

那么我该如何使用呢?选择一个超过您需要的行数的子集大小,然后添加一个Top(). 这个想法是你可以在昂贵的排序之前让你的巨大表显得更小。

就我个人而言,我一直在使用它来有效地限制我的桌子的大小。因此,在那个百万行表上执行top(20)...TABLESAMPLE(20 PERCENT)查询在 1600 毫秒内下降到 5600 次读取。还有一个REPEATABLE()选项,您可以在其中传递“种子”以进行页面选择。这应该导致稳定的样本选择。

无论如何,只是认为应该将其添加到讨论中。希望它可以帮助某人。


Dav*_*ett 19

Pradeep Adiga 的第一个建议, ORDER BY NEWID()很好,我过去曾因为这个原因使用过。

使用时要小心RAND()- 在许多上下文中,每个语句只执行一次,因此ORDER BY RAND()不会产生任何影响(因为您从 RAND() 中为每一行获得相同的结果)。

例如:

SELECT display_name, RAND() FROM tr_person
Run Code Online (Sandbox Code Playgroud)

从我们的 person 表中返回每个名字和一个“随机”数字,每行都是相同的。每次运行查询时,数字都会有所不同,但每次每行都相同。

为了表明RAND()ORDER BY子句中使用也是如此,我尝试:

SELECT display_name FROM tr_person ORDER BY RAND(), display_name
Run Code Online (Sandbox Code Playgroud)

结果仍然按名称排序,表明较早的排序字段(预期是随机的)没有影响,因此大概总是具有相同的值。

NEWID()不过,按顺序排序确实有效,因为如果不总是重新评估NEWID(),则在一个具有唯一标识符的语句中插入许多新行作为键时,UUID 的目的将被破坏,因此:

SELECT display_name FROM tr_person ORDER BY NEWID()
Run Code Online (Sandbox Code Playgroud)

确实“随机”排列名称。

其他数据库管理系统

对于 MSSQL(至少 2005 和 2008 年,如果我没记错的话,2000 年也是如此)。每次在所有 DBMS 中都应评估返回新 UUID 的函数NEWID() 在 MSSQL 下,但值得在文档和/或您自己的测试中验证这一点。其他任意结果函数的行为,如 RAND(),在 DBMS 之间更有可能有所不同,因此再次检查文档。

此外,我还看到在某些情况下忽略了 UUID 值的排序,因为 DB 假定该类型没有有意义的排序。如果您发现这种情况在排序子句中显式地将 UUID 转换为字符串类型,或者像CHECKSUM()在 SQL Server 中那样围绕它包装一些其他函数(这也可能会有小的性能差异,因为排序将在一个 32 位的值而不是 128 位的值,尽管它的好处是否超过了运行CHECKSUM()每个值的成本,我会让你先测试一下)。

边注

如果您想要任意但有些可重复的排序,请按行本身中一些相对不受控制的数据子集排序。例如,或者这些将以任意但可重复的顺序返回名称:

SELECT display_name FROM tr_person ORDER BY CHECKSUM(display_name), display_name -- order by the checksum of some of the row's data
SELECT display_name FROM tr_person ORDER BY SUBSTRING(display_name, LEN(display_name)/2, 128) -- order by part of the name field, but not in any an obviously recognisable order)
Run Code Online (Sandbox Code Playgroud)

任意但可重复的排序在应用程序中通常不太有用,但如果您想以各种顺序对结果测试某些代码,但希望能够以相同的方式重复每次运行多次(以获得平均时间多次运行的结果,或者测试您对代码所做的修复确实消除了先前由特定输入结果集突出显示的问题或低效率,或者只是为了测试您的代码是“稳定的”,因为每次都返回相同的结果如果以给定的顺序发送相同的数据)。

这个技巧也可以用来从函数中获得更多的任意结果,这些函数不允许在函数体内进行像 NEWID() 这样的非确定性调用。同样,这不是在现实世界中经常有用的东西,但如果你想要一个函数返回随机的东西并且“随机”已经足够好(但要小心记住决定的规则),这可能会派上用场当评估用户定义的函数时,即通常每行一次,或者您的结果可能不是您期望/要求的)。

表现

正如 EBarr 指出的那样,上述任何一项都可能存在性能问题。对于超过几行,您几乎可以保证在以正确顺序读回请求的行数之前看到输出到 tempdb,这意味着即使您正在寻找前 10 行,您也可能会找到完整的索引扫描(或更糟的是,表扫描)与大量写入 tempdb 一起发生。因此,与大多数事情一样,在生产中使用现实数据之前进行基准测试非常重要。


Pau*_*ite 5

许多表都有一个相对密集(很少有缺失值)的索引数字 ID 列。

这使我们能够确定现有值的范围,并使用该范围内随机生成的 ID 值选择行。当要返回的行数相对较少,并且 ID 值的范围密集(因此生成缺失值的机会足够小)时,这种方法效果最好。

为了说明这一点,以下代码从 Stack Overflow 用户表中选择了 100 个不同的随机用户,该表有 8,123,937 行。

第一步是确定 ID 值的范围,这是由于索引而进行的高效操作:

DECLARE 
    @MinID integer,
    @Range integer,
    @Rows bigint = 100;

--- Find the range of values
SELECT
    @MinID = MIN(U.Id),
    @Range = 1 + MAX(U.Id) - MIN(U.Id)
FROM dbo.Users AS U;
Run Code Online (Sandbox Code Playgroud)

范围查询

该计划从索引的每一端读取一行。

现在我们在范围内生成 100 个不同的随机 ID(在用户表中具有匹配的行)并返回这些行:

WITH Random (ID) AS
(
    -- Find @Rows distinct random user IDs that exist
    SELECT DISTINCT TOP (@Rows)
        Random.ID
    FROM dbo.Users AS U
    CROSS APPLY
    (
        -- Random ID
        VALUES (@MinID + (CONVERT(integer, CRYPT_GEN_RANDOM(4)) % @Range))
    ) AS Random (ID)
    WHERE EXISTS
    (
        SELECT 1
        FROM dbo.Users AS U2
            -- Ensure the row continues to exist
            WITH (REPEATABLEREAD)
        WHERE U2.Id = Random.ID
    )
)
SELECT
    U3.Id,
    U3.DisplayName,
    U3.CreationDate
FROM Random AS R
JOIN dbo.Users AS U3
    ON U3.Id = R.ID
-- QO model hint required to get a non-blocking flow distinct
OPTION (MAXDOP 1, USE HINT ('FORCE_LEGACY_CARDINALITY_ESTIMATION'));
Run Code Online (Sandbox Code Playgroud)

随机行查询

该计划显示,在这种情况下,需要 601 个随机数才能找到 100 个匹配行。它很快:

表“用户”。扫描计数 1,逻辑读取 1937,物理读取 2,预读读取 408
表“工作台”。扫描计数 0,逻辑读取 0,物理读取 0,预读读取 0
表“工作文件”。扫描计数 0,逻辑读取 0,物理读取 0,预读读取 0

 SQL Server 执行时间:
   CPU 时间 = 0 ms,经过时间 = 9 ms。

在 Stack Exchange Data Explorer 上试试。