Como provar a falta de ordem implícita em um banco de dados?

21

Recentemente, expliquei aos colegas a importância de ter uma coluna para classificar os dados em uma tabela do banco de dados, se necessário, por exemplo, para dados ordenados cronologicamente. Isso se mostrou um pouco difícil, porque eles poderiam simplesmente executar novamente sua consulta aparentemente sem parar e sempre retornaria o mesmo conjunto de linhas na mesma ordem.

Já notei isso antes e tudo o que realmente pude fazer é insistir que eles confiem em mim e não simplesmente assumir que uma tabela de banco de dados se comportará como um arquivo CSV ou Excel tradicional.

Por exemplo, executando a consulta (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');

criará uma tabela com uma ordem conceitual clara. Selecionar esses mesmos dados da maneira mais simples seria:

SELECT * FROM mytable;

Sempre me fornece os seguintes resultados:

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

Eu posso fazer isso repetidamente e sempre retornará para mim os mesmos dados na mesma ordem. No entanto, eu sei que essa ordem implícita pode ser quebrada, já a vi antes, principalmente em grandes conjuntos de dados, nos quais algum valor aleatório aparentemente será jogado no local "errado" quando selecionado. Mas me ocorreu que não sei como isso acontece ou como reproduzi-lo. Acho difícil obter resultados no Google porque a consulta de pesquisa tende a retornar apenas uma ajuda geral na classificação dos conjuntos de resultados.

Então, minhas perguntas são essencialmente estas:

  1. Como posso demonstrar de forma demonstrável e concreta que a ordem de retorno de linhas de uma consulta sem uma ORDER BYinstrução não é confiável, de preferência causando e mostrando uma discriminação da ordem implícita, mesmo quando a tabela em questão não é atualizada ou editada ?

  2. Faz alguma diferença se os dados são inseridos apenas uma vez em massa e nunca mais são atualizados?

Eu preferiria uma resposta baseada no postgres, já que essa é a que eu estou mais familiarizada, mas estou mais interessada na própria teoria.


fonte
6
"Nunca gravado ou atualizado novamente" - por que isso é uma tabela? Parece um arquivo. Ou um enum. Ou algo que não precisa estar em um banco de dados. Se for cronológico, não há uma coluna de data para ordenar? Se a cronologia importa, você acha que essas informações seriam importantes o suficiente para serem exibidas na tabela. De qualquer forma, os planos podem mudar devido a alguém abandonar ou criar um novo índice ou eventos como alterações de memória, sinalizadores de rastreamento ou outras influências. O argumento deles soa como “Eu nunca uso o cinto de segurança e nunca passei pelo para-brisa, por isso continuarei sem usar o cinto de segurança.” :-(
Aaron Bertrand
9
Alguns problemas lógicos simplesmente não podem ser resolvidos tecnicamente ou sem o envolvimento do RH. Se sua empresa deseja permitir práticas de desenvolvedor que dependem de acreditar no vodu e ignorar a documentação, e seu caso de uso é realmente limitado a uma pequena tabela que nunca é atualizada, deixe-os seguir o caminho e atualize seu currículo. Não vale a pena discutir.
Aaron Bertrand
1
Você não tem base para reivindicar "sempre". Você só pode reivindicar "sempre", "quando marquei". A linguagem tem uma definição - esse é o contrato com o usuário.
philipxy 25/01
10
Estou curioso para saber por que esses colegas são contra a adição da order bycláusula às consultas deles? Eles estão tentando economizar no armazenamento do código-fonte? desgaste do teclado? tempo necessário para digitar a temida cláusula?
mustaccio 25/01
2
Eu sempre pensei que os mecanismos de banco de dados deveriam permutar aleatoriamente as primeiras linhas de consultas para as quais a semântica não garante um pedido, para ajudar a facilitar o teste.
Doug McClean

Respostas:

30

Eu vejo três maneiras de tentar convencê-los:

  1. Deixe-os tentar a mesma consulta, mas com tabela maior (mais número de linhas) ou quando a tabela estiver sendo atualizada entre as execuções. Ou novas linhas são inseridas e algumas antigas são excluídas. Ou um índice é adicionado ou removido entre as execuções. Ou a tabela é aspirada (no Postgres). Ou os índices são reconstruídos (no SQL Server). Ou a tabela é alterada de cluster para um heap. Ou o serviço de banco de dados é reiniciado.

  2. Você pode sugerir que eles provem que execuções diferentes retornarão a mesma ordem. Eles podem provar isso? Eles podem fornecer uma série de testes que comprovam que qualquer consulta fornecerá o resultado na mesma ordem, não importa quantas vezes seja executada?

  3. Forneça a documentação de vários DBMS nesse assunto. Por exemplo:

PostgreSQL :

Classificando linhas

Depois que uma consulta produziu uma tabela de saída (depois que a lista de seleção foi processada), ela pode opcionalmente ser classificada. Se a classificação não for escolhida, as linhas serão retornadas em uma ordem não especificada. A ordem real nesse caso dependerá dos tipos de plano de varredura e associação e da ordem no disco, mas não deve ser invocada. Uma ordem de saída específica só pode ser garantida se a etapa de classificação for escolhida explicitamente.

SQL Server :

SELECT- ORDER BYCláusula (Transact-SQL)

Classifica os dados retornados por uma consulta no SQL Server. Use esta cláusula para:

Ordene o conjunto de resultados de uma consulta pela lista de colunas especificada e, opcionalmente, limite as linhas retornadas a um intervalo especificado. A ordem na qual as linhas são retornadas em um conjunto de resultados não é garantida, a menos que uma ORDER BYcláusula seja especificada.

Oracle :

order_by_clause

Use a ORDER BYcláusula para ordenar as linhas retornadas pela instrução Sem uma cláusula order_by_, não existe garantia de que a mesma consulta executada mais de uma vez recupere linhas na mesma ordem.

ypercubeᵀᴹ
fonte
Com tabelas muito pequenas que não são modificadas, você pode ver esse comportamento. Isso é esperado. Mas também não é garantido. A ordem pode mudar porque você adicionou um índice ou modificou um índice ou reiniciou o banco de dados e possivelmente muitos outros casos.
ypercubeᵀᴹ
6
Se o pedido for importante, quem for responsável por revisar seu código deverá rejeitar até usar ORDER BY. Os desenvolvedores dos DBMSs (Oracle, SQL Server, Postgres) dizem o mesmo sobre o que o seu produto garante e o que não (e são pagos muito mais do que eu serei, para que saibam o que estão dizendo, além de terem construído esses malditos coisas).
ypercubeᵀᴹ
1
Mesmo que o pedido pareça o mesmo agora, é certo que essas tabelas nunca serão atualizadas durante toda a vida útil do software que você está construindo? Que mais linhas não serão inseridas, nunca?
ypercubeᵀᴹ
1
Existe uma garantia de que essa tabela sempre será tão pequena? Existe uma garantia de que não serão adicionadas mais colunas? Eu posso ver dezenas de casos diferentes em que a tabela pode ser alterada no futuro (e algumas dessas alterações podem afetar a ordem de um resultado da consulta). Eu sugiro que você peça para eles responderem a tudo isso. Eles podem garantir que nada disso aconteça? E por que eles não adicionam um simples ORDER BY, que garanta o pedido, não importa como a tabela vai mudar ? Por que não adicionar um cofre, que não faz mal?
ypercubeᵀᴹ
10
A documentação deve ser suficiente. Qualquer outra coisa é questionada e, de qualquer forma, nunca será vista como definitiva, não importa o que você prove. Sempre será algo que você fez e pode ser explicado, provavelmente às suas custas, e não algo que seja . Armado com a documentação, envie sua "garantia" por escrito e simplesmente peça permissão por escrito para não retornar as linhas na ordem necessária (você não a receberá).
19

Esta é a história do cisne negro mais uma vez. Se você ainda não viu um, isso não significa que eles não existem. Felizmente, no seu caso, isso não levará a outra crise financeira mundial, simplesmente a alguns clientes insatisfeitos.

A documentação do Postgres diz isso explicitamente:

Se ORDER BY não for fornecido, as linhas serão retornadas na ordem que o sistema achar mais rápido para produzir.

"O sistema" neste caso compreende o próprio daemon do postgres (incluindo a implementação de seus métodos de acesso a dados e o otimizador de consultas), o sistema operacional subjacente, o layout lógico e físico do armazenamento do banco de dados, possivelmente até os caches da CPU. Como você, como usuário do banco de dados, não tem controle sobre essa pilha, não deve confiar que ela continuará a se comportar para sempre da maneira como se comporta neste minuto.

Seus colegas estão cometendo a falácia generalizada apressada . Para refutar seu argumento, basta mostrar que sua suposição está errada apenas uma vez, por exemplo, com este dbfiddle .

mustaccio
fonte
12

Considere o exemplo a seguir, onde temos três tabelas relacionadas. Pedidos, Usuários e Detalhes do Pedido. OrderDetails está vinculado a chaves estrangeiras na tabela Orders e na tabela Users. Essa é essencialmente uma configuração muito típica para bancos de dados relacionais; indiscutivelmente todo o propósito de um DBMS relacional .

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);

Aqui, estamos consultando a tabela OrderDetails em que o UserID é 15:

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

A saída da consulta é semelhante a:

╔════════════════╦═════════╦════════╗
║ OrderDetailsID ║ OrderID ║ UserID ║
╠════════════════╬═════════╬════════╣
00 2200115 ║ 2 ║ 15 ║
30 630215 ║ 3 ║ 15 ║
║ 1990215 ║ 3 ║ 15 ║
60 4960215 ║ 3 ║ 15 ║
7 100715 ║ 8 ║ 15 ║
30 3930815 ║ 9 ║ 15 ║
║ 6310815 ║ 9 ║ 15 ║
║ 4441015 ║ 11 ║ 15 ║
7 2171315 ║ 14 ║ 15 ║
3 3431415 ║ 15 ║ 15 ║
║ 4571415 ║ 15 ║ 15 ║
2 6421515 ║ 16 ║ 15 ║
7 2271715 ║ 18 ║ 15 ║
║ 2601715 ║ 18 ║ 15 ║
║ 3521715 ║ 18 ║ 15 ║
18 221815 ║ 19 ║ 15 ║
║ 3381915 ║ 20 ║ 15 ║
║ 4471915 ║ 20 ║ 15 ║
╚════════════════╩═════════╩════════╝

Como você pode ver, a ordem das linhas de saída não corresponde à ordem das linhas na tabela OrderDetails.

Adicionar um explícito ORDER BYgarante que as linhas sejam retornadas ao cliente na ordem desejada:

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 ║ UserID ║
╠════════════════╬═════════╬════════╣
15 3915 ║ 40 ║ 15 ║
7 100715 ║ 8 ║ 15 ║
18 221815 ║ 19 ║ 15 ║
99 299915 ║ 100 ║ 15 ║
82 368215 ║ 83 ║ 15 ║
38 603815 ║ 39 ║ 15 ║
30 630215 ║ 3 ║ 15 ║
28 728515 ║ 86 ║ 15 ║
22 972215 ║ 23 ║ 15 ║
201 992015 ║ 21 ║ 15 ║
17 1017115 ║ 72 ║ 15 ║
13 1113815 ║ 39 ║ 15 ║
╚════════════════╩═════════╩════════╝

Se a ordem de linhas é imperativo, e seus engenheiros sabem que a ordem é imperativo, eles devem sempre apenas quer usar um ORDER BYcomunicado, uma vez que pode custar-lhes a sua designação, se houve uma falha relacionada à ordem incorreta.

Um segundo exemplo, talvez mais instrutivo, usando a OrderDetailstabela acima, em que não estamos juntando nenhuma outra tabela, mas com um requisito simples de encontrar linhas que correspondam ao Código do Pedido e ao Código do Usuário, vemos o problema.

Criaremos um índice para dar suporte à consulta, como você provavelmente faria na vida real se o desempenho for de alguma forma importante (quando não é?).

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

Aqui está a consulta:

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

E os resultados:

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

A adição de uma ORDER BYcláusula garantirá definitivamente que também obtemos a classificação correta aqui.

Esses modelos são apenas exemplos simples, onde não é garantido que as linhas estejam "em ordem" sem uma ORDER BYdeclaração explícita . Existem muitos outros exemplos como esse e, como o código do mecanismo DBMS muda com bastante frequência, o comportamento específico pode mudar com o tempo.

Max Vernon
fonte
10

Como um exemplo prático, no Postgres, a ordem atualmente muda quando você atualiza uma linha:

% 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)

