如何证明数据库中缺少隐式顺序?


21

最近,我向同事们解释了在必要时使用一列对数据库表中的数据进行排序的重要性,例如,按时间顺序排列的数据。事实证明这有些困难,因为他们似乎可以无休止地重新运行查询,并且总是以相同的顺序返回相同的行集。

我之前已经注意到这一点,而我真正能做的就是坚持要他们信任我,而不仅仅是假设数据库表的行为就像传统的CSV或Excel文件一样。

例如,执行(PostgreSQL)查询

create table mytable (
    id INTEGER PRIMARY KEY,
    data TEXT
);
INSERT INTO mytable VALUES
    (0, 'a'),
    (1, 'b'),
    (2, 'c'),
    (3, 'd'),
    (4, 'e'),
    (5, 'f'),
    (6, 'g'),
    (7, 'h'),
    (8, 'i'),
    (9, 'j');

将创建具有明确概念顺序的表格。以最简单的方式选择相同的数据将是:

SELECT * FROM mytable;

总是给我以下结果:

 id | data 
----+------
  0 | a
  1 | b
  2 | c
  3 | d
  4 | e
  5 | f
  6 | g
  7 | h
  8 | i
  9 | j
(10 rows)

我可以一遍又一遍地执行此操作,它将始终以相同的顺序向我返回相同的数据。但是,我知道这种隐式顺序可以被打破,我之前已经看过,特别是在大型数据集中,选择时某些随机值显然会被扔到“错误”的地方。但是发生在我身上的是我不知道这是怎么发生的或如何重现的。我发现很难在Google上获得结果,因为搜索查询趋向于仅返回有关排序结果集的常规帮助。

所以,我的问题本质上是这些:

  1. 我如何能证明并具体地证明没有ORDER BY语句的查询的行返回顺序是不可靠的,最好即使在不更新或编辑所讨论的表的情况下,也可以通过引起并显示隐式顺序的细分来实现?

  2. 如果仅一次插入数据然后再也不进行更新,这根本没有任何区别吗?

我希望使用基于Postgres的答案,因为这是我最熟悉的答案,但我对理论本身更感兴趣。


6
“不再写入或再次更新”-为什么这是一张桌子?听起来像文件。还是一个枚举。或者不需要在数据库中的东西。如果是按时间顺序排列的,是否没有要排序的日期栏?如果按时间顺序排列很重要,那么您会认为该信息足够重要,可以在表格中使用。无论如何,由于有人删除或创建新索引或内存更改,跟踪标志或其他影响等事件,计划可能会更改。他们的论点听起来像是“我从不系安全带,也从未穿过挡风玻璃,所以我将继续不系安全带。” :-(
Aaron Bertrand

9
有些逻辑问题只是无法从技术上解决或没有人力资源部门参与才能解决。如果您的公司希望允许依赖于voodoo并忽略文档的开发人员实践,并且您的用例确实仅限于一个永远不会更新的小表,请让他们自行处理并更新您的简历。这是不值得争论的。
亚伦·伯特兰

1
您没有根据声称“将永远”。您只能声明“一直有”,“当我检查时”。语言具有定义-即与用户的合同。
philipxy

10
我很好奇为什么你们的这些同事反对在order by查询中添加该子句?他们是否试图节省源代码存储空间?键盘磨损?输入恐惧子句需要多少时间?
mustaccio

2
我一直认为数据库引擎应该随机排列语义不保证排序的查询的前几行,以帮助进行测试。
道格·麦克林

Answers:


30

我看到三种方法来说服他们:

  1. 让他们尝试相同的查询,但表更大(行数更多),或者在两次执行之间更新表时。或插入新行,并删除一些旧行。或者在两次执行之间添加或删除索引。或者表已被清理(在Postgres中)。或重建索引(在SQL Server中)。或者表从集群更改为堆。或者重新启动数据库服务。

  2. 您可以建议他们证明不同的执行将返回相同的顺序。他们可以证明吗?他们能否提供一系列测试来证明任何查询无论执行多少次,都会以相同的顺序给出结果?

  3. 提供有关该问题的各种DBMS的文档。例如:

PostgreSQL

排序行

查询产生输出表之后(处理选择列表之后),可以选择对它进行排序。如果未选择排序,则将以未指定的顺序返回行。在这种情况下,实际顺序将取决于扫描和联接计划的类型以及磁盘上的顺序,但是不能依赖它。只有明确选择了排序步骤,才能保证特定的输出顺序。

SQL Server

SELECT- ORDER BY子句(Transact-SQL)

对SQL Server中查询返回的数据进行排序。使用此子句可以:

通过指定的列列表对查询的结果集进行排序,并且可以选择将返回的行限制为指定的范围。除非ORDER BY指定了子句,否则不能保证结果集中返回行的顺序。

甲骨文

order_by_clause

使用ORDER BY子句对语句返回的行进行排序。如果没有order_by_clause,则不能保证多次执行同一查询将以相同顺序检索行。


对于非常小的未修改的表,您可能会看到此行为。这是预期的。但这也不保证。顺序可能会更改,因为您添加了索引或修改了索引,或者重新启动了数据库,并且可能还有许多其他情况。
ypercubeᵀᴹ

6
如果命令很重要,那么谁负责检查其代码,谁就应该拒绝,直到他们使用ORDER BY。DBMS的开发人员(Oracle,SQL Server,Postgres)都对他们的产品保证和不保证的东西说了同样的话(他们得到的报酬比我多得多,因此,除了建立了该死的东西外,他们知道他们在说什么)东西)。
ypercubeᵀᴹ

