Как заставить функцию postgres переупорядочивать строки, представляющие связанный список?

У меня есть такая таблица, которая представляет собой связанный список. Когда comes_after столбец равен null, это означает, что это первая запись в связанном списке.

id      | comes_after
--------+------------
"one"   | null
"two"   | "one"
"three" | "two"
"four"  | "three"

Как мне написать функцию с использованием SQL или PLPGSQL для изменения порядка строк? Функция function move_id_after (id_to_move string, after_id string) имеет 2 аргумента: id_to_move - идентификатор для перехода в новую позицию, а after_id - идентификатор, после которого следует переместить эту строку. Если after_id равно нулю, значит переместить его в начало списка.

Это моя попытка, но она не работает и не кажется идеальным способом сделать это. Как показано в примерах случаев, я хотел бы также иметь возможность перемещать строку в самое начало списка или в самый конец и обрабатывать случаи, когда ничего не нужно менять.

create function move_id_after (id_to_move string, after_id string) language plpgsql as $$
declare
  AFTER_id_to_move string;
  AFTER_after_id string;
  id_to_move_used_to_follow string;
begin
  select id from mytable where comes_after = id_to_move into AFTER_id_to_move;
  select id from mytable where comes_after = after_id into AFTER_after_id;
  update mytable set comes_after = id_to_move where id = AFTER_after_id;
  update mytable set comes_after = AFTER_after_id where id = id_to_move returning id into id_to_move_used_to_follow;
  update mytable set comes_after = id_to_move_used_to_follow where id = id_to_move_after;
end $$;

Вот примеры некоторых случаев, каким должен быть результат.

Переместить запись в другую позицию

select move_id_after("two", "three") должен стать:

id      | comes_after
--------+------------
"one"   | null
"three" | "one"
"two"   | "three"
"four"  | "two"

Переместить запись в позицию, в которой она уже находится

select move_id_after("three", "two") не должно быть изменений:

id      | comes_after
--------+------------
"one"   | null
"two"   | "one"
"three" | "two"
"four"  | "three"

Переместить первую запись на последнюю позицию

select move_id_after("one", "four") должен стать:

id      | comes_after
--------+------------
"two"   | null
"three" | "two"
"four"  | "three"
"one"   | "four"

Переместить последнюю запись в первую позицию

select move_id_after("four", null) должен стать:

id      | comes_after
--------+------------
"four"  | null
"one"   | "four"
"two"   | "one"
"three" | "two"

person user779159    schedule 16.04.2020    source источник
comment
Обычно это признак плохого дизайна, если статус записи зависит от других записей. [даже если этот вопрос касается порядка табуляции, что непонятно]   -  person wildplasser    schedule 18.04.2020


Ответы (1)


Если вы хотите указать порядок, вы должны использовать предложение ORDER BY. Любое другое решение работать не должно. Ваш дизайн непрактичен с большими данными. В вашем дизайне вы должны каждый раз вычислять какое-то значение для порядка, и этот расчет должен быть основан на рекурсивном вызове - это хороший дизайн для графовых баз данных и плохой для реляционных.

Отношение (таблица) - это не матрица, нет начала, нет конца. Например, когда вы хотите найти последнюю запись, вам нужно использовать рекурсивный CTE

 -- searching last record in list
with recursive x as (select 0 l, id 
                       from mytable 
                      where comes_after is null 
                     union all 
                     select l + 1, mytable.id 
                       from x join mytable on x.id = mytable.comes_after) 
  select id 
    from x 
   order by l desc 
    limit 1;

Я не знаю, какова ваша цель, но база данных отношений - плохой инструмент для этого.

Это может быть интересное школьное задание, но в реальной жизни оно может быть ужасным. Это противоречит принципам реляционных баз данных.

Более обычным решением является использование специального числового столбца, который можно использовать для предложения ORDER BY. Некоторым нравится

CREATE SEQUENCE test_o START WITH 1;

CREATE TABLE test(id SERIAL, v varchar, o numeric DEFAULT nextval('test_o'));

-- insert at end
INSERT INTO test(v) VALUES('ahoj');
INSERT INTO test(v) VALUES('nazdar');
INSERT INTO test(v) VALUES('bazar');

