使用本地 hash/array 更新多个记录的单个 Postgres 查询
Single Postgres query to update many records using a local hash/array
我想使用单个查询通过 Ruby 散列或数组更新我的 Postgres 数据库中的许多记录,而不必遍历每条记录并调用单独的更新。
# {:id => :color}
my_hash = {
1 => 'red',
2 => 'blue',
3 => 'green'
}
我怎么不想这样做,因为它执行三个串行查询:
my_hash.each do |id, color|
MyModel.where(id: id).update_all(color: color)
end
我想怎么做:
MyModel.connection.execute <<-SQL
UPDATE my_models
SET color=something
FROM somehow_using(my_hash)
WHERE maybe_id=something
SQL
您可以使用 case
:
update my_models
set color = case id
when 1 then 'red'
when 2 then 'blue'
when 3 then 'green'
end;
或将散列保存在单独的 table:
create table my_hash (id int, color text);
insert into my_hash values
(1, 'red'),
(2, 'blue'),
(3, 'green');
update my_models m
set color = h.color
from my_hash h
where h.id = m.id;
还有一个选择,如果您知道 select 散列为 jsonb
:
的方法
with hash as (
select '{"1": "red", "2": "blue", "3": "green"}'::jsonb h
)
update my_models
set color = value
from hash, jsonb_each_text(h)
where key::int = id;
OP ruby-fying klin的第三个选项:
sql = <<-SQL
with hash as (
select '#{my_hash.to_json}'::jsonb h
)
update my_models
set color = value
from hash, jsonb_each_text(h)
where key::int = id;
SQL
ActiveRecord::Base.connection.execute(sql)
另一种解决方案是将一系列更新连接成一个字符串并一次发送。缺点是它通过线路发送更多数据,但另一方面,PG 不必反序列化和处理 JSON.
ActiveRecord::Base.connection.execute(
my_hash.collect{|id,color| "UPDATE my_models SET color=#{color} WHERE id=#{id};"}.join('')
)
# And don't forget to sanitize the :id and :color values
对于可能在 2020 年以后遇到此问题的读者,看起来 upsert_all
做了 OP 在 Rails 6 中想要的:
MyModel.upsert_all([{ id: 1, color: "red" }, { id: 2, color: "blue" }, { id: 3, color: "green" }])
会生成类似
的东西
# Bulk Insert (26.3ms) INSERT INTO `my_models`(`id`,`color`)
# VALUES (1, 'red')...
# ON DUPLICATE KEY UPDATE `color`=VALUES(`color`)
受 this blog 启发的示例。
您可以使用 Postgres VALUES 函数执行此操作:
UPDATE my_models
SET color = temp.color
FROM (SELECT *
FROM (VALUES (1, 'red'), (2, 'blue'), (3, 'green'))
AS t(id, color)
) AS temp
WHERE my_models.id = temp.id
即使有数百个值,这也能很好地工作。要通过散列在 Ruby 中执行此操作,请使用类似以下内容的内容:
values = my_hash.map { |id, color| "(#{id}, '#{color}')" }.join(', ')
# => (1, 'red'), (2, 'blue'), (3, 'green')
stmt = <<~SQL
UPDATE my_models
SET color = temp.color
FROM (SELECT *
FROM (VALUES #{values})
AS t(id, color)
) AS temp
WHERE my_models.id = temp.id
SQL
MyModel.connection.exec_update(stmt)
请注意,您真的不想对用户输入执行此操作,除非您可以先对其进行清理或者您喜欢 SQL 注入攻击。我想像这样的东西会起作用,虽然我还没有真正尝试过:
values = my_hash.keys.map { |id| "(#{id}, ?)" }.join(', ')
# => (1, ?), (2, ?), (3, ?)
sql = <<~SQL
UPDATE my_models
SET color = temp.color
FROM (SELECT *
FROM (VALUES #{values})
AS t(id, color)
) AS temp
WHERE my_models.id = temp.id
SQL
stmt = ActiveRecord::Base.sanitize_sql([sql, *my_hash.values])
MyModel.connection.exec_update(stmt)
我想使用单个查询通过 Ruby 散列或数组更新我的 Postgres 数据库中的许多记录,而不必遍历每条记录并调用单独的更新。
# {:id => :color}
my_hash = {
1 => 'red',
2 => 'blue',
3 => 'green'
}
我怎么不想这样做,因为它执行三个串行查询:
my_hash.each do |id, color|
MyModel.where(id: id).update_all(color: color)
end
我想怎么做:
MyModel.connection.execute <<-SQL
UPDATE my_models
SET color=something
FROM somehow_using(my_hash)
WHERE maybe_id=something
SQL
您可以使用 case
:
update my_models
set color = case id
when 1 then 'red'
when 2 then 'blue'
when 3 then 'green'
end;
或将散列保存在单独的 table:
create table my_hash (id int, color text);
insert into my_hash values
(1, 'red'),
(2, 'blue'),
(3, 'green');
update my_models m
set color = h.color
from my_hash h
where h.id = m.id;
还有一个选择,如果您知道 select 散列为 jsonb
:
with hash as (
select '{"1": "red", "2": "blue", "3": "green"}'::jsonb h
)
update my_models
set color = value
from hash, jsonb_each_text(h)
where key::int = id;
OP ruby-fying klin的第三个选项:
sql = <<-SQL
with hash as (
select '#{my_hash.to_json}'::jsonb h
)
update my_models
set color = value
from hash, jsonb_each_text(h)
where key::int = id;
SQL
ActiveRecord::Base.connection.execute(sql)
另一种解决方案是将一系列更新连接成一个字符串并一次发送。缺点是它通过线路发送更多数据,但另一方面,PG 不必反序列化和处理 JSON.
ActiveRecord::Base.connection.execute(
my_hash.collect{|id,color| "UPDATE my_models SET color=#{color} WHERE id=#{id};"}.join('')
)
# And don't forget to sanitize the :id and :color values
对于可能在 2020 年以后遇到此问题的读者,看起来 upsert_all
做了 OP 在 Rails 6 中想要的:
MyModel.upsert_all([{ id: 1, color: "red" }, { id: 2, color: "blue" }, { id: 3, color: "green" }])
会生成类似
的东西# Bulk Insert (26.3ms) INSERT INTO `my_models`(`id`,`color`)
# VALUES (1, 'red')...
# ON DUPLICATE KEY UPDATE `color`=VALUES(`color`)
受 this blog 启发的示例。
您可以使用 Postgres VALUES 函数执行此操作:
UPDATE my_models
SET color = temp.color
FROM (SELECT *
FROM (VALUES (1, 'red'), (2, 'blue'), (3, 'green'))
AS t(id, color)
) AS temp
WHERE my_models.id = temp.id
即使有数百个值,这也能很好地工作。要通过散列在 Ruby 中执行此操作,请使用类似以下内容的内容:
values = my_hash.map { |id, color| "(#{id}, '#{color}')" }.join(', ')
# => (1, 'red'), (2, 'blue'), (3, 'green')
stmt = <<~SQL
UPDATE my_models
SET color = temp.color
FROM (SELECT *
FROM (VALUES #{values})
AS t(id, color)
) AS temp
WHERE my_models.id = temp.id
SQL
MyModel.connection.exec_update(stmt)
请注意,您真的不想对用户输入执行此操作,除非您可以先对其进行清理或者您喜欢 SQL 注入攻击。我想像这样的东西会起作用,虽然我还没有真正尝试过:
values = my_hash.keys.map { |id| "(#{id}, ?)" }.join(', ')
# => (1, ?), (2, ?), (3, ?)
sql = <<~SQL
UPDATE my_models
SET color = temp.color
FROM (SELECT *
FROM (VALUES #{values})
AS t(id, color)
) AS temp
WHERE my_models.id = temp.id
SQL
stmt = ActiveRecord::Base.sanitize_sql([sql, *my_hash.values])
MyModel.connection.exec_update(stmt)