阻止 SQL Server 对结果进行排序?

Man*_*ngo 6 sql-server order-by group-by nonclustered-index

我正在教一门课,我想向我的学生证明,如果没有明确的ORDER BY. 我观察到即使ORDER BY查询中不存在SQL Server 似乎也返回有序结果。这是一个问题,因为我担心我的学生会在这一点上感到困惑。

观察到的行为示例:

  • 默认情况下,数据似乎按主键顺序排列,即使主键是非集群的。
  • 如果我使用GROUP BY,结果集似乎按最后一列的顺序排列。
    例如GROUP BY this, that,以数据的顺序结束,这很奇怪,尤其是在没有索引的情况下。

我还没有在其他数据库(如 MySQL、PostgreSQL 和 SQLite)中观察到这种行为。SQL Server 是否有一些根本不同的地方,即使没有ORDER BY?

现在在我看来,在没有被询问的情况下对数据进行排序是一个额外的不必要步骤,会浪费处理时间。真的吗?

是否有某种全局设置可以禁用这种行为?有没有办法让 SQL Server 停止对数据进行排序?

这也使得向学生证明数据不是自动排序的变得更加困难。

谁能给我一个简单的演示脚本,显示结果从执行到执行的变化顺序?

Bre*_*zar 36

让我们假设你有电话簿的白页——记住,爷爷放在冰箱里的那个东西,这样他就可以给战争中的朋友们打电话。它按姓氏、名字组织。

如果我让你拿到电话簿并读出名字:

SELECT FirstName, LastName FROM dbo.PhoneBook
Run Code Online (Sandbox Code Playgroud)

通常会按照姓氏的顺序把它们读给我听。您不必对它们进行分类——它们只是那样出来的,因为它们已经是这样存储的。

这就是 SQL Server 的结果在默认情况下似乎已排序的原因。

问题是,这不能保证,并且有几种情况可以改变这种情况。例如,假设您走到电话簿前为我阅读姓氏,而其他人已经在阅读白页。您可以选择跟随他们,从电话簿的同一区域阅读。你会从他们已经在的地方开始(比如 M 的),然后当另一个人到达电话簿的末尾(Z 的)时,你会回到 A 并阅读你错过的部分。这称为旋转木马扫描,SQL Server 企业版默认执行此操作,您无法将其关闭。你也无法预测什么时候会得到它。

另一种情况 - 假设我只问你我们城市的名字,而不是姓氏:

SELECT FirstName FROM dbo.PhoneBook
Run Code Online (Sandbox Code Playgroud)

请注意,我没有要求对名称进行排序。起初,当我们只有白页时,您可能会使用这些白页并向我大喊答案——但它们似乎没有排序。(我们电话簿的白页按姓氏、名字排序,所以如果你只是从那里阅读,名字会到处都是。)

如果稍后有人只在名字上创建索引,SQL Server 足够聪明,可以将它用于我的仅名字查询,因为它是可用于满足我的查询的最窄/最小的对象。突然间,结果似乎是按名字排序的——但这只是巧合,因为有一个新索引可以满足我的查询。

例如GROUP BY this,that,以 of 的顺序结束数据that,这很奇怪,特别是如果没有索引的话。

为了进行分组,您可能必须对数据进行排序。以我们的电话簿为例,如果我让您按名字将每个人分组在一起,您必须按名字对他们进行排序才能完成该任务。请注意,相似之处在于电话簿和索引之间,而不是(除非表具有聚集索引,在这种情况下,表聚集索引,因此表/聚集索引具有固定的逻辑顺序)。


现在给一些温和的职业建议:因为这些概念对你来说是新的,我谦虚地建议,与其强迫 SQL Server 做你的投标,认为你会以某种方式调整它以使其更快,不如你采取一些步骤背部。您在这里提出了一个非常好奇的问题,所以我建议观看我的免费 SQL Server 培训视频系列,如何像 SQL Server 引擎一样思考。我使用 Stack Overflow 数据库中的页面来解释 SQL Server 如何提供查询结果。

我的建议似乎有点争议——毕竟,我在这里推销我自己的培训——但它是免费的,甚至是开源的,在 MIT 许可下获得许可,供人们重复使用。在您通过观看我的练习掌握了它之后,您可以使用幻灯片并训练您的队友。


小智 14

社区维基答案

这也使得向学生证明数据不是自动排序的变得更加困难。

这是我喜欢的演示:

CREATE TABLE dbo.Example
(
    [data]  integer NOT NULL,
    padding character(8000) NOT NULL DEFAULT ''
);
GO
-- Add 50 rows with [data] numbered from 1 to 50
INSERT dbo.Example 
    ([data]) 
SELECT SV.number
FROM master.dbo.spt_values AS SV
WHERE SV.number BETWEEN 1 AND 50
AND SV.[type] = N'P';
GO
-- Add a nonclustered primary key on [data]
ALTER TABLE dbo.Example
ADD CONSTRAINT [PK dbo.Example data]
PRIMARY KEY NONCLUSTERED ([data]);
Run Code Online (Sandbox Code Playgroud)

以下查询以冷缓存开始,并强制使用 PK:

-- Flush dirty pages to disk
CHECKPOINT;
GO
-- Drop clean buffers from memory
DBCC DROPCLEANBUFFERS;
GO
-- Query forced to use the PK ordered by [data]
SELECT TOP (15)
    E.[data],
    E.padding
FROM dbo.Example AS E 
    WITH (INDEX([PK dbo.Example data]))
WHERE
    E.[data] > 0;
Run Code Online (Sandbox Code Playgroud)

执行计划显示了在搜索中使用的索引:

计划

我们可能期望按顺序排序的行,[data]但输出通常随每次执行而变化:

输出

原因是嵌套循环连接使用无序预取。


例如GROUP BY this,that,以 of 的顺序结束数据that,这很奇怪,特别是如果没有索引的话。

当您(或 DBMS)想做时GROUP BY a,b,您可以通过以下方式进行:

  • 排序(按a,b顺序);或
  • b,a顺序排序;或
  • 散列组合 a,b

MySQL 只能采用第一种方式,因此它始终会按a,b-的顺序生成结果,除非您添加不同的ORDER BY.

SQL Server 的优化器具有所有 3 个可用选项 - 可能更多 - 因此您可以看到不同的结果,这取决于索引、表的大小、连接等。结果的(巧合)顺序可能只是所需的排序的剩余部分做分组。

如果您想向学生演示,请尝试使用不同索引的相同查询。或者同时尝试GROUP BY a,bGROUP BY b,a。你会得到相同或不同顺序的结果吗?在运行查询之前询问您的学生!


sep*_*pic 7

即使我不希望 MS SQL 也倾向于对结果进行排序。

默认情况下,数据似乎按主键顺序排列,即使主键是非集群的。

现在在我看来,在没有被询问的情况下对数据进行排序是一个额外的不必要步骤,会浪费处理时间。

从第一个到最后一个语句都是错误的。

除非你ORDER BY在你的SELECT服务器中指定什么都没有。

相反,您看到的是它简单地读取数据的顺序。

SQL Server 中基于磁盘的表有两种形式:聚簇表。

在堆的情况下,表只能按分配顺序读取(使用 IAM 页),如果是聚簇表,则可以按聚簇索引顺序或分配顺序读取

这是我的再现,它显示了我所说的 + 它表明“数据似乎按主键顺序排序,即使主键 是非簇的”是错误的:顾名思义,聚簇索引顺序与聚簇索引和不是主键。

我有一个dbo.Nums自然数 1..1000000的聚簇表,现在我用它来创建另一个聚簇表dbo.Nums2,该聚簇表将具有非聚簇 PK 但它永远不会按其顺序排序,因为我将在其上创建聚簇索引(即不会是 PK),正如我已经说过的,阅读此表的方法是CLUSTERED INDEX ORDERALLOCATION ORDER仅是:

-- create table dbo.Nums2 with 3 columns:
select n, 1000000 - n + 1 as n2, replicate('a', 200) as filler
into dbo.Nums2
from dbo.nums;

-- create clustered index on it + non-clustered Primary Key:
create unique clustered index ix2 on dbo.Nums2 (n2);
alter table dbo.nums2 add constraint PK_nums2_n primary key(n);  

-- clustered index scan demonstration
select *
from dbo.nums2;

-- allocation order scan demonstration:
select *
from dbo.nums2 with (nolock);
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

在这里你可以看到上面所说的:聚簇表可以通过两种方式读取:聚簇索引顺序(第一种情况)或分配顺序(第二种情况)。

您可以在一些不同的条件下实现分配顺序扫描,我演示了其中一种:我的查询在 Read Uncommitted 隔离级别下运行 + 我的表有超过 64 页。

回到你的问题。

以下是我的查询生成的计划:它们都没有 SORT 运算符。他们有 Ordered = False 的聚集索引扫描:

在此处输入图片说明

回顾一下:

  • 数据似乎没有被排序,它看起来是由服务器读取

  • 数据似乎不是按主键顺序排序的,即使主键是非集群的。它与 PK 定义无关。对于聚簇表,可能的扫描顺序是CLUSTERED INDEX ORDERALLOCATION ORDER

  • 没有多余的不必要的步骤,其废弃物处理时间。如果要求服务器仅以某种方式返回数据,则服务器不会对数据进行排序

这也使得向学生证明数据不是自动排序的变得更加困难。

有没有办法让 MSSQL 停止对数据进行排序?

它不订购数据。

如果要演示它,最简单的方法是使用堆。堆只能在分配顺序扫描中读取。所以只需以无序方式输入值;

declare @t table (id int);
insert into @t values (1), (20), (3), (40), (5), (60), (7), (80);

select *
from @t;
Run Code Online (Sandbox Code Playgroud)

我想我开始明白了:表中的数据已经按分配顺序排列了,如果我碰巧使用了 IDENTITY,那么该分配顺序也是按排序顺序排列的

那是错的。这是示例:我的表有一个标识列,但是分配顺序与标识无关:

declare @t table (id int identity);
insert into @t default values; -- 1
insert into @t default values; -- 2
insert into @t default values; -- 3
delete @t where id in (2, 3);
insert into @t default values; -- 4
insert into @t default values; -- 5
insert into @t default values; -- 6
delete @t where id = 6;
insert into @t default values; -- 7
delete @t where id = 1;
insert into @t default values; -- 8
insert into @t default values; -- 9
delete @t where id = 1;
insert into @t default values; -- 10

select *
from @t;
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明