views:

45

answers:

2

In order to fix a bug, I have to iterate over all the rows in a table, updating a cached count of children to what its real value should be. The structure of the things in the table form a tree.

In rails, the following does what I want:

Thing.all.each do |th|
  Thing.connection.update(
    "
      UPDATE #{Thing.quoted_table_name} 
        SET children_count = #{th.children.count}
        WHERE id = #{th.id}
    "
  )
end

Is there any way of doing this in a single MySQL query? Alternatively, is there any way of doing this in multiple queries, but in pure MySQL?

I want something like

UPDATE table_name
  SET children_count = (
    SELECT COUNT(*) 
      FROM table_name AS tbl 
      WHERE tbl.parent_id = table_name.id
  )

except the above doesn't work (I understand why it doesn't).

A: 

your subselect update should work; let's try touching it up a bit:

UPDATE table_name
  SET children_count = (
    SELECT COUNT(sub_table_name.id) 
      FROM sub_table_name 
      WHERE sub_table_name.parent_id = table_name.id
  )

Or if the sub-table is the same table:

UPDATE table_name as top_table
  SET children_count = (
    SELECT COUNT(sub_table.id) 
      FROM (select * from table_name) as sub_table
      WHERE sub_table.parent_id = top_table.id
  )

But that's not super efficient I'm guessing.

Steven Soroka
To clarify, the sub_table here is the same as the original table.It's a tree structure, so the parent_id on each row refers to the id of another row in the same table.Also, can sub selects refers to things outside of the scope of that select? In this case, table_name.id
Jamie Wong
yes, they can. You just need to alias them differently. I updated my answer with another example above
Steven Soroka
@Steven: MySQL will not allow this: `ERROR 1093 (HY000): You can't specify target table 'top_table' for update in FROM clause`
Ike Walker
fixed that with selecting FROM (select * from table_name), though it's kind of a mess (but works)
Steven Soroka
A: 

You probably got this error, right?

ERROR 1093 (HY000): You can't specify target table 'table_name' for update in FROM clause

The easiest way around this is probably to select the child counts into a temporary table, then join to that table for the updates.

This should work, assuming the depth of the parent/child relationship is always 1. Based on your original update this seems like a safe assumption.

I added an explicit write lock on the table to assure that no rows are modified after I create the temp table. You should only do this if you can afford to have it locked for the duration of this update, which will depend on the amount of data.

lock tables table_name write;

create temporary table temp_child_counts as
select parent_id, count(*) as child_count
from table_name 
group by parent_id;

alter table temp_child_counts add unique key (parent_id);

update table_name
inner join temp_child_counts on temp_child_counts.parent_id = table_name.id
set table_name.child_count = temp_child_counts.child_count;

unlock tables;
Ike Walker
creating a temp table to solve this is a bad idea.
Steven Soroka
@Steven Why do you think this is a bad idea?
Ike Walker
because you could solve it easier and faster with a subselect.
Steven Soroka
@Steven The subselect won't work for exactly the reason Ike specifies at the top of the post. The reason you can't specify the same table is not because MySQL gets confused, it's because it doesn't make sense to be modifying a table based on a subselect on the same table.
Jamie Wong
@Steven: That would be possible if the child_count column were in a separate table from the parent_id column, but since the two columns are in the same table you cannot solve this with a sub-select.
Ike Walker
@Ike Your solution works, so you've been up voted. I'll accept your answer in a few days if nobody can present a more concise solution.
Jamie Wong