Postgresql – Update Select in postgresql

postgresqlupdate

I have three tables, in form of grandparent, parent, and child. By this I mean a foreign key exists in child linking to parent, and another foreign key exists in parent linking to grandparent. Child and grandparent's only connection is via parent, and I currently have to join to ask questions like who are child's grandparents and who are grandparent's children.

I realised that a tiny bit of denormalisation is going to make my application code nicer to write, so, I want to add another foreign key in child, which links directly to grandparent (which never changes).
This of course is redundant information, but it means I don't have to join to ask questions like give me all of child's grandparents, or give me all of grandparents grandchildren. This will make my app faster and my application code better, and I don't really need advice on the whys of doing this, but the how.

I've added the columns and I'm looking for advice on an UPDATE query to update the database with the correct values for the grandparent id in the child's new foreign key grandparent_id.

The actual tables are not grandparent, parent and child, but minicourse, topic and lesson, as below.

I tried the below query, from here, but, as I suspected, it is not quite what's needed here. The below updates all children with the same value (i.e. the wrong value, seems to be the first one it gets) of the grandparent id. I want each child updated with the correct grandparent_id.
I'm rather new to operations like this in sql, so help would be much appreciated.

UPDATE lessons
SET minicourse_id=subquery.minicourse_id
FROM (
  SELECT topics.minicourse_id
  FROM topics
  JOIN lessons
    ON topics.id=lessons.topic_id
) AS subquery;

I could just do it manually (there are only about 10 minicourses) by running an update like this 10 times, having found what the values are by regular queries:

UPDATE lessons 
SET 
  minicourse_id =<I enter an actual minicourse_id here> 
WHERE 
  lesson_id in(<I enter a manually-found list of lesson_id's here>);

but I thought it could be cool to learn how to do it with sql instead.

Best Answer

The problem with your query is that the subquery is not correlated. You dodn't need and you shouldn't use lessons inside the subquery.

UPDATE lessons
SET minicourse_id = subquery.minicourse_id
FROM (
  SELECT topics.minicourse_id
  FROM topics
  WHERE topics.id = lessons.topic_id   -- this refers to the "lesson"
                                       -- of the main query
) AS subquery ;

In fact, it can be written without a subquery at all:

UPDATE lessons 
SET minicourse_id = topics.minicourse_id 
FROM topics 
WHERE topics.id = lessons.topic_id ;

or with a different subquery:

UPDATE lessons 
SET minicourse_id 
    = ( SELECT minicourse_id 
        FROM topics 
        WHERE id = lessons.topic_id
      ) ;

Regarding your design, I assume you have added a lessons.minicourse_id column and this foreign key:

ALTER TABLE lessons
    ADD minicourse_id INT ;

ALTER TABLE lessons
    ADD FOREIGN KEY (minicourse_id) 
        REFERENCES minicourses (id) ;

While this achieves what you want, there is a small issue: you may end up with rows in child (lessons) that refer to a grandparent (minicourse) A and also to a parent (topics) that refers to a different grandparent B.

Of course if all your applications and users have code that is correct, this won't happen. But I suggest you enforce this in the database level and not (only) in the application level. This is quite easy to do, with the following change in the foreign keys.

It basically sets the FK to minicourses to be "through" lessons, without a direct FK. You'll still be able to use direct joins between lessons and minicourses:

ALTER TABLE lessons
    ADD minicourse_id INT ;

-- the UPDATE is the same!
UPDATE lessons 
SET minicourse_id = topics.minicourse_id 
FROM topics 
WHERE topics.id = lessons.topic_id ;

ALTER TABLE lessons
    ALTER minicourse_id SET NOT NULL ;

-- we need this for the FK below
ALTER TABLE topics 
    ADD CONSTRAINT minicourse_topic_UQ 
        UNIQUE (minicourse_id, id) ;

-- the FK is "lessons -> topics"
ALTER TABLE lessons
    ADD CONSTRAINT lessons_to_topics_FK2
        FOREIGN KEY (minicourse_id, topic_id) 
        REFERENCES topics (minicourse_id, id) ;

-- drop the previous FK to topics
ALTER TABLE lessons
    DROP CONSTRAINT lessons_to_topics_FK ;