How to update multiple different values at the same time in one query
How to create a raw SQL to update in Rails different values for each record in one single query

Senior Product Engineer, working in Ruby and Rails. Passionate about idea generation, creativity, and programming.
I curate the Short Ruby Newsletter.
Context
You want to update the same column for multiple records in one query and set different values for each record.
Example:
class Car < ActiveRecord::Base
# has attributes: id, license_plate
end
Let's say you want to update license plates and you have a list of car ids and associated license plates:
updates = {
1 => "EU-DE-123245"
2 => "EU-NL-12456"
3 => "EU-FR-12455"
4 => "EU-RO-87613"
}
Problem
You can do this in multiple queries. Here is an example:
updates.each do |car_id, license_place|
Car.find(car_id).update_column(:license_place, license_place)
end
The problem with this solution is that it will do multiple queries and sometimes this could come with a performance penalty.
So you want to find a way to update all records with different values in one single query.
Solution
# @param column [String] The name of the column to be updated with new values
# @param updates [Array<Hash>] The hash key is the id of the record and hash value is the new value to be set
# @example Update amount for records with id 1 and 2
# update_column_for_all_records("amount", [ {
# 1 => "EU-DE-123245"
# 2 => "EU-NL-12456"
# 3 => "EU-FR-12455"
# 4 => "EU-RO-87613"
# }])
def update_column_for_all_records(column, updates)
cases = []
ids = updates.keys.map(&:to_i)
Car.where(id: ids).each do
id = _1.id
new_value = updates[id]
cases << sanitize_case(id, new_value)
end
update_query = "#{column} = CASE id #{cases.join(" ")} END"
Car.where(id: ids).update_all(update_query)
end
def sanitize_case(id, new_value)
Car.sanitize_sql [ "WHEN :id THEN :value", { id: id, value: new_value} ]
end
Explanation of the solution
We are using a kind of raw SQL to update all records in one query.
What we want to execute in SQL is something like this:
UPDATE
"cars"
SET
"cars"."license_plate" = CASE "cars"."id"
WHEN 1 THEN
'EU-DE-123245'
WHEN 2 THEN
'EU-NL-12456'
END
WHERE
"cars"."id" IN(1, 2)
Things to consider
If you use this in #Rails please take into consideration:
update_all will not execute callbacks nor validations
As specified in the Rails docs, update_all:
It does not instantiate the involved models and it does not trigger Active Record callbacks or validations.
Thus it will also not update the updated_at column.
You might need to cast your values in the WHEN THEN query
For example, if you will need to update a column that has the format timestamp then you might need to change the method to support casting and add to the cases array something like:
cases << "WHEN #{id} THEN CAST('#{new_value.to_fs(:db)}' as timestamp)"
Notice in this line the expectation is that new_value is a Rails DateTime object and I am using to_fs(:db) (see more here) to make sure I am converting the proper value to timestamp.
Here is a more full example with casting:
# @param column [String] The name of the column to be updated with new values
# @param updates [Array<Hash>] The hash key is the id of the record and hash value is the new value to be set
# @example Update amount for records with id 1 and 2
# update_column_for_all_records("amount", [{ id: 1, value: "EU-DE-123245", type: "string" }, {id: 2, value: "EU-NL-12456", type: "text"}])
def update_column_for_all_records(column, updates)
cases = []
ids = updates.map { _1[:id] }
Car.where(id: ids).each do
id = _1[:id]
new_value = _1[:value]
cases << sanitize_case(id, new_value, _1[:type])
end
update_query = "#{column} = CASE id #{cases.join(" ")} END"
Car.where(id: ids).update_all(update_query)
end
def sanitize_case(id, new_value, type)
native_type = adapter_type(type)
Car.sanitize_sql [ "WHEN :id THEN CAST(:value as #{native_type})", { id: id, value: new_value} ]
end
# This works for PostgreSQL for example
def adapter_type(type)
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES.dig(type.to_s, :name) || "text"
end




