このエントリーをはてなブックマークに追加
ラベル MySQL の投稿を表示しています。 すべての投稿を表示
ラベル MySQL の投稿を表示しています。 すべての投稿を表示

2017年1月9日月曜日

同じレコードがないときだけインサートする!

あるアイテムを持っていない人だけ、別のアイテムをあげたい!
もしくはその逆で、あるアイテムを持っている人に追加でアイテムをあげたい!

そういうことってないでしょうか?
先日、僕がそのような状況になり、四苦八苦しておりました。
本日は復習をかねて、調べた内容を記述していきたいと思います。

内容


本日は主に下記の基本的なところを書きたいと思います。
  • EXISTS / NOT EXISTS
  • INSERT ... SELECT...
  • 上記2つを合わせて

準備


まずはテスト用のテーブル、レコードを作成しておきます。
実はMySQLのリファレンスを読んだのですが、いまいちピンとこなかったので、実際に似たテーブルを作成して試してみました。
CREATE TABLE cities(id int(11) NOT NULL AUTO_INCREMENT, name varchar(32) NOT NULL DEFAULT '0', PRIMARY KEY (id)) DEFAULT CHARSET=utf8;
CREATE TABLE stores(id int(11) NOT NULL AUTO_INCREMENT, name varchar(32) NOT NULL DEFAULT '0', PRIMARY KEY (id)) DEFAULT CHARSET=utf8;
CREATE TABLE cities_stores(id int(11) NOT NULL AUTO_INCREMENT, city_id int(11) NOT NULL DEFAULT 0, store_id int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id)) DEFAULT CHARSET=utf8;
INSERT INTO cities (id, name) VALUES (1, '渋谷'),(2, '新宿'),(3, '池袋');
INSERT INTO stores (id, name) VALUES (1, 'セブンイレブン'),(2, 'ファミリーマート'),(3, 'ローソン'),(4, 'ミニストップ'),(5, 'サークルK'), (6, 'スーパー');
INSERT INTO cities_stores (city_id, store_id) VALUES (1, 1), (1, 2), (1, 3), (2, 3), (2, 4), (2, 5), (3, 1), (3, 3), (3, 5);
cities(街)、stores(店)、cities_stores(街と店の紐付け)を作成し、それぞれの街にいくつかの店があるとします。
具体的には下記の組み合わせです。
渋谷 セブン、ファミマ、ローソン
新宿 ローソン、ミニストップ、サークルK
池袋 セブン、ローソン、サークルK
どこにもない スーパー

EXISTS / NOT EXISTS


まずは、EXIST句、NOT EXISTS句です。
リファレンスはこちらになります。
これは、その条件のものが存在していればTRUEを、存在していなければFALSEを返し、TRUEのものだけ取得してくれます。
(NOT EXIST句は逆です。)
下記に例を示しますが、storesの中で街にないものは「スーパー」だけです。
# 1. cities_storesに存在する場合
SELECT DISTINCT name FROM stores
  WHERE EXISTS (SELECT * FROM cities_stores WHERE cities_stores.store_id = stores.id);
セブン、ファミマ、ローソン、ミニストップ、サークルK
# 2. cities_storesに存在しない場合
SELECT DISTINCT name FROM stores
  WHERE NOT EXISTS (SELECT * FROM cities_stores WHERE cities_stores.store_id = stores.id);
スーパー

INSERT ... SELECT...


次に、INSERT ... SELECT...構文です。
リファレンスはこちらになります。
これは、SELECTで引っ張ってきた値を使用してINSERT文を作成します。
具体的には下記です。
良い例が思いつかなかったため、storesから全件取得し、それをcitiesに突っ込みます。
INSERT INTO cities (name) SELECT stores.name FROM stores;
ご想像の通り、これでまるっとcitiesのレコードが増えます。
SELECT * FROM cities;
id name
1 渋谷
2 新宿
3 池袋
4 セブンイレブン
5 ファミリーマート
6 ローソン
7 ミニストップ
8 サークルK
9 スーパー

同じレコードがないときだけインサートする!


