Comment prouver le manque d'ordre implicite dans une base de données?


21

Récemment, j'expliquais à mes collègues l'importance d'avoir une colonne permettant de trier les données dans une table de base de données si cela est nécessaire, par exemple pour les données classées chronologiquement. Cela s'est révélé quelque peu difficile car ils pouvaient simplement réexécuter leur requête en apparence sans fin et cela renvoyait toujours le même ensemble de lignes dans le même ordre.

J'ai déjà remarqué cela et tout ce que je pouvais vraiment faire, c'est insister pour qu'ils me fassent confiance et ne pas simplement supposer qu'une table de base de données se comportera comme un fichier CSV ou Excel traditionnel.

Par exemple, exécuter la requête (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');

va créer un tableau avec un ordre conceptuel clair. La sélection de ces mêmes données de la manière la plus simple serait:

SELECT * FROM mytable;

Me donne toujours les résultats suivants:

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

Je peux le faire encore et encore et il me renverra toujours les mêmes données dans le même ordre. Cependant, je sais que cet ordre implicite peut être rompu, je l'ai déjà vu, en particulier dans les grands ensembles de données, où une valeur aléatoire sera apparemment jetée au «mauvais» endroit lorsqu'elle est sélectionnée. Mais il m'est venu à l'esprit que je ne sais pas comment cela se produit ni comment le reproduire. J'ai du mal à obtenir des résultats sur Google, car la requête de recherche a tendance à renvoyer une aide générale sur le tri des jeux de résultats.

Donc, mes questions sont essentiellement les suivantes:

  1. Comment puis-je prouver de façon concrète et concrète que l'ordre de retour des lignes d'une requête sans ORDER BYinstruction n'est pas fiable, de préférence en provoquant et en montrant une ventilation de l'ordre implicite même lorsque la table en question n'est pas mise à jour ou modifiée ?

  2. Cela fait-il une différence si les données ne sont insérées qu'une seule fois en masse et ne sont plus jamais mises à jour?

Je préférerais une réponse basée sur les post-gres car c'est celle que je connais le mieux mais je suis plus intéressé par la théorie elle-même.


6
"Jamais écrit ou mis à jour à nouveau" - pourquoi est-ce un tableau? Cela ressemble à un fichier. Ou une énumération. Ou quelque chose qui n'a pas besoin d'être dans une base de données. Si c'est chronologique, n'y a-t-il pas une colonne de date pour commander? Si la chronologie est importante, vous penseriez que les informations seraient suffisamment importantes pour figurer dans le tableau. Quoi qu'il en soit, les plans peuvent changer en raison de la chute ou de la création d'un nouvel index par quelqu'un, ou d'événements tels que des changements de mémoire, des indicateurs de trace ou d'autres influences. Leur argument sonne comme "Je ne porte jamais ma ceinture de sécurité et je n'ai jamais traversé mon pare-brise, donc je continuerai à ne pas porter ma ceinture de sécurité." :-(
Aaron Bertrand

9
Certains problèmes de logique ne peuvent tout simplement pas être résolus techniquement ou sans implication des RH. Si votre entreprise souhaite autoriser les développeurs à s'appuyer sur le vaudou et à ignorer la documentation, et que votre cas d'utilisation est vraiment limité à une petite table qui n'est jamais mise à jour, laissez-les faire leur chemin et mettez à jour votre CV. Ça ne vaut pas la peine de discuter.
Aaron Bertrand

1
Vous n'avez aucune base pour prétendre "sera toujours". Vous ne pouvez prétendre que "a toujours", "lorsque j'ai vérifié". La langue a une définition - c'est le contrat avec l'utilisateur.
philipxy

10
Je suis curieux de savoir pourquoi ces collègues sont contre l'ajout de la order byclause à leurs requêtes? Essayent-ils d'économiser sur le stockage du code source? l'usure du clavier? le temps qu'il faut pour taper la clause redoutée?
mustaccio

2
J'ai toujours pensé que les moteurs de base de données devraient permuter de manière aléatoire les premières lignes de requêtes pour lesquelles la sémantique ne garantit pas un ordre, afin de faciliter les tests.
Doug McClean

Réponses:


30

Je vois trois façons d'essayer de les convaincre:

  1. Laissez-les essayer la même requête mais avec une table plus grande (plus de nombre de lignes) ou lorsque la table est mise à jour entre les exécutions. Ou de nouvelles lignes sont insérées et certaines anciennes sont supprimées. Ou un index est ajouté ou supprimé entre les exécutions. Ou la table est aspirée (en Postgres). Ou les index sont reconstruits (dans SQL Server). Ou la table est passée de cluster à un tas. Ou le service de base de données est redémarré.

  2. Vous pouvez suggérer qu'ils prouvent que différentes exécutions renverront le même ordre. Peuvent-ils le prouver? Peuvent-ils fournir une série de tests prouvant que toute requête donnera le résultat dans le même ordre, quel que soit le nombre de fois où elle sera exécutée?

  3. Fournissez la documentation des divers SGBD à ce sujet. Par exemple:

PostgreSQL :

Tri des lignes

Une fois qu'une requête a produit une table de sortie (après que la liste de sélection a été traitée), elle peut éventuellement être triée. Si le tri n'est pas choisi, les lignes seront retournées dans un ordre non spécifié. L'ordre réel dans ce cas dépendra des types de plan d'analyse et de jointure et de l'ordre sur le disque, mais il ne faut pas s'y fier. Un ordre de sortie particulier ne peut être garanti que si l'étape de tri est explicitement choisie.

SQL Server :

SELECT- ORDER BYClause (Transact-SQL)

Trie les données renvoyées par une requête dans SQL Server. Utilisez cette clause pour:

Triez le jeu de résultats d'une requête selon la liste de colonnes spécifiée et, éventuellement, limitez les lignes renvoyées à une plage spécifiée. L'ordre dans lequel les lignes sont renvoyées dans un jeu de résultats n'est pas garanti, sauf si une ORDER BYclause est spécifiée.

Oracle :

order_by_clause

Utilisez la ORDER BYclause pour ordonner les lignes renvoyées par l'instruction. Sans order_by_clause, aucune garantie n'existe que la même requête exécutée plus d'une fois récupérera les lignes dans le même ordre.


Avec de très petites tables qui ne sont pas modifiées, vous pouvez voir ce comportement. C'est prévu. Mais ce n'est pas garanti non plus. L'ordre peut changer car vous avez ajouté un index ou vous avez modifié un index ou vous avez redémarré la base de données et peut-être de nombreux autres cas.
ypercubeᵀᴹ

6
Si la commande est importante, la personne responsable de la révision de son code doit la rejeter jusqu'à ce qu'elle utilise ORDER BY. Les développeurs des SGBD (Oracle, SQL Server, Postgres) disent tous la même chose de ce que leur produit garantit et de ce qui ne l'est pas (et ils sont payés beaucoup plus que moi, donc ils savent ce qu'ils disent, en plus d'avoir construit ces fichus des choses).
ypercubeᵀᴹ

1
Même si la commande est la même maintenant, est-il certain que ces tableaux ne seront jamais mis à jour pendant toute la durée de vie du logiciel que vous construisez? Qu'aucune ligne supplémentaire ne sera insérée, jamais?
ypercubeᵀᴹ

1
Y a-t-il une garantie que cette table sera toujours aussi petite? Y a-t-il une garantie qu'aucune autre colonne ne sera ajoutée? Je peux voir des dizaines de cas différents où la table peut être modifiée à l'avenir (et certaines de ces modifications peuvent affecter l'ordre d'un résultat de requête). Je vous suggère de leur demander de répondre à toutes ces questions. Peuvent-ils garantir que rien de tel ne se produira jamais? Et pourquoi n'ajoutent-ils pas un simple ORDER BY, qui garantira la commande, peu importe comment la table va changer ? Pourquoi ne pas ajouter un coffre-fort, qui ne fait pas de mal?
ypercubeᵀᴹ

10
La documentation doit être suffisante. Tout le reste est une supposition, et en tout cas, ne sera jamais considéré comme définitif, peu importe ce que vous prouvez. Ce sera toujours quelque chose que vous avez fait et explicable, probablement à vos frais, plutôt que quelque chose qui l'est . Armé de la documentation, soumettez votre "garantie" par écrit et demandez simplement l'autorisation écrite de ne pas retourner les lignes dans l'ordre requis (vous ne l'obtiendrez pas).

