운영 서버에서 DB index 적용 작업 진행

- 01~07 작업 문서를 운영 측정값 기준으로 정리 (문서의 DB 비밀번호 삭제)
- app_highest_record 중복 8,795행 점수 기준 정리, 인덱스 5개 + UNIQUE 키 2개 추가
- 02·04 공통 측정 스크립트와 측정 결과, comparison.txt 추가
- 서버 처리 시간 18~292배 단축, 결과 행 수 동일
- 운영 데이터 백업과 권한(비밀번호 해시) 파일은 커밋에서 제외

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 23:30:42 +09:00
parent ded12332d5
commit 03eea2098e
56 changed files with 6993 additions and 0 deletions
@@ -0,0 +1,33 @@
-- best_record: 랭킹용
ALTER TABLE best_record
ADD INDEX IF NOT EXISTS idx_maestro_app_dt (MaestroID, AppID, RecordDateTime),
ALGORITHM=INPLACE, LOCK=NONE;
-- best_record: 히스토리/저장용
ALTER TABLE best_record
ADD INDEX IF NOT EXISTS idx_maestro_player_app_dt (MaestroID, PlayerID, AppID, RecordDateTime),
ALGORITHM=INPLACE, LOCK=NONE;
-- typing_exam_record
ALTER TABLE typing_exam_record
ADD INDEX IF NOT EXISTS idx_maestro_writing_dt (MaestroID, WritingID, RecordDateTime),
ALGORITHM=INPLACE, LOCK=NONE;
ALTER TABLE typing_exam_record
ADD INDEX IF NOT EXISTS idx_maestro_player_writing_dt (MaestroID, PlayerID, WritingID, RecordDateTime),
ALGORITHM=INPLACE, LOCK=NONE;
-- license_score
ALTER TABLE license_score
ADD INDEX IF NOT EXISTS idx_maestro_player_dt (MaestroID, PlayerID, ScoreDateTime),
ALGORITHM=INPLACE, LOCK=NONE;
-- app_highest_record: UNIQUE 키
ALTER TABLE app_highest_record
ADD UNIQUE KEY IF NOT EXISTS uk_maestro_player_app (MaestroID, PlayerID, AppID),
ALGORITHM=INPLACE, LOCK=NONE;
-- typing_exam_highest_record: UNIQUE 키
ALTER TABLE typing_exam_highest_record
ADD UNIQUE KEY IF NOT EXISTS uk_maestro_player_writing (MaestroID, PlayerID, WritingID),
ALGORITHM=INPLACE, LOCK=NONE;
@@ -0,0 +1,8 @@
TABLE_NAME INDEX_NAME NON_UNIQUE cols
app_highest_record uk_maestro_player_app 0 MaestroID,PlayerID,AppID
best_record idx_maestro_app_dt 1 MaestroID,AppID,RecordDateTime
best_record idx_maestro_player_app_dt 1 MaestroID,PlayerID,AppID,RecordDateTime
license_score idx_maestro_player_dt 1 MaestroID,PlayerID,ScoreDateTime
typing_exam_highest_record uk_maestro_player_writing 0 MaestroID,PlayerID,WritingID
typing_exam_record idx_maestro_player_writing_dt 1 MaestroID,PlayerID,WritingID,RecordDateTime
typing_exam_record idx_maestro_writing_dt 1 MaestroID,WritingID,RecordDateTime
@@ -0,0 +1,64 @@
--------------
ALTER TABLE best_record
ADD INDEX IF NOT EXISTS idx_maestro_app_dt (MaestroID, AppID, RecordDateTime),
ALGORITHM=INPLACE, LOCK=NONE
--------------
Query OK, 0 rows affected (4.803 sec)
Records: 0 Duplicates: 0 Warnings: 0
--------------
ALTER TABLE best_record
ADD INDEX IF NOT EXISTS idx_maestro_player_app_dt (MaestroID, PlayerID, AppID, RecordDateTime),
ALGORITHM=INPLACE, LOCK=NONE
--------------
Query OK, 0 rows affected (4.832 sec)
Records: 0 Duplicates: 0 Warnings: 0
--------------
ALTER TABLE typing_exam_record
ADD INDEX IF NOT EXISTS idx_maestro_writing_dt (MaestroID, WritingID, RecordDateTime),
ALGORITHM=INPLACE, LOCK=NONE
--------------
Query OK, 0 rows affected (0.735 sec)
Records: 0 Duplicates: 0 Warnings: 0
--------------
ALTER TABLE typing_exam_record
ADD INDEX IF NOT EXISTS idx_maestro_player_writing_dt (MaestroID, PlayerID, WritingID, RecordDateTime),
ALGORITHM=INPLACE, LOCK=NONE
--------------
Query OK, 0 rows affected (0.370 sec)
Records: 0 Duplicates: 0 Warnings: 0
--------------
ALTER TABLE license_score
ADD INDEX IF NOT EXISTS idx_maestro_player_dt (MaestroID, PlayerID, ScoreDateTime),
ALGORITHM=INPLACE, LOCK=NONE
--------------
Query OK, 0 rows affected (0.042 sec)
Records: 0 Duplicates: 0 Warnings: 0
--------------
ALTER TABLE app_highest_record
ADD UNIQUE KEY IF NOT EXISTS uk_maestro_player_app (MaestroID, PlayerID, AppID),
ALGORITHM=INPLACE, LOCK=NONE
--------------
Query OK, 0 rows affected (1.076 sec)
Records: 0 Duplicates: 0 Warnings: 0
--------------
ALTER TABLE typing_exam_highest_record
ADD UNIQUE KEY IF NOT EXISTS uk_maestro_player_writing (MaestroID, PlayerID, WritingID),
ALGORITHM=INPLACE, LOCK=NONE
--------------
Query OK, 0 rows affected (0.156 sec)
Records: 0 Duplicates: 0 Warnings: 0
Bye
@@ -0,0 +1,308 @@
=== q1_hour_func ===
{
"query_optimization": {
"r_total_time_ms": 0.303570412
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 2.069071756,
"nested_loop": [
{
"read_sorted_file": {
"r_rows": 22,
"filesort": {
"sort_key": "best_record.RecordDateTime, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 2.050037968,
"r_used_priority_queue": false,
"r_output_rows": 22,
"r_buffer_size": "2047Kb",
"r_sort_mode": "sort_key,addon_fields",
"table": {
"table_name": "best_record",
"access_type": "ref",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "8",
"used_key_parts": ["MaestroID", "AppID"],
"ref": ["const", "const"],
"r_loops": 1,
"rows": 5851,
"r_rows": 22,
"r_table_time_ms": 2.024712928,
"r_other_time_ms": 0.015343053,
"r_engine_stats": {
"pages_accessed": 77
},
"filtered": 100,
"r_filtered": 100,
"index_condition": "cast(best_record.RecordDateTime as date) = @`day` and hour(best_record.RecordDateTime) = 9",
"attached_condition": "best_record.MaestroID <=> @maestro and best_record.AppID <=> @app"
}
}
}
}
]
}
}
=== q2_hour_range ===
{
"query_optimization": {
"r_total_time_ms": 0.23481673
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 0.259231588,
"nested_loop": [
{
"read_sorted_file": {
"r_rows": 22,
"filesort": {
"sort_key": "best_record.RecordDateTime, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 0.241578075,
"r_used_priority_queue": false,
"r_output_rows": 22,
"r_buffer_size": "2047Kb",
"r_sort_mode": "sort_key,addon_fields",
"table": {
"table_name": "best_record",
"access_type": "range",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "13",
"used_key_parts": ["MaestroID", "AppID", "RecordDateTime"],
"r_loops": 1,
"rows": 22,
"r_rows": 22,
"r_table_time_ms": 0.143388535,
"r_other_time_ms": 0.086477209,
"r_engine_stats": {
"pages_accessed": 69
},
"filtered": 100,
"r_filtered": 100,
"index_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= '2026-08-26 09:00:00' and best_record.RecordDateTime < '2026-08-26 10:00:00'"
}
}
}
}
]
}
}
=== q3_day_func ===
{
"query_optimization": {
"r_total_time_ms": 0.260151772
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 2.440415655,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 0.019813943,
"r_used_priority_queue": false,
"r_output_rows": 61,
"r_buffer_size": "1Kb",
"r_sort_mode": "sort_key,rowid",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "ref",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "8",
"used_key_parts": ["MaestroID", "AppID"],
"ref": ["const", "const"],
"r_loops": 1,
"rows": 5851,
"r_rows": 99,
"r_table_time_ms": 2.331223925,
"r_other_time_ms": 0.071694268,
"r_engine_stats": {
"pages_accessed": 308
},
"filtered": 100,
"r_filtered": 100,
"index_condition": "year(best_record.RecordDateTime) = 2026 and month(best_record.RecordDateTime) = 8 and dayofmonth(best_record.RecordDateTime) = 26",
"attached_condition": "best_record.MaestroID <=> @maestro and best_record.AppID <=> @app"
}
}
]
}
}
}
}
=== q4_day_range ===
{
"query_optimization": {
"r_total_time_ms": 0.335646795
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 0.467062948,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 0.017993581,
"r_used_priority_queue": false,
"r_output_rows": 61,
"r_buffer_size": "1Kb",
"r_sort_mode": "sort_key,rowid",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "range",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "13",
"used_key_parts": ["MaestroID", "AppID", "RecordDateTime"],
"r_loops": 1,
"rows": 99,
"r_rows": 99,
"r_table_time_ms": 0.318203324,
"r_other_time_ms": 0.112702428,
"r_engine_stats": {
"pages_accessed": 300
},
"filtered": 100,
"r_filtered": 100,
"index_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= '2026-08-26 00:00:00' and best_record.RecordDateTime < '2026-08-27 00:00:00'"
}
}
]
}
}
}
}
=== q5_month_func ===
{
"query_optimization": {
"r_total_time_ms": 0.248649482
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 4.299725667,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 0.068003533,
"r_used_priority_queue": false,
"r_output_rows": 262,
"r_buffer_size": "6Kb",
"r_sort_mode": "sort_key,rowid",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "ref",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "8",
"used_key_parts": ["MaestroID", "AppID"],
"ref": ["const", "const"],
"r_loops": 1,
"rows": 5851,
"r_rows": 709,
"r_table_time_ms": 3.78789381,
"r_other_time_ms": 0.408851363,
"r_engine_stats": {
"pages_accessed": 2138
},
"filtered": 100,
"r_filtered": 100,
"index_condition": "year(best_record.RecordDateTime) = 2026 and month(best_record.RecordDateTime) = 8",
"attached_condition": "best_record.MaestroID <=> @maestro and best_record.AppID <=> @app"
}
}
]
}
}
}
}
=== q6_month_range ===
{
"query_optimization": {
"r_total_time_ms": 0.306621019
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 2.496056728,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 0.065773089,
"r_used_priority_queue": false,
"r_output_rows": 262,
"r_buffer_size": "6Kb",
"r_sort_mode": "sort_key,rowid",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "range",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "13",
"used_key_parts": ["MaestroID", "AppID", "RecordDateTime"],
"r_loops": 1,
"rows": 709,
"r_rows": 709,
"r_table_time_ms": 2.033224622,
"r_other_time_ms": 0.360211684,
"r_engine_stats": {
"pages_accessed": 2131
},
"filtered": 0.057728633,
"r_filtered": 100,
"index_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= '2026-08-01 00:00:00' and best_record.RecordDateTime < '2026-09-01 00:00:00'"
}
}
]
}
}
}
}
@@ -0,0 +1,32 @@
{
"query_block": {
"select_id": 1,
"nested_loop": [
{
"read_sorted_file": {
"filesort": {
"sort_key": "best_record.RecordDateTime, best_record.PlayerID",
"table": {
"table_name": "best_record",
"access_type": "ref",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "8",
"used_key_parts": ["MaestroID", "AppID"],
"ref": ["const", "const"],
"rows": 5851,
"filtered": 100,
"index_condition": "cast(best_record.RecordDateTime as date) = @`day` and hour(best_record.RecordDateTime) = 9",
"attached_condition": "best_record.MaestroID <=> @maestro and best_record.AppID <=> @app"
}
}
}
}
]
}
}
@@ -0,0 +1,30 @@
{
"query_block": {
"select_id": 1,
"nested_loop": [
{
"read_sorted_file": {
"filesort": {
"sort_key": "best_record.RecordDateTime, best_record.PlayerID",
"table": {
"table_name": "best_record",
"access_type": "range",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "13",
"used_key_parts": ["MaestroID", "AppID", "RecordDateTime"],
"rows": 22,
"filtered": 100,
"index_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= <cache>(cast(@`day` as datetime) + interval @`hour` hour) and best_record.RecordDateTime < <cache>(cast(@`day` as datetime) + interval @`hour` + 1 hour)"
}
}
}
}
]
}
}
@@ -0,0 +1,32 @@
{
"query_block": {
"select_id": 1,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "ref",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "8",
"used_key_parts": ["MaestroID", "AppID"],
"ref": ["const", "const"],
"rows": 5851,
"filtered": 100,
"index_condition": "year(best_record.RecordDateTime) = 2026 and month(best_record.RecordDateTime) = 8 and dayofmonth(best_record.RecordDateTime) = 26",
"attached_condition": "best_record.MaestroID <=> @maestro and best_record.AppID <=> @app"
}
}
]
}
}
}
}
@@ -0,0 +1,30 @@
{
"query_block": {
"select_id": 1,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "range",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "13",
"used_key_parts": ["MaestroID", "AppID", "RecordDateTime"],
"rows": 99,
"filtered": 100,
"index_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= <cache>(cast(@`day` as datetime)) and best_record.RecordDateTime < <cache>(cast(@`day` as datetime) + interval 1 day)"
}
}
]
}
}
}
}
@@ -0,0 +1,32 @@
{
"query_block": {
"select_id": 1,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "ref",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "8",
"used_key_parts": ["MaestroID", "AppID"],
"ref": ["const", "const"],
"rows": 5851,
"filtered": 100,
"index_condition": "year(best_record.RecordDateTime) = 2026 and month(best_record.RecordDateTime) = 8",
"attached_condition": "best_record.MaestroID <=> @maestro and best_record.AppID <=> @app"
}
}
]
}
}
}
}
@@ -0,0 +1,30 @@
{
"query_block": {
"select_id": 1,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "range",
"possible_keys": [
"MaestroID",
"AppID",
"idx_maestro_app_dt",
"idx_maestro_player_app_dt"
],
"key": "idx_maestro_app_dt",
"key_length": "13",
"used_key_parts": ["MaestroID", "AppID", "RecordDateTime"],
"rows": 709,
"filtered": 0.057728633,
"index_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= <cache>(cast(date_format(@`day`,'%Y-%m-01') as datetime)) and best_record.RecordDateTime < <cache>(cast(date_format(@`day`,'%Y-%m-01') as datetime) + interval 1 month)"
}
}
]
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
start_time query_time rows_examined sql_text
2026-09-15 23:09:13.487911 00:00:04.819872 0 ALTER TABLE best_record \nADD INDEX IF NOT EXISTS idx_maestro_player_app_dt (MaestroID, PlayerID, AppID, RecordDateTime),
2026-09-15 23:09:08.684811 00:00:04.790929 0 ALTER TABLE best_record \nADD INDEX IF NOT EXISTS idx_maestro_app_dt (MaestroID, AppID, RecordDateTime),\nALGORITHM=INPLAC
2026-09-15 23:09:19.466802 00:00:01.063310 0 ALTER TABLE app_highest_record \nADD UNIQUE KEY IF NOT EXISTS uk_maestro_player_app (MaestroID, PlayerID, AppID),\nALGORIT
2026-09-15 23:08:21.126639 00:00:00.929698 935362 DELETE AHR FROM app_highest_record AHR\nJOIN (\n SELECT AppHighestRecordID FROM (\n SELECT AppHighestRecordID,\n
2026-09-15 23:09:18.319470 00:00:00.723314 0 ALTER TABLE typing_exam_record \nADD INDEX IF NOT EXISTS idx_maestro_writing_dt (MaestroID, WritingID, RecordDateTime),\nA
@@ -0,0 +1,12 @@
baseline q1_hour_func server_time_ms=74.61373851
baseline q2_hour_range server_time_ms=75.58458171
baseline q3_day_func server_time_ms=75.28739257
baseline q4_day_range server_time_ms=75.24298373
baseline q5_month_func server_time_ms=76.26824776
baseline q6_month_range server_time_ms=79.70009072
after q1_hour_func server_time_ms=2.069071756
after q2_hour_range server_time_ms=0.259231588
after q3_day_func server_time_ms=2.440415655
after q4_day_range server_time_ms=0.467062948
after q5_month_func server_time_ms=4.299725667
after q6_month_range server_time_ms=2.496056728
@@ -0,0 +1,356 @@
=== q1_hour_func ===
{
"query_optimization": {
"r_total_time_ms": 0.175154857
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 74.61373851,
"nested_loop": [
{
"read_sorted_file": {
"r_rows": 22,
"filesort": {
"sort_key": "best_record.RecordDateTime, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 74.59065391,
"r_used_priority_queue": false,
"r_output_rows": 22,
"r_buffer_size": "2047Kb",
"r_sort_mode": "sort_key,addon_fields",
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"r_loops": 1,
"rows": 10103,
"r_rows": 5851,
"r_table_time_ms": 15.56706792,
"r_other_time_ms": 52.50187814,
"r_engine_stats": {
"pages_accessed": 17780
},
"filtered": 100,
"r_filtered": 0.376004102,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and cast(best_record.RecordDateTime as date) = @`day` and hour(best_record.RecordDateTime) = 9"
}
}
}
}
]
}
}
=== q2_hour_range ===
{
"query_optimization": {
"r_total_time_ms": 0.183126443
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 75.58458171,
"nested_loop": [
{
"read_sorted_file": {
"r_rows": 22,
"filesort": {
"sort_key": "best_record.RecordDateTime, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 75.56186719,
"r_used_priority_queue": false,
"r_output_rows": 22,
"r_buffer_size": "2047Kb",
"r_sort_mode": "sort_key,addon_fields",
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"r_loops": 1,
"rows": 10103,
"r_rows": 5851,
"r_table_time_ms": 15.67992038,
"r_other_time_ms": 53.82031051,
"r_engine_stats": {
"pages_accessed": 17780
},
"filtered": 100,
"r_filtered": 0.376004102,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= '2026-08-26 09:00:00' and best_record.RecordDateTime < '2026-08-26 10:00:00'"
}
}
}
}
]
}
}
=== q3_day_func ===
{
"query_optimization": {
"r_total_time_ms": 0.205960987
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 75.28739257,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 0.028745721,
"r_used_priority_queue": false,
"r_output_rows": 61,
"r_buffer_size": "1Kb",
"r_sort_mode": "sort_key,rowid",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"r_loops": 1,
"rows": 10103,
"r_rows": 5851,
"r_table_time_ms": 15.68132066,
"r_other_time_ms": 53.54159505,
"r_engine_stats": {
"pages_accessed": 17780
},
"filtered": 100,
"r_filtered": 1.692018458,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and year(best_record.RecordDateTime) = 2026 and month(best_record.RecordDateTime) = 8 and dayofmonth(best_record.RecordDateTime) = 26"
}
}
]
}
}
}
}
=== q4_day_range ===
{
"query_optimization": {
"r_total_time_ms": 0.200909982
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 75.24298373,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 0.023154608,
"r_used_priority_queue": false,
"r_output_rows": 61,
"r_buffer_size": "1Kb",
"r_sort_mode": "sort_key,rowid",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"r_loops": 1,
"rows": 10103,
"r_rows": 5851,
"r_table_time_ms": 15.79226274,
"r_other_time_ms": 52.7619799,
"r_engine_stats": {
"pages_accessed": 17780
},
"filtered": 100,
"r_filtered": 1.692018458,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= '2026-08-26 00:00:00' and best_record.RecordDateTime < '2026-08-27 00:00:00'"
}
}
]
}
}
}
}
=== q5_month_func ===
{
"query_optimization": {
"r_total_time_ms": 0.213092406
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 76.26824776,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 0.069783887,
"r_used_priority_queue": false,
"r_output_rows": 262,
"r_buffer_size": "6Kb",
"r_sort_mode": "sort_key,rowid",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"r_loops": 1,
"rows": 10103,
"r_rows": 5851,
"r_table_time_ms": 16.3543846,
"r_other_time_ms": 53.27444188,
"r_engine_stats": {
"pages_accessed": 17780
},
"filtered": 100,
"r_filtered": 12.11758674,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and year(best_record.RecordDateTime) = 2026 and month(best_record.RecordDateTime) = 8"
}
}
]
}
}
}
}
=== q6_month_range ===
{
"query_optimization": {
"r_total_time_ms": 0.215072801
},
"query_block": {
"select_id": 1,
"r_loops": 1,
"r_total_time_ms": 79.70009072,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"r_loops": 1,
"r_total_time_ms": 0.072174363,
"r_used_priority_queue": false,
"r_output_rows": 262,
"r_buffer_size": "6Kb",
"r_sort_mode": "sort_key,rowid",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"r_loops": 1,
"rows": 10103,
"r_rows": 5851,
"r_table_time_ms": 19.07379578,
"r_other_time_ms": 54.36322855,
"r_engine_stats": {
"pages_accessed": 17780
},
"filtered": 100,
"r_filtered": 12.11758674,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= '2026-08-01 00:00:00' and best_record.RecordDateTime < '2026-09-01 00:00:00'"
}
}
]
}
}
}
}
@@ -0,0 +1,6 @@
baseline q1_hour_func server_time_ms=74.61373851
baseline q2_hour_range server_time_ms=75.58458171
baseline q3_day_func server_time_ms=75.28739257
baseline q4_day_range server_time_ms=75.24298373
baseline q5_month_func server_time_ms=76.26824776
baseline q6_month_range server_time_ms=79.70009072
@@ -0,0 +1,39 @@
{
"query_block": {
"select_id": 1,
"nested_loop": [
{
"read_sorted_file": {
"filesort": {
"sort_key": "best_record.RecordDateTime, best_record.PlayerID",
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"rows": 10103,
"filtered": 100,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and cast(best_record.RecordDateTime as date) = @`day` and hour(best_record.RecordDateTime) = 9"
}
}
}
}
]
}
}
@@ -0,0 +1,39 @@
{
"query_block": {
"select_id": 1,
"nested_loop": [
{
"read_sorted_file": {
"filesort": {
"sort_key": "best_record.RecordDateTime, best_record.PlayerID",
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"rows": 10103,
"filtered": 100,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= <cache>(cast(@`day` as datetime) + interval @`hour` hour) and best_record.RecordDateTime < <cache>(cast(@`day` as datetime) + interval @`hour` + 1 hour)"
}
}
}
}
]
}
}
@@ -0,0 +1,39 @@
{
"query_block": {
"select_id": 1,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"rows": 10103,
"filtered": 100,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and year(best_record.RecordDateTime) = 2026 and month(best_record.RecordDateTime) = 8 and dayofmonth(best_record.RecordDateTime) = 26"
}
}
]
}
}
}
}
@@ -0,0 +1,39 @@
{
"query_block": {
"select_id": 1,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"rows": 10103,
"filtered": 100,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= <cache>(cast(@`day` as datetime)) and best_record.RecordDateTime < <cache>(cast(@`day` as datetime) + interval 1 day)"
}
}
]
}
}
}
}
@@ -0,0 +1,39 @@
{
"query_block": {
"select_id": 1,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"rows": 10103,
"filtered": 100,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and year(best_record.RecordDateTime) = 2026 and month(best_record.RecordDateTime) = 8"
}
}
]
}
}
}
}
@@ -0,0 +1,39 @@
{
"query_block": {
"select_id": 1,
"filesort": {
"sort_key": "max(best_record.BestRecord) desc, best_record.PlayerID",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "best_record",
"access_type": "index_merge",
"possible_keys": ["MaestroID", "AppID"],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": ["MaestroID"]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": ["AppID"]
}
}
]
},
"rows": 10103,
"filtered": 100,
"attached_condition": "best_record.MaestroID = @maestro and best_record.AppID = @app and best_record.RecordDateTime >= <cache>(cast(date_format(@`day`,'%Y-%m-01') as datetime)) and best_record.RecordDateTime < <cache>(cast(date_format(@`day`,'%Y-%m-01') as datetime) + interval 1 month)"
}
}
]
}
}
}
}
@@ -0,0 +1,6 @@
baseline_explain_q1_hour_func.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
baseline_explain_q2_hour_range.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
baseline_explain_q3_day_func.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
baseline_explain_q4_day_range.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
baseline_explain_q5_month_func.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
baseline_explain_q6_month_range.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
day hour cnt
2026-08-26 9 22
2026-08-28 9 22
2026-08-26 10 21
2026-08-31 10 20
2026-08-25 10 19
@@ -0,0 +1,39 @@
=== 성능 개선 효과 (운영 DB, 2026-09-15) ===
측정 기준: params.sql (MaestroID 181, AppID 21, @day 2026-08-26, @hour 9)
측정 시점: 베이스라인 22:5x (인덱스 추가 전) / 개선 후 23:1x (인덱스 7개 추가 후)
| 쿼리 | access_type (전→후) | rows (전→후) | 서버 시간 ms (전→후, ANALYZE) | 결과 행 수 |
|--------------------|-----------------------|-----------------------|-------------------------------|-----------|
| q1 시간별 · 함수 | index_merge → ref | 10,103 → 5,851 (1.7배) | 74.61 → 2.07 (36배) | 22 = 22 |
| q2 시간별 · 범위 | index_merge → range | 10,103 → 22 (459배) | 75.58 → 0.26 (292배) | 22 = 22 |
| q3 일간 · 함수 | index_merge → ref | 10,103 → 5,851 (1.7배) | 75.29 → 2.44 (31배) | 61 = 61 |
| q4 일간 · 범위 | index_merge → range | 10,103 → 99 (102배) | 75.24 → 0.47 (161배) | 61 = 61 |
| q5 월간 · 함수 | index_merge → ref | 10,103 → 5,851 (1.7배) | 76.27 → 4.30 (18배) | 262 = 262 |
| q6 월간 · 범위 | index_merge → range | 10,103 → 709 (14배) | 79.70 → 2.50 (32배) | 262 = 262 |
사용 인덱스: 개선 후 6개 모두 idx_maestro_app_dt (MaestroID, AppID, RecordDateTime)
클라이언트 시간 (네트워크 포함, 2회차): 약 88~92ms → 13~18ms
참고 (스테이징): rows 5,804 → 1, cost 8.63 → 0.004
참고 (운영 01 사전 측정): 일간 랭킹 index_merge, rows 10,100
결과 행 수 동일 (4-3): [x] 예 [ ] 아니오
결론:
- 인덱스만의 효과 (q1·q3·q5, 함수형 조건):
rows 추정은 1.7배만 줄었지만 서버 처리 시간은 18~36배 단축.
기존에는 MaestroID·AppID 단일 인덱스 두 개의 교집합(index_merge)을 구하느라 느렸고,
복합 인덱스의 (MaestroID, AppID) 앞부분으로 바로 찾게(ref) 되었기 때문.
날짜 조건은 여전히 인덱스로 거르지 못해 해당 선생님·앱의 기록 약 5,851행을 확인함.
- 인덱스 + 쿼리 수정 효과 (q2·q4·q6, 범위 조건):
날짜까지 인덱스로 걸러(range) 조회 기간의 실제 기록만 읽음.
서버 처리 시간 32~292배 단축, 검사 행 수 14~459배 감소.
같은 조건에서 함수형 대비 추가로 1.7~9배 빠름 (q1→q2 8배, q3→q4 5배, q5→q6 1.7배).
중복 정리 (03-0):
- app_highest_record 8,795행 삭제 (4,823조합), 220,648행 = 220,648조합
- UNIQUE 키 2개 추가 완료 → 이후 중복 저장은 DB가 거부
Slow query log (04-5, 23:08 이후 1시간):
- 0.5초 이상은 작업 SQL 5건(ALTER 4건, 중복 DELETE 1건)뿐, 애플리케이션 쿼리 0건
- 새벽이 아닌 밤 시간이고 기간이 짧으므로 24시간 뒤 다시 확인 필요 (07단계)
@@ -0,0 +1,2 @@
dup_groups rows_to_delete
4823 8795
@@ -0,0 +1,47 @@
--------------
DELETE AHR FROM app_highest_record AHR
JOIN (
SELECT AppHighestRecordID FROM (
SELECT AppHighestRecordID,
ROW_NUMBER() OVER (
PARTITION BY MaestroID, PlayerID, AppID
ORDER BY CASE WHEN AppID = 105 THEN HighestRecord ELSE -HighestRecord END,
AppHighestRecordID DESC
) AS rn
FROM app_highest_record
) ranked
WHERE rn > 1
) dup ON dup.AppHighestRecordID = AHR.AppHighestRecordID
--------------
Query OK, 8795 rows affected (0.942 sec)
--------------
SELECT 'app_highest_record' AS table_name,
COUNT(*) AS total_rows,
COUNT(DISTINCT MaestroID, PlayerID, AppID) AS unique_combos
FROM app_highest_record
--------------
+--------------------+------------+---------------+
| table_name | total_rows | unique_combos |
+--------------------+------------+---------------+
| app_highest_record | 220648 | 220648 |
+--------------------+------------+---------------+
1 row in set (0.190 sec)
--------------
SELECT 'typing_exam_highest_record' AS table_name,
COUNT(*) AS total_rows,
COUNT(DISTINCT MaestroID, PlayerID, WritingID) AS unique_combos
FROM typing_exam_highest_record
--------------
+----------------------------+------------+---------------+
| table_name | total_rows | unique_combos |
+----------------------------+------------+---------------+
| typing_exam_highest_record | 20413 | 20413 |
+----------------------------+------------+---------------+
1 row in set (0.024 sec)
Bye
@@ -0,0 +1,12 @@
baseline_explain_q1_hour_func.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
baseline_explain_q2_hour_range.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
baseline_explain_q3_day_func.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
baseline_explain_q4_day_range.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
baseline_explain_q5_month_func.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
baseline_explain_q6_month_range.json access_type=index_merge key=MaestroID ∩ AppID rows=10103
after_explain_q1_hour_func.json access_type=ref key=idx_maestro_app_dt rows=5851
after_explain_q2_hour_range.json access_type=range key=idx_maestro_app_dt rows=22
after_explain_q3_day_func.json access_type=ref key=idx_maestro_app_dt rows=5851
after_explain_q4_day_range.json access_type=range key=idx_maestro_app_dt rows=99
after_explain_q5_month_func.json access_type=ref key=idx_maestro_app_dt rows=5851
after_explain_q6_month_range.json access_type=range key=idx_maestro_app_dt rows=709
@@ -0,0 +1 @@
SET @maestro = 181, @app = 21, @day = '2026-08-26', @hour = 9;
@@ -0,0 +1,2 @@
dup_groups rows_to_delete
4823 8795
@@ -0,0 +1,11 @@
MaestroID PlayerID AppID cnt
223 115830 2 33
306 93738 106 25
123 86825 106 21
82 2245 106 21
278 84908 106 20
49 114356 106 20
184 112221 101 19
127 71176 3 19
363 105234 106 18
75 10923 106 17
@@ -0,0 +1,66 @@
{
"query_block": {
"select_id": 1,
"filesort": {
"sort_key": "max(BR.BestRecord) desc",
"temporary_table": {
"nested_loop": [
{
"table": {
"table_name": "BR",
"access_type": "index_merge",
"possible_keys": [
"MaestroID",
"AppID",
"PlayerID"
],
"key_length": "4,4",
"index_merge": {
"intersect": [
{
"range": {
"key": "MaestroID",
"used_key_parts": [
"MaestroID"
]
}
},
{
"range": {
"key": "AppID",
"used_key_parts": [
"AppID"
]
}
}
]
},
"rows": 10100,
"filtered": 100,
"attached_condition": "BR.MaestroID = 181 and BR.AppID = 21 and year(BR.RecordDateTime) = 2026 and month(BR.RecordDateTime) = 9 and dayofmonth(BR.RecordDateTime) = 15"
}
},
{
"table": {
"table_name": "U",
"access_type": "eq_ref",
"possible_keys": [
"PRIMARY"
],
"key": "PRIMARY",
"key_length": "4",
"used_key_parts": [
"PlayerID"
],
"ref": [
"chocomae.BR.PlayerID"
],
"rows": 1,
"filtered": 100
}
}
]
}
}
}
}
@@ -0,0 +1,55 @@
ym cnt
2022-04 3
2022-05 3544
2022-06 7730
2022-07 7543
2022-08 4605
2022-09 4164
2022-10 4702
2022-11 4052
2022-12 2446
2023-01 3903
2023-02 1580
2023-03 18646
2023-04 22750
2023-05 19749
2023-06 18620
2023-07 16490
2023-08 13244
2023-09 17892
2023-10 19045
2023-11 23144
2023-12 17608
2024-01 15262
2024-02 6745
2024-03 19414
2024-04 27242
2024-05 25057
2024-06 21613
2024-07 23446
2024-08 17951
2024-09 21469
2024-10 22776
2024-11 22526
2024-12 19082
2025-01 12170
2025-02 8558
2025-03 26788
2025-04 30366
2025-05 29850
2025-06 32421
2025-07 28872
2025-08 19853
2025-09 33908
2025-10 22878
2025-11 28723
2025-12 28649
2026-01 16532
2026-02 8980
2026-03 66105
2026-04 70072
2026-05 58444
2026-06 65606
2026-07 56307
2026-08 51991
2026-09 39366
@@ -0,0 +1,6 @@
Variable_name Value
log_output FILE
log_queries_not_using_indexes OFF
long_query_time 3.000000
slow_query_log ON
slow_query_log_file /var/log/mysql/mariadb-slow.log
@@ -0,0 +1,67 @@
=== 운영 DB 사전 측정 결과 (2026-09-15) ===
1. 테이블 크기 (premeasure_table_sizes.txt)
- best_record data_mb: 72.5
- best_record index_mb: 72.1
- best_record 행 수: 약 1,152,399 (information_schema 추정치) / 월별 합계 1,210,482
- typing_exam_record data_mb: 10.3
- typing_exam_record index_mb: 10.9
2. 월별 적재량 (premeasure_monthly_load.txt)
- 최근 1개월 (2026-08): 51,991 행
- 2026-09 (15일까지): 39,366 행
- 최근 3개월 평균 (2026-06~08): 57,968 행/월
- 전년 같은 기간 평균 (2025-06~08): 27,049 행/월 → 약 2.1배 증가
3. 테스트용 선생님·앱 (premeasure_top_maestro_app.txt)
- MaestroID: 181
- AppID: 21
- 30일 기록 수: 1,089
4. 일간 랭킹 EXPLAIN (premeasure_explain_daily_ranking.json)
- access_type: index_merge (예상했던 ALL 아님)
- key: MaestroID ∩ AppID (기존 단일 인덱스 2개의 교집합)
- rows: 10,100 (예상했던 1,206,768 아님)
- 날짜 조건(YEAR/MONTH/DAYOFMONTH)은 인덱스로 거르지 못하고 10,100행을 하나씩 확인
- 참고: 원본 출력은 헤더와 \n 이스케이프 때문에 JSON이 아니었음 → 내용 변경 없이 JSON으로 복원
5. 최고기록 테이블 중복 (premeasure_*_duplicates.txt, premeasure_app_highest_dup_count.txt)
- app_highest_record 중복: 4,823 조합 / 삭제 대상 8,795 행
· 테이블 약 229,459행(추정치)의 약 3.8%
· 상위 10개 조합은 각 17~33행
· 03-0-1에서 작업 당일 다시 세어 비교 (그 사이 조금 늘어날 수 있음)
- typing_exam_highest_record 중복: 0 건 (빈 파일 = 조회 결과 없음)
6. 계정 권한 (premeasure_grants.txt)
- jisangs@182.217.174.221: SELECT, ALTER ON chocomae.* 만 있음
- 2026-09-15 임시 부여 후 (premeasure_grants_after.txt):
+ SUPER ON *.*
+ INSERT, DELETE ON chocomae.app_highest_record, chocomae.typing_exam_highest_record
+ SELECT ON mysql.slow_log
→ 05단계에서 회수 (slow log 복원 후)
- 원래 권한으로 할 수 없던 작업:
· 02-1 slow query log 변경 (SET GLOBAL → SUPER)
· 03-0-3 중복 삭제 (DELETE)
· 04-5, 07 mysql.slow_log 조회 (SELECT ON mysql.slow_log)
· 05 slow query log 복원 (SET GLOBAL)
· 06 삭제 행 복원 (INSERT) → 임시 부여됨
· 06 ANALYZE TABLE best_record (best_record INSERT 필요, 부여 안 함) → 관리자 계정
7. Slow query log 원래 설정 (premeasure_slow_log_settings.txt) — 05단계 복원 기준값
- slow_query_log: ON
- long_query_time: 3
- log_output: FILE
- log_queries_not_using_indexes: OFF
- slow_query_log_file: /var/log/mysql/mariadb-slow.log
- 이전 문서의 1-4 SET GLOBAL은 적용되지 않았음 (SUPER 권한 없음, 적용됐다면 long_query_time=1·log_output=TABLE)
8. 준비 확인
☑ 테이블 크기 확인
☑ 월별 적재량 확인
☑ EXPLAIN 결과 저장
☑ 중복 확인 (전체 개수 포함)
☑ 계정 권한 확인
☑ Slow query log 현재 설정 저장
☑ 작업 권한 임시 부여
다음: 02-baseline.md
@@ -0,0 +1,20 @@
TABLE_NAME approx_rows data_mb index_mb
best_record 1152399 72.5 72.1
app_highest_record 229459 16.5 15.8
typing_exam_record 131675 10.3 10.9
player 22394 3.5 0.4
active_app 17661 1.5 0.6
typing_exam_highest_record 21355 1.4 1.2
maestro_log 1944 0.4 0.0
maestro 418 0.1 0.0
license_score 514 0.1 0.0
license_maestro_password 1024 0.1 0.0
maestro_extension 450 0.0 0.0
admin 0 0.0 0.0
app 48 0.0 0.0
ads_client 0 0.0 0.0
license_time 0 0.0 0.0
writing 20 0.0 0.0
typing_exam_ads 0 0.0 0.0
banned_word 22 0.0 0.0
maestro_upgrade 109 0.0 0.0
@@ -0,0 +1,6 @@
MaestroID AppID cnt
181 21 1089
181 1 591
389 103 562
230 21 546
389 104 541
@@ -0,0 +1,6 @@
SELECT SQL_NO_CACHE PlayerID, BestRecord, RecordDateTime
FROM best_record
WHERE MaestroID = @maestro AND AppID = @app
AND DATE(RecordDateTime) = @day
AND HOUR(RecordDateTime) = @hour
ORDER BY RecordDateTime, PlayerID;
@@ -0,0 +1,6 @@
SELECT SQL_NO_CACHE PlayerID, BestRecord, RecordDateTime
FROM best_record
WHERE MaestroID = @maestro AND AppID = @app
AND RecordDateTime >= CAST(@day AS DATETIME) + INTERVAL @hour HOUR
AND RecordDateTime < CAST(@day AS DATETIME) + INTERVAL (@hour + 1) HOUR
ORDER BY RecordDateTime, PlayerID;
@@ -0,0 +1,8 @@
SELECT SQL_NO_CACHE PlayerID, MAX(BestRecord) AS HighScore
FROM best_record
WHERE MaestroID = @maestro AND AppID = @app
AND YEAR(RecordDateTime) = YEAR(@day)
AND MONTH(RecordDateTime) = MONTH(@day)
AND DAYOFMONTH(RecordDateTime) = DAYOFMONTH(@day)
GROUP BY PlayerID
ORDER BY HighScore DESC, PlayerID;
@@ -0,0 +1,7 @@
SELECT SQL_NO_CACHE PlayerID, MAX(BestRecord) AS HighScore
FROM best_record
WHERE MaestroID = @maestro AND AppID = @app
AND RecordDateTime >= CAST(@day AS DATETIME)
AND RecordDateTime < CAST(@day AS DATETIME) + INTERVAL 1 DAY
GROUP BY PlayerID
ORDER BY HighScore DESC, PlayerID;
@@ -0,0 +1,7 @@
SELECT SQL_NO_CACHE PlayerID, MAX(BestRecord) AS HighScore
FROM best_record
WHERE MaestroID = @maestro AND AppID = @app
AND YEAR(RecordDateTime) = YEAR(@day)
AND MONTH(RecordDateTime) = MONTH(@day)
GROUP BY PlayerID
ORDER BY HighScore DESC, PlayerID;
@@ -0,0 +1,7 @@
SELECT SQL_NO_CACHE PlayerID, MAX(BestRecord) AS HighScore
FROM best_record
WHERE MaestroID = @maestro AND AppID = @app
AND RecordDateTime >= CAST(DATE_FORMAT(@day, '%Y-%m-01') AS DATETIME)
AND RecordDateTime < CAST(DATE_FORMAT(@day, '%Y-%m-01') AS DATETIME) + INTERVAL 1 MONTH
GROUP BY PlayerID
ORDER BY HighScore DESC, PlayerID;
@@ -0,0 +1,41 @@
#!/bin/zsh
# 02·04단계 공통 측정 스크립트
# 사용법: zsh run_measure.sh baseline (02단계, 인덱스 추가 전)
# zsh run_measure.sh after (04단계, 인덱스 추가 후)
PREFIX=$1
if [[ $PREFIX != baseline && $PREFIX != after ]]; then
echo "사용법: zsh run_measure.sh baseline|after" >&2; exit 1
fi
cd "${0:A:h}" || exit 1
printf "Enter password: "; read -rs DBPW; echo
db() {
mariadb --defaults-extra-file=<(printf '[client]\npassword="%s"\n' "$DBPW") \
-h chocomae.jinaju.com -u jisangs chocomae "$@"
}
# 1) EXPLAIN: 쿼리마다 JSON 파일 1개
for q in queries/*.sql; do
out="${PREFIX}_explain_${q:t:r}.json"
{ cat params.sql; printf 'EXPLAIN FORMAT=JSON '; cat "$q"; } | db -N -r > "$out" \
|| { echo "EXPLAIN 실패: $q" >&2; exit 1; }
echo "저장: $out"
done
# 2) 실행 시간: 같은 쿼리 묶음을 2회 실행 (2회차 값 사용)
for round in 1 2; do
{ cat params.sql
for q in queries/*.sql; do
echo "SELECT '=== round ${round}: ${q:t:r} ===' AS msg;"
cat "$q"
done
} | db -vvv || { echo "실행 시간 측정 실패" >&2; exit 1; }
done > "${PREFIX}_perf.txt"
echo "저장: ${PREFIX}_perf.txt"
# 3) 요약: 쿼리 이름 / 결과 행 수 / 실행 시간 (2회차)
echo
grep -E '^\| === |rows? in set|Empty set' "${PREFIX}_perf.txt" | paste - - - | grep 'round 2' \
| sed -E 's/\| === round 2: (.*) === \|/\1/; s/\t1 row in set \([0-9.]+ sec\)//'
@@ -0,0 +1,18 @@
# 사용법: python3 summarize_analyze.py baseline [after]
import json
import sys
decoder = json.JSONDecoder()
for prefix in sys.argv[1:]:
text = open(f"{prefix}_analyze.txt", encoding="utf-8").read()
names = [line.split("=== ")[1].split(" ===")[0] for line in text.splitlines() if line.startswith("=== ")]
docs, pos = [], 0
while True:
start = text.find("{", pos)
if start < 0:
break
doc, pos = decoder.raw_decode(text, start)
docs.append(doc)
for name, doc in zip(names, docs):
block = doc["query_block"]
print(f"{prefix:8} {name:16} server_time_ms={block.get('r_total_time_ms')}")
@@ -0,0 +1,40 @@
# 사용법: python3 summarize_explain.py baseline [after]
import glob
import json
import sys
def find_table(node):
if isinstance(node, dict):
if isinstance(node.get("table"), dict):
return node["table"]
children = node.values()
elif isinstance(node, list):
children = node
else:
return None
for child in children:
found = find_table(child)
if found:
return found
return None
def keys_of(node):
if isinstance(node, dict):
for k, v in node.items():
if k == "key":
yield v
else:
yield from keys_of(v)
elif isinstance(node, list):
for v in node:
yield from keys_of(v)
for prefix in sys.argv[1:]:
for path in sorted(glob.glob(f"{prefix}_explain_*.json")):
with open(path, encoding="utf-8") as f:
table = find_table(json.load(f))
key = table.get("key") or "".join(keys_of(table.get("index_merge", {}))) or "-"
print(f"{path:40} access_type={str(table.get('access_type')):12} key={key:28} rows={table.get('rows')}")