web-dev-qa-db-fra.com

Ordre arbitraire des enregistrements dans une table

Un besoin courant lors de l'utilisation d'une base de données est d'accéder aux enregistrements dans l'ordre. Par exemple, si j'ai un blog, je veux pouvoir réorganiser mes articles de blog dans un ordre arbitraire. Ces entrées ont souvent beaucoup de relations, donc une base de données relationnelle semble logique.

La solution courante que j'ai vue est d'ajouter une colonne entière order:

CREATE TABLE AS your_table (id, title, sort_order)
AS VALUES
  (0, 'Lorem ipsum',   3),
  (1, 'Dolor sit',     2),
  (2, 'Amet, consect', 0),
  (3, 'Elit fusce',    1);

Ensuite, nous pouvons trier les lignes par order pour les obtenir dans le bon ordre.

Cependant, cela semble maladroit:

  • Si je veux déplacer l'enregistrement 0 au début, je dois réorganiser chaque enregistrement
  • Si je veux insérer un nouvel enregistrement au milieu, je dois réorganiser chaque enregistrement après
  • Si je veux supprimer un enregistrement, je dois réorganiser chaque enregistrement après

Il est facile d'imaginer des situations comme:

  • Deux enregistrements ont le même order
  • Il y a des lacunes dans le order entre les enregistrements

Cela pourrait se produire assez facilement pour un certain nombre de raisons.

Voici l'approche adoptée par des applications comme Joomla:

Example of Joomla's approach to ordering

Vous pourriez dire que l'interface ici est mauvaise, et qu'au lieu que les humains éditent directement les nombres, ils devraient utiliser des flèches ou glisser-déposer - et vous auriez probablement raison. Mais dans les coulisses, la même chose se produit.

Certaines personnes ont proposé d'utiliser une décimale pour stocker la commande, de sorte que vous pouvez utiliser "2,5" pour insérer un enregistrement entre les enregistrements de l'ordre 2 et 3. Et bien que cela aide un peu, c'est sans doute encore plus compliqué car vous pouvez vous retrouver avec décimales bizarres (où vous arrêtez-vous? 2,75? 2,875? 2,8125?)

Existe-t-il une meilleure façon de stocker la commande dans une table?

30
Tom Marthenal

Si je veux déplacer l'enregistrement 0 au début, je dois réorganiser chaque enregistrement

Non, il existe un moyen plus simple.

update your_table
set order = -1 
where id = 0;

Si je veux insérer un nouvel enregistrement au milieu, je dois réorganiser chaque enregistrement après

C'est vrai, sauf si vous utilisez un type de données qui prend en charge les valeurs "entre". Les types flottants et numériques vous permettent de mettre à jour une valeur à, disons, 2,5. Mais varchar (n) fonctionne aussi. (Pensez "a", "b", "c"; puis pensez "ba", "bb", "bc".)

Si je veux supprimer un enregistrement, je dois réorganiser chaque enregistrement après

Non, il existe un moyen plus simple. Supprimez simplement la ligne. Les lignes restantes seront toujours triées correctement.

Il est facile d'imaginer des situations comme:

Deux enregistrements ont le même ordre

Une contrainte unique peut empêcher cela.

Il y a des lacunes dans l'ordre entre les enregistrements

Les lacunes n'ont aucun effet sur la façon dont un dbms trie les valeurs dans une colonne.

Certaines personnes ont proposé d'utiliser une décimale pour stocker la commande, de sorte que vous pouvez utiliser "2,5" pour insérer un enregistrement entre les enregistrements de l'ordre 2 et 3. Et bien que cela aide un peu, c'est sans doute encore plus compliqué car vous pouvez vous retrouver avec décimales bizarres (où vous arrêtez-vous? 2,75? 2,875? 2,8125?)

Vous ne vous arrêtez pas avant d'avoir avoir pour. Le dbms a non problème de tri des valeurs qui ont 2, 7 ou 15 places après la virgule décimale.

Je pense que votre réel problème est que vous souhaitez voir les valeurs dans l'ordre trié sous forme d'entiers. Vous pouvez le faire.

create table your_table (
  id int primary key, 
  title varchar(13), 
  sort_order float
);

