Một số giải pháp
insert nhiều bản ghi vào cơ sở dữ liệu
Nguyễn Huy Hùng
Education Section, Mar. 03, 2017
1/14
Nội dung
1 Sử dụng duy nhất một transaction
2 Bỏ qua validate khi insert dữ liệu
3 Chèn vào một khối
4 Sử dụng method import
5 So sánh hiệu năng của các giải pháp
2/14
Tạo bảng User có 2 trường name và email để insert dữ liệu
3/14
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
1 Sử dụng duy nhất một transaction
Transaction là một nhóm có thứ tự các hoạt động thao tác trên cơ sở dữ liệu, và được xem như 1 đơn vị thao tác duy nhất
Giả sử cần insert 100 bản ghi vào bảng User:
Tạo một hash
4/14
data = {name: "bibi", email: "bibi@demo.com"}
1 Sử dụng duy nhất một transaction (2)
a/ Insert theo cách thông thường
Khi đó sẽ tạo ra 100 transaction, tương ứng với việc gọi đến 100 câu lệnh insert trong SQL
5/14
100.times{User.create data}
irb(main):009:0> 100.times{User.create data}
(0.1ms) begin transaction
SQL (0.2ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at")
VALUES (?, ?, ?, ?) [["name", "bibi"], ["email", "bibi@demo.com"], ["created_at",
2016-12-28 04:28:25 UTC], ["updated_at", 2016-12-28 04:28:25 UTC]]
(115.1ms) commit transaction
.
.
.
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at")
VALUES (?, ?, ?, ?) [["name", "bibi"], ["email", "bibi@demo.com"], ["created_at",
2016-12-28 04:28:38 UTC], ["updated_at", 2016-12-28 04:28:38 UTC]]
(244.5ms) commit transaction
=> 100
/* SQL */
INSERT INTO models (...) VALUES (...);
INSERT INTO models (...) VALUES (...);
INSERT INTO models (...) VALUES (...);
INSERT INTO models (...) VALUES (...);
1 Sử dụng duy nhất một transaction (3)
b/ Insert thông qua ActiveRecord
Khi đó việc insert chỉ phải thực hiện trong 1 transaction, tương ứng với 1 câu lệnh SQL
6/14
ActiveRecord::Base.transaction do 100.times{User.create data} end
irb(main):018:0* ActiveRecord::Base.transaction do 100.times{User.create data} end
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at")
VALUES (?, ?, ?, ?) [["name", "bibi"], ["email", "bibi@demo.com"], ["created_at",
2016-12-28 04:40:43 UTC], ["updated_at", 2016-12-28 04:40:43 UTC]]
SQL (0.2ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at")
VALUES (?, ?, ?, ?) [["name", "bibi"], ["email", "bibi@demo.com"], ["created_at",
2016-12-28 04:40:43 UTC], ["updated_at", 2016-12-28 04:40:43 UTC]]
.
.
.
SQL (0.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at")
VALUES (?, ?, ?, ?) [["name", "bibi"], ["email", "bibi@demo.com"], ["created_at",
2016-12-28 04:40:43 UTC], ["updated_at", 2016-12-28 04:40:43 UTC]]
(132.2ms) commit transaction
=> 100
/* SQL */
INSERT INTO models (...) VALUES
(...),
(...),
...
2 Bỏ qua validate khi insert dữ liệu
Nếu không phải kiểm tra tính hợp lệ của dữ liệu khi insert thì có thể tăng tốc hệ thống bằng cách chạy trực tiếp câu lệnh SQL
Ví dụ, thực hiện validate name trong model:
7/14
a/ Khi chạy lệnh User.create thì dữ liệu sẽ được validate
b/ Chạy trực tiếp câu lệnh SQL, không validate dữ liệu
class User < ApplicationRecord
validates :name, presence: true, length: {minimum: 6}
end
irb(main):012:0* User.create! name: "bibi", email: "bibi@demo.com"
(0.1ms) begin transaction
(0.1ms) rollback transaction
ActiveRecord::RecordInvalid: Validation failed: Name is too short (minimum is 6 characters)
irb(main):020:0> now = Time.current
=> Wed, 28 Dec 2016 06:14:01 UTC +00:00
irb(main):021:0> sql = "INSERT INTO users(name, email, created_at, updated_at)
VALUES('bibi', 'bibi@demo.com', '#{now}', '#{now}')"
=> "INSERT INTO users(name, email, created_at, updated_at) VALUES('bibi',
'bibi@demo.com', '2016-12-28 06:14:01 UTC', '2016-12-28 06:14:01 UTC')"
irb(main):022:0> User.connection.execute sql
(124.0ms) INSERT INTO users(name, email, created_at, updated_at)
VALUES('bibi', 'bibi@demo.com', '2016-12-28 06:14:01 UTC', '2016-12-28 06:14:01 UTC')
=> []
3 Chèn vào một khối
Ý tưởng: nối các chuỗi dữ liệu cần insert thành một chuỗi, sử dụng 1 câu lệnh insert
8/14
irb(main):038:0> data2 = []
=> []
irb(main):039:0> 3.times do data2.push "('qwerty', 'qwerty@demo.com', '#{now}', '#{now}')" end
=> 3
irb(main):040:0> sql = "INSERT INTO users(name, email, created_at, updated_at) VALUES #{data2.join(", ")}"
=> "INSERT INTO users(name, email, created_at, updated_at) VALUES ('qwerty', 'qwerty@demo.com',
'2016-12-28 06:14:01 UTC', '2016-12-28 06:14:01 UTC'), ('qwerty', 'qwerty@demo.com',
'2016-12-28 06:14:01 UTC', '2016-12-28 06:14:01 UTC'), ('qwerty', 'qwerty@demo.com',
'2016-12-28 06:14:01 UTC', '2016-12-28 06:14:01 UTC')"
irb(main):041:0> User.connection.execute sql
(104.1ms) INSERT INTO users(name, email, created_at, updated_at) VALUES ('qwerty', 'qwerty@demo.com',
'2016-12-28 06:14:01 UTC', '2016-12-28 06:14:01 UTC'), ('qwerty', 'qwerty@demo.com', '2016-12-28 06:14:01 UTC',
'2016-12-28 06:14:01 UTC'), ('qwerty', 'qwerty@demo.com', '2016-12-28 06:14:01 UTC', '2016-12-28 06:14:01 UTC')
=> []
4 Sử dụng method import
Bổ sung gem activerecord-import:
Thêm bản ghi vào cơ sở dữ liệu:
9/14
gem "activerecord-import", ">= 0.2.0"
irb(main):043:0> columns = [:name, :email]
=> [:name, :email]
irb(main):044:0> values = []
=> []
irb(main):045:0> 3.times do values.push ['member', 'member@demo.com'] end
=> 3
irb(main):046:0> User.import columns, values
(0.1ms) select sqlite_version(*)
(0.1ms) begin transaction
Class Create Many Without Validations Or Callbacks (0.3ms) INSERT INTO "users"
("name","email","created_at","updated_at") VALUES ('member','member@demo.com',
'2016-12-28 06:49:15.799046','2016-12-28 06:49:15.799073'),('member','member@demo.com',
'2016-12-28 06:49:15.799046','2016-12-28 06:49:15.799073'),('member','member@demo.com',
'2016-12-28 06:49:15.799046','2016-12-28 06:49:15.799073')
(0.1ms) SELECT changes();
(106.2ms) commit transaction
=> #<struct ActiveRecord::Import::Result failed_instances=[], num_inserts=1, ids=[828, 829, 830]>
4 Sử dụng method import (2)
Nếu không cần validate dữ liệu thì có thể thêm 'validate : false' vào câu lệnh import
10/14
irb(main):049:0> values2 = []
=> []
irb(main):050:0> 3.times do values2.push ['milu', 'milu@demo.com'] end
=> 3
irb(main):051:0> User.import columns, values2, validate: false
(0.3ms) begin transaction
Class Create Many Without Validations Or Callbacks (0.6ms) INSERT INTO "users"
("name","email","created_at","updated_at") VALUES ('milu','milu@demo.com',
'2016-12-28 07:05:06.714266','2016-12-28 07:05:06.714330'),('milu','milu@demo.com',
'2016-12-28 07:05:06.714266','2016-12-28 07:05:06.714330'),('milu','milu@demo.com',
'2016-12-28 07:05:06.714266','2016-12-28 07:05:06.714330')
(0.2ms) SELECT changes();
(118.3ms) commit transaction
=> #<struct ActiveRecord::Import::Result failed_instances=[], num_inserts=1, ids=[831, 832, 833]>
5 So sánh hiệu năng của các giải pháp
11/14
Tạo file lib/tasks/insert_user.rake để so sánh:
namespace :insert_user do
task :test => :environment do
CONN = ActiveRecord::Base.connection
TIMES = 1
def do_inserts
TIMES.times {User.create name: 'framgia', email:'framgia@demo.com'}
end
def raw_sql
TIMES.times {CONN.execute "INSERT INTO users (name, email, created_at, updated_at)"+
" VALUES('framgia', 'framgia@demo.com', '2016-12-28 06:14:01', '2016-12-28 06:14:01')"}
end
def mass_insert
data = []
TIMES.times do
data.push "('framgia', 'framgia@demo.com', '2016-12-28 06:14:01', '2016-12-28 06:14:01')"
end
sql = "INSERT INTO users (name, email, created_at, updated_at) VALUES #{data.join(', ')}"
CONN.execute sql
end
def activerecord_extensions_mass_insert validate = false
columns = [:name, :email]
values = []
TIMES.times do
values.push ['framgia', 'framgia@demo.com']
end
User.import columns, values, {validate: validate}
end
puts "Testing various insert methods for #{TIMES} inserts\n\n"
print "*base* ActiveRecord without transaction: "
start = Time.now.to_f
do_inserts
stop = Time.now.to_f
base = stop.real - start.real
puts "#{base} seconds\n\n"
print "1/ ActiveRecord with transaction:"
start = Time.now.to_f
ActiveRecord::Base.transaction{do_inserts}
stop = Time.now.to_f
diff = stop.real - start.real
puts sprintf("%2.2fx faster than base\n\n", base / diff)
print "2/ Raw SQL without transaction: "
start = Time.now.to_f
raw_sql
stop = Time.now.to_f
diff = stop.real - start.real
puts sprintf("%2.2fx faster than base\n\n", base / diff)
print "3/ Raw SQL with transaction: "
start = Time.now.to_f
ActiveRecord::Base.transaction{raw_sql}
stop = Time.now.to_f
diff = stop.real - start.real
puts sprintf("%2.2fx faster than base\n\n", base / diff)
print "4/ Single mass insert: "
start = Time.now.to_f
mass_insert
stop = Time.now.to_f
diff = stop.real - start.real
puts sprintf("%2.2fx faster than base\n\n", base / diff)
printf "5/ ActiveRecord::Extensions mass insert: "
start = Time.now.to_f
activerecord_extensions_mass_insert
stop = Time.now.to_f
diff = stop.real - start.real
puts sprintf("%2.2fx faster than base\n\n", base / diff)
print "6/ ActiveRecord::Extensions mass insert without validations: "
start = Time.now.to_f
activerecord_extensions_mass_insert false
stop = Time.now.to_f
diff = stop.real - start.real
puts sprintf("%2.2fx faster than base", base / diff)
end
end
5 So sánh hiệu năng của các giải pháp (2)
12/14
Kết quả thử nghiệm với 50 bản ghi:
hungnh@hungnh-X555LPB:~/Desktop/demo2$ rake insert_user:test
Testing various insert methods for 50 inserts
*base* ActiveRecord without transaction: 5.805713891983032 seconds
1/ ActiveRecord with transaction:32.51x faster than base
2/ Raw SQL without transaction: 0.88x faster than base
3/ Raw SQL with transaction: 43.69x faster than base
4/ Single mass insert: 20.79x faster than base
5/ ActiveRecord::Extensions mass insert: 46.47x faster than base
6/ ActiveRecord::Extensions mass insert without validations: 37.37x faster than base
ActiveRecord::Extensions mass insert có tốc độ thực thi nhanh nhất, gấp 46.47 lần base
5 So sánh hiệu năng của các giải pháp (3)
13/14
Kết quả thử nghiệm với 100 bản ghi:
hungnh@hungnh-X555LPB:~/Desktop/demo2$ rake insert_user:test
Testing various insert methods for 100 inserts
*base* ActiveRecord without transaction: 13.513744354248047 seconds
1/ ActiveRecord with transaction:53.16x faster than base
2/ Raw SQL without transaction: 0.85x faster than base
3/ Raw SQL with transaction: 99.32x faster than base
4/ Single mass insert: 28.13x faster than base
5/ ActiveRecord::Extensions mass insert: 53.09x faster than base
6/ ActiveRecord::Extensions mass insert without validations: 76.46x faster than base
Raw SQL with transaction có tốc độ thực thi nhanh nhất, gấp 99.32 lần base
Tài liệu tham khảo
- http://swaac.tamouse.org/swaac/2013/09/03/mass-inserting-data-in-rails-without-killing-your-performance-coffeepowered/
- https://viblo.asia/hoabattu1991/posts/6BAMYVVKGnjz
- https://github.com/zdennis/activerecord-import/wiki/Requiring
- http://weblog.jamisbuck.org/2015/10/10/bulk-inserts-in-activerecord.html
- http://railsguides.net/how-to-generate-rake-task/
14/14
Một số giải pháp insert nhiều bản ghi vào CSDL
By Nguyễn Huy Hùng
Một số giải pháp insert nhiều bản ghi vào CSDL
- 1,009