MySQLのARCHIVEストレージエンジンをRailsで使用する
この記事について
MySQLのARCHIVEストレージエンジンを使って、レコードの保存に挑戦します。
この記事では、modelのコールバックやMySQLのトリガーによるArchive用レコードの作成方法と、ストレージエンジンの違いについて簡単にまとめます。
例題
UserモデルとArchivedUserモデルを作成し、UserモデルはInnoDB、ArchivedUserモデルはARCHIVEストレージエンジンで作成したテーブルに保存します。
検証環境
docker-composeでRails/MySQL環境を構築しています。
- Ruby on Rails: 7.2.2
- Ruby: 3.3.5
- MySQL: 8.0
- Docker/docker-compose
- colima: 0.7.6
modelの作成
Userモデル・ArchivedUserモデルにはそれぞれnameとemailを持たせています。
# bundle exec rails g model user name:string email:string
invoke active_record
create db/migrate/20241103095659_create_users.rb
create app/models/user.rb
# bundle exec rails g model archived_user name:string email:string
invoke active_record
create db/migrate/20241103100449_create_archived_users.rb
create app/models/archived_user.rb
migrationファイル
usersテーブルのmigrationファイルは自動生成そのままにします。
class CreateUsers < ActiveRecord::Migration[7.2]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
archived_usersテーブルには、options: 'ENGINE=ARCHIVE'
を指定します。
class CreateArchivedUsers < ActiveRecord::Migration[7.2]
def change
create_table :archived_users, options: 'ENGINE=ARCHIVE' do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
migrate実行
# bundle exec rails db:migrate
== 20241103095659 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0315s
== 20241103095659 CreateUsers: migrated (0.0316s) =============================
== 20241103100449 CreateArchivedUsers: migrating ==============================
-- create_table(:archived_users, {:options=>"ENGINE=ARCHIVE"})
-> 0.0152s
== 20241103100449 CreateArchivedUsers: migrated (0.0153s) =====================
MySQLでストレージエンジンを確認する
usersテーブルはInnoDBストレージエンジン、archived_usersテーブルはARCHIVEストレージエンジンを使用していることがわかります。
mysql> SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_NAME LIKE '%users';
+----------------+--------------------+
| TABLE_NAME | ENGINE |
+----------------+--------------------+
| users | PERFORMANCE_SCHEMA |
| users | InnoDB |
| archived_users | ARCHIVE |
+----------------+--------------------+
3 rows in set (0.01 sec)
Userモデルにコールバックを設定する
before_destroy
コールバックで、archiveするようにしてみます。
class User < ApplicationRecord
before_destroy :archive!
private
def archive!
ArchivedUser.create!(name: name, email: email)
end
end
Userを生成・削除する
生成
rails console上でUserを作成します。ここでは、Faker gemを使用してnameとemailを設定しています。
# bundle exec rails c
Loading development environment (Rails 7.2.2)
app(dev)> User.create!(name: Faker::Name.name, email: Faker::Internet.email)
TRANSACTION (0.2ms) BEGIN
User Create (4.3ms) INSERT INTO `users` (`name`, `email`, `created_at`, `updated_at`) VALUES ('Damon Buckridge', 'lupe.jacobs@sporer.example', '2024-11-03 10:48:16.863843', '2024-11-03 10:48:16.863843')
TRANSACTION (3.2ms) COMMIT
=>
#<User:0x0000ffff93136af8
id: 1,
name: "Damon Buckridge",
email: "[FILTERED]",
created_at: "2024-11-03 10:48:16.863843000 +0000",
updated_at: "2024-11-03 10:48:16.863843000 +0000">
削除
rails console上で先ほど生成したUserを削除します。
app(dev)> User.first.destroy!
User Load (0.7ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
TRANSACTION (2.0ms) BEGIN
ArchivedUser Create (1.9ms) INSERT INTO `archived_users` (`name`, `email`, `created_at`, `updated_at`) VALUES ('Damon Buckridge', 'lupe.jacobs@sporer.example', '2024-11-03 10:48:28.826647', '2024-11-03 10:48:28.826647')
User Destroy (1.2ms) DELETE FROM `users` WHERE `users`.`id` = 1
TRANSACTION (3.5ms) COMMIT
=>
#<User:0x0000ffff93d1b698
id: 1,
name: "Damon Buckridge",
email: "[FILTERED]",
created_at: "2024-11-03 10:48:16.863843000 +0000",
updated_at: "2024-11-03 10:48:16.863843000 +0000">
ArchivedUserを確認する
ArchivedUser.first
を確認してみると、先ほど削除したUserがArchivedUserとして保存されています。
app(dev)> ArchivedUser.first
ArchivedUser Load (7.1ms) SELECT `archived_users`.* FROM `archived_users` ORDER BY `archived_users`.`id` ASC LIMIT 1
=>
#<ArchivedUser:0x0000ffff91b6eec8
id: 1,
name: "Damon Buckridge",
email: "[FILTERED]",
created_at: "2024-11-03 10:48:28.826647000 +0000",
updated_at: "2024-11-03 10:48:28.826647000 +0000">
MySQLのトリガーでArchiveする
MySQLのトリガーで、usersレコード削除にarchiveする方式にも挑戦してみました。
class CreateTriggerDeleteUser < ActiveRecord::Migration[7.2]
def change
reversible do |direction|
direction.up do
execute <<-SQL
CREATE TRIGGER delete_user_trigger
AFTER DELETE ON users
FOR EACH ROW
BEGIN
INSERT INTO archived_users (name, email, created_at, updated_at)
VALUES (OLD.name, OLD.email, OLD.created_at, OLD.updated_at);
END;
SQL
end
direction.down do
execute <<-SQL
DROP TRIGGER delete_user_trigger IF EXISTS delete_user_trigger;
SQL
end
end
end
end
# bundle exec rails db:migrate
== 20241103161050 CreateTriggerDeleteUser: migrating ==========================
-- execute(" CREATE TRIGGER delete_user_trigger\n AFTER DELETE ON users\n FOR EACH ROW\n BEGIN\n INSERT INTO archived_users (name, email, created_at, updated_at)\n VALUES (OLD.name, OLD.email, OLD.created_at, OLD.updated_at);\n END;\n")
-> 0.0165s
== 20241103161050 CreateTriggerDeleteUser: migrated (0.0167s) =================
modelのコールバックでデータの移動が発生しないように、before_destroy
をコメントアウトしておきます。
@@ -1,5 +1,5 @@
class User < ApplicationRecord
- before_destroy :archive!
+ # before_destroy :archive!
private
rails consoleから、Userを作成します。
app(dev)> User.create!(name: Faker::Name.name, email: Faker::Internet.email)
TRANSACTION (0.2ms) BEGIN
User Create (5.2ms) INSERT INTO `users` (`name`, `email`, `created_at`, `updated_at`) VALUES ('Erica Brekke', 'elliott.kuhlman@hackett.test', '2024-11-03 16:25:42.051505', '2024-11-03 16:25:42.051505')
TRANSACTION (4.5ms) COMMIT
=>
#<User:0x0000ffff8e67faf0
id: 2,
name: "Erica Brekke",
email: "[FILTERED]",
created_at: "2024-11-03 16:25:42.051505000 +0000",
updated_at: "2024-11-03 16:25:42.051505000 +0000">
作成したUserを削除します。before_destroy
コールバックでArchivedUserを作成していたときには、ArchivedUser Create
のログが出力されていましたが、MySQLのトリガーのみ設定してコールバックは無効化しているため、Rails側で処理が発生せずrails consoleにはログが出力されませんでした。
app(dev)> User.first.destroy!
User Load (0.6ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
TRANSACTION (1.7ms) BEGIN
User Destroy (5.9ms) DELETE FROM `users` WHERE `users`.`id` = 2
TRANSACTION (2.4ms) COMMIT
=>
#<User:0x0000ffff8d13fa00
id: 2,
name: "Erica Brekke",
email: "[FILTERED]",
created_at: "2024-11-03 16:25:42.051505000 +0000",
updated_at: "2024-11-03 16:25:42.051505000 +0000">
ArchivedUser.last
を確認してみると、削除したUserが格納されていました。
app(dev)> ArchivedUser.last
ArchivedUser Load (3.4ms) SELECT `archived_users`.* FROM `archived_users` ORDER BY `archived_users`.`id` DESC LIMIT 1
=>
#<ArchivedUser:0x0000ffff8d0f7548
id: 2,
name: "Erica Brekke",
email: "[FILTERED]",
created_at: "2024-11-03 16:25:42.051505000 +0000",
updated_at: "2024-11-03 16:25:42.051505000 +0000">
InnoDBとARCHIVEの違い
テーブルサイズ
InnoDBとARCHIVEエンジンでどれぐらいテーブルサイズが違うのか気になったので調査してみました。rails console上で500_000.times { User.create!(name: Faker::Name.name, email: Faker::Internet.email) }
を実行して、50万件のレコードを作成します。
MySQL上でレコード数やテーブルについて確認してみます。
mysql> SELECT COUNT(*) FROM users;
+----------+
| COUNT(*) |
+----------+
| 500000 |
+----------+
1 row in set (0.02 sec)
mysql> ANALYZE TABLE users;
+-----------------------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+-----------------------+---------+----------+----------+
| app_development.users | analyze | status | OK |
+-----------------------+---------+----------+----------+
1 row in set (0.00 sec)
mysql> SELECT table_name, engine, table_rows, data_length, index_length, data_length + index_length as total_size, round(((data_length + index_length) / 1024 / 1024), 2) as size_in_mb FROM information_schema.tables WHERE table_schema = 'app_development' AND table_name = 'users';
+------------+--------+------------+-------------+--------------+------------+------------+
| TABLE_NAME | ENGINE | TABLE_ROWS | DATA_LENGTH | INDEX_LENGTH | total_size | size_in_mb |
+------------+--------+------------+-------------+--------------+------------+------------+
| users | InnoDB | 498069 | 47792128 | 0 | 47792128 | 45.58 |
+------------+--------+------------+-------------+--------------+------------+------------+
1 row in set (0.00 sec)
おおよそ、45MBあることがわかりました。続いて、50万件のUsersを削除しました。usersテーブルにトリガーを設定してあるので、トリガーによりarchived_usersテーブルに50万レコードが作られます。(2レコード多いのは気にしないでください)
mysql> SELECT COUNT(*) FROM archived_users;
+----------+
| COUNT(*) |
+----------+
| 500002 |
+----------+
1 row in set (0.01 sec)
mysql> ANALYZE TABLE archived_users;
+--------------------------------+---------+----------+----------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+--------------------------------+---------+----------+----------------------------------------------------------+
| app_development.archived_users | analyze | note | The storage engine for the table doesn't support analyze |
+--------------------------------+---------+----------+----------------------------------------------------------+
1 row in set (0.01 sec)
mysql> SELECT table_name, engine, table_rows, data_length, index_length, data_length + index_length as total_size, round(((data_length + index_length) / 1024 / 1024), 2) as size_in_mb FROM information_schema.tables WHERE table_schema = 'app_development' AND table_name = 'archived_users';
+----------------+---------+------------+-------------+--------------+------------+------------+
| TABLE_NAME | ENGINE | TABLE_ROWS | DATA_LENGTH | INDEX_LENGTH | total_size | size_in_mb |
+----------------+---------+------------+-------------+--------------+------------+------------+
| archived_users | ARCHIVE | 500002 | 14855943 | 0 | 14855943 | 14.17 |
+----------------+---------+------------+-------------+--------------+------------+------------+
1 row in set (0.00 sec)
archived_usersテーブルは約14.17MBのようです。
結果として、同等のレコード数を持っている場合、InnoDBとARCHIVEでは差が発生することがわかりました。
使用可能なクエリ
ARCHIVE エンジンでは、INSERT、REPLACE および SELECT はサポートされますが、DELETE または UPDATE はサポートされません。
ARCHIVEストレージエンジンを使用している場合は、DELETE/UPDATEクエリが実行できないようです。rails console上から ArchivedUser.last.destroy
を実行してみると、例外が発生しレコードの削除ができませんでした。
app(dev)> ArchivedUser.last.destroy
ArchivedUser Load (321.5ms) SELECT `archived_users`.* FROM `archived_users` ORDER BY `archived_users`.`id` DESC LIMIT 1
TRANSACTION (0.2ms) BEGIN
ArchivedUser Destroy (0.7ms) DELETE FROM `archived_users` WHERE `archived_users`.`id` = 500002
TRANSACTION (0.2ms) ROLLBACK
(app):2:in `<main>': Mysql2::Error: Table storage engine for 'archived_users' doesn't have this option (ActiveRecord::StatementInvalid)
...
UPDATEも同様に、例外が発生して更新ができませんでした。
app(dev)> ArchivedUser.last.update(name: 'hoge')
ArchivedUser Load (320.9ms) SELECT `archived_users`.* FROM `archived_users` ORDER BY `archived_users`.`id` DESC LIMIT 1
TRANSACTION (0.4ms) BEGIN
ArchivedUser Update (0.7ms) UPDATE `archived_users` SET `archived_users`.`name` = 'hoge', `archived_users`.`updated_at` = '2024-11-10 13:24:17.492170' WHERE `archived_users`.`id` = 500002
TRANSACTION (0.3ms) ROLLBACK
(app):4:in `<main>': Mysql2::Error: Table storage engine for 'archived_users' doesn't have this option (ActiveRecord::StatementInvalid)
さいごに
普段はInnoDBエンジンでテーブルを作りがちでしたが、初めてARCHIVEDストレージエンジンを使用してのテーブル作成に臨んでみました。触れたり調べたりすることで知ることがたくさんあったので、これからもっと触れて理解を深めていきます。この記事をアップデートしたり、他の記事にまとめ直したりといったことにも挑戦していきます。
Discussion