ORDER BY和ROW_NUMBER()是否具有确定性?

时间:2013-09-04 11:50:28

标签: sql sql-server tsql

我已经在几个数据库引擎中不时使用SQL几年但是理论知识很少,所以我的问题可能对你们中的某些人来说非常“愚蠢”。但它现在变得很重要,所以我不得不问。

想象一下具有非唯一列status的表格网址。对于这个问题,假设我们有大量的行,并且状态在每条记录中具有相同的值。

想象一下我们执行多次查询:

SELECT * FROM Urls ORDER BY status
  1. 我们是否每次都获得相同的行顺序?如果我们做了如果添加一些新行会发生什么?它是否会更改订单,或者新记录会附加到结果的末尾?如果我们没有获得相同的订单 - 在什么条件下依赖此订单?

  2. ROW_NUMBER() OVER (ORDER BY status)会返回与上述查询相同的订单,还是基于不同的订购机制?

4 个答案:

答案 0 :(得分:9)

这很简单。如果您想要一个可以依赖的排序,那么您需要在ORDER BY子句中包含足够的列,以便所有这些列的组合对于每一行都是唯一的。没有其他保证。

对于单个表,通常可以通过列出“有趣”的列来排序,然后包括主键列,从而获得所需的内容。由于PK本身保证了唯一性,因此整个组合也保证唯一地定义排序,例如,如果Urls表的主键为{Site, Page, Ordinal},则以下内容会为您提供可靠的结果:

SELECT * FROM Urls ORDER BY status, Site, Page, Ordinal

答案 1 :(得分:7)

ORDER BY在SQL Server中不稳定(据我所知,在任何其他数据库中也不稳定)。稳定排序是按照与表中相同的顺序返回记录的排序。

高级别的原因很简单。表是集合。他们没有订单。所以“稳定”的排序没有意义。

较低级别的原因可能更为重要。数据库可以实现并行排序算法。默认情况下,此类算法不稳定。

如果您想要稳定排序,请在排序中包含一个键列。

documentation

中提到了这一点
  

使用OFFSET和查询请求之间获得稳定的结果   FETCH,必须满足以下条件:

     

基础数据   查询使用的一定不能更改。也就是说,要么触摸行   由查询未更新或来自查询的所有页面请求   使用快照或在单个事务中执行   可序列化的事务隔离。有关这些的更多信息   事务隔离级别,请参阅SET TRANSACTION ISOLATION LEVEL   (处理SQL)。

     

ORDER BY子句包含列或组合   保证唯一的列。

答案 2 :(得分:0)

我非常喜欢这些类型的问题,因为你可以进行性能分析。

首先,让我们使用带有一百万条随机记录的[urls]表创建一个样本[test]数据库。

见下面的代码。

-- Switch databases
USE [master];
go

-- Create simple database
CREATE DATABASE [test];
go

-- Switch databases
USE [test];
go

-- Create simple table
CREATE TABLE [urls]
    (
      my_id INT IDENTITY(1, 1)
                PRIMARY KEY ,
      my_link VARCHAR(255) ,
      my_status VARCHAR(15)
    );
go

-- http://stackoverflow.com/questions/1393951/what-is-the-best-way-to-create-and-populate-a-numbers-table