ここから本題になります。
想定としては、ある街に全ての店が進出してきた!ということにします。(意味が分かりませんが笑)
当然、既に存在している店もあるので、その店はインサートしないようにしたいです。
NOT EXISTS句とINSERT ... SELECT ...構文を併用して下記のように書きます。
(例として、渋谷にセブンイレブンとファミリーマートがきたということにします。セブンは既に存在しています。)
# セブンの場合、もう渋谷にあるのでインサートされない
INSERT INTO cities_stores (city_id, store_id)
  SELECT target_city_id, target_store_id FROM dual
  WHERE NOT EXISTS(SELECT * FROM cities_stores WHERE city_id = 1 AND store_id = 1);
# ファミマの場合、まだ渋谷にないのでインサートされる
INSERT INTO cities_stores (city_id, store_id)
  SELECT target_city_id, target_store_id FROM dual
  WHERE NOT EXISTS(SELECT * FROM cities_stores WHERE city_id = 1 AND store_id = 2);
dualはテーブルを参照する必要のない場合に使用するダミーテーブルで、WHERE句を指定したい場合などに入れる必要があるそうです。
SELECT Syntax
私自身は特に省略しても違和感はありませんが、Oracle データベースなどを扱っていた方々は 記述するのが当たり前のようです。

とりあえずSQL文はできましたが、これだとレコード数が少ない場合は良いのですが、
レコード数が多くなると面倒なので、ストアドプロシージャを使用してループ文を作りたいと思います。
ストアドプロシージャは機会があればいろいろ試して、ブログにしたいと思いますが、 ←よく分かっていないだけ
参考資料を下記に載せておきます。
CREATE PROCEDURE and CREATE FUNCTION Syntax
はじめてのMySQL ストアドプロシージャ・ストアドファンクション
DELIMITER //
CREATE PROCEDURE RegisterAllStoresWithCity(IN target_city_id INT)
BEGIN
  DECLARE target_store_id INT;
  DECLARE max_id_stores INT;

  SET target_store_id = 1;
  SET max_id_stores = (SELECT MAX(id) FROM stores);

  WHILE target_store_id <= max_id_stores DO
    INSERT INTO cities_stores (city_id, store_id)
      SELECT target_city_id, target_store_id FROM dual
      WHERE NOT EXISTS(SELECT * FROM cities_stores WHERE city_id = target_city_id AND store_id = target_store_id);

    SET target_store_id = target_store_id + 1;
end WHILE; END // DELIMITER ;
これでRegisterAllStoresWithCity()が登録されたので、下記で呼び出し、インサートしてみます。
CALL RegisterAllStoresWithCity(1);
これで、渋谷に全ての店が出店できました!
SELECT cs.id, c.name, s.name FROM cities_stores cs INNER JOIN cities c ON cs.city_id = c.id INNER JOIN stores s ON cs.store_id = s.id;
id cities.name stores.name
1 渋谷 セブンイレブン
2 渋谷 ファミリーマート
3 渋谷 ローソン
25 渋谷 ミニストップ
26 渋谷 サークルK
27 渋谷 スーパー
余談ですが、登録したRegisterAllStoresWithCityをリセットするには下記コマンドになるそうです。
DROP PROCEDURE IF EXISTS RegisterAllStoresWithCity;

2016年12月26日月曜日

InnoDBでauto_incrementの値が戻る?

Taroです、最近寒くて朝が辛いです。
本日は実際に困って調べた話にしたいと思います。
タイトルですが、InnoDBだとDBを再起動した際にauto_incrementが最適化されてしまうとのこと。
すでに多くの知見があるようですが、今回は実際に試してみました。
5.5.9のリリースにて、最適化にてリセットされる問題は解消されたようですが、今回は試しにやってみたいと思います。
Changes in MySQL 5.5.9 (2011-02-07, General Availability)

準備


