【SIer向けSQLパフォーマンスチューニングの基本】

SIer Tech Blog
2025年3月19日
【SIer向けSQLパフォーマンスチューニングの基本】
業務システムのパフォーマンスにおいて、SQLのチューニングは非常に重要な要素です。本記事では、SIerの現場で実際に使えるSQLパフォーマンスチューニングの手法について、具体例を交えながら解説します。
1. SQLパフォーマンスチューニングの基本
1.1 パフォーマンス問題の特定
パフォーマンスチューニングの第一歩は、問題の特定と計測です:
1.1.1 実行計画の確認
-- PostgreSQLでの実行計画の確認
EXPLAIN ANALYZE
SELECT c.customer_id,
c.customer_name,
COUNT(o.order_id) as order_count,
SUM(o.total_amount) as total_sales
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE c.status = 'active'
GROUP BY c.customer_id, c.customer_name;
-- Oracleでの実行計画の確認
EXPLAIN PLAN FOR
SELECT /*+ GATHER_PLAN_STATISTICS */
c.customer_id,
c.customer_name,
COUNT(o.order_id) as order_count,
SUM(o.total_amount) as total_sales
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE c.status = 'active'
GROUP BY c.customer_id, c.customer_name;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));
1.1.2 パフォーマンス指標の計測
主要なパフォーマンス指標:
- 実行時間: クエリの完了までにかかる時間
- 論理読み込み: バッファキャッシュからの読み込み回数
- 物理読み込み: ディスクからの読み込み回数
- CPU使用率: クエリ実行時のCPU使用率
- メモリ使用量: クエリ実行時のメモリ使用量
1.2 インデックス設計の基本
効果的なインデックス設計は、SQLパフォーマンスの要です:
1.2.1 基本的なインデックス作成
-- 単一列インデックス
CREATE INDEX idx_customers_status ON customers(status);
-- 複合インデックス
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date);
-- 関数インデックス
CREATE INDEX idx_customers_name_upper ON customers(UPPER(customer_name));
-- 部分インデックス(PostgreSQL)
CREATE INDEX idx_orders_active ON orders(order_date)
WHERE status = 'active';
1.2.2 インデックス選定の基準
インデックスを作成する際の判断基準:
- カーディナリティ: 列の値の種類の多さ
- 選択性: WHERE句での絞り込み効果
- 更新頻度: INSERT/UPDATE/DELETEの頻度
- 結合条件: JOINで使用される列
-- カーディナリティの確認
SELECT COUNT(DISTINCT column_name) / COUNT(*) as selectivity
FROM table_name;
-- インデックスの使用状況確認(PostgreSQL)
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes;
2. クエリチューニングの実践テクニック
2.1 JOINの最適化
2.1.1 結合順序の最適化
-- 結合順序を指定(Oracle)
SELECT /*+ LEADING(c o i) USE_NL(o i) */
c.customer_name,
o.order_date,
i.item_name
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
JOIN order_items i ON o.order_id = i.order_id
WHERE c.status = 'active';
-- 結合順序を指定(PostgreSQL)
SET join_collapse_limit = 1;
SELECT c.customer_name,
o.order_date,
i.item_name
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
JOIN order_items i ON o.order_id = i.order_id
WHERE c.status = 'active';
2.1.2 結合アルゴリズムの選択
-- Nested Loop Joinの強制(Oracle)
SELECT /*+ USE_NL(o i) */
c.customer_name,
o.order_date,
i.item_name
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
JOIN order_items i ON o.order_id = i.order_id;
-- Hash Joinの強制(PostgreSQL)
SET enable_nestloop = off;
SET enable_mergejoin = off;
SELECT c.customer_name,
o.order_date,
i.item_name
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
JOIN order_items i ON o.order_id = i.order_id;
2.2 サブクエリの最適化
2.2.1 相関サブクエリの書き換え
-- 相関サブクエリ(非効率)
SELECT customer_id,
customer_name,
(SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = c.customer_id) as order_count
FROM customers c;
-- 結合に書き換え(効率的)
SELECT c.customer_id,
c.customer_name,
COUNT(o.order_id) as order_count
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name;
2.2.2 EXISTS vs IN の使い分け
-- INの使用(大量データの場合は非効率)
SELECT *
FROM orders
WHERE customer_id IN (
SELECT customer_id
FROM customers
WHERE status = 'premium'
);
-- EXISTSの使用(より効率的)
SELECT *
FROM orders o
WHERE EXISTS (
SELECT 1
FROM customers c
WHERE c.customer_id = o.customer_id
AND c.status = 'premium'
);
2.3 集約関数の最適化
2.3.1 集約の効率化
-- 事前集計テーブルの利用
CREATE TABLE daily_sales_summary AS
SELECT order_date,
product_id,
COUNT(*) as order_count,
SUM(quantity) as total_quantity,
SUM(amount) as total_amount
FROM orders o
JOIN order_items i ON o.order_id = i.order_id
GROUP BY order_date, product_id;
-- マテリアライズドビューの作成(Oracle)
CREATE MATERIALIZED VIEW mv_monthly_sales
REFRESH ON DEMAND
AS
SELECT TRUNC(order_date, 'MM') as month,
product_id,
COUNT(*) as order_count,
SUM(amount) as total_amount
FROM orders
GROUP BY TRUNC(order_date, 'MM'), product_id;
2.3.2 HAVING句の最適化
-- 非効率なHAVING句の使用
SELECT customer_id,
COUNT(*) as order_count
FROM orders
GROUP BY customer_id
HAVING customer_id IN (
SELECT customer_id
FROM customers
WHERE status = 'active'
);
-- 最適化したクエリ
SELECT o.customer_id,
COUNT(*) as order_count
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE c.status = 'active'
GROUP BY o.customer_id;
3. パーティショニングとシャーディング
3.1 テーブルパーティショニング
3.1.1 範囲パーティショニング
-- PostgreSQLでの範囲パーティショニング
CREATE TABLE orders (
order_id SERIAL,
order_date DATE,
customer_id INTEGER,
amount DECIMAL(10,2)
) PARTITION BY RANGE (order_date);
CREATE TABLE orders_2024q1 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE orders_2024q2 PARTITION OF orders
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
-- Oracleでの範囲パーティショニング
CREATE TABLE orders (
order_id NUMBER,
order_date DATE,
customer_id NUMBER,
amount NUMBER(10,2)
)
PARTITION BY RANGE (order_date) (
PARTITION orders_2024q1 VALUES LESS THAN (TO_DATE('2024-04-01', 'YYYY-MM-DD')),
PARTITION orders_2024q2 VALUES LESS THAN (TO_DATE('2024-07-01', 'YYYY-MM-DD'))
);
3.1.2 リストパーティショニング
-- PostgreSQLでのリストパーティショニング
CREATE TABLE sales (
sale_id SERIAL,
sale_date DATE,
region VARCHAR(50),
amount DECIMAL(10,2)
) PARTITION BY LIST (region);
CREATE TABLE sales_east PARTITION OF sales
FOR VALUES IN ('東京', '神奈川', '千葉', '埼玉');
CREATE TABLE sales_west PARTITION OF sales
FOR VALUES IN ('大阪', '京都', '兵庫');
-- Oracleでのリストパーティショニング
CREATE TABLE sales (
sale_id NUMBER,
sale_date DATE,
region VARCHAR2(50),
amount NUMBER(10,2)
)
PARTITION BY LIST (region) (
PARTITION sales_east VALUES ('東京', '神奈川', '千葉', '埼玉'),
PARTITION sales_west VALUES ('大阪', '京都', '兵庫')
);
3.2 パーティションの管理
3.2.1 パーティションの追加と削除
-- PostgreSQLでのパーティション管理
-- 新しいパーティションの追加
CREATE TABLE orders_2024q3 PARTITION OF orders
FOR VALUES FROM ('2024-07-01') TO ('2024-10-01');
-- 古いパーティションの削除(アーカイブ後)
DROP TABLE orders_2023q1;
-- Oracleでのパーティション管理
-- 新しいパーティションの追加
ALTER TABLE orders ADD PARTITION orders_2024q3
VALUES LESS THAN (TO_DATE('2024-10-01', 'YYYY-MM-DD'));
-- 古いパーティションの削除
ALTER TABLE orders DROP PARTITION orders_2023q1;
4. 統計情報の管理
4.1 統計情報の収集
-- PostgreSQLでの統計情報収集
ANALYZE customers;
ANALYZE orders;
-- より詳細な統計情報の収集
ALTER TABLE customers ALTER COLUMN status SET STATISTICS 1000;
ANALYZE customers;
-- Oracleでの統計情報収集
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(
ownname => 'SCHEMA_NAME',
tabname => 'CUSTOMERS',
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
method_opt => 'FOR ALL COLUMNS SIZE AUTO'
);
END;
/
4.2 統計情報の監視と更新
-- PostgreSQLでの統計情報の確認
SELECT schemaname,
tablename,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables;
-- Oracleでの統計情報の確認
SELECT owner,
table_name,
last_analyzed,
num_rows,
blocks
FROM dba_tables
WHERE owner = 'SCHEMA_NAME';
5. メモリ管理とバッファキャッシュ
5.1 共有バッファの設定
-- PostgreSQLでのバッファ設定
ALTER SYSTEM SET shared_buffers = '2GB';
ALTER SYSTEM SET work_mem = '16MB';
ALTER SYSTEM SET maintenance_work_mem = '256MB';
-- Oracleでのバッファ設定
ALTER SYSTEM SET sga_target = 4G SCOPE=SPFILE;
ALTER SYSTEM SET pga_aggregate_target = 1G SCOPE=SPFILE;
5.2 バッファキャッシュの監視
-- PostgreSQLでのバッファキャッシュ監視
SELECT c.relname,
count(*) blocks,
round(count(*) * 8192.0 / 1024 / 1024, 2) size_mb
FROM pg_buffercache b
JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid)
GROUP BY c.relname
ORDER BY blocks DESC
LIMIT 10;
-- Oracleでのバッファキャッシュ監視
SELECT owner,
object_name,
subobject_name,
COUNT(*) blocks,
COUNT(*) * 8192 / 1024 / 1024 size_mb
FROM v$bh b
JOIN dba_objects o ON o.data_object_id = b.objd
GROUP BY owner, object_name, subobject_name
ORDER BY blocks DESC;
6. 実行計画の制御
6.1 ヒント句の使用
-- PostgreSQLでのヒント句(pg_hint_planが必要)
/*+ IndexScan(customers customers_pkey) */
SELECT *
FROM customers
WHERE customer_id = 1000;
-- Oracleでのヒント句
SELECT /*+ INDEX(customers customers_pkey) */
*
FROM customers
WHERE customer_id = 1000;
-- 結合方式の指定
SELECT /*+ USE_HASH(c o) */
c.customer_name,
o.order_date
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id;
6.2 実行計画の固定化
-- PostgreSQLでの実行計画の固定(prepared statement)
PREPARE customer_orders(int) AS
SELECT c.customer_name,
COUNT(*) as order_count
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.customer_id = $1
GROUP BY c.customer_name;
-- Oracleでの実行計画の固定(SQL Plan Baseline)
CREATE SQL_PLAN_BASELINE
SELECT c.customer_name,
COUNT(*) as order_count
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.customer_id = :1
GROUP BY c.customer_name;
7. 運用面での注意点
7.1 定期メンテナンス
-- PostgreSQLでの定期メンテナンス
-- テーブルの断片化解消
VACUUM FULL customers;
VACUUM ANALYZE orders;
-- インデックスの再構築
REINDEX TABLE customers;
-- Oracleでの定期メンテナンス
-- テーブルの断片化解消
ALTER TABLE customers MOVE;
ALTER TABLE orders MOVE;
-- インデックスの再構築
ALTER INDEX customers_pk REBUILD;
7.2 監視と分析
-- PostgreSQLでのクエリ実行統計
SELECT queryid,
calls,
total_time,
mean_time,
rows,
query
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;
-- Oracleでのクエリ実行統計
SELECT sql_id,
executions,
elapsed_time,
cpu_time,
buffer_gets,
disk_reads,
rows_processed
FROM v$sqlarea
ORDER BY elapsed_time DESC;
8. パフォーマンスチューニングのベストプラクティス
-
計測と分析を重視
- 問題の特定と定量的な評価
- ボトルネックの正確な把握
- 改善効果の測定
-
段階的な改善
- 最も効果の高い部分から着手
- 一度に複数の変更を避ける
- 各変更の効果を確認
-
テスト環境での検証
- 本番環境と同等のデータ量
- 実際の負荷パターンの再現
- 副作用の確認
-
ドキュメント化
- チューニングの経緯と理由
- 実施した変更の詳細
- 効果の測定結果
まとめ
SQLパフォーマンスチューニングは、システムの性能向上において重要な要素です。本記事で紹介した手法を基本として、以下の点に注意しながら実践してください:
-
問題の正確な把握
- 実行計画の分析
- パフォーマンス指標の測定
- ボトルネックの特定
-
適切な対策の選択
- インデックス設計
- クエリの最適化
- システム設定の調整
-
継続的な監視と改善
- 定期的なメンテナンス
- パフォーマンスの監視
- 新たな問題への対応
参考文献
- 「SQLパフォーマンス詳解」- Baron Schwartz他
- 「PostgreSQL実践入門」- 松井暢之
- 「Expert Oracle Database Architecture」- Thomas Kyte
- 「SQLアンチパターン」- Bill Karwin
パフォーマンスチューニングは継続的な改善プロセスです。システムの特性や要件に応じて、適切な手法を選択し、効果的なチューニングを実施してください。
関連記事
2025/3/25
【「動作保証はどこまで?」SIerのためのシステム保守の基本】
SIerエンジニアのためのシステム保守ガイド。業務システムの保守範囲の定義から具体的な保守活動まで、実践的なアプローチを解説します。
2025/3/24
【SIerが知るべきログ設計のベストプラクティス】
SIerエンジニアのためのログ設計ガイド。業務システムにおける効果的なログ設計から運用管理まで、実践的なベストプラクティスを解説します。
2025/3/23
【長年運用されている業務システムの"負債"とどう向き合うか?】
SIerエンジニアのための技術的負債管理ガイド。長年運用されてきた業務システムの負債を理解し、効果的に管理・改善していくための実践的なアプローチを解説します。