19

C'est à nouveau l'histoire du cygne noir. Si vous n'en avez pas encore vu, cela ne signifie pas qu'ils n'existent pas. Espérons que dans votre cas, cela ne conduira pas à une autre crise financière mondiale, simplement à quelques clients mécontents.

La documentation Postgres le dit explicitement:

Si ORDER BY n'est pas indiqué, les lignes sont renvoyées dans l'ordre que le système trouve le plus rapide à produire.

"Le système" dans ce cas comprend le démon postgres lui-même (y compris la mise en œuvre de ses méthodes d'accès aux données et l'optimiseur de requêtes), le système d'exploitation sous-jacent, la disposition logique et physique du stockage de la base de données, éventuellement même des caches CPU. Étant donné que vous, en tant qu'utilisateur de la base de données, n'avez aucun contrôle sur cette pile, vous ne devez pas vous fier à ce qu'elle continue de se comporter indéfiniment comme elle se comporte cette minute.

Vos collègues commettent l' erreur de généralisation hâtive . Pour réfuter leur argument, il suffit de montrer que leur supposition n'est fausse qu'une seule fois, par exemple par ce dbfiddle .


12

Prenons l'exemple suivant, où nous avons trois tables liées. Commandes, utilisateurs et détails de la commande. OrderDetails est lié avec des clés étrangères à la table Orders et à la table Users. Il s'agit essentiellement d'une configuration très typique pour les bases de données relationnelles; sans doute tout le but d'un SGBD relationnel .

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