1
即使现在的顺序看起来相同,也可以确定这些表在您所构建软件的整个生命周期中都不会更新吗?不会再插入更多行了吗?
ypercubeᵀᴹ

1
是否可以保证这张桌子总是那么小?是否可以保证不再添加任何列?我看到数十种不同的情况,将来表可能会更改(其中一些更改可能会影响查询结果的顺序)。我建议您请他们回答所有这些问题。他们能保证不会发生类似的事情吗?他们为什么不添加一个简单的ORDER BY无论表格如何更改,它都能保证顺序?为什么不添加安全无害的安全呢?
ypercubeᵀᴹ

10
文件应足够。不管您证明什么,其他任何事情都是在second测,无论如何也绝不会被视为确定的。它将始终是您所做的事情并且可以解释,可能是您自费,而不是这样做。掌握了文档,以书面形式提交“保修”,然后简单地寻求书面许可,不要按要求的顺序返回行(您将无法获得行)。

19

这又是黑天鹅的故事。如果您还没有看到它,那并不意味着它们不存在。希望对于您而言,这不会导致其他全球金融危机,仅会导致一些不满意的客户。

Postgres 文档明确指出:

如果未给出ORDER BY,则以系统认为最快的顺序返回行。

在这种情况下,“系统”包括postgres守护程序本身(包括其数据访问方法和查询优化器的实现),底层操作系统,数据库存储的逻辑和物理布局,甚至CPU缓存。由于您作为数据库用户无法控制该堆栈,因此您不应该依赖它继续永久地发挥其当前的作用。

您的同事犯了草率化的谬论。为了证明他们的观点,足以证明他们的假设仅一次错误,例如通过此dbfiddle


12

考虑以下示例,其中有三个相关表。订单,用户和OrderDetails。OrderDetails通过外键链接到Orders表和Users表。本质上,这是关系数据库的非常典型的设置。可以说,关系 DBMS 的全部目的。

USE tempdb;

IF OBJECT_ID(N'dbo.OrderDetails', N'U') IS NOT NULL
DROP TABLE dbo.OrderDetails;

IF OBJECT_ID(N'dbo.Orders', N'U') IS NOT NULL
DROP TABLE dbo.Orders;

IF OBJECT_ID(N'dbo.Users', N'U') IS NOT NULL
DROP TABLE dbo.Users;

CREATE TABLE dbo.Orders
(
    OrderID int NOT NULL
        CONSTRAINT OrderTestPK
        PRIMARY KEY
        CLUSTERED
    , SomeOrderData varchar(1000)
        CONSTRAINT Orders_somedata_df
        DEFAULT (CRYPT_GEN_RANDOM(1000))
);

CREATE TABLE dbo.Users
(
    UserID int NOT NULL
        CONSTRAINT UsersPK
        PRIMARY KEY
        CLUSTERED
    , SomeUserData varchar(1000)
        CONSTRAINT Users_somedata_df
        DEFAULT (CRYPT_GEN_RANDOM(1000))
);

CREATE TABLE dbo.OrderDetails
(
    OrderDetailsID int NOT NULL
        CONSTRAINT OrderDetailsTestPK
        PRIMARY KEY
        CLUSTERED
    , OrderID int NOT NULL
        CONSTRAINT OrderDetailsOrderID
        FOREIGN KEY
        REFERENCES dbo.Orders(OrderID)
    , UserID int NOT NULL
        CONSTRAINT OrderDetailsUserID
        FOREIGN KEY
        REFERENCES dbo.Users(UserID)
    , SomeOrderDetailsData varchar(1000)
        CONSTRAINT OrderDetails_somedata_df
        DEFAULT (CRYPT_GEN_RANDOM(1000))
);

INSERT INTO dbo.Orders (OrderID)
SELECT TOP(100) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM sys.syscolumns sc;

INSERT INTO dbo.Users (UserID)
SELECT TOP(100) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM sys.syscolumns sc;