-- Load table with 1M rows of data 
;
WITH    PASS0
          AS ( SELECT   1 AS C
               UNION ALL
               SELECT   1
             ),           --2 rows
        PASS1
          AS ( SELECT   1 AS C
               FROM     PASS0 AS A ,
                        PASS0 AS B
             ),  --4 rows
        PASS2
          AS ( SELECT   1 AS C
               FROM     PASS1 AS A ,
                        PASS1 AS B
             ),  --16 rows
        PASS3
          AS ( SELECT   1 AS C
               FROM     PASS2 AS A ,
                        PASS2 AS B
             ),  --256 rows
        PASS4
          AS ( SELECT   1 AS C
               FROM     PASS3 AS A ,
                        PASS3 AS B
             ),  --65536 rows
        PASS5
          AS ( SELECT   1 AS C
               FROM     PASS4 AS A ,
                        PASS4 AS B
             ),  --4,294,967,296 rows
        TALLY
          AS ( SELECT   ROW_NUMBER() OVER ( ORDER BY C ) AS Number
               FROM     PASS5
             )
    INSERT  INTO urls
            ( my_link ,
              my_status
            )
            SELECT 
      -- top 10 search engines + me
                    CASE ( Number % 11 )
                      WHEN 0 THEN 'www.ask.com'
                      WHEN 1 THEN 'www.bing.com'
                      WHEN 2 THEN 'www.duckduckgo.com'
                      WHEN 3 THEN 'www.dogpile.com'
                      WHEN 4 THEN 'www.webopedia.com'
                      WHEN 5 THEN 'www.clusty.com'
                      WHEN 6 THEN 'www.archive.org'
                      WHEN 7 THEN 'www.mahalo.com'
                      WHEN 8 THEN 'www.google.com'
                      WHEN 9 THEN 'www.yahoo.com'
                      ELSE 'www.craftydba.com'
                    END AS my_link ,

      -- ratings scale
                    CASE ( Number % 5 )
                      WHEN 0 THEN 'poor'
                      WHEN 1 THEN 'fair'
                      WHEN 2 THEN 'good'
                      WHEN 3 THEN 'very good'
                      ELSE 'excellent'
                    END AS my_status
            FROM    TALLY AS T
            WHERE   Number <= 1000000
go

其次,我们总是希望在我们的测试环境中进行性能分析时清除缓冲区和缓存。此外,我们希望打开统计信息I / O和时间来比较结果。

见下面的代码。

-- Show time & i/o
SET STATISTICS TIME ON
SET STATISTICS IO ON
GO

-- Remove clean buffers & clear plan cache
CHECKPOINT 
DBCC DROPCLEANBUFFERS 
DBCC FREEPROCCACHE
GO

第三,我们想尝试第一个TSQL语句。查看执行计划并捕获统计信息。

-- Try 1
SELECT * FROM urls ORDER BY my_status

/*
Table 'urls'. Scan count 5, logical reads 4987, physical reads 1, read-ahead reads 4918, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 3166 ms,  elapsed time = 8130 ms.
*/

enter image description here

第四,我们想尝试第二个TSQL语句。不要忘记清除查询计划缓存和缓冲区。如果不这样做,则查询所需的时间不到1秒,因为大多数信息都在内存中。查看执行计划并捕获统计信息。

-- Try 2
SELECT ROW_NUMBER() OVER (ORDER BY my_status) as my_rownum, * FROM urls

/*
Table 'urls'. Scan count 5, logical reads 4987, physical reads 1, read-ahead reads 4918, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 3276 ms,  elapsed time = 8414 ms.
*/

enter image description here

最后但同样重要的是,这是有趣的部分,即性能分析。

1 - 我们可以看到第二个计划是第一个计划的超级计划。因此,两个计划都会扫描聚簇索引并对数据进行排序。并行性用于将结果放在一起。

2 - 第二个计划/查询需要计算行号。它会对数据进行分段并计算此标量。因此,我们最终会在计划中再增加两名运营商。

第一个计划在8130毫秒运行并且第二个计划在8414毫秒运行并不奇怪。

始终查看查询计划。估计和实际。他们告诉你想要引擎计划做什么以及它实际做了什么。

在这个例子中,两个不同的TSQL语句提出了几乎相同的计划。

此致

约翰

www.craftydba.com

答案 3 :(得分:0)

任何sql问题的一般答案&#34;这个输出在&#34;是&#34;无论服务器是什么感觉,从查询到查询可能都不一样&#34;除非您特别要求订单。

甚至可以从myTable&#39;中选择前1000名myColumn。可以按任何顺序返回任何行;例如,服务器可以使用并行线程,并且第一个线程开始返回在表中间开始读取的结果,或者使用包含myColumn的索引,因此您获得了按字母顺序排列的第一个productName的行(这次;上次index有不同的统计数据,因此它选择了不同的索引,并为您提供了1000个最早的交易... ...

理论上,服务器甚至可以说&#34;我的内存缓存中有这10个页面与您的查询匹配,我会在等待磁盘返回其余部分时将这些页面传递给您...