2025.06.19
クエリ最適化だけで満足していませんか?SQLパフォーマンスチューニングの新視点
2025.06.19
DBクエリ最適化だけで満足していませんか?SQLパフォーマンスチューニングの新視点
SQLのパフォーマンスチューニングと聞くと、多くのエンジニアは「インデックスの追加」「JOINの見直し」「EXPLAINによる分析」といった手法を思い浮かべるでしょう。しかし、それだけで十分でしょうか?
実は、SQLのパフォーマンスを向上させるためには、クエリの書き方だけでなく、データベースの設計やアプリケーションとの連携も深く考慮する必要があります。本記事では、従来のアプローチとは異なる視点から、SQLパフォーマンスチューニングの重要なポイントを掘り下げます。
「正規化してスリムなテーブル設計をするのが正義」だと思っていませんか? もちろん、データの整合性を保つために正規化は重要です。しかし、極端に正規化しすぎると、データ取得時に複雑なJOINが発生し、パフォーマンスが低下することもあります。
例えば、以下のような2つのテーブルがあるとします。
-- ユーザー情報テーブル
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100)
);
-- ユーザーの注文情報テーブル
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
product_name VARCHAR(100),
price DECIMAL(10,2),
FOREIGN KEY (user_id) REFERENCES users(id)
);
この設計では、ユーザーの注文履歴を取得する際にJOINを使う必要があります。
SELECT users.name, orders.product_name, orders.price
FROM users
JOIN orders ON users.id = orders.user_id;
この方法はデータの一貫性を保ちやすい一方で、大量のデータがあるとJOINによる負荷が増えます。
そこで、非正規化の手法を用い、よく使う情報をあらかじめ格納した「キャッシュテーブル」を作成することで、クエリの負担を軽減できます。
CREATE TABLE user_order_summary (
user_id INT PRIMARY KEY,
name VARCHAR(50),
last_order_product VARCHAR(100),
total_spent DECIMAL(10,2)
);
このテーブルを適宜更新することで、シンプルなSELECT文で高速にデータ取得が可能になります。
SELECT * FROM user_order_summary WHERE user_id = 1;
SQLをチューニングするだけでなく、そもそもSQLを実行する回数を減らすことも重要です。
たとえば、RedisやMemcachedといったインメモリキャッシュを使うことで、同じデータを何度もDBに問い合わせる負担を減らせます。
Python + Redis の例:
import redis
import pymysql
# Redisに接続
cache = redis.Redis(host='localhost', port=6379, db=0)
# キャッシュから取得
user_id = 1
cached_data = cache.get(f'user:{user_id}')
if cached_data:
print("Cache Hit!", cached_data)
else:
# データベース接続
conn = pymysql.connect(host='localhost', user='root', password='', db='test')
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user_data = cursor.fetchone()
# キャッシュに保存
cache.set(f'user:{user_id}', user_data, ex=3600) # 1時間キャッシュ
print("Cache Miss!", user_data)
こうすることで、頻繁にアクセスされるデータはDBを経由せず、高速に取得できます。
大量データを一括処理するとき、トランザクション管理やバッチ処理の分割がカギになります。
NG例: 10万件のデータを1つずつINSERTする
INSERT INTO logs (message, created_at) VALUES ('log1', NOW());
INSERT INTO logs (message, created_at) VALUES ('log2', NOW());
... 10万回繰り返し ...
OK例: バルクインサートを使う
INSERT INTO logs (message, created_at) VALUES
('log1', NOW()),
('log2', NOW()),
('log3', NOW());
また、バッチ処理を分割することでデータベースの負荷を分散できます。
SELECT * FROM logs WHERE created_at BETWEEN '2024-01-01' AND '2024-01-31';
DBの設定を見直すだけで劇的に速くなることもあります!
例えば、MySQLのinnodb_buffer_pool_size
を適切に設定するだけで、ディスクI/Oの削減が可能です。
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SET GLOBAL innodb_buffer_pool_size = 2G;
PostgreSQLでは、work_mem
を調整すると、ORDER BYやJOINのパフォーマンスが向上します。
SHOW work_mem;
SET work_mem = '64MB';
SQLチューニングを行う際は、実際の本番環境のデータ量やアクセスパターンを考慮することが不可欠です。開発環境で数千件のデータしかない状態でテストし、本番環境の数百万件のデータで問題が発生するケースは珍しくありません。
プロファイリングツール(MySQLなら`slow_query_log`、PostgreSQLなら`pg_stat_statements`など)を活用し、本番環境のクエリパフォーマンスを定期的にチェックすることが重要です。
スロークエリログを有効にする(MySQL)
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1; -- 1秒以上のクエリを記録
SQLのパフォーマンスチューニングは、単にクエリを改善するだけではなく、データモデル、キャッシュ、バッチ処理、データベース設定、アクセスパターンなど、多角的に考える必要があります。
また、高速化はデータ整合性とトレードオフの関係だったりもします。
「SQLが遅い=クエリの問題」と決めつけず、広い視野で適切な改善策を考えてみる必要がありそうですね。
正規化 vs. 非正規化
https://clickhouse.com/docs/jp/data-modeling/denormalization
アプリケーションレイヤーのキャッシュを活用する
https://www.wallarm.com/jp/cloud-native-products-101/redis-vs-memcached-in-memory-data-store
バッチ処理の最適化
https://medium.com/nazar-io/sql-performance-killers-individual-inserts-vs-bulk-inserts-14176c201673
データベース設定の最適化
https://releem.com/docs/mysql-performance-tuning/innodb_buffer_pool_size
実際のアクセスパターンを考慮する
【記事への感想募集中!】
記事への感想・ご意見がありましたら、ぜひフォームからご投稿ください!【テクノデジタルではエンジニア/デザイナーを積極採用中です!】
下記項目に1つでも当てはまる方は是非、詳細ページへ!Qangaroo(カンガルー)
【テクノデジタルのインフラサービス】
当社では、多数のサービスの開発実績を活かし、
アプリケーションのパフォーマンスを最大限に引き出すインフラ設計・構築を行います。
AWSなどへのクラウド移行、既存インフラの監視・運用保守も承りますので、ぜひご相談ください。
詳細は下記ページをご覧ください。
最近の記事
タグ検索