[英]MySQL bulk insert on multiple tables
我有一个 MySQL 数据库,其中包含 2 个表products
和product_variants
。 一个产品有多个产品变体。 这里有一个示例:
products
+----+------+
| id | name |
+----+------+
| 1 | Foo |
| 2 | Bar |
+----+------+
product_variants
+----+-------------+--------+
| id | product_id | value |
+----+-------------+--------+
| 10 | 1 | red |
| 11 | 1 | green |
| 12 | 1 | blue |
| 13 | 2 | red |
| 14 | 2 | yellow |
+----+-------------+--------+
现在我需要以最有效和最快的方式批量插入大量产品及其变体。 我有一个包含许多产品(100k+)的 JSON,如下所示:
[
{
"name": "Foo",
"variants": [{ "value": "red" }, { "value": "green" }, { "value": "blue" }]
},
{
"name": "Bar",
"variants": [{ "value": "red" }, { "value": "yellow" }]
},
...
]
我应该从中生成一个查询来插入产品。
我的想法是使用这样的insert
查询:
INSERT INTO `products` (name) VALUES ("foo"), ("bar"), ...;
但是当时我不知道是什么product_id
(外键),以在插入查询使用product_variants
:
INSERT INTO `product_variants` (product_id,value) VALUES (?,"red"), (?,"green"), ...;
(事务中的这些查询)
我想手动指定产品 id,从最后一个 id 开始递增,但是当并发连接同时插入产品或同时运行 2 个或更多批量插入进程时,我会收到错误。
我可以使用什么策略来实现我的目标? 有没有标准的方法来做到这一点?
ps:如果可能的话,我不想改变这两个表的结构。
您可以使用last_insert_id()
从最后一条语句中获取最后生成的 ID。 但是,正如前面提到的,这只会获取语句的最后一个 ID,因此需要您单独处理每个产品。 不过,您可以批量插入变体。 但是从给定 JSON 的结构来看,我认为这使得遍历该 JSON 变得更加容易。 每个产品及其变体都应该插入到事务中,这样如果由于某种原因INSERT
到产品表中失败,产品的变体就不会添加到前一个产品中。
START TRANSACTION;
INSERT INTO products
(name)
VALUES ('Foo');
INSERT INTO product_variants
(product_id,
value)
VALUES (last_insert_id(),
'red'),
(last_insert_id(),
'green'),
(last_insert_id(),
'blue');
COMMIT;
START TRANSACTION;
INSERT INTO products
(name)
VALUES ('Bar');
INSERT INTO product_variants
(product_id,
value)
VALUES (last_insert_id(),
'red'),
(last_insert_id(),
'yellow');
COMMIT;
如果您已经在表中拥有 JSON,那么它可能可以(非常有效地)使用两个语句完成:
INSERT INTO Products (name)
SELECT name
FROM origial_table; -- to get the product names
INSERT INTO Variants (product_id, `value`)
SELECT ( SELECT id FROM Products WHERE name = ot.name ),
`value`
FROM origial_table AS ot;
实际上, name
和value
需要是合适的 JSON 表达式来提取值。
如果您担心第一个表中有很多重复的“产品”,请确保使用UNIQUE(name)
。 您可以通过此处描述的两步过程来避免“烧毁”ID:mysql.rjweb.org/doc.php/staging_table#normalization
最后,我使用了一种使用 MySQL 函数LAST_INSERT_ID()
的策略,如 @sticky-bit sad 但使用批量插入(许多产品一次插入)速度要快得多。
我附上一个简单的 Ruby 脚本来执行批量插入。 所有似乎也适用于并发插入。
我已经使用标志innodb_autoinc_lock_mode = 2
运行脚本,一切看起来都不错,但我不知道是否有必要将标志设置为 1:
require 'active_record'
require 'benchmark'
require 'mysql2'
require 'securerandom'
ActiveRecord::Base.establish_connection(
adapter: 'mysql2',
host: 'localhost',
username: 'root',
database: 'test',
pool: 200
)
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
class Product < ApplicationRecord
has_many :product_variants
end
class ProductVariant < ApplicationRecord
belongs_to :product
COLORS = %w[red blue green yellow pink orange].freeze
end
def migrate
ActiveRecord::Schema.define do
create_table(:products) do |t|
t.string :name
end
create_table(:product_variants) do |t|
t.references :product, null: false, foreign_key: true
t.string :color
end
end
end
def generate_data
d = []
100_000.times do
d << {
name: SecureRandom.alphanumeric(8),
product_variants: Array.new(rand(1..3)).map do
{ color: ProductVariant::COLORS.sample }
end
}
end
d
end
DATA = generate_data.freeze
def bulk_insert
# All inside a transaction
ActiveRecord::Base.transaction do
# Insert products
values = DATA.map { |row| "('#{row[:name]}')" }.join(',')
q = "INSERT INTO products (name) VALUES #{values}"
ActiveRecord::Base.connection.execute(q)
# Get last insert id
q = 'SELECT LAST_INSERT_ID()'
last_id, = ActiveRecord::Base.connection.execute(q).first
# Insert product variants
i = -1
values = DATA.map do |row|
i += 1
row[:product_variants].map { |subrow| "(#{last_id + i},'#{subrow[:color]}')" }
end.flatten.join(',')
q = "INSERT INTO product_variants (product_id,color) VALUES #{values}"
ActiveRecord::Base.connection.execute(q)
end
end
migrate
threads = []
# Spawn 100 threads that perform 200 single inserts each
100.times do
threads << Thread.new do
200.times do
Product.create(name: 'CONCURRENCY NOISE')
end
end
end
threads << Thread.new do
Benchmark.bm do |benchmark|
benchmark.report('Bulk') do
bulk_insert
end
end
end
threads.map(&:join)
运行脚本后,我检查了所有产品都与查询相关联的变体
SELECT *
FROM products
LEFT OUTER JOIN product_variants
ON (products.id = product_variants.product_id)
WHERE product_variants.product_id IS NULL
AND name != "CONCURRENCY NOISE";
正确地我没有得到任何行。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.