MySQLのバージョンは5.7.14になります。
$ mysql --version
mysql  Ver 14.14 Distrib 5.7.14, for osx10.10 (x86_64) using  EditLine wrapper
とりあえず、テスト用のDBを作成します。
mysql> create database test;
ストレージエンジンの一覧を見てみます。
現在はInnoDBがデフォルトですね。(というより、こんなにたくさんあるんですね。。。)
mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
今回はInnoDBとよく比較されるMyISAMでも試したいと思います。
InnoDBとMyISAMの簡単な特徴は下記になります。
  • InnoDBはレコード単位でロックされるが、MyISAMはテーブル単位。
  • トランザクション機能がMyISAMにはない。
  • InnoDBはMyISAMに比べてデータサイズが大きくなる。
  • InnoDBは更新系、MyISAMは参照系が得意。

それでは実際にそれぞれのエンジンのテーブルを作成します。
mysql> create table innodb_test (id int primary key auto_increment) engine=InnoDB;
mysql> create table myisam_test (id int primary key auto_increment) engine=MyISAM;
実際に各テーブルのストレージエンジンはinformation_schemaに移動すれば見ることができます。
mysql> use information_schema;
mysql> select table_name, engine from tables where table_schema = "test";
+-------------+--------+
| table_name  | engine |
+-------------+--------+
| innodb_test | InnoDB |
| myisam_test | MyISAM |
+-------------+--------+

レコード作成


それぞれのテーブルにレコードを作成します。
mysql> insert into innodb_test (id) values (0), (0), (0), (0), (0);
mysql> insert into myisam_test (id) values (0), (0), (0), (0), (0);
mysql> select * from innodb_test;
mysql> select * from myisam_test;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
|  4 |
|  5 |
+----+
とりあえず、2つレコードを削除しておきます。
mysql> delete from innodb_test where id in (4, 5);
mysql> delete from myisam_test where id in (4, 5);

テーブル最適化


それでは最適化してみます。
まずはInnoDBから。
mysql> optimize table innodb_test;
+------------------+----------+----------+-------------------------------------------------------------------+
| Table            | Op       | Msg_type | Msg_text                                                          |
+------------------+----------+----------+-------------------------------------------------------------------+
| test.innodb_test | optimize | note     | Table does not support optimize, doing recreate + analyze instead |
| test.innodb_test | optimize | status   | OK                                                                |
+------------------+----------+----------+-------------------------------------------------------------------+
およ、何かメッセージがでています。
どうやらInnoDBではoptimizeコマンドはALTER TABLEとして実行されるそうです。(その通知メッセージのようです。)
14.7.2.4 OPTIMIZE TABLE Syntax

続きましてMyISAMです。
mysql> optimize table myisam_test;
+------------------+----------+----------+----------+
| Table            | Op       | Msg_type | Msg_text |
+------------------+----------+----------+----------+
| test.myisam_test | optimize | status   | OK       |
+------------------+----------+----------+----------+
それでは、再度インサートします。
mysql> insert into innodb_test (id) values (0), (0);
mysql> insert into myisam_test (id) values (0), (0);
mysql> select * from innodb_test;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
|  6 |
|  7 |
+----+
mysql> select * from myisam_test;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
|  6 |
|  7 |
+----+
最適化ではやはり戻らないですね。
次は再起動を試してみます。

MySQL再起動


それでは再起動後にインサートしてみます。(再起動前はauto_incrementの値が13でした。)
mysql> select * from innodb_test;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
|  4 |
|  5 |
+----+

mysql> select * from myisam_test;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
| 13 |
| 14 |
+----+
InnoDBの方はauto incrementが戻っていました。
実際、テーブル情報を見ると、戻っていました。
mysql> show table status like 'innodb_test' \G;
*************************** 1. row ***************************
           Name: innodb_test
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 3
 Avg_row_length: 5461
    Data_length: 16384
Max_data_length: 0
   Index_length: 0
      Data_free: 0
 Auto_increment: 4
    Create_time: 2016-12-15 23:38:15
    Update_time: NULL
     Check_time: NULL
      Collation: utf8_general_ci
       Checksum: NULL
 Create_options: 
        Comment: 

mysql> show table status like 'myisam_test' \G;
*************************** 1. row ***************************
           Name: myisam_test
         Engine: MyISAM
        Version: 10
     Row_format: Fixed
           Rows: 3
 Avg_row_length: 7
    Data_length: 56