Não acho que as regras dessa ordem implícita existente estejam documentadas em nenhum lugar, estejam definitivamente sujeitas a alterações sem aviso prévio e, definitivamente, não sejam portáveis ​​nos mecanismos de banco de dados.

JoL
fonte
Ele está documentado: A resposta de ypercube cita a documentação nos dizendo que a ordem é não especificado.
Lightness Races com Monica
@LightnessRacesinOrbit Eu consideraria isso como a documentação explicitamente nos dizendo que não está documentada. Quero dizer, também é verdade que qualquer coisa que não esteja na documentação não é especificada. É um tipo de tautologia. Enfim, editei essa parte da resposta para ser mais específica.
JoL 28/01
3

não exatamente uma demonstração, mas muito tempo para comentar.

Em tabelas grandes, alguns bancos de dados fazem varreduras paralelas intercaladas:

Se duas consultas desejarem varrer a mesma tabela e chegarem quase ao mesmo tempo, a primeira poderá fazer parte da tabela quando a segunda iniciar.

A segunda consulta pode receber registros começando no meio da tabela (quando a primeira consulta está sendo concluída) e, em seguida, receber os registros desde o início da tabela.

Jasen
fonte
2

Crie um índice clusterizado que tenha a ordem "incorreta". Por exemplo, cluster em ID DESC. Isso geralmente gera a ordem inversa (embora isso também não seja garantido).

usr
fonte