insert into your_table values
(0, 'Lorem ipsum', 2.0),
(1, 'Dolor sit', 1.5),
(2, 'Amet, consect', 0.0),
(3, 'Elit fusce', 1.0);

-- This windowing function will "transform" the floats into sorted integers.
select id, title,
       row_number() over (order by sort_order)
from your_table

C'est très simple. Vous devez avoir une structure de "trou de cardinalité":

Vous devez avoir 2 colonnes:

  1. pk = 32 bits integer
  2. order = 64bit bigint (pasdouble)

Insérer/mettre à jour

  1. Lors de l'insertion du premier nouvel enregistrement, définissez order = round(max_bigint / 2).
  2. Lors de l'insertion au début du tableau, définissez order = round("order of first record" / 2)
  3. Lors de l'insertion à la fin du tableau, définissez order = round("max_bigint - order of last record" / 2) 4) Lors de l'insertion au milieu, définissez order = round("order of record before - order of record after" / 2)

Cette méthode a une très grande cardinalité. Si vous avez une erreur de contrainte ou si vous pensez que vous avez une petite cardinalité, vous pouvez reconstruire la colonne d'ordre (normaliser).

En situation maximale avec normalisation (avec cette structure) vous pouvez avoir un "trou de cardinalité" en 32 bits.

N'oubliez pas de ne pas utiliser de types à virgule flottante - l'ordre doit être une valeur précise!

8
user2382679

Généralement, la commande est effectuée en fonction de certaines informations contenues dans les enregistrements, le titre, l'ID ou tout ce qui est approprié pour cette situation particulière.

Si vous avez besoin d'une commande spéciale, l'utilisation d'une colonne entière n'est pas aussi mauvaise que cela puisse paraître. Par exemple, pour faire de la place pour un disque pour aller en 5e place, vous pouvez faire quelque chose comme:

update table_1 set place = place + 1 where place > 5.

Avec un peu de chance, vous pouvez déclarer que la colonne est unique et peut-être avoir une procédure pour faire des réarrangements "atomiques". Les détails dépendent du système mais c'est l'idée générale.

4
igelkott

… C'est sans doute encore plus compliqué parce que vous pouvez vous retrouver avec des décimales étranges (où vous arrêtez-vous? 2,75? 2,875? 2,8125?)

On s'en fout? Ces chiffres ne sont là que pour que l'ordinateur puisse les traiter, peu importe le nombre de chiffres fractionnaires dont ils disposent ou leur laideur.

L'utilisation de valeurs décimales signifie que pour déplacer l'élément F entre les éléments J et K, tout ce que vous avez à faire est de sélectionner les valeurs de commande pour J et K, puis de les faire la moyenne puis de mettre à jour F. Deux instructions SELECT et une instruction UPDATE (probablement effectuées en utilisant un isolement sérialisable pour éviter blocages).

Si vous souhaitez voir des entiers plutôt que des fractions dans la sortie, calculez les entiers dans l'application cliente ou utilisez les fonctions ROW_NUMBER () ou RANK () (si votre SGBDR les inclut).

4
Greenstone Walker

Dans mon propre projet, je prévois d'essayer une solution similaire à la solution du nombre décimal, mais en utilisant des tableaux d'octets à la place:

def pad(x, x_len, length):
    if x_len >= length:
        return x
    else:
        for _ in range(length - x_len):
            x += b"\x00"
        return x

def order_index(_from, _to, count, length=None):
    assert _from != _to
    assert _from < _to

    if not length:
        from_len = len(_from)
        to_len = len(_to)
        length = max(from_len, to_len)

        _from = pad(_from, from_len, length)
        _to = pad(_to, to_len, length)

    from_int = int.from_bytes(_from, "big")
    to_int = int.from_bytes(_to, "big")
    inc = (to_int - from_int)//(count + 1)
    if not inc:
        length += 1
        _from += b"\x00"
        _to += b"\x00"
        return order_index(_from, _to, count, length)

    return (int.to_bytes(from_int + ((x+1)*inc), length, "big") for x in range(count))
