1

I have a table like this that represents a linked list. When comes_after column is null it means it's the first record in the linked list.

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

How do I write a function using SQL or PLPGSQL to reorder the rows? The function function move_id_after (id_to_move string, after_id string) has 2 arguments, the id_to_move which is the id to move to a new position, and after_id which is the id to move that row after. If after_id is null it means to move it to the beginning of the list.

This is my attempt but it doesn't work, and it doesn't seem like the ideal way to be doing it. As shown in the example cases, I'd like to also be able to move a row to the very beginning of a list or to the very end, and handle the cases where nothing needs to change.

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

Here are examples of some cases of how the result should be.

Move a record to another position

select move_id_after("two", "three") should become:

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

Move a record to a position it's already in

select move_id_after("three", "two") should have no change:

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

Move the first record to last position

select move_id_after("one", "four") should become:

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

Move the last record to first position

select move_id_after("four", null) should become:

id      | comes_after
--------+------------
"four"  | null
"one"   | "four"
"two"   | "one"
"three" | "two"
user779159
  • 9,034
  • 14
  • 59
  • 89
  • It is generally a sign of bad design if a record's status depends on other records. [even if this question is about *tabbing order*, which is not clear] – wildplasser Apr 18 '20 at 00:44

1 Answers1

2

If you want to specify a order, then you have to use ORDER BY clause. Any other solution should not to work. Your design is not practical with bigger data. With your design, you have to calculate some value for order every time, and this calculation should be based on recursive call - it is good design for graph databases, and bad for relational.

Relation (table) is not matrix, there is not a begin, there is not a end. Example, when you want to search last record, you have to use recursive 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;

I don't know what is your target, but relation database is bad tool for this.

It's maybe interesting school task, but it can be terrible in real life. It is going against relation databases principles.

More usual solution is using special numeric column that you can use for ORDER BY clause. Some like

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 │
└────┴────────┴───┘

Insert after 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)
Pavel Stehule
  • 42,331
  • 5
  • 91
  • 94
  • For use cases where you need to be able to let users reorder a list, a linked list is one of a few viable options among those discussed in https://softwareengineering.stackexchange.com/questions/195308/storing-a-re-orderable-list-in-a-database. For ordering I'd do it in the application and not the database, as it works for my use case in terms of the number of records (although a recursive CTE is an option also described in that post). The problem is I can't get the postgres function I posted to work with the cases I've described, so hoping someone can help figure out where I've gone wrong. – user779159 Apr 16 '20 at 19:19
  • @user779159 - there are nothing special postgres's function for this purpose. The clean SQL solution is based on some rows numbers, strings that can be used for sorts, and where you every time generate new number, new value between some two values. – Pavel Stehule Apr 17 '20 at 02:38
  • Interesting, could you please create an answer with that solution for my question? If it accomplishes the same goal then it would be the accepted answer even though it's a different way. – user779159 Apr 17 '20 at 04:13
  • Thank you! Some questions about this solution. The docs say `numeric` type storage size is variable. Is there a way I can calculate how much storage space a given number takes such as "2.500" or "123.456789", because with lots of reordering there could be lots of decimal places? And instead of using `nextval` when inserting could I instead get the `max` of column `o` and add 1 to it? – user779159 Apr 18 '20 at 18:32
  • @user779159 - you can use a function `pg_column_size`, for your values it is 4, 6 and 11 bytes. `nextval` is much more faster then `max` on higher tables, but what you prefer can depends on more factors. – Pavel Stehule Apr 19 '20 at 03:15
  • Why does `select (1.999999999999999+2)/2` show `1.9999999999999995` like I'd expect but adding an extra "9" to the end shows `2.0000000000000000`? Shouldn't the extra digit increase the precision and I should see the extra "9" in the result too? I've noticed that if I run the SQL statement you suggested enough times it reaches this "limit" of precision. Do you know how to get around this? I thought `numeric` types are exact and have thousands of digits of precision. – user779159 Apr 20 '20 at 12:14
  • It looks so there is some minimal limit for scale 16, and higher depends on context. if you cast to numeric(100,99), then it is working. Probably it depends on div operation. The precision is increased when ()/2.0::numeric(100,99) – Pavel Stehule Apr 20 '20 at 12:45
  • Will the new saved value for `o` in the database take up only the space that's necessary to represent all decimal places in the number without trailing zeros, or will it take up the full 99 units precision worth of space in the database (taking up space for trailing zeros)? – user779159 Apr 20 '20 at 13:19
  • select pg_column_size(0.0), pg_column_size(0.000000000000::numeric(100,99)); – Pavel Stehule Apr 20 '20 at 13:26