MySQL массовая вставка на несколько столов - PullRequest
1 голос
/ 04 февраля 2020

У меня есть 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 |
+----+-------------+--------+

Теперь мне нужно массово вставить множество продуктов с их вариантами наиболее эффективным и быстрым способом. У меня есть JSON со многими продуктами (100k +), такими как:

[
  {
    "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"), ...;

(эти запросы внутри транзакции)

Я думал указать идентификаторы продукта вручную, с шагом по сравнению с последним идентификатором, но я получаю ошибки, когда одновременное соединение вставляет продукты одновременно или когда одновременно выполняется 2 или более процесса массовой вставки.

Какую стратегию я могу использовать для достижения своей цели? Есть ли стандартный способ сделать это?

ps: если возможно, я бы не хотел менять структуру двух таблиц.

Ответы [ 3 ]

2 голосов
/ 04 февраля 2020

Вы можете использовать last_insert_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;

дб <> скрипка

0 голосов
/ 05 февраля 2020

Наконец, я использовал стратегию, которая использует функцию MySQL LAST_INSERT_ID() наподобие @ sticky-bit sad, но использует массовую вставку (1 вставка для многих продуктов), которая намного быстрее.

Я прилагаю простой 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";

и правильно я не получаю строк.

0 голосов
/ 05 февраля 2020

Если у вас уже есть 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). И вы можете избежать «прожигания» идентификаторов с помощью двухэтапного процесса, описанного здесь: mysql .rjweb.org / do c .php / staging_table # normalization

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...