INSERT INTO dbo.OrderDetails (OrderDetailsID, OrderID, UserID)
SELECT TOP(10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    , o.OrderID
    , u.UserID
FROM sys.syscolumns sc
    CROSS JOIN dbo.Orders o
    CROSS JOIN dbo.Users u
ORDER BY NEWID();

CREATE INDEX OrderDetailsOrderID ON dbo.OrderDetails(OrderID);
CREATE INDEX OrderDetailsUserID ON dbo.OrderDetails(UserID);

在这里,我们要查询UserID为15的OrderDetails表:

SELECT od.OrderDetailsID
    , o.OrderID
    , u.UserID
FROM dbo.OrderDetails od
    INNER JOIN dbo.Users u ON u.UserID = od.UserID
    INNER JOIN dbo.Orders o ON od.OrderID = o.OrderID
WHERE u.UserID = 15

查询的输出如下:

╔════════════════╦═════════╦════════╗
║OrderDetailsID║OrderID║用户ID║
╠════════════════╬═════════╬════════╣
║2200115║2║15║
║630215║3║15║
215 1990215║3║15║
║4960215║3║15║
715 100715║8║15║
308 3930815║9║15║
║6310815║9║15║
║4441015║11║15║
║2171315║14║15║
║3431415║15║15║
║4571415║15║15║
║6421515║16║15║
║2271715║18║15║
║2601715║18║15║
║3521715║18║15║
║221815║19║15║
║3381915║20║15║
║4471915║20║15║
╚════════════════牛皮═════════牛皮════════╝

如您所见,行的输出顺序与OrderDetails表中的行顺序不匹配。

添加显式ORDER BY确保行将以所需顺序返回给客户端:

SELECT od.OrderDetailsID
    , o.OrderID
    , u.UserID
FROM dbo.OrderDetails od
    INNER JOIN dbo.Users u ON u.UserID = od.UserID
    INNER JOIN dbo.Orders o ON od.OrderID = o.OrderID
WHERE u.UserID = 15
ORDER BY od.OrderDetailsID;
╔════════════════╦═════════╦════════╗
║OrderDetailsID║OrderID║用户ID║
╠════════════════╬═════════╬════════╣
║3915║40║15║
715 100715║8║15║
║221815║19║15║
║299915║100║15║
║368215║83║15║
║603815║39║15║
║630215║3║15║
║728515║86║15║
║972215║23║15║
2015 992015║21║15║
║1017115║72║15║
138 1113815║39║15║
╚════════════════牛皮═════════牛皮════════╝

如果行顺序是必不可少的,并且您的工程师知道命令是必不可少的,那么他们应该只想使用一条ORDER BY语句,因为如果发生与错误顺序有关的故障,可能会花费他们的指定时间。

第二个示例,也许是更具启发性的示例,使用OrderDetails上面的表,其中我们没有联接任何其他表,但是有一个简单的要求,即找到与OrderID和UserID都匹配的行,我们看到了问题。

我们将创建一个索引来支持查询,就像在现实生活中,如果性能在任何方面都很重要(在什么时候不是吗?),您可能会做的那样。

CREATE INDEX OrderDetailsOrderIDUserID ON dbo.OrderDetails(OrderID, UserID);

这是查询:

SELECT od.OrderDetailsID
FROM dbo.OrderDetails od
WHERE od.OrderID = 15
    AND (od.UserID = 21 OR od.UserID = 22)

结果:

╔════════════════╗
║OrderDetailsID║
╠════════════════╣
║21421║
║5061421║
║7091421║
691 691422║
║3471422║
║7241422║
╚════════════════╝

添加ORDER BY子句绝对可以确保我们在这里也获得正确的排序。

这些模型只是简单的示例,其中如果没有明确的ORDER BY声明,则不能保证行是“有序的” 。还有更多类似的示例,并且由于DBMS引擎代码的更改非常频繁,因此特定行为可能会随时间而改变。


10

作为一个实际示例,在Postgres中,当您更新一行时,当前顺序会更改:

% SELECT * FROM mytable;
 id | data 
----+------
  0 | a
  1 | b
  2 | c
  3 | d
  4 | e
  5 | f
  6 | g
  7 | h
  8 | i
  9 | j
(10 rows)

% UPDATE mytable SET data = 'ff' WHERE id = 5;
UPDATE 1
% SELECT * FROM mytable;
 id | data 
----+------
  0 | a
  1 | b
  2 | c
  3 | d
  4 | e
  6 | g
  7 | h
  8 | i
  9 | j
  5 | ff
(10 rows)

我不认为任何地方都可以记录这种现有隐式排序的规则,它肯定会随时更改,恕不另行通知,并且绝对不是跨DB引擎的可移植行为。


记录在案:ypercube的答案引用了文件,告诉我们未指定顺序。
与莫妮卡(Monica)进行的轻度比赛

@LightnessRacesinOrbit我将其视为文档,明确告诉我们未记录。我的意思是,文档中未指定的所有内容也是未指定的。这是一种重言式。无论如何,我编辑了答案的这一部分以使其更加具体。
JoL

3

并非完全是演示,但评论太久。

在大型表上,某些数据库将进行交错并行扫描:

如果两个查询要扫描同一个表并几乎同时到达,则第二个查询开始时,第一个查询可能会穿过表。

第二个查询可以接收从表中间开始的记录(第一个查询完成时),然后从表的开头接收记录。


2

创建具有“错误”顺序的聚簇索引。例如,在上群集ID DESC。这通常会输出相反的顺序(尽管也不能保证)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.