Max_data_length: 1970324836974591
   Index_length: 2048
      Data_free: 35
 Auto_increment: 13
    Create_time: 2016-12-15 19:30:36
    Update_time: 2016-12-15 23:52:41
     Check_time: 2016-12-15 23:38:22
      Collation: utf8_general_ci
       Checksum: NULL
 Create_options: 
        Comment: 


最適化では確認できませんでしたが、再起動ではInnoDBの方はauto_incrementの値が戻っておりました。
InnoDBではauto incrementのカラムをメモリ上のみに持ち、再起動のタイミングで以下で値を取得しているようです。
SELECT MAX(ai_col) FROM table_name FOR UPDATE;

参考資料
15.8.6 AUTO_INCREMENT Handling in InnoDB
【MySQL】AUTO_INCREMENTの値が戻る@InnoDBエンジンのテーブル
Changes in MySQL 5.5.9 (2011-02-07, General Availability)
MySQLの「InnoDB」と「MyISAM」についての易しめな違い
14.7.2.4 OPTIMIZE TABLE Syntax

2016年12月17日土曜日

MySQL「auto_increment」と「on duplicate key update」

 こんにちは、Hiroです。
 早速ですが、既にレコードが存在していればUPDATEをし、存在していなければINSERTをするという処理は、開発をしていると時々必要になってきますよね。
 MySQLの場合、そのような時に「on duplicate key update」を使うと便利ですが、ちょっとした注意点がありますので、今回はそのことをブログにしました。


「on duplicate key update」とは


MySQLの公式リファレンスより引用します。
ON DUPLICATE KEY UPDATE を指定したとき、UNIQUEインデックスまたはPRIMARY KEYに重複した値を発生させる行が
挿入された場合は、MySQLによって古い行のUPDATEが実行されます。
また、MySQLの固有の構文であり、PostgreSQLやOracleではまた別の構文で実現させる必要があります。


注意点!「auto_increment」の増加


上記の記述だけを読むと重複した行の場合は、UPDATEが実行されるとあるので、PRIMARY KEYが「auto_increment」であっても、UPDATE時は増加しないと思えますが、直感に反して、「on duplicate key update」を使った場合は、UPDATE(レコード数は増加しない)でも、auto_increment値が増加してしまします。
実行結果をもとに説明していきます。


まず、テーブルの説明つについて。
あるスポットのいいね数を管理(カウントアップ/ダウン)するためのテーブルを例にしたいと思います。idカラムがPRIMARY KEYで「auto_increment」として定義しています。mst_spot_idがスポットを特定するためのidで、UNIQUEインデックスとなっています。
mysql> desc spot_like_counts;
+----------------------------+-----------+------+-----+---------+----------------+
| Field                                | Type      | Null   | Key | Default | Extra             |
+----------------------------+-----------+------+-----+---------+----------------+
| id                                    | int(11)   | NO    | PRI  | NULL    | auto_increment |
| mst_spot_id                 | int(11)   | NO    | UNI | NULL    |                        |
| like_count                     | int(11)   | NO    |         | 0          |                       |
| created_at                    | datetime | NO   |         | NULL   |                       |
| updated_at                   | datetime | NO   |         | NULL   |                       |
+----------------------------+----------+-------+-----+---------+----------------+
何もINSERTされていな空の状態では、当然「auto_increment」値は「1」となっています。
mysql> SHOW TABLE STATUS LIKE 'spot_like_counts'\G
*************************** 1. row ***************************
                    Name: spot_like_counts
                   Engine: InnoDB
                 Version: 10
         Row_format: Dynamic
                     Rows: 0
 Avg_row_length: 0
         Data_length: 16384
Max_data_length: 0
        Index_length: 0
             Data_free: 0
  Auto_increment: 1                     ← ここです。
         Create_time: 2016-12-16 12:03:21
        Update_time: NULL
          Check_time: NULL
                Collation: utf8mb4_general_ci
             Checksum: NULL
    Create_options: 
              Comment: 
