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

22 order-by data-integrity

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

我之前已经注意到这一点,我真正能做的就是坚持他们信任我,而不是简单地假设数据库表的行为类似于传统的 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');
Run Code Online (Sandbox Code Playgroud)

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

SELECT * FROM mytable;
Run Code Online (Sandbox Code Playgroud)

总是给我以下结果:

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

我可以一遍又一遍地这样做,它总是会以相同的顺序返回给我相同的数据。然而,我知道这种隐式顺序可以被打破,我以前看过它,特别是在大型数据集中,在选定时,某些随机值会显然会被抛出到“错误”位置。但我突然想到,我不知道这是如何发生的,也不知道如何重现它。我发现很难在 Google 上获得结果,因为搜索查询往往只返回有关排序结果集的一般帮助。

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

  1. 我怎样才能明确和具体地证明没有ORDER BY语句的查询的行的返回顺序是不可靠的,最好是通过导致并显示隐式顺序的细分,即使有问题的表没有更新或编辑

  2. 如果数据只一次性插入一次,然后再也不更新,这有什么区别吗?

我更喜欢基于 postgres 的答案,因为这是我最熟悉的答案,但我对理论本身更感兴趣。

ype*_*eᵀᴹ 31

我看到了三种尝试说服他们的方法:

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

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

  3. 在这方面提供各种 DBMS 的文档。例如:

PostgreSQL :

排序行

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

SQL 服务器

SELECT-ORDER BY子句(Transact-SQL)

对 SQL Server 中的查询返回的数据进行排序。使用该条款:

按指定的列列表对查询的结果集进行排序,并可选择将返回的行限制在指定范围内。除非ORDER BY指定了子句,否则无法保证结果集中返回行的顺序。

甲骨文

order_by_clause

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

  • 文档应该足够了。其他任何事情都是事后猜测,无论如何,无论您证明什么,都永远不会被视为确定的。它永远是*你做过的*和可解释的,可能由你承担,而不是*某事*。有了文档,以书面形式提交您的“保证”,并简单地寻求书面许可,不要按要求的顺序返回行(您不会得到它)。 (10认同)
  • 如果订单很重要,那么负责审查他们代码的人应该拒绝,直到他们使用 ORDER BY。DBMS 的开发人员(Oracle、SQL Server、Postgres)都对他们的产品保证什么和不保证什么都说同样的话(而且他们的报酬比我高得多,所以他们知道他们在说什么,除了已经构建了这些该死的事物)。 (6认同)

mus*_*cio 20

这又是一个黑天鹅的故事。如果你还没有见过,并不意味着它们不存在。希望在您的情况下,它不会导致另一场世界范围的金融危机,只会导致一些不满意的客户。

Postgres文档明确说明了这一点:

如果未给出 ORDER BY,则以系统发现最快生成的任何顺序返回行。

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

您的同事正在犯草率概括谬误。为了反驳他们的观点,证明他们的假设只错了一次就足够了,例如这个 dbfiddle


Han*_*non 12

考虑以下示例,其中我们有三个相关表。订单、用户和订单详细信息。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);
Run Code Online (Sandbox Code Playgroud)

在这里,我们查询 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
Run Code Online (Sandbox Code Playgroud)

查询的输出如下所示:

??????????????????????????????????????????
? 订单详情ID ? 订单号?用户身份 ?
??????????????????????????????????????????
? 2200115 ? 2 ? 15 ?
? 630215?3 ? 15 ?
? 1990215 ? 3 ? 15 ?
? 4960215 ? 3 ? 15 ?
? 100715?8 ? 15 ?
? 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;
Run Code Online (Sandbox Code Playgroud)
??????????????????????????????????????????
? 订单详情ID ? 订单号?用户身份 ?
??????????????????????????????????????????
? 3915?40 ? 15 ?
? 100715?8 ? 15 ?
? 221815?19 ? 15 ?
? 299915?100 ? 15 ?
? 368215?83 ? 15 ?
? 603815?39 ? 15 ?
? 630215?3 ? 15 ?
? 728515?86 ? 15 ?
? 972215 ? 23 ? 15 ?
? 992015?21 ? 15 ?
? 1017115?72 ? 15 ?
? 1113815?39 ? 15 ?
??????????????????????????????????????????

如果行的顺序是必要的,并且您的工程师知道顺序是必要的,他们应该只想使用ORDER BY语句,因为如果出现与错误顺序相关的失败,他们可能会失去他们的指定。

第二个可能更有启发性的例子,使用OrderDetails上面的表,我们没有连接任何其他表,但有一个简单的要求来找到匹配 OrderID 和 UserID 的行,我们看到了问题。

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

CREATE INDEX OrderDetailsOrderIDUserID ON dbo.OrderDetails(OrderID, UserID);
Run Code Online (Sandbox Code Playgroud)

这是查询:

SELECT od.OrderDetailsID
FROM dbo.OrderDetails od
WHERE od.OrderID = 15
    AND (od.UserID = 21 OR od.UserID = 22)
Run Code Online (Sandbox Code Playgroud)

结果:

??????????????????
? 订单详情ID ?
??????????????????
? 21421?
? 5061421?
? 7091421?
? 691422?
? 3471422?
? 7241422?
??????????????????

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

这些模型只是简单的例子,在没有明确ORDER BY声明的情况下,行不能保证“有序” 。像这样的例子还有很多,而且由于 DBMS 引擎代码更改非常频繁,因此特定行为可能会随着时间的推移而发生变化。


JoL*_*JoL 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)
Run Code Online (Sandbox Code Playgroud)

我认为这种现有隐式排序的规则没有记录在任何地方,肯定会在没有通知的情况下更改,并且绝对不是跨数据库引擎的可移植行为。