This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class AddSetPositionsStoredProc < ActiveRecord::Migration | |
STORED_PROC_NAME = 'set_positions' | |
CREATE_STORED_PROC_SQL = %Q{ | |
CREATE PROCEDURE #{STORED_PROC_NAME}() | |
BEGIN | |
DECLARE done INT DEFAULT 0; | |
DECLARE pid INT; | |
DECLARE curs CURSOR FOR SELECT DISTINCT parent_id FROM units; | |
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; | |
OPEN curs; | |
REPEAT | |
FETCH curs INTO pid; | |
IF NOT done THEN | |
SET @rownum := 0; | |
UPDATE units SET position = @rownum := @rownum + 1 | |
WHERE parent_id = pid | |
ORDER BY CASE WHEN position IS NULL THEN 1 ELSE 0 END, position, short_name; | |
END IF; | |
UNTIL done END REPEAT; | |
CLOSE curs; | |
SET @rownum := 0; | |
UPDATE units SET position = @rownum := @rownum + 1 | |
WHERE parent_id IS NULL | |
ORDER BY CASE WHEN position IS NULL THEN 1 ELSE 0 END, position, short_name; | |
END | |
} | |
def self.up | |
ActiveRecord::Base.connection.execute(CREATE_STORED_PROC_SQL) | |
end | |
def self.down | |
sql = "DROP PROCEDURE #{STORED_PROC_NAME}" | |
ActiveRecord::Base.connection.execute(sql) | |
end | |
end |
And here is the rake task to call it:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace :data do | |
desc 'Set positions per parent_id, position, short_name via stored procedure' | |
task :sp_set_positions => :environment do | |
sql = 'CALL set_positions()' | |
puts sql | |
ActiveRecord::Base.connection.execute(sql) | |
end | |
end |
The stored procedure took just over a minute to do the renumbering. After implementing I decided to compare this to a straight SQL version that would be portable to different databases - but not as fast. I tried to avoid an ActiveRecord tax here and did the SQL in raw execute statements. Here is the rake task:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace :data do | |
# on my slow machine takes 5m09.209s | |
desc 'Set positions per parent_id, position, short_name via SQL' | |
task :sql_set_positions => :environment do | |
parents_sql = 'SELECT DISTINCT parent_id FROM units' | |
children_sql = %q{ | |
SELECT | |
id FROM units | |
WHERE | |
parent_id = ? | |
ORDER BY | |
CASE WHEN position IS NULL THEN 1 ELSE 0 END, position, short_name | |
} | |
update_position_sql = 'UPDATE units SET position = ? WHERE id = ?' | |
parents_rs = Unit.connection.execute(parents_sql) | |
parents_rs.each do |parent| | |
parent_id = parent.first | |
sql = Unit.send(:sanitize_sql, [children_sql, parent_id]) | |
children_rs = Unit.connection.execute(sql) | |
position = 1 | |
children_rs.each do |child| | |
sql = Unit.send(:sanitize_sql, [update_position_sql, position, | |
child.first]) | |
Unit.connection.execute(sql) | |
position += 1 | |
end | |
end | |
# WHERE parent_id = NULL doesn't work so handle separately | |
sql = 'SELECT id FROM units WHERE parent_id IS NULL' | |
children_rs = Unit.connection.execute(sql) | |
position = 1 | |
children_rs.each do |child| | |
sql = Unit.send(:sanitize_sql, [update_position_sql, position, | |
child.first]) | |
Unit.connection.execute(sql) | |
position += 1 | |
end | |
end | |
end |
The final results showed the stored procedure technique to be about 5 times faster than the SQL technique.