>>> index = order_index(b"A", b"Z", 24)
>>> [x for x in index]
[b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y']
>>> 
>>> index = order_index(b"A", b"Z", 25)
>>> [x for x in index]
[b'A\xf6', b'B\xec', b'C\xe2', b'D\xd8', b'E\xce', b'F\xc4', b'G\xba', b'H\xb0', b'I\xa6', b'J\x9c', b'K\x92', b'L\x88', b'M~', b'Nt', b'Oj', b'P`', b'QV', b'RL', b'SB', b'T8', b'U.', b'V$', b'W\x1a', b'X\x10', b'Y\x06']

L'idée est que vous ne pouvez jamais manquer de valeurs intermédiaires possibles, car vous ajoutez simplement un b"\x00" aux enregistrements concernés si vous avez besoin de plus de valeurs. (int est illimité dans Python 3, sinon vous devrez choisir une tranche d'octets à la fin pour comparer, en supposant qu'entre deux valeurs adjacentes, les différences seraient emballées vers la fin.)

Par exemple, supposons que vous ayez deux enregistrements, b"\x00" et b"\x01", et vous voulez qu'un enregistrement passe entre eux. Aucune valeur disponible entre 0x00 et 0x01, donc vous ajoutez b"\x00" aux deux, et maintenant vous avez un tas de valeurs entre elles que vous pouvez utiliser pour insérer de nouvelles valeurs.

>>> records = [b"\x00", b"\x01", b"\x02"]
>>> values = [x for x in order_index(records[0], records[1], 3)]
>>> records = records + values
>>> records.sort()
>>> records
[b'\x00', b'\x00@', b'\x00\x80', b'\x00\xc0', b'\x01', b'\x02']

La base de données peut facilement le trier car tout finit par ordre lexicographique. Si vous supprimez un enregistrement, il est toujours en règle. Dans mon projet, j'ai fait b"\x00" et b"\xff" en tant qu'enregistrements FIRST et LAST, cependant, afin de les utiliser comme valeurs virtuelles "de" et "à" pour ajouter/ajouter de nouveaux enregistrements:

>>> records = []
>>> value = next(order_index(FIRST, LAST, 1))
>>> value
b'\x7f'
>>> records.append(value)
>>> value = next(order_index(records[0], LAST, 1))
>>> value
b'\xbf'
>>> records.append(value)
>>> records.sort()
>>> records
[b'\x7f', b'\xbf']
>>> value = next(order_index(FIRST, records[0], 1))
>>> value
b'?'
>>> records.append(value)
>>> records.sort()
>>> records
[b'?', b'\x7f', b'\xbf']
1
tjb1982

J'ai trouvé cette réponse beaucoup mieux. Citant entièrement:

Les bases de données sont optimisées pour certaines choses. La mise à jour rapide de nombreuses lignes en fait partie. Cela devient particulièrement vrai lorsque vous laissez la base de données faire son travail.

Considérer:

order song
1     Happy Birthday
2     Beat It
3     Never Gonna Give You Up
4     Safety Dance
5     Imperial March

Et vous voulez déplacer Beat It à la fin, vous auriez deux requêtes:

update table 
  set order = order - 1
  where order >= 2 and order <= 5;

update table
  set order = 5
  where song = 'Beat It'

Et c'est tout. Cela évolue très bien avec de très grands nombres. Essayez de mettre quelques milliers de chansons dans une liste de lecture hypothétique dans votre base de données et voyez combien de temps il faut pour déplacer une chanson d'un endroit à un autre. Comme ceux-ci ont des formes très standardisées:

update table 
  set order = order - 1
  where order >= ? and order <= ?;

update table
  set order = ?
  where song = ?

Vous disposez de deux instructions préparées que vous pouvez réutiliser très efficacement.

Cela offre des avantages importants - l'ordre de la table est quelque chose que vous pouvez raisonner. La troisième chanson a toujours un order de 3. La seule façon de garantir cela est d'utiliser des entiers consécutifs comme ordre. L'utilisation de listes pseudo-liées ou de nombres décimaux ou d'entiers avec des espaces ne vous permettra pas de garantir cette propriété; dans ces cas, la seule façon d'obtenir le nième morceau est de trier la table entière et d'obtenir le nième enregistrement.

Et vraiment, c'est beaucoup plus facile que vous ne le pensez. Il est simple de comprendre ce que vous voulez faire, de générer les deux instructions de mise à jour et pour que d'autres personnes les regardent et réalisent ce qui est fait.

1
vedant