-- sort data by o
SELECT * FROM test ORDER BY o;
INSERT INTO test(v, 

SELECT * FROM test ORDER BY o;
┌────┬────────┬───┐
│ id │   v    │ o │
╞════╪════════╪═══╡
│  1 │ ahoj   │ 1 │
│  2 │ nazdar │ 2 │
│  3 │ bazar  │ 3 │
└────┴────────┴───┘

Вставить после id=2:

INSERT INTO test(v, o)
  SELECT 'HELLO', 
         (SELECT (o +  lead(o,1) OVER (ORDER BY o))/2 
            FROM test 
           WHERE o >= (SELECT o 
                         FROM test 
                        WHERE id = 2) 
           ORDER BY o 
           LIMIT 1);

postgres=# SELECT * FROM test ORDER BY o;
┌────┬──────────┬────────────────────┐
│ id │    v     │         o          │
╞════╪══════════╪════════════════════╡
│  1 │ ahoj     │                  1 │
│  2 │ nazdar   │                  2 │
│  6 │ HELLO    │ 2.5000000000000000 │
│  3 │ bazar    │                  3 │
└────┴──────────┴────────────────────┘
(4 rows)
person Pavel Stehule    schedule 16.04.2020
comment
Для случаев использования, когда вам нужно разрешить пользователям переупорядочивать список, связанный список является одним из немногих жизнеспособных вариантов среди тех, которые обсуждаются в softwareengineering.stackexchange.com/questions/195308/. Для заказа я бы сделал это в приложении, а не в базе данных, так как это работает для моего варианта использования с точки зрения количества записей (хотя рекурсивный CTE - это вариант, также описанный в этом сообщении). Проблема в том, что я не могу заставить функцию postgres, которую я опубликовал, работать с описанными мною случаями, поэтому надеюсь, что кто-то поможет выяснить, где я ошибся. - person user779159; 16.04.2020
comment
@ user779159 - никакой специальной функции postgres для этого не существует. Чистое решение SQL основано на некоторых числах строк, строках, которые можно использовать для сортировки, и где вы каждый раз генерируете новое число, новое значение между некоторыми двумя значениями. - person Pavel Stehule; 17.04.2020
comment
Интересно, не могли бы вы дать ответ на мой вопрос с этим решением? Если он достигнет той же цели, то это будет принятый ответ, даже если это другой путь. - person user779159; 17.04.2020
comment
Спасибо! Некоторые вопросы по поводу этого решения. В документах говорится, что размер хранилища типа numeric является переменным. Есть ли способ подсчитать, сколько места для хранения занимает заданное число, например, 2.500 или 123.456789, потому что при большом переупорядочивании может быть много десятичных знаков? И вместо использования nextval при вставке, могу ли я вместо этого получить max столбца o и добавить к нему 1? - person user779159; 18.04.2020
comment
@ user779159 - можно использовать функцию pg_column_size, для ваших значений это 4, 6 и 11 байт. nextval намного быстрее, чем max на более высоких таблицах, но то, что вы предпочитаете, может зависеть от большего количества факторов. - person Pavel Stehule; 19.04.2020
comment
Почему select (1.999999999999999+2)/2 показывает 1.9999999999999995, как я ожидал, но добавление дополнительных 9 в конец показывает 2.0000000000000000? Разве дополнительная цифра не должна увеличивать точность, и я тоже должен увидеть в результате дополнительные 9? Я заметил, что если я запускаю предложенный вами оператор SQL достаточно много раз, он достигает этого предела точности. Вы знаете, как это обойти? Я думал, что numeric типа точны и имеют точность в тысячи разрядов. - person user779159; 20.04.2020
comment
Похоже, что есть какой-то минимальный предел для шкалы 16 и выше, в зависимости от контекста. если вы используете числовой (100,99), то он работает. Наверное, это зависит от работы div. Точность увеличивается, если () /2.0::numeric (100,99) - person Pavel Stehule; 20.04.2020
comment
Будет ли новое сохраненное значение для o в базе данных занимать только пространство, необходимое для представления всех десятичных разрядов в числе без конечных нулей, или оно будет занимать в базе данных с точностью до 99 единиц (занимая место для конечные нули)? - person user779159; 20.04.2020
comment
выберите pg_column_size (0.0), pg_column_size (0.000000000000 :: numeric (100,99)); - person Pavel Stehule; 20.04.2020