1 row in set (0.00 sec)
もう一度「INSERT...ON DUPLICATE KEY UPDATE」をするとmst_spot_idがUNIQUEインデックスとなっているため、既存のレコードが更新され、like_countが「2」となります。
mysql> INSERT INTO spot_like_counts(mst_spot_id, like_count, created_at, updated_at) values(1, 1, NOW(), NOW()) ON DUPLICATE KEY UPDATE like_count=like_count+1;
Query OK, 2 rows affected (0.02 sec)

mysql> SELECT * FROM spot_like_counts;
+----+----------------------------+-------------+---------------------+---------------------+
| id   | mst_spot_id                 | like_count | created_at          | updated_at          |
+----+----------------------------+-------------+---------------------+---------------------+
|  1   |                                     1 |                2 | 2016-12-16 12:08:03 | 2016-12-16 12:08:03 |
+----+----------------------------+-------------+---------------------+---------------------+
1 row in set (0.00 sec)
ここで、再度「auto_increment」値を確認するとレコードがINSERTされておらず、UPDATEされているにもかかわらず、 「auto_increment」値は1増加して、「3」となっています。
mysql> SHOW TABLE STATUS LIKE 'spot_like_counts'\G
*************************** 1. row ***************************
                    Name: spot_like_counts
                  Engine: InnoDB
                 Version: 10
         Row_format: Dynamic
                     Rows: 1
  Avg_row_length: 16384
         Data_length: 16384
 Max_data_length: 0
        Index_length: 0
             Data_free: 0
   Auto_increment: 3                     ← ここです。
         Create_time: 2016-12-16 12:03:21
        Update_time: 2016-12-16 12:21:50
          Check_time: NULL
               Collation: utf8mb4_general_ci
             Checksum: NULL
    Create_options: 
              Comment: 
1 row in set (0.00 sec)
ここで、mst_spot_idを1ではなく、2にしてINSERTを行うとidは「2」を飛ばして、「3」としてレコードがINSERTされます。
mysql> INSERT INTO spot_like_counts(mst_spot_id, like_count, created_at, updated_at) values(2, 1, NOW(), NOW()) ON DUPLICATE KEY UPDATE like_count=like_count+1;
Query OK, 1 row affected (0.02 sec)

mysql> SELECT * FROM spot_like_counts;
+----+----------------------------+--------------+---------------------+---------------------+
| id   | mst_spot_id                 | like_count  | created_at          | updated_at          |
+----+----------------------------+--------------+---------------------+---------------------+
|  1   |                                    1 |                 2 | 2016-12-16 12:08:03 | 2016-12-16 12:08:03 |
|  3   |                                    2 |                 1 | 2016-12-16 12:22:45 | 2016-12-16 12:22:45 |
+----+----------------------------+--------------+---------------------+---------------------+
2 rows in set (0.00 sec)

 SHOW TABLE STATUS LIKE 'spot_like_counts'\G
*************************** 1. row ***************************
                    Name: spot_like_counts
                  Engine: InnoDB
                 Version: 10
         Row_format: Dynamic
                     Rows: 2
  Avg_row_length: 8192
         Data_length: 16384
 Max_data_length: 0
        Index_length: 16384
             Data_free: 0
   Auto_increment: 4                     ← ここです。
         Create_time: 2016-12-16 12:03:21
        Update_time: 2016-12-16 12:22:45
          Check_time: NULL
               Collation: utf8mb4_general_ci
             Checksum: NULL
    Create_options: 
              Comment: 
1 row in set (0.00 sec)


まとめ


「on duplicate key update」を使うと「既にレコードが存在していればUPDATEをし、存在していなければINSERTをするという処理」が簡単に実現することができます。ただし、注意点としてはUPDATEであっても「auto_increment」値が増加し続けるということです。そのため、UPDATEが大量に発生するテーブルに対して利用すると「auto_increment」値が増加し続けてしまい、最大値まで達してしまう可能性があるということを認識して利用すべきでしょう。

次回は、Ruby on Railsで「on duplicate key update」を利用せずに、同じような機能を実現する方法を紹介したいと思います。

2016年12月3日土曜日