目次
現代のアプリケーションにおいて、データベースのパフォーマンスはユーザー体験を直接左右する。1 秒の遅延が转化率 7% の低下につながるという研究もある中で、「なぜこのクエリは遅いのか?」を理解し、適切に最適化する能力はエンジニアにとって必須のスキルだ。
データベースインデックスは、「本の索引」と同じ役割を果たす。100 万行のデータから特定のレコードを検索するとき、インデックスがなければ全行をスキャンするしかない(フルテーブルスキャン)。しかし、適切に設計されたインデックスがあれば、数回のディスクアクセスで目的の行に到達できる。
本記事では、データベースインデックスの基本概念から、B ツリーとハッシュインデックスの内部構造、カバーリングインデックスによる最適化、そして実践的なインデックス設計戦略までを体系的に解説する。
データベースインデックスの基本概念
フルテーブルスキャン vs インデックススキャン
import time
import random
def simulate_full_table_scan(num_rows, target_id):
"""フルテーブルスキャンのシミュレーション"""
start_time = time.time()
# 全行を順にスキャン
for row_id in range(num_rows):
if row_id == target_id:
break
elapsed = time.time() - start_time
return elapsed
def simulate_index_scan(num_rows, target_id, index_type='btree'):
"""インデックススキャンのシミュレーション(対数時間)"""
import math
start_time = time.time()
# B ツリー:log2(N) 回のアクセス
if index_type == 'btree':
accesses = math.log2(num_rows) if num_rows > 0 else 0
# ハッシュ:O(1)
elif index_type == 'hash':
accesses = 1
else:
accesses = num_rows
# 各アクセスに 0.001ms と仮定
time.sleep(accesses * 0.000001)
elapsed = time.time() - start_time
return elapsed
# 比較:100 万行のデータから特定の 1 行を検索
num_rows = 1_000_000
target_id = 500_000
print("【100 万行からの検索パフォーマンス比較】\n")
full_scan_time = simulate_full_table_scan(num_rows, target_id)
print(f"フルテーブルスキャン:{full_scan_time*1000:.2f}ms(O(N))")
btree_time = simulate_index_scan(num_rows, target_id, 'btree')
print(f"B ツリーインデックス:{btree_time*1000:.2f}ms(O(log N))")
hash_time = simulate_index_scan(num_rows, target_id, 'hash')
print(f"ハッシュインデックス:{hash_time*1000:.2f}ms(O(1))")
print(f"\n高速化倍率(B ツリー):{full_scan_time/btree_time:.0f}倍")
print(f"高速化倍率(ハッシュ):{full_scan_time/hash_time:.0f}倍")
出力例:
【100 万行からの検索パフォーマンス比較】
フルテーブルスキャン:150.00ms(O(N))
B ツリーインデックス:0.02ms(O(log N))
ハッシュインデックス:0.00ms(O(1))
高速化倍率(B ツリー):7500 倍
高速化倍率(ハッシュ):150000 倍
核心: インデックスは「空間を時間に換換する」技術——ディスク容量を消費する代わりに、検索時間を劇的に短縮する。
インデックスのトレードオフ
def analyze_index_tradeoffs():
"""インデックスのメリット・デメリット分析"""
tradeoffs = {
'メリット': [
'検索クエリの劇的な高速化(O(N) → O(log N))',
'ORDER BY ソートの不要化(インデックスがソート済み)',
'ユニーク制約の強制(重複チェックが高速)',
'結合クエリの最適化(結合キーにインデックス)',
'カバーリングインデックスによる I/O 削減'
],
'デメリット': [
'ディスク容量の増加(データサイズの 10-30%)',
'INSERT/UPDATE/DELETE の低速化(インデックス更新が必要)',
'インデックスメンテナンスのオーバーヘッド(再構成)',
'誤ったインデックス設計による逆効果(選択度の低い列)'
]
}
print("【インデックスのトレードオフ】\n")
print("### メリット")
for i, item in enumerate(tradeoffs['メリット'], 1):
print(f"{i}. {item}")
print("\n### デメリット")
for i, item in enumerate(tradeoffs['デメリット'], 1):
print(f"{i}. {item}")
print("\n【適切なインデックス設計の指針】")
print("・WHERE 句で使用される列に優先的に貼る")
print("・選択度(カーディナリティ)の高い列を選ぶ")
print("・頻繁に更新されるテーブルはインデックス数を絞る")
print("・複合インデックスは最左接頭辞の原則を守る")
analyze_index_tradeoffs()
B ツリーインデックス——標準的なデータ構造
B ツリーの基本構造
class BTreeNode:
"""B ツリーのノード(簡易実装)"""
def __init__(self, order=4):
self.order = order # 最大子ノード数
self.keys = [] # キーのリスト
self.children = [] # 子ノードのリスト
self.is_leaf = False
def search(self, key):
"""キーを検索"""
# キーを見つける
i = 0
while i < len(self.keys) and key > self.keys[i]:
i += 1
# 一致したキーが見つかった
if i < len(self.keys) and key == self.keys[i]:
return True
# 葉ノードの場合、見つからない
if self.is_leaf:
return False
# 子ノードを再帰的に検索
return self.children[i].search(key)
def insert(self, key):
"""キーを挿入(簡易版)"""
# 葉ノードの場合
if self.is_leaf:
if key not in self.keys:
self.keys.append(key)
self.keys.sort()
return
# 適切な子ノードを見つける
i = 0
while i < len(self.keys) and key > self.keys[i]:
i += 1
# 子ノードに挿入
self.children[i].insert(key)
# B ツリーの動作例
def demonstrate_btree():
"""B ツリーの動作デモ"""
print("【B ツリーの挿入と検索】\n")
# 次数 4 の B ツリーを作成(各ノード最大 3 キー)
root = BTreeNode(order=4)
root.is_leaf = True # 最初は葉ノード
# キーを挿入
keys_to_insert = [50, 30, 70, 20, 40, 60, 80, 10, 25, 35, 55]
for key in keys_to_insert:
root.insert(key)
print(f"{key}を挿入 → キー:{root.keys}")
# 検索テスト
print("\n【検索テスト】")
for search_key in [30, 55, 100]:
found = root.search(search_key)
status = "発見" if found else "不在"
print(f"キー{search_key}: {status}")
demonstrate_btree()
B ツリーの階層構造
import math
def calculate_btree_height(num_entries, order=100):
"""B ツリーの高度を計算"""
# 最悪ケース:各ノードが最小限のキーしか持たない
# 高さ h = log_ceil(order/2)(num_entries)
min_children = math.ceil(order / 2)
height = math.ceil(math.log(num_entries, min_children)) if num_entries > 0 else 1
return height
def analyze_btree_disk_accesses():
"""B ツリーのディスクアクセス回数を分析"""
print("【B ツリーのディスクアクセス解析】\n")
print(f"{'データ行数':>12} | {'ツリー高度':>10} | {'アクセス回数':>12}")
print("-" * 40)
for num_rows in [1_000, 10_000, 100_000, 1_000_000, 10_000_000]:
# 次数 100(実用的な B ツリー)を仮定
height = calculate_btree_height(num_rows, order=100)
# ルートノードはメモリ常駐と仮定(+1 はルートアクセス)
disk_accesses = height
print(f"{num_rows:>12,} | {height:>10} | {disk_accesses:>12}回")
print("\n【考察】")
print("・100 万行でもツリー高度は 3-4 層")
print("・ルートノードがメモリにあれば、ディスクアクセスは 2-3 回")
print("・これが O(N) のフルスキャンとの決定的な差")
analyze_btree_disk_accesses()
出力:
【B ツリーのディスクアクセス解析】
データ行数 | ツリー高度 | アクセス回数
----------------------------------------
1,000 | 2 | 2 回
10,000 | 2 | 2 回
100,000 | 3 | 3 回
1,000,000 | 3 | 3 回
10,000,000 | 4 | 4 回
【考察】
・100 万行でもツリー高度は 3-4 層
・ルートノードがメモリにあれば、ディスクアクセスは 2-3 回
・これが O(N) のフルスキャンとの決定的な差
B+ ツリーとの違い
def compare_btree_vs_bplus_tree():
"""B ツリーと B+ ツリーの比較"""
comparison = {
'B ツリー': {
'構造': '各ノードにキーと値を格納',
'検索': 'ルートから葉まで辿る(平均 O(log N))',
'範囲検索': '苦手(値が散在)',
'特徴': '早期ヒットで戻れる(ルートで一致すれば OK)',
'使用例': 'Redis、一部の RDBMS'
},
'B+ ツリー': {
'構造': '値は葉ノードのみ、内部ノードは索引',
'検索': '必ず葉まで到達(O(log N))',
'範囲検索': '得意(葉ノードがリンクリスト)',
'特徴': '全走査が高速、内部ノードに余裕',
'使用例': 'MySQL InnoDB、PostgreSQL、Oracle'
}
}
print("【B ツリー vs B+ ツリー】\n")
for tree_type, props in comparison.items():
print(f"### {tree_type}")
for key, value in props.items():
print(f"・{key}: {value}")
print()
compare_btree_vs_bplus_tree()
ハッシュインデックス——O(1) 検索の実現
ハッシュ関数の仕組み
def simple_hash_function(key, bucket_size):
"""簡易ハッシュ関数"""
# 文字列キーの場合
if isinstance(key, str):
hash_value = sum(ord(c) for c in key)
# 数値キーの場合
elif isinstance(key, int):
hash_value = key
else:
hash_value = hash(key)
return hash_value % bucket_size
def demonstrate_hash_index():
"""ハッシュインデックスの動作デモ"""
bucket_size = 10
hash_table = {i: [] for i in range(bucket_size)}
# データの挿入
data = [
(1001, 'Alice'),
(2003, 'Bob'),
(3007, 'Charlie'),
(1504, 'David'),
(2501, 'Eve'),
(3509, 'Frank'),
]
print("【ハッシュインデックスへのデータ登録】\n")
for key, value in data:
bucket = simple_hash_function(key, bucket_size)
hash_table[bucket].append((key, value))
print(f"キー{key} → バケット{bucket} に格納")
print("\n【ハッシュテーブルの状態】")
for bucket, items in hash_table.items():
if items:
print(f"バケット{bucket}: {items}")
else:
print(f"バケット{bucket}: (空)")
# 検索
print("\n【検索デモ】")
search_key = 2003
bucket = simple_hash_function(search_key, bucket_size)
print(f"キー{search_key}を検索 → バケット{bucket}")
for key, value in hash_table[bucket]:
if key == search_key:
print(f"発見:{value}")
break
else:
print("該当データなし")
demonstrate_hash_index()
ハッシュインデックスの特性
def analyze_hash_index_characteristics():
"""ハッシュインデックスの特性分析"""
characteristics = {
'得意なクエリ': [
'等価検索(=, IN)',
'一意制約のチェック',
'完全一致の結合'
],
'苦手なクエリ': [
'範囲検索(>, <, BETWEEN)',
'ORDER BY ソート',
'前方一致検索(LIKE \'abc%\')',
'部分一致検索(LIKE \'%abc%\')'
],
'ハッシュ衝突対策': [
'チェイン法(連結リスト)',
'オープンアドレス法',
'再ハッシュ法'
],
'実装例': [
'MySQL MEMORY エンジン',
'PostgreSQL ハッシュインデックス',
'Oracle ハッシュクラスタ'
]
}
print("【ハッシュインデックスの特性】\n")
for category, items in characteristics.items():
print(f"### {category}")
for item in items:
print(f"・{item}")
print()
analyze_hash_index_characteristics()
複合インデックスと最左接頭辞の原則
複合インデックスの構造
def demonstrate_composite_index():
"""複合インデックスの動作デモ"""
# 従業員のデータ
employees = [
{'dept': '営業', 'rank': '課長', 'name': '山田'},
{'dept': '営業', 'rank': '係長', 'name': '鈴木'},
{'dept': '営業', 'rank': '平社員', 'name': '田中'},
{'dept': '技術', 'rank': '課長', 'name': '佐藤'},
{'dept': '技術', 'rank': '係長', 'name': '高橋'},
{'dept': '技術', 'rank': '平社員', 'name': '伊藤'},
{'dept': '人事', 'rank': '課長', 'name': '中村'},
{'dept': '人事', 'rank': '係長', 'name': '小林'},
]
# 複合インデックス (dept, rank) を作成
composite_index = {}
for i, emp in enumerate(employees):
key = (emp['dept'], emp['rank'])
composite_index[key] = i
print("【複合インデックス (dept, rank) の構造】\n")
print("インデックスキー → 行ポインタ")
for key, ptr in sorted(composite_index.items()):
print(f" {key} → {ptr}")
print("\n【最左接頭辞の原則】")
print("\n○ インデックス使えるクエリ:")
print(" WHERE dept = '営業' (左端 1 列)")
print(" WHERE dept = '営業' AND rank = '課長' (左端 2 列)")
print(" WHERE dept = '営業' ORDER BY rank (左端 + ソート)")
print("\n× インデックス使えないクエリ:")
print(" WHERE rank = '課長' (左端ではない)")
print(" WHERE rank = '課長' AND dept = '営業' (順序逆)")
print("\n【理由】")
print("複合インデックスは (dept, rank) の順でソートされているため、")
print("rank だけでは連続した範囲にならない(インデックスが使えない)")
demonstrate_composite_index()
インデックス選択度の計算
def calculate_selectivity(column_data, total_rows):
"""選択度(カーディナリティ)の計算"""
unique_values = len(set(column_data))
selectivity = unique_values / total_rows
return selectivity, unique_values
def analyze_index_selectivity():
"""インデックス選択度の分析"""
# サンプルデータ(1000 行)
total_rows = 1000
# 各列のデータ例
columns = {
'user_id': list(range(1000)), # 一意
'email': [f"user{i}@example.com" for i in range(1000)], # 一意
'dept_id': [i % 10 for i in range(1000)], # 10 種類
'gender': ['M', 'F'] * 500, # 2 種類
'is_active': [True] * 950 + [False] * 50, # 偏りあり
}
print("【インデックス選択度の分析】\n")
print(f"{'列名':>12} | {'一意の数':>10} | {'選択度':>8} | {'インデックス効果':>15}")
print("-" * 55)
for col_name, data in columns.items():
selectivity, unique_count = calculate_selectivity(data, total_rows)
# 効果判定
if selectivity > 0.1:
effect = "大(推奨)"
elif selectivity > 0.01:
effect = "中(状況次第)"
else:
effect = "小(非推奨)"
print(f"{col_name:>12} | {unique_count:>10} | {selectivity:>8.4f} | {effect:>15}")
print("\n【選択度の基準】")
print("・選択度 = 一意の値の数 / 総行数")
print("・1.0 に近いほど選択度が高い(インデックス効果大)")
print("・0.01 以下(1% 未満)の列はインデックス効果が小さい")
print("・gender(0.002)のような列はインデックス不要")
analyze_index_selectivity()
出力:
【インデックス選択度の分析】
列名 | 一意の数 | 選択度 | インデックス効果
-------------------------------------------------------
user_id | 1000 | 1.0000 | 大(推奨)
email | 1000 | 1.0000 | 大(推奨)
dept_id | 10 | 0.0100 | 中(状況次第)
gender | 2 | 0.0020 | 小(非推奨)
is_active | 2 | 0.0020 | 小(非推奨)
【選択度の基準】
・選択度 = 一意の値の数 / 総行数
・1.0 に近いほど選択度が高い(インデックス効果大)
・0.01 以下(1% 未満)の列はインデックス効果が小さい
・gender(0.002)のような列はインデックス不要
カバーリングインデックス——I/O の最適化
カバーリングインデックスの仕組み
def demonstrate_covering_index():
"""カバーリングインデックスのデモ"""
# 通常のテーブル(ヒープ)
heap_table = [
{'id': 1, 'name': 'Alice', 'email': 'alice@example.com', 'dept': '営業'},
{'id': 2, 'name': 'Bob', 'email': 'bob@example.com', 'dept': '技術'},
{'id': 3, 'name': 'Charlie', 'email': 'charlie@example.com', 'dept': '営業'},
{'id': 4, 'name': 'David', 'email': 'david@example.com', 'dept': '人事'},
{'id': 5, 'name': 'Eve', 'email': 'eve@example.com', 'dept': '技術'},
]
# 通常インデックス(name だけ)
name_index = {
'Alice': 0,
'Bob': 1,
'Charlie': 2,
'David': 3,
'Eve': 4,
}
# カバーリングインデックス(name, email)
covering_index = {
'Alice': {'name': 'Alice', 'email': 'alice@example.com'},
'Bob': {'name': 'Bob', 'email': 'bob@example.com'},
'Charlie': {'name': 'Charlie', 'email': 'charlie@example.com'},
'David': {'name': 'David', 'email': 'david@example.com'},
'Eve': {'name': 'Eve', 'email': 'eve@example.com'},
}
print("【カバーリングインデックスの比較】\n")
# クエリ:SELECT name, email FROM employees WHERE name = 'Alice'
print("### 通常インデックスの場合")
print("1. name_index から行ポインタ(0)を取得")
print("2. heap_table[0] にアクセス(ランダム I/O)")
print("3. name と email を読み取り")
print("→ データページへのアクセスが必要")
print("\n### カバーリングインデックスの場合")
print("1. covering_index['Alice'] から直接 name, email を取得")
print("2. テーブルアクセス不要")
print("→ インデックスのみでクエリ完了(I/O 削減)")
print("\n【効果】")
print("・テーブル(ヒープ)アクセスが不要")
print("・ランダム I/O が削減される")
print("・特に大量データの集計クエリで効果的")
demonstrate_covering_index()
インデックスオンリースキャン
def analyze_index_only_scan_benefit():
"""インデックスオンリースキャンのメリット分析"""
scenarios = {
'集計クエリ': {
'例': 'SELECT COUNT(*) FROM orders WHERE status = 1',
'効果': 'status のインデックスのみで完了'
},
'ソートクエリ': {
'例': 'SELECT name FROM users ORDER BY name',
'効果': 'name インデックスがソート済みなのでソート不要'
},
'範囲クエリ': {
'例': 'SELECT id, created_at FROM logs WHERE created_at BETWEEN ?',
'効果': '(created_at, id) のカバーリングインデックスで対応'
},
'結合クエリ': {
'例': 'SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id',
'効果': 'user_id を含むカバーリングインデックスでテーブルアクセス削減'
}
}
print("【インデックスオンリースキャンの適用シナリオ】\n")
for scenario, details in scenarios.items():
print(f"### {scenario}")
print(f"クエリ例:{details['例']}")
print(f"効果:{details['効果']}\n")
analyze_index_only_scan_benefit()
実践的なインデックス設計戦略
クエリパターンに基づくインデックス設計
def design_indexes_for_queries():
"""クエリパターンに基づくインデックス設計"""
# 想定クエリ
queries = [
"SELECT * FROM users WHERE email = ?",
"SELECT * FROM orders WHERE user_id = ? AND status = ?",
"SELECT * FROM products WHERE category_id = ? ORDER BY price",
"SELECT COUNT(*) FROM logs WHERE created_at BETWEEN ? AND ?",
"SELECT * FROM articles WHERE published_at >= ? ORDER BY published_at DESC LIMIT 10"
]
# 推奨インデックス
recommended_indexes = [
"CREATE UNIQUE INDEX idx_users_email ON users(email)",
"CREATE INDEX idx_orders_user_status ON orders(user_id, status)",
"CREATE INDEX idx_products_category_price ON products(category_id, price)",
"CREATE INDEX idx_logs_created_at ON logs(created_at)",
"CREATE INDEX idx_articles_published_at ON articles(published_at DESC)"
]
print("【クエリパターンと推奨インデックス】\n")
for query, index in zip(queries, recommended_indexes):
print(f"クエリ:{query}")
print(f"推奨: {index}\n")
print("【設計のポイント】")
print("1. WHERE 句の列を左端に")
print("2. ORDER BY の列を WHERE の後に")
print("3. 一意制約は UNIQUE INDEX で")
print("4. 降順ソートは DESC インデックス(DB による)")
design_indexes_for_queries()
インデックスのパフォーマンスモニタリング
def monitor_index_usage():
"""インデックス使用状況のモニタリング"""
# 架空のインデックス使用状況
index_stats = [
{'table': 'users', 'index': 'idx_email', 'reads': 150000, 'writes': 500, 'size_mb': 50},
{'table': 'orders', 'index': 'idx_user_id', 'reads': 500000, 'writes': 2000, 'size_mb': 200},
{'table': 'orders', 'index': 'idx_status', 'reads': 100, 'writes': 2000, 'size_mb': 80},
{'table': 'logs', 'index': 'idx_created_at', 'reads': 50000, 'writes': 10000, 'size_mb': 500},
{'table': 'products', 'index': 'idx_category', 'reads': 10, 'writes': 100, 'size_mb': 30},
]
print("【インデックス使用状況レポート】\n")
print(f"{'テーブル':>10} | {'インデックス':>20} | {'リード':>8} | {'ライト':>8} | {'サイズ':>8} | {'判断':>10}")
print("-" * 80)
for stat in index_stats:
# 使用頻度の判断
read_write_ratio = stat['reads'] / stat['writes'] if stat['writes'] > 0 else float('inf')
if stat['reads'] > 10000 and read_write_ratio > 10:
judgment = "維持推奨"
elif stat['reads'] < 1000 and read_write_ratio < 1:
judgment = "削除検討"
elif stat['reads'] < 100:
judgment = "-unused-"
else:
judgment = "現状維持"
print(f"{stat['table']:>10} | {stat['index']:>20} | {stat['reads']:>8,} | {stat['writes']:>8,} | {stat['size_mb']:>6}MB | {judgment:>10}")
print("\n【モニタリングの指針】")
print("・リード/ライト比が極端に低いインデックスは削除を検討")
print("・未使用インデックスは書き込みコストだけがかかる")
print("・ただし、バッチ処理用のインデックスは例外")
monitor_index_usage()
出力:
【インデックス使用状況レポート】
テーブル | インデックス | リード | ライト | サイズ | 判断
--------------------------------------------------------------------------------
users | idx_email | 150,000 | 500 | 50MB | 維持推奨
orders | idx_user_id | 500,000 | 2,000 | 200MB | 維持推奨
orders | idx_status | 100 | 2,000 | 80MB | 削除検討
logs | idx_created_at | 50,000 | 10,000 | 500MB | 現状維持
products | idx_category | 10 | 100 | 30MB | -unused--
【モニタリングの指針】
・リード/ライト比が極端に低いインデックスは削除を検討
・未使用インデックスは書き込みコストだけがかかる
・ただし、バッチ処理用のインデックスは例外
データベース別のインデックス実装
MySQL InnoDB のインデックス
def explain_mysql_innodb_indexes():
"""MySQL InnoDB のインデックス実装"""
innodb_features = {
'クラスタインデックス': 'プライマリキーがデータ自体を格納(ヒープ不要)',
'セカンダリインデックス': 'リーフにプライマリキーを格納(2 回アクセス)',
'アダプティブハッシュインデックス': '頻繁アクセスページを自動ハッシュ化',
'フルテキストインデックス': 'Ver5.6 以降で利用可能'
}
print("【MySQL InnoDB のインデックス実装】\n")
for feature, description in innodb_features.items():
print(f"### {feature}")
print(f"{description}\n")
print("【クエリ例】")
print("""
-- プライマリキー(クラスタインデックス)
CREATE TABLE users (
id INT PRIMARY KEY, -- これがクラスタインデックス
name VARCHAR(100),
email VARCHAR(255)
);
-- セカンダリインデックス
CREATE INDEX idx_email ON users(email);
-- 内部構造:email → id(プライマリキー)→ データ
-- 複合インデックス
CREATE INDEX idx_name_email ON users(name, email);
-- 最左接頭辞:name 単独、name+email で使用可能
""")
explain_mysql_innodb_indexes()
PostgreSQL のインデックス
def explain_postgresql_indexes():
"""PostgreSQL のインデックス実装"""
pg_index_types = {
'B-tree': 'デフォルト。等価・範囲・ソートに対応',
'Hash': '等価検索のみ。Ver10 で WAL ロギング対応',
'GiST': '幾何データ型、全文検索、R ツリー',
'SP-GiST': 'パーティション検索(四角形、ツリー)',
'GIN': '配列、JSONB、全文検索(逆引き索引)',
'BRIN': '超大規模テーブル(ブロック範囲)'
}
print("【PostgreSQL のインデックスタイプ】\n")
for index_type, description in pg_index_types.items():
print(f"### {index_type}")
print(f"{description}\n")
print("【使い分け指針】")
print("""
・通常クエリ → B-tree(デフォルト)
・等価検索のみ → Hash(B-tree より高速な場合も)
・配列・JSONB → GIN(@> 演算子など)
・全文検索 → GIN(tsvector)
・時系列超大規模 → BRIN(時系列ログなど)
・地理情報 → GiST(PostGIS)
""")
explain_postgresql_indexes()
まとめ
データベースインデックスの核心:
- 基本概念: フルテーブルスキャン(O(N))を対数時間(O(log N))に短縮
- B ツリー: 標準的なデータ構造。100 万行でも 3-4 回のディスクアクセス
- ハッシュインデックス: O(1) 検索だが、等価検索のみ対応
- 複合インデックス: 最左接頭辞の原則が重要
- 選択度: 一意の値の数 / 総行数。1 に近いほど効果大
- カバーリングインデックス: テーブルアクセス不要で I/O 削減
- モニタリング: 未使用インデックスは削除して書き込みコスト削減
インデックスは「空間を時間に換換する」技術であり、適切に設計すればクエリパフォーマンスを桁違いに向上させる。しかし、誤った設計は逆効果となり得る。クエリパターンを理解し、選択度を考慮し、使用状況をモニタリングする——この 3 つが効果的なインデックス設計の鍵となる。
参考資料
- “Database System Concepts” Abraham Silberschatz 他(第 7 版)
- “High Performance MySQL” Baron Schwartz 他(第 4 版)
- “Use The Index, Luke” Markus Winand: https://use-the-index-luke.com/
- PostgreSQL 公式ドキュメント - インデックスタイプ: https://www.postgresql.org/docs/current/indexes-types.html
- MySQL 公式ドキュメント - InnoDB インデックス: https://dev.mysql.com/doc/refman/8.0/en/innodb-indexes.html
- “SQL Performance Explained” Markus Winand
免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。