繁体   English   中英

MySQL批量插入多个表

[英]MySQL bulk insert on multiple tables

我有一个 MySQL 数据库,其中包含 2 个表productsproduct_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;

实际上, namevalue需要是合适的 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM