MySQL道普請便り

第231回MySQLのJOINサブクエリなどを駆使したUPDATE文の活用法

データを更新する際、他のテーブルから算出した値をもとに更新したいという状況がよくあります。通常はバッチスクリプトを組んで1行ずつ処理する方法もありますが、MySQLの機能を活用すれば、一括で効率的にデータを更新することも可能です。

MySQLのUPDATE文は、単に1つのテーブルのデータを条件に従って更新するだけでなく、JOINやサブクエリ、CASE文、LIMITといった機能を組み合わせることで、複雑なデータ操作を実現できます。今回は、これらの機能を使って高度なUPDATE構文を活用する方法について、具体例を通して解説していきます。

なお、今回使用するMySQLのバージョンは8.4です。

サンプルデータの作成

今回は、customersとordersという2つのテーブルを使用して、データの更新操作を行います。

customersテーブル
顧客の情報を管理するテーブルで、顧客ID、名前、ステータス、メールアドレスを保持しています。ステータスには、Active(アクティブ)またはInactive(非アクティブ)といった情報が含まれています。
ordersテーブル
顧客IDに紐づく注文情報を保持しており、各注文のステータス(Completed、Pending、Cancelledなど)や注文日、金額を記録しています。customer_idはcustomersテーブルとの外部キーで、どの顧客がどの注文を行ったかを示します。

以下のSQLを使用してテーブルを作成します。

CREATE TABLE customers (
    customer_id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    status VARCHAR(50),
    email VARCHAR(100)
);

CREATE TABLE orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    order_status VARCHAR(50),
    total_amount DECIMAL(10, 2),
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

データを挿入し、以下のような内容でサンプルデータを作成します。

INSERT INTO customers (name, status, email) VALUES
('Alice Johnson', 'Active', 'alice@example.com'),
('Bob Smith', 'Inactive', 'bob@example.com'),
('Charlie Lee', 'Active', 'charlie@example.com'),
('Diana Ross', 'Inactive', 'diana@example.com'),
('Evan Clark', 'Active', 'evan@example.com');

INSERT INTO orders (customer_id, order_date, order_status, total_amount) VALUES
(1, '2024-01-15', 'Completed', 120.50),
(2, '2024-02-20', 'Completed', 300.00),
(1, '2024-03-10', 'Cancelled', 45.00),
(3, '2024-01-25', 'Completed', 85.75),
(4, '2024-03-05', 'Pending', 150.25),
(5, '2024-02-15', 'Completed', 200.00);

JOINを使った更新

他のテーブルから取得したデータをもとに、特定の条件に従ってデータを更新したい場合、JOINを使ったUPDATE文が非常に有効です。JOINを利用することで、複数のテーブルを結合し、条件に基づいて一度に複数の行を更新することができます。

以下の例では、customersとordersテーブルをJOINして、注文が完了している顧客でステータスがInactiveとなっている顧客のステータスをActiveに変更します。

mysql> UPDATE customers
    -> JOIN orders ON customers.customer_id = orders.customer_id
    -> SET customers.status = 'Active'
    -> WHERE orders.order_status = 'Completed' AND customers.status = 'Inactive';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

このクエリでは、ordersテーブルのorder_statusがCompletedである行に対応するcustomersテーブルの行で、ステータスがInactiveの顧客をActiveに更新しています。

JOINを使って他のテーブルの値を更新

JOINを使うことで、別のテーブルのデータを更新条件だけでなく、更新する値そのものとして使用することもできます。以下の例では、customersテーブルのstatusがActiveの行を、ordersテーブルのorder_statusの値で更新します。

mysql> SELECT * FROM customers;
+-------------+---------------+----------+---------------------+
| customer_id | name          | status   | email               |
+-------------+---------------+----------+---------------------+
|           1 | Alice Johnson | Active   | alice@example.com   |
|           2 | Bob Smith     | Active   | bob@example.com     |
|           3 | Charlie Lee   | Active   | charlie@example.com |
|           4 | Diana Ross    | Inactive | diana@example.com   |
|           5 | Evan Clark    | Active   | evan@example.com    |
+-------------+---------------+----------+---------------------+
5 rows in set (0.00 sec)

mysql> UPDATE customers
    -> JOIN orders ON customers.customer_id = orders.customer_id
    -> SET customers.status = orders.order_status
    -> WHERE customers.status = 'Active';
Query OK, 4 rows affected (0.01 sec)
Rows matched: 4  Changed: 4  Warnings: 0

変更後のcustomersテーブルの内容を確認すると、Activeだった顧客のステータスがordersテーブルの注文ステータス(Completed)に変更されていることがわかります。

mysql> SELECT * FROM customers;
+-------------+---------------+-----------+---------------------+
| customer_id | name          | status    | email               |
+-------------+---------------+-----------+---------------------+
|           1 | Alice Johnson | Completed | alice@example.com   |
|           2 | Bob Smith     | Completed | bob@example.com     |
|           3 | Charlie Lee   | Completed | charlie@example.com |
|           4 | Diana Ross    | Inactive  | diana@example.com   |
|           5 | Evan Clark    | Completed | evan@example.com    |
+-------------+---------------+-----------+---------------------+
5 rows in set (0.00 sec)

サブクエリを使った更新

データを更新する際、JOINを使うだけでなく、サブクエリを利用してより複雑な条件を満たす更新を行うことも可能です。特に、複数のテーブルを結合するだけでは解決できない場合や、特定の集計結果に基づいて更新したい場合にサブクエリが有効です。JOINを使ったUPDATE文では表現が難しい場合、サブクエリを使用することで、より柔軟に条件を設定できます。

以下の例では、ordersテーブルに完了済みの注文order_status = 'Completed'が存在する顧客を見つけ出し、その顧客のステータスをInactiveに更新しています。

mysql> UPDATE customers
    -> SET status = 'Inactive'
    -> WHERE customer_id IN (SELECT customer_id FROM orders WHERE order_status = 'Completed');

CASE文を使った更新

データを更新する際に、条件によって異なる値を設定したい場合、CASE文を使って一つのUPDATE文で複数の条件を処理することができます。CASE文を使用することで、複数の条件に応じて異なる値を設定することができ、特定の条件に合致したときにそれぞれ異なる結果を適用できるのが特徴です。

たとえば、顧客の注文の完了数に基づいて顧客のステータスを変更したい場合、CASE文を使用することで、一度のクエリでそれを実現できます。以下の例では、注文が2回以上完了した顧客を「Premium⁠⁠、1回だけ完了した顧客を「Active⁠⁠、それ以外の顧客を「Inactive」に変更します。

mysql> UPDATE customers
    -> SET status = CASE
    ->     WHEN (SELECT COUNT(*) FROM orders WHERE orders.customer_id = customers.customer_id AND order_status = 'Completed') > 2 THEN 'Premium'
    ->     WHEN (SELECT COUNT(*) FROM orders WHERE orders.customer_id = customers.customer_id AND order_status = 'Completed') = 1 THEN 'Active'
    ->     ELSE 'Inactive'
    -> END;

LIMIT付きのUPDATE

UPDATE文にはLIMITを利用して、更新するデータの量を調整することができます。たとえば、ステータスがPendingの注文を、最初の2件だけ「Processed」に更新する例を見てみましょう。

mysql> UPDATE orders
    -> SET order_status = 'Processed'
    -> WHERE order_status = 'Pending'
    -> LIMIT 2;

ORDER BYとLIMITの組み合わせ

LIMITを使用する際、更新されるデータの順序を制御するためにはORDER BYを併用するのが一般的です。ORDER BYを使わない場合、どの行が更新されるかは不定であるためです。ORDER BYを利用することで、更新されるデータを明確に制御できます。

mysql> UPDATE orders
    -> SET order_status = 'Processed'
    -> WHERE order_status = 'Pending'
    -> ORDER BY order_id LIMIT 2;

LIMITを使う理由

大量のデータを一度に更新する場合、いくつかの問題が発生する可能性があります。

更新による大量のロック
大量のデータが一度に更新されると、他のクエリが長時間ロック待ち状態になることがあります。これにより、システム全体のパフォーマンスが低下する恐れがあります。
レプリケーション遅延
レプリケーションを使用している環境では、大量のデータを一度に更新することで、レプリカサーバーに遅延が発生してしまう可能性があります。

そのようなケースでは、LIMITを使って一度に更新するデータ量を制限し、トランザクションを分割して実行することで、パフォーマンスを維持しながら段階的に更新することが可能です。

一度で複数テーブルを更新する

MySQLのUPDATE文を使って、複数のテーブルを同時に更新することが可能です。通常、UPDATE文では1つのテーブルのみを更新しますが、JOINを使うことで複数のテーブルを同時に更新することが可能です。

mysql> UPDATE customers, orders
    -> SET customers.status = 'Active', orders.order_status = 'Completed'
    -> WHERE customers.customer_id = orders.customer_id AND orders.order_status = 'Pending';

ただし、複数テーブルをJOINして同時に更新する場合、LIMITやORDER BYを使用して更新する行数を制限したり、更新の順序を指定することはできません。

まとめ

MySQLでは、JOINやサブクエリ、CASE文、LIMITを活用することで、単純なUPDATE文以上の高度なデータ操作が可能になります。これらの特殊なUPDATE構文を駆使することで、複雑なデータ操作を効率的に行い、大量のデータを正確かつスムーズに更新することができます。

ただし、複雑なクエリになるほど、誤った条件設定や不注意によるデータ更新が発生するリスクがあります。特に、大量のデータに対してロックが取得されてしまうと、他のクエリが長時間待機する可能性があります。こうした問題を避けるためにも、実際の運用環境で利用する前に検証環境で十分にテストを行い、意図した通りに動作することを確認することが重要です。また、データの更新をする際はトランザクションを利用して更新するようにし、誤った操作をしてしまってもRollbackして元に戻せるように実行してください。

おすすめ記事

記事・ニュース一覧