Ici, nous interrogeons la table OrderDetails où le UserID est 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

La sortie de la requête ressemble à:

╔════════════════╦═════════╦════════╗
║ OrderDetailsID ║ OrderID ║ UserID ║
╠════════════════╬═════════╬════════╣
║ 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 ║
╚════════════════╩═════════╩════════╝

Comme vous pouvez le voir, l'ordre de sortie des lignes ne correspond pas à l'ordre des lignes dans la table OrderDetails.

L'ajout d'une explicite ORDER BYgarantit que les lignes seront retournées au client dans l'ordre souhaité:

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 ║
╠════════════════╬═════════╬════════╣
║ 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 ║
╚════════════════╩═════════╩════════╝

Si l'ordre des lignes est impératif et que vos ingénieurs savent que l'ordre est impératif, ils ne devraient jamais vouloir utiliser une ORDER BYinstruction, car cela pourrait leur coûter leur désignation s'il y avait une défaillance liée à un ordre incorrect.

Un deuxième exemple, peut-être plus instructif, utilisant le OrderDetailstableau ci-dessus, où nous ne joignons aucun autre tableau, mais où nous avons simplement besoin de trouver des lignes correspondant à la fois à OrderID et à UserID, nous voyons le problème.

Nous allons créer un index pour prendre en charge la requête, comme vous le feriez probablement dans la vie réelle si les performances sont importantes (quand n'est-ce pas?).

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

Voici la requête:

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

Et les résultats:

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

L'ajout d'une ORDER BYclause garantira très certainement que nous obtenons le bon tri ici également.

Ces maquettes ne sont que de simples exemples où les lignes ne sont pas garanties d'être "en ordre" sans ORDER BYdéclaration explicite . Il existe de nombreux autres exemples comme celui-ci, et comme le code du moteur de SGBD change assez fréquemment, le comportement spécifique peut changer avec le temps.


10

À titre d'exemple pratique, dans Postgres, l'ordre change actuellement lorsque vous mettez à jour une ligne:

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

Je ne pense pas que les règles de cette commande implicite existante soient documentées nulle part, soient définitivement sujettes à changement sans préavis et ne sont certainement pas un comportement portable sur les moteurs de base de données.


Elle est documentée: la réponse de ypercube cite la documentation nous indiquant que la commande n'est pas spécifiée.
Courses de légèreté avec Monica le

@LightnessRacesinOrbit Je considérerais cela comme la documentation nous disant explicitement que ce n'est pas documenté. Je veux dire, il est également vrai que tout ce qui n'est pas dans la documentation n'est pas spécifié. C'est une sorte de tautologie. Quoi qu'il en soit, j'ai modifié cette partie de la réponse pour être plus précis.
JoL

3

pas exactement une démo, mais trop long pour un commentaire.

Sur les grandes tables, certaines bases de données effectueront des analyses parallèles entrelacées:

Si deux requêtes souhaitent analyser la même table et arriver presque en même temps, la première peut être à mi-chemin dans la table lorsque la seconde démarre.

La deuxième requête peut recevoir des enregistrements à partir du milieu de la table (à la fin de la première requête), puis recevoir les enregistrements à partir du début de la table.


2

Créez un index cluster qui a le «mauvais» ordre. Par exemple, cluster sur ID DESC. Cela produira souvent l'ordre inverse (bien que cela ne soit pas garanti non plus).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.