あるアイテムを持っていない人だけ、別のアイテムをあげたい!
もしくはその逆で、あるアイテムを持っている人に追加でアイテムをあげたい!
そういうことってないでしょうか?
先日、僕がそのような状況になり、四苦八苦しておりました。
本日は復習をかねて、調べた内容を記述していきたいと思います。
内容
本日は主に下記の基本的なところを書きたいと思います。
- 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;