{"index.json":{"schemaVersion":2,"app":"DBAtlas","updated":"2026-07-04","engines":"engines.json","rosetta":["rosetta/sessions.json","rosetta/storage.json","rosetta/performance.json","rosetta/backup.json","rosetta/security.json","rosetta/maintenance.json","rosetta/schema.json","rosetta/replication.json","rosetta/connect.json","rosetta/objects.json","rosetta/datamove.json","rosetta/monitoring.json","rosetta/automation.json","rosetta/lifecycle.json","rosetta/migration.json","rosetta/tuning.json","rosetta/tuning-adv.json","rosetta/advanced-features.json","rosetta/sql-syntax.json"],"cloud":["cloud/cloud-ops-1.json","cloud/cloud-ops-2.json"],"guide":{"bestPractices":["guide/best-practices.json","guide/best-practices-2.json","guide/best-practices-3.json","guide/best-practices-4.json"],"glossary":["guide/glossary.json","guide/glossary-2.json","guide/glossary-3.json"]},"rosettaOverlays":["rosetta/overlay-elasticsearch.json"],"types":"types/types-functions.json","errors":["errors/oracle.json","errors/postgres.json","errors/mysql.json","errors/mssql.json","errors/mongodb.json","errors/redis.json","errors/elasticsearch.json","errors/oracle-more.json","errors/postgres-more.json","errors/mysql-more.json","errors/mssql-more.json","errors/mongodb-more.json","errors/redis-more.json","errors/elasticsearch-more.json","errors/oracle-more2.json","errors/postgres-more2.json","errors/mysql-more2.json","errors/mssql-more2.json","errors/mongodb-more2.json","errors/redis-more2.json","errors/elasticsearch-more2.json","errors/oracle-more3.json","errors/postgres-more3.json","errors/mysql-more3.json","errors/mssql-more3.json","errors/mongodb-more3.json","errors/redis-more3.json","errors/elasticsearch-more3.json","errors/oracle-more4.json","errors/postgres-more4.json","errors/mysql-more4.json","errors/mssql-more4.json","errors/mongodb-more4.json","errors/redis-more4.json","errors/elasticsearch-more4.json"],"health":["health/oracle.json","health/postgres.json","health/mysql.json","health/mssql.json","health/mongodb.json","health/redis.json","health/elasticsearch.json"],"recipes":["recipes/oracle.json","recipes/oracle-ha.json","recipes/oracle-ops.json","recipes/postgres.json","recipes/postgres-ha.json","recipes/postgres-ops.json","recipes/mysql.json","recipes/mysql-ext.json","recipes/mssql.json","recipes/mssql-ext.json","recipes/mongodb.json","recipes/mongodb-ext.json","recipes/redis.json","recipes/redis-ext.json","recipes/elasticsearch.json","recipes/elasticsearch-ext.json"],"playbooks":["playbooks/playbooks.json","playbooks/hadr.json","playbooks/security.json","playbooks/cloud.json","playbooks/automation.json"],"solver":["solver/slow-query.json","solver/deadlock.json","solver/disk-full.json","solver/connection-storm.json","solver/replication-lag.json","solver/ora-01555.json","solver/backup-restore.json","solver/upgrade-wizard.json","solver/migration-wizard.json","solver/high-cpu.json","solver/memory-pressure.json","solver/install-setup.json","solver/corruption-recovery.json","solver/failover-drill.json","solver/security-incident.json","solver/audit-setup.json","solver/decide-ha.json","solver/decide-isolation.json","solver/decide-sqlnosql.json","solver/decide-shard-replica.json","solver/decide-backup.json","solver/decide-index.json","solver/decide-partition.json","solver/decide-pooling.json","solver/decide-cache.json","solver/decide-datatype.json","solver/decide-capacity.json","solver/slow-write.json","solver/io-bottleneck.json","solver/undo-temp-pressure.json","solver/long-transaction.json","solver/plan-regression.json","solver/replica-resync.json","solver/cache-hit-low.json","solver/arch-oltp.json","solver/arch-dwh.json","solver/arch-microservice.json","solver/arch-eventdriven.json","solver/arch-multiregion.json","solver/arch-highscale.json"],"reference":{"params":["reference/params-oracle.json","reference/params-postgres.json","reference/params-mysql.json","reference/params-mssql.json","reference/params-mongodb.json","reference/params-redis.json","reference/params-elasticsearch.json"],"waits":["reference/waits-oracle.json","reference/waits-postgres.json","reference/waits-mysql.json","reference/waits-mssql.json","reference/waits-mongodb.json","reference/waits-redis.json","reference/waits-elasticsearch.json"],"alerts":["reference/alerts-oracle.json","reference/alerts-postgres.json","reference/alerts-mysql.json","reference/alerts-mssql.json","reference/alerts-mongodb.json","reference/alerts-redis.json","reference/alerts-elasticsearch.json"]},"install":"install/index.json"},"engines.json":{"schemaVersion":1,"updated":"2026-07-03","engines":[{"id":"oracle","name":"Oracle","full":"Oracle Database","color":"#C74634","family":"sql","cli":"sqlplus","docs":"https://docs.oracle.com/en/database/"},{"id":"postgres","name":"PostgreSQL","full":"PostgreSQL","color":"#3461B5","family":"sql","cli":"psql","docs":"https://www.postgresql.org/docs/"},{"id":"mysql","name":"MySQL","full":"MySQL / MariaDB","color":"#00758F","family":"sql","cli":"mysql","docs":"https://dev.mysql.com/doc/"},{"id":"mssql","name":"SQL Server","full":"Microsoft SQL Server","color":"#6E3FC4","family":"sql","cli":"sqlcmd","docs":"https://learn.microsoft.com/en-us/sql/"},{"id":"mongodb","name":"MongoDB","full":"MongoDB","color":"#2E7D32","family":"document","cli":"mongosh","docs":"https://www.mongodb.com/docs/"},{"id":"redis","name":"Redis","full":"Redis","color":"#B45309","family":"keyvalue","cli":"redis-cli","docs":"https://redis.io/docs/"},{"id":"elasticsearch","name":"Elasticsearch","full":"Elasticsearch","color":"#BE2A6E","family":"search","cli":"curl / Kibana","docs":"https://www.elastic.co/guide/"}]},"rosetta/sessions.json":{"category":"세션 및 활동","categoryId":"sessions","order":1,"concepts":[{"id":"active-sessions","title":"활성 세션 / 실행 중인 쿼리 조회","aliases":["active sessions","running queries","who is connected","current activity","active session","running query","who is connected now","processlist","current op"],"engines":{"oracle":{"command":"SELECT sid, serial#, username, status, sql_id, machine, program\nFROM v$session\nWHERE type = 'USER' AND status = 'ACTIVE';","note":"v$session은 현재 활성 세션을 보여 준다. 전체 SQL 텍스트는 sql_id로 v$sql과 조인한다.","danger":"safe"},"postgres":{"command":"SELECT pid, usename, state, wait_event_type, query\nFROM pg_stat_activity\nWHERE state <> 'idle';","note":"state = 'active'는 그 시점에 쿼리를 실행 중인 세션을 보여 준다. 소요 시간은 backend_start / query_start로 계산한다.","danger":"safe"},"mysql":{"command":"SELECT id, user, host, db, command, time, state, info\nFROM information_schema.processlist\nWHERE command <> 'Sleep';\n-- Shortcut: SHOW FULL PROCESSLIST;","note":"MySQL 8 / MariaDB에서는 performance_schema.processlist가 락을 더 적게 잡는다.","danger":"safe"},"mssql":{"command":"SELECT r.session_id, s.login_name, r.status, r.command,\n       r.cpu_time, r.total_elapsed_time, t.text AS sql_text\nFROM sys.dm_exec_requests r\nJOIN sys.dm_exec_sessions s ON r.session_id = s.session_id\nCROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t\nWHERE s.is_user_process = 1;","note":"빠르게 확인하려면: EXEC sp_who2 'active';","danger":"safe"},"mongodb":{"command":"db.currentOp({ \"active\": true })","note":"장시간 실행 작업 필터링: db.currentOp({ active: true, secs_running: { $gt: 5 } })","danger":"safe"},"redis":{"command":"CLIENT LIST\n# For a summary: INFO clients","note":"Redis는 싱글 스레드이므로 '실행 중인 쿼리' 대신 연결된 클라이언트가 표시된다. 느린 명령은 SLOWLOG GET을 사용한다.","danger":"safe"}}},{"id":"blocking-sessions","title":"블로킹 / 락 대기 세션 찾기","aliases":["blocking","locks","lock wait","deadlock source","blocking session","lock waiting","who is blocking","who is blocking me","blocked","lock holder","what am I waiting on"],"engines":{"oracle":{"command":"SELECT blocking_session, sid, serial#, username,\n       sql_id, seconds_in_wait, event\nFROM v$session\nWHERE blocking_session IS NOT NULL;","note":"blocking_session 값이 채워진 세션은 blocking_session 값의 SID에 의해 블로킹된 상태다.","danger":"safe"},"postgres":{"command":"SELECT pid, usename, pg_blocking_pids(pid) AS blocked_by,\n       wait_event, query\nFROM pg_stat_activity\nWHERE cardinality(pg_blocking_pids(pid)) > 0;","note":"blocked_by 배열은 이 pid를 블로킹하는 PID를 보여 준다.","danger":"safe"},"mysql":{"command":"SELECT * FROM sys.innodb_lock_waits\\G\n-- MySQL 8: SELECT * FROM performance_schema.data_lock_waits;","note":"sys 스키마는 대기 중인 trx_id / 스레드와 블로킹하는 trx_id / 스레드를 읽기 쉬운 형태로 보여 준다.","danger":"safe"},"mssql":{"command":"SELECT r.session_id, r.blocking_session_id,\n       r.wait_type, r.wait_time, r.wait_resource\nFROM sys.dm_exec_requests r\nWHERE r.blocking_session_id <> 0;","note":"blocking_session_id <> 0인 요청은 블로킹된 상태다. 체인을 추적하려면 재귀 쿼리를 작성할 수 있다.","danger":"safe"},"mongodb":{"command":"db.currentOp({ \"waitingForLock\": true })","note":"WiredTiger는 문서/컬렉션 수준에서 락을 건다. 장시간 작업은 killOp로 종료할 수 있다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis commands run atomically one at a time; there are no row locks.\n# Closest diagnostic: SLOWLOG GET 10","note":"블로킹은 대개 장시간 실행되는 단일 명령(예: KEYS *, 대규모 SORT)에서 비롯된다. SLOWLOG로 확인할 수 있다.","danger":"safe"}}},{"id":"kill-session","title":"세션 / 쿼리 종료(kill)","aliases":["kill session","terminate","cancel query","terminate session","cancel the query","kill query","killop"],"engines":{"oracle":{"command":"ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE;\n-- To cancel only the running statement (18c+):\n-- ALTER SYSTEM CANCEL SQL 'sid,serial#';","note":"sid와 serial# 값은 v$session에서 가져온다. IMMEDIATE는 롤백을 즉시 시작한다.","danger":"danger"},"postgres":{"command":"-- Cancel only the running query (the session stays):\nSELECT pg_cancel_backend(pid);\n-- Close the session entirely:\nSELECT pg_terminate_backend(pid);","note":"먼저 pg_cancel_backend를 시도하고, 효과가 없으면 pg_terminate_backend를 사용한다.","danger":"danger"},"mysql":{"command":"-- Kill only the query:\nKILL QUERY <id>;\n-- Close the connection entirely:\nKILL CONNECTION <id>;","note":"<id> 값은 processlist의 Id 컬럼이다.","danger":"danger"},"mssql":{"command":"KILL <session_id>;\n-- To see rollback progress:\n-- KILL <session_id> WITH STATUSONLY;","note":"KILL은 열린 트랜잭션을 롤백한다. 대형 트랜잭션은 롤백에 오랜 시간이 걸릴 수 있다.","danger":"danger"},"mongodb":{"command":"db.killOp(<opid>)","note":"opid 값은 db.currentOp() 출력의 opid 필드에서 가져온다.","danger":"danger"},"redis":{"command":"CLIENT KILL ID <client-id>\n# or by address: CLIENT KILL ADDR 10.0.0.5:6379","note":"client-id는 CLIENT LIST로 확인한다. 이미 실행 중인 단일 명령은 중단할 수 없으며(Lua 스크립트는 예외: SCRIPT KILL) 완료될 때까지 기다려야 한다. BLPOP/XREAD BLOCK 같은 블로킹 명령에서 대기 중인 클라이언트를 연결을 끊지 않고 해제하려면 CLIENT UNBLOCK <client-id> [TIMEOUT|ERROR]를 사용한다. CLIENT UNPAUSE는 CLIENT PAUSE로 일시 중지된 클라이언트만 재개한다.","danger":"caution"}}},{"id":"connection-count","title":"연결 수 및 연결 한도 확인","aliases":["connections","max connections","connection limit","connection count","how many connections","is the connection pool full"],"engines":{"oracle":{"command":"SELECT COUNT(*) AS current_sessions FROM v$session WHERE type = 'USER';\nSELECT name, value FROM v$parameter WHERE name IN ('processes','sessions');","note":"한도는 processes/sessions 파라미터다. 사용률은 resource_limit 뷰에서도 확인할 수 있다.","danger":"safe"},"postgres":{"command":"SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';\nSHOW max_connections;","note":"pg_stat_activity에는 백그라운드 프로세스도 포함되므로 클라이언트 연결(backend_type = 'client backend')만 집계한다. superuser_reserved_connections 슬롯은 관리자용으로 예약된다.","danger":"safe"},"mysql":{"command":"SHOW STATUS LIKE 'Threads_connected';\nSHOW VARIABLES LIKE 'max_connections';","note":"Max_used_connections 상태 값은 최고치를 보여 준다.","danger":"safe"},"mssql":{"command":"SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process = 1;\nSELECT @@MAX_CONNECTIONS AS configured_max;","note":"@@MAX_CONNECTIONS는 이론상 상한이며, 실제 한도는 메모리와 워커 스레드에 따라 달라진다.","danger":"safe"},"mongodb":{"command":"db.serverStatus().connections","note":"current, available, totalCreated 필드를 반환한다. 한도는 net.maxIncomingConnections로 설정한다.","danger":"safe"},"redis":{"command":"INFO clients\nCONFIG GET maxclients","note":"connected_clients는 현재 연결 수, maxclients는 설정된 상한(기본값 10000)이다.","danger":"safe"}}}]},"rosetta/storage.json":{"category":"공간 및 스토리지","categoryId":"storage","order":2,"concepts":[{"id":"database-size","title":"데이터베이스 전체 크기 조회","aliases":["database size","db size","database size","how much space does it use","disk usage","total size"],"engines":{"oracle":{"command":"SELECT ROUND(SUM(bytes)/1024/1024/1024, 2) AS total_gb\nFROM dba_data_files;","note":"사용 중인 세그먼트만 보려면 dba_segments를 합산한다. TEMP는 dba_temp_files를 더한다.","danger":"safe"},"postgres":{"command":"SELECT pg_size_pretty(pg_database_size(current_database()));","note":"모든 데이터베이스: SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database ORDER BY 2 DESC;","danger":"safe"},"mysql":{"command":"SELECT table_schema AS db,\n       ROUND(SUM(data_length + index_length)/1024/1024, 1) AS mb\nFROM information_schema.tables\nGROUP BY table_schema\nORDER BY mb DESC;","note":"InnoDB에서는 이 값이 추정치다. 정확한 파일 크기는 OS 수준에서 ibdata/파일을 확인한다.","danger":"safe"},"mssql":{"command":"EXEC sp_spaceused;\n-- All databases:\nSELECT DB_NAME(database_id) AS db,\n       CAST(SUM(size) * 8.0 / 1024 AS DECIMAL(10,1)) AS mb\nFROM sys.master_files\nGROUP BY database_id;","note":"size 컬럼은 8 KB 페이지 단위이며, * 8 / 1024로 MB로 변환한다.","danger":"safe"},"mongodb":{"command":"db.stats(1024*1024)\n// returns the dataSize, storageSize, indexSize fields in MB","note":"storageSize는 압축된 디스크상 크기, dataSize는 압축 전 논리 크기다.","danger":"safe"},"redis":{"command":"INFO memory\n# the used_memory_human line gives the total size in memory\nDBSIZE  # number of keys","note":"Redis는 메모리 기반이므로 '크기'는 RAM 사용량을 뜻한다. RDB/AOF 파일 크기는 디스크에 저장된 상태다.","danger":"safe"}}},{"id":"largest-tables","title":"가장 큰 테이블 / 컬렉션 목록","aliases":["largest tables","biggest tables","top tables by size","largest table","tables using the most space","bigkeys"],"engines":{"oracle":{"command":"SELECT segment_name, segment_type,\n       ROUND(bytes/1024/1024, 1) AS mb\nFROM dba_segments\nWHERE segment_type IN ('TABLE','TABLE PARTITION')\nORDER BY bytes DESC\nFETCH FIRST 10 ROWS ONLY;","note":"인덱스도 보려면 segment_type 필터를 제거한다.","danger":"safe"},"postgres":{"command":"SELECT relname AS table,\n       pg_size_pretty(pg_total_relation_size(relid)) AS total\nFROM pg_catalog.pg_statio_user_tables\nORDER BY pg_total_relation_size(relid) DESC\nLIMIT 10;","note":"pg_total_relation_size는 테이블 + 인덱스 + TOAST의 합계다. 테이블만 보려면 pg_relation_size를 사용한다.","danger":"safe"},"mysql":{"command":"SELECT table_name,\n       ROUND((data_length + index_length)/1024/1024, 1) AS mb\nFROM information_schema.tables\nWHERE table_schema = DATABASE()\nORDER BY (data_length + index_length) DESC\nLIMIT 10;","note":"특정 스키마만 보려면 WHERE table_schema = 'name'을 사용한다.","danger":"safe"},"mssql":{"command":"SELECT TOP 10 OBJECT_NAME(object_id) AS table_name,\n       SUM(reserved_page_count) * 8 / 1024 AS mb\nFROM sys.dm_db_partition_stats\nGROUP BY object_id\nORDER BY mb DESC;","note":"reserved_page_count는 예약된 모든 페이지(데이터 + 인덱스)를 포함한다.","danger":"safe"},"mongodb":{"command":"db.getCollectionNames()\n  .map(c => ({ name: c, mb: +(db[c].stats().storageSize/1048576).toFixed(1) }))\n  .sort((a, b) => b.mb - a.mb)\n  .slice(0, 10)","note":"storageSize는 압축된 디스크상 크기다. stats()는 대형 컬렉션에서도 비용이 낮다.","danger":"safe"},"redis":{"command":"redis-cli --bigkeys\n# To rank by memory (bytes): redis-cli --memkeys","note":"둘 다 SCAN으로 전체 키스페이스를 스캔한다(서버를 블로킹하지는 않지만 부하를 준다). 차이는 지표다: --bigkeys는 타입별로 요소 수/길이 기준 가장 큰 키를, --memkeys는 MEMORY USAGE를 통한 메모리 사용량 기준으로 보고한다. 단일 키는 MEMORY USAGE <key>를 사용한다.","danger":"caution"}}},{"id":"free-space","title":"여유 공간 / 사용률 확인","aliases":["free space","disk full","free space","usage","space left","tablespace usage","disk is full"],"engines":{"oracle":{"command":"SELECT df.tablespace_name,\n       ROUND(SUM(df.bytes)/1024/1024) AS total_mb,\n       ROUND(NVL(fs.free, 0)/1024/1024) AS free_mb,\n       ROUND((1 - NVL(fs.free,0)/SUM(df.bytes)) * 100, 1) AS pct_used\nFROM dba_data_files df\nLEFT JOIN (SELECT tablespace_name, SUM(bytes) free\n           FROM dba_free_space GROUP BY tablespace_name) fs\n  ON df.tablespace_name = fs.tablespace_name\nGROUP BY df.tablespace_name, fs.free;","note":"AUTOEXTEND가 켜진 파일은 실제 한도가 maxbytes다. MAXSIZE는 dba_data_files.maxbytes에서 확인한다.","danger":"safe"},"postgres":{"command":"-- Datafile usage is at the OS level (df -h). Tablespace sizes from inside the DB:\nSELECT spcname, pg_size_pretty(pg_tablespace_size(spcname))\nFROM pg_tablespace;","note":"PostgreSQL은 디스크 여유 공간을 OS에 의존한다. 임계값 관리를 위해 운영 체제 모니터링(df)이 필수다.","danger":"safe"},"mysql":{"command":"SELECT table_schema,\n       ROUND(SUM(data_free)/1024/1024, 1) AS reclaimable_mb\nFROM information_schema.tables\nGROUP BY table_schema;","note":"data_free는 테이블 내부의 회수 가능한(단편화된) 공간이다. 디스크 여유 공간은 OS 수준에서 확인한다.","danger":"safe"},"mssql":{"command":"SELECT name AS file_name,\n       size/128.0 AS size_mb,\n       size/128.0 - CAST(FILEPROPERTY(name,'SpaceUsed') AS int)/128.0 AS free_mb\nFROM sys.database_files;","note":"SpaceUsed는 페이지 단위이며, 128로 나누면 MB가 된다(8 KB 페이지).","danger":"safe"},"mongodb":{"command":"var s = db.stats();\n({ fsTotalGB: +(s.fsTotalSize/1073741824).toFixed(1),\n   fsUsedGB:  +(s.fsUsedSize/1073741824).toFixed(1) })","note":"WiredTiger에서는 fsTotalSize / fsUsedSize가 OS 수준의 디스크 전체 크기와 사용 크기를 보여 준다.","danger":"safe"},"redis":{"command":"INFO memory\nCONFIG GET maxmemory\n# Usage = used_memory / maxmemory","note":"maxmemory가 0이면 한도가 없으며, RAM이 가득 차면 OS OOM killer가 개입한다. eviction 동작은 maxmemory-policy가 결정한다.","danger":"safe"}}},{"id":"row-count","title":"행 / 문서 수 조회(빠른 방법)","aliases":["row count","count rows","row count","how many records","document count","how many rows"],"engines":{"oracle":{"command":"-- Statistics-based (fast, estimated):\nSELECT num_rows FROM dba_tables WHERE table_name = 'CUSTOMER';\n-- Exact: SELECT COUNT(*) FROM customer;","note":"num_rows는 마지막 ANALYZE/DBMS_STATS 실행 기준이다. 정확한 값은 COUNT(*)를 사용한다.","danger":"safe"},"postgres":{"command":"-- Estimated (instant):\nSELECT reltuples::bigint FROM pg_class WHERE relname = 'customer';\n-- Exact: SELECT count(*) FROM customer;","note":"reltuples는 마지막 ANALYZE/VACUUM 시점의 추정치다. 대형 테이블에서 전체 COUNT는 비용이 크다.","danger":"safe"},"mysql":{"command":"-- Estimated (InnoDB):\nSELECT table_rows FROM information_schema.tables\nWHERE table_name = 'customer';\n-- Exact: SELECT COUNT(*) FROM customer;","note":"InnoDB에서 table_rows는 추정치이며 최대 50%까지 차이가 날 수 있다.","danger":"safe"},"mssql":{"command":"SELECT SUM(row_count) AS rows\nFROM sys.dm_db_partition_stats\nWHERE object_id = OBJECT_ID('dbo.customer')\n  AND index_id IN (0, 1);","note":"index_id 0(heap) 또는 1(clustered)이 행 수를 보여 준다. COUNT(*) 스캔보다 훨씬 빠르다.","danger":"safe"},"mongodb":{"command":"// Fast (from metadata):\ndb.customer.estimatedDocumentCount()\n// Exact (can count with a filter): db.customer.countDocuments({})","note":"estimatedDocumentCount는 메타데이터를 읽는다(즉시). countDocuments는 실제 스캔을 수행한다.","danger":"safe"},"redis":{"command":"DBSIZE\n# Specific pattern: count roughly with SCAN (do not use KEYS * in production)","note":"DBSIZE는 선택된 데이터베이스의 전체 키 수를 O(1)로 반환한다.","danger":"safe"}}}]},"rosetta/performance.json":{"category":"성능","categoryId":"performance","order":3,"concepts":[{"id":"top-slow-queries","title":"가장 느린 / 비용이 큰 쿼리 찾기","aliases":["slow queries","top queries","slowest queries","top resource consumers","expensive queries","slowlog"],"engines":{"oracle":{"command":"SELECT sql_id,\n       ROUND(elapsed_time/1e6, 1) AS elapsed_sec,\n       executions,\n       ROUND(elapsed_time/1e6/NULLIF(executions,0), 3) AS sec_per_exec,\n       sql_text\nFROM v$sql\nORDER BY elapsed_time DESC\nFETCH FIRST 10 ROWS ONLY;","note":"이력 분석은 AWR(dba_hist_sqlstat)을 사용한다. sql_id로 DBMS_XPLAN.DISPLAY_CURSOR를 실행하면 실행 계획을 볼 수 있다.","danger":"safe"},"postgres":{"command":"SELECT calls,\n       ROUND(total_exec_time::numeric, 1) AS total_ms,\n       ROUND(mean_exec_time::numeric, 2) AS mean_ms,\n       query\nFROM pg_stat_statements\nORDER BY total_exec_time DESC\nLIMIT 10;","note":"pg_stat_statements 확장(shared_preload_libraries)이 필요하다. pg_stat_statements_reset()으로 초기화한다.","danger":"safe"},"mysql":{"command":"SELECT digest_text,\n       count_star AS calls,\n       ROUND(sum_timer_wait/1e12, 2) AS total_sec,\n       ROUND(avg_timer_wait/1e9, 2) AS avg_ms\nFROM performance_schema.events_statements_summary_by_digest\nORDER BY sum_timer_wait DESC\nLIMIT 10;","note":"대안: slow_query_log = ON과 long_query_time을 설정한 슬로 쿼리 로그.","danger":"safe"},"mssql":{"command":"SELECT TOP 10\n       qs.execution_count,\n       qs.total_elapsed_time/1000 AS total_ms,\n       qs.total_elapsed_time/qs.execution_count/1000 AS avg_ms,\n       SUBSTRING(t.text, 1, 200) AS sql_text\nFROM sys.dm_exec_query_stats qs\nCROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) t\nORDER BY qs.total_elapsed_time DESC;","note":"Query Store가 활성화되어 있으면 더 풍부한 이력 분석을 위해 sys.query_store_runtime_stats를 사용한다.","danger":"safe"},"mongodb":{"command":"// Enable the profiler for operations over 100ms:\ndb.setProfilingLevel(1, { slowms: 100 })\n// Then read the slowest ones:\ndb.system.profile.find().sort({ millis: -1 }).limit(10)","note":"프로파일러는 쓰기 오버헤드를 추가한다. 진단 후 db.setProfilingLevel(0)으로 끈다.","danger":"caution"},"redis":{"command":"SLOWLOG GET 10\n# Set the threshold (microseconds): CONFIG SET slowlog-log-slower-than 10000","note":"slowlog-log-slower-than은 마이크로초 단위다(10000 = 10 ms). SLOWLOG RESET으로 비운다.","danger":"safe"}}},{"id":"explain-plan","title":"쿼리 실행 계획 확인(EXPLAIN)","aliases":["explain","execution plan","query plan","explain plan","show plan","explain analyze","why slow"],"engines":{"oracle":{"command":"EXPLAIN PLAN FOR\nSELECT * FROM customer WHERE city = 'Ankara';\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);\n-- For the actual executed plan:\n-- SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('&sql_id', NULL, 'ALLSTATS LAST'));","note":"EXPLAIN PLAN은 예상 실행 계획을 보여 준다. 실제 행 수는 DISPLAY_CURSOR + STATISTICS_LEVEL=ALL을 사용한다.","danger":"safe"},"postgres":{"command":"EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)\nSELECT * FROM customer WHERE city = 'Ankara';","note":"ANALYZE는 쿼리를 실제로 실행한다(INSERT/UPDATE/DELETE는 주의. 트랜잭션 안에서 시도한 뒤 ROLLBACK한다).","danger":"caution"},"mysql":{"command":"EXPLAIN ANALYZE\nSELECT * FROM customer WHERE city = 'Ankara';\n-- Estimate only (does not execute): EXPLAIN FORMAT=JSON SELECT ...;","note":"EXPLAIN ANALYZE(MySQL 8.0.18+)는 쿼리를 실행한다. FORMAT=JSON은 실행하지 않고 상세 정보를 보여 준다.","danger":"caution"},"mssql":{"command":"SET STATISTICS PROFILE ON;\nSELECT * FROM dbo.customer WHERE city = 'Ankara';\nSET STATISTICS PROFILE OFF;\n-- Estimated plan only (does not execute):\n-- SET SHOWPLAN_XML ON;","note":"SSMS에서 'Include Actual Execution Plan'(Ctrl+M)은 그래픽 실행 계획을 보여 준다.","danger":"caution"},"mongodb":{"command":"db.customer.find({ city: \"Ankara\" }).explain(\"executionStats\")","note":"executionStats는 실제 반환된 문서 수와 인덱스 사용 여부(IXSCAN vs COLLSCAN)를 보여 준다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no query planner.\n# The complexity of each command is documented (e.g. GET O(1), SORT O(N+M*log(M))).","note":"성능은 명령 복잡도와 키/데이터 구조 크기에 따라 결정된다. SLOWLOG로 측정한다.","danger":"safe"}}},{"id":"unused-indexes","title":"사용되지 않는 인덱스 탐지","aliases":["unused indexes","unused index","redundant index","index usage","drop index candidate"],"engines":{"oracle":{"command":"-- Start monitoring:\nALTER INDEX customer_city_idx MONITORING USAGE;\n-- Check after a while (12c+, all schemas):\nSELECT owner, index_name, used FROM dba_object_usage;","note":"USED = NO이면 모니터링 기간 동안 해당 인덱스가 사용되지 않은 것이다. 모니터링은 NOMONITORING USAGE로 끈다. 기존 v$object_usage는 세션 사용자의 인덱스만 보여 주며 12c부터 deprecated다. 12.2+에서는 인덱스 사용 현황이 DBA_INDEX_USAGE에 자동으로 기록된다.","danger":"safe"},"postgres":{"command":"SELECT relname AS table, indexrelname AS index, idx_scan\nFROM pg_stat_user_indexes\nWHERE idx_scan = 0\nORDER BY relname;","note":"idx_scan = 0은 통계가 마지막으로 초기화된 이후 인덱스가 사용되지 않았음을 뜻한다. UNIQUE/PK 인덱스는 삭제하지 않는다.","danger":"safe"},"mysql":{"command":"SELECT * FROM sys.schema_unused_indexes;","note":"performance_schema가 활성화되어 있어야 하고 서버가 일정 기간 가동된 상태여야 한다. UNIQUE 제약 조건은 제외된다.","danger":"safe"},"mssql":{"command":"SELECT OBJECT_NAME(i.object_id) AS table_name, i.name AS index_name\nFROM sys.indexes i\nLEFT JOIN sys.dm_db_index_usage_stats s\n  ON i.object_id = s.object_id AND i.index_id = s.index_id\n  AND s.database_id = DB_ID()\nWHERE i.type_desc = 'NONCLUSTERED'\n  AND COALESCE(s.user_seeks + s.user_scans + s.user_lookups, 0) = 0;","note":"usage_stats는 서버 재시작 시 초기화된다. 충분한 관찰 기간을 둔다.","danger":"safe"},"mongodb":{"command":"db.customer.aggregate([{ $indexStats: {} }])\n// Indexes with low/0 accesses.ops are candidates","note":"accesses.ops는 accesses.since 시점 이후의 접근 횟수다. _id 인덱스는 삭제할 수 없다.","danger":"safe"},"redis":{"command":"# Not applicable: core Redis has no secondary indexes.\n# If the RediSearch module is used, check statistics with FT.INFO <index>.","note":"데이터 모델이 키-값이므로 전통적인 인덱스 개념이 없다.","danger":"safe"}}},{"id":"cache-hit-ratio","title":"캐시 적중률 측정","aliases":["cache hit ratio","buffer cache","cache hit","hit ratio","buffer pool hit"],"engines":{"oracle":{"command":"SELECT metric_name, ROUND(value, 2) AS pct\nFROM v$sysmetric\nWHERE metric_name = 'Buffer Cache Hit Ratio'\n  AND intsize_csec = (SELECT MAX(intsize_csec) FROM v$sysmetric);","note":"적중률이 낮다고 항상 나쁜 것은 아니다. 단독으로 보지 말고 워크로드 맥락에서 해석한다.","danger":"safe"},"postgres":{"command":"SELECT ROUND(sum(blks_hit) * 100.0 / NULLIF(sum(blks_hit) + sum(blks_read), 0), 2) AS cache_hit_pct\nFROM pg_stat_database;","note":"일반적으로 99% 이상이 목표다. 낮으면 shared_buffers와 OS 캐시를 검토한다.","danger":"safe"},"mysql":{"command":"SELECT ROUND((1 - (\n    (SELECT variable_value FROM performance_schema.global_status WHERE variable_name='Innodb_buffer_pool_reads') /\n    (SELECT variable_value FROM performance_schema.global_status WHERE variable_name='Innodb_buffer_pool_read_requests')\n  )) * 100, 2) AS buffer_pool_hit_pct;","note":"reads는 디스크 읽기, read_requests는 전체 읽기다. 낮으면 innodb_buffer_pool_size를 늘린다.","danger":"safe"},"mssql":{"command":"SELECT ROUND(\n  100.0 * (SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name='Buffer cache hit ratio')\n        / NULLIF((SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name='Buffer cache hit ratio base'), 0), 2) AS pct;","note":"최신 SQL Server에서는 Page Life Expectancy(PLE)가 메모리 압박을 더 의미 있게 보여 주는 지표다.","danger":"safe"},"mongodb":{"command":"var c = db.serverStatus().wiredTiger.cache;\n({ configuredMB: +(c['maximum bytes configured']/1048576).toFixed(0),\n   inCacheMB:    +(c['bytes currently in the cache']/1048576).toFixed(0),\n   readIntoCache: c['pages read into cache'],\n   evictedPages:  c['unmodified pages evicted'] })","note":"'pages read into cache'와 eviction이 높으면 WiredTiger 캐시가 너무 작다는 뜻이다.","danger":"safe"},"redis":{"command":"INFO stats\n# keyspace_hits / (keyspace_hits + keyspace_misses) = hit ratio","note":"적중률이 지속적으로 낮으면 TTL/eviction 설정이나 접근 패턴을 검토해야 한다.","danger":"safe"}}}]},"rosetta/backup.json":{"category":"백업 및 복구","categoryId":"backup","order":4,"concepts":[{"id":"logical-dump","title":"논리 백업(덤프) 수행","aliases":["backup","dump","export","take backup","logical dump","logical backup","pg_dump","mysqldump","mongodump"],"engines":{"oracle":{"command":"expdp system/password DIRECTORY=dpump_dir \\\n  SCHEMAS=hr DUMPFILE=hr_%U.dmp LOGFILE=hr_exp.log PARALLEL=4","note":"Data Pump를 사용한다. DIRECTORY 객체는 CREATE DIRECTORY로 미리 정의해야 한다. 물리 백업은 RMAN을 사용한다.","danger":"safe"},"postgres":{"command":"pg_dump -U postgres -h localhost -Fc -f db.dump mydb\n# -Fc = compressed custom format (restored with pg_restore)","note":"-Fc는 유연한 복원(선택적 테이블/병렬)을 지원한다. 클러스터 전체(롤 포함)는 pg_dumpall을 사용한다.","danger":"safe"},"mysql":{"command":"mysqldump -u root -p --single-transaction \\\n  --routines --triggers --events mydb > db.sql","note":"--single-transaction은 InnoDB에서 락 없이 일관된 백업을 제공한다. 대용량 데이터를 위한 병렬 옵션: mydumper/myloader 또는 MySQL Shell util.dumpInstance()/dumpSchemas()(mysqlpump는 8.0.34에서 deprecated되고 8.4에서 제거됨).","danger":"safe"},"mssql":{"command":"BACKUP DATABASE [MyDB]\nTO DISK = N'C:\\backup\\MyDB_full.bak'\nWITH COMPRESSION, CHECKSUM, INIT;","note":"INIT는 기존 파일을 덮어쓰고, NOINIT는 추가한다. 로그 백업은 BACKUP LOG를 사용한다(FULL 복구 모델).","danger":"safe"},"mongodb":{"command":"mongodump --uri=\"mongodb://localhost:27017\" \\\n  --archive=db.archive.gz --gzip --db=mydb","note":"샤딩된 클러스터에서는 일관성을 위해 밸런서를 중지한 상태로 mongos를 통해 수행한다.","danger":"safe"},"redis":{"command":"BGSAVE\n# Takes an RDB snapshot in the background (dump.rdb).\n# Non-blocking copy: redis-cli --rdb /backup/dump.rdb","note":"BGSAVE는 fork를 통해 백그라운드에서 실행된다. SAVE는 서버를 블로킹한다(운영 환경에서 사용 금지). AOF를 활성화하면 데이터 손실이 줄어든다.","danger":"safe"}}},{"id":"restore-dump","title":"백업에서 복원","aliases":["restore","import","restore backup","restore from backup","recover","pg_restore","mongorestore"],"engines":{"oracle":{"command":"impdp system/password DIRECTORY=dpump_dir \\\n  SCHEMAS=hr DUMPFILE=hr_%U.dmp LOGFILE=hr_imp.log","note":"다른 스키마로 로드하려면 REMAP_SCHEMA를, 다른 테이블스페이스로 로드하려면 REMAP_TABLESPACE를 사용한다.","danger":"danger"},"postgres":{"command":"# custom format:\npg_restore -U postgres -d target_db --clean --if-exists db.dump\n# plain SQL: psql -U postgres -d target_db -f db.sql","note":"--clean은 기존 객체를 삭제한다. 병렬 복원은 -j <n>을 사용한다.","danger":"danger"},"mysql":{"command":"mysql -u root -p target_db < db.sql","note":"대상 데이터베이스가 미리 존재해야 한다(CREATE DATABASE). 대용량 파일은 source 명령이 권장된다.","danger":"danger"},"mssql":{"command":"RESTORE DATABASE [MyDB]\nFROM DISK = N'C:\\backup\\MyDB_full.bak'\nWITH REPLACE, RECOVERY;","note":"특정 시점 복구는 전체 백업을 WITH NORECOVERY로 복원한 뒤 STOPAT으로 로그 백업을 적용한다.","danger":"danger"},"mongodb":{"command":"mongorestore --uri=\"mongodb://localhost:27017\" \\\n  --archive=db.archive.gz --gzip --drop","note":"--drop은 복원 전에 대상 컬렉션을 삭제한다. 선택적 복원은 --nsInclude를 사용한다.","danger":"danger"},"redis":{"command":"# 1) Stop Redis   2) Copy dump.rdb to the 'dir' location\n# 3) Start Redis (the RDB is loaded automatically at startup)\nredis-cli CONFIG GET dir   # find the target directory","note":"AOF가 활성화되어 있으면 RDB 대신 AOF가 로드된다. RDB에서 복원하려면 appendonly no로 시작하거나 AOF를 재생성한다.","danger":"danger"}}},{"id":"last-backup-status","title":"마지막 백업 상태 / 시간 확인","aliases":["last backup","backup status","when was the last backup","backup state","backup history","rpo"],"engines":{"oracle":{"command":"SELECT session_key, input_type, status,\n       start_time, end_time,\n       output_bytes_display\nFROM v$rman_backup_job_details\nORDER BY start_time DESC\nFETCH FIRST 10 ROWS ONLY;","note":"RMAN 이력은 RMAN> LIST BACKUP SUMMARY로도 볼 수 있다.","danger":"safe"},"postgres":{"command":"-- WAL archiving health (for physical backup/PITR):\nSELECT last_archived_wal, last_archived_time,\n       failed_count, last_failed_time\nFROM pg_stat_archiver;","note":"논리 백업(pg_dump)은 DB 내부에 기록되지 않는다. 백업 도구/작업의 로그를 확인한다.","danger":"safe"},"mysql":{"command":"-- There is no built-in backup record; check the backup tool's log.\n-- For the binlog-based PITR window:\nSHOW BINARY LOGS;","note":"SHOW BINARY LOGS는 사용 가능한 binlog 파일(PITR 범위)을 보여 준다. 백업 이력은 도구(Xtrabackup 등)의 로그에 있다.","danger":"safe"},"mssql":{"command":"SELECT database_name,\n       MAX(backup_finish_date) AS last_backup,\n       CASE type WHEN 'D' THEN 'Full' WHEN 'I' THEN 'Diff'\n                 WHEN 'L' THEN 'Log' END AS backup_type\nFROM msdb.dbo.backupset\nGROUP BY database_name, type\nORDER BY database_name;","note":"msdb.dbo.backupset은 SQL Server가 수행한 모든 백업을 기록한다.","danger":"safe"},"mongodb":{"command":"// There is no built-in backup record. PITR window = oplog duration:\nrs.printReplicationInfo()  // oplog first/last event times","note":"oplog 범위가 replica set에서 얼마나 과거까지 PITR할 수 있는지를 결정한다. 백업 이력은 도구의 로그에 있다.","danger":"safe"},"redis":{"command":"INFO persistence\n# check the rdb_last_save_time (epoch), rdb_last_bgsave_status,\n# aof_last_bgrewrite_status lines","note":"rdb_last_save_time은 마지막으로 성공한 RDB 저장 시각(Unix epoch)을, rdb_changes_since_last_save는 아직 백업되지 않은 쓰기 수를 보여 준다.","danger":"safe"}}}]},"rosetta/security.json":{"category":"사용자 및 권한","categoryId":"security","order":5,"concepts":[{"id":"list-users","title":"사용자 / 롤 목록","aliases":["list users","show users","list users","roles","who has access","accounts"],"engines":{"oracle":{"command":"SELECT username, account_status, default_tablespace, created\nFROM dba_users\nORDER BY username;","note":"account_status는 LOCKED/EXPIRED 상태를 보여 준다. 롤은 dba_roles를 사용한다.","danger":"safe"},"postgres":{"command":"SELECT rolname, rolsuper, rolcanlogin, rolvaliduntil\nFROM pg_roles\nORDER BY rolname;\n-- Shortcut (psql): \\du","note":"PostgreSQL에서 사용자는 LOGIN 속성을 가진 롤이며, rolcanlogin으로 구분한다.","danger":"safe"},"mysql":{"command":"SELECT user, host, account_locked, password_expired\nFROM mysql.user\nORDER BY user;","note":"MySQL에서 계정은 사용자 + 호스트 쌍이다('app'@'%'와 'app'@'localhost'는 서로 다르다).","danger":"safe"},"mssql":{"command":"-- Server logins:\nSELECT name, type_desc, is_disabled FROM sys.server_principals\nWHERE type IN ('S','U','G');\n-- Database users:\nSELECT name, type_desc FROM sys.database_principals\nWHERE type IN ('S','U','G');","note":"로그인은 서버 수준, 사용자는 데이터베이스 수준이며, 둘은 SID로 매핑된다.","danger":"safe"},"mongodb":{"command":"// Users in the current database:\ndb.getUsers()\n// All users in the system: use admin; db.system.users.find({}, {user:1, db:1, roles:1})","note":"사용자는 생성된 데이터베이스(authenticationDatabase)에 정의된다.","danger":"safe"},"redis":{"command":"ACL LIST\n# Names only: ACL USERS\n# Your own identity: ACL WHOAMI","note":"Redis 6+ ACL 체계. 그 이전에는 단일 'default' 사용자와 requirepass만 있었다.","danger":"safe"}}},{"id":"create-user-grant","title":"사용자 생성 및 권한 부여","aliases":["create user","grant","add user","create user","grant privilege","grant permission","new user"],"engines":{"oracle":{"command":"CREATE USER app IDENTIFIED BY \"Strong#Pass1\";\nGRANT CREATE SESSION TO app;\nGRANT SELECT, INSERT, UPDATE ON hr.employee TO app;","note":"CREATE SESSION이 없으면 사용자가 접속할 수 없다. 쿼터: ALTER USER app QUOTA 100M ON users;","danger":"caution"},"postgres":{"command":"CREATE ROLE app LOGIN PASSWORD 'Strong#Pass1';\nGRANT CONNECT ON DATABASE mydb TO app;\nGRANT USAGE ON SCHEMA public TO app;\nGRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO app;","note":"향후 생성될 테이블: ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app;","danger":"caution"},"mysql":{"command":"CREATE USER 'app'@'%' IDENTIFIED BY 'Strong#Pass1';\nGRANT SELECT, INSERT, UPDATE ON mydb.* TO 'app'@'%';","note":"'%'는 모든 호스트에서의 접속을 허용한다. 보안을 위해 가능하면 특정 IP/호스트를 지정한다. CREATE USER/GRANT는 즉시 적용되며, FLUSH PRIVILEGES는 권한 테이블에 DML을 직접 실행한 경우에만 필요하다.","danger":"caution"},"mssql":{"command":"CREATE LOGIN app WITH PASSWORD = 'Strong#Pass1';\nUSE mydb;\nCREATE USER app FOR LOGIN app;\nGRANT SELECT, INSERT, UPDATE ON SCHEMA::dbo TO app;","note":"로그인은 서버 전체에 적용되며, 사용자는 각 데이터베이스에 별도로 생성한다. 롤 부여: ALTER ROLE db_datareader ADD MEMBER app;","danger":"caution"},"mongodb":{"command":"db.createUser({\n  user: \"app\",\n  pwd: \"Strong#Pass1\",\n  roles: [ { role: \"readWrite\", db: \"mydb\" } ]\n})","note":"사용자를 생성한 db가 해당 사용자의 auth db가 된다. 기본 제공 롤: read, readWrite, dbAdmin, clusterAdmin...","danger":"caution"},"redis":{"command":"ACL SETUSER app on >Strong#Pass1 ~app:* +@read +@write\n# To persist: ACL SAVE if an aclfile is defined, otherwise CONFIG REWRITE (users are written to redis.conf)","note":"on = 활성화, >password는 비밀번호 추가, ~app:*는 키 패턴, +@read/+@write는 명령 카테고리다. 영구 저장하지 않으면 재시작 시 사라진다. ACL SAVE는 aclfile이 설정된 경우에만 동작하고 그렇지 않으면 오류를 반환하므로, 이 경우 CONFIG REWRITE를 사용한다.","danger":"caution"}}},{"id":"show-privileges","title":"사용자 권한 조회","aliases":["show grants","privileges","show privileges","who can access what","permissions","roles of user"],"engines":{"oracle":{"command":"SELECT privilege FROM dba_sys_privs WHERE grantee = 'APP'\nUNION ALL SELECT granted_role FROM dba_role_privs WHERE grantee = 'APP'\nUNION ALL SELECT privilege||' ON '||owner||'.'||table_name\n          FROM dba_tab_privs WHERE grantee = 'APP';","note":"grantee는 대문자로 저장된다. 롤을 통해 간접 부여된 권한은 role_role_privs도 따라가며 확인한다.","danger":"safe"},"postgres":{"command":"SELECT grantee, table_schema, table_name, privilege_type\nFROM information_schema.role_table_grants\nWHERE grantee = 'app';\n-- Role memberships (direct): \\drg app (PG16+; earlier \\du app)","note":"롤 멤버십(상속)은 pg_auth_members에 저장된다. \\du/\\drg는 직접 멤버십만 보여 주므로, 간접 멤버십은 pg_has_role('app', '<role>', 'MEMBER')를 사용한다.","danger":"safe"},"mysql":{"command":"SHOW GRANTS FOR 'app'@'%';","note":"롤(MySQL 8)은 먼저 SET ROLE ALL을 실행하거나 SHOW GRANTS FOR 'app'@'%' USING 'role_name';을 사용한다.","danger":"safe"},"mssql":{"command":"SELECT dp.name AS principal, p.permission_name,\n       p.state_desc, o.name AS object\nFROM sys.database_permissions p\nJOIN sys.database_principals dp ON p.grantee_principal_id = dp.principal_id\nLEFT JOIN sys.objects o ON p.major_id = o.object_id\nWHERE dp.name = 'app';","note":"롤을 통해 부여된 권한은 sys.database_role_members의 멤버십도 확인한다.","danger":"safe"},"mongodb":{"command":"db.getUser(\"app\", { showPrivileges: true, showCredentials: false })","note":"showPrivileges는 롤의 확장된(유효) 권한도 나열한다.","danger":"safe"},"redis":{"command":"ACL GETUSER app","note":"허용된 명령, 키 패턴(~), pub/sub 채널(&), 비밀번호 해시를 반환한다.","danger":"safe"}}},{"id":"change-password","title":"사용자 비밀번호 변경","aliases":["change password","reset password","change password","reset password","alter password"],"engines":{"oracle":{"command":"ALTER USER app IDENTIFIED BY \"New#Pass2\";","note":"잠긴 계정 해제: ALTER USER app ACCOUNT UNLOCK; 비밀번호가 만료된 경우 같은 명령으로 재설정된다.","danger":"caution"},"postgres":{"command":"ALTER ROLE app WITH PASSWORD 'New#Pass2';","note":"비밀번호를 로그에 남기지 않으려면 \\password app(psql) 메타 명령을 사용한다. 클라이언트 측에서 비밀번호를 해시한다.","danger":"caution"},"mysql":{"command":"ALTER USER 'app'@'%' IDENTIFIED BY 'New#Pass2';","note":"올바른 호스트를 선택했는지 확인한다. 'app'@'localhost'는 별도 계정이다. ALTER USER는 즉시 적용되며 FLUSH PRIVILEGES는 필요 없다.","danger":"caution"},"mssql":{"command":"ALTER LOGIN app WITH PASSWORD = 'New#Pass2';\n-- For a user changing their own password: add OLD_PASSWORD","note":"MUST_CHANGE로 첫 로그인 시 변경을 강제할 수 있다: WITH PASSWORD='...' MUST_CHANGE, CHECK_POLICY=ON.","danger":"caution"},"mongodb":{"command":"db.changeUserPassword(\"app\", \"New#Pass2\")","note":"사용자의 auth 데이터베이스로 전환했는지 확인한다(use mydb).","danger":"caution"},"redis":{"command":"# Clear old passwords and set the new one:\nACL SETUSER app resetpass >New#Pass2\n# Persistence: ACL SAVE if an aclfile is defined, otherwise CONFIG REWRITE","note":"resetpass는 기존 비밀번호를 모두 제거하고, >는 추가만 한다. 영구 저장은 aclfile을 사용하면 ACL SAVE, 사용자가 redis.conf에 정의되어 있으면 CONFIG REWRITE를 실행한다(aclfile이 없으면 ACL SAVE는 오류를 반환한다).","danger":"caution"}}}]},"rosetta/maintenance.json":{"category":"유지보수 및 관리","categoryId":"maintenance","order":6,"concepts":[{"id":"update-statistics","title":"통계 갱신(옵티마이저용)","aliases":["update statistics","analyze","update stats","gather stats","optimizer stats","analyze for planner"],"engines":{"oracle":{"command":"EXEC DBMS_STATS.GATHER_TABLE_STATS('HR', 'EMPLOYEE');\n-- Whole schema: EXEC DBMS_STATS.GATHER_SCHEMA_STATS('HR');","note":"AUTO 작업이 보통 야간에 수집한다. 문제가 생기면 수동으로 수집한다. ESTIMATE_PERCENT=>AUTO_SAMPLE_SIZE가 권장된다.","danger":"safe"},"postgres":{"command":"ANALYZE customer;\n-- Both cleanup and analyze: VACUUM ANALYZE customer;","note":"Autovacuum이 자동으로 수행한다. 대량 데이터 적재 직후에는 수동 ANALYZE를 실행해야 정확한 실행 계획이 나온다.","danger":"safe"},"mysql":{"command":"ANALYZE TABLE customer;","note":"InnoDB에서 짧은 읽기 락을 건다. 대형 테이블은 활동이 적은 시간에 실행한다.","danger":"safe"},"mssql":{"command":"UPDATE STATISTICS dbo.customer WITH FULLSCAN;\n-- Whole database: EXEC sp_updatestats;","note":"FULLSCAN이 가장 정확하지만 비용이 가장 크다. 대형 테이블에서는 샘플링(SAMPLE)이 나을 수 있다.","danger":"safe"},"mongodb":{"command":"// There are no manual statistics; the planner measures at runtime.\n// To clear a stale plan cache:\ndb.customer.getPlanCache().clear()","note":"MongoDB는 비용 추정 대신 실제 시도(plan racing)를 수행하므로 수동 통계가 필요 없다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no query optimizer / statistics.","note":"명령 복잡도는 고정되어 있으며, 성능은 데이터 구조 선택에 달려 있다.","danger":"safe"}}},{"id":"rebuild-index","title":"인덱스 재구축 / 재구성","aliases":["rebuild index","reindex","refresh index","reorganize","index maintenance","defrag index"],"engines":{"oracle":{"command":"ALTER INDEX customer_city_idx REBUILD ONLINE;","note":"ONLINE은 테이블 락 없이 재구축한다(Enterprise Edition). 다른 테이블스페이스로 이동: ... REBUILD TABLESPACE new_ts.","danger":"caution"},"postgres":{"command":"REINDEX INDEX CONCURRENTLY customer_city_idx;\n-- Whole table: REINDEX TABLE CONCURRENTLY customer;","note":"CONCURRENTLY(PG 12+)는 테이블 쓰기를 블로킹하지 않고 재구축한다. 느리지만 안전하다.","danger":"caution"},"mysql":{"command":"-- Rebuild InnoDB indexes + table:\nALTER TABLE customer ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE;\n-- Alternative: OPTIMIZE TABLE customer;","note":"LOCK=NONE은 동시 쓰기를 허용한다. 모든 ALTER에서 INPLACE가 가능한 것은 아니다.","danger":"caution"},"mssql":{"command":"-- Rebuild for high fragmentation:\nALTER INDEX ALL ON dbo.customer REBUILD WITH (ONLINE = ON);\n-- Lighter option for low fragmentation:\n-- ALTER INDEX ALL ON dbo.customer REORGANIZE;","note":"일반적인 기준: 단편화 5-30%는 REORGANIZE, >30%는 REBUILD. ONLINE은 Enterprise 기능이다.","danger":"caution"},"mongodb":{"command":"db.customer.reIndex()","note":"모든 인덱스를 포그라운드에서 재구축하며 컬렉션에 락을 건다. 6.0+에서는 standalone에서만 실행된다(replica set 멤버에서는 허용되지 않음). replica set에서는 노드별로 순차(rolling) dropIndex + createIndex로 단일 인덱스를 재구축한다.","danger":"danger"},"redis":{"command":"# Not applicable (core). If RediSearch is used:\n# FT.ALTER / rebuilding is done per the module documentation.","note":"키-값 모델에는 전통적인 인덱스 유지보수가 없다.","danger":"safe"}}},{"id":"reclaim-space","title":"여유 공간 회수(shrink / vacuum / compact)","aliases":["reclaim space","vacuum","shrink","compact","free up space","clean up bloat","bloat"],"engines":{"oracle":{"command":"ALTER TABLE customer ENABLE ROW MOVEMENT;\nALTER TABLE customer SHRINK SPACE;","note":"SHRINK는 high water mark(HWM)를 낮춘다. 인덱스도 축소하려면 SHRINK SPACE CASCADE를 사용한다.","danger":"caution"},"postgres":{"command":"-- Online, light locking (does not return space to the OS):\nVACUUM (VERBOSE, ANALYZE) customer;\n-- Return space to the OS (locks the table!):\n-- VACUUM FULL customer;","note":"VACUUM FULL은 AccessExclusiveLock을 잡고 테이블을 다시 쓴다. 피크 시간에는 실행하지 않는다. 대안: pg_repack.","danger":"caution"},"mysql":{"command":"OPTIMIZE TABLE customer;","note":"InnoDB에서는 테이블을 재구축하고 data_free 공간을 반환한다. 대형 테이블에서는 오래 걸린다.","danger":"caution"},"mssql":{"command":"-- Generally NOT RECOMMENDED (causes fragmentation). If really needed:\nDBCC SHRINKFILE (N'MyDB_data', 1024);  -- target 1024 MB","note":"데이터 파일을 축소하면 인덱스 단편화가 발생하므로 이후 인덱스를 REBUILD한다. 로그는 먼저 백업한다.","danger":"danger"},"mongodb":{"command":"db.runCommand({ compact: \"customer\" })","note":"WiredTiger에서는 여유 공간을 운영 체제에 반환하려고 시도한다(항상 가능하지는 않음). 4.4+에서는 읽기/쓰기를 블로킹하지 않고 같은 컬렉션의 일부 DDL 작업만 블로킹한다. IO 부하가 크므로 secondary에서 유지보수 시간에 실행하는 것이 안전하다.","danger":"danger"},"redis":{"command":"MEMORY PURGE\n# Tries to return jemalloc fragmentation to the OS","note":"activedefrag yes 설정은 온라인 자동 조각 모음을 수행한다. MEMORY DOCTOR는 권장 사항을 제시한다.","danger":"caution"}}},{"id":"version-uptime","title":"버전 및 가동 시간 조회","aliases":["version","uptime","server version","how long has it been running","server version","days since startup"],"engines":{"oracle":{"command":"SELECT banner_full FROM v$version;\nSELECT startup_time, ROUND(SYSDATE - startup_time, 1) AS up_days\nFROM v$instance;","note":"v$instance는 status, host_name 같은 인스턴스 정보도 제공한다.","danger":"safe"},"postgres":{"command":"SELECT version();\nSELECT pg_postmaster_start_time() AS started,\n       now() - pg_postmaster_start_time() AS uptime;","note":"server_version_num은 숫자 비교에 편리하다(예: 160002 = 16.2).","danger":"safe"},"mysql":{"command":"SELECT VERSION();\nSHOW GLOBAL STATUS LIKE 'Uptime';  -- seconds","note":"Uptime은 초 단위이며, 86400으로 나누면 일 수가 된다.","danger":"safe"},"mssql":{"command":"SELECT @@VERSION;\nSELECT sqlserver_start_time,\n       DATEDIFF(DAY, sqlserver_start_time, GETDATE()) AS up_days\nFROM sys.dm_os_sys_info;","note":"SERVERPROPERTY('ProductVersion')과 ('Edition')은 버전/에디션 정보를 제공한다.","danger":"safe"},"mongodb":{"command":"db.version()\ndb.serverStatus().uptime   // seconds","note":"serverStatus()에는 host, process, connections 정보도 포함된다.","danger":"safe"},"redis":{"command":"INFO server\n# redis_version and uptime_in_seconds / uptime_in_days lines","note":"INFO server는 os, run_id, tcp_port 정보도 제공한다.","danger":"safe"}}},{"id":"show-config-param","title":"설정 파라미터 조회","aliases":["show config","parameter","show setting","config parameter","setting","memory setting","variable"],"engines":{"oracle":{"command":"SHOW PARAMETER sga_target;\n-- or: SELECT name, value, isdefault FROM v$parameter WHERE name = 'sga_target';","note":"변경: ALTER SYSTEM SET ... SCOPE=BOTH;(일부는 SCOPE=SPFILE + 재시작만 가능).","danger":"safe"},"postgres":{"command":"SHOW shared_buffers;\n-- Details: SELECT name, setting, unit, context FROM pg_settings WHERE name = 'shared_buffers';","note":"context 컬럼은 변경 시 재시작이 필요한지 reload(SELECT pg_reload_conf())로 충분한지 알려 준다.","danger":"safe"},"mysql":{"command":"SHOW VARIABLES LIKE 'innodb_buffer_pool_size';","note":"GLOBAL과 SESSION의 구분에 유의한다. SHOW GLOBAL VARIABLES는 서버 전체 값을 보여 준다.","danger":"safe"},"mssql":{"command":"SELECT name, value, value_in_use, is_dynamic\nFROM sys.configurations\nWHERE name LIKE '%max server memory%';","note":"변경: sp_configure + RECONFIGURE. is_dynamic=0인 옵션은 재시작이 필요하다.","danger":"safe"},"mongodb":{"command":"db.adminCommand({ getParameter: 1, cursorTimeoutMillis: 1 })\n// All startup options: db.adminCommand({ getCmdLineOpts: 1 })","note":"런타임에 변경 가능한 항목은 setParameter를 사용한다. 영구 설정은 mongod.conf 파일에 있다.","danger":"safe"},"redis":{"command":"CONFIG GET maxmemory\n# Wildcard: CONFIG GET max*","note":"런타임 변경은 CONFIG SET, 영구 저장은 CONFIG REWRITE를 사용한다(redis.conf에 기록).","danger":"safe"}}}]},"rosetta/schema.json":{"category":"스키마 및 객체","categoryId":"schema","order":7,"concepts":[{"id":"list-tables","title":"테이블 / 컬렉션 목록","aliases":["list tables","show tables","list all tables","table list","list collections","collections","which tables exist"],"engines":{"oracle":{"command":"SELECT table_name FROM user_tables ORDER BY table_name;\n-- All schemas: SELECT owner, table_name FROM dba_tables ORDER BY owner, table_name;","note":"user_tables는 현재 스키마의 테이블을, all_tables는 접근 가능한 테이블을 반환한다.","danger":"safe"},"postgres":{"command":"SELECT table_schema, table_name\nFROM information_schema.tables\nWHERE table_type = 'BASE TABLE' AND table_schema NOT IN ('pg_catalog','information_schema')\nORDER BY 1, 2;\n-- psql shortcut: \\dt *.*","note":"public 스키마만: WHERE table_schema = 'public'.","danger":"safe"},"mysql":{"command":"SHOW TABLES;\n-- Another schema: SHOW TABLES FROM mydb;","note":"상세 정보: SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema = DATABASE();","danger":"safe"},"mssql":{"command":"SELECT s.name AS schema_name, t.name AS table_name\nFROM sys.tables t\nJOIN sys.schemas s ON t.schema_id = s.schema_id\nORDER BY s.name, t.name;","note":"간단한 방법: SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE';","danger":"safe"},"mongodb":{"command":"show collections\n// or: db.getCollectionNames()","note":"시스템 컬렉션도 보려면: db.getCollectionInfos().","danger":"safe"},"redis":{"command":"SCAN 0 MATCH * COUNT 100\n# Continue with SCAN <cursor> ... using the returned cursor","note":"Redis에는 '테이블'이 없으며 키가 나열된다. 운영 환경에서 KEYS *는 절대 사용하지 않는다(블로킹 발생). SCAN은 커서 기반이다.","danger":"caution"}}},{"id":"describe-table","title":"테이블 컬럼 / 구조 조회(describe)","aliases":["describe","desc table","columns","table columns","table structure","show schema","field types","fields"],"engines":{"oracle":{"command":"SELECT column_name, data_type, data_length, nullable\nFROM user_tab_columns\nWHERE table_name = 'EMPLOYEE'\nORDER BY column_id;\n-- Shortcut (SQL*Plus): DESC employee","note":"테이블 이름은 대문자로 저장된다(따옴표 없이 생성한 경우).","danger":"safe"},"postgres":{"command":"SELECT column_name, data_type, is_nullable, column_default\nFROM information_schema.columns\nWHERE table_name = 'customer'\nORDER BY ordinal_position;\n-- psql shortcut: \\d customer","note":"\\d는 인덱스, 제약 조건, 외래 키도 보여 준다.","danger":"safe"},"mysql":{"command":"DESCRIBE customer;\n-- or: SHOW COLUMNS FROM customer;","note":"전체 정의는 SHOW CREATE TABLE customer;를 사용한다(아래 'DDL 생성' 항목 참고).","danger":"safe"},"mssql":{"command":"SELECT c.name, t.name AS type, c.max_length, c.is_nullable\nFROM sys.columns c\nJOIN sys.types t ON c.user_type_id = t.user_type_id\nWHERE c.object_id = OBJECT_ID('dbo.customer')\nORDER BY c.column_id;\n-- Shortcut: EXEC sp_help 'dbo.customer';","note":"sp_help는 컬럼 외에 인덱스와 제약 조건도 나열한다.","danger":"safe"},"mongodb":{"command":"// The schema is flexible; infer it from a sample document:\ndb.customer.findOne()\n// For field coverage: db.customer.aggregate([{ $sample: { size: 100 } }])","note":"MongoDB에는 고정 스키마가 없으며 필드는 문서마다 다를 수 있다. $jsonSchema 검증기가 정의되어 있으면 db.getCollectionInfos로 볼 수 있다.","danger":"safe"},"redis":{"command":"TYPE mykey        # string | list | set | hash | zset | stream\nOBJECT ENCODING mykey   # internal encoding","note":"고정 스키마가 없으며 각 키의 타입과 인코딩을 개별적으로 조회한다. 해시 필드는 HGETALL을 사용한다.","danger":"safe"}}},{"id":"list-indexes","title":"테이블 인덱스 목록","aliases":["list indexes","show indexes","show table indexes","which indexes","table indexes","getindexes"],"engines":{"oracle":{"command":"SELECT index_name, column_name, column_position\nFROM user_ind_columns\nWHERE table_name = 'EMPLOYEE'\nORDER BY index_name, column_position;","note":"인덱스 타입/유일성은 user_indexes와 조인한다.","danger":"safe"},"postgres":{"command":"SELECT indexname, indexdef\nFROM pg_indexes\nWHERE tablename = 'customer';","note":"indexdef는 전체 CREATE INDEX 문을 보여 준다. psql의 \\d customer로도 나열된다.","danger":"safe"},"mysql":{"command":"SHOW INDEX FROM customer;","note":"Non_unique, Seq_in_index, Cardinality 컬럼은 인덱스 구조와 선택도를 보여 준다.","danger":"safe"},"mssql":{"command":"EXEC sp_helpindex 'dbo.customer';\n-- Details: join sys.indexes + sys.index_columns","note":"sp_helpindex는 인덱스 이름, 타입, 컬럼을 반환한다. 필터링/포함 컬럼은 sys 뷰를 사용한다.","danger":"safe"},"mongodb":{"command":"db.customer.getIndexes()","note":"각 인덱스의 키와 유일성, TTL/partial 같은 옵션을 반환한다.","danger":"safe"},"redis":{"command":"# Not applicable (core). If the RediSearch module is present:\n# FT._LIST        (all indexes)\n# FT.INFO idx     (details of one index)","note":"키-값 모델에는 보조 인덱스가 없으며, RediSearch/RedisJSON 같은 모듈에서만 제공된다.","danger":"safe"}}},{"id":"create-index","title":"인덱스 생성","aliases":["create index","add index","build index","new index","createindex"],"engines":{"oracle":{"command":"CREATE INDEX customer_city_idx ON customer(city) ONLINE;","note":"ONLINE은 테이블 락 없이 생성한다. 유일 인덱스는 CREATE UNIQUE INDEX, 복합 인덱스는 (a, b)를 사용한다.","danger":"caution"},"postgres":{"command":"CREATE INDEX CONCURRENTLY customer_city_idx ON customer(city);","note":"CONCURRENTLY는 테이블 쓰기를 블로킹하지 않지만 트랜잭션 블록 안에서는 실행할 수 없고 더 느리다.","danger":"caution"},"mysql":{"command":"CREATE INDEX customer_city_idx ON customer(city);\n-- Lock-free: ... ALGORITHM=INPLACE, LOCK=NONE;","note":"InnoDB는 대부분의 인덱스를 온라인으로 추가한다. 대형 테이블에서는 LOCK=NONE으로 쓰기를 허용한다.","danger":"caution"},"mssql":{"command":"CREATE NONCLUSTERED INDEX IX_customer_city\nON dbo.customer (city)\nWITH (ONLINE = ON);","note":"ONLINE은 Enterprise 기능이다. 커버링 컬럼은 INCLUDE (col1, col2)를 사용한다.","danger":"caution"},"mongodb":{"command":"db.customer.createIndex({ city: 1 })\n// Unique: { city: 1 }, { unique: true }\n// Built in the background without blocking (default behavior in 4.2+)","note":"1은 오름차순, -1은 내림차순이다. 대형 컬렉션에서는 secondary에서 먼저 생성하면 부하가 줄어든다.","danger":"caution"},"redis":{"command":"# Not applicable (core). If RediSearch is present:\n# FT.CREATE idx ON HASH PREFIX 1 customer: SCHEMA city TAG","note":"보조 인덱스는 RediSearch 모듈에서만 제공된다. 기본 Redis는 키로 접근한다.","danger":"safe"}}},{"id":"show-ddl","title":"객체 DDL(CREATE 문) 생성","aliases":["show ddl","get ddl","create statement","generate ddl","table definition","show create table","generate script"],"engines":{"oracle":{"command":"SELECT DBMS_METADATA.GET_DDL('TABLE', 'EMPLOYEE', 'HR') FROM dual;","note":"인덱스는 'INDEX', 패키지는 'PACKAGE' 등을 사용한다. SET LONG 100000으로 긴 출력이 잘리지 않게 한다.","danger":"safe"},"postgres":{"command":"-- There is no single function in SQL; from the shell:\npg_dump -t public.customer --schema-only mydb","note":"psql에서 \\d customer는 구조를 읽기 쉽게 보여 준다. 전체 DDL은 pg_dump --schema-only를 사용한다.","danger":"safe"},"mysql":{"command":"SHOW CREATE TABLE customer\\G","note":"전체 CREATE TABLE(컬럼, 인덱스, 엔진, charset)을 단일 명령으로 반환한다.","danger":"safe"},"mssql":{"command":"-- T-SQL has no single built-in command. Options:\n-- 1) SSMS: right-click > Script Table as > CREATE To\n-- 2) EXEC sp_help 'dbo.customer';  (structure summary)\n-- 3) mssql-scripter tool (command line)","note":"프로시저/뷰 본문: SELECT OBJECT_DEFINITION(OBJECT_ID('dbo.usp_name'));","danger":"safe"},"mongodb":{"command":"// There is no DDL (schemaless). View collection options/validator:\ndb.getCollectionInfos({ name: \"customer\" })","note":"options.validator($jsonSchema)가 정의되어 있으면 그것이 유일한 '스키마 정의'다. 인덱스는 getIndexes()로 별도 조회한다.","danger":"safe"},"redis":{"command":"# Not applicable: there is no schema/DDL concept.","note":"데이터 구조는 애플리케이션 코드에서 정의되며, 영구 저장은 RDB/AOF 파일에 이루어진다.","danger":"safe"}}}]},"rosetta/replication.json":{"category":"복제 및 HA","categoryId":"replication","order":8,"concepts":[{"id":"replication-status","title":"복제 상태 및 지연 조회","aliases":["replication status","replica lag","replication status","lag","slave lag","standby lag","how far behind"],"engines":{"oracle":{"command":"-- Role and mode:\nSELECT database_role, open_mode FROM v$database;\n-- Data Guard lag:\nSELECT name, value, unit FROM v$dataguard_stats\nWHERE name LIKE '%lag%';","note":"apply lag와 transport lag는 Data Guard의 주요 지연 지표다. standby에서 실행한다.","danger":"safe"},"postgres":{"command":"-- On the PRIMARY:\nSELECT client_addr, state, sync_state,\n       write_lag, flush_lag, replay_lag\nFROM pg_stat_replication;\n-- On the STANDBY:\nSELECT now() - pg_last_xact_replay_timestamp() AS replay_lag;","note":"replay_lag는 재적용 지연이다. sync_state = 'sync'는 동기 replica를 뜻한다.","danger":"safe"},"mysql":{"command":"SHOW REPLICA STATUS\\G\n-- Before 8.0.22: SHOW SLAVE STATUS\\G","note":"Seconds_Behind_Source가 지연을 보여 주며, Replica_IO_Running과 Replica_SQL_Running은 'Yes'여야 한다. SHOW SLAVE STATUS는 8.0.22–8.3에서 별칭이며 8.4에서 제거되었다.","danger":"safe"},"mssql":{"command":"SELECT ar.replica_server_name, drs.synchronization_state_desc,\n       drs.log_send_queue_size, drs.redo_queue_size\nFROM sys.dm_hadr_database_replica_states drs\nJOIN sys.availability_replicas ar ON drs.replica_id = ar.replica_id;","note":"Always On용. redo_queue_size가 크면 secondary replica가 뒤처지고 있다는 뜻이다.","danger":"safe"},"mongodb":{"command":"rs.printSecondaryReplicationInfo()\n// Detailed status: rs.status()","note":"각 secondary가 primary보다 몇 초 뒤처져 있는지 보여 준다. optimeDate 값을 비교한다.","danger":"safe"},"redis":{"command":"INFO replication\n# Check the role, connected_slaves, master_repl_offset,\n# and slaveN:...,offset=... lines","note":"master_repl_offset과 slave offset의 차이가 지연이다. master_link_status는 'up'이어야 한다.","danger":"safe"}}},{"id":"list-replicas","title":"replica / 클러스터 노드 목록","aliases":["list replicas","show replicas","list replicas","nodes","cluster members","slaves","standby list"],"engines":{"oracle":{"command":"-- Standby destinations:\nSELECT dest_id, destination, status, target\nFROM v$archive_dest\nWHERE status <> 'INACTIVE';\n-- RAC instances: SELECT inst_id, instance_name, status FROM gv$instance;","note":"Data Guard 구성은 DGMGRL> SHOW CONFIGURATION으로도 볼 수 있다.","danger":"safe"},"postgres":{"command":"SELECT application_name, client_addr, state, sync_state\nFROM pg_stat_replication;","note":"PRIMARY에서만 연결된 replica를 나열한다. 각 행이 하나의 standby다.","danger":"safe"},"mysql":{"command":"SHOW REPLICAS;\n-- Before 8.0.22: SHOW SLAVE HOSTS;","note":"소스 서버에서 실행한다. report-host가 설정된 replica를 보여 준다. SHOW SLAVE HOSTS는 8.0.22–8.3에서 별칭이며 8.4에서 제거되었다.","danger":"safe"},"mssql":{"command":"SELECT ar.replica_server_name, ars.role_desc,\n       ars.connected_state_desc, ars.synchronization_health_desc\nFROM sys.dm_hadr_availability_replica_states ars\nJOIN sys.availability_replicas ar ON ars.replica_id = ar.replica_id;","note":"role_desc는 PRIMARY/SECONDARY를 구분하고, synchronization_health_desc는 전반적인 상태를 보여 준다.","danger":"safe"},"mongodb":{"command":"rs.status().members.map(m => ({ name: m.name, state: m.stateStr, health: m.health }))","note":"stateStr은 PRIMARY/SECONDARY/ARBITER를 반환한다. health 1은 정상을 뜻한다.","danger":"safe"},"redis":{"command":"INFO replication\n# If Sentinel is used:\n# SENTINEL replicas <master-name>","note":"slave0, slave1... 줄은 각 replica의 ip/port/offset을 보여 준다.","danger":"safe"}}},{"id":"promote-failover","title":"replica 승격 / 페일오버(promote)","aliases":["promote","failover","switchover","promote","role change","stepdown","make master","make primary"],"engines":{"oracle":{"command":"-- Data Guard broker (DGMGRL):\nSWITCHOVER TO 'standby_db';     -- planned, no data loss\n-- FAILOVER TO 'standby_db';     -- on loss of the primary, emergency","note":"SWITCHOVER는 계획된 역할 전환이고, FAILOVER는 primary에 접근할 수 없을 때 수행하며 데이터 손실 위험이 있다.","danger":"danger"},"postgres":{"command":"-- Promote the standby to primary:\nSELECT pg_promote();\n-- or from the shell: pg_ctl promote -D /var/lib/pgsql/data","note":"승격 전에 기존 primary가 실제로 중지되었는지 확인한다(split-brain 위험). 승격 후 기존 primary를 새 primary의 replica로 다시 합류시키려면 pg_rewind가 필요하며, 이를 위해 wal_log_hints = on 또는 데이터 체크섬이 활성화되어 있어야 한다.","danger":"danger"},"mysql":{"command":"-- On the replica (manual):\n-- 1) Stop receiving new events, wait until the received relay log is fully applied:\nSTOP REPLICA IO_THREAD;\nSELECT RECEIVED_TRANSACTION_SET FROM performance_schema.replication_connection_status;\nSELECT @@GLOBAL.gtid_executed;  -- the received set must be fully contained here\n-- 2) Remove replication and enable writes:\nSTOP REPLICA;\nRESET REPLICA ALL;\nSET GLOBAL read_only = OFF;  -- also turns off super_read_only\n-- 3) Fence the old source against writes, then redirect the application.","note":"relay log가 모두 적용되기 전에 RESET REPLICA ALL을 실행하면 수신했지만 적용되지 않은 트랜잭션이 손실된다. replica는 보통 read_only/super_read_only가 활성화된 상태로 운영되며, 이를 끄기 전까지 쓰기가 허용되지 않는다. 기존 소스가 여전히 쓰기 가능하면 split-brain 위험이 있다. 이 구문은 8.0.22+용이다(이전에는 STOP SLAVE / RESET SLAVE ALL, 옛 형식은 8.4에서 제거됨). 운영 환경에서 자동적이고 안전한 페일오버를 원하면 InnoDB Cluster/Group Replication, Orchestrator 또는 MHA를 사용한다.","danger":"danger"},"mssql":{"command":"-- On the target secondary replica (synchronous):\nALTER AVAILABILITY GROUP [AG_Name] FAILOVER;\n-- Force it, with risk of data loss:\n-- ALTER AVAILABILITY GROUP [AG_Name] FORCE_FAILOVER_ALLOW_DATA_LOSS;","note":"동기 replica로의 계획된 페일오버는 데이터 손실이 없다. FORCE는 재해 상황에서만 사용한다.","danger":"danger"},"mongodb":{"command":"// Trigger an election on the current primary:\nrs.stepDown(60)   // step down from primary for 60 s","note":"stepDown은 새 선출을 시작한다. 특정 노드를 우선시하려면 멤버의 priority 값을 조정한다.","danger":"danger"},"redis":{"command":"# Make the replica an independent primary:\nREPLICAOF NO ONE\n# With Sentinel (recommended): SENTINEL FAILOVER <master-name>","note":"Sentinel/Cluster를 사용한다면 수동 REPLICAOF 대신 SENTINEL FAILOVER를 사용한다. 그렇지 않으면 split-brain 위험이 있다.","danger":"danger"}}},{"id":"open-transactions","title":"열린 / 장시간 실행 트랜잭션 찾기","aliases":["open transactions","long running transaction","open transaction","long transaction","idle in transaction","oldest transaction","undo"],"engines":{"oracle":{"command":"SELECT s.sid, s.serial#, s.username,\n       t.start_time, t.used_ublk AS undo_blocks\nFROM v$transaction t\nJOIN v$session s ON t.ses_addr = s.saddr\nORDER BY t.start_time;","note":"used_ublk가 높으면 대형 트랜잭션이 undo를 많이 소비하고 있다는 뜻이며, commit/rollback을 기다리는 중일 수 있다.","danger":"safe"},"postgres":{"command":"SELECT pid, usename, state,\n       now() - xact_start AS xact_age, query\nFROM pg_stat_activity\nWHERE xact_start IS NOT NULL\nORDER BY xact_start;","note":"'idle in transaction' 상태의 장시간 트랜잭션은 VACUUM과 튜플 정리를 막으므로 모니터링한다.","danger":"safe"},"mysql":{"command":"SELECT trx_id, trx_state, trx_started,\n       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS age_sec,\n       trx_mysql_thread_id\nFROM information_schema.innodb_trx\nORDER BY trx_started;","note":"trx_mysql_thread_id로 PROCESSLIST와 연관 짓고, 필요하면 KILL한다.","danger":"safe"},"mssql":{"command":"DBCC OPENTRAN;\n-- Detailed:\nSELECT st.session_id, dt.database_transaction_begin_time\nFROM sys.dm_tran_database_transactions dt\nJOIN sys.dm_tran_session_transactions st ON st.transaction_id = dt.transaction_id\nWHERE dt.database_id = DB_ID()\nORDER BY dt.database_transaction_begin_time;","note":"장시간 열린 트랜잭션은 로그 truncation을 막아 로그 파일을 비대하게 만든다.","danger":"safe"},"mongodb":{"command":"db.adminCommand({\n  currentOp: 1,\n  \"transaction\": { $exists: true }\n})","note":"장시간 실행되는 다중 문서 트랜잭션은 transactionLifetimeLimitSeconds(기본값 60 s) 후 자동으로 중단된다.","danger":"safe"},"redis":{"command":"# Not applicable: MULTI/EXEC transactions are short and atomic; they do not stay open.","note":"Redis 트랜잭션은 락을 잡지 않는다. 장시간 실행되는 단일 명령은 SLOWLOG를 확인한다.","danger":"safe"}}}]},"rosetta/connect.json":{"category":"연결 및 CLI","categoryId":"connect","order":9,"concepts":[{"id":"connect-cli","title":"명령줄에서 접속(CLI / 연결 문자열)","aliases":["connect","connection string","connect","how do i connect","cli connect","login","dsn","uri"],"engines":{"oracle":{"command":"sqlplus app_user/password@//host:1521/service_name\n# As SYSDBA: sqlplus sys/password@//host:1521/orcl AS SYSDBA","note":"//host:port/service는 'Easy Connect' 형식이다. 또는 tnsnames.ora의 TNS 별칭을 사용한다.","danger":"safe"},"postgres":{"command":"psql -h host -p 5432 -U postgres -d mydb\n# With a URI: psql \"postgresql://app_user:password@host:5432/mydb?sslmode=require\"","note":"명령줄에 비밀번호를 입력하지 않으려면 ~/.pgpass 파일이나 PGPASSWORD 환경 변수를 사용한다.","danger":"safe"},"mysql":{"command":"mysql -h host -P 3306 -u root -p mydb\n# Require TLS: --ssl-mode=REQUIRED","note":"-p만 단독으로 지정한다(비밀번호 입력 프롬프트가 표시됨). 'mysql -pPassword' 형식은 프로세스 목록에 비밀번호가 노출된다.","danger":"safe"},"mssql":{"command":"sqlcmd -S host,1433 -U sa -P password -d mydb\n# Windows authentication: sqlcmd -S host -E -d mydb","note":"새 go-sqlcmd에서는 TLS를 위해 -C(서버 인증서 신뢰)와 -N(암호화) 플래그가 필요할 수 있다.","danger":"safe"},"mongodb":{"command":"mongosh \"mongodb://app_user:password@host:27017/mydb?authSource=admin\"\n# Replica set: ...mongodb://h1,h2,h3/db?replicaSet=rs0","note":"authSource는 보통 사용자가 정의된 데이터베이스다(대부분 admin).","danger":"safe"},"redis":{"command":"redis-cli -h host -p 6379 -a password\n# With TLS: redis-cli -h host -p 6379 --tls -a password\n# Redis 6+ ACL: redis-cli -h host --user app --pass password","note":"-a는 비밀번호를 명령줄로 전달한다(경고 출력). 안전한 로그인에는 REDISCLI_AUTH 환경 변수를 사용한다.","danger":"safe"}}},{"id":"list-databases","title":"데이터베이스 / 스키마 목록","aliases":["list databases","show databases","list databases","which databases","list schemas","pdbs","show dbs"],"engines":{"oracle":{"command":"-- Schemas (users):\nSELECT username FROM dba_users ORDER BY username;\n-- Multitenant (CDB) PDBs:\nSELECT name, open_mode FROM v$pdbs;","note":"Oracle에서 '데이터베이스'는 인스턴스에 해당한다. 구분은 스키마(사용자) 수준 또는 CDB 내 PDB 수준에서 이루어진다.","danger":"safe"},"postgres":{"command":"SELECT datname FROM pg_database WHERE datistemplate = false;\n-- psql shortcut: \\l","note":"스키마: SELECT schema_name FROM information_schema.schemata;","danger":"safe"},"mysql":{"command":"SHOW DATABASES;","note":"MySQL에서 'database'와 'schema'는 동의어다.","danger":"safe"},"mssql":{"command":"SELECT name, state_desc FROM sys.databases ORDER BY name;","note":"state_desc는 ONLINE/OFFLINE/RESTORING 같은 데이터베이스 상태를 보여 준다.","danger":"safe"},"mongodb":{"command":"show dbs\n// or: db.adminCommand({ listDatabases: 1 })","note":"빈 데이터베이스(아직 데이터가 기록되지 않은 경우)는 목록에 나타나지 않는다.","danger":"safe"},"redis":{"command":"INFO keyspace\n# db0:keys=..., db1:keys=... lines show the logical DBs in use","note":"Redis에는 번호가 붙은 논리 DB가 있다(기본 0-15). Cluster 모드에서는 DB 0만 사용한다.","danger":"safe"}}},{"id":"switch-database","title":"활성 데이터베이스 / 스키마 변경","aliases":["switch database","use database","change database","switch","change schema","change schema","use db"],"engines":{"oracle":{"command":"-- Change the default schema:\nALTER SESSION SET CURRENT_SCHEMA = hr;\n-- Switch to a PDB (inside a CDB):\nALTER SESSION SET CONTAINER = pdb1;","note":"CURRENT_SCHEMA는 객체 이름 해석만 바꾸며 권한은 바꾸지 않는다.","danger":"safe"},"postgres":{"command":"-- Switch to another DB inside psql (reconnects):\n\\c mydb\n-- Schema search path (same connection):\nSET search_path TO hr, public;","note":"PostgreSQL에서 연결은 단일 데이터베이스에 묶이므로, DB를 바꾸려면 다시 접속해야 한다.","danger":"safe"},"mysql":{"command":"USE mydb;","note":"연결을 끊지 않고 활성 스키마를 변경한다.","danger":"safe"},"mssql":{"command":"USE mydb;","note":"배치 컨텍스트를 변경하며, 이후 명령은 이 데이터베이스에서 실행된다.","danger":"safe"},"mongodb":{"command":"use mydb","note":"데이터베이스가 없어도 전환된다. 실제 생성은 첫 문서가 기록될 때 이루어진다.","danger":"safe"},"redis":{"command":"SELECT 1\n# Switches to a logical DB index between 0 and 15","note":"Cluster 모드에서 SELECT는 0만 지원한다. 논리 DB 구분 대신 키 접두사를 사용한다.","danger":"safe"}}}]},"rosetta/objects.json":{"category":"객체 관리","categoryId":"objects","order":10,"concepts":[{"id":"create-partition","title":"파티션 생성 / 분할 / 삭제","aliases":["partition","partition","split","split partition","drop partition","add partition","remove partition"],"engines":{"oracle":{"command":"-- Add a RANGE partition\nALTER TABLE sales ADD PARTITION p2026 VALUES LESS THAN (TO_DATE('2027-01-01','YYYY-MM-DD'));\n\n-- Split an existing partition\nALTER TABLE sales SPLIT PARTITION pmax AT (TO_DATE('2027-01-01','YYYY-MM-DD')) INTO (PARTITION p2026, PARTITION pmax);\n\n-- Drop a partition\nALTER TABLE sales DROP PARTITION p2020 UPDATE INDEXES;","note":"RANGE/LIST/HASH 파티셔닝을 지원한다. DROP PARTITION은 해당 파티션의 모든 행을 삭제하며, UPDATE INDEXES를 지정하면 글로벌 인덱스가 유효하게 유지된다. Interval 파티셔닝은 파티션을 자동으로 추가할 수 있다.","danger":"caution","priv":"테이블에 대한 ALTER","minver":"Partitioning Option"},"postgres":{"command":"-- Declarative partitioning: parent table\nCREATE TABLE sales (id bigint, created_at date, amount numeric) PARTITION BY RANGE (created_at);\n\n-- Add a new partition\nCREATE TABLE sales_2026 PARTITION OF sales FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');\n\n-- Attach a standalone (existing) table as a partition\nCREATE TABLE sales_2027 (LIKE sales INCLUDING ALL);\nALTER TABLE sales ATTACH PARTITION sales_2027 FOR VALUES FROM ('2027-01-01') TO ('2028-01-01');\n\n-- Detach a partition (the table is not dropped; it remains a standalone table)\nALTER TABLE sales DETACH PARTITION sales_2026;","note":"기본 PostgreSQL에는 SPLIT/MERGE PARTITION이 없다(PG17 기준). 분할하려면 파티션을 DETACH하고, 새 경계로 파티션 두 개를 만든 뒤 데이터를 옮기고 ATTACH한다. DETACH ... CONCURRENTLY(PG14+)는 락 유지 시간을 줄인다.","danger":"caution","priv":"테이블 소유자","minver":"PG10"},"mysql":{"command":"-- Partitioned table\nCREATE TABLE sales (id BIGINT, created_at DATE) PARTITION BY RANGE (YEAR(created_at)) (\n  PARTITION p2024 VALUES LESS THAN (2025),\n  PARTITION p2025 VALUES LESS THAN (2026),\n  PARTITION pmax VALUES LESS THAN MAXVALUE\n);\n\n-- Add / split / drop a partition\nALTER TABLE sales REORGANIZE PARTITION pmax INTO (\n  PARTITION p2026 VALUES LESS THAN (2027),\n  PARTITION pmax VALUES LESS THAN MAXVALUE\n);\nALTER TABLE sales DROP PARTITION p2024;","note":"파티셔닝 컬럼은 모든 PRIMARY KEY / UNIQUE 키에 포함되어야 한다. RANGE의 분할/병합은 REORGANIZE PARTITION으로 수행한다. DROP PARTITION은 행도 함께 삭제한다(RANGE/LIST).","danger":"caution","priv":"테이블에 대한 ALTER, DROP","minver":"5.7"},"mssql":{"command":"-- 1) Partition function and scheme\nCREATE PARTITION FUNCTION pf_date (date) AS RANGE RIGHT FOR VALUES ('2025-01-01','2026-01-01');\nCREATE PARTITION SCHEME ps_date AS PARTITION pf_date ALL TO ([PRIMARY]);\n\n-- 2) Add a new boundary (split)\nALTER PARTITION SCHEME ps_date NEXT USED [PRIMARY];\nALTER PARTITION FUNCTION pf_date() SPLIT RANGE ('2027-01-01');\n\n-- 3) Remove a boundary (merge)\nALTER PARTITION FUNCTION pf_date() MERGE RANGE ('2025-01-01');","note":"partition function + scheme을 정의하고, CREATE TABLE ... ON ps_date(created_at)로 해당 scheme 위에 테이블을 생성한다. SPLIT 전에 NEXT USED 파일 그룹을 지정해야 한다. 데이터를 제거하려면 SWITCH PARTITION으로 스테이징 테이블로 전환한다.","danger":"caution","priv":"테이블에 대한 ALTER, ALTER ANY DATASPACE","minver":"2016 SP1(모든 에디션)"},"mongodb":{"command":"// MongoDB uses sharding instead of table partitioning\nsh.shardCollection(\"db.sales\", { created_at: 1 });\n\n// Native time-series collection for time series\ndb.createCollection(\"measurements\", { timeseries: { timeField: \"ts\", granularity: \"hours\" } });","note":"고전적 의미의 파티셔닝은 없다. 수평 분할은 샤딩을 통해 chunk 단위로 이루어진다. 시간 기반 데이터는 time-series 컬렉션이 자동 버킷팅을 제공한다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no table/partition concept; scaling is done with Redis Cluster hash slots (16384).","note":"Redis Cluster는 키를 16384개의 해시 슬롯에 분산한다. 이는 테이블 파티셔닝이 아니라 애플리케이션 수준의 분할이며, 수동 파티션 명령은 없다.","danger":"safe"},"elasticsearch":{"command":"# Time-based automatic splitting (rollover) with an ILM policy + data stream\nPUT _ilm/policy/logs-policy\n{ \"policy\": { \"phases\": { \"hot\": { \"actions\": { \"rollover\": { \"max_size\": \"50gb\", \"max_age\": \"30d\" } } } } } }\n\nPUT _index_template/logs-tmpl\n{ \"index_patterns\": [\"logs*\"], \"data_stream\": {}, \"template\": { \"settings\": { \"index.lifecycle.name\": \"logs-policy\" } } }","note":"Elasticsearch에서는 인덱스를 샤드로 나누고, 시계열은 ILM + data stream으로 새 backing index를 생성(rollover)하여 분할한다. 오래된 인덱스는 ILM으로 삭제할 수 있다.","danger":"safe"}}},{"id":"sequence","title":"시퀀스 / 자동 증가 값 생성","aliases":["sequence","sequence","auto increment","identity","auto-increment","nextval","serial"],"engines":{"oracle":{"command":"-- Sequence\nCREATE SEQUENCE customer_seq START WITH 1 INCREMENT BY 1 NOCACHE;\nSELECT customer_seq.NEXTVAL FROM dual;\n\n-- or an identity column (12c+)\nCREATE TABLE customer (id NUMBER GENERATED ALWAYS AS IDENTITY, name VARCHAR2(100));","note":"고전적 방법은 CREATE SEQUENCE + NEXTVAL/CURRVAL이다. 12c부터 GENERATED AS IDENTITY 컬럼도 지원한다. CACHE 값은 성능을 높이지만 번호 공백이 생길 수 있다.","danger":"safe","priv":"CREATE SEQUENCE","minver":"IDENTITY는 12c"},"postgres":{"command":"-- Sequence\nCREATE SEQUENCE customer_seq START 1 INCREMENT 1;\nSELECT nextval('customer_seq');\n\n-- Modern method: identity column\nCREATE TABLE customer (id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text);\n-- Classic: serial (creates a sequence behind the scenes)\nCREATE TABLE logs (id serial PRIMARY KEY, message text);","note":"GENERATED ... AS IDENTITY는 SQL 표준이며 권장된다. serial은 레거시 약식 표기다. nextval/currval로 수동 접근한다. setval로 값을 재설정할 수 있다.","danger":"safe","priv":"스키마에 대한 CREATE","minver":"IDENTITY는 PG10"},"mysql":{"command":"-- AUTO_INCREMENT column\nCREATE TABLE customer (id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100));\n\n-- Set the starting value\nALTER TABLE customer AUTO_INCREMENT = 1000;\n\n-- Get the last inserted value\nSELECT LAST_INSERT_ID();","note":"MySQL에는 독립된(표준) 시퀀스 객체가 없으며 AUTO_INCREMENT 컬럼을 사용한다(테이블당 하나, 인덱스 필수). MariaDB 10.3+는 CREATE SEQUENCE를 지원한다.","danger":"safe","priv":"테이블에 대한 CREATE, ALTER"},"mssql":{"command":"-- IDENTITY column\nCREATE TABLE customer (id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(100));\n\n-- Standalone sequence object\nCREATE SEQUENCE customer_seq AS INT START WITH 1 INCREMENT BY 1;\nSELECT NEXT VALUE FOR customer_seq;","note":"IDENTITY(seed,increment)는 테이블 컬럼에 묶인다. CREATE SEQUENCE(2012+)는 테이블과 독립적이고 여러 테이블에서 공유할 수 있는 카운터를 제공하며, NEXT VALUE FOR로 값을 가져온다.","danger":"safe","priv":"CREATE SEQUENCE / 테이블에 대한 ALTER","minver":"SEQUENCE는 2012"},"mongodb":{"command":"// MongoDB has no auto counter; counter pattern with findOneAndUpdate\ndb.counters.findOneAndUpdate(\n  { _id: \"customer\" },\n  { $inc: { seq: 1 } },\n  { upsert: true, returnDocument: \"after\" }\n);","note":"기본 _id는 ObjectId다(이미 유일하며 대략 증가함). 순차 정수가 필요하면 별도 counters 컬렉션에서 $inc로 원자적으로 생성한다.","danger":"safe"},"redis":{"command":"# Atomic counter\nINCR customer:id\n# or with a specific step\nINCRBY customer:id 10","note":"INCR/INCRBY는 원자적이며 분산 환경에서 유일한 증가 ID를 생성하는 데 적합하다. 키가 없으면 0부터 시작한다.","danger":"safe"},"elasticsearch":{"command":"# Not applicable: Elasticsearch does not offer a sequence object. Document IDs are assigned automatically;\n# the internal _seq_no and _primary_term fields are used for concurrency control.\nPOST logs/_doc\n{ \"message\": \"sample\" }","note":"ID 없이 POST하면 ES가 임의의 유일한 _id를 할당한다(증가하지 않음). _seq_no는 쓰기마다 증가하는 내부 시퀀스 번호지만, 애플리케이션 카운터가 아니라 낙관적 동시성 제어용이다.","danger":"safe"}}},{"id":"add-constraint","title":"제약 조건 추가 또는 비활성화(FK/CHECK/UNIQUE)","aliases":["constraint","constraint","foreign key","check","unique","foreign key","disable constraint"],"engines":{"oracle":{"command":"-- Add a foreign key\nALTER TABLE orders ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customer(id);\n-- CHECK\nALTER TABLE product ADD CONSTRAINT chk_price CHECK (price >= 0);\n-- Disable / enable\nALTER TABLE orders DISABLE CONSTRAINT fk_customer;\nALTER TABLE orders ENABLE CONSTRAINT fk_customer;","note":"Oracle 제약 조건은 DISABLE/ENABLE로 일시적으로 끌 수 있다(대량 적재에 유용). ENABLE VALIDATE는 기존 행을 검사하고, NOVALIDATE는 검사하지 않는다.","danger":"caution","priv":"테이블에 대한 ALTER, 대상에 대한 REFERENCES"},"postgres":{"command":"-- Foreign key and CHECK\nALTER TABLE orders ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customer(id);\nALTER TABLE product ADD CONSTRAINT chk_price CHECK (price >= 0);\n\n-- Instead of disabling the FK: NOT VALID + validate later\nALTER TABLE orders ADD CONSTRAINT fk2 FOREIGN KEY (c_id) REFERENCES customer(id) NOT VALID;\nALTER TABLE orders VALIDATE CONSTRAINT fk2;\nALTER TABLE orders DROP CONSTRAINT fk2;","note":"PostgreSQL에는 제약 조건 DISABLE이 없다. 대형 테이블에서 락을 줄이려면 NOT VALID로 제약 조건을 추가하고 나중에 VALIDATE한다. ALTER TABLE ... DISABLE TRIGGER ALL은 FK의 내부 트리거도 비활성화하며 superuser가 필요하다. 꺼진 동안 삽입된 데이터는 검증되지 않고, 다시 활성화해도 소급 검사하지 않는다(무결성이 조용히 깨질 위험). 대신 제약 조건을 DROP하고 NOT VALID로 다시 추가한 뒤 VALIDATE CONSTRAINT를 실행한다.","danger":"caution","priv":"테이블 소유자, 대상에 대한 REFERENCES"},"mysql":{"command":"-- Foreign key and CHECK\nALTER TABLE orders ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customer(id);\nALTER TABLE product ADD CONSTRAINT chk_price CHECK (price >= 0);\n\n-- Temporarily disable FK checks for the session\nSET FOREIGN_KEY_CHECKS = 0;\n-- ... bulk operation ...\nSET FOREIGN_KEY_CHECKS = 1;","note":"InnoDB가 필요하다. 개별 제약 조건을 비활성화하는 대신 세션 수준의 SET FOREIGN_KEY_CHECKS=0을 사용한다. CHECK 제약 조건은 MySQL 8.0.16부터 실제로 적용된다(이전에는 무시됨).","danger":"caution","priv":"테이블에 대한 ALTER, REFERENCES","minver":"CHECK는 8.0.16"},"mssql":{"command":"-- Foreign key and CHECK\nALTER TABLE orders ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customer(id);\nALTER TABLE product ADD CONSTRAINT chk_price CHECK (price >= 0);\n\n-- Disable / re-enable (validate with WITH CHECK)\nALTER TABLE orders NOCHECK CONSTRAINT fk_customer;\nALTER TABLE orders WITH CHECK CHECK CONSTRAINT fk_customer;","note":"SQL Server에서 FK/CHECK 제약 조건은 NOCHECK CONSTRAINT로 비활성화한다. 다시 활성화할 때 WITH CHECK CHECK CONSTRAINT를 사용하지 않으면 제약 조건이 'not trusted' 상태로 남아 쿼리 옵티마이저가 활용할 수 없다.","danger":"caution","priv":"테이블에 대한 ALTER, 대상에 대한 REFERENCES"},"mongodb":{"command":"// Constraint-like checks with schema validation rules\ndb.runCommand({ collMod: \"product\",\n  validator: { $jsonSchema: { required: [\"price\"], properties: { price: { bsonType: \"number\", minimum: 0 } } } },\n  validationLevel: \"strict\"\n});\n// Unique index for uniqueness\ndb.customer.createIndex({ email: 1 }, { unique: true });","note":"외래 키가 없다(애플리케이션 계층에서 보장). UNIQUE에 해당하는 것은 unique 인덱스다. CHECK 대신 $jsonSchema 검증을 사용하며, validationAction: 'warn'으로 비활성화와 같은 효과를 낸다.","danger":"caution"},"redis":{"command":"# Not applicable: Redis has no FK/CHECK/UNIQUE constraints. Uniqueness is enforced at the application level with SET or\n# SETNX/conditional writes.\nSET customer:email:sample@x.com 1 NX","note":"관계 무결성이나 제약 조건 개념이 없다. 유일성은 애플리케이션 측에서 SET ... NX(없을 때만 쓰기)나 별도 SET/HASH 확인으로 관리한다.","danger":"safe"},"elasticsearch":{"command":"# Not applicable: Elasticsearch has no FK/CHECK. Field types are enforced with the mapping,\n# and uniqueness with a fixed _id.\nPUT product/_doc/ABC-123\n{ \"price\": 100 }","note":"엔진에 관계와 제약 조건이 존재하지 않는다. 타입 검사는 mapping으로 이루어진다. UNIQUE 효과를 내려면 자연 키를 _id로 사용하거나(같은 _id로 쓰면 덮어씀) op_type=create로 충돌을 거부한다.","danger":"safe"}}},{"id":"create-view","title":"뷰 생성 / 수정","aliases":["view","view","create view","alter view","or replace"],"engines":{"oracle":{"command":"CREATE OR REPLACE VIEW active_customer AS\nSELECT id, name, email FROM customer WHERE status = 'ACTIVE';\n\n-- Drop\nDROP VIEW active_customer;","note":"CREATE OR REPLACE VIEW는 생성과 수정을 모두 수행한다. 특정 조건에서 갱신 가능한 뷰에는 WITH CHECK OPTION을 추가할 수 있다.","danger":"safe","readonly":true,"priv":"CREATE VIEW"},"postgres":{"command":"CREATE OR REPLACE VIEW active_customer AS\nSELECT id, name, email FROM customer WHERE status = 'ACTIVE';\n\nDROP VIEW active_customer;","note":"CREATE OR REPLACE에서는 새 컬럼을 끝에만 추가할 수 있다. 컬럼 타입/순서를 바꾸려면 먼저 DROP해야 한다. 단순 뷰는 자동으로 갱신 가능하다.","danger":"safe","readonly":true,"priv":"스키마에 대한 CREATE"},"mysql":{"command":"CREATE OR REPLACE VIEW active_customer AS\nSELECT id, name, email FROM customer WHERE status = 'ACTIVE';\n\n-- Modify\nALTER VIEW active_customer AS SELECT id, name FROM customer;\nDROP VIEW active_customer;","note":"정의는 CREATE OR REPLACE VIEW 또는 ALTER VIEW로 갱신한다. 실행 방식은 ALGORITHM = MERGE/TEMPTABLE로 지정할 수 있으며, WITH CHECK OPTION은 갱신 가능한 뷰를 보호한다.","danger":"safe","readonly":true,"priv":"테이블에 대한 CREATE VIEW"},"mssql":{"command":"CREATE OR ALTER VIEW active_customer AS\nSELECT id, name, email FROM customer WHERE status = 'ACTIVE';\n\nDROP VIEW active_customer;","note":"CREATE OR ALTER VIEW(2016 SP1+)는 단일 문으로 생성/수정한다. 이전 버전에서는 먼저 CREATE한 뒤 ALTER VIEW를 사용한다. 뷰 문은 배치의 첫 문이어야 한다.","danger":"safe","readonly":true,"priv":"CREATE VIEW","minver":"OR ALTER는 2016 SP1"},"mongodb":{"command":"// Read-only view (on top of an aggregation pipeline)\ndb.createView(\"active_customer\", \"customer\", [ { $match: { status: \"ACTIVE\" } }, { $project: { name: 1, email: 1 } } ]);\n\ndb.active_customer.drop();","note":"db.createView는 소스 컬렉션과 aggregation 파이프라인 위에 읽기 전용 뷰를 생성한다. 뷰에는 쓸 수 없으며, 정의를 바꾸려면 삭제 후 다시 생성한다(또는 collMod 사용).","danger":"safe","readonly":true},"redis":{"command":"# Not applicable: Redis has no view concept; derived views are provided on the application\n# side or with RedisJSON/RediSearch indexes.","note":"쿼리 기반으로 정의되는 뷰를 지원하지 않는다. 비슷한 용도에는 미리 계산된 키를 유지하거나 RediSearch로 검색 가능한 인덱스를 구축한다.","danger":"safe"},"elasticsearch":{"command":"# Filtered logical view with an alias\nPOST _aliases\n{ \"actions\": [ { \"add\": { \"index\": \"customer\", \"alias\": \"active_customer\",\n  \"filter\": { \"term\": { \"status\": \"ACTIVE\" } } } } ] }","note":"고전적인 SQL 뷰는 없다. 필터링된 alias가 가장 가까운 대안이며 쿼리에 필터를 자동 적용한다. 컬럼 선택은 쿼리에서 _source 필터링을 사용한다.","danger":"safe","readonly":true}}},{"id":"materialized-view","title":"구체화된 뷰(materialized view) 생성 및 갱신","aliases":["materialized view","mview","refresh","refresh","cached view","materialize"],"engines":{"oracle":{"command":"-- FAST refresh first requires an MV log on the source table:\nCREATE MATERIALIZED VIEW LOG ON sales\n  WITH ROWID, SEQUENCE (region, amount) INCLUDING NEW VALUES;\n\nCREATE MATERIALIZED VIEW mv_revenue\nBUILD IMMEDIATE REFRESH FAST ON COMMIT\nAS SELECT region, SUM(amount) total, COUNT(*) cnt, COUNT(amount) amount_cnt\n   FROM sales GROUP BY region;\n\n-- Manual refresh\nEXEC DBMS_MVIEW.REFRESH('MV_REVENUE', 'C');","note":"REFRESH FAST에는 materialized view log가 필요하다. COMPLETE('C')는 전체 재계산, FAST('F')는 증분 갱신이다. ON COMMIT/ON DEMAND가 갱신 시점을 결정한다.","danger":"caution","priv":"CREATE MATERIALIZED VIEW"},"postgres":{"command":"CREATE MATERIALIZED VIEW mv_revenue AS\nSELECT region, SUM(amount) AS total FROM sales GROUP BY region;\n\n-- Refresh (CONCURRENTLY requires a unique index)\nCREATE UNIQUE INDEX ON mv_revenue (region);\nREFRESH MATERIALIZED VIEW CONCURRENTLY mv_revenue;","note":"REFRESH는 전체를 재계산한다(증분 아님). CONCURRENTLY는 읽기를 블로킹하지 않지만 unique 인덱스가 필요하고 더 느리다.","danger":"caution","priv":"테이블 소유자","minver":"PG9.3(CONCURRENTLY는 9.4)"},"mysql":{"command":"-- MySQL/MariaDB has no built-in materialized views.\n-- Approach: summary table + refresh with a trigger or a scheduled event\nCREATE TABLE mv_revenue (region VARCHAR(50) PRIMARY KEY, total DECIMAL(18,2));\n\nCREATE EVENT ev_mv_revenue ON SCHEDULE EVERY 1 HOUR DO\n  REPLACE INTO mv_revenue SELECT region, SUM(amount) FROM sales GROUP BY region;","note":"materialized view를 기본 지원하지 않는다. 요약 테이블을 두고 트리거로 즉시 또는 EVENT SCHEDULER로 주기적으로 다시 채운다.","danger":"caution","priv":"스키마에 대한 CREATE, EVENT"},"mssql":{"command":"-- Indexed view (materialized view equivalent)\nCREATE VIEW dbo.mv_revenue WITH SCHEMABINDING AS\nSELECT region, SUM(ISNULL(amount, 0)) AS total, COUNT_BIG(*) AS cnt\nFROM dbo.sales GROUP BY region;\nGO\nCREATE UNIQUE CLUSTERED INDEX ix_mv_revenue ON dbo.mv_revenue (region);","note":"SQL Server는 indexed view를 사용한다. WITH SCHEMABINDING이 필수이며, unique clustered 인덱스를 생성하면 데이터가 물리적으로 저장되고 자동 유지된다(별도 REFRESH 불필요). GROUP BY가 있으면 COUNT_BIG(*)가 필요하고, SUM 안의 컬럼이 nullable이면 SUM(ISNULL(amount, 0))으로 작성한다(그렇지 않으면 오류 8662). 뷰는 QUOTED_IDENTIFIER와 ANSI_NULLS ON 상태에서 생성해야 한다. 쿼리 옵티마이저의 자동 뷰 매칭은 Enterprise(및 Developer) 에디션에서만 가능하며, Standard에서는 WITH (NOEXPAND) 힌트로 뷰를 참조한다.","danger":"caution","priv":"CREATE VIEW, 스키마에 대한 ALTER"},"mongodb":{"command":"// On-demand materialized view: write the result to a collection with $merge\ndb.sales.aggregate([\n  { $group: { _id: \"$region\", total: { $sum: \"$amount\" } } },\n  { $merge: { into: \"mv_revenue\", whenMatched: \"replace\", whenNotMatched: \"insert\" } }\n]);","note":"$out 또는 $merge로 aggregation 결과를 영구 컬렉션에 기록하면 온디맨드 materialized view를 얻을 수 있다. 갱신하려면 파이프라인을 다시 실행한다(수동 또는 스케줄러).","danger":"caution"},"redis":{"command":"# Not applicable: Redis has no materialized views. The computed result is written to a key\n# and cached with a TTL.\nSET mv:revenue:region1 12345 EX 3600","note":"materialized view 개념이 없다. 가장 가까운 방법은 미리 계산한 값을 키(String/Hash/Sorted Set)에 두고 EX/TTL로 일정 시간 캐시하는 것이다.","danger":"safe"},"elasticsearch":{"command":"# Continuous/materialized summary index with a transform\nPUT _transform/mv_revenue\n{ \"source\": { \"index\": \"sales\" },\n  \"dest\": { \"index\": \"mv_revenue\" },\n  \"pivot\": { \"group_by\": { \"region\": { \"terms\": { \"field\": \"region\" } } },\n            \"aggregations\": { \"total\": { \"sum\": { \"field\": \"amount\" } } } } }\nPOST _transform/mv_revenue/_start","note":"transform은 소스 인덱스를 피벗하여 요약 대상 인덱스로 구체화한다. 이 예시는 batch transform으로, _start로 한 번 실행되고 멈춘다. 새 데이터가 들어올 때 자동으로 갱신되는 continuous transform을 원하면 구성에 시간 필드와 함께 \"sync\": { \"time\": { \"field\": \"<time_field>\", \"delay\": \"60s\" } }를 추가한다(continuous: true 설정은 없다).","danger":"caution"}}},{"id":"rename-object","title":"테이블 / 객체 이름 변경","aliases":["rename","rename","change name","rename table","sp_rename"],"engines":{"oracle":{"command":"-- Table\nALTER TABLE old_name RENAME TO new_name;\n-- or\nRENAME old_name TO new_name;\n-- Column\nALTER TABLE customer RENAME COLUMN name TO full_name;","note":"테이블/뷰/시퀀스에는 RENAME 또는 ALTER ... RENAME TO를 사용한다. 의존 객체(뷰, 시노님)가 무효화되어 재컴파일이 필요할 수 있다.","danger":"caution","priv":"객체에 대한 ALTER"},"postgres":{"command":"ALTER TABLE old_name RENAME TO new_name;\nALTER TABLE customer RENAME COLUMN name TO full_name;\nALTER VIEW old_view RENAME TO new_view;","note":"ALTER ... RENAME TO는 대부분의 객체 타입에서 동작한다. 외래 키와 인덱스 참조는 이름이 바뀌어도 자동으로 유지된다(OID 기반).","danger":"caution","priv":"테이블 소유자"},"mysql":{"command":"RENAME TABLE old_name TO new_name;\n-- or\nALTER TABLE old_name RENAME TO new_name;\n-- Column (8.0+)\nALTER TABLE customer RENAME COLUMN name TO full_name;","note":"RENAME TABLE은 원자적이며 단일 문으로 여러 테이블을 옮길 수 있다. 컬럼 이름 변경은 8.0+에서 RENAME COLUMN을 사용하며, 그 이전에는 CHANGE를 사용하고 타입도 지정해야 한다.","danger":"caution","priv":"기존 이름에 대한 ALTER, DROP / 새 이름에 대한 CREATE, INSERT"},"mssql":{"command":"-- Table\nEXEC sp_rename 'dbo.old_name', 'new_name';\n-- Column\nEXEC sp_rename 'dbo.customer.name', 'full_name', 'COLUMN';","note":"SQL Server에는 RENAME TABLE 문이 없으며 시스템 프로시저 sp_rename을 사용한다. 스키마 접두사는 테이블 이름에는 붙이지만 새 이름에는 붙이지 않는다. 의존 뷰/프로시저는 수동으로 갱신해야 한다.","danger":"caution","priv":"객체에 대한 ALTER, CONTROL"},"mongodb":{"command":"// Rename a collection\ndb.old_name.renameCollection(\"new_name\");\n// or the admin command\ndb.adminCommand({ renameCollection: \"db.old_name\", to: \"db.new_name\" });","note":"renameCollection은 컬렉션 이름을 변경한다. 필드 이름을 바꾸려면 updateMany와 $rename 연산자를 사용한다. 샤딩된 컬렉션은 5.0+에서 이름을 바꿀 수 있으며, 이전 버전은 지원하지 않는다.","danger":"caution","priv":"renameCollectionSameDB / clusterManager"},"redis":{"command":"# Rename a key\nRENAME old_key new_key\n# Without overwriting if the target exists\nRENAMENX old_key new_key","note":"RENAME은 키 이름을 바꾸며, 대상이 이미 있으면 덮어쓴다. RENAMENX는 대상이 없을 때만 동작한다. 소스 키가 없으면 오류를 반환한다.","danger":"caution"},"elasticsearch":{"command":"# An index cannot be renamed directly; reindex + alias approach\nPOST _reindex\n{ \"source\": { \"index\": \"old_name\" }, \"dest\": { \"index\": \"new_name\" } }\nDELETE old_name\n# or use an alias for a seamless switch\nPOST _aliases\n{ \"actions\": [ { \"add\": { \"index\": \"new_name\", \"alias\": \"customer\" } } ] }","note":"인덱스 이름 변경 명령은 없다. _reindex로 새 이름으로 재색인하고 기존 인덱스를 삭제한다. 애플리케이션이 인덱스 이름에 의존하지 않도록 고정 alias 뒤에서 운영하는 것이 권장된다.","danger":"caution"}}}]},"rosetta/datamove.json":{"category":"데이터 이동","categoryId":"datamove","order":11,"concepts":[{"id":"bulk-import","title":"데이터 대량 적재(CSV / 파일에서)","aliases":["import","bulk load","load csv","bulk insert","load data","copy from","import data"],"engines":{"oracle":{"command":"-- SQL*Loader (from the shell)\nsqlldr app_user/password control=customer.ctl log=load.log\n\n-- Contents of customer.ctl:\n-- LOAD DATA INFILE 'customer.csv'\n-- INTO TABLE customer FIELDS TERMINATED BY ','\n-- (id, name, email)\n\n-- Alternative: with an external table\nCREATE TABLE ext_customer (id NUMBER, name VARCHAR2(100))\nORGANIZATION EXTERNAL (TYPE ORACLE_LOADER DEFAULT DIRECTORY dir_csv\n  ACCESS PARAMETERS (FIELDS TERMINATED BY ',') LOCATION ('customer.csv'));","note":"가장 일반적인 도구는 SQL*Loader(컨트롤 파일 사용)다. 파일을 쿼리처럼 읽으려면 external table을 사용한다. 대용량에는 direct path load(DIRECT=TRUE)가 빠르다.","danger":"caution","priv":"테이블에 대한 INSERT, DIRECTORY 객체"},"postgres":{"command":"-- Server side (file on the server; requires superuser/pg_read_server_files)\nCOPY customer (id, name, email) FROM '/data/customer.csv' WITH (FORMAT csv, HEADER true);\n\n-- Client side (psql, file on the client)\n\\copy customer (id, name, email) FROM 'customer.csv' WITH (FORMAT csv, HEADER true)","note":"COPY는 서버의 파일을 읽으며 권한이 필요하다. psql \\copy는 클라이언트 파일을 읽으며 추가 권한이 필요 없다. CSV를 적재하는 가장 빠른 방법이다.","danger":"caution","priv":"테이블에 대한 INSERT(COPY는 pg_read_server_files)"},"mysql":{"command":"-- Server side\nLOAD DATA INFILE '/data/customer.csv' INTO TABLE customer\nFIELDS TERMINATED BY ',' ENCLOSED BY '\"' LINES TERMINATED BY '\\n' IGNORE 1 LINES;\n\n-- From a client file\nLOAD DATA LOCAL INFILE 'customer.csv' INTO TABLE customer\nFIELDS TERMINATED BY ',' IGNORE 1 LINES;","note":"LOAD DATA INFILE은 가장 빠른 대량 적재 방법이다. LOCAL은 클라이언트 파일을 읽으며 서버(local_infile=ON)와 클라이언트 양쪽에서 활성화해야 한다. secure_file_priv가 서버 경로를 제한한다.","danger":"caution","priv":"FILE(LOCAL은 INSERT)"},"mssql":{"command":"-- T-SQL BULK INSERT\nBULK INSERT customer FROM 'C:\\data\\customer.csv'\nWITH (FORMAT='CSV', FIRSTROW=2, FIELDTERMINATOR=',', ROWTERMINATOR='0x0a');\n\n-- bcp from the shell\nbcp mydb.dbo.customer in customer.csv -c -t, -S server -T","note":"BULK INSERT는 SQL Server 서비스 계정이 접근 가능한 경로에서 파일을 읽는다. bcp는 명령줄 도구이며 -c는 문자 모드, -t는 필드 구분자다. 최대 속도를 위해 TABLOCK으로 최소 로깅을 노린다.","danger":"caution","priv":"INSERT, ADMINISTER BULK OPERATIONS"},"mongodb":{"command":"# Import CSV / JSON (from the shell)\nmongoimport --db=shop --collection=customer --type=csv --headerline --file=customer.csv\n\n# For a JSON array\nmongoimport --db=shop --collection=customer --file=customer.json --jsonArray","note":"mongoimport는 CSV/TSV/JSON을 적재한다. --headerline은 첫 줄을 필드 이름으로 사용한다. 기존 문서를 갱신하려면 --mode=upsert --upsertFields를 사용한다. 대용량 데이터는 --numInsertionWorkers로 속도를 높인다.","danger":"caution","priv":"insert(readWrite)"},"redis":{"command":"# Fastest: feed commands in protocol format and pipe them\ncat commands.txt | redis-cli --pipe\n\n# Example lines in commands.txt:\n# SET customer:1 \"Alice\"\n# SET customer:2 \"Bob\"","note":"대량 적재에는 redis-cli --pipe(mass insert)를 사용한다. 단일 연결로 RESP 프로토콜 형식의 명령을 스트리밍하므로 매우 빠르다. 또는 pipelining으로 명령을 묶는다.","danger":"caution"},"elasticsearch":{"command":"# _bulk API (NDJSON: an action line + a source line per document)\ncurl -s -H 'Content-Type: application/x-ndjson' -XPOST 'localhost:9200/customer/_bulk' --data-binary @data.ndjson\n\n# Contents of data.ndjson:\n# { \"index\": {} }\n# { \"id\": 1, \"name\": \"Alice\" }\n# { \"index\": {} }\n# { \"id\": 2, \"name\": \"Bob\" }","note":"_bulk API는 NDJSON을 받는다(문서마다 action 줄 + data 줄, 파일은 \\n으로 끝나야 함). CSV는 Logstash, Filebeat 또는 elasticdump를 사용한다. 적재 중 refresh_interval을 비활성화하면 속도가 빨라진다.","danger":"caution","priv":"index / create index 권한"}}},{"id":"bulk-export","title":"데이터 내보내기(CSV / 파일로)","aliases":["export","export data","export csv","copy to","outfile","dump","extract data"],"engines":{"oracle":{"command":"-- CSV with SQL*Plus (spool)\nSET MARKUP CSV ON\nSPOOL customer.csv\nSELECT id, name, email FROM customer;\nSPOOL OFF\n\n-- Logical export with Data Pump (from the shell)\nexpdp app_user/password tables=customer directory=dir_dmp dumpfile=customer.dmp","note":"CSV는 SQL*Plus SET MARKUP CSV ON + SPOOL을 사용한다(12.2+). 스키마/테이블을 옮길 때는 Data Pump(expdp)가 바이너리 .dmp를 생성하며, 이를 impdp로 다른 Oracle 데이터베이스에 가져온다.","danger":"safe","readonly":true,"priv":"테이블에 대한 SELECT","minver":"MARKUP CSV는 12.2"},"postgres":{"command":"-- Server side\nCOPY (SELECT id, name, email FROM customer) TO '/data/customer.csv' WITH (FORMAT csv, HEADER true);\n\n-- Client side (psql)\n\\copy (SELECT id, name, email FROM customer) TO 'customer.csv' WITH (FORMAT csv, HEADER true)","note":"COPY ... TO는 쿼리 결과를 파일에 쓴다(서버 측, 권한 필요). psql \\copy는 클라이언트 파일에 쓴다. HEADER true는 헤더 줄을 추가한다.","danger":"safe","readonly":true,"priv":"테이블에 대한 SELECT"},"mysql":{"command":"-- To a file on the server\nSELECT id, name, email FROM customer\nINTO OUTFILE '/data/customer.csv'\nFIELDS TERMINATED BY ',' ENCLOSED BY '\"' LINES TERMINATED BY '\\n';\n\n-- To the client: mysql -e ... > file (tab-separated)\n-- mysql -e \"SELECT * FROM customer\" shop > customer.tsv","note":"SELECT ... INTO OUTFILE은 서버에 파일을 생성한다(secure_file_priv 경로와 FILE 권한 필요, 덮어쓰기 불가). 클라이언트 측 출력은 mysql -e로 리다이렉트한다.","danger":"safe","readonly":true,"priv":"SELECT, FILE"},"mssql":{"command":"-- Export with bcp (from the shell)\nbcp \"SELECT id, name, email FROM shop.dbo.customer\" queryout customer.csv -c -t, -S server -T\n\n-- or the whole table\nbcp shop.dbo.customer out customer.dat -n -S server -T","note":"bcp out/queryout은 데이터를 파일에 쓴다: -c 문자 모드, -t, 필드 구분자, -n 네이티브 바이너리 형식. 쿼리 결과는 queryout을 사용한다. SSMS의 'Results to File'도 선택지다.","danger":"safe","readonly":true,"priv":"테이블에 대한 SELECT"},"mongodb":{"command":"# Export CSV (fields must be specified)\nmongoexport --db=shop --collection=customer --type=csv --fields=id,name,email --out=customer.csv\n\n# As JSON\nmongoexport --db=shop --collection=customer --out=customer.json","note":"mongoexport는 CSV/JSON을 생성한다. CSV는 --fields로 컬럼을 명시해야 한다. 필터링은 --query를 사용한다. 전체 바이너리 백업에는 mongodump가 권장된다.","danger":"safe","readonly":true,"priv":"find(read)"},"redis":{"command":"# Scan keys (SCAN instead of KEYS in production)\nredis-cli --scan --pattern 'customer:*' > keys.txt\n\n# For a full data backup, download an RDB snapshot to the client side (non-blocking)\nredis-cli --rdb ./dump.rdb","note":"구조화된 CSV 내보내기는 기본 제공되지 않는다. 키 목록은 --scan(비블로킹)을, 전체 데이터는 RDB 파일을 사용한다. redis-cli --rdb는 서버에서 백그라운드로 생성한 스냅샷을 로컬 파일로 내려받는다. 서버에 파일을 두려면 BGSAVE를 사용한다. SAVE는 메인 스레드를 블로킹하므로 운영 환경에서 사용하지 않는다. 값까지 얻으려면 scan 결과를 루프로 읽는다.","danger":"safe","readonly":true},"elasticsearch":{"command":"# Export with elasticdump\nelasticdump --input=http://localhost:9200/customer --output=customer.json --type=data\n\n# For large data, paged reads with scroll / point-in-time\nGET customer/_search?scroll=1m\n{ \"size\": 1000, \"query\": { \"match_all\": {} } }","note":"직접적인 CSV 내보내기는 없다. 전체 데이터를 얻으려면 elasticdump를 사용하거나 scroll/PIT(point-in-time)로 _search를 페이징한다. CSV 변환에는 Logstash csv output이나 외부 도구가 필요하다.","danger":"safe","readonly":true,"priv":"인덱스에 대한 read"}}},{"id":"copy-between","title":"테이블 / 환경 간 데이터 복사","aliases":["copy","copy","insert select","ctas","reindex","move data","between tables"],"engines":{"oracle":{"command":"-- Into an existing table\nINSERT INTO target_tbl (id, name) SELECT id, name FROM source_tbl WHERE status = 'ACTIVE';\n\n-- By creating a new table (CTAS)\nCREATE TABLE target_tbl AS SELECT * FROM source_tbl;\n\n-- From a remote database via a database link\nINSERT INTO target_tbl SELECT * FROM source_tbl@remote_db;","note":"같은 데이터베이스 안에서는 INSERT ... SELECT 또는 CREATE TABLE AS SELECT(CTAS)를 사용한다. 환경 간 복사에는 database link(@remote_db)나 Data Pump가 권장된다. APPEND 힌트는 direct-path로 속도를 높인다.","danger":"caution","priv":"소스에 대한 SELECT, 대상에 대한 INSERT/CREATE"},"postgres":{"command":"-- Same database\nINSERT INTO target_tbl (id, name) SELECT id, name FROM source_tbl;\nCREATE TABLE target_tbl AS SELECT * FROM source_tbl;\n\n-- Between environments (psql pipe)\n-- pg_dump -t source_tbl sourcedb | psql targetdb\n-- or read the remote table with postgres_fdw and INSERT","note":"같은 DB 안에서는 INSERT ... SELECT / CTAS를 사용한다. 서버 간에는 pg_dump | psql 파이프라인을 쓰거나 postgres_fdw로 foreign table을 통해 복사한다.","danger":"caution","priv":"소스에 대한 SELECT, 대상에 대한 INSERT/CREATE"},"mysql":{"command":"-- Same database\nINSERT INTO target_tbl (id, name) SELECT id, name FROM source_tbl;\nCREATE TABLE target_tbl AS SELECT * FROM source_tbl;\n\n-- Between servers (mysqldump pipe)\n-- mysqldump sourcedb source_tbl | mysql -h targethost targetdb","note":"같은 DB 안에서는 INSERT ... SELECT 또는 CREATE TABLE ... AS SELECT. 다른 서버 간에는 mysqldump 출력을 mysql 클라이언트로 파이프하는 방법이 일반적이다. CTAS는 인덱스와 제약 조건을 복사하지 않는다.","danger":"caution","priv":"소스에 대한 SELECT, 대상에 대한 INSERT/CREATE"},"mssql":{"command":"-- Into an existing table\nINSERT INTO target_tbl (id, name) SELECT id, name FROM source_tbl;\n\n-- By creating a new table\nSELECT * INTO target_tbl FROM source_tbl;\n\n-- Between servers: linked server\nINSERT INTO target_tbl SELECT * FROM [REMOTE_SRV].shop.dbo.source_tbl;","note":"SQL Server에서 새 테이블로의 복사는 SELECT ... INTO(CTAS에 해당)로 수행한다. 서버 간에는 linked server(4부분 이름), bcp out/in 또는 SSIS를 사용한다.","danger":"caution","priv":"소스에 대한 SELECT, 대상에 대한 INSERT / CREATE TABLE"},"mongodb":{"command":"// Write an aggregation result to another collection\ndb.source_tbl.aggregate([\n  { $match: { status: \"ACTIVE\" } },\n  { $out: \"target_tbl\" }\n]);\n\n// Merge into an existing collection (upsert)\ndb.source_tbl.aggregate([ { $merge: { into: \"target_tbl\", whenMatched: \"merge\", whenNotMatched: \"insert\" } } ]);","note":"$out은 대상 컬렉션을 통째로 교체하고, $merge는 기존 문서와 병합/삽입한다(같은 DB 또는 다른 DB). 서버 간 이동에는 mongodump | mongorestore 또는 mongoexport/mongoimport를 사용한다.","danger":"caution","priv":"소스에 대한 find, 대상에 대한 insert"},"redis":{"command":"# Move a single key to another server (deleted from the source)\nMIGRATE target_host 6379 customer:1 0 5000\n\n# To copy (without deleting): DUMP + RESTORE\nDUMP customer:1          # returns the serialized value\nRESTORE customer:1new 0 \"<dump-data>\"","note":"MIGRATE는 키를 대상 인스턴스로 원자적으로 옮기고 소스에서 삭제한다(COPY 옵션을 쓰면 삭제하지 않음). 같은 인스턴스 안에서 복사하려면 COPY 명령(6.0+)이나 DUMP+RESTORE를 사용한다.","danger":"caution"},"elasticsearch":{"command":"# Within the same cluster\nPOST _reindex\n{ \"source\": { \"index\": \"source_tbl\" }, \"dest\": { \"index\": \"target_tbl\" } }\n\n# From a remote cluster (remote reindex; requires the whitelist setting)\nPOST _reindex\n{ \"source\": { \"remote\": { \"host\": \"http://remote:9200\" }, \"index\": \"source_tbl\" },\n  \"dest\": { \"index\": \"target_tbl\" } }","note":"_reindex는 인덱스 간 복사를 수행하며, query로 필터링하고 script로 변환할 수 있다. 클러스터 간에는 remote reindex를 사용한다(대상에 reindex.remote.whitelist 정의 필요). 대규모 작업은 slices로 병렬화한다.","danger":"caution","priv":"소스에 대한 read, 대상에 대한 write"}}},{"id":"generate-insert","title":"데이터를 INSERT 스크립트로 생성","aliases":["generate insert","insert script","generate script","data dump","inserts","generate script"],"engines":{"oracle":{"command":"-- DBMS_METADATA for object DDL\nSELECT DBMS_METADATA.GET_DDL('TABLE','CUSTOMER') FROM dual;\n\n-- Generating row data as INSERTs is not built in;\n-- use SQL Developer 'Export > Insert' or SQLcl 'SET SQLFORMAT INSERT':\n-- SET SQLFORMAT INSERT\n-- SELECT * FROM customer;","note":"Oracle에는 데이터를 바로 INSERT 스크립트로 변환하는 SQL 명령이 없다. DDL은 DBMS_METADATA.GET_DDL을, 행 INSERT는 SQLcl(SET SQLFORMAT INSERT)이나 SQL Developer Export 마법사를 사용한다.","danger":"safe","readonly":true,"priv":"테이블에 대한 SELECT"},"postgres":{"command":"-- Data only, as INSERT statements\npg_dump --data-only --inserts --table=customer shop > insert.sql\n\n-- With column names (more robust to schema differences)\npg_dump --data-only --column-inserts --table=customer shop > insert.sql","note":"pg_dump --inserts는 각 행을 개별 INSERT로 쓰고, --column-inserts는 컬럼 이름도 추가한다(스키마 차이에 강함). 기본값(플래그 없음)은 COPY 형식을 생성하며 적재가 훨씬 빠르다.","danger":"safe","readonly":true,"priv":"테이블에 대한 SELECT"},"mysql":{"command":"-- Data only (INSERTs), without the table definition\nmysqldump --no-create-info shop customer > insert.sql\n\n-- One INSERT per row (more readable)\nmysqldump --no-create-info --skip-extended-insert shop customer > insert.sql","note":"mysqldump는 기본적으로 INSERT 문을 생성한다. --no-create-info는 CREATE TABLE을 생략하고 데이터만 출력한다. --skip-extended-insert는 각 행을 개별 INSERT로 만든다(기본값은 여러 행을 한 문으로 묶음).","danger":"safe","readonly":true,"priv":"테이블에 대한 SELECT"},"mssql":{"command":"-- SSMS: right-click > Tasks > Generate Scripts >\n--   Advanced > 'Types of data to script' = Data only (or Schema and data)\n\n-- Command-line alternative (sqlpackage / mssql-scripter tool)\n-- mssql-scripter -S server -d shop -t customer --data-only > insert.sql","note":"SQL Server에는 이를 위한 기본 T-SQL 명령이 없다. 가장 일반적인 방법은 SSMS 'Generate Scripts' 마법사에서 Data only를 선택하는 것이다. 명령줄에서는 Microsoft의 mssql-scripter 도구가 --data-only로 INSERT를 생성한다.","danger":"safe","readonly":true,"priv":"테이블에 대한 SELECT"},"mongodb":{"command":"# Produces a binary/BSON backup instead of INSERT SQL\nmongodump --db=shop --collection=customer --out=./backup\n\n# As JSON text (can be adapted to insertMany)\nmongoexport --db=shop --collection=customer --jsonArray --out=customer.json","note":"SQL INSERT 개념이 없다. 이식 가능한 백업에는 mongodump(BSON, mongorestore로 복원)를 사용한다. 코드에 넣을 insertMany용으로는 mongoexport --jsonArray 출력을 활용할 수 있다.","danger":"safe","readonly":true,"priv":"find(read)"},"redis":{"command":"# Portable serialization of a single key\nDUMP customer:1\n# The output is restored with RESTORE:\n# RESTORE customer:1 0 \"<dump-data>\"\n\n# RDB snapshot for all data\nredis-cli --rdb dump.rdb","note":"SQL INSERT 스크립트는 생성되지 않는다. 키 단위로는 DUMP + RESTORE 직렬화를, 전체 데이터는 RDB(--rdb) 파일을 사용한다. 명령 목록이 필요하면 SCAN으로 순회하며 타입별로 적절한 SET/HSET 명령을 만든다.","danger":"safe","readonly":true},"elasticsearch":{"command":"# The INSERT equivalent is _bulk NDJSON; produce it with elasticdump\nelasticdump --input=http://localhost:9200/customer --output=customer_bulk.json --type=data\n\n# Manual: converting a _search result to _bulk NDJSON requires an external script","note":"SQL INSERT는 생성되지 않는다. Elasticsearch의 적재 단위는 _bulk NDJSON이다. 기존 데이터를 재적재 가능한 형태로 추출하려면 elasticdump(type=data)를 사용한다. _search + PIT 출력은 외부 스크립트로 NDJSON으로 변환할 수 있다.","danger":"safe","readonly":true,"priv":"인덱스에 대한 read"}}}]},"rosetta/monitoring.json":{"category":"모니터링 및 진단","categoryId":"monitoring","order":12,"concepts":[{"id":"current-locks","title":"현재 락 목록","aliases":["locks","lock list","lock list","current locks","active locks"],"engines":{"oracle":{"command":"SELECT s.sid, s.serial#, l.type, DECODE(l.lmode,0,'None',1,'Null',2,'Row-S',3,'Row-X',4,'Share',5,'S/Row-X',6,'Exclusive') AS lmode, o.object_name, s.username, s.blocking_session\nFROM v$lock l\nJOIN v$session s ON s.sid = l.sid\nLEFT JOIN v$locked_object lo ON lo.session_id = l.sid\nLEFT JOIN dba_objects o ON o.object_id = lo.object_id\nORDER BY s.sid;","note":"v$lock과 v$locked_object를 통해 활성 락과 블로킹 세션(blocking_session)을 보여 준다.","danger":"safe","readonly":true,"priv":"v$lock, v$session에 대한 SELECT"},"postgres":{"command":"SELECT l.locktype, l.mode, l.granted, a.pid, a.usename, a.state, a.wait_event_type, a.query\nFROM pg_locks l\nJOIN pg_stat_activity a ON a.pid = l.pid\nWHERE l.pid <> pg_backend_pid()\nORDER BY l.granted, a.pid;","note":"pg_locks와 pg_stat_activity를 조인한다. granted=false인 행은 대기 중인(블로킹된) 락이다.","danger":"safe","readonly":true,"priv":"pg_monitor 롤(모든 쿼리 확인용)"},"mysql":{"command":"SELECT ENGINE, OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS, THREAD_ID\nFROM performance_schema.data_locks;\n-- For waiters: SELECT * FROM performance_schema.data_lock_waits;","note":"performance_schema.data_locks는 현재 InnoDB 락을, data_lock_waits는 대기 관계를 나열한다.","danger":"safe","readonly":true,"priv":"performance_schema에 대한 SELECT","minver":"8.0"},"mssql":{"command":"SELECT tl.resource_type, tl.resource_database_id, tl.request_mode, tl.request_status, tl.request_session_id, es.login_name, er.blocking_session_id\nFROM sys.dm_tran_locks tl\nLEFT JOIN sys.dm_exec_sessions es ON es.session_id = tl.request_session_id\nLEFT JOIN sys.dm_exec_requests er ON er.session_id = tl.request_session_id\nORDER BY tl.request_session_id;","note":"sys.dm_tran_locks는 현재 락 요청을 보여 준다. request_status = WAIT인 요청은 블로킹된 요청이다.","danger":"safe","readonly":true,"priv":"VIEW SERVER STATE"},"mongodb":{"command":"db.currentOp({ \"$or\": [ { \"waitingForLock\": true }, { \"locks\": { \"$exists\": true } } ] })","note":"db.currentOp는 락을 기다리는 작업(waitingForLock)과 락을 보유한 작업을 보여 준다. MongoDB는 행 수준이 아니라 문서/컬렉션 수준에서 락을 건다.","danger":"safe","priv":"inprog / clusterMonitor 롤"},"redis":{"command":"# Not applicable: Redis has no row/table lock concept; commands run sequentially on a single thread. For blocking analysis, use the SLOWLOG and LATENCY commands.","note":"Redis는 싱글 스레드이므로 전통적인 락 목록이 없다.","danger":"safe"},"elasticsearch":{"command":"GET _cluster/pending_tasks","note":"Elasticsearch에는 행 락이 없다. 이 명령은 master에 대기 중인 클러스터 변경 작업(pending tasks)을 나열한다.","danger":"safe","readonly":true}}},{"id":"long-operations","title":"장시간 실행 작업의 진행률 확인","aliases":["progress","operation progress","long operations","long-running operations","longops"],"engines":{"oracle":{"command":"SELECT sid, serial#, opname, target, sofar, totalwork,\n       ROUND(sofar / NULLIF(totalwork,0) * 100, 2) AS pct_done,\n       time_remaining, elapsed_seconds\nFROM v$session_longops\nWHERE totalwork > 0 AND sofar < totalwork\nORDER BY start_time;","note":"v$session_longops는 6초 이상 실행되는 작업의 진행률과 남은 시간(time_remaining)을 보여 준다.","danger":"safe","readonly":true,"priv":"v$session_longops에 대한 SELECT"},"postgres":{"command":"SELECT pid, phase, heap_blks_total, heap_blks_scanned,\n       ROUND(heap_blks_scanned::numeric / NULLIF(heap_blks_total,0) * 100, 2) AS pct\nFROM pg_stat_progress_vacuum;\n-- Others: pg_stat_progress_create_index, _cluster, _analyze, _copy, _basebackup","note":"pg_stat_progress_* 뷰는 VACUUM, CREATE INDEX, CLUSTER, ANALYZE, COPY 작업의 진행률을 보고한다.","danger":"safe","readonly":true,"minver":"9.6"},"mysql":{"command":"SELECT t.processlist_id, st.event_name, st.work_completed, st.work_estimated,\n       ROUND(st.work_completed / NULLIF(st.work_estimated,0) * 100, 2) AS pct\nFROM performance_schema.events_stages_current st\nJOIN performance_schema.threads t ON t.thread_id = st.thread_id\nWHERE st.work_estimated > 0;","note":"stage instrumentation이 활성화되어 있으면 events_stages_current가 work_completed/work_estimated로 진행률을 보여 준다(예: ALTER TABLE).","danger":"safe","readonly":true,"priv":"performance_schema에 대한 SELECT"},"mssql":{"command":"SELECT session_id, command, percent_complete,\n       DATEADD(ms, estimated_completion_time, GETDATE()) AS est_finish,\n       total_elapsed_time / 1000 AS elapsed_sec\nFROM sys.dm_exec_requests\nWHERE percent_complete > 0\nORDER BY percent_complete;","note":"sys.dm_exec_requests.percent_complete는 BACKUP, RESTORE, DBCC, 인덱스 재구축 같은 작업의 진행률을 보여 준다.","danger":"safe","readonly":true,"priv":"VIEW SERVER STATE"},"mongodb":{"command":"db.currentOp({ \"active\": true, \"secs_running\": { \"$gte\": 3 } })","note":"db.currentOp 출력에서 장시간 실행 작업과, 인덱스 생성의 경우 'progress' 필드(done/total)가 진행률을 보여 준다.","danger":"safe","priv":"inprog / clusterMonitor 롤"},"redis":{"command":"INFO persistence\n# Keys: rdb_bgsave_in_progress, aof_rewrite_in_progress, rdb_last_bgsave_status, loading, loading_loaded_perc","note":"INFO persistence 섹션은 RDB/AOF 저장 또는 rewrite 작업 진행 여부와 로딩 진행률을 보고한다.","danger":"safe","readonly":true},"elasticsearch":{"command":"GET _tasks?detailed=true&actions=*reindex,*update/byquery,*delete/byquery\n# For recovery/shard relocation: GET _cat/recovery?v&active_only=true","note":"_tasks?detailed는 장시간 실행 작업(reindex, update_by_query)의 상태를, _cat/recovery는 진행 중인 샤드 복구 진행률을 보여 준다.","danger":"safe","readonly":true}}},{"id":"session-trace","title":"세션 / 쿼리 추적(trace / profiling 활성화)","aliases":["trace","enable tracing","profiling","session trace","query tracing"],"engines":{"oracle":{"command":"-- For a specific session (10046 event tracing):\nBEGIN\n  DBMS_MONITOR.SESSION_TRACE_ENABLE(session_id => 123, serial_num => 4567, waits => TRUE, binds => TRUE);\nEND;\n/\n-- To disable: DBMS_MONITOR.SESSION_TRACE_DISABLE(123, 4567);","note":"DBMS_MONITOR.SESSION_TRACE_ENABLE은 세션에 대한 10046 trace 파일을 생성한다. 작업이 끝나면 반드시 비활성화한다(trace는 오버헤드와 디스크 사용을 늘린다).","danger":"caution","priv":"DBMS_MONITOR에 대한 EXECUTE"},"postgres":{"command":"LOAD 'auto_explain';\nSET auto_explain.log_min_duration = 0;\nSET auto_explain.log_analyze = true;\n-- Simple alternative: SET log_min_duration_statement = 0;","note":"auto_explain은 쿼리 실행 계획을, log_min_duration_statement는 느린/전체 쿼리를 로그 파일에 기록한다. 값이 0이면 모든 쿼리를 기록하므로 로그 양에 주의한다.","danger":"caution","priv":"SUPERUSER(LOAD용) 또는 세션 수준 SET"},"mysql":{"command":"-- performance_schema approach (recommended):\nUPDATE performance_schema.setup_consumers SET ENABLED='YES' WHERE NAME LIKE 'events_statements%';\n-- Legacy/simple method (being deprecated):\nSET profiling = 1;\n-- run the query, then:\nSHOW PROFILES;","note":"쿼리 추적은 performance_schema events_statements로 수행한다. SET profiling 방식은 오래되었지만(deprecated) 세션 단위로 빠르게 측정할 수 있다.","danger":"caution","priv":"performance_schema에 대한 SELECT/UPDATE"},"mssql":{"command":"CREATE EVENT SESSION [trace_slow] ON SERVER\nADD EVENT sqlserver.sql_statement_completed(\n  WHERE duration > 1000000)\nADD TARGET package0.event_file(SET filename=N'trace_slow.xel');\nGO\nALTER EVENT SESSION [trace_slow] ON SERVER STATE = START;","note":"Extended Events(XEvents)는 최신 추적 방식이다. 세션을 생성해 START로 시작하고, 작업이 끝나면 STOP/DROP으로 중지한다.","danger":"caution","priv":"ALTER ANY EVENT SESSION"},"mongodb":{"command":"db.setProfilingLevel(2)\n// 1 = slow operations only (slowms threshold), 0 = off\n// Results: db.system.profile.find().sort({ ts: -1 }).limit(10)","note":"setProfilingLevel(2)는 모든 작업을 system.profile 컬렉션에 기록한다. 레벨 2는 오버헤드가 있으므로 운영 환경에서는 1(느린 작업만)이 권장된다.","danger":"caution","priv":"dbAdmin / enableProfiler"},"redis":{"command":"MONITOR\n# Streams all commands arriving at the server in real time. Exit: Ctrl-C","note":"MONITOR는 모든 명령을 실시간으로 보여 주지만 성능이 크게 떨어진다. 운영 환경에서는 짧게, 신중하게 사용한다. 대안: SLOWLOG GET.","danger":"caution"},"elasticsearch":{"command":"GET my-index/_search\n{\n  \"profile\": true,\n  \"query\": { \"match\": { \"title\": \"elastic\" } }\n}\n// Persistent slow query tracing: the index.search.slowlog.threshold.query.warn setting","note":"\"profile\": true는 단일 검색의 query/aggregation 소요 시간 분석을 보여 준다. 지속적인 모니터링에는 slowlog 임계값을 설정한다.","danger":"safe","readonly":true}}},{"id":"top-wait-events","title":"상위 대기 이벤트 조회","aliases":["wait events","wait event list","top waits","waits"],"engines":{"oracle":{"command":"SELECT event, total_waits, time_waited, ROUND(time_waited/100,2) AS sec_waited\nFROM v$system_event\nWHERE wait_class <> 'Idle'\nORDER BY time_waited DESC\nFETCH FIRST 20 ROWS ONLY;\n-- For real-time analysis: v$active_session_history (ASH)","note":"v$system_event는 누적 대기 이벤트를, v$active_session_history는 최근 몇 분간의 활성 세션 샘플링을 보여 준다(Diagnostics Pack 라이선스 필요).","danger":"safe","readonly":true,"priv":"v$system_event에 대한 SELECT"},"postgres":{"command":"SELECT wait_event_type, wait_event, count(*)\nFROM pg_stat_activity\nWHERE wait_event IS NOT NULL AND state = 'active'\nGROUP BY 1, 2\nORDER BY 3 DESC;","note":"pg_stat_activity는 현재 대기 상태를 보여 준다. 반복 샘플링(polling)하면 가장 빈번한 대기 이벤트를 파악할 수 있다.","danger":"safe","readonly":true,"priv":"pg_monitor 롤"},"mysql":{"command":"SELECT event_name, count_star, sum_timer_wait,\n       ROUND(sum_timer_wait/1000000000000, 2) AS sec_waited\nFROM performance_schema.events_waits_summary_global_by_event_name\nWHERE count_star > 0\nORDER BY sum_timer_wait DESC\nLIMIT 20;","note":"events_waits_summary_global_by_event_name은 이벤트별 총 대기 시간(피코초 단위)을 보여 준다. wait instrument가 활성화되어 있어야 한다.","danger":"safe","readonly":true,"priv":"performance_schema에 대한 SELECT"},"mssql":{"command":"SELECT wait_type, waiting_tasks_count, wait_time_ms,\n       signal_wait_time_ms, wait_time_ms - signal_wait_time_ms AS resource_wait_ms\nFROM sys.dm_os_wait_stats\nWHERE wait_time_ms > 0\nORDER BY wait_time_ms DESC;","note":"sys.dm_os_wait_stats는 인스턴스 시작 이후 누적된 대기 유형을 보여 준다. DBCC SQLPERF로 초기화할 수 있다.","danger":"safe","readonly":true,"priv":"VIEW SERVER STATE"},"mongodb":{"command":"db.serverStatus().wiredTiger.concurrentTransactions\n// For general waits/congestion:\n// db.serverStatus().globalLock  and  db.serverStatus().wiredTiger.cache","note":"WiredTiger에는 고전적인 대기 이벤트가 없다. concurrentTransactions(ticket) 고갈, globalLock.currentQueue, 캐시 압박이 혼잡을 나타낸다.","danger":"safe","priv":"clusterMonitor 롤"},"redis":{"command":"CONFIG SET latency-monitor-threshold 100\nLATENCY LATEST\nLATENCY HISTORY command\n# For the slowest commands: SLOWLOG GET 10","note":"latency monitor 임계값을 설정하면 LATENCY LATEST/HISTORY가 가장 큰 지연을 유발하는 이벤트를 보고한다.","danger":"safe"},"elasticsearch":{"command":"GET _nodes/hot_threads","note":"_nodes/hot_threads는 CPU를 가장 많이 소비하는 스레드의 스택 트레이스를 보여 주어 병목을 드러낸다.","danger":"safe","readonly":true}}},{"id":"os-resource","title":"DB 내부에서 서버 자원 사용량 조회(CPU/IO)","aliases":["os resource","cpu io","resource usage","server resources","system metrics"],"engines":{"oracle":{"command":"SELECT stat_name, value FROM v$osstat\nWHERE stat_name IN ('NUM_CPUS','BUSY_TIME','IDLE_TIME','IOWAIT_TIME','PHYSICAL_MEMORY_BYTES');\n-- Real-time metrics: SELECT metric_name, value FROM v$sysmetric WHERE group_id = 2;","note":"v$osstat은 OS CPU/메모리 카운터를, v$sysmetric은 최근 구간의 실시간 DB 지표(CPU, IOPS)를 보여 준다.","danger":"safe","readonly":true,"priv":"v$osstat, v$sysmetric에 대한 SELECT"},"postgres":{"command":"SELECT backend_type, object, context, reads, writes, hits, evictions\nFROM pg_stat_io\nORDER BY reads DESC;\n-- Note: CPU usage is not visible from inside the DB; an OS tool (top, sar) is required.","note":"pg_stat_io는 읽기/쓰기/캐시 적중 카운터를 보여 준다. PostgreSQL은 CPU 사용량을 노출하지 않으므로 운영 체제 도구가 필요하다.","danger":"safe","readonly":true,"minver":"16"},"mysql":{"command":"SELECT * FROM sys.host_summary;\n-- For InnoDB I/O and thread details:\nSHOW ENGINE INNODB STATUS\\G","note":"sys 스키마 요약 뷰(host_summary)는 IO/지연 분석을 보여 준다. SHOW ENGINE INNODB STATUS는 buffer pool과 IO 활동을 보여 준다.","danger":"safe","readonly":true,"priv":"sys에 대한 SELECT, PROCESS"},"mssql":{"command":"SELECT object_name, counter_name, cntr_value\nFROM sys.dm_os_performance_counters\nWHERE counter_name LIKE '%CPU%' OR counter_name LIKE '%Page life expectancy%';\n-- CPU history: sys.dm_os_ring_buffers (RING_BUFFER_SCHEDULER_MONITOR)","note":"sys.dm_os_performance_counters는 DB 성능 카운터를, dm_os_ring_buffers는 최근 약 256분간의 SQL/기타 프로세스 CPU 비율을 보여 준다.","danger":"safe","readonly":true,"priv":"VIEW SERVER STATE"},"mongodb":{"command":"db.serverStatus().metrics\n// Relevant fields: .opcounters, .network, .wiredTiger.cache, .mem, .extra_info.page_faults","note":"serverStatus()는 메모리, 네트워크, opcounters, WiredTiger 캐시 지표를 보여 준다. 상세한 OS CPU/IO는 mongostat이나 운영 체제 도구를 사용한다.","danger":"safe","priv":"clusterMonitor 롤"},"redis":{"command":"INFO cpu\nINFO stats\n# used_cpu_sys, used_cpu_user, instantaneous_ops_per_sec, total_net_input_bytes","note":"INFO cpu는 Redis 프로세스의 CPU 사용량을, INFO stats는 초당 작업 수와 네트워크 트래픽을 보고한다.","danger":"safe","readonly":true},"elasticsearch":{"command":"GET _nodes/stats/os,process\n// Look at the os.cpu.percent, os.mem, process.cpu.percent, fs (disk) fields","note":"_nodes/stats/os,process는 노드별 CPU 사용률, 메모리, 프로세스 지표를 보여 준다. 디스크는 fs 섹션을 사용한다.","danger":"safe","readonly":true}}}]},"rosetta/automation.json":{"category":"자동화 및 감사","categoryId":"automation","order":13,"concepts":[{"id":"schedule-job","title":"예약 작업 생성","aliases":["schedule job","scheduled job","cron","create job","scheduled task"],"engines":{"oracle":{"command":"BEGIN\n  DBMS_SCHEDULER.CREATE_JOB(\n    job_name        => 'NIGHTLY_MAINT',\n    job_type        => 'PLSQL_BLOCK',\n    job_action      => 'BEGIN my_maintenance_proc; END;',\n    start_date      => SYSTIMESTAMP,\n    repeat_interval => 'FREQ=DAILY; BYHOUR=3',\n    enabled         => TRUE);\nEND;\n/","note":"DBMS_SCHEDULER.CREATE_JOB은 예약 작업을 정의한다. repeat_interval은 calendaring 표현식으로 반복 주기를 지정한다.","danger":"caution","priv":"CREATE JOB"},"postgres":{"command":"SELECT cron.schedule('nightly-vacuum', '0 3 * * *', 'VACUUM ANALYZE');\n-- Requires the pg_cron extension. Alternative: pgAgent.","note":"pg_cron 확장은 cron.schedule로 표준 cron 구문의 작업을 정의한다. 확장이 설치되어 있지 않으면 pgAgent나 운영 체제의 cron을 사용한다.","danger":"caution","priv":"pg_cron 설치 필요, 보통 superuser/확장 소유자"},"mysql":{"command":"SET GLOBAL event_scheduler = ON;\nCREATE EVENT nightly_cleanup\nON SCHEDULE EVERY 1 DAY STARTS '2026-01-01 03:00:00'\nDO\n  DELETE FROM logs WHERE created_at < NOW() - INTERVAL 30 DAY;","note":"CREATE EVENT는 event scheduler를 통해 실행된다. event_scheduler 전역 변수가 ON이어야 하며, 그렇지 않으면 이벤트가 실행되지 않는다.","danger":"caution","priv":"EVENT"},"mssql":{"command":"USE msdb;\nEXEC dbo.sp_add_job @job_name = N'NightlyMaint';\nEXEC dbo.sp_add_jobstep @job_name = N'NightlyMaint', @step_name = N'Step1',\n     @subsystem = N'TSQL', @command = N'EXEC dbo.usp_Maintenance;';\nEXEC dbo.sp_add_schedule @schedule_name = N'Daily3AM',\n     @freq_type = 4, @freq_interval = 1, @active_start_time = 030000;\nEXEC dbo.sp_attach_schedule @job_name = N'NightlyMaint', @schedule_name = N'Daily3AM';\nEXEC dbo.sp_add_jobserver @job_name = N'NightlyMaint';","note":"SQL Server Agent 작업은 msdb 프로시저로 생성한다. 작업, 단계, 일정을 정의하고 서버에 연결한다. SQL Server Agent 서비스가 실행 중이어야 한다.","danger":"caution","priv":"SQLAgentUserRole 또는 sysadmin"},"mongodb":{"command":"# Not applicable: MongoDB has no built-in scheduler. Use external cron/systemd timers, an application scheduler or a MongoDB Atlas Scheduled Trigger.","note":"Community 에디션에는 예약 작업이 없으며, 외부 스케줄러나 Atlas Trigger가 필요하다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no built-in job scheduler. Use EXPIRE/keyspace notifications for key expiry and external cron for periodic tasks.","note":"Redis는 예약 작업을 실행하지 않는다. TTL/keyspace notification과 외부 cron으로 처리한다.","danger":"safe"},"elasticsearch":{"command":"PUT _slm/policy/daily-snapshot\n{\n  \"schedule\": \"0 30 1 * * ?\",\n  \"name\": \"<snap-{now/d}>\",\n  \"repository\": \"backup_repo\",\n  \"config\": { \"indices\": [\"*\"] },\n  \"retention\": { \"expire_after\": \"30d\" }\n}","note":"SLM(Snapshot Lifecycle Management)은 예약 스냅샷을 수행한다. 데이터 수명 주기에는 ILM을, 고급 자동화에는 Watcher를 사용한다.","danger":"caution","priv":"manage_slm 클러스터 권한"}}},{"id":"list-jobs","title":"예약 작업 목록 / 상태 조회","aliases":["list jobs","show jobs","job status","scheduled jobs","job history"],"engines":{"oracle":{"command":"SELECT job_name, enabled, state, last_start_date, next_run_date\nFROM dba_scheduler_jobs\nORDER BY next_run_date;\n-- Run history: SELECT job_name, status, actual_start_date, run_duration\n--                 FROM dba_scheduler_job_run_details ORDER BY log_date DESC;","note":"dba_scheduler_jobs는 정의된 작업과 상태를, dba_scheduler_job_run_details는 과거 실행의 성공/실패 결과를 보여 준다.","danger":"safe","readonly":true,"priv":"dba_scheduler_jobs에 대한 SELECT"},"postgres":{"command":"SELECT jobid, schedule, command, nodename, active FROM cron.job;\n-- Run history:\nSELECT jobid, status, return_message, start_time, end_time\nFROM cron.job_run_details ORDER BY start_time DESC LIMIT 20;","note":"pg_cron에서 cron.job은 정의된 작업을 나열하고, cron.job_run_details는 각 실행의 상태와 반환 메시지를 보여 준다.","danger":"safe","readonly":true,"priv":"pg_cron 설치 필요, cron 스키마 접근 권한"},"mysql":{"command":"SELECT event_schema, event_name, status, interval_value, interval_field, last_executed\nFROM information_schema.events\nORDER BY last_executed DESC;","note":"information_schema.events는 정의된 이벤트, 상태(ENABLED/DISABLED), 마지막 실행 시각을 나열한다.","danger":"safe","readonly":true,"priv":"EVENT / SELECT"},"mssql":{"command":"SELECT j.name, j.enabled, h.run_status, h.run_date, h.run_time\nFROM msdb.dbo.sysjobs j\nLEFT JOIN msdb.dbo.sysjobhistory h ON h.job_id = j.job_id AND h.step_id = 0\nORDER BY h.run_date DESC, h.run_time DESC;","note":"msdb.dbo.sysjobs는 정의된 Agent 작업을, sysjobhistory는 실행 이력과 결과(run_status)를 보여 준다.","danger":"safe","readonly":true,"priv":"SQLAgentReaderRole / msdb 접근 권한"},"mongodb":{"command":"# Not applicable: there is no built-in scheduler, so there are no jobs to list. If Atlas Triggers are used, view them in the Atlas UI/API.","note":"Community MongoDB에는 작업 테이블이 없다. Atlas Trigger는 Atlas 인터페이스에서 관리한다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no concept of scheduled jobs, so there are no jobs to list.","note":"Redis에는 작업 스케줄러가 없으므로 나열할 작업이 없다.","danger":"safe"},"elasticsearch":{"command":"GET _slm/policy\nGET _ilm/policy\n// For Watcher states: GET _watcher/stats  and  GET _watcher/_query/watches","note":"_slm/policy는 스냅샷 정책을, _ilm/policy는 수명 주기 정책을 나열하고, _watcher/stats는 Watcher 실행 상태를 보여 준다.","danger":"safe","readonly":true,"priv":"read_slm / read_ilm 권한"}}},{"id":"enable-audit","title":"감사 / 로깅 활성화","aliases":["audit","auditing","logging","enable audit","audit log"],"engines":{"oracle":{"command":"-- Unified Auditing (12c+):\nCREATE AUDIT POLICY table_audit\n  ACTIONS SELECT, INSERT, UPDATE, DELETE ON hr.employees;\nAUDIT POLICY table_audit;\n-- Monitoring: SELECT * FROM unified_audit_trail ORDER BY event_timestamp DESC;","note":"Unified Auditing에서는 정책을 생성하고 AUDIT로 활성화한다. 기록은 unified_audit_trail 뷰에 저장된다.","danger":"caution","priv":"AUDIT SYSTEM / AUDIT_ADMIN 롤","minver":"12.1"},"postgres":{"command":"-- pgaudit extension (recommended; first shared_preload_libraries = 'pgaudit' + restart + CREATE EXTENSION pgaudit):\nALTER SYSTEM SET pgaudit.log = 'write, ddl';\nSELECT pg_reload_conf();\n-- Simple built-in method:\nALTER SYSTEM SET log_statement = 'ddl';\nSELECT pg_reload_conf();","note":"pgaudit는 상세한 감사 로깅을 제공한다. 서버 전체에 적용하려면 ALTER SYSTEM으로 설정한다(세션 수준 SET은 해당 세션만 감사). 확장 없이는 log_statement('ddl','mod','all')로 기본 로깅을 활성화한다. 로그 양에 주의한다.","danger":"caution","priv":"superuser(ALTER SYSTEM)"},"mysql":{"command":"-- Enterprise/MariaDB audit plugin:\nINSTALL PLUGIN audit_log SONAME 'audit_log.so';\nSET GLOBAL audit_log_policy = 'ALL';\n-- Simple alternative (all queries):\nSET GLOBAL general_log = 'ON';","note":"audit_log 플러그인(Enterprise, MariaDB는 server_audit)이 상세한 감사 기록을 남긴다. general_log는 모든 쿼리를 기록하지만 성능이 저하된다.","danger":"caution","priv":"SUPER / SYSTEM_VARIABLES_ADMIN"},"mssql":{"command":"USE master;\nCREATE SERVER AUDIT Audit_A TO FILE (FILEPATH = 'C:\\\\audit\\\\');\nALTER SERVER AUDIT Audit_A WITH (STATE = ON);\nUSE mydb;\nCREATE DATABASE AUDIT SPECIFICATION db_spec\n  FOR SERVER AUDIT Audit_A\n  ADD (SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo BY public)\n  WITH (STATE = ON);","note":"먼저 SERVER AUDIT 대상(파일/이벤트 로그)을 생성하고 활성화한 뒤, 서버 또는 데이터베이스 AUDIT SPECIFICATION으로 추적할 이벤트를 정의한다.","danger":"caution","priv":"ALTER ANY SERVER AUDIT / CONTROL SERVER"},"mongodb":{"command":"# MongoDB Enterprise / Atlas only. Configuration (mongod.conf):\n#   auditLog:\n#     destination: file\n#     format: JSON\n#     path: /var/log/mongodb/audit.json\n#     filter: '{ atype: { $in: [\"authCheck\",\"createCollection\",\"dropCollection\"] } }'\n# mongod --auditDestination file --auditPath /var/log/mongodb/audit.json","note":"감사는 Enterprise와 Atlas에서만 제공되며, mongod 구성의 auditLog 섹션으로 활성화한다. Community 에디션에는 없다.","danger":"caution"},"redis":{"command":"ACL LOG\n# Shows rejected authentication/command/key access attempts.\n# To reset: ACL LOG RESET\n# For general command auditing: an external proxy or MONITOR (with care).","note":"ACL LOG는 보안 이벤트(인증 실패, 권한 없는 명령/키)를 자동으로 기록한다. 전체 명령 감사는 기본 제공되지 않는다.","danger":"safe","priv":"ACL 권한(기본값은 admin)"},"elasticsearch":{"command":"# elasticsearch.yml (on all nodes, restart required):\nxpack.security.audit.enabled: true\n# Optional event filter:\nxpack.security.audit.logfile.events.include: [ \"authentication_failed\", \"access_denied\" ]","note":"감사 로깅은 xpack.security.audit.enabled로 활성화한다. elasticsearch.yml에서 설정하며 노드 재시작이 필요하다(동적 설정 아님). 상용 라이선스가 필요하다.","danger":"caution"}}},{"id":"current-config-diff","title":"기본값이 아닌 설정 목록","aliases":["config diff","changed settings","non-default settings","modified settings","modified config"],"engines":{"oracle":{"command":"SELECT name, value, isdefault\nFROM v$parameter\nWHERE isdefault = 'FALSE'\nORDER BY name;","note":"v$parameter에서 isdefault = 'FALSE'인 행은 기본값과 다른(명시적으로 설정된) 파라미터를 보여 준다.","danger":"safe","readonly":true,"priv":"v$parameter에 대한 SELECT"},"postgres":{"command":"SELECT name, setting, unit, source\nFROM pg_settings\nWHERE source NOT IN ('default', 'override')\nORDER BY name;","note":"pg_settings.source가 'default'와 'override' 이외의 값이면 구성 파일, 명령줄 또는 세션에서 설정이 변경된 것이다.","danger":"safe","readonly":true},"mysql":{"command":"SHOW GLOBAL VARIABLES;\n-- MySQL does not automatically compare variables with defaults; with performance_schema\n-- you can see the source of each setting:\nSELECT VARIABLE_NAME, VARIABLE_SOURCE, VARIABLE_VALUE\nFROM performance_schema.variables_info\nWHERE VARIABLE_SOURCE <> 'COMPILED';","note":"SHOW GLOBAL VARIABLES는 모든 설정을 반환하지만 기본값과의 차이를 직접 보여 주지 않는다. performance_schema.variables_info의 VARIABLE_SOURCE 컬럼은 컴파일된 기본값 이외의 변경(파일, 명령줄, 동적)을 보여 준다.","danger":"safe","readonly":true,"minver":"8.0"},"mssql":{"command":"SELECT name, value, value_in_use, is_dynamic, is_advanced\nFROM sys.configurations\nWHERE value <> value_in_use\nORDER BY name;\n-- value <> value_in_use: changes not yet RECONFIGUREd or awaiting a restart (is_dynamic = 0).","note":"sys.configurations는 서버 설정을 반환한다. value와 value_in_use가 다르면 RECONFIGURE로 적용되지 않은(또는 재시작 대기 중인) 변경이 있다는 뜻이다. sys.configurations는 기본값을 저장하지 않으므로(minimum/maximum은 허용 범위일 뿐임), 기본값과의 차이를 보려면 문서화된 기본값과 비교한다.","danger":"safe","readonly":true,"priv":"VIEW SERVER STATE"},"mongodb":{"command":"db.adminCommand({ getCmdLineOpts: 1 })\n// The 'parsed' field shows settings explicitly provided (configuration file/command line) beyond the defaults.","note":"getCmdLineOpts 명령의 'parsed' 섹션은 명시적으로 지정된(기본값이 아닌) 설정만 나열한다.","danger":"safe","priv":"clusterMonitor / hostInfo 권한"},"redis":{"command":"CONFIG GET *\n# Redis lists all settings but does not compare them with defaults.\n# To see differences, compare the output with the default redis.conf values for your version.","note":"CONFIG GET *는 실행 중인 모든 설정을 반환한다. 기본값과의 비교 기능은 없으므로 출력을 해당 버전의 기본 redis.conf 값과 비교한다.","danger":"safe","readonly":true},"elasticsearch":{"command":"GET _cluster/settings\n// Returns only explicitly set persistent and transient settings.\n// For everything including defaults: GET _cluster/settings?include_defaults=true&flat_settings=true","note":"_cluster/settings는 기본값이 아닌(persistent/transient) 클러스터 설정을 반환한다. include_defaults=true를 지정하면 비교를 위해 기본값도 표시된다.","danger":"safe","readonly":true}}}]},"rosetta/lifecycle.json":{"category":"설치 및 업그레이드","categoryId":"lifecycle","order":14,"concepts":[{"id":"check-version","title":"설치된 버전 및 패치 수준 조회","aliases":["version","patch level","server version","patch level check","release","build"],"engines":{"oracle":{"command":"SELECT * FROM v$version;\n-- Registered RU/patch history (SQL layer):\nSELECT patch_id, action, status, action_time\nFROM dba_registry_sqlpatch ORDER BY action_time;\n-- From the OS shell (patches installed in ORACLE_HOME):\n-- $ORACLE_HOME/OPatch/opatch lspatches","note":"v$version 배너가 버전을 보여 준다. dba_registry_sqlpatch는 적용된 Release Update(RU)와 one-off 패치가 SQL 계층에 등록되었는지를 보여 주며, 'opatch lspatches'는 홈의 바이너리 패치를 보여 준다.","danger":"safe","readonly":true,"priv":"v$version, dba_registry_sqlpatch에 대한 SELECT"},"postgres":{"command":"SELECT version();\n-- Numeric version only:\nSHOW server_version;\nSELECT current_setting('server_version_num');","note":"version()은 빌드 정보를 포함한 전체 텍스트를 반환한다. PostgreSQL에는 별도의 패치 번호가 없으며, 마이너 버전(예: 16.3)이 수정 수준을 나타낸다.","danger":"safe","readonly":true},"mysql":{"command":"SELECT VERSION();\n-- Detailed build information:\nSHOW VARIABLES LIKE 'version%';","note":"VERSION()은 예를 들어 '8.0.36'을 반환하며, 마지막 구성 요소가 패치(릴리스) 수준이다. version_comment 변수는 빌드가 Community/Enterprise인지 MariaDB인지를 나타낸다.","danger":"safe","readonly":true},"mssql":{"command":"SELECT @@VERSION;\n-- Structured version fields:\nSELECT SERVERPROPERTY('ProductVersion')       AS version,\n       SERVERPROPERTY('ProductLevel')         AS service_pack,\n       SERVERPROPERTY('ProductUpdateLevel')   AS cu_level,\n       SERVERPROPERTY('ProductUpdateReference') AS kb_number,\n       SERVERPROPERTY('Edition')              AS edition;","note":"ProductUpdateLevel은 적용된 Cumulative Update(예: CU12)를, ProductUpdateReference는 관련 KB 번호를 보여 준다. ProductLevel은 'RTM' 또는 이전 버전에서는 'SP'를 보여 준다.","danger":"safe","readonly":true},"mongodb":{"command":"db.version()\n// Detailed build information:\ndb.serverBuildInfo()\n// or: db.adminCommand({ buildInfo: 1 })","note":"db.version()은 버전 문자열(예: '7.0.11')을 반환한다. MongoDB에서는 수정 사항이 새 패치 버전으로 릴리스되며, 별도의 '패치 수준' 개념은 없다.","danger":"safe"},"redis":{"command":"INFO server\n# redis-cli one-liner: redis-cli INFO server | grep redis_version","note":"INFO server 출력의 redis_version 필드가 설치된 버전을 보여 준다. Redis에서는 수정 사항이 새 패치 버전(예: 7.2.5)으로 배포되며, redis_git_sha1 필드는 빌드 식별자도 보여 준다.","danger":"safe"},"elasticsearch":{"command":"GET /\n# curl: curl -s http://localhost:9200/","note":"루트 엔드포인트는 version.number(예: '8.14.1'), lucene_version, build_flavor를 반환한다. 모든 노드의 버전 확인: GET _cat/nodes?v&h=name,version","danger":"safe","readonly":true}}},{"id":"check-upgrade-readiness","title":"업그레이드 전 호환성/준비 상태 점검","aliases":["upgrade readiness","preupgrade","compatibility check","readiness","compatibility check","deprecation"],"engines":{"oracle":{"command":"-- Recommended method for 19c and later (AutoUpgrade analyze mode):\n-- $ java -jar autoupgrade.jar -config db.cfg -mode analyze\n-- Legacy/alternative: preupgrade script (produces a report only):\n-- $ $NEW_ORACLE_HOME/jdk/bin/java -jar $NEW_ORACLE_HOME/rdbms/admin/preupgrade.jar FILE TEXT DIR /tmp","note":"AutoUpgrade 'analyze' 모드는 데이터를 변경하지 않고 업그레이드 전 보고서를 생성하며, 수정해야 할 경고와 자동으로 해결될 항목을 나열한다. 실제 업그레이드 전에 반드시 실행한다.","danger":"safe"},"postgres":{"command":"# With the new version's binaries, compatibility check only (moves no data):\n$ pg_upgrade \\\n    --old-datadir=/var/lib/pgsql/16/data \\\n    --new-datadir=/var/lib/pgsql/17/data \\\n    --old-bindir=/usr/pgsql-16/bin \\\n    --new-bindir=/usr/pgsql-17/bin \\\n    --check","note":"--check는 데이터를 변경하지 않고 업그레이드 가능 여부를 검증한다(누락된 확장, 호환되지 않는 데이터 타입 등을 보고). --check는 기존 클러스터가 실행 중일 때도 실행할 수 있으며, 실제 업그레이드는 두 클러스터를 모두 중지한 상태에서 수행한다.","danger":"safe"},"mysql":{"command":"-- 8.0 and later: Upgrade Checker utility (MySQL Shell):\n-- $ mysqlsh -- util check-for-server-upgrade { --user=root --host=localhost }\n-- Table consistency for legacy 5.7->8.0:\n$ mysqlcheck --all-databases --check-upgrade -u root -p","note":"MySQL Shell의 util.checkForServerUpgrade()는 제거된 기능, 예약어 충돌, 호환되지 않는 정의를 보고한다. 8.0부터는 서버가 시작 시 딕셔너리 업그레이드를 직접 수행하므로 별도의 mysql_upgrade가 더 이상 필요 없다.","danger":"safe"},"mssql":{"command":"-- Run from the desktop with Data Migration Assistant (DMA):\n-- DmaCmd.exe /AssessmentName=... /AssessmentDatabases=...\n-- (Produces compatibility/deprecated feature and blocker reports.)","note":"Data Migration Assistant(DMA)는 기존 Upgrade Advisor를 대체했으며, 데이터베이스별로 deprecated 기능, breaking change, 차단 요소를 보고한다. 업그레이드 전에 실행하고 발견 사항을 해결한다.","danger":"safe"},"mongodb":{"command":"// Verify the current Feature Compatibility Version (FCV):\ndb.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })\n// Compatibility items (deprecations) in the release notes are reviewed manually.","note":"바이너리 버전을 업그레이드하기 전에 FCV가 현재 버전과 같아야 하며, 그렇지 않으면 업그레이드가 차단된다. MongoDB에는 자동 'deprecation 스캐너'가 없으므로 릴리스 호환성 노트를 수동으로 검토한다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no separate pre-upgrade readiness tool.\n# Checklist: read the target version's release notes for RDB/AOF persistence format,\n# changed configuration directives, and removed commands/arguments.","note":"Redis 업그레이드는 일반적으로 상위 호환된다. 공식 준비 상태 스캐너가 없으므로 RDB/AOF 파일 형식, 제거된 구성 지시어, 명령 변경 사항을 릴리스 노트에서 수동으로 확인한다.","danger":"safe"},"elasticsearch":{"command":"GET _migration/deprecations\n# For a specific component: GET <index>/_migration/deprecations","note":"Deprecation Info API는 다음 메이저 버전으로 이동할 때 문제를 일으킬 클러스터, 노드, 인덱스, ML 설정을 'critical' / 'warning'으로 나열한다. Kibana Upgrade Assistant는 같은 데이터를 시각화한다.","danger":"safe","readonly":true}}},{"id":"apply-patch","title":"패치 적용","aliases":["apply patch","patching","patch","minor upgrade","hotfix","ru apply"],"engines":{"oracle":{"command":"# 1) Shut down the database, apply the binary patch with OPatch:\n$ $ORACLE_HOME/OPatch/opatch apply\n# 2) Open the database and load the SQL layer changes:\n$ $ORACLE_HOME/OPatch/datapatch -verbose","note":"opatch apply는 Oracle Home의 바이너리를 갱신하고, 이어서 datapatch가 열린 데이터베이스에 딕셔너리/SQL 변경을 적용해 dba_registry_sqlpatch에 기록한다. RAC에서는 rolling 설치 가능한 패치를 노드별로 적용할 수 있다.","danger":"caution","priv":"OS: ORACLE_HOME 소유자, SQL: SYSDBA"},"postgres":{"command":"# Minor version patch = replace binaries + restart (no dump needed):\n$ sudo systemctl stop postgresql-16\n$ sudo dnf update postgresql16-server   # or apt upgrade\n$ sudo systemctl start postgresql-16","note":"같은 메이저 버전 계열 내의 마이너 패치(16.3 -> 16.4)는 데이터 형식을 바꾸지 않는다. 패키지를 갱신하고 서비스를 재시작하면 충분하며 pg_upgrade나 dump/restore는 필요 없다.","danger":"caution"},"mysql":{"command":"# Minor/patch update: update the package and restart the service\n$ sudo systemctl stop mysqld\n$ sudo dnf update mysql-community-server   # or apt upgrade\n$ sudo systemctl start mysqld","note":"MySQL 8.0+는 시작 시 데이터 딕셔너리를 자동으로 업그레이드하므로 별도의 mysql_upgrade 단계가 필요 없다. 복제 환경에서는 replica를 먼저 패치하고 소스를 나중에 패치한다.","danger":"caution"},"mssql":{"command":"-- Run the Cumulative Update (CU) or GDR security update installer:\n-- SQLServer2022-KB<no>-x64.exe /quiet /IAcceptSQLServerLicenseTerms\n--   /Action=Patch /InstanceName=MSSQLSERVER\n-- The instance then restarts; verify the version with SERVERPROPERTY.","note":"CU(Cumulative Update)와 GDR(General Distribution Release) 업데이트는 setup 설치 프로그램으로 적용하며 인스턴스 재시작이 필요하다. Always On 그룹에서는 secondary replica를 먼저 패치하고 페일오버하여 다운타임을 최소화한다.","danger":"caution"},"mongodb":{"command":"// 'Rolling' patch on a replica set: one node at a time\n// 1) Stop mongod, replace the binary with the new patch version, restart\n// 2) Secondaries first, the primary last\n// Step down the primary manually before patching it:\nrs.stepDown()","note":"같은 메이저 버전(7.0.x) 내의 패치 업데이트는 secondary부터 rolling 방식으로 수행한다. primary는 rs.stepDown()으로 역할을 넘긴 뒤 패치하여 쓰기 다운타임을 최소화한다.","danger":"caution"},"redis":{"command":"# Patch via rolling restart in a replicated deployment:\n# 1) Stop one replica node, install the new version, start it, wait for sync\n# 2) Repeat for the remaining replicas\n# 3) Finally, fail over the master and patch it:\nredis-cli -h <replica> REPLICAOF NO ONE   # promote the upgraded replica","note":"패치 버전(7.2.x)은 rolling restart로 적용한다. 먼저 replica를 업그레이드하고 재동기화한 뒤, replica를 master로 승격하고 기존 master를 패치한다. Sentinel/Cluster가 페일오버를 자동화한다.","danger":"caution"},"elasticsearch":{"command":"# Rolling upgrade (patch within the same major version), per node:\nPUT _cluster/settings\n{ \"persistent\": { \"cluster.routing.allocation.enable\": \"primaries\" } }\n# -> stop the node, update the package, start the node, then:\nPUT _cluster/settings\n{ \"persistent\": { \"cluster.routing.allocation.enable\": null } }","note":"패치/마이너 업그레이드는 rolling upgrade로 수행한다. 샤드 할당을 제한하고, 노드를 하나씩 갱신·재시작한 뒤 할당을 다시 활성화하고 클러스터가 green으로 돌아올 때까지 기다린다.","danger":"caution"}}},{"id":"upgrade-major","title":"메이저 버전 업그레이드","aliases":["major upgrade","major version","version upgrade","release upgrade","version upgrade"],"engines":{"oracle":{"command":"# Recommended: AutoUpgrade (analyze + fixups + deploy in a single flow):\n$ java -jar $NEW_ORACLE_HOME/rdbms/admin/autoupgrade.jar -config db.cfg -mode deploy\n# Graphical alternative: DBUA (Database Upgrade Assistant)\n$ $NEW_ORACLE_HOME/bin/dbua","note":"AutoUpgrade는 분석, 사전 수정(pre-fixups), 업그레이드, 사후 단계를 단일 명령으로 실행하며, 다운타임을 줄이기 위해 여러 데이터베이스를 순차적으로 처리할 수 있다. DBUA는 같은 작업을 안내형 인터페이스로 수행한다. 사전 전체 백업은 필수다.","danger":"danger","priv":"SYSDBA + OS: 새 ORACLE_HOME 소유자"},"postgres":{"command":"# pg_upgrade to the new major version (with both clusters stopped):\n$ pg_upgrade \\\n    --old-datadir=/var/lib/pgsql/16/data \\\n    --new-datadir=/var/lib/pgsql/17/data \\\n    --old-bindir=/usr/pgsql-16/bin \\\n    --new-bindir=/usr/pgsql-17/bin \\\n    --link   # fast migration with hard links\n# Then gather statistics (analyze_new_cluster.sh was removed in PG14):\n$ vacuumdb --all --analyze-in-stages","note":"pg_upgrade는 메이저 버전 이전(16 -> 17)을 in-place로 수행한다. --link 모드는 파일을 복사하는 대신 하드 링크를 만들어 시간을 단축하지만 기존 클러스터를 사용할 수 없게 만든다. 이전 후 통계를 다시 수집해야 한다.","danger":"danger"},"mysql":{"command":"# In-place upgrade (same datadir, new binaries):\n$ sudo systemctl stop mysqld\n$ sudo dnf install mysql-community-server-8.4   # replacing the old package\n$ sudo systemctl start mysqld   # the server upgrades the dictionary automatically","note":"MySQL 8.x부터 in-place 메이저 업그레이드를 지원한다. 새 바이너리로 시작한 서버가 데이터 딕셔너리와 시스템 테이블을 자동으로 변환한다(기존의 별도 mysql_upgrade 단계는 제거됨). 한 번에 메이저 버전 하나씩 업그레이드한다.","danger":"danger"},"mssql":{"command":"-- In-place major version upgrade (with setup):\n-- setup.exe /q /ACTION=upgrade /INSTANCENAME=MSSQLSERVER\n--   /IAcceptSQLServerLicenseTerms\n-- Alternative: install a new instance side by side and move the databases (restore/attach).","note":"setup /ACTION=upgrade는 인스턴스를 새 메이저 버전으로 in-place 업그레이드한다. 더 안전한 방법은 side-by-side로 설치한 뒤 backup/restore로 데이터베이스를 옮기고, 별도 단계에서 호환성 수준을 올리는 것이다.","danger":"danger"},"mongodb":{"command":"// Stepwise major upgrade (e.g. 6.0 -> 7.0), one major version at a time:\n// 1) Roll the binary to 7.0 on all nodes\n// 2) Once the cluster is stable, raise the FCV:\ndb.adminCommand({ setFeatureCompatibilityVersion: \"7.0\", confirm: true })","note":"MongoDB의 메이저 업그레이드는 두 단계로 이루어진다. 먼저 모든 노드의 바이너리를 새 버전으로 rolling 교체하고, 클러스터가 안정되면 FCV를 새 버전으로 올린다. 한 번에 메이저 버전 하나씩만 올릴 수 있으며, 다운그레이드 가능성을 위해 FCV는 별도로 관리된다.","danger":"danger"},"redis":{"command":"# Major upgrade via a replica (no downtime):\n# 1) Create a replica on the new version, wait for full sync from the master\n# 2) Prepare the application; fail over the master:\nredis-cli -h <new-replica> REPLICAOF NO ONE   # make the new version the master\n# 3) Upgrade the old master, attach it as a replica of the new master","note":"Redis 메이저 버전 이전(6.x -> 7.x)은 새 버전으로 replica를 구성해 동기화한 뒤 master로 승격하는 방식으로 수행한다. 이를 통해 쓰기 다운타임을 거의 0으로 줄인다. RDB 파일은 이후 버전과 호환된다.","danger":"danger"},"elasticsearch":{"command":"# Major versions cannot be skipped; one major at a time, via the last minor first (e.g. 6.8 -> 7.17 -> 8.x):\n# 1) Upgrade to the last minor (7.17), resolve the GET _migration/deprecations warnings\n# 2) Rolling upgrade, per node: set allocation to \"primaries\" -> update the node -> start -> wait for green\n# Alternative: full-cluster restart (stop all nodes -> upgrade -> start together); this also moves only one major","note":"Elasticsearch는 어떤 방법으로도 메이저 버전 건너뛰기를 지원하지 않는다. rolling upgrade와 full-cluster restart 모두 다음 메이저(n -> n+1)로만 이동하며, 소스가 마지막 마이너(예: 7.17)여야 한다. 두 메이저 이전(예: 6.x)에 생성된 인덱스는 새 버전에서 열 수 없으므로 이전 전에 _reindex로 재구축해야 한다(8.x는 이를 read-only archive index로 제한적으로만 읽을 수 있음). 이전 전에 스냅샷을 생성하고 deprecation 경고를 해결한다.","danger":"danger"}}},{"id":"set-compatibility","title":"호환성 수준 / 기능 버전 설정","aliases":["compatibility level","database compatibility level","feature version","fcv","compatible","compat level"],"engines":{"oracle":{"command":"-- Raise the COMPATIBLE initialization parameter (irreversible!):\nALTER SYSTEM SET COMPATIBLE = '19.0.0' SCOPE=SPFILE;\n-- Then restart the instance. To see the current value:\nSHOW PARAMETER compatible;","note":"COMPATIBLE 파라미터는 어느 버전의 기능과 파일 형식을 활성화할지 결정한다. 한 번 올리고 인스턴스를 재시작하면 되돌릴 수 없다(다운그레이드 차단). 따라서 업그레이드 성공이 확인될 때까지 단계적으로 올린다.","danger":"danger","priv":"ALTER SYSTEM(SYSDBA)"},"postgres":{"command":"# Not applicable: PostgreSQL has no database compatibility level.\n# If legacy behavior is needed, set the relevant GUCs (e.g. SET standard_conforming_strings=off)\n# or manage the major migration with dump/restore.","note":"PostgreSQL에는 SQL Server/Oracle 같은 '호환성 수준' 개념이 없으며, 버전 동작은 바이너리 버전에 직접 묶여 있다. 버전 간 이전은 pg_dump/pg_restore 또는 pg_upgrade로 수행한다.","danger":"safe"},"mysql":{"command":"# Not applicable: MySQL has no configurable database compatibility level.\n# Behavioral differences are managed with sql_mode:\nSET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';","note":"MySQL에는 버전 기반 호환성 수준이 없으며, SQL 동작은 주로 바이너리 버전과 sql_mode 설정에 따라 달라진다. 레거시 애플리케이션 동작을 유지하려면 sql_mode를 세밀하게 조정한다.","danger":"caution"},"mssql":{"command":"-- Set the database compatibility level (e.g. SQL Server 2022 = 160):\nALTER DATABASE Sales SET COMPATIBILITY_LEVEL = 160;\n-- To see the current value:\nSELECT name, compatibility_level FROM sys.databases;","note":"COMPATIBILITY_LEVEL은 쿼리 옵티마이저와 언어 기능이 어느 버전처럼 동작할지 결정한다(150=2019, 160=2022). 인스턴스를 업그레이드한 후에도 데이터베이스는 이전 수준에 머무를 수 있으며, 단계적으로 올리면서 실행 계획 회귀를 모니터링하는 것이 권장된다. 다시 낮출 수 있다.","danger":"caution"},"mongodb":{"command":"// Set the Feature Compatibility Version (FCV):\ndb.adminCommand({ setFeatureCompatibilityVersion: \"7.0\", confirm: true })\n// Read the current value:\ndb.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })","note":"FCV는 버전과 함께 도입되는 영구 데이터 형식 변경과 새 기능이 언제 활성화될지 제어한다. 바이너리 업그레이드 후 클러스터가 안정된 상태에서 올린다. 낮추는 것은 같은 바이너리 버전이 낮은 FCV를 계속 지원하는 경우에만 가능하다.","danger":"caution","priv":"clusterManager / root 롤"},"redis":{"command":"# Not applicable: Redis has no compatibility level / feature version concept.\n# Behavior depends directly on the running binary version.","note":"Redis에는 설정 가능한 호환성 수준이 없으며, 기능 집합은 전적으로 실행 중인 버전에 따라 달라진다. 호환성은 RDB/AOF 파일 형식 수준에서 상위 방향으로만 유지된다.","danger":"safe"},"elasticsearch":{"command":"# Not applicable: Elasticsearch has no configurable compatibility level.\n# An index is tied to the format of the version it was created in; very old indices\n# cannot be read in the new version and require a reindex.","note":"Elasticsearch에는 호환성 수준 설정이 없으며, 각 인덱스는 생성된 메이저 버전의 형식을 가진다. 이전 메이저 버전에서 생성된 인덱스는 읽을 수 있지만, 그보다 오래된 인덱스는 _reindex로 새 버전에 옮겨야 한다.","danger":"safe"}}},{"id":"post-upgrade-tasks","title":"업그레이드 후 작업(recompile/analyze)","aliases":["post upgrade","post-upgrade","recompile","analyze","refresh statistics","invalid objects"],"engines":{"oracle":{"command":"-- 1) Compile invalid objects in parallel:\n@?/rdbms/admin/utlrp.sql\n-- 2) Refresh dictionary/fixed object statistics:\nEXEC DBMS_STATS.GATHER_DICTIONARY_STATS;\nEXEC DBMS_STATS.GATHER_FIXED_OBJECTS_STATS;\n-- Check remaining invalid objects:\nSELECT owner, object_name, object_type FROM dba_objects WHERE status='INVALID';","note":"utlrp.sql은 업그레이드 후 INVALID가 된 패키지/뷰/트리거를 재컴파일한다. 이후 딕셔너리 및 fixed object 통계를 수집하면 옵티마이저가 새 버전에서 올바른 실행 계획을 생성한다.","danger":"caution","priv":"SYSDBA"},"postgres":{"command":"# 1) Regather statistics after pg_upgrade (mandatory):\n$ vacuumdb --all --analyze-in-stages\n-- 2) Bring extensions to the new version:\nALTER EXTENSION postgis UPDATE;\n-- 3) If the collation changed, rebuild text indexes (can take long):\nREINDEX DATABASE database_name;","note":"pg_upgrade(PG17 이하)는 플래너 통계를 이전하지 않는다. analyze-in-stages는 먼저 대략적인 통계를, 이어서 전체 통계를 수집해 시스템을 빠르게 사용 가능하게 만든다. 확장은 ALTER EXTENSION ... UPDATE로, collation이 바뀐 경우 인덱스는 REINDEX로 갱신해야 한다.","danger":"caution"},"mysql":{"command":"-- 8.0+ dictionary upgrade happens automatically at startup; additionally:\nANALYZE TABLE customer;   -- refresh statistics of critical tables\n-- Legacy 5.7 -> if needed on first startup:\n-- $ mysql_upgrade -u root -p   (before 8.0.16)","note":"MySQL 8.0.16 이후에는 mysql_upgrade 기능이 서버로 옮겨져 별도로 실행하지 않는다. 업그레이드 후 대형/핵심 테이블에 ANALYZE TABLE로 통계를 갱신하면 실행 계획 품질이 유지된다.","danger":"safe"},"mssql":{"command":"-- 1) Correct page/row usage counters:\nDBCC UPDATEUSAGE(Sales);\n-- 2) Refresh statistics (sp_updatestats uses sampling; FULLSCAN for critical tables):\nEXEC sp_updatestats;\n-- UPDATE STATISTICS dbo.CriticalTable WITH FULLSCAN;\n-- 3) Raise the compatibility level to the new version (while monitoring):\nALTER DATABASE Sales SET COMPATIBILITY_LEVEL = 160;","note":"업그레이드 후 DBCC UPDATEUSAGE는 이전 버전에서 넘어온 부정확한 공간/행 카운터를 바로잡고, sp_updatestats는 통계를 갱신한다. 호환성 수준을 올릴 때는 새 Cardinality Estimator(CE)가 유발할 수 있는 실행 계획 회귀를 모니터링한다.","danger":"caution"},"mongodb":{"command":"// 1) Verify the FCV upgrade:\ndb.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })\n// 2) Check indexes and compatibility warnings in the server logs:\ndb.customer.getIndexes()\n// If an index is incompatible: dropIndex + createIndex (rolling); do NOT use reIndex","note":"메이저 업그레이드 후 FCV가 목표 버전에 도달했는지 확인한다. 인덱스 재구축은 일상적인 업그레이드 단계가 아니다. 릴리스 노트에 언급되었거나 로그에 경고를 남기는 인덱스는 dropIndex + createIndex로 다시 생성한다. 6.0+에서 reIndex는 standalone에서만 동작하며 replica set 멤버에서는 허용되지 않는다.","danger":"caution"},"redis":{"command":"# Usually no extra task is needed. Recommended: verify persistence and replication:\nredis-cli INFO replication   # is the master/replica link healthy\nredis-cli INFO persistence   # rdb_last_bgsave_status / aof_last_bgrewrite_status\nredis-cli DBSIZE             # is the key count as expected","note":"Redis 업그레이드 후에는 스키마 compile/analyze 작업이 없다. 복제가 다시 수립되었는지, RDB/AOF 영속성이 정상인지, 키 수가 유지되었는지 확인하는 것이 작업이다.","danger":"safe"},"elasticsearch":{"command":"# Verify cluster health:\nGET _cluster/health\n# Move indices created in an old major version that can no longer be read:\nPOST _reindex\n{ \"source\": { \"index\": \"old_index\" }, \"dest\": { \"index\": \"new_index\" } }","note":"업그레이드 후 클러스터 상태가 'green'이고 모든 샤드가 할당되었는지 확인한다. 이전 메이저 버전보다 오래된 형식의 인덱스는 새 버전에서 읽을 수 없으므로 _reindex로 현재 형식으로 옮겨야 한다.","danger":"caution"}}}]},"rosetta/migration.json":{"category":"마이그레이션 및 이전","categoryId":"migration","order":15,"concepts":[{"id":"compare-schema","title":"두 스키마/환경 비교(schema diff)","aliases":["schema diff","diff","compare schemas","compare","schema difference","ddl diff"],"engines":{"oracle":{"command":"-- Extract the schema DDL from both environments and take a text diff\n-- (generate deterministic DDL with DBMS_METADATA)\nEXEC DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,'SQLTERMINATOR',TRUE);\nEXEC DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,'STORAGE',FALSE);\nSELECT DBMS_METADATA.GET_DDL('TABLE', table_name) FROM user_tables ORDER BY table_name;\n-- Spool the output to a file in both environments and compare with diff/fc.\n\n-- Export the schema as a changelog with SQLcl's Liquibase integration:\n-- SQL> lb generate-schema\n-- Commercial: the Oracle SQL Developer 'Database Diff' tool is also used.","note":"Oracle에는 단일 명령으로 스키마를 비교하는 기본 기능이 없다. 일반적인 방법은 두 환경에서 DBMS_METADATA.GET_DDL로(STORAGE/세그먼트 속성을 끈 상태로) 결정적인 DDL을 생성한 뒤 텍스트 diff를 하는 것이다. 시각적/자동 비교에는 SQL Developer의 Database Diff 마법사나 SQLcl을 사용한다.","danger":"safe","readonly":true,"priv":"SELECT_CATALOG_ROLE 또는 스키마 읽기 권한"},"postgres":{"command":"# Dump only the schema from both databases and diff them\npg_dump --schema-only --no-owner --no-privileges sourcedb > source_schema.sql\npg_dump --schema-only --no-owner --no-privileges targetdb  > target_schema.sql\ndiff -u source_schema.sql target_schema.sql\n\n# Tool that generates applicable ALTERs: apgdiff (Another PostgreSQL Diff Tool)\n# apgdiff source_schema.sql target_schema.sql > migration.sql\n# Alternatives: migra, Liquibase diffChangeLog","note":"가장 간단한 방법은 두 환경에서 pg_dump --schema-only 출력을 받아 diff로 비교하는 것이다. apgdiff와 migra는 차이를 바로 실행 가능한 ALTER 문으로 변환하는 도구다. 결정적인 출력을 위해 --no-owner --no-privileges를 추가한다.","danger":"safe","readonly":true,"priv":"SELECT / 스키마 읽기(pg_dump용 객체 접근 권한)"},"mysql":{"command":"# Schema/object comparison with MySQL Utilities (5.x):\nmysqldbcompare --server1=root:password@host1 --server2=root:password@host2 \\\n  shop:shop --difftype=sql --run-all-tests\n\n# Built-in method: take CREATE statements from both environments and diff them\nmysqldump --no-data --skip-comments shop > source_schema.sql\n# SHOW CREATE TABLE customer;  (to see a single table definition)","note":"MySQL Utilities의 mysqldbcompare 도구는 두 서버 간 객체/스키마 차이를 보고한다(difftype=sql이면 적용 가능한 문으로 출력). 도구 없이 하는 방법은 두 환경에서 mysqldump --no-data(스키마만) 또는 SHOW CREATE TABLE 출력을 받아 diff로 비교하는 것이다. mysqldbcompare는 현재 deprecated이며, MariaDB/8.x에는 Liquibase나 Skeema가 권장된다.","danger":"safe","readonly":true,"priv":"두 스키마에 대한 SELECT, SHOW VIEW"},"mssql":{"command":"-- Schema comparison/deploy report with SqlPackage (what will change before applying):\nsqlpackage /Action:DeployReport /SourceFile:source.dacpac \\\n  /TargetConnectionString:\"Server=target;Database=shop;Integrated Security=true\" \\\n  /OutputPath:diff_report.xml\n\n-- For data differences (row level), the tablediff utility:\ntablediff -sourceserver S1 -sourcedatabase shop -sourcetable customer \\\n  -destinationserver S2 -destinationdatabase shop -destinationtable customer","note":"스키마 차이는 SqlPackage /Action:DeployReport가 대상에 적용될 변경 사항을 XML 보고서로 반환하고, /Action:Script는 실행 가능한 diff 스크립트를 생성한다. Visual Studio / Azure Data Studio의 'Schema Compare'도 같은 엔진을 사용한다. 행 수준 데이터 차이에는 tablediff 명령줄 유틸리티가 있다.","danger":"safe","readonly":true,"priv":"VIEW DEFINITION(DeployReport는 대상에 대한 읽기 권한)"},"mongodb":{"command":"// The schema is implicit; extract and compare the field structure of both environments.\n// Read the $jsonSchema validator definition (collection rules):\ndb.getCollectionInfos({ name: \"customer\" })[0].options.validator\n\n// External tools for inferred schema comparison by sampling:\n// mongodb-schema (npm) or the Compass 'Schema' tab\n// Take samples with mongoexport and diff the field/type distribution of both environments.","note":"MongoDB는 스키마리스이므로 고정 DDL이 없다. 비교는 두 가지 방법으로 한다. 컬렉션에 정의된 $jsonSchema 검증 규칙을 비교하거나(getCollectionInfos 사용), mongodb-schema / Compass로 데이터에서 추론한 스키마를 생성해 필드와 타입 분포를 비교한다. 인덱스는 db.collection.getIndexes() 출력을 diff한다.","danger":"safe","readonly":true,"priv":"read(getCollectionInfos, listIndexes)"},"redis":{"command":"# Not applicable: Redis is a schemaless key-value store; there are no\n# table/column definitions (DDL) to compare.\n# Closest equivalent: compare the key space (namespace) structure\n# redis-cli --scan --pattern 'customer:*' | sort > source_keys.txt","note":"해당 없음: Redis에는 스키마 개념(테이블/컬럼/DDL)이 없으므로 고전적인 schema diff가 적용되지 않는다. 구조 비교가 필요하면 SCAN으로 키 명명 패턴(namespace)만 나열해 비교할 수 있다.","danger":"safe","readonly":true},"elasticsearch":{"command":"# Fetch the index mapping from both clusters and compare\ncurl -s 'http://source:9200/customer/_mapping?pretty' > source_mapping.json\ncurl -s 'http://target:9200/customer/_mapping?pretty'  > target_mapping.json\ndiff -u source_mapping.json target_mapping.json\n\n# To compare settings as well:\n# GET customer/_settings   and   GET customer/_mapping","note":"Elasticsearch에서 스키마에 해당하는 것은 인덱스 mapping이다. 표준 방법은 두 클러스터에서 GET <index>/_mapping 출력(필요하면 _settings도)을 받아 diff로 비교하는 것이다. mapping 필드는 대부분 변경할 수 없으므로 차이가 발견되면 새 인덱스 + _reindex가 필요할 수 있다.","danger":"safe","readonly":true,"priv":"view_index_metadata / monitor"}}},{"id":"export-for-migration","title":"마이그레이션용 일관된 export","aliases":["export","consistent export","consistent export","take a dump","migration export","snapshot"],"engines":{"oracle":{"command":"# Consistent logical export with Data Pump (at a single SCN)\nexpdp system/password schemas=shop directory=DIR_DMP \\\n  dumpfile=shop_%U.dmp logfile=shop_exp.log \\\n  flashback_scn=<scn> parallel=4\n\n# Current SCN for point-in-time consistency:\n# SELECT current_scn FROM v$database;\n# Alternative: FLASHBACK_TIME=SYSTIMESTAMP","note":"Data Pump(expdp)는 마이그레이션을 위한 표준 논리 export 도구다. 읽기 일관성을 위해 FLASHBACK_SCN=<scn>(또는 FLASHBACK_TIME)을 지정하면 모든 테이블이 단일 SCN 기준으로 일관되게 export된다. 운영 중인 시스템에서는 이것이 매우 중요하다. PARALLEL은 속도를 높이고, %U는 여러 파일을 생성한다.","danger":"caution","readonly":true,"priv":"DATAPUMP_EXP_FULL_DATABASE, DIRECTORY 객체","minver":"10g(Data Pump)"},"postgres":{"command":"# Single compressed file in custom format (restorable in parallel)\npg_dump -Fc -Z6 -f shop.dump shop\n\n# Directory format (parallel dump + parallel restore):\npg_dump -Fd -j4 -f shop_dir shop\n\n# For the whole cluster + roles/tablespaces:\n# pg_dumpall --globals-only > roles.sql","note":"마이그레이션에는 pg_dump -Fc(custom) 또는 -Fd(directory) 형식이 권장되며, 둘 다 pg_restore로 선택적/병렬 적재가 가능하다. pg_dump는 단일 트랜잭션 안에서 실행되므로 출력이 자동으로 일관된 스냅샷이 된다. 롤과 테이블스페이스 같은 클러스터 전체 객체는 pg_dumpall --globals-only로 별도 덤프한다.","danger":"safe","readonly":true,"priv":"SELECT(덤프 대상 모든 객체)"},"mysql":{"command":"# Consistent, lock-free export for InnoDB\nmysqldump --single-transaction --quick --routines --triggers --events \\\n  --source-data=2 shop > shop.sql\n\n# Note: older versions use --master-data=2 instead of --source-data.\n# Parallel, fast alternative: mydumper -B shop -o ./backup --threads 4","note":"InnoDB에서 --single-transaction은 단일 일관된 스냅샷을 제공하며 테이블에 락을 걸지 않는다. --source-data=2(이전 --master-data=2)는 복제/CDC를 시작하는 데 필요한 binlog 위치/GTID 정보를 출력 맨 위에 주석으로 기록한다. 무중단 전환에 필수다. 대용량 데이터에는 mydumper가 병렬로 동작한다.","danger":"safe","readonly":true,"priv":"SELECT, LOCK TABLES, RELOAD, REPLICATION CLIENT(--source-data용)"},"mssql":{"command":"-- Full backup for physical migration (to the same/compatible version):\nBACKUP DATABASE shop TO DISK = 'C:\\backup\\shop.bak' WITH COMPRESSION, CHECKSUM;\n\n-- Portable/version-independent logical export (BACPAC = schema+data):\n-- sqlpackage /Action:Export /SourceConnectionString:\"...\" /TargetFile:shop.bacpac\n\n-- For a single table: bcp shop.dbo.customer out customer.dat -n -S server -T","note":"같은 버전 또는 더 새 버전의 SQL Server로 마이그레이션할 때는 BACKUP ... RESTORE가 가장 신뢰할 수 있는 경로다(CHECKSUM으로 무결성 검증). 버전/플랫폼에 독립적인 마이그레이션이나 Azure SQL로의 이전에는 SqlPackage /Action:Export로 스키마+데이터를 단일 .bacpac 파일로 내보내는 방법이 권장된다. 개별 테이블에는 bcp를 사용한다.","danger":"safe","readonly":true,"priv":"BACKUP DATABASE(db_backupoperator) 또는 Export용 읽기 권한"},"mongodb":{"command":"# Consistent binary (BSON) export; point-in-time consistency with the oplog\nmongodump --uri=\"mongodb://source:27017\" \\\n  --oplog --out=./backup --numParallelCollections=4\n\n# --oplog captures changes made during the dump (requires a replica set);\n# it works only with a FULL dump and cannot be combined with --db / --collection.\n# On restore, mongorestore --oplogReplay brings it to a single point in time.\n# If you need a single database: take it with --db=shop (without the oplog) or use --nsInclude='shop.*' on restore.","note":"mongodump는 마이그레이션을 위한 표준 바이너리(BSON) export다. replica set에서 --oplog 옵션은 덤프 실행 중 기록된 변경을 oplog에서 캡처해 일관된 시점을 제공한다(mongorestore --oplogReplay와 함께 사용). 이 옵션은 전체 덤프에서만 유효하다. 대형 클러스터의 클러스터 수준 마이그레이션에는 mongosync/라이브 도구가 더 적합하다.","danger":"safe","readonly":true,"priv":"read(oplog용 local db에 대한 read / clusterMonitor)"},"redis":{"command":"# Create an RDB snapshot in the background (non-blocking)\nredis-cli BGSAVE\n# Check for completion:\nredis-cli INFO persistence | grep rdb_bgsave_in_progress\n\n# Move the resulting file (path: CONFIG GET dir + dbfilename)\n# e.g. /var/lib/redis/dump.rdb  ->  copy to the target server","note":"Redis에서 일관된 export는 RDB 스냅샷이다. BGSAVE는 fork를 통해 메인 프로세스를 블로킹하지 않고 백그라운드에서 일관된 dump.rdb를 생성한다(SAVE는 블로킹하므로 사용하지 않는다). 파일 경로는 CONFIG GET dir과 dbfilename으로 확인하며, 이 dump.rdb를 대상 서버의 데이터 디렉터리로 복사해 이전한다.","danger":"safe","readonly":true},"elasticsearch":{"command":"# 1) Register a snapshot repository (shared file system / S3)\nPUT _snapshot/migration_repo\n{ \"type\": \"fs\", \"settings\": { \"location\": \"/mnt/es_backup\" } }\n\n# 2) Take a consistent snapshot\nPUT _snapshot/migration_repo/migration_1?wait_for_completion=true\n{ \"indices\": \"customer,orders\", \"include_global_state\": false }","note":"Elasticsearch에서 마이그레이션을 위한 일관된 export 방법은 스냅샷이다. 먼저 리포지토리(fs, S3, GCS 등)를 등록한 뒤 스냅샷을 생성한다. 스냅샷은 증분 방식이며 생성 시점의 일관된 상태를 캡처한다. 소량 데이터/단일 인덱스에는 elasticdump도 사용할 수 있지만 표준은 스냅샷이다.","danger":"safe","readonly":true,"priv":"manage / create_snapshot"}}},{"id":"import-for-migration","title":"대상으로 import","aliases":["import","import data","restore","load","load into target","load"],"engines":{"oracle":{"command":"# Import with Data Pump; remap schema and tablespace\nimpdp system/password directory=DIR_DMP dumpfile=shop_%U.dmp \\\n  logfile=shop_imp.log \\\n  remap_schema=shop:shop_new \\\n  remap_tablespace=USERS:DATA parallel=4\n\n# For existing objects: TABLE_EXISTS_ACTION=TRUNCATE|APPEND|REPLACE","note":"impdp는 expdp로 받은 .dmp 파일을 대상에 적재한다. REMAP_SCHEMA는 소스를 다른 스키마로, REMAP_TABLESPACE는 다른 테이블스페이스로 옮기며, 환경 간 마이그레이션에서 자주 사용된다. 기존 테이블에 대한 동작은 TABLE_EXISTS_ACTION으로, 속도는 PARALLEL로 지정한다.","danger":"caution","priv":"DATAPUMP_IMP_FULL_DATABASE, 대상 테이블스페이스 쿼터"},"postgres":{"command":"# Parallel restore (from custom/directory format)\npg_restore -j4 -d targetdb --no-owner --clean --if-exists shop.dump\n\n# A specific table only: pg_restore -d targetdb -t customer shop.dump\n# For a plain .sql dump:  psql -d targetdb -f shop.sql","note":"pg_restore -j는 custom/directory 형식 덤프를 병렬(다중 프로세스)로 적재하며, 대형 데이터베이스에서 적재 속도를 크게 높인다. --clean --if-exists는 대상의 객체를 먼저 삭제하고, --no-owner는 다른 롤 구조에 맞춘다. 일반 SQL 덤프는 psql -f로 적재한다(병렬 아님).","danger":"caution","priv":"대상 데이터베이스에 대한 CREATE / 소유자 권한"},"mysql":{"command":"# Load a plain SQL dump\nmysql --binary-mode shop < shop.sql\n\n# Parallel/fast load (if taken with mydumper):\nmyloader --directory=./backup --database=shop --threads=4 --overwrite-tables\n\n# Tip: SET FOREIGN_KEY_CHECKS=0; unique_checks=0 before loading speeds it up.","note":"mysqldump SQL 덤프는 mysql 클라이언트로 리다이렉트하여 적재한다(mysql shop < dump.sql). 병렬로 훨씬 빠르게 적재하려면 mydumper로 받은 디렉터리를 myloader --threads로 복원한다. 적재 중 FOREIGN_KEY_CHECKS와 UNIQUE_CHECKS를 비활성화하면 성능이 향상된다.","danger":"caution","priv":"INSERT, CREATE(--overwrite/--clean에는 DROP도)"},"mssql":{"command":"-- Restore from a physical backup (move file paths to the new environment)\nRESTORE DATABASE shop FROM DISK = 'C:\\backup\\shop.bak'\nWITH MOVE 'shop_data' TO 'D:\\data\\shop.mdf',\n     MOVE 'shop_log'  TO 'D:\\log\\shop.ldf', REPLACE;\n\n-- Import from a BACPAC (schema+data):\n-- sqlpackage /Action:Import /TargetConnectionString:\"...\" /SourceFile:shop.bacpac","note":".bak 백업은 RESTORE DATABASE로 적재한다. 다른 디스크 구성으로 옮길 때는 WITH MOVE로 데이터/로그 파일을 새 경로로 지정한다. 버전 독립/Azure SQL 대상에는 SqlPackage /Action:Import가 .bacpac 파일(스키마 + 데이터)을 새 데이터베이스로 생성한다.","danger":"caution","priv":"RESTORE는 dbcreator/sysadmin, Import는 CREATE DATABASE"},"mongodb":{"command":"# Restore the BSON backup; replay the oplog to land on a single point in time\nmongorestore --uri=\"mongodb://target:27017\" --oplogReplay \\\n  --numParallelCollections=4 ./backup\n\n# Renaming: --nsFrom='shop.*' --nsTo='shop_new.*'\n# Drop existing data before loading: --drop","note":"mongorestore는 mongodump BSON 출력을 대상에 적재한다. --oplogReplay는 --oplog로 받은 백업의 oplog 변경을 재생해 덤프를 단일 일관된 시점으로 맞춘다. 데이터베이스/컬렉션 이름은 --nsFrom/--nsTo로 바꾸고, 기존 데이터는 --drop으로 지울 수 있다.","danger":"caution","priv":"readWrite(oplogReplay는 restore/clusterAdmin)"},"redis":{"command":"# Stop the server, place dump.rdb in the data directory, start it again\n# 1) systemctl stop redis  (or SHUTDOWN NOSAVE)\n# 2) cp dump.rdb  <path found with CONFIG GET dir>/dump.rdb\n# 3) systemctl start redis   -> Redis loads the RDB into memory at startup\n\n# Alternative (live): feed a RESP command stream with redis-cli --pipe","note":"Redis는 시작 시에만 RDB를 적재한다. 따라서 대상 서버를 중지하고, 옮겨 온 dump.rdb를 데이터 디렉터리(CONFIG GET dir)에 둔 뒤 서버를 재시작한다. 시작 시 RDB에서 메모리가 채워진다. 서버를 중지하지 않고 적재하려면 redis-cli --pipe로 RESP 형식의 키를 스트리밍한다.","danger":"caution"},"elasticsearch":{"command":"# Register the same repository on the target cluster, then restore the snapshot\nPUT _snapshot/migration_repo\n{ \"type\": \"fs\", \"settings\": { \"location\": \"/mnt/es_backup\" } }\n\nPOST _snapshot/migration_repo/migration_1/_restore\n{ \"indices\": \"customer,orders\",\n  \"rename_pattern\": \"(.+)\", \"rename_replacement\": \"restored_$1\" }","note":"대상 클러스터에 소스와 같은 스냅샷 리포지토리를 등록한 뒤(파일에 접근 가능해야 함) _snapshot/.../_restore로 인덱스를 복원한다. 인덱스 이름은 rename_pattern/rename_replacement로 바꿀 수 있다. 기존 인덱스는 열린 상태에서 복원할 수 없으므로 먼저 닫거나 이름을 바꾼다.","danger":"caution","priv":"manage(create_index, restore)"}}},{"id":"heterogeneous-migration","title":"다른 데이터베이스로 마이그레이션(이기종)","aliases":["heterogeneous","heterogeneous migration","cross-database","different database","convert database","convert"],"engines":{"oracle":{"command":"# Oracle -> PostgreSQL: Ora2Pg (schema + data + PL/SQL conversion)\nora2pg -c ora2pg.conf -t TABLE   -o schema.sql\nora2pg -c ora2pg.conf -t COPY    -o data.sql   # data in COPY format\nora2pg -t SHOW_REPORT --estimate_cost           # migration cost estimate\n\n# Oracle -> (multiple targets) managed/online: AWS DMS or Oracle GoldenGate","note":"Oracle에서의 이기종 마이그레이션(특히 PostgreSQL로)에 가장 널리 쓰이는 오픈소스 도구는 Ora2Pg다. 테이블, 제약 조건, PL/SQL, 데이터를 대상 방언으로 변환하며, --estimate_cost로 마이그레이션 공수를 추정한다. 온라인/저다운타임 마이그레이션에는 AWS DMS나 Oracle GoldenGate(CDC 사용)를 사용한다.","danger":"caution","readonly":true,"priv":"소스 스키마에 대한 SELECT(DBA 뷰 읽기 권한)"},"postgres":{"command":"# MySQL -> PostgreSQL: pgloader (schema+data+type conversion in one command)\npgloader mysql://app_user:password@source/shop \\\n         postgresql://app_user:password@target/shop\n\n# Also from SQLite/MSSQL sources: with a pgloader ./migration.load script\n# For a managed/online PostgreSQL target: AWS DMS","note":"pgloader는 다른 엔진(MySQL, SQLite, MS SQL)에서 PostgreSQL로 마이그레이션하는 표준 도구다. 연결 문자열을 주면 단일 명령으로 스키마를 생성하고 타입을 매핑하며 데이터를 빠르게 복사한다. 사용자 정의 변환/타입 매핑은 .load 스크립트에 정의한다. 소스가 Oracle이면 Ora2Pg가, 온라인 마이그레이션에는 AWS DMS가 권장된다.","danger":"caution","priv":"대상에 대한 CREATE, 소스에 대한 SELECT"},"mysql":{"command":"# PostgreSQL/Oracle/MSSQL -> MySQL: AWS DMS (managed) is the most common path\n# or adapt the source-specific export for MySQL:\n#   pg_dump --inserts ... | (type/syntax fixes) | mysql shop\n\n# MySQL <-> MariaDB (compatible): mysqldump output loads directly\nmysqldump --single-transaction shop | mysql -h target_mariadb shop","note":"다른 엔진에서 MySQL로 마이그레이션하는 데 지배적인 단일 오픈 도구는 없다. 가장 일반적인 방법은 AWS DMS(관리형, CDC 지원)를 쓰거나 소스 덤프의 구문/타입을 수정해 mysql 클라이언트로 적재하는 것이다. MySQL과 MariaDB는 대부분 호환되므로 mysqldump 출력은 보통 그대로 이전된다.","danger":"caution","priv":"대상에 대한 CREATE, INSERT"},"mssql":{"command":"# SQL Server -> PostgreSQL: with pgloader\npgloader mssql://app_user:password@source/shop \\\n         postgresql://app_user:password@target/shop\n\n# SQL Server -> PostgreSQL compatibility layer: Babelfish for Aurora PostgreSQL\n# (runs T-SQL and the TDS protocol on top of PostgreSQL)\n# To other targets: SSMA (SQL Server Migration Assistant) or AWS DMS","note":"SQL Server에서 PostgreSQL로의 마이그레이션에는 pgloader(mssql:// 소스)를 사용한다. 애플리케이션을 바꾸지 않고 T-SQL을 유지하려면 Babelfish for Aurora PostgreSQL이 호환 계층을 제공한다. Microsoft의 SSMA 도구는 다른 엔진(Oracle, MySQL, DB2)에서 SQL Server로의 마이그레이션을 자동화한다. 온라인 마이그레이션에는 AWS DMS를 사용한다.","danger":"caution","priv":"소스에 대한 SELECT, 대상에 대한 CREATE"},"mongodb":{"command":"# Relational <-> MongoDB conversion requires schema mapping (ETL):\n#   - Relational Migrator (official MongoDB tool): maps tables to the document model\n#   - or generate JSON from the source -> load with mongoimport\nmongoexport --db=shop --collection=customer --type=csv --fields=id,name > c.csv\n# (To an SQL target: mongoexport CSV -> relational COPY/LOAD DATA)","note":"MongoDB와 SQL 엔진 간 마이그레이션에는 행<->문서 모델 변환(ETL)이 필요하며 자동화된 단일 명령은 없다. SQL에서 MongoDB로는 MongoDB Relational Migrator가 테이블을 문서 스키마로 매핑한다. 단순한 경우에는 소스에서 CSV/JSON을 생성해 mongoimport로 적재한다. 반대 방향에서는 mongoexport 출력을 COPY/LOAD DATA로 대상 관계형 테이블에 적재한다.","danger":"caution","priv":"소스에 대한 read, 대상에 대한 write"},"redis":{"command":"# With RIOT (Redis Input/Output Tools), to/from Redis from other sources:\n#   Relational -> Redis:\nriot db-import --jdbc-url \"jdbc:postgresql://host/shop\" \\\n  --sql \"SELECT * FROM customer\" hset --keyspace customer --keys id\n#   Redis -> another Redis / file:  riot replicate / riot file-export","note":"이기종 마이그레이션에서 Redis는 보통 대상/소스로서의 캐시 계층이다. 관계형 또는 파일 소스에서 Redis로 데이터를 적재하려면 Redis 공식 도구 RIOT를 사용한다(db-import는 SQL 결과를 HSET/SET 구조로 매핑). Redis에서 Redis로 또는 파일로는 riot replicate / file-export 명령이 있다.","danger":"caution"},"elasticsearch":{"command":"# Relational/other source -> Elasticsearch: Logstash (JDBC input)\n# logstash.conf: input { jdbc { statement => \"SELECT * FROM customer\" } }\n#                output { elasticsearch { index => \"customer\" } }\n\n# ES -> ES (version/cluster upgrade): reindex from a remote source\nPOST _reindex\n{ \"source\": { \"remote\": { \"host\": \"http://old:9200\" }, \"index\": \"customer\" },\n  \"dest\": { \"index\": \"customer\" } }","note":"다른 데이터베이스에서 Elasticsearch로 마이그레이션하는 가장 일반적인 도구는 Logstash(JDBC/file input -> elasticsearch output)이며, Elastic 자체의 connector/Beats도 사용된다. 기존 Elasticsearch 클러스터에서 새 클러스터로 옮길 때는 remote reindex('remote' 소스를 지정한 _reindex)가 일반적이며, 대상에 reindex.remote.whitelist를 정의해야 한다.","danger":"caution","priv":"대상에 대한 write/create_index"}}},{"id":"setup-cdc-replication","title":"지속적 복제 / CDC 구성(무중단 전환)","aliases":["cdc","replication","replication","zero-downtime migration","live migration","change data capture"],"engines":{"oracle":{"command":"# CDC-based replication with Oracle GoldenGate (outline)\n# 1) Enable supplemental logging on the source:\nALTER DATABASE ADD SUPPLEMENTAL LOG DATA;\n# 2) Extract (capture changes from redo) and Replicat (apply to the target) processes:\n#   GGSCI> ADD EXTRACT ext1, INTEGRATED TRANLOG, BEGIN NOW\n#   GGSCI> ADD REPLICAT rep1, INTEGRATED\n# Alternative: initial load with Data Pump + incremental capture with GoldenGate","note":"Oracle에서 무중단/이기종 CDC의 표준 도구는 Oracle GoldenGate다. Extract 프로세스가 redo log에서 변경을 캡처하고 Replicat이 이를 대상에 적용하며, supplemental logging을 활성화해야 한다. 일반적인 마이그레이션: 초기 적재는 Data Pump로 수행하고, 이후 GoldenGate가 변경을 대상으로 스트리밍하여 다운타임이 거의 없는 전환을 가능하게 한다.","danger":"caution","priv":"GoldenGate 관리자 권한, supplemental logging"},"postgres":{"command":"-- Source (publisher): create a publication (wal_level must be logical)\nCREATE PUBLICATION migration_pub FOR ALL TABLES;\n\n-- Target (subscriber): create a subscription (initial copy + continuous stream)\nCREATE SUBSCRIPTION migration_sub\n  CONNECTION 'host=source dbname=shop user=repl password=***'\n  PUBLICATION migration_pub;","note":"PostgreSQL은 내장 논리 복제로 CDC를 제공한다. 소스에서 CREATE PUBLICATION, 대상에서 CREATE SUBSCRIPTION을 실행한다. 구독이 시작되면 기존 데이터를 복사(initial copy)한 뒤 WAL에서 변경을 지속적으로 스트리밍한다. wal_level=logical이 필요하다. 대규모/복잡한 시나리오에는 pglogical이나 Debezium이 권장된다.","danger":"caution","priv":"소스에 REPLICATION 롤, 대상에 CREATE SUBSCRIPTION을 위한 superuser(PG16+에서는 pg_create_subscription 롤로도 충분)","minver":"10(논리 복제)"},"mysql":{"command":"-- Source: binlog must be enabled and in ROW format (log_bin, binlog_format=ROW)\n-- Grant a replication user: GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';\n\n-- Define the source on the target (start from the binlog position of the initial load):\nCHANGE REPLICATION SOURCE TO\n  SOURCE_HOST='source', SOURCE_USER='repl', SOURCE_PASSWORD='***',\n  SOURCE_LOG_FILE='binlog.000001', SOURCE_LOG_POS=1547;\nSTART REPLICA;","note":"MySQL에서 무중단 전환은 binlog 기반 복제로 수행한다. 초기 적재는 mysqldump --source-data로 수행하고 binlog 위치/GTID를 기록한다. 대상에서는 CHANGE REPLICATION SOURCE TO(8.0.23+ 구문, 이전에는 CHANGE MASTER TO)로 해당 위치부터 복제를 시작한다. START REPLICA는 변경을 지속적으로 적용한다. GTID 기반 구성이 더 안전하다.","danger":"caution","priv":"REPLICATION SLAVE(repl 사용자), REPLICATION CLIENT","minver":"8.0.23(CHANGE REPLICATION SOURCE)"},"mssql":{"command":"-- Option A: Transactional Replication (publisher->distributor->subscriber) — SSMS wizard\n\n-- Option B: Change Data Capture (track changes as tables)\nEXEC sys.sp_cdc_enable_db;              -- enable CDC at database level\nEXEC sys.sp_cdc_enable_table\n  @source_schema = N'dbo', @source_name = N'customer',\n  @role_name = NULL;                    -- enable CDC at table level\n-- Changes are read from the cdc.dbo_customer_CT table and moved to the target.","note":"SQL Server의 지속적 복제에는 두 가지 주요 경로가 있다. 대상으로 직접 스트리밍하려면 Transactional Replication(publisher/subscriber)을 구성한다. 자체 ETL로 변경을 옮기려면 Change Data Capture(sp_cdc_enable_db + sp_cdc_enable_table)를 활성화하고 _CT 테이블에서 읽는다. 이기종 대상에는 CDC를 AWS DMS/Debezium과 결합한다.","danger":"caution","priv":"sysadmin(CDC 활성화용), SQL Server Agent 실행 필요"},"mongodb":{"command":"// Watch changes live with Change Streams (requires a replica set/sharded cluster)\nconst cs = db.customer.watch([], { fullDocument: \"updateLookup\" });\nwhile (cs.hasNext()) { printjson(cs.next()); }  // apply to the target\n\n// Official tool for managed cluster-to-cluster replication/migration: mongosync\n// mongosync --cluster0 \"mongodb://source\" --cluster1 \"mongodb://target\"","note":"MongoDB에서는 CDC에 Change Streams를 사용한다. db.collection.watch()는 oplog에서 insert/update/delete 이벤트를 실시간으로 스트리밍하며 이를 대상에 적용한다(replica set 필요). 클러스터 간 전체 지속 마이그레이션/복제에는 MongoDB 공식 Cluster-to-Cluster Sync 도구인 mongosync(Atlas 전용이 아니며 자체 관리 클러스터에서도 동작)가 초기 적재 + 지속 동기화를 관리한다.","danger":"caution","priv":"changeStream(read), mongosync는 clusterAdmin"},"redis":{"command":"# Make the target instance a replica of the source (live synchronization)\n# On the target Redis:\nREPLICAOF source_host 6379\n# Monitor sync status:\nINFO replication   # master_link_status must be up\n\n# Once the cutover is complete, make the target standalone:\nREPLICAOF NO ONE","note":"Redis에서 무중단 전환은 기본 복제로 수행한다. 대상 서버에서 REPLICAOF <source host port>(이전 SLAVEOF)를 실행하면 대상이 먼저 전체 RDB를 받은 뒤 명령 스트림으로 지속 동기화된다. INFO replication으로 master_link_status:up을 확인하고, 전환이 완료되면 REPLICAOF NO ONE으로 대상을 standalone으로 만든다.","danger":"caution"},"elasticsearch":{"command":"# Incremental reindex from the old cluster to the new one within the same version family\nPOST _reindex?wait_for_completion=false\n{ \"source\": { \"remote\": { \"host\": \"http://old:9200\" }, \"index\": \"customer\",\n    \"query\": { \"range\": { \"updated_at\": { \"gt\": \"<last-cursor>\" } } } },\n  \"dest\": { \"index\": \"customer\" } }\n\n# Licensed feature: Cross-Cluster Replication (CCR) — continuous following\n# On the target (follower) cluster: PUT /customer/_ccr/follow { \"remote_cluster\": \"old\", \"leader_index\": \"customer\" }","note":"Elasticsearch의 실질적인 지속 복제 기능은 Cross-Cluster Replication(CCR)이다. 대상 클러스터가 소스의 leader index를 팔로우하며 변경을 지속적으로 가져온다(상용/Platinum 라이선스 필요). 라이선스가 없으면 주기적인 remote reindex(시간/시퀀스 필드 기반 증분)로 거의 지속적인 동기화를 구현한다.","danger":"caution","priv":"CCR은 manage/read_ccr, remote reindex는 whitelist"}}},{"id":"verify-migration","title":"마이그레이션 후 검증(행 수/체크섬)","aliases":["verify","verification","row count","checksum","row count","consistency check"],"engines":{"oracle":{"command":"-- Compare row counts on source and target\nSELECT 'customer' table_name, COUNT(*) row_count FROM customer;\n\n-- To find content differences at object level, DBMS_COMPARISON (via db link)\nBEGIN DBMS_COMPARISON.CREATE_COMPARISON(\n  comparison_name => 'COMPARE_CUSTOMER',\n  schema_name => 'SHOP', object_name => 'CUSTOMER',\n  dblink_name => 'TARGET_DB'); END;\n/\n-- SELECT DBMS_COMPARISON.COMPARE('COMPARE_CUSTOMER', scan_info) FROM dual;","note":"가장 빠른 확인은 양쪽에서 테이블별 COUNT(*)를 비교하는 것이다. 실제 행 수준 내용 차이를 찾으려면 DBMS_COMPARISON 패키지를 사용한다. database link를 통해 소스와 대상 테이블을 행 단위로 비교하고 차이를 보고/수렴할 수 있다. 체크섬은 ORA_HASH 또는 STANDARD_HASH로 컬럼 다이제스트를 구할 수 있다.","danger":"safe","readonly":true,"priv":"양쪽에 대한 SELECT, DBMS_COMPARISON에 대한 EXECUTE"},"postgres":{"command":"-- Row count\nSELECT count(*) FROM customer;\n\n-- Ordered digest (checksum) of the whole table content — compare on both sides\nSELECT md5(string_agg(t.row_md5, '' ORDER BY t.row_md5)) AS digest\nFROM (SELECT md5(customer::text) AS row_md5 FROM customer) t;","note":"첫 단계는 각 테이블의 count(*)를 비교하는 것이다. 내용 검증은 각 행의 텍스트 표현을 md5하고 정렬 순서대로 이어 붙인 뒤 다시 해시한다. 이렇게 하면 양쪽에서 순서에 무관한 체크섬을 얻는다. 대용량 데이터는 변경 가능/키 컬럼에 대해서만 다이제스트를 구한다. 재현성을 위해 PK 기준 정렬이 중요하다.","danger":"safe","readonly":true,"priv":"테이블에 대한 SELECT"},"mysql":{"command":"-- Built-in table checksum (run on both servers and compare)\nCHECKSUM TABLE customer;\n\n-- Row count\nSELECT COUNT(*) FROM customer;\n\n-- Preferred tool for live/large environments (compares row chunks online):\n-- pt-table-checksum h=source,D=shop  (Percona Toolkit)","note":"MySQL에서 CHECKSUM TABLE은 테이블 내용의 단일 체크섬을 반환한다. 소스와 대상에서 실행해 비교한다(엔진/버전이 같아야 함). 행 수는 COUNT(*)를 사용한다. 운영 중인 환경과 복제 일관성 확인에서는 Percona Toolkit의 pt-table-checksum이 업계 표준이며, chunk 단위로 온라인에서 낮은 영향으로 비교한다.","danger":"safe","readonly":true,"priv":"테이블에 대한 SELECT"},"mssql":{"command":"-- Full row-level comparison (official tool that produces differences)\ntablediff -sourceserver S1 -sourcedatabase shop -sourcetable customer \\\n  -destinationserver S2 -destinationdatabase shop -destinationtable customer\n\n-- Quick aggregate checksum (single query, compare on both sides):\nSELECT COUNT_BIG(*) row_count, CHECKSUM_AGG(BINARY_CHECKSUM(*)) digest FROM dbo.customer;","note":"SQL Server에서 행 수준 차이를 찾는 공식 명령줄 도구는 tablediff다(어떤 행이 추가/변경되었는지 보고하고, 선택 옵션 -f로 수정 스크립트를 작성). 빠른 전체 확인에는 CHECKSUM_AGG(BINARY_CHECKSUM(*))가 단일 테이블 다이제스트를 제공하며, COUNT_BIG(*)와 함께 양쪽을 비교한다(행 순서와 무관).","danger":"safe","readonly":true,"priv":"두 테이블에 대한 SELECT"},"mongodb":{"command":"// Compare document counts (on source and target)\ndb.customer.countDocuments({});\n\n// For a content checksum, dbHash (md5 digest at collection/DB level):\ndb.runCommand({ dbHash: 1, collections: [\"customer\"] });\n// -> compare the md5 field across both environments.","note":"첫 확인은 countDocuments()로 양쪽의 문서 수를 비교하는 것이다(estimatedDocumentCount는 빠르지만 메타데이터에 의존하므로 정확한 수는 countDocuments를 사용). 내용 검증에는 dbHash 명령이 컬렉션/데이터베이스 수준의 md5 다이제스트를 생성하므로 두 환경의 해시를 비교한다. 인덱스는 getIndexes()로 별도 확인한다.","danger":"safe","readonly":true,"priv":"read(dbHash 접근 권한)"},"redis":{"command":"# Compare key counts (on source and target)\nredis-cli DBSIZE\n\n# For a memory/data summary and key distribution:\nredis-cli INFO keyspace     # db0:keys=... information\n# For full content verification: DUMP each key and compare with the target\n# (e.g. redis-cli --scan | xargs -I{} redis-cli DUMP {})","note":"Redis의 기본 검증은 DBSIZE로 전체 키 수를 비교하는 것이며, 데이터베이스별 분포는 INFO keyspace가 보여 준다. Redis에는 기본 테이블 체크섬이 없다. 정확한 내용 검증이 필요하면 키별 DUMP 직렬화를 구해 양쪽을 비교한다(TTL 차이에 유의). riot compare 도구도 사용할 수 있다.","danger":"safe","readonly":true},"elasticsearch":{"command":"# Compare document counts (on source and target clusters)\nGET customer/_count\n\n# Details: for health/segment/storage comparison\nGET _cat/indices/customer?v&h=index,docs.count,store.size\n# Content by sampling: compare GET customer/_doc/<id> for the same _ids","note":"Elasticsearch의 기본 검증은 GET <index>/_count로 두 클러스터의 문서 수를 비교하는 것이다(_cat/indices docs.count도 빠른 요약을 제공). 비교 기준은 _count로 한다. _cat/indices docs.count는 Lucene 수준에서 집계하므로(nested 하위 문서 포함, 예: nested 항목 두 개를 가진 문서 하나는 3으로 집계) _count와 다를 수 있다. 내용 검증은 같은 _id 집합에서 문서를 샘플링하거나 해시 필드로 비교한다.","danger":"safe","readonly":true,"priv":"인덱스에 대한 read / monitor"}}}]},"rosetta/tuning.json":{"category":"성능 튜닝","categoryId":"tuning","order":16,"concepts":[{"id":"plan-baseline","title":"쿼리 실행 계획 고정 / 강제(plan baseline / hint)","aliases":["plan baseline","force plan","pin plan","hint","plan stability","query store force"],"engines":{"oracle":{"command":"-- Load the good plan from the cursor cache as a baseline:\nDECLARE n NUMBER; BEGIN\n  n := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(sql_id => '&sql_id');\nEND;\n/","note":"SQL Plan Management는 실행 계획을 고정하며, 회귀에 대한 가장 강력한 보호 수단이다. /*+ ... */ 힌트도 사용할 수 있다.","danger":"caution","priv":"ADMINISTER SQL MANAGEMENT OBJECT"},"postgres":{"command":"-- There is no built-in plan pinning; the pg_hint_plan extension:\n/*+ IndexScan(customer customer_city_idx) */ SELECT ... ;","note":"대안: enable_seqscan 같은 설정으로 세션 수준에서 유도한다. 영구 고정에는 pg_hint_plan을 사용한다.","danger":"caution"},"mysql":{"command":"SELECT /*+ INDEX(customer customer_city_idx) */ * FROM customer WHERE city='Ankara';","note":"옵티마이저 힌트는 쿼리에 포함된다. 영구적인 재작성에는 query rewrite 플러그인을 사용한다.","danger":"safe"},"mssql":{"command":"-- Force the good plan in Query Store:\nEXEC sp_query_store_force_plan @query_id = 42, @plan_id = 17;","note":"Query Store가 활성화되어 있어야 한다. 대안으로 OPTION (USE PLAN N'...')이나 plan guide를 사용한다.","danger":"caution","priv":"데이터베이스에 대한 ALTER"},"mongodb":{"command":"// Force an index for the query:\ndb.customer.find({ city: \"Ankara\" }).hint({ city: 1 })\n// Persistent: db.customer.getPlanCache() / index filter","note":"hint는 특정 인덱스를 강제한다. 오래된 실행 계획은 planCacheClear로 지운다.","danger":"safe"},"redis":{"command":"# Not applicable: there is no query planner.","note":"성능은 데이터 구조와 명령 복잡도에 따라 결정된다.","danger":"safe"},"elasticsearch":{"command":"# There is no plan pinning; optimize the query/index and routing.\nGET <index>/_search { \"profile\": true, \"query\": {...} }","note":"profile로 어떤 쿼리가 비용이 큰지 확인한다. 샤드/routing으로 유도한다.","danger":"safe"}}},{"id":"gather-stats-advanced","title":"고급 옵션으로 통계 수집(히스토그램 등)","aliases":["gather stats","histogram","statistics","analyze advanced","method_opt","estimate percent"],"engines":{"oracle":{"command":"EXEC DBMS_STATS.GATHER_TABLE_STATS(\n  ownname=>'HR', tabname=>'EMPLOYEE',\n  estimate_percent=>DBMS_STATS.AUTO_SAMPLE_SIZE,\n  method_opt=>'FOR ALL COLUMNS SIZE AUTO', degree=>4);","note":"method_opt는 히스토그램을, degree는 병렬도를 제어한다. 통계를 고정하려면 LOCK_TABLE_STATS를 사용한다.","danger":"safe"},"postgres":{"command":"ALTER TABLE customer ALTER COLUMN city SET STATISTICS 500;\nANALYZE customer;","note":"컬럼별 샘플링 목표다. default_statistics_target이 전역 기본값을 지정한다.","danger":"safe"},"mysql":{"command":"ANALYZE TABLE customer UPDATE HISTOGRAM ON city WITH 32 BUCKETS;","note":"MySQL 8.0의 히스토그램 지원. 필터링에 쓰이지만 선택도가 낮은 컬럼에 유용하다.","danger":"safe"},"mssql":{"command":"UPDATE STATISTICS dbo.customer WITH FULLSCAN;\n-- Filtered statistics: CREATE STATISTICS ... WHERE ...","note":"FULLSCAN이 가장 정확하다. 대형 테이블에는 SAMPLE을 사용한다. 자동 갱신은 AUTO_UPDATE_STATISTICS.","danger":"safe"},"mongodb":{"command":"// There are no manual statistics; the planner measures at runtime.\ndb.customer.getPlanCache().clear()","note":"오래된 plan cache를 지우는 것 외에는 수동 통계가 필요 없다.","danger":"safe"},"redis":{"command":"# Not applicable.","note":"통계/옵티마이저가 없다.","danger":"safe"},"elasticsearch":{"command":"# There is no statistics gathering; refresh searchability:\nPOST <index>/_refresh","note":"쿼리 시점에 field data와 doc values를 사용한다. force merge는 세그먼트 수를 줄인다.","danger":"safe"}}},{"id":"perf-report","title":"성능 보고서 생성(AWR / ASH 및 대응 기능)","aliases":["awr","ash","performance report","performance report","profile","workload report"],"engines":{"oracle":{"command":"-- AWR report (SQL*Plus):\n@?/rdbms/admin/awrrpt.sql\n-- ASH report: @?/rdbms/admin/ashrpt.sql\n-- Live: SELECT * FROM v$active_session_history WHERE sample_time > SYSDATE-1/24;","note":"AWR/ASH는 Diagnostics Pack 라이선스가 필요하다. 라이선스가 필요 없는 대안은 Statspack이다.","danger":"safe","priv":"SELECT_CATALOG_ROLE"},"postgres":{"command":"-- pg_stat_statements summary + pg_profile/pgBadger:\nSELECT queryid, calls, total_exec_time, mean_exec_time, query\nFROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;","note":"AWR에 해당하는 기능으로는 pg_profile 확장(주기적 스냅샷)이나 pgBadger 로그 분석을 사용한다.","danger":"safe"},"mysql":{"command":"-- sys schema summaries:\nSELECT * FROM sys.statement_analysis LIMIT 20;\nSELECT * FROM sys.io_global_by_file_by_bytes LIMIT 10;","note":"sys 스키마는 performance_schema를 읽기 쉬운 보고서로 만든다. Enterprise Monitor가 더 포괄적이다.","danger":"safe"},"mssql":{"command":"-- Query Store reports (SSMS) or:\nSELECT TOP 20 * FROM sys.query_store_runtime_stats ORDER BY avg_duration DESC;","note":"Query Store는 시간에 따른 실행 계획/성능을 보관한다. sp_BlitzFirst(커뮤니티)는 빠른 진단을 제공한다.","danger":"safe"},"mongodb":{"command":"# Live from the shell: mongostat --rowcount 10 ; mongotop\n// or Atlas Performance Advisor / profiler:\ndb.system.profile.find().sort({millis:-1}).limit(20)","note":"mongostat/mongotop은 순간 부하를 보여 주고, 프로파일러는 느린 작업을 기록한다.","danger":"safe"},"redis":{"command":"INFO stats\nredis-cli --latency-history\nSLOWLOG GET 20","note":"--latency와 --latency-history는 지연 프로파일을, INFO는 모든 카운터를 보여 준다.","danger":"safe"},"elasticsearch":{"command":"GET _nodes/stats\nGET _cat/thread_pool?v&h=node_name,name,active,queue,rejected\nGET _nodes/hot_threads","note":"hot_threads는 비용이 가장 큰 스레드를, _nodes/stats는 종합 지표를 보여 준다. 모니터링(Kibana)은 이력 기반이다.","danger":"safe"}}},{"id":"optimizer-params","title":"옵티마이저 / 병렬 처리 설정 조회·변경","aliases":["optimizer","parallelism","parallel","maxdop","optimizer_switch","cost","planner settings"],"engines":{"oracle":{"command":"SHOW PARAMETER optimizer;\n-- Session: ALTER SESSION SET optimizer_mode = FIRST_ROWS_10;\n-- Parallelism: ALTER SESSION FORCE PARALLEL QUERY PARALLEL 4;","note":"optimizer_index_cost_adj, parallel_degree_policy 같은 설정은 실행 계획 선택에 영향을 주므로 신중하게 변경한다.","danger":"caution"},"postgres":{"command":"SHOW random_page_cost;\nSHOW max_parallel_workers_per_gather;\n-- Session: SET enable_seqscan = off;","note":"SSD에서는 random_page_cost ~1.1이 권장된다. enable_* 플래그는 진단용이며 영구적인 해결책이 아니다.","danger":"caution"},"mysql":{"command":"SELECT @@optimizer_switch;\nSET SESSION optimizer_switch='index_merge=off';","note":"optimizer_switch는 개별 최적화를 켜고 끈다. 세션 수준에서 테스트한다.","danger":"caution"},"mssql":{"command":"-- Instance-level MAXDOP and cost threshold:\nEXEC sp_configure 'max degree of parallelism', 8; RECONFIGURE;\n-- Database level: ALTER DATABASE SCOPED CONFIGURATION SET MAXDOP = 8;","note":"MAXDOP=0은 무제한이다. OLTP에서는 보통 4-8. 'cost threshold for parallelism'의 기본값 5는 대부분의 경우 너무 낮다.","danger":"caution"},"mongodb":{"command":"# There are no optimizer parameters; indexes and query shape are decisive.\ndb.customer.find({...}).explain('allPlansExecution')","note":"allPlansExecution은 플래너가 평가한 모든 실행 계획을 보여 준다.","danger":"safe"},"redis":{"command":"# Not applicable.","note":"플래너가 없다. io-threads와 데이터 구조 선택이 성능을 결정한다.","danger":"safe"},"elasticsearch":{"command":"GET <index>/_settings?include_defaults=true\n# search limits such as search.max_buckets, index.max_result_window","note":"옵티마이저가 없다. 샤드 수, routing, 쿼리 구조가 성능을 결정한다.","danger":"safe"}}},{"id":"sql-monitor","title":"실행 중/최근 SQL 실시간 모니터링","aliases":["sql monitor","live monitoring","real time sql","live query","current sql","monitor active queries"],"engines":{"oracle":{"command":"SET LONG 1000000 LONGCHUNKSIZE 1000000 LINESIZE 250 PAGESIZE 0\nSELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR(sql_id=>'&sql_id', type=>'TEXT') AS report FROM dual;\n-- or: SELECT sql_id, status, elapsed_time FROM v$sql_monitor WHERE status='EXECUTING';","note":"Real-Time SQL Monitoring은 장시간/병렬 쿼리를 실시간으로 보여 준다(Tuning Pack).","danger":"safe"},"postgres":{"command":"SELECT pid, now()-query_start AS dur, state, wait_event, query\nFROM pg_stat_activity WHERE state='active' ORDER BY query_start;","note":"auto_explain을 사용하면 느린 쿼리의 실행 계획을 자동으로 로그에 기록할 수 있다.","danger":"safe"},"mysql":{"command":"SELECT thread_id, event_name, timer_wait/1e9 AS ms, sql_text\nFROM performance_schema.events_statements_current;","note":"events_statements_current는 현재 실행 중인 문을 보여 준다.","danger":"safe"},"mssql":{"command":"SELECT r.session_id, r.status, r.wait_type, r.percent_complete, t.text\nFROM sys.dm_exec_requests r JOIN sys.dm_exec_sessions s ON s.session_id = r.session_id CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t\nWHERE s.is_user_process = 1;","note":"SSMS의 'Live Query Statistics'는 실행 계획 진행 상황을 실시간으로 보여 준다.","danger":"safe"},"mongodb":{"command":"db.currentOp({ active: true, secs_running: { $gt: 1 } })","note":"장시간 실행 작업을 실시간으로 보여 주며, killOp로 종료할 수 있다.","danger":"safe"},"redis":{"command":"MONITOR","note":"모든 명령을 실시간으로 스트리밍한다. 운영 환경에서 성능에 영향을 주므로 짧게만 사용한다. 대신 SLOWLOG를 권장한다.","danger":"caution"},"elasticsearch":{"command":"GET _tasks?detailed=true&group_by=parents","note":"실행 중인 작업을 실시간으로 보여 준다. 장시간 작업은 POST _tasks/<id>/_cancel로 취소한다.","danger":"safe"}}}]},"rosetta/tuning-adv.json":{"category":"고급 튜닝","categoryId":"tuning-adv","order":17,"concepts":[{"id":"parallel-query","title":"병렬 쿼리 실행 / 병렬도 설정","aliases":["parallel","parallelism","dop","degree of parallelism","parallel query"],"engines":{"oracle":{"command":"-- At query level with a hint:\nSELECT /*+ PARALLEL(t, 4) */ * FROM big_table t;\n-- Force at session level:\nALTER SESSION FORCE PARALLEL QUERY PARALLEL 4;","note":"PARALLEL 힌트는 테이블에 병렬 스캔을 적용한다. degree=DOP. 과도한 DOP는 서버 자원을 고갈시키므로 신중하게 선택한다. parallel_degree_policy=AUTO는 이를 자동으로 관리한다.","danger":"caution"},"postgres":{"command":"-- Set the number of parallel workers for the session:\nSET max_parallel_workers_per_gather = 4;\n-- The planner adds a Gather/Gather Merge node:\nEXPLAIN (ANALYZE) SELECT count(*) FROM big_table;","note":"실행 계획에 Gather/Parallel Seq Scan이 나타나면 병렬 처리가 적용된 것이다. max_parallel_workers와 min_parallel_table_scan_size도 영향을 준다.","danger":"safe"},"mysql":{"command":"-- Limited InnoDB parallelism (certain operations only):\nSET GLOBAL innodb_parallel_read_threads = 4;\n-- Effective for COUNT(*) and primary key scans.","note":"MySQL은 범용 병렬 쿼리 실행을 제공하지 않는다. innodb_parallel_read_threads는 clustered 인덱스 스캔(예: COUNT(*))만 병렬화한다. 복잡한 JOIN은 싱글 스레드로 실행된다.","danger":"safe"},"mssql":{"command":"-- MAXDOP hint at query level:\nSELECT * FROM dbo.big_table OPTION (MAXDOP 4);\n-- Instance level: sp_configure 'max degree of parallelism', 4;","note":"MAXDOP은 병렬 스레드 수를 제한하며, MAXDOP 1은 병렬 처리를 비활성화한다. 'cost threshold for parallelism'을 초과하면 실행 계획이 병렬로 전환된다.","danger":"safe"},"mongodb":{"command":"// Allow disk use for a large aggregation:\ndb.sale.aggregate([ { $group: { _id: \"$city\", t: { $sum: \"$amount\" } } } ], { allowDiskUse: true })\n// A sharded cluster processes each shard in parallel.","note":"샤딩된 클러스터에서는 쿼리가 각 샤드에서 병렬로 실행된다. 단일 노드에서는 aggregation 단계가 대부분 순차적이다. allowDiskUse는 메모리 한도를 넘는 작업을 가능하게 한다.","danger":"safe"},"redis":{"command":"# Not applicable: there is no concept of query parallelism.","note":"명령 실행은 싱글 스레드다. Cluster(샤딩)로 확장하거나 네트워크 I/O 병렬화에 io-threads를 사용한다.","danger":"safe"},"elasticsearch":{"command":"# Shard count = unit of parallelism; a query runs in parallel across shards.\n# Parallelism by slicing within a single shard (with scroll/PIT):\nGET <index>/_search\n{ \"slice\": { \"id\": 0, \"max\": 4 }, \"query\": { \"match_all\": {} } }","note":"검색은 관련된 모든 샤드에서 동시에 실행된다. primary 샤드 수가 기본 병렬 수준을 결정한다. slices는 대규모 scroll/reindex 작업을 분할한다.","danger":"safe"}}},{"id":"bind-variables","title":"바인드 변수 / 파라미터 스니핑 관리","aliases":["bind","bind variable","parameter","parameter sniffing","cursor sharing","prepared statement"],"engines":{"oracle":{"command":"-- Use binds instead of literals (for plan sharing):\nSELECT * FROM customer WHERE city = :city;\n-- Share legacy code that uses literals:\nALTER SESSION SET cursor_sharing = FORCE;","note":"바인드 변수는 hard parse를 줄인다. Adaptive Cursor Sharing은 분포가 치우친 바인드 값에 대해 여러 실행 계획을 유지해 스니핑 문제를 완화한다. cursor_sharing=FORCE는 신중하게 사용한다.","danger":"caution"},"postgres":{"command":"-- Parameterize with a prepared statement:\nPREPARE q (text) AS SELECT * FROM customer WHERE city = $1;\nEXECUTE q('Ankara');\n-- Set plan cache behavior:\nSET plan_cache_mode = force_custom_plan;","note":"plan_cache_mode=auto는 generic plan과 custom plan 중에서 선택한다. force_custom_plan은 실행할 때마다 파라미터별 실행 계획을 생성한다(스니핑과 유사한 문제의 해결책).","danger":"safe"},"mysql":{"command":"-- Server-side prepared statement:\nPREPARE stmt FROM 'SELECT * FROM customer WHERE city = ?';\nSET @city = 'Ankara';\nEXECUTE stmt USING @city;\nDEALLOCATE PREPARE stmt;","note":"Prepared statement는 SQL 인젝션을 막고 파싱 비용을 낮춘다. MySQL은 드라이버 수준의 파라미터화도 지원한다.","danger":"safe"},"mssql":{"command":"-- Parameterized query (sp_executesql):\nEXEC sp_executesql N'SELECT * FROM dbo.customer WHERE city = @city', N'@city nvarchar(50)', @city = N'Ankara';\n-- Handle sniffing with a hint:\nSELECT ... OPTION (RECOMPILE);\n-- or OPTION (OPTIMIZE FOR (@city = 'Ankara'));","note":"OPTION(RECOMPILE)은 실행할 때마다 파라미터별 실행 계획을 생성한다(CPU 비용이 큼). OPTIMIZE FOR는 특정/대표 값으로 실행 계획을 고정하며, OPTIMIZE FOR UNKNOWN은 평균 선택도를 사용한다.","danger":"caution"},"mongodb":{"command":"// The query shape is the key of the plan cache:\ndb.customer.find({ city: \"Ankara\" }).explain(\"queryPlanner\")\n// Clear a stale plan:\ndb.customer.getPlanCache().clear()","note":"같은 형태(같은 필드/연산자)의 쿼리는 캐시된 실행 계획을 공유하며, 리터럴 값이 달라도 새 실행 계획을 만들지 않는다. 값 분포가 크게 치우쳐 있으면 planCacheClear로 초기화한다.","danger":"safe"},"redis":{"command":"# Not applicable: there is no query planner or parameter sniffing.","note":"명령은 직접 실행된다. 파라미터화가 아니라 키 설계가 성능을 결정한다.","danger":"safe"},"elasticsearch":{"command":"# Reuse the query with a parameterized search template:\nPOST _scripts/customer_city\n{ \"script\": { \"lang\": \"mustache\", \"source\": { \"query\": { \"term\": { \"city\": \"{{city}}\" } } } } }\nGET <index>/_search/template\n{ \"id\": \"customer_city\", \"params\": { \"city\": \"Ankara\" } }","note":"search template(Mustache)은 쿼리 본문을 파라미터화한다. 애플리케이션은 고정된 템플릿을 다른 값으로 호출하여 쿼리 일관성과 캐시 활용도를 높인다.","danger":"safe"}}},{"id":"partition-pruning","title":"파티션 프루닝 검증","aliases":["partition pruning","partition elimination","partition","pruning","pstart pstop"],"engines":{"oracle":{"command":"EXPLAIN PLAN FOR\nSELECT * FROM sale WHERE sale_date = DATE '2026-01-15';\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);\n-- Look for 'PARTITION RANGE ITERATOR/SINGLE' and the Pstart/Pstop columns in the plan.","note":"Pstart/Pstop은 관련 파티션만 읽는다는 것을 보여 준다(KEY보다 숫자가 이상적). 파티션 키에 함수/암묵적 변환을 적용하면 프루닝이 되지 않는다.","danger":"safe"},"postgres":{"command":"SET enable_partition_pruning = on;\nEXPLAIN (ANALYZE)\nSELECT * FROM sale WHERE sale_date = DATE '2026-01-15';\n-- Verify in the plan that only the relevant partition (child table) is scanned.","note":"enable_partition_pruning이 켜져 있으면 일치하는 파티션만 실행 계획에 나타나며, 나머지는 'never executed'로 표시된다. 상수 값은 계획 시점에, 파라미터는 실행 시점에 프루닝된다.","danger":"safe"},"mysql":{"command":"-- Show which partitions are scanned:\nEXPLAIN SELECT * FROM sale WHERE sale_date = '2026-01-15';\n-- 5.7 and earlier: EXPLAIN PARTITIONS SELECT ... (removed in 8.0)","note":"MySQL 5.7+에서 일반 EXPLAIN 출력의 'partitions' 컬럼은 스캔할 파티션을 나열한다(EXPLAIN PARTITIONS 구문은 8.0에서 제거됨). 관련 파티션만 나타나야 하며, 모든 파티션이 나열되면 프루닝이 되지 않는 것이다.","danger":"safe"},"mssql":{"command":"-- Find which partition a value falls into:\nSELECT $PARTITION.pf_sale_date('2026-01-15') AS partition_no;\n-- Examine 'Actual Partition Count' and 'Actual Partitions Accessed' in the actual plan.","note":"$PARTITION 함수는 값의 파티션 번호를 반환한다. 실제 실행 계획에서 접근한 파티션 수가 1에 가까우면 프루닝이 적용된 것이고, 모든 파티션을 스캔하면 조건절이 맞지 않는 것이다.","danger":"safe"},"mongodb":{"command":"// Targeted query using the shard key in a sharded cluster:\ndb.sale.find({ shop_id: 42 }).explain(\"executionStats\")\n// In explain, only the relevant shard should appear under 'shards'.","note":"쿼리에 샤드 키가 포함되면 mongos는 해당 샤드로만 라우팅한다(targeted). 그렇지 않으면 모든 샤드로 전파된다(scatter-gather). explain 출력에서 접근한 샤드를 확인한다.","danger":"safe"},"redis":{"command":"# Not applicable: there is no table/partition concept.","note":"데이터는 Cluster 해시 슬롯으로 분산되며, 쿼리 프루닝 대신 키로 직접 접근한다.","danger":"safe"},"elasticsearch":{"command":"# Route to the relevant shard with routing:\nGET <index>/_search?routing=customer42\n{ \"query\": { \"term\": { \"customer_id\": 42 } } }\n# Query only the relevant indices with a time-based index pattern:\nGET sale-2026-01/_search","note":"custom routing은 쿼리를 단일 샤드로 좁힌다(프루닝과 유사). 시계열 데이터는 날짜 패턴 인덱스 이름(sale-2026-01)을 사용하면 관련 인덱스만 검색된다. 와일드카드 대신 좁은 패턴을 사용한다.","danger":"safe"}}},{"id":"result-cache","title":"결과 / 쿼리 캐시 사용","aliases":["result cache","result caching","query cache","query result cache","request cache"],"engines":{"oracle":{"command":"-- Result cache at query level:\nSELECT /*+ RESULT_CACHE */ city, COUNT(*) FROM customer GROUP BY city;\n-- Global mode: ALTER SYSTEM SET result_cache_mode = FORCE;","note":"Server Result Cache는 결과를 SGA에 보관하며, 기반 테이블이 변경되면 자동으로 무효화된다. RESULT_CACHE_MODE=FORCE는 적용 가능한 모든 쿼리에, MANUAL은 힌트를 통해서만 적용한다. 자주 변경되는 테이블에서는 효과가 적다.","danger":"caution"},"postgres":{"command":"-- There is no built-in result cache.\n-- Precompute with a materialized view:\nCREATE MATERIALIZED VIEW mv_city_summary AS\n  SELECT city, COUNT(*) FROM customer GROUP BY city;\nREFRESH MATERIALIZED VIEW mv_city_summary;","note":"PostgreSQL은 쿼리 결과 캐시를 제공하지 않는다. materialized view(수동/예약 REFRESH)나 애플리케이션 수준 캐시(Redis)를 사용한다. 참고: Memoize 노드는 nested loop 내부의 행 수준 캐시를 제공하며, 쿼리 결과를 캐시하는 것이 아니다.","danger":"safe"},"mysql":{"command":"-- Query Cache was REMOVED in MySQL 8.0.\n-- Alternative: a materialized summary table or an application cache:\nCREATE TABLE city_summary AS SELECT city, COUNT(*) c FROM customer GROUP BY city;","note":"기존 query_cache_type은 MySQL 8.0에서 완전히 제거되었다(확장성 문제 때문). 대신 InnoDB buffer pool, 요약 테이블 또는 외부 캐시(Redis/Memcached)를 사용한다.","danger":"safe"},"mssql":{"command":"-- There is no built-in query result cache.\n-- Precompute with an indexed (materialized) view:\nCREATE VIEW dbo.vw_city_summary WITH SCHEMABINDING AS\n  SELECT city, COUNT_BIG(*) c FROM dbo.customer GROUP BY city;\nCREATE UNIQUE CLUSTERED INDEX ix ON dbo.vw_city_summary(city);","note":"SQL Server는 결과 캐시를 두지 않는다(plan cache는 둔다). 반복되는 집계에는 indexed view가 자동으로 유지된다. 실행 계획에서의 자동 매칭은 Enterprise(및 Developer) 에디션에만 있으며, Standard에서는 WITH (NOEXPAND) 힌트로 뷰를 조회해야 한다. 또한 buffer pool이 자주 읽는 페이지를 메모리에 유지한다.","danger":"safe"},"mongodb":{"command":"// There is no built-in query result cache.\n// Pre-aggregate and write to a collection ($merge):\ndb.sale.aggregate([\n  { $group: { _id: \"$city\", t: { $sum: \"$amount\" } } },\n  { $merge: \"city_summary\" }\n])","note":"MongoDB는 결과 캐시를 제공하지 않으며, WiredTiger가 자주 접근하는 데이터를 메모리에 유지한다. 비용이 큰 aggregation에는 $merge/$out으로 미리 계산한 요약 컬렉션이나 애플리케이션 캐시(Redis)를 사용한다.","danger":"safe"},"redis":{"command":"# Redis is already a cache layer; cache the result manually:\nSET query:city_summary \"{...}\" EX 300\nGET query:city_summary\n# If already computed, return it with GET; otherwise compute it and write it with SET.","note":"일반적인 cache-aside 패턴: 키가 있으면 GET으로 빠르게 반환하고, 없으면 소스에서 계산해 EX(TTL)와 함께 SET한다. Redis는 보통 다른 데이터베이스의 결과 캐시 역할을 한다.","danger":"safe"},"elasticsearch":{"command":"# Shard request cache (enabled by default for size:0 aggregations):\nGET <index>/_search?request_cache=true\n{ \"size\": 0, \"aggs\": { \"city\": { \"terms\": { \"field\": \"city\" } } } }\n# Setting: PUT <index>/_settings { \"index.requests.cache.enable\": true }","note":"shard request cache는 size:0(aggregation 전용) 요청의 샤드별 결과를 캐시하며, 인덱스가 refresh되면 무효화된다. size>0(hit 반환) 쿼리는 기본적으로 캐시되지 않는다.","danger":"safe"}}}]},"rosetta/advanced-features.json":{"category":"고급 기능","categoryId":"advanced-features","order":18,"concepts":[{"id":"full-text-search","title":"전문 검색(Full-text search)","aliases":["full text","full text","fts","search","text search"],"engines":{"oracle":{"command":"-- Oracle Text: create a CONTEXT index\nCREATE INDEX article_idx ON article(content) INDEXTYPE IS CTXSYS.CONTEXT;\n-- Search with CONTAINS (label 1 ties CONTAINS to SCORE):\nSELECT id, SCORE(1) FROM article\nWHERE CONTAINS(content, 'database AND performance', 1) > 0\nORDER BY SCORE(1) DESC;","note":"Oracle Text(이전 interMedia)는 CONTEXT 인덱스를 통해 언어 인식 전문 검색을 제공한다. CONTAINS 연산자는 AND/OR/NEAR 같은 검색 연산자를 지원한다. 인덱스는 CTX_DDL.SYNC_INDEX 또는 자동 SYNC로 유지한다.","danger":"safe"},"postgres":{"command":"-- Search with tsvector/tsquery, accelerated by a GIN index:\nCREATE INDEX article_fts_idx ON article\n  USING GIN (to_tsvector('english', content));\nSELECT id, ts_rank(to_tsvector('english', content), q) AS score\nFROM article, to_tsquery('english', 'database & performance') q\nWHERE to_tsvector('english', content) @@ q\nORDER BY score DESC;","note":"PostgreSQL 내장 전문 검색은 @@ 연산자로 tsvector(문서)와 tsquery(쿼리)를 매칭한다. GIN 인덱스는 대용량 텍스트 검색 속도를 높인다. 언어 구성('english')이 어간 추출과 불용어 처리를 수행한다.","danger":"safe"},"mysql":{"command":"-- Create a FULLTEXT index (InnoDB/MyISAM):\nALTER TABLE article ADD FULLTEXT INDEX ft_content (content);\n-- Search in natural language / boolean mode:\nSELECT id, MATCH(content) AGAINST('database performance') AS score\nFROM article\nWHERE MATCH(content) AGAINST('+database +performance' IN BOOLEAN MODE);","note":"MySQL FULLTEXT 인덱스는 MATCH ... AGAINST 구문으로 조회한다. IN NATURAL LANGUAGE MODE는 관련도 점수를 반환하고, IN BOOLEAN MODE는 +/-/* 연산자로 세밀하게 제어한다. InnoDB는 5.6부터 전문 검색을 지원한다.","since":"MySQL 5.6","xnote":"InnoDB 전문 검색은 MySQL 5.6에서 도입되었으며, 그 이전에는 MyISAM에만 있었다.","danger":"safe"},"mssql":{"command":"-- Create a Full-Text Catalog and index:\nCREATE FULLTEXT CATALOG ft_catalog;\nCREATE FULLTEXT INDEX ON dbo.article(content)\n  KEY INDEX PK_article ON ft_catalog;\n-- Search with CONTAINS / FREETEXT:\nSELECT id FROM dbo.article\nWHERE CONTAINS(content, '\"database\" AND \"performance\"');","note":"SQL Server Full-Text Search는 먼저 Full-Text Catalog가 필요하고, 그다음 테이블에 Full-Text Index를 만든다(unique KEY INDEX 필수). CONTAINS는 정확/근접 검색과 논리 연산자를, FREETEXT는 의미 기반(언어학적) 검색을 지원한다. Full-Text Search 구성 요소가 설치되어 있어야 한다.","danger":"safe"},"mongodb":{"command":"// Create a text index:\ndb.article.createIndex({ content: \"text\" })\n// Search with $text and sort by relevance score:\ndb.article.find(\n  { $text: { $search: \"database performance\" } },\n  { score: { $meta: \"textScore\" } }\n).sort({ score: { $meta: \"textScore\" } })","note":"MongoDB text 인덱스는 $text 연산자로 전문 검색을 제공하며, 컬렉션당 text 인덱스는 하나만 가질 수 있다. textScore 메타 필드가 관련도를 보여 준다. 고급 검색에는 Atlas Search($search, Lucene 기반)를 사용한다.","danger":"safe"},"redis":{"command":"# Requires the RediSearch module (Redis Stack).\n# Define the index:\nFT.CREATE article_idx ON HASH PREFIX 1 article: SCHEMA content TEXT\n# Full-text search:\nFT.SEARCH article_idx \"database performance\"","note":"기본 Redis는 전문 검색을 제공하지 않는다. RediSearch 모듈(Redis Stack에 포함)이 FT.CREATE로 인덱싱을, FT.SEARCH로 검색을 제공한다. 어간 추출, fuzzy 검색, 필터링, 정렬을 지원한다. 모듈이 설치되어 있지 않으면 이 기능을 사용할 수 없다.","danger":"safe"},"elasticsearch":{"command":"# Elasticsearch is natively a full-text search engine.\n# match query (analyzed text search):\nGET article/_search\n{\n  \"query\": {\n    \"match\": { \"content\": \"database performance\" }\n  }\n}","note":"Elasticsearch는 본질적으로 Lucene 기반 전문 검색 엔진이며, text 필드는 색인 시점에 분석된다(토큰화, 어간 추출). match 쿼리는 관련도 점수(_score) 순으로 정렬된 결과를 반환한다. 언어별 analyzer(예: 'english')를 지정할 수 있다.","danger":"safe"}}},{"id":"json-query","title":"JSON 데이터 조회","aliases":["json","json query","json query","jsonb","document query"],"engines":{"oracle":{"command":"-- Extract a scalar value with JSON_VALUE:\nSELECT JSON_VALUE(data, '$.customer.city') AS city FROM orders;\n-- Expand into rows with JSON_TABLE:\nSELECT t.product, t.quantity\nFROM orders s,\n     JSON_TABLE(s.data, '$.items[*]'\n       COLUMNS (product VARCHAR2(50) PATH '$.product',\n                quantity NUMBER PATH '$.quantity')) t;","note":"Oracle(12c+)은 SQL/JSON 경로 표현식으로 JSON 데이터를 조회한다. JSON_VALUE는 단일 스칼라 값을, JSON_QUERY는 객체/배열 조각을 반환하며, JSON_TABLE은 JSON을 관계형 행-컬럼 구조로 변환한다. 21c+는 네이티브 JSON 데이터 타입을 지원한다.","since":"Oracle 12c","xnote":"SQL/JSON 쿼리는 Oracle 12c에서, 네이티브 JSON 데이터 타입은 21c에서 도입되었다.","danger":"safe"},"postgres":{"command":"-- jsonb operators:\nSELECT data->'customer'->>'city' AS city FROM orders;\n-- Containment (@>) and a GIN index:\nCREATE INDEX ix ON orders USING GIN (data);\nSELECT * FROM orders\nWHERE data @> '{\"status\": \"approved\"}';","note":"PostgreSQL jsonb 타입은 풍부한 연산자를 제공한다: ->(json 객체/요소), ->>(텍스트 값), #>(경로), @>(포함), ?(키 존재). GIN 인덱스는 @>와 ? 쿼리 속도를 높인다. jsonb_path_query로 SQL/JSON path 쿼리도 가능하다.","since":"PostgreSQL 9.4","xnote":"json 타입은 PostgreSQL 9.2에서, jsonb 타입은 9.4에서 도입되었다.","danger":"safe"},"mysql":{"command":"-- JSON_EXTRACT or shorthand operators:\nSELECT JSON_EXTRACT(data, '$.customer.city') AS city FROM orders;\n-- ->> returns unquoted text:\nSELECT data->>'$.customer.city' AS city FROM orders\nWHERE data->>'$.status' = 'approved';","note":"MySQL(5.7+)은 JSON_EXTRACT()로 네이티브 JSON 타입을 조회한다. -> 연산자는 JSON_EXTRACT의, ->>는 JSON_UNQUOTE(JSON_EXTRACT())의 약식 표기다. JSON 필드 검색은 generated column + 인덱스로 가속할 수 있다.","since":"MySQL 5.7","xnote":"네이티브 JSON 데이터 타입은 MySQL 5.7에서 도입되었다.","danger":"safe"},"mssql":{"command":"-- Extract a scalar value with JSON_VALUE:\nSELECT JSON_VALUE(data, '$.customer.city') AS city FROM orders;\n-- Expand into rows with OPENJSON:\nSELECT k.product, k.quantity\nFROM orders s\nCROSS APPLY OPENJSON(s.data, '$.items')\n  WITH (product NVARCHAR(50) '$.product', quantity INT '$.quantity') k;","note":"SQL Server(2016+)는 JSON을 NVARCHAR 컬럼에 저장한다. JSON_VALUE는 스칼라 값을, JSON_QUERY는 객체/배열을 반환하며, OPENJSON은 JSON을 테이블 형태로 변환한다. ISJSON()으로 유효성을 검사한다. JSON 경로 조회는 computed column + 인덱스로 가속할 수 있다.","since":"SQL Server 2016","xnote":"JSON 함수(JSON_VALUE/OPENJSON)는 SQL Server 2016에서 도입되었다.","danger":"safe"},"mongodb":{"command":"// MongoDB is natively a document (BSON/JSON) database.\n// Access nested fields with dot notation:\ndb.orders.find({ \"customer.city\": \"Ankara\" })\n// Array element and projection:\ndb.orders.find(\n  { \"items.product\": \"pen\" },\n  { \"items.$\": 1 }\n)","note":"MongoDB는 모든 데이터를 문서(JSON과 유사한 BSON)로 저장하므로 조회가 네이티브로 이루어진다. 점 표기법으로 중첩 필드와 배열 요소에 접근한다. 복잡한 변환은 Aggregation Pipeline으로 수행하며, 인덱스로 중첩 필드를 빠르게 조회할 수 있다.","danger":"safe"},"redis":{"command":"# Requires the RedisJSON module (Redis Stack).\n# Write a JSON document:\nJSON.SET order:1 $ '{\"customer\":{\"city\":\"Ankara\"},\"status\":\"approved\"}'\n# Read a value with JSONPath:\nJSON.GET order:1 $.customer.city","note":"기본 Redis에는 JSON 타입이 없다. RedisJSON 모듈(Redis Stack)이 JSON.SET/JSON.GET으로 JSONPath 기반 읽기와 쓰기를 제공한다. RediSearch와 함께 사용하면 JSON 문서를 인덱싱하고 조회할 수 있다. 모듈이 없으면 JSON은 문자열로 저장되어 애플리케이션에서 파싱한다.","danger":"safe"},"elasticsearch":{"command":"# Documents are already JSON; query fields directly:\nGET orders/_search\n{ \"query\": { \"term\": { \"customer.city\": \"Ankara\" } } }\n# For nested arrays, use the nested type and a nested query:\nGET orders/_search\n{ \"query\": { \"nested\": { \"path\": \"items\",\n  \"query\": { \"match\": { \"items.product\": \"pen\" } } } } }","note":"Elasticsearch는 문서를 네이티브 JSON으로 저장하고 인덱싱한다. 중첩 객체 배열에서 요소 경계를 유지하려면 'nested' 필드 타입과 nested 쿼리를 사용한다. 그렇지 않으면 object 타입 배열이 평탄화된다(교차 매칭 위험). object와 nested의 구분은 정확성에 매우 중요하다.","danger":"safe"}}},{"id":"spatial-geo","title":"지리 / 공간 쿼리","aliases":["spatial","geographic","geo","gis","location","map"],"engines":{"oracle":{"command":"-- Oracle Spatial: SDO_GEOMETRY column and spatial index\nCREATE INDEX location_sidx ON shop(location)\n  INDEXTYPE IS MDSYS.SPATIAL_INDEX;\n-- Shops within 5 km:\nSELECT name FROM shop\nWHERE SDO_WITHIN_DISTANCE(location,\n  SDO_GEOMETRY(2001, 4326,\n    SDO_POINT_TYPE(32.85, 39.92, NULL), NULL, NULL),\n  'distance=5 unit=KM') = 'TRUE';","note":"Oracle Spatial은 지리 데이터를 SDO_GEOMETRY 타입에 저장하며, USER_SDO_GEOM_METADATA 항목과 공간 인덱스가 필요하다. SDO_WITHIN_DISTANCE, SDO_RELATE, SDO_NN 같은 연산자가 거리/교차/최근접 이웃 쿼리를 처리한다. SRID 4326은 WGS84(위도/경도) 체계다.","danger":"safe"},"postgres":{"command":"-- PostGIS extension:\nCREATE EXTENSION IF NOT EXISTS postgis;\nCREATE INDEX location_gix ON shop USING GIST (location);\n-- Shops within 5 km (geography, meters):\nSELECT name FROM shop\nWHERE ST_DWithin(location,\n  ST_SetSRID(ST_MakePoint(32.85, 39.92), 4326)::geography,\n  5000);","note":"PostGIS는 PostgreSQL을 완전한 기능의 공간 데이터베이스로 만든다. geometry(평면)와 geography(구면) 타입, 그리고 ST_Distance/ST_DWithin/ST_Intersects/ST_Contains 같은 수백 개의 ST_ 함수를 제공한다. GiST/SP-GiST 인덱스가 공간 쿼리 속도를 높인다.","danger":"safe"},"mysql":{"command":"-- Spatial column (with SRID) and SPATIAL index:\nALTER TABLE shop MODIFY location POINT NOT NULL SRID 4326;\nALTER TABLE shop ADD SPATIAL INDEX (location);\n-- Compute distance (meters, geography):\nSELECT name,\n  ST_Distance(location,\n    ST_SRID(POINT(32.85, 39.92), 4326)) AS distance\nFROM shop\nORDER BY distance LIMIT 10;","note":"MySQL(5.7+)은 OGC 표준 공간 데이터 타입(POINT, POLYGON 등)과 ST_ 함수를 지원한다. SPATIAL 인덱스(R-tree)는 InnoDB/MyISAM과 NOT NULL 컬럼에서만 생성할 수 있으며, 옵티마이저가 사용하려면 컬럼이 SRID 속성(SRID 4326)으로 정의되어야 한다. SRID 4326에서 ST_Distance는 측지 거리를 미터 단위로 반환한다. POINT(x, y) + ST_SRID에서 x는 경도, y는 위도이며, SRID 4326의 WKT 입력(ST_GeomFromText)에서는 위도-경도 순서다.","danger":"safe"},"mssql":{"command":"-- geography type and spatial index:\nCREATE SPATIAL INDEX location_sidx ON dbo.shop(location);\n-- Shops within 5 km (STDistance returns meters):\nSELECT name FROM dbo.shop\nWHERE location.STDistance(\n  geography::Point(39.92, 32.85, 4326)) <= 5000;","note":"SQL Server는 두 가지 공간 타입을 제공한다: geography(구면, 위도/경도)와 geometry(평면). STDistance, STIntersects, STContains 같은 메서드가 쿼리를 처리한다. 공간 인덱스(그리드 기반)가 성능을 높인다. geography::Point(latitude, longitude, SRID)의 인수 순서에 유의한다.","danger":"safe"},"mongodb":{"command":"// 2dsphere index (for GeoJSON):\ndb.shop.createIndex({ location: \"2dsphere\" })\n// Shops nearest to a given point ($near):\ndb.shop.find({\n  location: {\n    $near: {\n      $geometry: { type: \"Point\", coordinates: [32.85, 39.92] },\n      $maxDistance: 5000\n    }\n  }\n})","note":"MongoDB는 지리 데이터를 GeoJSON 형식으로 저장하고 2dsphere 인덱스로 구면 쿼리를 실행한다. $near(거리순 정렬), $geoWithin(영역 내), $geoIntersects 연산자를 지원한다. 좌표는 [경도, 위도] 순서이며, $maxDistance는 미터 단위다.","danger":"safe"},"redis":{"command":"# Built-in geospatial commands (built on sorted sets):\nGEOADD shops 32.85 39.92 \"kizilay\"\nGEOADD shops 32.86 39.93 \"cankaya\"\n# Search within 5 km of a point:\nGEOSEARCH shops FROMLONLAT 32.85 39.92\n  BYRADIUS 5 km ASC","note":"Redis는 GEOADD(longitude latitude member)로 지리적 위치를 Sorted Set에 저장한다. GEOSEARCH(Redis 6.2+, 기존 GEORADIUS 대체)는 반경 또는 박스 안의 멤버를 거리순으로 반환한다. GEODIST는 두 지점 간 거리를 보여 준다. 기본 기능이므로 모듈이 필요 없다.","danger":"safe"},"elasticsearch":{"command":"# Map the field as geo_point:\nPUT shop\n{ \"mappings\": { \"properties\": { \"location\": { \"type\": \"geo_point\" } } } }\n# Query within 5 km:\nGET shop/_search\n{ \"query\": { \"geo_distance\": {\n  \"distance\": \"5km\",\n  \"location\": { \"lat\": 39.92, \"lon\": 32.85 } } } }","note":"Elasticsearch는 geo_point(점)와 geo_shape(다각형/선) 필드 타입을 지원한다. 필터링은 geo_distance(반경), geo_bounding_box(박스), geo_polygon 쿼리로, 정렬은 geo_distance로, 집계는 geohash_grid/geotile_grid로 수행한다. 좌표는 lat/lon 또는 [lon, lat] 배열로 지정한다.","danger":"safe"}}},{"id":"window-functions-adv","title":"윈도우 함수(ROW_NUMBER/RANK/LAG)","aliases":["window function","window function","row_number","rank","lag","over","analytic"],"engines":{"oracle":{"command":"-- Rank by amount within each city and fetch the previous value:\nSELECT city, name, amount,\n  ROW_NUMBER() OVER (PARTITION BY city ORDER BY amount DESC) AS row_num,\n  RANK()       OVER (PARTITION BY city ORDER BY amount DESC) AS rnk,\n  LAG(amount)   OVER (PARTITION BY city ORDER BY created_at)      AS prev_amount\nFROM sale;","note":"Oracle은 분석(윈도우) 함수를 선도적으로 도입했으며 ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, SUM/AVG OVER, NTILE, FIRST_VALUE를 완전히 지원한다. OVER 안에서 PARTITION BY는 그룹을, ORDER BY는 순서를, 프레임(ROWS/RANGE BETWEEN)은 슬라이딩 윈도우를 정의한다.","since":"Oracle 8i","danger":"safe"},"postgres":{"command":"-- Window functions with OVER():\nSELECT city, name, amount,\n  ROW_NUMBER() OVER (PARTITION BY city ORDER BY amount DESC) AS row_num,\n  RANK()       OVER (PARTITION BY city ORDER BY amount DESC) AS rnk,\n  LAG(amount)   OVER (PARTITION BY city ORDER BY created_at)      AS prev_amount\nFROM sale;","note":"PostgreSQL은 표준 SQL 구문으로 윈도우 함수를 완전히 지원한다. ROW_NUMBER/RANK/DENSE_RANK, LAG/LEAD, OVER를 사용한 집계, NTILE, FIRST_VALUE/LAST_VALUE를 사용할 수 있다. 반복되는 윈도우 정의는 WINDOW 절로 이름을 붙여 공유할 수 있다.","since":"PostgreSQL 8.4","danger":"safe"},"mysql":{"command":"-- MySQL 8.0+ window functions:\nSELECT city, name, amount,\n  ROW_NUMBER() OVER (PARTITION BY city ORDER BY amount DESC) AS row_num,\n  RANK()       OVER (PARTITION BY city ORDER BY amount DESC) AS rnk,\n  LAG(amount)   OVER (PARTITION BY city ORDER BY created_at)      AS prev_amount\nFROM sale;","note":"MySQL은 8.0에서 윈도우 함수를 추가했다(MariaDB 10.2+도 지원). ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, NTILE, OVER를 사용한 집계를 사용할 수 있다. 8.0 이전 버전에는 이 함수들이 없으며, 과거에는 사용자 변수로 흉내 냈다.","since":"MySQL 8.0","xnote":"MySQL 8.0 이전에는 윈도우 함수가 없었으며 사용자 변수로 흉내 냈다.","danger":"safe"},"mssql":{"command":"-- Window functions with OVER():\nSELECT city, name, amount,\n  ROW_NUMBER() OVER (PARTITION BY city ORDER BY amount DESC) AS row_num,\n  RANK()       OVER (PARTITION BY city ORDER BY amount DESC) AS rnk,\n  LAG(amount)   OVER (PARTITION BY city ORDER BY created_at)      AS prev_amount\nFROM dbo.sale;","note":"SQL Server는 2005(ROW_NUMBER/RANK)와 2012(LAG/LEAD, 프레임, OVER를 사용한 집계)부터 윈도우 함수를 지원한다. PARTITION BY, ORDER BY, ROWS/RANGE 프레임을 모두 사용할 수 있다. 페이징에는 OFFSET/FETCH나 ROW_NUMBER가 일반적이다.","since":"SQL Server 2005","xnote":"순위 함수는 2005에서, LAG/LEAD와 프레임은 2012에서 도입되었다.","danger":"safe"},"mongodb":{"command":"// Window calculations with $setWindowFields:\ndb.sale.aggregate([\n  { $setWindowFields: {\n      partitionBy: \"$city\",\n      sortBy: { amount: -1 },\n      output: {\n        row_num: { $documentNumber: {} },\n        rnk: { $rank: {} },\n        prev_amount: { $shift: { output: \"$amount\", by: -1 } }\n      }\n  } }\n])","note":"MongoDB 5.0에서 도입된 $setWindowFields aggregation 단계가 윈도우 함수를 제공한다: $documentNumber(ROW_NUMBER), $rank/$denseRank, $shift(LAG/LEAD), 집계 함수, 슬라이딩 윈도우(window: { documents/range }). partitionBy와 sortBy는 SQL의 PARTITION BY/ORDER BY에 해당한다.","since":"MongoDB 5.0","xnote":"$setWindowFields 단계는 MongoDB 5.0에서 도입되었다.","danger":"safe"},"redis":{"command":"# Not applicable: there are no SQL-like window functions.","note":"Redis는 키-값 저장소이며 분석용 윈도우 함수가 없다. 정렬된 결과에는 Sorted Set을 사용할 수 있지만(ZADD/ZRANGE로 순위 유사 접근), PARTITION BY/LAG 같은 관계형 윈도우 연산은 애플리케이션 계층에서 수행한다.","danger":"safe"},"elasticsearch":{"command":"# Not applicable: there are no SQL-like window functions.\n# Closest equivalent: bucket aggregations.\nGET sale/_search\n{ \"size\": 0, \"aggs\": { \"by_city\": {\n  \"terms\": { \"field\": \"city\" },\n  \"aggs\": { \"total\": { \"sum\": { \"field\": \"amount\" } } } } } }","note":"Elasticsearch는 고전적인 윈도우 함수(ROW_NUMBER/RANK/LAG)를 제공하지 않는다. 그룹화와 정렬은 aggregation 프레임워크로 수행한다. terms aggregation은 PARTITION BY에, top_hits는 상위 레코드에, derivative/serial_diff(pipeline aggregation)는 LAG와 유사한 연속 차이에 가깝다.","danger":"safe"}}},{"id":"columnstore-analytics","title":"컬럼형 스토리지 / 분석","aliases":["columnstore","columnar","column store","analytics","olap","columnar"],"engines":{"oracle":{"command":"-- Database In-Memory: keep the table in memory in columnar format\nALTER TABLE sale INMEMORY;\n-- With priority and compression:\nALTER TABLE sale INMEMORY PRIORITY HIGH\n  MEMCOMPRESS FOR QUERY HIGH;","note":"Oracle Database In-Memory(12.1.0.2+)는 디스크의 행 형식을 유지하면서(dual format) 테이블을 메모리에 컬럼 형식(In-Memory Column Store, IM)으로 보관해 분석 쿼리 속도를 높인다. INMEMORY는 별도 라이선스 옵션이다. inmemory_size 파라미터가 IM 영역 크기를 지정한다.","since":"Oracle 12.1.0.2","xnote":"Database In-Memory 컬럼 스토어는 Oracle 12.1.0.2에서 도입되었다(별도 라이선스).","danger":"caution"},"postgres":{"command":"-- Core PostgreSQL is row-based; columnar storage needs an extension:\n-- Citus columnar access method:\nCREATE EXTENSION IF NOT EXISTS citus;\nCREATE TABLE sale_columnar (...) USING columnar;\n-- Alternatives: the Hydra/cstore_fdw extensions.","note":"기본 PostgreSQL은 행 기반(heap) 스토리지만 제공한다. 컬럼형 스토리지에는 Citus 'columnar' 접근 방식(USING columnar), Hydra 또는 오래된 cstore_fdw 확장을 사용하며, 분석/데이터 웨어하우스 워크로드에 압축과 스캔 속도를 제공한다.","danger":"safe"},"mysql":{"command":"-- Not applicable: core InnoDB does not offer columnar storage.\n-- For analytics: MySQL HeatWave (cloud, in-memory columnar)\n-- or integration with external ClickHouse/Apache Doris.","note":"표준 MySQL(InnoDB)은 행 기반이며 내장 columnstore가 없다. 분석 가속에는 MySQL HeatWave(Oracle Cloud의 인메모리 컬럼형 가속기)를 사용한다. 온프레미스에서는 보통 ClickHouse나 Apache Doris 같은 외부 컬럼형 엔진을 사용한다.","danger":"safe"},"mssql":{"command":"-- Clustered columnstore index (for analytic tables):\nCREATE CLUSTERED COLUMNSTORE INDEX cci_sale ON dbo.sale;\n-- Nonclustered columnstore on a rowstore table (HTAP):\nCREATE NONCLUSTERED COLUMNSTORE INDEX ncci_sale\n  ON dbo.sale (city, product, amount, created_at);","note":"SQL Server columnstore 인덱스(2012에서 읽기 전용 nonclustered, 2014에서 갱신 가능한 clustered, 2016에서 갱신 가능한 nonclustered)는 데이터를 컬럼 단위로 높은 압축률로 저장하며, batch mode와 함께 집계/스캔 중심 분석 쿼리를 크게 가속한다. clustered는 데이터 웨어하우스에, nonclustered는 HTAP에 적합하다.","since":"SQL Server 2012","xnote":"columnstore 인덱스는 2012(읽기 전용), 갱신 가능한 clustered columnstore는 2014, 갱신 가능한 nonclustered columnstore는 2016에 도입되었다.","danger":"safe"},"mongodb":{"command":"-- Not applicable: MongoDB is document (row-like) based.\n-- For analytics: dedicated Analytics Nodes in Atlas\n-- or export to a columnar format ($out)/an external engine.","note":"MongoDB는 문서를 행과 유사한 BSON으로 저장하며 내장 컬럼형(columnstore) 스토리지가 없다. 분석 워크로드에는 Atlas replica set의 전용 Analytics Node, Atlas Data Federation, 또는 컬럼형 시스템(예: Parquet)으로의 데이터 내보내기를 사용한다.","danger":"safe"},"redis":{"command":"-- Not applicable: there is no concept of columnar storage.","note":"Redis는 인메모리 키-값 저장소이며 행/컬럼 스토리지 구분이 없다. 분석 집계는 Sorted Set, HyperLogLog(근사 집계), RedisTimeSeries 같은 특수 구조로 수행하며, 대규모 OLAP에는 적합하지 않다.","danger":"safe"},"elasticsearch":{"command":"# doc_values: fields are stored column-wise on disk (enabled by default).\n# Required for sorting/aggregation; active on non-text fields.\nPUT sale\n{ \"mappings\": { \"properties\":\n  { \"amount\": { \"type\": \"double\", \"doc_values\": true } } } }","note":"정렬과 집계를 위해 Elasticsearch는 필드를 doc_values라는 디스크 기반 컬럼형 구조로 보관한다(numeric/keyword/date 필드에서 기본 활성화). 이 컬럼형 배치가 분석/aggregation 쿼리 속도를 높이며, text 필드에서는 기본적으로 비활성화되어 있다.","danger":"safe"}}},{"id":"in-memory","title":"인메모리 테이블 / 가속","aliases":["in memory","in-memory","memory table","acceleration","in-memory oltp"],"engines":{"oracle":{"command":"-- Keep the table in memory with Database In-Memory:\nALTER TABLE orders INMEMORY;\n-- Memory size (init parameter):\nALTER SYSTEM SET inmemory_size = 4G SCOPE=SPFILE;","note":"Oracle Database In-Memory는 자주 조회되는 테이블을 메모리에 컬럼 형식으로 보관해 분석을 가속한다(별도 라이선스). OLTP에서는 Buffer Cache와 KEEP pool도 자주 접근하는 블록을 메모리에 유지한다. 인메모리 솔루션은 디스크의 행 형식을 통해 내구성을 보장한다.","danger":"caution"},"postgres":{"command":"-- Fast table without durability (writes no WAL):\nCREATE UNLOGGED TABLE temp_calc (id int, data text);\n-- Session-private temporary table (tends to stay in memory):\nCREATE TEMP TABLE session_data (id int) ON COMMIT DELETE ROWS;","note":"PostgreSQL은 전용 인메모리 테이블 엔진을 제공하지 않는다. UNLOGGED 테이블은 WAL 쓰기를 생략해 쓰기 속도를 높인다(크래시 시 내용 손실, 내구성 없음). TEMP 테이블은 세션 동안 유지되며 가능하면 temp_buffers를 통해 메모리에 보관된다. 자주 쓰는 데이터는 shared_buffers에 캐시된다.","danger":"caution"},"mysql":{"command":"-- Table using the MEMORY (HEAP) storage engine:\nCREATE TABLE cache_table (\n  id INT PRIMARY KEY,\n  data VARCHAR(100)\n) ENGINE=MEMORY;","note":"MySQL MEMORY(이전 HEAP) 스토리지 엔진은 테이블 데이터를 전부 RAM에 보관한다. 매우 빠르지만 서버를 재시작하면 데이터가 사라진다(구조는 유지됨). BLOB/TEXT를 지원하지 않으며 고정 행 크기를 사용한다. 내구성이 필요한 자주 쓰는 데이터에는 InnoDB buffer pool이 권장된다.","danger":"caution"},"mssql":{"command":"-- In-Memory OLTP: memory-optimized table\nCREATE TABLE dbo.session_state (\n  id INT NOT NULL PRIMARY KEY NONCLUSTERED,\n  data NVARCHAR(100)\n) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);","note":"SQL Server In-Memory OLTP(Hekaton)는 memory-optimized 테이블(MEMORY_OPTIMIZED=ON)과 latch-free 구조로 높은 동시성의 OLTP를 제공한다. DURABILITY=SCHEMA_AND_DATA는 데이터의 내구성을 유지하고, SCHEMA_ONLY는 스키마만 보존한다. memory-optimized 파일 그룹이 필요하다.","since":"SQL Server 2014","xnote":"In-Memory OLTP(Hekaton)는 SQL Server 2014에서 도입되었다.","danger":"caution"},"mongodb":{"command":"-- Hot data is kept in the WiredTiger cache automatically.\n-- Set the cache size (default ~50% of RAM):\n// mongod --wiredTigerCacheSizeGB 8\n// or in the configuration: storage.wiredTiger.engineConfig.cacheSizeGB","note":"MongoDB에는 별도의 인메모리 테이블 유형이 없다(기존 In-Memory Storage Engine은 Enterprise 전용이었으며 deprecated됨). 기본 WiredTiger 엔진은 자주 접근하는 데이터와 인덱스를 메모리 캐시에 유지해 읽기 속도를 높이며, OS 파일 캐시도 기여한다.","danger":"safe"},"redis":{"command":"# Redis already runs entirely in memory; nothing extra is needed.\n# Persistence and memory policy settings:\nCONFIG SET maxmemory 4gb\nCONFIG SET maxmemory-policy allkeys-lru","note":"Redis는 모든 데이터를 RAM에 보관하며, 이것이 설계상 속도의 원천이다. 영속성은 RDB(스냅샷) 및/또는 AOF(명령 로그)로 제공한다. maxmemory 한도와 eviction 정책(LRU/LFU/random)이 메모리가 가득 찼을 때의 동작을 결정한다.","danger":"safe"},"elasticsearch":{"command":"-- Not applicable: there is no fully in-memory table mode.\n# Place hot data on fast nodes with a hot/cold architecture:\nPUT sale/_settings\n{ \"index.routing.allocation.require.data\": \"hot\" }","note":"Elasticsearch는 데이터를 디스크(Lucene 세그먼트)에 저장하며 전용 인메모리 테이블 엔진을 제공하지 않는다. 속도는 OS 파일 캐시(page cache), doc_values, hot-warm-cold 아키텍처에 의존한다. 충분한 RAM과 heap 설정이 성능에 매우 중요하다.","danger":"safe"}}},{"id":"time-series","title":"시계열 데이터","aliases":["time series","time series","timeseries","iot","metrics","sensor"],"engines":{"oracle":{"command":"-- Range partitioning by date:\nCREATE TABLE measurements (\n  ts TIMESTAMP, sensor_id NUMBER, value NUMBER)\nPARTITION BY RANGE (ts)\nINTERVAL (NUMTODSINTERVAL(1,'DAY'))\n( PARTITION p0 VALUES LESS THAN (TIMESTAMP '2026-01-01 00:00:00') );","note":"Oracle에서 시계열 데이터는 보통 RANGE(INTERVAL) 파티셔닝으로 관리한다. 새 파티션이 자동으로 생성되고 오래된 파티션은 쉽게 삭제된다(파티션 프루닝으로 쿼리 가속). 압축(ILM/ADO)과 집계용 materialized view도 사용된다.","danger":"safe"},"postgres":{"command":"-- Hypertable with the TimescaleDB extension:\nCREATE EXTENSION IF NOT EXISTS timescaledb;\nCREATE TABLE measurements (ts TIMESTAMPTZ, sensor_id INT, value DOUBLE PRECISION);\nSELECT create_hypertable('measurements', 'ts');\n-- Summarize with a continuous aggregate.","note":"PostgreSQL은 네이티브 파티셔닝(PARTITION BY RANGE)으로 시계열을 보관할 수 있다. TimescaleDB 확장은 자동 시간 기반 chunk 분할, 압축, 보존 정책, continuous aggregate를 갖춘 hypertable을 추가한다. 대규모 IoT/메트릭 워크로드에 널리 쓰이는 솔루션이다.","danger":"safe"},"mysql":{"command":"-- RANGE partitioning by date:\nCREATE TABLE measurements (\n  ts DATETIME, sensor_id INT, value DOUBLE,\n  PRIMARY KEY (ts, sensor_id))\nPARTITION BY RANGE (TO_DAYS(ts)) (\n  PARTITION p2026_01 VALUES LESS THAN (TO_DAYS('2026-02-01')),\n  PARTITION pmax VALUES LESS THAN MAXVALUE);","note":"MySQL은 전용 시계열 엔진을 제공하지 않는다. RANGE 파티셔닝(TO_DAYS/UNIX_TIMESTAMP 사용)으로 오래된 데이터를 빠르게 삭제하고 파티션 프루닝을 적용할 수 있다. 대용량 시계열에는 보통 외부 솔루션(TimescaleDB, InfluxDB)이나 요약 테이블이 권장된다.","danger":"safe"},"mssql":{"command":"-- Partitioned table (partition function + scheme) or\n-- system-versioned temporal table (history kept automatically):\nCREATE TABLE dbo.measurements (\n  id INT PRIMARY KEY, value DECIMAL(10,2),\n  valid_from DATETIME2 GENERATED ALWAYS AS ROW START,\n  valid_to DATETIME2 GENERATED ALWAYS AS ROW END,\n  PERIOD FOR SYSTEM_TIME (valid_from, valid_to))\nWITH (SYSTEM_VERSIONING = ON);","note":"SQL Server는 partition function/scheme으로 파티셔닝하여 시계열을 관리하며, 넓은 날짜 범위에 걸친 데이터에 프루닝과 빠른 아카이빙을 제공한다. 또한 system-versioned temporal 테이블(SYSTEM_VERSIONING)은 행 변경 이력을 자동으로 보관하고 특정 시점 조회(FOR SYSTEM_TIME)를 지원한다.","since":"SQL Server 2016","xnote":"System-versioned temporal 테이블은 SQL Server 2016에서 도입되었다.","danger":"safe"},"mongodb":{"command":"// Time-series collection (MongoDB 5.0+):\ndb.createCollection(\"measurements\", {\n  timeseries: {\n    timeField: \"ts\",\n    metaField: \"sensor_id\",\n    granularity: \"seconds\"\n  }\n})","note":"MongoDB 5.0에서 도입된 time-series 컬렉션은 시간과 메타 필드 기준으로 데이터를 압축된 컬럼 유사(bucketed) 형식으로 자동 저장한다. 디스크와 인덱스 사용량을 줄이고 범위 쿼리 속도를 높인다. timeField는 필수이며, metaField(소스/태그)와 granularity가 성능을 좌우한다.","since":"MongoDB 5.0","xnote":"time-series 컬렉션은 MongoDB 5.0에서 도입되었다.","danger":"safe"},"redis":{"command":"# Requires the RedisTimeSeries module (Redis Stack).\n# Create a series with a retention period:\nTS.CREATE sensor:1 RETENTION 86400000 LABELS type temperature\n# Add a sample (with an automatic timestamp):\nTS.ADD sensor:1 * 23.5","note":"기본 Redis에는 시계열 타입이 없다. RedisTimeSeries 모듈(Redis Stack)이 TS.CREATE/TS.ADD를 통한 고속 샘플 수집, 자동 보존(RETENTION), 다운샘플링(TS.CREATERULE), TS.RANGE/TS.MRANGE를 통한 레이블 기반 범위 쿼리를 제공한다. 모듈이 없으면 Sorted Set으로 흉내 낸다.","danger":"safe"},"elasticsearch":{"command":"# Data stream: automatically rolling indexes for time series\nPUT _index_template/measurements-tpl\n{ \"index_patterns\": [\"measurements*\"], \"data_stream\": {},\n  \"template\": {\n    \"settings\": { \"index.mode\": \"time_series\", \"index.routing_path\": [\"sensor_id\"] },\n    \"mappings\": { \"properties\": {\n      \"@timestamp\": { \"type\": \"date\" },\n      \"sensor_id\": { \"type\": \"keyword\", \"time_series_dimension\": true },\n      \"temperature\": { \"type\": \"double\", \"time_series_metric\": \"gauge\" } } } } }\nPUT _data_stream/measurements","note":"Elasticsearch는 data stream으로 시계열 데이터를 관리한다. 시간이 지나면 새 backing index가 자동으로 생성되고(rollover), ILM이 오래된 데이터를 hot/warm/cold 계층으로 옮기고 삭제한다. TSDS(index.mode: time_series)는 메트릭 데이터 전용 스토리지 최적화를 제공한다. 최소 하나의 time_series_dimension 필드, 이를 가리키는 index.routing_path, @timestamp 필드가 필수다(없으면 템플릿이 거부된다).","danger":"safe"}}},{"id":"vector-search","title":"벡터 검색(AI/임베딩)","aliases":["vector search","vector search","embedding","ai","semantic search","knn"],"engines":{"oracle":{"command":"-- Oracle 23ai native VECTOR type:\nCREATE TABLE docs (id NUMBER, content CLOB, embedding VECTOR);\n-- Similarity search (cosine distance):\nSELECT id FROM docs\nORDER BY VECTOR_DISTANCE(embedding, :query_vector, COSINE)\nFETCH FIRST 5 ROWS ONLY;","note":"Oracle Database 23ai는 네이티브 VECTOR 데이터 타입과 AI Vector Search 기능을 제공한다. VECTOR_DISTANCE 함수는 COSINE/EUCLIDEAN/DOT 메트릭으로 유사도를 계산하며, 근사 검색을 위해 HNSW와 IVF 벡터 인덱스를 지원한다. 임베딩은 데이터베이스 내부 또는 외부 모델로 생성한다.","since":"Oracle 23ai","xnote":"네이티브 VECTOR 타입과 AI Vector Search는 Oracle 23ai에서 도입되었다.","danger":"safe"},"postgres":{"command":"-- pgvector extension:\nCREATE EXTENSION IF NOT EXISTS vector;\nCREATE TABLE docs (id int, embedding vector(1536));\nCREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);\n-- 5 nearest neighbors (<-> Euclidean, <=> cosine):\nSELECT id FROM docs ORDER BY embedding <=> :query_vec LIMIT 5;","note":"pgvector 확장은 PostgreSQL에 vector 타입과 유사도 연산자를 추가한다: <->(L2/유클리드), <=>(코사인), <#>(내적). HNSW 또는 IVFFlat 인덱스가 근사 최근접 이웃(ANN) 검색 속도를 높인다. RAG와 시맨틱 검색 애플리케이션에서 널리 사용된다.","xnote":"기본 PostgreSQL에는 vector 타입이 없으므로 pgvector 확장이 필요하다.","danger":"safe"},"mysql":{"command":"-- MySQL 9.0+ native VECTOR type:\nCREATE TABLE docs (id INT, embedding VECTOR(1536));\n-- Cosine distance (DISTANCE is HeatWave-only):\nSELECT id, DISTANCE(embedding, :query_vec, 'COSINE') AS d\nFROM docs ORDER BY d LIMIT 5;","note":"MySQL 9.0은 네이티브 VECTOR 데이터 타입과 STRING_TO_VECTOR/VECTOR_TO_STRING, VECTOR_DIM 함수를 추가했다(Community에도 포함). DISTANCE() 함수와 대규모 근사 벡터 검색은 Oracle Cloud의 MySQL HeatWave에서만 제공된다. Community/Enterprise에서는 애플리케이션이나 외부 벡터 저장소에서 거리를 계산한다. 이전 온프레미스 버전은 외부 벡터 저장소가 필요하다.","since":"MySQL 9.0","xnote":"네이티브 VECTOR 데이터 타입은 MySQL 9.0에서 도입되었다.","danger":"safe"},"mssql":{"command":"-- SQL Server 2025 native vector type:\nCREATE TABLE docs (id INT, embedding VECTOR(1536));\n-- Nearest neighbors by cosine distance:\nSELECT TOP 5 id,\n  VECTOR_DISTANCE('cosine', embedding, @query_vec) AS distance\nFROM dbo.docs ORDER BY distance;","note":"SQL Server 2025(및 Azure SQL)는 네이티브 vector 데이터 타입과 VECTOR_DISTANCE 함수(cosine/euclidean/dot)를 제공한다. 근사 최근접 이웃 검색을 위한 DiskANN 기반 벡터 인덱스는 개발 중이다. 임베딩은 Azure OpenAI 같은 외부 모델로 생성해 저장한다.","since":"SQL Server 2025","xnote":"네이티브 vector 데이터 타입은 SQL Server 2025에서 도입되었다.","danger":"safe"},"mongodb":{"command":"// Atlas Vector Search ($vectorSearch aggregation stage):\ndb.docs.aggregate([\n  { $vectorSearch: {\n      index: \"vector_idx\",\n      path: \"embedding\",\n      queryVector: [0.12, 0.87, /* ... */],\n      numCandidates: 100,\n      limit: 5\n  } }\n])","note":"MongoDB Atlas Vector Search는 $vectorSearch aggregation 단계로 임베딩을 조회하며, 내부적으로 HNSW 기반 근사 최근접 이웃 검색을 수행한다. 벡터 인덱스는 Atlas Search 인덱스로 정의한다. 이 기능은 관리형 Atlas 서비스 전용이다(온프레미스 Community 에디션에서는 사용 불가).","danger":"safe"},"redis":{"command":"# Requires the RediSearch (Redis Stack) VECTOR field type.\n# Define an HNSW vector index:\nFT.CREATE docs_idx ON HASH PREFIX 1 docs: SCHEMA\n  embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINE\n# KNN search (with a query vector):\nFT.SEARCH docs_idx \"*=>[KNN 5 @embedding $vec]\" PARAMS 2 vec \"...\"","note":"Redis는 RediSearch 모듈(Redis Stack)을 통해 벡터 검색을 제공한다. VECTOR 필드 타입은 FLAT(정확) 또는 HNSW(근사) 인덱스와 COSINE/L2/IP 메트릭을 지원한다. 최근접 이웃은 FT.SEARCH 안의 KNN 구문으로 조회한다. 저지연 시맨틱 검색과 RAG에 사용된다.","danger":"safe"},"elasticsearch":{"command":"# dense_vector field and kNN search:\nPUT docs\n{ \"mappings\": { \"properties\": { \"embedding\":\n  { \"type\": \"dense_vector\", \"dims\": 1536, \"similarity\": \"cosine\" } } } }\nGET docs/_search\n{ \"knn\": { \"field\": \"embedding\", \"query_vector\": [0.12, 0.87],\n  \"k\": 5, \"num_candidates\": 100 } }","note":"Elasticsearch는 dense_vector 필드 타입과 내장 knn 검색으로 근사 최근접 이웃(HNSW) 쿼리를 지원하며, 유사도는 cosine/dot_product/l2_norm을 사용할 수 있다. 벡터 검색을 고전적인 텍스트 쿼리와 결합하면(하이브리드 검색 / RRF) 관련도를 높일 수 있다.","since":"Elasticsearch 8.0","xnote":"내장 dense_vector kNN 검색은 Elasticsearch 8.x에서 도입되었다.","danger":"safe"}}},{"id":"cdc-change-tracking","title":"변경 추적(CDC / change tracking)","aliases":["cdc","change data capture","change tracking","change tracking","replication feed","binlog"],"engines":{"oracle":{"command":"-- Read changes from redo logs with LogMiner:\n-- 1) Add the archived/redo log file to analyze:\nEXECUTE DBMS_LOGMNR.ADD_LOGFILE(\n  LOGFILENAME => '/u01/arch/1_1234_1122334455.arc',\n  OPTIONS => DBMS_LOGMNR.NEW);\n-- 2) Start the session:\nEXECUTE DBMS_LOGMNR.START_LOGMNR(\n  OPTIONS => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG);\nSELECT sql_redo, sql_undo FROM V$LOGMNR_CONTENTS\nWHERE seg_name = 'ORDERS';\n-- 3) When finished: EXECUTE DBMS_LOGMNR.END_LOGMNR;\n-- For enterprise CDC/replication, Oracle GoldenGate is used.","note":"Oracle은 redo log를 기반으로 변경을 추적한다. LogMiner는 redo/아카이브 로그를 SQL로 변환해 과거 DML을 조사하며, 분석할 로그 파일은 DBMS_LOGMNR.ADD_LOGFILE로 추가한다. 의미 있는 결과를 얻으려면 최소한 minimal supplemental logging을 활성화해야 한다(ALTER DATABASE ADD SUPPLEMENTAL LOG DATA). 지속적이고 저지연인 CDC와 이기종 복제에는 Oracle GoldenGate(별도 제품)가 표준이며, 기존 Streams/Change Data Capture 기능은 deprecated되었다.","danger":"caution"},"postgres":{"command":"-- Create a logical replication (logical decoding) slot:\nSELECT * FROM pg_create_logical_replication_slot(\n  'cdc_slot', 'wal2json');\n-- Consume changes as JSON:\nSELECT data FROM pg_logical_slot_get_changes('cdc_slot', NULL, NULL);","note":"PostgreSQL logical decoding은 output plugin(wal2json, pgoutput, test_decoding)을 통해 WAL 레코드를 읽을 수 있는 변경 스트림으로 변환하며, replication slot이 변경 유실을 막는다. wal_level은 logical이어야 한다. Debezium은 이 메커니즘을 사용해 엔터프라이즈 CDC를 제공한다.","danger":"caution"},"mysql":{"command":"-- The row-based binlog is the CDC source (binlog_format=ROW):\nSHOW BINARY LOGS;\nSHOW BINLOG EVENTS IN 'binlog.000042';\n-- Usually Debezium/Maxwell reads the binlog and turns it into a stream.","note":"MySQL에서 CDC 소스는 binary log다. binlog_format=ROW이면 모든 행 변경이 기록된다. Debezium이나 Maxwell 같은 도구는 replica처럼 binlog를 읽어 Kafka로 이벤트 스트림을 생성한다. GTID를 활성화하면 위치를 안정적으로 추적하기 쉬워진다.","danger":"caution"},"mssql":{"command":"-- Enable Change Data Capture on the database and the table:\nEXEC sys.sp_cdc_enable_db;\nEXEC sys.sp_cdc_enable_table\n  @source_schema = N'dbo', @source_name = N'orders',\n  @role_name = NULL;\n-- For lightweight tracking: ALTER DATABASE ... SET CHANGE_TRACKING = ON;","note":"SQL Server는 두 가지 메커니즘을 제공한다. Change Data Capture(CDC)는 트랜잭션 로그에서 모든 DML(이전/새 값 포함)을 변경 테이블에 기록하며 SQL Server Agent가 필요하다. Change Tracking은 어떤 행이 변경되었는지(순 변경)만 가볍게 추적하며 전체 데이터 이력은 보관하지 않는다.","since":"SQL Server 2008","xnote":"CDC와 Change Tracking은 SQL Server 2008에서 도입되었다.","danger":"caution"},"mongodb":{"command":"// Watch changes in real time with Change Streams:\nconst stream = db.orders.watch([\n  { $match: { operationType: { $in: [\"insert\", \"update\"] } } }\n]);\nwhile (stream.hasNext()) { printjson(stream.next()); }","note":"oplog 기반의 MongoDB Change Streams는 컬렉션/데이터베이스/클러스터 수준의 insert, update, delete 이벤트를 실시간 스트림으로 전달한다. replica set 또는 샤딩된 클러스터가 필요하다. resumeToken을 사용하면 중단 후 이어서 스트림을 재개할 수 있으며, CDC 연동의 기반이 된다.","since":"MongoDB 3.6","xnote":"Change Streams는 MongoDB 3.6에서 도입되었다.","danger":"safe"},"redis":{"command":"# Listen for key changes with keyspace notifications:\nCONFIG SET notify-keyspace-events KEA\n# Subscribe to changed keys via Pub/Sub:\nPSUBSCRIBE __keyevent@0__:set\nPSUBSCRIBE __keyevent@0__:expired","note":"Redis keyspace notification은 키에 대한 이벤트(SET, DEL, EXPIRE 등)를 Pub/Sub 채널로 발행하며, notify-keyspace-events 설정으로 활성화한다(K=keyspace, E=keyevent, A=모든 이벤트). 신뢰할 수 있고 영속적인 변경 스트림에는 Redis Streams(XADD/XREAD)가 권장된다.","danger":"safe"},"elasticsearch":{"command":"-- Not applicable: there is no built-in CDC/change stream.\n# Closest equivalent: incremental query on a timestamp field:\nGET orders/_search\n{ \"query\": { \"range\": { \"updated_ts\": { \"gt\": \"2026-07-05T00:00:00\" } } },\n  \"sort\": [ { \"updated_ts\": \"asc\" } ] }","note":"Elasticsearch는 변경 데이터 캡처(CDC) 스트림을 제공하지 않으며, 문서가 변경된 시점을 기본적으로 발행하지 않는다. 일반적인 방법은 모든 문서에 updated_ts 필드를 추가하고 타임스탬프 기준 증분 쿼리(range + sort)를 실행하는 것이다. 보통은 소스 데이터베이스의 CDC를 Elasticsearch로 공급한다.","danger":"safe"}}}]},"rosetta/sql-syntax.json":{"category":"SQL 구문 및 키워드","categoryId":"sql-syntax","order":19,"concepts":[{"id":"with-cte","title":"WITH(Common Table Expression / CTE)","aliases":["with","cte","common table expression","temporary query","temporary result set"],"engines":{"oracle":{"command":"WITH region AS (\n  SELECT city, COUNT(*) AS n\n  FROM customer\n  GROUP BY city\n)\nSELECT city, n FROM region WHERE n > 10;","note":"WITH는 쿼리 안에 이름이 있는 임시 결과 집합(CTE)을 정의한다. 서브쿼리를 반복해서 쓰는 대신 한 번 정의하고 이름으로 참조한다. 복잡한 쿼리를 읽기 쉬운 조각으로 나눈다. Oracle 9i부터 지원한다.","since":"Oracle 9i","danger":"safe"},"postgres":{"command":"WITH region AS (\n  SELECT city, COUNT(*) AS n\n  FROM customer\n  GROUP BY city\n)\nSELECT city, n FROM region WHERE n > 10;","note":"CTE는 복잡한 쿼리를 이름이 있는 블록으로 나눈다. PostgreSQL 12 이전에는 CTE가 최적화 경계(항상 구체화)였으며, 12부터는 인라인될 수 있고 WITH ... AS MATERIALIZED로 이전 동작을 강제할 수 있다.","danger":"safe"},"mysql":{"command":"WITH region AS (\n  SELECT city, COUNT(*) AS n\n  FROM customer\n  GROUP BY city\n)\nSELECT city, n FROM region WHERE n > 10;","note":"CTE는 쿼리 안에 이름이 있는 임시 결과 집합을 정의해 가독성을 높인다. MySQL 8.0(및 MariaDB 10.2)부터 지원하며, 이전 버전에서는 derived table(FROM 안의 서브쿼리)이 필요하다.","since":"MySQL 8.0","xnote":"MySQL 8.0 이전에는 CTE가 없었으며 derived table(FROM 안의 서브쿼리)로 같은 작업을 했다.","danger":"safe"},"mssql":{"command":"WITH region AS (\n  SELECT city, COUNT(*) AS n\n  FROM customer\n  GROUP BY city\n)\nSELECT city, n FROM region WHERE n > 10;","note":"CTE는 이름이 있는 임시 결과 집합이다. SQL Server에서는 CTE 바로 앞의 문이 세미콜론으로 끝나야 하므로 WITH를 ';WITH'로 쓰는 경우가 많다. 단일 문(SELECT/INSERT/UPDATE/DELETE) 동안 유효하다.","danger":"safe"},"mongodb":{"command":"// Closest equivalent: aggregation pipeline stages\ndb.customer.aggregate([\n  { $group: { _id: \"$city\", n: { $sum: 1 } } },\n  { $match: { n: { $gt: 10 } } }\n])","note":"MongoDB에는 SQL/CTE가 없으며 aggregation pipeline 단계가 같은 역할을 한다. 각 단계는 이전 단계의 출력을 입력으로 받아 CTE 체인과 유사한 단계별 처리를 제공한다. 중간 결과에는 $facet이나 $lookup도 사용할 수 있다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no concept of SQL or CTEs.","note":"Redis는 쿼리 언어가 없는 키-값 데이터 저장소다. 중간 결과는 클라이언트 측에서 결합하거나, 서버 측 로직에는 Lua 스크립트(EVAL)를 사용한다.","danger":"safe"},"elasticsearch":{"command":"# CTEs are not supported in Elasticsearch SQL.\nPOST _sql\n{ \"query\": \"SELECT city, COUNT(*) n FROM customer GROUP BY city HAVING COUNT(*) > 10\" }","note":"Elasticsearch SQL(_sql API)은 WITH/CTE 구문을 지원하지 않으며, 그룹화와 필터링은 단일 SELECT로 작성한다. 네이티브 방식에서는 'terms' aggregation과 'bucket_selector' pipeline aggregation으로 같은 작업을 한다.","danger":"safe"}}},{"id":"recursive-cte","title":"재귀 CTE(WITH RECURSIVE)","aliases":["recursive","recursion","with recursive","connect by","hierarchy","tree query"],"engines":{"oracle":{"command":"-- Hierarchy with CONNECT BY:\nSELECT id, name, manager_id, LEVEL\nFROM employee\nSTART WITH manager_id IS NULL\nCONNECT BY PRIOR id = manager_id;\n\n-- Alternative (11gR2+): standard recursive WITH (the RECURSIVE keyword is not written)\nWITH tree (id, name, manager_id) AS (\n  SELECT id, name, manager_id FROM employee WHERE manager_id IS NULL\n  UNION ALL\n  SELECT c.id, c.name, c.manager_id\n  FROM employee c JOIN tree a ON c.manager_id = a.id\n)\nSELECT * FROM tree;","note":"조직도, 부품 구성표(BOM) 같은 계층(트리) 데이터를 순회하는 데 사용한다. Oracle의 고전적인 방법은 CONNECT BY / PRIOR와 LEVEL 의사 컬럼이며, START WITH가 루트 행을 지정한다. 11gR2부터 표준 재귀 CTE도 지원한다.","since":"Oracle 11gR2","xnote":"CONNECT BY가 고전적인 방법이며, 표준 재귀 WITH(RECURSIVE 키워드 없음)는 Oracle 11gR2에서 도입되었다.","danger":"safe"},"postgres":{"command":"WITH RECURSIVE tree (id, name, manager_id, level) AS (\n  SELECT id, name, manager_id, 1\n  FROM employee WHERE manager_id IS NULL\n  UNION ALL\n  SELECT c.id, c.name, c.manager_id, a.level + 1\n  FROM employee c JOIN tree a ON c.manager_id = a.id\n)\nSELECT * FROM tree ORDER BY level;","note":"WITH RECURSIVE는 앵커 쿼리와 재귀 부분을 UNION ALL로 결합하며, 재귀 부분은 결과가 빌 때까지 반복 실행된다. 계층 구조와 그래프 탐색에 적합하다. 무한 루프는 UNION(ALL 대신)이나 순환 방지로 막는다.","since":"PostgreSQL 8.4","danger":"safe"},"mysql":{"command":"WITH RECURSIVE tree (id, name, manager_id, level) AS (\n  SELECT id, name, manager_id, 1\n  FROM employee WHERE manager_id IS NULL\n  UNION ALL\n  SELECT c.id, c.name, c.manager_id, a.level + 1\n  FROM employee c JOIN tree a ON c.manager_id = a.id\n)\nSELECT * FROM tree ORDER BY level;","note":"MySQL 8.0(및 MariaDB 10.2+)은 표준을 따르는 WITH RECURSIVE 구문을 지원한다. 재귀 깊이는 cte_max_recursion_depth 시스템 변수(기본값 1000)로 제한된다. 이전 버전에서는 저장 프로시저에서 루프를 작성해야 했다.","since":"MySQL 8.0","xnote":"MySQL 8.0(MariaDB 10.2) 이전에는 재귀 쿼리가 없었으며 저장 프로시저에서 루프를 작성했다.","danger":"safe"},"mssql":{"command":"WITH tree (id, name, manager_id, level) AS (\n  SELECT id, name, manager_id, 1\n  FROM employee WHERE manager_id IS NULL\n  UNION ALL\n  SELECT c.id, c.name, c.manager_id, a.level + 1\n  FROM employee c JOIN tree a ON c.manager_id = a.id\n)\nSELECT * FROM tree ORDER BY level\nOPTION (MAXRECURSION 100);","note":"SQL Server에서는 재귀 CTE에 RECURSIVE 키워드를 쓰지 않는다. UNION ALL을 포함하고 자기 자신을 참조하는 CTE는 자동으로 재귀로 처리된다. 기본 재귀 한도는 100이며 OPTION (MAXRECURSION n)으로 바꾼다(0 = 무제한, 신중하게 사용).","since":"SQL Server 2005","danger":"safe"},"mongodb":{"command":"// Closest equivalent: the $graphLookup stage\ndb.employee.aggregate([\n  { $match: { manager_id: null } },\n  { $graphLookup: {\n      from: \"employee\",\n      startWith: \"$id\",\n      connectFromField: \"id\",\n      connectToField: \"manager_id\",\n      as: \"subordinates\"\n  } }\n])","note":"MongoDB에는 재귀 CTE가 없으며 계층 순회에는 $graphLookup aggregation 단계를 사용한다. connectFromField/connectToField가 연결 필드를, maxDepth가 깊이 한도를 지정한다. 트리와 그래프 관계를 단일 쿼리로 해결한다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no recursive queries.","note":"Redis는 쿼리 언어를 제공하지 않는다. 계층/그래프 순회는 애플리케이션 코드나 서버 측 Lua 스크립트로 수행하며, 기본 Redis에서는 트리 탐색을 클라이언트 측에서 작성한다. (과거에 쓰이던 RedisGraph 모듈은 2023년에 지원이 종료되었다.)","danger":"safe"},"elasticsearch":{"command":"# Not applicable: recursive queries are not supported.\n# Approach: model with parent-child (join field) or nested,\n# deep hierarchies are traversed on the application side.","note":"Elasticsearch는 재귀 쿼리를 제공하지 않는다(WITH RECURSIVE / CONNECT BY에 해당하는 기능 없음). 계층 관계는 'join'(parent/child) 필드 타입이나 트리 경로를 필드에 저장하는 방식(materialized path)으로 모델링하며, 다단계 순회는 보통 애플리케이션에서 수행한다.","danger":"safe"}}},{"id":"joins","title":"JOIN 유형(INNER/LEFT/RIGHT/FULL/CROSS)","aliases":["join","inner join","left join","right join","full join","cross join","combine tables","table join"],"engines":{"oracle":{"command":"-- INNER: matching rows\nSELECT m.name, s.amount\nFROM customer m JOIN orders s ON m.id = s.customer_id;\n-- LEFT: customers without orders are included too\nSELECT m.name, s.amount\nFROM customer m LEFT JOIN orders s ON m.id = s.customer_id;\n-- CROSS: Cartesian product\nSELECT r.name, u.name FROM color r CROSS JOIN product u;","note":"JOIN은 지정한 조건에 따라 두 테이블을 결합한다. INNER는 일치하는 행만, LEFT/RIGHT는 한쪽의 모든 행을(일치하지 않으면 NULL), FULL은 양쪽의 행을, CROSS는 모든 조합을 반환한다. Oracle은 표준 ANSI JOIN 외에 기존 (+) 외부 조인 구문도 지원한다.","danger":"safe"},"postgres":{"command":"SELECT m.name, s.amount\nFROM customer m INNER JOIN orders s ON m.id = s.customer_id;\n\nSELECT m.name, s.amount\nFROM customer m LEFT JOIN orders s ON m.id = s.customer_id;\n\nSELECT m.name, s.amount\nFROM customer m FULL OUTER JOIN orders s ON m.id = s.customer_id;","note":"PostgreSQL은 모든 표준 JOIN 유형(INNER, LEFT/RIGHT/FULL OUTER, CROSS)을 완전히 지원한다. USING(my_column)은 같은 이름의 컬럼에 대한 약식 표기이며, NATURAL JOIN은 같은 이름의 모든 컬럼으로 자동 조인한다(신중하게 사용).","danger":"safe"},"mysql":{"command":"SELECT m.name, s.amount\nFROM customer m INNER JOIN orders s ON m.id = s.customer_id;\n\nSELECT m.name, s.amount\nFROM customer m LEFT JOIN orders s ON m.id = s.customer_id;\n\n-- MySQL has no FULL OUTER JOIN:\nSELECT m.name, s.amount FROM customer m LEFT JOIN orders s ON m.id = s.customer_id\nUNION\nSELECT m.name, s.amount FROM customer m RIGHT JOIN orders s ON m.id = s.customer_id;","note":"MySQL/MariaDB는 INNER, LEFT, RIGHT, CROSS JOIN을 지원하지만 FULL OUTER JOIN은 직접 제공하지 않으며, LEFT와 RIGHT 결과를 UNION으로 결합해 흉내 낸다. 기존의 쉼표식 조인은 카티션 곱을 만들므로 ON/WHERE로 제한해야 한다.","danger":"safe"},"mssql":{"command":"SELECT m.name, s.amount\nFROM customer m INNER JOIN orders s ON m.id = s.customer_id;\n\nSELECT m.name, s.amount\nFROM customer m LEFT JOIN orders s ON m.id = s.customer_id;\n\nSELECT m.name, s.amount\nFROM customer m FULL OUTER JOIN orders s ON m.id = s.customer_id;","note":"SQL Server는 모든 표준 JOIN 유형을 지원한다. 기존 *= / =* 외부 조인 연산자는 제거되었으므로 ANSI JOIN 구문을 사용해야 한다. APPLY(CROSS/OUTER APPLY)는 행 단위 상관 서브쿼리를 위한 추가 조인 형식이다.","danger":"safe"},"mongodb":{"command":"// Closest equivalent: $lookup (left outer join)\ndb.customer.aggregate([\n  { $lookup: {\n      from: \"orders\",\n      localField: \"id\",\n      foreignField: \"customer_id\",\n      as: \"orders\"\n  } }\n])","note":"MongoDB에는 고전적인 JOIN이 없다. $lookup aggregation 단계가 다른 컬렉션에서 일치하는 문서를 배열로 가져온다(left outer join과 유사). INNER와 같은 결과를 얻으려면 이후 $match로 빈 배열을 걸러 내고 $unwind로 배열을 행으로 펼친다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no tables or JOINs.","note":"Redis는 관계형 조인을 제공하지 않는다. 데이터는 보통 비정규화하거나, 여러 키를 읽어 클라이언트 측에서 관계를 구성한다. 제한적인 조인 유사 쿼리에는 RediSearch(FT.AGGREGATE)를 사용할 수 있다. RedisGraph 모듈은 2023년에 지원이 종료되었다.","danger":"safe"},"elasticsearch":{"command":"# Not applicable: there is no general JOIN in a distributed design.\n# Limited options: nested, parent/child (join field),\n# terms lookup. Data is usually denormalized at indexing time.","note":"분산 아키텍처 때문에 Elasticsearch는 임의의 테이블 간 JOIN을 제공하지 않는다. 제한적인 관계에는 단일 인덱스 안의 'nested' 필드 타입과 같은 샤드 위의 'join'(parent/child) 필드 타입이 있다. 가장 일반적인 해결책은 색인 시점에 데이터를 비정규화하는 것이다.","danger":"safe"}}},{"id":"window-functions","title":"윈도우 함수(OVER / PARTITION BY)","aliases":["window","window function","over","partition by","row_number","rank","lag","lead","analytic function"],"engines":{"oracle":{"command":"SELECT name, department, salary,\n  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_no,\n  RANK()       OVER (PARTITION BY department ORDER BY salary DESC) AS rank_no,\n  LAG(salary)    OVER (PARTITION BY department ORDER BY salary)      AS prev_salary\nFROM employee;","note":"윈도우 함수는 행을 그룹화하지 않고(행 수를 줄이지 않고) 각 행에 대해 '윈도우' 범위로 계산한다. PARTITION BY는 윈도우를 나누고, ORDER BY는 정렬한다. Oracle은 이러한 분석 함수를 선도적으로 도입했으며 ROW_NUMBER, RANK, DENSE_RANK, LAG/LEAD, SUM() OVER 등 풍부한 함수를 제공한다.","since":"Oracle 8i","danger":"safe"},"postgres":{"command":"SELECT name, department, salary,\n  ROW_NUMBER() OVER w AS row_no,\n  RANK()       OVER w AS rank_no,\n  LEAD(salary)   OVER w AS next_salary\nFROM employee\nWINDOW w AS (PARTITION BY department ORDER BY salary DESC);","note":"PostgreSQL은 모든 표준 윈도우 함수를 지원하며, 반복되는 윈도우 정의에 WINDOW ... AS로 이름을 붙일 수 있다. ROWS/RANGE/GROUPS 프레임 정의로 누적 합계, 이동 평균 등을 계산할 수 있다.","since":"PostgreSQL 8.4","danger":"safe"},"mysql":{"command":"SELECT name, department, salary,\n  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_no,\n  DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_no,\n  LAG(salary)    OVER (PARTITION BY department ORDER BY salary)      AS prev_salary\nFROM employee;","note":"MySQL 8.0(및 MariaDB 10.2+)은 윈도우 함수를 지원하며, 그 이전에는 사용자 변수나 서브쿼리로 흉내 냈다. ROW_NUMBER, RANK, DENSE_RANK, NTILE, LAG/LEAD, FIRST_VALUE/LAST_VALUE와 집계 함수의 OVER 형식을 사용할 수 있다.","since":"MySQL 8.0","xnote":"MySQL 8.0 이전에는 윈도우 함수가 없었으며 변수/self-join으로 흉내 냈다.","danger":"safe"},"mssql":{"command":"SELECT name, department, salary,\n  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_no,\n  RANK()       OVER (PARTITION BY department ORDER BY salary DESC) AS rank_no,\n  SUM(salary)    OVER (PARTITION BY department) AS dept_total\nFROM employee;","note":"SQL Server는 2005부터 순위 윈도우 함수를, 2012부터 프레임 집계와 LAG/LEAD 같은 함수를 지원한다. ROW_NUMBER는 페이징(OFFSET/FETCH와 함께)과 그룹별 상위 N개 행 찾기에 흔히 사용된다.","since":"SQL Server 2005","xnote":"순위 윈도우 함수는 2005에서, 프레임과 LAG/LEAD는 2012에서 도입되었다.","danger":"safe"},"mongodb":{"command":"// Closest equivalent: $setWindowFields (MongoDB 5.0+)\ndb.employee.aggregate([\n  { $setWindowFields: {\n      partitionBy: \"$department\",\n      sortBy: { salary: -1 },\n      output: { row_no: { $rank: {} },\n                prev_salary: { $shift: { output: \"$salary\", by: -1 } } }\n  } }\n])","note":"MongoDB 5.0+의 $setWindowFields 단계는 SQL 윈도우 함수에 해당한다. partitionBy와 sortBy로 윈도우를 정의하며 $rank/$denseRank/$documentNumber, $shift(LAG/LEAD), 누적 합계 같은 출력을 생성한다. 이전 버전에서는 $group + 코드로 해결했다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no window functions.","note":"Redis는 분석용 윈도우 함수를 제공하지 않는다. 정렬된 결과와 순위에는 Sorted Set(ZSET) 구조를 사용한다. ZADD로 점수를 부여하고 ZRANK/ZREVRANK로 순위를, ZRANGE로 범위를 읽는다. 복잡한 분석은 애플리케이션에서 수행한다.","danger":"safe"},"elasticsearch":{"command":"# Closest equivalent: top_hits and pipeline aggregations\nGET employee/_search\n{\n  \"size\": 0,\n  \"aggs\": { \"dept\": { \"terms\": { \"field\": \"department\" },\n    \"aggs\": { \"top_paid\": { \"top_hits\": {\n      \"sort\": [ { \"salary\": \"desc\" } ], \"size\": 3 } } } } }\n}","note":"Elasticsearch에는 직접적인 윈도우 함수가 없다. 그룹별 상위 N개 레코드에는 'top_hits' 하위 aggregation을, 이동/누적 계산에는 'derivative', 'cumulative_sum', 'moving_fn' 같은 pipeline aggregation을 사용한다. 정렬과 분할은 terms aggregation으로 수행한다.","danger":"safe"}}},{"id":"group-rollup","title":"GROUP BY, HAVING, ROLLUP/CUBE/GROUPING SETS","aliases":["group by","having","rollup","cube","grouping sets","grouping","subtotal","subtotals"],"engines":{"oracle":{"command":"-- Basic grouping + HAVING\nSELECT city, COUNT(*) AS n\nFROM customer\nGROUP BY city\nHAVING COUNT(*) > 5;\n\n-- Subtotals and grand total with ROLLUP\nSELECT city, district, SUM(balance)\nFROM customer\nGROUP BY ROLLUP(city, district);","note":"GROUP BY는 지정한 컬럼으로 행을 그룹화하고 집계 함수를 적용한다. HAVING은 그룹화된 결과를 필터링한다(WHERE는 그룹화 전에 행을 필터링). ROLLUP은 계층적 소계 + 총계를, CUBE는 모든 조합을, GROUPING SETS는 선택한 그룹 조합을 생성한다. Oracle은 세 가지를 모두 지원한다.","danger":"safe"},"postgres":{"command":"SELECT city, district, SUM(balance)\nFROM customer\nGROUP BY GROUPING SETS ((city, district), (city), ());\n\n-- CUBE produces all subset combinations\nSELECT city, district, SUM(balance)\nFROM customer\nGROUP BY CUBE(city, district);","note":"PostgreSQL 9.5+는 ROLLUP, CUBE, GROUPING SETS를 표준에 맞게 지원한다. GROUPING() 함수는 컬럼이 합계 행(NULL)에 속하는지 알려 준다. HAVING은 집계 후에, WHERE는 집계 전에 필터링한다.","since":"PostgreSQL 9.5","xnote":"ROLLUP, CUBE, GROUPING SETS는 PostgreSQL 9.5에서 도입되었다.","danger":"safe"},"mysql":{"command":"SELECT city, COUNT(*) AS n\nFROM customer\nGROUP BY city\nHAVING n > 5;\n\n-- ROLLUP in MySQL:\nSELECT city, district, SUM(balance)\nFROM customer\nGROUP BY city, district WITH ROLLUP;","note":"MySQL/MariaDB는 GROUP BY ... WITH ROLLUP 구문으로 소계/총계를 생성하며, CUBE와 GROUPING SETS는 지원하지 않는다. HAVING에서 SELECT 별칭을 사용할 수 있다(Oracle/SQL Server에서는 일반적으로 동작하지 않음). ROLLUP 행에서는 그룹화 컬럼이 NULL로 표시된다.","danger":"safe"},"mssql":{"command":"SELECT city, COUNT(*) AS n\nFROM customer\nGROUP BY city\nHAVING COUNT(*) > 5;\n\n-- ROLLUP / CUBE / GROUPING SETS\nSELECT city, district, SUM(balance)\nFROM customer\nGROUP BY ROLLUP(city, district);","note":"SQL Server는 GROUP BY와 함께 최신 구문의 ROLLUP(), CUBE(), GROUPING SETS(2008+)를 지원하며, 기존 WITH ROLLUP/WITH CUBE 형식은 deprecated다. GROUPING()과 GROUPING_ID()는 합계 행을 구분하는 데 도움이 된다.","since":"SQL Server 2008","xnote":"최신 GROUPING SETS 구문은 SQL Server 2008에서 도입되었다.","danger":"safe"},"mongodb":{"command":"// Closest equivalent: $group + $sum\ndb.customer.aggregate([\n  { $group: { _id: \"$city\", n: { $sum: 1 } } },\n  { $match: { n: { $gt: 5 } } }\n])\n// $facet is used for ROLLUP-like multi-level totals","note":"MongoDB에서 $group은 SQL GROUP BY에 해당한다. _id가 그룹화 키를, accumulator($sum, $avg, $max)가 집계를 정의한다. 그룹화 후 필터링은 $match(HAVING에 해당)로 수행한다. ROLLUP/CUBE 같은 다단계 합계는 $facet으로 여러 파이프라인을 실행해 얻는다.","danger":"safe"},"redis":{"command":"# Not applicable: core Redis has no GROUP BY.\n# Aggregation with RediSearch:\nFT.AGGREGATE customer_idx \"*\" GROUPBY 1 @city REDUCE COUNT 0 AS n","note":"기본 Redis는 그룹화를 제공하지 않는다. RediSearch 모듈의 FT.AGGREGATE 명령이 GROUPBY와 REDUCE(COUNT, SUM, AVG)로 SQL과 유사한 집계를 수행한다. FILTER 단계는 HAVING처럼 그룹화된 결과를 필터링한다. 모듈이 없으면 애플리케이션에서 집계한다.","danger":"safe"},"elasticsearch":{"command":"GET customer/_search\n{\n  \"size\": 0,\n  \"aggs\": {\n    \"by_city\": { \"terms\": { \"field\": \"city\" },\n      \"aggs\": { \"total\": { \"sum\": { \"field\": \"balance\" } } } }\n  }\n}","note":"Elasticsearch에서 그룹화는 'terms' bucket aggregation으로, 집계는 metric aggregation(sum, avg, cardinality)으로 수행한다. HAVING과 유사한 필터링에는 'bucket_selector' pipeline aggregation을 사용한다. 다단계 소계는 중첩 aggregation으로 구성한다.","danger":"safe"}}},{"id":"case-expr","title":"CASE 표현식(조건부 값)","aliases":["case","conditional","when","then","decode","if then","conditional value"],"engines":{"oracle":{"command":"-- Searched CASE\nSELECT name,\n  CASE WHEN salary >= 20000 THEN 'high'\n       WHEN salary >= 10000 THEN 'medium'\n       ELSE 'low' END AS tier\nFROM employee;\n\n-- Oracle-specific shortcut: DECODE (equality comparison)\nSELECT name, DECODE(status, 'A', 'Active', 'P', 'Passive', 'Unknown') FROM employee;","note":"CASE는 조건에 따라 표현식을 다른 값으로 매핑하며, 단순 형식(CASE my_column WHEN value)과 검색 형식(CASE WHEN condition)이 있다. Oracle의 DECODE도 비슷한 역할을 하지만 동등 비교만 하고 NULL을 같은 값으로 취급한다. 이식성을 위해 CASE를 권장한다.","danger":"safe"},"postgres":{"command":"SELECT name,\n  CASE WHEN salary >= 20000 THEN 'high'\n       WHEN salary >= 10000 THEN 'medium'\n       ELSE 'low' END AS tier\nFROM employee;","note":"PostgreSQL은 표준 CASE 표현식을 완전히 지원하며, 값을 생성하는 모든 위치(SELECT, WHERE, ORDER BY, GROUP BY)에서 사용할 수 있다. Oracle 방식의 DECODE는 없으므로 CASE를 사용하거나, 짧은 조건에는 COALESCE/NULLIF를 사용한다.","danger":"safe"},"mysql":{"command":"SELECT name,\n  CASE WHEN salary >= 20000 THEN 'high'\n       WHEN salary >= 10000 THEN 'medium'\n       ELSE 'low' END AS tier\nFROM employee;\n\n-- IF() for a short condition:\nSELECT name, IF(active = 1, 'Yes', 'No') FROM employee;","note":"MySQL/MariaDB는 표준 CASE 표현식을 지원하며, 빠른 양자택일 조건을 위한 IF(condition, if_true, if_false) 함수도 있다(SQL Server의 IIF와 유사). CASE는 모든 표현식 위치에서 사용할 수 있다.","danger":"safe"},"mssql":{"command":"SELECT name,\n  CASE WHEN salary >= 20000 THEN 'high'\n       WHEN salary >= 10000 THEN 'medium'\n       ELSE 'low' END AS tier\nFROM employee;\n\n-- Shortcut: IIF() (2012+)\nSELECT name, IIF(active = 1, 'Yes', 'No') FROM employee;","note":"SQL Server는 표준 CASE를 지원하며, 2012부터 양자택일 조건용 IIF() 약식과 다중 매칭용 CHOOSE()가 있다. IIF는 실제로 CASE로 변환된다. CASE는 ORDER BY와 computed column에서도 사용할 수 있다.","danger":"safe"},"mongodb":{"command":"// Closest equivalent: $switch or $cond\ndb.employee.aggregate([\n  { $project: { name: 1, tier: {\n      $switch: { branches: [\n        { case: { $gte: [\"$salary\", 20000] }, then: \"high\" },\n        { case: { $gte: [\"$salary\", 10000] }, then: \"medium\" } ],\n        default: \"low\" } } } }\n])","note":"MongoDB aggregation에서 CASE에 해당하는 것은 $switch(다중 분기)와 $cond(인수 3개의 if-then-else) 연산자다. $project나 $addFields 안에서 조건부 파생 필드를 만드는 데 사용한다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no in-query conditional expression.","note":"Redis에는 CASE 같은 쿼리 표현식이 없다. 조건부 로직은 애플리케이션 코드나 서버 측 Lua 스크립트(EVAL)로 작성한다. RediSearch FT.AGGREGATE 안의 APPLY로 제한적인 표현식 평가가 가능하다.","danger":"safe"},"elasticsearch":{"command":"# Closest equivalent: a condition inside a runtime field or script\nGET employee/_search\n{\n  \"runtime_mappings\": { \"tier\": { \"type\": \"keyword\",\n    \"script\": \"if (doc['salary'].value >= 20000) emit('high'); else if (doc['salary'].value >= 10000) emit('medium'); else emit('low');\" } },\n  \"fields\": [\"tier\"]\n}","note":"Elasticsearch에서 쿼리 내 CASE 로직은 Painless 스크립트(runtime field, script_fields 또는 scripted metric)로 작성한다. if/else 흐름은 Painless 언어로 정의한다. 'range' aggregation도 분류를 위한 CASE와 유사한 버킷을 생성할 수 있다.","danger":"safe"}}},{"id":"union-set","title":"UNION / UNION ALL / INTERSECT / EXCEPT(MINUS)","aliases":["union","union all","intersect","except","minus","set operations","combine","intersection","difference"],"engines":{"oracle":{"command":"-- Union without duplicates\nSELECT city FROM customer\nUNION\nSELECT city FROM supplier;\n\n-- Intersection and difference (MINUS instead of EXCEPT in Oracle)\nSELECT city FROM customer INTERSECT SELECT city FROM supplier;\nSELECT city FROM customer MINUS SELECT city FROM supplier;","note":"집합 연산자는 두 쿼리의 결과 집합을 결합한다. UNION은 중복을 제거하고(더 느림), UNION ALL은 모두 유지하며(더 빠름), INTERSECT는 공통 행을, 차집합 연산자는 첫 번째 집합에만 있는 행을 반환한다. Oracle은 차집합에 표준 EXCEPT 대신 MINUS를 사용한다(21c부터 EXCEPT도 있음).","danger":"safe"},"postgres":{"command":"SELECT city FROM customer\nUNION\nSELECT city FROM supplier;\n\nSELECT city FROM customer INTERSECT SELECT city FROM supplier;\nSELECT city FROM customer EXCEPT SELECT city FROM supplier;","note":"PostgreSQL은 표준 UNION, UNION ALL, INTERSECT, EXCEPT를 완전히 지원한다(차집합은 MINUS가 아닌 EXCEPT). 각 연산자의 ALL 형식(INTERSECT ALL, EXCEPT ALL)은 중복 개수를 보존한다. 컬럼 수와 타입이 호환되어야 한다.","danger":"safe"},"mysql":{"command":"SELECT city FROM customer\nUNION\nSELECT city FROM supplier;\n\n-- INTERSECT/EXCEPT arrive with MySQL 8.0.31+ and MariaDB 10.3+\nSELECT city FROM customer INTERSECT SELECT city FROM supplier;","note":"MySQL은 오랫동안 UNION / UNION ALL만 지원했으며, INTERSECT와 EXCEPT는 MySQL 8.0.31(MariaDB 10.3)에서야 추가되었다. 이전 버전에서는 교집합에 INNER JOIN/EXISTS를, 차집합에 LEFT JOIN ... IS NULL 또는 NOT EXISTS를 사용한다.","since":"MySQL 8.0.31","xnote":"INTERSECT/EXCEPT는 MySQL 8.0.31(MariaDB 10.3)에서 도입되었으며, 그 이전에는 JOIN/EXISTS로 해결했다.","danger":"safe"},"mssql":{"command":"SELECT city FROM customer\nUNION\nSELECT city FROM supplier;\n\nSELECT city FROM customer INTERSECT SELECT city FROM supplier;\nSELECT city FROM customer EXCEPT SELECT city FROM supplier;","note":"SQL Server는 표준 UNION, UNION ALL, INTERSECT, EXCEPT(2005+)를 지원하며, 차집합 연산자는 EXCEPT다(MINUS 아님). INTERSECT/EXCEPT는 NULL 값을 같은 값으로 취급해 행을 비교한다.","since":"SQL Server 2005","danger":"safe"},"mongodb":{"command":"// Closest equivalent: $unionWith (MongoDB 4.4+)\ndb.customer.aggregate([\n  { $project: { city: 1, _id: 0 } },\n  { $unionWith: { coll: \"supplier\",\n      pipeline: [ { $project: { city: 1, _id: 0 } } ] } },\n  { $group: { _id: \"$city\" } }  // UNION (distinct) effect\n])","note":"MongoDB 4.4+의 $unionWith 단계는 두 컬렉션의 결과를 결합한다(UNION ALL과 유사). 중복을 제거하려면 이후 $group으로 중복을 제거한다. INTERSECT/EXCEPT는 직접 대응하는 기능이 없으며 $lookup이나 집합 연산자($setIntersection, $setDifference)로 모델링한다.","danger":"safe"},"redis":{"command":"# Set operations with the Set data type:\nSUNION city_customer city_supplier    # union\nSINTER city_customer city_supplier    # intersection\nSDIFF  city_customer city_supplier    # difference","note":"Redis에는 SQL이 없지만 Set 데이터 타입이 네이티브 집합 연산을 제공한다: SUNION(합집합), SINTER(교집합), SDIFF(차집합)과 결과를 저장하는 STORE 변형. 이 연산은 쿼리 결과가 아니라 미리 구성된 집합 멤버에 대해 동작한다.","danger":"safe"},"elasticsearch":{"command":"# There is no direct set operator.\n# For a union, search multiple indices in one query:\nGET customer,supplier/_search\n{ \"query\": { \"match_all\": {} } }","note":"Elasticsearch는 UNION/INTERSECT/EXCEPT를 제공하지 않는다. 합집합 효과는 쉼표로 구분한 여러 인덱스를 조회해 얻고, 교집합과 차집합은 bool 쿼리의 must/should/must_not 절이나 애플리케이션 측 집합 로직으로 처리한다.","danger":"safe"}}},{"id":"exists-in","title":"EXISTS / IN / NOT EXISTS(서브쿼리)","aliases":["exists","in","not exists","not in","subquery","sub-select","correlated subquery"],"engines":{"oracle":{"command":"-- Customers with orders (EXISTS)\nSELECT name FROM customer m\nWHERE EXISTS (SELECT 1 FROM orders s WHERE s.customer_id = m.id);\n\n-- List membership with IN\nSELECT name FROM customer\nWHERE city IN ('Ankara', 'Izmir');\n\n-- Customers with no orders at all (NOT EXISTS)\nSELECT name FROM customer m\nWHERE NOT EXISTS (SELECT 1 FROM orders s WHERE s.customer_id = m.id);","note":"EXISTS는 상관 서브쿼리가 최소 한 행을 반환하는지 확인한다(첫 일치에서 멈추므로 빠르다). IN은 값이 목록/서브쿼리 결과에 있는지 확인한다. 주의: 서브쿼리가 NULL을 생성할 수 있으면 NOT IN은 예상치 못한 결과를 내므로 NOT EXISTS가 더 안전하다. Oracle 옵티마이저는 둘을 같은 실행 계획으로 변환하는 경우가 많다.","danger":"safe"},"postgres":{"command":"SELECT name FROM customer m\nWHERE EXISTS (SELECT 1 FROM orders s WHERE s.customer_id = m.id);\n\nSELECT name FROM customer\nWHERE city IN (SELECT city FROM target_cities);\n\nSELECT name FROM customer m\nWHERE NOT EXISTS (SELECT 1 FROM orders s WHERE s.customer_id = m.id);","note":"PostgreSQL은 EXISTS/IN/NOT EXISTS를 완전히 지원한다. NOT IN에서 서브쿼리에 NULL이 있으면 전체 결과가 비어 버릴 수 있으므로 NOT EXISTS나 LEFT JOIN ... IS NULL이 권장된다. 플래너는 대부분의 경우 IN과 EXISTS를 동일하게 semi-join으로 처리한다.","danger":"safe"},"mysql":{"command":"SELECT name FROM customer m\nWHERE EXISTS (SELECT 1 FROM orders s WHERE s.customer_id = m.id);\n\nSELECT name FROM customer\nWHERE city IN ('Ankara', 'Izmir');\n\nSELECT name FROM customer m\nWHERE NOT EXISTS (SELECT 1 FROM orders s WHERE s.customer_id = m.id);","note":"MySQL/MariaDB는 EXISTS/IN/NOT EXISTS를 지원한다. MySQL 5.6 이전에는 상관 서브쿼리가 느릴 수 있었으며, 5.6+는 semi-join과 materialization 최적화로 IN 서브쿼리를 가속했다. NOT IN + NULL 함정은 여기서도 적용된다.","danger":"safe"},"mssql":{"command":"SELECT name FROM customer m\nWHERE EXISTS (SELECT 1 FROM orders s WHERE s.customer_id = m.id);\n\nSELECT name FROM customer\nWHERE city IN (SELECT city FROM target_cities);\n\nSELECT name FROM customer m\nWHERE NOT EXISTS (SELECT 1 FROM orders s WHERE s.customer_id = m.id);","note":"SQL Server는 EXISTS/IN/NOT EXISTS를 지원하며, 옵티마이저는 대부분 이를 semi/anti-join으로 처리한다. NOT IN 서브쿼리에 NULL이 있으면 결과가 예상치 않게 비어 버릴 수 있으므로 NOT EXISTS가 안전한 선택이다. 비슷한 멤버십 로직에 INTERSECT/EXCEPT도 사용할 수 있다.","danger":"safe"},"mongodb":{"command":"// IN equivalent: the $in operator\ndb.customer.find({ city: { $in: [\"Ankara\", \"Izmir\"] } })\n\n// EXISTS equivalent: match check with $lookup + $match\ndb.customer.aggregate([\n  { $lookup: { from: \"orders\", localField: \"id\",\n      foreignField: \"customer_id\", as: \"s\" } },\n  { $match: { \"s.0\": { $exists: true } } }  // at least one order\n])","note":"MongoDB에서 값 목록 멤버십은 $in / $nin 연산자로 처리한다. 서브쿼리 EXISTS에 해당하는 방법은 $lookup으로 일치하는 문서 배열을 가져와 배열이 비었는지로 필터링하는 것이다. 필드 존재 확인은 별도의 { field: { $exists: true } } 연산자다(행 관계가 아니라 필드의 존재 여부).","danger":"safe"},"redis":{"command":"# Membership check with Set/Hash:\nSISMEMBER target_cities \"Ankara\"   # is it in the set? (IN-like)\nEXISTS customer:1                 # does the key exist?","note":"Redis에는 서브쿼리가 없다. 값 멤버십에는 Set에 SISMEMBER를 사용한다(IN과 유사). EXISTS 명령은 서브쿼리가 아니며 지정한 키가 존재하는지를 반환한다. 관계형 존재 확인은 여러 키를 읽어 애플리케이션 측에서 수행한다.","danger":"safe"},"elasticsearch":{"command":"# IN equivalent: terms query\nGET customer/_search\n{ \"query\": { \"terms\": { \"city\": [\"Ankara\", \"Izmir\"] } } }\n\n# Field existence: exists query\nGET customer/_search\n{ \"query\": { \"exists\": { \"field\": \"phone\" } } }","note":"Elasticsearch에서 IN에 해당하는 것은 'terms' 쿼리(여러 값 중 하나와 일치)이며, NOT IN은 bool must_not 안의 terms다. 'exists' 쿼리는 SQL EXISTS에 해당하지 않으며, 문서에서 필드에 값이 있는지 확인한다. 테이블 간 상관 EXISTS에는 보통 비정규화가 필요하다.","danger":"safe"}}},{"id":"limit-paging","title":"행 제한 / 페이징(LIMIT/OFFSET, FETCH, ROWNUM, TOP)","aliases":["limit","offset","fetch","rownum","top","paging","page through results","pagination","row limit"],"engines":{"oracle":{"command":"-- 12c+ standard: OFFSET/FETCH\nSELECT name, salary FROM employee\nORDER BY salary DESC\nOFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;\n\n-- Older versions: with ROWNUM\nSELECT * FROM (\n  SELECT name, salary FROM employee ORDER BY salary DESC\n) WHERE ROWNUM <= 10;","note":"결과 집합을 제한하거나 페이징하는 데 사용한다. Oracle 12c부터 표준 OFFSET n ROWS FETCH NEXT m ROWS ONLY를 지원한다. 그 이전에는 ROWNUM 의사 컬럼을 사용했으며, ROWNUM은 정렬 전에 할당되므로 ORDER BY 서브쿼리로 감싸는 것이 필수다.","since":"Oracle 12c","xnote":"Oracle 12c 이전에는 ROWNUM으로 행을 제한했다.","danger":"safe"},"postgres":{"command":"-- LIMIT/OFFSET\nSELECT name, salary FROM employee\nORDER BY salary DESC\nLIMIT 10 OFFSET 20;\n\n-- Standard equivalent: FETCH\nSELECT name, salary FROM employee\nORDER BY salary DESC\nOFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;","note":"PostgreSQL은 LIMIT n OFFSET m과 표준 OFFSET ... FETCH 형식을 모두 지원한다. 큰 OFFSET 값은 느리므로, 깊은 페이징에는 키 기반(keyset / 'WHERE id > last_id') 페이지네이션이 더 효율적이다. ORDER BY 없는 LIMIT은 결과 순서를 보장하지 않는다.","danger":"safe"},"mysql":{"command":"-- LIMIT [offset,] row_count\nSELECT name, salary FROM employee\nORDER BY salary DESC\nLIMIT 10 OFFSET 20;\n\n-- Short form: LIMIT offset, row_count\nSELECT name, salary FROM employee ORDER BY salary DESC LIMIT 20, 10;","note":"MySQL/MariaDB는 LIMIT 키워드를 두 가지 형식으로 사용한다: LIMIT row_count OFFSET offset 또는 LIMIT offset, row_count. MySQL 8.0.19+는 표준 FETCH 구문도 허용한다. 깊은 OFFSET 성능 문제에는 keyset 페이지네이션이 권장된다.","danger":"safe"},"mssql":{"command":"-- First N with TOP\nSELECT TOP 10 name, salary FROM employee ORDER BY salary DESC;\n\n-- OFFSET/FETCH for paging (2012+), ORDER BY mandatory\nSELECT name, salary FROM employee\nORDER BY salary DESC\nOFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;","note":"SQL Server는 첫 N개 행에 TOP n(TOP (n) PERCENT / WITH TIES 변형 포함)을, 2012부터 페이징에 OFFSET ... FETCH를 사용한다. OFFSET/FETCH가 동작하려면 ORDER BY가 필수다. SQL Server에는 LIMIT 키워드가 없다.","since":"SQL Server 2012","xnote":"OFFSET/FETCH 페이징은 2012에서 도입되었으며, 그 이전에는 TOP이나 ROW_NUMBER로 수행했다.","danger":"safe"},"mongodb":{"command":"// Paging with skip/limit\ndb.employee.find()\n  .sort({ salary: -1 })\n  .skip(20)\n  .limit(10)","note":"MongoDB에서 limit()은 반환할 문서 수를, skip()은 건너뛸 문서 수를 지정하며, sort()와 함께 사용해 페이징을 제공한다. 큰 skip 값은 비용이 크므로, 깊은 페이징에는 마지막으로 본 값을 기준으로 한 범위(keyset) 쿼리가 권장된다.","danger":"safe"},"redis":{"command":"# Range read on a sorted set (ZSET):\nZREVRANGE salary_sorted 20 29    # members ranked 21-30\n# Range on a list:\nLRANGE queue 0 9              # first 10 elements","note":"Redis에는 SQL LIMIT이 없지만 범위 읽기 명령이 있다: Sorted Set에는 ZRANGE/ZREVRANGE start stop(인덱스 기반) 또는 ZRANGEBYSCORE ... LIMIT offset count, List에는 LRANGE. 이 명령들이 페이징과 유사한 접근을 제공한다.","danger":"safe"},"elasticsearch":{"command":"GET employee/_search\n{\n  \"from\": 20,\n  \"size\": 10,\n  \"sort\": [ { \"salary\": \"desc\" } ]\n}","note":"Elasticsearch에서 'size'는 반환할 hit 수를, 'from'은 건너뛸 수를 지정한다(LIMIT/OFFSET에 해당). from+size는 최대 index.max_result_window(기본값 10,000)까지 가능하며, 깊은 페이징에는 'search_after'(keyset)나 PIT(point-in-time)를 사용한다.","danger":"safe"}}},{"id":"merge-upsert","title":"MERGE / UPSERT(있으면 갱신, 없으면 삽입)","aliases":["merge","upsert","on conflict","on duplicate key","insert or update","update if exists"],"engines":{"oracle":{"command":"MERGE INTO customer m\nUSING (SELECT 1 AS id, 'Ali' AS name, 500 AS balance FROM dual) k\nON (m.id = k.id)\nWHEN MATCHED THEN\n  UPDATE SET m.name = k.name, m.balance = k.balance\nWHEN NOT MATCHED THEN\n  INSERT (id, name, balance) VALUES (k.id, k.name, k.balance);","note":"MERGE(upsert)는 소스와 대상을 매칭해 일치하는 행은 갱신하고 일치하지 않는 행은 삽입하며, 대량 데이터 동기화에 적합하다. Oracle은 MERGE를 강력하게 지원하며 WHEN MATCHED ... DELETE와 조건부 갱신도 작성할 수 있다. 단일 행 소스에는 dual 테이블을 사용한다.","since":"Oracle 9i","danger":"caution"},"postgres":{"command":"-- The PostgreSQL way: INSERT ... ON CONFLICT\nINSERT INTO customer (id, name, balance)\nVALUES (1, 'Ali', 500)\nON CONFLICT (id) DO UPDATE\n  SET name = EXCLUDED.name, balance = EXCLUDED.balance;","note":"PostgreSQL의 고전적인 upsert는 INSERT ... ON CONFLICT (unique column) DO UPDATE / DO NOTHING이며, 충돌한 행의 새 값은 EXCLUDED 별칭으로 접근한다. PostgreSQL 15부터 표준 MERGE 문도 지원한다. ON CONFLICT는 단일 테이블에서 원자적이며 널리 사용된다.","since":"PostgreSQL 15","xnote":"표준 MERGE 문은 PostgreSQL 15에서 도입되었으며, 그 이전에는 INSERT ... ON CONFLICT(9.5)로 upsert를 수행했다.","danger":"caution"},"mysql":{"command":"INSERT INTO customer (id, name, balance)\nVALUES (1, 'Ali', 500)\nON DUPLICATE KEY UPDATE\n  name = VALUES(name), balance = VALUES(balance);","note":"MySQL/MariaDB는 upsert에 INSERT ... ON DUPLICATE KEY UPDATE를 사용한다. unique 키나 primary 키가 충돌하면 행이 갱신된다. 새 값은 VALUES(my_column)(8.0.19+에서는 'new.my_column' 행 별칭)으로 접근한다. INSERT IGNORE는 충돌 시 행을 조용히 건너뛴다. MySQL에는 표준 MERGE가 없다.","danger":"caution"},"mssql":{"command":"MERGE INTO customer AS m\nUSING (VALUES (1, 'Ali', 500)) AS k(id, name, balance)\nON m.id = k.id\nWHEN MATCHED THEN\n  UPDATE SET m.name = k.name, m.balance = k.balance\nWHEN NOT MATCHED THEN\n  INSERT (id, name, balance) VALUES (k.id, k.name, k.balance);","note":"SQL Server는 2008부터 MERGE 문을 지원하며, 대상의 남는 행을 삭제하는 WHEN NOT MATCHED BY SOURCE 같은 추가 분기를 제공한다. MERGE 문은 세미콜론으로 끝나야 한다. 참고: 일부 경계 사례 버그 때문에 단순한 upsert에는 INSERT/UPDATE 문을 따로 작성하는 것이 나을 수 있다.","since":"SQL Server 2008","danger":"caution"},"mongodb":{"command":"// Find-and-update or insert with upsert:true\ndb.customer.updateOne(\n  { id: 1 },\n  { $set: { name: \"Ali\", balance: 500 } },\n  { upsert: true }\n)","note":"MongoDB에서 upsert는 네이티브 옵션이다. updateOne/updateMany/findOneAndUpdate에 { upsert: true }를 추가하면 일치하는 문서는 갱신되고, 없으면 새 문서가 생성된다. 대량 동기화에는 aggregation $merge 단계(대상 컬렉션에 쓰기/갱신)가 SQL MERGE에 더 가깝다.","danger":"caution"},"redis":{"command":"# SET is a natural upsert: it overwrites the key if present, otherwise creates it.\nSET customer:1 '{\"name\":\"Ali\",\"balance\":500}'\n# Update/add a hash field:\nHSET customer:1 name Ali balance 500","note":"Redis에서 쓰기 명령은 본질적으로 upsert다. SET/HSET은 키나 필드가 있으면 갱신하고, 없으면 생성한다. 없을 때만 쓰려면 SET ... NX 옵션을, 있을 때만 쓰려면 XX를 사용한다. '있으면 갱신, 없으면 삽입'의 구분은 이미 대부분 명령의 기본 동작이다.","danger":"safe"},"elasticsearch":{"command":"# An index call with a document id acts like an upsert (replaces if present):\nPUT customer/_doc/1\n{ \"name\": \"Ali\", \"balance\": 500 }\n# Partial update + insert if absent:\nPOST customer/_update/1\n{ \"doc\": { \"balance\": 500 }, \"doc_as_upsert\": true }","note":"Elasticsearch에서 같은 _id로 index(PUT)를 호출하면 문서가 있으면 전체를 교체하고, 없으면 생성한다(upsert와 유사). 없으면 삽입하는 부분 갱신에는 _update API에서 'doc_as_upsert': true나 'upsert' 블록을 사용한다. 스크립트 기반 upsert도 가능하다.","danger":"caution"}}},{"id":"returning-output","title":"변경된 행 반환(RETURNING / OUTPUT)","aliases":["returning","output","inserted","deleted","returned value","modified row"],"engines":{"oracle":{"command":"-- Single row: RETURNING ... INTO (PL/SQL bind variable)\nUPDATE employee SET salary = salary * 1.1\nWHERE id = 5\nRETURNING salary INTO :new_salary;\n\n-- Get back the key generated by an INSERT\nINSERT INTO employee (name) VALUES ('Ayse')\nRETURNING id INTO :new_id;","note":"RETURNING은 별도의 SELECT 없이 INSERT/UPDATE/DELETE로 변경된 행의 값(예: 자동 생성 ID)을 반환한다. Oracle에서는 단일 행은 RETURNING ... INTO로 바인드 변수에, 여러 행은 RETURNING ... BULK COLLECT INTO로 컬렉션에 반환한다.","danger":"safe"},"postgres":{"command":"INSERT INTO employee (name, salary)\nVALUES ('Ayse', 15000)\nRETURNING id, name;\n\nUPDATE employee SET salary = salary * 1.1\nWHERE department = 'IT'\nRETURNING id, salary;","note":"PostgreSQL의 RETURNING 절은 매우 유연하다. INSERT, UPDATE, DELETE, MERGE 문의 영향을 받은 행의 모든 컬럼(계산식 포함)을 결과 집합으로 바로 반환한다. 생성된 serial/identity 값을 얻는 가장 깔끔한 방법이다.","since":"PostgreSQL 8.2","danger":"safe"},"mysql":{"command":"-- MySQL has no RETURNING; the generated key is retrieved separately:\nINSERT INTO employee (name, salary) VALUES ('Ayse', 15000);\nSELECT LAST_INSERT_ID();\n\n-- MariaDB 10.5+ supports INSERT ... RETURNING:\nINSERT INTO employee (name) VALUES ('Ayse') RETURNING id;","note":"MySQL은 표준 RETURNING을 지원하지 않는다. INSERT로 생성된 auto-increment 키는 LAST_INSERT_ID()로 가져오고, 갱신된 값은 별도의 SELECT로 읽는다. MariaDB는 10.5부터 INSERT ... RETURNING을, 10.0부터 DELETE ... RETURNING을 제공한다.","danger":"safe"},"mssql":{"command":"INSERT INTO employee (name, salary)\nOUTPUT INSERTED.id, INSERTED.name\nVALUES ('Ayse', 15000);\n\nUPDATE employee SET salary = salary * 1.1\nOUTPUT DELETED.salary AS old_salary, INSERTED.salary AS new_salary\nWHERE id = 5;","note":"SQL Server에서 이에 해당하는 것은 OUTPUT 절이다. INSERTED(새 값)와 DELETED(이전 값) 의사 테이블에 접근하며, UPDATE에서는 둘 다 사용할 수 있다. OUTPUT ... INTO로 결과를 테이블/테이블 변수에 쓸 수 있다. 변경 내용을 감사 테이블에 복사하는 데 적합하다.","since":"SQL Server 2005","xnote":"OUTPUT 절은 SQL Server 2005에서 도입되었다.","danger":"safe"},"mongodb":{"command":"// findOneAndUpdate returns the modified document\ndb.employee.findOneAndUpdate(\n  { id: 5 },\n  { $mul: { salary: 1.1 } },\n  { returnDocument: \"after\" }   // return the updated version\n)","note":"MongoDB에서 findOneAndUpdate / findOneAndDelete / findOneAndReplace 명령은 영향을 받은 문서를 단일 원자적 작업으로 반환한다. returnDocument: 'before'는 변경 전 버전을, 'after'는 변경 후 버전을 반환한다. insertOne은 삽입된 문서의 _id를 반환한다.","danger":"safe"},"redis":{"command":"# Some commands return the old/new value directly:\nSET counter 0 GET       # (6.2+) returns the old value and writes the new one (replaces GETSET)\nINCR visits          # increments and returns the new value","note":"Redis에는 별도의 RETURNING이 없지만 많은 쓰기 명령이 결과를 바로 반환한다. INCR/INCRBY는 새 값을, SET ... GET은 이전 값을(GETSET은 6.2부터 deprecated), LPOP/RPOP은 제거된 요소를 반환한다. 원자적 읽기-쓰기에서 이 동작이 RETURNING을 대신한다.","danger":"safe"},"elasticsearch":{"command":"# The _update response can return the result:\nPOST employee/_update/5\n{\n  \"script\": { \"source\": \"ctx._source.salary *= 1.1\" },\n  \"_source\": true\n}\n# The response returns the updated document in get._source","note":"Elasticsearch에서 index/update 작업은 결과 메타데이터(_id, _version, result)를 반환한다. 갱신된 문서 본문을 얻으려면 _update 요청에 '_source': true를 추가한다. 응답의 get 필드에 현재 _source가 포함되므로 별도의 GET 호출이 필요 없다.","danger":"safe"}}},{"id":"lateral-apply","title":"LATERAL / CROSS APPLY(행 단위 서브쿼리)","aliases":["lateral","cross apply","outer apply","row-by-row subquery","correlated table function"],"engines":{"oracle":{"command":"-- Each customer's 3 most recent orders (LATERAL, 12c+)\nSELECT m.name, s.created_at, s.amount\nFROM customer m,\n     LATERAL (SELECT created_at, amount FROM orders\n              WHERE customer_id = m.id\n              ORDER BY created_at DESC FETCH FIRST 3 ROWS ONLY) s;\n\n-- Alternative: CROSS APPLY (12c+)\nSELECT m.name, s.amount\nFROM customer m\nCROSS APPLY (SELECT amount FROM orders WHERE customer_id = m.id) s;","note":"LATERAL/APPLY는 FROM 안의 서브쿼리가 왼쪽 테이블의 컬럼을 참조할 수 있게 하므로 '각 행에 대한 상위 N개 행' 같은 상관 조인을 작성할 수 있다. Oracle 12c는 ANSI LATERAL과 CROSS/OUTER APPLY 구문을 모두 지원한다.","since":"Oracle 12c","xnote":"LATERAL과 CROSS/OUTER APPLY는 Oracle 12c에서 도입되었다.","danger":"safe"},"postgres":{"command":"SELECT m.name, s.created_at, s.amount\nFROM customer m\nCROSS JOIN LATERAL (\n  SELECT created_at, amount FROM orders\n  WHERE customer_id = m.id\n  ORDER BY created_at DESC LIMIT 3\n) s;","note":"PostgreSQL에서 왼쪽 행에 의존하는 서브쿼리는 LATERAL 키워드로 작성한다(JOIN LATERAL / CROSS JOIN LATERAL / LEFT JOIN LATERAL). '그룹별 상위 N개'와 집합 반환 함수로 행을 펼치는 데 매우 유용하다. CROSS APPLY 구문은 없으며 LATERAL을 사용한다.","since":"PostgreSQL 9.3","xnote":"LATERAL은 PostgreSQL 9.3에서 도입되었다.","danger":"safe"},"mysql":{"command":"-- MySQL 8.0.14+ supports LATERAL\nSELECT m.name, s.amount\nFROM customer m\nJOIN LATERAL (\n  SELECT amount FROM orders\n  WHERE customer_id = m.id\n  ORDER BY created_at DESC LIMIT 3\n) s ON true;","note":"MySQL 8.0.14부터 LATERAL derived table을 지원하며, 서브쿼리가 외부 테이블의 컬럼을 참조할 수 있다. 그 이전과 CROSS APPLY에는 대응 기능이 없었으며, '그룹별 상위 N개'는 윈도우 함수나 상관 서브쿼리로 해결했다. MariaDB에는 아직 LATERAL이 없다.","since":"MySQL 8.0.14","xnote":"LATERAL derived table은 MySQL 8.0.14에서 도입되었으며, MariaDB에는 없다.","danger":"safe"},"mssql":{"command":"SELECT m.name, s.created_at, s.amount\nFROM customer m\nCROSS APPLY (\n  SELECT TOP 3 created_at, amount FROM orders\n  WHERE customer_id = m.id\n  ORDER BY created_at DESC\n) s;\n-- Use OUTER APPLY to return non-matching rows too","note":"SQL Server에서 LATERAL에 해당하는 것은 APPLY 연산자다: CROSS APPLY(INNER JOIN LATERAL과 유사, 일치하지 않는 행 제외)와 OUTER APPLY(LEFT JOIN LATERAL과 유사, 일치하지 않으면 NULL). 각 행에 대해 테이블 반환 함수를 호출하거나 그룹별 상위 N개 쿼리에 흔히 사용한다.","since":"SQL Server 2005","xnote":"CROSS/OUTER APPLY 연산자는 SQL Server 2005에서 도입되었다.","danger":"safe"},"mongodb":{"command":"// Closest equivalent: $lookup with a sub-pipeline\ndb.customer.aggregate([\n  { $lookup: {\n      from: \"orders\",\n      let: { mid: \"$id\" },\n      pipeline: [\n        { $match: { $expr: { $eq: [\"$customer_id\", \"$$mid\"] } } },\n        { $sort: { created_at: -1 } }, { $limit: 3 }\n      ],\n      as: \"last3\"\n  } }\n])","note":"MongoDB에서 LATERAL/APPLY에 해당하는 것은 하위 파이프라인과 let 변수를 사용하는 $lookup 형식이다. 외부 문서의 필드를 let으로 $$variable 형태로 하위 파이프라인에 전달하고, 그 안에서 매칭/정렬/제한을 수행한다. 이것으로 '각 문서에 관련된 상위 N개' 패턴을 해결한다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no subqueries / LATERAL.","note":"Redis는 행 단위 상관 서브쿼리를 제공하지 않는다. '각 항목에 관련된 상위 N개' 패턴은 애플리케이션 측 루프(예: 키별 ZRANGE)나 서버 측 Lua 스크립트로 해결한다.","danger":"safe"},"elasticsearch":{"command":"# Closest equivalent: terms + top_hits sub-aggregation\nGET orders/_search\n{\n  \"size\": 0,\n  \"aggs\": { \"by_customer\": { \"terms\": { \"field\": \"customer_id\" },\n    \"aggs\": { \"last3\": { \"top_hits\": {\n      \"sort\": [ { \"created_at\": \"desc\" } ], \"size\": 3 } } } } }\n}","note":"Elasticsearch에는 LATERAL/APPLY가 없다. '그룹별 상위 N개 레코드' 패턴은 terms aggregation 안에 중첩한 top_hits 하위 aggregation으로 구현한다. 상관 서브쿼리 대신 단일 인덱스에서의 그룹화와 그룹 내 샘플링이다.","danger":"safe"}}},{"id":"pivot","title":"PIVOT / UNPIVOT(행↔열)","aliases":["pivot","unpivot","crosstab","cross tab","rows to columns","transpose"],"engines":{"oracle":{"command":"-- Turn sales by month into columns\nSELECT * FROM (\n  SELECT month, amount FROM sale\n)\nPIVOT (SUM(amount) FOR month IN ('Jan' AS jan, 'Feb' AS feb, 'Mar' AS mar));\n\n-- UNPIVOT: turn columns back into rows\nSELECT product, month, amount\nFROM sale_wide\nUNPIVOT (amount FOR month IN (jan AS 'Jan', feb AS 'Feb'));","note":"PIVOT은 행의 고유 값을 컬럼으로(크로스 탭), UNPIVOT은 컬럼을 다시 행으로 변환하며, 요약/보고서 테이블에 사용한다. Oracle은 11g부터 내장 PIVOT/UNPIVOT 연산자를 제공한다. 컬럼 목록은 정적이어야 한다(동적 컬럼에는 PIVOT XML이나 코드가 필요).","since":"Oracle 11g","xnote":"내장 PIVOT/UNPIVOT 연산자는 Oracle 11g에서 도입되었다.","danger":"safe"},"postgres":{"command":"-- The tablefunc extension for crosstab\nCREATE EXTENSION IF NOT EXISTS tablefunc;\nSELECT * FROM crosstab(\n  'SELECT product, month, amount FROM sale ORDER BY 1,2'\n) AS ct(product text, jan numeric, feb numeric, mar numeric);\n\n-- Without an extension: conditional aggregation\nSELECT product,\n  SUM(amount) FILTER (WHERE month = 'Jan') AS jan,\n  SUM(amount) FILTER (WHERE month = 'Feb') AS feb\nFROM sale GROUP BY product;","note":"PostgreSQL에는 내장 PIVOT 연산자가 없다. 두 가지 방법이 있다: tablefunc 확장의 crosstab() 함수, 또는 이식 가능한 조건부 집계(SUM(...) FILTER (WHERE ...) 또는 CASE 사용). FILTER 절은 PostgreSQL 고유의 깔끔한 피벗 기법이다.","danger":"safe"},"mysql":{"command":"-- There is no PIVOT; conditional aggregation with manual CASE\nSELECT product,\n  SUM(CASE WHEN month = 'Jan' THEN amount ELSE 0 END) AS jan,\n  SUM(CASE WHEN month = 'Feb' THEN amount ELSE 0 END) AS feb,\n  SUM(CASE WHEN month = 'Mar' THEN amount ELSE 0 END) AS mar\nFROM sale\nGROUP BY product;","note":"MySQL/MariaDB에는 PIVOT/UNPIVOT 연산자가 없으며, 크로스 탭은 GROUP BY와 대상 컬럼별 SUM(CASE WHEN ... THEN ... END)로 직접 작성한다. 컬럼이 동적이면 prepared statement로 쿼리를 문자열로 생성한다.","danger":"safe"},"mssql":{"command":"SELECT * FROM (\n  SELECT product, month, amount FROM sale\n) src\nPIVOT (\n  SUM(amount) FOR month IN ([Jan], [Feb], [Mar])\n) pvt;\n\n-- Converting back with UNPIVOT is also supported","note":"SQL Server는 2005부터 PIVOT과 UNPIVOT 연산자를 네이티브로 지원한다. PIVOT 컬럼 목록은 대괄호로 묶은 정적 값으로 지정하며, 동적 컬럼에는 동적 SQL로 쿼리를 구성한다. 작은 요약에는 조건부 집계(CASE)가 대안이다.","since":"SQL Server 2005","xnote":"PIVOT/UNPIVOT 연산자는 SQL Server 2005에서 도입되었다.","danger":"safe"},"mongodb":{"command":"// PIVOT equivalent: conditional aggregation inside $group\ndb.sale.aggregate([\n  { $group: {\n      _id: \"$product\",\n      jan: { $sum: { $cond: [ { $eq: [\"$month\", \"Jan\"] }, \"$amount\", 0 ] } },\n      feb: { $sum: { $cond: [ { $eq: [\"$month\", \"Feb\"] }, \"$amount\", 0 ] } }\n  } }\n])","note":"MongoDB에는 PIVOT 연산자가 없으며, 크로스 탭은 $group 단계에서 대상 컬럼별 $sum + $cond(조건부 집계)로 구성한다. 반대 방향(UNPIVOT)은 $objectToArray와 $unwind로 필드를 행으로 펼친다.","danger":"safe"},"redis":{"command":"# Not applicable: Redis has no pivot/cross tab.","note":"Redis에는 행과 열을 회전하는 개념이 없다. 요약/크로스 탭은 애플리케이션 측에서 계산하며, 자주 요청되는 요약은 미리 계산해 Hash 필드(컬럼당 필드 하나)에 저장할 수 있다. RediSearch FT.AGGREGATE로 그룹 집계는 가능하지만 진정한 피벗은 아니다.","danger":"safe"},"elasticsearch":{"command":"# Closest equivalent: nested terms aggregations\nGET sale/_search\n{\n  \"size\": 0,\n  \"aggs\": { \"product\": { \"terms\": { \"field\": \"product\" },\n    \"aggs\": { \"month\": { \"terms\": { \"field\": \"month\" },\n      \"aggs\": { \"total\": { \"sum\": { \"field\": \"amount\" } } } } } }\n}","note":"Elasticsearch에는 PIVOT 연산자가 없다. 크로스 요약은 중첩 terms aggregation(외부 키 / 내부 키)으로 구성하며, 결과는 컬럼이 아닌 중첩 버킷으로 반환되어 클라이언트에서 테이블로 변환한다. transform 작업으로 그룹화된 피벗 유사 대상 인덱스를 생성할 수 있다.","danger":"safe"}}},{"id":"coalesce-null","title":"NULL 처리(COALESCE/NVL/NULLIF/IS NULL)","aliases":["coalesce","nvl","nullif","is null","ifnull","isnull","null handling","empty value"],"engines":{"oracle":{"command":"-- COALESCE: returns the first non-NULL value (standard)\nSELECT name, COALESCE(phone, mobile, 'none') AS contact FROM customer;\n\n-- Oracle-specific NVL / NVL2\nSELECT NVL(balance, 0) FROM customer;\nSELECT NVL2(discount, 'yes', 'no') FROM orders;\n\n-- NULLIF and IS NULL\nSELECT NULLIF(a, b) FROM t;   -- NULL if a=b\nSELECT name FROM customer WHERE phone IS NULL;","note":"NULL(알 수 없는 값) 처리: COALESCE는 목록에서 NULL이 아닌 첫 값을 반환한다(표준, 다중 인수). Oracle의 NVL(a,b)은 인수 2개짜리 대응 함수이며, NVL2는 값이 NULL인지 여부에 따라 다른 값을 반환한다. NULLIF(a,b)는 두 값이 같으면 NULL을 반환한다. NULL은 항상 IS NULL / IS NOT NULL로 검사한다(= NULL은 동작하지 않는다).","danger":"safe"},"postgres":{"command":"SELECT name, COALESCE(phone, mobile, 'none') AS contact FROM customer;\nSELECT NULLIF(divisor, 0) FROM t;   -- prevent division by zero\nSELECT name FROM customer WHERE phone IS NULL;\n-- The equivalent of Oracle NVL is COALESCE","note":"PostgreSQL은 표준 COALESCE, NULLIF, IS NULL/IS NOT NULL을 제공하며, Oracle의 NVL 대신 COALESCE를 사용한다. NULL 비교에는 =가 아닌 IS를 사용한다. 두 NULL을 같은 값으로 취급해야 하면 'IS NOT DISTINCT FROM' 연산자를 사용한다.","danger":"safe"},"mysql":{"command":"SELECT name, COALESCE(phone, mobile, 'none') AS contact FROM customer;\n\n-- MySQL shortcut: IFNULL (two arguments)\nSELECT IFNULL(balance, 0) FROM customer;\nSELECT NULLIF(a, b) FROM t;\nSELECT name FROM customer WHERE phone IS NULL;","note":"MySQL/MariaDB는 표준 COALESCE와 NULLIF를 지원하며, IFNULL(a,b)은 인수 2개짜리 약식이다(Oracle NVL과 유사). <=> 연산자는 NULL 안전 동등 비교를 제공한다(두 NULL을 같은 값으로 취급). NULL은 IS NULL / IS NOT NULL로 검사한다.","danger":"safe"},"mssql":{"command":"SELECT name, COALESCE(phone, mobile, 'none') AS contact FROM customer;\n\n-- SQL Server shortcut: ISNULL (two arguments)\nSELECT ISNULL(balance, 0) FROM customer;\nSELECT NULLIF(a, b) FROM t;\nSELECT name FROM customer WHERE phone IS NULL;","note":"SQL Server는 표준 COALESCE(다중 인수, 데이터 타입 승격)와 인수 2개짜리 ISNULL(a,b)을 제공하며, 둘은 미묘하게 다르다(COALESCE는 표준이며 타입 우선순위 면에서 더 유연하다). NULLIF와 IS NULL도 지원한다. ISNULL은 Oracle NVL에 해당한다(이름이 비슷하다).","danger":"safe"},"mongodb":{"command":"// COALESCE equivalent: $ifNull\ndb.customer.aggregate([\n  { $project: { name: 1,\n      contact: { $ifNull: [\"$phone\", \"$mobile\", \"none\"] } } }\n])\n// NULL / missing field check:\ndb.customer.find({ phone: { $in: [null] } })","note":"MongoDB aggregation에서 COALESCE에 해당하는 것은 $ifNull 연산자다(다중 인수, null/missing이 아닌 첫 값 반환). 쿼리에서 { field: null }은 값이 null인 문서와 필드가 아예 없는 문서를 모두 매칭하므로, { field: { $type: 'null' } }이나 $exists로 구분을 명확히 한다.","danger":"safe"},"redis":{"command":"# Instead of NULL, Redis has the concept of 'no such key/field':\nEXISTS customer:1              # does the key exist?\nHGET customer:1 phone        # returns nil if the field is absent\nGET missing_key                  # (nil)","note":"Redis에는 SQL 의미의 NULL이 없으며, 키나 Hash 필드가 없는 것이 이에 해당하고 이때 명령은 nil을 반환한다. 기본값 로직(COALESCE 같은)은 애플리케이션 측에서 nil 검사로 적용한다. 필드 존재는 HEXISTS로 확인한다.","danger":"safe"},"elasticsearch":{"command":"# Default assignment when a field is missing (missing bucket / null_value)\n# Find documents without the field in a query:\nGET customer/_search\n{ \"query\": { \"bool\": { \"must_not\": { \"exists\": { \"field\": \"phone\" } } } } }\n# Map empty values to a specific value with null_value in the mapping","note":"Elasticsearch에서 null이거나 없는 필드는 'exists' 쿼리로 확인한다(must_not exists = 필드가 없는 문서). COALESCE와 같은 기본값이 필요하면 mapping에 'null_value'를 정의하거나, aggregation에서 'missing' 파라미터로 누락된 항목에 값을 지정한다. 기본값은 runtime field/스크립트로도 생성할 수 있다.","danger":"safe"}}}]},"rosetta/overlay-elasticsearch.json":{"engineId":"elasticsearch","byConcept":{"active-sessions":{"command":"GET _tasks?detailed=true&actions=*search*\n# To see node pressure: GET _nodes/hot_threads","note":"Task API는 현재 실행 중인 작업(search, reindex, bulk)을 반환한다. 순간 CPU 부하는 hot_threads가 가장 활발한 스레드를 보여 준다.","danger":"safe"},"blocking-sessions":{"command":"# Not applicable: Elasticsearch has no concept of row/document locks or blocking.\n# Closest diagnosis: find long-running tasks\nGET _tasks?detailed=true","note":"느려짐은 보통 블로킹보다는 장시간 실행 작업(대규모 검색, reindex)에서 비롯된다. running_time_in_nanos 필드로 소요 시간을 판단한다.","danger":"safe"},"kill-session":{"command":"POST _tasks/<task_id>/_cancel\n# task_id format: nodeId:taskNumber (e.g. oTUltX4IQMOUUVeiohTt8A:12345)","note":"task_id 값은 GET _tasks 출력에서 가져온다. 취소 가능한(cancellable:true) 작업만 중지할 수 있다.","danger":"danger"},"connection-count":{"command":"GET _nodes/stats/http","note":"current_open은 현재 열린 HTTP 연결 수를, total_opened는 노드 시작 이후 전체 연결 수를 보여 준다. HTTP 연결 상한은 http.max_content_length가 아니라 운영 체제/소켓 한도에 따라 결정된다.","danger":"safe"},"database-size":{"command":"GET _cluster/stats\n# For a simple summary: GET _cat/indices?v&h=index,store.size","note":"_cluster/stats는 indices.store.size_in_bytes로 클러스터 전체의 총 디스크 사용량을 보여 준다. _cat/indices는 인덱스별 크기를 읽기 쉬운 형태로 나열한다.","danger":"safe"},"largest-tables":{"command":"GET _cat/indices?v&s=store.size:desc&h=index,docs.count,store.size","note":"Elasticsearch에서 \"테이블\"에 해당하는 것은 인덱스다. s=store.size:desc는 가장 큰 인덱스를 맨 위로 정렬한다.","danger":"safe"},"free-space":{"command":"GET _cat/allocation?v","note":"disk.used, disk.avail, disk.percent 컬럼은 노드별 디스크 사용량을 보여 준다. watermark 임계값(85%/90%/95%)을 넘으면 샤드 할당이 중단된다.","danger":"safe"},"row-count":{"command":"GET <index>/_count\n# Shortcut: GET _cat/count/<index>?v","note":"_count는 문서 수를 빠르게 반환한다. 필터링된 수는 본문에 쿼리를 전달한다: GET <index>/_count {\"query\":{\"term\":{\"status\":\"active\"}}}","danger":"safe"},"top-slow-queries":{"command":"PUT <index>/_settings\n{\n  \"index.search.slowlog.threshold.query.warn\": \"1s\",\n  \"index.search.slowlog.threshold.query.info\": \"500ms\",\n  \"index.search.slowlog.threshold.fetch.warn\": \"1s\"\n}\n# Queries exceeding the threshold are written to the <cluster>_index_search_slowlog.json log file","note":"Elasticsearch에는 중앙 슬로 쿼리 테이블이 없으며, search slowlog가 임계값을 넘은 쿼리를 노드 로그 파일에 기록한다. 로그는 그곳에서 확인한다.","danger":"caution"},"explain-plan":{"command":"GET <index>/_search\n{\n  \"profile\": true,\n  \"query\": { \"match\": { \"title\": \"test\" } }\n}\n# To check query validity/parsing:\n# GET <index>/_validate/query?explain=true","note":"profile:true는 각 쿼리 구성 요소와 aggregation 단계의 소요 시간을 분석한다. _validate/query?explain은 쿼리의 Lucene 대응 형태와 거부된 이유를 보여 준다.","danger":"safe"},"unused-indexes":{"command":"# Not applicable: secondary indexes in ES differ from relational databases.\n# Closest: assess rarely used/unaccessed indices by size and search count\nGET _cat/indices?v&h=index,docs.count,store.size\nGET <index>/_stats/search","note":"사용되지 않는 필드 인덱스 대신, _stats/search로 검색 횟수(search.query_total)가 적거나 더 이상 조회되지 않는 인덱스를 찾아 닫거나 삭제한다.","danger":"safe"},"cache-hit-ratio":{"command":"GET _nodes/stats/indices/query_cache\n# Ratio = hit_count / (hit_count + miss_count)","note":"query_cache는 filter context의 결과를 캐시한다. request_cache는 GET _nodes/stats/indices/request_cache로 별도 확인한다.","danger":"safe"},"logical-dump":{"command":"# First define a repository (once): PUT _snapshot/<repo> {\"type\":\"fs\",\"settings\":{\"location\":\"/mnt/backups\"}}\nPUT _snapshot/<repo>/<snapshot_1>?wait_for_completion=true\n{\n  \"indices\": \"*\",\n  \"include_global_state\": true\n}","note":"Elasticsearch에서 논리 덤프에 해당하는 것은 스냅샷 API다. wait_for_completion=true는 작업이 끝날 때까지 기다린다. 대규모 스냅샷은 false로 두고 상태를 별도로 조회한다.","danger":"safe"},"restore-dump":{"command":"POST _snapshot/<repo>/<snapshot_1>/_restore\n{\n  \"indices\": \"*\",\n  \"rename_pattern\": \"(.+)\",\n  \"rename_replacement\": \"restored_$1\"\n}","note":"기존 인덱스를 복원하려면 먼저 닫거나(POST <index>/_close) rename_pattern으로 새 이름으로 복원한다. 열린 인덱스 위에 복원할 수는 없다.","danger":"danger"},"last-backup-status":{"command":"GET _snapshot/<repo>/_all\n# For in-progress/latest snapshot details:\n# GET _snapshot/<repo>/<snapshot_1>/_status","note":"state 필드는 SUCCESS / IN_PROGRESS / PARTIAL / FAILED 값을 가진다. _status는 샤드별 상세 진행률을 보여 준다.","danger":"safe"},"list-users":{"command":"GET _security/user","note":"X-Pack Security가 활성화되어 있어야 한다. 단일 사용자: GET _security/user/<user>. 기본 제공 사용자도 목록에 나타난다.","danger":"safe"},"create-user-grant":{"command":"# 1) Create a role\nPOST _security/role/<role>\n{\n  \"cluster\": [\"monitor\"],\n  \"indices\": [ { \"names\": [\"log-*\"], \"privileges\": [\"read\"] } ]\n}\n# 2) Create the user and assign the role\nPOST _security/user/<user>\n{\n  \"password\": \"StrongPassword123\",\n  \"roles\": [\"<role>\"]\n}","note":"권한 부여는 롤 기반이다. 먼저 인덱스/클러스터 권한을 담은 롤을 정의한 뒤 사용자에게 할당한다. 비밀번호는 최소 6자여야 한다.","danger":"caution"},"show-privileges":{"command":"GET _security/user/<user>\nGET _security/role/<role>\n# Check whether a specific privilege is held:\n# POST _security/user/<user>/_has_privileges { \"index\": [ { \"names\": [\"log-*\"], \"privileges\": [\"read\"] } ] }","note":"사용자의 롤은 _security/user로, 롤에 포함된 권한은 _security/role로 확인한다. _has_privileges는 특정 작업이 허용되는지 검증한다.","danger":"safe"},"change-password":{"command":"POST _security/user/<user>/_password\n{\n  \"password\": \"NewStrongPassword123\"\n}","note":"자신의 비밀번호를 바꾸려면 POST _security/user/_password(사용자 이름 없이)로 충분하다. 기본 제공 elastic 사용자에는 bin/elasticsearch-reset-password 도구도 사용할 수 있다.","danger":"caution"},"update-statistics":{"command":"# Not applicable: Elasticsearch has no cost-based optimizer or table statistics gathering.\n# Closest: make newly written documents searchable\nPOST <index>/_refresh","note":"Lucene은 세그먼트 기반이므로 별도의 통계 갱신 단계가 필요 없다. _refresh는 아직 검색할 수 없는 문서를 즉시 검색 가능하게 만든다(기본적으로 1초마다 자동 수행).","danger":"safe"},"rebuild-index":{"command":"POST _reindex\n{\n  \"source\": { \"index\": \"old_index\" },\n  \"dest\":   { \"index\": \"new_index\" }\n}\n# For large data: run in the background with POST _reindex?wait_for_completion=false","note":"Reindex는 mapping 변경이나 손상된 인덱스 재구축에 사용한다. 끊김 없이 전환하려면 alias가 새 인덱스를 가리키도록 한다.","danger":"caution"},"reclaim-space":{"command":"POST <index>/_forcemerge?only_expunge_deletes=true\n# Full merge on read-only/old indices: POST <index>/_forcemerge?max_num_segments=1","note":"삭제/갱신된 문서는 세그먼트에 tombstone으로 남으며, forcemerge가 이 공간을 회수한다. 활발히 쓰기가 일어나는 인덱스에서는 비용이 크므로 사용량이 적은 시간에 실행한다.","danger":"caution"},"version-uptime":{"command":"GET /\n# Uptime (JVM uptime):\n# GET _nodes/stats/jvm\n# Short summary: GET _cat/nodes?v&h=name,uptime,version","note":"GET / 루트 엔드포인트는 버전, Lucene 버전, 클러스터 이름을 반환한다. jvm.uptime_in_millis는 노드 가동 시간이다.","danger":"safe"},"show-config-param":{"command":"GET _cluster/settings?include_defaults=true&flat_settings=true","note":"include_defaults=true는 기본값도 보여 준다. persistent와 transient 설정은 별도 섹션으로 반환된다. 노드 수준 설정은 GET _nodes/settings를 사용한다.","danger":"safe"},"list-tables":{"command":"GET _cat/indices?v","note":"Elasticsearch에서 관계형 테이블에 해당하는 것은 인덱스다. health, status, docs.count, store.size 컬럼으로 요약을 제공한다. 기본 출력은 열린 인덱스와 닫힌 인덱스를 나열하지만 숨겨진 인덱스(예: .ds- backing index, 시스템 인덱스)는 나열하지 않는다. 포함하려면 &expand_wildcards=all을, 열린 인덱스만 보려면 &expand_wildcards=open을 추가한다.","danger":"safe"},"describe-table":{"command":"GET <index>/_mapping","note":"mapping은 필드 타입과 분석 설정을 보여 주며 DESCRIBE TABLE에 해당한다. 단일 필드: GET <index>/_mapping/field/<field>.","danger":"safe"},"list-indexes":{"command":"GET <index>/_mapping","note":"ES에서 필드는 기본적으로 인덱싱되며 별도의 보조 인덱스 테이블이 없다. mapping에서 \"index\": false인 필드는 검색할 수 없다. 클러스터 전체의 인덱스(테이블): GET _cat/indices?v.","danger":"safe"},"create-index":{"command":"PUT <index>\n{\n  \"settings\": { \"number_of_shards\": 1, \"number_of_replicas\": 1 },\n  \"mappings\": {\n    \"properties\": {\n      \"title\": { \"type\": \"text\" },\n      \"created_at\": { \"type\": \"date\" }\n    }\n  }\n}\n# To add fields later: PUT <index>/_mapping {\"properties\":{...}}","note":"인덱스를 생성한 후에는 primary 샤드 수를 그 자리에서 바꿀 수 없다(replica 수는 동적). 필요하면 _split / _shrink API나 _reindex로 새 인덱스를 만든다. 기존 필드의 타입은 바꿀 수 없으며 새 필드는 추가할 수 있다.","danger":"caution"},"show-ddl":{"command":"GET <index>","note":"단일 호출로 settings, mappings, aliases를 반환하며, 이 세 가지가 함께 인덱스의 \"DDL\"에 해당한다. 같은 출력을 새 PUT <index>의 본문으로 재사용할 수 있다.","danger":"safe"},"replication-status":{"command":"GET _cluster/health\n# For ongoing shard copy/recovery: GET _cat/recovery?v&active_only=true\n# For cross-cluster replication (CCR): GET <index>/_ccr/stats","note":"status는 샤드 상태를 green/yellow/red로 요약한다. yellow = replica 미할당. active_shards_percent는 완료 비율을 보여 준다.","danger":"safe"},"list-replicas":{"command":"GET _cat/shards?v&h=index,shard,prirep,state,node\n# For node roles: GET _cat/nodes?v","note":"prirep 컬럼은 p(primary) 또는 r(replica) 값을 가지며, 샤드가 노드에 어떻게 분산되어 있는지와 상태(STARTED/RELOCATING)를 보여 준다.","danger":"safe"},"promote-failover":{"command":"# Master election is automatic; no manual promote is needed.\n# To convert a CCR follower into an independent (writable) index:\nPOST <index>/_ccr/pause_follow\nPOST <index>/_close\nPOST <index>/_ccr/unfollow\nPOST <index>/_open","note":"노드 장애 시 Elasticsearch는 master를 자동으로 다시 선출하고 primary 샤드를 재할당한다. 수동 전환은 CCR follower 인덱스를 leader에서 분리해 쓰기 가능하게 만들 때만 필요하다.","danger":"danger"},"open-transactions":{"command":"# Not applicable: Elasticsearch has no multi-document transactions (BEGIN/COMMIT).\n# Operations are atomic at the document level; the closest diagnosis is running tasks:\nGET _tasks?detailed=true","note":"단일 문서에 대한 쓰기는 원자적이며 버전이 관리된다(낙관적 동시성: if_seq_no / if_primary_term). 열린 트랜잭션 추적 개념은 없다.","danger":"safe"},"connect-cli":{"command":"curl -u user:password -k https://host:9200\n# Kibana Dev Tools console (recommended): Kibana > Management > Dev Tools\n# With an API key: curl -H \"Authorization: ApiKey <base64key>\" https://host:9200","note":"REST 기반이므로 별도의 CLI 클라이언트 대신 curl이나 Kibana Dev Tools를 사용한다. -k는 자체 서명 TLS 인증서 검증을 건너뛴다(운영 환경에서는 CA 사용).","danger":"safe"},"list-databases":{"command":"GET _cat/indices?v\n# For aliases (logical views): GET _aliases","note":"Elasticsearch에는 별도의 \"데이터베이스\" 계층이 없다. 가장 가까운 개념은 인덱스와 이를 묶는 alias다. data stream: GET _data_stream.","danger":"safe"},"switch-database":{"command":"# Not applicable: the target index is specified directly in the request path; there is no session context to change.\nGET <index>/_search\n# In Kibana, a data view (index pattern) is selected as the default view","note":"USE <db> 같은 명령은 없으며, 모든 요청의 경로에 대상 인덱스/alias를 지정한다. 여러 인덱스를 검색하려면 쉼표로 구분한다: GET log-2024,log-2025/_search.","danger":"safe"}}},"errors/oracle.json":{"engineId":"oracle","engine":"Oracle","order":1,"errors":[{"code":"ORA-00001","title":"Unique constraint violated","severity":"caution","meaning":"삽입 또는 갱신되는 행이 UNIQUE 또는 PRIMARY KEY 제약 조건을 위반한다. 즉, 동일한 값이 이미 존재한다.","cause":"동일한 키 값으로 행을 다시 삽입하거나, 고유해야 하는 컬럼에 중복 데이터를 입력하거나, 복제/재실행 중 같은 레코드를 두 번 처리한 경우.","fix":"오류 메시지의 제약 조건 이름으로 위반된 컬럼을 찾는다: SELECT column_name FROM all_cons_columns WHERE constraint_name = 'CONSTRAINT_NAME'. 중복 데이터는 GROUP BY ... HAVING COUNT(*) > 1로 찾는다. 애플리케이션에서는 기존 레코드에 대해 INSERT 대신 UPDATE 또는 MERGE를 사용한다. 시퀀스 값이 뒤처졌다면 ALTER SEQUENCE로 앞당긴다.","tags":["unique","uniqueness","constraint","unique constraint","primary key","duplicate","duplicate key","insert"]},{"code":"ORA-00054","title":"Resource busy and acquire with NOWAIT specified or timeout expired","severity":"caution","meaning":"락을 걸려는 리소스(행 또는 테이블)를 다른 세션이 잠그고 있으며, NOWAIT가 지정되었거나 DDL_LOCK_TIMEOUT이 만료되어 즉시 획득하지 못했다.","cause":"보통 다른 트랜잭션이 테이블을 잠그고 있는 동안 DDL(ALTER TABLE, DROP, CREATE INDEX)을 시도한 경우, 또는 SELECT ... FOR UPDATE NOWAIT로 잠긴 행을 만난 경우.","fix":"락을 보유한 세션을 찾는다: SELECT * FROM v$locked_object. v$session과 v$lock을 조인해 블로킹 세션을 식별한다. 트랜잭션이 끝나기를 기다리거나, 필요하면 ALTER SYSTEM KILL SESSION으로 종료한다. DDL에는 대기 시간을 설정할 수 있다: ALTER SESSION SET DDL_LOCK_TIMEOUT = 60; DDL은 한가한 유지보수 시간대에 실행한다.","tags":["lock","locked","ddl","nowait","busy","resource busy","timeout"]},{"code":"ORA-00060","title":"Deadlock detected while waiting for resource","severity":"danger","meaning":"둘 이상의 세션이 서로가 보유한 리소스를 기다리며 데드락 상태에 빠졌다. Oracle은 순환을 감지해 희생자를 선택하고 해당 세션의 문장을 롤백한다.","cause":"서로 다른 순서로 획득한 행 락, 서로 다른 트랜잭션이 같은 행을 교차로 갱신, 인덱스 없는 외래 키로 인해 부모 테이블 갱신 시 걸리는 테이블 락, 또는 bitmap 인덱스에 대한 동시 DML.","fix":"alert log에 기록된 데드락 트레이스(graph) 파일을 검토해 충돌하는 SQL 문과 행을 찾는다. 애플리케이션에서는 항상 같은 순서로 테이블/행에 접근한다. 외래 키 컬럼에 인덱스를 생성한다(인덱스 없는 FK는 대표적인 데드락 원인이다). 트랜잭션을 짧게 유지하고 커밋 빈도를 검토한다. 데드락이 발생한 트랜잭션은 애플리케이션에서 잡아 재시도한다.","tags":["deadlock","deadlock detected","lock","locking","tx","transaction"]},{"code":"ORA-01017","title":"Invalid username/password; logon denied","severity":"caution","meaning":"데이터베이스 접속 시 입력한 사용자 이름 또는 비밀번호가 올바르지 않아 로그온이 거부되었다.","cause":"잘못된 비밀번호, 잘못된 사용자 이름, 대소문자 구분 비밀번호 설정(SEC_CASE_SENSITIVE_LOGON)이 예상과 다름, 변경된 비밀번호, 또는 애플리케이션 연결 문자열에 남은 오래된 자격 증명.","fix":"사용자 이름과 비밀번호를 확인한다. 필요하면 DBA로 비밀번호를 재설정한다: ALTER USER app_user IDENTIFIED BY new_password. 사용자가 존재하고 잠기지 않았는지 확인한다: SELECT username, account_status FROM dba_users. 12c+ 비밀번호 해시 버전에서는 11g 클라이언트가 접속에 문제를 겪을 수 있으므로 SQLNET.ALLOWED_LOGON_VERSION_SERVER 설정을 검토한다.","tags":["auth","credentials","password","password","logon","login","privilege"]},{"code":"ORA-01555","title":"Snapshot too old: rollback segment number too small","severity":"danger","meaning":"장시간 실행되는 쿼리가 시작 시점 기준의 읽기 일관성 이미지를 구성하는 데 필요한 과거 undo 데이터에 접근하지 못했다. 해당 undo 정보가 덮어써졌기 때문이다.","cause":"작은 undo 테이블스페이스, 짧은 UNDO_RETENTION, 매우 많은 동시 DML 중의 장시간 쿼리, 또는 쿼리 안에서 같은 테이블을 커밋하는 루프(fetch across commit).","fix":"UNDO_RETENTION을 늘리고 undo 테이블스페이스를 확장한다. 가능하면 자동 관리(AUTOEXTEND on)에 맡긴다. 긴 쿼리는 작게 나누거나 비피크 시간에 실행한다. RETENTION GUARANTEE를 검토한다: ALTER TABLESPACE undotbs1 RETENTION GUARANTEE. 루프 안에서 커밋하는 애플리케이션 로직을 수정한다.","tags":["undo","snapshot","rollback","read consistency","consistency","long query"]},{"code":"ORA-01652","title":"Unable to extend temp segment by N in tablespace","severity":"danger","meaning":"Oracle이 임시(temp) 세그먼트를 확장할 충분한 여유 공간을 테이블스페이스에서 찾지 못했다. 보통 정렬, hash join 또는 global temporary table 작업 중 발생한다.","cause":"TEMP 테이블스페이스가 가득 참. 대규모 ORDER BY, GROUP BY, DISTINCT, hash join 또는 인덱스 생성 작업이 PGA 메모리에 들어가지 않아 디스크로 넘침. 데이터 파일의 AUTOEXTEND가 꺼져 있거나 디스크 공간이 부족함.","fix":"TEMP 사용량을 모니터링한다: SELECT * FROM v$sort_usage / v$tempseg_usage. TEMP 테이블스페이스에 파일을 추가하거나 크기를 늘린다: ALTER TABLESPACE temp ADD TEMPFILE '...' SIZE 2G AUTOEXTEND ON. 영구 테이블스페이스가 가득 찼다면 데이터 파일을 추가한다. 쿼리 최적화(적절한 인덱스, 더 큰 PGA_AGGREGATE_TARGET)로 디스크 정렬을 줄인다.","tags":["temp","tablespace","free space","space","sort","sorting","extend"]},{"code":"ORA-04031","title":"Unable to allocate N bytes of shared memory","severity":"danger","meaning":"Oracle이 공유 메모리 풀(보통 shared pool 또는 large pool)에서 요청한 크기의 연속 메모리를 할당하지 못했다. SGA 구성 요소가 메모리 압박을 받고 있다.","cause":"바인드 변수 없이(리터럴로 가득 찬) 고유한 SQL 문이 많아 생긴 shared pool 단편화, 부족한 SHARED_POOL_SIZE, 과도한 동시 접속, 대형 PL/SQL 패키지, 또는 잘못된 ASMM/AMM 크기 설정.","fix":"오류 메시지가 가리키는 풀(shared pool, large pool 등)을 확인한다. 단기 조치: ALTER SYSTEM FLUSH SHARED_POOL(주의: 운영 환경에서는 모든 SQL을 다시 파싱하게 되어 일시적인 hard parse 폭주가 발생하며, 단편화를 영구적으로 해결하지 않는다). 근본 해결: 바인드 변수 사용을 강제하고(CURSOR_SHARING 포함), SGA_TARGET / SHARED_POOL_SIZE / LARGE_POOL_SIZE를 늘린다. 자주 사용하는 패키지는 DBMS_SHARED_POOL.KEEP으로 메모리에 고정한다. 메모리 분포는 V$SGASTAT으로 확인한다.","tags":["memory","shared memory","shared pool","sga","ora-4031","bind"]},{"code":"ORA-12154","title":"TNS: could not resolve the connect identifier specified","severity":"caution","meaning":"연결에 사용한 서비스 이름(connect identifier / TNS alias)을 해석하지 못했다. Oracle Net이 주어진 이름을 서버/서비스 정보로 매핑할 수 없다.","cause":"tnsnames.ora에 alias가 없음, TNS_ADMIN 환경 변수가 잘못된 디렉터리를 가리킴, alias 이름 오타, EZConnect/LDAP 네이밍 구성 누락, 또는 sqlnet.ora의 NAMES.DIRECTORY_PATH 순서가 잘못됨.","fix":"사용하는 alias가 tnsnames.ora에 정의되어 있는지 확인한다. 연결을 테스트한다: tnsping service_name. TNS_ADMIN이 올바른 폴더를 가리키는지 확인한다. 대안으로 EZConnect를 시도한다: app_user/password@host:1521/service_name. sqlnet.ora의 NAMES.DIRECTORY_PATH = (TNSNAMES, EZCONNECT) 줄을 확인한다.","tags":["tns","connect","connection","network","networking","tnsnames","service"]},{"code":"ORA-12514","title":"TNS: listener does not currently know of service requested in connect descriptor","severity":"caution","meaning":"리스너는 실행 중이지만 요청한 서비스 이름을 인식하지 못한다. 즉, 서비스가 리스너에 등록되어 있지 않다.","cause":"데이터베이스 인스턴스가 완전히 OPEN되지 않음, 서비스가 아직 리스너에 등록되지 않음(12c+에서는 LREG, 11g 이하에서는 PMON 등록 지연), 잘못된 SERVICE_NAME 사용, 또는 SID와 서비스 이름 혼동.","fix":"리스너가 알고 있는 서비스를 확인한다: lsnrctl status 및 lsnrctl services. 데이터베이스가 OPEN 상태인지 확인한다. 서비스 등록을 트리거한다: SQL에서 ALTER SYSTEM REGISTER. LOCAL_LISTENER 파라미터와 올바른 SERVICE_NAME 사용을 확인한다. pluggable database에서는 PDB가 OPEN 상태인지 확인한다.","tags":["tns","listener","service","service_name","connection","network","networking"]},{"code":"ORA-00942","title":"Table or view does not exist","severity":"caution","meaning":"쿼리에서 참조한 테이블 또는 뷰가 존재하지 않거나 접근하는 사용자에게 보이지 않는다.","cause":"객체가 실제로 존재하지 않음, 다른 스키마에 있는데 스키마 접두어를 지정하지 않음, 사용자에게 객체 권한(SELECT 등)이 없음, synonym 누락, 또는 객체 이름 오타.","fix":"객체의 존재 여부와 소유자를 확인한다: SELECT owner, object_type FROM all_objects WHERE object_name = 'TABLE_NAME'. 다른 스키마에 있다면 schema.table로 완전 한정하거나 GRANT SELECT ON app_schema.my_table TO app_user로 권한을 부여한다. 필요하면 public/private synonym을 생성한다: CREATE SYNONYM my_table FOR app_schema.my_table. 대소문자와 따옴표 식별자에 주의한다.","tags":["table","missing table","view","missing view","grant","privilege","synonym","alias"]},{"code":"ORA-01031","title":"Insufficient privileges","severity":"caution","meaning":"사용자에게 시도하는 작업에 필요한 권한(시스템 권한 또는 객체 권한)이 없다.","cause":"필요한 시스템 또는 객체 권한이 부여되지 않음, 권한이 롤을 통해서만 부여됨(definer-rights PL/SQL 블록 안에서는 롤이 적용되지 않음), SYSDBA/SYSOPER 없이 관리 작업을 시도함, 또는 definer-rights 프로시저에 대한 직접 grant 누락.","fix":"누락된 권한을 부여한다. 예: GRANT CREATE TABLE TO app_user 또는 GRANT SELECT ON app_schema.my_table TO app_user. 이름이 있는 PL/SQL 객체에서는 권한을 롤이 아니라 직접 부여해야 한다는 점을 기억한다. SYSDBA가 필요한 작업은 올바른 자격 증명으로 접속한다. 현재 권한을 확인한다: SELECT * FROM session_privs / user_sys_privs.","tags":["privilege","privileges","grant","permission","auth","role grant","role"]},{"code":"ORA-00257","title":"Archiver error. Connect internal only, until freed","severity":"danger","meaning":"아카이버(ARCH) 프로세스가 redo log를 아카이브하지 못한다. 보통 아카이브 대상(FRA 또는 아카이브 로그 대상)이 가득 찬 경우이다. 데이터베이스는 새 트랜잭션을 중단시킨다.","cause":"Flash Recovery Area(DB_RECOVERY_FILE_DEST) 또는 아카이브 로그 대상이 가득 참, 디스크 공간 부족, RMAN 백업이 아카이브 로그를 삭제하지 않음, 또는 DB_RECOVERY_FILE_DEST_SIZE 한도 도달.","fix":"공간을 확보한다: 오래된 아카이브 로그를 RMAN으로 FRA 밖의 대상에 백업한 뒤 삭제한다: RMAN> BACKUP ARCHIVELOG ALL FORMAT '/backup/arc_%U' DELETE INPUT(FRA가 가득 찬 상태에서 FRA로 백업하면 실패한다). Data Guard 환경에서는 아직 standby로 전송되거나 적용되지 않은 아카이브를 삭제하지 않는다. 그렇지 않으면 standby에 gap이 생긴다(CONFIGURE ARCHIVELOG DELETION POLICY TO APPLIED ON ALL STANDBY). FRA 사용량을 확인한다: SELECT * FROM v$recovery_file_dest / v$flash_recovery_area_usage. DB_RECOVERY_FILE_DEST_SIZE를 일시적으로 늘리거나 대상을 더 큰 디스크로 옮긴다. 디스크 수준의 여유 공간을 확인한다. 정기적인 아카이브 정리 정책을 수립한다.","tags":["archive","archivelog","archiver","fra","disk","space","redo"]},{"code":"ORA-01000","title":"Maximum open cursors exceeded","severity":"caution","meaning":"세션이 OPEN_CURSORS 파라미터가 허용하는 것보다 많은 커서를 열었다. 새 커서를 열 수 없다.","cause":"애플리케이션이 연 Statement/ResultSet 객체를 닫지 않음(커서 누수), 워크로드에 비해 OPEN_CURSORS가 너무 낮게 설정됨, 또는 루프에서 새 prepared statement를 계속 준비하고 닫지 않음.","fix":"열린 커서를 모니터링한다: SELECT * FROM v$open_cursor WHERE sid = ...  세션별 개수는 v$sesstat으로 확인한다. 근본 해결은 애플리케이션에서 모든 Statement/ResultSet을 finally 블록에서 닫는 것이다(커서 누수 제거). 임시 방편으로 OPEN_CURSORS를 늘린다: ALTER SYSTEM SET OPEN_CURSORS = 1000. JDBC statement cache 크기를 검토한다.","tags":["cursor","open cursor","open_cursors","leak","cursor leak","jdbc"]},{"code":"ORA-00018","title":"Maximum number of sessions exceeded","severity":"danger","meaning":"데이터베이스가 SESSIONS 파라미터가 허용하는 최대 세션 수에 도달해 새 세션을 열 수 없다.","cause":"워크로드 요구보다 낮게 설정된 SESSIONS/PROCESSES, 과도하게 큰 커넥션 풀 구성, 커넥션 누수, 또는 닫히지 않는 세션의 누적.","fix":"현재 세션 수를 확인한다: SELECT COUNT(*) FROM v$session. 한도를 확인한다: SELECT * FROM v$resource_limit WHERE resource_name IN ('sessions','processes'). PROCESSES를 늘려 간접적으로 SESSIONS도 늘린다(SPFILE 경유, 재시작 필요): ALTER SYSTEM SET PROCESSES = 500 SCOPE=SPFILE. 애플리케이션의 커넥션 풀 크기와 누수를 검토한다.","tags":["session","session limit","sessions","processes","connection","connections","limit"]},{"code":"ORA-28000","title":"The account is locked","severity":"caution","meaning":"접속하려는 데이터베이스 사용자 계정이 잠겨 있어 로그온에 실패한다.","cause":"프로파일의 FAILED_LOGIN_ATTEMPTS 한도 초과(반복된 잘못된 비밀번호), 또는 DBA가 계정을 수동으로 잠금(ALTER USER ... ACCOUNT LOCK). 로그인 실패로 인한 잠금은 PASSWORD_LOCK_TIME 후 자동으로 해제된다. 참고: PASSWORD_LIFE_TIME 만료는 계정을 잠그지 않는다. 계정은 EXPIRED / EXPIRED(GRACE) 상태가 되며 ORA-28001/ORA-28002를 반환한다.","fix":"계정 상태를 확인한다: SELECT username, account_status, lock_date FROM dba_users WHERE username = 'APP_USER'. 잠금을 해제한다: ALTER USER app_user ACCOUNT UNLOCK. 재발을 막으려면 로그인 실패의 원인을 찾고, 필요하면 프로파일의 FAILED_LOGIN_ATTEMPTS를 검토한다. 비밀번호가 만료되었다면 새 비밀번호를 지정한다.","tags":["account","user account","locked","account locked","auth","password","profile"]},{"code":"ORA-01578","title":"Oracle data block corrupted (file #, block #)","severity":"danger","meaning":"지정된 데이터 파일의 데이터 블록이 손상되었다. Oracle은 해당 블록이 일관성 검사(checksum)에 실패했다고 보고한다. 이는 물리적 손상이다.","cause":"디스크/하드웨어 장애, 스토리지 계층 오류, 운영체제 또는 컨트롤러 문제, 정전 후 일부만 기록된 블록, 또는 손상된 백업/복원.","fix":"손상 여부와 범위를 확인한다: RMAN> VALIDATE DATABASE / VALIDATE CHECK LOGICAL DATAFILE n; DBVERIFY(dbv)도 실행하고 v$database_block_corruption을 확인한다. 백업이 있으면 블록 수준으로 복구한다: RMAN> RECOVER DATAFILE n BLOCK m; 또는 목록의 모든 블록에 대해 RMAN> RECOVER CORRUPTION LIST;(이전 BLOCKRECOVER 명령은 11g부터 deprecated). 인덱스 블록이라면 인덱스를 재구축한다. 항상 하드웨어/디스크 상태를 점검한다. 근본 원인은 대개 스토리지이다.","tags":["corruption","block corruption","block","corrupt block","datafile","rman","disk"]},{"code":"ORA-01722","title":"Invalid number","severity":"safe","meaning":"숫자 컨텍스트가 필요한 곳에 숫자로 변환할 수 없는 문자열이 사용되었다. 암시적 또는 명시적 변환이 실패했다.","cause":"숫자 컬럼과 텍스트 비교(예: WHERE num_col = 'ABC'), 숫자 필드에 문자가 포함된 값 삽입, TO_NUMBER에 부적합한 포맷, 또는 소수점/그룹 구분자 불일치(쉼표 vs 마침표).","fix":"변환에 실패한 표현식을 찾는다. 대개 WHERE 조건 또는 INSERT 값이다. 텍스트/숫자 비교에서 타입 호환성을 보장하고 필요하면 명시적 변환을 사용한다: TO_NUMBER(my_column) 또는 텍스트 쪽에 TO_CHAR. 애플리케이션 계층에서 숫자 컬럼에 비숫자 입력이 들어가지 않도록 막는다. NLS_NUMERIC_CHARACTERS 설정에 주의한다.","tags":["number","numeric","conversion","type conversion","to_number","datatype","data type"]},{"code":"ORA-02049","title":"Timeout: distributed transaction waiting for lock","severity":"danger","meaning":"분산 트랜잭션이 원격 데이터베이스의 락을 기다리는 동안 DISTRIBUTED_LOCK_TIMEOUT을 초과했다. 트랜잭션이 타임아웃되었다.","cause":"database link를 통해 실행되는 트랜잭션이 원격 측의 잠긴 행을 기다림. 원격 세션의 긴 트랜잭션, 네트워크 지연, 또는 양쪽에서 보유한 충돌 락.","fix":"원격 데이터베이스에서 블로킹 락과 세션을 찾아 해결한다(v$lock, v$session). 분산 트랜잭션은 짧게 유지하고 필요한 락은 가능한 한 늦게 획득한다. 필요하면 워크로드에 맞게 DISTRIBUTED_LOCK_TIMEOUT을 늘린다(SPFILE, 재시작 필요). in-doubt 분산 트랜잭션은 DBA_2PC_PENDING 뷰를 확인하고 필요하면 수동으로 해결한다.","tags":["distributed","distributed transaction","dblink","lock","lock wait","timeout","2pc"]},{"code":"enq: TX - row lock contention","title":"대기: 다른 트랜잭션이 보유한 행 수준 락에 세션이 블로킹됨","severity":"caution","meaning":"세션이 다른 트랜잭션이 보유한 행 락의 해제를 기다리고 있다. 전형적인 애플리케이션 수준 락 대기 이벤트이다.","cause":"같은 행을 갱신하려는 동시 트랜잭션, 오랫동안 커밋되지 않은 트랜잭션, 인덱스 없는 외래 키로 인한 광범위한 락, 또는 순차 키에 대한 집중(hot block).","fix":"블로킹 세션과 블로킹된 세션을 찾는다: SELECT * FROM v$session WHERE blocking_session IS NOT NULL; v$lock과 대조한다. 긴 트랜잭션이 근본 원인이라면 커밋 전략을 검토한다. 외래 키 컬럼에 인덱스를 생성한다. hot row에 대해서는 설계(sequence, hash)를 재검토한다. 필요하면 블로킹 세션을 종료한다.","tags":["wait","wait event","enq","row lock","row lock wait","tx","contention","lock"]},{"code":"log file sync","title":"대기: 커밋 시 LGWR이 redo를 디스크에 플러시하기를 세션이 기다림","severity":"caution","meaning":"세션이 COMMIT하면 LGWR이 redo 레코드를 redo log 파일에 기록(flush)하기를 기다린다. 이 대기가 높으면 커밋 지연이 있다는 뜻이다.","cause":"느린 redo log 디스크(높은 I/O 지연), 너무 자주 커밋하는 애플리케이션(row-by-row commit), LGWR 프로세스의 CPU 압박, 또는 매우 많은 redo 양.","fix":"AWR/Statspack으로 평균 대기 시간을 측정한다. redo log 파일을 빠르고 지연이 낮은 디스크(SSD)로 옮긴다. 애플리케이션의 불필요한 커밋 빈도를 줄이고 배치 작업을 묶는다. 필요하면 업무 요구 사항에 비추어 COMMIT_LOGGING/COMMIT_WAIT(commit_write) 옵션을 검토한다. 경고: NOWAIT/BATCH는 내구성을 낮추며, 커밋된 것으로 보이는 트랜잭션이 크래시 시 유실될 수 있다. 유실을 감수할 수 있는 작업에 한해 세션 또는 문장 수준에서만 사용하고, 시스템 전체에 적용하지 않는다. LGWR과 스토리지 지연을 함께 분석한다.","tags":["wait","wait event","log file sync","commit","lgwr","redo","latency"]},{"code":"db file sequential read","title":"대기: 단일 블록 물리 읽기, 보통 인덱스 또는 테이블 조회","severity":"caution","meaning":"세션이 디스크에서 단일 블록이 읽히기를 기다린다. 보통 인덱스 접근이나 ROWID에 의한 단일 블록 읽기에서 나타나며, 일반적으로 예상되는 이벤트이다.","cause":"buffer cache에 없는 블록을 디스크에서 읽음, 선택도가 낮거나 잘못된 인덱스 사용, 부족한 buffer cache, 또는 느린 스토리지 계층.","fix":"값이 높다면 읽기를 가장 많이 하는 SQL 문을 찾는다(AWR Top SQL, v$sql). 적절한 인덱스와 올바른 접근 경로로 쿼리를 개선하고 불필요한 nested loop를 줄인다. buffer cache 크기(DB_CACHE_SIZE)를 검토한다. 스토리지 I/O 지연을 확인한다. 이 이벤트는 대개 정상이다. 비정상적으로 높을 때는 SQL 튜닝에 집중한다.","tags":["wait","wait event","db file sequential read","io","disk io","index","index access","single block"]},{"code":"db file scattered read","title":"대기: 다중 블록 물리 읽기, 보통 full table scan 또는 index fast full scan","severity":"caution","meaning":"세션이 디스크에서 여러 블록(multiblock)이 읽히기를 기다린다. 보통 full table scan 또는 index fast full scan 중 발생한다.","cause":"적절한 인덱스 부재로 인한 full table scan, 오래된 통계로 인한 잘못된 실행 계획, 매우 큰 테이블 스캔, 또는 선택된 접근 경로가 테이블 스캔으로 대체됨.","fix":"full scan을 수행하는 SQL 문을 식별하고 필요한 인덱스를 추가하거나 기존 인덱스가 사용되도록 한다. 통계를 갱신한다: DBMS_STATS.GATHER_TABLE_STATS. 대규모 스캔이 실제로 필요하다면 parallel query나 파티셔닝을 검토한다. DB_FILE_MULTIBLOCK_READ_COUNT와 스토리지 처리량을 검토한다.","tags":["wait","wait event","db file scattered read","full scan","full table scan","io","disk io","multiblock"]},{"code":"buffer busy waits","title":"대기: 다른 세션이 이미 읽거나 수정 중인 버퍼에 대한 경합","severity":"caution","meaning":"세션이 다른 세션이 동시에 읽거나 수정하고 있는 버퍼 블록에 접근하기를 기다린다. 메모리 내 블록 경합이다.","cause":"hot block에 대한 집중, 단조 증가 키를 가진 인덱스의 오른쪽 끝 경합, 부족한 freelist/비 ASSM 세그먼트, 또는 같은 블록에 대한 많은 동시 DML.","fix":"영향받는 세그먼트와 block class를 찾는다(v$session, v$segment_statistics, ASH). hot 인덱스 블록에는 reverse key 또는 hash 파티션 인덱스를 검토한다. ASSM(Automatic Segment Space Management)을 사용한다. 동시성이 높으면 INITRANS/PCTFREE를 늘린다. 애플리케이션 접근 패턴을 분산한다.","tags":["wait","wait event","buffer busy","hot block","hot blocks","contention","block contention"]},{"code":"gc buffer busy","title":"대기(RAC): 인스턴스 간 전송 중인 cache fusion 블록에 대한 경합","severity":"caution","meaning":"RAC 환경에서 세션이 다른 인스턴스와 cache fusion으로 주고받는 중이거나 해당 인스턴스가 사용 중으로 보유한 global 블록에 접근하기를 기다린다.","cause":"RAC 노드 간에 공유되는 동일한 hot block(cross-instance hot block), 인터커넥트 지연/병목, 순차 키 인덱스에 대한 노드 간 경합, 또는 불균형한 워크로드 분산.","fix":"v$segment_statistics와 GV$ 뷰로 영향받는 세그먼트를 찾는다. 인터커넥트 상태와 지연을 확인한다(전용 고속 네트워크). hot 인덱스에는 reverse key 또는 hash 파티셔닝을 사용한다. 서비스별로 워크로드를 노드에 분리해 같은 블록에 대한 노드 간 경합을 줄인다.","tags":["wait","wait event","rac","gc buffer busy","cache fusion","interconnect","global"]},{"code":"library cache lock","title":"대기: library cache 객체 핸들 경합(parse/DDL 직렬화)","severity":"caution","meaning":"세션이 library cache 내 객체(SQL, PL/SQL, 테이블 정의)의 락을 기다리고 있다. 보통 parse와 DDL 작업의 직렬화로 인해 발생한다.","cause":"많이 사용되는 객체에 대한 DDL(ALTER/CREATE), 자주 재컴파일되는 PL/SQL 객체(invalidation), 과도한 hard parse, 종속 객체의 invalidation, 또는 대량의 동시 접속 폭주.","fix":"블로킹 세션과 객체를 찾는다(v$session, ASH, 필요하면 library cache lock 진단). 많이 사용되는 객체에 대한 DDL은 피크 시간에 실행하지 않는다. 종속성 체인을 줄이고 불필요한 재컴파일을 방지한다. 바인드 변수를 사용해 hard parse를 줄인다. 접속 폭주는 커넥션 풀로 완화한다.","tags":["wait","wait event","library cache lock","parse","parsing","ddl","hard parse"]}]},"errors/postgres.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"errors":[{"code":"23505","title":"unique_violation","severity":"caution","meaning":"삽입 또는 갱신된 행이 UNIQUE 제약 조건 또는 PRIMARY KEY를 위반한다. 동일한 값이 이미 테이블에 존재한다.","cause":"같은 고유 키로 다시 삽입, 고유 컬럼에 중복 데이터 입력, 수동 삽입으로 시퀀스 값이 뒤처짐, 또는 두 개의 동시 요청이 같은 키를 삽입.","fix":"오류 상세의 제약 조건 이름과 키 값이 어떤 행이 충돌하는지 보여 준다. 기존 행을 갱신하려면 INSERT ... ON CONFLICT (my_column) DO UPDATE 또는 DO NOTHING을 사용한다. 중복을 찾는다: SELECT my_column, COUNT(*) FROM my_table GROUP BY my_column HAVING COUNT(*) > 1. 시퀀스가 뒤처졌다면: SELECT setval('seq_name', (SELECT MAX(id) FROM my_table)).","tags":["unique","unique","constraint","constraint","duplicate","duplicate","23505","primary key"]},{"code":"23503","title":"foreign_key_violation","severity":"caution","meaning":"FOREIGN KEY 제약 조건이 위반되었다. 삽입/갱신된 행이 참조하는 부모 행이 존재하지 않거나, 삭제하려는 부모 행에 아직 종속된 자식 행이 있다.","cause":"존재하지 않는 부모 키를 참조하는 INSERT/UPDATE, 아직 참조되고 있는 부모 행의 삭제/갱신 시도, 또는 잘못된 순서로 로드된 데이터(자식 먼저, 그다음 부모).","fix":"부모 행을 먼저 삽입한 뒤 자식 행을 삽입한다. 삭제 시 연쇄 동작이 필요하면 제약 조건을 ON DELETE CASCADE 또는 ON DELETE SET NULL로 정의한다. 대량 로드에서는 부모-자식 순서로 데이터를 로드한다. 참조하는 행을 찾는다: SELECT * FROM child WHERE fk_column = value.","tags":["foreign key","foreign key","fk","constraint","constraint","reference","23503"]},{"code":"23502","title":"not_null_violation","severity":"caution","meaning":"NOT NULL 제약 조건이 있는 컬럼에 NULL을 삽입하거나 해당 컬럼을 NULL로 설정하려고 했다.","cause":"INSERT에서 필수 컬럼 누락, 명시적인 NULL 할당, 컬럼에 DEFAULT 없음, 또는 애플리케이션에서 빈 값/누락된 값이 넘어옴.","fix":"컬럼에 유효한 값을 제공하거나 적절한 DEFAULT를 정의한다: ALTER TABLE my_table ALTER COLUMN my_column SET DEFAULT value. 컬럼이 실제로 필수가 아니라면 제약 조건을 제거한다: ALTER TABLE my_table ALTER COLUMN my_column DROP NOT NULL. 애플리케이션 계층에 누락 데이터 검증을 추가한다.","tags":["not null","null","constraint","constraint","required","23502","insert"]},{"code":"40P01","title":"deadlock_detected","severity":"danger","meaning":"둘 이상의 트랜잭션이 서로가 보유한 락을 기다리며 데드락에 빠졌다. PostgreSQL은 순환을 감지해 트랜잭션 중 하나를 취소한다.","cause":"서로 다른 순서로 획득한 락, 같은 행에 대한 교차 갱신, 긴 트랜잭션, 또는 외래 키 검사 중 걸리는 충돌 락.","fix":"서버 로그의 데드락 상세가 충돌하는 쿼리를 보여 준다. 모든 트랜잭션에서 같은 순서로 테이블/행에 접근한다. 트랜잭션을 짧게 유지한다. 취소된 트랜잭션은 애플리케이션에서 잡아 재시도한다. pg_locks와 pg_stat_activity를 조인해 락을 분석한다. 필요하면 SELECT ... FOR UPDATE로 락 순서를 명시한다.","tags":["deadlock","deadlock","lock","lock","40P01","tx","transaction"]},{"code":"40001","title":"serialization_failure","severity":"caution","meaning":"SERIALIZABLE 또는 REPEATABLE READ 격리 수준에서 트랜잭션이 동시 변경과 충돌해 직렬화할 수 없어 취소되었다. 데이터 일관성을 보호하는 예상된 동작이다.","cause":"같은 데이터에 동시에 접근하는 트랜잭션을 직렬화 가능한 순서로 배열할 수 없음, 높은 동시성에서의 쓰기 충돌, 또는 SSI(Serializable Snapshot Isolation)가 감지한 위험한 종속성.","fix":"버그가 아니라 예상된 신호이다: 트랜잭션을 그대로 재시도한다. 애플리케이션에 자동 재시도 로직을 추가한다. 충돌을 줄이려면 트랜잭션을 짧게 하고, 실제로 필요하지 않다면 더 낮은 격리 수준(READ COMMITTED)을 사용한다. hot row에 대한 경합을 줄이도록 설계한다.","tags":["serialization","serialization","isolation","isolation","retry","retry","40001","mvcc"]},{"code":"53300","title":"too_many_connections","severity":"danger","meaning":"서버가 max_connections가 허용하는 최대 동시 접속 수에 도달해 새 접속을 받을 수 없다.","cause":"커넥션 풀이 없거나 크기가 잘못됨, 커넥션 누수, 수명이 짧은 접속을 많이 여는 애플리케이션, 또는 워크로드보다 낮게 설정된 max_connections.","fix":"접속 출처를 식별한다: SELECT usename, state, COUNT(*) FROM pg_stat_activity GROUP BY 1,2. 오래 idle 상태인 클라이언트 접속은 대상을 한정해 종료한다(필터 없는 일괄 종료는 모든 애플리케이션과 풀에 영향을 준다): SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle' AND backend_type = 'client backend' AND usename = 'app_user' AND state_change < now() - interval '10 minutes' AND pid <> pg_backend_pid(). 근본 해결은 PgBouncer 같은 커넥션 풀이다. max_connections를 늘리면 메모리 비용이 들므로 풀링을 우선한다. superuser용으로 예약된 접속이 관리 접근을 보장한다.","tags":["connection","connection","max_connections","too many","53300","pool","pool","pgbouncer"]},{"code":"53100","title":"disk_full","severity":"danger","meaning":"디스크가 가득 차서 서버가 파일에 쓸 수 없었다. 데이터 디렉터리, WAL 또는 임시 파일이 있는 디스크에 남은 공간이 없다.","cause":"데이터 증가, 통제되지 않은 WAL 누적(아카이빙 또는 replication slot 지연), 대용량 임시 파일(거대한 정렬), 비대해진 로그 파일, 또는 테이블/인덱스 bloat.","fix":"디스크 사용량을 확인하고 불필요한 파일을 정리한다. WAL 누적을 확인한다: pg_stat_replication과 pg_replication_slots(사용하지 않는 slot은 삭제한다. 지연된 slot은 WAL을 보존한다). 디스크가 가득 찬 상태에서는 VACUUM FULL이나 REINDEX를 실행하지 않는다: 테이블을 다시 쓰기 때문에 테이블 크기만큼의 추가 공간이 필요해 상황을 악화시킬 수 있다. 일반 VACUUM도 운영체제에 공간을 (거의) 반환하지 않는다. pg_wal 디렉터리의 파일을 절대 수동으로 삭제하지 않는다. 먼저 WAL/slot, 로그, temp 원인을 해결해 공간을 확보하고 로그와 temp 파일의 배치를 검토한다. 영구 해결책으로 디스크 용량을 늘리고 모니터링/알림을 설정한다.","tags":["disk","space","space","full","full","wal","53100","vacuum"]},{"code":"57014","title":"query_canceled","severity":"safe","meaning":"쿼리가 취소되었다. 보통 statement_timeout 만료 또는 사용자/클라이언트의 취소(pg_cancel_backend, 드라이버 취소)로 인해 발생한다.","cause":"statement_timeout 초과, pg_cancel_backend에 의한 취소, 또는 애플리케이션/클라이언트가 보낸 취소 요청. 유사한 상황은 다른 코드를 가진다: lock_timeout 만료는 55P03, idle_in_transaction_session_timeout은 25P03(FATAL), hot standby의 복제 충돌로 인한 취소는 40001(경우에 따라 40P01)을 반환한다.","fix":"쿼리가 실제로 오래 걸린다면 튜닝한다(EXPLAIN ANALYZE, 인덱스, 실행 계획). 타임아웃이 너무 낮다면 해당 세션/롤의 statement_timeout을 조정한다. 의도적인 취소는 조치가 필요 없다. 락과 standby 충돌로 인한 취소는 55P03 및 40001 항목을 참고한다.","tags":["cancel","cancel","timeout","timeout","statement_timeout","57014","standby"]},{"code":"42P01","title":"undefined_table","severity":"caution","meaning":"쿼리에서 참조한 테이블(또는 뷰)이 존재하지 않거나 search_path에서 찾을 수 없다.","cause":"테이블이 실제로 존재하지 않음, search_path에 없는 다른 스키마에 있음, 테이블 이름의 오타/대소문자 오류, 따옴표 식별자와의 대소문자 불일치, 또는 객체가 아직 생성되지 않았거나 삭제됨.","fix":"테이블의 존재 여부와 스키마를 확인한다: SELECT schemaname, tablename FROM pg_tables WHERE tablename = 'my_table'. 다른 스키마에 있다면 schema.table로 한정하거나 search_path를 설정한다: SET search_path TO app_schema, public. 따옴표 없는 식별자는 소문자로 변환된다는 점을 기억한다. 생성 시 따옴표를 사용했다면 같은 방식으로 참조한다.","tags":["table","table","undefined","42P01","search_path","schema","schema"]},{"code":"42703","title":"undefined_column","severity":"caution","meaning":"쿼리에서 참조한 컬럼이 테이블 또는 현재 범위에 존재하지 않는다.","cause":"컬럼 이름 오타, 컬럼이 삭제/이름 변경됨, 잘못된 테이블 참조, 따옴표 식별자의 대소문자 불일치, 또는 다중 테이블 쿼리에서 모호하거나 잘못된 한정.","fix":"컬럼 목록을 확인한다: \\d my_table 또는 SELECT column_name FROM information_schema.columns WHERE table_name = 'my_table'. 이름과 테이블을 수정하고, JOIN에서는 컬럼을 table.column으로 한정한다. 생성 시 따옴표를 사용했다면 같은 대소문자로 참조한다.","tags":["column","column","undefined","42703","typo","typo","schema"]},{"code":"42501","title":"insufficient_privilege","severity":"caution","meaning":"롤에 시도하는 작업에 필요한 권한이 없다.","cause":"테이블/스키마/데이터베이스에 필요한 GRANT가 부여되지 않음, 스키마에 대한 USAGE 누락, 객체가 다른 롤 소유, 또는 행 수준 보안(RLS) 정책이 접근을 차단.","fix":"누락된 권한을 부여한다: GRANT SELECT, INSERT ON my_table TO app_role; 스키마에는 GRANT USAGE ON SCHEMA app_schema TO app_role. 일괄 부여에는 GRANT ... ON ALL TABLES IN SCHEMA를, 향후 객체에는 ALTER DEFAULT PRIVILEGES를 사용한다. 현재 권한을 확인한다: \\dp my_table. RLS가 활성화되어 있다면 관련 정책을 검토한다.","tags":["privilege","privilege","permission","grant","42501","permission","rls"]},{"code":"28P01","title":"invalid_password","severity":"caution","meaning":"인증 시 제공한 비밀번호가 틀려 해당 롤의 로그인이 거부되었다.","cause":"잘못된 비밀번호, 변경된 비밀번호, 애플리케이션 연결 문자열의 오래된 자격 증명, 또는 pg_hba.conf에서 요구하는 인증 방식(scram-sha-256, md5)과의 불일치.","fix":"비밀번호를 확인하고 필요하면 재설정한다: ALTER ROLE app_role WITH PASSWORD 'new_password'. pg_hba.conf의 방식이 클라이언트와 호환되는지 확인한다(오래된 클라이언트는 scram-sha-256에서 문제가 생길 수 있다). 변경 후 SELECT pg_reload_conf()로 설정을 다시 로드한다.","tags":["auth","identity","password","password","28P01","scram","md5","hba"]},{"code":"08006","title":"connection_failure","severity":"danger","meaning":"클라이언트와 서버 간 연결을 수립하지 못했거나 예기치 않게 끊어졌다.","cause":"서버가 실행 중이 아니거나 재시작 중, 네트워크/방화벽 문제, 잘못된 호스트/포트, 접근을 막는 listen_addresses 설정, 네트워크에 의해 끊긴 idle 연결, 또는 과부하 서버.","fix":"서버가 기동되어 올바른 호스트/포트에서 리스닝하는지 확인한다(pg_isready host port). postgresql.conf의 listen_addresses와 pg_hba.conf의 접근 규칙을 확인한다. 네트워크/방화벽과 DNS 설정을 확인한다. 커넥션 풀의 TCP keepalive와 타임아웃 설정을 검토한다. 서버 로그에서 크래시/재시작 흔적을 찾는다.","tags":["connection","connection","network","network","08006","failure","disconnect"]},{"code":"55P03","title":"lock_not_available","severity":"caution","meaning":"요청한 락을 즉시 획득하지 못했다. 보통 다른 세션이 보유한 락을 NOWAIT로 요청했거나 lock_timeout이 만료되었을 때 발생한다.","cause":"SELECT ... FOR UPDATE NOWAIT 또는 LOCK TABLE ... NOWAIT 사용 중 행/테이블이 잠겨 있음, 또는 lock_timeout 만료. 대부분의 경우 긴 트랜잭션이 락을 보유하고 있다.","fix":"블로킹 세션을 찾는다: pg_locks와 pg_stat_activity를 조인한다(대기/블로킹 관계). 긴 트랜잭션을 완료하거나 필요하면 pg_terminate_backend(pid)로 종료한다. NOWAIT 대신 적절한 lock_timeout으로 대기하고 재시도 로직을 추가한다. 트랜잭션을 짧게 해 락 보유 시간을 줄인다.","tags":["lock","lock","nowait","lock_timeout","55P03","not available","busy"]},{"code":"54000","title":"program_limit_exceeded","severity":"caution","meaning":"PostgreSQL의 구현/포맷 한도를 초과했다. 예: 행 또는 필드의 최대 크기, 인덱스 행 크기, 쿼리의 파라미터/컬럼 수 한도.","cause":"매우 큰 값 저장 시도(예: 1 GB를 넘는 필드), B-tree 페이지 한도(약 2704바이트)를 넘는 인덱스 값, 또는 너무 많은 컬럼이나 쿼리 파라미터 사용.","fix":"초과된 한도를 확인한다(보통 메시지에 나와 있다). 매우 큰 텍스트/데이터는 구조를 검토하거나 분할한다. 긴 텍스트 컬럼은 B-tree 대신 hash 인덱스나 표현식/부분 인덱스로 인덱싱한다. 쿼리의 파라미터/컬럼 수를 줄이거나 배치를 분할한다.","tags":["limit","limit","program_limit","54000","index size","size","row size"]},{"code":"22001","title":"string_data_right_truncation","severity":"caution","meaning":"텍스트 길이가 대상 컬럼의 정의 길이(예: VARCHAR(n))를 초과해 값이 잘릴 상황이므로 PostgreSQL이 이를 거부하고 오류를 발생시킨다.","cause":"VARCHAR(n)/CHAR(n) 컬럼에 n보다 긴 텍스트를 삽입 또는 갱신, 애플리케이션에서 예상보다 긴 데이터가 넘어옴, 또는 필요에 비해 너무 좁게 정의된 컬럼 길이.","fix":"값을 컬럼에 맞게 줄이거나 컬럼 길이를 늘린다: ALTER TABLE my_table ALTER COLUMN my_column TYPE VARCHAR(500). 길이 제한이 없는 텍스트가 필요하면 TEXT 타입을 사용한다. 애플리케이션에서 길이를 검증한다. 의도적으로 자르려면 LEFT(value, n)을 명시적으로 적용한다.","tags":["truncation","truncation","varchar","string","text","22001","length","length"]},{"code":"25P02","title":"in_failed_sql_transaction","severity":"caution","meaning":"트랜잭션 앞부분에서 이미 오류가 발생해 더 이상 명령을 실행할 수 없다. 트랜잭션이 aborted 상태이며 ROLLBACK만 허용된다.","cause":"트랜잭션 블록의 이전 문장이 실패했는데(예: 제약 조건 위반) 같은 트랜잭션에 새 명령을 계속 보냄. 애플리케이션이 오류를 잡지 않고 트랜잭션을 계속 진행함.","fix":"트랜잭션을 롤백한다: ROLLBACK 후 다시 시작한다. 블록의 오류를 격리하고 계속 진행하려면 SAVEPOINT를 사용하고 ROLLBACK TO SAVEPOINT로 해당 지점으로 돌아간다. 애플리케이션에서는 각 문장의 오류를 잡고 트랜잭션을 올바르게 관리한다(commit/rollback 결정을 명시적으로).","tags":["transaction","transaction","aborted","aborted","25P02","rollback","savepoint"]},{"code":"53200","title":"out_of_memory","severity":"danger","meaning":"서버가 작업에 필요한 메모리를 할당하지 못했다. 백엔드 프로세스 또는 서버 전체가 메모리 압박을 받고 있다.","cause":"너무 높게 설정된 work_mem/maintenance_work_mem이 높은 동시성으로 곱해짐, 한 쿼리 내 과도한 hash/sort 작업, 지나치게 많은 접속, 메모리 누수, 또는 운영체제 OOM killer의 압박.","fix":"서버 로그의 메모리 컨텍스트와 OOM killer 흔적을 확인한다. 쿼리 수와 동시성을 고려해 work_mem을 적절히 설정한다(각 sort/hash마다 별도로 할당된다). 풀링으로 접속 수를 제한한다. 운영체제의 메모리/overcommit 설정을 검토한다. 무거운 쿼리를 최적화해 메모리 요구량을 낮춘다.","tags":["memory","memory","out of memory","53200","work_mem","oom"]}]},"errors/mysql.json":{"engineId":"mysql","engine":"MySQL","order":3,"errors":[{"code":"1045","title":"Access denied for user (using password: YES/NO)","severity":"caution","meaning":"사용자 이름, 비밀번호 또는 접속하는 호스트에 대한 인증이 실패해 로그인이 거부되었다.","cause":"잘못된 비밀번호, 접속하는 호스트(user@host)에서의 권한 없음, 계정이 존재하지 않음, 인증 플러그인 불일치(caching_sha2_password vs mysql_native_password), 또는 연결 문자열의 오래된 자격 증명.","fix":"사용자와 호스트 매칭을 확인한다: SELECT user, host, plugin FROM mysql.user. 올바른 호스트에 권한을 부여한다(예: 'app_user'@'%'). 비밀번호를 재설정한다: ALTER USER 'app_user'@'host' IDENTIFIED BY 'new_password';(ALTER USER/GRANT는 즉시 적용되며, FLUSH PRIVILEGES는 grant 테이블을 DML로 직접 수정한 경우에만 필요하다). 'using password: NO'가 보이면 클라이언트가 비밀번호를 전혀 보내지 않는 것이다. 플러그인 불일치는 인증 플러그인을 검토한다: MySQL 8.4에서는 mysql_native_password 플러그인이 기본적으로 비활성화되어 있다(--mysql-native-password=ON일 때만 로드). 이 플러그인을 사용하는 오래된 계정과 오래된 클라이언트는 접속할 수 없다. ALTER USER ... IDENTIFIED WITH caching_sha2_password BY '...';로 계정을 마이그레이션하고 클라이언트 드라이버를 업그레이드한다.","tags":["auth","authentication","access denied","access","password","password","1045","privilege"]},{"code":"1062","title":"Duplicate entry for key","severity":"caution","meaning":"삽입 또는 갱신된 행이 UNIQUE 인덱스 또는 PRIMARY KEY를 위반한다. 동일한 키 값이 이미 존재한다.","cause":"같은 고유 키로 반복된 INSERT, 중복 데이터 입력, 뒤처진 AUTO_INCREMENT 카운터, 또는 기존 테이블에 고유 인덱스를 추가할 때 이미 존재하는 중복 행.","fix":"오류 메시지에 충돌한 키 값과 인덱스 이름이 표시된다. 기존 행을 갱신하려면 INSERT ... ON DUPLICATE KEY UPDATE 또는 INSERT IGNORE / REPLACE를 검토한다. 중복을 찾는다: SELECT my_column, COUNT(*) FROM my_table GROUP BY my_column HAVING COUNT(*) > 1. 고유 인덱스를 추가할 수 없다면 먼저 중복을 정리한다.","tags":["duplicate","duplicate entry","unique","unique key","1062","primary key","insert"]},{"code":"1213","title":"Deadlock found when trying to get lock; try restarting transaction","severity":"danger","meaning":"InnoDB가 둘 이상의 트랜잭션이 서로의 락을 기다리는 데드락을 감지하고, 트랜잭션 중 하나를 롤백해 순환을 끊었다.","cause":"서로 다른 순서로 획득한 행 락, 같은 행에 대한 교차 갱신, 인덱스 부재로 넓은 행 범위가 잠김(gap lock), 또는 외래 키 검사 중 충돌.","fix":"최근 데드락 상세를 확인한다: SHOW ENGINE INNODB STATUS(LATEST DETECTED DEADLOCK 섹션). 모든 트랜잭션에서 같은 순서로 행/테이블에 접근하고 트랜잭션을 짧게 유지한다. 적절한 인덱스를 추가해 잠기는 범위를 좁힌다. 롤백된 트랜잭션은 애플리케이션에서 잡아 재시도한다(retry 로직).","tags":["deadlock","deadlock detected","lock","row lock","innodb","1213","tx","transaction"]},{"code":"1205","title":"Lock wait timeout exceeded; try restarting transaction","severity":"danger","meaning":"트랜잭션이 필요한 행 락을 innodb_lock_wait_timeout초 동안 기다렸지만 획득하지 못하고 타임아웃되었다.","cause":"다른 트랜잭션이 commit이나 rollback 없이 락을 오래 보유함. idle 상태의 열린 트랜잭션, 장시간 실행되는 대량 갱신, 또는 경합이 심한 hot row.","fix":"블로킹 트랜잭션을 찾는다: performance_schema.data_lock_waits와 data_locks를 조인한다(이전 버전에서는 information_schema.innodb_lock_waits/innodb_trx). 블로킹 트랜잭션을 커밋/롤백하거나 필요하면 KILL <trx_mysql_thread_id>로 종료한다. 트랜잭션을 짧게 유지하고 idle 상태로 두지 않는다(autocommit/애플리케이션 로직). 필요하면 innodb_lock_wait_timeout을 워크로드에 맞게 조정한다.","tags":["lock","lock wait","timeout","timed out","innodb","1205","wait","waiting"]},{"code":"1040","title":"Too many connections","severity":"danger","meaning":"서버가 max_connections가 허용하는 최대 동시 접속 수에 도달해 새 접속을 거부하고 있다.","cause":"크기가 잘못된 커넥션 풀, 커넥션 누수, 수명이 짧은 많은 접속, 오래 대기 중인(Sleep) 접속, 또는 워크로드에 비해 너무 낮은 max_connections 값.","fix":"접속을 확인한다: SHOW PROCESSLIST 또는 SELECT * FROM performance_schema.processlist. 불필요한 sleep 접속은 KILL로 종료한다. max_connections를 늘린다: SET GLOBAL max_connections = 500(영구 적용은 my.cnf). 근본 해결은 커넥션 풀을 사용하고 wait_timeout으로 idle 접속을 닫는 것이다. SUPER/CONNECTION_ADMIN용으로 긴급 관리 접속 1개가 추가로 예약되어 있다.","tags":["connection","connections","too many","max_connections","1040","pool","connection pool"]},{"code":"1064","title":"You have an error in your SQL syntax","severity":"safe","meaning":"SQL 문에 구문 오류가 있어 서버가 파싱하지 못했다. 메시지에 오류 근처의 텍스트가 표시된다.","cause":"오타, 쉼표나 괄호의 누락/과잉, 예약어를 따옴표 없이 컬럼/테이블 이름으로 사용, MySQL 버전이 지원하지 않는 구문, 또는 따옴표/이스케이프 오류.","fix":"메시지의 'near ...' 부분이 오류가 시작되는 위치를 보여 주므로 바로 앞부분을 확인한다. 예약어는 백틱으로 감싼다: `order`, `group`. 괄호와 쉼표의 짝을 맞춘다. 사용 중인 버전이 지원하는 구문인지 확인한다. 동적 SQL을 생성한다면 최종 쿼리 전체를 로그로 남겨 확인한다.","tags":["syntax","syntax error","1064","parse","parsing","reserved word","reserved keyword"]},{"code":"1146","title":"Table doesn't exist","severity":"caution","meaning":"쿼리에서 참조한 테이블이 존재하지 않거나 현재 데이터베이스에서 찾을 수 없다.","cause":"테이블이 실제로 존재하지 않음, 잘못된 데이터베이스 선택, 테이블 이름 오타, Linux에서의 테이블 이름 대소문자 구분(lower_case_table_names), 또는 InnoDB 테이블 파일(.ibd) 누락/손상(5.7 이하에서는 .frm 정의 파일도 해당. 8.0에서는 정의가 data dictionary에 저장됨).","fix":"테이블을 확인한다: SHOW TABLES; 올바른 데이터베이스를 선택한다: USE mydb. 대소문자 구분에 주의한다(특히 Linux). SELECT ... FROM information_schema.tables로 테이블 이름과 스키마를 확인한다. InnoDB 파일 문제라면 복원/복구가 필요할 수 있다.","tags":["table","table missing","doesn't exist","not found","1146","schema","database"]},{"code":"1054","title":"Unknown column in field list","severity":"caution","meaning":"쿼리에서 참조한 컬럼이 테이블 또는 현재 범위에 존재하지 않는다.","cause":"컬럼 이름 오타, 컬럼이 삭제/이름 변경됨, 잘못된 테이블 참조, JOIN에서 모호하거나 잘못된 한정, 또는 따옴표 없이 쓴 문자열 값이 컬럼으로 해석됨(WHERE name = 'value' 대신 WHERE name = value).","fix":"컬럼 목록을 확인한다: SHOW COLUMNS FROM my_table 또는 DESCRIBE my_table. 컬럼 이름을 수정하고 JOIN에서는 my_table.my_column으로 한정한다. 문자열 리터럴은 작은따옴표로 감싼다. 백틱은 식별자 전용이다. information_schema.columns로 확인할 수 있다.","tags":["column","column name","unknown","unknown column","1054","typo","misspelling"]},{"code":"2002","title":"Can't connect to local MySQL server through socket","severity":"danger","meaning":"클라이언트가 Unix 소켓을 통해 로컬 MySQL 서버에 접속하지 못했다. 보통 서버가 실행 중이 아니거나 소켓 경로가 잘못된 경우이다.","cause":"MySQL/MariaDB 서비스가 실행 중이 아니거나 크래시됨, 소켓 파일 경로(예: /var/run/mysqld/mysqld.sock)가 클라이언트와 서버 간에 다름, 권한 문제, 또는 서버가 기동되지 않음.","fix":"서비스 상태를 확인하고 기동한다(systemctl status mysql / start mysql). 올바른 소켓 경로를 확인한다: SHOW VARIABLES LIKE 'socket' 및 my.cnf의 socket 설정. 경로가 다르면 --socket으로 명시하거나 TCP를 사용한다: mysql -h 127.0.0.1 -P 3306. 서버 로그에서 기동/크래시 오류를 확인한다.","tags":["connection","connect","socket","unix socket","2002","server","local","localhost"]},{"code":"2003","title":"Can't connect to MySQL server (TCP/IP)","severity":"danger","meaning":"클라이언트가 지정된 호스트와 포트에서 TCP/IP로 MySQL 서버에 접속하지 못했다.","cause":"서버가 실행 중이 아님, 잘못된 호스트/포트, 방화벽이 3306 포트를 차단, 서버가 localhost에만 바인딩됨(bind-address = 127.0.0.1), 또는 skip-networking 활성화.","fix":"서버가 기동되어 올바른 포트에서 리스닝하는지 확인한다. 원격 접속을 위해 my.cnf에서 bind-address = 0.0.0.0(또는 적절한 IP)으로 설정하고 skip-networking이 꺼져 있는지 확인한다. 방화벽에서 3306 포트를 연다. 호스트/포트를 확인한다: mysql -h host -P 3306 -u app_user -p. 사용자에게 원격 호스트에서의 권한이 있는지 확인한다.","tags":["connection","connect","tcp","port","2003","network","firewall","bind-address"]},{"code":"2006","title":"MySQL server has gone away","severity":"caution","meaning":"명령 실행 중 클라이언트와 서버 간 연결이 끊어졌다. 서버가 연결을 닫았거나 패킷을 거부했을 수 있다.","cause":"wait_timeout/interactive_timeout 만료로 닫힌 idle 연결, max_allowed_packet보다 큰 쿼리/데이터, 서버 재시작 또는 크래시, 또는 네트워크 단절.","fix":"idle 연결 문제는 커넥션 풀의 validation을 사용하고 wait_timeout을 검토한다. 큰 데이터를 보낸다면 max_allowed_packet을 늘린다(예: SET GLOBAL max_allowed_packet = 64*1024*1024, 영구 적용은 my.cnf). 서버 로그에서 재시작/크래시와 OOM 흔적을 확인한다. 끊어진 연결에 대비해 애플리케이션에 재접속 로직을 추가한다.","tags":["connection","connection lost","gone away","disconnect","2006","timeout","max_allowed_packet"]},{"code":"1114","title":"The table is full","severity":"danger","meaning":"저장 한도 또는 사용 가능한 공간의 끝에 도달해 테이블에 더 이상 데이터를 쓸 수 없다.","cause":"디스크 가득 참, MEMORY 테이블의 max_heap_table_size 한도 도달, 임시 테이블의 tmp_table_size 초과, innodb_data_file_path로 정의한 고정 크기 테이블스페이스가 가득 참, 또는 파일 시스템/테이블별 크기 한도.","fix":"먼저 디스크 공간을 확인하고 확보한다. MEMORY/임시 테이블에는 max_heap_table_size와 tmp_table_size를 늘린다. InnoDB에서는 innodb_data_file_path가 autoextend인지 확인하거나 공간을 추가한다. 큰 임시 결과를 디스크로 넘기는 쿼리를 최적화한다. 영구 해결책으로 스토리지 용량을 늘린다.","tags":["table full","table is full","disk","space","1114","space","memory","innodb"]},{"code":"1215","title":"Cannot add foreign key constraint","severity":"caution","meaning":"정의하려는 외래 키 제약 조건을 생성할 수 없다. MySQL이 제약 조건을 추가하지 못한다.","cause":"자식 컬럼과 참조 컬럼 간의 데이터 타입/길이/부호(signed-unsigned) 또는 character set/collation 불일치, 참조 컬럼에 적절한 인덱스/PRIMARY KEY 없음, 서로 다른 스토리지 엔진을 사용하는 테이블(한쪽이 MyISAM), 또는 참조 테이블이 아직 존재하지 않음.","fix":"양쪽 컬럼 타입이 정확히 같은지 확인한다(예: 둘 다 INT UNSIGNED). 참조 컬럼에 인덱스가 있는지 확인한다(보통 PRIMARY KEY 또는 UNIQUE). 두 테이블 모두 InnoDB인지 확인한다. 문자열 키에는 같은 charset/collation을 사용한다. 상세 정보: SHOW ENGINE INNODB STATUS(LATEST FOREIGN KEY ERROR).","tags":["foreign key","foreign key constraint","fk","1215","constraint","constraint error","innodb","type"]},{"code":"1451","title":"Cannot delete or update a parent row: a foreign key constraint fails","severity":"caution","meaning":"부모 행을 삭제하거나 키를 변경하려 했지만, 종속된 자식 행이 있어 외래 키 제약 조건이 허용하지 않는다.","cause":"참조되는 부모 행에 아직 자식 행이 있고, 제약 조건이 ON DELETE/UPDATE에 RESTRICT/NO ACTION을 사용함.","fix":"먼저 종속된 자식 행을 삭제하거나 다른 부모로 옮긴 뒤 부모 행을 삭제한다. 자동 동작을 원하면 제약 조건을 ON DELETE CASCADE 또는 ON DELETE SET NULL로 다시 정의한다. 종속 행을 찾는다: SELECT * FROM child_table WHERE fk_column = value.","tags":["foreign key","foreign key constraint","fk","1451","parent","parent row","delete","reference"]},{"code":"1452","title":"Cannot add or update a child row: a foreign key constraint fails","severity":"caution","meaning":"자식 행을 삽입 또는 갱신하고 있지만, 가리키는 부모 키 값이 참조 테이블에 존재하지 않아 외래 키 제약 조건이 이를 거부한다.","cause":"존재하지 않는 부모 키를 참조하는 INSERT/UPDATE, 잘못된 순서로 로드된 데이터(자식 먼저), 또는 삭제된 부모 행.","fix":"부모 행을 먼저 삽입한 뒤 자식 행을 삽입한다. 참조 값이 존재하는지 확인한다: SELECT * FROM parent_table WHERE key_column = value. 대량 로드에서 부모-자식 순서를 지킨다. SET FOREIGN_KEY_CHECKS=0은 통제된 유지보수 작업에서만 사용하고 완료 후 다시 1로 설정한다.","tags":["foreign key","foreign key constraint","fk","1452","child","child row","reference","insert"]},{"code":"1366","title":"Incorrect string/integer value for column","severity":"caution","meaning":"컬럼의 타입 또는 character set과 호환되지 않는 값이 컬럼에 삽입되었다. 보통 문자 인코딩 또는 타입 불일치로 발생한다.","cause":"latin1 컬럼에 4바이트 이모지/문자(utf8mb4) 기록, 숫자 컬럼에 비숫자 텍스트 입력, 잘못된 연결 charset, 또는 예상과 다른 타입의 값 전송.","fix":"문자 문제는 컬럼/테이블을 utf8mb4로 변환한다: ALTER TABLE my_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 그리고 연결 charset을 utf8mb4로 설정한다. 숫자 필드는 애플리케이션에서 타입을 검증한다. 서버가 strict SQL 모드(STRICT)로 동작한다는 점을 기억한다. 근본 원인은 데이터의 타입/인코딩이다.","tags":["charset","character","incorrect value","1366","utf8mb4","encoding","type","collation"]},{"code":"1044","title":"Access denied for user to database","severity":"caution","meaning":"사용자가 올바르게 인증되었지만 지정된 데이터베이스에서 작업할 권한이 없다.","cause":"사용자에게 데이터베이스에 대한 GRANT 없음, 잘못된 데이터베이스 이름, 다른 호스트(user@host)에 정의된 권한, 또는 존재하지 않는 데이터베이스.","fix":"사용자의 권한을 확인한다: SHOW GRANTS FOR 'app_user'@'host'. 필요한 권한을 부여한다: GRANT ALL PRIVILEGES ON mydb.* TO 'app_user'@'host';(GRANT는 즉시 적용되며 FLUSH PRIVILEGES는 필요 없다). 올바른 호스트 매칭과 데이터베이스 이름을 확인한다. 최소 권한 원칙에 따라 필요한 권한만 부여한다.","tags":["access denied","access","database","database access","1044","grant","privilege","permission"]},{"code":"1264","title":"Out of range value for column","severity":"caution","meaning":"숫자 또는 날짜 컬럼에 타입이 허용하는 범위를 벗어난 값이 삽입되었다. 이 오류는 strict SQL 모드에서 발생한다.","cause":"TINYINT/INT 같은 타입의 한도 초과(예: TINYINT에 200, UNSIGNED 컬럼에 음수 값), 날짜/숫자 오버플로, 또는 예상보다 큰 값 전송.","fix":"필요에 따라 컬럼 타입을 넓히거나(예: TINYINT 대신 INT, INT 대신 BIGINT) 부호(SIGNED/UNSIGNED)를 수정한다. 애플리케이션에서 값 범위를 검증한다. 조용히 잘라내는 동작을 원하더라도(권장하지 않음) sql_mode에서 STRICT를 제거하지 말고 데이터를 타입에 맞춘다.","tags":["out of range","value out of range","1264","value","int","tinyint","unsigned","overflow"]}]},"errors/mssql.json":{"engineId":"mssql","engine":"SQL Server","order":4,"errors":[{"code":"1205","title":"Transaction (Process ID) was deadlocked on lock resources with another process and has been chosen as the deadlock victim","severity":"danger","meaning":"둘 이상의 트랜잭션이 서로가 보유한 락을 기다리고 있다(데드락). 순환을 끊기 위해 SQL Server가 트랜잭션 하나를 희생자로 선택해 롤백한다.","cause":"서로 다른 순서로 테이블/행을 잠그는 트랜잭션, 장시간 실행되는 트랜잭션, 인덱스 부재로 인한 넓은 스캔, lock escalation, 높은 동시성이 대표적인 원인이다.","fix":"애플리케이션에서 1205 오류를 잡아 트랜잭션을 자동으로 재시도한다. 모든 트랜잭션에서 같은 순서로 객체에 접근하고 트랜잭션을 짧게 유지한다. 데드락 그래프를 분석한다: 상세 정보는 Extended Events(system_health 세션) 또는 trace flag 1222를 사용한다. 적절한 인덱스를 추가해 스캔과 락을 줄인다. 필요하면 READ COMMITTED SNAPSHOT(RCSI) 격리 수준을 검토한다.","tags":["deadlock","lock","locking","victim","rollback","1205","retry"]},{"code":"2627","title":"Violation of PRIMARY KEY / UNIQUE KEY constraint. Cannot insert duplicate key in object","severity":"caution","meaning":"PRIMARY KEY 또는 UNIQUE 제약 조건으로 보호되는 컬럼에 이미 존재하는 값을 삽입 또는 갱신하려 했다. 중복 키는 허용되지 않는다.","cause":"같은 고유 값을 두 번째로 삽입, 기존 값과 충돌하는 UPDATE, 고유성 검사 누락, 또는 두 개의 동시 요청이 같은 행을 생성.","fix":"삽입 전에 행의 존재 여부를 확인하거나 MERGE / IF NOT EXISTS를 사용한다. 멱등한 삽입에는 \"INSERT ... WHERE NOT EXISTS\" 패턴을 적용한다. 오류를 잡아 의미 있는 메시지를 반환한다. 중복 데이터가 실제로 필요하다면 제약 조건을 검토한다. 키 생성 시 충돌을 피하려면 IDENTITY 또는 SEQUENCE를 사용한다.","tags":["unique","primary key","uniqueness","duplicate","constraint","constraint violation","2627"]},{"code":"2601","title":"Cannot insert duplicate key row in object with unique index. The duplicate key value is ...","severity":"caution","meaning":"UNIQUE INDEX가 중복 키 값 때문에 행을 거부했다. 제약 조건에는 2627, 고유 인덱스에는 2601이 발생한다.","cause":"UNIQUE INDEX로 보호되는 컬럼에 기존 값을 다시 기록, 동시 삽입, 또는 filtered unique index가 포함하는 행 간 충돌.","fix":"중복 값을 제거하거나 갱신한다. 중복을 허용해야 한다면 IGNORE_DUP_KEY = ON 옵션을 검토한다(중복은 조용히 건너뛴다). 삽입 전에 NOT EXISTS 검사를 추가한다. filtered index가 있다면 WHERE 절과 충돌 여부를 확인한다.","tags":["unique index","duplicate key","unique idx","duplicate","2601","index"]},{"code":"547","title":"The INSERT/UPDATE/DELETE statement conflicted with the FOREIGN KEY / CHECK constraint","severity":"caution","meaning":"문장이 FOREIGN KEY 또는 CHECK 제약 조건을 위반했다. 존재하지 않는 부모 행을 참조했거나, CHECK 규칙을 위반하는 값을 기록했거나, 아직 참조되고 있는 부모 행을 삭제하려 했다.","cause":"FK: 삽입된 자식 행이 참조하는 부모 행이 존재하지 않음. DELETE/UPDATE: 삭제/변경하려는 부모 행에 종속된 자식 행이 있음. CHECK: 컬럼 값이 정의된 조건을 충족하지 않음.","fix":"부모 행을 먼저 삽입한 뒤 자식 행을 삽입한다. 삭제 시에는 종속 행을 먼저 제거하거나 적절한 경우 ON DELETE CASCADE를 정의한다. CHECK 위반은 값을 규칙에 맞춘다. 오류 텍스트의 제약 조건 이름으로 위반된 제약 조건을 식별한다.","tags":["foreign key","fk","check","constraint","constraint violation","referential","547"]},{"code":"18456","title":"Login failed for user","severity":"danger","meaning":"인증 중 로그인 시도가 거부되었다. 사용자 이름/비밀번호가 틀렸거나, 계정이 비활성화되었거나, 사용자에게 데이터베이스 접근 권한이 없을 수 있다.","cause":"잘못된 비밀번호, 존재하지 않는 로그인, 비활성화/잠긴 계정, Windows 인증만 활성화된 상태에서 SQL 로그인 사용, 또는 기본 데이터베이스에 접근 불가. 오류 텍스트의 State 코드가 실제 원인을 나타낸다(State 5: 로그인이 존재하지 않음, State 7: 로그인이 비활성화되고 비밀번호도 틀림, State 8: 잘못된 비밀번호(가장 흔함), State 18: 비밀번호 변경 필요, State 38: 지정한 데이터베이스를 열 수 없음, State 58: 서버가 Windows 전용 인증 모드인데 SQL 로그인을 시도함).","fix":"SQL Server 오류 로그의 State 값으로 근본 원인을 찾는다(보안상 상세 정보는 클라이언트에 전송되지 않는다). 사용자 이름/비밀번호를 확인한다. 서버가 'SQL Server and Windows Authentication Mode'(혼합 모드)로 동작하는지 확인하고, 변경했다면 서비스를 재시작한다. 대상 데이터베이스에서 로그인의 사용자 매핑과 접근 권한을 확인한다.","tags":["login","session","authentication","authentication failed","password","18456","state"]},{"code":"8152","title":"String or binary data would be truncated","severity":"caution","meaning":"컬럼의 정의 길이보다 긴 데이터를 쓰려고 했다. SQL Server는 데이터 손실을 막기 위해 문장을 중단한다(2628은 컬럼/값을 보고하는 같은 오류의 새 버전이다).","cause":"예를 들어 VARCHAR(10) 컬럼에 15자를 기록, 원본보다 좁은 대상 컬럼, 데이터 타입/길이 불일치, 또는 ETL/전송에서의 필드 크기 부족.","fix":"데이터에 맞게 컬럼을 넓히거나(ALTER TABLE ... ALTER COLUMN) 데이터를 줄인다. SQL Server 2019+에서는 넘친 테이블/컬럼/값을 보여 주는 2628 오류를 사용한다. 이전 버전에서는 LEN()으로 컬럼을 하나씩 확인해 문제 행을 찾는다. 전송 시 원본과 대상 스키마의 길이를 맞춘다.","tags":["truncation","truncate","overflow","varchar","length","column length","8152","2628"]},{"code":"2628","title":"String or binary data would be truncated in table, column ...; truncated value","severity":"caution","meaning":"8152의 개선 버전(SQL Server 2019 이상). 어느 테이블, 어느 컬럼, 어떤 값이 잘릴지를 명시적으로 보고한다.","cause":"대상 컬럼의 정의 길이보다 긴 텍스트/바이너리 값 기록. 대개 스키마 불일치 또는 예상치 못한 길이의 입력 데이터.","fix":"오류 텍스트의 테이블과 컬럼 이름을 확인하고, 컬럼을 넓히거나 원본 값을 줄인다. SQL Server 2019에서 2628은 호환성 수준 150+에서만 기본으로 발생한다. 더 낮은 수준에서는 ALTER DATABASE SCOPED CONFIGURATION SET VERBOSE_TRUNCATION_WARNINGS = ON 또는 trace flag 460으로 활성화한다. SQL Server 2016 SP2 CU6 및 2017 CU12부터는 trace flag 460으로 같은 상세 메시지를 얻을 수 있다. 전송 계층에서 필드 길이를 대상에 맞춘다.","tags":["truncation","truncate","overflow","column","column name","length","2628","460"]},{"code":"1222","title":"Lock request time out period exceeded","severity":"caution","meaning":"쿼리가 락 획득을 기다리는 동안 타임아웃이 만료되었다. 다른 트랜잭션이 필요한 리소스를 너무 오래 잠그고 있어 요청이 취소되었다.","cause":"SET LOCK_TIMEOUT으로 설정한 대기 시간 초과, 장시간 실행되는 블로킹 트랜잭션, 커밋되지 않은 열린 트랜잭션, 긴 스캔과 락 충돌.","fix":"블로커를 찾는다: sys.dm_exec_requests, sys.dm_tran_locks와 blocking_session_id 컬럼을 확인하거나 sp_who2 / sp_whoisactive를 사용한다. 긴 트랜잭션을 짧게 하고 커밋한다. 필요하면 LOCK_TIMEOUT을 조정한다. 읽기 위주 시스템에서는 RCSI/snapshot isolation으로 읽기-쓰기 충돌을 줄인다.","tags":["lock timeout","lock wait timeout","blocking","blocked session","1222","lock_timeout"]},{"code":"9002","title":"The transaction log for database is full due to ...","severity":"danger","meaning":"데이터베이스의 트랜잭션 로그 파일이 가득 차 새 트랜잭션을 로깅할 수 없다. 데이터베이스는 쓰기를 거부하며 읽기 전용처럼 동작할 수 있다.","cause":"FULL 복구 모델에서 로그 백업 없음, 오래 열린 트랜잭션, 복제/AlwaysOn 지연, 또는 로그가 증가할 수 없음(autogrow 비활성화 또는 디스크 가득 참). log_reuse_wait_desc가 원인을 보여 준다.","fix":"sys.databases의 log_reuse_wait_desc를 확인한다. FULL 모델에서 LOG_BACKUP이면 로그 백업(BACKUP LOG)을 수행한다. ACTIVE_TRANSACTION이면 열린 트랜잭션을 찾아 커밋/롤백한다(DBCC OPENTRAN). 디스크 공간을 확보하거나 로그 파일의 autogrow 설정을 수정한다. 로그가 실제로 커졌다면 백업 후 DBCC SHRINKFILE로 축소한다. 로그 체인이 필요 없다면 복구 모델을 검토한다.","tags":["transaction log","log is full","log full","9002","backup log","recovery model"]},{"code":"1105","title":"Could not allocate space for object in database because the filegroup is full","severity":"danger","meaning":"데이터 파일을 담는 filegroup이 가득 차 새 데이터에 공간을 할당할 수 없다. 삽입/갱신이 실패한다.","cause":"데이터 파일이 증가할 수 없음(autogrowth 비활성화 또는 MAXSIZE 도달), 디스크 여유 공간 없음, PRIMARY filegroup 소진, 또는 파일 크기 한도.","fix":"디스크 공간을 확보하거나 새 디스크/파일을 추가한다. 데이터 파일의 autogrowth를 활성화하거나 크기를 늘린다(ALTER DATABASE ... MODIFY FILE). filegroup에 새 파일을 추가한다. 불필요한/오래된 데이터를 아카이브하고 삭제한다. tempdb의 경우 tempdb 파일과 해당 디스크를 확인한다.","tags":["filegroup full","file full","allocate space","disk","autogrow","1105"]},{"code":"208","title":"Invalid object name","severity":"safe","meaning":"쿼리에서 참조한 테이블, 뷰 또는 객체를 찾을 수 없다. SQL Server가 지정한 이름을 해석하지 못했다.","cause":"객체 이름 오타, 잘못된 데이터베이스/스키마 컨텍스트(dbo가 아닌 스키마), 객체가 실제로 존재하지 않거나 아직 생성되지 않음, 또는 두 부분 이름(schema.table) 누락.","fix":"객체 이름과 스키마를 정확히 기재하고 한정된 이름을 사용한다(예: dbo.Customers). 올바른 데이터베이스에 접속했는지 확인한다(USE). 객체 존재 여부를 확인한다: SELECT * FROM sys.objects WHERE name = 'YourObjectName'. 임시 테이블은 같은 세션 안에서 생성되었는지 확인한다.","tags":["invalid object","invalid object name","table not found","table does not exist","schema","app schema","208"]},{"code":"229","title":"The SELECT/EXECUTE permission was denied on the object","severity":"caution","meaning":"사용자에게 객체에 대해 요청한 작업(SELECT, INSERT, EXECUTE 등)을 수행할 권한이 없다. 보안상 접근이 거부되었다.","cause":"필요한 권한이 사용자/롤에 부여되지 않음, 잘못된 스키마 소유권(ownership chaining 단절), 객체에 대한 명시적 DENY, 또는 사용자가 충분한 롤의 멤버가 아님.","fix":"필요한 권한을 부여한다: GRANT SELECT ON dbo.MyTable TO UserName(또는 GRANT EXECUTE ON dbo.MyProcedure TO ...). 사용자를 적절한 롤(예: db_datareader)에 추가한다. 명시적 DENY가 있으면 제거한다. 최소 권한 원칙에 따라 권한을 구성한다. sys.database_permissions로 기존 권한을 감사한다.","tags":["permission denied","access denied","grant","privilege","security","security permissions","229"]},{"code":"4060","title":"Cannot open database requested by the login. The login failed","severity":"danger","meaning":"인증은 성공할 수 있지만 요청한 데이터베이스를 열 수 없어 연결이 거부된다. 보통 18456과 함께 나타난다.","cause":"데이터베이스가 존재하지 않거나 OFFLINE/SINGLE_USER/RESTORING/SUSPECT 상태, 해당 데이터베이스에 로그인의 사용자 매핑 없음, 또는 연결 문자열의 기본 데이터베이스에 접근 불가.","fix":"데이터베이스가 존재하고 ONLINE인지 확인한다: SELECT name, state_desc FROM sys.databases. 데이터베이스에 로그인용 사용자를 생성하고 권한을 부여한다. 연결 문자열의 Initial Catalog/Database 값을 확인한다. 데이터베이스가 RESTORING/SUSPECT 상태라면 복구/복원 절차를 따른다.","tags":["cannot open database","database cannot be opened","4060","login","state","offline"]},{"code":"233","title":"A connection was successfully established with the server, but then an error occurred (no process is on the other end of the pipe)","severity":"danger","meaning":"클라이언트가 서버에 도달했지만 핸드셰이크 중 연결이 끊어졌다. 보통 Named Pipes/TCP 구성, 프로토콜 설정 또는 서버 측 거부로 인해 발생한다. 10054(연결이 강제로 종료됨)와 관련이 있다.","cause":"Shared Memory/Named Pipes/TCP 프로토콜 비활성화, 원격 연결 불허, 계정이 로그인할 수 없음(18456 참고), 방화벽/네트워크 단절, 또는 서버가 연결을 강제로 종료(10054).","fix":"SQL Server Configuration Manager에서 TCP/IP와 필요하면 Named Pipes를 활성화하고 서비스를 재시작한다. 원격 연결이 허용되는지 확인한다. 방화벽에서 SQL 포트(기본 1433)를 연다. 인증 문제는 오류 로그의 18456 State 코드를 확인한다. 네트워크 안정성과 TLS/암호화 설정을 확인한다.","tags":["connection","connection error","pipe","10054","tcp","named pipes","233","network"]},{"code":"823","title":"The operating system returned error to SQL Server during a read/write at offset in file (I/O error)","severity":"danger","meaning":"SQL Server가 데이터/로그 파일을 읽거나 쓰는 중 운영체제 수준의 I/O 오류를 받았다. 디스크 하드웨어 또는 스토리지 서브시스템 문제를 가리키는 심각한 무결성 경고이다.","cause":"불량 디스크 섹터, 결함 있는 컨트롤러/드라이버, 스토리지/SAN 문제, 파일 시스템 오류, 또는 전혀 읽을 수 없는 페이지(hard I/O error). 대개 하드웨어 관련.","fix":"가능하면 즉시 정상 백업을 수행한다. Windows 이벤트 로그와 스토리지 로그를 확인하고 하드웨어/디스크 상태를 테스트한다. DBCC CHECKDB로 무결성을 점검한다. suspect_pages 테이블(msdb.dbo.suspect_pages)을 확인한다. 손상이 있으면 정상 백업에서 복원하거나 페이지 수준 복원을 수행한다. 하드웨어를 교체/수리한다.","tags":["io error","i/o error","disk","corruption","data corruption","823","checkdb"]},{"code":"824","title":"SQL Server detected a logical consistency-based I/O error (page corruption)","severity":"danger","meaning":"SQL Server가 페이지를 읽었지만 내용이 논리적으로 일관되지 않다(잘못된 checksum, torn page 또는 잘못된 page ID). 데이터 페이지가 손상되었다.","cause":"스토리지 서브시스템 오류, 갑작스러운 정전/torn page, 컨트롤러 또는 드라이버 문제, PAGE_VERIFY CHECKSUM이 잡아낸 조용한 손상. 823과 마찬가지로 대부분 하드웨어/스토리지 관련.","fix":"DBCC CHECKDB를 실행해 손상 범위를 파악한다. 마지막 정상 백업에서 복원하거나 페이지 수준 복원으로 영향받은 페이지를 복구한다. msdb.dbo.suspect_pages를 확인한다. PAGE_VERIFY = CHECKSUM이 활성화되어 있는지 확인한다. 하드웨어/스토리지를 점검한다. REPAIR_ALLOW_DATA_LOSS는 최후의 수단이며 데이터 손실을 일으킨다.","tags":["page corruption","corrupt page","consistency","824","checkdb","checksum","torn page"]},{"code":"701","title":"There is insufficient system memory in resource pool to run this query","severity":"danger","meaning":"SQL Server가 해당 resource pool에서 쿼리 실행에 필요한 메모리를 할당하지 못했다. 메모리 압박으로 작업이 실패했다.","cause":"서버 메모리 부족, 낮은 max server memory 설정, 메모리를 많이 쓰는 쿼리(대규모 sort/hash), 메모리 누수, 많은 동시 요청, 또는 Resource Governor pool 한도.","fix":"max server memory 설정을 검토하고 충분한 물리 RAM을 확보한다. 메모리를 많이 쓰는 쿼리를 최적화한다(적절한 인덱스, sort/hash 감소). 메모리를 모니터링한다: sys.dm_os_memory_clerks, DBCC MEMORYSTATUS, Perfmon 메모리 카운터. Resource Governor pool 한도를 조정한다. 필요하면 동시성을 줄인다.","tags":["memory","memory pressure","insufficient memory","resource pool","701","oom"]},{"code":"845","title":"Time-out occurred while waiting for buffer latch","severity":"danger","meaning":"데이터 페이지의 buffer latch를 기다리는 중 타임아웃이 발생했다. SQL Server가 페이지 접근에 필요한 짧은 내부 락(latch)을 적절한 시간 안에 획득하지 못했다. 대개 과도한 I/O 또는 메모리 압박의 징후이다.","cause":"느리거나 과부하된 디스크 서브시스템, 메모리 압박으로 페이지를 계속 디스크에서 읽음, 하드웨어 문제, 과도한 paging, 또는 스토리지 병목.","fix":"디스크 I/O 지연을 측정한다(sys.dm_io_virtual_file_stats, Perfmon disk latency). 스토리지 성능과 하드웨어 상태를 확인한다. 메모리 충분성과 max server memory 설정을 검토한다. wait type을 확인한다(sys.dm_os_wait_stats, PAGEIOLATCH 대기). 스토리지 병목을 제거하고 필요하면 더 빠른 디스크/더 많은 RAM을 확보한다.","tags":["buffer latch","latch timeout","845","io","pageiolatch","memory","memory pressure"]},{"code":"3201","title":"Cannot open backup device. Operating system error (backup device error)","severity":"caution","meaning":"백업 또는 복원 중 대상 백업 디바이스/파일을 열 수 없었다. SQL Server가 지정한 경로에 접근하지 못했다.","cause":"잘못되었거나 존재하지 않는 폴더 경로, SQL Server 서비스 계정에 대상 폴더 쓰기 권한 없음, 네트워크 공유(UNC) 접근 불가, 디스크 가득 참, 또는 다른 프로세스가 파일을 잠금.","fix":"백업 경로가 존재하고 정확히 기재되었는지 확인한다. SQL Server 서비스 계정에 대상 폴더 쓰기 권한을 부여한다. UNC 공유는 서비스 계정의 접근 권한을 확인한다. 디스크 여유 공간이 충분한지 확인한다. 로컬 경로라면 드라이브 문자가 서버에서 보이는지 확인한다(클라이언트 드라이브가 아님).","tags":["backup device","backup file","backup","restore","3201","permission denied","permission","os error"]}]},"errors/mongodb.json":{"engineId":"mongodb","engine":"MongoDB","order":5,"errors":[{"code":"E11000","title":"E11000 duplicate key error collection","severity":"caution","meaning":"삽입 또는 갱신이 unique index로 보호되는 필드에 컬렉션에 이미 존재하는 값을 쓰려고 했다. MongoDB가 중복 키를 거부한다.","cause":"_id 충돌, unique index가 있는 필드에 같은 값을 두 번째로 기록, upsert 중 경쟁 조건, 또는 unique index(비 sparse 인덱스)에서 여러 문서의 null 값 충돌.","fix":"쓰기 전에 문서 존재 여부를 확인하거나 upsert를 사용한다: db.collection.updateOne(filter, {$set:{...}}, {upsert:true}). 오류 메시지의 인덱스 이름과 dup key 값을 확인한다. 고유성이 실제로 필요하지 않다면 인덱스를 삭제하고, null이 문제라면 partial/sparse 인덱스를 사용한다. 애플리케이션에서 오류를 잡아 재시도하거나 사용자에게 알린다.","tags":["duplicate key","duplicate key","unique index","unique","e11000","upsert","_id"]},{"code":"11000","title":"DuplicateKey (오류 코드 11000)","severity":"caution","meaning":"중복 키 오류의 숫자 코드(E11000 텍스트와 함께 반환된다). 쓰기 작업이 unique index 제약을 위반했다.","cause":"같은 고유 필드를 다시 기록, 동시 upsert가 같은 키를 생성, 또는 bulk write 내 중복 문서.","fix":"애플리케이션 코드에서 오류 코드 11000을 잡아 멱등 동작을 구현한다(존재하면 갱신, 없으면 삽입). bulk 작업에서는 ordered:false를 사용해 중복 외의 문서가 기록되도록 한다. 고유성 규칙과 인덱스 정의를 검토한다.","tags":["duplicatekey","11000","duplicate","unique","unique","bulk write","write error"]},{"code":"13","title":"Unauthorized (오류 코드 13)","severity":"caution","meaning":"인증된 사용자에게 시도한 작업 또는 리소스에 대한 권한이 없다. 사용자는 로그인했지만 롤이 이 작업을 포함하지 않는다.","cause":"필요한 롤(예: readWrite, dbAdmin)이 사용자에게 부여되지 않음, 잘못된 데이터베이스에서 작업 실행, 또는 관리 명령에 대한 권한 부족. 불완전한 RBAC 구성.","fix":"사용자의 롤을 확인한다: db.getUser(\"app_user\"). 필요한 롤을 부여한다: db.grantRolesToUser(\"app_user\", [{role:\"readWrite\", db:\"mydb\"}]). 작업이 올바른 데이터베이스에서 실행되는지 확인한다. 최소 권한 원칙에 따라 적절한 built-in 또는 사용자 정의 롤을 할당한다. 연결 문자열의 authSource 값을 확인한다.","tags":["unauthorized","unauthorized","authorization","role","role","rbac","13","permission"]},{"code":"18","title":"AuthenticationFailed (오류 코드 18)","severity":"caution","meaning":"인증에 실패했다. 제공한 사용자 이름, 비밀번호 또는 자격 증명이 유효하지 않아 사용자가 로그인하지 못했다.","cause":"잘못된 사용자 이름/비밀번호, 잘못된 authSource 데이터베이스, 인증 메커니즘 불일치(SCRAM-SHA-1/SCRAM-SHA-256), 또는 지정한 데이터베이스에 사용자가 정의되지 않음.","fix":"사용자 이름과 비밀번호를 확인한다. 연결 문자열에 올바른 authSource(사용자가 생성된 데이터베이스, 보통 admin)를 지정한다: ...?authSource=admin. 인증 메커니즘이 서버와 호환되는지 확인한다. 사용자가 올바른 데이터베이스에 생성되었는지 확인한다. 비밀번호에 특수 문자가 있으면 URL 인코딩한다.","tags":["authentication failed","authentication","password","password","authsource","18","scram"]},{"code":"11600","title":"InterruptedAtShutdown (오류 코드 11600)","severity":"danger","meaning":"서버가 종료를 시작해 작업이 중단되었다. mongod 프로세스가 정지 중이어서 진행 중인 작업을 완료하지 못했다.","cause":"mongod의 재시작/종료, replica set primary stepdown 중 종료, 계획된 유지보수, 크래시, 또는 OOM killer에 의한 프로세스 종료.","fix":"서버가 다시 기동되기를 기다린 뒤 작업을 재시도한다. 애플리케이션에서 일시적 오류를 잡아 자동으로 재시도하고 retryable writes를 활성화한다. 예기치 않은 종료는 mongod 로그와 시스템 로그(OOM, 크래시)를 검토한다. 고가용성을 위해 replica set을 사용한다.","tags":["interrupted","shutdown","shutdown","downtime","11600","retry","replica set"]},{"code":"50","title":"MaxTimeMSExpired (오류 코드 50)","severity":"caution","meaning":"작업이 maxTimeMS로 설정한 시간 한도를 초과해 서버가 취소했다. 쿼리가 할당된 시간 안에 완료되지 않았다.","cause":"장시간 실행 쿼리, 인덱스 부재로 인한 컬렉션 스캔(COLLSCAN), 대용량 데이터, 무거운 aggregation 작업 또는 높은 서버 부하.","fix":"쿼리를 최적화한다: explain()으로 실행 계획을 확인하고 적절한 인덱스를 추가한다. 시간이 실제로 더 필요하다면 maxTimeMS를 늘린다. 무거운 aggregation을 단순화하거나 $match로 일찍 필터링한다. 페이지네이션으로 결과를 분할한다. 서버 부하와 리소스 용량을 확인한다.","tags":["maxtimems","timeout","timeout","50","slow query","slow query","index"]},{"code":"251","title":"NoSuchTransaction (오류 코드 251)","severity":"caution","meaning":"참조한 다중 문서 트랜잭션을 찾을 수 없다. 트랜잭션이 이미 종료되었거나 타임아웃되었거나, 유효하지 않은 트랜잭션 번호가 사용되었다.","cause":"트랜잭션이 transactionLifetimeLimitSeconds를 초과해 자동으로 중단됨, 세션 종료, 잘못되거나 오래된 txnNumber, 또는 오류 후 이미 중단된 트랜잭션을 계속 진행.","fix":"트랜잭션을 처음부터(새 세션으로) 다시 시작한다. 트랜잭션을 짧게 유지하고 기본 수명(60 s) 안에 끝나도록 설계한다. TransientTransactionError 레이블이 붙은 오류는 트랜잭션 전체를 안전하게 재시도한다. 긴 트랜잭션이 필요하다면 transactionLifetimeLimitSeconds 설정을 검토한다.","tags":["nosuchtransaction","transaction","transaction","251","session","abort","retry"]},{"code":"112","title":"WriteConflict (오류 코드 112)","severity":"caution","meaning":"두 개의 동시 작업이 같은 문서를 수정하려 할 때 발생하는 쓰기 충돌이다. 낙관적 동시성 제어로 인해 WiredTiger가 작업 중 하나를 롤백한다.","cause":"같은 문서에 대한 높은 동시 쓰기, 장시간 트랜잭션 중 충돌하는 갱신, hot 문서에 대한 심한 경합.","fix":"작업을 재시도한다. WriteConflict는 대개 일시적이며 재시도로 해결된다(TransientTransactionError 레이블이 붙는다). 트랜잭션을 짧게 유지하고 다루는 문서 수를 줄인다. 같은 문서에 대한 경합을 줄이도록 데이터 모델을 검토한다. 애플리케이션에서 exponential backoff로 재시도를 구현한다.","tags":["writeconflict","write conflict","112","concurrency","concurrency","retry","wiredtiger"]},{"code":"133","title":"FailedToSatisfyReadPreference (오류 코드 133)","severity":"danger","meaning":"지정한 read preference(예: primary, secondary)를 충족하는 적절한 replica set 멤버를 찾지 못했다. 드라이버가 요청을 라우팅할 노드를 선택하지 못했다.","cause":"replica set 멤버에 연결할 수 없거나 다운됨, 선출 진행 중 primary 없음, 네트워크 파티션, tag set 조건에 맞는 멤버 없음, 또는 과반수(quorum)에 도달할 수 없음.","fix":"replica set 상태를 확인한다: rs.status(). 노드 상태와 네트워크 연결을 확인한다. primary 선출이 완료되기를 기다린 뒤 재시도한다. read preference 설정을 검토하고 필요하면 더 유연한 설정(예: primaryPreferred)을 사용한다. tag set을 사용한다면 일치하는 멤버가 있는지 확인한다.","tags":["read preference","readpreference","133","replica set","election","secondary","primary"]},{"code":"6","title":"HostUnreachable (오류 코드 6)","severity":"danger","meaning":"드라이버/클라이언트가 대상 MongoDB 서버(호스트)에 도달하지 못했다. 네트워크 수준에서 연결을 수립할 수 없었다.","cause":"서버 다운, 잘못된 호스트/포트, 방화벽 차단, DNS 해석 문제, 네트워크 단절, 또는 클라이언트를 제외하는 bindIp 구성.","fix":"호스트 이름과 포트(기본 27017)가 올바른지 확인한다. mongod 서비스가 실행 중인지 확인한다. 방화벽과 네트워크 접근을 테스트한다(ping/telnet). mongod.conf의 bindIp 설정이 클라이언트 접근을 허용하는지 확인한다. DNS/SRV 레코드를 확인한다(mongodb+srv 연결의 경우).","tags":["hostunreachable","host","connection","network","network","6","firewall","bindip"]},{"code":"89","title":"NetworkTimeout (오류 코드 89)","severity":"danger","meaning":"서버와 통신하는 중 네트워크 작업이 할당된 시간 안에 완료되지 않아 타임아웃되었다. 연결은 수립되었지만 응답이 제때 도착하지 않았다.","cause":"느리거나 불안정한 네트워크, 과부하 서버, 응답을 지연시키는 장시간 작업, 너무 낮게 설정된 socket/connect 타임아웃 값, 또는 네트워크 혼잡.","fix":"네트워크 지연과 안정성을 측정한다. 서버 부하와 리소스를 확인한다. 드라이버의 connectTimeoutMS와 socketTimeoutMS를 현실적인 값으로 설정한다. 일시적 오류를 잡아 재시도한다. 느린 쿼리를 최적화해 응답 시간을 줄인다.","tags":["networktimeout","network timeout","89","timeout","socket","network","retry"]},{"code":"262","title":"ExceededTimeLimit (오류 코드 262)","severity":"caution","meaning":"작업이 서버 측에서 허용된 시간 한도를 초과해 종료되었다. maxTimeMS 또는 내부 시간 한도 초과가 이 오류를 일으킬 수 있다.","cause":"장시간 실행 쿼리/명령, 인덱스 부재로 인한 느린 실행, 무거운 aggregation, 대용량 데이터 세트 스캔, 또는 높은 서버 부하.","fix":"explain()으로 쿼리를 확인하고 적절한 인덱스를 추가한다. 작업을 작은 단위로 분할한다. 실제 필요에 맞게 maxTimeMS를 설정한다. aggregation pipeline 초반에 $match/$project로 데이터를 줄인다. 서버 리소스가 충분한지 확인한다.","tags":["exceededtimelimit","time limit","262","timeout","maxtimems","slow","index"]},{"code":"292","title":"QueryExceededMemoryLimitNoDiskUseAllowed (오류 코드 292)","severity":"caution","meaning":"쿼리 또는 aggregation 스테이지가 허용된 메모리 한도(기본 100 MB)를 초과했고 디스크 쓰기가 허용되지 않아 실패했다. 보통 sort 또는 group 스테이지에서 나타난다.","cause":"인덱스 없는 대용량 데이터에 대한 메모리 내 정렬, 큰 $group/$sort aggregation 스테이지, 매우 큰 중간 결과 세트, allowDiskUse:false 설정.","fix":"allowDiskUse:true 옵션으로 aggregation에서 디스크 사용을 허용한다: db.collection.aggregate(pipeline, {allowDiskUse:true}). 정렬 필드에 인덱스를 생성해 메모리 사용을 줄인다. pipeline 초반에 $match로 필터링해 처리 데이터를 줄인다. 필요하면 결과를 페이지네이션한다. MongoDB 6.0+에서는 대부분의 경우 디스크 사용이 기본으로 활성화된다.","tags":["memory limit","memory limit","292","allowdiskuse","sort","aggregation","group"]},{"code":"26","title":"NamespaceNotFound (오류 코드 26)","severity":"safe","meaning":"참조한 네임스페이스(database.collection)를 찾을 수 없다. 지정한 컬렉션 또는 인덱스가 존재하지 않는다.","cause":"컬렉션 이름 오타, 아직 생성되지 않은 컬렉션, 잘못된 데이터베이스 컨텍스트, 또는 존재하지 않는 컬렉션에 대한 drop/인덱스 작업 시도.","fix":"컬렉션 이름과 데이터베이스를 확인한다: show collections. 이름은 대소문자를 구분한다는 점을 기억한다. MongoDB는 첫 쓰기 시 컬렉션을 자동으로 생성한다. 필요하면 db.createCollection()으로 명시적으로 생성한다. drop 같은 작업은 사전에 컬렉션 존재 여부를 확인한다.","tags":["namespacenotfound","namespace","collection missing","26","collection","not found","not found"]},{"code":"43","title":"CursorNotFound (오류 코드 43)","severity":"caution","meaning":"서버 측 커서가 더 이상 존재하지 않는다. 클라이언트가 다음 배치를 요청할 때(getMore) 커서를 찾을 수 없어 오류가 반환된다.","cause":"커서 타임아웃(기본 10분 비활성), 배치 간 너무 긴 대기, 서버 재시작, 또는 커서를 명시적으로 닫음.","fix":"커서 결과를 더 빨리 처리하고 배치 간에 오래 기다리지 않는다. 장시간 작업에는 noCursorTimeout 옵션을 신중하게 사용하고(mongosh: cursor.noCursorTimeout(); PyMongo: no_cursor_timeout=True) 항상 커서를 닫는다. 세션 내 커서는 이 옵션이 있어도 세션 타임아웃(localLogicalSessionTimeoutMinutes, 기본 30 min)에 닫힌다. 대용량 결과 세트는 더 작은 batchSize로 순회하거나 범위 기반 페이지네이션을 사용한다. 작업을 다시 시작해 다시 쿼리한다.","tags":["cursornotfound","cursor","cursor","43","getmore","timeout","batch","pagination"]},{"code":"211","title":"KeyNotFound (오류 코드 211)","severity":"caution","meaning":"예상한 키/값을 찾을 수 없다. 보통 내부 키 관리, 세션 키 또는 oplog/keys 컬렉션과 관련된 불일치를 나타낸다.","cause":"admin.system.keys 컬렉션의 서명 키 누락/삭제, replica set/sharded cluster의 시계 동기화 문제, keyfile/키 교체 문제, 또는 손상된 내부 상태.","fix":"replica set 멤버 간 시계 동기화(NTP)를 확인한다. admin.system.keys의 내용과 키 생성을 확인한다. 클러스터 상태를 검토한다(rs.status(), sh.status()). 지속되면 mongod 로그를 분석하고 keyfile/identity 구성을 검토한다. 키가 재생성될 수 있도록 primary가 정상 동작하는지 확인한다.","tags":["keynotfound","key","211","keys","session","ntp","cluster","cluster"]}]},"errors/redis.json":{"engineId":"redis","engine":"Redis","order":6,"errors":[{"code":"WRONGTYPE","title":"WRONGTYPE Operation against a key holding the wrong kind of value","severity":"caution","meaning":"키의 데이터 타입에 맞지 않는 명령이 해당 키에 실행되었다. 예를 들어 string을 담은 키에 list(LPUSH) 또는 hash(HSET) 명령을 적용한 경우.","cause":"키가 예상과 다른 타입임, 같은 키 이름을 다른 데이터 타입으로 사용, 애플리케이션 로직의 타입 혼동, 또는 오래되거나 남은 키를 덮어씀.","fix":"키 타입을 확인한다: TYPE mykey. 키의 실제 타입에 맞는 명령을 선택한다. 키 이름으로 타입을 구분한다(예: user:1:tags는 set, user:1:name은 string). 필요하면 기존 키를 삭제(DEL)하고 올바른 타입으로 다시 생성한다. 애플리케이션 계층에서 타입 일관성을 강제한다.","tags":["wrongtype","wrong type","type","data type","key","key"]},{"code":"OOM","title":"OOM command not allowed when used memory > 'maxmemory'","severity":"danger","meaning":"Redis의 메모리 사용량이 maxmemory 한도에 도달했고, eviction 정책이 새 데이터를 위한 공간을 확보할 수 없어 쓰기 명령이 거부된다. 메모리가 가득 차 쓰기가 불가능하다.","cause":"maxmemory 한도 초과, noeviction 정책 선택(키가 전혀 제거되지 않음), TTL 없는 데이터의 지속적인 증가, 메모리 누수, 또는 한도에 비해 부족한 RAM.","fix":"메모리 사용량을 확인한다: INFO memory 및 MEMORY USAGE. 적절한 eviction 정책(예: allkeys-lru 또는 volatile-lru)을 선택한다: CONFIG SET maxmemory-policy allkeys-lru. 키에 TTL을 설정해 자동으로 정리되게 한다. maxmemory/RAM을 늘린다. 큰 키를 찾아 줄인다(redis-cli --bigkeys). 불필요한 데이터를 삭제한다.","tags":["oom","out of memory","memory","maxmemory","eviction","memory","danger"]},{"code":"NOAUTH","title":"NOAUTH Authentication required","severity":"caution","meaning":"Redis가 인증을 요구하지만 클라이언트가 인증 전에 명령을 실행하려 했다. 먼저 AUTH로 로그인해야 한다.","cause":"서버에 requirepass/ACL 비밀번호가 설정됨, 클라이언트가 AUTH 명령을 보내지 않음, 연결 문자열에 비밀번호 누락, 또는 재연결 후 AUTH를 반복하지 않음.","fix":"명령 실행 전에 인증한다: AUTH password(ACL 사용 시 AUTH app_user password). 클라이언트/연결 문자열에 비밀번호를 추가한다. 커넥션 풀을 사용한다면 모든 새 연결에서 AUTH가 수행되는지 확인한다. 비밀번호는 안전하게 보관한다(환경 변수/secrets manager).","tags":["noauth","authentication","authentication","auth","password","requirepass","acl"]},{"code":"WRONGPASS","title":"WRONGPASS invalid username-password pair or user is disabled","severity":"caution","meaning":"AUTH 명령에 지정한 사용자 이름/비밀번호 쌍이 유효하지 않거나 ACL 사용자가 비활성화되어 있다. 인증이 거부되었다.","cause":"잘못된 비밀번호, 잘못된 ACL 사용자 이름, ACL에서 사용자 비활성화, requirepass 값 변경, 또는 비밀번호의 이스케이프/인코딩 문제.","fix":"올바른 사용자 이름과 비밀번호를 입력한다. ACL 사용자를 확인한다: ACL LIST 및 ACL GETUSER app_user. 사용자가 비활성화되었다면 활성화한다(ACL SETUSER app_user on ...). requirepass로 설정한 비밀번호를 확인한다(CONFIG GET requirepass). 비밀번호를 재설정할 때는 모든 클라이언트가 갱신되었는지 확인한다.","tags":["wrongpass","password","invalid password","auth","acl","user","invalid"]},{"code":"READONLY","title":"READONLY You can't write against a read only replica","severity":"caution","meaning":"쓰기 명령이 replica(slave) 노드로 전송되었다. replica는 기본적으로 읽기 전용이며 쓰기는 master에서만 수행할 수 있다.","cause":"클라이언트가 master 대신 replica에 접속, 페일오버 후 기존 master가 replica가 됨, Sentinel/Cluster에서 잘못된 노드로 라우팅, 또는 로드 밸런서가 쓰기를 replica로 전달.","fix":"쓰기 요청을 master 노드로 라우팅한다. Sentinel을 사용한다면 Sentinel에서 현재 master를 조회한다(SENTINEL get-master-addr-by-name). Cluster에서는 올바른 샤드 master에 쓴다. 클라이언트 라이브러리에서 읽기/쓰기 분리를 구성한다. 페일오버 후에는 연결을 갱신해 새 master를 가리키게 한다.","tags":["readonly","read only","replica","slave","write","write","master","failover"]},{"code":"MASTERDOWN","title":"MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'","severity":"danger","meaning":"replica와 master 간 링크가 끊어졌고, replica-serve-stale-data가 'no'로 설정되어 있어 replica가 오래된 데이터로 요청에 응답하기를 거부한다.","cause":"master 노드가 크래시되었거나 연결할 수 없음, 네트워크 단절/파티션, master-replica 복제 링크 끊김, 또는 높은 부하로 master가 응답하지 못함.","fix":"master 노드의 상태와 네트워크 연결을 확인한다. 복제 상태를 확인한다: INFO replication. master를 다시 기동하거나 Sentinel/Cluster를 통해 페일오버가 일어나게 한다. 오래된 데이터 제공이 허용된다면 replica-serve-stale-data yes를 검토한다(일관성 위험에 유의). 고가용성을 위해 Sentinel/Cluster를 사용한다.","tags":["masterdown","master down","replication","replication","stale","failover","sentinel","danger"]},{"code":"CLUSTERDOWN","title":"CLUSTERDOWN The cluster is down / Hash slot not served","severity":"danger","meaning":"Redis Cluster가 완전히 동작하지 않는다. 일부 hash slot을 담당하는 노드가 없어 클러스터가 요청을 거부한다.","cause":"도달 가능한 master 노드 부족, 하나 이상의 slot이 미할당/미커버, 과반수(quorum) 상실, 노드 크래시, 또는 cluster-require-full-coverage가 yes인 상태에서 일부 slot에 도달 불가.","fix":"클러스터 상태를 확인한다: CLUSTER INFO 및 CLUSTER NODES. 다운된 master를 다시 기동하거나 replica가 승격되게 한다. 미할당 slot을 재할당한다(redis-cli --cluster fix 또는 CLUSTER ADDSLOTS). 모든 slot이 커버되는지 확인한다. 부분 동작이 허용된다면 cluster-require-full-coverage no를 검토한다.","tags":["clusterdown","cluster","cluster","hash slot","slot","quorum","coverage","danger"]},{"code":"LOADING","title":"LOADING Redis is loading the dataset in memory","severity":"caution","meaning":"Redis가 기동 중이며 디스크의 데이터 세트(RDB/AOF)를 메모리로 로드하고 있다. 로드가 완료될 때까지 대부분의 명령이 일시적으로 거부된다.","cause":"서버가 막 기동됨, 큰 RDB/AOF 파일 로드 중, replica가 master로부터 full sync 수행 중, 또는 재시작 후 복구 과정.","fix":"로드가 끝나기를 기다린다. 소요 시간은 데이터 크기에 따라 다르다. 상태를 모니터링한다: INFO persistence(loading:1이면 대기, 0이면 준비 완료). 애플리케이션에서 이 일시적 오류를 잡아 짧은 간격으로 재시도한다. 로드 시간을 줄이려면 데이터 세트/영속성 전략(RDB vs AOF)을 검토한다. 충분한 RAM과 빠른 디스크를 확보한다.","tags":["loading","loading","startup","startup","rdb","aof","sync","persistence"]},{"code":"BUSY","title":"BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE","severity":"caution","meaning":"장시간 실행되는 Lua 스크립트 또는 함수가 busy-reply-threshold(Redis 7 이전 이름은 lua-time-limit, 기본 5000 ms)를 초과했다. Redis는 단일 스레드이므로 이 동안 다른 명령을 처리할 수 없으며, SCRIPT KILL(함수는 FUNCTION KILL) 또는 SHUTDOWN NOSAVE만 허용된다.","cause":"무한 루프 또는 매우 오래 실행되는 Lua 스크립트/함수, 무거운 연산, 대용량 데이터를 순회하는 스크립트, 또는 잘못 작성된 EVAL 호출.","fix":"스크립트가 데이터를 수정하지 않았다면 중지한다: SCRIPT KILL. 스크립트가 이미 쓰기를 수행했다면 SCRIPT KILL은 동작하지 않는다. 데이터 일관성을 지키기 위한 최후의 수단으로 SHUTDOWN NOSAVE가 필요할 수 있다(저장되지 않은 데이터는 유실되므로 주의한다). 스크립트는 짧고 결정적으로 작성한다. 긴 작업은 분할한다. busy-reply-threshold(이전 lua-time-limit) 설정을 검토한다.","tags":["busy","busy server","lua","script","script kill","eval","time limit"]},{"code":"MISCONF","title":"MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk","severity":"danger","meaning":"Redis가 RDB 스냅샷을 생성하도록 구성되어 있지만 디스크에 쓸 수 없다. 이 상태에서 stop-writes-on-bgsave-error가 활성화되어 있으면 Redis는 모든 쓰기 명령을 거부한다.","cause":"디스크 가득 참, 파일 시스템 권한 문제, 쓰기 불가 디렉터리, 백그라운드 저장(BGSAVE) 오류, 메모리 부족으로 인한 fork 실패, 또는 디스크 하드웨어 문제.","fix":"디스크 공간을 확보하고 RDB 디렉터리(dir)의 쓰기 권한을 확인한다. 최근 BGSAVE 오류를 확인한다: INFO persistence 및 Redis 로그. 문제가 해결될 때까지 일시적으로 쓰기를 허용하려면 stop-writes-on-bgsave-error no를 설정할 수 있다(내구성 위험에 유의). fork를 위한 충분한 메모리와 vm.overcommit_memory=1 설정을 확보한다. 영속성 구성(save/appendonly)을 확인한다.","tags":["misconf","persist","durability","rdb","bgsave","disk","snapshot","danger"]},{"code":"CROSSSLOT","title":"CROSSSLOT Keys in request don't hash to the same slot","severity":"caution","meaning":"Redis Cluster에서 다중 키 명령(예: MGET, MSET, SUNION)을 사용했지만 키가 서로 다른 hash slot에 속한다. 다중 키 작업은 모든 키가 같은 slot에 있어야 한다.","cause":"클러스터 모드에서 여러 키에 동시에 접근하는 명령을 서로 다른 slot에 분산된 키로 호출함. hash tag를 사용하지 않아 키가 서로 다른 노드에 분산됨.","fix":"관련 키를 같은 slot에 두려면 hash tag를 사용한다: 키 이름의 일부를 중괄호로 고정한다(예: {user1}:profile과 {user1}:settings는 같은 slot으로 간다). 다중 키 명령은 같은 hash tag를 공유하는 키에만 사용한다. 대안으로 작업을 단일 키 명령으로 분할한다.","tags":["crossslot","cross slot","cluster","cluster","hash tag","slot","mget","mset"]},{"code":"MOVED","title":"MOVED slot ip:port (클러스터 리다이렉트)","severity":"safe","meaning":"Redis Cluster에서 요청한 키를 담는 hash slot이 다른 노드에 속한다. 서버가 클라이언트를 올바른 노드로 영구 리다이렉트한다. 오류라기보다 리다이렉트이다.","cause":"cluster-aware가 아닌 클라이언트가 잘못된 노드에 요청을 보냄, 노드 간 slot 재분배(resharding), 또는 클라이언트가 캐시한 slot 맵이 오래됨.","fix":"cluster-aware 클라이언트 라이브러리를 사용한다. 이러한 라이브러리는 MOVED 응답을 자동으로 따라가 올바른 노드로 리다이렉트하고 slot 맵을 갱신한다. redis-cli로 접속할 때는 -c 플래그를 사용한다. 클라이언트가 slot 토폴로지를 주기적으로 갱신하는지 확인한다(CLUSTER SLOTS/SHARDS).","tags":["moved","cluster","cluster","redirect","redirect","slot","resharding"]},{"code":"ASK","title":"ASK slot ip:port (클러스터 리다이렉트)","severity":"safe","meaning":"Redis Cluster에서 slot이 다른 노드로 마이그레이션 중이며, 요청한 키가 일시적으로 대상 노드에 있을 수 있다. 서버는 이 요청에 한해 클라이언트를 대상 노드로 리다이렉트한다(임시 리다이렉트).","cause":"resharding 중 마이그레이션되는 slot에서 이미 이전된 키에 접근함. MOVED와 달리 영구적이지 않으며 해당 시점에만 적용된다.","fix":"cluster-aware 클라이언트는 ASK 리다이렉트를 자동으로 처리한다: 대상 노드에 ASKING 명령을 보내고 요청을 재시도하지만 slot 맵을 영구적으로 갱신하지는 않는다. 적절한 클라이언트 라이브러리를 사용하고 redis-cli에는 -c 플래그를 사용한다. resharding이 완료되면 리다이렉트는 MOVED가 되어 영구적이 된다.","tags":["ask","asking","cluster","cluster","migration","migration","redirect","redirect"]},{"code":"NOSCRIPT","title":"NOSCRIPT No matching script. Please use EVAL","severity":"caution","meaning":"EVALSHA로 호출한 Lua 스크립트의 SHA1 digest를 서버의 스크립트 캐시에서 찾을 수 없다. Redis가 이 해시에 해당하는 스크립트를 알지 못한다.","cause":"서버가 재시작되었거나 SCRIPT FLUSH로 스크립트 캐시가 비워짐, 스크립트가 로드된 적 없음, 또는 페일오버 후 새 노드의 캐시가 비어 있음.","fix":"EVAL로 스크립트 전체 본문을 실행하거나(Redis가 자동으로 캐시한다), 먼저 SCRIPT LOAD로 로드한 뒤 반환된 SHA를 EVALSHA에 사용한다. 애플리케이션에서 NOSCRIPT 오류를 잡아 EVALSHA에서 EVAL로 fallback하는 로직을 구현한다. 많은 Redis 클라이언트 라이브러리가 이를 자동으로 수행한다.","tags":["noscript","script","evalsha","eval","lua","sha","script load","cache"]},{"code":"EXECABORT","title":"EXECABORT Transaction discarded because of previous errors","severity":"caution","meaning":"큐잉 단계에서 발생한 오류로 인해 MULTI/EXEC 블록의 트랜잭션이 중단되었다. EXEC는 어떤 명령도 실행하지 않고 트랜잭션을 거부한다.","cause":"MULTI와 EXEC 사이의 명령에 구문 오류가 있거나, 존재하지 않는 명령 이름 또는 잘못된 인수 개수 같은 큐잉 중 검출된 오류. 이러한 오류는 트랜잭션 전체를 무효화한다.","fix":"MULTI 블록 내 명령의 구문과 인수를 수정한다. 명령을 큐잉할 때 응답을 확인한다. 어떤 명령이 QUEUED 대신 오류를 반환하면 EXEC 시 트랜잭션이 중단된다. 유효한 명령으로 트랜잭션을 다시 구성한다. 참고: Redis 트랜잭션은 롤백하지 않는다. 큐잉 오류만 EXECABORT로 이어지며, 실행 시 오류는 다른 명령을 멈추지 않는다.","tags":["execabort","transaction","transaction","multi","exec","discard","queue","queue"]}]},"errors/elasticsearch.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":7,"errors":[{"code":"cluster_block_exception","title":"ClusterBlockException[blocked by: [TOO_MANY_REQUESTS/12/disk usage exceeded flood-stage watermark, index has read-only-allow-delete block]] (7.x 이전: FORBIDDEN/12/index read-only / allow delete (api))","severity":"danger","meaning":"디스크 사용량이 flood stage watermark(기본 95%, 대용량 디스크에서는 max_headroom으로 결정)를 초과해, Elasticsearch가 디스크가 완전히 차지 않도록 해당 노드에 샤드가 있는 인덱스에 read_only_allow_delete 블록을 설정했다. 이 블록 때문에 인덱스에 쓰기(문서 index/update/delete)가 불가능하며 인덱스 자체 삭제만 허용된다. ES 8.x는 이 오류를 HTTP 429로 반환한다.","cause":"하나 이상의 노드에서 디스크 사용량이 cluster.routing.allocation.disk.watermark.flood_stage 임계값을 초과함. 블록을 수동으로 제거해도 디스크가 여전히 가득 차 있으면 Elasticsearch가 다시 적용한다. 로그 순환 없는 대형 인덱스, 정리되지 않은 스냅샷 또는 부족한 디스크 용량이 대표적인 원인이다.","fix":"먼저 디스크 공간을 확보한다: 오래되거나 불필요한 인덱스를 삭제하거나(DELETE /old-index), 스냅샷을 생성한 뒤 인덱스를 제거하거나, 디스크를 확장한다. 7.4+에서는 디스크 사용량이 high watermark 아래로 내려가면 블록이 자동으로 제거된다. 기다리지 않고 즉시 해제하려면 수동으로 제거한다: PUT /_all/_settings {\"index.blocks.read_only_allow_delete\": null}. 영구 해결책으로 ILM(Index Lifecycle Management)으로 오래된 데이터를 자동 삭제/아카이브하고 watermark 설정(disk.watermark.low/high/flood_stage)을 검토한다.","tags":["disk","read-only","watermark","flood-stage","cluster-block","danger"]},{"code":"index_read_only_allow_delete","title":"TOO_MANY_REQUESTS/12/disk usage exceeded flood-stage watermark, index has read-only-allow-delete block (legacy: FORBIDDEN/12/index read-only / allow delete (api))","severity":"danger","meaning":"index.blocks.read_only_allow_delete가 true여서 인덱스가 읽기 전용이다. 새 문서를 쓰거나 갱신할 수 없고 인덱스 삭제만 가능하다. Elasticsearch는 보통 디스크 flood stage watermark를 초과하면 이를 자동으로 설정한다. ES 8.x는 쓰기 요청에 HTTP 429를 반환한다.","cause":"디스크 사용량이 flood stage 임계값을 초과해 블록이 자동으로 설정되었거나, 설정을 수동으로 true로 지정함. 디스크 공간을 확보하지 않고 블록을 제거하면 Elasticsearch가 다시 적용한다.","fix":"충분한 디스크 공간을 확보한다. 7.4+에서는 자동으로 설정된 블록이 디스크 사용량이 high watermark 아래로 내려가면 스스로 해제된다. 수동으로 설정된 블록이거나 기다리지 않고 해제하려면 블록을 제거한다: PUT /_all/_settings {\"index.blocks.read_only_allow_delete\": null}. 단일 인덱스의 경우: PUT /index-name/_settings {\"index.blocks.read_only_allow_delete\": null}. 계속 재발한다면 디스크 용량을 늘리고 ILM 정책을 설정하며 _cat/allocation으로 어느 노드가 차고 있는지 모니터링한다.","tags":["read-only","disk","forbidden","watermark","block","danger"]},{"code":"high_disk_watermark_exceeded","title":"high disk watermark [90%] exceeded / flood stage disk watermark [95%] exceeded","severity":"danger","meaning":"노드의 디스크 사용량이 high watermark(기본 90%) 또는 flood stage(기본 95%) 임계값을 초과했다. high watermark를 초과하면 Elasticsearch는 해당 노드의 샤드를 다른 노드로 옮기려 하고, flood stage를 초과하면 인덱스를 읽기 전용으로 만든다.","cause":"디스크가 차고 있음: 계속 증가하는 로그/메트릭 인덱스, 삭제되지 않는 오래된 인덱스, 정리되지 않은 스냅샷 저장소, 너무 작은 디스크, 또는 불균형한 샤드 분산으로 단일 노드가 과도하게 참.","fix":"디스크 사용량을 확인한다: GET _cat/allocation?v. 오래된 인덱스를 삭제하거나 스냅샷 후 제거하고 디스크를 확장한다. 임계값을 일시적으로 높일 수 있다(신중하게. 8.x에서는 max_headroom 검증 때문에 세 값을 함께 설정한다): PUT _cluster/settings {\"persistent\":{\"cluster.routing.allocation.disk.watermark.low\":\"88%\",\"cluster.routing.allocation.disk.watermark.high\":\"93%\",\"cluster.routing.allocation.disk.watermark.flood_stage\":\"96%\"}}. 해결되면 같은 설정을 null로 지정해 기본값으로 되돌린다(transient 설정은 deprecated이므로 사용하지 않는다). 영구 해결책: ILM을 이용한 수명 주기 관리, 정기적인 인덱스 정리와 용량 계획. flood stage로 인덱스가 읽기 전용이 되었다면 블록을 별도로 제거한다.","tags":["disk","watermark","flood-stage","allocation","capacity","danger"]},{"code":"circuit_breaking_exception","title":"CircuitBreakingException[[parent] Data too large, data for [<...>] would be [...], which is larger than the limit of [...]]","severity":"danger","meaning":"요청이 메모리(heap) circuit breaker 한도를 초과할 것이어서 거부되었다. Elasticsearch는 노드가 OutOfMemoryError로 크래시되지 않도록 요청을 사전에 중단한다. [parent] breaker는 전체 메모리 사용량을, [fielddata] breaker는 text 필드의 fielddata 메모리를 보호한다.","cause":"부족한 JVM heap, 매우 크거나 복잡한 aggregation, 큰 결과 세트, 큰 bulk 요청, text 필드에 로드된 fielddata, 또는 heap을 채우는 많은 무거운 동시 쿼리. 대개 heap 압박의 증상이다.","fix":"쿼리를 줄인다: aggregation 크기, bulk 배치 크기, 반환 필드를 줄인다. text 필드의 fielddata 대신 keyword 하위 필드(.keyword)로 집계한다. JVM heap을 늘린다(서버 RAM의 약 50%, 최대 약 31GB). heap 압박을 모니터링한다: GET _cat/nodes?v&h=name,heap.percent. breaker 한도를 일시적으로 올리기보다 근본 원인(메모리/쿼리)을 해결한다. GET _nodes/stats/breaker로 어떤 breaker가 동작했는지 확인한다.","tags":["circuit-breaker","data-too-large","heap","memory","fielddata","oom","danger"]},{"code":"mapper_parsing_exception","title":"mapper_parsing_exception: failed to parse field [...] of type [...]","severity":"caution","meaning":"문서를 인덱싱하는 중 필드에 보낸 값이 매핑에 정의된 해당 필드의 타입과 맞지 않아 파싱할 수 없었다. 예를 들어 integer로 정의된 필드에 텍스트를 보내거나, date로 정의된 필드에 잘못된 날짜 형식을 보낸 경우.","cause":"필드 타입 불일치(숫자 필드에 문자열, date 필드에 잘못된 형식), 예상 날짜 형식과 맞지 않는 날짜, 잘못되거나 불완전한 JSON, 동적으로 잘못 매핑된 필드, 또는 객체가 예상되는 곳에 스칼라 값 전송.","fix":"오류 메시지의 필드 이름과 예상 타입을 확인한다: GET /index-name/_mapping. 문서의 값을 필드 타입에 맞추거나 올바른 타입으로 매핑을 재정의한다. date 필드는 format 파라미터를 문서 형식에 맞춘다. 기존 필드의 타입은 변경할 수 없으므로 필요하면 새 인덱스를 만들고 _reindex로 데이터를 옮긴다. 애플리케이션 계층에서 데이터를 검증한다.","tags":["mapping","parse","mapper","field-type","indexing","caution"]},{"code":"strict_dynamic_mapping_exception","title":"strict_dynamic_mapping_exception: mapping set to strict, dynamic introduction of [...] within [...] is not allowed","severity":"caution","meaning":"인덱스 또는 객체 매핑이 \"dynamic\": \"strict\"로 설정되어 있어, 매핑에 정의되지 않은 새 필드를 포함한 문서가 거부되었다. strict 모드는 스키마 밖의 필드가 조용히 추가되는 것을 막는다.","cause":"매핑이 dynamic:strict인 상태에서 매핑에 없는 필드(오타, 새 필드, 예상치 못한 필드)를 문서에 보냄. 대개 스키마 통제를 엄격히 유지하기 위한 의도적인 안전장치이다.","fix":"문서의 추가 필드나 오타 필드를 수정하거나 제거한다. 필드가 실제로 필요하다면 먼저 매핑에 추가한다: PUT /index-name/_mapping {\"properties\": {\"new_field\": {\"type\": \"...\"}}}. 새 필드가 자동으로 추가되기를 원하면 dynamic을 \"true\"로, 저장은 하되 인덱싱하지 않으려면 \"false\"로 설정한다. 스키마 규율을 지키기 위해 strict 모드는 의도적으로 사용한다.","tags":["mapping","strict","dynamic","schema","field","caution"]},{"code":"illegal_argument_exception_fielddata","title":"illegal_argument_exception: Fielddata is disabled on text fields by default. Set fielddata=true on [your_field] in order to load fielddata in memory","severity":"caution","meaning":"분석된 text 필드에 aggregation, 정렬 또는 스크립트를 시도했다. 이러한 작업은 메모리 비용이 큰 fielddata가 필요하기 때문에 text 필드에서 기본적으로 비활성화되어 있다.","cause":"text 필드에 직접 aggregation/정렬을 적용함. Elasticsearch에서 text는 전문 검색에, keyword는 정확한 값 기반 aggregation/정렬에 사용되며, 둘을 혼용하면 이 오류가 발생한다.","fix":"aggregation/정렬에는 필드의 keyword 하위 필드를 사용한다(예: \"my_field.keyword\"). 필드가 text 전용이라면 매핑에 keyword 하위 필드(multi-field)를 추가하고 _reindex를 실행한다. text 필드에 fielddata를 활성화(fielddata=true)할 수는 있지만 높은 메모리 소비와 circuit breaker 위험 때문에 권장하지 않으며, 불가피한 경우에만 사용한다.","tags":["fielddata","text","keyword","aggregation","sorting","mapping","caution"]},{"code":"search_phase_execution_exception","title":"search_phase_execution_exception: all shards failed / Failed to execute phase [query]","severity":"danger","meaning":"검색 요청이 필요한 샤드 중 하나 또는 전부에서 실패했다. 실제 원인을 감싸는 일반적인 검색 오류이며, \"caused by\" 섹션에 실제 문제(파싱 오류, breaker, 부적합한 필드 타입, 샤드 도달 불가 등)가 나온다.","cause":"잘못된 쿼리 구문, 잘못된 필드 타입에 대한 aggregation, too_many_clauses, circuit breaker 동작, 미할당/도달 불가 샤드, 쿼리 타임아웃 또는 노드 크래시. 근본 원인은 응답의 내부 exception에 있다.","fix":"응답의 \"caused by\" / \"failed_shards\" 상세에서 실제 exception을 찾아 그에 맞게 수정한다(쿼리 수정, keyword 필드 사용, breaker/heap 설정, 샤드 할당). 클러스터 상태를 확인한다: GET _cluster/health 및 GET _cat/shards?v. 부분 결과가 허용되고 일부 샤드가 실패했다면 allow_partial_search_results 동작을 검토한다.","tags":["search","shard","search","all-shards-failed","query","danger"]},{"code":"too_many_clauses","title":"too_many_clauses: maxClauseCount is set to 1024 (query contains too many clauses)","severity":"caution","meaning":"쿼리가 허용된 최대 Lucene clause 수를 초과했다. ES 8.x에서는 이 한도가 고정이 아니며 노드의 heap 크기와 검색 스레드 수로부터 자동 계산된다(heap이 작은 노드에서는 1024까지 내려갈 수 있다). 오류 메시지에 현재 값이 표시된다. 매우 큰 bool 쿼리와 많은 term으로 확장되는 쿼리가 원인이 된다.","cause":"프로그램으로 생성한 수천 개의 should/must clause를 가진 bool 쿼리, 많은 term으로 확장되는 wildcard/prefix/fuzzy/query_string 쿼리, 깊게 중첩된 bool 구조. 참고: terms 쿼리(단일 필드의 값 목록)는 이 한도의 대상이 아니며, 그 상한은 index.max_terms_count(기본 65,536)이다.","fix":"쿼리를 단순화한다: 단일 필드에 대한 많은 term clause를 하나의 terms 쿼리로 변환한다(큰 목록은 terms lookup). wildcard/prefix 대신 더 구체적인 쿼리 타입이나 index_prefixes/keyword 필드를 사용한다. ES 8.x에서 indices.query.bool.max_clause_count 설정은 deprecated이며 무시된다. 한도를 높이는 방법은 노드 heap을 늘리는 것이다. 쿼리 생성 로직을 검토한다.","tags":["too-many-clauses","max-clause-count","bool","terms","query","caution"]},{"code":"version_conflict_engine_exception","title":"version_conflict_engine_exception: [_doc][id]: version conflict, current version [x] is different than the one provided [y] (HTTP 409)","severity":"caution","meaning":"문서 갱신/삭제 요청이 낙관적 동시성 제어에 의해 거부되었다. 문서 버전(또는 _seq_no/_primary_term)이 요청의 값과 일치하지 않는다. 즉, 읽은 뒤 다른 누군가가 문서를 수정했다. HTTP 409를 반환한다.","cause":"같은 문서에 대한 동시 갱신, 오래된 version/seq_no로 쓰기 시도, 이미 존재하는 문서를 생성(op_type=create)하려는 시도, 또는 _bulk 중 경쟁 조건.","fix":"문서를 다시 읽고 현재 _seq_no와 _primary_term(또는 version)으로 요청을 재시도한다(read-modify-write 루프). 자동 재시도에는 _update 요청의 retry_on_conflict 파라미터를 사용한다. _update_by_query/_delete_by_query에서는 conflicts=proceed를 지정해 충돌 문서를 건너뛰고 계속 진행한다. 생성 전용이 의도라면 충돌은 정상이므로 애플리케이션에서 409를 잡아 적절히 처리한다(재시도 또는 건너뛰기).","tags":["version-conflict","409","concurrency","optimistic","seq-no","caution"]},{"code":"document_missing_exception","title":"document_missing_exception: [_doc][id]: document missing","severity":"caution","meaning":"갱신(_update)할 문서를 인덱스에서 찾을 수 없다. 갱신에는 기존 문서가 필요하며, 문서가 없으면 이 exception이 발생한다.","cause":"문서가 이전에 삭제됨, 생성된 적 없음, 잘못된 _id 사용, 잘못된 인덱스로 요청, 또는 custom routing으로 기록된 문서에 routing 없이 접근. (GET과 _update는 실시간이므로 refresh 지연은 이 오류의 원인이 아니다.)","fix":"_id와 대상 인덱스가 올바른지 확인한다: GET /index-name/_doc/id. 있으면 갱신, 없으면 생성하는 동작을 원하면 _update 요청에서 upsert(또는 doc_as_upsert:true)를 사용한다. index(PUT)로 문서를 직접 생성한다. 애플리케이션에서 이 exception을 잡아 upsert 로직으로 fallback한다.","tags":["document-missing","update","upsert","document","not-found","caution"]},{"code":"index_not_found_exception","title":"index_not_found_exception: no such index [index_name] (HTTP 404)","severity":"caution","meaning":"요청한 인덱스(또는 alias)가 존재하지 않는다. 지정한 이름의 인덱스를 찾을 수 없어 작업이 HTTP 404로 거부되었다.","cause":"인덱스가 생성된 적 없음, 삭제됨, 인덱스/alias 이름 오타, 클라이언트가 잘못된 환경(클러스터)에 접속, 또는 action.auto_create_index가 비활성화된 상태에서 존재하지 않는 인덱스에 쓰기 시도.","fix":"기존 인덱스를 나열한다: GET _cat/indices?v. 이름과 alias를 확인한다: GET _alias. 인덱스를 생성한다: PUT /index-name(필요한 mappings/settings 포함). 또는 index template으로 자동 생성을 구성한다. 자동 생성을 원한다면 action.auto_create_index 설정을 확인한다. 잘못된 클러스터에 접속하지 않았는지 확인한다.","tags":["index-not-found","404","index","alias","not-found","caution"]},{"code":"resource_already_exists_exception","title":"resource_already_exists_exception: index [index_name/uuid] already exists (HTTP 400)","severity":"safe","meaning":"생성하려는 인덱스(또는 리소스)가 이미 존재한다. 같은 이름의 인덱스가 있어 생성 요청이 거부되었다. 이전 버전에서는 index_already_exists_exception으로도 나타난다.","cause":"같은 인덱스를 두 번 생성하려 함, 멱등하지 않은 설정 스크립트, 동시 생성 요청, 또는 같은 이름을 다른 설정으로 다시 생성하려는 시도.","fix":"인덱스가 이미 존재한다면 생성할 필요 없이 계속 진행한다. 다시 생성해야 한다면 먼저 삭제(DELETE /index-name)한 뒤 생성한다(주의: 데이터 손실). 설정 스크립트를 멱등하게 만든다: 먼저 GET _cat/indices로 존재 여부를 확인한다. 스키마 관리에 index template이나 ILM/rollover를 사용해 충돌을 방지한다.","tags":["already-exists","resource-already-exists","index","create","idempotent","safe"]},{"code":"cluster_health_red","title":"cluster health status: RED / unassigned shards (클러스터 상태 RED / 미할당 샤드)","severity":"danger","meaning":"클러스터 상태 RED는 최소 하나의 primary 샤드가 미할당되어 사용할 수 없음을 의미한다. 일부 데이터를 읽거나 쓸 수 없는 심각한 상태로, 부분적 데이터 손실 위험이 있다. (YELLOW는 replica만 미할당이며 primary는 정상이다.)","cause":"노드가 크래시/이탈해 그 노드의 primary 샤드에 사본이 남지 않음, 디스크 watermark로 할당이 차단됨, 손상된 샤드, 스냅샷 복원 중 오류, 또는 샤드 할당을 막는 allocation 설정.","fix":"샤드가 미할당된 이유를 확인한다: GET _cluster/allocation/explain 또는 GET _cat/shards?v&h=index,샤드,prirep,state,unassigned.reason. 크래시된 노드를 복구하고, 디스크 문제라면 공간을 확보한다(watermark). 할당이 일시적으로 차단되었다면 재할당을 트리거한다: POST _cluster/reroute?retry_failed=true. primary가 완전히 유실되었다면 스냅샷에서 복원한다. RED 상태에서는 primary 복구에 먼저 집중한다.","tags":["cluster-health","red","unassigned","shard","primary","danger"]},{"code":"no_shard_available_action_exception","title":"no_shard_available_action_exception: No shard available for [...]","severity":"danger","meaning":"요청을 처리할 수 있는 활성 샤드 사본을 찾지 못했다. 해당 샤드가 미할당 상태이거나 복구 중이거나, 이를 호스팅하는 노드에 도달할 수 없어 요청을 수행할 수 없다.","cause":"노드 크래시/네트워크 파티션, 미할당 샤드, 진행 중인 샤드 복구, 디스크 watermark로 인한 할당 차단, 또는 클러스터가 RED/YELLOW 상태.","fix":"클러스터와 샤드 상태를 확인한다: GET _cluster/health 및 GET _cat/shards?v. 샤드가 미할당된 이유를 확인한다: GET _cluster/allocation/explain. 크래시된 노드를 복구하고 디스크 문제를 해결한 뒤 복구가 끝나기를 기다린다: GET _cat/recovery?v&active_only=true. 필요하면 POST _cluster/reroute?retry_failed=true로 할당을 재시도한다.","tags":["no-shard-available","shard","unassigned","recovery","cluster","danger"]},{"code":"es_rejected_execution_exception","title":"es_rejected_execution_exception: rejected execution of ... on ... (queue capacity ...) (HTTP 429)","severity":"danger","meaning":"해당 thread pool(search, write/bulk 등)의 큐 용량이 가득 차 작업이 거부되었다. 과부하 상태에서 Elasticsearch는 요청을 거절해 스스로를 보호하며(backpressure) HTTP 429(Too Many Requests)를 반환한다.","cause":"매우 많은 동시 요청, 매우 크거나 매우 빈번한 bulk 요청, 무거운 aggregation/검색 부하, 부족한 노드/CPU 리소스, 또는 워크로드에 비해 너무 작은 thread pool 큐.","fix":"클라이언트 측에서 exponential backoff로 재시도한다. 429는 보통 일시적이다. bulk 배치 크기와 전송 속도를 줄이고 동시성을 낮춘다. 거부된 작업을 모니터링한다: GET _cat/thread_pool?v&h=node_name,name,active,queue,rejected. 지속적인 부하에는 노드/리소스를 추가한다(수평 확장). 큐 크기를 늘리는 것은 최후의 수단이며 근본 원인은 용량이다.","tags":["rejected-execution","429","thread-pool","queue","bulk","backpressure","danger"]},{"code":"master_not_discovered_exception","title":"master_not_discovered_exception / MasterNotDiscoveredException","severity":"danger","meaning":"클러스터가 활성 master 노드를 선출하거나 찾지 못했다. master가 없으면 샤드 할당, 매핑 변경, 클러스터 상태 갱신이 불가능해 클러스터가 작업을 처리할 수 없게 된다.","cause":"master-eligible 노드의 과반수(quorum)에 도달할 수 없음, 네트워크 파티션(split brain 방지), 잘못된 discovery.seed_hosts / cluster.initial_master_nodes 구성, 신규 설치에서 bootstrap 미수행, 또는 master-eligible 노드 수 부족.","fix":"master-eligible 노드가 실행 중이며 서로를 볼 수 있는지 확인한다: GET _cat/nodes?v 및 GET _cluster/state. discovery.seed_hosts와 cluster.initial_master_nodes 설정을 확인한다(최초 설정 시). 네트워크/방화벽과 노드 간(transport 9300) 연결을 확인한다. 충분한 수(가급적 3개)의 master-eligible 노드를 실행 상태로 유지한다. 로그에서 master 선출 메시지를 확인한다.","tags":["master","discovery","quorum","split-brain","cluster","danger"]},{"code":"security_exception","title":"security_exception: unable to authenticate / action [...] is unauthorized for user [...] (HTTP 401/403)","severity":"danger","meaning":"인증(401 Unauthorized) 또는 인가(403 Forbidden)에 실패했다. 401은 자격 증명이 누락되었거나 틀렸음을, 403은 사용자가 인증되었지만 이 작업/인덱스에 필요한 롤/권한이 없음을 나타낸다.","cause":"잘못된 사용자 이름/비밀번호 또는 API key, Authorization 헤더 누락, 만료되었거나 유효하지 않은 토큰, 사용자에게 해당 인덱스 또는 클러스터 권한 없음, 또는 롤 할당 누락/오류.","fix":"401의 경우: 올바른 자격 증명/API key를 보내고 Authorization 헤더를 확인한다. 403의 경우: 사용자의 롤과 권한을 확인하고(GET _security/user/app_user, GET _security/role/app_role) 필요한 인덱스/클러스터 권한을 추가한다. 최소 권한 원칙을 따른다. API key를 사용한다면 만료와 권한을 확인한다.","tags":["security","401","403","authentication","authorization","security","danger"]},{"code":"validation_exception","title":"validation_exception / illegal_argument_exception: Validation Failed: ...","severity":"caution","meaning":"요청의 파라미터 또는 설정이 검증에 실패했다. Elasticsearch가 유효하지 않거나 일관되지 않은 값을 포함한 요청을 실행하지 않고 거부했다(예: 잘못된 샤드 수, 호환되지 않는 설정 조합, 필수 필드 누락).","cause":"유효하지 않은 인덱스 설정(예: 음수/0인 number_of_shards), 기존 인덱스에서 변경 불가 설정의 갱신 시도, 호환되지 않는 파라미터 조합, 필수 필드 누락, 또는 범위를 벗어난 값.","fix":"오류 메시지의 검증 상세를 읽는다. 어떤 파라미터가 왜 유효하지 않은지 명확히 나와 있다. 값을 유효한 범위/형식으로 맞춘다. 변경 불가(static) 설정은 기존 인덱스가 아니라 새 인덱스 생성 시 지정한다(필요하면 _reindex). API가 요구하는 필수 필드를 제공한다. 설정의 문서화된 유효 값을 확인한다.","tags":["validation","validation","settings","illegal-argument","invalid","caution"]},{"code":"task_cancelled_exception","title":"task_cancelled_exception: Task was cancelled / TaskCancelledException","severity":"caution","meaning":"실행 중인 task(search, _reindex, _update_by_query 등)가 취소되었다. 취소는 _tasks/_cancel에 대한 사용자 요청, 클라이언트의 HTTP 연결 종료(클라이언트 타임아웃/중단. Elasticsearch가 관련 검색을 자동으로 취소한다), 상위 task의 취소 또는 만료된 async search로 발생할 수 있다.","cause":"사용자가 POST _tasks/<task_id>/_cancel로 task를 취소함(X-Opaque-Id는 task를 식별하고 찾는 데 도움을 줄 뿐 취소하지 않는다), 클라이언트 타임아웃으로 연결이 끊김, 장시간 실행되는 고비용 쿼리, 또는 adaptive resource management가 무거운 task를 중지함.","fix":"task가 예기치 않게 취소되었다면 클라이언트 타임아웃과 연결 설정을 검토하고 무거운 쿼리를 최적화한다(데이터 축소, 더 구체적인 필터). 긴 작업은 _reindex/_update_by_query를 비동기(wait_for_completion=false)로 실행하고 GET _tasks로 모니터링한다. 활성 task를 확인한다: GET _tasks?detailed=true. 필요하면 고비용 쿼리를 분할하거나 scroll/PIT를 사용한다.","tags":["task-cancelled","task","timeout","reindex","cancel","caution"]}]},"errors/oracle-more.json":{"engineId":"oracle","engine":"Oracle","order":1,"errors":[{"code":"ORA-00604","title":"error occurred at recursive SQL level","severity":"danger","meaning":"Oracle이 내부적으로 백그라운드에서 실행하는 recursive SQL 중 오류가 발생했다. 실제 오류를 감추는 래퍼(외부) 오류이며, 실제 원인은 바로 아래에 나열된 두 번째 오류 코드에 있다.","cause":"data dictionary 접근, 실패하는 DDL 트리거, 가득 찬 SYSTEM 테이블스페이스, 임시 공간 부족, 손상된 data dictionary 객체, 또는 세션 로그인 시 실행되는 LOGON 트리거의 실패.","fix":"오류 스택에서 ORA-00604 다음의 두 번째 줄을 읽는다. 그것이 실제 원인이므로 이를 해결한다. 예를 들어 ORA-01653이 보이면 테이블스페이스를 확장한다. DDL/LOGON 트리거가 의심되면 일시적으로 비활성화한다: ALTER TRIGGER trigger_name DISABLE; DBA_FREE_SPACE로 SYSTEM 및 TEMP 테이블스페이스 사용량을 확인한다.","tags":["recursive","recursion","dictionary","data dictionary","trigger","wrapper"]},{"code":"ORA-01438","title":"value larger than specified precision allowed for this column","severity":"caution","meaning":"NUMBER 컬럼에 정의된 precision과 scale 한도를 초과하는 숫자가 기록되었다. 예를 들어 정수부가 3자리인 값은 NUMBER(4,2) 컬럼에 들어가지 않는다.","cause":"컬럼의 NUMBER(p,s) 정의가 데이터에 비해 너무 좁음. 계산 결과가 예상보다 큼. 소수부 scale이 정수부에 남는 자릿수를 줄임(NUMBER(5,3)은 정수 2자리만 허용).","fix":"실제 데이터 범위에 맞게 컬럼 용량을 넓힌다: ALTER TABLE my_table MODIFY (my_column NUMBER(12,2)); 애플리케이션에서 값을 반올림하거나 ROUND/TRUNC로 scale에 맞춘다. 넘치는 행을 찾으려면 테스트하면서 쿼리 범위를 좁히고, 입력 검증을 추가한다.","tags":["number","precision","precision","scale","scale","overflow","numeric"]},{"code":"ORA-01400","title":"cannot insert NULL into (column)","severity":"caution","meaning":"NOT NULL 제약 조건이 있는 컬럼에 NULL 값을 삽입하거나 설정했다. 필수 필드는 비워 둘 수 없다.","cause":"INSERT에서 필수 컬럼에 값을 제공하지 않음, UPDATE로 컬럼을 NULL로 설정, 컬럼에 DEFAULT 미정의, 또는 서브쿼리/애플리케이션에서 빈 값이 넘어옴.","fix":"오류 메시지에 나온 컬럼에 유효한 값을 제공한다. 컬럼에 적절한 기본값을 정의한다: ALTER TABLE my_table MODIFY (my_column DEFAULT 0); (DEFAULT는 INSERT에서 컬럼을 완전히 생략한 경우에만 적용된다. 명시적으로 보낸 NULL 값에는 12c+ DEFAULT ON NULL을 사용한다: ALTER TABLE my_table MODIFY (my_column DEFAULT ON NULL 0);) 규칙이 실제로 필수가 아니라면 제약 조건을 제거한다: ALTER TABLE my_table MODIFY (my_column NULL); 빈 값은 전송 전에 애플리케이션에서 검증한다.","tags":["not null","null","empty","insert","constraint","constraint","mandatory"]},{"code":"ORA-02291","title":"integrity constraint violated - parent key not found","severity":"danger","meaning":"외래 키 제약 조건이 위반되었다. 자식 테이블에 삽입된 값에 대응하는 부모 행을 찾지 못했다. 즉, 참조된 레코드가 존재하지 않는다.","cause":"존재하지 않는 부모 레코드를 참조하는 INSERT/UPDATE, 부모 테이블 행이 아직 삽입되지 않음, 잘못된 로드 순서(부모보다 자식 먼저), 또는 대소문자나 공백 차이로 인한 매칭 실패.","fix":"먼저 부모 테이블에 해당 레코드를 삽입한 뒤 자식 테이블에 삽입한다. 참조가 존재하는지 확인한다: SELECT * FROM parent_table WHERE key_col = value; 대량 로드에서는 올바른 순서(부모 먼저)로 테이블을 처리한다. 위반 행을 찾으려면 NOT IN / anti-join으로 자식 테이블의 값을 부모 테이블과 비교한다.","tags":["fk","foreign key","foreign key","constraint","constraint","parent","reference"]},{"code":"ORA-02292","title":"integrity constraint violated - child record found","severity":"danger","meaning":"부모 행을 삭제하거나 키를 변경했지만, 종속된 자식 레코드가 있어 외래 키 제약 조건이 허용하지 않았다.","cause":"자식 테이블에 레코드가 있는 부모 행을 DELETE하려 함. FK가 ON DELETE CASCADE 없이 정의됨. 종속 레코드를 먼저 정리하지 않음.","fix":"먼저 종속된 자식 레코드를 삭제하거나 다른 부모 레코드로 옮긴 뒤 부모 행을 삭제한다. 업무 규칙이 허용한다면 외래 키를 ON DELETE CASCADE로 다시 생성한다: ALTER TABLE child DROP CONSTRAINT fk_name; ALTER TABLE child ADD CONSTRAINT fk_name FOREIGN KEY (my_column) REFERENCES parent(key_col) ON DELETE CASCADE; 종속 행을 찾으려면 부모 키로 자식 테이블을 조회한다.","tags":["fk","foreign key","foreign key","child","child record","delete","cascade"]},{"code":"ORA-00979","title":"not a GROUP BY expression","severity":"caution","meaning":"SELECT 목록에 집계 함수(SUM, COUNT 등)와 함께 집계되지 않은 컬럼을 사용했지만 해당 컬럼이 GROUP BY 절에 추가되지 않았다. Oracle은 그 컬럼을 어떻게 그룹화할지 알 수 없다.","cause":"SELECT에는 있지만 GROUP BY에는 없는 컬럼. 집계 함수 밖의 표현식이 GROUP BY 목록에서 누락됨. HAVING 또는 select 표현식의 불일치.","fix":"집계되지 않은 모든 컬럼을 GROUP BY 목록에 추가한다: SELECT department, COUNT(*) FROM employee GROUP BY department; 또는 해당 컬럼도 집계 함수(MAX, MIN)로 감싸거나, 분석 함수(OVER())로 그룹화를 재설계한다.","tags":["group by","grouping","aggregate","aggregation","sum","count","query"]},{"code":"ORA-00918","title":"column ambiguously defined","severity":"caution","meaning":"쿼리의 여러 테이블에 같은 이름의 컬럼이 있고 테이블/alias 접두어 없이 사용되어, Oracle이 어느 테이블의 컬럼인지 판단할 수 없다.","cause":"조인된 두 테이블 모두에 같은 이름의 컬럼이 있음(예: 양쪽의 ID). SELECT, WHERE 또는 ORDER BY에서 컬럼을 한정하지 않고(접두어 없이) 사용함.","fix":"모호한 모든 컬럼을 테이블 이름 또는 alias로 한정한다: SELECT a.id, b.id FROM tableA a JOIN tableB b ON a.id = b.a_id; 테이블에 짧은 alias를 붙이고 모든 컬럼에 접두어를 붙여 가독성과 명확성을 높인다.","tags":["ambiguous","ambiguous column","column","column","join","alias","query"]},{"code":"ORA-06550","title":"line/column PLS-... PL/SQL compilation error","severity":"caution","meaning":"PL/SQL 블록, 프로시저 또는 익명 블록을 컴파일하는 중 구문 또는 의미 오류가 발견되었다. 코드가 실행되기 전 컴파일 단계에서 실패했다.","cause":"오타, 선언되지 않은 변수나 객체 참조, 잘못된 개수/타입의 파라미터로 호출, 세미콜론 누락, 존재하지 않는 프로시저/패키지 호출, 또는 권한 누락.","fix":"오류 메시지의 줄과 컬럼 번호로 이동해 PLS-XXXX 하위 코드를 읽는다. 모든 컴파일 오류를 보려면: SHOW ERRORS; 또는 SELECT * FROM user_errors WHERE name = 'OBJECT_NAME'; 객체 이름, 파라미터 시그니처, 필요한 GRANT를 확인한 뒤 다시 컴파일한다.","tags":["plsql","compilation","compile","pls","syntax","procedure","code"]},{"code":"ORA-04068","title":"existing state of packages has been discarded","severity":"caution","meaning":"실행 중인 세션이 패키지의 상태를 사용하는 동안 패키지가 재컴파일되어 그 상태가 무효화되었다. 세션이 보유한 패키지 인스턴스가 더 이상 유효하지 않다.","cause":"패키지 body 또는 specification이 이에 종속된 세션이 활성 상태인 동안 ALTER/CREATE OR REPLACE로 재컴파일됨. 보통 운영 중인 시스템에 애플리케이션을 배포할 때 발생한다. ORA-04061/04065와 함께 나타난다.","fix":"호출을 다시 실행(재시도)하면 대개 해결된다. 두 번째 시도에서 패키지가 새 형태로 로드된다. 애플리케이션에서 ORA-04068을 잡아 자동으로 재시도한다. 패키지 변경은 사용자 활동이 적은 유지보수 시간대에 적용한다. stateless 패키지 설계는 이 문제를 줄인다.","tags":["package","package","state","package state","recompile","compile","retry"]},{"code":"ORA-01427","title":"single-row subquery returns more than one row","severity":"caution","meaning":"단일 행을 반환할 것으로 예상된 서브쿼리가 둘 이상의 행을 반환했다. 예를 들어 등호(=) 비교의 오른쪽 서브쿼리가 여러 값을 생성한 경우.","cause":"= 또는 할당이 기대되는 곳에서 여러 행을 반환하는 서브쿼리, 충분히 좁혀지지 않은 WHERE 조건, 상관 조건에서 고유하지 않은 매칭, SELECT 목록의 스칼라 서브쿼리가 여러 행을 반환.","fix":"서브쿼리가 단일 행을 반환하도록 범위를 좁힌다(추가 WHERE, 고유 키). 여러 값이 실제로 예상된다면 = 대신 IN을 사용한다: WHERE id IN (SELECT id FROM ...). 또는 서브쿼리에 집계(MAX, MIN)를 추가하거나 ROWNUM = 1로 한 행으로 줄인다. 먼저 서브쿼리만 단독으로 실행해 몇 행을 반환하는지 확인한다.","tags":["subquery","subquery","single row","single row","in","scalar","query"]},{"code":"ORA-12899","title":"value too large for column","severity":"caution","meaning":"정의된 최대 길이보다 긴 값이 문자/데이터 컬럼에 기록되었다. 예를 들어 15자 텍스트는 VARCHAR2(10) 컬럼에 들어가지 않는다.","cause":"컬럼 폭이 데이터에 비해 부족함. 멀티바이트 문자 집합(UTF8)에서 문자 수가 바이트 한도를 초과함(BYTE semantics). 데이터 마이그레이션 중 다른 문자 집합. 연결(concatenation) 결과가 길어짐.","fix":"컬럼을 넓힌다: ALTER TABLE my_table MODIFY (my_column VARCHAR2(100)); 멀티바이트 내용에는 CHAR semantics를 사용한다: my_column VARCHAR2(100 CHAR). 긴 값을 줄여야 한다면 SUBSTR로 자른다. 오류 메시지에 실제 길이와 허용 길이가 나오므로 LENGTH/LENGTHB로 넘치는 레코드를 찾는다.","tags":["varchar2","length","length","too large","column","column","charset"]},{"code":"ORA-30926","title":"unable to get a stable set of rows in the source tables","severity":"danger","meaning":"MERGE(또는 갱신 가능한 조인) 문에서 소스 측이 같은 대상 행에 대해 둘 이상의 매칭 행을 생성했다. Oracle이 어느 소스 값으로 갱신할지 결정할 수 없어 작업을 중단했다.","cause":"MERGE ... USING 소스에 ON 조건에 대한 중복 매칭이 있음. 소스 쿼리가 대상 키 기준으로 고유하지 않음. 여러 소스 행이 같은 대상 행에 매핑됨.","fix":"소스 쿼리를 대상 키 기준으로 고유하게 만든다. 중복 매칭을 찾는다: SELECT key_col, COUNT(*) FROM (source_query) GROUP BY key_col HAVING COUNT(*) > 1; DISTINCT 또는 집계/분석 함수(ROW_NUMBER)로 소스의 중복을 제거한 뒤 MERGE를 다시 실행한다. ON 조건이 충분히 선택적인지 확인한다.","tags":["merge","merge","duplicate","duplicate rows","source","source","stable"]},{"code":"ORA-08103","title":"object no longer exists","severity":"danger","meaning":"실행 중인 쿼리가 접근하는 객체(테이블, 인덱스, LOB 세그먼트)가 더 이상 존재하지 않거나 물리 구조가 변경되었다. 보통 쿼리 진행 중 해당 객체에 DDL/TRUNCATE가 발생한 경우이다.","cause":"쿼리가 열려 있는 동안 같은 객체가 TRUNCATE/DROP/SHRINK됨. 이동(MOVE) 또는 파티션 유지보수. 손상된 인덱스/세그먼트. LOB 세그먼트 접근 중 하위 구조가 사라짐. 동시 DDL 충돌.","fix":"객체가 여전히 존재하고 유효한지 확인한다: SELECT status FROM user_objects WHERE object_name = 'NAME'; 해당 인덱스/객체를 재구축(REBUILD)하거나 쿼리를 다시 실행한다. 장시간 쿼리가 사용 중인 객체에 TRUNCATE/DROP을 하지 않는다. 손상이 의심되면 ANALYZE ... VALIDATE STRUCTURE로 확인한다.","tags":["object","object","truncate","drop","ddl","corruption","segment"]},{"code":"ORA-01843","title":"not a valid month","severity":"caution","meaning":"텍스트를 DATE로 변환하는 중 월 부분을 유효한 월로 해석할 수 없었다. 대개 날짜 포맷이 데이터 형식과 맞지 않는다.","cause":"TO_DATE에 format mask를 전달하지 않았고 세션의 NLS_DATE_FORMAT이 데이터와 맞지 않음. 일과 월이 뒤바뀜(DD/MM vs MM/DD 혼동). 언어/로캘 차이(월 이름). 손상되었거나 예상치 못한 날짜 텍스트.","fix":"TO_DATE 호출에는 항상 명시적 포맷을 전달한다: TO_DATE('2026-07-04', 'YYYY-MM-DD'). 월 이름이 포함된 텍스트는 언어를 지정한다: TO_DATE(str, 'DD-MON-YYYY', 'NLS_DATE_LANGUAGE=ENGLISH'). 필요하면 세션 포맷을 설정한다: ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'. 로드 전에 유효하지 않은 날짜 텍스트를 검증한다.","tags":["date","date","to_date","nls","format","month","month"]},{"code":"ORA-14400","title":"inserted partition key does not map to any partition","severity":"danger","meaning":"삽입된 행의 파티션 키 값이 테이블에 정의된 어떤 파티션에도 매핑되지 않는다. 해당 값을 포함하는 range 또는 list 파티션이 없다.","cause":"RANGE 파티셔닝에서 가장 높은 범위보다 큰 날짜/값이 삽입되었고 MAXVALUE 파티션이 없음. LIST 파티셔닝에서 정의되지 않은 새 값. 월/연 경계에서 새 파티션을 미리 생성하지 않음.","fix":"누락된 파티션을 추가한다: ALTER TABLE my_table ADD PARTITION p_2026_08 VALUES LESS THAN (TO_DATE('2026-09-01','YYYY-MM-DD')); RANGE에는 안전장치로 MAXVALUE 파티션을 추가한다. LIST에는 예상치 못한 값을 받을 DEFAULT 파티션을 정의한다. 새 파티션이 자동으로 생성되도록 자동 파티셔닝(INTERVAL partitioning)을 사용한다.","tags":["partition","partition","partition key","range","list","maxvalue","insert"]},{"code":"ORA-01536","title":"space quota exceeded for tablespace","severity":"danger","meaning":"지정한 테이블스페이스에 대한 사용자의 공간 quota가 소진되었다. 테이블스페이스에 여유 공간이 있더라도 사용자에게 부여된 한도를 초과했기 때문에 새 공간을 할당할 수 없다.","cause":"사용자에게 제한된 quota가 있음(UNLIMITED 아님). 데이터 증가로 quota가 가득 참. 테이블/인덱스에 과도한 로드. 잘못된 테이블스페이스에 기록. 세그먼트가 예상보다 빠르게 증가.","fix":"사용자의 quota를 늘린다: ALTER USER app_user QUOTA UNLIMITED ON tablespace_name; 또는 특정 값을 지정한다: QUOTA 500M ON users. 현재 사용량을 확인한다: SELECT * FROM dba_ts_quotas WHERE username = 'APP_USER'. 불필요하게 큰 세그먼트를 아카이브/삭제하거나 객체를 적절한 테이블스페이스로 옮긴다.","tags":["quota","quota","tablespace","space","space","user","user"]},{"code":"direct path read","title":"대기: buffer cache를 거치지 않고 PGA로 블록을 직접 읽는 세션","severity":"caution","meaning":"세션이 database buffer cache를 통하지 않고 PGA 메모리로 블록을 직접 읽는 동안 이 대기 이벤트에서 시간을 보낸다. 보통 대규모 full table scan 및 parallel query와 관련이 있다.","cause":"대형 테이블의 full scan으로 디스크에서 많은 블록을 읽음, parallel query, 임시 테이블스페이스로 넘치는 sort/hash 작업, 직렬 full scan에 대한 serial direct path read 결정, 인덱스를 사용할 수 없는 쿼리.","fix":"AWR/ASH로 무거운 full scan을 수행하는 SQL을 식별하고 적절한 인덱스를 추가한다. 쿼리 튜닝으로 대형 테이블의 불필요한 스캔을 줄인다. 스토리지 지연(I/O)을 조사하고 필요하면 더 빠른 디스크로 옮긴다. 병렬도를 검토한다. TEMP로 넘치는 작업에는 PGA_AGGREGATE_TARGET 크기를 검토한다.","tags":["wait","wait","direct path read","full scan","full table scan","io","pga"]},{"code":"latch: cache buffers chains","title":"대기: cache buffers chains latch 경합","severity":"danger","meaning":"buffer cache의 블록을 보호하는 cache buffers chains(CBC) latch에 경합이 있다. 많은 세션이 자주 접근되는 같은 블록(hot block)에 접근하려 할 때 이 대기가 증가한다.","cause":"자주 읽히는 hot block. 높은 논리적 읽기를 발생시키는 비효율적인 SQL. nested loop join에서 같은 인덱스 root 블록에 반복 접근. 고르지 않게 분산된 무거운 동시 접근. 작지만 많이 읽히는 테이블.","fix":"논리적 읽기를 가장 많이 발생시키는 SQL을 찾고(V$SQL, ASH) 인덱스/접근 계획을 개선해 버퍼 접근을 줄인다. hot block을 식별한다(X$BH / 관련 뷰). 불필요한 nested loop를 hash join으로 전환하는 것을 검토한다. 작고 많이 읽히는 참조 테이블은 접근 패턴을 분산하거나 캐시한다.","tags":["latch","cache buffers chains","cbc","contention","contention","hot block","wait","wait"]}]},"errors/postgres-more.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"errors":[{"code":"42883","title":"undefined_function","severity":"caution","meaning":"쿼리에서 호출한 함수(또는 연산자)를 이 이름과 이 인수 타입으로 찾을 수 없다. PostgreSQL은 이름과 인수 타입 모두로 함수를 해석하며, 시그니처가 맞지 않으면 이 오류가 발생한다.","cause":"함수 이름 오타. 정의된 시그니처와 맞지 않는 인수 타입(예: 텍스트 대신 숫자). 명시적 캐스트 누락. 함수의 스키마가 search_path에 없음. 필요한 extension이 설치되지 않음.","fix":"함수가 존재하는지 확인하고 시그니처를 확인한다: \\df function_name. 인수를 명시적으로 캐스트한다: SELECT fn(value::text). 스키마 접두어를 추가하거나 search_path를 설정한다. extension이 필요하다면 설치한다: CREATE EXTENSION extension_name; 연산자 오류는 양쪽 타입을 일치시킨다.","tags":["function","function not found","undefined","operator","cast","type conversion","42883"]},{"code":"42804","title":"datatype_mismatch","severity":"caution","meaning":"예상 데이터 타입과 제공된 데이터 타입이 일치하지 않는다. 예를 들어 컬럼에 잘못된 타입의 값을 할당하거나, CASE 분기가 서로 다른 타입을 반환하거나, UNION 분기의 타입이 맞지 않는 경우.","cause":"컬럼 타입과 호환되지 않는 할당. CASE/COALESCE 분기가 서로 다른 타입을 생성. UNION의 대응 컬럼 타입이 다름. RETURN한 값이 함수의 반환 타입과 맞지 않음. boolean이 기대되는 곳에 boolean이 아닌 값.","fix":"표현식을 올바른 타입으로 캐스트한다: value::integer 또는 CAST(value AS numeric). 모든 CASE 분기가 같은 타입을 반환하게 한다. UNION에서는 양쪽 대응 컬럼의 타입을 맞춘다. WHERE 조건이 boolean을 산출하는지 확인한다.","tags":["type","data type","mismatch","type mismatch","cast","union","42804"]},{"code":"22P02","title":"invalid_text_representation","severity":"caution","meaning":"변환 중 텍스트 값이 대상 데이터 타입의 유효한 표현이 아니었다. 예를 들어 텍스트 'abc'를 integer로 변환하려 하면 이 오류가 발생한다.","cause":"숫자 컬럼/파라미터에 비숫자 텍스트 전송. 빈 문자열('')을 숫자/날짜로 변환. UUID 또는 enum에 대한 유효하지 않은 텍스트. 잘못된 JSON. 예상 형식이 아닌 입력 데이터.","fix":"변환되는 값이 실제로 유효한지 확인한다. 타입 변환 전에 애플리케이션에서 입력을 검증한다. 빈 문자열은 NULL로 변환한다: NULLIF(value, ''). PostgreSQL 16+에서 안전하게 변환하려면 pg_input_is_valid('abc', 'integer')로 값을 미리 테스트하고 오류 상세는 pg_input_error_info()를 사용한다. 이전 버전에서는 검증 체크(regex)를 검토한다. 어느 행이 문제인지 범위를 좁힌다.","tags":["cast","type conversion","invalid text","invalid input","integer","uuid","22P02"]},{"code":"22003","title":"numeric_value_out_of_range","severity":"caution","meaning":"숫자 값이 대상 타입이 허용하는 범위를 벗어났다. 예를 들어 integer 컬럼에 20억보다 큰 값을 기록하거나, numeric(5,2) 컬럼에 너무 큰 숫자를 기록한 경우.","cause":"smallint/integer/bigint 한도 초과. numeric(p,s) precision/scale 초과. 산술 오버플로. 합계/곱이 타입 용량을 초과. 시퀀스가 타입의 상한에 도달.","fix":"컬럼/파라미터를 더 넓은 타입으로 변경한다(integer 대신 bigint, numeric precision 증가): ALTER TABLE my_table ALTER COLUMN my_column TYPE bigint; 업무 규칙에 따라 값을 반올림(ROUND)하거나 검증한다. 중간 계산이 오버플로된다면 표현식을 numeric으로 계산한다. 시퀀스 오버플로는 타입을 넓힌다.","tags":["numeric","number","out of range","overflow","numeric overflow","integer","22003"]},{"code":"23514","title":"check_violation","severity":"caution","meaning":"CHECK 제약 조건이 위반되었다. 삽입 또는 갱신된 행이 테이블에 정의된 논리 조건(예: price > 0)을 충족하지 않는다.","cause":"업무 규칙을 위반하는 데이터(음수 가격, 유효하지 않은 상태 코드, 날짜 범위 위반). 애플리케이션 측 검증 없음. 데이터에 비해 너무 엄격한 제약 조건. 대량 로드 중 규칙을 위반하는 레코드.","fix":"오류 메시지의 제약 조건 이름으로 어떤 조건이 위반되었는지 찾고 정의를 확인한다: \\d table_name. 조건을 반전시켜 위반 행을 조회한다. 데이터를 수정하거나, 규칙이 실제로 바뀌었다면 제약 조건을 갱신한다: ALTER TABLE my_table DROP CONSTRAINT name; ALTER TABLE my_table ADD CONSTRAINT name CHECK (new_condition);","tags":["check","constraint","check constraint","violation","constraint violation","validation","23514"]},{"code":"22012","title":"division_by_zero","severity":"caution","meaning":"산술 표현식이 0으로 나누기(또는 나머지 연산)를 시도했다. PostgreSQL은 0으로 나누기를 허용하지 않으며 쿼리를 중단한다.","cause":"제수 컬럼/표현식이 0으로 평가됨. 비율/백분율 계산에서 분모가 0. 0인 COUNT 또는 SUM으로 나눔. 필터링되지 않은 데이터의 예상치 못한 0 값.","fix":"분모가 0일 때 NULL을 반환하도록 NULLIF를 사용한다: numerator / NULLIF(denominator, 0). 원하면 COALESCE로 결과를 기본값으로 바꾼다: COALESCE(numerator / NULLIF(denominator,0), 0). 또는 CASE WHEN denominator = 0 THEN NULL ELSE numerator/denominator END를 사용한다. 데이터에서 0인 경우를 미리 필터링한다.","tags":["division","divide","zero","divide by zero","nullif","arithmetic","22012"]},{"code":"42601","title":"syntax_error","severity":"caution","meaning":"SQL 문에서 구문 오류가 발견되었다. PostgreSQL이 쿼리를 파싱하지 못했으며 오류의 대략적인 위치(position)를 보고한다.","cause":"쉼표나 괄호의 누락/과잉. 예약 키워드(user, order 등)를 따옴표 없이 컬럼 이름으로 사용. 세미콜론 누락. 명령어 오타. 닫히지 않은 문자열 리터럴. 버전별 구문 사용.","fix":"오류 메시지의 'position' 값으로 문제 위치를 찾고 바로 앞부분을 확인한다. 예약어는 큰따옴표로 감싼다: \"order\". 괄호와 따옴표의 짝이 맞는지 확인한다. 복잡한 쿼리는 조각으로 나누어 테스트한다. 올바른 구문은 사용 중인 버전의 문서를 참고한다.","tags":["syntax","syntax error","parse","parser","keyword","position","42601"]},{"code":"42P07","title":"duplicate_table","severity":"caution","meaning":"생성하려는 테이블(또는 유사 객체)이 이미 존재한다. 같은 이름의 객체가 있어 CREATE가 실패했다.","cause":"CREATE TABLE로 기존 테이블을 다시 생성. 스크립트를 두 번 실행. 마이그레이션 재적용. 같은 이름의 뷰/시퀀스/인덱스와 이름 충돌.","fix":"객체가 이미 존재하면 생성을 건너뛰도록 IF NOT EXISTS를 사용한다: CREATE TABLE IF NOT EXISTS my_table (...). 정말로 다시 생성하려면 먼저 삭제한다: DROP TABLE IF EXISTS my_table; 마이그레이션을 재실행해도 안전하게(멱등하게) 만든다. \\dt로 기존 객체를 확인한다.","tags":["duplicate","duplicate object","table","relation","create","already exists","42P07"]},{"code":"42701","title":"duplicate_column","severity":"caution","meaning":"테이블 정의 또는 ALTER 작업에서 같은 컬럼 이름이 두 번 이상 사용되었거나, 이미 존재하는 컬럼을 다시 추가했다.","cause":"CREATE TABLE에서 같은 컬럼 이름을 두 번 작성. ALTER TABLE ADD COLUMN으로 이미 존재하는 컬럼 추가. 복사-붙여넣기 실수. 마이그레이션 재실행.","fix":"테이블 정의에서 중복된 컬럼 이름을 제거하거나 이름을 바꾼다. 기존 컬럼을 다시 추가하지 않도록 먼저 확인한다: \\d table_name. 버전이 지원한다면 안전하게 추가한다: ALTER TABLE my_table ADD COLUMN IF NOT EXISTS my_column type; 마이그레이션이 멱등한지 확인한다.","tags":["duplicate","duplicate column","column","column exists","alter","add column","42701"]},{"code":"3D000","title":"invalid_catalog_name","severity":"danger","meaning":"접속하거나 접근하려는 데이터베이스가 존재하지 않는다. 지정한 이름의 데이터베이스를 찾지 못했다.","cause":"연결 문자열의 데이터베이스 이름 오타. 데이터베이스가 아직 생성되지 않았거나 삭제됨. 대소문자 구분. 잘못된 서버에 접속. 환경(env) 구성의 잘못된 이름.","fix":"기존 데이터베이스를 나열한다: psql -l 또는 SELECT datname FROM pg_database. 연결 문자열의 데이터베이스 이름을 수정한다. 필요하면 생성한다: CREATE DATABASE database_name; 올바른 서버/포트에 접속하는지 확인한다. 애플리케이션 구성의 DB 이름을 확인한다.","tags":["database","database not found","catalog","does not exist","missing","connection","3D000"]},{"code":"3F000","title":"invalid_schema_name","severity":"caution","meaning":"참조한 스키마가 존재하지 않거나 search_path에서 찾을 수 없다. 지정한 이름의 스키마가 없다.","cause":"스키마 이름 오타. 스키마가 생성된 적 없음. search_path에 스키마가 포함되지 않음. 대소문자 차이. 스키마 접두어로 객체에 접근할 때 잘못된 접두어.","fix":"기존 스키마를 나열한다: \\dn 또는 SELECT schema_name FROM information_schema.schemata. 스키마 이름을 수정한다. 필요하면 생성한다: CREATE SCHEMA schema_name; search_path를 설정한다: SET search_path TO schema_name, public; 올바른 스키마 접두어로 객체에 접근한다.","tags":["schema","schema not found","search_path","does not exist","missing","invalid","3F000"]},{"code":"0A000","title":"feature_not_supported","severity":"caution","meaning":"사용하려는 기능이 이 컨텍스트 또는 이 PostgreSQL 버전에서 지원되지 않는다. 요청한 작업 자체는 유효할 수 있지만 현재 상황에서는 적용할 수 없다.","cause":"해당 버전에서 아직 제공되지 않는 구문/기능. 특정 컬럼/테이블 타입에 지원되지 않는 ALTER. foreign data wrapper(FDW)가 지원하지 않는 작업. logical replication/제약 조건 조합의 제한. 특정 데이터 타입에서 허용되지 않는 변환.","fix":"전체 오류 메시지를 읽는다. 대개 무엇이 지원되지 않는지 명확히 나와 있다. 대안 방식을 사용한다(예: 컬럼 타입을 직접 바꾸는 대신 새 컬럼 + 복사 + 이름 변경). 기능이 새 버전에서 추가되었다면 업그레이드를 검토한다. FDW/extension 제한에 걸렸다면 문서를 확인한다.","tags":["not supported","unsupported","feature","unsupported feature","version","upgrade","0A000"]},{"code":"57P03","title":"cannot_connect_now","severity":"danger","meaning":"서버가 지금은 연결을 받을 수 없다. 보통 데이터베이스가 아직 기동 중이거나 복구를 수행하고 있다. 시스템이 준비되면 연결을 열 수 있다.","cause":"PostgreSQL이 막 기동되어 아직 startup/recovery 단계에 있음. 크래시 복구. 백업에서 복원 중. standby가 아직 쿼리를 받을 준비가 되지 않음. 종료 진행 중.","fix":"잠시 기다렸다가 다시 접속한다. startup이 완료되면 서버가 연결을 받는다. 서버 로그에서 복구 진행 상황을 모니터링한다. 오래 걸린다면 대량의 WAL replay나 복구 문제가 있을 수 있다. 애플리케이션에 재시도 로직을 추가한다.","tags":["connection","connection refused","starting up","startup","recovery","crash recovery","57P03"]},{"code":"2BP01","title":"dependent_objects_still_exist","severity":"caution","meaning":"삭제하려는 객체에 다른 객체가 종속되어 있어 삭제가 차단되었다. 종속 객체가 존재하는 한 DROP은 실패한다.","cause":"뷰/외래 키/함수가 종속된 테이블을 삭제하려 함. 타입이나 스키마에 종속된 객체. CASCADE 없는 DROP. 다른 객체가 참조하는 컬럼/시퀀스.","fix":"종속 객체를 먼저 제거하거나 CASCADE로 삭제한다: DROP TABLE my_table CASCADE; (CASCADE는 종속 객체도 삭제한다는 점을 기억하고 주의한다). 종속성을 확인하려면 오류 상세(DETAIL 줄)를 읽거나 pg_depend/관련 카탈로그를 확인한다. 삭제 순서를 올바르게 계획한다.","tags":["dependency","dependent objects","cascade","drop","objects","object","2BP01"]},{"code":"55006","title":"object_in_use","severity":"danger","meaning":"대상 객체(보통 데이터베이스)를 다른 세션이 사용 중이어서 작업을 수행할 수 없다. 예를 들어 활성 연결이 있는 데이터베이스는 삭제할 수 없다.","cause":"DROP DATABASE 시 데이터베이스에 열린 연결이 있음. 사용 중인 객체에 배타적 락이 필요한 작업. 다른 세션이 객체를 보유.","fix":"대상 데이터베이스에 대한 열린 연결을 찾아 종료한다: SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'db_name' AND pid <> pg_backend_pid(); (필요하면) 새 연결을 차단한다. PostgreSQL 13+에서는 연결을 강제로 닫으면서 삭제한다: DROP DATABASE db_name WITH (FORCE);","tags":["in use","being accessed","object","object in use","drop database","connection","55006"]},{"code":"22007","title":"invalid_datetime_format","severity":"caution","meaning":"텍스트 값을 날짜/시간 타입(date, time, timestamp)으로 변환할 때 유효한 형식이 아니었다. 값이 예상 날짜-시간 형식과 맞지 않는다.","cause":"예상치 못한 형식의 날짜 텍스트(일/월/연 순서 혼동). 유효하지 않은 일/시 값. 빈 문자열('')을 날짜로 변환. 구성 요소 누락 또는 과잉. 로캘/형식 불일치. to_timestamp/to_date의 잘못된 format mask.","fix":"올바른 형식으로 날짜 텍스트를 변환한다: to_date('2026-07-04', 'YYYY-MM-DD') 또는 to_timestamp(str, 'YYYY-MM-DD HH24:MI:SS'). ISO 8601 형식(YYYY-MM-DD)을 우선한다. NULLIF(value,'')로 빈 값을 NULL로 바꾼다. 로드 전에 입력 데이터를 검증하고 문제 행의 범위를 좁힌다.","tags":["date","date format","datetime","format","timestamp","to_date","22007"]}]},"errors/mysql-more.json":{"engineId":"mysql","engine":"MySQL","order":3,"errors":[{"code":"1052","title":"Column 'x' in field list is ambiguous","severity":"caution","meaning":"쿼리의 여러 테이블에 같은 이름의 컬럼이 있고 테이블/alias 접두어 없이 사용되어, MySQL이 어느 테이블의 컬럼인지 알 수 없다.","cause":"조인된 두 테이블 모두에 같은 이름의 컬럼이 있음(예: 양쪽의 id). SELECT, WHERE, ON, GROUP BY 또는 ORDER BY에서 컬럼을 한정하지 않고(접두어 없이) 작성함.","fix":"모호한 모든 컬럼을 테이블 이름 또는 alias로 한정한다: SELECT a.id, b.id FROM a JOIN b ON a.id = b.a_id; 테이블에 짧은 alias를 붙이고 충돌하는 모든 컬럼에 접두어를 붙인다. 공통 컬럼 기준 조인에서는 USING(my_column)으로 모호성을 줄인다.","tags":["ambiguous","ambiguous column","column","column name","join","alias","1052"]},{"code":"1136","title":"Column count doesn't match value count","severity":"caution","meaning":"INSERT 문의 컬럼 수가 VALUES 목록의 값 수와 일치하지 않는다. 각 컬럼은 정확히 하나의 값을 가져야 한다.","cause":"INSERT의 컬럼 목록과 값 목록의 길이가 다름. 컬럼 목록 없는 INSERT에서 값이 테이블의 컬럼 수와 맞지 않음. 쉼표 누락/과잉. 테이블 구조는 변경되었지만 스크립트는 갱신되지 않음.","fix":"컬럼 수와 값 수를 같게 만든다. 항상 명시적 컬럼 목록을 사용한다: INSERT INTO my_table (a, b, c) VALUES (1, 2, 3); 테이블 구조를 확인한다: DESCRIBE my_table; 값을 컬럼과 일대일로 맞추고 과잉/누락된 쉼표를 수정한다.","tags":["column count","column count mismatch","insert","values","mismatch","value count","1136"]},{"code":"1048","title":"Column 'x' cannot be null","severity":"caution","meaning":"NOT NULL로 정의된 컬럼에 NULL을 삽입하거나 해당 컬럼을 NULL로 설정하려 했다. 필수 필드는 비워 둘 수 없다.","cause":"INSERT에서 필수 컬럼에 값을 지정하지 않음. UPDATE로 컬럼을 NULL로 설정. 컬럼에 DEFAULT 없음. 애플리케이션/서브쿼리에서 빈 값이 넘어옴. strict SQL 모드가 빈 값을 조용히 채우지 않음.","fix":"필수 컬럼에 유효한 값을 제공한다. 컬럼에 적절한 기본값을 정의한다: ALTER TABLE my_table MODIFY my_column INT NOT NULL DEFAULT 0; 규칙이 불필요하다면 NULL을 허용한다: ALTER TABLE my_table MODIFY my_column INT NULL; 빈 값은 전송 전에 애플리케이션에서 검증한다.","tags":["not null","null","empty value","insert","cannot be null","mandatory","1048"]},{"code":"1292","title":"Incorrect datetime value / truncated incorrect value","severity":"caution","meaning":"대상 컬럼 타입(특히 날짜/시간 또는 숫자)으로 변환할 때 값이 유효하지 않거나 형식이 잘못되었다. strict 모드에서 MySQL은 이를 오류로 거부한다.","cause":"'0000-00-00' 같은 유효하지 않은 날짜나 값. 잘못된 날짜 형식. 숫자 컬럼의 비숫자 텍스트. strict SQL 모드(STRICT_TRANS_TABLES) 활성화. 다른 character set/collation으로 인한 변환 문제.","fix":"값을 컬럼 타입에 맞는 유효한 형식으로 맞춘다(날짜는 'YYYY-MM-DD'). 비어 있거나 유효하지 않은 날짜 대신 NULL을 사용한다. 필요하면 STR_TO_DATE로 명시적 형식을 지정한다: STR_TO_DATE(str, '%Y-%m-%d'). 일시적인 허용이 필요하다면 sql_mode를 검토하되, 가급적 데이터를 수정한다. 범위를 좁혀 문제 행을 찾는다.","tags":["datetime","date","truncated","incorrect value","strict mode","format","1292"]},{"code":"1406","title":"Data too long for column","severity":"caution","meaning":"컬럼의 정의된 최대 길이보다 긴 값이 기록되었다. 예를 들어 20자 문자열은 VARCHAR(10) 컬럼에 들어가지 않는다.","cause":"데이터에 비해 너무 작은 컬럼 폭. 멀티바이트 문자 집합(utf8mb4)의 문자가 예상보다 많은 공간을 차지. 연결(concatenation) 결과가 길어짐. strict SQL 모드 활성화(그렇지 않으면 값이 조용히 잘린다).","fix":"컬럼을 넓힌다: ALTER TABLE my_table MODIFY my_column VARCHAR(255); 매우 긴 내용에는 TEXT 타입을 검토한다. 값을 줄여야 한다면 애플리케이션에서 또는 SUBSTRING으로 자른다. 넘치는 행을 찾는다: SELECT * FROM my_table WHERE CHAR_LENGTH(my_column) > 10; 입력 검증을 추가한다.","tags":["data too long","length","varchar","column","column length","truncate","1406"]},{"code":"1071","title":"Specified key was too long; max key length is 767 bytes","severity":"caution","meaning":"생성하려는 인덱스(또는 키)가 허용된 최대 바이트 길이를 초과했다. 특히 멀티바이트 문자 집합에서는 VARCHAR 인덱스가 이 한도에 빨리 도달한다.","cause":"utf8mb4(문자당 4바이트)의 넓은 VARCHAR 컬럼에 대한 인덱스. 오래된 InnoDB row format(REDUNDANT/COMPACT. 5.7 이하에서는 비활성화된 innodb_large_prefix도 해당). 다중 컬럼 복합 인덱스의 총 길이가 한도 초과. 큰 primary key.","fix":"인덱스 컬럼 길이를 줄이거나 prefix 인덱스를 사용한다: CREATE INDEX idx ON my_table (my_column(191)); DYNAMIC/COMPRESSED row format으로 3072바이트 한도로 전환한다(MySQL 8.0에서는 DYNAMIC이 기본값이다. innodb_large_prefix는 8.0에서 제거되었으며 5.7 이하에서만 필요하다). 필요하면 컬럼의 character set/길이를 검토한다.","tags":["index","index length","key too long","key","prefix","utf8mb4","1071"]},{"code":"1050","title":"Table already exists","severity":"caution","meaning":"생성하려는 테이블이 이미 존재한다. 같은 이름의 테이블이 있어 CREATE TABLE이 실패했다.","cause":"기존 테이블을 다시 생성. 스크립트를 두 번 실행. 마이그레이션 재적용. 다른 객체(뷰)가 같은 이름을 사용.","fix":"테이블이 없을 때만 생성하도록 IF NOT EXISTS를 사용한다: CREATE TABLE IF NOT EXISTS my_table (...); 정말로 다시 생성하려면 먼저 삭제한다: DROP TABLE IF EXISTS my_table; 기존 테이블을 나열한다: SHOW TABLES; 마이그레이션을 재실행해도 안전하게(멱등하게) 만든다.","tags":["table exists","table already exists","duplicate","duplicate table","create table","1050"]},{"code":"1051","title":"Unknown table 'x'","severity":"caution","meaning":"참조한 테이블이 존재하지 않는다. 삭제, 변경 또는 쿼리 중 지정한 이름의 테이블을 찾지 못했다.","cause":"테이블 이름 오타. 테이블이 생성된 적 없거나 이미 삭제됨. 잘못된 데이터베이스(스키마) 선택. 대소문자 구분(Linux에서는 테이블 이름이 대소문자를 구분한다). 존재하지 않는 테이블에 DROP TABLE.","fix":"기존 테이블을 나열하고(SHOW TABLES;) 올바른 데이터베이스를 선택한다: USE mydb; 테이블 이름과 대소문자를 수정한다. 삭제 시 오류를 피하려면: DROP TABLE IF EXISTS my_table; 다른 스키마에 있다면 schema.table로 한정한다.","tags":["unknown table","table not found","does not exist","drop","not exists","1051"]},{"code":"1091","title":"Can't DROP 'x'; check that column/key exists","severity":"caution","meaning":"ALTER TABLE로 삭제하려는 컬럼, 인덱스 또는 외래 키가 존재하지 않는다. 존재하지 않는 객체는 삭제할 수 없다.","cause":"DROP COLUMN/INDEX/FOREIGN KEY에 존재하지 않는 객체 이름 지정. 이름 오타. 객체가 이미 삭제됨. 외래 키 이름과 인덱스 이름 혼동. 마이그레이션 두 번째 실행.","fix":"객체의 실제 이름을 확인한다: SHOW CREATE TABLE my_table; 또는 SHOW INDEX FROM my_table; 올바른 이름으로 삭제한다. 외래 키는 symbol 이름을 사용한다: ALTER TABLE my_table DROP FOREIGN KEY fk_name; MySQL에는 DROP COLUMN IF EXISTS 구문이 없다(MariaDB에만 있다). 멱등한 마이그레이션에서는 먼저 information_schema.columns로 컬럼 존재 여부를 확인한다: SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'my_table' AND column_name = 'my_column';","tags":["cant drop","cannot drop","column","index","foreign key","alter","1091"]},{"code":"1093","title":"You can't specify target table 'x' for update in FROM clause","severity":"caution","meaning":"UPDATE 또는 DELETE 문에서 갱신/삭제 대상 테이블 자체를 서브쿼리(FROM 안)의 소스로 직접 사용할 수 없다. MySQL은 같은 테이블이 이런 방식으로 대상이자 소스가 되는 것을 허용하지 않는다.","cause":"UPDATE my_table ... WHERE id IN (SELECT ... FROM my_table ...)처럼 서브쿼리가 같은 대상 테이블을 직접 읽음. DELETE에서 같은 테이블을 참조하는 서브쿼리.","fix":"MySQL이 별도의 사본으로 취급하도록 서브쿼리를 derived table로 감싸고 새 alias를 붙인다: UPDATE my_table SET c = 1 WHERE id IN (SELECT id FROM (SELECT id FROM my_table WHERE condition) AS t); 또는 self-join으로 작성한다: UPDATE my_table a JOIN my_table b ON ... SET a.c = ...;","tags":["update","delete","target table","same table","subquery","derived table","1093"]},{"code":"1305","title":"PROCEDURE does not exist","severity":"caution","meaning":"호출한 저장 프로시저 또는 함수를 지정한 데이터베이스에서 찾지 못했다. 이 이름의 루틴이 정의되어 있지 않다.","cause":"프로시저 이름 오타. 프로시저가 다른 데이터베이스에 있거나 생성된 적 없음. USE로 잘못된 스키마 선택. 프로시저를 호출할 EXECUTE 권한 없음. 대소문자 구분.","fix":"프로시저가 존재하는지, 어느 스키마에 있는지 확인한다: SHOW PROCEDURE STATUS WHERE Db = 'mydb'; 올바른 데이터베이스를 선택하거나(USE mydb;) 완전 한정으로 호출한다: CALL mydb.my_procedure(...); 필요한 권한을 부여한다: GRANT EXECUTE ON PROCEDURE mydb.my_procedure TO app_user;","tags":["procedure","routine","does not exist","not found","stored procedure","call","1305"]},{"code":"1227","title":"Access denied; you need (at least one of) the PRIVILEGE","severity":"danger","meaning":"사용자에게 시도한 작업에 필요한 권한이 없다. 작업이 계정에 부여된 권한을 넘어서는 권리를 요구한다.","cause":"SUPER, PROCESS, RELOAD 또는 CREATE USER 같은 관리 권한 누락. 전역 뷰/테이블을 읽는 데 필요한 권한이 부여되지 않음. 상위 권한이 필요한 특정 작업(SET GLOBAL, binlog 접근).","fix":"현재 권한을 확인한다: SHOW GRANTS FOR CURRENT_USER(); 권한이 있는 계정으로 필요한 권한을 부여한다: GRANT PROCESS ON *.* TO 'app_user'@'host';(GRANT는 즉시 적용되며 FLUSH PRIVILEGES는 필요 없다). 가능하면 SUPER 대신 dynamic privilege(예: SYSTEM_VARIABLES_ADMIN)를 사용한다. 최소 권한 원칙을 따른다.","tags":["access denied","privilege","missing privilege","permission","grant","access","1227"]},{"code":"3730","title":"Cannot drop table referenced by a foreign key constraint","severity":"danger","meaning":"다른 테이블의 외래 키가 이 테이블을 참조하고 있어 삭제할 수 없다. 참조가 존재하는 한 DROP은 차단된다.","cause":"FK로 연결된 자식 테이블이 아직 존재하는데 부모 테이블을 삭제하려 함. 종속성 순서를 지키지 않음. 외래 키를 먼저 제거하지 않음.","fix":"먼저 참조하는 외래 키를 제거한 뒤(ALTER TABLE child_table DROP FOREIGN KEY fk_name;) 테이블을 삭제한다. 종속 테이블/제약 조건을 찾는다: SELECT * FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME = 'my_table'; 대량 작업에서는 SET FOREIGN_KEY_CHECKS = 0;을 일시적으로 사용할 수 있지만, 주의하고 작업 후 다시 1로 설정한다.","tags":["foreign key","fk","drop table","referenced","reference","constraint","3730"]},{"code":"2013","title":"Lost connection to MySQL server during query","severity":"danger","meaning":"쿼리 실행 중 클라이언트와 MySQL 서버 간 연결이 끊어졌다. 서버가 응답하거나 결과를 반환하기 전에 연결이 끊겼다.","cause":"net_read_timeout/net_write_timeout을 초과하는 장시간 쿼리. 메모리 부족(OOM)으로 서버가 쿼리/연결을 종료. 네트워크 단절. max_allowed_packet 초과. 서버 크래시 또는 재시작. 매우 큰 결과 세트.","fix":"네트워크 안정성과 서버 오류 로그를 확인하고 OOM이나 크래시가 있었는지 살핀다. 타임아웃을 늘린다: SET GLOBAL net_read_timeout = 120; 큰 쿼리는 배치로 분할한다. max_allowed_packet을 늘린다. 애플리케이션에 연결 재시도 로직을 추가한다. 긴 쿼리를 최적화한다.","tags":["lost connection","connection dropped","timeout","network","network outage","oom","2013"]},{"code":"1153","title":"Got a packet bigger than 'max_allowed_packet' bytes","severity":"danger","meaning":"클라이언트 또는 서버가 허용된 최대 패킷 크기(max_allowed_packet)보다 큰 데이터 패킷을 보내려 했다. 보통 큰 BLOB/TEXT 값이나 매우 큰 bulk INSERT에서 발생한다.","cause":"매우 큰 BLOB/TEXT 데이터 삽입. 거대한 단일 INSERT 또는 다중 행 bulk INSERT. 큰 결과를 반환하는 쿼리. 작은 max_allowed_packet 값. mysqldump로 큰 행 가져오기.","fix":"max_allowed_packet을 늘리고(서버와 클라이언트 모두) 다시 접속한다: SET GLOBAL max_allowed_packet = 67108864; (64MB). 영구 적용하려면 my.cnf의 [mysqld] 아래에 max_allowed_packet = 64M을 추가한다. 큰 bulk 작업은 작은 배치로 분할한다. 매우 큰 값을 정말 저장해야 하는지 검토한다.","tags":["max_allowed_packet","packet","packet size","blob","large data","insert","1153"]},{"code":"1969","title":"MariaDB: Query execution was interrupted (max_statement_time exceeded)","severity":"caution","meaning":"MariaDB 전용(ER_STATEMENT_TIMEOUT): max_statement_time으로 정의한 최대 실행 시간을 초과해 쿼리가 중단되었다. MySQL에는 이 코드가 없으며, MySQL의 대응 오류는 3024(ER_QUERY_TIMEOUT, max_execution_time)이다.","cause":"MariaDB에서 세션/전역 max_statement_time 한도 또는 SET STATEMENT max_statement_time=N FOR ...로 설정한 한도 초과. 비효율적인 실행 계획, 인덱스 누락, 대용량 데이터 스캔 또는 락 대기.","fix":"EXPLAIN으로 쿼리를 분석하고 인덱스 추가와 실행 계획 개선으로 속도를 높인다. 쿼리가 실제로 오래 실행되어야 한다면 MariaDB에서 한도를 높인다: SET SESSION max_statement_time = 60;(초) 또는 SET STATEMENT max_statement_time = 60 FOR SELECT ...; MySQL을 사용한다면 3024 항목을 참고한다(max_execution_time, 밀리초. MAX_EXECUTION_TIME 힌트). 큰 작업은 분할한다.","tags":["timeout","statement timeout","interrupted","query interrupted","max_execution_time","query","1969"]}]},"errors/mssql-more.json":{"engineId":"mssql","engine":"SQL Server","order":4,"errors":[{"code":"8134","title":"Divide by zero error encountered","severity":"caution","meaning":"산술 표현식의 제수가 0이어서 0으로 나누기 오류가 발생했다. SQL Server는 나눗셈을 완료할 수 없어 쿼리를 중단한다.","cause":"제수로 사용된 컬럼이나 표현식이 0으로 평가됨, 평균/비율 계산에서 분모가 0, NULLIF 없는 나눗셈, 또는 데이터의 예상치 못한 0 값.","fix":"NULLIF로 분모를 보호한다(예: value / NULLIF(denominator, 0)). 결과는 NULL이 되며 ISNULL/COALESCE로 기본값을 지정할 수 있다. CASE WHEN denominator = 0 THEN NULL ELSE value/denominator END를 사용한다. 나누기 전에 WHERE로 0 값을 걸러낸다. 필요하면 SET ARITHABORT/ANSI_WARNINGS 동작을 검토한다.","tags":["math","arithmetic","divide by zero","division by zero","8134"]},{"code":"245","title":"Conversion failed when converting the value to data type","severity":"caution","meaning":"값을 대상 데이터 타입으로 변환할 수 없었다. 예를 들어 문자열 값을 int나 date로 변환할 때 부적합한 내용 때문에 암시적/명시적 변환이 실패했다.","cause":"비숫자 텍스트를 int/decimal로 변환, 유효하지 않은 날짜 형식, 컬럼 비교에서의 암시적 타입 변환, WHERE 또는 JOIN 조건의 호환되지 않는 타입, 또는 잘못된 형식의 입력 데이터.","fix":"안전한 변환 함수를 사용한다: TRY_CAST 또는 TRY_CONVERT(실패 시 NULL 반환). ISNUMERIC/TRY_CONVERT로 문제 행을 식별한다. 컬럼 타입을 맞춰 암시적 변환을 제거한다. 입력을 검증하고 날짜에는 명시적 형식(예: CONVERT style)을 사용한다.","tags":["conversion","type conversion","cast","convert","data type","245"]},{"code":"515","title":"Cannot insert the value NULL into column","severity":"caution","meaning":"NOT NULL로 정의된 컬럼에 NULL 값을 삽입하거나 갱신하려 했다. 컬럼이 NULL을 허용하지 않아 작업이 거부되었다.","cause":"INSERT 문에서 필수 컬럼 누락, 소스 데이터의 누락/빈 필드, 기본값 없는 NOT NULL 컬럼, 또는 애플리케이션이 NULL을 전송.","fix":"필수 컬럼에 유효한 값을 제공한다. 적절한 컬럼에 DEFAULT 제약 조건을 추가한다. 소스 데이터의 누락 필드를 검증하고 ISNULL/COALESCE로 기본값을 지정한다. 컬럼이 실제로 필수일 필요가 없다면 NULL을 허용하도록 스키마를 검토한다.","tags":["null","not null","insert","constraint","constraint violation","515"]},{"code":"8115","title":"Arithmetic overflow error converting expression to data type","severity":"caution","meaning":"산술 연산 또는 변환이 생성한 값이 대상 데이터 타입이 담을 수 있는 범위를 초과했다. 숫자가 너무 커서 오버플로가 발생했다.","cause":"int 범위를 초과하는 합계/곱(SUM), 작은 타입의 컬럼에 큰 값 할당, 부족한 decimal precision/scale, 또는 IDENTITY 값이 타입 한도에 도달.","fix":"중간 계산에는 더 넓은 타입을 사용한다(예: CAST(my_column AS bigint)로 SUM). decimal precision/scale을 늘린다. 데이터에 맞게 컬럼 타입을 키운다. 오버플로 위험이 있는 표현식은 계산 전에 변환하고 입력 범위를 제한한다.","tags":["overflow","arithmetic overflow","arithmetic","math","int","bigint","8115"]},{"code":"512","title":"Subquery returned more than 1 value","severity":"caution","meaning":"단일 값을 기대하는 컨텍스트(=, <, >, 비교 또는 할당)에서 사용된 서브쿼리가 둘 이상의 행을 반환했다. SQL Server는 단일 스칼라 값을 기대했지만 여러 값을 만났다.","cause":"비교에서 단일 값을 반환한다고 가정한 서브쿼리가 여러 행을 반환, 고유하지 않은 매칭 조건, 누락된 필터, 또는 잘못 구성된 상관 쿼리.","fix":"서브쿼리를 단일 행으로 줄인다(예: TOP 1, MAX/MIN 또는 적절한 GROUP BY). 여러 값이 예상된다면 = 대신 IN 또는 EXISTS를 사용한다. JOIN으로 다시 작성한다. 서브쿼리가 왜 여러 행을 반환하는지 조사해 필터/키 조건을 수정한다.","tags":["subquery","scalar subquery","more than one","512","in","exists"]},{"code":"207","title":"Invalid column name","severity":"caution","meaning":"쿼리에서 참조한 컬럼 이름을 테이블/뷰에서 찾을 수 없다. 그 이름의 컬럼이 없거나 범위를 벗어났다.","cause":"철자/대소문자 오류, 잘못된 테이블 alias, 아직 추가되지 않았거나 이름이 바뀐 컬럼, 잘못된 스키마/테이블 컨텍스트, 또는 따옴표 대신 대괄호를 생략함.","fix":"컬럼 이름과 테이블을 확인한다(sys.columns 또는 Object Explorer 확인). 올바른 alias와 스키마를 사용한다. 새로 추가된 컬럼은 객체가 최신 상태인지 확인하고 필요하면 저장 프로시저를 재컴파일한다. 공백/특수 문자가 포함된 이름에는 대괄호를 사용한다.","tags":["column","column name","invalid column","invalid column name","alias","207"]},{"code":"156","title":"Incorrect syntax near the keyword","severity":"caution","meaning":"키워드 근처에 구문 오류가 있다. 예약어가 예상치 못한 위치에 사용되어 SQL Server가 문장을 파싱하지 못했다.","cause":"예약어(예: ORDER, USER, TABLE)를 대괄호 없이 컬럼/객체 이름으로 사용, 쉼표/괄호 누락, 잘못된 순서의 절, 또는 잘못 배치된 절(WHERE, GROUP BY).","fix":"오류가 가리키는 키워드 주변을 확인한다. 예약어를 개체 이름으로 사용하는 경우 대괄호로 묶는다(예: [User]). 누락된 쉼표, 괄호, 절 순서를 수정한다. 쿼리를 부분으로 나누어 구문을 테스트한다.","tags":["syntax","syntax error","keyword","reserved word","156"]},{"code":"2812","title":"Could not find stored procedure (저장 프로시저를 찾을 수 없음)","severity":"caution","meaning":"EXEC로 호출한 저장 프로시저를 현재 데이터베이스/스키마 범위에서 찾을 수 없다. 해당 이름의 프로시저가 존재하지 않는다.","cause":"프로시저 이름 오타, 스키마 접두사 누락 또는 오류(예: dbo. 생략), 프로시저가 다른 데이터베이스에 있음, 아직 생성되지 않음, 또는 권한 때문에 보이지 않음.","fix":"프로시저 이름과 스키마를 확인하고 호출 시 정규화된 전체 이름을 사용한다(예: EXEC dbo.ProcedureName). 올바른 데이터베이스에 있는지 확인한다(USE DatabaseName). 프로시저가 존재하는지 확인한다(sys.procedures / OBJECT_ID). 필요한 EXECUTE 권한을 부여하고 배포 스크립트가 실행되었는지 확인한다.","tags":["stored procedure","procedure","sp","not found","procedure not found","2812"]},{"code":"8623","title":"The query processor ran out of internal resources and could not produce a query plan (쿼리 프로세서 내부 리소스 부족으로 쿼리 계획 생성 불가)","severity":"danger","meaning":"쿼리가 지나치게 복잡하여 쿼리 프로세서가 내부 리소스를 모두 소진했고 실행 계획을 생성하지 못했다. 쿼리가 너무 크거나 복잡하여 최적화할 수 없다.","cause":"매우 긴 IN 목록이나 다수의 OR/UNION 절이 있는 쿼리, 과도한 JOIN 수, 자동 생성된 거대한 쿼리, 매개변수가 너무 많은 쿼리, 또는 지나치게 깊은 식 트리.","fix":"쿼리를 단순화한다. 긴 IN 목록은 임시 테이블/테이블 반환 매개변수로 옮기고, OR 조건은 UNION ALL이나 재작성으로 줄이며, 복잡한 쿼리는 부분으로 나눈다. JOIN 수를 줄인다. 자동 생성 쿼리는 배치로 나눈다. 필요 시 인덱스와 쿼리 설계를 검토한다.","tags":["query processor","internal resources","plan","complex query","8623","danger"]},{"code":"4104","title":"The multi-part identifier could not be bound (다중 부분 식별자를 바인딩할 수 없음)","severity":"caution","meaning":"점으로 구분된 다중 부분 식별자(예: table.column 또는 alias.column)를 확인할 수 없다. SQL Server가 이 이름을 쿼리 내 테이블/열에 바인딩하지 못했다.","cause":"잘못되었거나 정의되지 않은 테이블 별칭, FROM/JOIN에 없는 테이블 참조, 다른 테이블에 귀속된 열, 별칭 정의 후 실제 테이블 이름 사용, 또는 오타.","fix":"사용한 별칭이 FROM/JOIN 절에 정의되어 있는지 확인한다. 열을 올바른 테이블/별칭으로 한정한다. 테이블에 별칭을 지정했다면 모든 곳에서 실제 이름이 아닌 별칭을 사용한다. 누락된 테이블을 JOIN에 추가하고 이름을 확인한다.","tags":["identifier","multi-part","multi-part identifier","bind","alias","4104"]},{"code":"2714","title":"There is already an object named in the database (데이터베이스에 같은 이름의 개체가 이미 있음)","severity":"caution","meaning":"생성하려는 개체(테이블, 뷰, 프로시저 등)와 같은 이름의 개체가 데이터베이스에 이미 존재한다. 이름 충돌로 CREATE 문이 실패했다.","cause":"같은 CREATE 스크립트를 두 번 실행, 기존 개체를 먼저 삭제하지 않음, 다른 스키마를 전제로 함, 멱등성이 없는 배포/마이그레이션 스크립트, 또는 이름 충돌.","fix":"먼저 개체 존재 여부를 확인한다. IF OBJECT_ID('dbo.Name') IS NULL로 조건부 생성하거나 CREATE OR ALTER를 사용한다(프로시저/뷰/함수). 필요하면 기존 개체를 DROP하고 다시 생성한다. 마이그레이션 스크립트는 멱등하게 작성한다. 올바른 스키마/데이터베이스에서 작업 중인지 확인한다.","tags":["already exists","object exists","create","object","duplicate object","2714"]},{"code":"601","title":"Could not continue scan with NOLOCK due to data movement (데이터 이동으로 NOLOCK 스캔을 계속할 수 없음)","severity":"caution","meaning":"NOLOCK(READ UNCOMMITTED) 스캔 중 동시 데이터 이동(페이지 분할/이동)으로 인해 스캔을 계속할 수 없었다. 락 없는 읽기가 일관되지 않은 상태에 도달했다.","cause":"NOLOCK 힌트 또는 READ UNCOMMITTED 격리 수준으로 읽는 동안 동시 업데이트/삭제/페이지 분할로 스캔 대상 페이지가 재배치됨.","fix":"쿼리를 다시 실행한다(대개 일시적이다). 가능하면 NOLOCK 힌트를 피하고, 차단 없이 일관된 읽기를 위해 READ COMMITTED SNAPSHOT(RCSI) 격리를 사용한다. 쓰기가 많은 테이블에서는 더티 리드보다 스냅샷 격리를 우선한다.","tags":["nolock","scan","data movement","601","isolation","read uncommitted"]},{"code":"50000","title":"Custom error raised with RAISERROR (RAISERROR로 발생시킨 사용자 정의 오류)","severity":"caution","meaning":"메시지 ID를 지정하지 않고 RAISERROR로 발생시킨 사용자 정의 오류의 기본 번호(50000)이다. 애플리케이션/프로시저 로직이 의도적으로 오류를 발생시켰다.","cause":"비즈니스 규칙 위반, 유효성 검사 실패, 의도적인 흐름 제어 또는 사용자 정의 예외 메시지를 위해 저장 프로시저나 스크립트에서 RAISERROR 또는 THROW로 발생시킨 사용자 정의 오류.","fix":"RAISERROR/THROW 메시지 텍스트를 읽어 실제 비즈니스 규칙 위반을 파악한다. 애플리케이션 측에서 TRY...CATCH로 이 오류를 잡고 ERROR_MESSAGE()/ERROR_NUMBER()로 세부 정보를 얻는다. sp_addmessage로 메시지를 정의하여 사용자 정의 오류에 의미 있는 번호를 부여한다. 비즈니스 로직에서 원인 조건을 수정한다.","tags":["raiserror","custom error","user-defined error","throw","50000","try catch"]},{"code":"3902","title":"The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION (대응하는 BEGIN TRANSACTION이 없는 COMMIT TRANSACTION)","severity":"caution","meaning":"COMMIT TRANSACTION이 실행되었으나 대응하는 열린 BEGIN TRANSACTION이 없다. 트랜잭션 수(@@TRANCOUNT)가 0이므로 커밋할 트랜잭션을 찾지 못했다.","cause":"BEGIN/COMMIT/ROLLBACK 사용의 불균형, 오류 후 트랜잭션이 이미 롤백됨, 중첩 트랜잭션 수의 잘못된 관리, 또는 CATCH 블록에서 ROLLBACK 후 COMMIT을 다시 호출함.","fix":"BEGIN과 COMMIT 문의 수를 맞춘다. 커밋 전에 @@TRANCOUNT를 확인한다(IF @@TRANCOUNT > 0 COMMIT). TRY...CATCH에서는 오류 시 XACT_STATE()/@@TRANCOUNT에 따라 ROLLBACK하고 불필요한 COMMIT을 방지한다. 중첩 트랜잭션 로직을 검토한다.","tags":["transaction","commit","begin","trancount","transaction count","3902"]}]},"errors/mongodb-more.json":{"engineId":"mongodb","engine":"MongoDB","order":5,"errors":[{"code":"121","title":"DocumentValidationFailure","severity":"caution","meaning":"삽입 또는 업데이트하려는 문서가 컬렉션에 정의된 JSON Schema 유효성 검사 규칙(validator)을 충족하지 않아 거부되었다.","cause":"문서가 스키마를 따르지 않음: 필수 필드 누락, 잘못된 BSON 타입, 허용 범위/패턴을 벗어난 값, 추가 필드 금지, 또는 validationAction이 'error'인 상태에서 규칙 위반.","fix":"오류 세부 정보(errInfo)를 확인하여 어떤 규칙을 위반했는지 파악한다. 필수 필드를 추가하고 타입을 수정하여 문서를 스키마에 맞춘다. collMod로 컬렉션 validator를 검토한다. validationLevel/validationAction을 일시적으로 완화할 수 있으나 데이터 품질을 위해 규칙을 유지하는 것이 바람직하다.","tags":["validation","validation","schema","schema","121","document"]},{"code":"40","title":"ConflictingUpdateOperators","severity":"caution","meaning":"업데이트가 같은 필드(경로)에 서로 충돌하는 업데이트 연산자를 둘 이상 적용했다. MongoDB는 한 작업에서 같은 경로를 두 번 수정할 수 없다.","cause":"한 업데이트 문서에서 같은 필드를 대상으로 하는 충돌 연산자(예: $set과 $inc를 함께 사용하거나 부모 경로와 자식 경로를 동시에 수정), 잘못 구성된 업데이트, 또는 중첩 경로 충돌.","fix":"같은 필드에 대한 충돌 연산자를 하나로 합치거나 업데이트를 별도 작업으로 나눈다. 부모 경로와 자식 경로(예: a와 a.b)를 동시에 수정하지 않는다. 업데이트 문서를 검토하여 각 필드가 한 번만 수정되도록 한다.","tags":["update","update","operator","conflict","conflict","40"]},{"code":"66","title":"ImmutableField (cannot update _id)","severity":"caution","meaning":"변경할 수 없는 필드, 일반적으로 문서의 _id 필드를 업데이트하려 했다. _id는 문서 생성 후 변경할 수 없다.","cause":"_id 필드를 변경하려는 업데이트, 허용되지 않는 방식으로 샤드 키 필드를 업데이트, 또는 변경 불가로 취급되는 필드에 새 값을 설정.","fix":"_id 값을 업데이트하지 않는다. 다른 _id가 필요하면 새 _id로 문서를 삽입하고 이전 문서를 삭제한다(insert + delete). 샤드 키 업데이트는 MongoDB 규칙을 따른다. 업데이트 문서에서 변경 불가 필드를 제거하고 수정 가능한 필드만 설정한다.","tags":["immutable","_id","immutable","update","shard key","66"]},{"code":"2","title":"BadValue","severity":"caution","meaning":"명령 또는 쿼리에 유효하지 않거나 예상치 못한 값이 전달되었다. MongoDB가 인수를 허용 가능한 값으로 해석할 수 없었다.","cause":"유효하지 않은 연산자 인수, 범위를 벗어난 숫자, 지원되지 않는 옵션 값, 잘못된 형식의 정렬 지정, 잘못된 형식의 쿼리 식, 또는 부적절한 매개변수 조합.","fix":"오류 메시지의 필드/값 힌트를 바탕으로 인수를 수정한다. 명령 및 쿼리 옵션을 문서와 대조하여 검증한다. sort/projection/aggregate 식의 형식을 확인한다. 값의 범위와 타입을 유효 범위로 맞춘다.","tags":["badvalue","invalid value","argument","2","query"]},{"code":"14","title":"TypeMismatch","severity":"caution","meaning":"제공된 값의 타입이 예상 BSON 타입과 일치하지 않는다. 필드 또는 인수에 다른 데이터 타입이 예상되었다.","cause":"숫자가 필요한 곳에 텍스트 제공, 날짜 대신 문자열 사용, 배열이 필요한 곳에 단일 값 전송, aggregate/쿼리 연산자에 잘못된 타입의 인수 전달, 또는 스키마의 타입 불일치.","fix":"필드/인수에 올바른 BSON 타입을 제공한다. 데이터 저장 전 애플리케이션 계층에서 타입을 변환한다(예: 숫자는 실제 Number 값으로 기록). 쿼리 및 aggregate 연산자가 기대하는 타입을 확인한다. 스키마 validator로 타입 일관성을 강제한다.","tags":["typemismatch","type mismatch","bson","type","14"]},{"code":"10334","title":"BSONObjectTooLarge","severity":"danger","meaning":"BSON 문서가 허용 최대 크기(16 MB)를 초과했다. MongoDB는 이 한도를 넘는 단일 문서를 저장하거나 반환할 수 없다.","cause":"문서에 데이터/배열 요소를 계속 추가하여 16 MB 한도 초과, 매우 큰 내장 배열, 집계로 생성된 거대한 문서, 또는 문서에 직접 저장된 대용량 바이너리 데이터.","fix":"데이터 모델을 검토한다. 큰 배열은 별도 컬렉션으로 분리하고 참조 모델을 사용한다. 대용량 바이너리 파일에는 GridFS를 사용한다. 무제한 증가하는 배열에 상한을 둔다(예: capped/bucket 패턴). 집계 스테이지에서 문서 크기를 관리한다.","tags":["bson","16mb","too large","large document","gridfs","10334","danger"]},{"code":"46","title":"LockBusy","severity":"caution","meaning":"요청한 락 또는 리소스를 다른 작업이 보유하고 있어 즉시 획득할 수 없었고, 작업은 대기하지 않고 거부되었다. 타임아웃으로 끝나는 락 대기는 별도 코드(24 LockTimeout)이다.","cause":"같은 컬렉션 또는 데이터베이스에 대한 동시 관리/DDL 작업(예: 인덱스 빌드 두 개 동시 실행, collMod, drop 또는 rename), 진행 중인 유지보수 작업과의 충돌, 또는 락을 잠시도 기다릴 수 없는 내부 작업이 사용 중인 리소스에 부딪힘.","fix":"충돌 작업을 찾는다. db.currentOp()로 같은 네임스페이스에서 실행 중인 DDL 또는 유지보수 작업을 확인한다. 충돌 작업이 끝난 후 명령을 재시도한다. 관리/DDL 작업은 순차적으로 실행하고 같은 컬렉션에서 병렬로 실행하지 않는다. 지속되면 mongod 로그에서 리소스를 보유한 작업을 조사한다.","tags":["lock","lock","lockbusy","busy","ddl","46"]},{"code":"96","title":"OperationFailed","severity":"danger","meaning":"일반적인 작업 실패이다. 명령 실행 중 예기치 않은 오류가 발생하여 작업을 완료할 수 없었다. 더 구체적인 코드가 없을 때 사용되는 일반 오류이다.","cause":"서버 측 내부 오류, 리소스 고갈, 일관되지 않은 상태, 하위 시스템 장애(스토리지, 복제), 또는 특정 명령에 고유한 다양한 실패 조건.","fix":"전체 오류 메시지 텍스트와 서버 로그(mongod.log)를 검토하여 근본 원인을 파악한다. 조건이 일시적일 수 있으므로 작업을 재시도한다. 리소스(디스크, 메모리)와 replica set 상태를 확인한다. 문제가 반복되면 명령과 입력 데이터를 검토한다.","tags":["operationfailed","operation failed","generic error","96","danger"]},{"code":"72","title":"InvalidOptions","severity":"caution","meaning":"명령에 유효하지 않거나 서로 호환되지 않는 옵션이 전달되었다. 지정한 옵션 조합은 이 명령에서 지원되지 않는다.","cause":"지원되지 않는 옵션 이름, 상호 배타적 옵션의 동시 사용, 실행 중인 버전에서 유효하지 않은 옵션, 유효하지 않은 write concern/read concern 조합, 또는 인덱스/collation 옵션 충돌.","fix":"문서에서 명령이 허용하는 옵션을 확인하고 호환되지 않는 옵션을 제거한다. 사용 중인 버전이 해당 옵션을 지원하는지 확인한다. write/read concern과 collation 설정을 검토한다. 유효한 옵션 조합만 사용한다.","tags":["invalidoptions","invalid option","options","72","command"]},{"code":"9","title":"FailedToParse","severity":"caution","meaning":"서버가 제출된 명령, 쿼리 또는 식을 파싱할 수 없었다. 구조가 구문상 유효하지 않거나 예상 형식이 아니다.","cause":"잘못된 형식의 JSON/BSON 구조, 누락되거나 불필요한 필드, 잘못된 연산자 사용, 유효하지 않은 aggregation pipeline 구문, 또는 예상 형식과 맞지 않는 명령 문서.","fix":"문서에 따라 명령/쿼리/파이프라인 구조를 수정한다. JSON 구조를 검증한다(괄호/쉼표 누락, 잘못된 필드 이름). 집계 스테이지의 구문을 확인한다. 연산자를 올바르게 사용하고 인수를 예상 형식으로 제공하는지 확인한다.","tags":["failedtoparse","parse","parsing","syntax","9","query"]},{"code":"134","title":"ReadConcernMajorityNotAvailableYet","severity":"caution","meaning":"majority read concern으로 읽기를 요청했으나 과반수 커밋된 스냅샷을 아직 사용할 수 없다. 서버가 현재 이 일관성 수준을 제공할 수 없다.","cause":"replica set가 막 시작되었거나 불안정함, 과반수 쓰기가 아직 커밋되지 않음, 복제 지연, 연결 가능한 멤버 부족, 또는 클러스터가 아직 시작 단계에 있음.","fix":"잠시 기다린 후 재시도한다. 과반수 스냅샷을 사용할 수 있게 되면 읽기가 성공한다. replica set 상태와 복제 지연을 확인한다(rs.status()). 충분한 멤버가 실행 중인지 확인한다. 필수가 아니라면 더 낮은 read concern(예: local)을 고려한다.","tags":["readconcern","majority","majority","replica","134","consistency"]},{"code":"202","title":"NetworkInterfaceExceededTimeLimit","severity":"danger","meaning":"네트워크 요청(예: 멤버 간 통신 또는 클라이언트 연결)이 시간 제한을 초과했다. 네트워크 타임아웃으로 작업을 완료할 수 없었다.","cause":"네트워크 지연/장애, 느리거나 연결할 수 없는 대상 멤버, 과도한 부하, 잘못 구성된 타임아웃 값, DNS/연결 문제, 또는 지리적으로 멀리 떨어진 멤버.","fix":"네트워크 연결과 지연을 확인하고 멤버 간 연결 가능 여부를 검증한다. 대상 멤버의 상태와 부하를 확인한다. 환경에 맞게 타임아웃 설정(connect/socket timeout)을 구성한다. 재시도 로직을 추가한다. 지속되면 인프라/네트워크 구성을 검토한다.","tags":["network","network","timeout","timeout","202","danger"]}]},"errors/redis-more.json":{"engineId":"redis","engine":"Redis","order":6,"errors":[{"code":"NOPERM","title":"NOPERM this user has no permissions to run the command or access one of the keys (명령 실행 또는 키 접근 권한 없음)","severity":"caution","meaning":"인증된 ACL 사용자가 실행하려는 명령 또는 접근하는 키에 대한 권한이 없다. Redis가 ACL 규칙에 따라 작업을 거부한다.","cause":"ACL 사용자에게 명령/카테고리 권한이 없음, 키 패턴이 허용 목록에 없음, 채널(pub/sub) 제한 적용, 또는 사용자에게 좁은 범위의 권한만 부여됨.","fix":"사용자 권한을 확인한다: ACL GETUSER app_user. 필요한 명령/카테고리와 키 패턴을 추가한다(예: ACL SETUSER app_user +get +set ~app:*). 최소 권한 원칙에 따라 필요한 권한만 부여한다. 올바른 사용자로 연결되었는지 확인한다.","tags":["noperm","acl","privilege","permission denied","permission","user"]},{"code":"ERR value is not an integer or out of range","title":"ERR value is not an integer or out of range (값이 정수가 아니거나 범위를 벗어남)","severity":"caution","meaning":"정수를 기대하는 명령(예: INCR, EXPIRE, LPOP count)이 숫자가 아니거나 범위를 벗어난 값을 받았다. Redis가 값을 정수로 해석할 수 없었다.","cause":"INCR/DECR 대상 키가 텍스트(정수가 아닌) 값을 보유, 숫자 대신 텍스트로 주어진 인수, 범위에 비해 너무 크거나 작은 숫자, 또는 허용되지 않는 음수 값.","fix":"키 값을 확인한다: GET mykey 및 TYPE mykey. 숫자 연산의 경우 키가 정수 값을 보유하는지 확인한다. 인수가 올바른 범위 내의 유효한 정수인지 확인한다. 필요하면 값을 재설정하거나(SET mykey 0) 올바른 타입으로 다시 생성한다.","tags":["err","integer","not an integer","range","incr","value"]},{"code":"ERR value is not a valid float","title":"ERR value is not a valid float (값이 유효한 부동소수점 수가 아님)","severity":"caution","meaning":"소수(float)를 기대하는 명령(예: INCRBYFLOAT, ZADD 점수)이 유효한 부동소수점 수가 아닌 값을 받았다. Redis가 값을 float로 파싱할 수 없었다.","cause":"INCRBYFLOAT 대상 키가 유효한 float를 보유하지 않음, 숫자가 아닌 인수, 잘못된 소수 구분 기호(쉼표가 아닌 점이 필요), 또는 잘못된 형식의 숫자.","fix":"GET으로 키 값을 확인하고 유효한 float를 포함하는지 확인한다. 소수 구분 기호로 점을 사용한다(예: 3.14). 인수가 숫자이며 올바른 형식인지 확인한다. 필요하면 값을 유효한 float로 재설정한다.","tags":["err","float","decimal","incrbyfloat","zadd","value"]},{"code":"ERR increment or decrement would overflow","title":"ERR increment or decrement would overflow (증가 또는 감소 시 오버플로 발생)","severity":"caution","meaning":"결과가 64비트 부호 있는 정수 범위를 넘어 오버플로되므로 INCR/DECR(또는 INCRBY/DECRBY)이 거부되었다. 값이 상한 또는 하한을 초과하게 된다.","cause":"키 값이 int64 한계(약 922경)에 매우 근접하여 증가/감소 시 오버플로, 매우 큰 INCRBY 인수, 또는 통제 없이 증가하는 카운터.","fix":"카운터 값이 int64 범위 내에 머물도록 설계한다. 큰 카운트에는 스케일링이나 주기적 재설정을 적용한다. 증가량(INCRBY 인수)을 합리적인 범위로 유지한다. 매우 큰 값이 필요하면 애플리케이션 계층에서 다른 카운팅 전략을 사용한다.","tags":["err","overflow","integer overflow","incr","decr","int64"]},{"code":"ERR max number of clients reached","title":"ERR max number of clients reached (최대 클라이언트 수 도달)","severity":"danger","meaning":"연결된 클라이언트 수가 maxclients 한도에 도달하여 새 연결을 받을 수 없다. Redis가 추가 클라이언트 연결을 거부한다.","cause":"연결 누수(닫히지 않는 연결), 부족한 maxclients 값, 잘못 구성된 커넥션 풀, 클라이언트의 급격한 증가, 또는 낮은 운영 체제 파일 디스크립터 한도.","fix":"현재 연결을 확인한다: CLIENT LIST 및 INFO clients. 연결 누수를 수정하고 클라이언트 풀 크기를 올바르게 설정하며 연결을 재사용한다. 필요하면 maxclients(CONFIG SET maxclients)와 OS ulimit(파일 디스크립터) 한도를 높인다. 유휴 연결에 timeout을 설정한다.","tags":["err","maxclients","connection","client","limit","danger"]},{"code":"BUSYKEY","title":"BUSYKEY Target key name already exists (대상 키 이름이 이미 존재함)","severity":"caution","meaning":"대상 키가 이미 존재하여 RESTORE(또는 유사 명령)가 실패했다. 기본적으로 Redis는 기존 키를 덮어쓰지 않는다.","cause":"RESTORE로 복원하려는 키 이름이 데이터베이스에 이미 존재, 이전 데이터가 정리되지 않음, 또는 같은 키에 대한 여러 번의 복원 시도.","fix":"기존 키를 덮어쓰려면 RESTORE에 REPLACE 옵션을 추가한다. 또는 대상 키를 먼저 삭제하거나(DEL mykey) 다른 키 이름을 사용한다. 사전에 EXISTS mykey로 존재 여부를 확인한다.","tags":["busykey","restore","already exists","target key","replace","key"]},{"code":"ERR no such key","title":"ERR no such key","severity":"caution","meaning":"작업할 키를 데이터베이스에서 찾을 수 없다. RENAME, DUMP, MOVE처럼 기존 키가 필요한 명령은 존재하지 않는 키에서 동작하지 않는다.","cause":"키가 생성된 적 없음, TTL로 만료되어 제거됨, 잘못된 키 이름/철자, 잘못된 데이터베이스(SELECT) 인덱스, 또는 이미 DEL로 삭제됨.","fix":"키 존재 여부를 확인한다: EXISTS mykey. 올바른 키 이름과 올바른 데이터베이스(SELECT db)를 사용하는지 확인한다. RENAME은 원본 키가 존재해야 한다. TTL로 만료되는 키에 대한 로직을 검토한다. 필요하면 키를 다시 생성한다.","tags":["err","no such key","key not found","rename","dump","key"]},{"code":"ERR DUMP payload version or checksum are wrong","title":"ERR DUMP payload version or checksum are wrong (DUMP 페이로드 버전 또는 체크섬 오류)","severity":"caution","meaning":"RESTORE에 전달된 직렬화 데이터(DUMP 출력)가 유효하지 않다. 버전이 호환되지 않거나 체크섬 검증에 실패했다. Redis가 데이터를 안전하게 복원할 수 없다.","cause":"DUMP 데이터 손상/변경, 호환되지 않는 서로 다른 Redis 버전 간 이동, 인코딩/디코딩 중 손상(예: 잘못된 바이트 처리), 또는 누락되거나 잘린 페이로드.","fix":"DUMP 데이터가 손상 없이(바이너리 안전하게) 전송되는지 확인한다. 텍스트 인코딩을 거치지 말고 원시 바이트로 전송한다. 원본과 대상 Redis 버전이 호환되는지 확인한다. 새로 DUMP를 받아 다시 전송한다. 마이그레이션에는 RESTORE 대신 MIGRATE 명령을 고려한다.","tags":["err","dump","restore","checksum","checksum mismatch","version"]},{"code":"NOREPLICAS","title":"NOREPLICAS Not enough good replicas to write (쓰기에 필요한 정상 replica 부족)","severity":"danger","meaning":"min-replicas-to-write 설정 때문에 정상 replica가 충분하지 않아 쓰기가 거부된다. Redis가 데이터 안전을 위해 쓰기를 차단한다.","cause":"연결되었거나 최신 상태인 replica 부족, replica 장애, min-replicas-max-lag 한도를 넘는 복제 지연, 네트워크 문제, 또는 페일오버 후 replica 누락.","fix":"replica 상태를 확인한다: INFO replication. 중단된 replica를 다시 가동하고 복제 지연을 줄인다. 네트워크 연결을 수정한다. 환경에 맞게 min-replicas-to-write / min-replicas-max-lag 설정을 검토한다(내구성과 가용성 간의 균형). 충분한 replica를 확보한다.","tags":["noreplicas","replica","write","min-replicas","replication","danger"]},{"code":"ERR This instance has cluster support disabled","title":"ERR This instance has cluster support disabled (이 인스턴스는 클러스터 지원이 비활성화됨)","severity":"caution","meaning":"클러스터 명령(CLUSTER ...)을 실행했으나 이 Redis 인스턴스는 클러스터 모드로 시작되지 않았다. 클러스터 지원이 비활성화되어 명령이 거부되었다.","cause":"cluster-enabled yes 미설정, 인스턴스가 standalone 모드로 실행 중, standalone 서버에 클러스터 명령 전송, 또는 잘못된 서버에 연결.","fix":"클러스터 기능을 사용하려면 인스턴스를 클러스터 모드로 시작한다. 구성에 cluster-enabled yes를 설정하고 재시작한 후 노드로 클러스터를 구성한다. standalone 모드를 유지한다면 클러스터 명령을 사용하지 않는다. 올바른(클러스터) 서버에 연결되었는지 확인한다.","tags":["err","cluster","cluster mode","disabled","standalone","config"]},{"code":"ERR unknown subcommand","title":"ERR unknown subcommand or wrong number of arguments (알 수 없는 하위 명령 또는 잘못된 인수 개수)","severity":"caution","meaning":"컨테이너 명령(예: CONFIG, CLIENT, ACL, XINFO)이 유효하지 않은 하위 명령을 받았거나 하위 명령에 잘못된 개수의 인수가 전달되었다. Redis가 하위 명령을 인식하지 못했다.","cause":"하위 명령 이름 오타, 사용 중인 Redis 버전에 없는 하위 명령, 잘못되었거나 누락된 인수 개수, 또는 잘못된 하위 명령 구문.","fix":"올바른 하위 명령 이름과 인수를 사용한다. 명령의 HELP 하위 명령을 참조한다(예: CONFIG HELP, CLIENT HELP). 사용 중인 Redis 버전이 해당 하위 명령을 지원하는지 확인한다. 문서에 따라 인수 개수와 구문을 수정한다.","tags":["err","subcommand","unknown subcommand","argument","config","client"]},{"code":"ERR Client sent AUTH, but no password is set","title":"ERR AUTH <password> called without any password configured for the default user (Redis 6+) / ERR Client sent AUTH, but no password is set (Redis <6) (비밀번호가 설정되지 않은 상태에서 AUTH 호출)","severity":"caution","meaning":"클라이언트가 AUTH로 인증을 시도했으나 서버에 비밀번호(requirepass) 또는 ACL 비밀번호가 정의되어 있지 않다. 검증할 자격 증명이 없어 AUTH가 거부되었다. 이 경우 Redis 6+는 \"ERR AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?\"를 반환하며, \"Client sent AUTH, but no password is set\"은 Redis 6 이전의 메시지이다.","cause":"서버의 requirepass가 비어 있음, 클라이언트가 불필요하게 AUTH 전송, 서버에 비밀번호가 없는데 연결 문자열에 비밀번호 정의, 또는 구성 불일치.","fix":"서버가 비밀번호를 요구하지 않는다면 클라이언트에서 AUTH 명령/비밀번호 설정을 제거한다. 비밀번호 보호가 필요하면 서버에 requirepass를 설정하거나(CONFIG SET requirepass password) ACL 사용자에 비밀번호를 정의한다. 클라이언트와 서버 구성을 일치시킨다.","tags":["err","auth","password","requirepass","authentication","acl"]}]},"errors/elasticsearch-more.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":7,"errors":[{"code":"action_request_validation_exception","title":"action_request_validation_exception","severity":"caution","meaning":"요청이 실행 전 유효성 검사에 실패했다. 필수 필드가 누락되었거나 필드가 유효하지 않아 Elasticsearch가 작업을 거부한다.","cause":"필수 매개변수 누락(예: 인덱스 이름 미지정), 빈 본문, 유효하지 않은 필드 조합, 또는 bulk/index 요청에 필수 정보 미제공.","fix":"오류 메시지의 누락/유효하지 않은 필드 목록을 검토하고 필수 필드를 제공한다. API 문서에 따라 요청 본문과 매개변수를 완성한다. bulk 요청에서는 각 액션 행에 올바른 메타데이터와 본문이 있는지 확인한다.","tags":["validation","validation error","request","request error","action"]},{"code":"index_closed_exception","title":"index_closed_exception","severity":"caution","meaning":"대상 인덱스가 닫혀 있다. 닫힌 인덱스는 읽기/쓰기 요청을 처리하지 않으므로 작업을 수행할 수 없었다.","cause":"인덱스가 이전에 close API로 닫힘, 유지보수/ILM 작업 중 닫힘, 또는 리소스 절약을 위해 비활성화됨.","fix":"인덱스를 다시 연다: POST /index_name/_open. 연 후에는 샤드가 할당될 때까지(green/yellow) 기다린다. 인덱스가 닫힌 이유(ILM 정책, 수동 작업)를 조사한다. 거의 접근하지 않는 인덱스를 닫아 두는 전략을 검토한다.","tags":["index closed","closed index","open","index","closed"]},{"code":"unavailable_shards_exception","title":"unavailable_shards_exception","severity":"danger","meaning":"작업에 필요한 하나 이상의 샤드를 사용할 수 없어 요청(대개 쓰기)을 완료할 수 없었다. 충분한 활성 샤드 복사본을 찾지 못했다.","cause":"노드 장애/이탈로 인한 미할당 샤드, 디스크 워터마크 초과, replica 누락, 충족되지 않은 wait_for_active_shards 조건, 또는 red 클러스터 상태.","fix":"클러스터 및 샤드 상태를 확인한다: GET _cluster/health 및 GET _cat/shards?v. 샤드가 미할당된 이유를 조사한다: GET _cluster/allocation/explain. 중단된 노드를 다시 가동하고 디스크 공간을 확보하며(워터마크) replica 설정을 검토한다. 필요하면 wait_for_active_shards를 적절히 설정한다.","tags":["shard","unavailable","inaccessible","cluster","allocation","danger"]},{"code":"query_shard_exception","title":"query_shard_exception","severity":"caution","meaning":"특정 샤드에서 쿼리 실행 중 실패했다. 대개 쿼리가 매핑과 호환되지 않거나 구조가 유효하지 않아 발생한다.","cause":"존재하지 않거나 잘못된 타입의 필드를 쿼리, text 필드에 대한 집계/정렬 시도(fielddata 비활성화), 유효하지 않은 쿼리 구문, 잘못된 분석, 또는 호환되지 않는 필드 타입.","fix":"오류 세부 정보의 field/reason을 검토한다. 쿼리한 필드의 매핑을 확인한다: GET /index/_mapping. 정렬/집계에는 text 대신 keyword 하위 필드(field.keyword)를 사용한다. 쿼리 구문을 수정하고 필드 타입이 쿼리와 호환되는지 확인한다.","tags":["query","query error","shard","mapping","fielddata","query shard"]},{"code":"script_exception","title":"script_exception","severity":"caution","meaning":"스크립트(대개 Painless)를 컴파일하거나 실행하는 중 오류가 발생했다. 스크립트가 잘못 작성되었거나 런타임에 예외를 발생시켰다.","cause":"Painless 구문 오류, null 값 접근, 타입 불일치, 존재하지 않는 필드/변수 사용, 스크립트 컴파일 한도 초과, 또는 허용되지 않는 API 호출.","fix":"오류 메시지의 컴파일/런타임 세부 정보를 검토한다. 스크립트 구문과 필드 접근을 수정하고 null 검사를 추가한다(예: doc['field'].size() > 0). 자주 사용하는 스크립트는 매개변수화하여(stored/params) 컴파일 한도를 넘지 않도록 한다. 올바른 필드 이름과 타입을 사용한다.","tags":["script","painless","compilation","runtime","script exception"]},{"code":"number_format_exception","title":"number_format_exception","severity":"caution","meaning":"숫자여야 하는 값을 숫자로 변환할 수 없었다. 필드 또는 매개변수가 숫자를 기대하는 곳에 유효하지 않거나 숫자가 아닌 문자열이 제공되었다.","cause":"숫자(integer/long/double) 필드에 숫자가 아닌 텍스트 전송, 빈 문자열을 숫자로 변환 시도, 잘못된 소수 형식, 또는 매핑과 호환되지 않는 입력 데이터.","fix":"필드에 유효한 숫자 값을 전송한다. 인덱싱 전에 원본 데이터를 검증/변환한다. 빈 값에는 null을 사용하거나 필드를 생략한다. 매핑의 coerce 설정과 필드 타입을 검토한다. 소수 구분 기호로 점을 사용한다.","tags":["number format","number","conversion","numeric","format"]},{"code":"connect_transport_exception","title":"connect_transport_exception","severity":"danger","meaning":"transport(9300) 계층을 통해 노드에 연결할 수 없었다. 노드 간 또는 클라이언트-클러스터 간 통신을 수립하지 못해 작업을 수행할 수 없었다.","cause":"대상 노드 중단/연결 불가, 네트워크/방화벽 문제, 잘못된 transport 주소/포트, TLS 구성 불일치, DNS 문제, 또는 클러스터 이름/버전 불일치.","fix":"대상 노드가 실행 중이고 transport 포트(기본 9300)가 네트워크상에서 연결 가능한지 확인한다. 방화벽/네트워크 규칙과 discovery 구성을 확인한다. TLS/인증서 설정이 모든 노드에서 일관되는지 확인한다. 노드 로그를 검토하고 cluster.name/버전 호환성을 확인한다.","tags":["transport","connection","connect","network","node","danger"]},{"code":"repository_missing_exception","title":"repository_missing_exception","severity":"caution","meaning":"스냅샷/복원 작업에서 참조한 스냅샷 리포지토리를 찾을 수 없다. 지정한 이름으로 등록된 리포지토리가 없다.","cause":"리포지토리가 등록된 적 없음, 이름 오타, 리포지토리 삭제됨, 다른 클러스터에 정의됨, 또는 클러스터 상태 재설정 후 다시 등록되지 않음.","fix":"등록된 리포지토리를 나열한다: GET _snapshot/_all. 올바른 이름과 설정으로 리포지토리를 다시 등록한다: PUT _snapshot/repo_name(type과 settings 포함). 리포지토리 이름의 철자가 올바른지 확인한다. 모든 노드에서 공유 리포지토리(fs/s3 등)에 접근할 수 있는지 확인한다.","tags":["repository","snapshot repository","snapshot","missing","backup","restore"]},{"code":"concurrent_snapshot_execution_exception","title":"concurrent_snapshot_execution_exception","severity":"caution","meaning":"충돌하는 스냅샷 작업이 동시에 실행 중이어서 새 스냅샷/복원 요청이 거부되었다. 리포지토리 또는 인덱스에서 다른 스냅샷 작업이 아직 진행 중이다.","cause":"이전 스냅샷이 아직 진행 중, 같은 인덱스에 영향을 주는 동시 스냅샷/복원 충돌, SLM(snapshot lifecycle) 정책과 수동 스냅샷의 충돌, 또는 짧은 간격의 트리거.","fix":"실행 중인 스냅샷을 확인한다: GET _snapshot/repo/_current 및 GET _snapshot/_status. 현재 작업이 끝날 때까지 기다리거나 필요하면 취소한다: DELETE _snapshot/repo/snapshot_name. 스냅샷이 겹치지 않도록 일정을 잡는다. 수동 작업을 SLM과 조율한다.","tags":["snapshot","concurrent","conflict","simultaneous","slm","backup"]},{"code":"invalid_index_name_exception","title":"invalid_index_name_exception","severity":"caution","meaning":"지정한 인덱스 이름이 Elasticsearch 명명 규칙을 따르지 않는다. 이름에 유효하지 않은 문자가 있거나 형식 규칙을 위반한다.","cause":"대문자, 허용되지 않는 문자(\\ / * ? \" < > | 공백 , #), _, - 또는 +로 시작하는 이름, 너무 긴 이름(255바이트 초과), 또는 . / ..와 같은 예약 이름. (점으로 시작하는 이름은 금지되지 않으나 숨김/시스템 인덱스용으로 예약되어 있어 경고가 발생한다.)","fix":"인덱스 이름을 규칙에 맞춘다. 소문자만 사용하고 금지 문자를 피하며, 이름을 _, - 또는 +로 시작하지 않고, 시스템/숨김 인덱스가 아니라면 점으로 시작하는 이름을 사용하지 않는다. 이름을 255바이트 미만으로 유지한다. 인덱스 이름을 생성할 때 애플리케이션에서 이 규칙을 강제한다.","tags":["index name","index naming","invalid","invalid name","naming"]},{"code":"resource_not_found_exception","title":"resource_not_found_exception","severity":"caution","meaning":"요청한 리소스(문서, 인덱스, 태스크, 파이프라인, 모델 등)를 찾을 수 없다. 지정한 ID/이름의 리소스가 존재하지 않는다.","cause":"존재하지 않는 문서 ID, 삭제되었거나 생성된 적 없는 인덱스/파이프라인, 철자가 틀린 이름, 아직 완료되지 않았거나 이미 정리된 태스크, 또는 잘못된 클러스터로 보낸 요청.","fix":"리소스 이름/ID와 존재 여부를 확인한다(예: GET /index/_doc/id, GET _ingest/pipeline/name). 올바른 인덱스와 클러스터로 요청을 보내는지 확인한다. 삭제/생성 작업 순서를 검토한다. 태스크는 _tasks API로 상태를 확인한다.","tags":["not found","resource not found","resource","missing resource","404"]},{"code":"process_cluster_event_timeout_exception","title":"process_cluster_event_timeout_exception","severity":"danger","meaning":"클러스터 상태 업데이트(예: 인덱스 생성, 매핑 변경)가 할당된 시간 내에 완료되지 않아 타임아웃되었다. master 노드가 업데이트를 제때 처리하지 못했다.","cause":"master 노드의 높은 부하, 매우 크거나 복잡한 클러스터 상태, 빈번하고 집중적인 매핑/템플릿 변경, 느린 master, 또는 master 선출 문제.","fix":"master 노드 부하와 상태를 확인한다: GET _cluster/health, GET _cat/master, 대기 중인 태스크는 GET _cluster/pending_tasks. 클러스터 상태를 축소한다(불필요한 인덱스/매핑/템플릿 정리, dynamic mapping 폭증 방지). DDL 성격의 무거운 작업은 트래픽이 적은 시간대에 배치로 실행한다. 필요하면 master 리소스를 늘린다.","tags":["cluster event","timeout","timed out","master","cluster state","danger"]}]},"errors/oracle-more2.json":{"engineId":"oracle","engine":"Oracle","order":1,"errors":[{"code":"ORA-00955","title":"name is already used by an existing object (이름이 기존 개체에서 이미 사용 중)","severity":"caution","meaning":"생성하려는 개체(테이블, 인덱스, 뷰, 시퀀스, 시노님 등)의 이름을 같은 스키마의 다른 개체가 이미 사용하고 있다. Oracle에서 개체 이름은 스키마 내에서 고유하며, 테이블, 뷰, 시퀀스처럼 유형이 달라도 같은 네임스페이스를 공유한다.","cause":"CREATE 문에서 선택한 이름이 기존 개체와 충돌한다. 흔한 함정: 테이블과 같은 이름으로 시퀀스나 뷰를 생성하려 함, 또는 자동 생성된 인덱스 이름(예: 기본 키 인덱스)이 지정한 이름과 충돌함.","fix":"다른 이름을 선택하거나 충돌하는 개체를 제거한다. 어떤 개체가 이름을 사용하는지 확인하려면: SELECT object_type, object_name FROM user_objects WHERE object_name = 'YOUR_NAME'; 정말 교체하려면 기존 개체를 DROP한다(먼저 데이터를 백업). CREATE OR REPLACE는 뷰, 시노님, 타입, PL/SQL 개체(프로시저, 함수, 패키지, 트리거)에 사용할 수 있으며 테이블, 인덱스, 시퀀스에는 없다.","tags":["ddl","create","name","conflict","duplicate","object"]},{"code":"ORA-02449","title":"unique/primary keys in table referenced by foreign keys (테이블의 고유/기본 키가 외래 키에서 참조됨)","severity":"caution","meaning":"삭제하려는 테이블의 기본 키 또는 고유 키를 다른 테이블의 외래 키가 참조하고 있다. Oracle은 이 종속성을 먼저 제거하지 않으면 테이블을 삭제할 수 없게 한다.","cause":"다른 테이블이 FK로 이 테이블을 참조하는 상태에서 CASCADE CONSTRAINTS 없이 DROP TABLE을 실행함. Oracle은 참조 무결성을 강제하여 고아 외래 키를 방지한다.","fix":"종속 외래 키와 함께 삭제하려면: DROP TABLE table_name CASCADE CONSTRAINTS; 이 명령은 다른 테이블의 관련 FK 제약 조건도 제거한다(해당 테이블의 데이터는 삭제하지 않음). 어떤 제약 조건이 종속되어 있는지 확인하려면: SELECT table_name, constraint_name FROM all_constraints WHERE r_constraint_name IN (SELECT constraint_name FROM user_constraints WHERE table_name = 'TABLE_NAME');","tags":["drop","foreign key","foreign key reference","constraint","cascade","reference"]},{"code":"ORA-01950","title":"no privileges on tablespace 'name' (테이블스페이스에 대한 권한 없음)","severity":"danger","meaning":"사용자가 쓰려는 테이블스페이스에 할당량(quota)이 없다. Oracle에서 사용자가 개체용 공간을 할당하려면 해당 테이블스페이스에 할당량이 있어야 한다.","cause":"사용자에게 테이블스페이스 할당량이 부여되지 않음. 대개 UNLIMITED TABLESPACE 시스템 권한이 없는 새 사용자가 기본 테이블스페이스에 테이블/인덱스를 생성하거나 데이터를 삽입하려 할 때 발생한다. INSERT, CREATE TABLE 또는 인덱스 증가 중에 나타난다.","fix":"DBA로서 사용자에게 할당량을 부여한다: ALTER USER app_user QUOTA UNLIMITED ON tablespace_name; 또는 특정 한도: ALTER USER app_user QUOTA 500M ON users; 대안으로 GRANT UNLIMITED TABLESPACE TO app_user;를 부여할 수 있으나(주의해서 사용) 모든 테이블스페이스에 무제한 권한을 주므로 일반적으로 권장되지 않는다.","tags":["tablespace","quota","tablespace quota","privilege","privilege","insert"]},{"code":"ORA-01110","title":"data file N: 'path'","severity":"danger","meaning":"지정된 번호와 경로의 데이터 파일에 문제가 있음을 알리는 보조(2차) 메시지다. 단독으로 나타나지 않으며, 실제 문제를 보고하는 다른 ORA 오류 뒤에 출력되고, 이 줄은 어떤 데이터 파일이 영향을 받았는지만 알려 준다.","cause":"보통 데이터 파일 오프라인(ORA-00376), 미디어 복구 필요(ORA-01113), 블록 손상(ORA-01578), 파일 읽기 불가 같은 1차 오류와 함께 나타난다. 디스크 장애, 실수로 인한 삭제, 파일 이동, 백업에서의 불완전한 복원이 원인이 될 수 있다.","fix":"이 줄 앞에 있는 실제 ORA 오류를 읽고 해결한다. 영향을 받은 파일 확인: SELECT file#, name, status FROM v$datafile; 복구가 필요하면: RECOVER DATAFILE 'path'; 이후 ALTER DATABASE DATAFILE 'path' ONLINE; 파일이 물리적으로 유실되었다면 RMAN으로 복원 및 복구한다: RESTORE DATAFILE N; RECOVER DATAFILE N;","tags":["datafile","data file","recovery","media recovery","offline","rman"]},{"code":"ORA-16038","title":"log N sequence# M cannot be archived","severity":"danger","meaning":"Oracle이 온라인 redo 로그 파일을 아카이브할 수 없어 해당 로그 그룹을 재사용할 수 없다. ARCHIVELOG 모드에서 아카이브가 멈추면 데이터베이스가 새 트랜잭션을 받지 못하고 행(hang) 상태가 될 수 있다.","cause":"보통 아카이브 로그 대상 또는 Fast Recovery Area(FRA)가 가득 찼거나, 디스크 공간이 부족하거나, 아카이브 대상에 접근할 수 없거나, 권한/마운트 문제가 있다. ORA-19809(복구 파일 한도 초과) 또는 ORA-00312와 함께 자주 나타난다.","fix":"아카이브 대상/FRA의 공간을 확보한다: 오래된 아카이브 로그를 RMAN으로 FRA 외부 대상에 백업한 후 삭제하거나(DELETE ARCHIVELOG ALL COMPLETED BEFORE ...; Data Guard 환경에서는 standby로 아직 전송/적용되지 않은 아카이브 로그를 삭제하지 않는다), db_recovery_file_dest_size를 늘린다. 대상 상태 확인: ARCHIVE LOG LIST; 및 SELECT dest_name, status, error FROM v$archive_dest WHERE status != 'INACTIVE'; 문제가 해결되면 아카이브가 중단된 지점부터 재개된다.","tags":["archivelog","archive","redo","fra","recovery area","archiver stuck"]},{"code":"ORA-19809","title":"limit exceeded for recovery files","severity":"danger","meaning":"Fast Recovery Area(FRA)에 할당된 공간(db_recovery_file_dest_size)이 가득 찼다. 아카이브 로그, flashback 로그, 백업을 위한 공간이 남지 않아 Oracle이 새 복구 파일을 생성할 수 없고 데이터베이스가 행 상태가 될 수 있다.","cause":"아카이브 로그를 정기적으로 백업하고 정리하지 않아 FRA에 누적되었거나, flashback 로그 사용량이 증가했거나, 할당된 크기가 실제 워크로드에 비해 너무 작다. ORA-16038 같은 아카이브 오류와 함께 나타난다.","fix":"FRA 사용량 확인: SELECT * FROM v$recovery_file_dest; 및 SELECT * FROM v$flash_recovery_area_usage; 불필요한 아카이브 로그를 RMAN으로 삭제하거나: DELETE ARCHIVELOG ALL BACKED UP 1 TIMES TO DISK; (Data Guard 환경에서는 먼저 standby에 적용되었는지 확인한다. CONFIGURE ARCHIVELOG DELETION POLICY TO APPLIED ON ALL STANDBY 설정을 권장한다) 영구 조치로 크기를 늘린다: ALTER SYSTEM SET db_recovery_file_dest_size = 50G SCOPE=BOTH; 아카이브 로그를 정기적으로 백업하는 RMAN 작업을 구성한다.","tags":["fra","recovery area","db_recovery_file_dest_size","archivelog","space","rman"]},{"code":"ORA-00376","title":"file N cannot be read at this time","severity":"danger","meaning":"지정된 번호의 데이터 파일이 오프라인 상태이거나 접근할 수 없어 현재 읽을 수 없다. 해당 테이블스페이스의 데이터에 접근하는 쿼리와 작업이 이 오류로 실패한다.","cause":"데이터 파일이 오류 후 자동으로 또는 수동으로 오프라인되었거나, 미디어 복구를 기다리고 있거나, 디스크에 접근할 수 없게 되었거나, NOARCHIVELOG 모드에서 복구할 수 없는 파일이 오프라인 상태로 남아 있다. 보통 파일을 식별하는 ORA-01110 줄이 함께 출력된다.","fix":"파일 상태 확인: SELECT file#, name, status FROM v$datafile WHERE status != 'ONLINE'; 복구 후 온라인으로 전환: RECOVER DATAFILE N; 이후 ALTER DATABASE DATAFILE N ONLINE; 물리적 손상/유실이 있으면 RMAN으로 복원 및 복구한다: RESTORE DATAFILE N; RECOVER DATAFILE N; 근본 원인인 디스크/하드웨어 문제도 해결한다.","tags":["datafile","data file","offline","recovery","media recovery","read"]},{"code":"ORA-12520","title":"TNS:listener could not find available handler for requested type of server (리스너가 요청한 서버 유형의 핸들러를 찾지 못함)","severity":"danger","meaning":"리스너가 요청된 연결 유형(dedicated 또는 shared)에 맞는 서버 프로세스(핸들러)를 찾지 못했다. 클라이언트는 리스너에 도달하지만 연결을 서버 프로세스로 넘길 수 없다.","cause":"인스턴스가 리스너에 등록되지 않았거나 등록이 깨졌거나, PROCESSES/SESSIONS 한도에 도달하여 새 dedicated 프로세스를 생성할 수 없거나, shared server 구성에서 dispatcher가 없거나 부족하거나, 인스턴스가 막 시작되어 아직 리스너에 등록되지 않았다.","fix":"리스너가 인식하는 서비스 확인: lsnrctl services (핸들러 수와 상태). 인스턴스를 리스너에 재등록: ALTER SYSTEM REGISTER; PROCESSES 한도 확인: SELECT resource_name, current_utilization, max_utilization, limit_value FROM v$resource_limit WHERE resource_name IN ('processes','sessions'); 필요하면 ALTER SYSTEM SET processes=... SCOPE=SPFILE; 로 늘린 후 인스턴스를 재시작한다.","tags":["listener","tns","connection","handler","processes","dispatcher"]},{"code":"ORA-28001","title":"the password has expired","severity":"caution","meaning":"접속하려는 데이터베이스 사용자의 프로파일 PASSWORD_LIFE_TIME이 경과하여 비밀번호가 만료되었다. 비밀번호를 변경하기 전까지 세션을 열 수 없다.","cause":"사용자 프로파일에 정의된 비밀번호 유효 기간이 끝나고 유예 기간도 경과했다. Oracle 11g 이상에서는 DEFAULT 프로파일의 비밀번호가 180일마다 만료되며, 특히 오래 사용하는 애플리케이션 서비스 계정에서 예기치 않은 장애를 일으킨다.","fix":"비밀번호 변경: ALTER USER app_user IDENTIFIED BY new_password; 애플리케이션 계정에서 문제가 반복되지 않도록 비밀번호 만료가 없는 프로파일을 사용한다: CREATE PROFILE app_profile LIMIT PASSWORD_LIFE_TIME UNLIMITED; 이후 ALTER USER app_user PROFILE app_profile; 만료 전에 경고하는 모니터링도 유용하다. 상태는 DBA_USERS의 ACCOUNT_STATUS로 확인한다.","tags":["password","password expired","expired","profile","account","login"]},{"code":"ORA-38029","title":"object statistics are locked","severity":"caution","meaning":"테이블(또는 스키마)의 옵티마이저 통계가 잠겨 있어 통계 수집/삭제가 거부되었다. 잠긴 통계는 DBMS_STATS로 다시 계산되지 않도록 보호된다.","cause":"이전에 DBMS_STATS.LOCK_TABLE_STATS 또는 LOCK_SCHEMA_STATS가 호출되었거나(예: 통계를 고정하기 위해), Data Pump import(특히 CONTENT=METADATA_ONLY)가 의도치 않게 통계를 잠긴 상태로 남겼다. 잠긴 상태에서 GATHER_TABLE_STATS를 실행하면 이 오류가 발생한다.","fix":"통계 잠금 해제: EXEC DBMS_STATS.UNLOCK_TABLE_STATS('APP_SCHEMA','MY_TABLE'); 또는 스키마 수준에서 DBMS_STATS.UNLOCK_SCHEMA_STATS('APP_SCHEMA'); 잠금을 해제하지 않고 수집하려면 FORCE=>TRUE를 사용한다: EXEC DBMS_STATS.GATHER_TABLE_STATS('APP_SCHEMA','MY_TABLE', force=>TRUE); 잠금 상태 확인: SELECT table_name, stattype_locked FROM dba_tab_statistics WHERE stattype_locked IS NOT NULL;","tags":["statistics","optimizer statistics","dbms_stats","locked","lock","optimizer"]}]},"errors/postgres-more2.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"errors":[{"code":"54001","title":"statement_too_complex","severity":"caution","meaning":"문장이 PostgreSQL이 처리할 수 있는 수준보다 복잡하다. 보통 매우 깊게 중첩된 표현식이나 많은 중첩 연산자로 인해 표현식 트리가 parser/planner 스택 한도를 초과한다. Class 54(program_limit_exceeded)에 속한다.","cause":"과도하게 깊은 괄호/중첩 서브쿼리나 OR/AND 체인, 자동 생성된 거대한 WHERE ... IN (...) 목록, 재귀 뷰에서 생기는 매우 큰 표현식 트리가 원인이다. max_stack_depth 한도에 도달한다.","fix":"쿼리를 단순화한다: 긴 IN 목록은 임시 테이블이나 VALUES 집합으로 옮겨 JOIN하고, 중첩 표현식을 분리하며, ORM이 생성한 거대한 쿼리를 검토한다. 꼭 필요하고 OS 스택 한도가 허용한다면 max_stack_depth를 신중히 높이되(예: SET max_stack_depth = '4MB';) OS ulimit보다 낮게 유지한다.","tags":["program_limit","complex","stack","nested","query","max_stack_depth"]},{"code":"58030","title":"io_error","severity":"danger","meaning":"PostgreSQL이 파일을 읽거나 쓰는 중 운영체제 수준의 입출력(I/O) 오류를 만났다. 데이터베이스 파일 접근에 물리적 또는 파일 시스템 문제가 있음을 나타낸다. Class 58(system_error)에 속한다.","cause":"디스크 장애 또는 배드 섹터, 디스크 가득 참, 파일 시스템 오류, 네트워크 연결 스토리지(NFS/SAN) 장애, 권한 문제, OS 파일 디스크립터/inode 한도 소진이 원인이다.","fix":"디스크 상태와 여유 공간을 확인한다(df -h, dmesg, SMART 데이터). PostgreSQL 로그에서 어떤 파일이 영향을 받았는지 확인한다. 파일 시스템 오류를 수정하고 필요하면 하드웨어를 교체한다. 데이터 손상이 의심되면 마지막 정상 백업에서 복원을 계획하고, 이후에는 체크섬을 활성화해 둔다(initdb --data-checksums).","tags":["system_error","io","disk","hardware","file","corruption"]},{"code":"22004","title":"null_value_not_allowed","severity":"caution","meaning":"NULL을 허용하지 않는 곳에 NULL이 전달되었다. 컬럼 NOT NULL 제약 조건(23502)과는 다르며, 보통 함수나 프로시저 컨텍스트에서 NULL을 받지 않는 파라미터나 연산에 NULL을 전달할 때 발생한다. Class 22(data_exception)에 속한다.","cause":"NOT NULL로 선언된 PL/pgSQL 변수에 NULL을 대입하거나, EXECUTE에 NULL 쿼리 문자열을 전달하거나, NULL을 받지 않는 내장 함수 인수에 NULL을 전달한 경우다. 참고: STRICT로 지정된 함수는 NULL 입력 시 오류를 발생시키지 않고 실행하지 않은 채 NULL을 반환한다.","fix":"실패한 줄(CONTEXT)을 찾아 해당 위치에 도달하는 값을 NULL 검사로 보호하거나 COALESCE로 기본값을 지정한다: COALESCE(value, default_value). PL/pgSQL에서는 NOT NULL 변수에 대입하기 전에 값을 검증하고, 동적 SQL에서는 EXECUTE에 전달하는 문자열이 NULL이 아닌지 확인한다. 애플리케이션 계층에서 필수 필드가 NULL로 전송되지 않도록 한다.","tags":["data_exception","null","function","strict","parameter","coalesce"]},{"code":"42P18","title":"indeterminate_datatype","severity":"caution","meaning":"PostgreSQL이 컨텍스트로부터 표현식이나 파라미터의 데이터 타입을 추론할 수 없었다. 타입 힌트 없이 타입이 지정되지 않은 플레이스홀더($1 등)나 단독 NULL을 사용할 때 가장 흔히 발생한다.","cause":"prepared statement의 타입 미지정 파라미터, \"WHERE $1 = $2\"처럼 양쪽이 모두 불확정인 비교, 컨텍스트 없는 NULL/배열 리터럴 사용이 원인이다. planner가 어떤 타입을 사용할지 결정할 수 없다.","fix":"불확정 표현식에 명시적 캐스트를 추가한다: $1::text, $1::integer, NULL::date 등. 예: SELECT $1::int + 1; 드라이버/ORM 파라미터에 타입을 지정한다. 컬럼 비교 시 적어도 한쪽은 타입이 알려져 있도록 한다.","tags":["syntax","type","indeterminate","cast","parameter","prepared"]},{"code":"42P02","title":"undefined_parameter","severity":"caution","meaning":"쿼리가 존재하지 않는 파라미터를 참조한다. PostgreSQL은 참조된 위치 파라미터(예: $2)가 정의되지 않았다고 보고한다. Class 42(syntax_error_or_access_rule_violation)에 속한다.","cause":"PREPARE로 준비한 문장에서 제공한 것보다 많은 파라미터를 참조하거나(예: 파라미터 두 개를 보내고 $3 사용), 파라미터 번호를 건너뛰었거나, 동적으로 연결한 SQL에서 $N 인덱스가 잘못된 경우다.","fix":"파라미터 번호가 제공하는 값의 개수와 일치하는지, $1부터 시작해 빈틈없이 이어지는지 확인한다. PREPARE 문에서 파라미터 타입을 완전히 정의한다: PREPARE p(int, text) AS SELECT ... WHERE a=$1 AND b=$2; ORM/드라이버를 사용한다면 바인딩된 파라미터 수와 쿼리의 플레이스홀더를 비교한다.","tags":["syntax","parameter","prepared","undefined","placeholder","sql"]},{"code":"2F003","title":"prohibited_sql_statement_attempted","severity":"caution","meaning":"SQL 함수/루틴 안에서 해당 컨텍스트에서 금지된 문장을 시도했다. Class 2F(sql_routine_exception)에 속한다. 예를 들어 읽기 전용이어야 하는 함수가 데이터를 수정하려는 경우다.","cause":"PostgreSQL 코어는 이 코드를 거의 발생시키지 않으며 보통 확장이나 절차 언어 코드에서 발생한다. 참고: IMMUTABLE/STABLE로 지정된 함수가 INSERT/UPDATE/DELETE를 실행하면 2F003이 아니라 코드 0A000(feature_not_supported)으로 '... is not allowed in a non-volatile function'이 발생한다.","fix":"오류 메시지에서 어떤 문장이 금지되었는지 확인하고 관련 루틴/확장의 문서를 확인한다. non-volatile 함수 오류(0A000)가 발생하면 데이터를 수정하는 함수를 VOLATILE로 정의한다(기본값): CREATE FUNCTION ... LANGUAGE plpgsql VOLATILE. 읽기 전용이어야 하는 위치(인덱스 표현식, 제약 조건 검사)에서 데이터 수정 호출을 제거한다.","tags":["sql_routine","function","volatile","prohibited","immutable","side effect"]},{"code":"40003","title":"statement_completion_unknown","severity":"danger","meaning":"트랜잭션 결과를 알 수 없다. 트랜잭션 완료 여부가 확인되기 전에(특히 COMMIT 중) 서버와의 연결이 끊겼다. 클라이언트는 변경 사항이 영구 반영되었는지 확실히 알 수 없다. Class 40(transaction_rollback)에 속한다.","cause":"COMMIT을 보낸 후 응답이 도착하기 전에 네트워크가 끊기거나, 서버가 크래시되거나, 페일오버가 발생했다. 트랜잭션은 서버에서 완료되었을 수도 있고 롤백되었을 수도 있다.","fix":"트랜잭션을 무작정 재시도하지 않는다. 먼저 실제로 적용되었는지 확인한다(예: 예상되는 행을 조회). 트랜잭션을 멱등하게 설계하고 고유 비즈니스 키/멱등성 키를 사용한다. 커넥션 풀에 견고한 재연결 및 검증 로직을 구현한다.","tags":["transaction","commit","unknown","connection","idempotent","failover"]},{"code":"08003","title":"connection_does_not_exist","severity":"danger","meaning":"존재하지 않는(이미 닫힌) 데이터베이스 연결에서 작업을 시도했다. 애플리케이션이 유효하지 않은 연결 핸들로 쿼리를 보내려 했다. Class 08(connection_exception)에 속한다.","cause":"연결 타임아웃, 서버에 의한 종료, 네트워크 장애, 실제로는 죽은 연결을 풀에서 가져온 경우, 또는 애플리케이션 코드에서 close()된 연결을 재사용한 경우다.","fix":"커넥션 풀에서 생존 확인을 활성화하고(validation query / keepalive), 죽은 연결을 풀에서 제거하는 설정을 구성한다(예: HikariCP의 connectionTestQuery/maxLifetime). 애플리케이션에서는 사용 전에 연결이 열려 있는지 확인하고, 끊기면 자동으로 재연결한다. TCP keepalive와 idle_in_transaction_session_timeout 값을 검토한다.","tags":["connection","connection","closed","pool","pool","timeout"]},{"code":"25006","title":"read_only_sql_transaction","severity":"caution","meaning":"읽기 전용 트랜잭션 안에서 데이터 수정 명령(INSERT/UPDATE/DELETE/DDL)을 시도했다. 트랜잭션이 읽기 전용으로 구성되어 있다. Class 25(invalid_transaction_state)에 속한다.","cause":"SET TRANSACTION READ ONLY 또는 default_transaction_read_only=on 설정, 또는 가장 흔하게는 읽기 replica(hot standby / streaming replica)에 대한 쓰기 시도가 원인이다. standby 서버는 정의상 읽기 전용이다.","fix":"쓰기를 primary 서버로 라우팅하고, 읽기와 쓰기를 분리하는 연결 라우팅을 확인한다. 실수로 설정된 경우: SET TRANSACTION READ WRITE; 또는 서버 전체에서 default_transaction_read_only=off. 애플리케이션이 읽기-쓰기 트래픽을 올바른 노드로 보내는지 확인한다.","tags":["transaction","read only","read only","replica","standby","primary"]},{"code":"21000","title":"cardinality_violation","severity":"caution","meaning":"단일 행이 예상되는 곳에서 여러 행이 반환되었다. 스칼라 서브쿼리가 여러 행을 반환했거나, MERGE/ON CONFLICT DO UPDATE가 같은 대상 행에 두 번째로 영향을 주려 했다. Class 21에 속한다.","cause":"SELECT 목록이나 WHERE = (subquery) 형태에서 사용한 서브쿼리가 여러 행을 반환했거나, 단일 INSERT ... ON CONFLICT DO UPDATE에서 같은 충돌 키가 두 번 이상 제안되었거나('cannot affect row a second time'), MERGE에서 여러 소스 행이 하나의 대상 행과 매칭된 경우다. UPDATE ... FROM은 다중 매칭 시 오류를 발생시키지 않으며, 대상 행은 소스 행 중 하나로만 예측할 수 없게 갱신된다.","fix":"서브쿼리가 단일 행을 반환하도록 한다: LIMIT 1을 추가하거나, 집계(MAX/MIN)를 사용하거나, 매칭을 고유하게 만드는 조건을 지정한다. MERGE와 ON CONFLICT 패턴에서는 DISTINCT ON/집계로 소스 쪽 중복을 제거하고, UPDATE ... FROM에서도 조용히 잘못된 결과가 나오지 않도록 동일하게 처리한다. 예상치 못한 다중 매칭을 찾으려면 서브쿼리를 단독으로 실행해 행 수를 확인한다.","tags":["cardinality","subquery","subquery","single row","join","scalar"]}]},"errors/mysql-more2.json":{"engineId":"mysql","engine":"MySQL","order":3,"errors":[{"code":"1927","title":"MariaDB: ER_CONNECTION_KILLED: Connection was killed (연결이 종료됨)","severity":"danger","meaning":"MariaDB 전용: 실행 중인 연결/스레드가 서버에 의해 종료되어 진행 중인 쿼리가 중단되고 클라이언트 연결이 끊겼다. 이 코드는 MySQL에는 없으며, MySQL에서는 쿼리가 중단되면 1317(ER_QUERY_INTERRUPTED)로, 연결이 끊기면 클라이언트 측에서 2013(Lost connection)으로 나타난다.","cause":"관리자나 모니터링 도구가 KILL 명령을 보냈거나, 서버가 종료 중이거나, idle_transaction_timeout/wait_timeout 같은 MariaDB 유휴 연결 한도에 걸렸거나, 리소스 부족으로 서버가 연결을 끊은 경우다. 클라우드/프록시 환경의 유지보수 중에도 나타난다.","fix":"쿼리가 kill된 이유를 조사한다: 장시간 실행 쿼리는 SHOW PROCESSLIST와 slow query log, 서버 유지보수/재시작 일정, 연결 kill 정책을 확인한다. 애플리케이션에서 이 오류를 잡아 안전하게(멱등한 경우) 재시도한다. 긴 쿼리를 최적화하고 분할하며, 커넥션 풀에서 자동 재연결을 사용한다.","tags":["connection","connection killed","kill","thread","shutdown","retry"]},{"code":"1290","title":"ER_OPTION_PREVENTS_STATEMENT: running with the --read-only option (--read-only 옵션으로 실행 중)","severity":"caution","meaning":"MySQL 서버가 --read-only 또는 --super-read-only 옵션으로 실행 중이어서 데이터 수정 문장(INSERT/UPDATE/DELETE/DDL)을 거부했다. read_only에서는 CONNECTION_ADMIN/SUPER 권한 사용자가 여전히 쓸 수 있지만, super_read_only가 활성화되면 그들도 쓸 수 없으며 메시지는 '--super-read-only option'으로 표시된다.","cause":"read_only=ON 또는 super_read_only=ON이며, 가장 흔하게는 replica에 대한 실수로 인한 쓰기 시도다. replica와 Group Replication secondary는 데이터 일관성을 위해 읽기 전용으로 유지되며, 페일오버 자동화는 이전 primary를 super_read_only로 전환한다. 페일오버/스위치오버 중 잘못된 노드에 쓰는 경우에도 발생한다.","fix":"쓰기를 primary(source) 서버로 라우팅한다. 이 서버가 실제로 새 primary가 되어야 한다면: SET GLOBAL read_only = OFF; (read_only를 끄면 super_read_only도 자동으로 꺼지고, super_read_only를 켜면 read_only도 켜진다). HA 도구(Orchestrator/Group Replication/InnoDB Cluster)가 관리하는 노드에서는 수동으로 변경하지 않는다. 상태 확인: SELECT @@read_only, @@super_read_only;","tags":["read only","super_read_only","read-only mode","replica","primary","write","failover","1290"]},{"code":"1836","title":"ER_READ_ONLY_MODE: Running in read-only mode (읽기 전용 모드로 실행 중)","severity":"caution","meaning":"InnoDB가 읽기 전용 모드(innodb_read_only=ON)로 실행 중이거나 데이터 디렉터리/미디어가 읽기 전용이어서 InnoDB 테이블에 대한 쓰기와 DDL이 거부된다. 참고: read_only/super_read_only가 활성화된 상태에서 쓰기를 시도하면 이 코드가 아니라 1290(ER_OPTION_PREVENTS_STATEMENT)이 반환된다.","cause":"서버가 innodb_read_only=ON으로 시작되었으며(예: 읽기 전용 미디어, 공유 데이터 디렉터리, 분석용 복사본), 이 모드에서는 InnoDB 테이블에 INSERT/UPDATE/DELETE, CREATE/DROP/ALTER TABLE 같은 변경을 할 수 없다.","fix":"쓰기가 필요하면 innodb_read_only 없이 서버를 시작한다: my.cnf에서 innodb_read_only=ON 줄을 제거하고 재시작한다(이 변수는 동적이 아니므로 재시작이 필요하다). 상태 확인: SELECT @@innodb_read_only; 데이터 디렉터리와 파일 시스템에 쓰기가 가능한지 확인한다. replica/HA secondary에 쓰는 경우(1290)라면 쓰기를 primary로 라우팅한다.","tags":["innodb_read_only","read only","read-only mode","innodb","1836"]},{"code":"3819","title":"ER_CHECK_CONSTRAINT_VIOLATED: Check constraint is violated (CHECK 제약 조건 위반)","severity":"caution","meaning":"삽입하거나 갱신하는 행이 테이블에 정의된 CHECK 제약 조건을 위반한다. MySQL 8.0.16 이상에서는 CHECK 제약 조건이 실제로 적용된다(이전 버전은 무시했다).","cause":"컬럼 값이 CHECK 표현식의 조건을 만족하지 않는다(예: CHECK (price >= 0)이 있는데 음수 가격 입력). 이전 MySQL 버전에서 8.0.16+로 업그레이드한 후, 이전에 무시되던 제약 조건이 적용되면서 예기치 않게 나타날 수 있다.","fix":"값이 제약 조건을 만족하도록 하고 애플리케이션 측 검증을 추가한다. 제약 조건 정의 확인: SHOW CREATE TABLE my_table; 제약 조건이 정말 불필요하다면: ALTER TABLE my_table DROP CHECK constraint_name; 또는 일시적으로 ALTER TABLE my_table ALTER CHECK constraint_name NOT ENFORCED; 잘못된 데이터의 원인도 수정한다.","tags":["check","constraint","check constraint","validation","8.0","violation"]},{"code":"1118","title":"ER_TOO_BIG_ROWSIZE: Row size too large","severity":"caution","meaning":"정의하려는 테이블의 행 크기가 스토리지 엔진이 허용하는 최대 행 크기를 초과한다. BLOB/TEXT가 아닌 컬럼의 최대 행 크기는 65535바이트이며, InnoDB에는 페이지 크기에 따른 추가 한도가 있다.","cause":"넓은 컬럼(특히 긴 VARCHAR/CHAR)을 많이 정의했거나, utf8mb4 같은 멀티바이트 문자 집합에서 문자당 최대 4바이트를 차지하거나, InnoDB의 COMPACT/REDUNDANT 행 형식에서 큰 컬럼이 인라인으로 저장된 경우다.","fix":"일부 큰 컬럼을 TEXT/BLOB로 변환하거나(off-page 저장 가능), 불필요하거나 너무 넓은 컬럼을 줄이거나, 테이블을 둘로 분할한다. InnoDB에서는 ROW_FORMAT=DYNAMIC 또는 COMPRESSED를 사용하면 큰 컬럼이 off-page로 이동되어 도움이 된다: ALTER TABLE my_table ROW_FORMAT=DYNAMIC; innodb_page_size도 고려한다.","tags":["row size","row size too large","innodb","varchar","row_format","limit"]},{"code":"1170","title":"ER_BLOB_KEY_WITHOUT_LENGTH: BLOB/TEXT column used in key specification without a key length (키 길이 없이 BLOB/TEXT 컬럼을 키에 사용함)","severity":"caution","meaning":"인덱스/키 정의에 접두사 길이를 지정하지 않고 BLOB 또는 TEXT 컬럼을 사용했다. 이 타입은 전체를 인덱싱할 수 없으며 처음 N개 문자/바이트만 인덱싱할 수 있다.","cause":"CREATE TABLE 또는 CREATE INDEX에서 길이 없이 TEXT/BLOB 컬럼에 INDEX/UNIQUE/PRIMARY KEY를 정의한 경우다. MySQL은 잠재적으로 매우 큰 이 값들을 전체 인덱싱할 수 없으므로 접두사 길이를 요구한다.","fix":"인덱스에 접두사 길이를 지정한다(예: INDEX (description(255))). 예: ALTER TABLE my_table ADD INDEX idx_description (description(191)); (InnoDB 키 접두사 한도 때문에 utf8mb4에서는 191을 흔히 사용한다). 전문 검색이 필요하면 FULLTEXT 인덱스를 고려한다. 짧고 고유해야 하는 필드라면 컬럼을 VARCHAR로 변환하는 것이 더 적합할 수 있다.","tags":["index","key","blob","text","prefix","prefix length"]},{"code":"1592","title":"ER_BINLOG_UNSAFE_STATEMENT: Unsafe statement written to the binary log (binary log에 안전하지 않은 문장 기록)","severity":"caution","meaning":"STATEMENT 기반 binlog 형식을 사용하는 중에 결과가 비결정적인(replica에서 다른 결과를 낼 수 있는) 문장이 실행되었다. source와 replica 간 데이터 불일치 위험이 있으므로 MySQL은 binlog에 기록할 때 경고를 발생시킨다.","cause":"binlog_format=STATEMENT에서: UUID(), SYSDATE(), USER() 같은 비결정적 함수(NOW()는 STATEMENT에서도 안전하게 복제된다), ORDER BY 없이 LIMIT이 있는 UPDATE/DELETE, AUTO_INCREMENT가 섞인 INSERT, 특정 사용자 정의/시스템 함수가 원인이다. 이 문장들은 모든 서버에서 같은 결과를 내지 않을 수 있다.","fix":"binlog 형식을 행 기반으로 전환한다(권장. ROW는 MySQL 5.7.7부터 기본값이다): my.cnf에 binlog_format=ROW 또는 SET PERSIST binlog_format = ROW; (binlog_format 변수는 8.0.34부터 deprecated이며 변경은 새 세션부터 적용된다). ROW 형식은 비결정적 문장을 안전하게 복제한다. STATEMENT를 유지해야 한다면 비결정적 함수/표현식을 상수 값으로 바꾸고 LIMIT이 있는 DML에 결정적인 ORDER BY를 추가한다.","tags":["binlog","replication","unsafe","statement","row","nondeterministic"]},{"code":"3140","title":"ER_INVALID_JSON_TEXT: Invalid JSON text","severity":"caution","meaning":"유효하지 않은(구문이 깨진) JSON 텍스트가 JSON 컬럼이나 JSON 함수에 전달되었다. MySQL은 값을 JSON으로 파싱할 수 없어 작업을 거부한다.","cause":"닫히지 않은 따옴표/괄호, 후행 쉼표, 작은따옴표, 이스케이프 문자 오류, 또는 JSON 컬럼에 일반 텍스트를 보낸 경우다. JSON_SET, JSON_INSERT 같은 함수나 JSON 타입 컬럼에 잘못된 형식의 텍스트를 전달할 때 나타난다.","fix":"JSON 텍스트를 올바른 구문으로 수정한다: 큰따옴표를 사용하고, 후행 쉼표를 제거하고, 이스케이프를 확인한다. 텍스트 유효성 테스트: SELECT JSON_VALID('{...}'); (유효하면 1 반환). 애플리케이션 계층에서 신뢰할 수 있는 라이브러리로 JSON을 직렬화하고 서버로 보내기 전에 검증한다.","tags":["json","invalid","syntax","json_valid","parse","data"]},{"code":"1526","title":"ER_NO_PARTITION_FOR_GIVEN_VALUE: Table has no partition for value (해당 값에 대한 파티션 없음)","severity":"caution","meaning":"삽입하는 행의 파티션 키 값이 정의된 어떤 파티션에도 맞지 않는다. RANGE 또는 LIST 파티셔닝에서 들어오는 값을 포함하는 파티션이 없어 삽입이 실패한다.","cause":"RANGE 파티셔닝에서 MAXVALUE 파티션 없이 최상위 경계를 넘는 값(예: 미래 날짜)을 삽입하거나, LIST 파티셔닝에서 목록에 없는 값이 들어온 경우다. 새 기간의 파티션을 제때 추가하지 않은 시간 기반 파티션 테이블에서 흔하다.","fix":"값을 포함하는 파티션을 추가한다. RANGE의 새 상위 파티션: ALTER TABLE my_table ADD PARTITION (PARTITION p2026 VALUES LESS THAN (2027)); 모든 값을 받는 파티션이 필요하면 RANGE에는 MAXVALUE를, LIST에는 해당 값을 정의한다. 시간 기반 테이블에서는 파티션을 자동으로 추가하는 유지보수 작업(event/scheduler)을 구성한다.","tags":["partition","partitioning","range","list","maxvalue","insert"]}]},"errors/mssql-more2.json":{"engineId":"mssql","engine":"SQL Server","order":4,"errors":[{"code":"1101","title":"Could not allocate a new page because of insufficient disk space in filegroup (파일 그룹의 디스크 공간 부족으로 새 페이지를 할당할 수 없음)","severity":"danger","meaning":"SQL Server가 객체에 새 페이지를 할당하려 했지만 파일 그룹의 디스크 공간이 부족하다. 데이터 파일이 커질 수도 없고 빈 페이지를 찾을 수도 없어 쓰기가 실패한다.","cause":"데이터 파일이 있는 디스크가 가득 찼거나, 파일의 autogrow가 비활성화되었거나 MAXSIZE에 도달했거나, 파일 그룹의 모든 파일이 크기 상한에 도달했다. 대량 INSERT/인덱스 생성 중에 흔하다.","fix":"디스크 공간을 확보하거나 파일 그룹에 공간을 추가한다. 파일을 키우거나 autogrow를 활성화한다: ALTER DATABASE db MODIFY FILE (NAME = logical_name, SIZE = 20GB, MAXSIZE = UNLIMITED, FILEGROWTH = 512MB); 또는 파일 그룹에 새 파일을 (다른 디스크에) 추가한다. 파일 사용량은 sys.database_files로, 여유 공간은 DBCC SQLPERF로 확인한다.","tags":["disk","filegroup","file group","autogrow","space","allocation"]},{"code":"3729","title":"Cannot DROP because it is being referenced by object (객체가 참조 중이어서 DROP 불가)","severity":"caution","meaning":"삭제하려는 객체(테이블, 타입, 함수 등)를 다른 객체가 참조하고 있어 그 의존성 때문에 삭제할 수 없다. 특히 스키마 바인딩된 객체가 이렇게 보호된다.","cause":"WITH SCHEMABINDING으로 생성된 뷰나 함수가 의존하는 테이블/객체를 삭제하려 하거나, 외래 키가 참조하는 테이블을 삭제하거나, 테이블/파라미터가 아직 사용 중인 사용자 정의 타입을 제거하려는 경우다.","fix":"참조하는 객체를 먼저 삭제하거나 의존성을 제거한다. 의존성 확인: SELECT referencing_schema_name, referencing_entity_name FROM sys.dm_sql_referencing_entities('app_schema.object', 'OBJECT'); 스키마 바인딩된 뷰는 먼저 ALTER VIEW로 SCHEMABINDING을 제거하거나 뷰를 삭제한 후 테이블을 삭제한다.","tags":["drop","reference","schemabinding","view","dependencies","dependency"]},{"code":"3961","title":"Snapshot isolation transaction failed because the object was modified by a DDL statement (객체가 DDL로 변경되어 스냅샷 격리 트랜잭션 실패)","severity":"caution","meaning":"스냅샷 격리 수준에서 실행 중인 트랜잭션이 접근한 객체가 다른 동시 트랜잭션의 DDL 문(스키마 변경)으로 변경되어 실패했다. 메타데이터는 버전 관리되지 않으므로 스냅샷 트랜잭션이 계속될 수 없다.","cause":"SNAPSHOT 격리 수준(SET TRANSACTION ISOLATION LEVEL SNAPSHOT)으로 시작된 트랜잭션이 실행 중일 때 다른 세션이 해당 트랜잭션이 접근하는 테이블에 ALTER TABLE/CREATE INDEX 같은 DDL을 실행한 경우다. 스냅샷 트랜잭션은 행 버전은 볼 수 있지만 스키마 버전은 볼 수 없다. 이 오류는 READ COMMITTED SNAPSHOT(RCSI)에서는 발생하지 않으며, RCSI는 문장마다 새 스냅샷을 사용한다.","fix":"트랜잭션을 재시도한다. 보통 DDL이 완료되면 성공한다. 영구 조치로 스냅샷 워크로드가 많지 않은 유지보수 시간에 스키마 변경(DDL)을 수행한다. 애플리케이션에서 이 오류를 잡아 자동 재시도 로직을 구현한다. 장시간 실행되는 스냅샷 트랜잭션을 짧게 줄인다.","tags":["snapshot","isolation level","ddl","concurrency","retry","isolation"]},{"code":"1785","title":"Introducing FOREIGN KEY constraint may cause cycles or multiple cascade paths (FOREIGN KEY 제약 조건이 순환 또는 다중 cascade 경로를 만들 수 있음)","severity":"caution","meaning":"추가하려는 외래 키가 cascade(ON DELETE/UPDATE CASCADE) 작업에서 순환이나 같은 테이블로 향하는 다중 cascade 삭제/갱신 경로를 만들 수 있다. SQL Server는 모호한 cascade 동작을 피하기 위해 이를 거부한다.","cause":"cascade 동작이 있는 FK가 테이블 간 순환 참조(A->B->A)나 한 테이블로 향하는 다중 cascade 경로를 만드는 경우다. SQL Server는 모호한 cascade 순서와 무한 루프를 막기 위해 이를 허용하지 않는다.","fix":"외래 키를 ON DELETE NO ACTION / ON UPDATE NO ACTION으로 정의한다(cascade 제거): ALTER TABLE child ADD CONSTRAINT fk ... FOREIGN KEY (...) REFERENCES parent(...) ON DELETE NO ACTION; 필요한 연쇄 삭제는 INSTEAD OF DELETE 트리거로 구현한다: 트리거가 먼저 자식 행을 삭제한 후 부모 행을 삭제한다(예: DELETE c FROM child c JOIN deleted d ON c.parent_id = d.id; DELETE e FROM parent e JOIN deleted d ON e.id = d.id;). AFTER DELETE 트리거는 동작하지 않는다. FK 검사가 트리거보다 먼저 실행되므로 삭제가 오류 547로 거부된다. 데이터 모델의 순환 의존성을 검토한다.","tags":["foreign key","fk","cascade","circular reference","cycle","constraint"]},{"code":"9421","title":"XML parsing: illegal xml character","severity":"caution","meaning":"SQL Server가 텍스트를 XML로 파싱하는 중 허용되지 않는(유효하지 않은) 문자를 만났다. 이 문자 때문에 XML 타입으로의 변환이나 XML 컬럼/변수에 대한 대입이 실패했다.","cause":"XML 1.0에서 금지된 제어 문자(예: 0x00-0x1F 범위의 일부 문자), 잘못된 인코딩(인코딩 불일치), 다른 소스에서 온 손상되었거나 정제되지 않은 데이터가 원인이다. 보통 CAST(... AS XML) 또는 XML 컬럼에 INSERT할 때 나타난다.","fix":"XML로 변환하기 전에 텍스트에서 유효하지 않은 문자를 제거한다. 소스 인코딩(UTF-8/UTF-16)을 대상과 호환되게 하고 XML 선언에 올바른 인코딩을 명시한다. REPLACE/NCHAR로 제어 문자를 제거하거나, 애플리케이션 계층에서 표준 라이브러리로 XML을 생성하고 검증한다. 원시 텍스트를 저장해야 한다면 NVARCHAR(MAX)를 고려한다.","tags":["xml","parse","character","encoding","invalid","cast"]},{"code":"15151","title":"Cannot find the object/principal because it does not exist or you do not have permission (객체/보안 주체가 존재하지 않거나 권한이 없어 찾을 수 없음)","severity":"caution","meaning":"참조한 객체나 보안 주체(사용자, 역할, 로그인)가 존재하지 않거나, 이에 접근/작업할 권한이 없다. 보안상 SQL Server는 두 경우를 하나의 메시지로 통합한다.","cause":"GRANT/ALTER/DROP 같은 명령에서 존재하지 않는 사용자, 역할, 스키마 또는 객체 이름을 사용했거나, 잘못된 데이터베이스 컨텍스트에서 실행했거나(객체가 다른 DB에 있음), 작업에 필요한 권한이 없는 경우다. 오타, 대소문자, 스키마 접두사가 흔한 원인이다.","fix":"객체/보안 주체 이름의 철자가 정확한지, 올바른 데이터베이스에 있는지 확인한다. 사용자: SELECT name, type_desc FROM sys.database_principals; 객체는 sys.objects를 조회한다. 실행 계정에 필요한 권한(예: ALTER ANY USER, CONTROL)이 있는지 확인한다. 올바른 스키마 접두사(dbo.object 등)를 사용한다.","tags":["permission","principal","privilege","not found","grant","security"]},{"code":"4834","title":"You do not have permission to use the bulk load statement (bulk load 문을 사용할 권한 없음)","severity":"caution","meaning":"BULK INSERT(또는 OPENROWSET BULK)로 대량 로드를 실행하는 데 필요한 권한이 없다. 대량 로드는 파일 시스템과 서버 리소스에 직접 접근하므로 별도 권한이 필요하다.","cause":"로그인에 서버 수준 ADMINISTER BULK OPERATIONS 권한이 없거나 bulkadmin 서버 역할의 멤버가 아니다(Azure SQL Database에서는 대신 데이터베이스 수준 ADMINISTER DATABASE BULK OPERATIONS가 필요하다). 대상 테이블에 대한 INSERT 권한과 SQL Server 서비스 계정의 소스 파일 읽기 권한도 필요하다.","fix":"필요한 권한을 부여한다: GRANT ADMINISTER BULK OPERATIONS TO login_name; (서버 수준) 또는 사용자를 적절한 역할에 추가한다. Azure SQL DB는 ADMINISTER DATABASE BULK OPERATIONS를 사용한다. SQL Server 서비스 계정에 대상 파일/공유에 대한 읽기 권한이 있는지도 확인한다. 최소 권한 원칙에 따라 필요한 계정에만 부여한다.","tags":["bulk insert","privilege","permission","bulk load","bulkadmin","file"]},{"code":"10928","title":"Resource limit for the database has been reached (Azure SQL) (데이터베이스 리소스 한도 도달)","severity":"danger","meaning":"Azure SQL Database에서 데이터베이스가 리소스 한도(동시 요청/worker 또는 세션 등)에 도달해 새 요청이 거부되었다. 선택한 서비스 계층(DTU 또는 vCore)의 상한에 도달했음을 의미한다.","cause":"선택한 계층이 허용하는 최대 동시 요청(worker) 또는 세션 수를 초과했거나, 갑작스러운 부하 급증, worker 스레드를 소모하는 비효율적/장시간 쿼리, 너무 큰 커넥션 풀이 원인이다.","fix":"동시성을 줄인다: 쿼리를 최적화해 실행 시간을 단축하고, 불필요하게 열린 트랜잭션을 닫고, 커넥션 풀 크기를 적정하게 유지한다. 지속적인 부하에는 서비스 계층을 스케일 업한다(더 높은 DTU/vCore). sys.dm_db_resource_stats로 리소스 사용량을 모니터링한다. 재시도 가능한 오류에는 애플리케이션에서 지수 백오프 재시도를 구현한다.","tags":["azure","limit","resource","dtu","vcore","concurrency"]},{"code":"40544","title":"The database has reached its size quota (Azure SQL) (데이터베이스 크기 할당량 도달)","severity":"danger","meaning":"Azure SQL Database가 서비스 계층에 설정된 최대 데이터 크기 할당량에 도달했다. 데이터베이스가 더 이상 커질 수 없어 새 쓰기가 실패한다.","cause":"데이터가 계층 한도까지 증가했거나, 큰 인덱스/로그가 공간을 차지하거나, 선택한 계층(예: Basic/Standard S 시리즈)이 허용하는 MAXSIZE가 작은 경우다. 아카이브되지 않은 이력 데이터도 할당량을 빠르게 채운다.","fix":"공간을 확보한다: 불필요한 데이터를 삭제/아카이브하고, 사용하지 않는 인덱스를 삭제하고, 테이블을 압축한다(data compression). 영구 조치로 MAXSIZE를 늘리거나 상위 서비스 계층으로 이동한다: ALTER DATABASE db MODIFY (MAXSIZE = 250 GB); sys.database_files와 관련 DMV로 크기 사용량을 모니터링한다. 증가 추세를 바탕으로 확장을 미리 계획한다.","tags":["azure","size","quota","maxsize","size quota","scaling"]},{"code":"40613","title":"Database is not currently available (Azure SQL) (데이터베이스를 현재 사용할 수 없음)","severity":"danger","meaning":"Azure SQL에서 대상 데이터베이스에 현재 접근할 수 없다. 보통 일시적이며, 플랫폼이 데이터베이스를 이동, 재구성 또는 복구하는 동안 잠깐 나타난다.","cause":"Azure 측의 자동 스케일링/페일오버, 패치, 부하 분산 또는 리소스 재배치가 원인이며, 드물게 더 긴 플랫폼 이벤트일 수 있다. 데이터베이스가 다시 온라인이 될 때까지 연결이 거부된다.","fix":"잠시 기다린 후 재시도한다. 일시적인 오류다. 애플리케이션에 지수 백오프 자동 재시도를 구현한다. Azure SDK/재시도 라이브러리는 이 오류 코드를 일시적 오류로 인식한다. 문제가 지속되면 Azure Service Health와 리전 장애를 확인한다. 고가용성을 위해 geo-replication/failover group을 고려한다.","tags":["azure","temporary","transient","availability","retry","failover"]}]},"errors/mongodb-more2.json":{"engineId":"mongodb","engine":"MongoDB","order":5,"errors":[{"code":"31 RoleNotFound","title":"Role not found","severity":"caution","meaning":"참조한 역할이 MongoDB에 정의되어 있지 않다. 사용자에게 역할을 부여하거나, 역할을 갱신하거나, 역할의 권한을 조회할 때 존재하지 않는 역할 이름을 사용했다.","cause":"grantRolesToUser/createUser에서 철자가 틀렸거나 생성된 적 없는 역할 이름을 사용했거나, 잘못된 데이터베이스에서 역할을 찾았거나(역할은 특정 DB에 속한다), 아직 createRole로 정의하지 않은 사용자 정의(내장이 아닌) 역할을 사용한 경우다.","fix":"역할 이름과 역할이 속한 데이터베이스를 확인한다. 기존 역할 목록: db.getRoles({showBuiltinRoles: false}) 또는 모든 DB를 대상으로 하는 해당 관리 쿼리. 사용자 정의 역할이 없으면 먼저 생성한다: db.createRole({role:'reader', privileges:[...], roles:[]}). 부여 시 올바른 db를 지정한다: db.grantRolesToUser('app_user', [{role:'reader', db:'app'}]).","tags":["role","roles","authorization","access control","createrole","user"]},{"code":"263 OperationNotSupportedInTransaction","title":"Operation not supported inside a multi-document transaction (multi-document 트랜잭션 내에서 지원되지 않는 작업)","severity":"caution","meaning":"multi-document 트랜잭션 안에서 해당 컨텍스트에서 지원되지 않는 작업을 시도했다. 일부 명령(특히 특정 DDL 및 메타데이터 작업)은 트랜잭션 안에서 실행할 수 없다.","cause":"트랜잭션 안에서 컬렉션/인덱스 생성(버전에 따라 다름), 특정 count/list 계열 명령, 트랜잭션 세션에서 유효하지 않은 관리 명령 같은 지원되지 않는 작업을 실행한 경우다. 이전 MongoDB 버전은 트랜잭션 내 DDL을 지원하지 않는다(일부는 4.4+에서 도입되었다).","fix":"지원되지 않는 작업을 트랜잭션 밖으로 옮긴다(예: 트랜잭션 시작 전에 필요한 컬렉션/인덱스를 생성). MongoDB 버전을 확인한다. 4.4+는 트랜잭션 안에서 일부 DDL을 지원한다. 드라이버에서 해당 작업을 별도 세션으로 실행한다. 트랜잭션은 지원되는 CRUD 작업으로 제한한다.","tags":["transaction","multi-document transaction","ddl","unsupported","session","crud"]},{"code":"20 IllegalOperation","title":"Illegal operation","severity":"caution","meaning":"요청한 작업이 서버의 현재 상태나 대상 객체의 타입에 대해 유효하지 않거나 금지되어 있다. MongoDB는 논리적으로 허용되지 않는 작업을 거부할 때 이 일반 오류를 반환한다.","cause":"뷰에서 컬렉션 전용 작업을 시도하거나, 두 데이터베이스 간 잘못된 rename, 시스템 컬렉션에 대한 금지된 변경, replica set/sharding 구성이 허용하지 않는 관리 작업을 시도한 경우다. 컨텍스트에 따라 다르다.","fix":"오류 메시지의 세부 내용을 읽는다. 보통 어떤 작업이 왜 금지되었는지 명시되어 있다. 작업을 대상 객체의 타입에 맞게 조정한다(예: 뷰 대신 기반 컬렉션에서 작업). rename/move 작업은 MongoDB의 제약 사항(같은 DB 내 renameCollection 등)을 검토한다. 클러스터 상태(replica set/sharding)를 확인한다.","tags":["illegal","invalid","operation","view","rename","system"]},{"code":"48 NamespaceExists","title":"Namespace (collection) already exists","severity":"safe","meaning":"생성하려는 컬렉션(namespace)이 이미 존재한다. MongoDB가 존재함을 보고하므로 같은 이름과 옵션으로 새 컬렉션을 만들 필요가 없다.","cause":"기존 컬렉션을 createCollection으로 다시 생성하려 하거나(다른 옵션일 수 있음), 스키마/설정 스크립트가 여러 번 실행되었거나, 기존 이름을 capped collection이나 특수 옵션으로 재정의하려는 경우다.","fix":"보통 무시해도 안전하다. 컬렉션이 이미 존재한다는 의미다. 설정 스크립트를 멱등하게 만든다: 생성 전에 존재 여부를 확인한다(db.getCollectionNames().includes('name')). 옵션을 변경하려면 컬렉션을 삭제 후 재생성해야 한다(먼저 데이터를 백업한다): db.my_collection.drop() 후 createCollection.","tags":["namespace","collection","collection exists","createcollection","already exists","idempotent"]},{"code":"11601 Interrupted","title":"Operation was interrupted","severity":"caution","meaning":"실행 중인 작업이 중단되었다. 외부 개입이나 서버 상태 변화로 인해 MongoDB가 작업 완료 전에 종료했다.","cause":"관리자/도구가 db.killOp()로 작업을 종료했거나, 서버가 종료 중이거나, replica set에서 primary가 변경되었다(stepdown/failover). (maxTimeMS 초과 시에는 이 오류가 아니라 50 MaxTimeMSExpired가 반환된다.) 장시간 실행 쿼리가 이렇게 중지될 수 있다.","fix":"작업이 중단된 이유를 조사한다: currentOp/killOp 이력, 페일오버 이벤트, 서버 유지보수. 작업이 멱등하면 안전하게 재시도한다. 긴 쿼리를 최적화하고 단축하며 적절한 인덱스를 추가한다. 드라이버에서 일시적 오류 재시도 로직을 사용하고, replica set 페일오버 시 자동 재연결되도록 한다.","tags":["interrupted","interruption","killop","failover","stepdown","retry"]},{"code":"85 IndexOptionsConflict","title":"Index already exists with different options / different name (다른 옵션/이름의 인덱스가 이미 존재)","severity":"caution","meaning":"생성하려는 인덱스가 기존 인덱스와 충돌한다. 같은 이름의 인덱스가 다른 옵션을 가지고 있거나, 같은 키와 옵션의 동등한 인덱스가 다른 이름으로 이미 존재한다. MongoDB는 충돌하는 정의로 같은 인덱스를 다시 생성하도록 허용하지 않는다.","cause":"같은 이름과 키지만 다른 옵션(예: expireAfterSeconds, partialFilterExpression, collation)으로 인덱스를 생성하거나, 기존 인덱스와 동등한 인덱스를 다른 이름으로 재정의한 경우다('Index already exists with a different name'). 애플리케이션/마이그레이션 스크립트에서 인덱스 정의가 시간이 지나며 어긋나는 것이 전형적인 원인이다. 같은 이름에 다른 키를 사용하면 대신 86 IndexKeySpecsConflict가 발생한다.","fix":"기존 인덱스 확인: db.my_collection.getIndexes(). 옵션을 정말 변경하려면 이전 인덱스를 삭제하고 새로 생성한다: db.my_collection.dropIndex('index_name') 후 createIndex(...). 그렇지 않으면 정의를 기존 인덱스와 정확히 일치시키거나 새 인덱스에 다른 이름을 지정한다. 마이그레이션 스크립트를 멱등하게 만든다.","tags":["index","indexes","options","conflict","unique","createindex"]},{"code":"67 CannotCreateIndex","title":"Cannot create index","severity":"caution","meaning":"제약 조건이나 한도 때문에 인덱스 생성이 실패했다. 기존 데이터나 구성 때문에 MongoDB가 요청한 인덱스를 만들 수 없었다.","cause":"컬렉션당 허용되는 최대 인덱스 수(64)를 초과했거나, 키가 너무 크거나, 호환되지 않는 옵션 조합(예: TTL 인덱스에 부적합한 필드 타입)인 경우다. 참고: 기존 데이터에 중복이 있는 상태에서 unique 인덱스를 생성하면 이 코드가 아니라 E11000 DuplicateKey가 발생한다.","fix":"인덱스 수 한도에 도달했다면 사용하지 않는 인덱스를 삭제한다($indexStats로 사용량 분석). 키 크기/타입 호환성과 옵션 조합을 확인한다. 큰 컬렉션에서는 부하가 낮을 때 인덱스를 생성한다(4.2부터 인덱스 생성이 쓰기를 오래 블로킹하지 않으며 background 옵션은 무시된다).","tags":["index","indexes","unique","duplicate","limit","ttl"]},{"code":"27 IndexNotFound","title":"Index not found","severity":"safe","meaning":"참조한 인덱스를 컬렉션에서 찾을 수 없다. 보통 존재하지 않는 인덱스를 삭제하거나 이름을 바꾸려 할 때 나타난다.","cause":"dropIndex/hideIndex 같은 명령에 잘못된 인덱스 이름이나 키 지정을 주었거나, 인덱스가 이미 삭제되었거나, 자동 생성된 인덱스 이름이 예상과 다른 경우다(MongoDB는 field_1_field2_-1 같은 이름을 생성한다).","fix":"기존 인덱스와 정확한 이름 목록 확인: db.my_collection.getIndexes(). 올바른 이름이나 키 문서로 작업을 반복한다: db.my_collection.dropIndex({field: 1}) 또는 db.my_collection.dropIndex('field_1'). 삭제 스크립트는 인덱스가 없어도 실패하지 않도록 작성한다(존재 여부 확인).","tags":["index","indexes","dropindex","not found","getindexes","name"]},{"code":"79 UnknownReplWriteConcern","title":"Unknown replica set write concern","severity":"caution","meaning":"지정한 write concern을 인식할 수 없다. 작업에 사용한 w 값(사용자 정의/태그 write concern 이름 또는 숫자 값)이 replica set 구성에 정의되어 있지 않다.","cause":"getLastErrorModes에 정의되지 않은 사용자 정의 write concern 이름을 사용한 경우다(예: 'majority' 대신 오타, 또는 사용자 정의 태그 이름). 구성과 요청이 일치하지 않는다. 참고: 투표 멤버 수보다 큰 숫자 w를 요청하는 것은 다른 오류다(100 UnsatisfiableWriteConcern).","fix":"사용하는 write concern 이름이 replica set 설정에 정의되어 있는지 확인한다. 현재 구성 확인: rs.conf() (settings.getLastErrorModes). 사용자 정의 모드를 사용한다면 먼저 정의하거나 표준 값('majority' 또는 유효한 숫자 w)으로 전환한다. 드라이버/애플리케이션 연결 문자열의 writeConcern 설정을 검토한다.","tags":["write concern","write acknowledgement","replica set","majority","getlasterrormodes","config"]}]},"errors/redis-more2.json":{"engineId":"redis","engine":"Redis","order":6,"errors":[{"code":"ERR DEBUG command not allowed","title":"DEBUG command not allowed","severity":"caution","meaning":"DEBUG 명령을 실행하려 했지만 서버가 허용하지 않는다. DEBUG는 저수준의 위험한 진단 명령으로, 최신 Redis 버전에서는 보안상 기본적으로 비활성화되거나 제한된다.","cause":"Redis 7+에서는 enable-debug-command 설정 없이 DEBUG 하위 명령이 실행되지 않거나, 관리형 클라우드 Redis 서비스가 보통 명령을 완전히 비활성화하거나, ACL로 명령이 거부된 경우다. 테스트/진단 스크립트(예: DEBUG SLEEP, DEBUG OBJECT)에서 이 오류가 자주 발생한다.","fix":"운영 환경에서는 DEBUG를 사용하지 않는다. 로컬/테스트 환경에서 꼭 필요하다면 구성에서 활성화한다: redis.conf에 enable-debug-command yes(또는 'local'). 클라우드 서비스에서는 보통 명령을 활성화할 수 없으므로 동등한 정보를 위해 MEMORY USAGE, OBJECT ENCODING, INFO 같은 안전한 명령을 사용한다. ACL 제한이 적용된 경우 권한을 검토한다.","tags":["debug","command","prohibited","security","acl","config"]},{"code":"ERR Can't save in background: fork failed","title":"Background save failed: fork failed","severity":"danger","meaning":"Redis가 백그라운드 RDB 스냅샷이나 AOF rewrite를 위한 자식 프로세스 생성(fork)에 실패했다. 영속성 작업을 실행할 수 없어 데이터를 디스크에 쓸 수 없다.","cause":"운영체제가 fork에 충분한 메모리를 할당할 수 없다. 보통 overcommit_memory가 비활성화되어 있어 Redis가 사용 중인 메모리의 사본만큼을 예약하려 할 때 시스템이 거부한다. 물리/가상 메모리 부족이나 프로세스 수 한도도 원인이 될 수 있다.","fix":"Linux에서 메모리 overcommit을 활성화한다: sysctl vm.overcommit_memory=1 (영구 적용하려면 /etc/sysctl.conf에 추가). 서버 메모리를 늘리거나 Redis 메모리 사용량을 낮춘다(maxmemory). 충분한 swap 공간을 확보한다. 프로세스/파일 디스크립터 한도(ulimit)를 확인한다. 영속성이 중요하다면 이 설정 없이 운영 환경에 배포하지 않는다.","tags":["fork","rdb","aof","persistence","overcommit","memory"]},{"code":"NOPROTO unsupported protocol version","title":"Unsupported RESP protocol version","severity":"caution","meaning":"클라이언트가 서버가 지원하지 않는 RESP 프로토콜 버전을 요청했다. 보통 HELLO 명령으로 RESP3(버전 3)를 요청했는데 서버가 RESP2만 지원할 때 반환된다.","cause":"HELLO 3을 보냈지만 서버가 이전 Redis 버전(6.0 이전)이거나 RESP3를 지원하지 않는 경우, 또는 클라이언트 라이브러리가 기본으로 RESP3를 요청하는데 서버가 오래된 경우다. 버전 불일치다.","fix":"클라이언트가 RESP2를 사용하도록 구성하거나 HELLO로 유효한 버전을 요청한다(HELLO 2). 서버를 RESP3를 지원하는 버전(Redis 6+)으로 업그레이드한다. 클라이언트 라이브러리의 프로토콜 버전 설정을 검토한다. 대부분의 라이브러리는 자동으로 RESP2로 폴백할 수 있다. 서버 버전 확인: INFO server (redis_version).","tags":["resp","protocol","hello","resp3","version","compatibility"]},{"code":"ERR Unknown option or number of arguments for CONFIG SET","title":"Unknown option or number of arguments for CONFIG SET (older versions: Unsupported CONFIG parameter) (CONFIG SET의 알 수 없는 옵션 또는 인수 개수 오류)","severity":"caution","meaning":"CONFIG SET으로 변경하려는 구성 파라미터를 이 Redis 버전에서 인식하지 못하거나 인수 개수가 잘못되었다. Redis 7의 전체 메시지는 \"ERR Unknown option or number of arguments for CONFIG SET - '<name>'\"이며, 이전 버전은 \"ERR Unsupported CONFIG parameter\"를 반환한다. CONFIG GET은 알 수 없는 파라미터에 오류를 발생시키지 않고 빈 목록을 반환한다.","cause":"파라미터 이름의 철자가 틀렸거나, 다른 Redis 버전의 파라미터를 사용했거나(버전 간 파라미터가 추가/제거될 수 있음), 관리형 클라우드 서비스가 특정 파라미터 변경을 허용하지 않는 경우다. 이전 스크립트를 더 새로운/오래된 버전에서 실행하는 것이 전형적인 원인이다.","fix":"유효한 파라미터 이름을 확인한다. 패턴으로 현재 설정 목록을 확인한다(예: CONFIG GET 'maxmemory*'). 사용하는 Redis 버전의 문서에서 파라미터 목록을 확인하고, INFO server로 버전을 확인한다. 클라우드 서비스에서 제한된다면 제공자가 허용하는 파라미터 집합을 사용한다. 스크립트를 대상 버전에 맞게 갱신한다.","tags":["config","parameter","unsupported","version","config set","config get"]},{"code":"ERR MULTI calls can not be nested","title":"MULTI calls can not be nested","severity":"caution","meaning":"MULTI(트랜잭션) 블록이 이미 열려 있는 상태에서 MULTI를 다시 보냈다. Redis는 중첩 트랜잭션 블록을 지원하지 않으며 한 번에 하나의 MULTI만 활성화될 수 있다.","cause":"애플리케이션 로직이 이전 MULTI를 EXEC/DISCARD로 닫지 않고 새 MULTI를 시작했거나, 커넥션 풀의 같은 연결을 트랜잭션 상태를 제대로 초기화하지 않고 재사용했거나, 라이브러리 래퍼가 모르게 MULTI를 두 번 호출한 경우다.","fix":"모든 MULTI 블록을 하나의 EXEC(취소하려면 DISCARD)로 닫고, 이전 블록을 마친 후에만 새 블록을 연다. 코드 흐름에서 트랜잭션 시작/종료를 명확히 구분한다. 연결을 풀에 반환하기 전에 열린 트랜잭션이 없는지 확인한다. 중첩 로직이 필요하면 Lua 스크립트(EVAL)로 원자적 작업을 수행하는 것을 고려한다.","tags":["multi","transaction","nested","nested","exec","discard"]},{"code":"ERR Lua script attempted to access nonexistent global variable","title":"Lua script attempted to access a nonexistent global variable (Lua 스크립트가 존재하지 않는 전역 변수에 접근)","severity":"caution","meaning":"EVAL/EVALSHA로 실행한 Lua 스크립트가 정의되지 않은 전역 변수에 접근하려 했다. 스크립트 환경을 보호하기 위해 Redis는 전역 변수 생성/읽기를 제한하고 이런 접근을 오류로 반환한다.","cause":"스크립트에서 실수로 지역 변수 대신 전역 변수를 사용했거나(local 키워드 누락), 오타로 정의되지 않은 이름에 접근했거나, 전역 변수에 상태를 유지하려 한 경우다. 샌드박스화된 Redis Lua 환경은 이를 허용하지 않는다.","fix":"스크립트의 모든 변수를 local로 선언한다: local x = ... 오타를 수정하고 모든 변수가 사용 전에 정의되었는지 확인한다. 스크립트는 상태가 없어야 하며, 영구 상태는 전역 Lua 변수가 아니라 Redis 키(KEYS/redis.call)를 사용한다. 스크립트를 짧고 결정적으로 유지한다.","tags":["lua","eval","script","global","variable","local"]},{"code":"ERR unknown command","title":"Unknown command","severity":"caution","meaning":"Redis가 보낸 명령을 인식하지 못한다. 명령 이름이 틀렸거나, 이 버전에서 사용할 수 없거나, 비활성화/이름 변경되었을 수 있다.","cause":"명령 이름의 철자가 틀렸거나, 명령이 더 새로운 Redis 버전에서 추가되었거나(이전 서버에 없음), 모듈이 필요한 명령(예: RedisJSON, RediSearch)을 모듈 로드 없이 호출했거나, rename-command/ACL로 명령이 비활성화된 경우다. 때로는 클라이언트의 잘못된 인수 분리로 명령 이름이 손상된 것처럼 보이기도 한다.","fix":"명령 이름과 구문을 확인한다(COMMAND DOCS 명령 또는 문서). 서버 버전 확인: INFO server. 모듈 명령이라면 모듈이 로드되었는지 확인한다: MODULE LIST. rename-command로 명령이 숨겨졌다면 구성을 검토한다. 클라이언트 라이브러리가 명령을 올바르게 인코딩하는지 확인한다.","tags":["command","unknown","unknown","version","module","acl"]},{"code":"TRYAGAIN Multiple keys request during rehashing of slot","title":"Multiple keys request during rehashing of slot (cluster) (slot rehashing 중 다중 키 요청)","severity":"caution","meaning":"Redis Cluster에서 hash slot이 노드 간 이동 중(rehashing/resharding)일 때 같은 slot에 속한 여러 키를 포함하는 요청이 들어왔고, 일부 키는 이미 이동되었고 일부는 이동되지 않았다. 서버는 잠시 후 작업을 재시도하도록 요청한다.","cause":"slot 마이그레이션 진행 중에 다중 키 명령(MGET, MSET, 다중 키 Lua, 트랜잭션)을 실행한 경우다. 마이그레이션 중인 slot에서는 일부 키가 소스 노드에, 일부가 대상 노드에 있어 명령을 원자적으로 실행할 수 없다.","fix":"클라이언트에서 이 오류를 잡아 잠시 기다린 후 재시도한다(TRYAGAIN은 일시적이며 resharding이 끝나면 해소된다). 다중 키 작업에서는 hash tag로 모든 키가 같은 slot에 있도록 보장한다(예: {app_user123}.profile, {app_user123}.settings). 클러스터 리밸런싱(resharding)은 트래픽이 적은 시간에 수행한다.","tags":["cluster","slot","rehash","resharding","tryagain","hash tag"]},{"code":"ERR DISCARD without MULTI","title":"DISCARD without MULTI","severity":"caution","meaning":"열린 MULTI 블록이 없는 상태에서 DISCARD를 보냈다. DISCARD는 진행 중인 트랜잭션을 취소할 때만 사용하며, 취소할 것이 없으면 이 오류를 반환한다.","cause":"MULTI 없이 DISCARD를 호출했거나, 트랜잭션이 이미 EXEC/DISCARD로 닫힌 후 다시 호출했거나, 오류 처리 코드에서 무조건 DISCARD를 호출했거나, 연결 상태가 예상과 다른 경우다(풀에서 가져온 연결에 MULTI가 열려 있지 않음).","fix":"실제로 MULTI가 열려 있을 때만 DISCARD를 호출하고 코드에서 트랜잭션 상태를 추적한다. 오류 핸들러에서는 DISCARD 호출 전에 트랜잭션이 열려 있는지 확인한다. 라이브러리의 트랜잭션 래퍼를 사용해 상태를 자동으로 관리한다. 커넥션 풀의 상태가 깨끗하도록 보장한다.","tags":["discard","multi","transaction","cancel","state","error handling"]}]},"errors/elasticsearch-more2.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":7,"errors":[{"code":"search_context_missing_exception","title":"No search context found for id (scroll expired) (id에 대한 search context 없음)","severity":"caution","meaning":"scroll 쿼리가 사용하는 search context가 서버에 더 이상 존재하지 않는다. 보통 scroll context가 타임아웃되거나 정리된 후 저장해 둔 scroll_id로 계속 진행하려 할 때 나타난다.","cause":"scroll에 지정한 keep-alive가 만료되었거나(요청 간 간격이 너무 긺), context가 수동으로 정리되었거나(clear scroll), 노드 재시작 시 context가 유실된 경우다. scroll context는 노드 메모리에 유지되므로 수명이 제한된다.","fix":"모든 scroll 요청에 충분한 keep-alive를 전달하고(예: scroll=5m) 페이지 간 지연을 줄인다. 최신 Elasticsearch에서는 깊은 페이지네이션에 scroll 대신 search_after + Point in Time(PIT)을 사용한다. 확장성이 더 좋고 권장되는 방법이다. 완료되면 context를 해제해(clear scroll) 메모리가 낭비되지 않도록 한다.","tags":["scroll","search_after","pit","context","timeout","pagination"]},{"code":"aliases_not_found_exception","title":"Aliases missing","severity":"safe","meaning":"작업하려는 alias가 존재하지 않는다. 보통 존재하지 않는 alias를 제거하거나 그에 대해 작업할 때 반환된다.","cause":"alias 이름의 철자가 틀렸거나, alias가 이미 삭제되었거나, _aliases API의 remove 작업이 연결된 적 없는 alias를 지정한 경우다. 와일드카드 패턴에 일치하는 alias가 없을 때도 나타난다.","fix":"기존 alias 목록 확인: GET _alias, 특정 인덱스는 GET my-index/_alias. 올바른 이름으로 작업을 재시도한다. 삭제 스크립트는 alias가 없어도 실패하지 않도록 작성하고, _aliases 일괄 작업에서는 조건부로 처리한다. alias를 원자적으로 재지정하려면 하나의 _aliases 요청에서 add와 remove를 함께 사용한다.","tags":["alias","alias name","not found","aliases","index","routing"]},{"code":"snapshot_missing_exception","title":"Snapshot is missing","severity":"caution","meaning":"참조한 스냅샷을 지정한 리포지토리에서 찾을 수 없다. 복원, 삭제 또는 상태 요청에서 존재하지 않는 스냅샷 이름을 사용했음을 나타낸다.","cause":"스냅샷 이름의 철자가 틀렸거나, 스냅샷이 이미 삭제되었거나, 잘못된 리포지토리를 조회했거나, 백업이 완료되지 않은 경우다. 리포지토리 정의가 변경되었거나 접근할 수 없을 때도 비슷한 결과가 나온다.","fix":"리포지토리의 스냅샷 목록 확인: GET _snapshot/repo_name/_all (특정 이름은 GET _snapshot/repo_name/snapshot_name). 올바른 리포지토리와 스냅샷 이름으로 재시도한다. 리포지토리 정의 확인: GET _snapshot/repo_name. 백업 정책(SLM)이 성공적으로 실행되어 스냅샷을 생성하는지 확인한다.","tags":["snapshot","backup","repository","repo","restore","not found"]},{"code":"index_template_missing_exception","title":"Index template is missing","severity":"safe","meaning":"참조한 인덱스 템플릿을 찾을 수 없다. 템플릿을 조회, 삭제하거나 템플릿 기반 작업을 수행할 때 존재하지 않는 템플릿 이름을 사용했다.","cause":"템플릿 이름의 철자가 틀렸거나, 템플릿이 이미 삭제되었거나, legacy 템플릿 API와 composable index template API를 혼동했거나, 템플릿이 생성된 적이 없는 경우다. 설정 스크립트가 완료되지 않았을 때도 나타난다.","fix":"기존 템플릿 목록을 확인한다. composable 템플릿: GET _index_template 또는 GET _index_template/name. legacy 템플릿: GET _template. 올바른 API와 이름으로 재시도한다. 템플릿이 없으면 생성한다: PUT _index_template/name {...}. 삭제/갱신 스크립트를 멱등하게 만들어 템플릿이 없어도 실패하지 않도록 한다.","tags":["index template","template","composable","legacy","not found","mapping"]},{"code":"settings_exception","title":"Invalid settings","severity":"caution","meaning":"지정한 설정이 유효하지 않아 작업이 거부되었다. 인덱스 또는 클러스터 설정을 적용할 때 설정 이름, 값, 또는 해당 컨텍스트에서 변경 가능한지 여부에 문제가 있다.","cause":"알 수 없거나 철자가 틀린 설정 이름, 잘못된 타입이거나 허용 범위를 벗어난 값, 인덱스 생성 시에만 설정할 수 있는 정적 설정(예: number_of_shards)을 운영 중인 인덱스에서 변경하려는 시도, 제거되었거나 이름이 바뀐 설정 사용이 원인이다.","fix":"설정 이름과 값을 문서와 대조해 검증한다. 운영 중인 인덱스의 동적 설정 갱신: PUT my-index/_settings {\"index.number_of_replicas\": 2}. 정적 설정(예: number_of_shards)은 생성 시에만 설정되며, 변경하려면 reindex가 필요하다. 현재 설정 확인: GET my-index/_settings.","tags":["settings","setting","invalid","static","dynamic","reindex"]},{"code":"shard_not_found_exception","title":"Shard not found","severity":"danger","meaning":"대상 샤드를 찾을 수 없다. Elasticsearch가 지정한 인덱스의 해당 샤드에 도달할 수 없어 요청을 완료하지 못했다.","cause":"샤드가 아직 할당되지 않았거나(unassigned) 복구 중이거나, 노드가 클러스터를 떠나 샤드가 일시적으로 없거나, 샤드로 직접 라우팅된 저수준 요청이 더 이상 유효하지 않은 라우팅 정보를 가지고 있는 경우다. 클러스터 상태가 저하되었을 때 나타난다.","fix":"클러스터와 샤드 상태 확인: GET _cluster/health 및 GET _cat/shards?v. 샤드가 할당되지 않은 이유 확인: GET _cluster/allocation/explain. 노드가 유실되었다면 복구하고 디스크/할당 설정(watermark)을 검토한다. 복구가 진행 중이면 완료될 때까지 기다린다. 단일 노드가 유실되어도 데이터를 사용할 수 있도록 충분한 replica 수를 유지한다.","tags":["shard","shard","unassigned","unassigned shard","recovery","cluster health"]},{"code":"not_x_content_exception","title":"Compressor detection error / body is not valid content (압축 감지 오류 / 본문이 유효한 콘텐츠가 아님)","severity":"caution","meaning":"Elasticsearch가 요청 본문을 유효한 구조화 콘텐츠(JSON 같은 XContent)로 인식하지 못했다. 전송된 데이터가 예상 형식이 아니거나 손상되었다.","cause":"요청 본문이 유효하지 않은 JSON이거나, Content-Type 헤더가 잘못되었거나 없거나(application/json 미전송), 본문이 실수로 압축/바이너리로 전송되었거나, 줄바꿈 구분 JSON(NDJSON)을 기대하는 _bulk 같은 엔드포인트에 일반 JSON을 보낸 경우다. 이스케이프/인코딩 오류도 원인이 된다.","fix":"요청 본문이 유효한 JSON인지 확인하고 Content-Type: application/json 헤더를 보낸다. _bulk 엔드포인트에는 각 줄이 별도 JSON 객체이고 마지막 줄이 줄바꿈으로 끝나는 NDJSON을 사용한다(Content-Type: application/x-ndjson). 클라이언트 라이브러리가 본문을 올바르게 직렬화하는지 확인한다. 압축을 사용한다면 적절한 Content-Encoding 헤더를 보낸다.","tags":["json","xcontent","content-type","bulk","ndjson","request body"]},{"code":"document_source_missing_exception","title":"Document source is missing","severity":"caution","meaning":"작업에 문서의 _source 필드가 필요하지만 저장되어 있지 않다. source가 비활성화된 인덱스에서 _source가 필요한 작업(예: 부분 업데이트, source 조회)을 시도했다.","cause":"인덱스 매핑에 _source: {enabled: false}가 설정되어 있다. 문서의 원본 JSON이 저장되지 않으므로 부분 업데이트, reindex, _source를 반환하는 쿼리가 동작할 수 없다. 공간 절약을 위해 source를 비활성화한 구성이 이 문제를 일으킨다.","fix":"_source가 필요한 작업에는 매핑에서 source가 활성화되어 있어야 한다. 새 인덱스를 생성할 때 _source를 활성화된 상태로 둔다(기본적으로 활성화되어 있다). 기존 인덱스에서 _source가 비활성화되어 있다면 업데이트 대신 전체 문서를 다시 인덱싱한다(index API로 덮어쓰기). 공간 절약을 위해서는 source를 완전히 비활성화하는 대신 _source 필터링(includes/excludes) 또는 synthetic _source를 고려한다.","tags":["_source","source","update","mapping","reindex","document source"]}]},"errors/oracle-more3.json":{"engineId":"oracle","engine":"Oracle","order":1,"errors":[{"code":"ORA-00845","title":"MEMORY_TARGET not supported on this system (이 시스템에서 MEMORY_TARGET 미지원)","severity":"danger","meaning":"MEMORY_TARGET / MEMORY_MAX_TARGET으로 요청한 자동 메모리 관리를 운영체제의 공유 메모리 구성이 지원하지 않아 인스턴스를 시작할 수 없다. Linux에서는 보통 /dev/shm이 요청한 메모리 대상보다 작기 때문이다.","cause":"MEMORY_TARGET이 /dev/shm(tmpfs로 마운트)보다 크게 설정되었거나, HugePages가 활성화된 상태에서 MEMORY_TARGET(AMM)을 사용한 경우다. AMM은 HugePages와 호환되지 않지만 ASMM(SGA_TARGET)은 호환된다. Oracle Automatic Memory Management는 /dev/shm에 파일을 생성하는 방식으로 동작하므로 이 공간이 부족하면 인스턴스가 시작되지 않는다.","fix":"/dev/shm을 MEMORY_TARGET보다 크게 만든다: tmpfs를 다시 마운트하고(mount -o remount,size=8G /dev/shm) /etc/fstab에 영구 반영한다. 또는 AMM 대신 개별 설정을 사용한다: MEMORY_TARGET과 MEMORY_MAX_TARGET을 0으로 설정하고 SGA_TARGET과 PGA_AGGREGATE_TARGET을 정의한다. HugePages를 사용한다면 AMM을 비활성화하고 ASMM(SGA_TARGET)을 사용한다.","tags":["memory_target","memory","amm","dev/shm","hugepages","startup"]},{"code":"ORA-27102","title":"out of memory","severity":"danger","meaning":"시작 중에 Oracle 인스턴스가 운영체제에 요청한 공유 메모리(SGA) 세그먼트를 할당하지 못했다. 보통 ORA-27102 및 하위 수준 OSD/Linux 오류 코드(예: Linux-x86_64 Error: 28: No space left on device)와 함께 나타난다.","cause":"SGA에 요청한 메모리가 커널의 공유 메모리 한도(shmmax, shmall)를 초과하거나, 서버의 물리 RAM/swap이 부족하거나, HugePages 구성이 없거나 부족해 큰 SGA를 할당할 수 없는 경우다. SGA_MAX_SIZE를 늘린 후에 흔하다.","fix":"커널 공유 메모리 파라미터를 늘린다: /etc/sysctl.conf에서 kernel.shmmax와 kernel.shmall을 SGA를 수용하도록 설정하고 sysctl -p를 실행한다. HugePages를 사용한다면 SGA를 기준으로 vm.nr_hugepages를 계산해 설정한다. 서버의 전체 RAM과 swap을 확인하고, 필요하면 SGA_MAX_SIZE / SGA_TARGET을 낮춘다.","tags":["sga","memory","shmmax","hugepages","startup","osd"]},{"code":"ORA-04030","title":"out of process memory when trying to allocate N bytes (N바이트 할당 중 프로세스 메모리 부족)","severity":"danger","meaning":"서버 프로세스가 PGA(Program Global Area), 즉 프로세스 전용 메모리에 공간을 할당하지 못했다. SGA의 공유 메모리와 달리 단일 세션/프로세스 자체 메모리가 고갈된 것이다.","cause":"매우 큰 sort/hash 작업, 과도하게 큰 PL/SQL 컬렉션, 통제되지 않는 루프에서 누적되는 변수, 또는 운영체제 프로세스 메모리 한도(ulimit -v/-d, vm.max_map_count, 주소 공간)로 프로세스 메모리가 고갈되었다. 32비트 인스턴스에서는 주소 공간 한도도 원인이 될 수 있다.","fix":"오류 두 번째 줄의 할당 범주(예: 'pga heap', 'koh-kghu sessi')를 확인해 메모리를 소비하는 대상을 찾는다. PGA_AGGREGATE_LIMIT / PGA_AGGREGATE_TARGET을 검토한다. 운영체제 한도를 확인한다(ulimit -a; oracle 사용자의 memlock, as, stack). 쿼리/PL/SQL을 최적화하고 대량 처리 배치 크기를 줄인다. 필요하면 trace 파일과 함께 Oracle Support에 문의한다.","tags":["pga","process memory","memory","ulimit","sort","plsql"]},{"code":"ORA-12537","title":"TNS:connection closed","severity":"danger","meaning":"통신 채널이 예기치 않게 닫혀 클라이언트와 서버 간 TNS 연결이 종료되었다. 연결을 수립하는 중이거나 직후에 세션이 끊긴다.","cause":"리스너가 클라이언트를 서버 프로세스로 넘겼지만 프로세스를 생성할 수 없었거나 즉시 종료되었다: 인스턴스 크래시, PROCESSES 한도 도달, 운영체제 리소스 부족, 방화벽/중간 계층의 연결 차단, 또는 sqlnet.ora의 TCP.VALIDNODE_CHECKING/TCP.INVITED_NODES 규칙이 클라이언트를 거부한 경우다. 때로는 클라이언트와 서버 사이의 유휴 방화벽이 연결을 끊는다.","fix":"listener.log와 sqlnet.log를 확인한다. PROCESSES/SESSIONS 한도 확인: SELECT * FROM v$resource_limit WHERE resource_name IN ('processes','sessions'); 서버 측 sqlnet.ora의 TCP.INVITED_NODES/TCP.EXCLUDED_NODES 목록이 클라이언트 IP를 거부하지 않는지 확인한다(로그온 버전 불일치는 이 오류가 아니라 ORA-28040을 발생시킨다). 방화벽의 유휴 타임아웃을 늘리거나 SQLNET.EXPIRE_TIME으로 keepalive를 보낸다. alert.log에서 인스턴스 크래시 기록을 확인한다.","tags":["tns","connection","listener","processes","firewall","sqlnet"]},{"code":"ORA-03135","title":"connection lost contact","severity":"danger","meaning":"이전에 수립되어 정상 동작하던 세션이 서버와의 연결을 잃었다. 양쪽이 더 이상 서로에게 도달할 수 없어 장시간 실행 쿼리나 유휴 연결이 끊겼다.","cause":"서버 프로세스가 예기치 않게 종료되었거나(ORA-00600/07445, kill, 리소스 한도), RESOURCE_LIMIT 프로파일이 세션을 종료했거나, 방화벽/로드 밸런서가 유휴 연결을 끊었거나, 네트워크 장애/패킷 손실이 있었다. 보통 오랫동안 응답하지 않는 쿼리나 유휴 연결에서 발생한다.","fix":"alert.log와 관련 trace 파일에서 프로세스가 종료된 이유를 찾는다(ORA-00600/ORA-07445?). 네트워크 측에서는 방화벽 유휴 타임아웃을 늘리고 sqlnet.ora의 SQLNET.EXPIRE_TIME으로 dead connection detection을 활성화한다. 사용자 프로파일의 CONNECT_TIME/IDLE_TIME 한도를 확인한다. 서버 리소스 압박(메모리, CPU, PROCESSES)을 점검한다.","tags":["connection","sqlnet","idle","firewall","dead connection","network"]},{"code":"ORA-01033","title":"ORACLE initialization or shutdown in progress (ORACLE 초기화 또는 종료 진행 중)","severity":"danger","meaning":"접속하려는 데이터베이스 인스턴스가 시작(mount/open) 또는 종료 중이다. 이 중간 상태에서는 일반 사용자 연결을 받지 않는다.","cause":"인스턴스가 NOMOUNT/MOUNT 상태에 멈춰 있거나(예: 미디어 복구 대기), SHUTDOWN이 진행 중이거나, 오류로 인해 OPEN에 도달하기 전에 데이터베이스가 행 상태다. 막 시작된 인스턴스에 시작이 완료되기 전에 접속해도 이 오류가 반환된다.","fix":"SYSDBA로 접속해 상태를 확인한다: sqlplus / as sysdba; 이후 SELECT status FROM v$instance; 및 SELECT open_mode FROM v$database; 인스턴스 열기 시도: ALTER DATABASE OPEN; 복구가 필요하면 RECOVER DATABASE;를 실행한 후 연다. alert.log에서 open을 막는 근본 오류(데이터 파일/redo 누락 또는 손상)를 찾는다. 종료가 멈췄다면 SHUTDOWN ABORT 후 STARTUP을 시도한다.","tags":["startup","shutdown","mount","open","recovery","instance"]},{"code":"ORA-00600","title":"internal error code, arguments: [...]","severity":"danger","meaning":"Oracle 커널의 내부 일관성 검사가 실패했다. 커널 코드가 예상치 못한 조건을 만났음을 나타내는 일반 내부 오류이며, 대괄호 안의 인수가 어떤 코드 경로가 트리거되었는지 식별한다.","cause":"블록/데이터 손상, 수정되지 않은 Oracle 소프트웨어 버그, 일관되지 않은 패치 수준, 메모리/하드웨어 문제, 예상치 못한 엣지 케이스가 원인이 될 수 있다. 첫 번째 인수(예: [ktbdchk1])는 문제가 어떤 계층과 관련되는지 나타내며 지원 분석의 핵심이다.","fix":"이 오류는 거의 항상 Oracle Support와 함께 처리해야 한다. 생성된 trace 파일과 alert.log 항목을 보존하고, My Oracle Support에서 ORA-600 Troubleshooter와 'ORA-600 Lookup Tool'로 첫 번째 인수를 검색한다. 적절한 패치/PSU/RU를 적용한다. 손상이 의심되면 DBVERIFY/RMAN VALIDATE로 블록 무결성을 확인한다. 중요한 환경에서는 SR을 열지 않고 운영 환경에서 시험하지 않는다.","tags":["internal","ora-600","kernel","bug","trace","support"]},{"code":"ORA-07445","title":"exception encountered: core dump","severity":"danger","meaning":"Oracle 서버 프로세스가 운영체제 수준 예외(예: SIGSEGV segmentation fault, access violation)를 받은 후 예기치 않게 크래시되어 core dump를 생성했다. ORA-00600과 달리 운영체제의 시그널로 오류가 트리거된다.","cause":"보통 Oracle 소프트웨어 버그, 메모리 손상, 손상된 데이터/인덱스 블록, 호환되지 않는 라이브러리, 하드웨어/메모리 장애가 원인이다. 메시지의 함수 이름과 시그널 유형(예: SIGSEGV)이 크래시된 코드 경로를 식별한다.","fix":"생성된 trace 및 incident 파일과 alert.log를 보존하고, 함수 이름 + 시그널로 My Oracle Support를 검색한다. 보통 패치가 필요하다. 하드웨어/메모리 테스트를 실행하고 운영체제와 Oracle 패치 수준을 맞춘다. 손상이 의심되면 RMAN VALIDATE / DBVERIFY를 실행한다. 재발하면 SR을 열고 core dump를 Support에 보낸다.","tags":["ora-7445","core dump","sigsegv","kernel","bug","trace"]},{"code":"ORA-01102","title":"cannot mount database in EXCLUSIVE mode","severity":"danger","meaning":"다른 인스턴스(또는 이전 인스턴스의 잔여 파일)가 여전히 락을 보유한 것으로 보여 데이터베이스를 EXCLUSIVE 모드로 마운트할 수 없다. 인스턴스가 MOUNT 단계에서 멈춘다.","cause":"이전 인스턴스가 정상 종료되지 않아 락 파일($ORACLE_HOME/dbs 아래의 lk<SID>, 또는 잔여 공유 메모리/세마포어)이 남아 있거나, 다른 인스턴스가 이미 같은 데이터베이스에 연결되어 있거나, RAC가 아닌 설치에서 두 번째 인스턴스가 시작된 경우다. 이전 인스턴스의 잔여 프로세스/공유 메모리나 오래된 lk<SID> 파일이 가장 흔한 원인이다.","fix":"실제로 다른 인스턴스가 실행 중이지 않은지 확인한다: ps -ef | grep pmon 및 ipcs -m (공유 메모리 세그먼트). 잔여물이 있으면 인스턴스를 ABORT하고 정리하며, 필요하면 ipcrm으로 잔여 세그먼트/세마포어를 제거한다. $ORACLE_HOME/dbs 아래의 lk<SID> 락 파일을 삭제한다(인스턴스가 확실히 내려간 경우에만). 이후 STARTUP MOUNT를 시도한다. RAC에서는 CLUSTER_DATABASE와 노드 상태를 확인한다.","tags":["mount","exclusive","lock","startup","pmon","instance"]},{"code":"ORA-29913","title":"error in executing ODCIEXTTABLEOPEN callout (ODCIEXTTABLEOPEN callout 실행 오류)","severity":"caution","meaning":"외부 테이블에 접근할 때 소스 파일을 읽거나 파싱하는 중 오류가 발생했다. Oracle이 외부 테이블 데이터를 가져오기 위해 호출하는 access driver(ORACLE_LOADER / ORACLE_DATAPUMP) 루틴에서 실패했다.","cause":"보통 DIRECTORY 객체가 가리키는 경로에 파일이 없거나, Oracle OS 사용자에게 파일/디렉터리 읽기 권한이 없거나, log/bad 파일 쓰기 권한이 없거나, access parameter(필드 구분자, 레코드 형식)가 잘못된 경우다. ORA-29400, KUP-04xxx 같은 하위 오류가 자주 함께 나타난다.","fix":"하위 오류(KUP-xxxx)를 읽어 근본 원인을 파악한다. DIRECTORY 경로와 파일 존재 여부 확인: SELECT directory_name, directory_path FROM all_directories; Oracle OS 사용자에게 파일과 디렉터리 읽기 권한, log/bad 디렉터리 쓰기 권한을 부여한다. 외부 테이블 정의의 ACCESS PARAMETERS 절을 소스 파일 형식에 맞춘다. 필요하면 DIRECTORY에 대한 READ/WRITE 권한을 확인한다.","tags":["external table","external table","directory","oracle_loader","permission","kup"]}]},"errors/postgres-more3.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"errors":[{"code":"08001","title":"sqlclient_unable_to_establish_sqlconnection","severity":"danger","meaning":"클라이언트가 PostgreSQL 서버에 연결을 수립하지 못했다. 연결이 세션이 되기 전에 네트워크/주소 수준에서 실패했다.","cause":"잘못된 host/port, 서버가 수신하지 않는 주소(listen_addresses = 'localhost'인데 원격 접속), 서버 다운/접근 불가, 방화벽의 포트 차단(기본 5432), DNS 해석 실패가 원인이다. 잘못 구성된 커넥션 풀도 원인이 될 수 있다.","fix":"서버가 실행 중이고 올바른 주소에서 수신하는지 확인한다: postgresql.conf의 listen_addresses = '*'(또는 해당 IP)와 port 설정을 확인한 후 서버를 재시작한다(listen_addresses와 port는 재시작이 필요하며 reload로는 부족하다). 클라이언트에서 접근 테스트: psql -h host -p 5432 -U app_user. 방화벽에서 5432/tcp를 연다. host/port/DNS 값과 연결 문자열을 검토한다.","tags":["connection","connection","network","listen_addresses","port","firewall"]},{"code":"08004","title":"sqlserver_rejected_establishment_of_sqlconnection","severity":"danger","meaning":"서버에 도달했지만 서버가 연결 수립을 거부했다. 네트워크 접근은 가능하지만 서버가 정책상 이 연결을 받아들이지 않았다.","cause":"PostgreSQL 서버는 이 코드를 거의 생성하지 않으며 주로 클라이언트 드라이버나 커넥션 풀이 보고한다. 유사한 거부에 대한 서버의 실제 코드는 다르다: pg_hba.conf 거부('no pg_hba.conf entry for host', reject 방식)는 28000(invalid_authorization_specification), max_connections 소진은 53300(too_many_connections), 시작/종료 중 거부된 연결은 57P03을 반환한다.","fix":"서버 로그에서 거부의 실제 SQLSTATE와 이유를 확인한다. pg_hba 거부(28000)라면 클라이언트 IP, 사용자, 데이터베이스에 맞는 줄을 추가하고(예: host all all 10.0.0.0/24 scram-sha-256) SELECT pg_reload_conf();로 reload한다. 연결 포화(53300)라면 SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';로 클라이언트 연결 수를 max_connections와 비교한다.","tags":["connection","pg_hba","rejected","authentication","max_connections","reject"]},{"code":"57P01","title":"admin_shutdown","severity":"danger","meaning":"관리자 명령으로 인해 활성 연결이 종료되었다. 서버가 종료 중이거나 세션이 수동으로 종료될 때 클라이언트가 이 오류를 받는다('terminating connection due to administrator command').","cause":"DBA가 pg_terminate_backend()로 세션을 종료했거나, 서버가 fast/immediate 모드로 종료 중이거나(smart shutdown은 세션을 종료하지 않고 끝날 때까지 기다린다), 구성 변경/계획된 유지보수를 위해 기존 연결이 끊긴 경우다. 일부 페일오버 시나리오에서는 이전 primary도 이렇게 연결을 끊는다.","fix":"보통 영구적인 오류가 아니며 애플리케이션이 재연결(재시도)해야 한다. 예상치 못한 경우라면 서버 로그에서 shutdown/terminate 이유를 조사한다. 계획된 유지보수에 대비해 애플리케이션에 자동 재연결과 커넥션 풀 상태 점검을 구현한다. 서버 로그는 누가 세션을 종료했는지 보여 주지 않으므로, pg_terminate_backend 호출을 추적하려면 log_statement(또는 pgaudit)로 감사 기록을 남긴다.","tags":["shutdown","terminate","connection","maintenance","failover","retry"]},{"code":"53400","title":"configuration_limit_exceeded","severity":"danger","meaning":"구성 한도를 초과해 작업이 실패했다. 정의된 한도(예: temp_file_limit, replication slot 또는 WAL sender 수) 때문에 서버가 요청을 거부했다.","cause":"전형적인 예: 쿼리의 임시 파일 사용량이 temp_file_limit를 초과('temporary file size exceeds temp_file_limit'), 모든 replication slot이 사용 중('all replication slots are in use', max_replication_slots), max_wal_senders 한도 초과. 참고: max_prepared_transactions 소진은 53200을, prepared transaction 비활성화(값 0)는 55000을 반환한다.","fix":"오류 메시지에 어떤 한도를 초과했는지 나와 있다. temp_file_limit의 경우 쿼리를 최적화하고(EXPLAIN ANALYZE로 디스크로 넘치는 sort/hash 단계를 찾는다), 필요하면 세션 수준에서 work_mem을 늘리거나 한도를 의도적으로 높인다. slot이 소진되었다면 소유자를 확인한 후 사용하지 않는 slot을 정리하거나: SELECT slot_name, slot_type, active FROM pg_replication_slots; max_replication_slots / max_wal_senders를 늘리고 서버를 재시작한다.","tags":["limit","temp_file_limit","replication slot","max_wal_senders","configuration","resource"]},{"code":"XX001","title":"data_corrupted","severity":"danger","meaning":"PostgreSQL이 테이블/heap 페이지에서 데이터 손상을 감지했다. 읽은 블록의 내용이 일관되지 않아('invalid page in block') 쿼리를 안전하게 완료할 수 없다.","cause":"디스크/스토리지 장애, 파일 시스템 손상, 갑작스러운 정전 시 fsync 보장이 지켜지지 않음, 결함 있는 하드웨어(RAM/컨트롤러), 또는 드물게 소프트웨어 버그가 원인이다. 체크섬이 활성화되어 있으면 손상이 더 일찍 감지된다.","fix":"먼저 하드웨어/스토리지를 확인하고, 가능하면 즉시 물리 백업(PITR)으로 되돌릴 계획을 세운다. 정상 백업이 있으면 복원한다. 복구를 위해 데이터 손실을 감수하고 최후의 수단으로만 zero_damaged_pages = on으로 손상된 페이지를 0으로 채우고 읽을 수 있는 데이터를 내보낼 수 있다(데이터 손실이 발생한다). data_checksums를 활성화하고(기존 클러스터에서는 PG12+에서 서버 정지 상태로 pg_checksums --enable), RAM 테스트를 실행하며 정기적으로 PITR 백업을 수행한다.","tags":["corruption","corruption","checksum","page","hardware","pitr"]},{"code":"XX002","title":"index_corrupted","severity":"danger","meaning":"인덱스 구조에서 손상이 감지되었다. 인덱스가 가리키는 데이터가 heap의 실제 행과 일치하지 않아 인덱스를 사용하는 쿼리가 잘못된 결과를 반환하거나 실패할 수 있다.","cause":"하드웨어/스토리지 장애, 운영체제 glibc/ICU 업그레이드 후 collation(정렬 규칙) 변경으로 깨진 B-tree 순서, 또는 갑작스러운 장애가 원인이다. 특히 운영체제 collation 업데이트는 텍스트 인덱스에 조용한 손상을 일으킨다.","fix":"손상된 인덱스를 재생성한다: REINDEX INDEX idx_name; 또는 REINDEX TABLE my_table; 큰 테이블에서는 락을 줄이기 위해 REINDEX INDEX CONCURRENTLY를 사용한다. collation이 원인이면 영향을 받은 모든 텍스트 인덱스를 재생성하고 amcheck 확장(bt_index_check)으로 검증한다. 근본 원인이 하드웨어라면 스토리지를 교체한다.","tags":["index","index","corruption","reindex","collation","amcheck"]},{"code":"25001","title":"active_sql_transaction","severity":"caution","meaning":"트랜잭션 밖에서만, 또는 트랜잭션 시작 시점에만 실행할 수 있는 명령을 트랜잭션이 활성 상태일 때 호출했다. 일부 명령은 트랜잭션 블록 안에서 실행되지 않는다.","cause":"SET TRANSACTION ISOLATION LEVEL 같은 설정은 블록의 첫 쿼리 전에 실행해야 한다('SET TRANSACTION ISOLATION LEVEL must be called before any query'). 마찬가지로 일부 명령은 열린 BEGIN/트랜잭션 안에서 거부된다. 보통 애플리케이션이 autocommit을 비활성화해 트랜잭션을 암묵적으로 시작하는 것이 원인이다.","fix":"트랜잭션 수준 설정은 쿼리가 실행되기 전, 트랜잭션의 맨 처음에 실행한다: BEGIN; SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; ... 클라이언트 라이브러리가 암묵적 트랜잭션을 여는지 확인한다(autocommit 설정). 트랜잭션 블록 안에서 실행할 수 없는 명령은 먼저 COMMIT/ROLLBACK으로 현재 트랜잭션을 닫는다.","tags":["transaction","transaction","isolation","set transaction","autocommit","begin"]},{"code":"42P16","title":"invalid_table_definition","severity":"caution","meaning":"테이블 정의가 유효하지 않아 DDL 명령이 거부되었다. 요청한 테이블/컬럼 구조가 PostgreSQL 규칙을 위반한다.","cause":"기본 키를 두 개 이상 정의('multiple primary keys for table are not allowed'), 상속과 충돌하는 정의, generated/identity 컬럼 제약, ALTER TABLE로 만들 수 없는 구조로의 변경 같은 경우다. 자동 생성된 마이그레이션 스크립트에서 흔하다.","fix":"오류 메시지의 구체적인 이유를 읽는다. PRIMARY KEY는 하나만 정의하고, 다중 컬럼 키가 필요하면 PRIMARY KEY (a, b) 형태의 복합 키를 사용한다. identity/generated 컬럼 규칙을 검토한다. \\d my_table로 현재 테이블 정의를 확인하고 마이그레이션 스크립트를 수정한다.","tags":["ddl","table","primary key","definition","alter table","migration"]},{"code":"P0001","title":"raise_exception","severity":"caution","meaning":"PL/pgSQL 함수나 트리거가 RAISE EXCEPTION으로 의도적으로 오류를 발생시켰다. 이 오류는 데이터베이스 엔진이 아니라 사용자(또는 라이브러리)가 작성한 비즈니스 규칙 코드에서 생성된다.","cause":"비즈니스 로직의 검증이 실패하면 개발자가 RAISE EXCEPTION 'message' USING ERRCODE = ...;를 호출했다. 예를 들어 트리거가 허용되지 않는 갱신을 막거나 일관성 규칙을 적용하기 위해 사용자 정의 오류를 발생시킨다.","fix":"오류 메시지와 HINT/DETAIL 필드가 함수가 무엇을 거부했는지 설명하므로 그에 맞게 비즈니스 규칙을 충족시킨다. 원인을 찾으려면 관련 함수/트리거 코드(RAISE 문)를 확인한다. 특정 SQLSTATE가 필요하면 일반적인 P0001 대신 RAISE ... USING ERRCODE로 의미 있는 코드를 지정해 애플리케이션이 구분할 수 있게 한다.","tags":["plpgsql","raise","trigger","function","business rule","exception"]}]},"errors/mysql-more3.json":{"engineId":"mysql","engine":"MySQL","order":3,"errors":[{"code":"1049","title":"Unknown database 'name'","severity":"caution","meaning":"접속하거나 사용하려는 데이터베이스(스키마)가 서버에 존재하지 않는다. USE 문이나 연결 문자열의 기본 데이터베이스 이름이 존재하지 않는 스키마를 가리킨다.","cause":"데이터베이스가 생성된 적이 없거나, 이름의 철자가 틀렸거나(대소문자 구분은 운영체제에 따라 다를 수 있음), 삭제되었거나, 애플리케이션이 잘못된 환경(dev/prod)에 연결하는 경우다. 새 서버로 이전할 때 스키마가 마이그레이션되지 않은 것도 흔한 원인이다.","fix":"기존 데이터베이스 목록 확인: SHOW DATABASES; 이름의 철자와 대소문자가 일치하는지 확인한다(lower_case_table_names 설정이 중요하다). 존재하지 않으면 생성한다: CREATE DATABASE database_name CHARACTER SET utf8mb4; 또는 백업에서 복원한다. 애플리케이션 연결 구성에서 대상 환경을 확인한다.","tags":["database","database","schema","use","name","connection"]},{"code":"1364","title":"Field 'name' doesn't have a default value (필드에 기본값 없음)","severity":"caution","meaning":"DEFAULT 없이 NOT NULL로 정의된 컬럼에 값을 지정하지 않고 행을 삽입했다. strict SQL 모드에서 MySQL은 누락된 필수 컬럼을 자동으로 채울 수 없어 오류를 발생시킨다.","cause":"필수(NOT NULL, DEFAULT 없음) 컬럼이 INSERT에서 누락되었고 sql_mode에 STRICT_TRANS_TABLES/STRICT_ALL_TABLES가 활성화되어 있다. 이전의 관대한 모드용으로 작성된 애플리케이션을 strict 모드가 활성화된 서버로 옮기면 이 오류가 발생한다.","fix":"INSERT에 누락된 컬럼의 값을 추가하거나 컬럼에 적절한 DEFAULT를 정의한다: ALTER TABLE my_table MODIFY my_column ... NOT NULL DEFAULT 'value'; 컬럼이 실제로 선택 사항이라면 NULL을 허용한다. 임시/호환성 우회책으로 sql_mode를 완화할 수 있지만 데이터 품질을 위해 strict 모드 유지를 권장한다.","tags":["insert","not null","default","strict mode","sql_mode","column"]},{"code":"1206","title":"The total number of locks exceeds the lock table size (전체 락 수가 락 테이블 크기 초과)","severity":"danger","meaning":"InnoDB가 보유한 락에 할당된 메모리(buffer pool 안의 락 구조)를 소진했다. 단일 트랜잭션이 너무 많은 행/gap 락을 누적할 때 발생하며 트랜잭션은 롤백된다.","cause":"매우 큰 단일 트랜잭션: 수백만 행을 한 번에 UPDATE/DELETE, 인덱스 없는 WHERE 절로 테이블 전체 락, 워크로드에 비해 너무 작은 innodb_buffer_pool_size가 원인이다. 대규모 배치 작업이 전형적인 트리거다.","fix":"큰 DML을 작은 배치로 나눈다(예: 루프에서 LIMIT으로 한 번에 10,000행씩 삭제/갱신하고 배치마다 COMMIT). WHERE 컬럼에 인덱스를 만들어 락이 걸리는 행 수를 줄인다. 영구 조치로 innodb_buffer_pool_size를 늘린다(락 구조가 여기서 할당된다). 불필요하게 긴 트랜잭션을 피한다.","tags":["innodb","lock","lock","buffer pool","batch","memory"]},{"code":"1236","title":"Got fatal error 1236 from master when reading data from binary log (binary log 읽기 중 master로부터 치명적 오류 1236 수신)","severity":"danger","meaning":"replica(slave) 서버가 source(master)의 binary log에서 데이터를 읽는 중 치명적 오류를 받아 복제가 중지되었다. replica가 source의 binlog에서 예상하는 위치를 찾을 수 없다.","cause":"source에서 필요한 binary log 파일이 삭제되었거나(binlog_expire_logs_seconds — 8.0 이전에는 expire_logs_days, 8.4에서 제거 — 또는 PURGE BINARY LOGS), GTID 갭/불일치, source 크래시 후 손상된 binlog 위치, 또는 replica가 너무 뒤처져 더 이상 존재하지 않는 로그를 요청하는 경우다. 'Could not find first log file name in binary log index file'이 전형적인 하위 메시지다.","fix":"복제 상태 확인: SHOW REPLICA STATUS\\G (8.0.22+; 이전 버전에서는 SHOW SLAVE STATUS, 이전 구문은 8.4에서 제거됨). source에 있는 binlog 목록 확인: SHOW BINARY LOGS; 누락된 로그가 purge되었다면 새 백업/GTID 위치에서 replica를 재구축해야 한다(mysqldump --source-data — 8.0.26 이전에는 --master-data, 8.0.26+에서는 deprecated 별칭 — 또는 CLONE plugin). 워크로드에 맞게 source의 binlog 보존 기간을 늘리고, GTID를 사용한다면 GTID 갭을 확인한다.","tags":["replication","replication","binlog","master","gtid","slave"]},{"code":"1129","title":"Host 'host' is blocked because of many connection errors (연결 오류가 많아 호스트 차단됨)","severity":"danger","meaning":"클라이언트 호스트의 연속 연결 오류가 임계값(max_connect_errors)을 초과해 MySQL이 해당 호스트를 차단했다. 그때까지 실패한 handshake 때문에 호스트가 차단 목록에 올랐다.","cause":"로드 밸런서/모니터링 도구가 TCP 포트에 불완전하거나 잘못된 handshake를 보내거나, 네트워크 문제로 연결이 중간에 끊기거나, 잘못 구성된 클라이언트가 실패하는 연결을 계속 시도하는 경우다. 오류 카운터가 max_connect_errors에 도달하면 호스트가 차단된다.","fix":"즉시 차단을 해제한다: SQL에서 TRUNCATE TABLE performance_schema.host_cache; (8.0.23+; FLUSH HOSTS는 8.0.23에서 deprecated되고 8.4에서 제거됨) 또는 명령줄에서 mysqladmin flush-hosts. 차단된 호스트 확인: SELECT host, ip, sum_connect_errors FROM performance_schema.host_cache; 근본 원인을 해결한다: 헬스 체크/모니터링 도구가 완전한 MySQL handshake를 수행하도록 구성하고 네트워크 안정성을 확인한다. 정상 환경에서는 max_connect_errors를 높여 허용치를 늘릴 수 있지만, 중요한 것은 실제 연결 오류를 해결하는 것이다.","tags":["connection","host blocked","max_connect_errors","flush hosts","handshake","network"]},{"code":"1194","title":"Table 'name' is marked as crashed and should be repaired (테이블이 crashed로 표시되어 복구 필요)","severity":"danger","meaning":"보통 MyISAM 엔진(MariaDB에서는 Aria도)으로 저장된 테이블이 손상/크래시로 표시되어 복구 전까지 사용할 수 없다. 테이블에 접근하는 쿼리가 이 오류로 실패한다.","cause":"갑작스러운 서버 크래시/정전, 디스크 가득 참, 하드웨어 장애, mysqld 프로세스 kill로 MyISAM 테이블이 정상적으로 닫히지 않았다. MyISAM은 트랜잭션이나 크래시 안전성을 제공하지 않으므로 InnoDB보다 훨씬 자주 발생한다.","fix":"테이블을 복구한다: REPAIR TABLE table_name; 또는 오프라인에서 myisamchk -r. 복구 전에 항상 데이터 파일을 백업한다. 영구 조치로 테이블을 InnoDB로 옮긴다: ALTER TABLE my_table ENGINE=InnoDB; 이렇게 하면 크래시에 안전해진다. 디스크 상태와 myisam_recover_options 설정을 확인한다.","tags":["myisam","crashed","repair","corruption","corruption","innodb"]},{"code":"1615","title":"Prepared statement needs to be re-prepared (prepared statement 재준비 필요)","severity":"caution","meaning":"서버 측 prepared statement가 의존하는 메타데이터(테이블/뷰 정의)가 변경되어 무효화되었으므로 실행이 거부되었다. 문장을 다시 준비해야 한다.","cause":"문장 준비 후 기반 테이블에 대한 DDL(ALTER TABLE), 테이블 재생성, 메타데이터 캐시가 가득 차 항목이 제거됨, 또는 너무 작은 table_definition_cache/table_open_cache 한도가 원인이다. DDL이 많은 시스템과 일부 ORM/풀 시나리오에서 흔하다.","fix":"대부분의 클라이언트 라이브러리는 이를 잡아 자동으로 문장을 다시 준비한다. 애플리케이션에서는 이 오류를 잡아 prepare + execute를 재시도한다. 자주 재발하면 table_definition_cache와 table_open_cache를 늘린다. 애플리케이션 트래픽 중 불필요한 DDL을 피한다.","tags":["prepared statement","re-prepare","ddl","metadata","cache","retry"]},{"code":"1553","title":"Cannot drop index needed in a foreign key constraint (외래 키 제약 조건에 필요한 인덱스 삭제 불가)","severity":"caution","meaning":"외래 키 제약 조건에 필요한 인덱스를 삭제하려 한다. InnoDB는 FK가 의존하는 인덱스를 제거하도록 허용하지 않는다.","cause":"InnoDB에서는 모든 외래 키가 참조하는 컬럼에 인덱스를 필요로 한다. DROP INDEX로 그 인덱스를 삭제하면 FK 제약 조건이 지원을 잃게 되므로 거부된다. 보통 테이블 재구성(인덱스 정리) 중에 발생한다.","fix":"먼저 외래 키 제약 조건을 삭제한 후 인덱스를 삭제한다: ALTER TABLE my_table DROP FOREIGN KEY fk_name; 이후 DROP INDEX idx_name ON my_table; 필요하면 다른 적절한 인덱스에 의존하도록 FK를 다시 생성한다. 기존 FK 목록: SELECT constraint_name, table_name FROM information_schema.key_column_usage WHERE referenced_table_name IS NOT NULL;","tags":["foreign key","foreign key","index","drop index","innodb","constraint"]},{"code":"3948","title":"Loading local data is disabled; this must be enabled on both the client and server sides (로컬 데이터 로드가 비활성화됨. 클라이언트와 서버 양쪽에서 활성화 필요)","severity":"caution","meaning":"LOAD DATA LOCAL INFILE로 클라이언트 측 파일에서 데이터를 로드하려 했지만 클라이언트 및/또는 서버 측에서 기능이 비활성화되어 있다. 보안상 MySQL 8에서는 LOCAL이 기본적으로 비활성화되어 있다.","cause":"서버에서 local_infile 시스템 변수가 OFF이거나 클라이언트 드라이버가 LOCAL INFILE을 활성화하지 않았다. 악의적인 서버가 클라이언트 파일을 읽을 수 있으므로 이 기능은 기본적으로 제한된다.","fix":"정말 필요하다면 양쪽에서 활성화한다: 서버에서 SET GLOBAL local_infile = 1; 클라이언트에서는 드라이버의 LOCAL INFILE 플래그를 활성화한다(예: allowLoadLocalInfile 연결 옵션 / --local-infile=1). 보안 측면에서는 파일을 서버에 두고 LOCAL 없이 LOAD DATA INFILE을 사용하거나 대량 INSERT/import 도구를 사용하는 것이 바람직하다. 작업이 끝나면 local_infile을 다시 비활성화한다.","tags":["load data","local infile","local_infile","security","import","loading"]},{"code":"1075","title":"Incorrect table definition; there can be only one auto column and it must be defined as a key (잘못된 테이블 정의. auto 컬럼은 하나만 가능하며 키로 정의해야 함)","severity":"caution","meaning":"AUTO_INCREMENT 규칙을 위반해 테이블 정의가 유효하지 않다. 테이블에는 자동 증가 컬럼이 하나만 있을 수 있으며, 그 컬럼은 키(PRIMARY KEY 또는 UNIQUE/인덱스)의 일부여야 한다.","cause":"AUTO_INCREMENT 컬럼을 두 개 이상 정의했거나, AUTO_INCREMENT 컬럼을 어떤 키(인덱스)에도 포함하지 않은 경우다. CREATE TABLE 또는 ALTER TABLE 중에 발생한다.","fix":"AUTO_INCREMENT 컬럼을 하나만 두고 키의 일부로 만든다: CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, ...); 기존 테이블을 수정하려면 먼저 컬럼을 키에 추가한다: ALTER TABLE t ADD PRIMARY KEY (id); 이후 AUTO_INCREMENT를 정의한다. auto 컬럼이 두 개 이상 필요하다면 설계를 재검토한다. MySQL은 이를 지원하지 않는다.","tags":["auto_increment","table","primary key","key","ddl","definition"]}]},"errors/mssql-more3.json":{"engineId":"mssql","engine":"SQL Server","order":4,"errors":[{"code":"102","title":"Incorrect syntax near 'token'","severity":"caution","meaning":"Transact-SQL 파서가 지정된 토큰 근처에서 잘못된 구문을 발견해 문장을 컴파일할 수 없었다. 쿼리는 실행 전에 거부된다.","cause":"오타, 누락된 쉼표/괄호, 대괄호 없이 사용한 예약어(예: USER, ORDER), 종료되지 않은 문자열 리터럴, 다른 SQL 방언에서 복사한 호환되지 않는 구문이 원인이다. 동적 SQL의 문자열 연결 실수도 흔한 원인이다.","fix":"보고된 토큰 주변을 주의 깊게 확인한다. 오류는 종종 앞 줄에 있다. 예약어는 대괄호로 감싸고([Order]) 괄호와 쉼표의 짝이 맞는지 확인한다. 동적 SQL을 만든다면 PRINT @sql로 생성된 텍스트를 확인하고 실행해 본다. 구문이 버전/호환성 수준에서 지원되는지 확인한다.","tags":["syntax","syntax error","parse","tsql","dynamic sql","keyword"]},{"code":"8114","title":"Error converting data type X to Y","severity":"caution","meaning":"타입이 호환되지 않아 값을 한 데이터 타입에서 다른 타입으로 변환할 수 없었다. 오류 245와 달리 보통 파라미터/표현식 수준의 타입 변환 실패다(예: varchar에서 numeric으로).","cause":"숫자가 아니거나 형식이 잘못된 텍스트를 숫자 컬럼/파라미터에 보냈거나, scale/precision 오버플로, 빈 문자열('')을 numeric으로 변환 시도, 파라미터화된 쿼리에 잘못된 타입의 인수 전달이 원인이다. ORM이 생성하는 암묵적 변환에서 흔하다.","fix":"변환되는 값이 대상 타입에 맞는지 확인한다. 안전한 변환에는 TRY_CONVERT / TRY_CAST를 사용한다. 실패 시 NULL을 반환하므로 WHERE로 해당 행을 걸러낼 수 있다. 파라미터 타입을 컬럼 타입과 일치시킨다. ISNUMERIC/TRY_CAST로 데이터를 스캔해 문제 행을 찾고 빈 문자열을 정리한다.","tags":["convert","cast","data type","conversion","numeric","try_convert"]},{"code":"1204","title":"The instance of the SQL Server Database Engine cannot obtain a LOCK resource (SQL Server Database Engine 인스턴스가 LOCK 리소스를 확보할 수 없음)","severity":"danger","meaning":"SQL Server가 락 관리자에 필요한 메모리를 할당할 수 없어 더 이상 락을 생성할 수 없다. 락 메모리 풀이 고갈되어 작업이 실패한다.","cause":"동시에 너무 많은 세분화된(행/페이지) 락 보유: 인덱스 없는 대규모 또는 대량 DML, lock escalation 방지, 서버의 전반적인 메모리 압박, 장시간 열린 트랜잭션이 원인이다. 가끔 외부 메모리 압박이 트리거가 된다.","fix":"대규모 작업을 작은 배치로 나눠 동시 락 수를 줄인다. 적절한 인덱스를 사용해 스캔/락이 걸리는 행 수를 줄인다. lock escalation을 비활성화하는 구성을 검토한다: trace flag 1211/1224 및 ALTER TABLE ... SET (LOCK_ESCALATION = DISABLE); SELECT name, lock_escalation_desc FROM sys.tables; 와 DBCC TRACESTATUS(-1);로 확인한다. 서버 메모리와 max server memory 설정을 확인하고 메모리 압박을 완화한다.","tags":["lock","lock memory","memory","lock escalation","batch","resource"]},{"code":"3906","title":"Failed to update database because the database is read-only (데이터베이스가 읽기 전용이어서 갱신 실패)","severity":"caution","meaning":"읽기 전용으로 표시된 데이터베이스에서 데이터를 수정(INSERT/UPDATE/DELETE/DDL)하려 했다. SQL Server가 쓰기를 거부했다.","cause":"데이터베이스가 READ_ONLY로 설정되었거나, 연결이 Availability Group의 secondary replica(읽기용)로 갔거나, 스냅샷이나 복원 후 데이터베이스가 읽기 전용으로 남았거나, 파일 시스템이 읽기 전용인 경우다. 애플리케이션이 실수로 secondary replica로 라우팅되는 것이 전형적인 원인이다.","fix":"정말 쓰기가 가능해야 한다면: ALTER DATABASE mydb SET READ_WRITE; Availability Group에서는 애플리케이션을 primary replica로 라우팅한다(ReadWrite 연결 라우팅 / ApplicationIntent 확인). 읽기 전용 스냅샷에는 쓸 수 없으므로 원본 데이터베이스를 대상으로 한다. 파일 권한과 파일 시스템도 확인한다.","tags":["read only","read-only database","availability group","replica","read_write","write"]},{"code":"15023","title":"User already exists in the current database (현재 데이터베이스에 사용자가 이미 존재)","severity":"caution","meaning":"데이터베이스에 새 사용자를 추가하는 중 같은 이름의 사용자가 이미 해당 데이터베이스에 존재하는 것으로 확인되었다. 로그인을 기존 사용자에 매핑할 때(orphaned user)도 나타난다.","cause":"보통 데이터베이스를 백업에서 다른 서버로 복원하면 로그인 SID가 데이터베이스 사용자와 일치하지 않는다(orphaned user). CREATE USER로 같은 이름을 다시 추가하거나 로그인-사용자 매핑을 수동으로 설정하려 하면 이 오류가 발생한다.","fix":"orphaned user를 기존 로그인에 다시 매핑한다: ALTER USER app_user WITH LOGIN = login_name; (legacy sp_change_users_login보다 권장). 사용자가 실제로 불필요하다면: DROP USER app_user; orphan 찾기: SELECT dp.name FROM sys.database_principals dp LEFT JOIN sys.server_principals sp ON dp.sid = sp.sid WHERE sp.sid IS NULL AND dp.type IN ('S','U');","tags":["user","database user","login","orphaned","restore","orphaned user"]},{"code":"18452","title":"Login failed. The login is from an untrusted domain and cannot be used with Windows authentication (로그인 실패. 신뢰할 수 없는 도메인의 로그인은 Windows 인증에 사용할 수 없음)","severity":"danger","meaning":"Windows 인증 로그인 시도가 신뢰할 수 없는 도메인에서 와서(또는 SQL 인증이 필요한 곳에서 Windows 인증을 사용해) 거부되었다. 서버가 이 ID를 신뢰하지 않는다.","cause":"클라이언트가 SQL Server가 신뢰하지 않는 도메인/워크그룹의 Windows ID로 접속하거나, 머신이 서로 다른/신뢰하지 않는 도메인에 있거나, Kerberos/SPN 구성이 잘못되었거나, SQL 인증이 예상되는데 연결 문자열이 Integrated Security를 사용하는 경우다. mixed mode가 비활성화된 상태에서 SQL 로그인을 시도해도 비슷한 증상이 나타난다.","fix":"SQL 인증이 필요하다면 서버를 Mixed Mode로 전환하고 연결 문자열에 사용자 이름/비밀번호를 사용한다(Integrated Security=false). Windows 인증이 필요하다면 도메인 트러스트와 DNS/Kerberos SPN 구성을 확인한다. 클라이언트와 서버가 같은 도메인 또는 신뢰하는 도메인에 있는지 확인한다. 오류 로그의 'state' 값으로 원인을 좁힌다.","tags":["login","authentication","windows","domain","kerberos","mixed mode"]},{"code":"605","title":"Attempt to fetch logical page belongs to a different allocation unit (가져오려는 논리 페이지가 다른 allocation unit에 속함)","severity":"danger","meaning":"페이지를 읽는 중 SQL Server가 예상한 객체/allocation unit이 아닌 다른 객체에 속한 페이지를 발견했다. 심각한 물리적/논리적 데이터베이스 손상을 강하게 시사한다.","cause":"스토리지/디스크 장애, 컨트롤러나 드라이버 버그, 갑작스러운 정전 시 쓰기 캐시 손상, 파일 시스템 손상으로 페이지 연결이 깨진 경우다. 'transient 605'는 가끔 일시적인 I/O 문제에서 비롯될 수 있지만, 지속되는 경우는 실제 손상이다.","fix":"즉시 DBCC CHECKDB ('mydb') WITH NO_INFOMSGS, ALL_ERRORMSGS를 실행해 손상 범위를 파악한다. 가능하면 정상으로 확인된 백업에서 복원한다(데이터 손실을 최소화하는 PITR 권장). 스토리지/하드웨어를 테스트하고 PAGE_VERIFY CHECKSUM 설정을 확인한다. REPAIR 옵션은 데이터 손실을 일으킬 수 있으므로 백업 후 최후의 수단으로만 사용하며, 중요한 환경에서는 Microsoft 지원에 문의한다.","tags":["corruption","database corruption","page","allocation unit","dbcc checkdb","storage"]},{"code":"4922","title":"ALTER TABLE ALTER COLUMN failed because one or more objects access this column (하나 이상의 객체가 이 컬럼에 접근하므로 ALTER TABLE ALTER COLUMN 실패)","severity":"caution","meaning":"컬럼의 타입/속성을 변경하려 했지만 해당 컬럼에 의존하는 객체(인덱스, 제약 조건, 계산 컬럼, 통계, 스키마 바인딩된 뷰 등)가 이를 막았다. 의존성이 제거될 때까지 컬럼을 변경할 수 없다.","cause":"컬럼이 인덱스, PRIMARY KEY/UNIQUE/CHECK/DEFAULT 제약 조건, 계산 컬럼 표현식, WITH SCHEMABINDING으로 정의된 뷰/함수, 통계 객체에서 사용된다. 타입/크기 변경이 이 객체들을 무효화하므로 ALTER가 거부된다.","fix":"먼저 의존 객체를 찾아 일시적으로 삭제하거나 비활성화하고, 컬럼을 변경한 후 다시 생성한다. sys.sql_expression_dependencies와 sys.indexes/sys.default_constraints를 조회해 의존성을 확인한다. 일반적인 순서: DROP CONSTRAINT/INDEX -> ALTER COLUMN -> 다시 CREATE. 스키마 바인딩된 뷰는 일시적으로 SCHEMABINDING을 제거했다가 다시 추가한다.","tags":["alter column","dependency","index","constraint","schemabinding","ddl"]},{"code":"7202","title":"Could not find server 'name' in sys.servers (sys.servers에서 서버를 찾을 수 없음)","severity":"caution","meaning":"쿼리에서 linked server 또는 4부분 이름(server.database.schema.object)으로 참조한 서버 이름을 구성된 linked server 중에서 찾을 수 없다. SQL Server가 이름을 해석하지 못했다.","cause":"linked server가 정의된 적이 없거나, 이름의 철자가 틀렸거나, 다른 환경(dev/prod)에 있는 linked server가 운영 환경에 없거나, OPENQUERY/4부분 이름이 잘못된 서버 이름을 가리키는 경우다. 스크립트를 서버 간에 옮길 때 흔하다.","fix":"정의된 linked server 목록 확인: SELECT name FROM sys.servers; 또는 EXEC sp_linkedservers. 필요하면 추가한다: EXEC sp_addlinkedserver @server='SERVER', ...; 이후 sp_addlinkedsrvlogin으로 로그인 매핑을 설정한다. 쿼리의 4부분 이름이 올바른 linked server 이름을 사용하는지 확인하고, 로컬 객체에만 접근한다면 서버 접두사를 제거한다.","tags":["linked server","remote server","sys.servers","openquery","four-part name","configuration"]},{"code":"40501","title":"The service is currently busy. Retry the request after 10 seconds (서비스가 현재 바쁨. 10초 후 재시도)","severity":"danger","meaning":"Azure SQL Database에서 리소스 압박으로 서비스가 일시적으로 요청을 거부했다(engine throttling). 시스템이 바쁘며 클라이언트가 잠시 후 재시도할 것으로 예상된다.","cause":"CPU, 연결, 트랜잭션 로그 쓰기 속도, worker 수 같은 리소스가 선택한 서비스 계층(DTU/vCore)의 한도에 도달했거나, 갑작스러운 부하 급증, 리소스를 소모하는 장시간/비효율적 쿼리가 원인이다. 오류와 함께 오는 'reason code'가 어떤 리소스가 throttling되는지 보여 준다.","fix":"애플리케이션에 지수 백오프 재시도 로직을 구현한다. 일시적 오류에 대한 예상된 동작이다. 리소스 사용량 검토: sys.dm_db_resource_stats 및 Azure 포털 메트릭. 쿼리/인덱스를 최적화하고 불필요한 동시성을 줄인다. 포화가 지속되면 서비스 계층(DTU/vCore)을 스케일 업한다.","tags":["azure","throttling","busy","retry","resource","dtu"]}]},"errors/mongodb-more3.json":{"engineId":"mongodb","engine":"MongoDB","order":5,"errors":[{"code":"10107 NotWritablePrimary","title":"not master / not writable primary","severity":"danger","meaning":"쓰기(또는 primary가 필요한 다른 작업)가 현재 primary가 아닌 replica set 멤버로 전송되었다. primary만 쓰기를 받으므로 작업이 거부되었다.","cause":"애플리케이션이 secondary에 쓰고 있거나, 페일오버(election) 중 이전 primary가 secondary가 되었는데 클라이언트가 여전히 연결되어 있거나, 드라이버의 읽기/쓰기 preference와 토폴로지 정보가 오래된 경우다. 연결 문자열에 replicaSet 파라미터가 없는 것이 전형적인 원인이다.","fix":"드라이버에 모든 replica set 멤버와 replica set 이름을 구성해(연결 문자열에 replicaSet=... 및 모든 호스트) 드라이버가 primary를 자동으로 찾고 페일오버 시 재라우팅하도록 한다. 쓰기를 primary로 라우팅하는 기본 읽기/쓰기 preference를 사용한다. 일시적 오류이므로 애플리케이션에서 retryable writes를 활성화한다. rs.status()로 어떤 멤버가 PRIMARY인지 확인한다.","tags":["replica set","primary","failover","write","election","retryable"]},{"code":"91 ShutdownInProgress","title":"The server is in the process of shutting down (서버가 종료 중)","severity":"danger","meaning":"서버(mongod/mongos)가 종료 중이어서 작업을 완료할 수 없었다. 종료 중인 노드는 새 요청을 거부하고 진행 중인 작업을 종료한다.","cause":"계획된 재시작/유지보수, replica set에서 primary stepdown에 앞선 종료, 컨테이너/오케스트레이터가 보낸 종료 시그널, 리소스 고갈로 인한 프로세스 중지가 원인이다. 롤링 업그레이드 중에는 예상되는 동작이다.","fix":"보통 일시적이다. 애플리케이션에서 재시도 로직과 replica set 연결을 사용해 요청이 다른 정상 멤버로 라우팅되도록 한다. 예상치 못한 경우 mongod 로그에서 종료 이유(시그널, OOM killer, 리소스)를 확인한다. 롤링 유지보수 중에는 primary를 종료하기 전에 깔끔하게 step down한다(rs.stepDown()).","tags":["shutdown","shutting down","maintenance","stepdown","replica set","retry"]},{"code":"189 PrimarySteppedDown","title":"Primary stepped down while operation was in progress (작업 진행 중 primary가 step down됨)","severity":"danger","meaning":"작업이 실행되는 중에 primary가 step down되어 더 이상 쓰기를 받지 않는다. 따라서 진행 중인 명령이 중단되었다.","cause":"replica set에서 election이 트리거되었다: 유지보수를 위해 primary를 수동으로 step down했거나, 과반수와의 연결이 끊겼거나, priority 변경이 있었거나, 네트워크 분할이 발생한 경우다. 페일오버 시점에 진행 중인 작업이 이 오류를 받는다.","fix":"보통 재시도 가능한 오류다. 드라이버에서 retryable writes/reads가 활성화되어 있으면 작업이 새 primary로 자동 라우팅될 수 있다. 애플리케이션에서 이 오류를 잡아 재시도한다. 자주 발생한다면 replica set 상태를 조사한다: rs.status()로 멤버 상태, 네트워크 지연, oplog 지연을 확인하고 불필요한 election을 줄인다(heartbeat/timeout 설정).","tags":["stepdown","election","failover","primary","replica set","retry"]},{"code":"24 LockTimeout","title":"Operation exceeded time limit waiting for a lock (락 대기 중 작업 시간 한도 초과)","severity":"caution","meaning":"작업이 필요한 락(예: 데이터베이스/컬렉션 수준)을 기다리는 중 시간 한도를 초과해 중단되었다. 락을 제때 획득할 수 없었다.","cause":"장시간 실행 작업이나 DDL 명령(예: 컬렉션에 대한 충돌하는 관리 작업)이 필요한 락을 보유하거나, 동시 쓰기가 많거나, maxTimeMS로 제한된 작업이 락을 기다리는 중 만료된 경우다. 충돌하는 인덱스/컬렉션 작업에서 나타난다.","fix":"락을 보유한 작업을 찾는다: db.currentOp()로 활성 및 대기 중인 작업을 확인하고 필요하면 db.killOp()로 종료한다. 충돌하는 관리 명령(인덱스 생성, collMod, compact)은 비피크 시간으로 옮긴다. 작업을 짧게 유지하고 불필요하게 긴 트랜잭션을 피한다. 필요하면 워크로드에 맞게 maxTimeMS를 조정한다.","tags":["lock","lock wait","timeout","currentop","ddl","concurrency"]},{"code":"7 HostNotFound","title":"Could not find host","severity":"danger","meaning":"MongoDB가 연결하려는 호스트 이름을 해석하거나 찾을 수 없었다. 드라이버나 replica set 멤버가 지정된 호스트에 도달할 수 없어 연결을 수립할 수 없다.","cause":"DNS 해석 실패, 연결 문자열의 잘못되었거나 오래된 호스트 이름, 클라이언트가 해석할 수 없는 replica set 멤버 호스트 이름(rs.conf), 또는 네트워크/접근 문제가 원인이다. 컨테이너 환경에서는 외부에서 해석할 수 없는 내부 호스트 이름이 전형적인 원인이다.","fix":"호스트 이름이 올바르고 해석 가능한지 확인한다(ping/nslookup으로 DNS 테스트). 연결 문자열의 호스트를 갱신한다. replica set 멤버 이름을 클라이언트와 멤버 모두가 해석할 수 있도록 하고, 필요하면 rs.conf()의 호스트 이름을 접근 가능한 FQDN/IP로 갱신한다. SRV(mongodb+srv)를 사용한다면 DNS SRV 레코드를 확인한다.","tags":["host","dns","connection","replica set","network","srv"]},{"code":"175 QueryPlanKilled","title":"Query plan killed during execution","severity":"caution","meaning":"기반 데이터/컬렉션이 변경되어 실행 중인 쿼리의 계획이 무효화되었고 kill되었다. 안전하게 계속할 수 없어 쿼리가 중단되었다.","cause":"쿼리가 실행되는 동안(특히 열린 커서가 있을 때) 컬렉션이 삭제되었거나, 사용 중인 인덱스가 삭제되었거나, rename/재구성이 발생했다. 장시간 실행되는 getMore 호출 중 기반 변경이 전형적인 트리거다.","fix":"쿼리/커서를 다시 시작한다(재시도). 쿼리가 실행되는 동안 컬렉션이나 인덱스에 파괴적인 DDL(drop, dropIndex, rename)이 실행되지 않도록 하고, 이런 유지보수는 쿼리 트래픽 외 시간에 예약한다. 장시간 읽기를 줄이고 필요한 인덱스가 영구적으로 유지되도록 한다.","tags":["query","cursor","getmore","drop","index","plan"]},{"code":"11602 InterruptedDueToReplStateChange","title":"Operation interrupted due to a replication state change (복제 상태 변경으로 작업 중단)","severity":"danger","meaning":"replica set 상태가 변경되어(예: primary 변경) 작업이 중단되었다. 새 토폴로지에서 안전하게 완료할 수 없어 진행 중인 명령이 종료되었다.","cause":"election, primary stepdown, 멤버 역할 변경, 구성 변경 중에 진행 중인 작업이 중단된다. 189 PrimarySteppedDown과 근본 원인이 같으며 보통 페일오버 시점에 나타난다.","fix":"재시도 가능한 오류다. 드라이버에서 retryable writes/reads가 활성화되어 있으면 새 primary를 대상으로 작업을 재시도할 수 있다. 애플리케이션에서 이 오류를 잡아 재시도한다. 자주 재발하면 election의 원인을 조사한다: 네트워크 불안정, heartbeat 타임아웃, 과부하, oplog 지연. rs.status()로 replica set 상태를 모니터링한다.","tags":["replica set","election","failover","interrupted","retry","state change"]},{"code":"136 CappedPositionLost","title":"Capped collection position lost","severity":"caution","meaning":"capped(고정 크기) 컬렉션을 읽는 커서(보통 tailable cursor)가 위치를 잃었다. 커서가 가리키던 문서를 컬렉션이 순환적으로 덮어써 더 이상 존재하지 않는다.","cause":"capped collection이 가득 차면 가장 오래된 문서가 자동으로 제거되고 덮어써진다. 소비 속도보다 빠르게 쓰이는 컬렉션을 따라가는 tailable cursor는 뒤처지고 위치가 덮어써진다. oplog 같은 capped 구조를 tail하는 consumer에서 나타난다.","fix":"커서 위치를 다시 잡고(새 tailable cursor 열기) tailing을 재시작한다. 애플리케이션에서 이 오류를 잡아 읽기를 새로 고친다. 근본 원인이 쓰기 속도를 따라가지 못하는 consumer라면 capped collection/oplog 크기를 늘리거나 consumer 속도를 높인다. 변경 사항을 안정적으로 추적하려면 가능하면 tailable cursor 대신 change stream을 사용한다.","tags":["capped","tailable cursor","oplog","cursor","position","change stream"]},{"code":"28 PathNotViable","title":"Cannot use the part of the path to traverse the element (경로의 일부로 요소를 탐색할 수 없음)","severity":"caution","meaning":"업데이트가 점으로 구분된 필드 경로(예: a.b.c)를 탐색하려 했지만, 경로의 일부가 하위 필드를 가질 수 없는 타입(스칼라 값 또는 잘못된 타입의 요소)에 도달했다. 따라서 경로를 탐색할 수 없었다.","cause":"문서의 필드 'a'가 객체/배열이 아닌 스칼라(예: 숫자/문자열)인데 a.b.c 같은 중첩 경로에 쓰려 하거나, 배열 인덱스가 예상되는 곳에서 객체를 발견하는 등 타입이 불일치하는 경우다. 스키마가 일관되지 않은 문서에서 흔하다($set으로 깊은 경로를 갱신할 때).","fix":"업데이트 전에 경로가 예상 타입(객체/배열)인지 확인한다. 호환되지 않는 문서를 수정하거나 문서 구조에 따라 조건부로 업데이트한다. 필요하면 먼저 필드를 올바른 타입으로 변환한 후(예: 스칼라 값을 객체로 교체) 중첩 필드를 쓴다. 스키마 일관성을 위해 schema validation을 고려한다.","tags":["update","path","dotted","type","set","schema"]}]},"errors/redis-more3.json":{"engineId":"redis","engine":"Redis","order":6,"errors":[{"code":"BUSYGROUP Consumer Group name already exists","title":"Consumer group already exists","severity":"caution","meaning":"XGROUP CREATE로 stream에 consumer group을 생성하려 했지만 같은 이름의 그룹이 해당 stream에 이미 존재한다. Redis는 기존 그룹을 덮어쓰지 않는다.","cause":"애플리케이션이 시작 시 무조건 그룹을 생성하려 하는데 그룹이 이전 실행에서 남아 있거나, 여러 인스턴스가 같은 그룹을 병렬로 생성하려 하는 경우다. 멱등하지 않은 시작 로직의 전형이다.","fix":"그룹 생성을 멱등하게 만든다: 오류를 잡아 무시한다(그룹이 이미 있어도 괜찮다). XGROUP CREATE stream group $ MKSTREAM을 사용하면 stream이 아직 없을 때 자동으로 생성된다. XINFO GROUPS stream으로 기존 그룹을 확인한다. 정말 초기화하려면 먼저 XGROUP DESTROY stream group으로 삭제한다.","tags":["stream","consumer group","xgroup","busygroup","idempotent","mkstream"]},{"code":"NOGROUP No such key or consumer group","title":"No such key or consumer group in stream","severity":"caution","meaning":"stream 소비 명령(XREADGROUP, XACK, XCLAIM 등)을 실행했지만 지정한 키(stream) 또는 consumer group이 존재하지 않는다. 작업할 그룹/stream을 찾을 수 없었다.","cause":"그룹이 생성된 적이 없거나(XGROUP CREATE 생략), stream 키가 삭제되었거나 쓰인 적이 없거나, 그룹 이름/키의 철자가 틀렸거나, XGROUP DESTROY로 그룹이 제거된 경우다. 그룹이 생성되기 전에 consumer가 시작될 때 흔하다.","fix":"먼저 stream과 그룹이 존재하는지 확인한다: XINFO STREAM stream 및 XINFO GROUPS stream. 그룹이 없으면 생성한다: XGROUP CREATE stream group $ MKSTREAM (필요하면 stream도 생성). 애플리케이션 시작 시 그룹을 생성하는 단계를 추가한다(BUSYGROUP은 무시). 키와 그룹 이름의 철자가 올바른지 확인한다.","tags":["stream","consumer group","nogroup","xreadgroup","xgroup","key"]},{"code":"NOTBUSY No scripts in execution right now","title":"No scripts in execution right now","severity":"safe","meaning":"SCRIPT KILL 또는 FUNCTION KILL을 보냈지만 현재 실행 중인(그리고 kill 가능한) Lua 스크립트/함수가 없다. 종료할 대상이 없어 명령이 거부되었다.","cause":"중지하려 했을 때 스크립트가 이미 끝났거나, SCRIPT KILL을 잘못된 노드로 보냈거나(스크립트는 다른 인스턴스에서 실행 중), SCRIPT KILL 전에 BUSY 상태가 스스로 해소된 경우다. 보통 타이밍 문제다.","fix":"실제로 멈춘 스크립트가 없다면 할 일이 없다. 안전한/정보성 응답이다. BUSY 오류가 발생하면(스크립트가 busy-reply-threshold를 초과, Redis 7 이전에는 lua-time-limit), 스크립트가 실행 중인 올바른 노드에서 SCRIPT KILL(함수는 FUNCTION KILL)을 시도한다. 스크립트가 데이터를 수정했다면 SCRIPT KILL이 동작하지 않으며, 최후의 수단으로 SHUTDOWN NOSAVE가 필요할 수 있다. 긴 스크립트는 나누고 시간 한도를 지킨다.","tags":["lua","script kill","notbusy","busy","script","function"]},{"code":"DENIED Redis is running in protected mode","title":"Redis is running in protected mode","severity":"danger","meaning":"Redis가 protected mode로 실행 중이어서 외부(loopback이 아닌 주소)의 비밀번호 없는 연결을 거부했다. 보안을 위해 인증되지 않은 원격 접근이 차단된다.","cause":"모든 인터페이스에 바인딩된(또는 느슨한 bind 설정의) Redis가 비밀번호(requirepass) 설정 없이 외부 클라이언트의 연결을 받았다. protected-mode는 기본적으로 켜져 있으며 이 상황에서 loopback이 아닌 요청을 차단한다. 보안 설정이 없는 Redis가 실수로 인터넷에 노출되었음을 나타낸다.","fix":"올바른 해결책은 보안을 강화하는 것이다: requirepass(또는 ACL 사용자)로 강력한 비밀번호를 설정하고 bind로 신뢰할 수 있는 인터페이스에서만 수신한다. 원격 접근이 필요하다면 비밀번호/TLS로 보호하고 방화벽으로 6379 포트를 제한한다. protected-mode no는 폐쇄된/안전한 네트워크에서만 일시적으로 설정할 수 있으며, 인터넷에 노출된 설치에서는 절대 권장하지 않는다.","tags":["protected mode","security","denied","requirepass","bind","auth"]},{"code":"ERR Can't execute '<cmd>': only (P|S)SUBSCRIBE / (P|S)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context","title":"Command not allowed in subscribe context (subscribe 컨텍스트에서 허용되지 않는 명령)","severity":"caution","meaning":"연결이 Pub/Sub subscribe 모드에 들어간 후 이 모드에서 허용되지 않는 일반 명령(예: GET/SET)을 실행하려 했다. subscribe 모드의 연결은 제한된 명령 집합만 받는다. 이전 버전의 메시지는 \"ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context\"였다.","cause":"SUBSCRIBE/PSUBSCRIBE/SSUBSCRIBE로 subscribe 모드로 전환된 연결에서 데이터 명령을 보낸 경우다. RESP2 프로토콜에서 구독 중인 연결은 (P|S)SUBSCRIBE, (P|S)UNSUBSCRIBE, PING, QUIT, RESET만 허용한다. 하나의 연결을 pub/sub와 일반 작업에 공유하는 것이 전형적인 실수다.","fix":"Pub/Sub에는 전용 연결을 사용하고 일반 명령에는 다른 연결을 연다(대부분의 클라이언트 라이브러리는 자동으로 처리한다). subscribe 모드를 벗어나려면 UNSUBSCRIBE/PUNSUBSCRIBE를 보낸다. 이후 일반 명령이 동작한다. RESP3 프로토콜(HELLO 3)에서는 구독 중인 연결에서도 다른 명령이 허용되므로, 드라이버가 지원한다면 RESP3로 전환한다.","tags":["pubsub","subscribe","context","resp","connection","hello"]},{"code":"ERR SELECT is not allowed in cluster mode","title":"SELECT is not allowed in cluster mode","severity":"caution","meaning":"Redis Cluster 모드에서 SELECT로 다른 논리 데이터베이스(DB 인덱스)로 전환하려 했다. 클러스터 모드는 데이터베이스 0만 지원하며 여러 DB를 사용할 수 없다.","cause":"standalone Redis에서 SELECT로 DB 0-15를 전환하던 애플리케이션이 Redis Cluster로 이전한 후에도 같은 명령을 계속 보내는 경우다. 클러스터 아키텍처는 slot 기준으로 데이터를 분산하므로 다중 DB 개념을 지원하지 않는다.","fix":"클러스터 모드에서는 SELECT 호출을 제거한다. DB 0만 사용된다. 논리적 분리가 필요하면 키 접두사(namespace)를 사용한다(예: app1:key). 특정 키를 함께 처리해야 한다면 hash tag({user123})를 사용해 같은 slot에 배치한다. 애플리케이션 구성을 cluster-aware하게 만든다.","tags":["cluster","select","database","namespace","hash tag","slot"]},{"code":"ERR wrong number of arguments for command","title":"Wrong number of arguments for command","severity":"caution","meaning":"Redis 명령이 예상보다 적거나 많은 인수로 전송되었다. 구문을 충족하지 않아 명령이 처리되지 않고 거부되었다.","cause":"누락되었거나 초과된 파라미터(예: SET key value 대신 SET key만), 빈 값/undefined로 전달된 변수, 잘못된 인수 순서로 호출된 명령, 클라이언트 라이브러리의 잘못된 배열/인수 전개(spread)가 원인이다. 동적으로 생성한 명령에서 흔하다.","fix":"명령의 올바른 시그니처를 확인한다(예: SET key value [options]). 공식 명령 레퍼런스를 참고한다. 클라이언트 코드에서 인수(특히 배열에서 오는 인수)가 비어 있거나 누락되지 않았는지 확인한다. 사용자 입력으로 명령을 만든다면 사전에 인수 개수를 검증한다. redis-cli에서 명령을 직접 실행해 예상 인수 개수를 확인한다.","tags":["syntax","arguments","argument","command","syntax","cli"]},{"code":"ERR Background save already in progress","title":"Background save already in progress","severity":"caution","meaning":"새 백그라운드 저장(BGSAVE)이나 AOF rewrite를 시작하려 했지만 이미 백그라운드 저장이 진행 중이다. Redis는 동시에 두 번째 저장 프로세스를 허용하지 않는다.","cause":"이전 BGSAVE/BGREWRITEAOF가 완료되기 전에 새로 트리거했거나, 큰 데이터셋에서 fork/저장이 오래 걸리는 동안 모니터링 스크립트나 애플리케이션이 BGSAVE를 반복 호출한 경우다. 느린 디스크나 큰 RDB 파일은 프로세스를 길어지게 한다.","fix":"실행 중인 저장이 끝날 때까지 기다린다. INFO persistence의 rdb_bgsave_in_progress, aof_rewrite_in_progress 필드와 LASTSAVE 타임스탬프로 상태를 확인한다. 이전 저장이 끝나기 전에 새 BGSAVE를 트리거하지 않도록 애플리케이션/자동화 로직을 조건부로 만든다. 저장이 너무 오래 걸리면 디스크 속도와 RDB 크기를 조사한다.","tags":["bgsave","persistence","rdb","aof","background save","fork"]},{"code":"ERR bit offset is not an integer or out of range","title":"Bit offset is not an integer or out of range (비트 오프셋이 정수가 아니거나 범위를 벗어남)","severity":"caution","meaning":"SETBIT/GETBIT/BITCOUNT 같은 비트 연산에 지정한 비트 오프셋이 유효하지 않다. 정수가 아니거나 허용 범위를 벗어났다. Redis가 해당 오프셋에서 비트 연산을 수행할 수 없다.","cause":"숫자가 아닌 값을 오프셋으로 전달했거나, 음수 오프셋을 사용했거나, 허용 상한을 넘는 매우 큰 오프셋을 지정한 경우다(비트 문자열은 최대 512 MB, 즉 약 2^32비트). 비트맵 기반 카운터/플래그 구조의 계산 오류에서 발생한다.","fix":"오프셋이 허용 범위(0 ~ 약 2^32-1, 문자열은 최대 512 MB) 내의 음이 아닌 정수인지 확인한다. 오프셋 계산(예: 사용자 ID에서 파생된 값)의 오버플로/음수 값을 검증한다. 매우 큰 오프셋이 필요하면 데이터를 여러 키로 분할하도록 설계한다(샤딩).","tags":["bitmap","setbit","offset","integer","range","bit"]}]},"errors/elasticsearch-more3.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":7,"errors":[{"code":"node_not_connected_exception","title":"Node is not connected","severity":"danger","meaning":"노드가 통신해야 하는 다른 노드와 현재 연결되어 있지 않다. 클러스터 내부(transport 계층) 연결이 끊겨 요청을 대상 노드로 전달할 수 없었다.","cause":"네트워크 장애나 패킷 손실, 대상 노드의 크래시/재시작, 방화벽/포트 차단(기본 transport 포트 9300), GC pause로 인한 노드의 일시적 무응답, DNS/호스트 해석 문제가 원인이다. 롤링 재시작 중 일시적으로 나타날 수 있다.","fix":"클러스터와 노드 상태를 확인한다: GET _cluster/health와 GET _cat/nodes?v로 어떤 노드가 없는지 확인한다. 노드 간 네트워크 접근과 transport 포트(9300)의 방화벽 규칙을 확인한다. 영향을 받은 노드의 로그에서 크래시, GC pause, OOM을 확인한다. 네트워크 안정성과 discovery/seed host 구성을 검토한다.","tags":["transport","node","connection","network","9300","cluster"]},{"code":"receive_timeout_transport_exception","title":"Request timed out waiting for a response from another node (다른 노드의 응답 대기 중 요청 타임아웃)","severity":"danger","meaning":"노드가 다른 노드에 요청을 보냈지만 구성된 시간 내에 응답이 오지 않아 요청이 타임아웃되었다. 노드 간(transport) 호출을 완료할 수 없었다.","cause":"대상 노드의 높은 부하, 긴 GC pause, 느린 디스크/CPU 압박, 네트워크 지연/패킷 손실, 또는 대상 노드가 너무 바빠 응답할 수 없는 경우다. 무거운 쿼리, 대규모 bulk 작업, 샤드 리밸런싱 중에 흔하다.","fix":"영향을 받은 노드의 리소스 사용량을 확인한다: GET _nodes/stats로 JVM heap, GC, thread pool 포화를 확인한다. GET _cat/thread_pool?v로 거부/대기 중인 작업을 확인한다. 네트워크 지연을 측정한다. 부하를 줄이고(더 작은 쿼리/bulk 크기) heap과 하드웨어 리소스를 검토한다. 지속되면 노드를 추가해 부하를 분산한다.","tags":["transport","timeout","gc","load","network","thread pool"]},{"code":"routing_missing_exception","title":"Routing value is required for this document (이 문서에는 routing 값이 필요)","severity":"caution","meaning":"문서에 접근/인덱싱하려면 routing 값이 필요하지만 제공되지 않았다. routing 없이는 Elasticsearch가 문서가 어떤 샤드에 속하는지 결정할 수 없었다.","cause":"인덱스 매핑에 _routing required=true가 정의되어 있거나, parent-join(parent/child) 관계를 사용해 자식 문서에 routing이 필요하거나, index/get/update/delete 요청에서 routing 파라미터가 누락된 경우다. 사용자 정의 routing으로 쓴 문서를 routing 없이 읽는 것이 전형적인 원인이다.","fix":"요청에 routing 값을 명시적으로 지정한다(예: index/get/update/delete 호출에 ?routing=value). parent-join에서는 자식 문서의 routing으로 부모의 id를 전달한다. 문서를 쓸 때 사용한 것과 같은 routing을 읽기/갱신/삭제에도 사용한다. 매핑의 _routing 요구 사항(required)을 검토한다.","tags":["routing","shard","parent-child","join","mapping","index"]},{"code":"too_many_buckets_exception","title":"Trying to create too many buckets","severity":"caution","meaning":"aggregation 쿼리가 허용 상한(search.max_buckets, 기본 65,536)보다 많은 bucket을 생성하려 했다. 메모리를 보호하기 위해 쿼리가 거부되었다.","cause":"카디널리티가 높은 필드에서 매우 큰 size나 매우 세밀한 interval로 terms/date_histogram 같은 aggregation을 실행하거나, 중첩 aggregation이 bucket 수를 곱으로 늘리는 경우다. 과도한 bucket 생성은 노드 메모리를 고갈시킬 수 있으므로 한도가 적용된다.","fix":"생성되는 bucket 수를 줄인다: terms aggregation의 size를 낮추고, date_histogram interval을 넓히고, 불필요한 중첩 aggregation을 제거한다. 많은 결과가 필요하면 composite aggregation으로 페이지 단위로 조회한다. 한도를 높이는 것은 최후의 수단이며 메모리 위험이 따른다. search.max_buckets는 PUT _cluster/settings로 변경할 수 있지만 신중하게 한다.","tags":["aggregation","bucket","max_buckets","terms","composite","memory"]},{"code":"shard_lock_obtain_failed_exception","title":"Obtaining shard lock timed out","severity":"danger","meaning":"Elasticsearch가 샤드 작업에 필요한 로컬 디렉터리 락(shard lock)을 타임아웃 내에 획득하지 못했다. 샤드를 시작하거나 재할당할 수 없었다.","cause":"같은 샤드의 이전 복사본(close/relocation 후)이 아직 락을 해제하지 않았거나, 느리거나 멈춘 I/O, 디스크 문제, 빠른 노드 재시작 후 정리되지 않은 오래된 샤드 상태가 원인이다. 보통 일시적이지만 지속되면 샤드가 UNASSIGNED 상태로 남는다.","fix":"샤드가 할당되지 않은 이유 확인: GET _cluster/allocation/explain. 대부분 락이 곧 해제되고 클러스터가 스스로 재시도한다. 필요하면 POST _cluster/reroute?retry_failed=true로 재할당을 트리거한다. 지속되면 영향을 받은 노드의 로그에서 I/O/디스크 오류를 확인하고 디스크 상태를 점검한다. 문제 노드를 깔끔하게 재시작하면 락이 해제될 수 있다.","tags":["shard","lock","unassigned","allocation","io","reroute"]},{"code":"parsing_exception","title":"Failed to parse the search request / query DSL (검색 요청 / query DSL 파싱 실패)","severity":"caution","meaning":"제출한 쿼리 본문(query DSL)을 파싱할 수 없었다. 요청 JSON 구조나 쿼리 구문이 Elasticsearch가 기대하는 형식과 맞지 않아 쿼리를 실행할 수 없었다.","cause":"유효하지 않은 JSON, 잘못 배치되었거나 알 수 없는 쿼리 키, 잘못된 구조로 사용한 쿼리 유형(예: bool, range), 버전 간 제거/변경된 필드 이름, 괄호 초과/누락이 원인이다. 직접 작성하거나 동적으로 생성한 쿼리에서 흔하다.","fix":"오류 메시지의 줄/위치 힌트를 이용해 쿼리 본문을 확인하고 JSON이 유효한지 검증한다. query DSL 구조를 공식 레퍼런스와 비교한다(bool/must/filter 배치 등). 버전 업그레이드에서 제거된 구문을 갱신한다. 쿼리를 단계적으로 단순화해 문제 부분을 격리하고, 가능하면 클라이언트 라이브러리의 query builder를 사용한다.","tags":["query dsl","parse","json","syntax","search","bool"]},{"code":"max_bytes_length_exceeded_exception","title":"Document contains at least one immense term that is too long (너무 긴 거대한 term이 문서에 포함됨)","severity":"caution","meaning":"인덱싱 중인 문서에서 term(특히 keyword/분석되지 않는 필드 값)이 허용 최대 바이트 길이(32766바이트)를 초과했다. Lucene은 그렇게 긴 term을 인덱싱할 수 없다.","cause":"매우 긴 텍스트 값을 keyword 필드에 (통째로) 쓴 경우다. 예: 문서 본문 전체, base64 데이터, 긴 URL/식별자를 keyword 필드에 넣은 경우. keyword 필드는 분석되지 않으므로 값이 단일 term으로 인덱싱되어 한도를 초과한다.","fix":"긴 텍스트에는 text(분석) 필드를 사용한다. keyword는 정확한 일치가 필요한 짧은 값 전용이다. 값을 제한하려면 매핑에 ignore_above를 사용한다(이보다 긴 값은 인덱싱되지 않지만 저장은 된다). 필요 없다면 매우 긴 필드를 index:false로 설정한다. 데이터 유형에 따라 필드 설계(text vs keyword)를 재검토한다.","tags":["keyword","term","ignore_above","lucene","mapping","length"]},{"code":"illegal_argument_exception_total_fields","title":"Limit of total fields has been exceeded","severity":"caution","meaning":"인덱스 매핑의 전체 필드 수가 허용 상한(index.mapping.total_fields.limit, 기본 1000)을 초과했다. 새 필드를 추가할 수 없어 인덱싱이 실패한다.","cause":"보통 mapping explosion이다. dynamic mapping이 활성화된 상태에서 비정형/가변 키 JSON 문서(예: 키가 사용자 데이터에서 오는 객체)가 새 필드를 모두 매핑에 추가한다. 시간이 지나면서 필드 수가 빠르게 늘어 한도에 도달한다.","fix":"근본 원인을 해결하려면 가변 키 데이터를 객체 대신 flattened 필드나 key-value 쌍 배열로 모델링하고, dynamic mapping을 제한한다(dynamic: false 또는 strict). 정말 더 많은 필드가 필요하다면 한도를 신중히 높인다: PUT idx/_settings {\"index.mapping.total_fields.limit\": 2000}. GET idx/_mapping으로 필드 수를 확인해 mapping explosion을 모니터링한다.","tags":["mapping","total_fields","dynamic mapping","flattened","limit","field"]},{"code":"verification_exception","title":"verification_exception: Found N problem(s) — Unknown column / Unknown index (ES|QL, SQL, EQL) (N개 문제 발견 — 알 수 없는 컬럼/인덱스)","severity":"caution","meaning":"ES|QL(_query), SQL(_sql) 또는 EQL 쿼리가 구문상으로는 파싱되었지만 의미 검증에 실패했다: 참조한 인덱스, 필드(컬럼) 또는 함수를 찾을 수 없거나 타입이 호환되지 않는다. 오류 텍스트에 문제가 있는 줄/컬럼 위치가 표시된다(예: \"line 1:18: Unknown column [field]\"). 스냅샷 리포지토리 검증 실패는 이 코드가 아니라 repository_verification_exception이다.","cause":"철자가 틀렸거나 매핑에 없는 필드 이름, 존재하지 않거나 어떤 인덱스와도 일치하지 않는 인덱스 패턴, text 필드에 대한 지원되지 않는 작업(예: keyword 하위 필드 대신 text로 그룹화), 호환되지 않는 타입 간 비교나 알 수 없는 함수가 원인이다.","fix":"오류 메시지의 line:column 정보를 이용해 쿼리를 수정한다. 필드 이름과 타입 확인: GET idx/_mapping (SQL은 DESCRIBE idx / SHOW COLUMNS). GET _cat/indices로 인덱스 패턴을 확인한다. 그룹화/정렬에는 keyword 하위 필드(field.keyword)를 사용하고, 타입 불일치에는 명시적 변환을 적용한다(ES|QL: TO_STRING, TO_INTEGER 등).","tags":["esql","sql","eql","verification","unknown column","query"]}]},"errors/oracle-more4.json":{"engineId":"oracle","engine":"Oracle","order":1,"errors":[{"code":"ORA-19804","title":"cannot reclaim N bytes disk space from M bytes limit (M바이트 한도에서 N바이트 디스크 공간을 회수할 수 없음)","severity":"danger","meaning":"Oracle이 새 파일(아카이브 로그, 백업, flashback 로그)을 위해 Fast Recovery Area(FRA)에 공간을 만들려 했지만, DB_RECOVERY_FILE_DEST_SIZE로 정의된 논리적 한도에 도달해 필요한 공간을 회수할 수 없었다.","cause":"FRA 크기(DB_RECOVERY_FILE_DEST_SIZE)는 물리 디스크와 무관한 상한이며, 아카이브 로그, RMAN 백업 또는 flashback 로그가 이를 채웠다. 근본 원인은 보통 아카이브 로그를 정기적으로 백업·삭제하지 않았거나 불필요한 백업을 정리하지 않은 것이다.","fix":"FRA 사용량 확인: SELECT * FROM v$recovery_area_usage; 및 SELECT * FROM v$recovery_file_dest; 아카이브 로그를 FRA 외부 대상에 백업하고 삭제하거나(RMAN: BACKUP ARCHIVELOG ALL FORMAT '/backup/arc_%U' DELETE INPUT;), 더 이상 필요 없는 백업을 정리한다(RMAN: DELETE OBSOLETE;). Data Guard 환경에서는 standby에 아직 적용되지 않은 아카이브 로그를 삭제하지 않는다. 영구 조치로 디스크 공간이 있으면 DB_RECOVERY_FILE_DEST_SIZE를 늘린다. Flashback Database가 필요 없다면 비활성화를 고려한다.","tags":["fra","recovery area","archived log","rman","db_recovery_file_dest_size","disk"]},{"code":"ORA-00494","title":"enqueue held for too long by 'inst N, osid M' (enqueue가 너무 오래 보유됨)","severity":"danger","meaning":"중요한 리소스 enqueue(예: CF - control file enqueue)를 프로세스가 허용 시간보다 훨씬 오래 보유해, Oracle이 시스템 행을 방지하기 위해 해당 프로세스를 종료했다. alert.log에 'killing enqueue blocker' 항목과 함께 나타난다.","cause":"백그라운드 또는 사용자 프로세스가 I/O 병목, 높은 CPU 압박, OS 정체(swapping, 느린 디스크)로 인해 중요한 enqueue를 허용 시간 이상 보유했다. control file(CF) enqueue의 경우 이 시간은 _controlfile_enqueue_timeout(기본 900 s)으로 설정된다. 숨겨진(underscore) 파라미터는 Oracle Support의 안내하에서만 변경해야 한다. 보통 스토리지 지연이나 LGWR/CKPT 같은 느린 프로세스가 원인이다.","fix":"alert.log와 종료된 프로세스의 trace 파일을 검토해 어떤 enqueue(보통 control file)와 어떤 백그라운드 프로세스가 원인인지 파악한다. 실제 원인은 보통 스토리지/I/O 지연이므로 디스크 응답 시간, multipath, 컨트롤러 상태를 확인한다. CPU/메모리 압박과 swap 사용을 줄인다. 재발하면 trace 분석 결과를 Oracle Support와 공유한다.","tags":["enqueue","control file","i-o","storage","hang","background process"]},{"code":"ORA-04021","title":"timeout occurred while waiting to lock object (객체 락 대기 중 타임아웃 발생)","severity":"caution","meaning":"객체(패키지, 프로시저, 타입 등)에 대한 library cache lock/pin을 획득하려고 기다리는 중 타임아웃이 발생했다. 보통 다른 세션이 실행 중이거나 락을 건 PL/SQL 객체를 컴파일하거나 변경하려 할 때 발생한다.","cause":"변경/컴파일하려는 객체가 다른 세션에서 활발히 실행 중이거나 다른 DDL에 의해 잠겨 있어 배타적 library cache lock을 획득할 수 없다. 장시간 실행되는 프로시저 호출이나 멈춘 세션이 락을 보유하고 있을 수 있다.","fix":"객체를 잠근 세션을 찾는다: SELECT * FROM dba_kgllock WHERE kgllkhdl IN (SELECT p1raw FROM v$session WHERE event LIKE 'library cache%'); 또는 v$access와 v$session으로 객체를 사용하는 세션을 식별한다. 워크로드가 낮은 유지보수 시간에 DDL/컴파일을 재시도한다. 필요하면 세션을 안전하게 종료한다. 애플리케이션을 잠시 중지하면 락을 획득하기 쉬워진다.","tags":["library cache","lock","plsql","ddl","compile","timeout"]},{"code":"ORA-04061","title":"existing state of package/procedure has been invalidated (패키지/프로시저의 기존 상태가 무효화됨)","severity":"caution","meaning":"호출된 PL/SQL 객체(패키지, 프로시저, 함수)가 의존하는 객체가 변경되어 기존 상태가 무효화되었다. ORA-04068과 함께 나타나는 상태 손실 조건의 변형이며, 세션이 보유한 패키지 상태가 더 이상 유효하지 않다.","cause":"세션이 패키지를 사용하는 동안 해당 패키지나 의존 객체에 DDL이 적용되었다(테이블 구조 변경, 재컴파일, 권한 변경). 그 결과 세션의 기존 패키지 상태가 무효화되었다. 애플리케이션 실행 중 스키마 변경이 이루어지는, 배포가 잦은 환경에서 나타난다.","fix":"같은 호출은 보통 두 번째 시도에서 성공한다(상태가 초기화되었으므로). 애플리케이션에서 이 오류를 잡아 안전하게 재시도한다. 스키마 변경은 애플리케이션 트래픽이 적을 때나 유지보수 시간에 수행한다. 무효 객체를 일괄 컴파일한다: EXEC DBMS_UTILITY.COMPILE_SCHEMA('APP_SCHEMA'); 패키지 상태(전역 변수)를 최소화하는 설계가 이 영향을 줄인다.","tags":["plsql","package","invalidate","ddl","state","recompile"]},{"code":"ORA-01466","title":"unable to read data - table definition has changed (데이터를 읽을 수 없음 - 테이블 정의 변경됨)","severity":"caution","meaning":"과거 시점의 데이터를 읽는 쿼리(Flashback Query, AS OF 또는 consistent read)가 조회한 과거 시점과 현재 사이에 테이블 정의(DDL)가 변경되어 실패했다. Oracle은 현재 정의로 그 시점의 데이터를 안전하게 읽을 수 없다.","cause":"Flashback/AS OF 쿼리가 대상으로 하는 타임스탬프 이후에 테이블에 DDL(컬럼 추가/삭제, 타입 변경)이 적용되었다. 또한 클라이언트와 서버 간 시계 차이나 아주 최근(몇 초 전) 과거에 대한 쿼리도 SCN/시간 해상도 때문에 이 오류를 발생시킬 수 있다.","fix":"테이블의 DDL 변경 이전 시간/SCN을 대상으로 flashback 쿼리를 실행한다. 가능하면 타임스탬프 대신 정확한 SCN을 사용한다(AS OF SCN). 아주 최근 과거를 조회할 때는 몇 초 더 이전으로 간다. 영구적인 이력 접근이 필요하면 DDL 대신 버전 관리 또는 아카이브 테이블 설계를 고려한다.","tags":["flashback","as of","ddl","consistent read","scn","query"]},{"code":"ORA-08176","title":"consistent read failure; rollback data not available (consistent read 실패. rollback 데이터 없음)","severity":"danger","meaning":"consistent read 이미지를 구성하는 중 쿼리가 undo를 생성하지 않는 작업으로 수정된 블록을 만났다. 이 블록에 대한 undo/rollback 데이터가 없어 일관된 이미지를 구성할 수 없었다. ORA-01555와 달리 undo가 덮어써진 것이 아니라 처음부터 생성되지 않았다. (serializable 격리 오류는 별도 코드인 ORA-08177이다.)","cause":"쿼리가 진행 중일 때 같은 객체에서 undo를 생성하지 않는 작업이 실행된 경우다: direct-path load(INSERT /*+ APPEND */, SQL*Loader DIRECT=TRUE), CREATE INDEX / index rebuild 또는 discrete transaction.","fix":"direct-path load나 인덱스 작업이 끝난 후 쿼리를 다시 실행한다(재시도). 긴 리포트와 같은 객체에 대한 direct-path load / 인덱스 유지보수 작업을 같은 시간대에 예약하지 않는다. 재발하면 alert log와 ASH로 충돌하는 작업을 식별하고 일정을 분리한다.","tags":["undo","consistent read","direct path","append","create index","retry"]},{"code":"ORA-00020","title":"maximum number of processes (N) exceeded (최대 프로세스 수(N) 초과)","severity":"danger","meaning":"인스턴스의 프로세스 수가 PROCESSES 초기화 파라미터로 정의된 상한에 도달해 새 연결/프로세스를 생성할 수 없다. 새 세션이 접속할 수 없으며 관리자도 접속하지 못할 수 있다.","cause":"과도하게 큰 애플리케이션 커넥션 풀, 닫히지 않는 연결(누수), 예상보다 많은 동시 사용자, 너무 낮게 설정된 PROCESSES 파라미터로 한도를 초과한다. 때로는 멈춘 백그라운드 작업이나 parallel query 프로세스도 프로세스를 소비한다.","fix":"sqlplus -prelim / as sysdba로 접속할 수 있지만 이 연결은 SQL을 실행할 수 없으며(v$session 조회 불가), oradebug(예: hanganalyze)와 SHUTDOWN ABORT에만 사용할 수 있다. 슬롯을 확보하려면 OS 수준에서 유휴 애플리케이션 서버 프로세스를 신중히 kill한다(ps -ef | grep 'LOCAL=NO'). 이후 일반 SYSDBA 연결로 v$session/v$process에서 누수된 세션을 찾아 ALTER SYSTEM KILL SESSION으로 닫는다. 영구 조치로 PROCESSES를 늘리고 인스턴스를 재시작한다(spfile에 SESSIONS가 명시적으로 설정되어 있으면 다시 계산되지 않으므로 함께 갱신한다). 애플리케이션 측에서는 커넥션 풀 최대값과 유휴 연결 정리를 수정한다.","tags":["processes","process","connection","limit","pool","init parameter"]},{"code":"ORA-01113","title":"file N needs media recovery","severity":"danger","meaning":"데이터 파일을 온라인으로 전환하거나 데이터베이스를 여는 중이지만 파일이 일관되지 않아 진행하기 전에 먼저 미디어 복구를 적용해야 한다. 파일이 control file이 기대하는 것보다 오래되었거나 일관되지 않은 SCN에 있다.","cause":"데이터 파일이 오래된 백업에서 복원되었거나, 오프라인 동안 redo가 적용되지 않았거나, 인스턴스/스토리지 크래시 후 파일이 최신 상태가 아니다. 보통 복원 후 recover 단계를 건너뛰었거나 파일이 오랫동안 오프라인 상태였던 것이 원인이다.","fix":"파일 또는 데이터베이스를 복구한다: RECOVER DATAFILE N; 또는 RECOVER DATABASE; 이후 ALTER DATABASE DATAFILE N ONLINE; 또는 ALTER DATABASE OPEN; 필요한 아카이브 로그에 접근할 수 있는지 확인한다. RMAN 사용 시: 필요하면 RESTORE 후 RECOVER DATAFILE N;을 실행한다. alert.log에서 어떤 로그가 요청되는지 확인한다.","tags":["media recovery","datafile","redo","restore","recover","scn"]},{"code":"ORA-02393","title":"exceeded call limit on CPU usage","severity":"caution","meaning":"세션에 할당된 리소스 프로파일에 정의된 단일 호출당 허용 CPU 시간(CPU_PER_CALL)을 초과해 Oracle이 해당 호출을 종료했다. 리소스 제한을 위한 안전장치다.","cause":"사용자 프로파일에 CPU_PER_CALL 한도가 정의되어 있고 RESOURCE_LIMIT=TRUE이며, 단일 SQL/PLSQL 호출이 이 CPU 임계값을 초과했다. 보통 예상보다 훨씬 많은 CPU를 소비하는 비효율적/통제되지 않은 쿼리가 원인이다.","fix":"쿼리를 최적화해(적절한 인덱스, 올바른 실행 계획, 불필요한 full table scan 제거) CPU 소비가 한도 아래로 내려가도록 한다. 비즈니스 요구가 정당하다면 해당 사용자의 프로파일에서 CPU_PER_CALL을 늘리거나 UNLIMITED로 설정한다: ALTER PROFILE ... LIMIT CPU_PER_CALL ...; 프로파일 대신 Database Resource Manager로 리소스를 더 유연하게 관리하는 것을 고려한다.","tags":["profile","cpu_per_call","resource_limit","resource","query","optimization"]},{"code":"ORA-12801","title":"error signaled in parallel query server Pnnn (parallel query server에서 오류 발생)","severity":"danger","meaning":"병렬로 실행되는 쿼리에서 parallel execution server(parallel query slave) 중 하나가 오류를 만났고, 이 오류가 query coordinator로 전달되었다. ORA-12801은 래퍼이며 실제 오류는 그 다음 두 번째 줄에 있다.","cause":"병렬 작업 중 slave 프로세스가 실제 오류를 만났다: TEMP 테이블스페이스 부족(ORA-01652), PGA 메모리 고갈(ORA-04030), 데이터 손상, 병렬 프로세스 한도, 또는 다른 기반 SQL 오류. ORA-12801은 병렬 컨텍스트에서 발생했다는 것만 보고한다.","fix":"ORA-12801 바로 다음에 오는 실제 오류 코드에 집중해 해결한다(예: TEMP 확장, PGA 튜닝, 손상 블록 복구). 필요하면 쿼리를 직렬로 실행하거나, PARALLEL 힌트를 제거하거나, 병렬도를 낮춰 실제 오류를 격리한다. PARALLEL_MAX_SERVERS와 리소스 사용량을 검토한다.","tags":["parallel","parallel query","slave","temp","pga","wrapper"]}]},"errors/postgres-more4.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"errors":[{"code":"23P01","title":"exclusion_violation","severity":"danger","meaning":"EXCLUDE(배제) 제약 조건을 위반했다. 이 제약 조건 유형은 특정 연산자(예: 범위 중첩 &&)에 따라 두 행이 충돌하지 않도록 하며, 삽입/갱신된 행이 기존 행과 충돌해 거부되었다.","cause":"보통 GiST 기반 EXCLUDE 제약 조건이 정의되어 있고(예: 예약/회의실 예약에서 tstzrange &&로 시간 중첩 금지), 새 행의 범위/값이 기존 행과 겹치는 경우다. 두 동시 트랜잭션이 겹치는 범위를 삽입할 때도 발생할 수 있다.","fix":"삽입 전에 쿼리로 중첩 여부를 확인하거나, 애플리케이션에서 오류를 잡아 사용자에게 의미 있는 메시지를 보여 준다. 해당 범위/조건을 조회해 충돌하는 기존 행을 찾는다. 비즈니스 규칙이 중첩을 허용한다면 제약 조건을 완화하고, 그렇지 않으면 입력을 수정한다. 동시성 상황에서는 트랜잭션을 안전하게 재시도해 직렬화한다.","tags":["exclude","constraint violation","gist","range","overlap","constraint"]},{"code":"23001","title":"restrict_violation","severity":"danger","meaning":"SQL 표준 restrict_violation 코드다. 중요: PostgreSQL은 ON DELETE/ON UPDATE RESTRICT 또는 NO ACTION으로 정의된 외래 키 위반을 이 코드가 아니라 23503(foreign_key_violation)으로 보고한다. 실제로 PostgreSQL 코어에서 이 코드를 볼 일은 없다.","cause":"종속 자식 행이 있는 부모 행을 삭제하거나 키를 변경하면 PostgreSQL은 23503을 반환한다. 23001은 보통 다른 데이터베이스에서 이식한 애플리케이션 코드나 확장의 사용자 정의 오류로 나타난다. NO ACTION과 달리 RESTRICT는 검사를 트랜잭션 끝으로 연기할 수 없고 즉시 실패한다.","fix":"먼저 종속 자식 행을 삭제/갱신하거나 다른 부모로 옮긴 후 부모 행을 처리한다. 비즈니스 규칙이 허용한다면 외래 키를 ON DELETE CASCADE 또는 SET NULL로 재정의하는 것을 고려한다. 대량 삭제에서는 삭제 순서를 올바르게 한다(자식 먼저, 그다음 부모).","tags":["foreign key","restrict","fk","delete","reference","constraint"]},{"code":"42P17","title":"invalid_object_definition","severity":"danger","meaning":"객체의 정의(함수, collation, 트리거, 룰 등)가 유효하지 않다. PostgreSQL은 주어진 정의가 내부적으로 일관되지 않거나 허용할 수 없음을 나타내며, 작성된 대로는 객체를 생성/변경할 수 없다.","cause":"흔한 원인: 함수의 모순된 속성 조합(예: IMMUTABLE로 지정했지만 volatile처럼 동작), 유효하지 않은 collation/문자 집합 정의, 부적합한 트리거/룰 정의. row-level security(RLS)와 일부 DDL의 일관되지 않은 옵션도 원인이 될 수 있다.","fix":"오류 세부 정보(DETAIL/HINT)를 읽어 정의의 어느 부분이 거부되었는지 파악한다. 함수 volatility 레이블을 실제 동작에 맞춘다(VOLATILE/STABLE/IMMUTABLE). collation 이름이 pg_collation에 존재하는지 확인한다. 문서화된 구문/규칙에 따라 정의를 수정하고 다시 실행한다.","tags":["function","collation","definition","volatility","ddl","object"]},{"code":"42710","title":"duplicate_object","severity":"caution","meaning":"생성하려는 객체(역할, 타입, 제약 조건, 확장, 스키마, 인덱스 이름 등)가 이미 존재한다. PostgreSQL은 같은 이름/정의의 두 번째 객체를 허용하지 않는다.","cause":"같은 이름의 객체가 이미 있는 상태에서 CREATE ROLE, CREATE TYPE, CREATE EXTENSION, ADD CONSTRAINT 같은 명령을 실행했다. 보통 스크립트를 두 번 실행했거나, 멱등하지 않은 마이그레이션, 또는 두 동시 세션이 같은 객체를 생성하려 한 것이 원인이다.","fix":"객체가 존재하는지 확인한다(역할은 pg_roles, 타입은 pg_type, 확장은 pg_extension). 마이그레이션을 멱등하게 작성한다: 가능한 경우 CREATE ... IF NOT EXISTS를 사용하거나 먼저 DROP ... IF EXISTS를 실행한다. 제약 조건 충돌이라면 기존 제약 조건의 이름을 바꾸거나 삭제 후 재생성한다.","tags":["duplicate","role","type","extension","constraint","migration"]},{"code":"22008","title":"datetime_field_overflow","severity":"caution","meaning":"날짜/시간 값을 계산하거나 파싱하는 중 필드 중 하나가 유효 범위를 벗어났다. 예를 들어 13월이나 32일 같은 불가능한 구성 요소, 또는 지원되는 시간 범위를 넘는 결과가 생성되었다.","cause":"유효하지 않은 날짜 입력(예: '2026-02-30'), 범위를 벗어난 구성 요소, 또는 지원되는 날짜 범위를 초과하는 INTERVAL 덧셈이 원인이다. 잘못된 형식/로캘로 파싱하거나 검증되지 않은 사용자 입력을 삽입하는 것이 전형적인 원인이다.","fix":"삽입 전에 애플리케이션에서 날짜 입력을 검증하고 올바른 형식으로 보낸다(가능하면 ISO 8601: YYYY-MM-DD). TO_DATE/TO_TIMESTAMP로 파싱할 때는 형식 마스크를 입력에 맞춘다. 산술 결과를 지원되는 날짜 범위 내로 유지하고, 의심스러운 값은 범위 검사를 거친다.","tags":["date","time","overflow","interval","validation","format"]},{"code":"22026","title":"string_data_length_mismatch","severity":"caution","meaning":"비트 문자열의 길이가 작업에서 요구하는 고정 길이와 일치하지 않는다. PostgreSQL에서는 주로 다른 길이의 비트 문자열을 고정 길이 bit(n) 타입에 쓰거나, 길이가 다른 비트 문자열에 비트 AND/OR/XOR를 적용할 때 나타난다.","cause":"bit(n) 컬럼에 n보다 짧거나 긴 비트 문자열을 쓰거나(예: bit(4) 컬럼에 B'101': 'bit string length 3 does not match type bit(4)'), 길이가 다른 비트 문자열에 &, |, #를 적용한 경우다('cannot AND bit strings of different sizes'). 텍스트나 문자 인코딩 때문이 아니며, char(n)/varchar(n)에 너무 긴 값은 대신 22001을 발생시킨다.","fix":"값을 대상 길이로 맞춘다: 명시적 캐스트를 사용하거나(B'101'::bit(4)는 오른쪽을 0으로 채우고 초과분은 잘라낸다), 길이가 달라질 수 있다면 컬럼을 bit varying(n)으로 정의한다. 비트 연산 전에 피연산자를 같은 길이로 캐스트하고 length()로 길이를 확인한다.","tags":["bit","length","bit string","varbit","conversion","validation"]},{"code":"2201B","title":"invalid_regular_expression","severity":"caution","meaning":"정규 표현식 작업(~, ~*, SIMILAR TO, regexp_matches, substring 등)에 지정한 패턴이 유효하지 않다. 구문이 잘못되어 PostgreSQL이 패턴을 컴파일할 수 없었다.","cause":"괄호 짝이 맞지 않거나, 유효하지 않은 문자 클래스, 잘못된 이스케이프 시퀀스, 지원되지 않는 구문을 가진 정규식 패턴이다. 흔한 원인은 특수 문자를 이스케이프하지 않고 사용자 입력을 그대로 패턴으로 사용하는 것이다.","fix":"POSIX/ARE 구문에 따라 정규식 패턴을 수정하고 괄호, 대괄호, 이스케이프 시퀀스를 확인한다. 사용자 입력을 패턴이 아닌 리터럴 텍스트로 검색하는 경우: PostgreSQL에는 내장 regexp_quote 함수가 없으므로 regexp_replace로 특수 문자 앞에 백슬래시를 붙여 이스케이프하거나, 정규식을 쓰지 않고 strpos()/position() 또는 LIKE ... ESCAPE를 사용한다. 별도 도구에서 테스트해 패턴을 검증한다.","tags":["regex","regular expression","pattern","similar to","escape","syntax"]},{"code":"44000","title":"with_check_option_violation","severity":"danger","meaning":"WITH CHECK OPTION으로 정의된 뷰를 통한 INSERT/UPDATE가 결과 행이 더 이상 뷰의 WHERE 조건을 만족하지 않아 거부되었다. 뷰는 자신의 필터 밖으로 벗어나는 행을 쓰지 못하게 한다.","cause":"갱신 가능한 뷰가 WITH CHECK OPTION으로 정의되어 있고, 쓰기가 행을 뷰의 정의 조건 밖으로 옮기는 경우다(예: 뷰가 status='active'로 필터링하는데 행을 'inactive'로 설정). row-level security(RLS) 정책에서도 비슷한 검사가 발생할 수 있다.","fix":"쓰는 값이 뷰의 WHERE 조건을 만족하도록 하거나, 권한이 허용한다면 뷰 대신 기반 테이블에서 직접 작업한다. 비즈니스 규칙상 행이 필터를 벗어나도 된다면 WITH CHECK OPTION 없이 뷰를 재정의한다. RLS의 경우 정책의 WITH CHECK 표현식을 검토한다.","tags":["view","check option","updatable view","rls","insert","update"]},{"code":"40002","title":"transaction_integrity_constraint_violation","severity":"danger","meaning":"트랜잭션 끝에 검사되는 무결성 제약 조건이 COMMIT 시 위반된 경우의 SQL 표준 코드다. 중요: PostgreSQL은 실제로 이 코드를 생성하지 않으며, COMMIT 시 연기된(DEFERRED) 제약 조건 위반을 해당 제약 조건 자체의 코드로 반환한다: 외래 키는 23503, 고유성은 23505.","cause":"DEFERRABLE INITIALLY DEFERRED로 정의된 외래 키나 unique 제약 조건이 트랜잭션의 중간 단계에서 일시적으로 깨진 후 COMMIT 시점까지 수정되지 않았다. PostgreSQL에서는 23503/23505로 보고된다. 예를 들어 상호 참조하는 행을 잘못된 순서로 삽입하고 수정하지 않으면 발생한다.","fix":"COMMIT 전에 모든 연기된 제약 조건이 충족되도록 트랜잭션 로직을 수정한다(누락된 참조 추가, 충돌 해소). 필요하면 트랜잭션 안에서 SET CONSTRAINTS ... IMMEDIATE로 제약 조건을 조기에 검사해 문제 단계를 더 빨리 찾는다. 상호 참조의 경우 삽입/갱신 순서를 조정한다.","tags":["deferred","constraint violation","commit","foreign key","transaction","constraint"]}]},"errors/mysql-more4.json":{"engineId":"mysql","engine":"MySQL","order":3,"errors":[{"code":"1055","title":"Expression not in GROUP BY (only_full_group_by) (GROUP BY에 없는 표현식)","severity":"caution","meaning":"GROUP BY에도 없고 집계 함수로 감싸지도 않은 컬럼을 SELECT 목록이나 HAVING/ORDER BY에서 사용했다. ONLY_FULL_GROUP_BY SQL 모드가 활성화되어 있어 MySQL이 이 비결정적 표현식을 거부한다.","cause":"쿼리가 그룹화된 컬럼이 아닌 컬럼을 직접 선택하지만, 그 컬럼은 그룹당 단일 값이 아닐 수 있다. ONLY_FULL_GROUP_BY는 MySQL 5.7+에서 기본적으로 활성화되며, 이전의 관대한 GROUP BY 동작에 의존하는 쿼리가 이 오류를 발생시킨다.","fix":"컬럼을 GROUP BY 목록에 추가하거나 집계 함수(MAX(), MIN(), ANY_VALUE())로 감싼다. 값이 그룹 내에서 실제로 고유하다는 것을 안다면 ANY_VALUE()로 모호성을 의도적으로 억제한다. 쿼리를 고치는 대신 모드를 비활성화하면(sql_mode에서 ONLY_FULL_GROUP_BY 제거) 잘못된 결과의 위험이 되돌아오므로 피한다.","tags":["group by","only_full_group_by","sql_mode","any_value","aggregate","query"]},{"code":"1267","title":"Illegal mix of collations","severity":"caution","meaning":"비교나 결합 작업(JOIN, WHERE, UNION, CONCAT)에서 collation이 다른 두 문자열이 결합되어 MySQL이 어떤 collation 규칙을 적용할지 결정할 수 없었다.","cause":"보통 문자 집합/collation이 다른 컬럼을 비교하거나(예: utf8mb4_general_ci vs. utf8mb4_unicode_ci, 또는 latin1 vs. utf8mb4), 컬럼과 연결 collation이 일치하지 않는 경우다. 테이블/데이터베이스가 서로 다른 기본값으로 생성되었을 때 흔하다.","fix":"비교의 양쪽을 같은 collation으로 맞춘다: COLLATE로 명시적 collation을 지정하거나(예: col1 = col2 COLLATE utf8mb4_unicode_ci) CONVERT(... USING ...)를 사용한다. 영구 조치로 영향을 받는 컬럼/테이블을 하나의 일관된 문자 집합과 collation으로 변환한다. 연결 collation(collation_connection)도 맞춘다.","tags":["collation","collation mismatch","character set","join","utf8mb4","convert"]},{"code":"1030","title":"Got error N from storage engine","severity":"danger","meaning":"스토리지 엔진(InnoDB/MyISAM)이 작업을 완료하지 못하고 저수준 오류 코드를 반환했다. SQL 계층 아래의 디스크, 파일 시스템 또는 엔진 내부 문제에 대한 일반 보고다. 괄호/메시지 안의 하위 코드가 실제 원인을 보여 준다.","cause":"흔한 원인: 디스크/임시 공간 가득 참(예: errno 28 - No space left on device), 파일 권한 문제, 손상된 테이블 파일, InnoDB 테이블스페이스 문제, OS 수준 I/O 오류. 큰 임시 테이블을 생성하는 sort/group 쿼리도 원인이 될 수 있다.","fix":"하위 오류 코드를 해석한다(Linux에서는 perror N으로 의미 확인, 예: perror 28). 디스크 사용량과 tmpdir 공간을 확인하고 공간을 확보한다. 파일 소유권/권한을 확인한다. InnoDB는 오류 로그의 세부 내용을 검토하고, 손상이 의심되면 CHECK TABLE을 실행하고 필요하면 복구한다. 거대한 임시 결과를 생성하는 쿼리를 최적화한다.","tags":["storage engine","innodb","disk","errno","tmpdir","i-o"]},{"code":"1197","title":"Multi-statement transaction required more than 'max_binlog_cache_size' (다중 문장 트랜잭션이 'max_binlog_cache_size'보다 많은 공간을 요구함)","severity":"danger","meaning":"단일 트랜잭션이 생성한 binlog 데이터가 max_binlog_cache_size로 정의된 메모리/캐시 한도를 초과했다. 트랜잭션이 binary log에 들어가지 않아 실패하고 롤백되었다.","cause":"매우 큰 트랜잭션(수백만 행의 단일 INSERT/UPDATE/DELETE 또는 거대한 bulk load)이 binlog 캐시를 채웠다. binary logging이 활성화된 경우(복제/시점 복구) 큰 트랜잭션은 이 한도를 쉽게 초과할 수 있다.","fix":"큰 트랜잭션을 작은 배치로 나누고 배치마다 COMMIT한다. 정말 큰 트랜잭션을 피할 수 없다면 max_binlog_cache_size(필요하면 binlog_cache_size도)를 늘린다. ETL/bulk load에서는 중간 COMMIT을 포함한 분할 로드로 이 오류와 장시간 락 보유를 모두 방지한다.","tags":["binlog","max_binlog_cache_size","transaction","batch","replication","bulk"]},{"code":"1785","title":"Statement violates GTID consistency","severity":"danger","meaning":"GTID(Global Transaction Identifier) 기반 복제가 활성화된 상태에서 실행한 문장이 GTID 일관성 규칙을 위반한다. GTID 모드는 단일 GTID에 안전하게 할당할 수 없는 특정 문장 유형을 허용하지 않는다.","cause":"1785(ER_GTID_UNSAFE_NON_TRANSACTIONAL_TABLE)는 특히 비트랜잭션(예: MyISAM) 테이블을 트랜잭션(InnoDB) 테이블과 같은 트랜잭션에서 갱신하거나, autocommit 없이 다중 문장 트랜잭션에서 갱신할 때 발생한다. 이전 버전에서는 CREATE TABLE ... SELECT(8.0.21 이전. 8.0.21+에서는 atomic DDL을 지원하는 엔진에서 허용)와 트랜잭션 안의 CREATE/DROP TEMPORARY TABLE(8.0.13 이전. 8.0.13+에서는 binlog_format이 ROW/MIXED일 때 허용)도 GTID 일관성을 위반한다(이들은 별도 오류 코드로 보고된다). enforce_gtid_consistency=ON일 때 거부된다.","fix":"같은 트랜잭션에서 비트랜잭션 엔진 테이블과 트랜잭션 테이블을 섞지 않는다. MyISAM 테이블은 단일 autocommit 문장으로 갱신하거나 모든 테이블을 InnoDB로 옮긴다: ALTER TABLE my_table ENGINE=InnoDB; 이전 버전(8.0.21 이전)에서는 CREATE TABLE ... SELECT를 두 단계로 나누고(CREATE TABLE 후 INSERT ... SELECT), 8.0.13 이전에서는 명시적 트랜잭션 밖에서 임시 테이블을 생성·삭제한다.","tags":["gtid","replication","consistency","temporary table","myisam","innodb"]},{"code":"3024","title":"Query execution was interrupted, maximum statement execution time exceeded (쿼리 실행이 중단됨. 최대 문장 실행 시간 초과)","severity":"caution","meaning":"SELECT 쿼리가 정의된 최대 실행 시간(max_execution_time / MAX_EXECUTION_TIME 힌트)을 초과해 MySQL이 중단했다. 장시간 실행 쿼리가 리소스를 점유하지 않도록 하는 타임아웃 안전장치다.","cause":"쿼리가 세션/전역 max_execution_time 값이나 SELECT의 MAX_EXECUTION_TIME(N) 옵티마이저 힌트보다 오래 실행되었다. 보통 비효율적인 실행 계획, 누락된 인덱스, 대규모 스캔, 높은 동시성에서의 지연이 원인이다. (읽기 전용 SELECT에만 적용된다.)","fix":"EXPLAIN으로 쿼리를 분석하고 최적화한다(적절한 인덱스, 더 선택적인 조건, full table scan 감소). 쿼리가 정당하게 오래 실행되어야 한다면 해당 SELECT의 MAX_EXECUTION_TIME 힌트나 세션 max_execution_time을 늘린다. 무거운 분석 쿼리는 오프라인/replica에서 실행한다.","tags":["timeout","max_execution_time","query","explain","index","select"]},{"code":"1298","title":"Unknown or incorrect time zone","severity":"caution","meaning":"지정한 시간대 이름을 MySQL이 인식하지 못했다. CONVERT_TZ() 호출, time_zone 설정, 또는 이름 있는 시간대(예: 'Europe/Istanbul')를 참조하는 작업이 해당 시간대가 서버에 정의되어 있지 않아 실패했다.","cause":"MySQL이 이름 있는 시간대를 사용하려면 mysql 스키마의 시간대 테이블(time_zone, time_zone_name 등)이 로드되어 있어야 한다. 이 테이블이 비어 있으면 숫자 오프셋('+03:00')만 동작하고 이름 있는 시간대는 알 수 없어 이 오류가 발생한다.","fix":"시간대 테이블을 로드한다. Linux에서는 mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql로 시스템 zoneinfo 데이터를 가져온 후 서버를 재시작하거나 FLUSH TABLES를 실행한다. 이름 있는 시간대를 로드할 수 없다면 숫자 오프셋('+03:00')을 사용한다. 이름의 철자를 확인한다(대소문자, 'Europe/Istanbul').","tags":["time zone","timezone","convert_tz","zoneinfo","mysql_tzinfo_to_sql","offset"]},{"code":"1418","title":"Function has none of DETERMINISTIC, NO SQL, or READS SQL DATA (binary logging enabled) (함수에 DETERMINISTIC, NO SQL, READS SQL DATA 중 어느 것도 없음)","severity":"caution","meaning":"저장 함수를 생성하는 중이지만 binary logging이 활성화되어 있고 함수에 결정성/데이터 접근 특성이 하나도 선언되지 않았다. 복제 안전성 측면에서 모호하므로 MySQL이 이런 함수를 거부한다.","cause":"log_bin이 ON이고 log_bin_trust_function_creators=OFF인 상태에서 DETERMINISTIC, NO SQL, READS SQL DATA 특성 없이 함수를 정의했다. 이 특성이 없으면 MySQL은 함수가 replica에서 같은 결과를 낸다고 확신할 수 없다.","fix":"올바른 특성으로 함수의 동작을 선언한다: 실제로 결정적이면 DETERMINISTIC, 데이터에 접근하지 않으면 NO SQL, 읽기만 하면 READS SQL DATA를 추가한다. 함수가 안전하다고 확신한다면 중앙 정책으로 log_bin_trust_function_creators=1을 설정할 수 있지만, 특성을 올바르게 선언하는 것이 가장 안전한 방법이다. 특성이 함수의 실제 동작과 일치하는지 확인한다.","tags":["stored function","deterministic","binary log","replication","log_bin_trust_function_creators","reads sql data"]},{"code":"1231","title":"Variable can't be set to the value of ... (변수를 해당 값으로 설정할 수 없음)","severity":"caution","meaning":"시스템/세션 변수에(SET으로) 대입하는 값이 유효하지 않다. 허용 집합을 벗어나거나, 타입이 잘못되었거나, 형식이 부적합해 값이 거부되었다.","cause":"흔한 예: sql_mode에 유효하지 않거나 철자가 틀린 모드 이름을 대입하거나, enum 변수에 정의되지 않은 옵션을 지정하거나, NULL을 받지 않는 변수에 NULL을 대입한 경우다. 단위/범위가 잘못된 숫자 값도 원인이 될 수 있다.","fix":"변수가 받는 유효한 값과 타입을 확인한다. ENUM/SET 타입 변수(예: SET 타입인 sql_mode)는 값/모드 이름을 정확히 쓰고 여러 값은 쉼표로 구분한다. 문서에서 유효한 값 범위를 확인한다. sql_mode 같은 목록은 현재 값(SELECT @@sql_mode)을 읽고 그 위에 항목을 안전하게 추가/제거한다.","tags":["set","variable","sql_mode","enum","value","invalid"]}]},"errors/mssql-more4.json":{"engineId":"mssql","engine":"SQL Server","order":4,"errors":[{"code":"8645","title":"A time out occurred while waiting for memory resources to execute the query (쿼리 실행을 위한 메모리 리소스를 기다리는 중 타임아웃 발생)","severity":"danger","meaning":"쿼리가 실행에 필요한 memory grant를 위해 resource semaphore에서 대기하다가 타임아웃되었다. 메모리 압박으로 grant를 할당할 수 없어 작업이 취소되었다.","cause":"메모리를 많이 사용하는 동시 쿼리(대규모 sort, hash join, hash aggregate)가 쿼리 메모리 풀 전체를 소비해 새 쿼리가 semaphore에서 대기하다 타임아웃되었다. 과도한 memory grant 추정(오래된 통계, 잘못된 실행 계획)이나 부족한 서버 메모리가 근본 원인이다.","fix":"sys.dm_exec_query_memory_grants와 resource semaphore DMV로 메모리를 기다리거나 보유한 쿼리를 식별한다. 통계를 갱신하고 memory grant를 부풀리는 실행 계획을 수정한다(적절한 인덱스, 더 나은 추정). 메모리를 많이 사용하는 쿼리의 동시성을 줄이고, 필요하면 Resource Governor로 memory grant를 제한한다. 서버 메모리 구성(max server memory)을 검토한다.","tags":["memory grant","resource semaphore","memory","timeout","hash join","statistics"]},{"code":"8651","title":"Could not perform the operation because the requested memory grant was not available (요청한 memory grant를 사용할 수 없어 작업을 수행할 수 없음)","severity":"danger","meaning":"쿼리에 필요한 memory grant를 사용할 수 없어 작업을 수행할 수 없었다. 8645와 달리 대기가 아니라 요청한 큰 메모리 블록을 아예 제공하지 못한 것이며, 보통 심각한 메모리 압박을 나타낸다.","cause":"서버의 사용 가능한 쿼리 메모리가 단일 연산자(예: 매우 큰 sort/hash)가 요청한 grant를 충족하지 못한다. 과도하게 높은 메모리 추정, 낮은 max server memory, 외부 메모리 압박, 또는 낮은 Resource Governor 한도인 REQUEST_MAX_MEMORY_GRANT_PERCENT(workload group)나 MAX_MEMORY_PERCENT(resource pool)가 원인일 수 있다.","fix":"memory grant를 부풀리는 쿼리를 최적화한다(통계 갱신, 적절한 인덱싱, 쿼리 분할). 전체 및 사용 가능한 메모리를 모니터링하고, max server memory 설정과 외부 애플리케이션의 메모리 압박을 확인한다. Resource Governor를 사용 중이라면 workload group의 REQUEST_MAX_MEMORY_GRANT_PERCENT와 pool의 MAX_MEMORY_PERCENT 설정을 검토한다. 메모리를 많이 사용하는 배치 작업은 비피크 시간으로 옮긴다.","tags":["memory grant","memory","resource governor","statistics","hash","pressure"]},{"code":"5061","title":"ALTER DATABASE failed because a lock could not be placed on database (데이터베이스에 락을 걸 수 없어 ALTER DATABASE 실패)","severity":"danger","meaning":"ALTER DATABASE 작업(예: single-user 모드 전환, 파일 변경, 옵션 설정)이 다른 활성 연결/세션이 데이터베이스를 사용하고 있어 필요한 락을 획득할 수 없었다.","cause":"데이터베이스가 사용 중일 때 작업을 시도해, 활성 사용자 연결 때문에 SET SINGLE_USER, RESTRICTED_USER 또는 유사한 옵션 변경에 필요한 배타적 락을 획득할 수 없었다. 애플리케이션 연결이 열려 있는 상태에서 잦은 배포/유지보수를 할 때 흔하다.","fix":"데이터베이스에 연결된 다른 세션을 식별하고(sys.dm_exec_sessions, sp_who2) 안전하게 닫는다. 유지보수 시에는 ROLLBACK 옵션으로 열린 트랜잭션을 롤백하면서 모드를 변경한다: ALTER DATABASE [db] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; 애플리케이션 트래픽이 적을 때나 유지보수 시간에 작업을 수행한다. 완료 후 MULTI_USER로 되돌리는 것을 잊지 않는다.","tags":["alter database","lock","single_user","connection","maintenance","rollback"]},{"code":"3702","title":"Cannot drop database because it is currently in use (사용 중이어서 데이터베이스 삭제 불가)","severity":"caution","meaning":"데이터베이스를 삭제하거나 이름을 바꾸려 했지만 활성 연결이 여전히 사용 중이어서 거부되었다. SQL Server는 사용 중인 데이터베이스를 삭제할 수 없다.","cause":"다른 세션(애플리케이션 풀, 열린 SSMS 탭, 백그라운드 작업)이 삭제하려는 데이터베이스에 연결되어 있다. 자신의 세션이 해당 데이터베이스를 컨텍스트(USE)로 사용하는 것만으로도 발생할 수 있다. 모니터링/백업 도구의 열린 연결도 흔한 원인이다.","fix":"삭제 전에 다른 데이터베이스로 전환한다(USE master;). 연결된 세션을 찾아 닫거나, 먼저 데이터베이스를 single-user 모드로 전환한다: ALTER DATABASE [db] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; 이후 DROP DATABASE [db];를 실행한다. 애플리케이션 커넥션 풀을 중지하고 모니터링 도구가 연결을 열지 않는지 확인한다.","tags":["drop database","connection","in use","single_user","use","maintenance"]},{"code":"15138","title":"The database principal owns a schema in the database, and cannot be dropped (database principal이 스키마를 소유하고 있어 삭제 불가)","severity":"caution","meaning":"데이터베이스 사용자/역할(database principal)을 삭제하려 했지만, 해당 principal이 데이터베이스의 스키마를 하나 이상 소유하고 있어 삭제할 수 없었다. 소유한 스키마를 다른 principal에게 이전하기 전까지 삭제할 수 없다.","cause":"제거하려는 사용자가 하나 이상의 스키마의 OWNER다(예: 기본적으로 자신의 이름으로 생성된 스키마, 또는 AUTHORIZATION으로 지정된 스키마). SQL Server는 소유자 없는 스키마가 남지 않도록 삭제를 막는다.","fix":"사용자가 소유한 스키마를 찾는다: SELECT name FROM sys.schemas WHERE principal_id = USER_ID('app_user'); 이 스키마들의 소유권을 다른 principal(예: dbo)에게 이전한다: ALTER AUTHORIZATION ON SCHEMA::[app_schema] TO dbo; 스키마가 더 이상 필요 없고 비어 있다면 DROP SCHEMA. 소유권 이전 후 DROP USER를 다시 실행한다.","tags":["schema","principal","user","owner","alter authorization","drop user"]},{"code":"1934","title":"INSERT/UPDATE failed because the following SET options have incorrect settings (다음 SET 옵션의 설정이 잘못되어 INSERT/UPDATE 실패)","severity":"danger","meaning":"필요한 SET 옵션이 잘못 설정되어 DML 작업(INSERT/UPDATE/DELETE)이 실패했다. 보통 indexed view, 계산 컬럼 인덱스, filtered index, spatial/XML 인덱스에 필수인 SET 설정이 충족되지 않을 때 발생한다.","cause":"영향을 받는 테이블/뷰에 특정 SET 옵션(특히 QUOTED_IDENTIFIER와 ANSI_NULLS ON, NUMERIC_ROUNDABORT OFF 등)이 필요한 객체가 있지만, 세션의 현재 SET 옵션이 그 요구 사항을 충족하지 않는다. 오래된 클라이언트 드라이버나 잘못된 SET 옵션을 명시적으로 설정하는 스크립트가 원인이다.","fix":"연결/세션에 필요한 옵션을 설정한다: SET QUOTED_IDENTIFIER ON; SET ANSI_NULLS ON; SET ANSI_PADDING ON; SET ANSI_WARNINGS ON; SET CONCAT_NULL_YIELDS_NULL ON; SET ARITHABORT ON; SET NUMERIC_ROUNDABORT OFF; 애플리케이션 드라이버가 연결 시 이 옵션을 올바르게 보내는지 확인한다. 오류 메시지에 잘못된 옵션이 명시되어 있으므로 해당 옵션을 수정한다.","tags":["set options","indexed view","computed column","quoted_identifier","ansi_nulls","dml"]},{"code":"468","title":"Cannot resolve the collation conflict","severity":"caution","meaning":"작업(JOIN, UNION, WHERE 비교 또는 동등 비교)에서 collation이 다른 두 문자열 표현식이 만나 SQL Server가 어떤 collation을 적용할지 결정할 수 없었다. 모호성 때문에 작업이 취소되었다.","cause":"비교하는 컬럼이 서로 다른 collation으로 정의되어 있다. 데이터베이스 collation이 서버/tempdb collation과 다르거나, collation이 다른 데이터베이스의 테이블을 조인할 때 가장 흔하다. 임시 테이블(tempdb collation)을 영구 테이블과 조인하는 것이 전형적인 트리거다.","fix":"비교에 명시적 collation을 지정한다: ... WHERE a.col = b.col COLLATE DATABASE_DEFAULT (또는 구체적인 collation 이름). 임시 테이블 컬럼을 생성할 때 COLLATE DATABASE_DEFAULT를 사용한다. 영구 조치로 영향을 받는 컬럼/데이터베이스를 하나의 일관된 collation으로 맞춘다. tempdb와의 불일치가 잦다면 DATABASE_DEFAULT가 가장 실용적인 해결책이다.","tags":["collation","collation","join","tempdb","database_default","conflict"]},{"code":"4864","title":"Bulk load data conversion error (type mismatch or invalid character) (대량 로드 데이터 변환 오류)","severity":"caution","meaning":"대량 로드(BULK INSERT, bcp 또는 OPENROWSET(BULK...)) 중 소스 파일의 필드를 대상 컬럼의 데이터 타입으로 변환할 수 없었다. 보고된 행/컬럼의 데이터가 예상 타입과 일치하지 않는다.","cause":"소스 데이터 파일의 값이 대상 컬럼 타입에 맞지 않는다: 숫자 컬럼의 텍스트, 잘못된 날짜 형식, 예상치 못한 소수점/천 단위 구분자, 잘못된 문자 인코딩(UTF-8/UTF-16), 또는 잘못된 필드/행 종결자로 인한 컬럼 밀림. 헤더 행을 건너뛰지 않는 것도 흔한 원인이다.","fix":"오류가 보고한 행과 컬럼으로 가서 소스 데이터를 확인한다. 필드(FIELDTERMINATOR)와 행(ROWTERMINATOR) 종결자를 파일에 맞추고, FIRSTROW=2로 헤더 행을 건너뛴다. 올바른 코드 페이지/인코딩을 위해 CODEPAGE 또는 DATAFILETYPE을 설정한다. 날짜/숫자 형식을 대상 타입과 호환되게 한다. format file을 사용하면 매핑이 안정적이다.","tags":["bulk insert","bcp","conversion","fieldterminator","encoding","format file"]},{"code":"916","title":"The server principal is not able to access the database under the current security context (서버 보안 주체가 현재 보안 컨텍스트에서 데이터베이스에 접근할 수 없음)","severity":"caution","meaning":"로그인이 특정 데이터베이스에 접근하려 했지만 현재 보안 컨텍스트에서 접근 권한이 없다. 로그인은 서버에 연결할 수 있지만 대상 데이터베이스에 대응하는 사용자/권한이 없다.","cause":"로그인에 매핑된 데이터베이스 사용자가 없거나(orphaned 또는 생성된 적 없음), 데이터베이스 접근이 제한되어 있거나, guest 계정이 비활성화되어 있다. 흔한 사례는 SSMS Object Explorer가 로그인이 접근할 수 없는 데이터베이스의 메타데이터를 읽으려는 경우다.","fix":"대상 데이터베이스에 로그인용 사용자를 생성하고 필요한 권한을 부여한다: USE [db]; CREATE USER [login] FOR LOGIN [login]; 이후 적절한 역할/권한을 할당한다. orphaned user가 있다면 ALTER USER ... WITH LOGIN = ...으로 매핑한다. 접근이 실제로 필요하면 적절한 데이터베이스 역할에 추가하고, 그렇지 않다면 이 오류는 예상된 동작이다(해당 데이터베이스에 대한 접근 권한 없음).","tags":["login","user","access","privilege","security context","orphaned user"]}]},"errors/mongodb-more4.json":{"engineId":"mongodb","engine":"MongoDB","order":5,"errors":[{"code":"216 ElectionInProgress","title":"ElectionInProgress","severity":"caution","meaning":"replica set에서 현재 primary election이 진행 중이다. election이 완료될 때까지 primary가 정해지지 않으므로 쓰기(및 일부 읽기)가 일시적으로 거부된다.","cause":"현재 primary 다운, 네트워크 분할, 유지보수를 위한 step-down, 노드 재시작 후 클러스터가 새 primary를 선출하고 있다. election은 보통 몇 초가 걸리며, 이 시간 동안 도착한 작업이 이 상태를 만난다.","fix":"일시적인 상태다. 드라이버의 재시도 기능(retryable writes/reads)을 활성화해 election이 끝나면 작업이 자동으로 재시도되도록 한다. election이 잦거나 길다면 근본 원인을 조사한다: 네트워크 안정성, 노드 상태, 투표 구성, election 타임아웃 설정(settings.electionTimeoutMillis). rs.status()로 클러스터 상태를 확인한다.","tags":["replica set","election","primary","retry","step-down","high availability"]},{"code":"225 TransactionTooOld","title":"TransactionTooOld","severity":"caution","meaning":"multi-document 트랜잭션 안에서 명령을 실행했지만, 그 트랜잭션 번호가 서버가 해당 세션에서 본 최신 트랜잭션 번호보다 오래되었다. 즉 더 새로운 트랜잭션이 시작되어 이 이전 트랜잭션은 더 이상 유효하지 않다.","cause":"이전 트랜잭션이 완료되기 전에 같은 세션에서 새 트랜잭션이 시작되었거나, 동시성으로 인해 트랜잭션 번호가 순서 없이 증가했다. 보통 세션 객체를 잘못 공유하거나 여러 스레드가 같은 세션을 사용할 때 발생한다.","fix":"논리적 트랜잭션마다 세션 수명 주기를 올바르게 관리하고, 세션을 여러 스레드에서 동시에 공유하지 않는다. 드라이버가 권장하는 withTransaction/callback API로 트랜잭션을 실행한다(재시도와 번호 관리를 처리한다). 새 트랜잭션을 시작하기 전에 이전 트랜잭션을 commit/abort로 닫는다.","tags":["transaction","session","session misuse","concurrency","withtransaction","multi-document transaction"]},{"code":"246 SnapshotUnavailable","title":"SnapshotUnavailable","severity":"caution","meaning":"서버가 해당 시점에 대한 일관된 스냅샷을 제공할 수 없어 요청한 snapshot read concern(readConcern: snapshot)으로 읽기를 수행할 수 없었다. 보통 읽기 타임스탬프가 충분히 진행되지 않았거나 뒤처진 것과 관련이 있다.","cause":"readConcern 'snapshot'을 사용하지만(주로 multi-document 트랜잭션에서) 요청한 atLeast/at 타임스탬프에 대한 일관된 뷰가 아직 없는 경우다. 예: 새로 생성된 컬렉션/인덱스, 카탈로그 변경, 복제 지연. 이 경우 서버가 읽기를 거부한다.","fix":"보통 일시적이다. 잠시 후 작업을 재시도한다(드라이버의 transient transaction error 재시도 메커니즘이 이를 처리한다). 최근 DDL(컬렉션/인덱스 생성)을 실행했다면 그 이후에 트랜잭션을 시작한다. 복제 지연과 노드 상태를 확인한다. 스냅샷 읽기가 실제로 필요하지 않다면 더 적합한 readConcern을 선택한다.","tags":["snapshot","readconcern","transaction","timestamp","retry","replication"]},{"code":"257 TransactionTooLarge","title":"TransactionTooLarge","severity":"danger","meaning":"multi-document 트랜잭션이 허용 크기/작업 수 한도를 초과했다. 트랜잭션에 누적된 변경(oplog 항목 또는 메모리 사용량)이 상한을 넘어 트랜잭션이 거부되었다.","cause":"단일 트랜잭션에서 많은 문서를 수정/삽입해 트랜잭션이 생성한 전체 데이터가 너무 커졌다(예: WiredTiger 캐시에 유지해야 하는 변경량, 또는 transactionLimitBytes 같은 서버 한도). 4.2부터 트랜잭션을 여러 oplog 항목으로 나눌 수 있으므로 16 MB oplog 항목 한도만으로 결정되지는 않는다. 대량 데이터 마이그레이션/ETL에 트랜잭션을 사용하면 이 한도에 쉽게 도달한다.","fix":"트랜잭션을 더 작은 단위로 나누고 배치마다 별도 트랜잭션으로 커밋한다. multi-document 트랜잭션은 짧고 작게 설계되었으므로, 대량 갱신은 트랜잭션 없이(멱등하게, 청크 단위로) 수행하거나 bulk write로 분할하는 것이 바람직하다. 단일 트랜잭션에서 수천 개의 문서를 수정하지 않는다.","tags":["transaction","size","oplog","batch","bulk","bulk write"]},{"code":"276 IndexBuildAborted","title":"IndexBuildAborted","severity":"danger","meaning":"진행 중인 인덱스 생성이 완료 전에 중단/중지되었다. 대상 인덱스가 성공적으로 생성되지 않았다.","cause":"노드의 디스크/메모리 부족, dropIndexes를 통한 명시적 중단, primary step-down/replica 상태 변경, 고유성 위반(unique 인덱스 생성 중 충돌하는 문서), 노드 재시작으로 인덱스 생성이 중단되었을 수 있다.","fix":"mongod 로그에서 중단 이유(디스크 공간, 메모리, 고유성 충돌 등)를 찾아 해결한다. unique 인덱스를 생성한다면 먼저 충돌하는 문서를 정리한다. 충분한 디스크/메모리를 확보하고 replica 상태가 안정적인지 확인한다. 문제를 해결한 후 createIndex로 인덱스를 다시 생성하며, 큰 컬렉션에서는 부하가 낮은 시간을 선택한다.","tags":["index","index build","aborted","unique","disk","replica set"]},{"code":"86 IndexKeySpecsConflict","title":"IndexKeySpecsConflict","severity":"caution","meaning":"생성하려는 인덱스가 기존 인덱스와 같은 키 지정(같은 필드/순서)을 가지지만 옵션이나 이름이 다르다. MongoDB는 같은 키로 충돌하는 두 인덱스 정의를 허용하지 않는다.","cause":"같은 필드에 다른 이름이나 다른 옵션(예: 다른 collation, partialFilterExpression, unique 설정)으로 두 번째 인덱스를 생성하려 한 경우다. 보통 마이그레이션/코드에서 인덱스 정의를 변경하고 다시 실행할 때 발생한다.","fix":"기존 인덱스를 확인한다: db.collection.getIndexes() 같은 키를 가진 충돌 인덱스를 찾는다. 원하는 옵션으로 바꾸려면 먼저 이전 인덱스를 삭제한(dropIndex) 후 새 정의로 다시 생성한다. 이름만 다르다면 기존 인덱스를 계속 사용하고 불필요한 두 번째 정의를 만들지 않는다.","tags":["index","getindexes","collation","unique","conflict","createindex"]},{"code":"166 CommandNotSupportedOnView","title":"CommandNotSupportedOnView","severity":"caution","meaning":"이 명령은 뷰에서 지원되지 않는다. MongoDB 뷰는 읽기 전용이며 특정 읽기 작업만 받고, 쓰기나 지원되지 않는 명령은 거부된다.","cause":"뷰에서 insert/update/delete 같은 쓰기 작업이나 뷰에서 지원되지 않는 특정 명령(예: 일부 text/geo 작업, 직접 인덱스 생성)을 시도한 경우다. 애플리케이션이 뷰를 컬렉션으로 생각하고 쓰려 할 때 나타난다.","fix":"쓰기는 뷰의 기반(소스) 컬렉션에서 수행한다. 뷰는 읽기 전용이다. 명령이 뷰에서 지원되는지 확인하고 지원되는 읽기/aggregation 작업을 사용한다. 쓰기 가능한 구조가 필요하면 뷰 대신 materialized 컬렉션($merge/$out)을 설계한다.","tags":["view","read-only view","read only","aggregation","write","collection"]},{"code":"168 InvalidPipelineOperator","title":"InvalidPipelineOperator","severity":"caution","meaning":"aggregation pipeline에서 인식할 수 없거나 유효하지 않은 연산자/stage를 사용했다. MongoDB가 지정한 연산자 이름을 해석할 수 없어 pipeline을 거부했다.","cause":"철자가 틀린 연산자 이름(예: '$group' 대신 '$groups'), 사용 중인 MongoDB 버전에서 사용할 수 없는 새 연산자, 잘못된 컨텍스트에서 사용한 표현식이 원인이다. $ 접두사 누락이나 유효하지 않은 위치에서 사용한 연산자도 원인이 된다.","fix":"연산자/stage 이름의 철자와 $ 접두사를 확인한다. 서버의 MongoDB 버전이 해당 연산자를 지원하는지 확인한다(새 연산자는 이전 버전에 없다). pipeline을 문서화된 구문과 비교하고, 표현식을 올바른 stage 안에서 사용한다(예: $expr, $project). stage를 분리해 작은 샘플로 테스트한다.","tags":["aggregation","pipeline","operator","syntax","version","expr"]},{"code":"118 NamespaceNotSharded","title":"NamespaceNotSharded","severity":"caution","meaning":"샤딩된 컬렉션에서만 의미가 있는 작업을 아직 샤딩되지 않은 컬렉션/namespace에 대해 실행했다. 대상 컬렉션이 샤딩되지 않아 명령을 적용할 수 없었다.","cause":"샤딩이 활성화되지 않은 컬렉션에서 moveChunk, split 또는 특정 샤드 키 기반 관리 명령을 호출한 경우다. 보통 컬렉션을 shardCollection으로 샤딩하지 않았거나 잘못된 namespace를 대상으로 했기 때문이다.","fix":"컬렉션의 샤딩 상태를 확인한다: sh.status() 또는 config.collections. 작업에 실제로 샤딩이 필요하다면 먼저 데이터베이스에 샤딩을 활성화하고(sh.enableSharding) 컬렉션을 샤딩한다(적절한 샤드 키로 sh.shardCollection). 그렇지 않다면 샤딩되지 않은 컬렉션에는 이런 관리 명령이 필요 없다. 올바른 namespace를 대상으로 하는지 확인한다.","tags":["sharding","shard","namespace","shardcollection","movechunk","cluster"]}]},"errors/redis-more4.json":{"engineId":"redis","engine":"Redis","order":6,"errors":[{"code":"ERR Protocol error: invalid multibulk length","title":"Protocol error: invalid multibulk length (프로토콜 오류: 유효하지 않은 multibulk 길이)","severity":"danger","meaning":"Redis 서버가 클라이언트에서 온 바이트 스트림을 RESP(Redis Serialization Protocol)에 따라 파싱할 수 없었다. multibulk 명령이 선언한 요소 수가 유효하지 않아 프로토콜 오류로 연결이 닫혔다.","cause":"보통 Redis가 아닌 클라이언트(예: HTTP 요청, 잘못된 포트에 연결한 애플리케이션)가 Redis 포트에 원시 데이터를 보내거나, 깨진/불완전한 클라이언트 라이브러리, pipeline의 잘못된 프레이밍, 프로토콜 한도(proto-max-bulk-len)를 넘는 거대한 요청이 원인이다.","fix":"실제 Redis 클라이언트만 Redis 포트에 연결하는지 확인한다(잘못 들어온 HTTP/기타 프로토콜 트래픽 없음). 클라이언트 라이브러리를 업데이트하고 RESP 프레이밍을 깨는 사용자 정의/수동 명령 생성을 피한다. 매우 큰 값을 보낸다면 proto-max-bulk-len 한도와 요청 크기를 검토한다. 네트워크의 중간 프록시가 데이터를 손상시키지 않는지 확인한다.","tags":["protocol","resp","multibulk","client","pipeline","connection"]},{"code":"ERR Number of keys can't be greater than number of args","title":"EVAL/EVALSHA: numkeys exceeds argument count (numkeys가 인수 개수를 초과)","severity":"caution","meaning":"Lua 스크립트(EVAL/EVALSHA/FCALL)를 실행할 때 지정한 키 수(numkeys)가 명령에 실제로 전달된 인수 수보다 크다. Redis가 KEYS와 ARGV 배열을 나눌 수 없어 명령을 거부한다.","cause":"EVAL 구문은 'EVAL script numkeys key [key ...] arg [arg ...]'다. numkeys 값을 잘못 계산했거나(너무 크게 설정) 예상한 키를 명령에 추가하지 않았다. 흔한 프로그래밍 실수다.","fix":"numkeys를 명령에 실제로 전달한 키 수와 정확히 같게 한다. 클라이언트 라이브러리의 EVAL 래퍼(보통 KEYS와 ARGV를 별도 목록으로 받는다)를 사용해 numkeys가 자동 계산되도록 한다. 스크립트를 호출할 때 키는 KEYS로, 다른 파라미터는 ARGV로 올바르게 나눈다.","tags":["lua","eval","evalsha","numkeys","keys","script"]},{"code":"ERR WATCH inside MULTI is not allowed","title":"WATCH inside MULTI is not allowed","severity":"caution","meaning":"열린 MULTI 블록 안에서 WATCH를 보냈다. 낙관적 락을 위해 WATCH는 트랜잭션이 시작되기 전에 키를 감시 대상으로 등록해야 하며, MULTI 이후에 호출하면 거부된다. 이 오류는 트랜잭션을 중단하지 않으며 WATCH만 실패하고 아무것도 감시되지 않는다.","cause":"코드에서 WATCH와 MULTI의 순서가 반대로 작성되었거나(MULTI 먼저, 그다음 WATCH), 클라이언트 라이브러리의 pipeline/트랜잭션 래퍼 안에서 WATCH를 호출했거나, MULTI 상태로 남은 풀 연결을 사용한 경우다. 이 경우 감시되어야 할 키가 변경되어도 EXEC가 실행되므로 기대한 충돌 보호가 동작하지 않는다.","fix":"올바른 순서: WATCH key -> (값 읽기) -> MULTI -> 명령 -> EXEC. EXEC가 nil을 반환하면 감시한 키가 변경된 것이므로 작업을 처음부터 재시도한다. 클라이언트 라이브러리의 WATCH/트랜잭션 API(예: watch + pipeline 헬퍼)를 사용한다. 참고: SUBSCRIBE 같은 pub/sub 명령은 MULTI 안에서 거부되지 않고 큐에 쌓이며, EXEC 후 연결이 subscribe 모드로 전환된다. 이것도 권장되지 않으므로 pub/sub에는 전용 연결을 사용한다.","tags":["multi","watch","transaction","optimistic locking","exec","connection"]},{"code":"ERR WAIT cannot be used with replica instances","title":"WAIT cannot be used with replica instances (replica 인스턴스에서는 WAIT 사용 불가)","severity":"caution","meaning":"replica(replica/slave) 인스턴스에서 WAIT를 실행했다. 쓰기가 지정한 수의 replica에 도달할 때까지 기다리는 WAIT는 primary(master)에서만 의미가 있으며 replica에서는 사용할 수 없다.","cause":"애플리케이션이 실수로 replica 연결로 WAIT를 보냈다. 보통 연결이 읽기 replica로 라우팅되었거나, 역할 변경(페일오버) 후에도 클라이언트가 여전히 replica에 연결되어 있기 때문이다.","fix":"WAIT는 primary 노드로만 보낸다. 클라이언트/프록시 구성이 쓰기와 WAIT 호출을 master로 라우팅하는지 확인한다. 페일오버 후 역할 변경을 감지하고 연결을 새 primary로 재지정한다(ROLE 명령으로 역할을 확인할 수 있다).","tags":["wait","replica","master","write","failover","replication"]},{"code":"ERR Background append only file rewriting already in progress","title":"AOF rewrite already in progress","severity":"caution","meaning":"AOF(Append Only File) rewrite(BGREWRITEAOF)를 시작하려 했지만 이미 AOF rewrite가 진행 중이다. 두 개의 AOF rewrite를 동시에 실행할 수 없다.","cause":"이전 BGREWRITEAOF(수동 또는 auto-aof-rewrite-percentage 트리거에 의한 자동)가 아직 실행 중일 때 새 rewrite 요청이 도착했다. 큰 데이터셋에서는 AOF rewrite가 오래 걸려 이런 충돌이 생길 수 있다.","fix":"실행 중인 rewrite가 끝날 때까지 기다린다. INFO persistence 출력의 aof_rewrite_in_progress 필드를 확인한다. 작업이 정상적으로 실행 중이라면 다른 요청을 보내지 않는다. 충돌이 잦다면 자동 rewrite 임계값(auto-aof-rewrite-percentage/min-size)과 디스크 I/O 성능을 검토한다.","tags":["aof","bgrewriteaof","persistence","rewrite","info","disk"]},{"code":"IOERR error or timeout reading from target instance","title":"MIGRATE: I/O error or timeout to target","severity":"danger","meaning":"MIGRATE 명령(보통 Redis Cluster에서 slot/키 이동 중)이 대상 인스턴스에 연결하거나 응답을 읽는 중 I/O 오류나 타임아웃을 만났다. 키 마이그레이션을 안전하게 완료할 수 없었다.","cause":"소스와 대상 노드 간 네트워크 지연/장애, 대상 노드의 과부하 또는 무응답, 너무 짧은 MIGRATE 타임아웃, 또는 주어진 시간 내에 전송할 수 없는 큰 키가 원인이다. 클러스터 리밸런싱(resharding) 중에 흔하다.","fix":"소스와 대상 노드 간 네트워크 연결과 지연을 확인한다. MIGRATE에 더 긴 타임아웃을 지정한다(특히 큰 키). 대상 노드의 상태/부하와 메모리 상태를 확인한다. resharding 중 큰 키를 옮길 때는 부하가 낮은 시간을 선택하고, 필요하면 마이그레이션을 재시도한다.","tags":["migrate","cluster","resharding","timeout","network","i-o"]},{"code":"ERR Error compiling script (new function)","title":"Error compiling Lua script","severity":"caution","meaning":"EVAL로 보낸 Lua 스크립트를 Redis 내장 Lua 인터프리터가 컴파일할 수 없었다. 스크립트에 구문 오류가 있어 실행 시작 전에 거부되었다.","cause":"Lua 스크립트의 구문 오류: 닫히지 않은 괄호/따옴표, 철자가 틀린 키워드, 누락된 'end', 유효하지 않은 연산자, 지원되지 않는 Lua 구문. 클라이언트 측에서 스크립트를 잘못 연결/이스케이프한 것도 원인이 될 수 있다.","fix":"오류 메시지의 줄/문자 정보를 이용해 Lua 구문을 수정한다(괄호, 따옴표, 'end'의 짝 확인). 별도 Lua 환경에서 컴파일해 스크립트를 검증한다. 클라이언트가 스크립트를 올바르게 생성하는지 확인한다(이스케이프/연결 오류 없음). 검증된 스크립트를 SCRIPT LOAD로 미리 로드하고 EVALSHA로 호출한다.","tags":["lua","eval","compilation","syntax","script load","script"]},{"code":"ERR FAILOVER requires connected replicas.","title":"FAILOVER requires connected replicas","severity":"caution","meaning":"조정된(graceful) 페일오버를 시작하기 위해 FAILOVER를 실행했지만, primary에 연결되어 역할을 넘겨받을 적합한 replica가 없어 거부되었다. 역할을 넘길 대상 없이는 페일오버가 일어날 수 없다.","cause":"primary에 현재 연결된 replica가 없거나(replica가 다운, 네트워크 단절, 동기화되지 않음), 지정한 대상 replica가 적격하지 않거나 연결되어 있지 않다. 수동 FAILOVER(Redis 6.2+)를 호출할 때 복제 토폴로지가 필요한 조건을 충족하지 않는다. 참고: FAILOVER는 standalone(master/replica) 복제에서만 동작하며, Redis Cluster에서는 replica에서 CLUSTER FAILOVER를 사용한다.","fix":"정상이고 연결되어 있으며 최신 상태인 replica가 하나 이상 있는지 확인한다(INFO replication으로 connected_slaves와 동기화 상태 확인). replica의 네트워크/접근 문제를 해결해 primary에 연결되도록 한다. 특정 대상으로 페일오버하려면 FAILOVER TO host port를 사용하고 해당 replica가 적격한지 확인한다.","tags":["failover","replica","master","replication","info replication","high availability"]},{"code":"ERR invalid expire time in 'set' command","title":"invalid expire time in SET","severity":"caution","meaning":"SET(EX/PX/EXAT/PXAT 옵션)에 유효하지 않은 만료 시간을 지정했다. Redis는 0이나 음수 같은 의미 없는 TTL을 받지 않으므로 명령을 거부했다.","cause":"EX/PX로 0이나 음수 값을 보냈거나, 애플리케이션이 만료 시간을 잘못 계산해 유효하지 않은 숫자를 생성한 경우다. SETEX/GETEX 같은 명령도 양수가 아닌 기간에 대해 마찬가지로 이 오류를 반환한다. 일부 클라이언트 버그는 초와 밀리초를 혼동해서 생긴다.","fix":"만료 시간은 항상 0보다 큰 정수로 보낸다. EX는 초, PX는 밀리초를 기대한다. 애플리케이션에서 기간 계산을 검증하고(음수/0 결과가 나오면 안 된다) 단위(초 vs. 밀리초)를 혼동하지 않는다. 키를 즉시 삭제하려면 만료 시간을 설정하는 대신 DEL/UNLINK를 사용한다. 절대 시간은 EXAT/PXAT에 유효한 미래 Unix 타임스탬프를 전달한다.","tags":["set","expire","ttl","ex","px","expiration"]}]},"errors/elasticsearch-more4.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":7,"errors":[{"code":"bind_transport_exception","title":"bind_transport_exception (Address already in use) (주소가 이미 사용 중)","severity":"danger","meaning":"Elasticsearch 노드가 시작하는 중 transport(노드 간 통신) 또는 HTTP 포트에서 수신하기 위한 소켓을 바인딩할 수 없었다. 보통 'Address already in use' 하위 오류와 함께 나타나며 노드가 시작되지 않는다.","cause":"요청한 포트(기본 transport 9300, HTTP 9200)를 다른 프로세스가 이미 사용하고 있다. 가장 흔하게는 같은 노드의 이전 인스턴스가 정상 종료되지 않았거나, 같은 호스트의 다른 ES 인스턴스/서비스가 충돌하거나, network.host/port가 잘못 구성된 경우다. 권한/접근 제한도 바인딩을 막을 수 있다.","fix":"포트를 점유한 프로세스를 찾아(Linux: ss -ltnp | grep 9300 또는 lsof -i:9300) 충돌하는 프로세스를 종료하거나, 노드에 다른 포트를 지정한다(transport.port / http.port). 이전 ES 프로세스가 실제로 종료되었는지 확인한다. network.host와 포트 범위 설정이 올바르고 바인딩 가능한지 확인한다. 이후 노드를 재시작한다.","tags":["transport","port","9300","bind","network.host","startup"]},{"code":"translog_corrupted_exception","title":"translog_corrupted_exception","severity":"danger","meaning":"샤드의 트랜잭션 로그(translog)가 손상되어 읽을 수 없다. 복구 중 Elasticsearch가 translog의 무결성 검사 실패를 감지해 샤드를 시작할 수 없었다.","cause":"translog 파일은 보통 갑작스러운 정전, 디스크 가득 참, 파일 시스템 오류, 하드웨어/디스크 장애, 또는 데이터 디렉터리에 대한 외부 작업(복사, 수동 개입)으로 손상된다. 강제 종료(kill -9) 중 부분적으로 완료된 쓰기도 원인이 될 수 있다.","fix":"먼저 replica나 스냅샷에서 데이터를 복구해 본다. 정상 복사본이 있다면 이를 이용해 손상된 샤드를 재구축하는 것이 가장 안전하다. 다른 복사본이 없고 데이터 손실을 감수한다면 노드를 중지하고 bin/elasticsearch-shard remove-corrupted-data --index <index> --shard-id <n>으로 손상된 translog/데이터를 정리한다(아직 커밋되지 않은 데이터는 영구 손실된다. 이전 elasticsearch-translog 도구는 7.0에서 제거되었다). 도구는 노드 재시작 후 샤드를 할당하는 데 필요한 POST _cluster/reroute 명령(allocate_stale_primary, accept_data_loss: true)을 출력한다. 작업 전에 데이터 디렉터리를 백업한다. 디스크 상태와 여유 공간을 확인한다.","tags":["translog","shard","recovery","corruption","snapshot","disk"]},{"code":"recovery_engine_exception","title":"recovery_engine_exception","severity":"danger","meaning":"샤드 복구 중 엔진 수준 오류가 발생해 샤드를 정상적으로 열 수 없었다. 복구는 노드 재시작, replica 할당, 스냅샷 복원 중에 수행되는 단계다.","cause":"복구 중의 기반 문제: 손상된 Lucene 세그먼트나 translog, 디스크 공간 부족, 파일 권한, 소스와 대상 노드 간 네트워크 중단, 버전/호환성 문제. 보통 더 구체적인 하위 오류(손상, I/O)를 감싸고 있다.","fix":"기반(caused_by) 오류를 확인해 실제 문제(손상, 디스크, 네트워크 등)를 파악한다. 디스크 공간과 파일 권한을 확인한다. replica 할당이 실패하면 _cluster/allocation/explain으로 이유를 확인하고 필요하면 재할당을 트리거한다. 손상이 있다면 정상 replica/스냅샷에서 복구한다. 네트워크 관련이라면 노드 간 연결과 복구 설정을 확인한다.","tags":["recovery","shard","engine","allocation","corruption","disk"]},{"code":"snapshot_in_progress_exception","title":"snapshot_in_progress_exception","severity":"caution","meaning":"요청한 작업이 현재 진행 중인 스냅샷과 충돌해 거부되었다. 예를 들어 스냅샷을 생성하는 동안 인덱스를 삭제/닫거나 리포지토리를 수정하려는 시도가 차단된다.","cause":"스냅샷이 아직 실행 중일 때 해당 스냅샷에 포함된 인덱스를 삭제/닫거나, 리포지토리를 제거/수정하거나, 충돌하는 다른 스냅샷/복원 작업을 시작하려 한 경우다. Elasticsearch는 일관성을 유지하기 위해 이런 충돌 작업을 허용하지 않는다.","fix":"실행 중인 스냅샷이 끝날 때까지 기다린다: GET _snapshot/_status로 진행 상황을 모니터링한다. 작업을 꼭 취소해야 한다면 스냅샷을 삭제해 중지한다: DELETE _snapshot/repo/snapshot_name (실행 중인 스냅샷이 중단된다). 스냅샷이 완료되거나 취소된 후 원래 작업(인덱스 삭제, 리포지토리 변경)을 다시 실행한다.","tags":["snapshot","repository","restore","index deletion","conflict","backup"]},{"code":"repository_verification_exception","title":"repository_verification_exception","severity":"danger","meaning":"스냅샷 리포지토리를 등록하거나 검증하는 중 모든 노드가 리포지토리에 접근할 수 있는지 확인하는 검사가 실패했다. 리포지토리 검증을 통과하지 못해 스냅샷/복원 작업을 안정적으로 수행할 수 없다.","cause":"공유 파일 시스템(fs) 리포지토리가 모든 노드에서 같은 경로로 마운트/접근 가능하지 않거나, 구성에 path.repo가 없거나, 파일 권한이 부족하거나, 클라우드/오브젝트 스토어(S3, GCS, Azure)의 자격 증명/네트워크 접근이 잘못된 경우다. 일부 노드가 리포지토리에 쓸 수 없는 것이 전형적인 원인이다.","fix":"공유 fs 리포지토리의 경우 모든 노드의 elasticsearch.yml에 path.repo가 정의되어 있고 모든 노드에서 같은 경로가 읽기/쓰기 가능하게 마운트되어 있는지 확인한다. 파일 권한(elasticsearch 사용자)을 수정한다. 클라우드 리포지토리는 자격 증명(keystore), 버킷 이름, 네트워크/접근 권한을 확인한다. POST _snapshot/repo/_verify로 리포지토리를 다시 검증한다.","tags":["repository","snapshot","path.repo","permission","s3","verification"]},{"code":"reduce_search_phase_exception","title":"reduce_search_phase_exception","severity":"danger","meaning":"검색의 reduce 단계에서 오류가 발생했다. 조정 노드가 여러 샤드의 부분 결과/aggregation을 병합(reduce)하는 중 문제를 만나 최종 결과를 생성할 수 없었다.","cause":"보통 결과 병합 중 메모리/리소스 한도 초과(예: 매우 큰 aggregation, circuit breaker 작동), 호환되지 않거나 유효하지 않은 aggregation 구조, 또는 샤드가 반환한 부분 결과를 병합할 수 없는 경우다. 카디널리티가 매우 높은 aggregation에서 흔하다.","fix":"caused_by 세부 정보를 읽어 근본 원인을 파악한다. 무거운 aggregation을 가볍게 한다: 카디널리티가 높은 terms aggregation의 size를 낮추고, composite aggregation으로 페이지를 나누고, 불필요한 하위 aggregation을 줄인다. 메모리 압박을 모니터링하고 필요하면 쿼리를 더 작은 부분으로 나눈다. circuit breaker가 작동하면 쿼리 비용을 줄인다.","tags":["search","reduce","aggregation","coordinating node","memory","cardinality"]},{"code":"aggregation_execution_exception","title":"aggregation_execution_exception","severity":"caution","meaning":"aggregation 실행 중 오류가 발생했다. aggregation 정의는 파싱되었지만 대상 필드/값을 처리하는 런타임에 문제가 생겼다.","cause":"aggregation이 필드 타입과 호환되지 않거나(예: fielddata 없이 text 필드 집계), 정렬/계산 중 유효하지 않은 값, 존재하지 않는 경로(bucket path)를 참조하는 pipeline aggregation, 지원되지 않는 필드 타입 조합이 원인이다. 스크립트 기반 aggregation의 런타임 오류도 원인이 된다.","fix":"오류 세부 정보의 필드/aggregation 이름을 확인한다. 집계하는 필드가 적합한 타입인지 확인한다(text 대신 keyword/numeric, 필요하면 doc_values가 활성화된 필드 사용). pipeline aggregation에서는 buckets_path가 실제 bucket 이름과 일치하는지 확인한다. 스크립트 aggregation에 null/타입 검사를 추가한다. 적절한 필드 타입으로 매핑을 수정한다.","tags":["aggregation","execution","fielddata","keyword","pipeline","mapping"]},{"code":"failed_node_exception","title":"failed_node_exception","severity":"danger","meaning":"클러스터 전체 작업(예: 검색, 통계, 관리 명령) 중 클러스터의 특정 노드가 요청을 처리하거나 응답할 수 없었다. 조정 노드가 해당 노드의 실패를 이 예외로 감싼다.","cause":"노드가 크래시되었거나, 네트워크에서 이탈했거나, 과부하/GC pause로 응답할 수 없었거나, 디스크/리소스 문제, 또는 해당 노드의 샤드 오류가 원인이다. 클러스터 재구성(노드 이탈/합류) 중 일시적으로 나타날 수도 있다.","fix":"caused_by로 어떤 노드에서 왜 실패했는지 파악한다. 노드 상태 확인: GET _cat/nodes, GET _nodes/stats (CPU, 메모리, GC, 디스크). 노드가 리소스 압박을 받고 있다면 부하/heap 설정을 검토하고 네트워크 연결을 확인한다. 노드가 클러스터를 떠났다면 다시 합류하고 샤드 할당이 복구될 때까지 기다린다(GET _cluster/health).","tags":["node","cluster","network","gc","shard","resources"]},{"code":"elasticsearch_parse_exception","title":"elasticsearch_parse_exception","severity":"caution","meaning":"Elasticsearch가 요청 본문(JSON 쿼리, 설정 또는 구성)을 파싱하는 중 일반 파싱 오류를 만났다. 요청이 구조적으로 예상 형식과 일치하지 않는다.","cause":"형식이 잘못되었거나 유효하지 않은 JSON(누락된 쉼표, 닫히지 않은 괄호), 잘못 배치된 필드, 예상치 못한 값 타입(예: 숫자가 필요한 곳에 텍스트), 유효하지 않은 시간/범위 표현식이 원인이다. DSL 쿼리의 잘못된 중첩도 이 예외를 발생시킨다.","fix":"요청 본문을 JSON 검증기로 확인하고 괄호, 따옴표, 쉼표를 점검한다. Elasticsearch DSL 문서와 대조해 필드가 올바른 위치에 올바른 타입으로 있는지 확인한다. 오류 메시지의 위치/필드 힌트를 이용해 문제 부분을 수정한다. 클라이언트 라이브러리를 사용한다면 직렬화가 유효한 JSON을 생성하는지 확인한다.","tags":["parse","json","dsl","query","parsing","request"]}]},"health/oracle.json":{"engineId":"oracle","engine":"Oracle","order":1,"checks":[{"id":"version-uptime","group":"일반","title":"버전 및 가동 시간","command":"SELECT i.instance_name, i.version_full, i.status, i.startup_time, ROUND(SYSDATE - i.startup_time, 2) AS uptime_days FROM v$instance i;","expect":"인스턴스 상태는 OPEN이어야 한다. startup_time이 예상보다 최근이면(방금 시작됨) 계획되지 않은 재시작이 발생했을 수 있으므로 조사한다. version_full 컬럼은 18c+이며, 12c 이하에서는 version 컬럼을 사용한다.","severity":"safe"},{"id":"alertlog-errors","group":"일반","title":"alert log의 최근 치명적 오류(ORA-)","command":"SELECT originating_timestamp, message_text FROM v$diag_alert_ext WHERE message_text LIKE '%ORA-%' AND originating_timestamp > SYSTIMESTAMP - INTERVAL '1' DAY ORDER BY originating_timestamp DESC FETCH FIRST 50 ROWS ONLY;","expect":"최근 24시간 동안 ORA- 오류가 반환되지 않아야 한다. ORA-00600, ORA-07445, ORA-01578(블록 손상) 같은 항목은 즉각 조치가 필요하다.","severity":"danger"},{"id":"invalid-objects","group":"일반","title":"INVALID 객체","command":"SELECT owner, object_type, COUNT(*) AS invalid_count FROM dba_objects WHERE status = 'INVALID' GROUP BY owner, object_type ORDER BY invalid_count DESC;","expect":"INVALID 객체는 없는 것이 이상적이다. 있으면 EXEC DBMS_UTILITY.COMPILE_SCHEMA 또는 utlrp.sql로 재컴파일하며, SYS/SYSTEM 아래의 무효 객체가 우선이다.","severity":"caution"},{"id":"tablespace-usage","group":"용량","title":"테이블스페이스 사용률","command":"SELECT df.tablespace_name, ROUND(df.total_mb, 1) AS total_mb, ROUND(df.total_mb - NVL(fs.free_mb, 0), 1) AS used_mb, ROUND((df.total_mb - NVL(fs.free_mb, 0)) / df.total_mb * 100, 1) AS pct_used FROM (SELECT tablespace_name, SUM(bytes)/1024/1024 total_mb FROM dba_data_files GROUP BY tablespace_name) df LEFT JOIN (SELECT tablespace_name, SUM(bytes)/1024/1024 free_mb FROM dba_free_space GROUP BY tablespace_name) fs ON df.tablespace_name = fs.tablespace_name ORDER BY pct_used DESC;","expect":"pct_used가 85%를 넘으면 경고, 90%를 넘으면 위험 임계값이다. autoextend가 꺼진 테이블스페이스는 가득 찰 위험이 더 크므로 데이터 파일을 추가하거나 autoextend를 활성화한다.","severity":"danger"},{"id":"largest-segments","group":"용량","title":"가장 큰 세그먼트(테이블/인덱스)","command":"SELECT owner, segment_name, segment_type, ROUND(bytes/1024/1024/1024, 2) AS size_gb FROM dba_segments ORDER BY bytes DESC FETCH FIRST 20 ROWS ONLY;","expect":"공간을 가장 많이 사용하는 세그먼트를 보여 준다. 예상치 못한 증가는 아카이빙/정리나 파티셔닝이 필요하다는 신호일 수 있다.","severity":"safe"},{"id":"undo-temp-usage","group":"용량","title":"UNDO 및 TEMP 테이블스페이스 사용률","command":"SELECT ts.tablespace_name, ts.contents, ROUND(SUM(ss.used_blocks) * ts.block_size / 1024 / 1024, 1) AS used_mb FROM v$sort_segment ss JOIN (SELECT tablespace_name, contents, block_size FROM dba_tablespaces WHERE contents = 'TEMPORARY') ts ON ss.tablespace_name = ts.tablespace_name GROUP BY ts.tablespace_name, ts.contents, ts.block_size UNION ALL SELECT tablespace_name, 'UNDO', ROUND(SUM(bytes)/1024/1024, 1) FROM dba_undo_extents WHERE status = 'ACTIVE' GROUP BY tablespace_name;","expect":"TEMP 사용량이 계속 상한에 도달하면 대규모 sort/hash 작업이 있는 것이다. 활성 UNDO 공간이 retention을 충족하지 못하면 ORA-01555(snapshot too old) 위험이 있다.","severity":"caution"},{"id":"sessions-vs-limit","group":"용량","title":"세션 수 대비 한도","command":"SELECT resource_name, current_utilization, max_utilization, limit_value FROM v$resource_limit WHERE resource_name IN ('sessions','processes','transactions');","expect":"current_utilization이 limit_value에 가까워지면(특히 85% 이상) 새 연결이 거부될 수 있다. max_utilization은 과거 최고치를 보여 준다. processes 파라미터를 검토한다.","severity":"caution"},{"id":"fra-usage","group":"용량","title":"Flash Recovery Area(FRA) 사용률","command":"SELECT name, ROUND(space_limit/1024/1024/1024, 2) AS limit_gb, ROUND(space_used/1024/1024/1024, 2) AS used_gb, ROUND(space_used/space_limit*100, 1) AS pct_used FROM v$recovery_file_dest;","expect":"FRA가 85%에 도달하면 alert log에 경고가 기록되고, 100%가 되면 아카이브 로그 기록이 중단되어 데이터베이스가 멈춘다(archiver hung, ORA-00257). RMAN 백업/아카이브를 정리하거나 db_recovery_file_dest_size를 늘린다.","severity":"danger"},{"id":"buffer-cache-hit","group":"성능","title":"버퍼 캐시 적중률","command":"SELECT ROUND((1 - (phy.value - dir.value) / (con.value + cur.value)) * 100, 2) AS buffer_hit_ratio FROM v$sysstat con, v$sysstat cur, v$sysstat phy, v$sysstat dir WHERE con.name = 'consistent gets' AND cur.name = 'db block gets' AND phy.name = 'physical reads' AND dir.name = 'physical reads direct';","expect":"OLTP 시스템에서는 일반적으로 90% 이상이 기대된다. 지속적으로 낮으면 SGA/버퍼 캐시가 작거나 인덱싱이 부실할 수 있다. 단독으로 보지 말고 대기 이벤트와 함께 해석한다.","severity":"safe"},{"id":"top-sql","group":"성능","title":"가장 비용이 큰 SQL 문(경과 시간)","command":"SELECT sql_id, ROUND(elapsed_time/1000000, 1) AS elapsed_sec, executions, ROUND(elapsed_time/1000000/GREATEST(executions,1), 3) AS sec_per_exec, SUBSTR(sql_text, 1, 100) AS sql_text FROM v$sqlarea ORDER BY elapsed_time DESC FETCH FIRST 15 ROWS ONLY;","expect":"총 경과 시간이 가장 큰 쿼리. sec_per_exec이 높거나 executions가 매우 많은 쿼리는 튜닝 대상이며, AWR/SQL Tuning Advisor로 분석한다.","severity":"safe"},{"id":"long-active-sessions","group":"성능","title":"장시간 실행 중인 활성 세션","command":"SELECT s.sid, s.serial#, s.username, s.status, s.last_call_et AS active_seconds, s.event, SUBSTR(q.sql_text, 1, 80) AS sql_text FROM v$session s LEFT JOIN v$sqlarea q ON s.sql_id = q.sql_id WHERE s.status = 'ACTIVE' AND s.type = 'USER' AND s.last_call_et > 60 ORDER BY s.last_call_et DESC;","expect":"last_call_et(초)가 매우 큰 활성 세션은 멈춰 있거나 무거운 쿼리를 실행 중일 수 있다. event 컬럼은 대기 중인 자원을 보여 준다.","severity":"caution"},{"id":"blocking-sessions","group":"성능","title":"블로킹 및 대기 세션(락)","command":"SELECT bs.sid AS blocker_sid, bs.username AS blocker_user, ws.sid AS waiter_sid, ws.username AS waiter_user, ws.seconds_in_wait, ws.event FROM v$session ws JOIN v$session bs ON ws.blocking_session = bs.sid WHERE ws.blocking_session IS NOT NULL ORDER BY ws.seconds_in_wait DESC;","expect":"반환되는 행이 없어야 한다. 블로킹 세션은 애플리케이션 지연/데드락을 일으킨다. blocker_sid가 오랫동안 락을 보유하면(seconds_in_wait가 큼) 애플리케이션 팀과 함께 판단하고, 필요하면 ALTER SYSTEM KILL SESSION을 실행한다.","severity":"danger"},{"id":"rman-last-backup","group":"백업 및 복구","title":"마지막 성공 RMAN 백업 상태","command":"SELECT input_type, status, start_time, end_time, ROUND(output_bytes/1024/1024/1024, 2) AS output_gb FROM v$rman_backup_job_details ORDER BY start_time DESC FETCH FIRST 10 ROWS ONLY;","expect":"최근 백업의 status가 COMPLETED이고 start_time이 RPO 정책을 충족할 만큼 최신이어야 한다(예: 최근 24시간 이내). FAILED 또는 'COMPLETED WITH WARNINGS' 항목은 조사해야 한다.","severity":"danger"},{"id":"archivelog-mode","group":"백업 및 복구","title":"Archivelog 모드 및 archiver 상태","command":"SELECT log_mode, force_logging, (SELECT COUNT(*) FROM v$archived_log WHERE first_time > SYSDATE - 1) AS logs_last_24h FROM v$database;","expect":"운영 데이터베이스는 log_mode = ARCHIVELOG여야 한다(그렇지 않으면 특정 시점 복구 불가). 최근 24시간 동안 생성된 아카이브가 없으면 archiver가 멈췄을 수 있다.","severity":"danger"},{"id":"locked-expired-accounts","group":"보안","title":"잠기거나 만료된 계정","command":"SELECT username, account_status, lock_date, expiry_date, profile FROM dba_users WHERE account_status != 'OPEN' ORDER BY username;","expect":"LOCKED/EXPIRED 상태의 핵심 애플리케이션 계정은 예기치 않은 장애를 일으킨다. 기본 계정(SCOTT, HR 등)이 잠기거나 비활성화된 것은 바람직하다. EXPIRED(GRACE)는 비밀번호 만료가 임박했음을 나타낸다.","severity":"caution"},{"id":"privileged-users","group":"보안","title":"높은 권한 사용자(DBA 롤)","command":"SELECT grantee, granted_role, admin_option FROM dba_role_privs WHERE granted_role IN ('DBA','PDB_DBA') AND grantee NOT IN ('SYS','SYSTEM') ORDER BY grantee;","expect":"DBA 롤은 필요하고 알려진 계정만 보유해야 한다. 예상치 못한 grantee나 admin_option = YES인 계정은 권한 상승 위험이 있으므로 최소 권한 원칙에 따라 검토한다. SYSDBA/SYSOPER 권한 계정도 확인한다: SELECT username, sysdba, sysoper FROM v$pwfile_users;","severity":"caution"},{"id":"default-password-users","group":"보안","title":"기본 비밀번호를 바꾸지 않은 열린 계정","command":"SELECT du.username, du.account_status, du.profile, CASE WHEN dp.username IS NOT NULL THEN 'YES' ELSE 'NO' END AS has_default_password FROM dba_users du LEFT JOIN (SELECT username FROM dba_users_with_defpwd) dp ON du.username = dp.username WHERE du.account_status = 'OPEN' ORDER BY du.username;","expect":"has_default_password = YES인 계정은 즉시 비밀번호를 바꾸거나 잠가야 한다(ALTER USER ... ACCOUNT LOCK). 비밀번호 수명 정책은 DBA_PROFILES의 PASSWORD_LIFE_TIME도 확인한다.","severity":"caution"},{"id":"dataguard-lag","group":"복제","title":"Data Guard standby 지연","command":"SELECT name, value, unit, time_computed FROM v$dataguard_stats WHERE name IN ('apply lag','transport lag');","expect":"apply lag와 transport lag는 가능한 한 0에 가까워야 한다. 분/시간 단위의 지연은 네트워크 또는 MRP(Managed Recovery Process) 문제를 나타내며, 페일오버 대상이 위험해진다.","severity":"danger"},{"id":"dataguard-mrp","group":"복제","title":"standby 적용 프로세스(MRP) 상태","command":"SELECT name, role, action, thread#, sequence#, block# FROM v$dataguard_process WHERE name LIKE 'MRP%' OR role LIKE 'RFS%' ORDER BY name;","expect":"physical standby에서는 MRP0 프로세스가 action APPLYING_LOG 또는 WAIT_FOR_LOG 상태로 보여야 한다. MRP가 없으면 적용이 중단된 것이고, RFS가 없으면 primary에서 로그가 오지 않는 것이다. v$dataguard_process는 12.2+이며, 12.1 이하에서는 (현재 deprecated된) v$managed_standby(process, status 컬럼)를 사용한다.","severity":"danger"}]},"health/postgres.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"checks":[{"id":"version-uptime","group":"일반","title":"버전 및 가동 시간","command":"SELECT version() AS pg_version, pg_postmaster_start_time() AS started_at, date_trunc('second', now() - pg_postmaster_start_time()) AS uptime;","expect":"버전과 시작 시각을 보여 준다. 가동 시간이 예상보다 짧으면(최근 시작) 계획되지 않은 재시작이나 크래시가 발생했을 수 있으므로 로그를 확인한다.","severity":"safe"},{"id":"recent-errors","group":"일반","title":"체크포인트 통계","command":"-- PG17+:\nSELECT num_timed, num_requested, buffers_written, stats_reset FROM pg_stat_checkpointer;\n-- PG16 and earlier:\nSELECT checkpoints_timed, checkpoints_req, buffers_checkpoint, buffers_backend, stats_reset FROM pg_stat_bgwriter;","expect":"요청된 체크포인트 수(num_requested / checkpoints_req)가 시간 기반 체크포인트 수(num_timed / checkpoints_timed)에 비해 많으면 WAL이 너무 빨리 차는 것이므로 max_wal_size를 늘린다. PG17에서 카운터가 pg_stat_bgwriter에서 pg_stat_checkpointer로 이동했다.","severity":"caution"},{"id":"table-bloat","group":"일반","title":"비대해진 테이블 및 dead tuple 비율","command":"SELECT schemaname, relname, n_live_tup, n_dead_tup, CASE WHEN n_live_tup > 0 THEN round(n_dead_tup * 100.0 / n_live_tup, 1) ELSE 0 END AS dead_pct, last_autovacuum FROM pg_stat_user_tables WHERE n_dead_tup > 1000 ORDER BY n_dead_tup DESC LIMIT 20;","expect":"dead_pct가 높은(예: 20% 이상) 테이블은 비대해진 상태이며 VACUUM 성능과 디스크 사용량에 영향을 준다. last_autovacuum이 아주 오래되었으면 autovacuum 설정을 검토하거나 수동 VACUUM (ANALYZE)를 실행한다.","severity":"caution"},{"id":"database-size","group":"용량","title":"데이터베이스 크기","command":"SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size, pg_database_size(datname) AS size_bytes FROM pg_database WHERE datistemplate = false ORDER BY pg_database_size(datname) DESC;","expect":"데이터베이스 크기를 보여 준다. 예상치 못한 증가는 디스크 용량(df -h)과 함께 평가한다. 디스크가 가득 차면 PostgreSQL은 쓰기 작업을 중단한다.","severity":"danger"},{"id":"largest-tables","group":"용량","title":"가장 큰 테이블(인덱스 포함)","command":"SELECT schemaname, relname, pg_size_pretty(pg_total_relation_size(relid)) AS total_size, pg_size_pretty(pg_relation_size(relid)) AS table_size, pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) AS index_toast_size FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 20;","expect":"가장 많은 공간을 차지하는 테이블을 보여 준다. index_toast_size가 테이블 크기보다 훨씬 크면 과도하거나 사용되지 않는 인덱스가 있을 수 있다. 아카이빙이나 파티셔닝을 고려한다.","severity":"safe"},{"id":"temp-files","group":"용량","title":"임시 파일 사용량","command":"SELECT datname, temp_files, pg_size_pretty(temp_bytes) AS temp_size, stats_reset FROM pg_stat_database WHERE temp_files > 0 ORDER BY temp_bytes DESC;","expect":"temp_files/temp_bytes가 높으면 work_mem이 부족해 sort/hash 작업이 디스크로 넘치고 있다는 뜻이다. work_mem을 늘리거나 쿼리를 최적화한다.","severity":"caution"},{"id":"connections-vs-limit","group":"용량","title":"연결 수 대비 max_connections","command":"SELECT (SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend') AS current_connections, (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_connections, round((SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend') * 100.0 / (SELECT setting::int FROM pg_settings WHERE name = 'max_connections'), 1) AS pct_used;","expect":"클라이언트 연결만 집계한다(pg_stat_activity에는 백그라운드 프로세스도 포함되므로 backend_type = 'client backend' 필터가 필요하다). pct_used가 80%를 넘으면 새 연결이 거부될 위험이 있다(특히 superuser_reserved_connections에 도달한 경우). 연결 풀러(PgBouncer)가 권장된다.","severity":"caution"},{"id":"wal-directory-size","group":"용량","title":"WAL 디렉터리 크기 및 replication slot 보존","command":"SELECT (SELECT count(*) FROM pg_ls_waldir()) AS wal_file_count, pg_size_pretty((SELECT sum(size) FROM pg_ls_waldir())) AS wal_total_size, slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal FROM pg_replication_slots ORDER BY slot_name;","expect":"비활성(active = false) replication slot은 WAL을 누적시켜 pg_wal 디렉터리를 가득 채운다. retained_wal이 크면 slot의 소유자(slot_type, database, plugin)를 확인한다. active = false는 일시적으로 연결이 끊긴 standby나 논리 구독자/CDC(예: Debezium) slot을 의미할 수도 있다. 실제로 사용되지 않는다면 pg_drop_replication_slot으로 삭제한다. 삭제한 slot에 의존하던 standby는 재구축해야 하며, 구독자/CDC 측은 재동기화해야 한다. 영구적인 보호를 위해 max_slot_wal_keep_size(PG13+)로 상한을 설정한다.","severity":"danger"},{"id":"cache-hit-ratio","group":"성능","title":"버퍼 캐시 적중률","command":"SELECT sum(heap_blks_read) AS heap_read, sum(heap_blks_hit) AS heap_hit, round(sum(heap_blks_hit) * 100.0 / NULLIF(sum(heap_blks_hit) + sum(heap_blks_read), 0), 2) AS cache_hit_ratio FROM pg_statio_user_tables;","expect":"OLTP 워크로드에서는 일반적으로 99% 이상이 목표다. 지속적으로 낮으면 shared_buffers가 너무 작거나 작업 집합이 메모리에 들어가지 않는 것일 수 있다.","severity":"safe"},{"id":"top-queries","group":"성능","title":"가장 비용이 큰 쿼리(pg_stat_statements)","command":"SELECT round(total_exec_time::numeric, 1) AS total_ms, calls, round(mean_exec_time::numeric, 2) AS mean_ms, round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 1) AS pct, left(query, 100) AS query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 15;","expect":"pg_stat_statements가 shared_preload_libraries에 있어야 하고(재시작 필요) 데이터베이스에 설치되어 있어야 한다(CREATE EXTENSION pg_stat_statements). total_exec_time/mean_exec_time 이름은 PG13+용이며, PG12는 total_time/mean_time을 사용한다. 총 시간을 가장 많이 소비하는 쿼리가 튜닝 대상이며, mean_ms가 높은 쿼리는 EXPLAIN ANALYZE를 실행한다.","severity":"safe"},{"id":"long-running-queries","group":"성능","title":"장시간 실행 중인 활성 쿼리","command":"SELECT pid, usename, state, now() - query_start AS duration, wait_event_type, wait_event, left(query, 80) AS query FROM pg_stat_activity WHERE state = 'active' AND now() - query_start > interval '1 minute' AND backend_type = 'client backend' ORDER BY duration DESC;","expect":"1분 이상 실행 중인 활성 쿼리는 멈춰 있거나 무거울 수 있다. wait_event는 대기 대상을 보여 준다. 이 쿼리는 활성 쿼리만 나열하므로, 'idle in transaction' 세션(VACUUM이 dead tuple을 제거하지 못하게 함)은 state = 'idle in transaction'과 xact_start로 별도 확인한다.","severity":"caution"},{"id":"blocking-locks","group":"성능","title":"블로킹 세션 및 대기 중인 락","command":"SELECT blocked.pid AS blocked_pid, blocked.usename AS blocked_user, blocking.pid AS blocking_pid, blocking.usename AS blocking_user, now() - blocked.query_start AS blocked_duration, left(blocked.query, 60) AS blocked_query FROM pg_stat_activity blocked JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid)) WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;","expect":"반환되는 행이 없어야 한다. 블로킹 세션(blocking_pid)은 애플리케이션 지연과 타임아웃을 일으킨다. 오래 블로킹하는 트랜잭션을 조사하고 필요하면 pg_terminate_backend로 종료한다.","severity":"danger"},{"id":"archiver-status","group":"백업 및 복구","title":"WAL 아카이빙 상태(pg_stat_archiver)","command":"SELECT archived_count, last_archived_wal, last_archived_time, failed_count, last_failed_wal, last_failed_time, now() - last_archived_time AS since_last_archive FROM pg_stat_archiver;","expect":"failed_count는 0이어야 하고 last_failed_time은 last_archived_time보다 오래되어야 한다. since_last_archive가 매우 크면 아카이빙이 중단된 것이며(archive_command 실패), PITR 백업 체인이 끊긴다.","severity":"danger"},{"id":"last-basebackup","group":"백업 및 복구","title":"마지막 백업 및 WAL 아카이브 최신성","command":"-- Base backup time is not stored in the database; read it from the backup tool: pgbackrest info / barman list-backup <server>\nSELECT pg_is_in_recovery() AS in_recovery, last_archived_wal, last_archived_time, now() - last_archived_time AS since_last_archive, failed_count, last_failed_time FROM pg_stat_archiver;","expect":"마지막 base backup 시각은 백업 도구(pgBackRest / Barman / pg_basebackup 작업 로그)에서 확인하며, RPO 정책을 충족할 만큼 최신이어야 한다. PITR을 위해서는 WAL 아카이브도 연속적이어야 한다. since_last_archive가 크거나 failed_count가 증가하면 아카이브 체인이 끊겼을 수 있다.","severity":"danger"},{"id":"locked-expired-roles","group":"보안","title":"만료되었거나 만료 예정인 로그인 롤","command":"SELECT rolname, rolcanlogin, rolvaliduntil, rolconnlimit FROM pg_roles WHERE rolcanlogin = true AND (rolvaliduntil IS NOT NULL AND rolvaliduntil < now() + interval '7 days') ORDER BY rolvaliduntil;","expect":"rolvaliduntil이 지난 애플리케이션 롤은 로그인이 차단되어 장애를 일으킨다. 향후 7일 내 만료되는 비밀번호는 미리 갱신을 계획한다.","severity":"caution"},{"id":"superuser-roles","group":"보안","title":"높은 권한 롤(SUPERUSER / 생성 권한)","command":"SELECT rolname, rolsuper, rolcreaterole, rolcreatedb, rolbypassrls FROM pg_roles WHERE rolsuper = true OR rolcreaterole = true OR rolbypassrls = true ORDER BY rolsuper DESC, rolname;","expect":"SUPERUSER는 필요하고 알려진 계정에만 있어야 한다. rolbypassrls = true인 롤은 행 수준 보안(RLS)을 우회한다. 예상치 못한 높은 권한은 최소 권한 원칙에 따라 회수한다.","severity":"caution"},{"id":"hba-trust-auth","group":"보안","title":"취약한 인증(trust) 규칙","command":"SELECT type, database, user_name, address, auth_method FROM pg_hba_file_rules WHERE auth_method IN ('trust','password') ORDER BY line_number;","expect":"auth_method = trust는 비밀번호 없는 접근을 허용하므로 운영 환경에서 위험하며, 'password'는 비밀번호를 평문으로 전송한다. scram-sha-256으로 교체한다. (PG 10+ 필요.)","severity":"danger"},{"id":"replication-lag","group":"복제","title":"스트리밍 복제 지연(primary 측)","command":"SELECT client_addr, state, sync_state, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn)) AS sent_lag, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS replay_lag, write_lag, flush_lag, replay_lag AS replay_time_lag FROM pg_stat_replication ORDER BY client_addr;","expect":"primary에서 실행한다. state는 streaming이어야 하고 replay_lag(바이트/시간)는 작아야 한다. standby가 아예 보이지 않으면 연결이 끊긴 것이다. sync_state = async이면 동기 복제가 예상대로 동작하지 않는 것이다.","severity":"danger"},{"id":"replica-lag-standby","group":"복제","title":"복제 지연(standby 측)","command":"SELECT pg_is_in_recovery() AS in_recovery, CASE WHEN pg_is_in_recovery() THEN date_trunc('second', now() - pg_last_xact_replay_timestamp()) ELSE NULL END AS replication_delay, pg_last_wal_receive_lsn() AS receive_lsn, pg_last_wal_replay_lsn() AS replay_lsn;","expect":"standby 서버에서 실행한다. in_recovery는 true여야 한다. replication_delay가 분/시간 단위이면 적용 지연이 있을 수 있다. 다만 이 값은 primary에 쓰기가 없을 때도 커지므로(오탐) LSN 차이와 함께 해석한다. receive_lsn과 replay_lsn의 차이가 크면 WAL replay가 따라가지 못하는 것이다.","severity":"danger"}]},"health/mysql.json":{"engineId":"mysql","engine":"MySQL","order":3,"checks":[{"id":"version-uptime","group":"일반","title":"버전 및 가동 시간","command":"SELECT VERSION() AS mysql_version, VARIABLE_VALUE AS uptime_seconds, ROUND(VARIABLE_VALUE/86400, 2) AS uptime_days FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Uptime';","expect":"버전과 가동 시간(초)을 보여 준다. uptime_seconds가 예상보다 작으면 계획되지 않은 재시작이나 크래시가 발생했을 수 있으므로 error log를 확인한다.","severity":"safe"},{"id":"error-log-status","group":"일반","title":"최근 error log 항목(performance_schema)","command":"SELECT logged, prio, error_code, subsystem, LEFT(data, 120) AS message FROM performance_schema.error_log WHERE prio IN ('Error','Warning') ORDER BY logged DESC LIMIT 30;","expect":"최근 오류와 경고를 보여 준다(MySQL 8.0+ 필요). prio = 'Error' 항목(특히 InnoDB, 락, 디스크 오류)을 조사한다. 이전 버전에서는 서버의 log_error 파일을 확인한다.","severity":"danger"},{"id":"crashed-tables","group":"일반","title":"손상된 테이블 / 점검할 테이블","command":"SELECT table_schema, table_name, engine, table_rows, update_time FROM information_schema.tables WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys') AND engine IS NOT NULL ORDER BY table_schema, table_name;","expect":"각 테이블의 엔진과 마지막 갱신 시각을 나열한다. MyISAM 테이블은 크래시될 수 있으므로, 의심되면 CHECK TABLE ... 또는 mysqlcheck로 무결성을 검증한다. 핵심 데이터에는 InnoDB가 권장된다.","severity":"caution"},{"id":"schema-sizes","group":"용량","title":"스키마(데이터베이스) 크기","command":"SELECT table_schema, ROUND(SUM(data_length + index_length)/1024/1024, 1) AS size_mb, ROUND(SUM(data_free)/1024/1024, 1) AS free_mb FROM information_schema.tables GROUP BY table_schema ORDER BY SUM(data_length + index_length) DESC;","expect":"스키마 크기와 회수 가능한 data_free 공간을 보여 준다. 디스크 용량과 함께 평가한다. free_mb가 높으면 OPTIMIZE TABLE로 공간을 회수할 수 있다.","severity":"danger"},{"id":"largest-tables","group":"용량","title":"가장 큰 테이블(data_free / 단편화 포함)","command":"SELECT table_schema, table_name, engine, ROUND((data_length + index_length)/1024/1024, 1) AS total_mb, ROUND(data_free/1024/1024, 1) AS data_free_mb, table_rows FROM information_schema.tables WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys') ORDER BY (data_length + index_length) DESC LIMIT 20;","expect":"가장 큰 테이블을 보여 준다. data_free_mb가 높은 테이블은 단편화된 상태이며 OPTIMIZE TABLE로 공간을 회수할 수 있다. 매우 큰 테이블은 파티셔닝을 고려한다.","severity":"safe"},{"id":"innodb-tablespace","group":"용량","title":"InnoDB 테이블스페이스 및 임시 공간 사용량","command":"SELECT FILE_NAME, TABLESPACE_NAME, ROUND(TOTAL_EXTENTS * EXTENT_SIZE / 1024 / 1024, 1) AS total_mb, ROUND((TOTAL_EXTENTS - FREE_EXTENTS) * EXTENT_SIZE / 1024 / 1024, 1) AS used_mb FROM information_schema.files WHERE FILE_TYPE = 'TABLESPACE' ORDER BY TOTAL_EXTENTS DESC LIMIT 20;","expect":"InnoDB 데이터 파일이 얼마나 찼는지 보여 준다. innodb_file_per_table이 꺼져 있으면 ibdata1이 계속 커지며 줄어들지 않는다. temp 테이블스페이스 비대화는 SHOW ENGINE INNODB STATUS 출력도 확인한다.","severity":"caution"},{"id":"connections-vs-limit","group":"용량","title":"연결 수 및 max_connections 한도","command":"SELECT (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Threads_connected') AS connected, (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'max_connections') AS max_connections, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Max_used_connections') AS max_used, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Aborted_connects') AS aborted_connects;","expect":"max_used가 max_connections에 가까워지면(85% 이상) 'Too many connections' 오류 위험이 있다. aborted_connects가 높으면 인증/네트워크 문제를 뜻한다. 연결 풀을 사용한다.","severity":"caution"},{"id":"binlog-size","group":"용량","title":"Binary log 파일 및 전체 크기","command":"SHOW BINARY LOGS;","expect":"binlog 파일과 크기를 나열한다. 전체 크기가 디스크 공간을 압박하면 binlog_expire_logs_seconds를 검토한다(8.0 이전에는 expire_logs_days, 이 변수는 8.0에서 deprecated되고 8.4에서 제거됨). 복제를 사용 중이면 replica가 읽기 전에 purge하지 않는다.","severity":"caution"},{"id":"buffer-pool-hit","group":"성능","title":"InnoDB buffer pool 적중률","command":"SELECT (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests') AS read_requests, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') AS disk_reads, ROUND((1 - (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') / (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests')) * 100, 3) AS hit_ratio_pct;","expect":"hit_ratio_pct는 일반적으로 99% 이상을 목표로 한다. 계속 낮으면 innodb_buffer_pool_size가 부족한 것이다. 전용 서버에서는 RAM의 약 70-80%까지 늘리는 것을 고려한다(공유 서버에서는 더 적게).","severity":"safe"},{"id":"top-queries","group":"성능","title":"가장 비용이 큰 쿼리(sys.statement_analysis)","command":"SELECT query, db, exec_count, total_latency, avg_latency, rows_sent_avg, rows_examined_avg FROM sys.statement_analysis ORDER BY total_latency DESC LIMIT 15;","expect":"sys 스키마와 performance_schema가 활성화되어 있어야 한다. total_latency를 가장 많이 소비하는 쿼리가 튜닝 대상이다. rows_examined_avg가 rows_sent_avg보다 훨씬 크면 인덱스가 없는 것이므로 EXPLAIN으로 분석한다.","severity":"safe"},{"id":"long-running-queries","group":"성능","title":"장시간 실행 중인 활성 쿼리","command":"SELECT id, user, host, db, command, time AS seconds, state, LEFT(info, 80) AS query FROM information_schema.processlist WHERE command != 'Sleep' AND time > 60 ORDER BY time DESC;","expect":"60초 이상 실행 중인 쿼리는 멈춰 있거나 무거울 수 있다. state 컬럼은 대기 대상을 보여 준다. 'Sleep' 상태의 장시간 세션도 연결 풀을 고갈시키므로 필요하면 KILL <id>를 사용한다.","severity":"caution"},{"id":"blocking-locks","group":"성능","title":"블로킹 세션 및 대기 중인 락","command":"SELECT r.trx_id AS waiting_trx, r.trx_mysql_thread_id AS waiting_thread, TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW()) AS wait_seconds, LEFT(r.trx_query, 60) AS waiting_query, b.trx_mysql_thread_id AS blocking_thread, LEFT(b.trx_query, 60) AS blocking_query FROM performance_schema.data_lock_waits w JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_engine_transaction_id JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_engine_transaction_id ORDER BY wait_seconds DESC;","expect":"반환되는 행이 없어야 한다. 블로킹 트랜잭션(blocking_thread)은 애플리케이션 지연과 lock wait timeout 오류를 일으킨다. 오래 블로킹하는 트랜잭션을 조사하고 필요하면 KILL로 종료한다. (MySQL 8.0 구문.)","severity":"danger"},{"id":"binlog-enabled","group":"백업 및 복구","title":"Binary log 및 복구 설정","command":"SELECT (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'log_bin') AS log_bin, (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'binlog_format') AS binlog_format, (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'sync_binlog') AS sync_binlog, (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'innodb_flush_log_at_trx_commit') AS flush_log_commit;","expect":"특정 시점 복구를 위해 log_bin은 ON이어야 한다. 내구성을 위해 sync_binlog = 1과 innodb_flush_log_at_trx_commit = 1이 권장된다(그렇지 않으면 크래시 시 트랜잭션이 손실될 수 있다). 복제에는 binlog_format = ROW가 가장 안전하다.","severity":"danger"},{"id":"last-backup-note","group":"백업 및 복구","title":"마지막 백업 시각(binlog / 도구 지표)","command":"SELECT (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Uptime') AS uptime_seconds, (SELECT MAX(update_time) FROM information_schema.tables WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys')) AS last_data_change;","expect":"MySQL은 자체적으로 백업 상태를 추적하지 않는다. 마지막 성공 백업 시각은 mysqldump / MySQL Shell dump / Percona XtraBackup 도구 로그에서 확인한다(mysqlpump는 8.0.34에서 deprecated되고 8.4에서 제거됨). 백업은 RPO 정책이 요구하는 만큼 최신이어야 한다(예: 최근 24시간 이내).","severity":"danger"},{"id":"locked-expired-accounts","group":"보안","title":"잠긴 계정 또는 비밀번호가 만료된 계정","command":"SELECT user, host, account_locked, password_expired, password_lifetime FROM mysql.user ORDER BY user, host;","expect":"account_locked = 'Y' 또는 password_expired = 'Y'인 핵심 애플리케이션 계정은 장애를 일으킨다. password_lifetime이 NULL이고 전역 만료가 꺼져 있으면 비밀번호가 만료되지 않으며, 정책상 공백일 수 있다.","severity":"caution"},{"id":"anonymous-nopassword-users","group":"보안","title":"익명 사용자 및 비밀번호 없는 사용자","command":"SELECT user, host, plugin, IF(authentication_string = '', 'EMPTY_PASSWORD', 'set') AS password_status FROM mysql.user WHERE user = '' OR authentication_string = '' ORDER BY host;","expect":"반환되는 행이 없어야 한다. 익명 사용자(user = '')와 비밀번호 없는 계정은 심각한 보안 허점이다. mysql_secure_installation으로 제거하거나 강력한 비밀번호를 지정한다.","severity":"danger"},{"id":"super-privilege-users","group":"보안","title":"높은 권한(SUPER / GRANT) 사용자","command":"SELECT grantee, privilege_type FROM information_schema.user_privileges WHERE privilege_type IN ('SUPER','GRANT OPTION','SHUTDOWN','FILE','CREATE USER') ORDER BY grantee, privilege_type;","expect":"SUPER, FILE, GRANT OPTION 같은 권한은 필요하고 알려진 계정에만 있어야 한다. 예상치 못한 grantee는 권한 상승 위험이 있다. 특히 %(모든 호스트)로 정의된 높은 권한 계정에 주의한다.","severity":"caution"},{"id":"replica-status","group":"복제","title":"replica(standby) 상태 및 지연","command":"SHOW REPLICA STATUS;","expect":"replica에서 실행한다(MySQL 8.0.22+). Replica_IO_Running과 Replica_SQL_Running이 모두 'Yes'여야 한다. Seconds_Behind_Source는 작아야 하며, NULL이면 복제가 중단된 것이다. Last_Error 필드를 확인한다. (이전 버전에서는 SHOW SLAVE STATUS.)","severity":"danger"},{"id":"replication-threads","group":"복제","title":"복제 연결 및 applier 상태(performance_schema)","command":"SELECT c.channel_name, c.service_state AS io_state, a.service_state AS sql_state, c.last_error_message AS io_error, w.last_error_message AS sql_error FROM performance_schema.replication_connection_status c JOIN performance_schema.replication_applier_status a ON c.channel_name = a.channel_name LEFT JOIN performance_schema.replication_applier_status_by_coordinator w ON w.channel_name = a.channel_name;","expect":"io_state와 sql_state는 'ON'이어야 한다. io_error/sql_error가 채워져 있으면 복제가 실패해 중단된 것이다. multi-source 구성에서는 각 channel_name이 개별적으로 정상이어야 한다.","severity":"danger"}]},"health/mssql.json":{"engineId":"mssql","engine":"SQL Server","order":4,"checks":[{"id":"version-uptime","group":"일반","title":"버전 및 가동 시간","command":"SELECT @@VERSION AS version, sqlserver_start_time AS start_time, DATEDIFF(HOUR, sqlserver_start_time, GETDATE()) AS uptime_hours FROM sys.dm_os_sys_info;","expect":"버전과 패치 수준을 확인한다. 가동 시간이 매우 짧으면(몇 시간 미만) 예기치 않은 재시작이 발생했을 수 있으므로 SQL Server error log를 검토한다.","severity":"safe"},{"id":"database-states","group":"일반","title":"문제 상태의 데이터베이스(SUSPECT / RECOVERY_PENDING)","command":"SELECT name AS database_name, state_desc AS state, recovery_model_desc AS recovery_model FROM sys.databases WHERE state_desc <> N'ONLINE' ORDER BY name;","expect":"모든 운영 데이터베이스는 ONLINE이어야 한다. SUSPECT, RECOVERY_PENDING, EMERGENCY는 즉각 조치가 필요하다. 복구를 시작하고 error log를 검토한다.","severity":"danger"},{"id":"recent-errors","group":"일반","title":"최근 주요 오류(memory/error ring buffer)","command":"SELECT TOP 20 CAST(record AS XML).value('(//Record/@id)[1]', 'int') AS record_id, DATEADD(ms, timestamp - (SELECT ms_ticks FROM sys.dm_os_sys_info), GETDATE()) AS event_time, CAST(record AS XML) AS record_xml FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_EXCEPTION' ORDER BY timestamp DESC;","expect":"최근 exception/error 기록을 검토한다. 자주 반복되는 823/824/825(I/O 일관성) 오류는 디스크 손상을 가리킨다. 상세 내용은 서버에서 xp_readerrorlog로 error log도 읽는다.","severity":"caution"},{"id":"data-log-file-usage","group":"용량","title":"데이터 및 로그 파일 사용량","command":"SELECT DB_NAME() AS database_name, name AS logical_name, type_desc AS file_type, size/128.0 AS allocated_mb, CAST(FILEPROPERTY(name, 'SpaceUsed') AS INT)/128.0 AS used_mb, (size - CAST(FILEPROPERTY(name, 'SpaceUsed') AS INT))/128.0 AS free_mb, CASE WHEN max_size = -1 THEN N'Unlimited' WHEN max_size = 268435456 THEN N'2 TB (log)' ELSE CAST(max_size/128 AS VARCHAR(20)) + N' MB' END AS max_size_limit FROM sys.database_files;","expect":"각 데이터베이스에서 실행한다. 사용 공간이 할당 공간에 매우 가깝고(85%+) autogrowth에 상한이 있으면 증설을 계획하거나 디스크를 추가한다. 로그 파일이 계속 가득 차 있으면 백업/복구 모델 설정을 검토한다.","severity":"caution"},{"id":"largest-tables","group":"용량","title":"가장 큰 테이블(크기 및 행 수)","command":"SELECT TOP 20 s.name AS schema_name, t.name AS table_name, p.rows AS row_count, SUM(a.total_pages)*8/1024 AS total_mb, SUM(a.used_pages)*8/1024 AS used_mb FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id JOIN sys.indexes i ON t.object_id = i.object_id JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id JOIN sys.allocation_units a ON p.partition_id = a.container_id WHERE i.index_id <= 1 GROUP BY s.name, t.name, p.rows ORDER BY total_mb DESC;","expect":"공간을 가장 많이 소비하는 테이블을 파악한다. 예상치 못한 증가는 아카이빙, 인덱스 유지보수 또는 파티셔닝이 필요하다는 신호일 수 있다.","severity":"safe"},{"id":"tempdb-usage","group":"용량","title":"tempdb 공간 사용량","command":"SELECT SUM(user_object_reserved_page_count)*8/1024 AS user_objects_mb, SUM(internal_object_reserved_page_count)*8/1024 AS internal_objects_mb, SUM(version_store_reserved_page_count)*8/1024 AS version_store_mb, SUM(unallocated_extent_page_count)*8/1024 AS free_mb FROM sys.dm_db_file_space_usage;","expect":"tempdb의 여유 공간이 곧 바닥나면(unallocated가 낮음) 쿼리가 실패할 수 있다. version store가 계속 커지면 장시간 열린 트랜잭션이 있는 것이므로 sys.dm_tran_active_snapshot_database_transactions로 조사한다.","severity":"caution"},{"id":"connection-count","group":"용량","title":"연결 수 및 분포","command":"SELECT DB_NAME(dbid) AS database_name, COUNT(*) AS connection_count, SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS active FROM sys.sysprocesses WHERE dbid > 0 GROUP BY dbid ORDER BY connection_count DESC;","expect":"애플리케이션/데이터베이스별 연결 수를 추적한다. 단일 애플리케이션의 연결 수가 비정상적으로 많으면 연결 풀 누수일 수 있다. max worker threads 한도에 근접하지 않도록 한다.","severity":"safe"},{"id":"vlf-count","group":"용량","title":"로그 파일 VLF 수(단편화)","command":"SELECT DB_NAME(database_id) AS database_name, COUNT(*) AS vlf_count FROM sys.dm_db_log_info(DB_ID()) GROUP BY database_id;","expect":"VLF 수가 많으면(수백 개 이상, 예: 500+) 로그 파일이 아주 작은 단위로 증가했다는 뜻이며, 복구와 백업 성능을 떨어뜨린다. 로그를 축소한 뒤 한 번에 크게 다시 늘린다.","severity":"caution"},{"id":"buffer-cache-ple","group":"성능","title":"Buffer Cache Hit Ratio 및 Page Life Expectancy","command":"SELECT (SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name = 'Buffer cache hit ratio') AS hit_ratio_raw, (SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name = 'Buffer cache hit ratio base') AS hit_ratio_base, (SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name = 'Page life expectancy' AND object_name LIKE '%Buffer Manager%') AS page_life_expectancy_sec;","expect":"버퍼 캐시 적중률은 이상적으로 99% 이상이어야 한다(hit_ratio_raw / hit_ratio_base). Page Life Expectancy가 지속적으로 낮으면(경험칙: NUMA 노드당 4 GB마다 300 s 미만) 메모리 압박이 있는 것이며, RAM 증설이나 쿼리 최적화가 필요하다.","severity":"caution"},{"id":"expensive-queries","group":"성능","title":"가장 비용이 큰 쿼리(CPU 및 읽기)","command":"SELECT TOP 15 qs.total_worker_time/1000 AS total_cpu_ms, qs.execution_count AS execution_count, qs.total_logical_reads AS total_logical_reads, qs.total_worker_time/qs.execution_count/1000 AS avg_cpu_ms, SUBSTRING(st.text, (qs.statement_start_offset/2)+1, ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1) AS query_text FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st ORDER BY qs.total_worker_time DESC;","expect":"CPU와 논리 읽기 기준으로 가장 비용이 큰 쿼리를 파악한다. 읽기가 많은 쿼리는 인덱스 누락의 신호이므로 실행 계획을 검토하고 인덱스를 추가하거나 쿼리를 재작성한다.","severity":"safe"},{"id":"blocking-sessions","group":"성능","title":"블로킹 세션","command":"SELECT r.session_id AS blocked_session, r.blocking_session_id AS blocking_session, r.wait_type AS wait_type, r.wait_time/1000 AS wait_sec, r.status AS status, t.text AS query FROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t WHERE r.blocking_session_id <> 0 ORDER BY r.wait_time DESC;","expect":"블로킹 세션(blocking_session_id <> 0)은 쿼리들이 서로를 기다리게 한다는 뜻이다. 오래 지속되는 블로킹은 애플리케이션 타임아웃을 일으킨다. head blocker를 찾아 장시간 트랜잭션 종료를 고려한다.","severity":"danger"},{"id":"wait-stats","group":"성능","title":"상위 대기 통계(wait stats)","command":"SELECT TOP 15 wait_type AS wait_type, waiting_tasks_count AS waiting_tasks, wait_time_ms/1000 AS total_wait_sec, (wait_time_ms - signal_wait_time_ms)/1000 AS resource_wait_sec, signal_wait_time_ms/1000 AS cpu_wait_sec FROM sys.dm_os_wait_stats WHERE wait_type NOT IN (N'CLR_SEMAPHORE', N'CLR_AUTO_EVENT', N'CLR_MANUAL_EVENT', N'LAZYWRITER_SLEEP', N'SQLTRACE_BUFFER_FLUSH', N'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', N'WAITFOR', N'BROKER_TASK_STOP', N'BROKER_EVENTHANDLER', N'BROKER_RECEIVE_WAITFOR', N'BROKER_TRANSMITTER', N'CHECKPOINT_QUEUE', N'CHKPT', N'REQUEST_FOR_DEADLOCK_SEARCH', N'XE_TIMER_EVENT', N'XE_DISPATCHER_WAIT', N'XE_LIVE_TARGET_TVF', N'BROKER_TO_FLUSH', N'DISPATCHER_QUEUE_SEMAPHORE', N'HADR_FILESTREAM_IOMGR_IOCOMPLETION', N'HADR_WORK_QUEUE', N'HADR_TIMER_TASK', N'HADR_CLUSAPI_CALL', N'HADR_LOGCAPTURE_WAIT', N'HADR_NOTIFICATION_DEQUEUE', N'DIRTY_PAGE_POLL', N'SP_SERVER_DIAGNOSTICS_SLEEP', N'SOS_WORK_DISPATCHER', N'LOGMGR_QUEUE', N'ONDEMAND_TASK_QUEUE', N'FT_IFTS_SCHEDULER_IDLE_WAIT', N'PWAIT_EXTENSIBILITY_CLEANUP_TASK', N'PWAIT_ALL_COMPONENTS_INITIALIZED', N'STARTUP_DEPENDENCY_MANAGER', N'PREEMPTIVE_XE_DISPATCHER', N'PVS_PREALLOCATE', N'WAIT_XTP_OFFLINE_CKPT_NEW_LOG', N'WAIT_XTP_CKPT_CLOSE', N'WAIT_XTP_HOST_WAIT', N'PARALLEL_REDO_DRAIN_WORKER', N'PARALLEL_REDO_LOG_CACHE', N'PARALLEL_REDO_TRAN_LIST', N'PARALLEL_REDO_WORKER_SYNC', N'PARALLEL_REDO_WORKER_WAIT_WORK') AND wait_type NOT LIKE N'SLEEP[_]%' AND wait_type NOT LIKE N'QDS[_]%' AND waiting_tasks_count > 0 ORDER BY wait_time_ms DESC;","expect":"지배적인 대기 유형을 해석한다: PAGEIOLATCH = 디스크 I/O 병목, CXPACKET/CXCONSUMER = 병렬 처리, LCK_M = 락, WRITELOG = 로그 디스크, RESOURCE_SEMAPHORE = 메모리. signal wait 비율이 높으면 CPU 압박을 뜻한다.","severity":"safe"},{"id":"last-backup","group":"백업 및 복구","title":"마지막 백업 시각(전체 / 차등 / 로그)","command":"SELECT d.name AS database_name, d.recovery_model_desc AS recovery_model, MAX(CASE WHEN b.type = 'D' THEN b.backup_finish_date END) AS last_full_backup, MAX(CASE WHEN b.type = 'I' THEN b.backup_finish_date END) AS last_differential, MAX(CASE WHEN b.type = 'L' THEN b.backup_finish_date END) AS last_log_backup FROM sys.databases d LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name WHERE d.database_id > 4 GROUP BY d.name, d.recovery_model_desc ORDER BY last_full_backup;","expect":"모든 운영 데이터베이스에는 최근 전체 백업이 있어야 한다. last_full_backup이 NULL이거나 아주 오래되었으면(RPO 초과) 백업 작업이 실패하고 있는 것이다. FULL 복구 모델에서 로그 백업이 없으면 로그 파일이 계속 커진다.","severity":"danger"},{"id":"recovery-model","group":"백업 및 복구","title":"복구 모델 일관성","command":"SELECT name AS database_name, recovery_model_desc AS recovery_model, log_reuse_wait_desc AS log_reuse_wait FROM sys.databases WHERE database_id > 4 ORDER BY name;","expect":"핵심 데이터베이스는 특정 시점 복구를 위해 FULL을 사용해야 한다. log_reuse_wait_desc = LOG_BACKUP은 로그 백업이 대기 중이라는 뜻이며, ACTIVE_TRANSACTION은 장시간 실행 트랜잭션을 가리킨다.","severity":"caution"},{"id":"sysadmin-members","group":"보안","title":"sysadmin 롤 멤버","command":"SELECT p.name AS login_name, p.type_desc AS type, p.is_disabled AS disabled FROM sys.server_role_members rm JOIN sys.server_principals r ON rm.role_principal_id = r.principal_id JOIN sys.server_principals p ON rm.member_principal_id = p.principal_id WHERE r.name = N'sysadmin' ORDER BY p.name;","expect":"sysadmin 멤버십은 최소화해야 한다. 예상치 못한 로그인(특히 애플리케이션 계정이나 개인 사용자)은 권한 상승 위험이 있으므로 최소 권한 원칙을 적용한다.","severity":"danger"},{"id":"sa-and-password-policy","group":"보안","title":"sa 계정 및 비밀번호 정책이 비활성화된 로그인","command":"SELECT name AS login_name, is_disabled AS disabled, is_policy_checked AS password_policy_on, is_expiration_checked AS expiration_check, LOGINPROPERTY(name, 'PasswordLastSetTime') AS last_password_change FROM sys.sql_logins WHERE is_policy_checked = 0 OR name = N'sa' ORDER BY name;","expect":"sa 계정은 비활성화하거나 이름을 바꿔야 한다. is_policy_checked = 0인 SQL 로그인은 취약하거나 오래된 비밀번호 위험이 있으므로 비밀번호 정책(CHECK_POLICY)을 활성화한다.","severity":"caution"},{"id":"alwayson-sync","group":"복제","title":"Always On 동기화 상태 및 큐","command":"SELECT ag.name AS availability_group, ar.replica_server_name AS replica_server, DB_NAME(drs.database_id) AS database_name, drs.synchronization_state_desc AS sync_state, drs.synchronization_health_desc AS health, drs.log_send_queue_size AS log_send_queue_kb, drs.redo_queue_size AS redo_queue_kb FROM sys.dm_hadr_database_replica_states drs JOIN sys.availability_replicas ar ON drs.replica_id = ar.replica_id JOIN sys.availability_groups ag ON ar.group_id = ag.group_id ORDER BY ag.name, replica_server;","expect":"synchronization_state_desc는 SYNCHRONIZED(동기) 또는 SYNCHRONIZING(비동기)이어야 하고, synchronization_health는 HEALTHY여야 한다. log_send_queue나 redo_queue가 커지면 replica가 뒤처지는 것이며, 페일오버 시 데이터 손실/RTO 위험이 커진다.","severity":"caution"}]},"health/mongodb.json":{"engineId":"mongodb","engine":"MongoDB","order":5,"checks":[{"id":"version-uptime","group":"일반","title":"버전 및 가동 시간","command":"db.serverStatus().version; db.serverStatus().uptime","expect":"버전이 지원되며 패치되었는지 확인한다. uptime은 초 단위이며, 매우 작으면(수천 초) 예기치 않은 재시작이 발생한 것이므로 mongod 로그를 검토한다.","severity":"safe"},{"id":"replset-status","group":"일반","title":"replica set 멤버 상태(health)","command":"rs.status().members.forEach(function(m) { print(m.name + \" | \" + m.stateStr + \" | health=\" + m.health); })","expect":"모든 멤버는 health=1이어야 하며 PRIMARY는 정확히 하나여야 한다. health=0 또는 (not reachable/healthy), DOWN, RECOVERING 상태는 장애를 나타낸다. PRIMARY가 둘 이상이거나 없으면 선출 문제가 있는 것이다.","severity":"danger"},{"id":"dbstats-sizes","group":"용량","title":"데이터베이스 크기(data / storage / index)","command":"db.stats(1024*1024)","expect":"dataSize, storageSize, indexSize(MB)를 모니터링한다. storageSize가 dataSize보다 훨씬 크면 단편화가 있을 수 있다. indexSize를 RAM(WiredTiger 캐시) 안에 유지하는 것이 성능에 매우 중요하다.","severity":"safe"},{"id":"disk-space","group":"용량","title":"디스크 공간 사용량(fsUsedSize / fsTotalSize)","command":"var st = db.stats(); printjson({fsUsedSize_MB: (st.fsUsedSize/1048576).toFixed(0), fsTotalSize_MB: (st.fsTotalSize/1048576).toFixed(0), usedPct: (100*st.fsUsedSize/st.fsTotalSize).toFixed(1)})","expect":"fsTotalSize 대비 fsUsedSize 비율이 85%를 넘으면 디스크가 곧 가득 찬다. 디스크가 가득 차면 MongoDB는 쓰기를 중단한다. 디스크 추가나 데이터 아카이빙을 계획한다.","severity":"caution"},{"id":"largest-collections","group":"용량","title":"가장 큰 컬렉션","command":"db.getCollectionNames().map(function(c) { var s = db.getCollection(c).stats(); return {collection: c, size_MB: (s.size/1048576).toFixed(1), storage_MB: (s.storageSize/1048576).toFixed(1), index_MB: (s.totalIndexSize/1048576).toFixed(1), docs: s.count}; }).sort(function(a,b){return b.size_MB - a.size_MB;})","expect":"공간을 가장 많이 사용하는 컬렉션을 파악한다. 예상치 못한 증가는 아카이빙, TTL 인덱스 또는 샤딩이 필요하다는 신호일 수 있다. 인덱스 크기가 데이터 크기를 넘는 컬렉션을 검토한다.","severity":"safe"},{"id":"connections","group":"용량","title":"연결 수(current / available)","command":"db.serverStatus().connections","expect":"current가 available에 가까워지면 연결 한도에 곧 도달해 새 연결이 거부된다. 애플리케이션 연결 풀 설정과 누수 가능성을 확인한다.","severity":"caution"},{"id":"wiredtiger-cache","group":"성능","title":"WiredTiger 캐시 사용량 및 eviction","command":"var wt = db.serverStatus().wiredTiger.cache; printjson({used_MB: (wt['bytes currently in the cache']/1048576).toFixed(0), max_MB: (wt['maximum bytes configured']/1048576).toFixed(0), dirty_MB: (wt['tracked dirty bytes in the cache']/1048576).toFixed(0), pages_evicted: wt['unmodified pages evicted']})","expect":"사용 메모리가 최대 캐시의 80%를 넘으면 eviction이 시작되고, 95%를 넘으면 애플리케이션 스레드가 강제로 eviction에 투입되어 성능이 떨어진다. dirty page 비율이 높으면 쓰기 압박을 뜻한다.","severity":"caution"},{"id":"slow-queries-profiler","group":"성능","title":"느린 쿼리(프로파일러) 및 COLLSCAN","command":"printjson(db.getProfilingStatus()); db.system.profile.find({millis: {$gt: 100}}).sort({ts: -1}).limit(10).forEach(function(d){ print(d.ts + \" | \" + d.millis + \"ms | \" + (d.planSummary || '') + \" | \" + JSON.stringify(d.command)); })","expect":"이 점검은 읽기 전용이다. 먼저 프로파일러 상태(was: 0 꺼짐, 1 느린 작업만, 2 전체)를 보여 준 뒤 기록된 느린 작업을 나열한다. 프로파일러가 꺼져 있으면(was: 0) system.profile은 비어 있다. 진단을 위해 별도 단계로 db.setProfilingLevel(1, {slowms: 100})으로 활성화하고, 끝나면 db.setProfilingLevel(0)으로 끈다. planSummary에 COLLSCAN이 보이는 쿼리는 컬렉션 전체를 스캔하며 인덱스가 없는 것이다. millis 값이 큰 쿼리는 인덱싱으로 최적화한다. 프로파일러는 오버헤드를 추가하므로 운영 환경에서는 신중하게 사용한다.","severity":"caution"},{"id":"long-running-ops","group":"성능","title":"장시간 실행 작업(currentOp)","command":"db.getSiblingDB('admin').aggregate([{$currentOp: {}}, {$match: {active: true, secs_running: {$gt: 5}}}, {$project: {opid: 1, secs_running: 1, ns: 1, op: 1, planSummary: 1, waitingForLock: 1}}])","expect":"secs_running 값이 큰 활성 작업은 블로킹을 일으킬 수 있다. 필요하면 db.killOp(opid)로 종료한다. waitingForLock=true인 작업은 락을 기다리는 중이므로 근원 작업을 조사한다.","severity":"danger"},{"id":"oplog-window","group":"백업 및 복구","title":"Oplog 범위(복제 이력)","command":"rs.printReplicationInfo()","expect":"oplog 범위(설정된 oplog 크기와 시간 단위 로그 길이)는 충분히 길어야 한다. 범위가 짧으면(몇 시간) 느린 secondary가 전체 재동기화(initial sync)에 빠질 수 있다. 백업과 특정 시점 복구 전략은 oplog에 의존한다.","severity":"caution"},{"id":"backup-note","group":"백업 및 복구","title":"백업 상태 및 마지막 백업 메모","command":"var r = db.getSiblingDB('local').getCollectionNames().includes('oplog.rs') ? db.getReplicationInfo() : null; printjson(r ? {oplogWindowHours: r.timeDiffHours, tFirst: r.tFirst, tLast: r.tLast} : 'standalone: no oplog'); print('Backup verification: check mongodump/mongorestore or Ops Manager / Percona Backup (PBM) status externally.')","expect":"MongoDB에는 내장 예약 백업이 없으며 mongodump, 파일 시스템 스냅샷, Ops Manager 또는 PBM을 사용한다. 마지막 성공 백업 시각과 복원 테스트는 외부 도구에서 확인한다. replica set에서는 oplog 범위(oplogWindowHours)가 oplog 기반 PITR이 거슬러 갈 수 있는 한계를 정하므로 백업 주기보다 짧아서는 안 된다. RPO/RTO 목표 충족 여부를 확인한다.","severity":"danger"},{"id":"users-roles","group":"보안","title":"사용자 및 롤 목록","command":"db.getSiblingDB('admin').system.users.find({}, {user: 1, db: 1, roles: 1, _id: 0}).forEach(printjson)","expect":"정의된 사용자와 롤을 검토한다. 예상치 못한 계정이나 과도하게 넓은 롤(root, __system, dbOwner)은 권한 상승 위험이 있다. 최소 권한 원칙을 적용한다.","severity":"caution"},{"id":"auth-enabled","group":"보안","title":"인증(authorization) 활성화 여부","command":"db.getSiblingDB('admin').runCommand({getCmdLineOpts: 1}).parsed.security","expect":"security.authorization은 'enabled'여야 한다. 비활성화되어 있으면(또는 security 블록이 없으면) 데이터베이스가 인증 없이 접근 가능한 상태다. 심각한 보안 허점이므로 즉시 활성화한다.","severity":"danger"},{"id":"secondary-lag","group":"복제","title":"Secondary 복제 지연","command":"rs.printSecondaryReplicationInfo()","expect":"각 secondary의 'replication lag' 값은 낮아야 한다(몇 초). 지연이 크면(분 단위) secondary가 부하를 따라가지 못하거나 네트워크/디스크 문제가 있는 것이며, 페일오버 시 데이터 손실과 읽기 불일치 위험이 커진다.","severity":"caution"},{"id":"member-health","group":"복제","title":"멤버 상태 및 선출 상태","command":"rs.status().members.map(function(m){ return {member: m.name, state: m.stateStr, health: m.health, configVersion: m.configVersion, last_heartbeat: m.lastHeartbeatMessage || 'OK'}; })","expect":"모든 멤버가 정상(health=1)이고 예상 상태(PRIMARY/SECONDARY)여야 한다. lastHeartbeatMessage의 오류 메시지는 네트워크 분할이나 접근할 수 없는 멤버를 나타낸다. 과반수를 잃으면 쓰기가 중단된다.","severity":"caution"}]},"health/redis.json":{"engineId":"redis","engine":"Redis","order":6,"checks":[{"id":"server-version-uptime","group":"일반","title":"버전 및 가동 시간","command":"redis-cli INFO server | grep -E \"redis_version|uptime_in_days|redis_mode|os\"","expect":"redis_version은 지원되며 패치된 릴리스여야 한다. uptime_in_days가 매우 작으면 예기치 않은 재시작이 발생한 것이다. redis_mode(standalone/sentinel/cluster)로 구성을 확인한다.","severity":"safe"},{"id":"persistence-status","group":"일반","title":"영속성 상태(RDB / AOF)","command":"redis-cli INFO persistence | grep -E \"rdb_last_bgsave_status|aof_enabled|aof_last_bgrewrite_status|aof_last_write_status|rdb_bgsave_in_progress|loading\"","expect":"rdb_last_bgsave_status와 aof_last_write_status는 'ok'여야 한다. 'err' 값은 디스크/권한 문제나 디스크 가득 참을 나타내며 데이터 손실 위험이 있다. loading:1은 서버가 아직 데이터를 로딩 중이라는 뜻이다.","severity":"danger"},{"id":"memory-usage","group":"용량","title":"메모리 사용량(used_memory / maxmemory)","command":"redis-cli INFO memory | grep -E \"used_memory_human|used_memory:|maxmemory_human|maxmemory:|maxmemory_policy\"","expect":"used_memory가 maxmemory에 가까워지면(85%+) eviction이나 OOM 위험이 있다. maxmemory=0이면 한도가 없어 Redis가 RAM 전체를 소비하고 시스템을 다운시킬 수 있으므로, 항상 한도와 적절한 maxmemory_policy를 설정한다.","severity":"caution"},{"id":"memory-fragmentation","group":"용량","title":"메모리 단편화 비율","command":"redis-cli INFO memory | grep -E \"mem_fragmentation_ratio|mem_allocator\"","expect":"mem_fragmentation_ratio는 1.0~1.5 사이가 이상적이다. 1.5를 넘으면 단편화가 심한 것이다(메모리 낭비). 1.0 미만이면 Redis가 디스크로 스왑되고 있다는 뜻이다(심각한 성능 문제). 시스템 RAM을 확인한다.","severity":"caution"},{"id":"evicted-keys","group":"용량","title":"축출된 키(evicted_keys)","command":"redis-cli INFO stats | grep -E \"evicted_keys|expired_keys|keyspace_misses\"","expect":"evicted_keys가 계속 증가하면 Redis가 메모리 한도에 도달해 데이터를 버리고 있는 것이며, 원치 않는 데이터 손실로 이어질 수 있다. 메모리 추가, TTL 사용 또는 maxmemory_policy 검토를 고려한다.","severity":"caution"},{"id":"dbsize-keyspace","group":"용량","title":"키 수 및 keyspace 분포","command":"redis-cli DBSIZE; redis-cli INFO keyspace","expect":"전체 키 수(DBSIZE)와 데이터베이스별 분포(INFO keyspace: keys, expires, avg_ttl)를 모니터링한다. TTL 없이(expires가 낮음) 커지는 keyspace는 메모리 비대화를 일으킨다. 키 수가 갑자기 급증하면 비정상 동작의 신호일 수 있다.","severity":"safe"},{"id":"hit-ratio","group":"성능","title":"캐시 적중률(keyspace hits/misses)","command":"redis-cli INFO stats | grep -E \"keyspace_hits|keyspace_misses\"","expect":"적중률 = hits / (hits + misses). 캐시 용도에서는 비율이 높아야 한다(80%+). 비율이 낮으면 비효율적인 캐싱, 잘못된 TTL 또는 cache-miss 패턴을 뜻한다. 자주 요청되는 데이터가 캐시되는지 확인한다.","severity":"safe"},{"id":"slowlog","group":"성능","title":"느린 명령 로그(SLOWLOG)","command":"redis-cli SLOWLOG GET 10","expect":"느린 명령을 분석한다(기본 임계값 10ms). O(N) 명령(KEYS, SMEMBERS, 대형 HGETALL, SORT)은 싱글 스레드 Redis를 블로킹해 모든 요청을 지연시킨다. SCAN 같은 대안으로 교체한다.","severity":"caution"},{"id":"clients","group":"성능","title":"연결된 클라이언트 및 블로킹된 클라이언트(clients)","command":"redis-cli INFO clients | grep -E \"connected_clients|blocked_clients|maxclients|client_recent_max_input_buffer\"","expect":"connected_clients가 maxclients에 가까워지면 새 연결이 거부된다. blocked_clients 수가 많은 것(BLPOP/BRPOP 대기)은 설계상 의도일 수 있지만, 예상치 못한 증가는 혼잡의 신호다. 연결 누수를 확인한다.","severity":"safe"},{"id":"ops-per-sec","group":"성능","title":"순간 처리량(ops/sec)","command":"redis-cli INFO stats | grep -E \"instantaneous_ops_per_sec|total_commands_processed|rejected_connections|total_net_input_bytes\"","expect":"instantaneous_ops_per_sec는 현재 워크로드를 보여 준다. 갑작스러운 하락/급증은 이상 징후다. rejected_connections > 0이면 maxclients 한도에 도달한 것이다. 싱글 스레드이므로 ops가 매우 높으면 CPU 병목이 생길 수 있다.","severity":"safe"},{"id":"rdb-last-save","group":"백업 및 복구","title":"마지막 RDB 저장 및 대기 중인 변경","command":"redis-cli INFO persistence | grep -E \"rdb_last_save_time|rdb_changes_since_last_save|rdb_last_bgsave_status\"; redis-cli LASTSAVE","expect":"rdb_changes_since_last_save가 매우 크면 마지막 스냅샷 이후 많은 데이터가 쌓인 것이며, 크래시 시 이 변경은 손실된다. rdb_last_save_time(LASTSAVE, Unix 시간)이 아주 오래되었으면 저장 주기나 백업 빈도를 검토한다.","severity":"caution"},{"id":"aof-rewrite","group":"백업 및 복구","title":"AOF rewrite 상태","command":"redis-cli INFO persistence | grep -E \"aof_enabled|aof_last_bgrewrite_status|aof_rewrite_in_progress|aof_current_size|aof_base_size\"","expect":"내구성이 필요한 시나리오에서는 aof_enabled=1로 설정해야 한다. aof_last_bgrewrite_status는 'ok'여야 하며, 'err'는 디스크/영속성 문제를 나타낸다. aof_current_size가 base_size보다 훨씬 크면 rewrite가 필요하다.","severity":"caution"},{"id":"acl-auth","group":"보안","title":"ACL / 인증(requirepass) 설정","command":"redis-cli ACL LIST; redis-cli ACL GETUSER default","expect":"비밀번호로 보호된 사용자가 최소 하나 있어야 한다. ACL GETUSER default 출력의 flags에 'on'과 'nopass'가 표시되면(requirepass도 이 사용자의 비밀번호다) Redis가 인증 없이 접근 가능한 상태이며 심각한 보안 허점이다. 비밀번호는 해시로만 표시된다. CONFIG GET requirepass는 비밀번호를 평문으로 출력하므로 이 점검에 사용하지 않는다. ACL로 최소 권한 사용자를 정의한다.","severity":"danger"},{"id":"dangerous-commands","group":"보안","title":"위험한 명령 및 protected-mode","command":"redis-cli CONFIG GET protected-mode; redis-cli CONFIG GET bind; redis-cli COMMAND INFO flushall flushdb config keys","expect":"protected-mode는 'yes'여야 하고 bind는 안전한 인터페이스로 제한해야 한다. FLUSHALL, FLUSHDB, CONFIG, KEYS, DEBUG 같은 위험한 명령은 운영 환경에서 rename-command로 비활성화하거나 ACL로 제한한다.","severity":"caution"},{"id":"replication-status","group":"복제","title":"복제 상태(role, master link)","command":"redis-cli INFO replication","expect":"role(master/slave), connected_slaves, 각 replica의 상태를 확인한다. replica에서 master_link_status는 'up'이어야 하며, 'down'은 링크가 끊긴 것이다. master_last_io_seconds_ago가 크면(지연) 데이터 손실과 불일치 위험이 커진다.","severity":"danger"}]},"health/elasticsearch.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":7,"checks":[{"id":"cluster-health","group":"일반","title":"클러스터 상태(green / yellow / red)","command":"GET _cluster/health","expect":"status는 'green'이어야 한다: 모든 primary와 replica 샤드가 할당된 상태. 'yellow'는 primary는 온전하지만 일부 replica가 미할당된 상태(이중화 위험), 'red'는 최소 하나의 primary 샤드가 손실/미할당된 상태다(데이터 일부를 사용할 수 없으며 즉각 조치 필요). unassigned_shards는 0이어야 한다.","severity":"danger"},{"id":"cluster-version","group":"일반","title":"버전 및 클러스터 정보","command":"GET /","expect":"version.number는 보안 패치가 적용된 지원 버전이어야 한다. 오래된/EOL(end-of-life) 버전은 보안과 안정성 위험이 있다. GET _cat/nodes?v&h=name,version으로 모든 노드가 같은 버전인지도 확인한다(rolling upgrade 중 일시적인 혼재는 정상).","severity":"safe"},{"id":"pending-tasks","group":"일반","title":"대기 중인 클러스터 작업","command":"GET _cluster/pending_tasks","expect":"목록은 비어 있거나 매우 짧아야 한다. 계속 쌓이며 오래 대기하는 작업(time_in_queue가 큼)은 master 노드가 과부하이거나 클러스터 상태 갱신이 정체되었다는 뜻이며, 클러스터 전체의 응답 시간을 저하시킨다.","severity":"caution"},{"id":"disk-allocation-watermark","group":"용량","title":"디스크 할당 및 watermark 상태","command":"GET _cat/allocation?v","expect":"각 노드의 disk.percent는 high watermark(기본값 90%)보다 낮아야 한다. 85%를 넘으면 경고, 90%(high)는 샤드 재배치를 유발하고, 95%(flood_stage)는 인덱스를 read-only로 만든다. shards 컬럼이 노드 간에 균형을 이루는지 확인한다. 고르지 않은 분포는 특정 노드를 일찍 가득 차게 한다.","severity":"danger"},{"id":"largest-indices","group":"용량","title":"가장 큰 인덱스(store size)","command":"GET _cat/indices?v&s=store.size:desc","expect":"공간을 가장 많이 소비하는 인덱스와 doc.count 값을 추적한다. 로그/메트릭 인덱스의 무분별한 증가가 디스크 가득 참의 주원인이므로 ILM(Index Lifecycle Management)으로 rollover와 삭제 정책을 정의한다. status가 'close'이거나 health가 yellow/red인 인덱스에 특히 주의한다.","severity":"caution"},{"id":"jvm-heap-usage","group":"용량","title":"JVM heap 및 RAM 사용량(노드별)","command":"GET _cat/nodes?v&h=name,heap.percent,ram.percent,cpu,load_1m","expect":"heap.percent는 지속적으로 75% 미만이어야 한다. 85% 이상이 계속되면 메모리 압박, 잦은 GC 정지, circuit breaker 위험을 뜻한다. JVM heap은 서버 RAM의 약 50%, 최대 약 31GB(compressed oops 한도)여야 한다. 내려가지 않는 높은 heap에는 쿼리를 최적화하거나 노드를 추가한다.","severity":"caution"},{"id":"shard-count-limits","group":"용량","title":"샤드 수 및 노드당 샤드 한도","command":"GET _cluster/health?level=shards","expect":"active_shards와 노드당 샤드 수는 적정해야 한다. 기본 상한은 cluster.max_shards_per_node = 1000(데이터 노드당)이다. 이 한도에 근접하고 전체적으로 수천 개의 작은 샤드가 있으면(oversharding) master 압박과 메모리 낭비가 생긴다. 기존의 \"heap 1GB당 샤드 ~20개\" 규칙은 8.3+에는 적용되지 않는다. 현재 지침은 샤드당 10-50GB, 그리고 노드당 필드(mapping) 수에 기반한 heap 계획이다. initializing_shards / relocating_shards가 오래 높게 유지되어서는 안 된다. 샤드가 너무 많으면 더 크고 적은 샤드나 rollover를 사용한다.","severity":"caution"},{"id":"http-connections","group":"용량","title":"HTTP 연결 수","command":"GET _nodes/stats/http","expect":"http.current_open은 적정 범위 안에 있어야 한다. http.total_opened가 매우 빠르게 증가하면 클라이언트가 연결 풀을 사용하지 않고 요청마다 새 연결을 여는 것이다(비효율적). current_open이 비정상적으로 높으면 연결 누수나 자원 고갈 가능성을 가리킨다.","severity":"safe"},{"id":"thread-pool-rejections","group":"성능","title":"Thread pool 거부 및 큐 포화","command":"GET _cat/thread_pool?v&h=node_name,name,active,queue,rejected","expect":"rejected 컬럼은 0이어야 한다. write/bulk와 search 풀에서 rejected 값이 증가하면 클러스터가 과부하 상태로 HTTP 429(es_rejected_execution_exception)를 반환하고 있는 것이다. 큐가 계속 가득 차 있으면 혼잡의 신호다. 클라이언트에서 backoff를 적용하거나, 배치 크기를 줄이거나, 용량을 추가한다.","severity":"danger"},{"id":"query-cache-hit","group":"성능","title":"쿼리 캐시 적중률","command":"GET _nodes/stats/indices/query_cache","expect":"반복되는 필터 쿼리에서는 hit_count / (hit_count + miss_count) 비율이 높아야 한다. 적중률이 낮고 eviction이 많으면 캐시 크기가 부족하거나 쿼리가 너무 다양해 캐시되지 않는 것이다. 안정적인 필터(filter context)를 사용하면 적중률이 높아진다.","severity":"safe"},{"id":"slowlog-config","group":"성능","title":"슬로 쿼리 로그(slowlog) 설정","command":"GET /_all/_settings?filter_path=**.search.slowlog,**.indexing.slowlog","expect":"중요한 인덱스에는 검색 및 인덱싱 slowlog 임계값을 정의해야 한다(예: index.search.slowlog.threshold.query.warn: 10s). slowlog가 비활성화되어 있으면 느린 쿼리 진단이 어려워진다. 임계값을 활성화하고 로그를 모니터링해 비용이 큰 쿼리(넓은 aggregation, 와일드카드, 깊은 페이징)를 찾는다.","severity":"safe"},{"id":"hot-threads","group":"성능","title":"Hot threads","command":"GET _nodes/hot_threads","expect":"CPU를 가장 많이 소비하는 스레드를 보여 준다. CPU 사용률이 높거나 느려질 때 어떤 작업(무거운 aggregation, merge, GC, 스크립트)이 병목인지 판단하는 데 사용한다. 같은 비용이 큰 쿼리/merge가 계속 나타나면 해당 쿼리나 인덱싱 전략을 최적화한다.","severity":"safe"},{"id":"fielddata-memory","group":"성능","title":"Fielddata 메모리 사용량","command":"GET _cat/fielddata?v","expect":"Fielddata 메모리 사용량은 낮게 유지해야 한다. text 필드에서 집계/정렬하면 fielddata가 메모리에 로드되어 circuit breaker(Data too large) 오류와 OOM 위험이 커진다. fielddata가 큰 필드는 keyword 하위 필드(doc_values)로 전환한다.","severity":"caution"},{"id":"snapshot-status","group":"백업 및 복구","title":"스냅샷 리포지토리 및 최근 스냅샷 상태","command":"GET _snapshot/_all","expect":"스냅샷 리포지토리가 최소 하나 정의되어 있어야 한다. 리포지토리 존재를 확인한 뒤 최근 스냅샷의 상태를 확인한다(아래 verify 점검과 함께). 리포지토리가 없으면 클러스터에 백업이 없는 것이며, RED 상태에서 데이터가 영구적으로 손실될 수 있다. 정기 자동 스냅샷을 위해 SLM(Snapshot Lifecycle Management)을 구성한다.","severity":"danger"},{"id":"snapshot-latest-state","group":"백업 및 복구","title":"최근 스냅샷의 성공 상태","command":"GET _snapshot/<repo>/_all?verbose=false","expect":"최근 스냅샷의 state는 'SUCCESS'여야 한다. 'PARTIAL'은 일부 샤드를 백업하지 못한 것이고, 'FAILED'는 백업이 실패한 것이며, 둘 다 복원 시 데이터 손실을 의미한다. 가장 최근 스냅샷의 날짜(end_time)가 너무 오래되어서는 안 된다(RPO 목표를 충족하는 빈도로 백업한다).","severity":"danger"},{"id":"snapshot-repo-verify","group":"백업 및 복구","title":"스냅샷 리포지토리 검증","command":"POST _snapshot/<repo>/_verify","expect":"검증은 모든 노드가 리포지토리(공유 파일 시스템, S3, HDFS 등)에 접근할 수 있는지 확인하며, 접근 가능한 노드를 응답에 나열한다. 오류가 반환되면 일부 노드가 백업 리포지토리에 쓸 수 없는 것이며, 이 경우 스냅샷이 불완전하거나 실패한다. 리포지토리 접근 권한과 자격 증명을 수정한다.","severity":"caution"},{"id":"security-enabled","group":"보안","title":"보안(X-Pack security) 활성화 여부","command":"GET _xpack/usage","expect":"security.enabled와 security.available은 true여야 한다. 보안이 비활성화되어 있으면 클러스터에 인증 없이 접근할 수 있다. 이런 클러스터가 인터넷이나 신뢰할 수 없는 네트워크에 노출되면 심각한 취약점이 된다(데이터 유출/삭제). 인증, 롤 기반 접근, TLS를 활성화해야 한다.","severity":"danger"},{"id":"users-roles","group":"보안","title":"사용자 및 롤","command":"GET _security/user","expect":"정의된 사용자와 롤 할당을 검토한다. 기본 제공 계정(elastic, kibana_system 등)이 강력한 비밀번호로 보호되는지 확인한다. 최소 권한 원칙에 따라 애플리케이션마다 제한된 롤을 가진 별도 사용자를 정의하고, superuser 롤의 불필요한 광범위 사용을 피한다. 롤은 GET _security/role을 사용한다.","severity":"caution"},{"id":"tls-transport-http","group":"보안","title":"TLS / HTTPS 상태(transport 및 HTTP)","command":"GET _nodes/settings?filter_path=**.xpack.security.transport.ssl.enabled,**.xpack.security.http.ssl.enabled","expect":"xpack.security.transport.ssl.enabled는 true여야 한다(노드 간 암호화, 필수). 이상적으로는 http.ssl.enabled도 true여야 한다(클라이언트-클러스터 HTTPS). TLS가 비활성화되어 있으면 자격 증명과 데이터가 네트워크에 평문으로 전송된다. 인증서 만료와 검증 모드(verification_mode: certificate/full)도 확인한다.","severity":"danger"},{"id":"unassigned-shards","group":"복제","title":"미할당 샤드 및 원인","command":"GET _cat/shards?v&h=index,shard,prirep,state,unassigned.reason","expect":"모든 샤드의 state는 'STARTED'여야 한다. UNASSIGNED 상태의 샤드와 그 unassigned.reason(NODE_LEFT, ALLOCATION_FAILED, DISK_THRESHOLD 등)을 조사한다. 미할당 primary는 RED, 미할당 replica는 YELLOW 상태를 뜻한다. 근본 원인은 GET _cluster/allocation/explain을 사용한다.","severity":"danger"},{"id":"recovery-active","group":"복제","title":"진행 중인 샤드 복구","command":"GET _cat/recovery?v&active_only=true","expect":"정상적으로는 활성 복구가 없어야 한다(빈 목록). 노드 추가/제거나 페일오버 후의 일시적인 복구는 정상이지만, 진행 없이 오래 멈춰 있는 복구(%progress가 증가하지 않음)는 네트워크/디스크 병목을 가리킨다. 리밸런싱(relocation)이 워크로드에 영향을 주면 복구 throttling 설정(indices.recovery.max_bytes_per_sec)을 검토한다.","severity":"caution"},{"id":"replica-count","group":"복제","title":"인덱스별 replica 수","command":"GET _cat/indices?v&h=index,health,pri,rep,docs.count","expect":"운영 인덱스에서 rep(number_of_replicas)는 최소 1이어야 한다. replica가 0인 인덱스에서는 노드를 잃으면 데이터가 영구 손실된다. 다만 다중 노드 클러스터에서 replica 수는 (데이터 노드 수 - 1)을 넘으면 안 되며, 그렇지 않으면 초과 replica를 할당할 수 없어 상태가 YELLOW로 유지된다. health 컬럼이 green이 아닌 인덱스를 조사한다.","severity":"danger"}]},"recipes/oracle.json":{"engineId":"oracle","engine":"Oracle","order":1,"recipes":[{"id":"redo-switch","task":"Redo log 전환(현재 로그 파일 전환)","category":"Redo 및 아카이브","command":"ALTER SYSTEM SWITCH LOGFILE;","what":"현재 redo log 그룹을 닫고 다음 그룹으로 이동한다. archivelog 모드에서는 아카이빙을 유발한다.","when":"수동으로 아카이브를 생성하거나, 로그 전환을 강제하거나, 테스트할 때.","caution":"잦은 전환은 성능을 저하시킨다. 로그 파일이 너무 작으면 크기를 재검토한다.","tags":["redo","switch","logfile","archive","archive","alter system"]},{"id":"redo-add-group","task":"새 redo log 그룹 추가","category":"Redo 및 아카이브","command":"ALTER DATABASE ADD LOGFILE GROUP 4\n  ('/u01/oradata/ORCL/redo04a.log',\n   '/u02/oradata/ORCL/redo04b.log') SIZE 512M;","what":"지정한 크기와 멤버로 새 redo log 그룹을 생성한다(여러 멤버 = 미러).","when":"redo 처리량이 부족하거나 그룹 수를 늘리려 할 때.","caution":"그룹 번호가 기존 그룹과 겹치지 않는지 V$LOG로 확인한다.","tags":["redo","logfile","group","add","add","alter database"]},{"id":"redo-resize","task":"Redo log 그룹 크기 변경(drop + add)","category":"Redo 및 아카이브","command":"-- 1) The group to be resized must be INACTIVE; if not, switch:\nALTER SYSTEM SWITCH LOGFILE;\nALTER SYSTEM CHECKPOINT;\n-- 2) Drop the old group:\nALTER DATABASE DROP LOGFILE GROUP 1;\n-- 3) Add it again with the new size:\nALTER DATABASE ADD LOGFILE GROUP 1\n  ('/u01/oradata/ORCL/redo01a.log',\n   '/u02/oradata/ORCL/redo01b.log') SIZE 1G REUSE;","what":"redo log는 크기를 직접 바꿀 수 없으며, 그룹을 삭제하고 새 크기로 다시 추가하는 방식으로 변경한다.","when":"redo 파일이 너무 작거나 클 때, 체크포인트 빈도를 조정하기 위해.","caution":"INACTIVE 또는 UNUSED 그룹만 삭제할 수 있다. CURRENT/ACTIVE 그룹은 삭제하지 않는다. 항상 최소 두 개의 그룹이 남아 있어야 한다. REUSE는 기존 파일을 재사용한다.","tags":["redo","resize","size","drop","add","logfile","reuse"]},{"id":"enable-archivelog","task":"데이터베이스를 ARCHIVELOG 모드로 전환","category":"Redo 및 아카이브","command":"SHUTDOWN IMMEDIATE;\nSTARTUP MOUNT;\nALTER DATABASE ARCHIVELOG;\nALTER DATABASE OPEN;\n-- Verify:\nARCHIVE LOG LIST;","what":"데이터베이스를 archive log 모드로 전환한다. 가득 찬 redo log가 아카이브되어 핫 백업과 특정 시점 복구가 가능해진다.","when":"운영 데이터베이스의 백업 및 복구 전략을 위해(RMAN, Data Guard 이전에 필수).","caution":"인스턴스 재시작이 필요하므로 다운타임을 계획한다. 아카이브 대상(FRA 또는 LOG_ARCHIVE_DEST)에 충분한 공간이 있는지 확인한다. 가득 차면 DB가 정지한다. RAC: srvctl stop database -d DB로 모든 인스턴스를 중지하고, 단일 인스턴스를 STARTUP MOUNT로 열어 ARCHIVELOG로 전환한 뒤 종료하고 srvctl start database -d DB로 시작한다(11gR2+에서는 cluster_database=false가 필요 없다).","tags":["archivelog","archive","mode","mount","recovery","recovery"]},{"id":"fra-full-fix","task":"가득 찬 FRA / 아카이브 대상 해결","category":"Redo 및 아카이브","command":"-- Check usage:\nSELECT name, space_limit/1024/1024/1024 AS limit_gb,\n       space_used/1024/1024/1024  AS used_gb,\n       ROUND(space_used/space_limit*100,1) AS pct_used\nFROM   v$recovery_file_dest;\n\n-- Fix A) Increase the FRA size:\nALTER SYSTEM SET db_recovery_file_dest_size=100G SCOPE=BOTH;\n\n-- Fix B) Clean up already backed-up archives with RMAN:\n-- RMAN> DELETE ARCHIVELOG ALL BACKED UP 1 TIMES TO DISK COMPLETED BEFORE 'SYSDATE-3';\n-- If there is no backup (CAREFUL): RMAN> DELETE ARCHIVELOG ALL COMPLETED BEFORE 'SYSDATE-1';","what":"Fast Recovery Area나 아카이브 대상이 가득 차면 데이터베이스가 멈춘다(ORA-00257). 공간을 늘리거나 아카이브를 정리해 해결한다.","when":"ORA-00257 archiver 오류, DB가 새 트랜잭션을 받지 않을 때.","caution":"백업되지 않은 아카이브를 삭제하면 복구 가능성이 사라진다. 먼저 RMAN 백업을 수행한 뒤 삭제한다. OS에서 rm으로 삭제하지 않는다. 그렇게 하면 RMAN CROSSCHECK가 필요하다. FRA가 가득 찬 동안에는 FRA 밖의 대상으로 아카이브를 백업한다(FORMAT '/backup/arc_%U'). Data Guard 환경에서는 standby로 아직 전송/적용되지 않은 아카이브를 삭제하지 않는다. 그렇지 않으면 standby에 gap이 생긴다. CONFIGURE ARCHIVELOG DELETION POLICY TO APPLIED ON ALL STANDBY를 사용한다.","tags":["fra","recovery area","ora-00257","archiver","archive","full","cleanup"]},{"id":"redo-status","task":"Redo log 상태 조회","category":"Redo 및 아카이브","command":"SELECT l.group#, l.thread#, l.sequence#,\n       ROUND(l.bytes/1024/1024) AS mb,\n       l.members, l.archived, l.status,\n       lf.member AS file_path\nFROM   v$log l\n  JOIN v$logfile lf ON lf.group# = l.group#\nORDER BY l.group#, lf.member;","what":"redo log 그룹의 상태(CURRENT/ACTIVE/INACTIVE), 크기, 멤버 파일, 아카이브 상태를 보여 준다.","when":"redo 구성을 검토할 때, 또는 크기 변경 전에 어느 그룹이 활성인지 확인할 때.","caution":"조회 전용이며 위험 없음.","tags":["redo","status","status","v$log","v$logfile","logfile"]},{"id":"asm-add-disk","task":"ASM diskgroup에 디스크 추가","category":"ASM","command":"-- Connect to the ASM instance (sqlplus / as sysasm)\nALTER DISKGROUP DATA ADD DISK\n  '/dev/oracleasm/disks/DATA05' NAME DATA_0005,\n  '/dev/oracleasm/disks/DATA06' NAME DATA_0006\n  REBALANCE POWER 4;","what":"DATA diskgroup에 새 디스크를 추가하고 자동 rebalance를 시작하며, 용량이 늘어난다.","when":"ASM diskgroup이 차기 시작할 때, 또는 스토리지를 확장할 때.","caution":"SYSASM 권한이 필요하다. 디스크 크기는 비슷해야 한다. rebalance는 I/O 부하를 일으키므로 바쁜 시간에는 낮은 POWER를 사용한다.","tags":["asm","diskgroup","add disk","add disk","rebalance","alter diskgroup"]},{"id":"asm-drop-disk","task":"ASM diskgroup에서 디스크 제거","category":"ASM","command":"ALTER DISKGROUP DATA DROP DISK DATA_0005 REBALANCE POWER 4;\n\n-- WAIT until the drop completes (the disk can be physically removed once V$ASM_OPERATION is empty):\nSELECT group_number, operation, state, power,\n       sofar, est_work, est_minutes\nFROM   v$asm_operation;","what":"diskgroup에서 디스크를 제거하며, 해당 extent는 나머지 디스크로 이동한다(rebalance). 완료되면 디스크의 HDR_STATUS가 FORMER가 된다.","when":"고장 난 디스크 교체, 용량 축소, 스토리지 마이그레이션 시.","caution":"rebalance가 끝나기 전에 디스크를 물리적으로 분리하지 않는다. 그렇지 않으면 데이터가 손실된다. 나머지 디스크에 충분한 여유 공간이 있는지 확인한다.","tags":["asm","drop disk","remove disk","rebalance","v$asm_operation","alter diskgroup"]},{"id":"asm-rebalance-power","task":"ASM rebalance POWER 설정","category":"ASM","command":"-- Change the power of a running rebalance (1-1024, 0 stops it):\nALTER DISKGROUP DATA REBALANCE POWER 8;\n\n-- Stop the rebalance completely:\nALTER DISKGROUP DATA REBALANCE POWER 0;","what":"진행 중인 rebalance의 병렬도/속도를 설정한다. POWER가 높을수록 빠르지만 I/O가 늘어난다.","when":"너무 느린 rebalance를 가속하거나, 운영에 영향을 주는 rebalance를 늦추거나 멈출 때.","caution":"POWER 0은 rebalance를 일시 중지한다(디스크 drop이 완료되지 않음). 높은 값은 운영 I/O에 영향을 준다.","tags":["asm","rebalance","power","power","diskgroup","tuning"]},{"id":"asm-diskgroup-space","task":"ASM diskgroup 여유 공간 조회","category":"ASM","command":"SELECT name,\n       state,\n       type,\n       total_mb/1024                 AS total_gb,\n       free_mb/1024                  AS free_gb,\n       ROUND((1-free_mb/total_mb)*100,1) AS pct_used,\n       usable_file_mb/1024           AS usable_gb\nFROM   v$asm_diskgroup\nORDER BY name;","what":"각 diskgroup의 전체, 여유, 사용 가능한(이중화 반영 후) 공간을 보여 준다.","when":"용량 계획, 공간 부족 시, 디스크 추가 결정 전에.","caution":"usable_file_mb는 이중화를 반영한다. NORMAL/HIGH redundancy에서는 이 컬럼이 실제 사용 가능한 공간이다. 조회 전용이며 위험 없음.","tags":["asm","diskgroup","space","space","free","v$asm_diskgroup","capacity"]},{"id":"asm-monitor-rebalance","task":"ASM rebalance 모니터링","category":"ASM","command":"SELECT group_number, operation, pass, state, power,\n       sofar, est_work,\n       ROUND(sofar/NULLIF(est_work,0)*100,1) AS pct_done,\n       est_minutes\nFROM   v$asm_operation\nORDER BY group_number;","what":"진행 중인 rebalance/resync 작업의 진행률, power, 예상 남은 시간을 보여 준다.","when":"디스크 추가/제거 후 rebalance 완료 여부를 추적할 때.","caution":"빈 결과 = 활성 작업 없음(완료). 조회 전용이며 위험 없음.","tags":["asm","rebalance","monitor","monitor","v$asm_operation","progress"]},{"id":"add-tablespace","task":"새 테이블스페이스 추가","category":"스토리지","command":"-- File system:\nCREATE TABLESPACE customer_data\n  DATAFILE '/u01/oradata/ORCL/customer_data01.dbf'\n  SIZE 1G AUTOEXTEND ON NEXT 256M MAXSIZE 20G\n  EXTENT MANAGEMENT LOCAL\n  SEGMENT SPACE MANAGEMENT AUTO;\n\n-- If you use ASM:\nCREATE TABLESPACE customer_data\n  DATAFILE '+DATA' SIZE 1G AUTOEXTEND ON NEXT 256M MAXSIZE 20G;","what":"테이블/인덱스 세그먼트를 배치할 수 있는 새 영구 테이블스페이스를 생성한다.","when":"새 애플리케이션/스키마에 별도 스토리지를 할당할 때.","caution":"MAXSIZE를 지정한다. 그렇지 않으면 디스크가 가득 찰 수 있다. ASM에서는 파일 이름이 자동으로 생성된다.","tags":["tablespace","create","create","datafile","storage","storage","autoextend"]},{"id":"add-datafile","task":"기존 테이블스페이스에 데이터 파일 추가","category":"스토리지","command":"ALTER TABLESPACE customer_data\n  ADD DATAFILE '/u01/oradata/ORCL/customer_data02.dbf'\n  SIZE 2G AUTOEXTEND ON NEXT 256M MAXSIZE 30G;\n\n-- ASM:\nALTER TABLESPACE customer_data\n  ADD DATAFILE '+DATA' SIZE 2G AUTOEXTEND ON NEXT 256M MAXSIZE 30G;","what":"새 데이터 파일을 추가해 기존 테이블스페이스의 용량을 늘린다.","when":"테이블스페이스가 가득 차고 단일 파일이 smallfile 한도(32G/64G)에 근접할 때.","caution":"smallfile 테이블스페이스는 파일당 블록 한도가 있으므로 필요하면 새 파일을 추가한다. MAXSIZE를 지정한다.","tags":["datafile","add","add","tablespace","alter tablespace","storage"]},{"id":"resize-datafile","task":"데이터 파일 크기 변경 / autoextend 설정","category":"스토리지","command":"-- Grow/shrink to a fixed size:\nALTER DATABASE DATAFILE '/u01/oradata/ORCL/customer_data01.dbf' RESIZE 5G;\n\n-- Enable autoextend:\nALTER DATABASE DATAFILE '/u01/oradata/ORCL/customer_data01.dbf'\n  AUTOEXTEND ON NEXT 256M MAXSIZE 30G;\n\n-- Disable autoextend:\nALTER DATABASE DATAFILE '/u01/oradata/ORCL/customer_data01.dbf' AUTOEXTEND OFF;","what":"데이터 파일을 고정 크기로 설정하거나 자동 증가 동작을 구성한다.","when":"공간 회수(축소), 공간 사전 할당, 무분별한 증가 방지를 위해.","caution":"RESIZE로 축소할 때 파일 끝부분의 사용 중인 블록 아래로는 줄일 수 없다(ORA-03297). 먼저 세그먼트를 이동한다.","tags":["datafile","resize","size","autoextend","maxsize","alter database"]},{"id":"move-datafile-online","task":"데이터 파일 온라인 이동","category":"스토리지","command":"ALTER DATABASE MOVE DATAFILE\n  '/u01/oradata/ORCL/customer_data01.dbf'\n  TO '/u03/oradata/ORCL/customer_data01.dbf';\n\n-- Move into ASM:\nALTER DATABASE MOVE DATAFILE\n  '/u01/oradata/ORCL/customer_data01.dbf' TO '+DATA';\n\n-- Use KEEP to retain the old file instead of deleting it:\n-- ALTER DATABASE MOVE DATAFILE '...' TO '...' KEEP;","what":"데이터베이스가 열린 상태에서 데이터 파일을 새 위치로 이동한다(12c+). 다운타임 없이 스토리지를 변경한다.","when":"디스크/마운트 포인트 변경, ASM으로 마이그레이션, 빠른 스토리지로 이동할 때.","caution":"12c 이상과 Enterprise Edition이 필요하다. 이동은 추가 I/O를 발생시킨다. KEEP을 사용하지 않으면 기존 파일이 삭제된다.","since":"Oracle 12c","note":"온라인 데이터 파일 이동(ALTER DATABASE MOVE DATAFILE)은 Oracle 12c(12.1)에서 도입되었다. 그 이전에는 테이블스페이스/데이터 파일을 OFFLINE으로 전환하고 OS 수준에서 복사한 뒤 ALTER DATABASE RENAME FILE로 다시 매핑해야 했다.","tags":["datafile","move","move","online","alter database move datafile","asm"]},{"id":"temp-tablespace","task":"temp 테이블스페이스 추가 / 기본값 변경","category":"스토리지","command":"-- Create a new temp tablespace:\nCREATE TEMPORARY TABLESPACE temp2\n  TEMPFILE '/u01/oradata/ORCL/temp2_01.dbf'\n  SIZE 2G AUTOEXTEND ON NEXT 256M MAXSIZE 20G;\n\n-- Change the database default temp:\nALTER DATABASE DEFAULT TEMPORARY TABLESPACE temp2;\n\n-- Add a tempfile to the old temp:\nALTER TABLESPACE temp ADD TEMPFILE '/u01/oradata/ORCL/temp_02.dbf' SIZE 2G;","what":"sort/hash/global temporary table 작업을 위한 임시 공간을 제공하며, 기본값을 새 테이블스페이스로 옮길 수 있다.","when":"ORA-01652(unable to extend temp segment), temp 병목, temp 크기 변경 시.","caution":"기본값을 바꾼 뒤 기존 temp가 사용 중이면 바로 DROP하지 않는다. 세션이 모두 빠진 후 삭제한다.","tags":["temp","tablespace","tempfile","ora-01652","default temporary","storage"]},{"id":"tablespace-space","task":"테이블스페이스 여유 공간 조회","category":"스토리지","command":"SELECT df.tablespace_name,\n       ROUND(df.total_mb/1024,2)                 AS total_gb,\n       ROUND(NVL(fs.free_mb,0)/1024,2)           AS free_gb,\n       ROUND((df.total_mb-NVL(fs.free_mb,0))/df.total_mb*100,1) AS pct_used\nFROM  (SELECT tablespace_name, SUM(bytes)/1024/1024 total_mb\n         FROM dba_data_files GROUP BY tablespace_name) df\n  LEFT JOIN\n       (SELECT tablespace_name, SUM(bytes)/1024/1024 free_mb\n          FROM dba_free_space GROUP BY tablespace_name) fs\n    ON df.tablespace_name = fs.tablespace_name\nORDER BY pct_used DESC;","what":"각 테이블스페이스의 전체 공간, 여유 공간, 사용률을 보여 준다(현재 할당된 공간 기준, autoextend 가능분 제외).","when":"공간 모니터링, 확장이 필요한 테이블스페이스 찾기.","caution":"이 쿼리는 현재 데이터 파일 크기 기준이며, autoextend로 더 늘어날 수 있다. temp는 DBA_TEMP_FREE_SPACE를 사용한다. 조회 전용이며 위험 없음.","tags":["tablespace","space","space","free","dba_free_space","usage","storage"]},{"id":"alter-parameter","task":"파라미터 변경(SCOPE 지정)","category":"메모리 및 파라미터","command":"-- Both the running instance and the spfile (default, dynamic parameters):\nALTER SYSTEM SET open_cursors=500 SCOPE=BOTH;\n\n-- Only for the next restart (static parameters):\nALTER SYSTEM SET processes=1000 SCOPE=SPFILE;\n\n-- Immediate only, do not write to the spfile:\nALTER SYSTEM SET optimizer_mode='ALL_ROWS' SCOPE=MEMORY;\n\n-- View the current value:\nSHOW PARAMETER open_cursors;","what":"실행 중인 인스턴스 및/또는 spfile의 init 파라미터를 변경한다. SCOPE가 동작을 결정한다.","when":"튜닝, 용량 조정, 파라미터 변경이 필요한 오류 수정 시.","caution":"정적 파라미터(processes, sga_max_size...)는 SCOPE=SPFILE만 허용하며 재시작이 필요하다. 잘못된 값은 인스턴스를 시작할 수 없게 만들 수 있으므로, 핵심 파라미터는 먼저 pfile 백업을 받는다.","tags":["parameter","parameter","alter system","scope","spfile","tuning"]},{"id":"flush-pool","task":"Shared pool / buffer cache 비우기 (flush)","category":"메모리 및 파라미터","command":"-- Clear the SQL plan/cursor cache:\nALTER SYSTEM FLUSH SHARED_POOL;\n\n-- Clear the buffer cache (usually only for test/benchmark):\nALTER SYSTEM FLUSH BUFFER_CACHE;","what":"shared pool을 flush하면 캐시된 모든 SQL/PL-SQL과 딕셔너리 정보가 메모리에서 제거되고, buffer cache를 flush하면 모든 데이터 블록이 제거된다.","when":"실행 계획 문제를 테스트하거나, 일회성 대규모 작업 후, 또는 일관된 벤치마크 시작을 위해.","caution":"운영 환경에서는 주의! flush 후 모든 SQL이 다시 파싱되고(hard parse 폭주) 블록을 디스크에서 읽으므로 일시적으로 성능이 크게 떨어진다.","tags":["flush","shared pool","buffer cache","memory","alter system","cache"]},{"id":"spfile-pfile","task":"spfile에서 pfile 생성 및 그 반대","category":"메모리 및 파라미터","command":"-- spfile -> pfile (for backup/editing):\nCREATE PFILE='/tmp/init_orcl_backup.ora' FROM SPFILE;\n\n-- pfile -> spfile (make the edited pfile permanent; effective after restart):\nCREATE SPFILE FROM PFILE='/tmp/init_orcl_backup.ora';\n\n-- Create a pfile from the running instance values (11g+):\nCREATE PFILE='/tmp/init_current.ora' FROM MEMORY;","what":"바이너리 spfile과 텍스트 pfile 간에 변환한다. pfile은 직접 편집할 수 있다.","when":"파라미터 변경 전 백업, 손상된 spfile 복구, 정적 파라미터 수동 수정.","caution":"CREATE SPFILE FROM PFILE 후 변경 사항은 재시작 후에만 적용된다. 기존 spfile을 덮어쓰기 전에 경로를 확인한다.","tags":["spfile","pfile","create pfile","create spfile","parameter","backup"]},{"id":"sga-pga-target","task":"SGA / PGA 목표값 변경","category":"메모리 및 파라미터","command":"-- Automatic memory management (ASMM) targets:\nALTER SYSTEM SET sga_target=8G   SCOPE=BOTH;\nALTER SYSTEM SET pga_aggregate_target=4G SCOPE=BOTH;\n\n-- Upper limit (static, requires restart):\nALTER SYSTEM SET sga_max_size=12G SCOPE=SPFILE;\n\n-- View the current memory distribution:\nSELECT component, current_size/1024/1024 AS mb\nFROM   v$sga_dynamic_components ORDER BY component;","what":"SGA와 PGA 메모리 목표값을 설정하며, Oracle이 구성 요소를 자동으로 관리한다(ASMM/AMM).","when":"서버에 RAM이 추가되었을 때, 메모리 병목이나 메모리 압박 시.","caution":"sga_target은 sga_max_size를 넘을 수 없다. sga_max_size는 정적이며 재시작이 필요하다. 운영 체제와 다른 인스턴스를 위한 RAM을 남겨 둔다. 과도한 할당은 스와핑/OOM을 일으킨다.","tags":["sga","pga","memory","memory","target","sga_target","pga_aggregate_target"]},{"id":"kill-session","task":"세션 종료(kill session)","category":"세션 및 프로세스","command":"-- First find the session to kill:\nSELECT sid, serial#, username, status, machine, program\nFROM   v$session\nWHERE  username = 'CUSTOMER_APP' AND status = 'ACTIVE';\n\n-- Kill it (starts rollback immediately):\nALTER SYSTEM KILL SESSION '145,12345' IMMEDIATE;\n\n-- If it hangs, the OS process can be terminated:\n-- ALTER SYSTEM DISCONNECT SESSION '145,12345' IMMEDIATE;","what":"지정한 SID,SERIAL#의 세션을 종료하며, 열린 트랜잭션은 롤백된다.","when":"락을 보유하거나, 폭주 쿼리를 실행하거나, 연결을 누수하는 세션을 중지할 때.","caution":"열린 트랜잭션이 있으면 롤백에 오래 걸릴 수 있다(작업량이 적으면 빠르게 끝남). 잘못된 SID를 종료하지 않도록 먼저 쿼리로 확인한다. 대규모 DML의 롤백은 I/O 부하를 일으킨다.","tags":["kill session","kill session","v$session","alter system","sid","serial"]},{"id":"long-running-queries","task":"장시간 실행 쿼리 / 세션 조회","category":"세션 및 프로세스","command":"-- Active, long-running SQL:\nSELECT s.sid, s.serial#, s.username, s.status,\n       s.last_call_et AS seconds_active,\n       s.sql_id, q.sql_text,\n       s.event, s.machine, s.program\nFROM   v$session s\n  LEFT JOIN v$sql q ON q.sql_id = s.sql_id\n                   AND q.child_number = s.sql_child_number\nWHERE  s.type = 'USER'\n  AND  s.status = 'ACTIVE'\n  AND  s.last_call_et > 60\nORDER BY s.last_call_et DESC;\n\n-- Long operations with percentage progress:\nSELECT sid, serial#, opname, sofar, totalwork,\n       ROUND(sofar/NULLIF(totalwork,0)*100,1) AS pct, time_remaining\nFROM   v$session_longops\nWHERE  sofar < totalwork ORDER BY start_time DESC;","what":"60초 이상 실행 중인 활성 세션과 장시간 배치 작업의 진행률을 나열한다.","when":"느려짐 문의 시, 어떤 쿼리가 시스템을 바쁘게 만드는지 찾을 때.","caution":"last_call_et는 마지막 호출 이후의 시간이며, 유휴 세션에서도 클 수 있으므로 status='ACTIVE' 필터가 중요하다. 조회 전용이며 위험 없음.","tags":["long running","long query","v$session","v$sql","v$session_longops","active"]},{"id":"find-blocking","task":"블로킹 세션 찾기","category":"세션 및 프로세스","command":"SELECT bl.sid       AS blocker_sid,\n       bl.serial#   AS blocker_serial,\n       bl.username  AS blocker_user,\n       w.sid        AS waiter_sid,\n       w.serial#    AS waiter_serial,\n       w.username   AS waiter_user,\n       w.seconds_in_wait AS wait_sec,\n       w.event\nFROM   v$session w\n  JOIN v$session bl ON bl.sid = w.blocking_session\nWHERE  w.blocking_session IS NOT NULL\nORDER BY w.seconds_in_wait DESC;","what":"블로커와 대기자 세션 쌍을 대기 시간 및 이벤트와 함께 나열한다.","when":"\"애플리케이션 멈춤 / enq: TX 대기\" 같은 락 문제에서 원인을 찾을 때.","caution":"블로커를 종료하기 전에 업무 담당자와 확인한다. 중요한 작업일 수 있다. 조회 전용이며 위험 없음.","tags":["blocking","lock","lock","blocker","waiter","v$session","blocking_session"]},{"id":"gather-stats","task":"통계 수집(DBMS_STATS)","category":"유지보수","command":"-- Single table (including cascaded indexes):\nEXEC DBMS_STATS.GATHER_TABLE_STATS(ownname => 'CUSTOMER_APP', tabname => 'CUSTOMER', cascade => TRUE, estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE, degree => 4);\n\n-- Whole schema:\nEXEC DBMS_STATS.GATHER_SCHEMA_STATS(ownname => 'CUSTOMER_APP', estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE, cascade => TRUE, degree => 4);\n\n-- Whole database (run only in a maintenance window / off-peak hours):\nEXEC DBMS_STATS.GATHER_DATABASE_STATS(estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE, degree => 4);","what":"옵티마이저를 위한 테이블/인덱스 통계를 수집하여 올바른 실행 계획이 생성되도록 한다.","when":"대량 DML/로드 이후, 실행 계획이 악화될 때, 새 인덱스/테이블 생성 이후.","caution":"통계 변경은 실행 계획을 바꿀 수 있다. 중요한 시스템에서는 먼저 AWR로 테스트/비교한다. DEGREE 병렬 처리는 CPU를 사용한다. GATHER_DATABASE_STATS는 업무 시간대에 부하가 크다.","tags":["dbms_stats","statistics","statistics","gather","optimizer","maintenance"]},{"id":"rebuild-index-online","task":"온라인으로 인덱스 재구축","category":"유지보수","command":"-- Regular index:\nALTER INDEX customer_app.idx_customer_email REBUILD ONLINE PARALLEL 4;\n\n-- Moving it to a different tablespace:\nALTER INDEX customer_app.idx_customer_email\n  REBUILD ONLINE TABLESPACE customer_idx PARALLEL 4;\n\n-- Reset parallelism after the rebuild (so plans are not affected):\nALTER INDEX customer_app.idx_customer_email NOPARALLEL;","what":"DML을 블로킹하지 않고(ONLINE) 인덱스를 재구축한다. 단편화를 제거하며 테이블스페이스를 변경할 수 있다.","when":"인덱스 비대화/단편화, 인덱스를 다른 테이블스페이스로 이동, UNUSABLE 인덱스 복구.","caution":"ONLINE은 추가 임시 공간과 I/O를 사용한다. PARALLEL은 유지되므로 재구축 후 NOPARALLEL로 되돌린다. 그렇지 않으면 쿼리가 의도치 않게 병렬로 실행된다. 비트맵 인덱스는 버전에 따라 ONLINE을 지원하지 않는다. ONLINE 재구축은 Enterprise Edition에서만 사용할 수 있다.","tags":["index","rebuild","online","rebuild","alter index","fragmentation","maintenance"]},{"id":"rman-full-backup","task":"RMAN으로 빠른 전체 백업","category":"유지보수","command":"# From the shell: rman target /\nRUN {\n  ALLOCATE CHANNEL c1 DEVICE TYPE DISK;\n  ALLOCATE CHANNEL c2 DEVICE TYPE DISK;\n  BACKUP AS COMPRESSED BACKUPSET\n    DATABASE\n    PLUS ARCHIVELOG\n    TAG 'FULL_DAILY';\n  BACKUP CURRENT CONTROLFILE;\n  DELETE NOPROMPT OBSOLETE;\n  RELEASE CHANNEL c1;\n  RELEASE CHANNEL c2;\n}","what":"두 채널로 병렬 처리하여 데이터베이스 전체와 아카이브 로그의 압축 전체 백업을 수행하고, controlfile을 백업하며 불필요한(obsolete) 백업을 정리한다.","when":"일일/주간 전체 백업, 복구 지점 생성.","caution":"COMPRESSED는 CPU를 사용한다. DELETE OBSOLETE는 보존 정책에 따라 오래된 백업을 삭제하므로 정책(CONFIGURE RETENTION POLICY)을 올바르게 설정한다. ARCHIVELOG는 DB가 ARCHIVELOG 모드여야 한다. 백업 대상에 충분한 공간이 있는지 확인한다.","tags":["rman","backup","backup","full","compressed","archivelog","maintenance"]},{"id":"controlfile-trace","task":"controlfile trace 백업","category":"유지보수","command":"-- Generate a trace (CREATE CONTROLFILE script):\nALTER DATABASE BACKUP CONTROLFILE TO TRACE\n  AS '/tmp/create_controlfile.sql' REUSE;\n\n-- Binary controlfile copy:\nALTER DATABASE BACKUP CONTROLFILE TO '/backup/controlfile_orcl.ctl' REUSE;","what":"controlfile을 재생성하는 SQL 스크립트(trace) 및/또는 바이너리 사본을 생성한다.","when":"구조 변경(데이터 파일/redo 추가) 이후, 재해 복구 준비 시, 또는 모든 사본을 잃었을 때 controlfile 재생성용.","caution":"구조가 바뀔 때마다 trace 파일을 갱신한다. 오래된 스크립트는 잘못된 구조를 만든다. REUSE는 기존 파일을 덮어쓴다. 시스템에 위험은 없지만 생성된 파일은 안전하게 보관한다.","tags":["controlfile","trace","backup","backup","alter database","recovery","maintenance"]}]},"recipes/oracle-ha.json":{"engineId":"oracle","engine":"Oracle","order":1,"recipes":[{"id":"rac-srvctl-db-status","task":"srvctl로 RAC 데이터베이스, 인스턴스, 서비스 상태 확인","category":"RAC 및 Grid","command":"# Which node each instance runs on, and in what state:\nsrvctl status database -db <db_unique_name> -verbose\n# Configuration registered in Clusterware (Oracle home, spfile, disk group, policy, role):\nsrvctl config database -db <db_unique_name>\n# A single instance:\nsrvctl status instance -db <db_unique_name> -instance <instance>\n# Which instances the services are running on:\nsrvctl status service -db <db_unique_name>","what":"Clusterware(CRS)가 보는 데이터베이스, 각 인스턴스, 서비스의 실행 상태와 등록된 구성을 보여 준다. -verbose는 인스턴스 open mode를 추가한다.","when":"RAC 작업 전후의 첫 점검으로, 장애 시 영향을 받은 노드/인스턴스 확인용.","caution":"읽기 전용. 데이터베이스 Oracle home(oracle 사용자)에서 실행한다. 버전이 다른 Grid home의 srvctl은 잘못된 결과나 오류를 반환할 수 있다.","since":"12c","note":"긴 옵션 -db/-instance/-service는 12c+이다. 11g의 짧은 옵션(-d, -i, -s)은 19c에서도 동작하지만 deprecated 상태다.","tags":["rac","srvctl","status","instance","service","cluster","config"],"untested":true},{"id":"rac-srvctl-start-stop","task":"srvctl로 RAC 데이터베이스 또는 단일 인스턴스 중지 / 시작(rolling)","category":"RAC 및 Grid","command":"# Whole database (all instances):\nsrvctl stop database -db <db_unique_name> -stopoption immediate\nsrvctl start database -db <db_unique_name>\n# Rolling maintenance: stop one instance, moving its services to the other instances\nsrvctl stop instance -db <db_unique_name> -instance <instance> \\\n  -stopoption immediate -drain_timeout 300 -failover\nsrvctl start instance -db <db_unique_name> -instance <instance>\n# By node:\nsrvctl stop instance -db <db_unique_name> -node <node> -stopoption immediate\n# Start in a specific open mode (e.g. a physical standby in mount):\nsrvctl start database -db <db_unique_name> -startoption mount","what":"Clusterware를 통해 데이터베이스 또는 선택한 인스턴스를 제어된 방식으로 중지/시작한다. -failover를 지정하면 중지된 인스턴스의 서비스가 available 인스턴스로 이동하고, -drain_timeout은 세션이 빠질 때까지 대기한다.","when":"패치, OS 유지보수, 파라미터 변경 시 rolling(노드별) 재시작, 계획된 전체 종료.","caution":"RAC에서는 SQL*Plus SHUTDOWN 대신 srvctl을 사용한다. 그렇지 않으면 Clusterware가 리소스 TARGET을 알지 못해 인스턴스를 다시 시작할 수 있다. stop database는 모든 인스턴스를 종료한다 = 전체 서비스 중단. 서비스에 available 인스턴스가 정의되어 있지 않으면 -failover로 연결을 이동할 수 없다.","since":"12c","note":"-drain_timeout과 -failover는 12.2+이다. -stopoption abort는 긴급 상황에서만 사용한다. 다음 시작 시 인스턴스 복구가 필요하다.","tags":["rac","srvctl","stop","start","rolling","drain","shutdown"],"untested":true},{"id":"rac-service-add","task":"preferred/available 인스턴스를 지정하여 RAC 서비스 추가(Application Continuity 포함)","category":"RAC 및 Grid","command":"srvctl add service -db <db_unique_name> -service <service> \\\n  -preferred <inst1>,<inst2> -available <inst3> \\\n  -pdb <pdb> -role PRIMARY -policy AUTOMATIC \\\n  -failovertype TRANSACTION -commit_outcome TRUE \\\n  -failoverretry 30 -failoverdelay 10 \\\n  -clbgoal SHORT -rlbgoal SERVICE_TIME -notification TRUE \\\n  -drain_timeout 300 -stopoption IMMEDIATE\nsrvctl start service -db <db_unique_name> -service <service>\n# Verify:\nsrvctl config service -db <db_unique_name> -service <service>\nsrvctl status service -db <db_unique_name> -service <service>","what":"평소에는 preferred 인스턴스에서 실행되고 장애 시 available 인스턴스로 이동하는 애플리케이션용 서비스를 정의한다. -role PRIMARY를 지정하면 Data Guard에서 primary 역할일 때만 시작한다.","when":"애플리케이션을 인스턴스가 아닌 서비스에 연결하고, 워크로드를 노드별로 분리하며, 계획된 유지보수 중 끊김 없는 페일오버(drain/failover)를 가능하게 하기 위해.","caution":"애플리케이션은 SID나 인스턴스 이름이 아닌 이 서비스 이름으로 (SCAN을 통해) 연결해야 한다. -failovertype TRANSACTION(Application Continuity)은 애플리케이션/드라이버 지원이 필요하다. 확신이 없으면 -failovertype SELECT(TAF) 또는 NONE으로 시작한다. SYS$ 서비스나 db_unique_name과 같은 이름의 기본 서비스를 애플리케이션에 사용하지 않는다.","since":"12c","note":"-pdb는 12c+, -drain_timeout과 -stopoption은 12.2+이다. 변경은 srvctl modify service로 한다. preferred를 영구적으로 바꾸려면 -modifyconfig가 필요하다.","tags":["rac","service","srvctl","preferred","available","taf","application continuity"],"untested":true},{"id":"rac-service-relocate","task":"RAC 서비스를 다른 인스턴스로 재배치(relocate)","category":"RAC 및 Grid","command":"# First check the current placement:\nsrvctl status service -db <db_unique_name> -service <service>\n# Relocate by instance (sessions are drained):\nsrvctl relocate service -db <db_unique_name> -service <service> \\\n  -oldinst <inst1> -newinst <inst3> -drain_timeout 120 -wait YES\n# By node (policy-managed / singleton service):\nsrvctl relocate service -db <db_unique_name> -service <service> \\\n  -currentnode <node1> -targetnode <node2>\n# Session distribution per instance (SQL):\nSELECT inst_id, service_name, COUNT(*) AS sessions\nFROM   gv$session\nWHERE  type = 'USER'\nGROUP  BY inst_id, service_name\nORDER  BY service_name, inst_id;","what":"실행 중인 서비스를 한 인스턴스/노드에서 다른 곳으로 옮긴다. 새 연결은 대상 인스턴스로 가고 기존 세션은 drain_timeout 동안 스스로 종료되도록 둔다.","when":"유지보수 전 노드를 비우거나, 페일오버 후 서비스를 preferred 인스턴스로 되돌리거나, 부하를 재분산하기 위해.","caution":"재배치는 일시적이다. 서비스 구성(preferred/available)은 바뀌지 않으며 인스턴스가 재시작되면 서비스는 preferred 인스턴스로 돌아간다. -force는 기존 세션을 종료한다. 애플리케이션이 재연결하지 못하면 사용자는 서비스 중단을 겪는다.","since":"12c","note":"-drain_timeout과 -wait는 12.2+이다. SQL 쿼리는 단일 인스턴스에서도 실행되며, srvctl 부분은 RAC가 필요하다.","tags":["rac","service","relocate","move","srvctl","drain","gv$session"],"untested":true},{"id":"rac-crsctl-stat-res","task":"클러스터 리소스 상태 확인(crsctl stat res -t)","category":"RAC 및 Grid","command":"# From the Grid home, as the grid user:\ncrsctl stat res -t\n# OHASD (init) layer: ora.cssd, ora.crsd, ora.asm, ora.ctssd...\ncrsctl stat res -t -init\n# Only problem resources (TARGET is ONLINE but STATE is not):\ncrsctl stat res -t -w \"((TARGET = ONLINE) and (STATE != ONLINE))\"\n# Profile and runtime details of a single resource:\ncrsctl stat res ora.<db_unique_name>.db -p\ncrsctl stat res ora.<db_unique_name>.db -v","what":"Clusterware가 관리하는 모든 리소스(데이터베이스, 인스턴스, 리스너, SCAN, VIP, ASM, 디스크 그룹, 네트워크)를 노드별 TARGET/STATE와 함께 표 형태로 나열한다.","when":"노드 시작 후, 패치 후, 모든 장애 조사 시. OFFLINE, INTERMEDIATE, UNKNOWN 리소스를 찾기 위해.","caution":"읽기 전용. ora.* 리소스를 crsctl start/stop/modify로 직접 관리하지 않는다. 데이터베이스 리소스에는 srvctl을 사용한다(지원되는 방법).","note":"리소스 이름에는 소문자 db_unique_name이 들어간다(ora.orcl.db). -init 출력은 root/grid로만 전부 볼 수 있다.","tags":["rac","grid","crsctl","resource","resource","clusterware","status"],"untested":true},{"id":"rac-crsctl-check-cluster","task":"클러스터 스택 상태, 노드 목록, Clusterware 버전 확인","category":"RAC 및 Grid","command":"# CRS, CSS, EVM health check on all nodes:\ncrsctl check cluster -all\n# Full stack on this node, including OHASD:\ncrsctl check crs\n# Node list: number, status (Active/Inactive), pinned:\nolsnodes -n -s -t\n# Active version, patch level and upgrade state (after a rolling patch):\ncrsctl query crs activeversion -f\ncrsctl query crs softwareversion -all","what":"Cluster Ready Services, Cluster Synchronization Services, Event Manager 데몬이 모든 노드에서 실행 중인지, 클러스터 멤버, Clusterware의 active/software 버전을 보여 준다.","when":"아침 상태 점검, 노드 eviction 또는 재부팅 후, rolling 패치/업그레이드 후 모든 노드가 같은 수준인지 확인할 때.","caution":"읽기 전용. activeversion -f가 \"ROLLING PATCH\" 또는 다른 패치 수준을 보이면 rolling 작업이 완료되지 않은 것이다. 새 변경 전에 이를 마무리한다(crsctl stop rollingpatch / rootcrs -postpatch).","since":"12c","note":"activeversion -f와 softwareversion -all은 12c+이다.","tags":["rac","grid","crsctl","olsnodes","health","cluster","version"],"untested":true},{"id":"rac-ocr-backup","task":"OCR 무결성 확인 및 OCR 백업(ocrcheck / ocrconfig)","category":"RAC 및 Grid","command":"# OCR integrity and capacity check (run as root for the logical check):\nocrcheck\nocrcheck -config\n# Automatic backups (every 4 hours, daily, weekly):\nocrconfig -showbackup\n# Manual backup before a risky change (root):\nocrconfig -manualbackup\nocrconfig -showbackup manual\n# Move the backup location to a disk group (root):\nocrconfig -backuploc +<diskgroup>","what":"Oracle Cluster Registry(클러스터 구성 저장소)의 무결성, 위치, 백업을 보여 준다. -manualbackup은 즉시 물리 OCR 백업을 수행한다.","when":"노드 추가/제거, Grid 패치/업그레이드, voting disk 또는 네트워크(oifcfg) 변경 등 클러스터 구성 변경 직전.","caution":"ocrconfig -restore는 모든 노드에서 CRS를 중지한 상태로 exclusive 모드에서 실행해야 한다. 잘못된 백업을 복원하면 클러스터가 시작되지 않을 수 있으므로 Oracle Support와 함께 진행한다. 자동 백업은 기본적으로 단일 노드의 로컬 디스크에 남을 수 있으므로 위치를 공유 디스크 그룹으로 옮긴다.","note":"grid 사용자로 실행하면 ocrcheck는 논리 손상 검사를 건너뛴다(\"Logical corruption check bypassed\"). 디스크 그룹으로의 백업은 12c+이다.","tags":["rac","ocr","ocrcheck","ocrconfig","backup","backup","grid"],"untested":true},{"id":"rac-votedisk-query","task":"voting disk와 CSS 타임아웃 확인","category":"RAC 및 Grid","command":"# Voting disks, their state and disk group:\ncrsctl query css votedisk\n# Node eviction thresholds (seconds):\ncrsctl get css misscount\ncrsctl get css disktimeout\ncrsctl get css reboottime\n# Move the voting disks to another ASM disk group (root/grid):\ncrsctl replace votedisk +<diskgroup>","what":"클러스터 멤버십에 사용되는 voting disk(voting file)의 상태와 네트워크/디스크 heartbeat 타임아웃을 보여 준다. replace votedisk는 모든 voting file을 대상 디스크 그룹으로 옮긴다.","when":"스토리지 변경, 디스크 그룹 재설계, 노드 eviction 조사 시. voting disk의 과반수(quorum)에 접근 가능한지 확인하기 위해.","caution":"voting disk의 절반을 초과해 접근하지 못하는 노드는 스스로 재부팅한다. 대상 디스크 그룹에는 충분한 failure group이 필요하다(normal redundancy: 3, high: 5, 필요하면 quorum failgroup 사용). Oracle Support 승인 없이 misscount/disktimeout을 변경하지 않는다.","note":"기본값: misscount 30 s, disktimeout 200 s (Linux).","tags":["rac","voting disk","votedisk","css","misscount","quorum","eviction"],"untested":true},{"id":"rac-cluvfy-checks","task":"Cluster Verification Utility(cluvfy)로 사전/사후 점검 실행","category":"RAC 및 Grid","command":"# Full check after a Grid installation/change:\ncluvfy stage -post crsinst -n all -verbose\n# Before adding a node:\ncluvfy stage -pre nodeadd -n <new_node> -verbose\n# Component checks:\ncluvfy comp nodecon -n all -verbose\ncluvfy comp clocksync -n all -verbose\ncluvfy comp ocr -n all -verbose\ncluvfy comp asm -n all -verbose\n# Report only failed checks:\ncluvfy stage -post crsinst -n all -verbose | grep -iE \"failed|warning\"","what":"모든 노드에서 네트워크 연결, 시간 동기화, OCR, ASM, 사용자 equivalence, OS 사전 요구 사항을 검증하고 문제와 권장 조치를 보고한다.","when":"Grid 설치, 노드 추가, 업그레이드 전후. 원인을 알 수 없는 클러스터 문제의 기본 검증용.","caution":"읽기 전용이지만 ssh로 모든 노드에 접속하므로 grid 사용자에 ssh equivalence가 필요하다. -fixup 스크립트(root)는 검토 없이 실행하지 않는다. OS 파라미터를 변경한다.","note":"더 넓은 모범 사례 점검은 AHF의 orachk/exachk를 사용한다. My Oracle Support에서 최신 cluvfy를 내려받으면 점검 항목이 더 많다.","tags":["rac","cluvfy","check","verification","precheck","grid","orachk"],"untested":true},{"id":"rac-node-eviction-diag","task":"노드 eviction / 예기치 않은 노드 재부팅 원인 진단","category":"RAC 및 Grid","command":"# 1) When, which node, evicted by whom: CRS alert log (on every node)\ngrep -E \"CRS-1601|CRS-1607|CRS-1608|CRS-1609|CRS-1610|CRS-1611|CRS-1612|CRS-8011\" \\\n  $ORACLE_BASE/diag/crs/$(hostname -s)/crs/trace/alert.log\n# 2) CSS details: was the network heartbeat or the disk heartbeat lost\ngrep -iE \"evict|misscount|heartbeat|disk HB|network HB\" \\\n  $ORACLE_BASE/diag/crs/$(hostname -s)/crs/trace/ocssd.trc | tail -100\n# 3) Who triggered the reboot (cssdagent/cssdmonitor):\nls -lt $ORACLE_BASE/diag/crs/$(hostname -s)/crs/trace/ohasd_cssd*.trc\n# 4) OS resources at the time of the event (Cluster Health Monitor):\noclumon dumpnodeview -allnodes -s \"2026-01-15 03:10:00\" -e \"2026-01-15 03:20:00\"\n# 5) Diagnostic collection from all nodes (AHF/TFA):\ntfactl diagcollect -crs -os -last 4h\n# 6) OS side: /var/log/messages, journalctl -k (OOM, NIC, multipath, I/O errors)","what":"노드 eviction 연쇄를 순서대로 추적한다: CRS alert log의 CRS-16xx 메시지, ocssd.trc의 heartbeat 손실, CHM의 OS 지표, TFA로 수집한 전체 노드 로그.","when":"노드가 예기치 않게 재부팅되거나 클러스터에서 축출(evict)되었을 때, 근본 원인 분석(RCA)과 Oracle Support SR용.","caution":"모든 노드에서 이벤트 시점을 포함하는 로그를 수집한다. trace 파일은 순환되므로 증거가 사라질 수 있다. 노드 간 시간대가 같은지 확인한다(CTSS/NTP). 증거를 수집하기 전에 misscount 상향 같은 \"조치\"를 적용하지 않는다.","note":"일반적 원인: private interconnect 장애/패킷 손실, voting disk I/O 지연, CPU/메모리 고갈(OOM, swap), 시간 점프. 경로는 12c+ ADR 구조이며 11g에서는 $GRID_HOME/log/<host>/ 아래에 있다.","tags":["rac","eviction","reboot","ocssd","tfactl","oclumon","rca"],"untested":true},{"id":"rac-gv-cluster-waits","task":"GV$로 인스턴스별 클러스터 전체 wait event(gc 대기) 분석","category":"RAC 및 Grid","command":"-- Cluster-class waits per instance (since startup):\nSELECT inst_id, event, total_waits,\n       ROUND(time_waited_micro/1e6) AS time_waited_s,\n       ROUND(time_waited_micro/NULLIF(total_waits,0)/1000, 2) AS avg_ms\nFROM   gv$system_event\nWHERE  wait_class = 'Cluster'\nORDER  BY time_waited_micro DESC\nFETCH FIRST 15 ROWS ONLY;\n\n-- Sessions waiting right now, by instance and event:\nSELECT inst_id, event, COUNT(*) AS sessions\nFROM   gv$session\nWHERE  wait_class <> 'Idle' AND state = 'WAITING'\nGROUP  BY inst_id, event\nORDER  BY sessions DESC;\n\n-- ASH for the last 15 minutes: which instance is waiting on gc\nSELECT inst_id, event, COUNT(*) AS samples\nFROM   gv$active_session_history\nWHERE  sample_time > SYSTIMESTAMP - INTERVAL '15' MINUTE\n  AND  wait_class = 'Cluster'\nGROUP  BY inst_id, event\nORDER  BY samples DESC\nFETCH FIRST 10 ROWS ONLY;","what":"GV$ 뷰로 모든 인스턴스의 Cluster 클래스 대기(gc cr/current block 2-way/3-way, gc buffer busy, gc cr request)와 평균 지연을 하나의 쿼리로 보여 준다.","when":"사용자가 RAC에서 느려짐을 보고할 때, 문제가 interconnect/global cache에서 오는지와 어느 인스턴스에 집중되는지 판별하기 위해.","caution":"gv$active_session_history는 Diagnostics Pack 라이선스가 필요하다. avg_ms가 몇 ms를 넘으면 먼저 interconnect(MTU, 패킷 손실, netstat -s)와 LMS CPU를 점검한다. gc buffer busy는 대개 애플리케이션 hot block(시퀀스, right-hand 인덱스)을 가리킨다.","note":"단일 인스턴스에서도 실행된다(inst_id = 1 반환).","tags":["rac","gv$","gc","wait","wait event","interconnect","global cache"]},{"id":"asm-asmcmd-lsdg-du","task":"asmcmd로 디스크 그룹, 디스크, 디렉터리 사용량 조회(lsdg / lsdsk / du)","category":"ASM","command":"# As the grid user, with ORACLE_SID=+ASM1 (or +ASM):\nasmcmd lsdg\n# Disks in the disk group: -k sizes, -p state (header/mount/mode):\nasmcmd lsdsk -k -G <diskgroup>\nasmcmd lsdsk -p -G <diskgroup>\n# I/O error and timing statistics:\nasmcmd lsdsk --statistics -G <diskgroup>\n# Disks that can be added (CANDIDATE/PROVISIONED/FORMER):\nasmcmd lsdsk --candidate -p\n# Space used by a database directory (MB):\nasmcmd du +<diskgroup>/<db_unique_name>\nasmcmd ls -l -s +<diskgroup>/<db_unique_name>/DATAFILE","what":"명령줄에서 디스크 그룹의 redundancy, 전체/여유 공간, Usable_file_MB, 디스크 상태, 데이터베이스가 ASM에서 차지하는 공간을 보여 준다.","when":"용량 점검, 디스크 추가 전 후보 디스크 식별, \"disk group full\" 알림 시 공간을 소비하는 데이터베이스 확인용.","caution":"읽기 전용. lsdg의 Usable_file_MB가 음수이면 디스크/failgroup을 잃은 후 디스크 그룹이 redundancy를 복구할 수 없다. 즉시 공간을 확보하거나 디스크를 추가한다. du는 두 값을 반환한다: Used_MB와 Mirror_used_MB(미러링 포함 사용량).","note":"SQL 버전은 asm-diskgroup-space 및 asm-disk-health 레시피를 참고한다.","tags":["asm","asmcmd","lsdg","lsdsk","du","space","disk group"],"untested":true},{"id":"asm-disk-health","task":"SQL로 ASM 디스크 상태, 오류 카운터, 후보 디스크 확인","category":"ASM","command":"-- On the ASM instance (or from a DB instance; there only the disk groups in use are visible):\nSELECT dg.name AS diskgroup, d.disk_number, d.name AS disk_name, d.path,\n       d.header_status, d.mount_status, d.mode_status, d.state,\n       d.failgroup, d.total_mb, d.free_mb, d.read_errs, d.write_errs\nFROM   v$asm_disk d\nLEFT   JOIN v$asm_diskgroup dg ON dg.group_number = d.group_number\nORDER  BY dg.name, d.disk_number;\n\n-- Disk group summary: AU size, offline disks, voting files:\nSELECT name, type, allocation_unit_size/1048576 AS au_mb,\n       compatibility, database_compatibility, offline_disks, voting_files\nFROM   v$asm_diskgroup;\n\n-- Databases using this ASM:\nSELECT instance_name, db_name, status, software_version, compatible_version\nFROM   v$asm_client;","what":"각 디스크의 header_status(MEMBER, CANDIDATE, PROVISIONED, FORMER), mount/mode 상태, failgroup, 읽기/쓰기 오류 카운터, 디스크 그룹별 offline 디스크 수, ASM 클라이언트를 나열한다.","when":"디스크 추가 전 새 LUN이 CANDIDATE/PROVISIONED로 보이는지 확인할 때, 디스크 장애 시 어느 디스크가 OFFLINE인지와 I/O 오류를 확인할 때.","caution":"읽기 전용. v$asm_disk는 호출할 때마다 디스크 discovery를 수행하므로 디스크가 많은 시스템에서 느리다. 빈번한 모니터링에는 v$asm_disk_stat을 사용한다(discovery 없음, 새 디스크는 보이지 않음). header_status가 MEMBER인 디스크를 FORCE로 다른 디스크 그룹에 추가하지 않는다. 기존 데이터가 파괴된다.","note":"ASM이 없으면 행을 반환하지 않는다. 의미 있는 출력에는 ASM 인스턴스가 필요하다.","tags":["asm","v$asm_disk","disk","header_status","error","candidate","failgroup"]},{"id":"asm-diskgroup-attributes","task":"ASM 디스크 그룹 속성 조회 및 변경(compatible, disk_repair_time)","category":"ASM","command":"-- Current attributes:\nSELECT dg.name AS diskgroup, a.name, a.value\nFROM   v$asm_attribute a\nJOIN   v$asm_diskgroup dg ON dg.group_number = a.group_number\nWHERE  a.name IN ('compatible.asm','compatible.rdbms','au_size','disk_repair_time',\n                  'failgroup_repair_time','content.type','sector_size')\nORDER  BY dg.name, a.name;\n\n-- Change (ASM instance, as SYSASM):\nALTER DISKGROUP <diskgroup> SET ATTRIBUTE 'disk_repair_time' = '8h';\nALTER DISKGROUP <diskgroup> SET ATTRIBUTE 'compatible.asm'   = '19.0.0.0.0';\nALTER DISKGROUP <diskgroup> SET ATTRIBUTE 'compatible.rdbms' = '19.0.0.0.0';\n\n-- asmcmd equivalent:\n-- asmcmd lsattr -l -G <diskgroup>\n-- asmcmd setattr -G <diskgroup> disk_repair_time '8h'","what":"디스크 그룹 호환성 수준(compatible.asm/rdbms), AU 크기, 일시적으로 장애가 난 디스크를 drop하기 전까지 유지하는 시간(disk_repair_time)을 보여 주고 변경한다.","when":"Grid 업그레이드 후 새 ASM 기능(flex 디스크 그룹, 더 큰 파일 한도)을 활성화하거나, 계획된 스토리지 유지보수 중 disk_repair_time을 늘릴 때.","caution":"compatible.asm과 compatible.rdbms는 한 번 올리면 낮출 수 없다. compatible.rdbms가 이 디스크 그룹을 사용하는 가장 오래된 데이터베이스의 COMPATIBLE 설정보다 높으면 해당 데이터베이스는 마운트할 수 없다. compatible.asm을 먼저 올린 후 compatible.rdbms를 올린다. au_size는 디스크 그룹 생성 시에만 설정할 수 있다.","note":"ASM이 없으면 쿼리는 행을 반환하지 않는다. disk_repair_time 기본값은 12h이다.","tags":["asm","attribute","compatible","disk_repair_time","disk group","au_size","v$asm_attribute"],"untested":true},{"id":"dg-show-configuration","task":"Data Guard broker 구성 및 데이터베이스 상태 조회(DGMGRL)","category":"Data Guard","command":"# From the shell (connect with a password; also required for switchover/failover):\ndgmgrl sys@<primary_db_unique_name>\nSHOW CONFIGURATION VERBOSE;\nSHOW DATABASE VERBOSE '<standby_db_unique_name>';\n# Monitorable properties:\nSHOW DATABASE '<primary_db_unique_name>' 'LogXptStatus';\nSHOW DATABASE '<standby_db_unique_name>' 'RecvQEntries';\nSHOW DATABASE '<primary_db_unique_name>' 'InconsistentLogXptProps';\nSHOW DATABASE '<standby_db_unique_name>' 'StatusReport';","what":"broker 구성 전체 상태(SUCCESS/WARNING/ERROR), 보호 모드, 각 데이터베이스의 역할, transport/apply lag, broker와 spfile 간 불일치를 보여 준다.","when":"모든 Data Guard 작업(스위치오버, 페일오버, 파라미터 변경) 전후, 일일 상태 점검 시.","caution":"읽기 전용. broker가 활성화된 동안 log_archive_dest_n이나 log_archive_config 같은 Data Guard 파라미터를 SQL로 변경하지 않는다. broker가 자신의 값으로 덮어쓴다. 변경은 EDIT DATABASE ... SET PROPERTY로 하고, 19c+에서는 초기화 파라미터에 EDIT DATABASE ... SET PARAMETER를 사용한다.","note":"StatusReport는 데이터베이스별 ORA- 오류를 나열하며 WARNING 상태의 원인을 찾는 가장 빠른 방법이다. InconsistentProperties는 19c에서 deprecated되어 항상 값을 반환하지 않는다. redo transport 불일치에는 InconsistentLogXptProps를 사용한다.","tags":["data guard","dgmgrl","broker","configuration","standby","status","lag"],"untested":true},{"id":"dg-readiness-sql","task":"SQL로 primary/standby 역할 및 스위치오버 준비 상태 확인","category":"Data Guard","command":"SELECT name, db_unique_name, database_role, open_mode, protection_mode,\n       switchover_status, force_logging, flashback_on, dataguard_broker\nFROM   v$database;\n\n-- State of redo destinations and gaps (on the primary):\nSELECT dest_id, dest_name, status, type, database_mode,\n       recovery_mode, gap_status, error\nFROM   v$archive_dest_status\nWHERE  status <> 'INACTIVE';\n\n-- Missing archivelog range on the standby:\nSELECT thread#, low_sequence#, high_sequence# FROM v$archive_gap;","what":"데이터베이스 역할, open mode, 보호 모드, switchover_status(TO STANDBY / TO PRIMARY / SESSIONS ACTIVE / NOT ALLOWED), FORCE LOGGING 및 flashback 상태, redo destination의 오류와 gap을 보여 준다.","when":"스위치오버/페일오버 전 사전 점검, 새 standby 구축 후, broker를 사용하지 않는 환경의 기본 모니터링용.","caution":"읽기 전용. FORCE_LOGGING=NO이면 NOLOGGING 작업이 standby에 손상 블록을 남긴다. Data Guard primary에서는 ALTER DATABASE FORCE LOGGING을 반드시 활성화한다. FLASHBACK_ON=NO이면 페일오버 후 이전 primary를 reinstate할 수 없어 재구축해야 한다.","note":"primary에서도 실행된다. v$archive_gap은 physical standby에서만 의미가 있다.","tags":["data guard","v$database","switchover_status","force logging","gap","role","standby"]},{"id":"dg-lag-apply-rate","task":"standby에서 transport/apply lag, apply 속도, Data Guard 프로세스 측정","category":"Data Guard","command":"-- Run on the standby:\nSELECT name, value, unit, time_computed, datum_time\nFROM   v$dataguard_stats\nWHERE  name IN ('transport lag','apply lag','apply finish time','estimated startup time');\n\n-- Redo apply rate (KB/s):\nSELECT item, units, sofar, total, TO_CHAR(timestamp,'YYYY-MM-DD HH24:MI:SS') AS ts\nFROM   v$recovery_progress\nWHERE  item IN ('Active Apply Rate','Average Apply Rate','Maximum Apply Rate','Last Applied Redo')\nORDER  BY start_time DESC, item;\n\n-- Transport and apply processes (12.2+; replaces v$managed_standby):\nSELECT name, role, action, thread#, sequence#, block#, delay_mins\nFROM   v$dataguard_process\nORDER  BY role;","what":"standby가 primary보다 얼마나 뒤처졌는지(transport lag = 아직 수신하지 못한 redo, apply lag = 아직 적용하지 못한 redo), 밀린 redo가 언제 적용될지, apply 속도, 어떤 프로세스가 어느 sequence를 처리 중인지 보여 준다.","when":"RPO/RTO 모니터링, lag 알림 시 문제가 네트워크(transport)인지 standby I/O/CPU(apply)인지 판별하기 위해.","caution":"읽기 전용. datum_time이 오래되었으면(time_computed보다 크게 뒤처짐) standby가 primary로부터 데이터를 받지 못하는 것이며 lag 값은 신뢰할 수 없다. DelayMins가 설정되어 있는지 확인한다. 의도적인 지연은 apply lag처럼 보인다.","since":"12c","note":"v$dataguard_process는 12.2+이다. standby가 없으면 v$dataguard_stats는 행을 반환하지 않는다.","tags":["data guard","lag","lag","apply","transport","v$dataguard_stats","rpo"]},{"id":"dg-validate-database","task":"스위치오버/페일오버 전 broker로 standby 검증(VALIDATE DATABASE)","category":"Data Guard","command":"VALIDATE DATABASE '<standby_db_unique_name>';\nVALIDATE DATABASE VERBOSE '<standby_db_unique_name>';\n# spfile parameter differences between the two sides:\nVALIDATE DATABASE '<standby_db_unique_name>' SPFILE;\n# Network and static listener registrations (18c+):\nVALIDATE NETWORK CONFIGURATION FOR ALL;\nVALIDATE STATIC CONNECT IDENTIFIER FOR ALL;","what":"standby가 역할 전환 준비가 되었는지(\"Ready for Switchover / Failover: Yes\"), standby redo log 개수/크기, flashback 및 apply 상태, spfile 차이, broker가 인스턴스를 재시작하는 데 필요한 정적 서비스를 점검한다.","when":"모든 계획된 스위치오버 전 필수 단계로, 새 standby 구축 후 및 네트워크/리스너 변경 후.","caution":"읽기 전용. \"Ready for Switchover: No\" 또는 standby redo log 경고가 보이면 스위치오버를 진행하지 않는다. static connect identifier 오류는 스위치오버 중 broker가 인스턴스를 시작할 수 없다는 뜻이며 수동 개입이 필요해진다.","since":"12c","note":"VALIDATE DATABASE는 12.1+, SPFILE 옵션은 12.2+, NETWORK CONFIGURATION과 STATIC CONNECT IDENTIFIER는 18c+이다.","tags":["data guard","dgmgrl","validate","validate","switchover","standby redo log","broker"],"untested":true},{"id":"dg-switchover","task":"계획된 스위치오버 수행(DGMGRL, 데이터 손실 없는 역할 전환)","category":"Data Guard","command":"# Connect with a password (with OS authentication \"/\" the broker cannot restart the remote instance):\ndgmgrl sys@<primary_db_unique_name>\n# 1) Precheck:\nSHOW CONFIGURATION;\nVALIDATE DATABASE '<standby_db_unique_name>';\n# 2) Role change:\nSWITCHOVER TO '<standby_db_unique_name>';\n# 3) Verify: new primary and new standby\nSHOW CONFIGURATION;\nSHOW DATABASE '<primary_db_unique_name>';","what":"primary와 standby의 역할을 제어된 방식으로 맞바꾼다. primary의 모든 redo가 standby로 전송·적용되고, 이전 primary는 standby로 재시작되며 redo apply가 자동으로 시작된다.","when":"primary 사이트의 계획된 유지보수(OS, 하드웨어, rolling 업그레이드), DR 훈련, 데이터 센터 이전 시.","caution":"primary에 짧은 서비스 중단이 발생한다. 애플리케이션 연결은 -role PRIMARY로 정의된 서비스와 FAN/TAF/AC를 통해 새 primary로 라우팅되어야 한다. 장시간 트랜잭션과 큰 apply lag는 스위치오버를 길게 만든다. 먼저 lag를 0에 가깝게 줄인다. RAC에서는 broker가 인스턴스를 직접 관리하므로 srvctl로 개입하지 않는다.","since":"12c","note":"12.1+에서는 broker가 한 명령으로 모든 단계를 수행한다. broker 없이 11g에서는 SQL로 ALTER DATABASE COMMIT TO SWITCHOVER를 사용했다(12c+: ALTER DATABASE SWITCHOVER TO <db> VERIFY).","tags":["data guard","switchover","dgmgrl","role change","planned","standby","primary"],"untested":true},{"id":"dg-failover","task":"primary 손실 후 페일오버 수행(DGMGRL)","category":"Data Guard","command":"# Connect to the standby (the primary is unreachable):\ndgmgrl sys@<standby_db_unique_name>\n# 1) Assess the data that will be lost (Transport Lag / Apply Lag):\nSHOW DATABASE '<standby_db_unique_name>';\n# 2) Complete failover: all received redo is applied, then the role changes (recommended)\nFAILOVER TO '<standby_db_unique_name>';\n# 3) Emergency: immediately, without applying redo (more data loss)\n# FAILOVER TO '<standby_db_unique_name>' IMMEDIATE;\nSHOW CONFIGURATION;","what":"primary를 사용할 수 없을 때 standby를 primary 역할로 승격한다. complete 페일오버는 수신한 모든 redo를 먼저 적용하고, IMMEDIATE는 apply를 건너뛴다.","when":"primary 사이트, 스토리지 또는 데이터베이스를 복구 불가능하게 잃었고 RTO상 스위치오버를 기다릴 수 없을 때.","caution":"이 역할 전환은 되돌릴 수 없다. 먼저 primary가 정말로 다운되었는지 확인한다. 이전 primary가 열린 채로 애플리케이션이 계속 쓰면 split-brain과 데이터 분기가 발생한다(이전 primary를 종료/격리한다). MaxPerformance 모드에서는 transport lag만큼 데이터를 잃는다. 이후 새 primary 기준으로 모든 standby와 백업 전략을 재검토한다.","note":"이전 primary 복구는 dg-reinstate-database 레시피를 참고한다(flashback 필요).","tags":["data guard","failover","dgmgrl","disaster","disaster recovery","data loss","split-brain"],"untested":true},{"id":"dg-reinstate-database","task":"페일오버 후 이전 primary를 standby로 복구(REINSTATE)","category":"Data Guard","command":"# 1) Bring the old primary to MOUNT (sqlplus / as sysdba):\n#    SQL> STARTUP MOUNT;\n#    (RAC: srvctl start database -db <old_primary_db_unique_name> -startoption mount)\n# 2) From DGMGRL connected to the new primary:\ndgmgrl sys@<new_primary_db_unique_name>\nREINSTATE DATABASE '<old_primary_db_unique_name>';\n# 3) Verify:\nSHOW CONFIGURATION;\nSHOW DATABASE '<old_primary_db_unique_name>';","what":"flashback database로 이전 primary를 페일오버 SCN 이전 시점으로 되돌리고 physical standby로 변환한 후 새 primary로부터 redo 수신을 시작한다. standby를 처음부터 재구축할 필요가 없다.","when":"페일오버 후 이전 primary 서버/스토리지에 다시 접근할 수 있게 되었을 때, FSFO 자동 페일오버 후(대개 observer가 스스로 reinstate한다).","caution":"이전 primary에서 페일오버 이전에 flashback이 활성화되어 있어야 하고, flashback 로그가 페일오버 SCN을 포함해야 한다(db_flashback_retention_target, FRA 크기). 그렇지 않으면 reinstate가 실패하여 RMAN duplicate로 standby를 재구축해야 한다. 페일오버 후 이전 primary에 기록된 데이터는 영구적으로 손실된다.","note":"사전 점검: dg-readiness-sql 레시피의 flashback_on 컬럼.","tags":["data guard","reinstate","flashback","failover","old primary","standby","dgmgrl"],"untested":true},{"id":"dg-apply-transport-state","task":"broker로 redo apply 또는 redo transport 중지 / 시작, 지연 적용 구성","category":"Data Guard","command":"# Redo apply on the standby:\nEDIT DATABASE '<standby_db_unique_name>' SET STATE='APPLY-OFF';\nEDIT DATABASE '<standby_db_unique_name>' SET STATE='APPLY-ON';\n# Redo transport on the primary (to all standbys):\nEDIT DATABASE '<primary_db_unique_name>' SET STATE='TRANSPORT-OFF';\nEDIT DATABASE '<primary_db_unique_name>' SET STATE='TRANSPORT-ON';\n# Delayed apply (minutes; protection against logical errors):\nEDIT DATABASE '<standby_db_unique_name>' SET PROPERTY DelayMins=240;\nSHOW DATABASE '<standby_db_unique_name>' DelayMins;","what":"broker를 통해 standby의 redo apply(MRP) 또는 primary의 redo 전송을 영구적으로 중지/시작한다. DelayMins는 redo를 지정한 시간 동안 보류한 후 적용한다.","when":"standby 유지보수나 백업, 계획된 네트워크 중단, primary의 대량 적재 중 transport 제어, 실수로 인한 삭제에 대비한 지연 standby 구축 시.","caution":"broker가 활성화된 동안 SQL로 ALTER DATABASE RECOVER MANAGED STANDBY DATABASE CANCEL을 사용하지 않는다. broker가 상태를 되돌린다. APPLY-OFF 동안 apply lag와 페일오버 시간(RTO)이 늘어나고, TRANSPORT-OFF 동안 primary에서 생성된 데이터는 보호되지 않는다(RPO). FSFO가 활성화되어 있으면 이 작업이 페일오버를 막을 수 있다.","note":"DelayMins > 0이면 redo는 real-time apply가 아니라 지연 후 아카이브 로그에서 적용된다. 지연 중에도 standby는 redo를 계속 수신한다.","tags":["data guard","apply","transport","mrp","delaymins","stop","dgmgrl"],"untested":true},{"id":"dg-fsfo-observer","task":"Fast-Start Failover(FSFO) 활성화 및 observer 시작","category":"Data Guard","command":"# 1) Targets and threshold (seconds):\nEDIT DATABASE '<primary_db_unique_name>' SET PROPERTY FastStartFailoverTarget='<standby_db_unique_name>';\nEDIT DATABASE '<standby_db_unique_name>' SET PROPERTY FastStartFailoverTarget='<primary_db_unique_name>';\nEDIT CONFIGURATION SET PROPERTY FastStartFailoverThreshold=30;\n# 2) MaxAvailability for zero data loss (SYNC/FASTSYNC transport):\nEDIT DATABASE '<primary_db_unique_name>' SET PROPERTY LogXptMode='FASTSYNC';\nEDIT DATABASE '<standby_db_unique_name>' SET PROPERTY LogXptMode='FASTSYNC';\nEDIT CONFIGURATION SET PROTECTION MODE AS MAXAVAILABILITY;\n# Acceptable loss in MaxPerformance: EDIT CONFIGURATION SET PROPERTY FastStartFailoverLagLimit=30;\nENABLE FAST_START FAILOVER;\n# 3) Observer: on a third server, separate from primary and standby (passwordless connection via wallet)\ndgmgrl /@<primary_tns> \"START OBSERVER obs1 IN BACKGROUND FILE IS '/u01/app/oracle/dg/fsfo_obs1.dat' LOGFILE IS '/u01/app/oracle/dg/obs1.log' CONNECT IDENTIFIER IS <primary_tns>\"\n# 4) Verify:\nSHOW FAST_START FAILOVER;\nSHOW OBSERVER;","what":"primary에 접근할 수 없으면 observer가 임계값 경과 후 standby로 자동 페일오버하고, 이전 primary가 복귀하면 자동으로 reinstate하게 한다.","when":"사람의 개입을 기다리지 않고 RTO 목표가 분 단위인 중요 시스템에서.","caution":"observer는 primary나 standby와 같은 서버/데이터 센터에서 실행하면 안 된다. 그렇지 않으면 사이트 손실 시 FSFO가 실패하거나 네트워크 분할 중 잘못된 판단을 할 수 있다. 두 데이터베이스 모두 flashback이 활성화되어 있어야 한다. 임계값이 너무 낮으면 짧은 네트워크 끊김이 불필요한 페일오버로 이어진다. 계획된 유지보수 시에는 primary 종료 전에 DISABLE FAST_START FAILOVER를 하거나 observer를 고려한다.","since":"12c","note":"FASTSYNC는 12.2+, IN BACKGROUND와 다중 observer(SHOW OBSERVER)는 12.2+이다. IN BACKGROUND는 Oracle wallet(mkstore)에 자격 증명이 필요하다.","tags":["data guard","fsfo","fast-start failover","observer","automatic failover","maxavailability","dgmgrl"],"untested":true},{"id":"dg-snapshot-standby","task":"physical standby를 snapshot standby로 변환 및 복원(테스트 환경)","category":"Data Guard","command":"# Make the standby read-write (redo is still received, apply stops):\nCONVERT DATABASE '<standby_db_unique_name>' TO SNAPSHOT STANDBY;\nSHOW DATABASE '<standby_db_unique_name>';\n# ... testing / application validation ...\n# Convert back: all test changes are discarded, accumulated redo is applied\nCONVERT DATABASE '<standby_db_unique_name>' TO PHYSICAL STANDBY;\nSHOW CONFIGURATION;","what":"standby에 자동 guaranteed restore point를 만들고 read-write로 연다. 되돌릴 때는 그 지점으로 flashback한 후 primary에서 누적된 redo를 적용한다.","when":"별도 사본을 만들지 않고 운영 데이터로 업그레이드 리허설, 애플리케이션 릴리스 테스트, Real Application Testing(DB Replay)을 수행할 때.","caution":"snapshot standby 상태에서는 redo가 적용되지 않는다. RPO는 유지되지만 페일오버 시간(RTO)은 누적 redo만큼 늘어나며 FSFO 대상이 될 수 없다. flashback 로그가 FRA를 채울 수 있으므로 FRA 공간을 모니터링한다. 스냅샷에서 변경한 내용은 되돌릴 때 영구적으로 폐기된다.","note":"SQL 버전: ALTER DATABASE CONVERT TO SNAPSHOT STANDBY / CONVERT TO PHYSICAL STANDBY(broker 없이).","tags":["data guard","snapshot standby","test","restore point","flashback","convert","dgmgrl"],"untested":true},{"id":"rman-catalog-create","task":"RMAN recovery catalog 생성 및 데이터베이스 등록","category":"RMAN","command":"-- 1) In the catalog database (separate DB/PDB), with sqlplus:\nCREATE TABLESPACE rcat_ts DATAFILE '/u02/oradata/RCAT/rcat01.dbf' SIZE 1G AUTOEXTEND ON NEXT 256M;\nCREATE USER rcat IDENTIFIED BY \"<password>\"\n  DEFAULT TABLESPACE rcat_ts QUOTA UNLIMITED ON rcat_ts;\nGRANT RECOVERY_CATALOG_OWNER TO rcat;\n# 2) From the shell: create the catalog schema\nrman catalog rcat@<rcat_tns>\nCREATE CATALOG;\n# 3) Register the target database and verify:\nrman target / catalog rcat@<rcat_tns>\nREGISTER DATABASE;\nREPORT SCHEMA;\n# Resync controlfile records into the catalog (done automatically after backups):\nRESYNC CATALOG;","what":"controlfile 보존 기간(control_file_record_keep_time)과 무관하게 백업 메타데이터를 중앙 스키마에 저장하는 recovery catalog를 만들고 대상 데이터베이스를 등록한다.","when":"여러 데이터베이스의 백업 이력을 한곳에서 관리하고, Data Guard에서 primary/standby 백업을 상호 교환해 사용하며, 장기(KEEP) 백업과 stored script를 사용하기 위해.","caution":"catalog 데이터베이스는 보호 대상 데이터베이스와 다른 서버에 있어야 하며 그 자체도 백업해야 한다. catalog 스키마 버전은 사용 중인 가장 최신 RMAN 클라이언트보다 낮을 수 없다. 더 새로운 클라이언트는 UPGRADE CATALOG가 필요하다. Data Guard에서는 primary만 등록하고, standby는 CONFIGURE DB_UNIQUE_NAME ... CONNECT IDENTIFIER로 DB_UNIQUE_NAME 기준으로 정의한다.","note":"catalog 스키마는 별도 데이터베이스에 둔다. 대상과 같은 곳에 있으면 재해 시 catalog도 함께 잃는다.","tags":["rman","catalog","catalog","recovery catalog","register","backup","backup"],"untested":true},{"id":"rman-duplicate-standby","task":"RMAN DUPLICATE로 활성 데이터베이스에서 physical standby 생성","category":"RMAN","command":"# Prerequisites: standby instance in NOMOUNT with a pfile containing only db_name,\n# primary password file copied, static listener entries and tnsnames on both sides.\nrman target sys@<primary_tns> auxiliary sys@<standby_tns>\nRUN {\n  ALLOCATE CHANNEL p1 DEVICE TYPE DISK;\n  ALLOCATE CHANNEL p2 DEVICE TYPE DISK;\n  ALLOCATE AUXILIARY CHANNEL s1 DEVICE TYPE DISK;\n  ALLOCATE AUXILIARY CHANNEL s2 DEVICE TYPE DISK;\n  DUPLICATE TARGET DATABASE FOR STANDBY FROM ACTIVE DATABASE\n    USING COMPRESSED BACKUPSET SECTION SIZE 32G\n    DORECOVER\n    SPFILE\n      SET db_unique_name='<standby_db_unique_name>'\n      SET control_files='+DATA','+FRA'\n      SET db_create_file_dest='+DATA'\n      SET db_recovery_file_dest='+FRA'\n      SET fal_server='<primary_tns>'\n      SET log_archive_dest_2=''\n      SET dg_broker_start='FALSE'\n    NOFILENAMECHECK;\n}","what":"primary 데이터 파일을 네트워크를 통해 standby 서버로 복사하고(중간 백업 파일 없음), standby에 맞게 조정된 spfile을 만들며, DORECOVER로 사용 가능한 아카이브 로그를 적용한다.","when":"새 Data Guard standby 구축 시 또는 reinstate할 수 없는 standby 재구축 시.","caution":"primary에 네트워크 및 I/O 부하를 발생시킨다. 피크 시간에 실행하지 않거나 채널 수를 제한한다. NOFILENAMECHECK는 standby가 별도 서버에 있을 때만 안전하다. 같은 서버에서는 primary 파일을 덮어쓸 수 있다. 전후에 standby redo log(primary online redo와 같은 크기, thread당 한 그룹 추가)를 추가한 후 broker를 구성한다.","since":"12c","note":"USING [COMPRESSED] BACKUPSET과 SECTION SIZE를 사용하는 active duplicate는 12c+이다. 11g는 image copy를 사용한다. ASM 대신 파일 시스템을 사용하면 db_file_name_convert/log_file_name_convert를 설정한다.","tags":["rman","duplicate","standby","active database","data guard","setup","clone"],"untested":true},{"id":"rman-crosscheck-cleanup","task":"RMAN 백업 레코드 검증 및 정리(CROSSCHECK, EXPIRED, OBSOLETE)","category":"RMAN","command":"SHOW RETENTION POLICY;\n# 1) Compare records with the physical files (missing ones become EXPIRED):\nCROSSCHECK BACKUP;\nCROSSCHECK COPY;\nCROSSCHECK ARCHIVELOG ALL;\n# 2) Review before deleting:\nLIST EXPIRED BACKUP;\nLIST EXPIRED ARCHIVELOG ALL;\nREPORT OBSOLETE;\n# 3) Clean up:\nDELETE NOPROMPT EXPIRED BACKUP;\nDELETE NOPROMPT EXPIRED COPY;\nDELETE NOPROMPT EXPIRED ARCHIVELOG ALL;\nDELETE NOPROMPT OBSOLETE;","what":"CROSSCHECK는 RMAN 레코드의 백업이 디스크/테이프에 아직 존재하는지 확인한다. DELETE EXPIRED는 누락된 파일의 레코드를 제거하고, DELETE OBSOLETE는 보존 정책상 더 이상 필요 없는 백업을 레코드와 디스크 모두에서 삭제한다.","when":"OS 수준에서 백업 파일이 삭제/이동되었을 때, FRA가 가득 찼을 때, 정기 백업 유지보수 작업의 일부로.","caution":"DELETE OBSOLETE는 파일을 물리적으로 삭제한다. 먼저 REPORT OBSOLETE로 목록을 확인하고 보존 정책(RECOVERY WINDOW / REDUNDANCY)이 의도한 대로인지 확인한다. 테이프/미디어 관리자에 일시적으로 접근할 수 없을 때 CROSSCHECK를 실행하면 유효한 백업이 EXPIRED로 표시된다. 그 시점에는 DELETE EXPIRED를 실행하지 않는다.","note":"Data Guard에서는 아카이브 로그 삭제에 CONFIGURE ARCHIVELOG DELETION POLICY TO APPLIED ON ALL STANDBY를 사용한다.","tags":["rman","crosscheck","expired","obsolete","cleanup","retention","backup"],"untested":true},{"id":"rman-validate","task":"데이터베이스와 백업의 손상 검증(VALIDATE)","category":"RMAN","command":"# Physical + logical block check (without producing a backup):\nVALIDATE CHECK LOGICAL DATABASE;\n# Scan large datafiles in parallel (11g+, needs multiple channels):\nVALIDATE CHECK LOGICAL DATABASE SECTION SIZE 16G;\n# A single datafile / tablespace:\nVALIDATE DATAFILE 7;\nVALIDATE TABLESPACE users;\n# Read as if taking a backup and check (including archivelogs):\nBACKUP VALIDATE CHECK LOGICAL DATABASE ARCHIVELOG ALL;\n# Readability of a specific backup set:\nLIST BACKUP SUMMARY;\nVALIDATE BACKUPSET <bs_key>;\n-- Corrupt blocks found and the affected segment (SQL):\nSELECT file#, block#, blocks, corruption_change#, corruption_type\nFROM   v$database_block_corruption ORDER BY file#, block#;\nSELECT DISTINCT e.owner, e.segment_name, e.segment_type\nFROM   dba_extents e\nJOIN   v$database_block_corruption c\n  ON   e.file_id = c.file#\n AND   c.block# BETWEEN e.block_id AND e.block_id + e.blocks - 1;","what":"데이터 파일과 backup set을 읽어 물리적 블록 손상과 (CHECK LOGICAL 사용 시) 논리적 블록 손상을 탐지하고 결과를 v$database_block_corruption에 기록한다. SQL 쿼리는 손상 블록이 어느 테이블/인덱스에 속하는지 보여 준다.","when":"ORA-01578/ORA-08103 같은 손상 오류 후, 스토리지 문제 후, 정기(주간) 무결성 점검으로. 백업이 실제로 복원 가능함을 증명하기 위해.","caution":"전체 읽기를 수행하므로 대형 데이터베이스에서는 I/O 부하가 크다. 피크 시간 외에 실행한다. dba_extents 쿼리는 대형 데이터베이스에서 느릴 수 있다. 손상 블록이 임시(TEMP) 공간이나 여유 공간에 있으면 객체가 매칭되지 않는다.","since":"11g","note":"손상이 없으면 쿼리는 행을 반환하지 않는다. SECTION SIZE를 사용하는 VALIDATE는 11g+이다.","tags":["rman","validate","corruption","corruption","check logical","block","ora-01578"],"untested":true},{"id":"rman-restore-preview","task":"복원 시 사용될 백업 확인 및 검증(PREVIEW / VALIDATE)","category":"RMAN","command":"# Backups and archivelogs a restore needs (without reading the files):\nRESTORE DATABASE PREVIEW SUMMARY;\n# For a return to a point in time:\nRESTORE DATABASE UNTIL TIME \"TO_DATE('2026-01-15 03:00:00','YYYY-MM-DD HH24:MI:SS')\" PREVIEW;\n# Validate the backups by actually reading them (without restoring):\nRESTORE DATABASE VALIDATE;\nRESTORE ARCHIVELOG FROM TIME 'SYSDATE-1' VALIDATE;\nRESTORE CONTROLFILE VALIDATE;\nRESTORE SPFILE VALIDATE;","what":"PREVIEW는 catalog/controlfile을 기준으로 복원 시 사용될 backup set/copy와 아카이브 로그 범위를 보고한다. VALIDATE는 해당 백업을 실제로 읽어 손상이나 누락 파일이 없는지 검증한다.","when":"실제 복원 전 계획 수립, 백업 전략이 RPO 목표를 충족함을 증명, 주기적인 복원 테스트용.","caution":"PREVIEW는 레코드만 본다. 파일이 삭제되었다면 오해를 일으키므로 먼저 CROSSCHECK를 실행하거나 RESTORE ... VALIDATE를 사용한다. VALIDATE는 백업 전체를 읽으므로 테이프/네트워크에서는 오래 걸릴 수 있다. 날짜는 세션 NLS 설정이 아니라 TO_DATE 마스크에 의존하게 한다.","note":"PREVIEW RECALL은 테이프에서 recall해야 하는 백업도 요청할 수 있다(미디어 관리자 지원 필요).","tags":["rman","restore","preview","validate","restore test","backup","rpo"],"untested":true},{"id":"rman-block-media-recovery","task":"손상 블록 온라인 복구(block media recovery, RECOVER CORRUPTION LIST)","category":"RMAN","command":"# 1) Detect corrupt blocks (populates v$database_block_corruption):\nVALIDATE CHECK LOGICAL DATABASE;\n# 2) Repair all blocks in the list (the datafile stays online):\nRECOVER CORRUPTION LIST;\n# or the file/block from the error message (ORA-01578: file# 7, block# 233):\nRECOVER DATAFILE 7 BLOCK 233;\nRECOVER DATAFILE 7 BLOCK 233 DATAFILE 9 BLOCK 1001;\n# 3) Verify:\nVALIDATE DATAFILE 7;\nSELECT * FROM v$database_block_corruption;","what":"손상된 블록만 백업(또는 flashback 로그 / Active Data Guard standby)에서 복원하고 redo로 roll forward한다. 데이터 파일과 테이블스페이스는 온라인 상태로 유지되며, 애플리케이션은 해당 블록에 접근할 때만 오류를 받는다.","when":"ORA-01578 / ORA-01110과 함께 국소적 블록 손상이 나타날 때, 데이터 파일 전체 복원이 불필요한 다운타임을 유발할 때.","caution":"해당 블록을 포함한 정상 백업과 그 이후 현재까지의 모든 아카이브 로그가 필요하다. NOLOGGING 작업으로 인한 손상(ORA-26040)은 이 방법으로 복구할 수 없으며 객체를 재생성해야 한다. Enterprise Edition 기능이다. 손상의 원인(스토리지, HBA, 펌웨어)을 찾지 않으면 재발한다.","note":"Active Data Guard에서는 11.2+ 자동 block media recovery가 standby로부터 손상 블록을 자동으로 복구한다.","tags":["rman","block recovery","corrupt block","corruption","ora-01578","recover","repair"],"untested":true},{"id":"rman-tspitr","task":"테이블스페이스를 과거 시점으로 되돌리기(TSPITR)","category":"RMAN","command":"-- 1) Is the tablespace self-contained (SQL, on the target DB):\nEXEC DBMS_TTS.TRANSPORT_SET_CHECK(ts_list => 'USERS', incl_constraints => TRUE, full_check => TRUE);\nSELECT * FROM transport_set_violations;\n-- 2) Objects created AFTER the target time that TSPITR will lose:\nSELECT owner, name, tablespace_name,\n       TO_CHAR(creation_time,'YYYY-MM-DD HH24:MI:SS') AS created\nFROM   ts_pitr_objects_to_be_dropped\nWHERE  tablespace_name = 'USERS'\n  AND  creation_time > TO_DATE('2026-01-15 03:00:00','YYYY-MM-DD HH24:MI:SS');\n# 3) RMAN: with an automatic auxiliary instance\nRECOVER TABLESPACE users\n  UNTIL TIME \"TO_DATE('2026-01-15 03:00:00','YYYY-MM-DD HH24:MI:SS')\"\n  AUXILIARY DESTINATION '/u03/aux';\n# 4) The tablespace is left OFFLINE: take a backup first, then bring it online\nBACKUP TABLESPACE users;\nALTER TABLESPACE users ONLINE;","what":"RMAN이 임시 auxiliary 인스턴스에서 SYSTEM/UNDO/SYSAUX와 대상 테이블스페이스를 요청한 시점까지 복구한 후 transportable tablespace로 대상 데이터베이스에 다시 연결한다. 데이터베이스의 나머지는 현재 상태를 유지한다.","when":"잘못된 배치 작업이나 잘못된 DROP/TRUNCATE가 하나의 테이블스페이스(또는 자체 완결적인 테이블스페이스 집합)의 데이터만 손상시켰고 flashback 옵션으로 충분하지 않을 때.","caution":"해당 테이블스페이스에서 이루어진 모든 변경과 대상 시점 이후 생성된 모든 객체가 손실된다. 테이블스페이스 집합은 자체 완결적이어야 한다(다른 테이블스페이스에 인덱스/테이블, 파티션, LOB, FK가 없어야 함). SYSTEM, UNDO 또는 SYS 객체를 포함하는 테이블스페이스에는 사용할 수 없다. TSPITR 후 해당 테이블스페이스의 이전 백업은 사용할 수 없으므로 즉시 백업한다. AUXILIARY DESTINATION에는 SYSTEM+SYSAUX+UNDO+대상 테이블스페이스만큼의 여유 공간이 필요하다.","note":"단일 테이블에는 rman-recover-table이 덜 파괴적이다.","tags":["rman","tspitr","tablespace","pitr","point-in-time","auxiliary","rollback"],"untested":true},{"id":"rman-recover-table","task":"RMAN 백업에서 단일 테이블을 과거 시점으로 복구(RECOVER TABLE)","category":"RMAN","command":"# Restore under a new name (the original table is not touched):\nRECOVER TABLE hr.employees\n  UNTIL TIME \"TO_DATE('2026-01-15 03:00:00','YYYY-MM-DD HH24:MI:SS')\"\n  AUXILIARY DESTINATION '/u03/aux'\n  REMAP TABLE hr.employees:employees_rec;\n# Multitenant: a table inside a PDB\nRECOVER TABLE hr.employees OF PLUGGABLE DATABASE <pdb>\n  UNTIL SCN 12345678\n  AUXILIARY DESTINATION '/u03/aux'\n  REMAP TABLE hr.employees:employees_rec;\n# Only produce a Data Pump dump, do not import:\nRECOVER TABLE hr.employees\n  UNTIL SCN 12345678\n  AUXILIARY DESTINATION '/u03/aux'\n  DATAPUMP DESTINATION '/u03/dump'\n  DUMP FILE 'employees_rec.dmp'\n  NOTABLEIMPORT;","what":"RMAN이 자동 auxiliary 인스턴스를 요청한 시점으로 복구하고, 테이블(또는 파티션)을 Data Pump로 export하여 REMAP으로 다른 이름으로 대상 데이터베이스에 import한다. 이후 auxiliary 인스턴스는 제거된다.","when":"실수로 삭제/갱신되었거나 PURGE로 drop된 단일 테이블의 데이터를 Flashback Table/Query의 undo 보존 범위를 넘어 복구할 때.","caution":"AUXILIARY DESTINATION에는 SYSTEM, SYSAUX, UNDO와 테이블의 테이블스페이스만큼의 여유 공간이 필요하며 작업은 몇 시간 걸릴 수 있다. REMAP TABLE 없이 같은 이름의 테이블이 있으면 작업이 실패한다. 원본을 덮어쓰지 않도록 항상 REMAP 또는 NOTABLEIMPORT를 사용한다. SYS/SYSTEM 스키마의 테이블과 SYSTEM/SYSAUX 테이블스페이스의 테이블은 이 방법으로 복구할 수 없다. Enterprise Edition이 필요하다.","since":"12c","note":"필요 조건: 전체 백업과 대상 시점을 포함하는 아카이브 로그. 파티션의 경우: RECOVER TABLE hr.sales:p2025 ... REMAP TABLE hr.sales:p2025:sales_p2025_rec.","tags":["rman","recover table","table recovery","pitr","auxiliary","remap","accidental delete"],"untested":true},{"id":"pdb-create-from-seed","task":"PDB$SEED에서 새 PDB 생성","category":"멀티테넌트","command":"-- In CDB$ROOT, as SYSDBA:\nCREATE PLUGGABLE DATABASE <pdb>\n  ADMIN USER pdbadmin IDENTIFIED BY \"<password>\" ROLES = (DBA)\n  DEFAULT TABLESPACE users\n    DATAFILE '/u02/oradata/<cdb>/<pdb>/users01.dbf' SIZE 512M AUTOEXTEND ON NEXT 64M\n  FILE_NAME_CONVERT = ('/u02/oradata/<cdb>/pdbseed/', '/u02/oradata/<cdb>/<pdb>/')\n  STORAGE (MAXSIZE 50G);\n-- With OMF/ASM, instead of FILE_NAME_CONVERT: CREATE_FILE_DEST = '+DATA'\nALTER PLUGGABLE DATABASE <pdb> OPEN;\nALTER PLUGGABLE DATABASE <pdb> SAVE STATE;\nSELECT con_id, name, open_mode, restricted FROM v$pdbs ORDER BY con_id;","what":"seed 템플릿에서 비어 있는 새 pluggable database를 생성한다. 로컬 관리자 사용자, 기본 테이블스페이스, 전체 크기 한도를 같은 명령에서 정의한다.","when":"새 애플리케이션/스키마용으로 격리된 데이터베이스를 열 때, 테스트 및 개발 환경을 준비할 때.","caution":"새 PDB는 MOUNTED 상태로 올라온다. OPEN과 SAVE STATE(CDB 재시작 후에도 열린 상태 유지)가 필요하다. FILE_NAME_CONVERT 경로가 잘못되면 파일이 잘못된 디렉터리에 기록된다. OMF가 없으면 DATAFILE에 전체 파일 경로가 필수다(ORA-02236). 라이선스: 19c EE에서는 Multitenant 옵션 없이 CDB당 사용자 PDB 3개를 사용할 수 있다.","since":"12c","note":"RAC에서는 srvctl add service -pdb로 PDB에 애플리케이션 서비스를 추가한다.","tags":["multitenant","pdb","create","create pluggable database","seed","cdb","pdb$seed"]},{"id":"pdb-clone-local","task":"같은 CDB 내에서 PDB 복제(local / hot clone)","category":"멀티테넌트","command":"-- Hot clone prerequisites: local undo + ARCHIVELOG\nSELECT property_value AS local_undo\nFROM   database_properties WHERE property_name = 'LOCAL_UNDO_ENABLED';\nSELECT log_mode FROM v$database;\n\nCREATE PLUGGABLE DATABASE <new_pdb> FROM <pdb>\n  FILE_NAME_CONVERT = ('/<pdb>/', '/<new_pdb>/');\n-- OMF/ASM: CREATE PLUGGABLE DATABASE <new_pdb> FROM <pdb> CREATE_FILE_DEST = '+DATA';\nALTER PLUGGABLE DATABASE <new_pdb> OPEN;\nSELECT name, open_mode FROM v$pdbs WHERE name = UPPER('<new_pdb>');","what":"원본 PDB의 데이터 파일을 복사하여 같은 CDB에 새 PDB를 만든다. local undo와 ARCHIVELOG를 사용하면 원본이 read-write로 열려 있는 동안에도 실행할 수 있다(hot clone).","when":"운영 PDB의 테스트/개발 사본 생성, 업그레이드나 패치 리허설, 위험한 변경 전 빠른 사본 확보용.","caution":"복사는 원본에 추가 I/O를 발생시키며 대상에 원본만큼의 공간이 필요하다. shared undo 모드나 NOARCHIVELOG에서는 원본이 READ ONLY여야 한다(19c 문서). 복제본에는 운영 데이터가 들어 있으므로 열기 전에 마스킹, DB link, job(DBMS_SCHEDULER), 외부 시스템 연결을 점검한다.","since":"12c","note":"원본이 read-write인 상태의 hot clone은 19c에서 local undo + ARCHIVELOG가 필요하다. 빠른 thin copy에는 SNAPSHOT COPY를 사용한다(적합한 스토리지 필요).","tags":["multitenant","pdb","clone","clone","hot clone","copy","local undo"]},{"id":"pdb-clone-remote","task":"DB link로 다른 CDB에서 PDB 복제(remote clone)","category":"멀티테넌트","command":"-- 1) In the SOURCE CDB, a common user:\nCREATE USER c##clone_user IDENTIFIED BY \"<password>\" CONTAINER = ALL;\nGRANT CREATE SESSION, CREATE PLUGGABLE DATABASE TO c##clone_user CONTAINER = ALL;\n\n-- 2) In the TARGET CDB root, DB link and test:\nCREATE DATABASE LINK clone_link\n  CONNECT TO c##clone_user IDENTIFIED BY \"<password>\"\n  USING '//<source_host>:1521/<source_cdb_service>';\nSELECT * FROM dual@clone_link;\n\n-- 3) Clone and open:\nCREATE PLUGGABLE DATABASE <new_pdb> FROM <pdb>@clone_link\n  FILE_NAME_CONVERT = ('/<pdb>/', '/<new_pdb>/');\nALTER PLUGGABLE DATABASE <new_pdb> OPEN;\n\n-- 4) Violations (version, options, parameters, patches):\nSELECT name, cause, type, message\nFROM   pdb_plug_in_violations\nWHERE  name = UPPER('<new_pdb>') AND status <> 'RESOLVED';","what":"DB link를 통해 네트워크로 원본 CDB의 PDB를 대상 CDB로 복사한다. 중간 파일이나 unplug가 필요 없다.","when":"PDB를 다른 서버/CDB로 옮기거나, 별도 서버에 운영 환경의 테스트 사본을 만들거나, 더 새로운 릴리스의 CDB로 이전할 때.","caution":"원본과 대상 CDB는 endianness, 문자 집합(또는 대상이 AL32UTF8), 옵션이 호환되어야 한다. 더 새로운 릴리스로 복제된 PDB는 처음에 RESTRICTED로 열리며 datapatch/upgrade가 필요하다. 복제가 끝나면 DB link와 common user를 삭제한다. hot clone은 원본에 local undo + ARCHIVELOG가 필요하다.","since":"12c","note":"이동(원본 제거)하려면 12.2+ RELOCATE 옵션을 사용한다: CREATE PLUGGABLE DATABASE <pdb> FROM <pdb>@clone_link RELOCATE.","tags":["multitenant","pdb","remote clone","db link","clone","migration","relocate"]},{"id":"pdb-unplug-plug","task":"PDB를 unplug하여 다른(또는 같은) CDB에 plug","category":"멀티테넌트","command":"-- SOURCE CDB: close and unplug to an XML manifest\nALTER PLUGGABLE DATABASE <pdb> CLOSE IMMEDIATE;\nALTER PLUGGABLE DATABASE <pdb> UNPLUG INTO '/u01/stage/<pdb>.xml';\n-- (Single-file archive: UNPLUG INTO '/u01/stage/<pdb>.pdb' - includes datafiles, 12.2+)\nDROP PLUGGABLE DATABASE <pdb> KEEP DATAFILES;\n\n-- TARGET CDB: check compatibility first\nSET SERVEROUTPUT ON\nDECLARE\n  l_ok BOOLEAN;\nBEGIN\n  l_ok := DBMS_PDB.CHECK_PLUG_COMPATIBILITY(\n            pdb_descr_file => '/u01/stage/<pdb>.xml',\n            pdb_name       => '<pdb>');\n  DBMS_OUTPUT.PUT_LINE(CASE WHEN l_ok THEN 'COMPATIBLE' ELSE 'NOT COMPATIBLE - see PDB_PLUG_IN_VIOLATIONS' END);\nEND;\n/\nSELECT name, cause, type, status, message\nFROM   pdb_plug_in_violations\nWHERE  name = UPPER('<pdb>') AND status <> 'RESOLVED';\n\n-- NOCOPY if the files stay in place, COPY FILE_NAME_CONVERT if they move:\nCREATE PLUGGABLE DATABASE <pdb> USING '/u01/stage/<pdb>.xml' NOCOPY TEMPFILE REUSE;\n-- From a .pdb archive: CREATE PLUGGABLE DATABASE <pdb> USING '/u01/stage/<pdb>.pdb'\n--                    FILE_NAME_CONVERT = ('/u01/stage/', '/u02/oradata/<cdb>/<pdb>/');\nALTER PLUGGABLE DATABASE <pdb> OPEN;","what":"PDB를 CDB에서 분리하고 메타데이터를 XML manifest(또는 데이터 파일을 포함한 단일 .pdb 아카이브)로 기록한다. 호환성 점검 후 같은 파일로 대상 CDB에 다시 연결한다.","when":"같은 서버의 더 새로운 릴리스 CDB로 PDB를 옮길 때(unplug/plug 업그레이드), CDB 통합 시, 네트워크 복제가 불가능한 이전 시.","caution":"unplug 후 원본 CDB에서 PDB를 사용할 수 없다(다운타임). NOCOPY로 plug한 파일은 이제 대상 CDB에 속하므로 실패 시 되돌릴 수 있도록 먼저 파일 수준 사본이나 RMAN 백업을 확보한다. ERROR 유형 위반이 해결될 때까지 PDB는 RESTRICTED로 열린다. 같은 CDB에 다시 plug할 때 AS CLONE을 생략하면 GUID 충돌이 발생할 수 있다.","since":"12c","note":".pdb 아카이브(12.2+)는 데이터 파일을 포함하므로 단일 파일로 이동한다. XML 방식에서는 데이터 파일을 별도로 복사한다.","tags":["multitenant","pdb","unplug","plug","migration","xml manifest","check_plug_compatibility"]},{"id":"pdb-open-save-state","task":"PDB 열기 및 CDB 재시작 후에도 열린 상태 유지(SAVE STATE)","category":"멀티테넌트","command":"-- Open all PDBs (RAC: INSTANCES = ALL):\nALTER PLUGGABLE DATABASE ALL OPEN;\n-- ALTER PLUGGABLE DATABASE <pdb> OPEN INSTANCES = ALL;\n-- Save the current open state (opens automatically after a CDB restart):\nALTER PLUGGABLE DATABASE <pdb> SAVE STATE;\n-- Saved states:\nSELECT con_name, instance_name, state, restricted FROM dba_pdb_saved_states;\n-- Remove the saved state:\nALTER PLUGGABLE DATABASE <pdb> DISCARD STATE;\n-- If it opens RESTRICTED, find the reason:\nSELECT name, cause, type, message\nFROM   pdb_plug_in_violations\nWHERE  status <> 'RESOLVED';","what":"PDB를 READ WRITE로 열고 현재 open 상태를 저장하여 CDB 재시작 시 PDB가 같은 모드로 자동으로 열리게 한다.","when":"PDB 생성, plug, 복제 후. CDB 재시작 후 PDB가 MOUNTED로 남아 있는 것을 발견했을 때.","caution":"SAVE STATE는 명령 시점의 상태를 기록한다. PDB가 닫힌 상태에서 실행하면 \"closed\" 상태가 기록된다(또는 기록이 제거된다). RAC에서는 상태가 인스턴스별이다. 서비스 기반 관리(srvctl을 통한 PDB 서비스)에서는 SAVE STATE보다 Clusterware 시작이 권장된다. Data Guard standby에서는 open mode가 역할에 따라 달라진다.","since":"12c","note":"12.1.0.2+.","tags":["multitenant","pdb","open","save state","auto open","restricted","dba_pdb_saved_states"]},{"id":"pdb-resource-limits","task":"PDB별 CPU, 메모리, I/O, 스토리지 한도 설정","category":"멀티테넌트","command":"ALTER SESSION SET CONTAINER = <pdb>;\n-- CPU (instance caging with Resource Manager; at PDB level in 19c):\nALTER SYSTEM SET cpu_count = 2 SCOPE = BOTH;\n-- Memory (sga_min_size guarantee, sga_target upper limit):\nALTER SYSTEM SET sga_target = 512M SCOPE = BOTH;\nALTER SYSTEM SET sga_min_size = 96M SCOPE = BOTH;\nALTER SYSTEM SET pga_aggregate_limit = 2G SCOPE = BOTH;\n-- I/O (non-Exadata):\nALTER SYSTEM SET max_iops = 5000 SCOPE = BOTH;\nALTER SYSTEM SET max_mbps = 200 SCOPE = BOTH;\n-- Number of sessions (after the PDB is reopened):\nALTER SYSTEM SET sessions = 200 SCOPE = SPFILE;\n-- Storage:\nALTER PLUGGABLE DATABASE STORAGE (MAXSIZE 10G MAX_SHARED_TEMP_SIZE 2G);\n\n-- Settings across all PDBs, from the root:\nALTER SESSION SET CONTAINER = CDB$ROOT;\nSELECT p.name AS pdb, sp.name, sp.value\nFROM   v$system_parameter sp JOIN v$pdbs p ON p.con_id = sp.con_id\nWHERE  sp.name IN ('cpu_count','sga_target','sga_min_size','max_iops','max_mbps','pga_aggregate_limit')\nORDER  BY p.name, sp.name;","what":"PDB 수준 파라미터로 PDB가 CDB에서 사용할 수 있는 CPU, SGA/PGA, IOPS/MBPS, 전체 스토리지를 제한한다. \"noisy neighbor\" PDB가 다른 PDB에 영향을 주는 것을 막는다.","when":"통합 CDB에서, SLA가 다른 애플리케이션을 같은 CDB에서 운영할 때, 또는 한 PDB가 서버 전체를 고갈시키는 것이 보일 때.","caution":"PDB sga_target은 CDB sga_target보다 작아야 한다. 너무 낮은 값은 ORA-04031과 성능 문제를 일으킨다. sga_min_size의 합이 CDB SGA 전체를 예약해서는 안 된다. PDB의 cpu_count가 적용되려면 CDB에 resource manager plan이 활성화되어 있어야 한다. MAXSIZE에 도달하면 PDB는 ORA-65114를 받는다.","since":"12c","note":"23ai에서는 sga_min_size가 sga_target의 20% 미만이어야 하며(ORA-56746), 19c의 한도는 50%이다. MAX_IOPS/MAX_MBPS는 12.2+, PDB 메모리 파라미터는 12.2+이다.","tags":["multitenant","pdb","resource","resource","cpu_count","sga_target","maxsize"]},{"id":"pdb-container-context","task":"현재 컨테이너 확인 및 root에서 모든 PDB 조회(CDB_ / CONTAINERS)","category":"멀티테넌트","command":"-- Current container:\nSHOW CON_NAME\nSHOW CON_ID\nSHOW PDBS\nSELECT SYS_CONTEXT('USERENV','CON_NAME') AS con_name,\n       SYS_CONTEXT('USERENV','CON_ID')   AS con_id\nFROM   dual;\n\n-- All containers, from the root:\nSELECT con_id, name, open_mode FROM v$containers ORDER BY con_id;\n\n-- CDB_ views combine all open PDBs with CON_ID:\nSELECT c.name AS container, t.tablespace_name, t.status\nFROM   cdb_tablespaces t JOIN v$containers c ON c.con_id = t.con_id\nORDER  BY c.name, t.tablespace_name;\n\n-- Query any table/view across all PDBs with CONTAINERS():\nSELECT con_id, COUNT(*) AS users\nFROM   CONTAINERS(dba_users)\nGROUP  BY con_id ORDER BY con_id;","what":"세션이 연결된 컨테이너를 보여 준다. root에서는 CDB_ 뷰와 CONTAINERS() 절로 열려 있는 모든 PDB의 딕셔너리 정보를 하나의 쿼리로 수집한다.","when":"DDL/파라미터 변경 전 올바른 컨테이너에 있는지 확인하고, 모든 PDB에 걸친 공간, 사용자, 객체 인벤토리를 만들기 위해.","caution":"읽기 전용. root(CDB$ROOT)에 실수로 애플리케이션 객체나 로컬 사용자를 만들지 않는다. root에서의 ALTER SYSTEM은 CDB 전체에 영향을 준다. CDB_ 뷰와 CONTAINERS()는 열려 있는 PDB만 반환하며, MOUNTED PDB는 결과에 나타나지 않는다.","since":"12c","note":"컨테이너 전환: ALTER SESSION SET CONTAINER = <pdb>; (rosetta \"활성 데이터베이스 변경\" 참고).","tags":["multitenant","container","con_name","cdb_","containers","pdb","root"]},{"id":"pdb-refreshable-clone","task":"refreshable PDB 생성, refresh 및 독립 PDB로 전환","category":"멀티테넌트","command":"-- In the target CDB root (DB link to the source ready: see pdb-clone-remote):\nCREATE PLUGGABLE DATABASE <new_pdb> FROM <pdb>@clone_link\n  FILE_NAME_CONVERT = ('/<pdb>/', '/<new_pdb>/')\n  REFRESH MODE EVERY 60 MINUTES;\nSELECT pdb_name, status, refresh_mode, refresh_interval, last_refresh_scn\nFROM   dba_pdbs WHERE pdb_name = UPPER('<new_pdb>');\n\n-- Can only be opened READ ONLY; close it first to refresh manually:\nALTER PLUGGABLE DATABASE <new_pdb> OPEN READ ONLY;\nALTER PLUGGABLE DATABASE <new_pdb> CLOSE IMMEDIATE;\nALTER PLUGGABLE DATABASE <new_pdb> REFRESH;\n\n-- Change the mode (MANUAL) or convert to an independent, writable PDB (irreversible):\nALTER PLUGGABLE DATABASE <new_pdb> REFRESH MODE MANUAL;\nALTER PLUGGABLE DATABASE <new_pdb> REFRESH MODE NONE;\nALTER PLUGGABLE DATABASE <new_pdb> OPEN;","what":"원본 PDB의 redo를 주기적으로 또는 요청 시 적용하여 최신 상태로 유지되는 사본을 만든다. 보고용으로 READ ONLY로 열리며, REFRESH MODE NONE으로 독립된 read-write PDB가 된다.","when":"정기적으로 갱신되는 운영 환경의 보고/테스트 사본, 짧은 다운타임의 PDB 이전(최종 refresh + REFRESH MODE NONE), 또는 \"저비용 PDB 수준 standby\"용.","caution":"refresh 중에는 PDB가 닫혀 있어야 한다. READ ONLY로 열려 있는 동안 자동 refresh는 대기한다. refresh에 필요한 아카이브 로그는 원본에서 접근 가능해야 한다. REFRESH MODE NONE은 되돌릴 수 없으며 이후 사본은 더 이상 refresh할 수 없다. 원본에 local undo + ARCHIVELOG가 필요하다. 실제 DR 용도로 Data Guard를 대체하지 않는다.","since":"12c","note":"12.2+. REFRESH MODE NONE 후 복제본은 일반 PDB가 되어 READ WRITE로 열 수 있으며, 이는 되돌릴 수 없다.","tags":["multitenant","pdb","refreshable","refresh","refresh","reporting","clone"]}]},"recipes/oracle-ops.json":{"engineId":"oracle","engine":"Oracle","order":2,"recipes":[{"id":"awr-snapshot-report","task":"AWR 스냅샷 생성 및 두 스냅샷 간 AWR 보고서 생성","category":"성능","command":"-- 1) Manual snapshot before and after the event (run in CDB root):\nEXEC DBMS_WORKLOAD_REPOSITORY.CREATE_SNAPSHOT;\n\n-- 2) List snapshots:\nSELECT snap_id,\n       TO_CHAR(begin_interval_time,'YYYY-MM-DD HH24:MI') AS begin_time,\n       TO_CHAR(end_interval_time,'YYYY-MM-DD HH24:MI')   AS end_time\nFROM   dba_hist_snapshot\nWHERE  dbid = (SELECT dbid FROM v$database)\nORDER  BY snap_id DESC\nFETCH FIRST 24 ROWS ONLY;\n\n-- 3a) Interactive report (prompts for HTML/TEXT and snapshot range):\n-- @?/rdbms/admin/awrrpt.sql      (RAC: awrgrpt.sql, specific instance: awrrpti.sql)\n\n-- 3b) TEXT report from a script:\nSET LINESIZE 8000 PAGESIZE 0 TRIMSPOOL ON HEADING OFF FEEDBACK OFF\nSPOOL /tmp/awr_<begin_snap>_<end_snap>.txt\nSELECT output\nFROM   TABLE(DBMS_WORKLOAD_REPOSITORY.AWR_REPORT_TEXT(\n         l_dbid     => (SELECT dbid FROM v$database),\n         l_inst_num => (SELECT instance_number FROM v$instance),\n         l_bid      => <begin_snap>,\n         l_eid      => <end_snap>));\nSPOOL OFF\n-- For HTML: DBMS_WORKLOAD_REPOSITORY.AWR_REPORT_HTML (same parameters)","what":"문제 구간을 포함하는 두 AWR 스냅샷 사이의 보고서를 생성한다: load profile, 상위 대기 이벤트, 상위 SQL, 인스턴스 통계. 테이블 함수를 사용하면 스크립트나 스케줄러 job에서 보고서를 가져올 수 있다.","when":"특정 시간대의 성능 저하 분석, 부하 테스트나 배치 전후 비교, 용량 보고용.","caution":"Diagnostics Pack 라이선스가 필요하다(CONTROL_MANAGEMENT_PACK_ACCESS='DIAGNOSTIC' 또는 'DIAGNOSTIC+TUNING'). 라이선스가 없으면 파라미터를 'NONE'으로 설정하고 Statspack을 사용한다. 보고서는 인스턴스 재시작을 가로지를 수 없다. 1시간보다 긴 범위는 평균화되어 짧은 스파이크를 가린다. 구간을 좁게 잡는다.","note":"PDB 수준 AWR(12.2+)은 AWR_PDB_AUTOFLUSH_ENABLED=TRUE와 AWR_SNAPSHOT_TIME_OFFSET/AWR_PDB_MAX_PARALLEL_SLAVES를 확인한다. PDB 내부에서 생성한 보고서는 해당 PDB의 데이터만 보여 준다. 간격/보존: DBMS_WORKLOAD_REPOSITORY.MODIFY_SNAPSHOT_SETTINGS(interval => 30, retention => 43200).","tags":["awr","awr report","snapshot","awrrpt","dbms_workload_repository","performance report","diagnostics pack"]},{"id":"awr-diff-report","task":"두 기간 간 AWR diff(compare period) 보고서","category":"성능","command":"-- Interactive: @?/rdbms/admin/awrddrpt.sql   (RAC: awrgdrpt.sql)\n\n-- From a script: period 1 (good) = <bid1>-<eid1>, period 2 (bad) = <bid2>-<eid2>\nSET LINESIZE 8000 PAGESIZE 0 TRIMSPOOL ON HEADING OFF FEEDBACK OFF\nSPOOL /tmp/awr_diff.txt\nSELECT output\nFROM   TABLE(DBMS_WORKLOAD_REPOSITORY.AWR_DIFF_REPORT_TEXT(\n         dbid1 => (SELECT dbid FROM v$database), inst_num1 => 1,\n         bid1  => <bid1>, eid1 => <eid1>,\n         dbid2 => (SELECT dbid FROM v$database), inst_num2 => 1,\n         bid2  => <bid2>, eid2 => <eid2>));\nSPOOL OFF\n-- HTML: DBMS_WORKLOAD_REPOSITORY.AWR_DIFF_REPORT_HTML","what":"두 스냅샷 범위를 나란히 놓고 DB time, 대기 이벤트, load profile, SQL별 통계의 백분율 차이를 보여 준다. \"어제는 괜찮았는데 오늘은 느리다\"에 직접 답한다.","when":"업그레이드/패치, 파라미터 변경, 배포, 통계 수집 후 회귀 분석, 두 날의 같은 시간대 비교.","caution":"Diagnostics Pack 라이선스가 필요하다. 비교 기간은 길이가 같고 워크로드가 비슷해야 하며, 그렇지 않으면 차이 값이 오해를 일으킨다. 서로 다른 데이터베이스(예: prod vs test)를 비교하려면 awrextr.sql/awrload.sql로 AWR 데이터를 옮기고 해당 dbid2를 지정한다.","tags":["awr diff","comparison","compare period","awrddrpt","regression","performance","diagnostics pack"]},{"id":"ash-top-activity","task":"ASH로 최근 몇 분간의 상위 대기 이벤트와 상위 SQL 조회","category":"성능","command":"-- Last 30 min: wait event breakdown (each sample ~1 s of active session time)\nSELECT NVL(event, 'ON CPU') AS event, wait_class,\n       COUNT(*) AS samples,\n       ROUND(RATIO_TO_REPORT(COUNT(*)) OVER () * 100, 1) AS pct\nFROM   v$active_session_history\nWHERE  sample_time > SYSTIMESTAMP - INTERVAL '30' MINUTE\nGROUP  BY NVL(event, 'ON CPU'), wait_class\nORDER  BY samples DESC\nFETCH FIRST 10 ROWS ONLY;\n\n-- Last 30 min: top SQL (with CPU / User I/O breakdown)\nSELECT sql_id, sql_opname,\n       COUNT(*) AS samples,\n       SUM(CASE WHEN session_state = 'ON CPU' THEN 1 ELSE 0 END) AS cpu,\n       SUM(CASE WHEN wait_class = 'User I/O' THEN 1 ELSE 0 END)  AS user_io,\n       ROUND(RATIO_TO_REPORT(COUNT(*)) OVER () * 100, 1) AS pct\nFROM   v$active_session_history\nWHERE  sample_time > SYSTIMESTAMP - INTERVAL '30' MINUTE\n  AND  sql_id IS NOT NULL\nGROUP  BY sql_id, sql_opname\nORDER  BY samples DESC\nFETCH FIRST 10 ROWS ONLY;\n\n-- Older periods: DBA_HIST_ACTIVE_SESS_HISTORY (one sample every 10 s; COUNT(*)*10 = seconds)\n\n-- Ready-made ASH report (last 30 min):\nSET LINESIZE 8000 PAGESIZE 0 TRIMSPOOL ON HEADING OFF FEEDBACK OFF\nSELECT output\nFROM   TABLE(DBMS_WORKLOAD_REPOSITORY.ASH_REPORT_TEXT(\n         l_dbid     => (SELECT dbid FROM v$database),\n         l_inst_num => (SELECT instance_number FROM v$instance),\n         l_btime    => SYSDATE - 30/1440,\n         l_etime    => SYSDATE));\n-- Interactive: @?/rdbms/admin/ashrpt.sql","what":"Active Session History 샘플을 사용하여 최근 몇 분간 어떤 대기 이벤트와 어떤 SQL이 DB time을 소비했는지 백분율로 보여 준다. AWR 스냅샷을 기다리지 않고 장애를 진단한다.","when":"실시간 성능 저하 중, AWR 평균에서 사라지는 짧은 스파이크, 특정 세션이나 SQL이 무엇을 기다렸는지 확인할 때.","caution":"V$ACTIVE_SESSION_HISTORY와 DBA_HIST_ACTIVE_SESS_HISTORY 조회에는 Diagnostics Pack 라이선스가 필요하다. ASH는 샘플링 방식이다. 샘플 수는 시간을 추정할 뿐 정확한 측정값이 아니며, 매우 짧은 SQL은 나타나지 않을 수 있다. 메모리 내 ASH 버퍼는 바쁜 시스템에서 몇 시간 분량만 보관한다.","tags":["ash","active session history","top wait","top sql","wait","ashrpt","real-time analysis","diagnostics pack"]},{"id":"addm-task-report","task":"ADDM 분석 실행 및 보고서 확인","category":"성능","command":"-- Analyze the range between two snapshots (in CDB root):\nVAR tname VARCHAR2(60)\nBEGIN\n  :tname := 'ADDM_<begin_snap>_<end_snap>';\n  DBMS_ADDM.ANALYZE_DB(\n    task_name      => :tname,\n    begin_snapshot => <begin_snap>,\n    end_snapshot   => <end_snap>);\nEND;\n/\n\nSET LONG 1000000 LONGCHUNKSIZE 1000000 LINESIZE 200 PAGESIZE 0\nSELECT DBMS_ADDM.GET_REPORT(task_name => :tname) FROM dual;\n\n-- Findings summary (by impact):\nSET PAGESIZE 100\nSELECT finding_name, type, ROUND(impact/1e6) AS impact_sec, message\nFROM   dba_advisor_findings\nWHERE  task_name = :tname\nORDER  BY impact DESC;\n\n-- Interactive alternative: @?/rdbms/admin/addmrpt.sql\n-- Clean up the task when done:\nEXEC DBMS_ADDM.DELETE(task_name => :tname);","what":"Automatic Database Diagnostic Monitor가 두 AWR 스냅샷 사이의 DB time을 분석하여 병목과 그 영향(findings), 구체적인 권장 사항을 보고한다.","when":"AWR 보고서를 읽기 전에 \"어디를 봐야 하는가\" 요약을 빠르게 얻거나, 스냅샷마다 실행되는 자동 ADDM 외에 사용자 지정 범위를 분석할 때.","caution":"Diagnostics Pack 라이선스가 필요하다. ADDM 권장 사항(파라미터 증가, SQL Tuning Advisor 실행 등)을 맹목적으로 적용하지 않는다. 메모리 권장 사항은 AWR advisory 섹션과 대조해 검증한다. 범위는 재시작을 가로지를 수 없다.","tags":["addm","dbms_addm","advisor","finding","finding","recommendation","diagnostics pack","performance"]},{"id":"sql-monitor-report","task":"SQL Monitor 보고서 확인(실행 중이거나 완료된 장시간 SQL)","category":"성능","command":"-- List monitored SQL (longer than 5 s, parallel, or /*+ MONITOR */):\nSELECT sql_id, status, username, sql_exec_id,\n       TO_CHAR(sql_exec_start,'YYYY-MM-DD HH24:MI:SS') AS exec_start,\n       ROUND(elapsed_time/1e6,1) AS elapsed_s,\n       ROUND(cpu_time/1e6,1)     AS cpu_s,\n       buffer_gets, px_servers_allocated\nFROM   v$sql_monitor\nORDER  BY sql_exec_start DESC\nFETCH FIRST 20 ROWS ONLY;\n\n-- TEXT report (actual rows, time and activity per plan line):\nSET LONG 10000000 LONGCHUNKSIZE 10000000 LINESIZE 300 PAGESIZE 0 TRIMSPOOL ON\nSELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR(\n         sql_id       => '<sql_id>',\n         type         => 'TEXT',\n         report_level => 'ALL') AS report\nFROM   dual;\n\n-- Shareable HTML (interactive in a browser):\nSPOOL /tmp/sqlmon_<sql_id>.html\nSELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR(\n         sql_id => '<sql_id>', type => 'ACTIVE', report_level => 'ALL')\nFROM   dual;\nSPOOL OFF","what":"Real-Time SQL Monitoring은 장시간 또는 병렬 SQL 실행 계획의 각 단계별 실제 행 수, 경과 시간, 메모리/temp 사용량, ASH 활동을 보여 준다. SQL이 실행 중일 때도 진행 상황을 볼 수 있다.","when":"배치/보고서 쿼리가 멈췄는지와 남은 양을 확인할 때, 병렬 쿼리 분산(PX skew) 분석, 추정 행(E-Rows)과 실제 행의 차이를 찾을 때.","caution":"Tuning Pack 라이선스가 필요하다(Diagnostics Pack이 전제 조건). 기본적으로 CPU/I-O를 5 s 소비하거나 병렬로 실행되는 SQL만 모니터링된다. 그 외에는 /*+ MONITOR */ 힌트를 사용한다. 매우 큰 계획(_sqlmon_max_planlines, 기본값 300)은 모니터링되지 않는다. 항목은 제한된 시간 동안만 메모리에 남으며, 12c+에서는 이전 보고서를 DBA_HIST_REPORTS에서도 볼 수 있다.","tags":["sql monitor","report_sql_monitor","v$sql_monitor","real-time","long running query","parallel","tuning pack"]},{"id":"sql-trace-10046-tkprof","task":"세션의 10046 trace 활성화/비활성화 및 TKPROF로 요약","category":"성능","command":"-- 1) Find the target session and its trace file:\nSELECT s.sid, s.serial#, s.username, s.module, p.tracefile\nFROM   v$session s\n  JOIN v$process p ON p.addr = s.paddr\nWHERE  s.username = 'CUSTOMER_APP';\n\n-- 2) Enable trace (waits + binds = equivalent of level 12):\nBEGIN\n  DBMS_MONITOR.SESSION_TRACE_ENABLE(\n    session_id => <sid>,\n    serial_num => <serial#>,\n    waits      => TRUE,\n    binds      => TRUE,\n    plan_stat  => 'ALL_EXECUTIONS');\nEND;\n/\n-- Verify: SELECT sql_trace, sql_trace_waits, sql_trace_binds FROM v$session WHERE sid = <sid>;\n\n-- 3) Disable once the problem operation completes:\nEXEC DBMS_MONITOR.SESSION_TRACE_DISABLE(session_id => <sid>, serial_num => <serial#>);\n\n-- Alternatives:\n-- Your own session: ALTER SESSION SET EVENTS '10046 trace name context forever, level 12';\n-- Single SQL_ID, all sessions: ALTER SYSTEM SET EVENTS 'sql_trace[SQL: <sql_id>] wait=true, bind=true';\n--   disable: ALTER SYSTEM SET EVENTS 'sql_trace[SQL: <sql_id>] off';\n-- For a connection pool: DBMS_MONITOR.CLIENT_ID_TRACE_ENABLE / SERV_MOD_ACT_TRACE_ENABLE\n\n-- 4) Summarize with TKPROF on the OS (most expensive SQL first):\n-- $ tkprof /u01/app/oracle/diag/rdbms/orcl/ORCL/trace/ORCL_ora_12345.trc /tmp/ora_12345.prf \\\n--     sys=no sort=exeela,fchela,prsela waits=yes aggregate=yes","what":"Extended SQL trace(10046)는 세션이 실행하는 모든 SQL의 parse/execute/fetch 시간, 대기 이벤트, bind 값을 trace 파일에 기록한다. TKPROF는 이를 SQL별로 집계한 읽기 쉬운 보고서로 변환한다.","when":"AWR/ASH로 충분하지 않아 단일 사용자 작업이 시간을 어디에 쓰는지 end-to-end로 증명해야 할 때, 애플리케이션-데이터베이스 왕복(SQL*Net) 문제 분석 시.","caution":"라이선스가 필요 없다. trace 파일은 빠르게 커지고 대상 세션에 오버헤드를 준다. 짧게 유지하고 반드시 비활성화한다(DBA_ENABLED_TRACES로 남은 것을 확인). binds=TRUE는 민감 데이터(주민등록번호, 카드 번호 등)를 trace 파일에 기록한다. ALTER SESSION SET EVENTS는 ALTER SESSION 권한이 필요하다(없으면 ORA-01031). TKPROF는 기본적으로 bind 값을 보여 주지 않으므로 원본 .trc 파일을 보관한다.","note":"커넥션 풀이나 서비스 수준 trace는 여러 파일을 생성한다. trcsess로 병합한 후(trcsess output=combined.trc clientid=<id> *.trc) TKPROF를 실행한다.","tags":["10046","trace","tkprof","dbms_monitor","sql trace","wait","bind","session tracing"]},{"id":"xplan-allstats-last","task":"row source 통계와 함께 실제 실행 계획 조회(ALLSTATS LAST)","category":"성능","command":"-- 1) Collect row source statistics (for this query only):\nSET SERVEROUTPUT OFF\nSELECT /*+ gather_plan_statistics */ customer_id, SUM(amount)\nFROM   customer_app.sales\nWHERE  created_at >= DATE '2026-06-01'\nGROUP  BY customer_id;\n-- or for the whole session: ALTER SESSION SET statistics_level = ALL;\n\n-- 2) Plan of the last cursor run in the same session (E-Rows vs A-Rows):\nSELECT *\nFROM   TABLE(DBMS_XPLAN.DISPLAY_CURSOR(\n         sql_id          => NULL,\n         cursor_child_no => NULL,\n         format          => 'ALLSTATS LAST +PEEKED_BINDS +OUTLINE'));\n\n-- 3) A cursor from another session / from earlier:\nSELECT *\nFROM   TABLE(DBMS_XPLAN.DISPLAY_CURSOR('<sql_id>', NULL, 'ALLSTATS LAST +PEEKED_BINDS'));","what":"EXPLAIN PLAN은 추정치다. 이 방법은 실제로 실행된 계획을 단계별 Starts, E-Rows(추정), A-Rows(실제), A-Time, Buffers, 메모리 사용량과 함께 보여 준다. 카디널리티 오추정이 시작되는 단계를 찾는 가장 직접적인 방법이다.","when":"단일 느린 SQL 튜닝, 통계/히스토그램 필요성 증명, 힌트나 인덱스 변경 효과 측정 시.","caution":"라이선스가 필요 없다. SET SERVEROUTPUT ON 상태에서는 sql_id=>NULL이 DBMS_OUTPUT 호출을 마지막 커서로 보여 주므로 먼저 OFF로 설정한다. statistics_level=ALL을 시스템 전체에 활성화하지 않는다. 눈에 띄는 CPU 오버헤드가 생긴다. 힌트는 SQL 텍스트를 바꾸므로 새 sql_id가 된다. 사용자는 V$SQL_PLAN_STATISTICS_ALL에 대한 읽기 권한(SELECT_CATALOG_ROLE)이 필요하다.","tags":["dbms_xplan","display_cursor","allstats last","gather_plan_statistics","actual plan","execution plan","a-rows","cardinality"]},{"id":"sql-tuning-advisor-task","task":"SQL Tuning Advisor 작업 생성·실행 및 권장 사항 확인","category":"성능","command":"-- 1) Create and run a task for a SQL in the cursor cache:\nVAR task VARCHAR2(64)\nBEGIN\n  :task := DBMS_SQLTUNE.CREATE_TUNING_TASK(\n             sql_id      => '<sql_id>',\n             scope       => DBMS_SQLTUNE.SCOPE_COMPREHENSIVE,\n             time_limit  => 300,\n             task_name   => 'STA_<sql_id>',\n             description => 'Slow report query');\n  DBMS_SQLTUNE.EXECUTE_TUNING_TASK(task_name => :task);\nEND;\n/\n-- For a historical SQL in AWR: begin_snap => <b>, end_snap => <e>, sql_id => '<sql_id>'\n\n-- 2) Status and report:\nSELECT task_name, status FROM dba_advisor_log WHERE task_name = :task;\n\nSET LONG 1000000 LONGCHUNKSIZE 1000000 LINESIZE 200 PAGESIZE 0\nSELECT DBMS_SQLTUNE.REPORT_TUNING_TASK(:task) FROM dual;\n\n-- 3) Get the recommendations as an executable script:\nSELECT DBMS_SQLTUNE.SCRIPT_TUNING_TASK(:task) FROM dual;\n\n-- 4) When done:\nEXEC DBMS_SQLTUNE.DROP_TUNING_TASK(task_name => :task);","what":"SQL에 대해 옵티마이저를 확장된 \"tuning mode\"로 실행하여 누락된 통계, 새 인덱스, SQL profile, 대체 계획, SQL 재구성에 대한 권장 사항을 생성한다. profile 권장 사항은 테스트 실행으로 검증한다.","when":"문제가 되는 단일 SQL에 대해 빠른 second opinion이 필요할 때, 또는 코드를 바꾸지 않고 실행 계획 회귀를 SQL profile로 해결하고자 할 때.","caution":"Tuning Pack 라이선스가 필요하다. COMPREHENSIVE 범위는 테스트를 위해 SQL을 실행할 수 있다(DML은 쿼리 부분만). 운영 환경에서는 time_limit을 설정한다. 인덱스 권장 사항은 나머지 워크로드를 고려하지 않으므로 DML 비용은 별도로 평가한다. 사용자에게 ADVISOR 권한이 필요하다.","tags":["sql tuning advisor","dbms_sqltune","tuning task","recommendation","sql profile","index recommendation","tuning pack"]},{"id":"sql-profile-accept","task":"SQL profile 수락, 비활성화, 삭제","category":"성능","command":"-- 1) Accept the profile recommended by the tuning task:\nDECLARE\n  l_name VARCHAR2(128);\nBEGIN\n  l_name := DBMS_SQLTUNE.ACCEPT_SQL_PROFILE(\n              task_name   => '<task_name>',\n              name        => 'PROF_<sql_id>',\n              description => 'STA recommendation, CHG-1234',\n              force_match => TRUE,     -- also matches statements differing only in literals\n              replace     => TRUE);\n  DBMS_OUTPUT.PUT_LINE('Profile: ' || l_name);\nEND;\n/\n\n-- 2) Profiles and the cursors using them:\nSELECT name, category, status, force_matching,\n       TO_CHAR(created,'YYYY-MM-DD HH24:MI') AS created\nFROM   dba_sql_profiles\nORDER  BY created DESC;\n\nSELECT sql_id, child_number, plan_hash_value, sql_profile\nFROM   v$sql\nWHERE  sql_profile IS NOT NULL;\n\n-- 3) Temporarily disable / drop:\nEXEC DBMS_SQLTUNE.ALTER_SQL_PROFILE(name => 'PROF_<sql_id>', attribute_name => 'STATUS', value => 'DISABLED');\nEXEC DBMS_SQLTUNE.DROP_SQL_PROFILE(name => 'PROF_<sql_id>', ignore => TRUE);","what":"SQL profile은 옵티마이저에 추가적인 카디널리티/선택도 보정(OPT_ESTIMATE 등)을 제공하여 SQL 텍스트를 바꾸지 않고 더 나은 계획을 만들게 한다. FORCE_MATCH를 사용하면 리터럴만 다른 문장에도 적용된다.","when":"SQL을 변경할 수 없을 때(패키지 애플리케이션), STA가 검증된 개선을 권장할 때, 또는 실행 계획 회귀의 빠른 해결책으로.","caution":"Tuning Pack 라이선스가 필요하다. profile은 계획을 \"고정\"하지 않고 추정치를 보정하므로 데이터 분포가 바뀌면 계획이 여전히 바뀔 수 있다. 계획 안정성이 필수라면 SQL plan baseline을 사용한다. 둘 다 있으면 옵티마이저는 baseline의 accepted 계획 중에서 선택한다. 수락하기 전에 항상 검증 결과(보고서의 'Validation results' 섹션)를 읽는다.","tags":["sql profile","accept_sql_profile","dbms_sqltune","force_match","plan fix","dba_sql_profiles","tuning pack"]},{"id":"spm-load-from-cursor-cache","task":"cursor cache의 좋은 계획을 SQL plan baseline으로 로드","category":"성능","command":"-- 1) Compare the SQL's plans and their performance:\nSELECT sql_id, child_number, plan_hash_value, executions,\n       ROUND(elapsed_time/1e3/NULLIF(executions,0),1) AS ms_per_exec,\n       sql_plan_baseline\nFROM   v$sql\nWHERE  sql_id = '<sql_id>';\n\n-- 2) Load the chosen plan as a baseline:\nSET SERVEROUTPUT ON\nDECLARE\n  n PLS_INTEGER;\nBEGIN\n  n := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(\n         sql_id          => '<sql_id>',\n         plan_hash_value => <plan_hash_value>,\n         fixed           => 'NO',\n         enabled         => 'YES');\n  DBMS_OUTPUT.PUT_LINE(n || ' plan(s) loaded');\nEND;\n/\n\n-- 3) Verify the baseline:\nSELECT sql_handle, plan_name, origin, enabled, accepted, fixed, reproduced,\n       TO_CHAR(last_executed,'YYYY-MM-DD HH24:MI') AS last_exec\nFROM   dba_sql_plan_baselines\nWHERE  signature = (SELECT exact_matching_signature FROM v$sql\n                    WHERE sql_id = '<sql_id>' AND ROWNUM = 1);\n-- On subsequent executions v$sql.sql_plan_baseline should be populated.","what":"SQL Plan Management를 사용하여 선택한 계획을 ACCEPTED baseline으로 만든다. 이후 옵티마이저는 accepted baseline 계획만 사용하며 새로 발견한 계획은 evolve될 때까지 보류한다.","when":"실행 계획 회귀 시 검증된 좋은 계획으로 되돌리고, 업그레이드나 통계 변경 전 중요 SQL의 계획을 보호하기 위해.","caution":"Enterprise Edition에서는 추가 라이선스가 없다(SE2에서는 SQL당 baseline 계획 1개로 제한). ADMINISTER SQL MANAGEMENT OBJECT가 필요하다. baseline은 정확히 같은 SQL 텍스트(signature)에 적용되므로 리터럴을 사용하는 SQL에는 도움이 되지 않는다. 계획이 사용하는 인덱스가 삭제되면 계획을 재현할 수 없고(REPRODUCED=NO) 옵티마이저는 비용 기반 선택으로 돌아간다. OPTIMIZER_USE_SQL_PLAN_BASELINES는 TRUE(기본값)여야 한다.","note":"좋은 계획이 더 이상 cursor cache에 없으면 AWR에서 로드한다: 12.2+ DBMS_SPM.LOAD_PLANS_FROM_AWR(begin_snap, end_snap, basic_filter => q'[sql_id='<sql_id>']'); 이전 릴리스에서는 SQL Tuning Set을 거쳐 LOAD_PLANS_FROM_SQLSET을 사용한다.","tags":["spm","sql plan baseline","dbms_spm","load_plans_from_cursor_cache","plan pinning","plan regression","plan_hash_value"]},{"id":"spm-evolve-baseline","task":"수락되지 않은 SQL plan baseline 계획 evolve","category":"성능","command":"-- 1) Plans waiting for acceptance:\nSELECT sql_handle, plan_name, origin, enabled, accepted, fixed,\n       TO_CHAR(created,'YYYY-MM-DD HH24:MI') AS created\nFROM   dba_sql_plan_baselines\nWHERE  accepted = 'NO'\nORDER  BY created DESC;\n\n-- 2) Evolve task: test-executes the candidate plan against the accepted plan\nVAR tname VARCHAR2(128)\nVAR ename VARCHAR2(128)\nBEGIN\n  :tname := DBMS_SPM.CREATE_EVOLVE_TASK(\n              sql_handle => '<sql_handle>',\n              time_limit => 600,\n              task_name  => 'EVOLVE_<sql_handle>');\n  :ename := DBMS_SPM.EXECUTE_EVOLVE_TASK(task_name => :tname);\nEND;\n/\n\nSET LONG 1000000 LONGCHUNKSIZE 1000000 LINESIZE 200 PAGESIZE 0\nSELECT DBMS_SPM.REPORT_EVOLVE_TASK(\n         task_name => :tname, execution_name => :ename,\n         type => 'TEXT', level => 'TYPICAL', section => 'ALL')\nFROM   dual;\n\n-- 3) If the report has recommendations (plans proven better), accept them:\nSET SERVEROUTPUT ON\nDECLARE\n  n PLS_INTEGER;\nBEGIN\n  n := DBMS_SPM.IMPLEMENT_EVOLVE_TASK(task_name => :tname);\n  DBMS_OUTPUT.PUT_LINE(n || ' plan(s) accepted');\nEND;\n/\nEXEC DBMS_SPM.DROP_EVOLVE_TASK(task_name => :tname);","what":"baseline이 있으면 옵티마이저가 찾은 새 계획은 ACCEPTED=NO로 저장된다. evolve 작업은 이 후보들을 accepted 계획과 비교해 테스트 실행하고 확실히 더 나은 계획만 수락한다.","when":"baseline이 있는 SQL에서 새 인덱스나 새 통계로 더 나은 계획이 생겼을 수 있을 때. 자동 SYS_AUTO_SPM_EVOLVE_TASK(12c+)를 기다리지 않고 수동으로 결정하기 위해.","caution":"Enterprise Edition이 필요하다. evolve는 SQL을 실제로 실행한다(DML은 쿼리 부분만). 무거운 SQL에는 time_limit을 설정하고 피크 시간을 피한다. ADMINISTER SQL MANAGEMENT OBJECT와 ADVISOR가 필요하다. SYS로 파싱된 SQL은 evolve할 수 없다(ORA-16963). 애플리케이션 사용자로 실행한다.","since":"Oracle 12c","note":"12c 이전의 DBMS_SPM.EVOLVE_SQL_PLAN_BASELINE 함수는 deprecated이며, 12c+는 작업 기반 API(CREATE/EXECUTE/IMPLEMENT_EVOLVE_TASK)를 사용한다. 자동 작업 동작: DBMS_SPM.SET_EVOLVE_TASK_PARAMETER('SYS_AUTO_SPM_EVOLVE_TASK', 'ACCEPT_PLANS', 'FALSE').","tags":["spm","evolve","create_evolve_task","baseline","plan acceptance","dbms_spm","accepted"]},{"id":"spm-alter-drop-baseline","task":"SQL plan baseline 계획 고정, 비활성화, 삭제","category":"성능","command":"-- Show the plan content:\nSELECT *\nFROM   TABLE(DBMS_XPLAN.DISPLAY_SQL_PLAN_BASELINE(\n         sql_handle => '<sql_handle>',\n         plan_name  => '<plan_name>',\n         format     => 'TYPICAL'));\n\nSET SERVEROUTPUT ON\nDECLARE\n  n PLS_INTEGER;\nBEGIN\n  -- FIXED: this plan is preferred, new plans are not evolved automatically\n  n := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(\n         sql_handle => '<sql_handle>', plan_name => '<plan_name>',\n         attribute_name => 'fixed', attribute_value => 'YES');\n\n  -- Take the bad plan out of use (without dropping it):\n  n := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(\n         sql_handle => '<sql_handle>', plan_name => '<bad_plan_name>',\n         attribute_name => 'enabled', attribute_value => 'NO');\nEND;\n/\n\n-- Drop one plan or the SQL's entire baseline:\nDECLARE\n  n PLS_INTEGER;\nBEGIN\n  n := DBMS_SPM.DROP_SQL_PLAN_BASELINE(sql_handle => '<sql_handle>', plan_name => '<plan_name>');\n  -- without plan_name, all plans of the SQL are dropped:\n  -- n := DBMS_SPM.DROP_SQL_PLAN_BASELINE(sql_handle => '<sql_handle>');\n  DBMS_OUTPUT.PUT_LINE(n || ' plan(s) dropped');\nEND;\n/","what":"baseline 계획 속성을 관리한다. FIXED 계획은 다른 accepted 계획보다 우선하며 새 계획의 자동 evolve를 멈춘다. ENABLED=NO는 삭제하지 않고 계획을 비활성화한다. DROP은 영구적으로 제거한다.","when":"중요 SQL의 계획을 확실히 고정하거나, 실수로 수락된 나쁜 계획을 비활성화하거나, 더 이상 필요 없는 baseline을 정리하기 위해.","caution":"Enterprise Edition이 필요하다. FIXED 계획은 데이터가 커지면서 성능이 저하될 수 있고 옵티마이저가 더 나은 계획을 쓰지 못하게 하므로 주기적으로 검토한다. 모든 계획이 ENABLED=NO인 SQL은 자유롭게 최적화된다. DROP은 되돌릴 수 없으므로 먼저 DBMS_SPM.PACK_STGTAB_BASELINE으로 staging 테이블에 백업한다.","tags":["spm","baseline","fixed","alter_sql_plan_baseline","drop_sql_plan_baseline","disable plan","display_sql_plan_baseline"]},{"id":"bind-sensitivity-acs-check","task":"bind peeking 및 adaptive cursor sharing 상태 확인","category":"성능","command":"-- 1) Child cursors: bind-sensitive / bind-aware / shareable?\nSELECT sql_id, child_number, executions, plan_hash_value,\n       is_bind_sensitive, is_bind_aware, is_shareable,\n       ROUND(buffer_gets/NULLIF(executions,0)) AS gets_per_exec\nFROM   v$sql\nWHERE  sql_id = '<sql_id>'\nORDER  BY child_number;\n\n-- 2) ACS histogram and selectivity ranges:\nSELECT child_number, bucket_id, count\nFROM   v$sql_cs_histogram\nWHERE  sql_id = '<sql_id>'\nORDER  BY child_number, bucket_id;\n\nSELECT child_number, predicate, range_id, low, high\nFROM   v$sql_cs_selectivity\nWHERE  sql_id = '<sql_id>';\n\n-- 3) Peeked / captured bind values:\nSELECT child_number, name, position, datatype_string, value_string,\n       TO_CHAR(last_captured,'YYYY-MM-DD HH24:MI:SS') AS captured\nFROM   v$sql_bind_capture\nWHERE  sql_id = '<sql_id>'\nORDER  BY child_number, position;\n\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('<sql_id>', NULL, 'TYPICAL +PEEKED_BINDS'));\n\n-- 4) Why was a new child created?\nSELECT child_number, bind_mismatch, bind_equiv_failure,\n       optimizer_mismatch, load_optimizer_stats\nFROM   v$sql_shared_cursor\nWHERE  sql_id = '<sql_id>';","what":"옵티마이저는 첫 hard parse 시 bind 값을 엿보고(peek) 계획을 선택한다. adaptive cursor sharing은 히스토그램이 있는 컬럼에서 bind 값별로 다른 child cursor/계획을 만들 수 있다. 이 쿼리들은 커서가 bind-sensitive/aware인지, 어떤 값이 peek되었는지, child가 왜 생성되었는지 보여 준다.","when":"\"같은 SQL이 어떤 때는 빠르고 어떤 때는 느리다\"는 불만, 치우친 데이터 분포, child cursor 수가 폭증할 때(high version count).","caution":"라이선스가 필요 없으며 읽기 전용이다. V$SQL_BIND_CAPTURE는 샘플링되며(기본적으로 약 15분마다) WHERE/HAVING bind만 보관하므로 모든 실행의 값이 아니다. ACS는 몇 번의 나쁜 실행 후에야 커서를 bind-aware로 만든다. 중요 SQL에는 ACS에 의존하기보다 여러 계획을 SQL plan baseline에 로드하는 것이 더 결정적이다.","since":"Oracle 11g","note":"adaptive cursor sharing은 11g에서 도입되었다. high version count 분석에는 V$SQL_SHARED_CURSOR의 REASON 컬럼(XML)이 상세 원인을 제공한다.","tags":["bind peeking","adaptive cursor sharing","acs","bind variable","v$sql_bind_capture","child cursor","version count"]},{"id":"top-sql-awr-history","task":"과거 기간의 경과 시간/CPU 기준 상위 SQL 찾기(AWR + V$SQLAREA)","category":"성능","command":"-- AWR: top 20 by elapsed time within a snapshot range (per plan)\nSELECT *\nFROM  (SELECT st.sql_id,\n              st.plan_hash_value,\n              SUM(st.executions_delta)                       AS execs,\n              ROUND(SUM(st.elapsed_time_delta)/1e6, 1)       AS elapsed_s,\n              ROUND(SUM(st.cpu_time_delta)/1e6, 1)           AS cpu_s,\n              ROUND(SUM(st.iowait_delta)/1e6, 1)             AS io_wait_s,\n              SUM(st.buffer_gets_delta)                      AS buffer_gets,\n              ROUND(SUM(st.elapsed_time_delta)/1e3\n                    / NULLIF(SUM(st.executions_delta),0), 2) AS ms_per_exec,\n              MAX(st.parsing_schema_name)                    AS parsing_schema\n       FROM   dba_hist_sqlstat st\n       WHERE  st.dbid = (SELECT dbid FROM v$database)\n         AND  st.snap_id BETWEEN <begin_snap> AND <end_snap>\n       GROUP  BY st.sql_id, st.plan_hash_value\n       ORDER  BY elapsed_s DESC)\nWHERE ROWNUM <= 20;\n-- For CPU ranking: ORDER BY cpu_s DESC\n-- SQL text: SELECT DBMS_LOB.SUBSTR(sql_text, 4000, 1) FROM dba_hist_sqltext WHERE sql_id = '<sql_id>';\n-- Plan history of a single SQL: @?/rdbms/admin/awrsqrpt.sql\n\n-- Current (since instance startup, still in cache) by CPU:\nSELECT sql_id,\n       ROUND(cpu_time/1e6,1)     AS cpu_s,\n       ROUND(elapsed_time/1e6,1) AS elapsed_s,\n       executions, buffer_gets, disk_reads,\n       ROUND(cpu_time/1e3/NULLIF(executions,0),2) AS cpu_ms_per_exec,\n       parsing_schema_name,\n       SUBSTR(sql_text,1,80) AS sql_text\nFROM   v$sqlarea\nWHERE  parsing_schema_name NOT IN ('SYS','SYSTEM')\nORDER  BY cpu_time DESC\nFETCH FIRST 20 ROWS ONLY;","what":"DBA_HIST_SQLSTAT의 *_DELTA 컬럼을 합산하여 스냅샷 범위에서 가장 비용이 큰 SQL을 plan_hash_value별로 나열하며, 이를 통해 같은 SQL의 계획 변경도 드러난다. V$SQLAREA 쿼리는 아직 캐시에 있는 항목의 누적 값을 제공한다.","when":"\"어젯밤 02:00~04:00에 무엇이 시스템에 부하를 줬나?\", 계획 변경 탐지(같은 sql_id, 다른 plan_hash_value), 튜닝 후보 목록 작성용.","caution":"DBA_HIST_* 조회에는 Diagnostics Pack 라이선스가 필요하지만 V$SQLAREA는 필요 없다. AWR은 상위 SQL만(기본 top-N) 보관하므로 작지만 매우 빈번한 문장은 누락될 수 있다. V$SQLAREA 값은 커서가 age out되면 사라진다. delta 값은 재시작을 넘어서도 정확하지만 캐시 기반 쿼리는 0부터 다시 시작한다.","tags":["top sql","dba_hist_sqlstat","v$sqlarea","elapsed","cpu","most expensive query","plan change","diagnostics pack"]},{"id":"stats-stale-prefs","task":"오래된(stale) 통계 찾기 및 테이블별 통계 선호 설정(prefs)","category":"통계","command":"-- 1) Flush DML counters to the dictionary and list stale objects:\nEXEC DBMS_STATS.FLUSH_DATABASE_MONITORING_INFO;\n\nSELECT owner, table_name, partition_name, num_rows,\n       TO_CHAR(last_analyzed,'YYYY-MM-DD HH24:MI') AS last_analyzed,\n       stale_stats, stattype_locked\nFROM   dba_tab_statistics\nWHERE  owner = 'CUSTOMER_APP'\n  AND  (stale_stats = 'YES' OR last_analyzed IS NULL)\nORDER  BY table_name, partition_name;\n\nSELECT table_name, partition_name, inserts, updates, deletes, truncated\nFROM   dba_tab_modifications\nWHERE  table_owner = 'CUSTOMER_APP';\n\n-- 2) Per-table preferences (used by the auto stats job and manual GATHER calls):\nEXEC DBMS_STATS.SET_TABLE_PREFS('CUSTOMER_APP', 'SALES', 'STALE_PERCENT', '5');\nEXEC DBMS_STATS.SET_TABLE_PREFS('CUSTOMER_APP', 'SALES', 'METHOD_OPT', 'FOR ALL COLUMNS SIZE AUTO FOR COLUMNS SIZE 254 CUSTOMER_ID');\nEXEC DBMS_STATS.SET_TABLE_PREFS('CUSTOMER_APP', 'SALES', 'DEGREE', '4');\n\nSELECT DBMS_STATS.GET_PREFS('STALE_PERCENT', 'CUSTOMER_APP', 'SALES') AS stale_pct FROM dual;\nSELECT table_name, preference_name, preference_value\nFROM   dba_tab_stat_prefs WHERE owner = 'CUSTOMER_APP';\n\n-- 3) Gather only stale objects:\nEXEC DBMS_STATS.GATHER_SCHEMA_STATS(ownname => 'CUSTOMER_APP', options => 'GATHER STALE');\n-- Revert a preference: EXEC DBMS_STATS.DELETE_TABLE_PREFS('CUSTOMER_APP', 'SALES', 'STALE_PERCENT');","what":"변경 비율이 STALE_PERCENT(기본값 10%)를 넘으면 통계가 stale 상태가 된다. 선호 설정(preference)은 테이블별 샘플링, 히스토그램, 병렬도, stale 임계값을 영구 저장하여 야간 자동 통계 작업이 같은 규칙을 적용하게 한다.","when":"대형 테이블에서 10% 임계값이 너무 늦게 동작할 때, 특정 컬럼에 항상 히스토그램이 필요할 때, 수집 전략을 스크립트가 아닌 딕셔너리에 정의하기 위해.","caution":"라이선스가 필요 없다. 수동 GATHER 호출의 명시적 파라미터가 선호 설정보다 우선한다. METHOD_OPT를 변경하면 히스토그램이 추가/제거되어 계획이 바뀔 수 있다. DBA_TAB_MODIFICATIONS는 flush 후에만 최신이다(자동 flush는 약 15분마다).","tags":["stale stats","stale_percent","set_table_prefs","dba_tab_statistics","dba_tab_modifications","statistics preference","gather stale"]},{"id":"stats-lock-unlock","task":"테이블/스키마 통계 잠금 및 해제","category":"통계","command":"-- Lock (the auto stats job and normal GATHER calls no longer touch them):\nEXEC DBMS_STATS.LOCK_TABLE_STATS(ownname => 'CUSTOMER_APP', tabname => 'ORDERS');\nEXEC DBMS_STATS.LOCK_SCHEMA_STATS(ownname => 'CUSTOMER_APP');\n-- A single partition only: DBMS_STATS.LOCK_PARTITION_STATS('CUSTOMER_APP', 'SALE_PART', 'P2026_H1');\n\n-- Locked objects:\nSELECT owner, table_name, partition_name, stattype_locked\nFROM   dba_tab_statistics\nWHERE  owner = 'CUSTOMER_APP' AND stattype_locked IS NOT NULL;\n\n-- Deliberate gathering while locked (instead of ORA-20005):\nEXEC DBMS_STATS.GATHER_TABLE_STATS('CUSTOMER_APP', 'ORDERS', force => TRUE);\n\n-- Unlock:\nEXEC DBMS_STATS.UNLOCK_SCHEMA_STATS(ownname => 'CUSTOMER_APP');\nEXEC DBMS_STATS.UNLOCK_TABLE_STATS(ownname => 'CUSTOMER_APP', tabname => 'ORDERS');","what":"통계를 변경되지 않도록 잠근다. 자동 작업은 잠긴 객체를 건너뛰고 테이블 수준 GATHER 호출은 ORA-20005를 발생시킨다. force=>TRUE는 잠금을 해제하지 않고 수집한다.","when":"하루 중 비워졌다가 다시 채워지는 staging/휘발성 테이블의 대표 통계를 유지하거나, 중요 테이블의 계획을 통계 변경으로부터 보호하거나, DBMS_STATS.SET_TABLE_STATS로 설정한 값을 보존하기 위해.","caution":"라이선스가 필요 없다. 잠긴 통계는 시간이 지나면서 실제와 멀어져 카디널리티 오류를 일으키므로 잠긴 객체를 정기적으로 보고한다. 스키마 잠금은 나중에 스키마에 추가된 테이블에는 적용되지 않는다. 잠긴 테이블에서는 CREATE/REBUILD INDEX도 인덱스 통계를 자동으로 수집하지 않는다.","tags":["lock stats","lock statistics","unlock_table_stats","lock_schema_stats","ora-20005","stattype_locked","dbms_stats"]},{"id":"stats-restore-history","task":"통계를 과거 시점으로 복원","category":"통계","command":"-- 1) Retention and the oldest point you can return to:\nSELECT DBMS_STATS.GET_STATS_HISTORY_RETENTION    AS retention_days,\n       DBMS_STATS.GET_STATS_HISTORY_AVAILABILITY AS oldest_available\nFROM   dual;\n\n-- 2) Statistics history of the table:\nSELECT table_name, partition_name,\n       TO_CHAR(stats_update_time,'YYYY-MM-DD HH24:MI:SS TZH:TZM') AS stats_update_time\nFROM   dba_tab_stats_history\nWHERE  owner = 'CUSTOMER_APP' AND table_name = 'SALES'\nORDER  BY stats_update_time DESC;\n\n-- 3) Return to the point before the plan regressed:\nBEGIN\n  DBMS_STATS.RESTORE_TABLE_STATS(\n    ownname         => 'CUSTOMER_APP',\n    tabname         => 'SALES',\n    as_of_timestamp => TIMESTAMP '2026-10-07 22:00:00 +03:00',\n    no_invalidate   => FALSE);\nEND;\n/\n-- Whole schema: DBMS_STATS.RESTORE_SCHEMA_STATS('CUSTOMER_APP', <timestamp>)\n\n-- Change the retention (days):\n-- EXEC DBMS_STATS.ALTER_STATS_HISTORY_RETENTION(31);","what":"DBMS_STATS는 수집할 때마다 이전 통계를 SYSAUX에 저장한다(기본 31일). RESTORE_* 프로시저는 테이블/스키마 통계를 지정한 시점의 상태로 되돌린다. no_invalidate=>FALSE이면 커서가 즉시 다시 파싱된다.","when":"야간 통계 수집 후 실행 계획 회귀가 발생했을 때 가장 빠른 롤백 수단으로, 변경이 통계 때문인지 테스트하기 위해.","caution":"라이선스가 필요 없다. 잠긴 통계는 복원할 수 없다(먼저 잠금 해제). 다음 자동 수집이 복원 후 새 통계를 다시 가져오므로 영구 해결에는 테이블 선호 설정, 통계 잠금 또는 SQL plan baseline을 사용한다. 보존 기간이 길면 SYSAUX가 커진다(WRI$_OPTSTAT_* 테이블).","tags":["restore stats","restore statistics","restore_table_stats","dba_tab_stats_history","stats history","plan regression","dbms_stats"]},{"id":"stats-pending-publish","task":"통계를 pending으로 수집하여 테스트한 후 publish","category":"통계","command":"-- 1) Disable publishing for the table (new statistics go to pending):\nEXEC DBMS_STATS.SET_TABLE_PREFS('CUSTOMER_APP', 'ORDERS', 'PUBLISH', 'FALSE');\nEXEC DBMS_STATS.GATHER_TABLE_STATS('CUSTOMER_APP', 'ORDERS');\n\n-- 2) Compare pending and published values:\nSELECT owner, table_name, partition_name, num_rows,\n       TO_CHAR(last_analyzed,'YYYY-MM-DD HH24:MI') AS last_analyzed\nFROM   dba_tab_pending_stats\nWHERE  owner = 'CUSTOMER_APP';\nSELECT num_rows, last_analyzed FROM dba_tables\nWHERE  owner = 'CUSTOMER_APP' AND table_name = 'ORDERS';\n\n-- 3) Try plans with pending statistics in this session only:\nALTER SESSION SET optimizer_use_pending_statistics = TRUE;\nEXPLAIN PLAN FOR SELECT COUNT(*) FROM customer_app.orders WHERE status = 7;\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format => 'BASIC +ROWS'));\nALTER SESSION SET optimizer_use_pending_statistics = FALSE;\n\n-- 4a) If good, publish:\nEXEC DBMS_STATS.PUBLISH_PENDING_STATS('CUSTOMER_APP', 'ORDERS', no_invalidate => FALSE);\n-- 4b) If bad, discard:\n-- EXEC DBMS_STATS.DELETE_PENDING_STATS('CUSTOMER_APP', 'ORDERS');\n\n-- 5) Return to normal behavior:\nEXEC DBMS_STATS.SET_TABLE_PREFS('CUSTOMER_APP', 'ORDERS', 'PUBLISH', 'TRUE');","what":"PUBLISH=FALSE 선호 설정이 있으면 수집된 통계는 딕셔너리가 아닌 pending 영역에 기록되어 다른 세션에 영향을 주지 않는다. optimizer_use_pending_statistics를 사용하는 테스트 세션에서 계획을 검증한 후 한 명령으로 publish하거나 폐기한다.","when":"운영 환경의 중요 OLTP 테이블에서 갱신된 통계가 계획에 미치는 영향을 위험 없이 테스트하고, 대량 데이터 적재 후 통제된 전환을 위해.","caution":"라이선스가 필요 없다. PUBLISH=FALSE가 남아 있는 동안 자동 작업이 수집한 통계도 pending으로 쌓이고 publish되지 않으므로 선호 설정을 TRUE로 되돌리는 것을 잊지 않는다. EXPLAIN PLAN은 추정치이므로 가능하면 쿼리를 실행하고 DISPLAY_CURSOR로 확인한다.","since":"Oracle 11g","tags":["pending stats","publish","publish_pending_stats","optimizer_use_pending_statistics","statistics testing","dbms_stats","dba_tab_pending_stats"]},{"id":"stats-incremental-partitioned","task":"파티션 테이블의 incremental 통계 구성","category":"통계","command":"-- 1) Enable incremental mode (global statistics are derived from partition synopses):\nEXEC DBMS_STATS.SET_TABLE_PREFS('CUSTOMER_APP', 'SALE_PART', 'INCREMENTAL', 'TRUE');\nEXEC DBMS_STATS.SET_TABLE_PREFS('CUSTOMER_APP', 'SALE_PART', 'INCREMENTAL_STALENESS', 'USE_STALE_PERCENT');\nEXEC DBMS_STATS.SET_TABLE_PREFS('CUSTOMER_APP', 'SALE_PART', 'GRANULARITY', 'AUTO');\n\n-- 2) The first gather scans everything and builds synopses; later ones read only changed partitions:\nEXEC DBMS_STATS.GATHER_TABLE_STATS(ownname => 'CUSTOMER_APP', tabname => 'SALE_PART');\n\n-- 3) Verify:\nSELECT DBMS_STATS.GET_PREFS('INCREMENTAL', 'CUSTOMER_APP', 'SALE_PART') AS incremental FROM dual;\nSELECT partition_name, num_rows,\n       TO_CHAR(last_analyzed,'YYYY-MM-DD HH24:MI:SS') AS last_analyzed, stale_stats\nFROM   dba_tab_statistics\nWHERE  owner = 'CUSTOMER_APP' AND table_name = 'SALE_PART'\nORDER  BY partition_position NULLS FIRST;\n\n-- 4) Pre-build synopses on a staging table to be loaded via exchange (12c+):\n-- EXEC DBMS_STATS.SET_TABLE_PREFS('CUSTOMER_APP', 'SALE_STAGE', 'INCREMENTAL', 'TRUE');\n-- EXEC DBMS_STATS.SET_TABLE_PREFS('CUSTOMER_APP', 'SALE_STAGE', 'INCREMENTAL_LEVEL', 'TABLE');\n-- EXEC DBMS_STATS.GATHER_TABLE_STATS('CUSTOMER_APP', 'SALE_STAGE');","what":"incremental 모드에서는 파티션별로 synopsis가 저장된다. 전역(테이블 수준) 통계, 특히 NDV는 테이블 전체를 다시 스캔하지 않고 synopsis를 병합해 계산한다. 변경된 파티션만 다시 읽는다.","when":"새 데이터가 최근 파티션에만 들어오는 대형(TB 규모) range/interval 파티션 테이블에서, 전역 통계 수집이 유지보수 시간대를 초과할 때.","caution":"자체 라이선스는 없지만 Partitioning 옵션(유료 EE)이 이미 필요하다. synopsis는 SYSAUX 공간을 사용한다(12.2+ HyperLogLog 형식은 훨씬 작다). 첫 수집은 전체 스캔이므로 유지보수 시간대에 시작한다. ESTIMATE_PERCENT를 AUTO_SAMPLE_SIZE 이외의 값으로 설정하면 incremental이 비활성화된다.","since":"Oracle 11g","note":"INCREMENTAL_STALENESS와 INCREMENTAL_LEVEL 선호 설정은 12c에서 추가되었다. 12.2+에서 synopsis 형식은 APPROXIMATE_NDV_ALGORITHM(HYPERLOGLOG)으로 제어한다.","tags":["incremental stats","synopsis","partition statistics","global stats","incremental_staleness","dbms_stats","large table"]},{"id":"partition-split-merge-online","task":"온라인으로 파티션 분할(split) 및 병합(merge)","category":"파티셔닝","command":"-- SPLIT: split a range partition in two, keep indexes usable\nALTER TABLE customer_app.sale_part\n  SPLIT PARTITION p2026_h2 AT (DATE '2026-10-01')\n  INTO (PARTITION p2026_q3, PARTITION p2026_q4)\n  UPDATE INDEXES ONLINE;\n\n-- MERGE: merge two adjacent partitions\nALTER TABLE customer_app.sale_part\n  MERGE PARTITIONS p2026_q3, p2026_q4 INTO PARTITION p2026_h2\n  UPDATE INDEXES ONLINE;\n\n-- ADD: new partition at the end (when there is no MAXVALUE)\nALTER TABLE customer_app.sale_part\n  ADD PARTITION p2027 VALUES LESS THAN (DATE '2028-01-01');\n\n-- Check: partition bounds and UNUSABLE index partitions\nSELECT partition_name, partition_position, high_value, num_rows\nFROM   dba_tab_partitions\nWHERE  table_owner = 'CUSTOMER_APP' AND table_name = 'SALE_PART'\nORDER  BY partition_position;\n\nSELECT index_name, partition_name, status\nFROM   dba_ind_partitions\nWHERE  index_owner = 'CUSTOMER_APP' AND status <> 'USABLE';","what":"SPLIT은 경계값에서 파티션을 둘로(12c+: 여러 개로) 나누고, MERGE는 인접 파티션을 합친다. ONLINE을 사용하면 작업 중에도 DML이 계속되고, UPDATE INDEXES를 사용하면 로컬 및 전역 인덱스가 UNUSABLE이 되지 않는다.","when":"커진 MAXVALUE/\"기타\" 파티션을 분할하거나, 아카이브 전에 작은 오래된 파티션을 병합하거나, 파티션 구성을 변경할 때(월별에서 분기별로).","caution":"Partitioning 옵션(유료 EE)이 필요하며, ONLINE은 Enterprise Edition이 필요하다. SPLIT/MERGE는 데이터를 물리적으로 다시 쓰므로 대형 파티션에서는 느리고 redo/undo와 임시 공간을 생성한다. ONLINE 없이는 작업 중 테이블이 DML에 대해 잠기고, UPDATE INDEXES 없이는 전역 인덱스가 UNUSABLE이 된다. 빈 파티션 분할은 빠르다(fast split).","since":"Oracle 12.2","note":"ONLINE SPLIT은 12.2, ONLINE MERGE는 18c에 도입되었으며 둘 다 19c에서 사용할 수 있다. 이전 릴리스에서는 ONLINE 키워드를 빼고 유지보수 시간대에 작업을 실행한다.","tags":["split partition","merge partition","add partition","split a partition","merge partitions","update indexes","online","partitioning"]},{"id":"partition-exchange","task":"exchange partition으로 대량 적재 또는 데이터 아카이브","category":"파티셔닝","command":"-- 1) Staging table that exactly matches the target (12.2+):\nCREATE TABLE customer_app.sale_stage\n  FOR EXCHANGE WITH TABLE customer_app.sale_part;\n\n-- 2) Load the staging table and build indexes matching the local indexes:\nINSERT /*+ APPEND */ INTO customer_app.sale_stage\nSELECT * FROM customer_app.sale_load WHERE created_at >= DATE '2027-01-01';\nCOMMIT;\nCREATE INDEX customer_app.sale_stage_customer_ix ON customer_app.sale_stage (customer_id);\n\n-- 3) Swap (dictionary-only operation, no data movement):\nALTER TABLE customer_app.sale_part\n  EXCHANGE PARTITION p2027 WITH TABLE customer_app.sale_stage\n  INCLUDING INDEXES WITHOUT VALIDATION\n  UPDATE GLOBAL INDEXES;\n\n-- 4) Verify:\nSELECT COUNT(*) FROM customer_app.sale_part PARTITION (p2027);\nSELECT index_name, partition_name, status\nFROM   dba_ind_partitions\nWHERE  index_owner = 'CUSTOMER_APP' AND status <> 'USABLE';","what":"딕셔너리에서 파티션의 세그먼트와 일반 테이블의 세그먼트를 맞바꾼다. 수백만 행이 몇 초 만에 테이블로 \"들어오거나\" 아카이브를 위해 \"나간다\". FOR EXCHANGE WITH TABLE은 컬럼 및 숨은 컬럼 호환성을 보장하는 staging 테이블을 만든다.","when":"DWH/ETL 대량 적재, 오래된 파티션을 아카이브 테이블로 이동, 적재 중 운영 테이블에 영향을 주지 않고 인덱스와 통계를 준비할 때.","caution":"Partitioning 옵션(유료 EE)이 필요하다. WITHOUT VALIDATION은 staging 행이 파티션 경계에 맞는지 확인하지 않는다. 잘못된 데이터가 잘못된 파티션에 남아 쿼리가 잘못된 결과를 반환할 수 있으므로 확신이 없으면 WITH VALIDATION(전체 스캔)을 사용한다. UPDATE GLOBAL INDEXES 없이는 전역 인덱스가 UNUSABLE이 된다. unique/PK 제약 조건 상태는 양쪽이 일치해야 한다.","note":"CREATE TABLE ... FOR EXCHANGE WITH TABLE은 12.2에서 도입되었다. 그 이전에는 CREATE TABLE ... AS SELECT * FROM <table> WHERE 1=0으로 staging 테이블을 만들고 숨은/가상 컬럼 차이를 수동으로 확인한다.","tags":["exchange partition","staging","for exchange with table","bulk load","archive","update global indexes","partitioning"]},{"id":"partition-drop-truncate-gidx","task":"전역 인덱스를 사용 가능한 상태로 유지하며 파티션 truncate/drop","category":"파티셔닝","command":"-- Empty a partition / remove a partition (global indexes stay maintained):\nALTER TABLE customer_app.sale_part TRUNCATE PARTITION p2025 UPDATE INDEXES;\nALTER TABLE customer_app.sale_part DROP PARTITION p2025 UPDATE INDEXES;\n\n-- 12c+ asynchronous global index maintenance: index is VALID but carries orphaned entries\nSELECT index_name, status, orphaned_entries\nFROM   dba_indexes\nWHERE  owner = 'CUSTOMER_APP' AND table_name = 'SALE_PART';\n\n-- Clean orphaned entries now (normally SYS.PMO_DEFERRED_GIDX_MAINT_JOB does it at night):\nEXEC DBMS_PART.CLEANUP_GIDX(schema_name_in => 'CUSTOMER_APP', table_name_in => 'SALE_PART');\n-- or a single index: ALTER INDEX customer_app.sale_part_id_gidx COALESCE CLEANUP;\n\n-- Target the partition by value instead of name:\n-- ALTER TABLE customer_app.sale_part DROP PARTITION FOR (DATE '2025-06-15') UPDATE INDEXES;","what":"행 단위 DELETE 대신 파티션 수준에서 오래된 데이터를 즉시 제거한다. 12c+에서 UPDATE INDEXES는 모든 항목을 갱신하는 대신 딕셔너리에서 전역 인덱스만 표시한다(비동기 유지보수). 인덱스는 즉시 사용 가능하며 고아 항목은 나중에 정리된다.","when":"데이터 보존 정책에 따라 오래된 기간을 삭제할 때, ILM/purge 작업에서, 대형 테이블에서 DELETE의 undo/redo 부하를 피하기 위해.","caution":"Partitioning 옵션(유료 EE)이 필요하다. DROP/TRUNCATE는 되돌릴 수 없다(flashback table로 파티션을 되살릴 수 없음). 먼저 백업하거나 exchange로 아카이브한다. UPDATE INDEXES를 잊으면 전역 인덱스가 UNUSABLE이 되어 쿼리가 ORA-01502를 만날 수 있다. 자식 테이블이 참조(FK)하면 작업이 실패한다. interval 테이블에서는 마지막 range(transition) 파티션을 drop할 수 없다.","since":"Oracle 12c","tags":["drop partition","truncate partition","update indexes","orphaned_entries","cleanup_gidx","global index","data purge","partitioning"]},{"id":"interval-partitioning","task":"interval 파티셔닝 생성 및 기존 range 테이블을 interval로 전환","category":"파티셔닝","command":"-- New table: a partition is created automatically for each month\nCREATE TABLE customer_app.measurements (\n  ts        DATE   NOT NULL,\n  sensor_id NUMBER,\n  value     NUMBER)\nPARTITION BY RANGE (ts) INTERVAL (NUMTOYMINTERVAL(1, 'MONTH'))\n( PARTITION p_first VALUES LESS THAN (DATE '2026-01-01') );\n\n-- Convert an existing range table to interval (there must be no MAXVALUE partition):\nALTER TABLE customer_app.sale_part SET INTERVAL (NUMTOYMINTERVAL(1, 'MONTH'));\n-- Back to range: ALTER TABLE customer_app.sale_part SET INTERVAL ();\n\n-- Rename a system-named partition to a meaningful name:\nALTER TABLE customer_app.measurements RENAME PARTITION FOR (DATE '2026-03-15') TO p2026_03;\n\n-- Check:\nSELECT partition_name, partition_position, interval, high_value\nFROM   dba_tab_partitions\nWHERE  table_owner = 'CUSTOMER_APP' AND table_name = 'MEASUREMENTS'\nORDER  BY partition_position;\n\nSELECT table_name, partitioning_type, interval\nFROM   dba_part_tables\nWHERE  owner = 'CUSTOMER_APP';","what":"interval 파티셔닝은 마지막 경계를 넘는 첫 행이 들어오면 새 파티션을 자동으로 생성하여 다음 달 파티션을 잊어 생기는 ORA-14400 오류를 없앤다. SET INTERVAL은 데이터를 옮기지 않고 기존 range 테이블을 전환한다.","when":"시계열, 로그, 주문처럼 날짜 기준으로 커지는 테이블, 파티션 유지보수 스크립트를 줄이기 위해.","caution":"Partitioning 옵션(유료 EE)이 필요하다. 파티션 키는 단일 NUMBER/DATE/TIMESTAMP 컬럼이어야 하며 MAXVALUE 파티션은 먼저 제거해야 한다. 잘못되었거나 극단적인 날짜(예: 9999년)는 먼 미래의 파티션을 열어 파티션 번호 체계를 왜곡한다. 간격이 좁으면 1,048,575개 파티션 한도(ORA-14300)에 근접할 수 있으므로 애플리케이션에서 날짜를 검증한다. 자동 파티션은 테이블의 기본 테이블스페이스에 생성된다(STORE IN으로 지정).","since":"Oracle 11g","tags":["interval partitioning","interval","automatic partition","numtoyminterval","ora-14400","set interval","range partition","partitioning"]},{"id":"partition-wise-join-check","task":"파티션 키 및 partition-wise join 사용 확인","category":"파티셔닝","command":"-- 1) Do both tables have the same partitioning type, key and count?\nSELECT p.owner, p.table_name, p.partitioning_type, k.column_name,\n       p.partition_count, p.subpartitioning_type\nFROM   dba_part_tables p\n  JOIN dba_part_key_columns k\n    ON k.owner = p.owner AND k.name = p.table_name AND k.object_type = 'TABLE'\nWHERE  p.owner = 'CUSTOMER_APP'\n  AND  p.table_name IN ('CUSTOMER_H', 'ORDERS_H');\n\n-- 2) Explain the join and inspect the plan shape:\nEXPLAIN PLAN FOR\nSELECT /*+ PARALLEL(4) */ m.city, SUM(s.amount)\nFROM   customer_app.customer_h m\n  JOIN customer_app.orders_h s ON s.customer_id = m.id\nGROUP  BY m.city;\n\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format => 'BASIC +PARALLEL +PARTITION'));\n-- Full PWJ: HASH JOIN sits BELOW the PARTITION HASH ALL line (PX PARTITION HASH ALL in parallel)\n-- Partial PWJ: PX SEND PARTITION (KEY) on one side only\n-- No PWJ: PX SEND HASH / BROADCAST on both sides","what":"조인 키로 equi-partition된 두 테이블은 파티션 쌍 단위로 조인할 수 있으며(full partition-wise join), 이는 병렬 쿼리의 재분배 트래픽과 메모리를 크게 줄인다. 계획 형태로 PWJ 사용 여부를 확인한다.","when":"DWH에서 대형 fact-fact 또는 fact-dimension 조인을 설계할 때, 병렬 쿼리에서 PX 재분배가 많을 때(PX Deq Credit: send blkd), 파티셔닝 전략을 검증할 때.","caution":"라이선스: Partitioning 옵션(유료 EE). 병렬 쿼리는 Enterprise Edition이 필요하다. EXPLAIN PLAN은 추정치이므로 실제 계획은 DISPLAY_CURSOR/SQL Monitor를 사용한다. full PWJ에는 같은 파티션 수(hash는 2의 거듭제곱 권장)와 일치하는 키 데이터 타입이 필요하다. PWJ는 직렬 계획에도 나타날 수 있지만 실제 이득은 병렬 실행에서 나온다.","tags":["partition-wise join","pwj","equi-partitioning","hash partition","parallel query","dba_part_key_columns","partitioning"]},{"id":"online-redefinition-flow","task":"DBMS_REDEFINITION으로 온라인 테이블 재정의(전체 흐름)","category":"온라인 작업","command":"-- 1) Is it eligible? (use CONS_USE_ROWID if there is no PK)\nEXEC DBMS_REDEFINITION.CAN_REDEF_TABLE('CUSTOMER_APP', 'CUSTOMER', DBMS_REDEFINITION.CONS_USE_PK);\n\n-- 2) Interim table with the target structure: hash partitioned in this example\nCREATE TABLE customer_app.customer_int (\n  id NUMBER, name VARCHAR2(100), city VARCHAR2(30))\nPARTITION BY HASH (id) PARTITIONS 8;\n\n-- 3) Start: initial copy + change tracking through an MV log\nBEGIN\n  DBMS_REDEFINITION.START_REDEF_TABLE(\n    uname        => 'CUSTOMER_APP',\n    orig_table   => 'CUSTOMER',\n    int_table    => 'CUSTOMER_INT',\n    col_mapping  => NULL,\n    options_flag => DBMS_REDEFINITION.CONS_USE_PK);\nEND;\n/\n\n-- 4) Copy indexes, constraints, triggers, grants and statistics:\nSET SERVEROUTPUT ON\nDECLARE\n  l_errors PLS_INTEGER;\nBEGIN\n  DBMS_REDEFINITION.COPY_TABLE_DEPENDENTS(\n    uname            => 'CUSTOMER_APP',\n    orig_table       => 'CUSTOMER',\n    int_table        => 'CUSTOMER_INT',\n    copy_indexes     => DBMS_REDEFINITION.CONS_ORIG_PARAMS,\n    copy_triggers    => TRUE,\n    copy_constraints => TRUE,\n    copy_privileges  => TRUE,\n    ignore_errors    => FALSE,\n    num_errors       => l_errors,\n    copy_statistics  => TRUE);\n  DBMS_OUTPUT.PUT_LINE('Error count: ' || l_errors);\nEND;\n/\nSELECT object_name, base_table_name, ddl_txt FROM dba_redefinition_errors;\n\n-- 5) Close the gap before finishing (shortens finish; can be run several times):\nEXEC DBMS_REDEFINITION.SYNC_INTERIM_TABLE('CUSTOMER_APP', 'CUSTOMER', 'CUSTOMER_INT');\n\n-- 6) Swap under a short lock:\nEXEC DBMS_REDEFINITION.FINISH_REDEF_TABLE('CUSTOMER_APP', 'CUSTOMER', 'CUSTOMER_INT', dml_lock_timeout => 60);\n\n-- 7) Drop the interim table, which now holds the old structure:\nDROP TABLE customer_app.customer_int PURGE;\n\n-- On problems: EXEC DBMS_REDEFINITION.ABORT_REDEF_TABLE('CUSTOMER_APP', 'CUSTOMER', 'CUSTOMER_INT');\n-- Status: SELECT * FROM dba_redefinition_status WHERE base_table_owner = 'CUSTOMER_APP';","what":"애플리케이션이 계속 실행되는 동안 테이블 구조(파티셔닝, 컬럼 타입/순서, 테이블스페이스, 압축, LOB 설정)를 변경한다. 데이터는 interim 테이블로 복사되고, 변경은 MV log로 동기화되며, 마지막에 매우 짧은 락하에서 이름이 교체된다.","when":"대형 비파티션 테이블을 다운타임 없이 파티셔닝하거나, 컬럼 타입을 변경하거나, BasicFile LOB을 SecureFiles로 전환하는 등 ALTER TABLE이 온라인으로 할 수 없는 변경용.","caution":"Enterprise Edition이 필요하다(대상이 파티션 테이블이면 Partitioning 옵션도 필요). 이 과정에는 테이블 크기의 두 배 공간이 필요하며 redo와 MV log 오버헤드가 추가된다. FINISH는 짧은 배타 락을 잡으며 장시간 트랜잭션이 있으면 대기한다(dml_lock_timeout). DBA_REDEFINITION_ERRORS에서 COPY_TABLE_DEPENDENTS 오류를 항상 확인한다. FK가 NOVALIDATE로 복사될 수 있다. 먼저 테스트 환경에서 리허설한다.","note":"dml_lock_timeout 파라미터는 12c에서 추가되었다. 12.2+에서는 V$ONLINE_REDEF로 진행 상황을 모니터링한다. 단순 이동/압축 변경에는 DBMS_REDEFINITION 대신 ALTER TABLE ... MOVE ONLINE으로 충분하다.","tags":["dbms_redefinition","online redefinition","redefinition","zero downtime","convert to partitioned","can_redef_table","finish_redef_table","enterprise edition"]},{"id":"table-move-online","task":"테이블 또는 파티션 온라인 이동(ALTER TABLE MOVE ONLINE)","category":"온라인 작업","command":"-- Move the table to another tablespace / reorganize it (DML continues):\nALTER TABLE customer_app.orders MOVE ONLINE TABLESPACE customer_data;\n\n-- Compress while moving (BASIC; for OLTP compression use ROW STORE COMPRESS ADVANCED):\nALTER TABLE customer_app.orders MOVE ONLINE COMPRESS;\n\n-- A single partition:\nALTER TABLE customer_app.sale_part\n  MOVE PARTITION p2026_h1 ONLINE TABLESPACE customer_archive UPDATE INDEXES;\n\n-- Verify: indexes must stay VALID\nSELECT index_name, status FROM dba_indexes\nWHERE  owner = 'CUSTOMER_APP' AND table_name = 'ORDERS';\nSELECT table_name, tablespace_name, compression, compress_for\nFROM   dba_tables\nWHERE  owner = 'CUSTOMER_APP' AND table_name = 'ORDERS';","what":"테이블 세그먼트를 다시 쓴다: 단편화와 high water mark를 줄이고, 테이블스페이스를 변경하며, 압축을 적용한다. ONLINE을 사용하면 작업 중 DML이 블로킹되지 않고 인덱스가 자동으로 유지되어 VALID 상태를 유지한다.","when":"대량 삭제 후 공간을 회수하거나, 테이블을 새 테이블스페이스/스토리지로 옮기거나, 오래된 파티션을 아카이브 테이블스페이스로 압축할 때.","caution":"Enterprise Edition이 필요하며, ROW STORE COMPRESS ADVANCED는 Advanced Compression 옵션(유료)이 필요하다. 작업에는 테이블 크기만큼의 추가 공간이 필요하고 redo가 많이 생성된다. 시작과 끝에 짧은 락을 잡으며 장시간 열린 트랜잭션이 있으면 대기한다. ONLINE이나 UPDATE INDEXES 없는 MOVE는 모든 인덱스를 UNUSABLE로 만든다.","since":"Oracle 12.2","note":"테이블 수준 MOVE ONLINE은 12.2에서 도입되었고, 파티션 수준 MOVE PARTITION ONLINE은 12.1부터 존재한다. 12.1 이하에서는 테이블에 DBMS_REDEFINITION을 사용한다.","tags":["move online","alter table move","move table","reorganization","high water mark","compression","enterprise edition"]},{"id":"tde-keystore-setup","task":"TDE keystore 생성, 열기, master key 설정 및 백업(19c+)","category":"보안","command":"-- 1) Keystore location (static parameter, restart required), then keystore type:\nALTER SYSTEM SET WALLET_ROOT = '/u01/app/oracle/admin/ORCL/wallet' SCOPE = SPFILE;\n-- SHUTDOWN IMMEDIATE;  STARTUP;\nALTER SYSTEM SET TDE_CONFIGURATION = 'KEYSTORE_CONFIGURATION=FILE' SCOPE = BOTH;\n\n-- 2) In CDB root: create a software keystore (under WALLET_ROOT/tde), open it, set the master key\nADMINISTER KEY MANAGEMENT CREATE KEYSTORE IDENTIFIED BY \"<keystore_pwd>\";\nADMINISTER KEY MANAGEMENT SET KEYSTORE OPEN\n  IDENTIFIED BY \"<keystore_pwd>\" CONTAINER = ALL;\nADMINISTER KEY MANAGEMENT SET KEY USING TAG 'mk_2026_10'\n  IDENTIFIED BY \"<keystore_pwd>\" WITH BACKUP USING 'mk_before' CONTAINER = ALL;\n\n-- 3) Open automatically after restart (auto-login, cwallet.sso):\nADMINISTER KEY MANAGEMENT CREATE AUTO_LOGIN KEYSTORE\n  FROM KEYSTORE IDENTIFIED BY \"<keystore_pwd>\";\n\n-- 4) Backup (repeat after every SET KEY / rekey, store SEPARATELY from database backups):\nADMINISTER KEY MANAGEMENT BACKUP KEYSTORE USING 'daily'\n  FORCE KEYSTORE IDENTIFIED BY \"<keystore_pwd>\" TO '/backup/tde/';\n\n-- 5) Status:\nSELECT con_id, wrl_type, wrl_parameter, status, wallet_type, keystore_mode\nFROM   v$encryption_wallet;\nSELECT con_id, key_id, tag, creation_time, activation_time\nFROM   v$encryption_keys ORDER BY creation_time;","what":"Transparent Data Encryption용 소프트웨어 keystore를 united 모드(모든 PDB가 CDB keystore를 공유)로 구성하고, master encryption key를 생성하며, 재시작 후 열리도록 auto-login을 활성화한다. 19c+에서는 sqlnet.ora ENCRYPTION_WALLET_LOCATION 대신 WALLET_ROOT/TDE_CONFIGURATION이 권장 방법이다.","when":"저장 데이터 암호화가 필요한 컴플라이언스 프로젝트(GDPR, PCI-DSS, ISO 27001)에서 테이블스페이스/컬럼 암호화 전 첫 단계로.","caution":"온프레미스에서는 Advanced Security 옵션(유료)이 필요하며 Oracle Cloud(OCI/Autonomous) 라이선스에는 포함되어 있다. WALLET_ROOT는 정적 파라미터이므로 인스턴스 재시작(다운타임)이 필요하다. keystore와 비밀번호를 잃으면 암호화된 데이터는 영구적으로 읽을 수 없다. keystore 백업은 데이터베이스 백업과 분리하여 안전하게 보관한다. auto-login 파일(cwallet.sso)을 가진 사람은 누구나 데이터베이스를 열 수 있으므로 파일 권한을 제한하고 필요한 경우 LOCAL AUTO_LOGIN을 사용한다. Data Guard에서는 keystore를 standby에도 복사한다.","since":"Oracle 18c","note":"WALLET_ROOT와 TDE_CONFIGURATION은 18c에서 도입되어 19c에서 표준이다. 12c-18c의 sqlnet.ora ENCRYPTION_WALLET_LOCATION 방식은 19c에서 deprecated, 23ai에서 desupported이다. 23ai의 기본 알고리즘은 AES256이며, GOST와 SEED를 사용한 암호화는 desupported, 복호화는 deprecated이다. WALLET_ROOT는 정적 파라미터이며 인스턴스 재시작이 필요하다.","tags":["tde","keystore","wallet","master key","encryption","advanced security","wallet_root","administer key management"],"untested":true},{"id":"tde-tablespace-encryption","task":"TDE로 테이블스페이스 암호화(신규 및 기존, 온라인/오프라인)","category":"보안","command":"-- Prerequisite: keystore OPEN and master key set (v$encryption_wallet.status = 'OPEN')\n\n-- 1) New encrypted tablespace:\nCREATE TABLESPACE customer_enc\n  DATAFILE '+DATA' SIZE 1G AUTOEXTEND ON NEXT 256M MAXSIZE 20G\n  ENCRYPTION USING 'AES256' ENCRYPT;\n\n-- 2a) Encrypt an existing tablespace ONLINE (12.2+, needs extra space temporarily):\nALTER TABLESPACE customer_data ENCRYPTION ONLINE USING 'AES256' ENCRYPT;\n-- If not OMF: ... ENCRYPT FILE_NAME_CONVERT = ('customer_data01.dbf', 'customer_data01_enc.dbf');\n\n-- 2b) OFFLINE conversion (no extra space, tablespace unavailable meanwhile):\nALTER TABLESPACE customer_data OFFLINE NORMAL;\nALTER TABLESPACE customer_data ENCRYPTION OFFLINE ENCRYPT;\nALTER TABLESPACE customer_data ONLINE;\n\n-- 3) Progress and status:\nSELECT t.name AS tablespace_name, e.encryptionalg, e.status, e.con_id\nFROM   v$encrypted_tablespaces e\n  JOIN v$tablespace t ON t.ts# = e.ts# AND t.con_id = e.con_id;\nSELECT tablespace_name, encrypted FROM dba_tablespaces ORDER BY tablespace_name;","what":"테이블스페이스 암호화는 데이터 파일에 기록되는 모든 블록을 암호화한다. buffer cache에서는 데이터가 평문이므로 애플리케이션이나 SQL 변경이 필요 없다. 온라인 전환은 DML이 계속되는 동안 데이터 파일의 암호화된 사본을 만들어 전환하고, 오프라인 전환은 블록을 제자리에서 암호화한다.","when":"민감 데이터가 있는 애플리케이션 테이블스페이스를 저장 상태에서 암호화하거나, 데이터를 새 암호화 테이블스페이스로 옮기거나, 감사 지적 사항을 해소할 때.","caution":"온프레미스에서는 Advanced Security 옵션(유료)이 필요하다. 온라인 전환에는 암호화 대상 데이터 파일만큼의 추가 공간이 필요하며 I/O와 redo가 많이 발생한다. Data Guard standby도 같은 작업을 수행한다. 오프라인 전환은 Data Guard standby에서 별도로 실행해야 한다. SYSTEM/SYSAUX/UNDO는 12.2+에서 온라인 암호화가 가능하지만 각별한 주의가 필요하다. 암호화된 테이블스페이스의 백업은 keystore 없이 복원할 수 없다.","since":"Oracle 12.2","note":"온라인/오프라인 테이블스페이스 전환은 12.2에서 도입되었다. 새 테이블스페이스를 자동으로 암호화하려면 19c에서는 TABLESPACE_ENCRYPTION=AUTO_ENABLE(19.16+ RU) 또는 ENCRYPT_NEW_TABLESPACES=ALWAYS를 사용한다. 먼저 keystore가 열려 있고 master key가 설정되어 있어야 한다(tde-keystore-setup 참고).","tags":["tde","tablespace encryption","encryption online","aes256","v$encrypted_tablespaces","advanced security","at-rest"],"untested":true},{"id":"unified-audit-policy-lifecycle","task":"Unified Auditing 정책 생성, 조건부 활성화, 레코드 검토, 제거","category":"보안","command":"-- 1) Object + condition based policy (excluding the batch module):\nCREATE AUDIT POLICY pol_customer_dml\n  ACTIONS SELECT ON customer_app.customer,\n          UPDATE ON customer_app.customer,\n          DELETE ON customer_app.customer\n  WHEN 'SYS_CONTEXT(''USERENV'',''MODULE'') NOT LIKE ''batch%'''\n  EVALUATE PER SESSION;\n\n-- 2) Privileged operations policy:\nCREATE AUDIT POLICY pol_ddl_priv\n  PRIVILEGES DROP ANY TABLE, ALTER SYSTEM\n  ACTIONS DROP USER, ALTER USER, CREATE USER;\n\n-- 3) Enable:\nAUDIT POLICY pol_customer_dml EXCEPT customer_app;   -- everyone except the schema owner\nAUDIT POLICY pol_ddl_priv WHENEVER SUCCESSFUL;\nAUDIT POLICY ORA_LOGON_FAILURES WHENEVER NOT SUCCESSFUL;\n\n-- 4) Which policy is enabled for whom?\nSELECT policy_name, enabled_option, entity_name, success, failure\nFROM   audit_unified_enabled_policies ORDER BY policy_name;\nSELECT policy_name, audit_option, object_schema, object_name, audit_condition\nFROM   audit_unified_policies WHERE policy_name LIKE 'POL_%';\n\n-- 5) Records:\nSELECT event_timestamp, dbusername, action_name, object_schema, object_name,\n       return_code, unified_audit_policies, os_username, userhost\nFROM   unified_audit_trail\nWHERE  event_timestamp > SYSTIMESTAMP - INTERVAL '1' DAY\n  AND  unified_audit_policies LIKE '%POL_%'\nORDER  BY event_timestamp DESC;\n-- 19c (queued-write): EXEC DBMS_AUDIT_MGMT.FLUSH_UNIFIED_AUDIT_TRAIL;  (not in 23ai)\n\n-- 6) Disable and remove:\nNOAUDIT POLICY pol_customer_dml;\nNOAUDIT POLICY pol_ddl_priv;\nDROP AUDIT POLICY pol_customer_dml;\nDROP AUDIT POLICY pol_ddl_priv;","what":"Unified Auditing은 모든 감사 레코드를 단일 trail(AUDSYS 스키마, UNIFIED_AUDIT_TRAIL)에 수집한다. 정책은 객체 작업, 시스템 권한 또는 작업 단위로 정의하며, WHEN 조건과 BY/EXCEPT 사용자 목록으로 노이즈를 줄인다.","when":"민감 테이블 접근을 기록하고, 권한이 필요한 DDL과 권한 변경을 모니터링하며, 실패한 로그온 시도를 확인하고, 감사 요구 사항을 충족하기 위해.","caution":"추가 라이선스가 없다. 바쁜 테이블의 SELECT를 감사하면 레코드가 많아지고 SYSAUX가 커진다. 조건과 EXCEPT로 범위를 좁히고 DBMS_AUDIT_MGMT(CLEAN_AUDIT_TRAIL, SET_LAST_ARCHIVE_TIMESTAMP)로 아카이브/정리를 예약한다. 활성화된 정책은 삭제할 수 없으므로(ORA-46361) 먼저 NOAUDIT를 실행한다. 정책 변경은 새 세션부터 적용된다. 19c는 기본적으로 mixed 모드로 실행되므로 기존 AUDIT_TRAIL 레코드도 기록될 수 있다.","since":"Oracle 12c","note":"23ai에서는 기존 감사가 desupported되어 unified auditing만 존재하며, 레코드가 테이블에 직접 기록되므로 FLUSH_UNIFIED_AUDIT_TRAIL이 제거되었다. 19c에서 pure unified 모드로 전환하려면 uniaud_on으로 oracle 바이너리를 relink해야 한다.","tags":["unified auditing","audit policy","auditing","unified_audit_trail","create audit policy","noaudit","ora_logon_failures","security"]},{"id":"vpd-row-level-policy","task":"VPD로 행 수준 접근 정책 추가(DBMS_RLS)","category":"보안","command":"-- 1) Policy function returning the predicate:\nCREATE OR REPLACE FUNCTION customer_app.customer_vpd_fn (\n  p_schema IN VARCHAR2,\n  p_object IN VARCHAR2\n) RETURN VARCHAR2\nAS\nBEGIN\n  -- The schema owner sees all rows (NULL = no restriction)\n  IF SYS_CONTEXT('USERENV', 'SESSION_USER') = 'CUSTOMER_APP' THEN\n    RETURN NULL;\n  END IF;\n  -- Other users see only their own city\n  RETURN 'city = SYS_CONTEXT(''USERENV'', ''CLIENT_IDENTIFIER'')';\nEND;\n/\n\n-- 2) Attach the policy to the table:\nBEGIN\n  DBMS_RLS.ADD_POLICY(\n    object_schema   => 'CUSTOMER_APP',\n    object_name     => 'CUSTOMER',\n    policy_name     => 'CUSTOMER_CITY_VPD',\n    function_schema => 'CUSTOMER_APP',\n    policy_function => 'CUSTOMER_VPD_FN',\n    statement_types => 'SELECT, UPDATE, DELETE',\n    update_check    => TRUE,\n    policy_type     => DBMS_RLS.CONTEXT_SENSITIVE);\nEND;\n/\n\n-- 3) Check:\nSELECT object_owner, object_name, policy_name, function,\n       sel, upd, del, chk_option, enable, policy_type\nFROM   dba_policies WHERE object_owner = 'CUSTOMER_APP';\n-- Test (as the application user): EXEC DBMS_SESSION.SET_IDENTIFIER('Ankara');\n\n-- 4) Temporarily disable / drop:\nEXEC DBMS_RLS.ENABLE_POLICY('CUSTOMER_APP', 'CUSTOMER', 'CUSTOMER_CITY_VPD', FALSE);\nEXEC DBMS_RLS.DROP_POLICY('CUSTOMER_APP', 'CUSTOMER', 'CUSTOMER_CITY_VPD');","what":"Virtual Private Database는 정책 함수가 반환한 WHERE 조건을 테이블에 대한 모든 SQL에 자동으로 추가하여 같은 테이블의 사용자가 권한이 있는 행만 보고/수정하게 한다. update_check=>TRUE는 사용자가 볼 수 없는 값으로 행을 갱신하는 것을 막는다.","when":"멀티테넌트 애플리케이션, 지역/지점 기반 데이터 격리, 애플리케이션 코드를 건드리지 않고 행 보안을 추가할 때.","caution":"Enterprise Edition에 포함되어 있으며 추가 라이선스가 없다. CLIENT_IDENTIFIER는 클라이언트가 자유롭게 설정할 수 있으므로 운영 환경에서는 신뢰할 수 있는 패키지가 설정하는 application context(CREATE CONTEXT ... USING)를 사용한다. SYS와 EXEMPT ACCESS POLICY를 가진 사용자는 정책을 우회한다. 해당 권한 없이 수행한 Data Pump export는 경고 없이 일부 데이터만 내보낸다. 함수에 오류가 있으면 ORA-28112로 모든 접근이 차단된다. 잘못된 policy_type(예: STATIC)은 조건을 캐시하여 잘못된 결과를 반환한다.","tags":["vpd","virtual private database","dbms_rls","row security","row level security","add_policy","policy function","security"]},{"id":"password-profile-hardening","task":"비밀번호 정책 profile 생성 및 사용자에게 할당","category":"보안","command":"-- Hardened profile for application/human users:\nCREATE PROFILE app_user_prf LIMIT\n  FAILED_LOGIN_ATTEMPTS    5\n  PASSWORD_LOCK_TIME       1/24          -- 1 hour lock\n  PASSWORD_LIFE_TIME       90\n  PASSWORD_GRACE_TIME      7\n  PASSWORD_REUSE_TIME      365\n  PASSWORD_REUSE_MAX       10\n  INACTIVE_ACCOUNT_TIME    60            -- accounts unused for 60 days are locked\n  PASSWORD_VERIFY_FUNCTION ora12c_strong_verify_function;\n\n-- Service accounts: password never expires, gradual password rollover enabled:\nCREATE PROFILE service_account_prf LIMIT\n  FAILED_LOGIN_ATTEMPTS    UNLIMITED\n  PASSWORD_LIFE_TIME       UNLIMITED\n  PASSWORD_ROLLOVER_TIME   1;            -- old and new password both valid for 1 day\n\nALTER USER customer_app PROFILE app_user_prf;\n\n-- Check:\nSELECT username, profile, account_status,\n       TO_CHAR(expiry_date,'YYYY-MM-DD') AS expiry_date, lock_date\nFROM   dba_users\nWHERE  profile IN ('APP_USER_PRF', 'SERVICE_ACCOUNT_PRF');\n\nSELECT profile, resource_name, limit\nFROM   dba_profiles\nWHERE  profile = 'APP_USER_PRF' AND resource_type = 'PASSWORD'\nORDER  BY resource_name;","what":"profile은 로그인 실패 잠금, 비밀번호 수명, 재사용, 복잡도(verify function), 비활성 계정 잠금 규칙을 사용자 그룹에 적용한다. PASSWORD_ROLLOVER_TIME은 서비스 계정의 비밀번호를 변경하는 동안 애플리케이션이 서비스 중단 없이 전환할 수 있게 한다.","when":"보안 감사/CIS benchmark 요구 사항, 새 애플리케이션 사용자 생성 시, DEFAULT profile의 느슨한 값을 사용자 그룹별로 분리하기 위해.","caution":"추가 라이선스가 없다. 서비스 계정에 FAILED_LOGIN_ATTEMPTS 한도가 있으면 비밀번호가 틀린 앱 서버 하나가 계정을 잠가 운영 장애를 일으킬 수 있다. 애플리케이션 계정의 PASSWORD_LIFE_TIME은 예기치 않은 EXPIRED 상태를 유발할 수 있다. verify function은 SYS 아래에 있어야 한다(없으면 @?/rdbms/admin/utlpwdmg.sql. 이 스크립트는 DEFAULT profile도 수정한다). profile을 변경해도 기존 비밀번호는 다시 검증되지 않는다.","note":"INACTIVE_ACCOUNT_TIME은 12.2에서 추가되었다. PASSWORD_ROLLOVER_TIME은 21c에 도입되어 19.12 RU로 19c에 백포트되었다. rollover 기간 동안 이전 비밀번호로 로그인한 세션은 감사 레코드로 추적할 수 있다.","tags":["profile","password policy","password_verify_function","failed_login_attempts","inactive_account_time","password_rollover_time","account lock","security"]},{"id":"privilege-analysis-capture","task":"privilege analysis로 사용되지 않는 권한 찾기","category":"보안","command":"-- 1) Capture privilege usage for a specific user:\nBEGIN\n  DBMS_PRIVILEGE_CAPTURE.CREATE_CAPTURE(\n    name        => 'CAP_APP_USER',\n    description => 'Actual privilege usage of the application user',\n    type        => DBMS_PRIVILEGE_CAPTURE.G_CONTEXT,\n    condition   => 'SYS_CONTEXT(''USERENV'',''SESSION_USER'') = ''CUSTOMER_APP''');\n  DBMS_PRIVILEGE_CAPTURE.ENABLE_CAPTURE(name => 'CAP_APP_USER', run_name => 'RUN_2026_10');\nEND;\n/\n-- Other types: G_DATABASE (whole DB), G_ROLE (roles => role_name_list('DBA')), G_ROLE_AND_CONTEXT\n\n-- 2) After a representative workload (including month end), stop and generate results:\nBEGIN\n  DBMS_PRIVILEGE_CAPTURE.DISABLE_CAPTURE(name => 'CAP_APP_USER');\n  DBMS_PRIVILEGE_CAPTURE.GENERATE_RESULT(name => 'CAP_APP_USER', run_name => 'RUN_2026_10');\nEND;\n/\n\n-- 3) Used and never-used privileges:\nSELECT username, sys_priv, obj_priv, object_owner, object_name, used_role\nFROM   dba_used_privs   WHERE capture = 'CAP_APP_USER';\nSELECT username, sys_priv, obj_priv, object_owner, object_name\nFROM   dba_unused_privs WHERE capture = 'CAP_APP_USER';\n\n-- 4) Through which role chain was the privilege used?\nSELECT username, sys_priv, used_role, path\nFROM   dba_used_sysprivs_path WHERE capture = 'CAP_APP_USER';\n\n-- 5) Cleanup:\nEXEC DBMS_PRIVILEGE_CAPTURE.DROP_CAPTURE(name => 'CAP_APP_USER');","what":"privilege analysis는 선택한 범위(데이터베이스 전체, 롤, 조건)에서 실제로 사용된 시스템/객체 권한을 런타임에 기록한다. 결과는 사용되지 않은 권한을 나열하므로 최소 권한 원칙에 따라 REVOKE를 계획할 수 있다.","when":"DBA/ANY 권한을 가진 애플리케이션 계정을 정리할 때, 감사 전 권한 검토 시, 패키지 애플리케이션에 실제로 필요한 권한을 파악할 때.","caution":"2018년부터 Enterprise Edition에 포함되어 있다(이전에는 Database Vault 라이선스 필요). 추가 라이선스가 없다. 캡처 중에는 약간의 오버헤드가 있다. 결과는 캡처 기간의 워크로드만 반영한다. 월말, 연말, 긴급 작업이 실행되지 않았다면 \"미사용\"으로 표시된 권한도 필요할 수 있으므로 단계적으로 회수한다. 한 번에 하나의 캡처만 활성화할 수 있으며, 유일한 예외로 G_DATABASE 캡처는 하나의 롤 또는 context 캡처와 함께 활성화할 수 있다.","since":"Oracle 12c","tags":["privilege analysis","dbms_privilege_capture","unused privileges","least privilege","dba_unused_privs","privilege cleanup","security"]},{"id":"opatch-inventory-prereq","task":"OPatch로 인벤토리 확인 및 패치 사전 점검 실행","category":"패치 및 수명 주기","command":"# OPatch version (must meet the minimum in the patch README; update with patch 6880880 if needed)\n$ORACLE_HOME/OPatch/opatch version\n\n# Installed patches: short list / detailed inventory\n$ORACLE_HOME/OPatch/opatch lspatches\n$ORACLE_HOME/OPatch/opatch lsinventory -detail -oh $ORACLE_HOME\n\n# Pre-apply checks (in the unzipped patch directory):\ncd /u01/stage/<patch_id>\n$ORACLE_HOME/OPatch/opatch prereq CheckConflictAgainstOHWithDetail -ph ./\n$ORACLE_HOME/OPatch/opatch prereq CheckSystemSpace -ph ./\n\n# For Grid Infrastructure / RAC homes (as root), analyze mode:\n# opatchauto apply /u01/stage/<patch_id> -analyze","what":"Oracle Home에 적용된 바이너리 패치(RU, one-off)를 나열하고, 새 패치를 적용하기 전에 기존 패치와 충돌하는지와 디스크 공간이 충분한지 점검한다.","when":"분기별 RU/보안 패치 전, Support가 SR에서 인벤토리 출력을 요청할 때, 두 환경의 패치 수준을 비교할 때.","caution":"명령은 Oracle Home을 소유한 OS 사용자(oracle)로 실행한다. opatchauto는 root가 필요하다. prereq 점검은 읽기 전용이며 데이터베이스에 영향을 주지 않지만, 충돌이 보고되면 Oracle Support에서 merge/overlay 패치를 받기 전에는 적용하지 않는다. 적용 전에 Oracle Home(tar)과 인벤토리를 백업한다.","note":"SQL에서 인벤토리를 보려면 DBMS_QOPATCH.GET_OPATCH_LSINVENTORY를 사용할 수 있다(OPatch 인벤토리가 있는 설치 환경).","tags":["opatch","lsinventory","lspatches","prereq","patch","patch conflict","opatchauto","release update"],"untested":true},{"id":"datapatch-apply-verify","task":"바이너리 패치 후 datapatch -verbose로 SQL 계층 갱신","category":"패치 및 수명 주기","command":"# 1) After the binary patch (opatch apply), start the DB; all PDBs must be open:\nsqlplus / as sysdba <<EOF\nSTARTUP;\nALTER PLUGGABLE DATABASE ALL OPEN;\nEXIT;\nEOF\n\n# 2) Check only first, then apply:\ncd $ORACLE_HOME/OPatch\n./datapatch -prereq\n./datapatch -verbose\n# For specific PDBs: ./datapatch -verbose -pdbs PDB1,PDB2\n\n# 3) Recompile invalid objects:\nsqlplus / as sysdba @?/rdbms/admin/utlrp.sql\n\n# Logs: $ORACLE_BASE/cfgtoollogs/sqlpatch/","what":"datapatch는 Oracle Home에 있는 바이너리 패치의 SQL/PL-SQL 구성 요소(딕셔너리, 패키지)를 CDB root, PDB$SEED, 열려 있는 모든 PDB에 적용하고 결과를 DBA_REGISTRY_SQLPATCH에 기록한다. 롤백된 패치의 SQL 부분도 되돌린다.","when":"모든 RU/RUR/one-off 바이너리 패치나 롤백 후, 새로 패치된 home으로 이동하거나 unplug/plug된 PDB, DBA_REGISTRY_SQLPATCH에 WITH ERRORS가 표시될 때 재실행용.","caution":"닫혀 있거나 MOUNTED인 PDB는 패치되지 않으며 RESTRICTED 모드로 열릴 수 있으므로 먼저 모두 연다. 높은 부하에서 실행하지 않는다(딕셔너리 락, 패키지 무효화). RAC에서는 한 노드에서 한 번만 실행한다. Data Guard에서는 primary에서만 datapatch를 실행한다. 변경은 redo를 통해 standby에 전달된다(standby 바이너리를 먼저 패치한다).","note":"최근 19c RU에 포함된 datapatch는 패치 전에 딕셔너리와 환경 상태(invalid 객체, PDB 위반, 테이블스페이스/TEMP 상태)를 검증하는 -sanity_checks를 제공한다.","tags":["datapatch","sql patch","release update","dba_registry_sqlpatch","post-patch","utlrp","pdb","patching"],"untested":true},{"id":"sqlpatch-registry-check","task":"SQL 패치 이력, 구성 요소 상태, invalid 객체 확인","category":"패치 및 수명 주기","command":"-- 1) datapatch history (most recent at the bottom):\nSELECT patch_id, patch_type, action, status, source_version, target_version,\n       TO_CHAR(action_time,'YYYY-MM-DD HH24:MI') AS action_time, description\nFROM   dba_registry_sqlpatch\nORDER  BY action_time;\n\n-- Failed entries and their log files:\nSELECT install_id, patch_id, action, status, logfile\nFROM   dba_registry_sqlpatch\nWHERE  status <> 'SUCCESS';\n\n-- In a CDB, for all PDBs (from root):\nSELECT con_id, patch_id, action, status\nFROM   cdb_registry_sqlpatch ORDER BY con_id, action_time;\n\n-- 2) Components must be VALID:\nSELECT comp_id, comp_name, version_full, status\nFROM   dba_registry ORDER BY comp_id;\n\n-- 3) Invalid object summary (compare before/after utlrp):\nSELECT owner, object_type, COUNT(*) AS invalid_cnt\nFROM   dba_objects\nWHERE  status = 'INVALID'\nGROUP  BY owner, object_type\nORDER  BY invalid_cnt DESC;","what":"패치 후 SQL 계층이 실제로 갱신되었는지(바이너리와 일치하는 딕셔너리), 어떤 PDB에서 어떤 패치가 실패했는지, 딕셔너리 구성 요소가 VALID인지, invalid 객체 수를 한눈에 보여 준다.","when":"모든 datapatch 후 검증 단계로, 패치나 업그레이드 전 기준선을 확보하기 위해, PDB를 plug한 후 불일치가 의심될 때.","caution":"읽기 전용이며 라이선스가 필요 없다. SOURCE_VERSION/TARGET_VERSION은 18c+ RU 모델에서 채워진다. STATUS='WITH ERRORS'이면 LOGFILE의 오류를 해결하고 datapatch를 다시 실행한다. 일부 구성 요소(예: 사용하지 않는 XDB/JAVA)는 INVALID/REMOVED로 표시될 수 있으므로 패치 전 상태와 비교한다.","note":"SQL에서 바이너리 인벤토리를 보려면 SELECT XMLTRANSFORM(DBMS_QOPATCH.GET_OPATCH_LIST, DBMS_QOPATCH.GET_OPATCH_XSLT) FROM dual;을 사용한다. OPatch 인벤토리가 없는 설치 환경에서는 ORA-20013이 발생한다.","tags":["dba_registry_sqlpatch","patch history","datapatch","dba_registry","invalid objects","invalid object","patch verification"]},{"id":"scheduler-job-lifecycle","task":"스케줄러 job 생성, 수동 실행, 비활성화, 삭제","category":"스케줄러","command":"-- 1) Create disabled, set attributes, then enable:\nBEGIN\n  DBMS_SCHEDULER.CREATE_JOB(\n    job_name        => 'CUSTOMER_APP.NIGHTLY_STATS',\n    job_type        => 'PLSQL_BLOCK',\n    job_action      => q'[BEGIN DBMS_STATS.GATHER_SCHEMA_STATS(ownname => 'CUSTOMER_APP', options => 'GATHER STALE'); END;]',\n    start_date      => SYSTIMESTAMP,\n    repeat_interval => 'FREQ=DAILY; BYHOUR=2; BYMINUTE=30; BYSECOND=0',\n    enabled         => FALSE,\n    auto_drop       => FALSE,\n    comments        => 'Nightly stale statistics gathering');\n  DBMS_SCHEDULER.SET_ATTRIBUTE('CUSTOMER_APP.NIGHTLY_STATS', 'max_run_duration', INTERVAL '2' HOUR);\n  DBMS_SCHEDULER.SET_ATTRIBUTE('CUSTOMER_APP.NIGHTLY_STATS', 'logging_level', DBMS_SCHEDULER.LOGGING_FULL);\n  DBMS_SCHEDULER.ENABLE('CUSTOMER_APP.NIGHTLY_STATS');\nEND;\n/\n\n-- 2) Validate the calendar expression (next run time):\nSET SERVEROUTPUT ON\nDECLARE\n  l_next TIMESTAMP WITH TIME ZONE;\nBEGIN\n  DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING(\n    calendar_string   => 'FREQ=DAILY; BYHOUR=2; BYMINUTE=30; BYSECOND=0',\n    start_date        => SYSTIMESTAMP,\n    return_date_after => SYSTIMESTAMP,\n    next_run_date     => l_next);\n  DBMS_OUTPUT.PUT_LINE('Next run: ' || l_next);\nEND;\n/\n\n-- 3) Run now in the background (does not affect the schedule):\nEXEC DBMS_SCHEDULER.RUN_JOB('CUSTOMER_APP.NIGHTLY_STATS', use_current_session => FALSE);\n\n-- 4) Status:\nSELECT owner, job_name, enabled, state, run_count, failure_count,\n       last_start_date, next_run_date\nFROM   dba_scheduler_jobs WHERE owner = 'CUSTOMER_APP';\n\n-- 5) Stop (if running) / disable / drop:\n-- EXEC DBMS_SCHEDULER.STOP_JOB('CUSTOMER_APP.NIGHTLY_STATS', force => FALSE);\nEXEC DBMS_SCHEDULER.DISABLE('CUSTOMER_APP.NIGHTLY_STATS', force => TRUE);\nEXEC DBMS_SCHEDULER.DROP_JOB('CUSTOMER_APP.NIGHTLY_STATS', force => TRUE);","what":"DBMS_SCHEDULER로 예약 PL/SQL job을 정의한다. max_run_duration(초과 시 이벤트 발생)과 상세 로깅 수준을 설정하고, 캘린더 표현식을 미리 검증하며, 일정을 방해하지 않고 수동 실행할 수 있다.","when":"유지보수, ETL, 정리 같은 반복 데이터베이스 작업을 cron 대신 데이터베이스 내부에서 RAC/Data Guard를 인식하며 실행하기 위해.","caution":"추가 라이선스가 없다. start_date의 시간대가 중요하다. SYSTIMESTAMP는 데이터베이스 서버의 TZ를 사용하므로 DST 변경 시 어긋나지 않도록 명시적 지역(예: 'Europe/Istanbul')을 사용한다. JOB_QUEUE_PROCESSES=0이면 어떤 job도 실행되지 않는다. force=>TRUE로 DISABLE해도 실행 중인 인스턴스는 멈추지 않고 이후 실행만 막는다. 중단하려면 STOP_JOB을 사용한다. 다른 스키마에 job을 만들려면 CREATE ANY JOB이 필요하다.","tags":["dbms_scheduler","job","scheduled job","create_job","run_job","repeat_interval","disable job","scheduler"]},{"id":"scheduler-job-history","task":"스케줄러 job 실행 이력 및 실패 모니터링","category":"스케줄러","command":"-- 1) Recent runs: duration, CPU, error code and message\nSELECT owner, job_name, status, error#,\n       TO_CHAR(actual_start_date,'YYYY-MM-DD HH24:MI:SS') AS started,\n       run_duration, cpu_used,\n       SUBSTR(additional_info, 1, 200) AS info\nFROM   dba_scheduler_job_run_details\nWHERE  owner = 'CUSTOMER_APP'\nORDER  BY actual_start_date DESC\nFETCH FIRST 20 ROWS ONLY;\n\n-- 2) Failures in the last 24 hours (whole DB):\nSELECT owner, job_name, error#, actual_start_date, SUBSTR(errors, 1, 300) AS errors\nFROM   dba_scheduler_job_run_details\nWHERE  status <> 'SUCCEEDED'\n  AND  log_date > SYSTIMESTAMP - INTERVAL '1' DAY\nORDER  BY log_date DESC;\n\n-- 3) Currently running:\nSELECT owner, job_name, session_id, running_instance, elapsed_time, cpu_used\nFROM   dba_scheduler_running_jobs;\n\n-- 4) Lifecycle events (ENABLE/DISABLE/RUN/STOP...):\nSELECT log_date, owner, job_name, operation, status\nFROM   dba_scheduler_job_log\nWHERE  owner = 'CUSTOMER_APP'\nORDER  BY log_date DESC\nFETCH FIRST 20 ROWS ONLY;\n\n-- Log retention (days): EXEC DBMS_SCHEDULER.SET_SCHEDULER_ATTRIBUTE('log_history', '30');","what":"스케줄러 로그 뷰에서 각 실행의 시작 시각, 소요 시간, CPU 사용량, 상태, 오류 메시지와 실행 중인 job, job 수명 주기 이벤트를 나열한다.","when":"야간 job이 실행되지 않았거나 오래 걸렸을 때, job SLA 추적, 모니터링 시스템(OEM/Zabbix/Prometheus)에 실패 규칙을 정의할 때.","caution":"읽기 전용이며 라이선스가 필요 없다. 기본 로그 보존 기간은 30일이며 logging_level이 OFF인 job은 레코드를 남기지 않는다. ERRORS 컬럼은 12c+에 있으며 이전 릴리스에서는 ADDITIONAL_INFO를 확인한다. RAC에서는 RUNNING_INSTANCE가 job을 실행한 노드를 보여 준다.","tags":["scheduler","job history","dba_scheduler_job_run_details","failed job","dba_scheduler_running_jobs","job log","monitoring"]},{"id":"resource-plan-basic","task":"간단한 Resource Manager plan 생성 및 활성화","category":"스케줄러","command":"-- 1) Define groups, plan, directives and mapping inside a pending area (PDB or non-CDB):\nBEGIN\n  DBMS_RESOURCE_MANAGER.CLEAR_PENDING_AREA;\n  DBMS_RESOURCE_MANAGER.CREATE_PENDING_AREA;\n\n  DBMS_RESOURCE_MANAGER.CREATE_CONSUMER_GROUP('OLTP_GRP',  'Online transactions');\n  DBMS_RESOURCE_MANAGER.CREATE_CONSUMER_GROUP('REPORT_GRP', 'Reporting / batch');\n  DBMS_RESOURCE_MANAGER.CREATE_PLAN('DAYTIME_PLAN', 'Business hours plan');\n\n  DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(\n    plan => 'DAYTIME_PLAN', group_or_subplan => 'OLTP_GRP',\n    comment => 'OLTP first', mgmt_p1 => 70);\n  DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(\n    plan => 'DAYTIME_PLAN', group_or_subplan => 'REPORT_GRP',\n    comment => 'Reports limited', mgmt_p1 => 20,\n    parallel_degree_limit_p1 => 4,\n    switch_group => 'CANCEL_SQL', switch_time => 1800, switch_for_call => TRUE);\n  DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(\n    plan => 'DAYTIME_PLAN', group_or_subplan => 'OTHER_GROUPS',\n    comment => 'Everyone else (mandatory)', mgmt_p1 => 10);\n\n  DBMS_RESOURCE_MANAGER.SET_CONSUMER_GROUP_MAPPING(\n    attribute      => DBMS_RESOURCE_MANAGER.ORACLE_USER,\n    value          => 'REPORT_USER',\n    consumer_group => 'REPORT_GRP');\n\n  DBMS_RESOURCE_MANAGER.VALIDATE_PENDING_AREA;\n  DBMS_RESOURCE_MANAGER.SUBMIT_PENDING_AREA;\nEND;\n/\n-- Switch privilege required for the mapping to take effect:\nEXEC DBMS_RESOURCE_MANAGER_PRIVS.GRANT_SWITCH_CONSUMER_GROUP('REPORT_USER', 'REPORT_GRP', FALSE);\n\n-- 2) Activate (FORCE: prevents maintenance windows from switching the plan):\nALTER SYSTEM SET resource_manager_plan = 'FORCE:DAYTIME_PLAN' SCOPE = BOTH;\n\n-- 3) Monitor:\nSELECT sid, username, resource_consumer_group FROM v$session WHERE username IS NOT NULL;\nSELECT name, active_sessions, execution_waiters, consumed_cpu_time, cpu_wait_time\nFROM   v$rsrc_consumer_group;\n\n-- 4) Roll back: deactivate the plan, then delete it\n-- ALTER SYSTEM SET resource_manager_plan = '' SCOPE = BOTH;\n-- BEGIN DBMS_RESOURCE_MANAGER.CREATE_PENDING_AREA;\n--       DBMS_RESOURCE_MANAGER.DELETE_PLAN_CASCADE('DAYTIME_PLAN');\n--       DBMS_RESOURCE_MANAGER.SUBMIT_PENDING_AREA; END;\n-- /","what":"CPU 경합 시 어떤 워크로드가 우선할지 결정한다: OLTP 그룹에 CPU 70%, 보고서 그룹에 20%를 주고, 보고서 그룹의 병렬도를 4로 제한하며, 30분을 초과하는 호출을 취소한다(CANCEL_SQL). 사용자는 consumer group에 자동으로 매핑된다.","when":"주간 보고서가 OLTP를 느리게 하는 혼합 워크로드, 통제되지 않는 병렬 쿼리 제한, 폭주 쿼리의 자동 취소용.","caution":"Resource Manager는 Enterprise Edition이 필요하며(SE2에서는 매우 제한적) 추가 라이선스는 없다. mgmt_p1 비율은 CPU 경합 시에만 적용되며 한 레벨에서 합계가 100%를 넘을 수 없다. CANCEL_SQL은 실행 중인 문장을 ORA-00040으로 종료한다. 잘못된 매핑은 중요 작업을 종료시킬 수 있으므로 임계값을 신중히 테스트한다. plan이 활성화된 동안 CPU 제한은 'resmgr:cpu quantum' 대기 이벤트로 나타난다. CDB 수준에서 PDB 간 분배에는 CREATE_CDB_PLAN을 사용한다.","tags":["resource manager","dbms_resource_manager","resource plan","consumer group","cpu priority","cancel_sql","resmgr:cpu quantum","resource management"]}]},"recipes/postgres.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"recipes":[{"id":"wal-switch","task":"WAL 세그먼트 전환","category":"WAL 및 체크포인트","command":"SELECT pg_switch_wal();","what":"현재 WAL 세그먼트를 닫고 새 세그먼트로 전환하며 아카이브를 트리거한다.","when":"아카이브 파일을 생성하거나 백업 후 WAL을 강제로 내보낼 때.","caution":"PostgreSQL 10 이전에는 pg_switch_xlog()였다.","since":"PostgreSQL 10","note":"이 함수는 PostgreSQL 10에서 pg_switch_wal()로 이름이 바뀌었다. 그 이전에는 pg_switch_xlog()였다(WAL을 'xlog'라고 불렀다).","tags":["wal","switch","checkpoint","archive"]},{"id":"manual-checkpoint","task":"수동 CHECKPOINT 실행","category":"WAL 및 체크포인트","command":"CHECKPOINT;","what":"dirty buffer를 디스크에 기록하고 checkpoint 레코드를 생성하여 복구 시작 지점을 앞당긴다.","when":"백업 전, 유지보수 시간대, 서버 중지 전 디스크에 동기화할 때.","caution":"I/O 부하가 클 수 있으므로 운영 환경 피크 시간에 실행하지 않는다. superuser 또는 (PG15+) pg_checkpoint 롤만 실행할 수 있다.","tags":["checkpoint","wal","io","flush"]},{"id":"wal-status-size","task":"WAL 크기 및 현재 LSN 조회","category":"WAL 및 체크포인트","command":"SELECT pg_current_wal_lsn() AS current_lsn, pg_walfile_name(pg_current_wal_lsn()) AS current_wal_file, pg_size_pretty(sum(size)) AS wal_dir_size FROM pg_ls_waldir();","what":"현재 WAL LSN, 기록 중인 WAL 파일 이름, pg_wal 디렉터리의 전체 크기를 반환한다.","when":"WAL 누적, 디스크 사용량, checkpoint 간격을 모니터링하기 위해.","caution":"pg_ls_waldir와 pg_walfile_name은 PostgreSQL 10+에서 사용할 수 있으며 접근에 pg_monitor 롤이 필요할 수 있다.","tags":["wal","lsn","size","monitor"]},{"id":"archive-lag","task":"아카이브 지연 조회(아직 아카이브되지 않은 WAL)","category":"WAL 및 체크포인트","command":"SELECT last_archived_wal, last_failed_wal, archived_count, failed_count, pg_walfile_name(pg_current_wal_lsn()) AS current_wal FROM pg_stat_archiver;","what":"마지막으로 아카이브된 WAL 파일, 현재 기록 중인 WAL, 아카이브 실패 카운터를 비교하여 아카이브 지연을 보여 준다.","when":"archive_command가 뒤처지는지 모니터링하고 아카이브 실패를 포착하기 위해.","caution":"failed_count가 계속 증가하면 archive_command가 실패하고 있으며 pg_wal이 가득 찰 수 있다. 디스크가 넘치지 않도록 즉시 조치한다.","tags":["archive","wal","lag","monitor"]},{"id":"create-tablespace","task":"새 테이블스페이스 추가","category":"스토리지","command":"CREATE TABLESPACE ssd_ts OWNER postgres LOCATION '/mnt/ssd/pgdata';","what":"지정한 디렉터리에 새 테이블스페이스를 정의한다. 테이블과 인덱스를 해당 디스크에 배치할 수 있다.","when":"데이터를 다른 디스크(예: 빠른 SSD)로 분산하고 I/O를 분리하기 위해.","caution":"디렉터리는 미리 존재해야 하고 비어 있어야 하며 postgres 사용자가 소유해야 한다. 심볼릭 링크로 데이터를 다른 디스크에 분산하지 않는다. PostgreSQL 자체의 테이블스페이스 메커니즘을 사용한다.","tags":["tablespace","storage","disk","create"]},{"id":"move-table-tablespace","task":"테이블을 다른 테이블스페이스로 이동","category":"스토리지","command":"ALTER TABLE customer SET TABLESPACE ssd_ts;","what":"테이블의 모든 데이터를 새 테이블스페이스(디스크)로 물리적으로 옮긴다.","when":"테이블을 더 빠르거나 별도의 디스크로 옮기기 위해.","caution":"이동 중 테이블에 ACCESS EXCLUSIVE 락이 걸리며 이동이 끝날 때까지 테이블에 접근할 수 없다. 대형 테이블은 유지보수 시간대에 수행한다.","tags":["tablespace","table","move","storage"]},{"id":"move-index-tablespace","task":"인덱스를 다른 테이블스페이스로 이동","category":"스토리지","command":"ALTER INDEX customer_email_idx SET TABLESPACE ssd_ts;","what":"인덱스를 지정한 테이블스페이스로 옮긴다.","when":"인덱스를 데이터와 별도의 디스크에 두어 I/O를 분산하기 위해.","caution":"이동 중 ACCESS EXCLUSIVE 락이 걸리며 인덱스를 사용할 수 없다. ALTER TABLE ... SET TABLESPACE는 테이블의 인덱스를 포함하지 않으므로 한 번에 모두 옮길 수 없다. 인덱스마다 별도로 옮긴다.","tags":["tablespace","index","move","storage"]},{"id":"db-table-size","task":"데이터베이스 및 테이블 크기 조회","category":"스토리지","command":"SELECT relname AS table_name, pg_size_pretty(pg_total_relation_size(relid)) AS total_size, pg_size_pretty(pg_relation_size(relid)) AS data_only FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 20;","what":"인덱스와 TOAST를 포함한 전체 크기 기준으로 가장 큰 테이블 20개를 나열한다.","when":"디스크 사용량을 분석하고 어떤 테이블이 공간을 차지하는지 찾기 위해.","caution":"데이터베이스 전체 크기: SELECT pg_size_pretty(pg_database_size(current_database()));","tags":["size","table","database","storage"]},{"id":"table-bloat","task":"테이블 bloat 추정","category":"스토리지","command":"SELECT schemaname, relname AS table_name, n_dead_tup AS dead_rows, n_live_tup AS live_rows, round(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 1) AS dead_pct, last_autovacuum FROM pg_stat_user_tables WHERE n_dead_tup > 0 ORDER BY n_dead_tup DESC LIMIT 20;","what":"dead tuple 수와 마지막 autovacuum 시각을 기준으로 bloat 비율을 보여 준다.","when":"VACUUM 필요 여부와 autovacuum이 따라가고 있는지 판단하기 위해.","caution":"n_dead_tup은 DML 카운터로 계속 갱신되는 추정치다(VACUUM 후 감소하며, ANALYZE는 추정치만 보정한다). 정확한 물리적 bloat은 pgstattuple 확장(pgstattuple('customer'))이 더 정확하다.","tags":["bloat","dead-tuple","vacuum","storage"]},{"id":"vacuum-analyze-table","task":"특정 테이블 VACUUM ANALYZE","category":"유지보수","command":"VACUUM (ANALYZE, VERBOSE) customer;","what":"테이블의 dead tuple을 정리하여 공간을 재사용 가능하게 하고 플래너 통계를 갱신한다.","when":"대량 DELETE/UPDATE 후, 또는 쿼리 계획이 나빠졌을 때.","caution":"VACUUM은 디스크 공간을 OS에 반환하지 않는다. 공간 회수에는 ACCESS EXCLUSIVE 락을 잡는 VACUUM FULL이 필요하다. 일반 VACUUM은 테이블을 블로킹하지 않는다.","tags":["vacuum","analyze","maintenance","cleanup"]},{"id":"vacuum-freeze","task":"적극적인 VACUUM FREEZE 실행","category":"유지보수","command":"VACUUM (FREEZE, VERBOSE, ANALYZE) customer;","what":"테이블에서 대상이 되는 모든 tuple을 즉시 'frozen'으로 표시하여 transaction ID wraparound 위험을 줄인다.","when":"wraparound 경고가 나타났을 때, 또는 아카이브/읽기 전용이 될 대형 테이블에 예방적으로.","caution":"테이블 전체를 스캔하므로 I/O 부하가 크다. wraparound 긴급 상황에서는 데이터베이스가 사실상 읽기 전용이 될 수 있으므로 사전에 datfrozenxid를 모니터링한다.","tags":["vacuum","freeze","wraparound","maintenance"]},{"id":"reindex-concurrently","task":"락 없이 인덱스 재구축(REINDEX CONCURRENTLY)","category":"유지보수","command":"REINDEX INDEX CONCURRENTLY customer_email_idx;","what":"쓰기를 블로킹하지 않고 인덱스를 재구축하여 인덱스 bloat을 제거한다.","when":"인덱스가 비대해지거나 손상되었을 때, 유지보수 시간대가 없는 운영 시스템에서.","caution":"PostgreSQL 12+가 필요하다. 실패하면 _ccnew 접미사가 붙은 INVALID 인덱스가 남을 수 있으므로 pg_index.indisvalid를 확인하고 필요하면 DROP INDEX로 정리한다. 테이블 전체 재구축: REINDEX TABLE CONCURRENTLY customer;","since":"PostgreSQL 12","note":"REINDEX ... CONCURRENTLY는 PostgreSQL 12에서 도입되었다. 그 이전에는 REINDEX가 ACCESS EXCLUSIVE 락으로 인덱스를 잠갔다(락 없는 재구축에는 CREATE INDEX CONCURRENTLY + DROP이 필요했다).","tags":["reindex","concurrently","index","maintenance"]},{"id":"autovacuum-per-table","task":"특정 테이블의 autovacuum 튜닝","category":"유지보수","command":"ALTER TABLE customer SET (autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_threshold = 5000, autovacuum_analyze_scale_factor = 0.01);","what":"이 테이블에 한해 autovacuum 트리거 임계값을 낮춰 대형/바쁜 테이블이 더 자주 vacuum되게 한다.","when":"UPDATE/DELETE가 많은 대형 테이블에서 autovacuum이 뒤처질 때.","caution":"지나치게 적극적인 설정은 autovacuum의 지속 실행과 I/O 부하로 이어질 수 있다. 설정 되돌리기: ALTER TABLE customer RESET (autovacuum_vacuum_scale_factor);","tags":["autovacuum","tuning","table","maintenance"]},{"id":"statistics-target","task":"컬럼의 statistics target 높이기","category":"유지보수","command":"ALTER TABLE customer ALTER COLUMN city SET STATISTICS 500; ANALYZE customer;","what":"특정 컬럼에 대해 수집하는 히스토그램/샘플의 상세도를 높여 플래너의 추정을 개선한다.","when":"분포가 치우친 컬럼에서 나쁜 쿼리 계획이 발생할 때.","caution":"값이 높으면(기본값 100, 최대 10000) ANALYZE 시간과 계획 수립 시간이 늘어난다. 변경은 ANALYZE 후에만 적용된다.","tags":["statistics","planner","analyze","tuning"]},{"id":"alter-system-reload","task":"파라미터 변경 및 reload(ALTER SYSTEM)","category":"파라미터","command":"ALTER SYSTEM SET work_mem = '64MB'; SELECT pg_reload_conf();","what":"파라미터를 postgresql.auto.conf에 기록하고 구성을 reload한다(재시작이 필요 없는 파라미터는 즉시 적용).","when":"work_mem이나 로깅 설정처럼 reload로 적용할 수 있는 파라미터를 영구적으로 변경하기 위해.","caution":"shared_buffers, max_connections 같은 일부 파라미터는 reload가 아닌 재시작 시에만 적용된다. pg_settings.context = 'postmaster'인 파라미터는 재시작이 필요하다.","tags":["alter-system","reload","parameter","config"]},{"id":"session-set","task":"세션 단위 파라미터 변경(SET)","category":"파라미터","command":"SET work_mem = '256MB';","what":"현재 연결/세션에 한해 파라미터를 변경하며 세션이 끝나면 초기화된다.","when":"단일 무거운 쿼리나 배치 작업을 위해 설정을 일시적으로 높여야 할 때.","caution":"단일 트랜잭션으로 제한하려면 SET LOCAL을 사용한다. 전역 변경에는 ALTER SYSTEM이 필요하다.","tags":["set","session","parameter","work-mem"]},{"id":"show-setting","task":"현재 파라미터 값 조회","category":"파라미터","command":"SELECT name, setting, unit, context, source FROM pg_settings WHERE name = 'shared_buffers';","what":"파라미터의 유효 값, 단위, 적용 방식(context), 출처를 보여 준다.","when":"파라미터의 실행 중 값과 재시작 또는 reload 필요 여부를 확인하기 위해.","caution":"단축: SHOW shared_buffers; 또는 SELECT current_setting('shared_buffers'); context 컬럼이 'postmaster'이면 재시작이 필요하다.","tags":["show","pg_settings","parameter","current-setting"]},{"id":"physical-replica-basebackup","task":"물리 복제본 구축(pg_basebackup)","category":"복제","command":"pg_basebackup -h primary_host -U replicator -D /var/lib/postgresql/16/main -Fp -Xs -P -R --slot=replica_slot_1 -C","what":"primary에서 전체 물리 백업을 수행하고, WAL을 스트리밍하며(-Xs), standby.signal과 primary_conninfo를 기록하고(-R), replication slot을 생성한다(-C).","when":"새 streaming standby(읽기 복제본 또는 페일오버 대상)를 구축하기 위해.","caution":"대상 디렉터리(-D)는 비어 있어야 한다. primary의 pg_hba.conf가 replicator 사용자에게 'replication' 접근을 허용해야 하며, 충분한 wal_keep_size / slot이 있어야 한다. PostgreSQL 12+에서 -R은 recovery.conf 대신 standby.signal을 생성한다.","tags":["replication","basebackup","standby","physical"]},{"id":"promote-replica","task":"복제본을 primary로 승격","category":"복제","command":"SELECT pg_promote(wait := true, wait_seconds := 60);","what":"standby 서버를 recovery 모드에서 빼내 쓰기 가능한 primary로 전환한다.","when":"계획된 또는 긴급 페일오버에서 standby를 새 primary로 만들기 위해.","caution":"승격은 되돌릴 수 없으며 서버는 더 이상 이전 primary를 따를 수 없다. split-brain을 막기 위해 이전 primary가 정말로 다운되었는지 확인한다. 대안: pg_ctl promote.","since":"PostgreSQL 12","note":"pg_promote() SQL 함수는 PostgreSQL 12에서 도입되었다. 그 이전에는 trigger 파일(promote_trigger_file)이나 pg_ctl promote로 standby를 승격했다.","tags":["replication","promote","failover","standby"]},{"id":"replication-lag","task":"복제 지연 조회","category":"복제","command":"SELECT client_addr, state, sent_lsn, replay_lsn, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes, write_lag, flush_lag, replay_lag FROM pg_stat_replication;","what":"primary에서 각 standby의 replay 차이를 primary의 현재 WAL 위치 대비 바이트 단위로, write/flush/replay lag와 함께 보여 준다.","when":"streaming replication이 얼마나 뒤처졌는지 모니터링하기 위해(primary에서 실행).","caution":"standby에서 초 단위 지연: SELECT now() - pg_last_xact_replay_timestamp(); 이 값은 primary에 쓰기가 없을 때도 증가하므로 LSN 차이와 함께 해석한다. 결과가 비어 있으면 연결된 standby가 없는 것이다.","tags":["replication","lag","lsn","monitor"]},{"id":"replication-slot","task":"replication slot 생성 및 삭제","category":"복제","command":"SELECT pg_create_physical_replication_slot('replica_slot_1'); -- To drop: SELECT pg_drop_replication_slot('replica_slot_1');","what":"standby가 확인할 때까지 primary가 필요한 WAL을 제거하지 않도록 보장하는 물리 slot을 생성한다. 두 번째 명령은 slot을 제거한다.","when":"짧은 장애 중에 standby가 WAL을 잃지 않도록 하기 위해.","caution":"사용되지 않거나 방치된 slot은 WAL 누적과 디스크 가득 참으로 이어진다. slot 상태 모니터링: SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained FROM pg_replication_slots; 활성 slot은 삭제할 수 없다.","tags":["replication","slot","wal","cleanup"]},{"id":"logical-replication","task":"logical replication publication 및 subscription 구성","category":"복제","command":"-- Source (publisher): CREATE PUBLICATION pub_customer FOR TABLE customer; -- Target (subscriber): CREATE SUBSCRIPTION sub_customer CONNECTION 'host=primary_host dbname=appdb user=replicator password=secret' PUBLICATION pub_customer;","what":"원본에서 특정 테이블에 대한 publication을 만들고 대상에서 이에 연결하는 subscription을 만들어 테이블 수준 logical replication을 시작한다.","when":"버전 업그레이드, 선택한 테이블을 다른 서버로 복제, 단방향 데이터 배포용.","caution":"PostgreSQL 10+가 필요하다. 대상 테이블의 스키마가 미리 존재해야 하며 subscription이 초기 COPY를 자동으로 수행한다. wal_level은 logical이어야 한다(재시작 필요). DDL 변경과 시퀀스 값은 복제되지 않는다. 스키마 변경은 양쪽에 별도로 적용하고 전환 시 setval로 시퀀스를 동기화한다.","since":"PostgreSQL 10","note":"내장 logical replication(CREATE PUBLICATION / CREATE SUBSCRIPTION)은 PostgreSQL 10에서 도입되었다. 그 이전에는 같은 작업에 pglogical 같은 외부 확장이 필요했다.","tags":["logical","replication","publication","subscription"]},{"id":"terminate-backend","task":"backend 종료(pg_terminate_backend)","category":"세션","command":"SELECT pg_terminate_backend(12345);","what":"지정한 PID의 backend 프로세스를 강제로 종료하며 연결과 실행 중인 트랜잭션을 끊는다.","when":"멈춘/응답 없는 연결이나 락을 잡고 있는 세션을 닫기 위해.","caution":"실행 중인 트랜잭션은 롤백되며 커밋되지 않은 작업은 손실된다. 먼저 더 부드러운 pg_cancel_backend를 시도한다. pg_stat_activity에서 PID를 확인한다.","tags":["terminate","backend","session","kill"]},{"id":"cancel-backend","task":"실행 중인 쿼리 취소(pg_cancel_backend)","category":"세션","command":"SELECT pg_cancel_backend(12345);","what":"지정한 PID에서 현재 실행 중인 쿼리를 취소하며 연결은 유지된다.","when":"연결을 끊지 않고 장시간 실행 쿼리를 중지하기 위해.","caution":"활성 쿼리만 취소하며 idle in transaction 상태의 세션은 닫지 않는다. 이 경우 pg_terminate_backend가 필요하다.","tags":["cancel","backend","query","session"]},{"id":"active-blocking-queries","task":"활성 쿼리 및 블로킹 쿼리 조회","category":"세션","command":"SELECT pid, usename, state, wait_event_type, wait_event, pg_blocking_pids(pid) AS blocking_pids, now() - query_start AS duration, left(query, 100) AS query FROM pg_stat_activity WHERE state <> 'idle' AND pid <> pg_backend_pid() ORDER BY duration DESC;","what":"idle이 아닌 모든 backend와 이를 블로킹하는 PID, 실행 시간을 나열한다.","when":"락/블로킹 문제와 장시간 실행 쿼리를 진단하기 위해.","caution":"pg_blocking_pids는 PostgreSQL 9.6+에서 사용할 수 있다. blocking_pids 배열이 비어 있지 않으면 해당 PID가 락을 잡고 있으므로 필요하면 그 PID를 cancel/terminate한다.","since":"PostgreSQL 9.6","note":"pg_blocking_pids() 함수는 PostgreSQL 9.6에서 도입되었다. 그 이전에는 블로킹 프로세스를 찾기 위해 pg_locks를 수동으로 조인해야 했다.","tags":["activity","blocking","lock","session"]},{"id":"create-extension","task":"확장 추가","category":"익스텐션","command":"CREATE EXTENSION IF NOT EXISTS pg_stat_statements;","what":"현재 데이터베이스에 지정한 확장을 설치한다(함수, 뷰, 타입 등을 추가).","when":"pg_stat_statements, pgcrypto, postgis 같은 추가 기능이 필요할 때.","caution":"pg_stat_statements처럼 shared_preload_libraries가 필요한 확장은 먼저 postgresql.conf에 추가하고 서버를 재시작한다. 서버에 확장 파일(contrib 패키지)이 설치되어 있어야 한다.","tags":["extension","create","pg_stat_statements","plugin"]},{"id":"list-extensions","task":"설치된 확장 목록","category":"익스텐션","command":"SELECT extname AS extension, extversion AS version FROM pg_extension ORDER BY extname;","what":"현재 데이터베이스에 설치된 모든 확장과 버전을 나열한다.","when":"어떤 확장이 설치되어 있는지와 업데이트가 필요한지 확인하기 위해.","caution":"설치 가능한(아직 설치되지 않은) 확장 확인: SELECT * FROM pg_available_extensions; 업그레이드: ALTER EXTENSION pg_stat_statements UPDATE;","tags":["extension","list","pg_extension","plugin"]}]},"recipes/postgres-ha.json":{"engineId":"postgres","engine":"PostgreSQL","order":1,"recipes":[{"id":"patroni-cluster-status","task":"Patroni 클러스터 상태, 토폴로지, 이력 조회","category":"Patroni","command":"# Members, roles, state, timeline and lag (MB):\npatronictl -c /etc/patroni/patroni.yml list <cluster>\n# Extended info (pending restart, tags) + timestamp, refresh every 5 s:\npatronictl -c /etc/patroni/patroni.yml list <cluster> -e -t -w 5\n# Leader -> replica tree (including cascading replicas):\npatronictl -c /etc/patroni/patroni.yml topology <cluster>\n# Past failovers/switchovers (timeline, LSN, reason, time):\npatronictl -c /etc/patroni/patroni.yml history <cluster>","what":"DCS에 저장된 Patroni 클러스터 뷰를 보여 준다: leader/replica 역할, 멤버 상태(running/streaming), timeline, 복제 지연, 대기 중인 재시작. history는 timeline 변경과 변경이 발생한 LSN을 나열한다.","when":"모든 스위치오버/페일오버/유지보수 전후, 알림 발생 시 첫 진단 단계로.","caution":"출력은 DCS에 저장된 내용을 반영한다. DCS에 접근할 수 없으면 명령이 실패하거나 오래된 데이터를 보여 줄 수 있다. 모든 멤버가 같은 timeline에 있고 Lag in MB가 maximum_lag_on_failover 미만인지 확인하기 전에는 스위치오버를 시작하지 않는다.","untested":true,"tags":["patroni","cluster status","topology","history","lag"]},{"id":"patroni-switchover","task":"Patroni로 계획된 스위치오버 수행","category":"Patroni","command":"# Immediate switchover without confirmation:\npatronictl -c /etc/patroni/patroni.yml switchover <cluster> --leader <leader_member> --candidate <member> --force\n# Scheduled switchover (ISO 8601 timestamp with time zone):\npatronictl -c /etc/patroni/patroni.yml switchover <cluster> --leader <leader_member> --candidate <member> --scheduled '2026-10-10T02:00:00+03:00' --force\n# Cancel a pending scheduled switchover:\npatronictl -c /etc/patroni/patroni.yml flush <cluster> switchover","what":"정상 상태의 leader를 제어된 방식으로 강등하고 선택한 replica(--candidate)를 승격한다. 이전 leader는 자동으로 새 leader의 replica로 재합류한다. --scheduled는 작업을 지정한 시각으로 미룬다.","when":"유지보수(OS 패치, 하드웨어 교체, 마이너 버전 업그레이드) 전 primary를 다른 노드로 옮기거나, 페일오버 후 원래 역할로 되돌릴 때.","caution":"leader가 바뀌면 모든 연결이 끊어지므로 애플리케이션이 재연결할 수 있어야 한다. 후보의 lag가 maximum_lag_on_failover를 초과하면 Patroni는 스위치오버를 거부한다. synchronous_mode가 활성화되어 있으면 후보는 synchronous standby여야 한다.","note":"이전 Patroni 릴리스에서 --leader 옵션의 이름은 --master였으며, Patroni 4에서 --master가 제거되었다. --primary는 --leader와 같다.","untested":true,"tags":["patroni","switchover","planned switchover","maintenance","leader"]},{"id":"patroni-failover","task":"Patroni로 수동 페일오버 수행","category":"Patroni","command":"# Manual failover while the leader is unhealthy (candidate is mandatory):\npatronictl -c /etc/patroni/patroni.yml failover <cluster> --candidate <member> --force\n# Verify the result and the new timeline:\npatronictl -c /etc/patroni/patroni.yml list <cluster>\npatronictl -c /etc/patroni/patroni.yml history <cluster>","what":"leader의 상태와 관계없이 지정한 replica를 승격한다. 자동 페일오버가 일어나지 않았거나 잘못된 노드를 선택한 경우를 위한 것이다.","when":"leader에 접근할 수 없거나 비정상인데 자동 페일오버가 동작하지 않았을 때(예: pause 모드), 또는 특정 노드로 강제 이동해야 할 때.","caution":"비동기 복제에서는 후보가 아직 수신하지 못한 트랜잭션이 손실된다(RPO > 0). leader가 정상이면 페일오버 대신 스위치오버를 사용한다. 이전 leader가 복귀하면 pg_rewind(use_pg_rewind가 활성화된 경우)나 reinit이 필요할 수 있다.","note":"Patroni 3.2+에서는 페일오버에 --candidate가 필수다. failover의 --leader/--primary 옵션은 3.2에서 deprecated되었고 Patroni 4.0에서 제거되었다. leader를 지정해야 하면 switchover를 사용한다.","untested":true,"tags":["patroni","failover","manual failover","promote","rpo"]},{"id":"patroni-reinit-replica","task":"Patroni로 손상된 replica 재구축(reinit)","category":"Patroni","command":"# Wipe the replica's PGDATA and rebuild it (create_replica_methods: pgbackrest/basebackup):\npatronictl -c /etc/patroni/patroni.yml reinit <cluster> <member> --wait --force\n# Take the basebackup directly from the leader instead of the backup repository:\npatronictl -c /etc/patroni/patroni.yml reinit <cluster> <member> --from-leader --wait --force","what":"지정한 replica의 데이터 디렉터리를 삭제하고 Patroni의 create_replica_methods(pgBackRest restore 또는 pg_basebackup)에 따라 처음부터 재구축한다.","when":"timeline 불일치, pg_rewind 실패, WAL 누락, 데이터 손상으로 replica가 복제에 재합류하지 못할 때.","caution":"해당 멤버의 로컬 데이터가 모두 삭제되므로 명령이 올바른 멤버를 대상으로 하는지 재확인한다(Patroni는 leader에 대한 reinit을 거부한다). 대형 데이터베이스에서는 몇 시간이 걸릴 수 있고 leader에 I/O/네트워크 부하를 주며, 그동안 클러스터는 replica 하나가 부족한 상태로 운영된다. --from-leader는 최근 Patroni 4.x 릴리스에만 있다(patronictl reinit --help로 확인). create_replica_methods를 건너뛰고 leader에서 pg_basebackup을 수행한다.","untested":true,"tags":["patroni","reinit","rebuild replica","rebuild","basebackup"]},{"id":"patroni-edit-config","task":"Patroni에서 클러스터 전체 파라미터 변경(edit-config) 및 대기 중인 재시작 적용","category":"Patroni","command":"# Show the current dynamic configuration stored in the DCS:\npatronictl -c /etc/patroni/patroni.yml show-config <cluster>\n# Cluster-wide PostgreSQL parameter (distributed to all members):\npatronictl -c /etc/patroni/patroni.yml edit-config <cluster> -p 'work_mem=64MB' -p 'max_connections=500' --force\n# Patroni behavior parameter:\npatronictl -c /etc/patroni/patroni.yml edit-config <cluster> -s 'maximum_lag_on_failover=1048576' --force\n# Parameters requiring a restart are flagged as 'Pending restart':\npatronictl -c /etc/patroni/patroni.yml list <cluster> -e\n# Restart replicas first, then the leader:\npatronictl -c /etc/patroni/patroni.yml restart <cluster> --role replica --pending --force\npatronictl -c /etc/patroni/patroni.yml restart <cluster> --role leader --pending --force","what":"PostgreSQL 및 Patroni 파라미터를 DCS의 동적 구성에 기록한다. Patroni는 변경을 모든 멤버에 적용하고, reload 가능한 파라미터는 즉시 활성화하며, 재시작이 필요한 파라미터는 'Pending restart'로 표시한다.","when":"Patroni가 관리하는 클러스터의 모든 노드에서 max_connections, shared_buffers, work_mem 같은 파라미터를 일관되게 변경하기 위해.","caution":"Patroni 환경에서는 postgresql.conf를 수동으로 편집하거나 ALTER SYSTEM을 사용하지 않는다. Patroni가 postgresql.conf를 다시 쓰고, postgresql.auto.conf가 Patroni 설정을 덮어써 노드 간 불일치가 생긴다. standby의 max_connections, max_worker_processes, max_wal_senders, max_prepared_transactions, max_locks_per_transaction은 primary보다 낮을 수 없으므로 재시작 순서를 그에 맞게 계획한다.","untested":true,"tags":["patroni","edit-config","parameter","pending restart","dcs"]},{"id":"patroni-pause-resume","task":"Patroni 유지보수 모드 전환 및 해제(pause/resume)","category":"Patroni","command":"# Maintenance mode: automatic failover and Patroni's control over PostgreSQL stop\npatronictl -c /etc/patroni/patroni.yml pause <cluster> --wait\npatronictl -c /etc/patroni/patroni.yml list <cluster>          # should show 'Maintenance mode: on'\n# ... maintenance work (e.g. manual PostgreSQL restart, DCS maintenance) ...\npatronictl -c /etc/patroni/patroni.yml resume <cluster> --wait","what":"클러스터를 유지보수 모드로 전환한다. Patroni는 자동 페일오버를 하지 않고 중지된 PostgreSQL을 재시작하지 않는다. resume은 정상 관리로 되돌린다.","when":"DCS(etcd/Consul) 유지보수, Patroni 업그레이드 중, 또는 Patroni의 개입 없이 PostgreSQL을 수동으로 관리해야 할 때.","caution":"pause 상태에서 primary가 다운되면 자동 페일오버가 없어 장애가 계속된다. 유지보수 후 resume을 잊지 않는다. 알림에 'Maintenance mode' 점검을 추가한다. pause 상태에서 PostgreSQL을 수동으로 primary로 시작하면 split-brain이 발생할 수 있다.","untested":true,"tags":["patroni","pause","resume","maintenance mode","maintenance"]},{"id":"patroni-dcs-health","task":"Patroni DCS(etcd/Consul) 및 REST API 상태 확인","category":"Patroni","command":"# etcd v3 (default API in etcd 3.4+): member health, leader, raft term and DB size\netcdctl --endpoints=https://<etcd1>:2379,https://<etcd2>:2379,https://<etcd3>:2379 --cacert=/etc/etcd/ca.crt endpoint health --cluster -w table\netcdctl --endpoints=https://<etcd1>:2379 --cacert=/etc/etcd/ca.crt endpoint status --cluster -w table\netcdctl --endpoints=https://<etcd1>:2379 --cacert=/etc/etcd/ca.crt alarm list\n# Consul:\nconsul members\nconsul operator raft list-peers\n# Patroni REST API (on each member):\ncurl -s http://<member>:8008/cluster | python3 -m json.tool\ncurl -s -o /dev/null -w '%{http_code}\\n' http://<member>:8008/primary   # 200 = leader, 503 = not leader","what":"Patroni가 leader lock을 보관하는 DCS의 quorum, leader, 용량과 Patroni REST API를 통해 각 멤버가 보는 자신의 역할을 점검한다. /primary 엔드포인트는 로드 밸런서(HAProxy) 상태 점검의 기반이다.","when":"Patroni가 'DCS is not accessible'을 기록할 때, 예기치 않은 demote/페일오버 후, 정기 상태 점검으로.","caution":"DCS가 quorum을 잃으면 Patroni는 leader lock을 갱신할 수 없어 primary를 읽기 전용으로 강등한다(failsafe_mode, Patroni 3.0+가 활성화되지 않은 경우). 따라서 etcd 장애는 데이터베이스 쓰기 장애가 된다. etcd DB 크기가 quota(기본 2 GB)에 도달하면 NOSPACE 알람이 쓰기를 멈춘다. DCS는 별도 디스크를 사용하는 최소 3개 노드에서 운영한다.","untested":true,"tags":["patroni","etcd","consul","dcs health","rest api"]},{"id":"pgbackrest-archive-config","task":"pgBackRest용 WAL 아카이브 구성(archive-push/archive-get)","category":"백업 및 PITR","command":"# postgresql.conf (changing archive_mode requires a restart):\nwal_level = replica\narchive_mode = on\narchive_command = 'pgbackrest --stanza=<stanza> archive-push %p'\n# On the restore/standby side (in postgresql.conf on PG12+):\nrestore_command = 'pgbackrest --stanza=<stanza> archive-get %f \"%p\"'\n\n# /etc/pgbackrest/pgbackrest.conf (database host, local repo example):\n[global]\nrepo1-path=/var/lib/pgbackrest\nrepo1-retention-full=2\narchive-async=y\nspool-path=/var/spool/pgbackrest\nprocess-max=4\ncompress-type=zst\nstart-fast=y\n\n[<stanza>]\npg1-path=/var/lib/pgsql/16/data","what":"PostgreSQL이 완료된 모든 WAL 세그먼트를 pgBackRest 저장소로 보내고(archive-push) 복구 시 그곳에서 가져오게 한다(archive-get). archive-async는 spool 디렉터리를 통해 전송을 병렬·비동기로 처리한다.","when":"pgBackRest로 물리 백업과 PITR을 구성하는 첫 단계로, stanza-create 전에.","caution":"archive_command가 실패하는 동안 WAL이 pg_wal에 쌓이고 디스크가 가득 차면 primary가 멈춘다. pg_stat_archiver.failed_count를 모니터링한다. Patroni 환경에서는 이 파라미터를 postgresql.conf 대신 patroni.yml / edit-config로 설정한다. 서로 다른 두 클러스터가 같은 stanza로 아카이브하면 저장소가 손상된다.","untested":true,"tags":["pgbackrest","archiving","archive_command","wal archive","configuration"]},{"id":"pgbackrest-stanza-create-check","task":"pgBackRest stanza 생성 및 아카이브 검증(stanza-create/check)","category":"백업 및 PITR","command":"# Create the stanza in the repository (once):\nsudo -u postgres pgbackrest --stanza=<stanza> --log-level-console=info stanza-create\n# Verify configuration and archiving end to end (switches WAL and waits for the segment to reach the repo):\nsudo -u postgres pgbackrest --stanza=<stanza> --log-level-console=info check","what":"stanza-create는 저장소에 클러스터 메타데이터(system identifier, 버전)를 생성한다. check는 실제 WAL switch로 PostgreSQL 연결, archive_command, 저장소로의 WAL 도착을 테스트한다.","when":"초기 구성 시, archive_command나 저장소 변경 후, 모니터링/cron 상태 점검으로.","caution":"명령은 postgres(또는 저장소 소유자)로 실행한다. root로 실행하면 파일 소유권이 깨진다. 메이저 버전 업그레이드 후에는 stanza-create가 아니라 stanza-upgrade가 필요하다. check가 실패하면 백업이 완료되더라도 PITR이 불가능할 수 있다.","untested":true,"tags":["pgbackrest","stanza","stanza-create","check","archive verification"]},{"id":"pgbackrest-backup-full-diff-incr","task":"pgBackRest로 full, differential, incremental 백업 수행","category":"백업 및 PITR","command":"sudo -u postgres pgbackrest --stanza=<stanza> --type=full backup\nsudo -u postgres pgbackrest --stanza=<stanza> --type=diff backup   # changes since the last full\nsudo -u postgres pgbackrest --stanza=<stanza> --type=incr backup   # changes since the last backup of any type\n# cron example (postgres user): full on Sunday, diff on other days\n# 30 1 * * 0   pgbackrest --stanza=<stanza> --type=full backup\n# 30 1 * * 1-6 pgbackrest --stanza=<stanza> --type=diff backup","what":"full 백업은 클러스터 전체를 복사하고, diff는 마지막 full 이후 변경된 파일을, incr는 유형과 관계없이 마지막 백업 이후 변경된 파일을 복사한다. 마지막에 필요한 WAL이 아카이브되었는지 검증하고 보존 정책에 따라 expire가 자동으로 실행된다.","when":"정기 백업 일정: 일반적으로 주간 full + 일일 diff/incr, 대형 클러스터에서는 더 잦은 incr.","caution":"이전 full이 없으면 diff/incr 요청은 자동으로 full로 승격된다. incr 체인이 길수록 복원 시간과 의존성이 커지며, 체인의 백업 하나를 잃으면 이후 백업을 사용할 수 없다. primary 부하를 줄이려면 backup-standby=y로 standby에서 백업한다(조정을 위해 primary는 여전히 필요).","untested":true,"tags":["pgbackrest","backup","full","differential","incremental"]},{"id":"pgbackrest-info","task":"pgBackRest 백업 카탈로그와 WAL 아카이브 범위 조회(info)","category":"백업 및 PITR","command":"sudo -u postgres pgbackrest --stanza=<stanza> info\n# JSON for monitoring/automation:\nsudo -u postgres pgbackrest --stanza=<stanza> --output=json info\n# Details of a single backup (databases, LSN range):\nsudo -u postgres pgbackrest --stanza=<stanza> --set=<backup_label> info","what":"stanza 상태, 각 백업의 label, 유형, 시작/종료 시각, 크기, 의존하는 백업, 저장소 내 WAL 아카이브의 min/max 범위를 나열한다.","when":"restore/PITR 전 대상 시점이 포함되는지 확인하고, 백업이 정기적으로 실행되는지 모니터링하며, 보존 정책을 점검하기 위해.","caution":"info는 백업이 존재함을 보여 줄 뿐 복원 가능함을 보여 주지 않는다. 정기 테스트 복원과 pgbackrest verify로 검증한다. WAL min/max 범위에 gap이 있으면 PITR은 그 구간을 넘을 수 없다.","untested":true,"tags":["pgbackrest","info","backup list","catalog","monitoring"]},{"id":"pgbackrest-restore-latest","task":"pgBackRest로 최신 백업 복원","category":"백업 및 PITR","command":"# PostgreSQL must be stopped and PGDATA empty (otherwise use --delta)\nsudo systemctl stop postgresql-16\nsudo -u postgres pgbackrest --stanza=<stanza> --log-level-console=info restore\n# From a specific backup: --set=<backup_label>\n# When restoring to another server for testing, disable archiving:\n# sudo -u postgres pgbackrest --stanza=<stanza> --archive-mode=off restore\nsudo systemctl start postgresql-16","what":"최신 백업을 복원하고 recovery.signal과 restore_command를 기록한다. 시작 시 PostgreSQL은 아카이브된 모든 WAL을 가장 최근의 일관된 시점까지 재생하고 새 timeline으로 승격한다.","when":"primary의 데이터 디렉터리를 잃었거나 손상되었을 때, 또는 백업의 복원 가능성을 정기적으로 테스트할 때.","caution":"복원된 클러스터와 이전 클러스터가 동시에 같은 stanza로 아카이브하면 저장소가 손상된다. 테스트 복원에는 --archive-mode=off를 사용한다. Patroni 환경에서는 먼저 클러스터를 pause하거나 Patroni를 중지한다. 그렇지 않으면 Patroni가 개입한다. PGDATA 외부의 테이블스페이스나 경로가 있으면 --tablespace-map 및 link 옵션을 검토한다.","untested":true,"tags":["pgbackrest","restore","restore","recovery","disaster recovery"]},{"id":"pgbackrest-pitr-restore","task":"pgBackRest로 특정 시점 복원(PITR, --type=time)","category":"백업 및 PITR","command":"sudo systemctl stop postgresql-16\nsudo -u postgres pgbackrest --stanza=<stanza> --delta --type=time \"--target=2026-10-08 14:30:00+03\" --target-action=pause restore\nsudo systemctl start postgresql-16\n# Recovery stops at the target (read-only); validate the data:\npsql -X -c \"SELECT pg_is_in_recovery(), pg_last_xact_replay_timestamp();\"\n# If correct, end recovery and open read-write on a new timeline:\npsql -X -c \"SELECT pg_wal_replay_resume();\"","what":"대상 시점 이전에 완료된 백업을 복원하고 대상 시점까지만 WAL을 재생한다. target-action=pause는 대상 시점에서 멈춰 데이터를 검증할 수 있게 하며, pg_wal_replay_resume()으로 승격한다.","when":"실수로 인한 DELETE/DROP/UPDATE 같은 논리적 오류 직전 시점으로 되돌리기 위해.","caution":"이 클러스터에서는 대상 시점 이후의 모든 변경이 손실된다. 가능하면 별도 서버에서 PITR을 실행하고 영향받은 데이터만 pg_dump로 되돌린다. 대상에 시간대를 명시한다. pgBackRest 2.24+는 대상 이전의 적합한 백업을 자동으로 선택하며, 이전 릴리스에서는 --set으로 대상 이전에 완료된 백업을 지정한다. 대상이 백업 종료 시각보다 이르면 복원이 일관성에 도달할 수 없다.","untested":true,"tags":["pgbackrest","pitr","point in time recovery","point in time","recovery"]},{"id":"pgbackrest-delta-restore-standby","task":"pgBackRest delta restore로 standby 빠르게 재구축","category":"백업 및 PITR","command":"sudo systemctl stop postgresql-16\n# Keeps the existing PGDATA and fetches only differing files; writes standby.signal:\nsudo -u postgres pgbackrest --stanza=<stanza> --delta --type=standby \"--recovery-option=primary_conninfo=host=<primary_host> port=5432 user=replicator application_name=<standby_name>\" restore\nsudo systemctl start postgresql-16\npsql -X -c \"SELECT status, sender_host FROM pg_stat_wal_receiver;\"","what":"--delta는 기존 PGDATA의 파일을 백업과 비교하여(크기/타임스탬프 및 checksum) 다른 파일만 복사한다. --type=standby는 standby.signal을 기록하고 recovery-option은 primary_conninfo를 추가한다. standby는 먼저 아카이브로 따라잡은 후 streaming으로 전환한다.","when":"뒤처지거나 timeline이 분기된 standby, 또는 pg_rewind가 실패한 이전 primary를 전체 복사 없이 다시 replica로 만들 때.","caution":"delta는 대상 디렉터리에서 다르거나 추가된 파일을 백업의 파일로 교체/제거한다. 잘못된 서버에서 실행하면 그 서버의 데이터가 파괴된다. pgBackRest는 실행 중인 PostgreSQL 위로 복원하는 것을 거부하지만 서비스가 중지되었는지 직접 확인한다. 비밀번호는 primary_conninfo가 아니라 .pgpass에 둔다.","untested":true,"tags":["pgbackrest","delta restore","rebuild standby","rebuild","replica"]},{"id":"pgbackrest-retention-expire","task":"pgBackRest 보존 정책 구성 및 expire 실행","category":"백업 및 PITR","command":"# /etc/pgbackrest/pgbackrest.conf [global] - count based:\nrepo1-retention-full-type=count\nrepo1-retention-full=2\nrepo1-retention-diff=6\n# Time-based alternative (at least a 14-day PITR window):\n# repo1-retention-full-type=time\n# repo1-retention-full=14\n\n# Expire runs automatically after each backup; to trigger it manually:\nsudo -u postgres pgbackrest --stanza=<stanza> --log-level-console=info expire","what":"보관할 full/diff 백업 개수(count) 또는 full 백업 보관 일수(time)를 정의한다. expire는 정책 밖의 백업, 이에 의존하는 diff/incr 백업, 더 이상 필요 없는 WAL 아카이브를 삭제한다.","when":"저장소 크기를 통제하고 RPO/PITR 기간 요구 사항(예: 14일 전까지 되돌릴 수 있음)을 보장하기 위해.","caution":"PITR 기간은 보존된 가장 오래된 full 백업으로 제한된다. 주간 full과 retention-full=2이면 최대 약 2주 전까지 되돌릴 수 있다. repo1-retention-archive는 기본적으로 full 보존을 따른다. 더 낮게 설정하면 오래된 백업은 일관성 시점까지만 사용할 수 있다. expire는 되돌릴 수 없다.","untested":true,"tags":["pgbackrest","retention","retention period","expire","backup cleanup"]},{"id":"standby-manual-config","task":"standby 수동 구성(standby.signal + primary_conninfo)","category":"복제","command":"# On the standby, with PostgreSQL stopped (PGDATA came from a backup/copy):\ntouch $PGDATA/standby.signal\ncat >> $PGDATA/postgresql.auto.conf <<'EOF'\nprimary_conninfo = 'host=<primary_host> port=5432 user=replicator application_name=<standby_name>'\nprimary_slot_name = '<slot>'\nEOF\npg_ctl -D $PGDATA start\n# PG13+: change the connection on a running standby without restart (e.g. point it to a new primary):\npsql -X -c \"ALTER SYSTEM SET primary_conninfo = 'host=<new_primary_host> port=5432 user=replicator application_name=<standby_name>'\" -c \"SELECT pg_reload_conf();\"","what":"standby.signal 파일은 서버를 standby로 시작하게 한다. primary_conninfo는 streaming 연결을, primary_slot_name은 사용할 replication slot을 정의한다. PG13+에서는 primary_conninfo와 primary_slot_name을 reload로 변경할 수 있다.","when":"pg_basebackup -R 없이 준비한 데이터 디렉터리(pgBackRest, 스냅샷, rsync)를 standby로 만들거나, 기존 standby가 새 primary를 바라보게 할 때.","caution":"slot은 primary에 미리 생성해야 한다(pg_create_physical_replication_slot). 그렇지 않으면 walreceiver가 계속 실패한다. 비밀번호는 conninfo 대신 ~/.pgpass에 둔다. application_name은 synchronous_standby_names의 이름과 정확히 일치해야 한다. standby.signal을 잊으면 서버가 primary로 시작하여 별도 timeline으로 이동한다.","since":"PostgreSQL 12","note":"PostgreSQL 12 이전에는 이 설정이 recovery.conf(standby_mode = on)에 있었으며, PG12에서 standby.signal과 postgresql.conf로 옮겨졌다. PG13 이전에는 primary_conninfo 변경에 재시작이 필요했다.","tags":["standby","standby.signal","primary_conninfo","replica setup","streaming"]},{"id":"standby-wal-receiver-lag","task":"standby의 WAL 수신 및 replay 지연 모니터링(pg_stat_wal_receiver)","category":"복제","command":"-- Run on the standby:\nSELECT pg_is_in_recovery() AS in_recovery,\n       pg_last_wal_receive_lsn() AS receive_lsn,\n       pg_last_wal_replay_lsn() AS replay_lsn,\n       pg_size_pretty(pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn())) AS replay_backlog,\n       now() - pg_last_xact_replay_timestamp() AS last_replay_age,\n       pg_get_wal_replay_pause_state() AS pause_state;\nSELECT pid, status, sender_host, sender_port, slot_name, received_tli, flushed_lsn, latest_end_lsn, now() - last_msg_receipt_time AS since_last_msg FROM pg_stat_wal_receiver;","what":"standby가 WAL을 얼마나 수신했고 얼마나 적용했는지(replay backlog), 마지막으로 replay된 트랜잭션의 경과 시간, replay 일시 중지 여부, walreceiver 연결 상태(어느 primary, slot, timeline, 마지막 메시지 시각)를 보여 준다.","when":"primary에 접근할 수 없거나 cascading standby에서 지연을 진단할 때. 'standby behind' 알림 시 네트워크(수신) 문제와 replay(I/O, 충돌) 문제를 구분하기 위해.","caution":"pg_stat_wal_receiver가 행을 반환하지 않으면 standby가 streaming 중이 아니다(아카이브로 공급되거나 연결이 끊김). last_replay_age는 primary에 쓰기가 없어도 증가하므로 receive_lsn/replay_lsn 차이와 함께 해석한다. 수신은 진행되는데 replay backlog가 늘어나면 standby에 장시간 쿼리(max_standby_streaming_delay)가 있거나 I/O 병목이 있다.","since":"PostgreSQL 14","note":"pg_get_wal_replay_pause_state()는 PostgreSQL 14에서 추가되었으며, 그 이전에는 pg_is_wal_replay_paused()만 있었다. 쿼리의 나머지 부분은 PG10+에서 동작한다.","tags":["standby","replication lag","wal receiver","replay","lag"]},{"id":"pg-rewind-old-primary","task":"페일오버 후 pg_rewind로 이전 primary를 replica로 재합류","category":"복제","command":"# Prerequisite (must be set BEFORE the incident): wal_log_hints = on OR data checksums; full_page_writes = on\npg_controldata $PGDATA | grep -E 'wal_log_hints|checksum'\n# 1) The old primary must be stopped:\npg_ctl -D $PGDATA stop -m fast\n# 2) Dry run first:\npg_rewind --target-pgdata=$PGDATA --source-server='host=<new_primary_host> port=5432 user=postgres dbname=postgres' --progress --dry-run\n# 3) Actual rewind + standby configuration (standby.signal + primary_conninfo):\npg_rewind --target-pgdata=$PGDATA --source-server='host=<new_primary_host> port=5432 user=postgres dbname=postgres' --progress --write-recovery-conf\n# 4) Review postgresql.auto.conf copied from the source (port, primary_slot_name), then start:\npg_ctl -D $PGDATA start","what":"timeline 분기 시점 이후 변경된 블록을 새 primary에서 복사하여 이전 primary의 데이터 디렉터리를 새 primary와 일치시킨다. 전체 basebackup 없이 몇 분 안에 replica로 만든다.","when":"페일오버/promote 후 이전 primary가 복귀했지만 분기된 변경으로 새 timeline보다 뒤처져 있을 때.","caution":"페일오버 후 이전 primary에 남아 새 primary에 도달하지 못한 트랜잭션은 영구적으로 제거된다. pg_rewind는 원본의 설정 파일(postgresql.conf, postgresql.auto.conf, pg_hba.conf)도 복사하므로 시작 전에 포트와 slot 설정을 수정한다. 분기 시점 이후 필요한 WAL이 원본에 더 이상 없으면 -c(--restore-target-wal)로 아카이브에서 가져온다. 원본 연결에는 superuser 또는 문서에 나열된 함수 권한을 가진 롤이 필요하다. Patroni에서는 use_pg_rewind: true로 자동 수행된다.","since":"PostgreSQL 13","note":"-R/--write-recovery-conf와 -c/--restore-target-wal은 PostgreSQL 13에서 추가되었다. PG13+에서 pg_rewind는 비정상 종료된 대상에 대해 crash recovery를 직접 실행한다(--no-ensure-shutdown으로 비활성화). pg_rewind는 9.5부터 존재한다.","tags":["pg_rewind","after failover","old primary","timeline","rejoin"]},{"id":"sync-standby-quorum","task":"quorum(ANY) 또는 priority(FIRST) 방식 동기 복제 구성","category":"복제","command":"-- Quorum: commit completes once any 1 standby in the list confirms\nALTER SYSTEM SET synchronous_standby_names = 'ANY 1 (stby_a, stby_b)';\n-- Priority based: the first connected standby in the list is sync, the others 'potential'\n-- ALTER SYSTEM SET synchronous_standby_names = 'FIRST 1 (stby_a, stby_b)';\nSELECT pg_reload_conf();\nSELECT application_name, state, sync_state, sync_priority FROM pg_stat_replication ORDER BY application_name;","what":"커밋이 완료되기 전에 어느 standby가 확인해야 하는지 정의한다. ANY N은 quorum 기반이며(sync_state = quorum), FIRST N은 목록 순서상 먼저 연결된 N개 standby를 동기(sync)로, 나머지를 대기 standby(potential)로 만든다. 확인 수준은 synchronous_commit(remote_write, on, remote_apply)으로 설정한다.","when":"페일오버 시 데이터 손실이 없어야 하는 시스템(RPO = 0)에서. standby가 여러 개면 standby 하나의 장애로 쓰기가 멈추지 않도록 ANY가 권장된다.","caution":"필요한 수의 동기 standby가 연결되어 있지 않으면 커밋이 오류 없이 멈추고 애플리케이션이 정지한다. 목록의 이름은 standby의 application_name과 일치해야 한다. 긴급 시 단일 세션은 SET synchronous_commit = local로 우회할 수 있다. Patroni 환경에서는 수동 설정 대신 synchronous_mode로 관리한다.","since":"PostgreSQL 10","note":"FIRST 구문은 PostgreSQL 9.6, ANY(quorum commit)는 PostgreSQL 10에서 추가되었다. 키워드 없는 'N (a, b)' 형식은 FIRST를 의미한다.","tags":["synchronous replication","synchronous_standby_names","quorum","rpo","sync"]},{"id":"slot-retained-wal-monitor","task":"replication slot이 보존하는 WAL 모니터링 및 방치된 slot 안전하게 삭제","category":"복제","command":"SELECT slot_name, slot_type, database, active, active_pid, wal_status,\n       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal,\n       pg_size_pretty(safe_wal_size) AS safe_wal_size,\n       xmin, catalog_xmin\nFROM pg_replication_slots\nORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC NULLS LAST;\n-- Safe drop: drops only if not active (returns 0 rows if active)\nSELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots WHERE slot_name = '<slot>' AND NOT active;","what":"primary에서 각 slot이 보존하는 WAL 양, wal_status(reserved/extended/unreserved/lost), max_slot_wal_keep_size 대비 남은 안전 여유(safe_wal_size), VACUUM을 붙잡는 xmin/catalog_xmin 값을 보여 준다. 두 번째 쿼리는 사용 중이 아닌 경우에만 slot을 삭제한다.","when":"pg_wal 디렉터리가 커질 때, 디스크 알림 시, 정기 모니터링에서. standby나 subscriber를 영구 제거한 후 slot 정리용.","caution":"비활성 slot은 WAL을 무제한 보존하여 디스크를 채우고 primary를 멈출 수 있다. xmin/catalog_xmin을 잡고 있는 slot은 dead tuple 정리도 막아 bloat/wraparound 위험을 만든다. slot을 삭제하면 해당 standby/subscriber가 중단 지점부터 재개할 수 없으므로(재구축 필요) 삭제 전에 소유자를 확인한다. primary에서 실행한다(standby에서는 pg_current_wal_lsn()이 실패한다).","since":"PostgreSQL 13","note":"wal_status와 safe_wal_size는 PostgreSQL 13에서 추가되었다. PG16은 logical slot에 conflicting을, PG17은 inactive_since를 추가했다. inactive_since는 방치된 slot의 가장 직접적인 지표다.","tags":["replication slot","wal accumulation","disk full","drop slot","retained wal"]},{"id":"slot-max-wal-keep-size","task":"slot이 보존할 수 있는 WAL 제한(max_slot_wal_keep_size)","category":"복제","command":"ALTER SYSTEM SET max_slot_wal_keep_size = '100GB';\nSELECT pg_reload_conf();\nSELECT slot_name, active, wal_status, pg_size_pretty(safe_wal_size) AS safe_wal_size FROM pg_replication_slots;","what":"replication slot이 pg_wal에 보존할 수 있는 WAL의 상한을 설정한다(기본 -1 = 무제한). 초과하면 checkpoint가 오래된 WAL을 제거하고 slot은 wal_status = 'lost'가 된다. 파라미터는 reload로 적용된다.","when":"연결이 끊긴 standby나 subscriber가 primary 디스크를 채워 시스템 전체를 멈추는 위험에 대비해 slot을 사용하는 모든 클러스터의 안전장치로.","caution":"이 한도는 slot 소유자를 희생하여 primary를 살리는 쪽을 택한다. slot이 'lost'된 standby는 재구축해야 하고 logical subscription은 다시 생성해야 한다. pg_wal 디스크의 여유 공간보다 작고, 예상되는 가장 긴 장애 동안 생성되는 WAL보다 큰 값을 선택한다. wal_status가 'unreserved'이면 slot이 곧 손실되므로 알림을 발생시킨다.","since":"PostgreSQL 13","note":"max_slot_wal_keep_size는 PostgreSQL 13에서 추가되었다. 그 이전에는 slot이 보존하는 WAL을 제한할 방법이 없었고 유일한 보호 수단은 모니터링과 수동 slot 삭제였다.","tags":["max_slot_wal_keep_size","slot limit","wal accumulation","disk protection","replication slot"]},{"id":"logical-add-table-refresh","task":"publication에 테이블 추가 및 subscription 갱신(REFRESH PUBLICATION)","category":"논리 복제","command":"-- Publisher:\nALTER PUBLICATION pub_app ADD TABLE orders;\n-- PG15+: all tables in a schema: ALTER PUBLICATION pub_app ADD TABLES IN SCHEMA app_schema;\nSELECT pubname, schemaname, tablename FROM pg_publication_tables WHERE pubname = 'pub_app';\n-- Subscriber (the table structure must already exist):\nALTER SUBSCRIPTION sub_app REFRESH PUBLICATION WITH (copy_data = true);\nSELECT srrelid::regclass AS table_name, srsubstate FROM pg_subscription_rel ORDER BY 1;","what":"publisher에서 publication에 새 테이블을 추가한다. subscriber에서 REFRESH PUBLICATION은 새 테이블을 subscription에 포함하고 copy_data = true로 초기 복사(tablesync)를 시작한다. srsubstate: i = init, d = data copy, f = copy finished, s = synchronized, r = ready.","when":"실행 중인 logical replication에 새 테이블을 추가하거나 publication에서 테이블을 제거할 때.","caution":"FOR ALL TABLES나 TABLES IN SCHEMA publication이라도 REFRESH를 실행하기 전에는 subscriber가 새 테이블을 인식하지 않는다. subscriber에 테이블이 없으면 REFRESH가 실패한다. DDL은 복제되지 않는다. publish된 테이블에 PRIMARY KEY나 REPLICA IDENTITY가 없으면 publisher에서 UPDATE/DELETE가 실패한다. 대형 테이블의 초기 복사는 publisher에서 slot과 스냅샷을 오랫동안 유지한다.","since":"PostgreSQL 10","note":"ADD TABLES IN SCHEMA는 PostgreSQL 15에서 추가되었다(PG15는 WHERE를 사용한 행 필터와 컬럼 목록도 추가했다).","tags":["logical replication","publication","add table","refresh publication","subscription"]},{"id":"logical-subscription-monitor","task":"logical replication 모니터링(pg_stat_subscription, apply 오류, slot 지연)","category":"논리 복제","command":"-- Subscriber:\nSELECT subname, pid, leader_pid, relid::regclass AS syncing_table, received_lsn, latest_end_lsn, now() - last_msg_receipt_time AS since_last_msg, latest_end_time FROM pg_stat_subscription;\nSELECT subname, apply_error_count, sync_error_count, stats_reset FROM pg_stat_subscription_stats;\n-- Publisher:\nSELECT slot_name, plugin, database, active, wal_status,\n       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS confirmed_lag,\n       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal\nFROM pg_replication_slots WHERE slot_type = 'logical';","what":"subscriber에서는 apply/tablesync worker, 수신 및 확인된 LSN, 마지막 메시지 시각, 누적 apply/sync 오류 수를 보여 준다. publisher에서는 logical slot의 미확인(confirmed_flush) WAL과 보존 중인 전체 WAL을 보여 준다.","when":"logical replication 구성 중, 업그레이드/마이그레이션 전환 시 지연이 0에 가까워지는지 확인하기 위해, 오류 알림용.","caution":"pid가 NULL이면 apply worker가 실행 중이 아니다(subscription 비활성화 또는 반복 실패 후 재시작). 서버 로그를 확인한다. apply_error_count가 증가하면 충돌이 있다는 뜻이다. 멈춘 subscription은 publisher에 WAL과 catalog_xmin을 쌓으므로 publisher 디스크와 카탈로그 bloat을 모니터링한다.","since":"PostgreSQL 15","note":"pg_stat_subscription_stats는 PostgreSQL 15, pg_stat_subscription.leader_pid는 PG16(parallel apply)에서 추가되었고 PG17은 worker_type 컬럼을 추가했다. 기본 pg_stat_subscription 컬럼은 PG10부터 존재한다.","tags":["logical replication","subscription monitoring","pg_stat_subscription","apply error","lag"]},{"id":"logical-conflict-skip","task":"logical replication 충돌 해결(disable_on_error, SKIP)","category":"논리 복제","command":"-- 1) Prevent a failing apply worker from looping forever:\nALTER SUBSCRIPTION sub_app SET (disable_on_error = true);\n-- 2) Find the finish LSN of the conflicting transaction in the subscriber log:\n--    CONTEXT: ... in transaction 749, finished at 0/5035378\n-- 3a) Preferred: fix/delete the conflicting row on the subscriber, then re-enable:\nALTER SUBSCRIPTION sub_app ENABLE;\n-- 3b) Or skip the whole remote transaction (NONE of its changes are applied on the subscriber):\nALTER SUBSCRIPTION sub_app SKIP (lsn = '0/5035378');\nALTER SUBSCRIPTION sub_app ENABLE;\nSELECT subname, subenabled, subskiplsn FROM pg_subscription;","what":"중복 키, 행 누락, 권한 문제 같은 오류로 subscriber의 apply가 멈추면 disable_on_error가 subscription을 자동으로 비활성화한다. 충돌 데이터를 수정한 후 ENABLE하거나, 로그의 'finished at' LSN으로 식별되는 원격 트랜잭션 전체를 SKIP한다.","when":"subscriber에 대한 직접 쓰기, 불완전한 초기 복사, 양방향 쓰기 때문에 apply worker가 실패하고 계속 재시작할 때.","caution":"SKIP은 해당 트랜잭션의 충돌하지 않는 변경도 버리므로 양쪽이 영구적으로 달라진다. 데이터 수정을 우선하고, SKIP 후에는 테이블을 비교한다. 충돌이 해결될 때까지 publisher의 slot은 WAL을 계속 쌓는다. 권한으로 subscriber 테이블에 대한 애플리케이션 쓰기를 막는다.","since":"PostgreSQL 15","note":"SKIP과 disable_on_error는 PostgreSQL 15에서 추가되었다. 그 이전에는 subscriber에서 pg_replication_origin_advance()로 origin을 전진시키는 방법(더 위험함)을 사용했다.","tags":["logical replication","conflict","conflict","skip","duplicate key"]},{"id":"logical-sync-sequences","task":"logical replication에서 시퀀스 값을 subscriber로 동기화","category":"논리 복제","command":"# Generate the setval script on the publisher and apply it on the subscriber (at cutover, after writes are stopped):\npsql -h <publisher_host> -d <db> -XAtc \"SELECT format('SELECT setval(%L, %s, %s);', quote_ident(schemaname) || '.' || quote_ident(sequencename), COALESCE(last_value, start_value), (last_value IS NOT NULL)::text) FROM pg_sequences;\" | psql -h <subscriber_host> -d <db> -X","what":"logical replication은 시퀀스(identity 포함) 값을 전달하지 않으므로, publisher에서 모든 시퀀스의 마지막 값을 읽어 subscriber에 같은 값을 설정하는 setval 명령을 생성하고 직접 적용한다.","when":"logical replication을 이용한 업그레이드/마이그레이션 전환 시, 애플리케이션을 subscriber로 전환하기 직전에.","caution":"publisher가 아직 쓰기를 받는 동안 실행하면 값이 즉시 낡아 전환 후 중복 키 오류가 발생한다. 쓰기를 중지한 후 실행하거나 안전 여유를 더한다. DDL도 복제되지 않으므로 스키마 변경(ALTER TABLE, 새 인덱스)은 양쪽에 별도로, subscriber부터 적용한다. pg_sequences는 읽을 수 없는 시퀀스의 last_value를 NULL로 반환하므로 소유자나 superuser로 명령을 실행한다.","since":"PostgreSQL 10","tags":["logical replication","sequence","setval","ddl not replicated","cutover"]},{"id":"logical-orphan-slot-cleanup","task":"subscription 제거 및 publisher의 고아 logical slot 정리","category":"논리 복제","command":"-- Subscriber: while the publisher is unreachable DROP SUBSCRIPTION cannot drop the remote slot and fails; detach the slot first:\nALTER SUBSCRIPTION sub_app DISABLE;\nALTER SUBSCRIPTION sub_app SET (slot_name = NONE);\nDROP SUBSCRIPTION sub_app;\n-- Publisher: find and drop orphaned logical slots (including tablesync slots pg_<suboid>_sync_<relid>_<sysid>):\nSELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal FROM pg_replication_slots WHERE slot_type = 'logical' AND NOT active;\nSELECT pg_drop_replication_slot('sub_app');","what":"원격 slot을 건드리지 않고 subscription을 삭제한다(slot_name = NONE). 이후 publisher에서 더 이상 소비자가 없는 logical slot과 남은 tablesync slot을 찾아 제거한다.","when":"publisher에 접근할 수 없는 상태에서 subscription을 제거해야 할 때, 마이그레이션이 끝난 후, 또는 active = false인 logical slot이 publisher에 WAL을 쌓고 있을 때.","caution":"고아 logical slot은 WAL과 catalog_xmin을 모두 보존하므로 디스크가 차고 시스템 카탈로그가 비대해진다. 그러나 slot을 삭제하면 해당 subscription은 재개할 수 없으므로 삭제 전에 이 slot을 사용하는 subscriber(다른 서버 포함)가 없는지 확인한다. 활성 slot은 삭제할 수 없으며 먼저 연결된 walsender를 종료해야 한다.","since":"PostgreSQL 10","tags":["logical replication","slot cleanup","drop subscription","orphaned slot","catalog_xmin"]},{"id":"pg-upgrade-link-runbook","task":"pg_upgrade --link로 메이저 버전 업그레이드(end-to-end runbook)","category":"업그레이드","command":"# As the postgres user, in a writable directory; example 16 -> 17\nexport OLD_BIN=/usr/pgsql-16/bin NEW_BIN=/usr/pgsql-17/bin\nexport OLD_DATA=/var/lib/pgsql/16/data NEW_DATA=/var/lib/pgsql/17/data\n# 0) initdb the new cluster with the same encoding/locale and checksum setting as the old one (-k if the old cluster has checksums)\n# 1) Compatibility check (can run while the old cluster is up, changes no data):\n$NEW_BIN/pg_upgrade --old-bindir=$OLD_BIN --new-bindir=$NEW_BIN --old-datadir=$OLD_DATA --new-datadir=$NEW_DATA --check\n# 2) Stop the old cluster and upgrade with hard links:\n$OLD_BIN/pg_ctl -D $OLD_DATA stop -m fast\n$NEW_BIN/pg_upgrade --old-bindir=$OLD_BIN --new-bindir=$NEW_BIN --old-datadir=$OLD_DATA --new-datadir=$NEW_DATA --link --jobs=4\n# 3) Carry postgresql.conf/pg_hba.conf customizations over to the new cluster, start it:\n$NEW_BIN/pg_ctl -D $NEW_DATA -l /tmp/pg17_start.log start\n# 4) Gather statistics, run generated scripts if any:\n$NEW_BIN/vacuumdb --all --analyze-in-stages\n# psql -X -f update_extensions.sql   (if pg_upgrade generated it)\n# 5) After validation and a fresh backup, delete the old cluster:\n./delete_old_cluster.sh","what":"먼저 --check로 호환성을 검증한 후, 이전 클러스터의 데이터 파일을 복사하지 않고 새 버전에 hard link하여 데이터 크기와 관계없이 몇 분 안에 메이저 업그레이드를 수행한다. 이후 단계적으로 통계를 수집하고 이전 클러스터를 제거한다.","when":"대형 클러스터를 허용 가능한 짧은 중단(몇 분)으로 제자리에서 새 메이저 버전으로 옮기기 위해.","caution":"--link를 사용하면 새 클러스터를 시작한 이후에는 이전 클러스터를 더 이상 안전하게 사용할 수 없다. 되돌리는 유일한 방법은 업그레이드 전에 받은 백업이다. 이전과 새 데이터 디렉터리는 같은 파일 시스템에 있어야 한다. analyze가 끝날 때까지 새 클러스터의 계획이 나쁠 수 있다. pgBackRest를 사용한다면 stanza-upgrade를 실행하고 즉시 새 full 백업을 받는다. 이전 버전의 백업은 새 클러스터로 복원할 수 없다.","note":"physical standby는 pg_upgrade로 업그레이드되지 않는다. primary 업그레이드 후 문서의 rsync --hard-links --size-only 방법으로 갱신하거나 재구축한다. logical slot은 PG17 이전에는 이전되지 않으며(이전 클러스터가 PG17+이면 이전됨), 플래너 통계는 PG18 이전에는 이전되지 않는다.","tags":["pg_upgrade","major upgrade","version upgrade","link","check"]},{"id":"logical-upgrade-near-zero-downtime","task":"logical replication으로 거의 무중단 메이저 버전 업그레이드","category":"업그레이드","command":"# 0) On the old version wal_level = logical (restart); every table needs a PRIMARY KEY or REPLICA IDENTITY\n# 1) Move roles and schema to the new version (a 'role postgres already exists' error is expected):\npg_dumpall -h <old_host> --globals-only | psql -h <new_host> -X -d postgres\npg_dump -h <old_host> --schema-only --create -d <db> | psql -h <new_host> -X -d postgres\n# 2) Publication on the old version, subscription on the new version (initial copy + continuous stream):\npsql -h <old_host> -d <db> -X -c \"CREATE PUBLICATION upg_pub FOR ALL TABLES;\"\npsql -h <new_host> -d <db> -X -c \"CREATE SUBSCRIPTION upg_sub CONNECTION 'host=<old_host> dbname=<db> user=<repl_user>' PUBLICATION upg_pub;\"\n# 3) Wait until all tables are 'r' (ready) and lag is ~0:\npsql -h <new_host> -d <db> -X -c \"SELECT srsubstate, count(*) FROM pg_subscription_rel GROUP BY 1;\"\npsql -h <old_host> -d <db> -X -c \"SELECT slot_name, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS lag FROM pg_replication_slots WHERE slot_name = 'upg_sub';\"\n# 4) Cutover: stop application writes, confirm lag = 0, synchronize sequences (logical-sync-sequences)\n# 5) Drop the subscription (the remote slot is dropped too), point the application to the new server:\npsql -h <new_host> -d <db> -X -c \"DROP SUBSCRIPTION upg_sub;\"","what":"새 버전으로 빈 클러스터를 만들고 스키마를 옮긴 후 logical replication으로 이전 버전에서 데이터를 지속적으로 스트리밍한다. 최종 전환 단계(쓰기 중지, 시퀀스 동기화, 리디렉션)만 짧은 중단을 일으킨다. 역방향의 두 번째 복제로 롤백 경로를 열어 둘 수 있다.","when":"pg_upgrade가 요구하는 중단을 허용할 수 없을 때, 새 하드웨어/OS로 동시에 이전할 때, 여러 메이저 버전을 건너뛸 때.","caution":"DDL, 시퀀스, large object, materialized view 내용은 복제되지 않으므로 마이그레이션 중에는 스키마를 동결한다. primary key/replica identity가 없는 테이블에서는 publisher의 UPDATE/DELETE가 실패한다. 대형 데이터베이스에서는 초기 복사가 오래 걸리며 이전 서버에 WAL과 catalog_xmin을 붙잡는다. 데이터베이스마다 별도의 publication/subscription이 필요하다. 전환 전에 행 수/checksum을 비교한다.","since":"PostgreSQL 10","note":"PostgreSQL 17에서 추가된 pg_createsubscriber는 초기 복사 없이 physical standby를 logical subscriber로 전환하여 대형 데이터베이스의 초기 복사 시간을 없앤다.","tags":["upgrade","logical replication","zero downtime migration","major upgrade","cutover"]},{"id":"pgbouncer-show-pools-stats","task":"PgBouncer pool, 통계, 연결 조회(SHOW POOLS/STATS/CLIENTS/SERVERS)","category":"PgBouncer","command":"# Connect to the PgBouncer admin console (user must be in admin_users/stats_users):\npsql -h 127.0.0.1 -p 6432 -U pgbouncer -d pgbouncer -X\nSHOW POOLS;    -- if cl_waiting > 0 and maxwait grows, the pool is saturated\nSHOW STATS;    -- per-database total/average query, transaction and wait times\nSHOW CLIENTS;  -- client connections (state, source address, linked server connection)\nSHOW SERVERS;  -- server connections opened to PostgreSQL\nSHOW CONFIG;","what":"데이터베이스/사용자 pool별로 활성 및 대기 클라이언트(cl_active, cl_waiting), 활성/idle 서버 연결(sv_active, sv_idle), 최장 대기 시간(maxwait), 쿼리/트랜잭션 카운터를 보여 준다.","when":"애플리케이션이 연결 실패/타임아웃을 보고할 때, pool 크기 산정, 유지보수 전 활성 연결을 확인할 때.","caution":"관리 콘솔은 SQL을 받지 않으며 PgBouncer 명령만 실행한다. cl_waiting이 계속 높으면 먼저 서버 측의 장시간 트랜잭션이나 락을 점검한다. default_pool_size를 무작정 올리면 PostgreSQL이 max_connections 한도와 메모리 압박에 가까워진다. SHOW STATS 값은 누적이며 평균은 stats_period 간격 기준이다.","untested":true,"tags":["pgbouncer","show pools","connection pooling","statistics","connection pool"]},{"id":"pgbouncer-pause-resume-reload","task":"PgBouncer로 유지보수 중 연결 보류(PAUSE/RESUME) 및 구성 reload(RELOAD)","category":"PgBouncer","command":"psql -h 127.0.0.1 -p 6432 -U pgbouncer -d pgbouncer -X\nRELOAD;            -- apply pgbouncer.ini and auth_file changes without dropping connections\nPAUSE <db>;        -- closes server connections once running queries/transactions finish; new requests are queued\n-- ... maintenance: switchover, PostgreSQL restart, etc. ...\nRESUME <db>;\n-- If the host in [databases] was changed (after RELOAD), gradually refresh server connections:\nRECONNECT <db>;","what":"PAUSE는 클라이언트 연결을 끊지 않고 PostgreSQL 측 연결을 비우며 새 쿼리를 대기열에 넣는다. RESUME은 대기열을 다시 흐르게 한다. 따라서 짧은 primary 재시작이나 스위치오버가 애플리케이션에는 오류가 아닌 잠깐의 지연으로 보인다.","when":"짧은 유지보수 시간대(스위치오버, 마이너 버전 재시작, 파라미터 재시작) 및 pgbouncer.ini/userlist 변경 적용 시.","caution":"PAUSE는 모든 활성 트랜잭션이 끝난 후에야 반환된다. 장시간 쿼리가 PAUSE를 붙잡는다(필요하면 PostgreSQL에서 해당 세션을 종료한다). 대기 중인 클라이언트는 query_wait_timeout(기본 120 s)이 만료되면 오류를 받으므로 그 시간 안에 유지보수를 끝낸다. PAUSE 후 RESUME을 잊으면 전체 장애가 된다. 일부 설정(listen_addr, listen_port 등)은 RELOAD로 바뀌지 않으며 재시작이 필요하다.","untested":true,"tags":["pgbouncer","pause","resume","reload","maintenance"]},{"id":"pgbouncer-pool-mode","task":"PgBouncer pool_mode 선택 및 기본 크기 산정","category":"PgBouncer","command":"; /etc/pgbouncer/pgbouncer.ini\n[databases]\nappdb = host=<primary_host> port=5432 dbname=appdb pool_mode=transaction\nreportdb = host=<primary_host> port=5432 dbname=appdb pool_mode=session\n\n[pgbouncer]\nlisten_addr = 0.0.0.0\nlisten_port = 6432\nauth_type = scram-sha-256\nauth_file = /etc/pgbouncer/userlist.txt\npool_mode = transaction\nmax_client_conn = 5000\ndefault_pool_size = 20\nreserve_pool_size = 5\nmax_prepared_statements = 200\nquery_wait_timeout = 120","what":"session 모드는 연결이 유지되는 동안 클라이언트를 하나의 서버 연결에 묶는다. transaction 모드는 트랜잭션 동안에만 서버 연결을 할당하여 수십 개의 PostgreSQL 연결로 수천 개의 클라이언트를 처리한다. statement 모드는 쿼리마다 연결을 반환하며 다중 문장 트랜잭션을 허용하지 않는다. pool_mode는 데이터베이스별로도 재정의할 수 있다.","when":"수명이 짧은 연결을 많이 여는 OLTP/마이크로서비스 애플리케이션에는 transaction 모드, 세션 상태에 의존하는 작업(보고, 마이그레이션, LISTEN을 사용하는 서비스)에는 session 모드.","caution":"transaction 모드에서는 트랜잭션 간 세션 상태가 유지되지 않는다. SET(SET LOCAL 제외), 세션 수준 advisory lock, LISTEN, WITH HOLD 커서, 트랜잭션을 넘는 임시 테이블, SQL 수준 PREPARE는 신뢰할 수 없다. 프로토콜 수준 prepared statement는 PgBouncer 1.21+와 max_prepared_statements > 0에서만 지원된다(1.24부터 기본값 200, 이전에는 0). default_pool_size에 (데이터베이스, 사용자) 쌍의 수를 곱한 값이 PostgreSQL max_connections(superuser_reserved_connections 여유분 제외)를 넘어서는 안 된다.","untested":true,"tags":["pgbouncer","pool_mode","transaction mode","connection pooling","sizing"]}]},"recipes/postgres-ops.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"recipes":[{"id":"pgss-top-queries","task":"가장 비용이 큰 쿼리 찾기(pg_stat_statements: 총 시간 및 평균 시간)","category":"성능","command":"-- By total time: queries consuming the most server time\nSELECT queryid,\n       calls,\n       round(total_exec_time::numeric, 1) AS total_ms,\n       round(mean_exec_time::numeric, 2) AS mean_ms,\n       round(stddev_exec_time::numeric, 2) AS stddev_ms,\n       round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 1) AS pct_total,\n       rows / NULLIF(calls, 0) AS rows_per_call,\n       left(regexp_replace(query, '\\s+', ' ', 'g'), 120) AS query\nFROM pg_stat_statements\nWHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database())\nORDER BY total_exec_time DESC\nLIMIT 20;\n\n-- By mean time per call: individually slow queries\nSELECT queryid,\n       calls,\n       round(mean_exec_time::numeric, 2) AS mean_ms,\n       round(max_exec_time::numeric, 2) AS max_ms,\n       round(mean_plan_time::numeric, 2) AS mean_plan_ms,\n       left(regexp_replace(query, '\\s+', ' ', 'g'), 120) AS query\nFROM pg_stat_statements\nWHERE calls >= 50\nORDER BY mean_exec_time DESC\nLIMIT 20;","what":"현재 데이터베이스에서 서버 시간을 가장 많이 소비하는 정규화된 쿼리를 전체 대비 비율과 함께 나열한다. 두 번째 쿼리는 호출당 가장 느린 쿼리를 나열한다.","when":"CPU/IO 부하가 오르거나 사용자가 '데이터베이스가 느리다'고 보고할 때의 첫 확인 지점. 튜닝을 어디서 시작할지 결정하기 위해.","caution":"pg_stat_statements는 shared_preload_libraries에 추가하고 서버를 재시작한 후 데이터베이스에서 CREATE EXTENSION을 실행하기 전에는 아무것도 수집하지 않는다. 값은 마지막 reset 이후 누적이다. mean_ms 대비 stddev_ms가 높으면 호출 간 계획이나 데이터 분포가 달라진다는 뜻이다. mean_plan_time은 pg_stat_statements.track_planning = on(기본 off, 활성화 시 오버헤드 추가)일 때만 채워진다. 다른 사용자의 쿼리 텍스트를 보려면 superuser 또는 pg_read_all_stats 롤이 필요하다.","since":"PostgreSQL 13","note":"PostgreSQL 13에서 total_time/mean_time 컬럼은 total_exec_time/mean_exec_time으로 이름이 바뀌었고 계획 시간(total_plan_time)은 별도 컬럼이 되었다. PG12 이하에서는 이전 이름을 사용한다.","tags":["pg_stat_statements","slow query","most expensive query","top sql","performance","query tuning"]},{"id":"pgss-io-temp-heavy","task":"디스크 I/O, 임시 파일, WAL을 유발하는 쿼리 찾기(pg_stat_statements)","category":"성능","command":"SELECT queryid,\n       calls,\n       shared_blks_hit,\n       shared_blks_read,\n       round(100.0 * shared_blks_hit / NULLIF(shared_blks_hit + shared_blks_read, 0), 1) AS hit_pct,\n       pg_size_pretty(temp_blks_written * current_setting('block_size')::bigint) AS temp_written,\n       pg_size_pretty(wal_bytes) AS wal,\n       left(regexp_replace(query, '\\s+', ' ', 'g'), 120) AS query\nFROM pg_stat_statements\nORDER BY shared_blks_read + temp_blks_read + temp_blks_written DESC\nLIMIT 20;","what":"shared buffers 밖에서 가장 많은 블록을 읽고, 임시 파일에 가장 많이 쓰며(work_mem에 들어가지 않는 정렬/해시), 가장 많은 WAL을 생성하는 쿼리를 cache hit ratio와 함께 보여 준다.","when":"디스크 읽기 지연이 증가하거나, 임시 파일 알림이 발생하거나(log_temp_files), WAL 양이 갑자기 늘어날 때 원인 쿼리를 찾기 위해.","caution":"shared_blks_read는 OS page cache에서 처리된 읽기도 포함한다. 실제 디스크 시간에는 track_io_timing = on이 필요하다(먼저 pg_test_timing으로 오버헤드를 측정). temp_written이 높으면 해결책은 대개 롤/세션별 work_mem 상향 또는 쿼리/인덱스 수정이다. work_mem을 전역으로 올리면 메모리 위험이 커진다.","since":"PostgreSQL 13","note":"wal_bytes/wal_records 컬럼은 PG13에서 도입되었다. PG17에서 blk_read_time/blk_write_time은 shared_blk_read_time/shared_blk_write_time으로 이름이 바뀌었고 local_blk_*_time이 추가되었다.","tags":["pg_stat_statements","io","temp files","temp_blks_written","work_mem","wal"]},{"id":"pgss-reset","task":"pg_stat_statements 통계 reset(전체 또는 단일 쿼리)","category":"성능","command":"-- Last reset time and number of entries evicted because the table was full (dealloc)\nSELECT dealloc, stats_reset FROM pg_stat_statements_info;\n\n-- Reset a single query only (userid, dbid, queryid; 0 = all)\nSELECT pg_stat_statements_reset(0, 0, 5927525816675996975);\n\n-- Reset all statistics\nSELECT pg_stat_statements_reset();","what":"pg_stat_statements 카운터를 전체 또는 사용자/데이터베이스/queryid별로 reset한다. pg_stat_statements_info는 마지막 reset 시각과 공간 부족으로 제거된 항목 수를 보여 준다.","when":"튜닝 변경(인덱스, 파라미터, 배포) 전 깨끗한 측정을 시작하거나 단일 쿼리의 새 계획을 독립적으로 측정하기 위해.","caution":"reset은 되돌릴 수 없다. 누적 카운터에 의존하는 모니터링/APM 도구에 공백이 생기므로 먼저 스냅샷을 받는다(실행 계획 회귀 레시피 참고). dealloc이 계속 증가하면 pg_stat_statements.max가 너무 작은 것이다(변경에 재시작 필요). 기본적으로 superuser만 실행할 수 있으며 GRANT EXECUTE ON FUNCTION으로 권한을 준다.","since":"PostgreSQL 14","note":"파라미터 지정 reset(userid, dbid, queryid)은 PG12, pg_stat_statements_info 뷰는 PG14에서 도입되었다. PG17은 네 번째 파라미터 minmax_only를 추가했다: pg_stat_statements_reset(0, 0, 0, true)은 min/max 시간만 reset한다. stats_since와 minmax_stats_since 컬럼도 PG17이다.","tags":["pg_stat_statements","reset","clear statistics","queryid","statistics","baseline"]},{"id":"auto-explain-setup","task":"auto_explain으로 느린 쿼리의 실행 계획 로깅","category":"성능","command":"-- 1) Per session: effective immediately, no restart (superuser)\nLOAD 'auto_explain';\nSET auto_explain.log_min_duration = '500ms';\nSET auto_explain.log_analyze = on;\nSET auto_explain.log_buffers = on;\nSET auto_explain.log_timing = off;\nSET auto_explain.log_nested_statements = on;\n\n-- 2) Persistent: loaded by new sessions, a reload is enough.\n--    The module must be LOADed in this session before ALTER SYSTEM accepts auto_explain.* parameters.\nLOAD 'auto_explain';\nALTER SYSTEM SET session_preload_libraries = 'auto_explain';\nALTER SYSTEM SET auto_explain.log_min_duration = '2s';\nALTER SYSTEM SET auto_explain.log_analyze = on;\nALTER SYSTEM SET auto_explain.log_buffers = on;\nALTER SYSTEM SET auto_explain.log_timing = off;\nALTER SYSTEM SET auto_explain.sample_rate = 0.1;\nSELECT pg_reload_conf();","what":"임계값을 초과하는 쿼리의 실행 계획을 서버 로그에 기록한다(log_analyze가 on이면 실제 행 수와 buffer 데이터 포함). 애플리케이션의 실제 파라미터로 생성된 계획을 포착한다.","when":"쿼리가 애플리케이션에서 또는 특정 파라미터로만 느리고 수동 EXPLAIN으로 재현할 수 없을 때. 간헐적인 계획 변경을 포착하기 위해.","caution":"log_analyze = on은 임계값 미만을 포함한 모든 샘플 쿼리에 계측 오버헤드를 추가한다. log_timing = off와 sample_rate < 1로 줄일 수 있다. log_min_duration = 0은 운영 환경에서 로그를 넘치게 한다. ALTER SYSTEM SET session_preload_libraries는 기존 값을 덮어쓰므로 먼저 SHOW session_preload_libraries를 확인한다. session_preload_libraries는 기존 세션에 영향을 주지 않으며, 모든 프로세스에 로드하려면 shared_preload_libraries와 재시작이 필요하다.","note":"auto_explain은 contrib 모듈이며 CREATE EXTENSION이 필요 없다. log_wal(PG13+)과 log_settings(PG12+) 옵션도 사용할 수 있다.","tags":["auto_explain","execution plan","slow query","log","plan capture","explain"]},{"id":"explain-analyze-buffers-wal","task":"EXPLAIN (ANALYZE, BUFFERS, SETTINGS, WAL)으로 실행 계획 심층 분석","category":"성능","command":"-- ANALYZE really executes the statement: run DML inside a transaction and roll it back\nBEGIN;\nEXPLAIN (ANALYZE, BUFFERS, SETTINGS, WAL, VERBOSE)\nUPDATE customer SET balance = balance + 1 WHERE city = 'Ankara';\nROLLBACK;\n\n-- For SELECT\nEXPLAIN (ANALYZE, BUFFERS, SETTINGS)\nSELECT name FROM customer ORDER BY name DESC LIMIT 10 OFFSET 1000;","what":"실제 실행 통계와 함께 계획을 반환한다. BUFFERS는 노드별 shared hit/read/dirtied 및 temp 블록을, SETTINGS는 계획에 영향을 주는 기본값이 아닌 파라미터를, WAL은 생성된 WAL 레코드/바이트를 보여 준다.","when":"추정 행과 실제 행의 차이, 디스크로 넘치는 정렬/해시(Sort Method: external merge, temp read/written), DML의 WAL 비용을 분석하기 위해.","caution":"ANALYZE는 문장을 실제로 실행하므로 DML은 항상 BEGIN/ROLLBACK 안에서 실행한다(트리거와 시퀀스 증가는 여전히 발생한다). 읽기 팁: 추정 'rows'와 'actual rows' 차이가 10배 이상이면 통계 문제를 가리킨다. 루프 안의 노드는 시간과 행이 루프당 값이므로 전체는 loops를 곱한다. Index Only Scan의 'Heap Fetches'가 높으면 visibility map이 오래된 것이고, 'Workers Planned' > 'Workers Launched'이면 worker pool이 고갈된 것이다. 타이밍 오버헤드가 크면 TIMING OFF를 사용한다. COSTS OFF는 JIT 섹션도 숨긴다.","since":"PostgreSQL 13","note":"SETTINGS 옵션은 PG12, WAL은 PG13에서 도입되었다. PG16은 GENERIC_PLAN을, PG17은 SERIALIZE와 MEMORY를 추가했으며, PG18에서는 ANALYZE 사용 시 BUFFERS가 기본으로 켜진다.","tags":["explain analyze","buffers","execution plan","reading plans","wal","performance"]},{"id":"plan-regression-queryid","task":"queryid로 실행 계획 회귀 포착(pg_stat_statements 스냅샷 비교)","category":"성능","command":"-- 1) Take a reference snapshot (before deploy/upgrade/ANALYZE)\nCREATE TABLE IF NOT EXISTS pgss_snapshot AS\n  SELECT now() AS snap_time, userid, dbid, queryid, calls, total_exec_time,\n         shared_blks_hit, shared_blks_read, left(query, 200) AS query\n  FROM pg_stat_statements WITH NO DATA;\nINSERT INTO pgss_snapshot\n  SELECT now(), userid, dbid, queryid, calls, total_exec_time,\n         shared_blks_hit, shared_blks_read, left(query, 200)\n  FROM pg_stat_statements;\n\n-- 2) Later: queries whose time per call grew the most since the snapshot\nWITH base AS (\n  SELECT DISTINCT ON (userid, dbid, queryid) *\n  FROM pgss_snapshot\n  ORDER BY userid, dbid, queryid, snap_time DESC\n)\nSELECT s.queryid,\n       s.calls - b.calls AS new_calls,\n       round((b.total_exec_time / b.calls)::numeric, 2) AS old_mean_ms,\n       round(((s.total_exec_time - b.total_exec_time) / (s.calls - b.calls))::numeric, 2) AS new_mean_ms,\n       round((b.shared_blks_hit + b.shared_blks_read)::numeric / b.calls, 0) AS old_blks_per_call,\n       round((s.shared_blks_hit + s.shared_blks_read - b.shared_blks_hit - b.shared_blks_read)::numeric\n             / (s.calls - b.calls), 0) AS new_blks_per_call,\n       left(regexp_replace(s.query, '\\s+', ' ', 'g'), 100) AS query\nFROM pg_stat_statements s\nJOIN base b USING (userid, dbid, queryid)\nWHERE s.calls - b.calls >= 10\n  AND b.calls > 0\nORDER BY ((s.total_exec_time - b.total_exec_time) / (s.calls - b.calls))\n         / NULLIF(b.total_exec_time / b.calls, 0) DESC NULLS LAST\nLIMIT 20;\n\n-- 3) Is the suspicious queryid running right now?\nSELECT pid, state, now() - query_start AS runtime, left(query, 100) AS query\nFROM pg_stat_activity\nWHERE query_id = -4094682686355101727;","what":"pg_stat_statements의 누적 카운터를 테이블에 저장하고 이후 상태와 queryid별로 비교한다. 스냅샷 이후 호출의 호출당 평균 시간과 buffer를 이전 값 옆에 보여 준다.","when":"배포, 메이저 업그레이드, 통계 갱신, 파라미터 변경 후 계획이 바뀐 쿼리를 찾기 위해('어제는 빨랐는데 오늘은 느리다').","caution":"스냅샷과 비교 사이에 pg_stat_statements_reset()이 실행되거나 공간 부족으로 항목이 제거되면(dealloc) delta 값은 의미가 없다. pg_stat_statements_info.stats_reset을 확인한다. 호출당 buffer 증가는 캐싱과 무관하므로 시간 증가보다 계획 변경의 더 신뢰할 만한 신호다. 계획을 확인하려면 실제 파라미터로 쿼리를 EXPLAIN하거나 auto_explain을 사용한다. PostgreSQL에는 내장 계획 고정 기능이 없다(pg_hint_plan 확장 필요). 끝나면 스냅샷 테이블을 삭제한다.","since":"PostgreSQL 14","note":"pg_stat_activity.query_id와 compute_query_id 파라미터는 PG14에서 도입되었다(기본 auto: pg_stat_statements가 로드되면 계산). EXPLAIN VERBOSE 출력의 'Query Identifier' 줄이 같은 값을 보여 준다. 1단계와 2단계는 PG13에서도 동작한다.","tags":["plan regression","queryid","pg_stat_statements","snapshot","plan change","performance"]},{"id":"parallel-query-usage","task":"병렬 쿼리 용량 및 현재 worker 사용량 확인","category":"성능","command":"-- Parallel worker pool settings\nSELECT name, current_setting(name) AS value\nFROM pg_settings\nWHERE name IN ('max_worker_processes', 'max_parallel_workers', 'max_parallel_workers_per_gather',\n               'max_parallel_maintenance_workers', 'parallel_setup_cost', 'parallel_tuple_cost',\n               'min_parallel_table_scan_size', 'min_parallel_index_scan_size')\nORDER BY name;\n\n-- Queries currently running in parallel and their worker counts\nSELECT a.pid AS leader_pid,\n       count(w.pid) AS workers_running,\n       current_setting('max_parallel_workers')::int AS max_parallel_workers,\n       now() - a.query_start AS runtime,\n       left(a.query, 80) AS query\nFROM pg_stat_activity a\nJOIN pg_stat_activity w ON w.leader_pid = a.pid AND w.pid <> a.pid\nGROUP BY a.pid, a.query_start, a.query\nORDER BY workers_running DESC;\n\n-- Pin / reset the planned worker count for a single table\nALTER TABLE orders SET (parallel_workers = 4);\nALTER TABLE orders RESET (parallel_workers);","what":"병렬 쿼리 한도와 각 leader 프로세스가 현재 사용 중인 parallel worker 수를 보여 주고, 테이블별 worker 수 설정을 예시한다.","when":"분석 쿼리가 예상보다 느리거나, 계획에 'Workers Planned' > 'Workers Launched'가 나타나거나, CPU 코어가 놀고 있을 때.","caution":"max_parallel_workers는 max_worker_processes(재시작 필요)로 상한이 정해지며, 이 pool은 logical replication worker 및 기타 background worker와 공유된다. OLTP에서 max_parallel_workers_per_gather가 높으면 동시성 상황에서 CPU가 포화된다. 각 worker는 자체 work_mem도 사용한다. 데이터를 쓰거나 행을 잠그는 쿼리(CREATE TABLE AS / SELECT INTO 제외), 커서, PARALLEL UNSAFE 함수를 호출하는 쿼리는 병렬 계획을 받지 않는다. ALTER TABLE ... SET (parallel_workers)는 짧은 ShareUpdateExclusiveLock을 잡는다.","since":"PostgreSQL 13","note":"pg_stat_activity.leader_pid는 PG13에서 도입되었다. 그 이전에는 parallel worker가 backend_type = 'parallel worker'로만 나타나 leader와 연결할 수 없다.","tags":["parallel query","parallel worker","leader_pid","max_parallel_workers","gather","cpu"]},{"id":"jit-impact-check","task":"JIT 컴파일이 쿼리 시간에 미치는 영향 측정","category":"성능","command":"-- Share of JIT time in total execution time (PG15+)\nSELECT queryid,\n       calls,\n       jit_functions,\n       round((jit_generation_time + jit_inlining_time + jit_optimization_time + jit_emission_time)::numeric, 1) AS jit_ms,\n       round(total_exec_time::numeric, 1) AS total_ms,\n       round((100 * (jit_generation_time + jit_inlining_time + jit_optimization_time + jit_emission_time)\n             / NULLIF(total_exec_time, 0))::numeric, 1) AS jit_pct,\n       left(regexp_replace(query, '\\s+', ' ', 'g'), 100) AS query\nFROM pg_stat_statements\nWHERE jit_functions > 0\nORDER BY jit_pct DESC NULLS LAST\nLIMIT 20;\n\n-- Compare on a single query: check the \"JIT:\" section at the end of the output and Execution Time\nSET jit = on;\nEXPLAIN (ANALYZE) SELECT city, sum(balance) FROM customer GROUP BY city;\nSET jit = off;\nEXPLAIN (ANALYZE) SELECT city, sum(balance) FROM customer GROUP BY city;\nRESET jit;","what":"쿼리별로 실행 시간 중 JIT 컴파일 시간(generation, inlining, optimization, emission)의 비중을 보여 주고, JIT를 켜고 끈 상태로 같은 쿼리를 실행해 순 효과를 측정한다.","when":"짧은 OLTP나 보고서 쿼리에 수십~수백 ms의 원인 불명 지연이 있을 때. 특히 비용 추정이 부풀어 JIT가 트리거되는 파티션이 많은 테이블에서.","caution":"JIT는 계획 비용이 jit_above_cost(기본 100000)를 넘을 때만 동작한다. 비용 추정이 부풀면 짧은 쿼리에서 컴파일이 실행보다 오래 걸릴 수 있다. 전역으로 끄기보다 먼저 해당 롤/데이터베이스에 ALTER ROLE ... SET jit = off를 적용하거나 jit_above_cost를 높이는 것을 고려한다. EXPLAIN (ANALYZE, COSTS OFF)는 JIT 섹션을 출력하지 않는다. PostgreSQL이 LLVM 지원 없이 빌드되었으면 JIT는 사용되지 않는다. SELECT pg_jit_available();로 확인한다.","since":"PostgreSQL 15","note":"jit_* 컬럼은 PG15에서 pg_stat_statements에 추가되었고 PG17은 jit_deform_count/jit_deform_time을 추가했다. JIT는 PG11에서 도입되었으며 PG12부터 기본으로 켜져 있다.","tags":["jit","llvm","jit_above_cost","pg_stat_statements","compilation time","performance"]},{"id":"blocking-tree","task":"블로킹 트리 구성(root blocker 찾기)","category":"세션","command":"WITH RECURSIVE tree AS (\n  SELECT a.pid, a.pid AS root_pid, 0 AS level, ARRAY[a.pid] AS path\n  FROM pg_stat_activity a\n  WHERE cardinality(pg_blocking_pids(a.pid)) = 0\n    AND EXISTS (SELECT 1 FROM pg_stat_activity w WHERE a.pid = ANY (pg_blocking_pids(w.pid)))\n  UNION ALL\n  SELECT w.pid, t.root_pid, t.level + 1, t.path || w.pid\n  FROM tree t\n  JOIN pg_stat_activity w ON t.pid = ANY (pg_blocking_pids(w.pid))\n  WHERE w.pid <> ALL (t.path)\n), node AS (\n  SELECT DISTINCT ON (pid) pid, root_pid, level\n  FROM tree\n  ORDER BY pid, level DESC\n)\nSELECT repeat('  ', n.level) || n.pid AS pid_tree,\n       n.root_pid,\n       pg_blocking_pids(n.pid) AS blocked_by,\n       a.usename,\n       a.state,\n       a.wait_event_type || ':' || a.wait_event AS wait,\n       now() - a.xact_start AS xact_age,\n       left(regexp_replace(a.query, '\\s+', ' ', 'g'), 80) AS query\nFROM node n\nJOIN pg_stat_activity a USING (pid)\nORDER BY n.root_pid, n.level, n.pid;","what":"pg_blocking_pids()로 대기 체인을 재귀적으로 추적한다. 아무도 기다리지 않는 root blocker부터 시작해 모든 대기 세션을 직접 블로킹하는 PID와 트랜잭션 경과 시간과 함께 들여쓴 트리로 보여 준다.","when":"애플리케이션에 대량 타임아웃이나 대기열 적체가 발생할 때. 수십 개의 대기 세션 중 개입이 필요한 단일 root 세션을 찾기 위해.","caution":"root는 대개 락을 잡고 잊힌 'idle in transaction' 세션이다. query 컬럼은 락을 잡은 문장이 아니라 그 세션의 마지막 문장을 보여 준다. ACCESS EXCLUSIVE를 기다리는 DDL은 뒤에 대기하는 단순 SELECT도 블로킹하며 중간 노드로 나타난다. pg_blocking_pids()는 lock manager를 잠시 잠그므로 이 쿼리를 짧은 간격으로 반복 실행하지 않는다. 여러 경로로 도달 가능한 세션은 가장 깊은 수준에서 한 번만 표시된다.","since":"PostgreSQL 9.6","tags":["blocking","lock tree","lock chain","pg_blocking_pids","blocking session","lock"]},{"id":"lock-wait-detail","task":"객체 및 lock mode별 락 대기 상세 확인(pg_locks)","category":"세션","command":"SELECT DISTINCT\n       w.pid AS waiting_pid,\n       w.locktype,\n       coalesce(w.relation::regclass::text, 'xid ' || w.transactionid, w.virtualxid, w.objid::text) AS target,\n       w.mode AS wanted_mode,\n       b.pid AS blocker_pid,\n       b.mode AS blocker_mode,\n       b.granted AS blocker_holds,\n       now() - wa.state_change AS waiting_for,\n       left(wa.query, 60) AS waiting_query,\n       left(ba.query, 60) AS blocker_query\nFROM pg_locks w\nJOIN pg_locks b\n  ON b.pid = ANY (pg_blocking_pids(w.pid))\n AND b.locktype = w.locktype\n AND b.database IS NOT DISTINCT FROM w.database\n AND b.relation IS NOT DISTINCT FROM w.relation\n AND b.page IS NOT DISTINCT FROM w.page\n AND b.tuple IS NOT DISTINCT FROM w.tuple\n AND b.virtualxid IS NOT DISTINCT FROM w.virtualxid\n AND b.transactionid IS NOT DISTINCT FROM w.transactionid\n AND b.classid IS NOT DISTINCT FROM w.classid\n AND b.objid IS NOT DISTINCT FROM w.objid\n AND b.objsubid IS NOT DISTINCT FROM w.objsubid\nJOIN pg_stat_activity wa ON wa.pid = w.pid\nJOIN pg_stat_activity ba ON ba.pid = b.pid\nWHERE NOT w.granted\nORDER BY waiting_for DESC, waiting_pid;","what":"허가되지 않은 모든 락 요청(granted = false)에 대해 대상 객체(테이블, transaction id 등), 요청 모드, 블로킹 PID와 그 모드, blocker가 실제로 락을 보유하는지 여부, 대략적인 대기 시간을 보여 준다.","when":"어떤 테이블과 lock mode 충돌이 블로킹을 일으키는지 파악하기 위해. 예: AccessExclusiveLock을 기다리는 DDL, transactionid 대기로 나타나는 행 락.","caution":"매칭에 pg_blocking_pids()를 사용하므로 실제로 충돌하는 요청만 나열된다. blocker_holds = false는 blocker가 아직 락을 보유하지 않았지만 충돌하는 모드로 앞에 대기 중임을 뜻한다(lock queue). locktype = 'transactionid'는 행 수준 충돌이다. 대기자는 해당 행을 수정한 트랜잭션이 끝날 때까지 기다리며, 행 자체는 pg_locks에 보이지 않는다. 대형 시스템에서 pg_locks는 수천 행일 수 있으므로 자주 실행하지 않는다. waiting_for는 state_change 기준의 근사값이다.","since":"PostgreSQL 9.6","tags":["pg_locks","lock wait","lock waiting","accessexclusivelock","transactionid","lock"]},{"id":"long-idle-transactions","task":"장시간 실행 및 'idle in transaction' 트랜잭션 찾기","category":"세션","command":"SELECT pid,\n       usename,\n       datname,\n       application_name,\n       client_addr,\n       state,\n       now() - xact_start AS xact_age,\n       now() - state_change AS in_state_for,\n       backend_xid,\n       age(backend_xmin) AS xmin_age,\n       left(regexp_replace(query, '\\s+', ' ', 'g'), 100) AS last_query\nFROM pg_stat_activity\nWHERE xact_start IS NOT NULL\n  AND backend_type = 'client backend'\n  AND pid <> pg_backend_pid()\n  AND (now() - xact_start > interval '5 minutes' OR state LIKE 'idle in transaction%')\nORDER BY xact_start;","what":"열린 트랜잭션이 5분보다 오래되었거나 트랜잭션 안에서 idle 상태인 세션을 트랜잭션 경과 시간, 현재 상태 유지 시간, 할당된 XID, 보유한 xmin의 경과와 함께 보여 준다.","when":"bloat이 증가할 때, VACUUM이 'dead row versions cannot be removed yet'을 보고할 때, 락 대기 중, 또는 커넥션 풀 누수가 의심될 때.","caution":"장시간 트랜잭션(특히 REPEATABLE READ/SERIALIZABLE 또는 열린 커서를 보유한 트랜잭션)은 backend_xmin을 통해 VACUUM의 정리 기준선(xmin horizon)을 붙잡아 데이터베이스 전체에 bloat과 XID 노화를 일으킨다. 'idle in transaction (aborted)'는 오류가 발생해 ROLLBACK을 기다리는 세션이다. 영구 해결: 롤별 idle_in_transaction_session_timeout과 애플리케이션의 올바른 트랜잭션 경계.","tags":["long transaction","idle in transaction","xmin","bloat","session","long running"]},{"id":"terminate-idle-in-transaction","task":"장시간 'idle in transaction' 세션을 통제된 방식으로 일괄 종료","category":"세션","command":"-- 1) Dry run first: who will be affected?\nSELECT pid, usename, application_name, client_addr,\n       now() - state_change AS idle_for, left(query, 80) AS last_query\nFROM pg_stat_activity\nWHERE state IN ('idle in transaction', 'idle in transaction (aborted)')\n  AND now() - state_change > interval '15 minutes'\n  AND backend_type = 'client backend'\n  AND pid <> pg_backend_pid();\n\n-- 2) Terminate; wait up to 5 s for each process to exit\nSELECT pid, usename, now() - state_change AS idle_for,\n       pg_terminate_backend(pid, 5000) AS terminated\nFROM pg_stat_activity\nWHERE state IN ('idle in transaction', 'idle in transaction (aborted)')\n  AND now() - state_change > interval '15 minutes'\n  AND backend_type = 'client backend'\n  AND pid <> pg_backend_pid();","what":"먼저 트랜잭션을 열어 둔 채 일정 시간 idle 상태인 세션을 나열한 후 종료한다. timeout 파라미터는 프로세스가 실제로 종료될 때까지(또는 timeout 만료까지) 대기한다.","when":"락을 잡고 잊힌 세션이 블로킹을 일으키거나 xmin horizon을 붙잡아 VACUUM을 멈추게 할 때의 긴급 조치용.","caution":"pg_cancel_backend는 'idle in transaction' 세션에는 아무 효과가 없다(취소할 실행 중 쿼리가 없음). pg_terminate_backend가 필요하며 커밋되지 않은 모든 변경이 롤백된다. usename/application_name으로 필터를 좁히고 복제, 백업, 모니터링 세션은 제외한다. 커넥션 풀은 종료된 연결을 다시 연다. timeout이 만료되면 함수는 경고를 내고 false를 반환한다. 영구 해결책으로 롤별 idle_in_transaction_session_timeout을 설정한다.","since":"PostgreSQL 14","note":"인수 두 개인 pg_terminate_backend(pid, timeout)는 PG14에서 도입되었다. 그 이전에는 pg_terminate_backend(pid)만 있으며 시그널을 보낸 직후 반환한다.","tags":["pg_terminate_backend","idle in transaction","terminate session","kill session","bulk","lock"]},{"id":"role-timeouts","task":"롤 및 데이터베이스별 statement_timeout / lock_timeout 설정","category":"세션","command":"-- Application role: short queries, short lock waits, a limit on forgotten transactions\nALTER ROLE web_app SET statement_timeout = '30s';\nALTER ROLE web_app SET lock_timeout = '5s';\nALTER ROLE web_app SET idle_in_transaction_session_timeout = '10min';\n\n-- Let the reporting role run longer, only in this database\nALTER ROLE report_app IN DATABASE mydb SET statement_timeout = '15min';\n\n-- Show configured role/database settings\nSELECT coalesce(r.rolname, '(all)') AS role,\n       coalesce(d.datname, '(all)') AS database,\n       s.setconfig\nFROM pg_db_role_setting s\nLEFT JOIN pg_roles r ON r.oid = s.setrole\nLEFT JOIN pg_database d ON d.oid = s.setdatabase\nORDER BY 1, 2;\n\n-- Revert\nALTER ROLE web_app RESET lock_timeout;","what":"timeout 파라미터를 롤(필요하면 데이터베이스) 수준에서 영구 설정하여 전역 설정을 건드리지 않고 각 워크로드가 자체 한도로 실행되게 한다.","when":"폭주 쿼리가 리소스를 고갈시키고, DDL/락 대기열이 애플리케이션을 멈추며, 잊힌 트랜잭션이 bloat을 만드는 것을 막기 위해.","caution":"설정은 새 세션에만 적용되므로 기존 pool 연결을 재순환한다. 사용자는 세션에서 SET으로 값을 재정의할 수 있으므로 보안 경계가 아니다. postgresql.conf에서 statement_timeout을 전역으로 설정하지 않는다. pg_dump, VACUUM, CREATE INDEX 같은 유지보수 작업도 취소된다. 우선순위: role+database > role > database > postgresql.conf.","note":"idle_in_transaction_session_timeout은 PG9.6, idle_session_timeout은 PG14, transaction_timeout은 PG17에서 도입되었다.","tags":["statement_timeout","lock_timeout","idle_in_transaction_session_timeout","alter role","query timeout","timeout"]},{"id":"ddl-lock-timeout-retry","task":"lock_timeout과 재시도로 DDL 안전하게 적용(lock queue 방지)","category":"세션","command":"-- Single attempt: give up if the lock is not acquired within 3 s instead of queueing and blocking traffic\nSET lock_timeout = '3s';\nALTER TABLE customer ADD COLUMN IF NOT EXISTS segment text;\nRESET lock_timeout;\n\n-- Automatic retry: up to 5 attempts, 3 s apart\nDO $$\nDECLARE\n  attempt int := 0;\nBEGIN\n  LOOP\n    attempt := attempt + 1;\n    BEGIN\n      SET LOCAL lock_timeout = '2s';\n      ALTER TABLE customer ADD COLUMN IF NOT EXISTS segment text;\n      RAISE NOTICE 'OK, attempt %', attempt;\n      EXIT;\n    EXCEPTION WHEN lock_not_available THEN\n      RAISE NOTICE 'lock not acquired, attempt %', attempt;\n      IF attempt >= 5 THEN RAISE; END IF;\n      PERFORM pg_sleep(3);\n    END;\n  END LOOP;\nEND $$;","what":"ACCESS EXCLUSIVE가 필요한 DDL이 락을 얻지 못할 때 무한정 대기하거나 뒤에 대기하는 모든 쿼리를 블로킹하는 것(lock queue)을 막고, 짧은 간격으로 재시도한다.","when":"운영 환경의 바쁜 테이블에 ALTER TABLE, CREATE/DROP TRIGGER, ATTACH PARTITION, RENAME처럼 빠르지만 강한 락을 잡는 변경을 적용할 때.","caution":"lock_timeout이 없으면 장시간 SELECT 하나 뒤에 대기하는 ALTER TABLE이 이후의 모든 읽기/쓰기도 대기하게 만들어 몇 분 안에 커넥션 풀을 고갈시킬 수 있다. lock_timeout은 락 대기만 제한하며 테이블을 다시 쓰는 DDL(예: 컬럼 타입 변경)의 실행 시간은 제한하지 않는다. DO 블록은 단일 트랜잭션이며 시도 간 스냅샷을 유지하므로 긴 재시도 시나리오에서는 외부 셸 루프를 권장한다.","tags":["lock_timeout","ddl","alter table","lock queue","retry","lock"]},{"id":"connection-usage","task":"max_connections 대비 연결 사용량 측정","category":"세션","command":"-- Capacity: used slots and slots left for regular users\nSELECT max_conn,\n       reserved,\n       used,\n       max_conn - reserved - used AS free_for_normal_users,\n       round(100.0 * used / max_conn, 1) AS used_pct\nFROM (SELECT current_setting('max_connections')::int AS max_conn,\n             current_setting('superuser_reserved_connections')::int\n             + coalesce(current_setting('reserved_connections', true)::int, 0) AS reserved,\n             (SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend') AS used) x;\n\n-- Breakdown by database, user, application and state\nSELECT datname, usename, application_name, state,\n       count(*) AS sessions,\n       max(now() - state_change) AS longest_in_state\nFROM pg_stat_activity\nWHERE backend_type = 'client backend'\nGROUP BY datname, usename, application_name, state\nORDER BY sessions DESC;\n\n-- Role and database connection limits\nSELECT rolname, rolconnlimit FROM pg_roles WHERE rolconnlimit <> -1;\nSELECT datname, datconnlimit FROM pg_database WHERE datconnlimit <> -1;","what":"max_connections 대비 사용 중인 연결 슬롯과 일반 사용자에게 남은 슬롯, 데이터베이스/사용자/애플리케이션/상태별 분포, 구성된 CONNECTION LIMIT을 보여 준다.","when":"'too many clients already' / 'remaining connection slots are reserved' 오류 시, 용량 계획 시, pooler 필요성을 평가할 때.","caution":"max_connections는 클라이언트 backend만 제한한다. autovacuum worker, background worker, walsender는 별도 슬롯을 사용한다. 마지막 superuser_reserved_connections 슬롯(PG16+에서는 reserved_connections 추가)은 일반 사용자에게 닫혀 있다. max_connections를 수백, 수천으로 올리는 대신 PgBouncer 같은 pooler를 사용한다. 모든 연결은 별도 프로세스이며 메모리와 스냅샷 비용이 연결 수에 따라 증가한다. 'idle' 세션이 많으면 애플리케이션 pool이 과도하게 큰 것이다. 폭주하는 애플리케이션이 모든 슬롯을 소비하지 않도록 ALTER ROLE ... CONNECTION LIMIT으로 막는다. max_connections 변경에는 재시작이 필요하다.","note":"reserved_connections 파라미터와 pg_use_reserved_connections 롤은 PG16에서 도입되었다. 쿼리는 이전 버전에서도 동작한다(파라미터가 없으면 0으로 계산).","tags":["max_connections","connection count","connection","pooler","superuser_reserved_connections","session"]},{"id":"autovacuum-progress","task":"실행 중인 autovacuum/VACUUM 작업의 진행 상황 모니터링","category":"유지보수","command":"SELECT p.pid,\n       a.backend_type,\n       p.datname,\n       p.relid::regclass AS table_name,\n       p.phase,\n       pg_size_pretty(p.heap_blks_total * current_setting('block_size')::bigint) AS table_size,\n       round(100.0 * p.heap_blks_scanned / NULLIF(p.heap_blks_total, 0), 1) AS scanned_pct,\n       round(100.0 * p.heap_blks_vacuumed / NULLIF(p.heap_blks_total, 0), 1) AS vacuumed_pct,\n       p.index_vacuum_count,\n       now() - a.xact_start AS running_for,\n       a.wait_event_type || ':' || a.wait_event AS wait,\n       left(a.query, 80) AS query\nFROM pg_stat_progress_vacuum p\nJOIN pg_stat_activity a USING (pid)\nORDER BY running_for DESC;\n\n-- Busy autovacuum workers / upper limit\nSELECT count(*) FILTER (WHERE backend_type = 'autovacuum worker') AS av_workers_busy,\n       current_setting('autovacuum_max_workers') AS av_max_workers\nFROM pg_stat_activity;","what":"실행 중인 모든 VACUUM(autovacuum worker 또는 수동)에 대해 테이블, 단계, 스캔/vacuum된 heap 비율, index vacuum 횟수, 대기 이벤트와 바쁜 autovacuum worker 수를 보여 준다.","when":"대형 테이블의 VACUUM이 언제 끝날지, 모든 autovacuum worker가 바쁜지(대기열의 테이블), throttling 때문에 vacuum이 대기 중인지 판단하기 위해.","caution":"index_vacuum_count > 1은 dead tuple 목록이 메모리에 들어가지 않아 인덱스를 여러 번 스캔했다는 뜻이다. maintenance_work_mem/autovacuum_work_mem을 늘린다(PG17 이전에는 이 용도로 최대 1GB만 사용). wait = Timeout:VacuumDelay는 비용 기반 throttling(autovacuum_vacuum_cost_delay/cost_limit)이다. 쿼리 텍스트에 'to prevent wraparound'가 있는 autovacuum은 종료하지 않는다. 즉시 다시 시작된다. VACUUM FULL과 CLUSTER는 이 뷰가 아닌 pg_stat_progress_cluster에 나타난다.","since":"PostgreSQL 9.6","note":"PG17에서 max_dead_tuples/num_dead_tuples 컬럼은 max_dead_tuple_bytes/num_dead_item_ids로 바뀌었고 indexes_total/indexes_processed가 추가되었다. 따라서 쿼리는 버전과 무관한 컬럼을 사용한다.","tags":["autovacuum","vacuum progress","pg_stat_progress_vacuum","vacuum","maintenance","progress"]},{"id":"xid-wraparound-risk","task":"transaction ID wraparound 위험 측정(데이터베이스 및 테이블별 XID age)","category":"유지보수","command":"-- Per database: distance to autovacuum_freeze_max_age and the ~2^31 limit\nSELECT datname,\n       age(datfrozenxid) AS xid_age,\n       mxid_age(datminmxid) AS mxid_age,\n       round(100.0 * age(datfrozenxid) / current_setting('autovacuum_freeze_max_age')::int, 1) AS pct_of_freeze_max_age,\n       round(100.0 * age(datfrozenxid) / 2147483647, 1) AS pct_of_wraparound\nFROM pg_database\nORDER BY age(datfrozenxid) DESC;\n\n-- Oldest tables in the current database (including TOAST)\nSELECT c.oid::regclass AS table_name,\n       greatest(age(c.relfrozenxid), age(t.relfrozenxid)) AS xid_age,\n       mxid_age(c.relminmxid) AS mxid_age,\n       pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,\n       s.last_autovacuum,\n       s.last_vacuum\nFROM pg_class c\nLEFT JOIN pg_class t ON t.oid = c.reltoastrelid\nLEFT JOIN pg_stat_all_tables s ON s.relid = c.oid\nWHERE c.relkind IN ('r', 'm')\nORDER BY 2 DESC\nLIMIT 20;","what":"각 데이터베이스의 가장 오래된 unfrozen XID와 multixact age를 autovacuum_freeze_max_age 및 wraparound 한도 대비로 보여 준다. 현재 데이터베이스에서는 가장 오래된 테이블(TOAST 포함)을 크기와 마지막 vacuum 시각과 함께 보여 준다.","when":"정기 모니터링(일반적 임계값: pct_of_wraparound 50% 경고 / 75% 위험), 로그에 'must be vacuumed within N transactions'가 나타날 때, freeze 작업의 우선순위를 정할 때.","caution":"pct_of_freeze_max_age가 100을 넘는 테이블은 autovacuum이 의무적인(anti-wraparound) vacuum을 시작한다. I/O 부하가 크며 종료해도 다시 시작된다. age가 줄지 않으면 원인은 거의 항상 xmin horizon을 붙잡는 무언가(장시간 트랜잭션, prepared transaction, replication slot, hot_standby_feedback)이다. 이를 먼저 해결하지 않으면 VACUUM FREEZE로도 age를 낮출 수 없다. 한도에 가까워지면 서버는 먼저 경고하고, 수백만 개만 남으면 새 XID 할당(쓰기)을 거부한다. 대상 지정 freeze: VACUUM (FREEZE, VERBOSE) <table>;","note":"PG14는 vacuum_failsafe_age / vacuum_multixact_failsafe_age(기본 16억)를 추가했다. 테이블이 이 age에 도달하면 VACUUM은 cost delay와 인덱스 정리를 건너뛰는 긴급 모드로 전환된다.","tags":["wraparound","xid age","datfrozenxid","relfrozenxid","freeze","multixact"]},{"id":"xmin-horizon-holders","task":"VACUUM의 dead tuple 제거를 막는 xmin horizon 보유자 찾기","category":"유지보수","command":"SELECT 'session' AS source, pid::text AS id, usename::text AS owner, state AS detail,\n       age(backend_xmin) AS xmin_age\nFROM pg_stat_activity\nWHERE backend_xmin IS NOT NULL AND pid <> pg_backend_pid()\nUNION ALL\nSELECT 'prepared_xact', gid, owner::text, prepared::text, age(transaction)\nFROM pg_prepared_xacts\nUNION ALL\nSELECT 'replication_slot', slot_name::text, slot_type, active::text,\n       greatest(age(xmin), age(catalog_xmin))\nFROM pg_replication_slots\nWHERE xmin IS NOT NULL OR catalog_xmin IS NOT NULL\nUNION ALL\nSELECT 'standby_feedback', application_name, usename::text, state, age(backend_xmin)\nFROM pg_stat_replication\nWHERE backend_xmin IS NOT NULL\nORDER BY xmin_age DESC NULLS LAST;","what":"dead tuple 정리를 막을 수 있는 네 가지 원인을 xmin age 순으로 나열한다: snapshot을 보유한 세션, prepared transaction, replication slot, hot_standby_feedback을 보내는 standby.","when":"VACUUM VERBOSE가 'dead row versions cannot be removed yet'을 보고하고, bloat이 계속 증가하며 XID age가 줄지 않을 때.","caution":"가장 오래된 xmin이 정리 기준선을 결정한다. replication slot과 standby feedback은 모든 데이터베이스에 영향을 준다. prepared transaction은 재시작 후에도 남으므로 소유자를 확인하고 COMMIT PREPARED / ROLLBACK PREPARED '<gid>'로 닫는다. 사용하지 않는 slot을 삭제하면 하위 시스템(standby, CDC)이 깨지므로 먼저 소유자를 확인한다. standby에서 실행하면 해당 standby의 세션만 보여 준다.","tags":["xmin horizon","vacuum blocker","dead tuple","prepared transaction","replication slot","bloat"]},{"id":"pgstattuple-bloat","task":"pgstattuple로 테이블 및 인덱스 bloat 측정","category":"유지보수","command":"CREATE EXTENSION IF NOT EXISTS pgstattuple;\n\n-- Fast estimate: skips pages marked all-visible in the visibility map\nSELECT pg_size_pretty(table_len) AS table_size,\n       round(approx_tuple_percent::numeric, 1) AS live_pct,\n       round(dead_tuple_percent::numeric, 1) AS dead_pct,\n       round(approx_free_percent::numeric, 1) AS free_pct\nFROM pgstattuple_approx('orders');\n\n-- Exact measurement: reads the whole table\nSELECT pg_size_pretty(table_len) AS table_size,\n       round(dead_tuple_percent::numeric, 1) AS dead_pct,\n       round(free_percent::numeric, 1) AS free_pct,\n       pg_size_pretty(dead_tuple_len + free_space) AS reclaimable\nFROM pgstattuple('orders');\n\n-- B-tree indexes: leaf density (~90 when freshly built; below 50 is bloated)\nWITH idx AS MATERIALIZED (\n  SELECT i.indexrelid\n  FROM pg_index i\n  JOIN pg_class c ON c.oid = i.indexrelid\n  JOIN pg_am am ON am.oid = c.relam\n  WHERE am.amname = 'btree' AND i.indrelid = 'orders'::regclass\n)\nSELECT idx.indexrelid::regclass AS index_name,\n       pg_size_pretty(s.index_size) AS index_size,\n       s.avg_leaf_density,\n       s.leaf_fragmentation\nFROM idx\nCROSS JOIN LATERAL pgstatindex(idx.indexrelid) s;","what":"테이블의 live/dead tuple 및 여유 공간 비율(빠른 추정 및 정확한 값)과 회수 가능한 공간을 보여 준다. B-tree 인덱스는 leaf 밀도와 단편화를 보여 준다.","when":"pg_stat_user_tables의 n_dead_tup 추정치로 충분하지 않을 때. pg_repack, VACUUM FULL, REINDEX 결정을 데이터로 뒷받침하기 위해.","caution":"pgstattuple과 pgstatindex는 relation 전체를 읽으므로 대형 테이블에서 I/O 부하가 크다(AccessShareLock만 잡는다). 먼저 pgstattuple_approx를 사용한다. 기본적으로 superuser 또는 pg_stat_scan_tables 롤이 필요하다. 어느 정도의 여유 공간은 정상이다(fillfactor, HOT update용 공간). VACUUM은 공간을 재사용 가능하게 하지만 파일을 줄이지 않는다. 회수하려면 pg_repack 또는 VACUUM FULL(ACCESS EXCLUSIVE 락!)이 필요하다. avg_leaf_density가 낮은 인덱스는 REINDEX CONCURRENTLY로 재구축한다.","since":"PostgreSQL 12","note":"pgstattuple은 contrib 확장이며 pgstattuple_approx는 PG9.5에서 도입되었다. CTE의 MATERIALIZED(PG12+)는 B-tree가 아닌 인덱스에서 pgstatindex가 호출되어 실패하는 것을 막는다.","tags":["bloat","pgstattuple","pgstatindex","index bloat","dead tuple","free space"]},{"id":"pg-repack-online","task":"긴 락 없이 bloat된 테이블 재구성(pg_repack)","category":"유지보수","command":"-- Once per target database (the pg_repack package must be installed on the server)\nCREATE EXTENSION pg_repack;\n\n# Dry run: show what would be done\npg_repack -h localhost -U postgres -d mydb --table=public.orders --dry-run\n\n# Rewrite the table and its indexes: 2 parallel index builds, wait at most 60 s for locks\npg_repack -h localhost -U postgres -d mydb --table=public.orders --jobs=2 --wait-timeout=60 --no-kill-backend\n\n# Rebuild indexes only\npg_repack -h localhost -U postgres -d mydb --table=public.orders --only-indexes","what":"백그라운드에서 테이블의 압축된 사본을 만들고, 트리거로 동시 변경을 로그 테이블에 수집하며, 마지막에 짧은 ACCESS EXCLUSIVE 락으로 테이블을 교체한다. VACUUM FULL과 달리 작업 내내 읽기/쓰기를 블로킹하지 않는다.","when":"VACUUM으로 되돌릴 수 없는 심각한 bloat(pgstattuple로 확인)을 운영 시스템에서 회수해야 할 때.","caution":"테이블에 PRIMARY KEY 또는 NOT NULL 컬럼의 UNIQUE 인덱스가 있어야 한다. 일시적으로 테이블과 인덱스만큼의 추가 디스크 공간이 필요하며 WAL이 많이 생성된다(replica 지연, 아카이브 양). 시작과 끝에 ACCESS EXCLUSIVE 락이 필요하다. --wait-timeout이 만료되면 기본 동작은 충돌 쿼리를 취소하고 필요하면 세션을 종료하는 것이며, --no-kill-backend는 대신 해당 테이블을 건너뛴다. 실행 중에는 대상 테이블에 DDL을 실행하지 않는다. 클라이언트와 확장 버전이 일치해야 한다. 중단되면 DROP EXTENSION pg_repack CASCADE; CREATE EXTENSION pg_repack;으로 잔여물을 정리한다.","note":"pg_repack은 PostgreSQL 코어의 일부가 아니다(별도 패키지, 예: postgresql-17-repack).","tags":["pg_repack","bloat","online","vacuum full alternative","table reorganization","repack"]},{"id":"invalid-indexes","task":"INVALID 인덱스 찾기 및 정리","category":"유지보수","command":"SELECT n.nspname AS schema_name,\n       c.relname AS index_name,\n       t.relname AS table_name,\n       pg_size_pretty(pg_relation_size(c.oid)) AS index_size,\n       i.indisvalid,\n       i.indisready,\n       format('DROP INDEX CONCURRENTLY %I.%I;', n.nspname, c.relname) AS fix\nFROM pg_index i\nJOIN pg_class c ON c.oid = i.indexrelid\nJOIN pg_class t ON t.oid = i.indrelid\nJOIN pg_namespace n ON n.oid = c.relnamespace\nWHERE NOT i.indisvalid OR NOT i.indisready;\n\n-- Before dropping: is an index build still in progress?\nSELECT pid, relid::regclass AS table_name, index_relid::regclass AS index_name, command, phase\nFROM pg_stat_progress_create_index;","what":"실패한 CREATE INDEX CONCURRENTLY 또는 REINDEX CONCURRENTLY가 남긴 invalid 인덱스를 크기와 함께 나열하고 삭제 명령을 생성한다.","when":"실패하거나 취소된 온라인 인덱스 작업 후, 정기 상태 점검 및 메이저 업그레이드 전.","caution":"진행 중인 CREATE INDEX CONCURRENTLY / REINDEX CONCURRENTLY도 INVALID로 표시되므로 삭제 전에 pg_stat_progress_create_index를 확인한다. INVALID 인덱스는 쿼리에 사용되지 않지만 indisready = true이면 모든 INSERT/UPDATE에서 계속 유지되므로 순수한 오버헤드다. REINDEX INDEX CONCURRENTLY로 재구축할 수 있지만 원인(예: unique 위반)을 해결하지 않으면 다시 실패한다. REINDEX CONCURRENTLY 잔여물에는 _ccnew/_ccold 접미사가 붙는다. DROP INDEX CONCURRENTLY는 트랜잭션 블록 안에서 실행할 수 없다.","tags":["invalid index","indisvalid","create index concurrently","broken index","_ccnew","index"]},{"id":"extended-statistics","task":"상관관계가 있는 컬럼에 확장 통계 생성(CREATE STATISTICS)","category":"유지보수","command":"-- city and district are correlated: the planner assumes independence and underestimates rows\nCREATE STATISTICS customer_city_district_stx (dependencies, ndistinct, mcv) ON city, district FROM customer;\nANALYZE customer;\n\n-- Collected statistics\nSELECT statistics_name, attnames, kinds, n_distinct, dependencies\nFROM pg_stats_ext\nWHERE tablename = 'customer';\n\n-- Verify: estimated rows should now be close to actual rows\nEXPLAIN (ANALYZE, TIMING OFF)\nSELECT * FROM customer WHERE city = 'Ankara' AND district = 'Cankaya';","what":"플래너에 컬럼 간 상관관계(functional dependency, 결합 distinct 수, 결합 most-common value)를 알려 주어 다중 컬럼 WHERE 및 GROUP BY 조건의 행 추정을 바로잡는다.","when":"다중 컬럼 필터에서 EXPLAIN ANALYZE의 추정 행 수가 실제보다 크게 낮고(예: 12 vs. 570) 그 결과 잘못된 조인 방식/인덱스가 선택될 때.","caution":"ANALYZE 후에만 적용된다. 컬럼 조합 수가 statistics target(기본값 100)보다 훨씬 많으면 mcv 목록이 대부분을 포함하지 못하며, dependencies로 바로잡을 수 있는 추정을 mcv가 바로잡지 못할 수 있다. EXPLAIN으로 확인하고 필요하면 정의에서 mcv를 빼거나 ALTER STATISTICS customer_city_district_stx SET STATISTICS 1000;으로 target을 높인다. dependencies는 등호/IN 조건에만, ndistinct는 GROUP BY 추정에 사용된다. 정의마다 ANALYZE가 길어지므로 오추정이 입증된 컬럼 그룹에만 추가한다.","since":"PostgreSQL 12","note":"CREATE STATISTICS는 PG10(ndistinct, dependencies), mcv 종류는 PG12, ALTER STATISTICS ... SET STATISTICS는 PG13, 표현식 통계는 PG14에서 도입되었다.","tags":["extended statistics","create statistics","correlation","row estimate","cardinality","analyze"]},{"id":"analyze-staleness","task":"오래된 통계를 가진 테이블 찾기(ANALYZE 전략)","category":"유지보수","command":"SELECT schemaname,\n       relname AS table_name,\n       n_live_tup,\n       n_mod_since_analyze,\n       round(100.0 * n_mod_since_analyze / NULLIF(n_live_tup, 0), 1) AS mod_pct,\n       greatest(last_analyze, last_autoanalyze) AS last_analyzed,\n       round(current_setting('autovacuum_analyze_threshold')::int\n             + current_setting('autovacuum_analyze_scale_factor')::numeric * n_live_tup) AS autoanalyze_at\nFROM pg_stat_user_tables\nWHERE n_mod_since_analyze > 0\nORDER BY mod_pct DESC NULLS LAST\nLIMIT 20;\n\n-- After a bulk load, without waiting for autovacuum\nANALYZE (VERBOSE) orders;","what":"마지막 ANALYZE 이후 수정된 행 수, live 행 대비 비율, autovacuum이 ANALYZE를 트리거하는 대략적인 수정 임계값을 보여 준다.","when":"대량 적재/삭제 후, 계획에 잘못된 행 추정이 보일 때, 또는 대형 테이블에서 autoanalyze가 뒤처지는 것으로 의심될 때.","caution":"계산된 임계값은 전역 설정을 사용한다. 테이블별 autovacuum_analyze_* reloption(pg_class.reloptions)이 있으면 실제 임계값은 다르다. 대형 테이블에서는 기본 scale_factor(0.1)가 너무 늦게 트리거되므로 테이블별로 낮춘다. autovacuum은 파티션 테이블의 부모를 ANALYZE하지 않으므로 부모 수준 통계에는 수동 ANALYZE가 필요하다. 임시 테이블은 autovacuum이 보지 못한다. ANALYZE는 샘플을 읽으며 ShareUpdateExclusiveLock을 잡는다(DML을 블로킹하지 않음).","tags":["analyze","stale statistics","n_mod_since_analyze","autoanalyze","statistics","planner"]},{"id":"visibility-map-index-only","task":"visibility map 적용 범위 및 Index Only Scan 효율 확인","category":"유지보수","command":"-- Tables with a low all-visible page ratio: Index Only Scans must visit the heap\nSELECT c.oid::regclass AS table_name,\n       c.relpages,\n       c.relallvisible,\n       round(100.0 * c.relallvisible / NULLIF(c.relpages, 0), 1) AS all_visible_pct,\n       pg_size_pretty(pg_relation_size(c.oid)) AS heap_size,\n       s.n_ins_since_vacuum,\n       s.last_autovacuum\nFROM pg_class c\nJOIN pg_stat_user_tables s ON s.relid = c.oid\nWHERE c.relpages > 1000\nORDER BY all_visible_pct NULLS FIRST\nLIMIT 20;\n\n-- If \"Heap Fetches\" in the plan is close to the returned rows, the visibility map is stale\nEXPLAIN (ANALYZE, BUFFERS) SELECT id FROM customer WHERE id BETWEEN 1 AND 50000;\n\n-- Exact count (pg_visibility extension)\nCREATE EXTENSION IF NOT EXISTS pg_visibility;\nSELECT * FROM pg_visibility_map_summary('customer');","what":"테이블의 all-visible 페이지 비율, 마지막 vacuum 이후 삽입된 행, Index Only Scan이 heap을 방문해야 했던 빈도(Heap Fetches)를 보여 준다.","when":"Index Only Scan의 성능이 기대에 못 미칠 때, insert가 많거나 append-only인 테이블, covering(INCLUDE) 인덱스 설계를 검증할 때.","caution":"relpages/relallvisible은 마지막 VACUUM/ANALYZE가 기록한 추정치다. visibility map 비트는 VACUUM만 설정하며, 모든 UPDATE/DELETE는 해당 페이지의 비트를 지운다. 자주 갱신되는 테이블의 비율이 낮으면 테이블별로 autovacuum_vacuum_scale_factor를 낮춘다. pg_visibility_map_summary는 heap이 아닌 작은 VM fork를 읽는다.","since":"PostgreSQL 13","note":"n_ins_since_vacuum과 insert 기반 autovacuum(autovacuum_vacuum_insert_threshold/scale_factor)은 PG13에서 도입되었다. 그 이전에는 append-only 테이블이 anti-wraparound vacuum 전까지 vacuum되지 않아 visibility map이 설정되지 않을 수 있다.","tags":["visibility map","index only scan","heap fetches","relallvisible","pg_visibility","vacuum"]},{"id":"range-partition-monthly","task":"월별 range 파티션 테이블 생성(루프로 파티션 생성)","category":"파티셔닝","command":"CREATE TABLE sales (\n  id         bigint GENERATED ALWAYS AS IDENTITY,\n  created_at      date   NOT NULL,\n  customer_id bigint NOT NULL,\n  amount      numeric(12,2),\n  PRIMARY KEY (id, created_at)\n) PARTITION BY RANGE (created_at);\n\n-- An index defined on the parent propagates to all partitions automatically\nCREATE INDEX sales_customer_id_idx ON sales (customer_id);\n\n-- 12 monthly partitions for 2026\nDO $$\nDECLARE\n  m date;\nBEGIN\n  FOR m IN SELECT generate_series(date '2026-01-01', date '2026-12-01', interval '1 month')::date LOOP\n    EXECUTE format(\n      'CREATE TABLE IF NOT EXISTS %I PARTITION OF sales FOR VALUES FROM (%L) TO (%L)',\n      'sales_' || to_char(m, 'YYYY_MM'), m, (m + interval '1 month')::date);\n  END LOOP;\nEND $$;\n\n-- Hierarchy, bounds and sizes\nSELECT t.relid::regclass AS partition_name,\n       t.parentrelid::regclass AS parent,\n       t.isleaf,\n       t.level,\n       pg_get_expr(c.relpartbound, c.oid) AS bounds,\n       pg_size_pretty(pg_total_relation_size(t.relid)) AS total_size\nFROM pg_partition_tree('sales') t\nJOIN pg_class c ON c.oid = t.relid\nORDER BY t.level, bounds;","what":"날짜 컬럼에 RANGE 파티션 부모 테이블, 모든 파티션에 전파되는 인덱스, 루프로 월별 파티션 12개를 생성한다. 이후 계층 구조를 경계와 크기와 함께 나열한다.","when":"오래된 데이터를 주기적으로 제거(DETACH/DROP)하거나 쿼리가 주로 날짜 범위로 필터링하는, 계속 커지는 대형 테이블(주문, 로그, 시계열)용.","caution":"파티션 테이블에서 PRIMARY KEY/UNIQUE 제약 조건은 파티션 키를 포함해야 한다(전역 unique 인덱스가 없다). 하한은 포함, 상한은 제외다(FROM 포함, TO 제외). 미래 파티션을 미리 만드는 job(pg_cron, pg_partman)이 없으면 범위를 벗어난 INSERT가 실패한다. 파티션이 수천 개면 계획 시간과 락 오버헤드가 늘어난다. 부모에서는 CREATE INDEX CONCURRENTLY가 지원되지 않는다. 부모에 CREATE INDEX ... ON ONLY, 각 파티션에 CREATE INDEX CONCURRENTLY를 실행한 후 ALTER INDEX ... ATTACH PARTITION을 사용한다.","since":"PostgreSQL 12","note":"선언적 파티셔닝은 PG10, 부모의 인덱스/PRIMARY KEY와 default 파티션은 PG11, pg_partition_tree()는 PG12에서 도입되었다.","tags":["partition","range partition","declarative partitioning","monthly partition","pg_partition_tree","partitioning"]},{"id":"attach-partition-safely","task":"준비된 테이블을 짧은 락으로 파티션에 연결(ATTACH PARTITION)","category":"파티셔닝","command":"-- 1) Prepare the data in a separate table without touching the live table\nCREATE TABLE sales_2027_01 (LIKE sales INCLUDING DEFAULTS INCLUDING CONSTRAINTS);\n-- ... bulk load (COPY / INSERT) ...\nALTER TABLE sales_2027_01 ADD PRIMARY KEY (id, created_at);\nCREATE INDEX CONCURRENTLY sales_2027_01_customer_id_idx ON sales_2027_01 (customer_id);\n\n-- 2) A CHECK that exactly matches the bounds: avoids a full scan during ATTACH\nALTER TABLE sales_2027_01 ADD CONSTRAINT sales_2027_01_bound_chk\n  CHECK (created_at IS NOT NULL AND created_at >= DATE '2027-01-01' AND created_at < DATE '2027-02-01') NOT VALID;\nALTER TABLE sales_2027_01 VALIDATE CONSTRAINT sales_2027_01_bound_chk;\n\n-- 3) Attach (SHARE UPDATE EXCLUSIVE on the parent; reads/writes continue)\nSET lock_timeout = '5s';\nALTER TABLE sales ATTACH PARTITION sales_2027_01\n  FOR VALUES FROM ('2027-01-01') TO ('2027-02-01');\nRESET lock_timeout;\n\n-- 4) Drop the now redundant CHECK\nALTER TABLE sales_2027_01 DROP CONSTRAINT sales_2027_01_bound_chk;","what":"경계와 일치하는 사전 검증된 CHECK 제약 조건과 미리 만든 인덱스를 사용하여, 별도 테이블에 적재한 데이터를 전체 스캔이나 긴 락 없이 파티션으로 만든다.","when":"대량 적재(ETL)나 아카이브 복원 시: 운영 테이블을 건드리지 않고 데이터를 준비한 후 한 번에 온라인으로 전환한다.","caution":"ATTACH는 부모에 SHARE UPDATE EXCLUSIVE, 연결할 테이블에 ACCESS EXCLUSIVE를 잡는다. 일치하는 유효한 CHECK가 없으면 경계 검증을 위해 그 락 상태에서 테이블 전체를 스캔한다. 부모와 일치하는 인덱스가 없으면 ATTACH가 락 상태에서 인덱스를 만들므로 둘 다 미리 준비한다. default 파티션이 있으면 새 범위의 행이 없음을 증명하기 위해 default 파티션도 스캔한다(default 파티션 레시피 참고). 컬럼 이름과 타입은 부모와 정확히 일치해야 하며, LIKE ... INCLUDING으로 테이블을 만드는 것이 가장 안전하다.","since":"PostgreSQL 12","note":"부모에 SHARE UPDATE EXCLUSIVE만 잡는 ATTACH PARTITION은 PG12에서 도입되었다. PG11 이하에서는 부모가 ACCESS EXCLUSIVE로 잠긴다.","tags":["attach partition","add partition","check constraint","not valid","etl","partitioning"]},{"id":"detach-partition-concurrently","task":"운영 트래픽을 블로킹하지 않고 오래된 파티션 분리(DETACH CONCURRENTLY)","category":"파티셔닝","command":"-- Must run outside a transaction block\nALTER TABLE sales DETACH PARTITION sales_2026_01 CONCURRENTLY;\n\n-- If interrupted (cancel, lost connection) the partition stays 'detach pending'; complete it:\nALTER TABLE sales DETACH PARTITION sales_2026_01 FINALIZE;\n\n-- Any pending detach?\nSELECT inhrelid::regclass AS partition_name, inhparent::regclass AS parent\nFROM pg_inherits\nWHERE inhdetachpending;\n\n-- The detached table is now standalone: archive or drop it\nDROP TABLE sales_2026_01;","what":"파티션을 두 트랜잭션에 걸쳐 분리한다. 부모에 SHARE UPDATE EXCLUSIVE만 잡으므로 실행 중인 쿼리가 블로킹되지 않으며, 분리된 테이블에 파티션 경계와 동등한 CHECK 제약 조건을 추가한다.","when":"운영 시스템에서 보존/아카이브를 위해 오래된 기간을 분리할 때(이후 DROP, 다른 테이블스페이스로 이동, pg_dump 아카이브).","caution":"BEGIN ... COMMIT 블록 안에서 실행할 수 없고 부모에 default 파티션이 있으면 사용할 수 없다. 이 명령 이전에 시작되어 부모를 사용하는 모든 트랜잭션을 기다리므로 장시간 트랜잭션이 있으면 길어진다. 중단되면 파티션은 쿼리에서 숨겨지지만 분리되지 않은 상태('detach pending')로 남으며 FINALIZE로 완료해야 한다. CONCURRENTLY 없는 일반 DETACH는 더 빠르지만 부모에 ACCESS EXCLUSIVE가 필요하므로 lock_timeout과 함께 시도한다.","since":"PostgreSQL 14","tags":["detach partition","concurrently","finalize","retention","archiving","partitioning"]},{"id":"default-partition-pitfall","task":"default 파티션 함정: 새 파티션이 'would be violated by some row'로 실패","category":"파티셔닝","command":"-- Which periods have rows in the default partition?\nSELECT date_trunc('month', created_at)::date AS month, count(*) AS row_count\nFROM sales_default\nGROUP BY 1\nORDER BY 1;\n\n-- Move the rows into a new partition and attach it in one transaction\nBEGIN;\nSET LOCAL lock_timeout = '5s';\nLOCK TABLE sales_default IN ACCESS EXCLUSIVE MODE;\nCREATE TABLE sales_2027_02 (LIKE sales INCLUDING DEFAULTS INCLUDING CONSTRAINTS);\nWITH moved AS (\n  DELETE FROM sales_default\n  WHERE created_at >= DATE '2027-02-01' AND created_at < DATE '2027-03-01'\n  RETURNING *\n)\nINSERT INTO sales_2027_02 SELECT * FROM moved;\nALTER TABLE sales ATTACH PARTITION sales_2027_02\n  FOR VALUES FROM ('2027-02-01') TO ('2027-03-01');\nCOMMIT;","what":"default 파티션에 들어간 행을 기간별로 집계하고, 새 파티션 범위에 속하는 행을 새 테이블로 옮겨 연결하는 작업을 하나의 트랜잭션에서 수행한다.","when":"CREATE TABLE ... PARTITION OF 또는 ATTACH가 'updated partition constraint for default partition would be violated by some row'로 실패할 때, 또는 default 파티션이 커지는 것이 발견될 때.","caution":"default 파티션이 있으면 새 파티션을 만들 때마다 default를 전체 스캔하며 그동안 ACCESS EXCLUSIVE를 잡는다. default 파티션이 크면 유지보수 때마다 서비스가 중단된다. 행을 옮기는 동안 default는 쓰기가 막히므로 트래픽이 적을 때 수행한다. 영구 해결: 파티션을 미리 생성하고 모니터링(행 수 > 0이면 알림)으로 default를 비워 두거나, 아예 사용하지 않아 범위를 벗어난 INSERT가 실패하고 문제가 일찍 드러나게 한다. default 파티션은 DETACH ... CONCURRENTLY도 막는다.","since":"PostgreSQL 11","tags":["default partition","partition error","violated by some row","partition constraint","pitfall","partitioning"]},{"id":"partition-pruning-check","task":"partition pruning 동작 확인(계획 시점 및 실행 시점)","category":"파티셔닝","command":"SHOW enable_partition_pruning;\n\n-- Plan-time pruning: only the relevant partition should appear in the plan\nEXPLAIN (COSTS OFF)\nSELECT count(*) FROM sales WHERE created_at >= DATE '2026-03-01' AND created_at < DATE '2026-04-01';\n\n-- Run-time pruning: generic plan / parameterized query, \"Subplans Removed: N\"\nPREPARE q_sales(date) AS SELECT count(*) FROM sales WHERE created_at = $1;\nSET plan_cache_mode = force_generic_plan;\nEXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) EXECUTE q_sales('2026-03-15');\nRESET plan_cache_mode;\nDEALLOCATE q_sales;\n\n-- Pruning killer: a function on the key column scans every partition\nEXPLAIN (COSTS OFF)\nSELECT count(*) FROM sales WHERE date_trunc('month', created_at) = DATE '2026-03-01';","what":"계획 시점 pruning, generic plan에서의 실행 시점 pruning('Subplans Removed: N'), 파티션 키에 함수를 적용할 때 pruning이 사라지는 경우를 비교한다.","when":"파티션 테이블의 쿼리가 예상보다 느리거나, 계획이 모든 파티션을 스캔하거나, 애플리케이션이 ORM/prepared statement를 사용할 때.","caution":"파티션 키에 함수나 cast를 적용하면(date_trunc(created_at), created_at::text) pruning이 비활성화되므로 조건을 키에 대한 범위로 작성한다. generic plan에서는 pruning이 executor 시작 시 일어나며 'Subplans Removed'로만 나타난다. now() 같은 STABLE 표현식은 ANALYZE 없는 EXPLAIN에서도 보인다. enable_partition_pruning = off이면 pruning이 전혀 일어나지 않는다. 파티션이 많으면 Planning Time도 측정한다.","since":"PostgreSQL 12","note":"실행 시점 pruning은 PG11, plan_cache_mode는 PG12에서 도입되었으며 PG12는 파티션이 많을 때의 계획 성능도 크게 개선했다.","tags":["partition pruning","subplans removed","generic plan","plan_cache_mode","explain","partitioning"]},{"id":"pg-partman-setup","task":"pg_partman으로 파티션 생성 및 보존 자동화","category":"파티셔닝","command":"CREATE SCHEMA partman;\nCREATE EXTENSION pg_partman SCHEMA partman;\n\nCREATE TABLE public.measurements (\n  id    bigint GENERATED ALWAYS AS IDENTITY,\n  ts    timestamptz NOT NULL,\n  value numeric,\n  PRIMARY KEY (id, ts)\n) PARTITION BY RANGE (ts);\n\n-- Daily partitions, pre-create 7 days ahead (pg_partman 5.x syntax)\nSELECT partman.create_parent(\n  p_parent_table := 'public.measurements',\n  p_control      := 'ts',\n  p_interval     := '1 day',\n  p_premake      := 7\n);\n\n-- Drop partitions older than 30 days\nUPDATE partman.part_config\n   SET retention = '30 days',\n       retention_keep_table = false\n WHERE parent_table = 'public.measurements';\n\n-- Maintenance: run periodically (schedule with pg_cron or pg_partman_bgw)\nCALL partman.run_maintenance_proc();\n\n-- Any rows in the default partition?\nSELECT * FROM partman.check_default(p_exact_count := true);","what":"부모 테이블을 pg_partman에 등록한다. 미래 파티션을 미리 만들고, 보존 기간이 지난 파티션을 삭제하며, default 파티션에 들어간 행을 보고한다.","when":"일별/월별 파티션을 수동이나 사용자 스크립트로 관리하는 대신 표준적이고 관찰 가능한 자동화가 필요할 때(로그, 지표, 이벤트 테이블).","caution":"pg_partman 4.x와 5.x 사이에 API가 바뀌었다. 5.x에서 p_type은 'range'/'list'만 허용하며(기본 'range') 4.x의 p_type := 'native'는 유효하지 않다. 설치된 버전은 SELECT extversion FROM pg_extension WHERE extname = 'pg_partman';으로 확인한다. create_parent는 기본적으로 default 파티션도 만든다. retention_keep_table = false는 파티션을 영구 DROP하므로 아카이브가 필요하면 true로 두고 retention_schema를 사용한다. run_maintenance_proc()이 예약되어 있지 않으면 새 파티션이 생성되지 않아 데이터가 default에 들어간다. pg_partman_bgw는 shared_preload_libraries와 재시작이 필요하다.","note":"pg_partman은 PostgreSQL 코어의 일부가 아니다(별도 패키지, 예: postgresql-17-partman). 명령은 pg_partman 5.x 구문을 사용한다.","tags":["pg_partman","partition maintenance","retention","create_parent","automatic partitioning","partitioning"]},{"id":"least-privilege-roles","task":"최소 권한 롤 계층 구성(GRANT + ALTER DEFAULT PRIVILEGES)","category":"보안","command":"-- Group roles (NOLOGIN): object owner, read-only, read-write\nCREATE ROLE app_owner NOLOGIN;\nCREATE ROLE app_readonly NOLOGIN;\nCREATE ROLE app_readwrite NOLOGIN;\nGRANT app_readonly TO app_readwrite;\nCREATE SCHEMA app AUTHORIZATION app_owner;\n\n-- Restrict PUBLIC's default privileges\nREVOKE ALL ON DATABASE mydb FROM PUBLIC;\nREVOKE CREATE ON SCHEMA public FROM PUBLIC;\n\n-- Existing objects\nGRANT CONNECT ON DATABASE mydb TO app_readonly;\nGRANT USAGE ON SCHEMA app TO app_readonly;\nGRANT SELECT ON ALL TABLES IN SCHEMA app TO app_readonly;\nGRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_readwrite;\nGRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO app_readwrite;\n\n-- Objects app_owner will create IN THE FUTURE\nALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app GRANT SELECT ON TABLES TO app_readonly;\nALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app GRANT INSERT, UPDATE, DELETE ON TABLES TO app_readwrite;\nALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app GRANT USAGE, SELECT ON SEQUENCES TO app_readwrite;\n\n-- Login users get privileges only through group membership\nCREATE ROLE web_app LOGIN PASSWORD 'Change#2026' IN ROLE app_readwrite;\nCREATE ROLE report_app LOGIN PASSWORD 'Change#2026' IN ROLE app_readonly;\n\n-- Configured default privileges (psql: \\ddp)\nSELECT pg_get_userbyid(d.defaclrole) AS owner_role,\n       n.nspname AS schema_name,\n       d.defaclobjtype AS object_type,\n       d.defaclacl\nFROM pg_default_acl d\nLEFT JOIN pg_namespace n ON n.oid = d.defaclnamespace\nORDER BY 1, 2, 3;","what":"owner, 읽기 전용, 읽기-쓰기 그룹 롤의 계층을 만들고, 기존 및 향후 테이블/시퀀스에 올바른 권한을 부여하며, 애플리케이션 사용자에게는 그룹 멤버십으로만 권한을 준다.","when":"새 애플리케이션 스키마를 구성할 때, superuser나 테이블 소유자로 연결하는 애플리케이션을 바로잡을 때, 감사 전 권한을 단순화할 때.","caution":"ALTER DEFAULT PRIVILEGES는 FOR ROLE에 지정한 롤이 이후에 생성하는 객체에만 적용된다. 마이그레이션 도구가 다른 사용자로 테이블을 만들면 권한이 적용되지 않으므로 SET ROLE app_owner로 DDL을 실행한다. GRANT ... ON ALL TABLES는 그 시점에 존재하는 객체만 포함한다. PG15+에서 생성된 데이터베이스는 이미 public 스키마의 CREATE를 PUBLIC에 부여하지 않지만, 이전 버전에서 업그레이드된 데이터베이스는 REVOKE가 필요하다. REVOKE ALL ON DATABASE ... FROM PUBLIC은 CONNECT와 TEMP도 제거하므로 필요한 다른 롤에 별도로 부여한다. 비밀번호를 명령줄에 넣는 대신 psql \\password를 사용한다(로그와 history에 남지 않는다).","note":"pg_read_all_data / pg_write_all_data 롤은 PG14, 롤 멤버십의 INHERIT/SET 옵션(GRANT ... WITH INHERIT FALSE)은 PG16에서 도입되었다.","tags":["least privilege","role hierarchy","alter default privileges","grant","privilege","security"]},{"id":"row-level-security","task":"row-level security로 tenant 격리 구성","category":"보안","command":"ALTER TABLE app.orders ENABLE ROW LEVEL SECURITY;\n-- Make the table owner subject to the policy too\nALTER TABLE app.orders FORCE ROW LEVEL SECURITY;\n\nCREATE POLICY orders_tenant_isolation ON app.orders\n  FOR ALL\n  TO app_readwrite\n  USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::int)\n  WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::int);\n\n-- The application sets the tenant at the start of every transaction (pooler-friendly)\nBEGIN;\nSET LOCAL app.tenant_id = '42';\nSELECT * FROM app.orders;\nCOMMIT;\n\n-- Configured policies\nSELECT schemaname, tablename, policyname, roles, cmd, qual, with_check\nFROM pg_policies\nWHERE tablename = 'orders';","what":"테이블에 RLS를 활성화하고, 세션 변수의 tenant id로 행을 필터링하며(USING) 다른 tenant에 대한 쓰기를 차단하는(WITH CHECK) 정책을 정의한다.","when":"멀티테넌트 애플리케이션에서 애플리케이션 코드에 의존하지 않고 데이터베이스에서 데이터 격리를 강제하기 위해.","caution":"superuser와 BYPASSRLS 속성을 가진 롤은 항상 정책을 우회한다. 테이블 소유자는 FORCE ROW LEVEL SECURITY가 있을 때만 정책을 적용받는다. RLS가 활성화되어 있고 일치하는 정책이 없으면 결과가 비어 있다(오류 없음). current_setting(..., true)은 변수가 정의되지 않으면 NULL을 반환한다. NULLIF는 RESET 후 남은 빈 문자열이 ''::int로 실패하는 것을 막는다. 사용자는 SET으로 app.tenant_id를 바꿀 수 있으므로 신뢰할 수 있는 애플리케이션 계층만 설정해야 하며 최종 사용자에게 SQL 접근을 주어서는 안 된다. transaction 모드 pooler를 사용하면 SET 대신 SET LOCAL을 사용한다. 정책에 사용되는 tenant_id 컬럼에 인덱스를 만든다. RLS가 적용되는 사용자로 실행하면 pg_dump가 실패하므로 BYPASSRLS 롤로 백업한다.","since":"PostgreSQL 9.5","tags":["row level security","rls","create policy","multi-tenant","tenant isolation","security"]},{"id":"pg-hba-validate-reload","task":"pg_hba.conf 규칙 순서 검증 및 안전한 reload","category":"보안","command":"-- File location\nSHOW hba_file;\n\n# Rules are read top to bottom and the FIRST matching rule wins;\n# specific rules must come BEFORE general ones:\n#   hostssl  appdb  web_app  10.10.0.0/16  scram-sha-256\n#   host     all    all      10.10.0.0/16  reject\n#   host     all    all      0.0.0.0/0     scram-sha-256\n\n-- Validate BEFORE reloading: pg_hba_file_rules reads the file on disk\nSELECT line_number, type, database, user_name, address, netmask, auth_method, error\nFROM pg_hba_file_rules\nORDER BY line_number;\n\n-- Apply if the error column is empty\nSELECT pg_reload_conf();","what":"reload 전에 pg_hba.conf의 위치, PostgreSQL이 이를 해석하는 방식(줄, type, database, user, address, method), 구문 오류를 보여 준 후 구성을 reload한다.","when":"새 클라이언트 네트워크/애플리케이션을 추가할 때, md5에서 scram-sha-256으로 전환할 때, 'no pg_hba.conf entry for host' 오류를 조사할 때.","caution":"pg_hba_file_rules는 활성 규칙이 아니라 현재 파일 상태를 보여 준다. 어느 줄이라도 error가 비어 있지 않으면 reload 시 pg_hba.conf를 전혀 로드하지 않으며 이전 규칙이 유지된다(로그에 'pg_hba.conf was not reloaded'). 변경은 새 연결에만 적용된다. 앞쪽에 놓인 넓은 규칙(예: host all all 0.0.0.0/0 trust)은 그 뒤의 모든 제한을 무력화한다. SSL이 꺼져 있으면 hostssl 줄은 매칭될 수 없으며 error 컬럼에 메시지가 표시된다. 기본적으로 superuser만 뷰를 읽을 수 있다. root로 파일을 편집하는 도구가 소유권을 바꾸지 않도록 한다. postgres 사용자가 읽을 수 없으면 reload가 실패한다.","since":"PostgreSQL 10","note":"pg_hba_file_rules는 PG10에서 도입되었고, 최신 버전에는 rule_number와 file_name 컬럼도 있다. PG16은 pg_hba.conf의 include 지시자와 user/database 필드의 정규식 매칭을 지원한다.","tags":["pg_hba.conf","authentication","rule order","reload","pg_hba_file_rules","security"]},{"id":"ssl-enable-verify","task":"SSL/TLS 활성화 및 암호화 연결 확인(pg_stat_ssl)","category":"보안","command":"# Server certificate (self-signed for testing; use a corporate CA in production)\nopenssl req -new -x509 -days 365 -nodes -subj \"/CN=db.example.com\" -keyout server.key -out server.crt\nchmod 600 server.key    # in the data directory, owned by postgres\n\n-- Enable SSL (a reload is enough)\nALTER SYSTEM SET ssl = on;\nALTER SYSTEM SET ssl_cert_file = 'server.crt';\nALTER SYSTEM SET ssl_key_file = 'server.key';\nALTER SYSTEM SET ssl_min_protocol_version = 'TLSv1.2';\nSELECT pg_reload_conf();\n\n-- Which connections are encrypted?\nSELECT a.pid, a.usename, a.client_addr, a.application_name,\n       s.ssl, s.version, s.cipher, s.bits\nFROM pg_stat_ssl s\nJOIN pg_stat_activity a USING (pid)\nWHERE a.backend_type = 'client backend'\nORDER BY s.ssl, a.usename;\n\n# On the client, verify the server identity as well\npsql \"host=db.example.com dbname=postgres user=postgres sslmode=verify-full sslrootcert=root.crt\"","what":"재시작 없이 서버 인증서로 TLS를 활성화하고 최소 프로토콜 버전을 설정하며, 현재 연결의 암호화 여부를 TLS 버전 및 cipher와 함께 나열한다.","when":"보안 감사/컴플라이언스 요구 사항(PCI, GDPR), 네트워크로 전송되는 비밀번호와 데이터를 보호하기 위해.","caution":"ssl = on만으로는 암호화가 강제되지 않으며 클라이언트는 sslmode=prefer/disable로 여전히 비암호화 연결을 할 수 있다. 강제하려면 pg_hba.conf에서 host 대신 hostssl을 사용한다(hostnossl ... reject도 추가). sslmode=require는 서버 신원을 검증하지 않으므로(MITM에 취약) 인증서 검증에는 verify-full을 사용한다. 키 파일은 postgres 사용자 소유에 모드 0600이어야 한다(또는 root 소유에 0640). 그렇지 않으면 SSL이 로드되지 않으므로 reload 후 로그와 SHOW ssl을 확인한다. 인증서 갱신도 reload로 반영된다.","since":"PostgreSQL 10","note":"재시작 대신 reload로 SSL 설정을 변경하는 기능은 PG10, ssl_min_protocol_version은 PG12에서 도입되었으며 PG13부터 기본값은 TLSv1.2이다.","tags":["ssl","tls","pg_stat_ssl","hostssl","certificate","encryption"]},{"id":"scram-migration","task":"md5 비밀번호에서 scram-sha-256으로 전환","category":"보안","command":"-- 1) Store new passwords as SCRAM\nALTER SYSTEM SET password_encryption = 'scram-sha-256';\nSELECT pg_reload_conf();\n\n-- 2) Find roles still carrying md5 password hashes (requires superuser)\nSELECT rolname,\n       CASE\n         WHEN rolpassword LIKE 'SCRAM-SHA-256$%' THEN 'scram-sha-256'\n         WHEN rolpassword LIKE 'md5%' THEN 'md5'\n         WHEN rolpassword IS NULL THEN 'no password'\n         ELSE 'other'\n       END AS password_type,\n       rolvaliduntil\nFROM pg_authid\nWHERE rolcanlogin\nORDER BY password_type, rolname;\n\n-- 3) Reset the password of each md5 role (preferably in psql: \\password legacy_app)\nALTER ROLE legacy_app PASSWORD 'New#Password2';\n\n# 4) Once all roles use SCRAM, change md5 -> scram-sha-256 in pg_hba.conf and reload","what":"새 비밀번호가 SCRAM-SHA-256으로 저장되게 하고, 아직 md5 해시를 가진 로그인 롤을 찾으며, 비밀번호 갱신 후 pg_hba.conf를 scram-sha-256으로 전환하는 단계를 제공한다.","when":"보안 감사에서 md5가 지적될 때, PG14+로 업그레이드한 후, md5가 deprecated된 버전을 준비할 때.","caution":"기존 md5 해시에서 SCRAM을 도출할 수 없으므로 모든 비밀번호를 평문으로 다시 설정해야 한다(애플리케이션 팀과 조율 필요). pg_hba.conf의 'md5' 방식은 비밀번호가 SCRAM인 롤에 대해 자동으로 SCRAM 인증을 수행하므로 전환 중 md5를 유지하는 것은 안전한 중간 단계다. 그러나 방식이 'scram-sha-256'이 되면 md5 비밀번호를 가진 롤은 더 이상 연결할 수 없다. 오래된 클라이언트 드라이버(libpq < 10, 오래된 JDBC/.NET 드라이버)는 SCRAM을 지원하지 않을 수 있으므로 먼저 드라이버를 업그레이드한다. ALTER ROLE ... PASSWORD는 log_statement를 통해 비밀번호를 로그에 기록할 수 있다. psql \\password는 전송 전에 클라이언트에서 해시한다.","since":"PostgreSQL 10","note":"SCRAM-SHA-256은 PG10에서 도입되었고, password_encryption 기본값은 PG14에서 scram-sha-256이 되었다. PG18은 md5 비밀번호를 deprecated하고 경고를 출력한다.","tags":["scram-sha-256","md5","password_encryption","password","authentication","security"]},{"id":"pgaudit-object-audit","task":"pgaudit으로 롤 및 객체 수준 감사 로그 유지","category":"보안","command":"-- Prerequisite: shared_preload_libraries = 'pgaudit' + restart, then:\nCREATE EXTENSION pgaudit;\n\n-- Session auditing: server-wide, only DDL and role/privilege changes\nALTER SYSTEM SET pgaudit.log = 'ddl, role';\nALTER SYSTEM SET pgaudit.log_relation = on;\nALTER SYSTEM SET pgaudit.log_parameter = off;\nSELECT pg_reload_conf();\n\n-- All reads/writes of a specific application role\nALTER ROLE web_app SET pgaudit.log = 'read, write';\n\n-- Object auditing: privileges granted to the auditor role = operations to log\nCREATE ROLE auditor NOLOGIN;\nALTER SYSTEM SET pgaudit.role = 'auditor';\nSELECT pg_reload_conf();\nGRANT SELECT, DELETE ON customer TO auditor;\nGRANT UPDATE (balance) ON customer TO auditor;","what":"서버 전체의 DDL과 권한 변경, 특정 롤의 모든 읽기/쓰기, auditor 롤에 부여된 민감 테이블/컬럼 접근을 'AUDIT:' 접두사가 붙은 구조화된 로그 줄로 기록한다.","when":"GDPR, PCI-DSS, SOX 같은 컴플라이언스 감사에서 누가 민감 데이터에 접근했는지 증명하기 위해.","caution":"서버 전체에 pgaudit.log = 'all' 또는 'read'를 활성화하면 로그 양과 I/O가 크게 늘어나므로 롤 또는 객체 감사로 범위를 좁힌다. superuser는 감사를 끌 수 있으므로(ALTER SYSTEM/ALTER ROLE) 레코드를 중앙 로그 시스템(syslog/SIEM)으로 보내 데이터베이스 서버와 독립적으로 보관한다. log_parameter = on은 bind 파라미터(개인 정보)도 기록한다. 롤 설정은 새 세션부터 적용된다. pgaudit 메이저 버전은 PostgreSQL 메이저 버전과 일치해야 한다(pgaudit 17.x = PG17).","note":"pgaudit은 PostgreSQL 코어의 일부가 아니다(별도 패키지, 예: postgresql-17-pgaudit).","tags":["pgaudit","audit","auditing","object audit","log","security"]},{"id":"memory-settings-check","task":"메모리 설정 검토(shared_buffers, work_mem, maintenance_work_mem)","category":"파라미터","command":"SELECT name,\n       current_setting(name) AS value,\n       source,\n       context\nFROM pg_settings\nWHERE name IN ('shared_buffers', 'effective_cache_size', 'work_mem', 'hash_mem_multiplier',\n               'maintenance_work_mem', 'autovacuum_work_mem', 'max_connections',\n               'max_parallel_workers_per_gather', 'huge_pages', 'temp_buffers', 'wal_buffers')\nORDER BY name;\n\n-- Rough ceilings: work_mem x max_connections and the autovacuum memory ceiling\nSELECT pg_size_pretty(pg_size_bytes(current_setting('work_mem'))\n                      * current_setting('max_connections')::int) AS work_mem_x_max_connections,\n       pg_size_pretty(pg_size_bytes(current_setting('maintenance_work_mem'))\n                      * current_setting('autovacuum_max_workers')::int) AS autovacuum_mem_ceiling;\n\n-- Trace of operations that did not fit in work_mem: temp files per database\nSELECT datname, temp_files, pg_size_pretty(temp_bytes) AS temp_size, stats_reset\nFROM pg_stat_database\nWHERE datname IS NOT NULL\nORDER BY temp_bytes DESC;","what":"메모리 관련 파라미터를 값, 출처(기본값/구성 파일), 변경 context(postmaster = 재시작)와 함께 보여 준다. work_mem과 autovacuum의 대략적인 메모리 상한, 데이터베이스별 임시 파일 양도 보여 준다.","when":"새 서버를 구축할 때, RAM을 추가한 후, OOM killer 발생 후, 또는 임시 파일 사용량이 많을 때.","caution":"work_mem은 연결당이 아니라 정렬/해시 노드당(해시는 hash_mem_multiplier 배) 할당되며 parallel worker마다 별도로 할당된다. 복잡한 쿼리는 work_mem의 몇 배를 사용할 수 있으므로 계산된 곱은 상한이 아니라 대략적인 경고다. 일반적인 시작점: shared_buffers는 RAM의 ~25%(재시작 필요), effective_cache_size는 ~50-75%(플래너 힌트일 뿐 아무것도 할당하지 않음), maintenance_work_mem은 512MB-2GB. autovacuum_work_mem = -1이면 각 autovacuum worker가 maintenance_work_mem을 사용할 수 있다. temp_bytes가 높으면 work_mem을 전역이 아닌 롤이나 쿼리별로 높인다. Linux에서 shared_buffers가 크면 huge_pages를 고려한다.","note":"hash_mem_multiplier는 PG13에서 도입되었다(PG15부터 기본값 2.0).","tags":["shared_buffers","work_mem","maintenance_work_mem","memory","memory tuning","temp_bytes"]},{"id":"pending-restart-config-errors","task":"재시작 대기 중인 파라미터 및 구성 파일 오류 조회","category":"파라미터","command":"-- Parameters changed but not effective until a restart\nSELECT name, setting, unit, context, source, sourcefile, pending_restart\nFROM pg_settings\nWHERE pending_restart;\n\n-- Lines in configuration files that were not applied or have errors\nSELECT sourcefile, sourceline, seqno, name, setting, applied, error\nFROM pg_file_settings\nWHERE NOT applied OR error IS NOT NULL\nORDER BY sourcefile, sourceline;\n\n-- Where does a parameter's effective value come from?\nSELECT name, setting, source, sourcefile, sourceline\nFROM pg_settings\nWHERE name = 'work_mem';","what":"파일에서 변경되었지만 재시작을 기다리는 파라미터, 적용되지 않았거나 오류가 있는 구성 파일 줄, 파라미터의 유효 값이 어느 파일/줄에서 오는지 보여 준다.","when":"ALTER SYSTEM이나 postgresql.conf 변경 후 reload가 기대한 효과를 냈는지 확인하고, 계획된 재시작 전 대기 중인 변경을 나열하기 위해.","caution":"pg_file_settings는 현재 파일 상태를 읽는다. 파라미터가 여러 곳에 정의되어 있으면 마지막 정의만 applied = true다(postgresql.auto.conf가 마지막에 읽히므로 ALTER SYSTEM이 항상 우선한다). applied = false이고 error가 빈 줄은 다른 정의에 가려진 것이며 오류가 아니다. 재시작이 필요한 파라미터는 error에 'setting could not be applied'를 표시한다. 오류가 있는 파일은 reload 시 일부만 적용될 수 있으므로 변경 후 pg_reload_conf()를 실행한 다음 이 쿼리와 서버 로그를 확인한다. postgresql.auto.conf는 직접 편집하지 말고 ALTER SYSTEM RESET을 사용한다.","since":"PostgreSQL 9.5","tags":["pending_restart","pg_file_settings","restart","configuration error","alter system","config"]},{"id":"checkpoint-stats","task":"checkpoint 동작 검토(PG17+ pg_stat_checkpointer, PG16 이하 pg_stat_bgwriter)","category":"파라미터","command":"-- PG17+\nSELECT num_timed,\n       num_requested,\n       round(100.0 * num_requested / NULLIF(num_timed + num_requested, 0), 1) AS req_pct,\n       restartpoints_timed,\n       restartpoints_req,\n       round((write_time / 1000)::numeric / NULLIF(num_timed + num_requested, 0), 1) AS avg_write_s,\n       round((sync_time / 1000)::numeric / NULLIF(num_timed + num_requested, 0), 1) AS avg_sync_s,\n       pg_size_pretty(buffers_written * current_setting('block_size')::bigint) AS written_by_checkpoint,\n       stats_reset\nFROM pg_stat_checkpointer;\n\n-- PG16 and earlier\nSELECT checkpoints_timed,\n       checkpoints_req,\n       round(100.0 * checkpoints_req / NULLIF(checkpoints_timed + checkpoints_req, 0), 1) AS req_pct,\n       round((checkpoint_write_time / 1000)::numeric / NULLIF(checkpoints_timed + checkpoints_req, 0), 1) AS avg_write_s,\n       round((checkpoint_sync_time / 1000)::numeric / NULLIF(checkpoints_timed + checkpoints_req, 0), 1) AS avg_sync_s,\n       pg_size_pretty(buffers_checkpoint * current_setting('block_size')::bigint) AS written_by_checkpoint,\n       pg_size_pretty(buffers_clean * current_setting('block_size')::bigint) AS written_by_bgwriter,\n       pg_size_pretty(buffers_backend * current_setting('block_size')::bigint) AS written_by_backends,\n       maxwritten_clean,\n       stats_reset\nFROM pg_stat_bgwriter;\n\n-- Related settings\nSELECT name, current_setting(name) AS value\nFROM pg_settings\nWHERE name IN ('checkpoint_timeout', 'max_wal_size', 'min_wal_size',\n               'checkpoint_completion_target', 'log_checkpoints')\nORDER BY name;","what":"예약된 checkpoint와 WAL 양 때문에 강제된 checkpoint의 비율, checkpoint당 평균 write/sync 시간, 기록된 buffer 양(PG16 이하에서는 bgwriter 및 backend 쓰기 포함)을 보여 준다.","when":"쓰기가 많은 기간에 I/O 스파이크가 보이거나, WAL 생성량이 많거나, 로그에 'checkpoints are occurring too frequently' 경고가 나타날 때.","caution":"req_pct가 높으면(일반적으로 10-20% 초과) max_wal_size가 가득 차서 checkpoint가 강제되는 것이다. max_wal_size를 늘리고 log_checkpoints = on으로 'checkpoint starting: wal' 줄을 확인한다. checkpoint_timeout을 늘리면 I/O가 분산되지만 crash recovery가 길어진다. PG16 이하에서 written_by_backends가 높으면 backend가 dirty page를 직접 써야 한다는 뜻이다(shared_buffers/bgwriter 부족). PG17에서는 이 정보가 pg_stat_io에 있다. 평균에는 작업이 없어 건너뛴 예약 checkpoint도 포함되므로 대략적인 수치로 읽는다. 카운터는 stats_reset 이후 누적이다.","since":"PostgreSQL 17","note":"PG17에서 checkpointer 통계는 pg_stat_bgwriter에서 새 pg_stat_checkpointer 뷰로 옮겨졌고(checkpoints_timed → num_timed, checkpoints_req → num_requested, buffers_checkpoint → buffers_written), buffers_backend와 buffers_backend_fsync는 제거되었다. PG18은 num_done과 slru_written 컬럼을 추가했다.","tags":["checkpoint","pg_stat_checkpointer","pg_stat_bgwriter","max_wal_size","checkpoint_timeout","wal"]},{"id":"relation-size-breakdown","task":"테이블 크기를 heap / index / TOAST로 나눠 조회 및 가장 큰 인덱스 목록","category":"스토리지","command":"SELECT n.nspname AS schema_name,\n       c.relname AS table_name,\n       pg_size_pretty(pg_total_relation_size(c.oid)) AS total,\n       pg_size_pretty(pg_relation_size(c.oid)) AS heap,\n       pg_size_pretty(pg_indexes_size(c.oid)) AS indexes,\n       pg_size_pretty(pg_total_relation_size(c.oid) - pg_relation_size(c.oid) - pg_indexes_size(c.oid)) AS toast_fsm_vm,\n       round(100.0 * pg_total_relation_size(c.oid) / NULLIF(pg_database_size(current_database()), 0), 1) AS pct_of_db\nFROM pg_class c\nJOIN pg_namespace n ON n.oid = c.relnamespace\nWHERE c.relkind IN ('r', 'm')\n  AND n.nspname NOT IN ('pg_catalog', 'information_schema')\nORDER BY pg_total_relation_size(c.oid) DESC\nLIMIT 20;\n\n-- Largest indexes and their usage counts\nSELECT s.schemaname,\n       s.relname AS table_name,\n       s.indexrelname AS index_name,\n       pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size,\n       s.idx_scan,\n       i.indisunique,\n       i.indisprimary\nFROM pg_stat_user_indexes s\nJOIN pg_index i ON i.indexrelid = s.indexrelid\nORDER BY pg_relation_size(s.indexrelid) DESC\nLIMIT 20;","what":"가장 큰 테이블을 전체, heap, 인덱스, TOAST(+FSM/VM)로 나누어 데이터베이스 내 비중과 함께 보여 주고, 가장 큰 인덱스를 스캔 횟수와 unique/primary 플래그와 함께 보여 준다.","when":"디스크 사용량이 증가할 때 증가가 어디서(데이터, 인덱스, TOAST) 오는지 찾고, 사용되지 않는 대형 인덱스를 발견하기 위해.","caution":"크기 함수는 relation마다 파일 시스템에 접근하므로 테이블이 수만 개인 데이터베이스에서는 느릴 수 있다. 파티션 부모(relkind 'p')는 자체 크기가 0 바이트이므로 제외된다. 계층 전체 합계: SELECT pg_size_pretty(sum(pg_total_relation_size(relid))) FROM pg_partition_tree('sales'); idx_scan = 0인 대형 인덱스는 삭제 후보지만, 카운터는 마지막 통계 reset 이후의 기간과 이 서버만 포함한다(replica의 사용량은 보이지 않음). unique/primary 인덱스는 제약 조건을 강제하므로 삭제하지 않는다.","tags":["table size","index size","toast","disk usage","pg_total_relation_size","storage"]},{"id":"extension-update-check","task":"업데이트 대기 중인 확장 찾기 및 업데이트","category":"익스텐션","command":"SELECT e.extname,\n       e.extversion AS installed,\n       a.default_version AS available,\n       n.nspname AS schema_name,\n       format('ALTER EXTENSION %I UPDATE;', e.extname) AS fix\nFROM pg_extension e\nJOIN pg_available_extensions a ON a.name = e.extname\nJOIN pg_namespace n ON n.oid = e.extnamespace\nWHERE e.extversion IS DISTINCT FROM a.default_version\nORDER BY e.extname;\n\n-- Is there an update path?\nSELECT * FROM pg_extension_update_paths('pgstattuple') WHERE path IS NOT NULL;\n\nALTER EXTENSION pgstattuple UPDATE;","what":"데이터베이스에 설치된 각 확장의 버전을 서버 패키지의 기본 버전과 비교하고, 업데이트 대기 중인 확장에 대한 ALTER EXTENSION 명령을 생성한다.","when":"PostgreSQL 마이너/메이저 업그레이드 및 확장 패키지 업데이트 후, 정기 유지보수 시.","caution":"OS 패키지를 업그레이드해도 SQL 측은 자동으로 업데이트되지 않는다. ALTER EXTENSION ... UPDATE를 데이터베이스마다 별도로 실행해야 한다(특히 pg_upgrade 후 중요). 업데이트 스크립트는 락을 잡고 객체를 변경할 수 있으므로(예: PostGIS) 먼저 테스트하고 릴리스 노트를 읽는다. 특정 버전으로 이동: ALTER EXTENSION pgstattuple UPDATE TO '1.5'; 일부 확장(예: pg_stat_statements)은 공유 라이브러리를 다시 로드하기 위해 재시작도 필요하다.","tags":["alter extension update","extension version","extension","pg_available_extensions","upgrade","update"]},{"id":"pg-stat-io-analysis","task":"pg_stat_io로 I/O 발생원 분석(backend 유형, 객체, context)","category":"익스텐션","command":"SELECT backend_type,\n       object,\n       context,\n       reads,\n       pg_size_pretty(reads * op_bytes) AS read_size,\n       writes,\n       pg_size_pretty(writes * op_bytes) AS write_size,\n       extends,\n       hits,\n       round(100.0 * hits / NULLIF(hits + reads, 0), 1) AS hit_pct,\n       evictions,\n       reuses,\n       fsyncs,\n       round(read_time::numeric, 1) AS read_ms,\n       round(write_time::numeric, 1) AS write_ms\nFROM pg_stat_io\nWHERE coalesce(reads, 0) + coalesce(writes, 0) + coalesce(extends, 0) + coalesce(evictions, 0) > 0\nORDER BY coalesce(reads, 0) + coalesce(writes, 0) DESC;\n\n-- Who writes? Ideally most writes come from the checkpointer and background writer\nSELECT backend_type,\n       pg_size_pretty(sum(writes * op_bytes)) AS written,\n       round(100.0 * sum(writes) / NULLIF(sum(sum(writes)) OVER (), 0), 1) AS pct_of_writes,\n       sum(fsyncs) AS fsyncs\nFROM pg_stat_io\nWHERE object = 'relation' AND writes > 0\nGROUP BY backend_type\nORDER BY sum(writes) DESC;","what":"backend 유형(client backend, autovacuum, checkpointer...), 객체(relation/temp relation), context(normal, vacuum, bulkread, bulkwrite)별 읽기, 쓰기, extend, eviction, fsync와 쓰기가 어떤 프로세스 유형에서 오는지 보여 준다.","when":"I/O 병목 시 부하가 쿼리, autovacuum, checkpoint 중 어디서 오는지 판별하고 shared_buffers/bgwriter 설정을 검증하기 위해.","caution":"pg_stat_io는 클러스터 전체 범위이며 마지막 reset 이후 누적이다. 변화를 보려면 두 샘플의 차이를 구한다(reset: SELECT pg_stat_reset_shared('io');). read_time/write_time은 track_io_timing = on일 때만 채워진다. reads에는 OS page cache에서 처리된 읽기도 포함된다. client backend의 쓰기나 eviction이 높으면 backend가 공간 확보를 위해 buffer를 직접 쓰는 것이므로 shared_buffers, bgwriter_lru_maxpages, checkpoint 설정을 검토한다. context = 'vacuum'/'bulkread' 행은 ring buffer 사용을 반영하며 그곳의 reuse는 정상이다.","since":"PostgreSQL 16","note":"pg_stat_io는 PG16에서 도입되었다. PG18은 op_bytes 컬럼을 제거하고 read_bytes/write_bytes/extend_bytes를 추가했으며 이 뷰에서 WAL I/O도 보고한다. PG18에서는 reads * op_bytes 대신 read_bytes를 사용한다.","tags":["pg_stat_io","io statistics","evictions","fsync","shared_buffers","io"]}]},"recipes/mysql.json":{"engineId":"mysql","engine":"MySQL / MariaDB","order":3,"recipes":[{"id":"rotate-binlog","task":"binary log 순환","category":"Binlog 및 로그","command":"FLUSH BINARY LOGS;","what":"현재 binary log 파일을 닫고 새 파일로 전환한다.","when":"백업 전 깨끗한 binlog 시작 지점을 만들거나 파일 크기를 제한하기 위해.","caution":"자주 순환하면 작은 binlog 파일이 많이 생긴다. MariaDB에서도 같은 명령이 동작한다. FLUSH LOGS는 모든 로그를 순환한다.","tags":["binlog","rotate","flush","log"]},{"id":"purge-binlog","task":"오래된 binary log 삭제(PURGE)","category":"Binlog 및 로그","command":"PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;","what":"지정한 날짜보다 오래된 binary log 파일을 디스크에서 삭제한다.","when":"binlog가 디스크를 채우고 있을 때, 더 이상 필요 없는 오래된 로그를 정리하기 위해.","caution":"삭제할 binlog를 replica가 아직 필요로 하면 복제가 깨진다. 먼저 SHOW REPLICA STATUS로 모든 replica가 해당 지점을 지났는지 확인한다. 특정 파일까지 삭제: PURGE BINARY LOGS TO 'mysql-bin.000123'; 자동 정리에는 binlog_expire_logs_seconds 설정을 사용한다.","tags":["binlog","purge","cleanup","log"]},{"id":"show-binlogs","task":"binary log 상태 조회","category":"Binlog 및 로그","command":"SHOW BINARY LOGS;","what":"서버의 모든 binary log 파일과 크기를 나열한다.","when":"binlog 디스크 사용량과 존재하는 파일을 확인하기 위해.","caution":"현재 기록 중인 binlog 파일과 위치: SHOW BINARY LOG STATUS; (MySQL 8.2+). 이전 버전은 SHOW MASTER STATUS를 사용한다(8.2–8.3에서 deprecated, 8.4에서 제거).","tags":["binlog","status","show","log"]},{"id":"enable-general-slow-log","task":"general log 및 slow query log 활성화","category":"Binlog 및 로그","command":"SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL general_log = 'ON';","what":"slow query log(1초 초과 쿼리)와 모든 쿼리를 기록하는 general log를 런타임에 활성화한다.","when":"느린 쿼리나 전체 쿼리 트래픽을 일시적으로 분석하기 위해.","caution":"general_log는 성능과 디스크에 상당한 오버헤드를 준다. 진단이 끝나면 반드시 끈다(SET GLOBAL general_log='OFF'). 이 설정은 재시작 시 초기화되므로 영구 적용에는 SET PERSIST를 사용한다.","tags":["slow-log","general-log","log","diagnostics"]},{"id":"innodb-redo-capacity","task":"InnoDB redo log 크기 변경","category":"스토리지","command":"SET GLOBAL innodb_redo_log_capacity = 2147483648;","what":"InnoDB redo log 전체 용량(바이트 단위, 여기서는 2 GB)을 런타임에 동적으로 설정한다.","when":"쓰기가 많은 워크로드에서 checkpoint 빈도를 줄이고 성능을 개선하기 위해.","caution":"innodb_redo_log_capacity는 MySQL 8.0.30에서 도입되어 기존 innodb_log_file_size + innodb_log_files_in_group 설정을 대체한다. 이전 버전/MariaDB에서는 my.cnf에서 innodb_log_file_size를 변경하며 재시작이 필요하다. 영구 적용: SET PERSIST innodb_redo_log_capacity = 2147483648;","since":"MySQL 8.0.30","note":"innodb_redo_log_capacity는 MySQL 8.0.30에서 도입되어 redo log의 동적(재시작 없는) 크기 변경을 가능하게 했다. 이전에는 innodb_log_file_size + innodb_log_files_in_group을 설정하고 서버를 재시작해야 했다.","tags":["innodb","redo-log","capacity","storage"]},{"id":"innodb-undo-tablespace","task":"InnoDB undo 테이블스페이스 추가 / 관리","category":"스토리지","command":"CREATE UNDO TABLESPACE undo_003 ADD DATAFILE 'undo_003.ibu';","what":"새 InnoDB undo 테이블스페이스를 생성한다. undo log가 데이터 파일과 별도로 관리되며 truncate할 수 있게 된다.","when":"장시간 트랜잭션으로 undo log가 비대해질 때 공간을 관리하거나 undo를 별도 디스크로 옮기기 위해.","caution":"파일 이름은 .ibu로 끝나야 한다. undo 테이블스페이스를 삭제하려면 먼저 비활성화해야 한다: ALTER UNDO TABLESPACE undo_003 SET INACTIVE; 그 다음 DROP UNDO TABLESPACE undo_003; MySQL 8.0+ 기능이다.","since":"MySQL 8.0","note":"SQL로 undo 테이블스페이스를 생성/삭제하는 기능(CREATE/DROP UNDO TABLESPACE)은 MySQL 8.0에서 도입되었다. 이전에는 undo 테이블스페이스 수가 innodb_undo_tablespaces로 고정되어 런타임에 관리할 수 없었다.","tags":["innodb","undo-log","tablespace","storage"]},{"id":"create-tablespace","task":"general 테이블스페이스 추가","category":"스토리지","command":"CREATE TABLESPACE ssd_ts ADD DATAFILE 'ssd_ts.ibd' ENGINE=InnoDB;","what":"여러 테이블이 공유할 수 있는 InnoDB general 테이블스페이스를 생성한다.","when":"특정 파일/디스크에 테이블을 묶거나 파일 수를 줄이기 위해.","caution":"DATAFILE 경로가 상대 경로이면 datadir 아래에 생성된다. 다른 디스크에 두려면 절대 경로와 필요한 권한이 있어야 한다. 테이블을 이곳으로 옮기기: ALTER TABLE customer TABLESPACE ssd_ts; 테이블스페이스는 비어 있을 때만 삭제할 수 있다.","tags":["tablespace","innodb","storage","create"]},{"id":"optimize-table","task":"테이블 OPTIMIZE","category":"스토리지","command":"OPTIMIZE TABLE customer;","what":"테이블을 재구축하여 단편화를 제거하고, 사용되지 않는 공간을 회수하며, 인덱스 통계를 갱신한다.","when":"많은 DELETE/UPDATE로 비대해진 InnoDB 테이블의 공간을 회수하기 위해.","caution":"InnoDB에서 OPTIMIZE는 실제로 테이블을 재구축하는 온라인 ALTER로 실행되지만 여전히 I/O와 임시 디스크 공간이 필요하다. 대형 테이블은 테이블 크기만큼의 여유 디스크가 필요하다. 락을 줄이려면 트래픽이 적을 때 실행한다.","tags":["optimize","table","defragment","storage"]},{"id":"table-schema-size","task":"테이블 및 스키마 크기 조회","category":"스토리지","command":"SELECT table_schema AS schema_name, table_name AS table_name, ROUND((data_length + index_length) / 1024 / 1024, 1) AS size_mb FROM information_schema.tables WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys') ORDER BY (data_length + index_length) DESC LIMIT 20;","what":"데이터와 인덱스 크기 합계(MB) 기준으로 가장 큰 테이블 20개를 나열한다.","when":"디스크 사용량을 분석하고 어떤 테이블이 공간을 차지하는지 찾기 위해.","caution":"InnoDB에서 information_schema의 크기는 근사값이다. 스키마별 합계는 table_name 없이 table_schema로만 GROUP BY한다.","tags":["size","table","schema","information-schema"]},{"id":"analyze-table","task":"테이블 통계 갱신(ANALYZE TABLE)","category":"유지보수","command":"ANALYZE TABLE customer;","what":"옵티마이저가 더 나은 계획을 선택할 수 있도록 테이블의 인덱스/키 분포 통계를 다시 계산한다.","when":"대량 데이터 변경 후 나쁜 쿼리 계획이 나타날 때.","caution":"실행 중 테이블에 짧은 metadata lock이 걸린다. InnoDB에서는 샘플 기반이므로 정확도를 높이려면 innodb_stats_persistent_sample_pages를 늘린다.","tags":["analyze","statistics","optimizer","maintenance"]},{"id":"rebuild-table","task":"테이블/인덱스 재구축","category":"유지보수","command":"ALTER TABLE customer ENGINE=InnoDB;","what":"같은 엔진으로 테이블을 재구축하며, 인덱스와 데이터 페이지를 다시 만들어 단편화와 인덱스 bloat을 제거한다.","when":"null ALTER로 테이블을 새로 고치거나 손상된 페이지 구조를 복구하기 위해.","caution":"대형 테이블은 테이블 크기만큼의 임시 디스크가 필요하다. 락 줄이기: ALTER TABLE customer ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE;","tags":["rebuild","alter","innodb","maintenance"]},{"id":"check-table","task":"테이블 무결성 확인(CHECK TABLE)","category":"유지보수","command":"CHECK TABLE customer;","what":"테이블과 인덱스의 손상 여부를 확인하고 상태를 반환한다.","when":"예기치 않은 오류가 발생하거나 손상이 의심될 때.","caution":"확인 중 테이블에 읽기 락이 걸린다. 손상이 발견되면 InnoDB는 대개 dump/restore로 복구한다. REPAIR TABLE은 MyISAM/Archive 같은 엔진에서만 동작한다.","tags":["check","integrity","corruption","maintenance"]},{"id":"set-global-persist","task":"전역 변수 변경 및 영구 저장","category":"파라미터","command":"SET PERSIST innodb_buffer_pool_size = 4294967296;","what":"실행 중인 서버와 재시작 이후 모두에 변수를 영구적으로 설정한다(mysqld-auto.cnf).","when":"재시작 없이 서버 변수를 영구 변경하기 위해.","caution":"SET PERSIST는 MySQL 8.0+ 기능이며 MariaDB는 지원하지 않는다(MariaDB에서는 my.cnf를 편집하고 재시작). 런타임 전용 변경에는 SET GLOBAL을 사용한다. 재시작 전용 변수에는 SET PERSIST_ONLY가 필요하다.","since":"MySQL 8.0","note":"SET PERSIST는 MySQL 8.0에서 도입되었으며 변수를 mysqld-auto.cnf에 기록하여 영구 적용한다. 이전에는(그리고 MariaDB에서는) 영구 적용에 my.cnf 수동 편집과 재시작이 필요했다.","tags":["set-persist","global","variable","parameter"]},{"id":"show-variables","task":"변수의 현재 값 조회","category":"파라미터","command":"SHOW VARIABLES LIKE 'innodb_buffer_pool_size';","what":"지정한 서버 변수(또는 패턴과 일치하는 변수)의 유효 값을 보여 준다.","when":"파라미터의 실행 중 값을 확인하기 위해.","caution":"세션 값이 아닌 전역 값은 SHOW GLOBAL VARIABLES를 사용한다. 값의 출처(파일, 명령줄, persist) 확인: SELECT * FROM performance_schema.variables_info WHERE variable_name = 'innodb_buffer_pool_size';","tags":["show-variables","parameter","current-value","config"]},{"id":"flush-privileges","task":"grant 테이블 다시 로드(FLUSH PRIVILEGES)","category":"파라미터","command":"FLUSH PRIVILEGES;","what":"mysql.user 같은 grant 테이블을 직접 수정한 후 메모리에 다시 로드한다.","when":"grant 테이블에 수동 INSERT/UPDATE로 적용한 변경을 활성화하기 위해.","caution":"GRANT/REVOKE/CREATE USER는 이미 권한을 즉시 적용하므로 그 후의 FLUSH PRIVILEGES는 불필요하다. grant 테이블을 직접 수정할 때만 필요하다.","tags":["flush-privileges","grants","privileges","reload"]},{"id":"setup-replica","task":"replica 구성(CHANGE REPLICATION SOURCE)","category":"복제","command":"CHANGE REPLICATION SOURCE TO SOURCE_HOST='source_host', SOURCE_PORT=3306, SOURCE_USER='replicator', SOURCE_PASSWORD='secret', SOURCE_AUTO_POSITION=1;","what":"GTID 기반 auto-positioning으로 지정한 source를 따르는 replica로 이 서버를 구성한다.","when":"새 replica를 구성하거나 복제 source를 변경하기 위해.","caution":"MySQL 8.0.23+ 용어다. 이전 버전은 CHANGE MASTER TO ... MASTER_HOST를 사용한다(8.0.23–8.3에서 별칭, 8.4에서 제거). SOURCE_AUTO_POSITION=1은 GTID가 활성화되어 있어야 하며(gtid_mode=ON), 그렇지 않으면 SOURCE_LOG_FILE과 SOURCE_LOG_POS를 지정해야 한다. 명령 실행 전에 복제를 중지해야 한다. 복제 사용자가 8.0+ 기본값인 caching_sha2_password로 생성되었다면 연결에 TLS가 필요하다. SOURCE_SSL=1을 추가하거나, TLS 없이 GET_SOURCE_PUBLIC_KEY=1을 설정한다. 그렇지 않으면 I/O 스레드가 오류 2061로 연결에 실패한다.","since":"MySQL 8.0.23","note":"CHANGE REPLICATION SOURCE TO(SOURCE_HOST 등)는 MySQL 8.0.23에서 도입되었다. 이전에는 CHANGE MASTER TO ... MASTER_HOST / MASTER_AUTO_POSITION을 사용했다.","tags":["replication","change-source","replica","setup"]},{"id":"start-stop-replica","task":"replica 시작 / 중지","category":"복제","command":"STOP REPLICA; START REPLICA;","what":"복제 I/O 및 SQL 스레드를 중지하고 다시 시작한다.","when":"유지보수, 구성 변경, 복제 오류 수정을 위해.","caution":"MySQL 8.0.22+ 용어다. 이전 버전은 STOP SLAVE / START SLAVE를 사용한다. SQL 스레드만: STOP REPLICA SQL_THREAD; STOP REPLICA 후 CHANGE REPLICATION SOURCE를 실행할 수 있다.","since":"MySQL 8.0.22","note":"START REPLICA / STOP REPLICA는 MySQL 8.0.22에서 도입되었다. 이전에는 START SLAVE / STOP SLAVE를 사용했다(이전 구문은 8.0.22–8.3에서 별칭으로 동작하며 8.4에서 제거되었다).","tags":["replication","start","stop","replica"]},{"id":"replica-status","task":"replica 상태 조회(SHOW REPLICA STATUS)","category":"복제","command":"SHOW REPLICA STATUS\\G","what":"replica의 source 연결, 스레드 상태, 지연, 오류를 상세히 보여 준다.","when":"복제가 정상인지와 얼마나 뒤처졌는지 확인하기 위해.","caution":"MySQL 8.0.22+ 용어다. 이전 버전은 SHOW SLAVE STATUS를 사용한다(8.0.22–8.3에서 별칭, 8.4에서 제거). 주요 필드: Replica_IO_Running과 Replica_SQL_Running은 'Yes'여야 하며, Seconds_Behind_Source는 지연을, Last_Error는 오류를 보여 준다.","since":"MySQL 8.0.22","note":"SHOW REPLICA STATUS는 MySQL 8.0.22에서 도입되었다. 이전에는 SHOW SLAVE STATUS를 사용했으며 필드 이름은 Slave_IO_Running / Seconds_Behind_Master 형태였다.","tags":["replication","status","replica","lag"]},{"id":"skip-gtid-transaction","task":"문제 트랜잭션 건너뛰기(GTID)","category":"복제","command":"STOP REPLICA; SET GTID_NEXT='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:15'; BEGIN; COMMIT; SET GTID_NEXT='AUTOMATIC'; START REPLICA;","what":"지정한 GTID로 빈 트랜잭션을 커밋하여 해당 트랜잭션을 '적용됨'으로 처리하고 복제가 진행되게 한다.","when":"replica에서 중복되거나 적용할 수 없는 이벤트가 복제를 멈췄을 때 그 이벤트를 건너뛰기 위해.","caution":"트랜잭션을 건너뛰면 replica와 source 간 데이터 불일치가 생긴다. 건너뛰는 이벤트의 영향을 알 때만 사용한다. GTID 값은 SHOW REPLICA STATUS의 Last_Error / Retrieved_Gtid_Set에서 가져온다. GTID가 꺼져 있으면 SET GLOBAL sql_replica_skip_counter = 1;을 사용한다(8.0.26+. 이전에는 sql_slave_skip_counter이며 8.0.26+에서 deprecated 별칭).","tags":["replication","gtid","skip","recovery"]},{"id":"kill-query-connection","task":"쿼리 또는 연결 종료(KILL)","category":"세션","command":"KILL QUERY 4567;","what":"연결을 닫지 않고 지정한 연결 ID에서 현재 실행 중인 쿼리를 종료한다.","when":"장시간 실행되거나 멈춘 쿼리를 중지하기 위해.","caution":"연결(세션) 전체를 닫으려면: KILL CONNECTION 4567; (또는 간단히 KILL 4567). 실행 중인 쓰기 트랜잭션을 종료하면 롤백되며, 대형 테이블에서는 오래 걸릴 수 있다.","tags":["kill","query","connection","session"]},{"id":"process-list","task":"프로세스 목록 조회","category":"세션","command":"SELECT id, user, host, db, command, time, state, LEFT(info, 100) AS query FROM performance_schema.processlist WHERE command <> 'Sleep' ORDER BY time DESC;","what":"idle이 아닌 모든 연결, 실행 중인 쿼리, 경과 시간을 나열한다.","when":"활성 세션과 장시간 실행 쿼리를 진단하기 위해.","caution":"단축: SHOW FULL PROCESSLIST; performance_schema.processlist는 모든 컬럼을 제공하며 WHERE로 필터링할 수 있다. time 컬럼은 초 단위다.","tags":["processlist","session","activity","performance-schema"]},{"id":"blocking-locks","task":"블로킹 락 조회(data_lock_waits)","category":"세션","command":"SELECT r.trx_id AS waiting_trx, r.trx_mysql_thread_id AS waiting_thread, LEFT(r.trx_query,80) AS waiting_query, b.trx_id AS blocking_trx, b.trx_mysql_thread_id AS blocking_thread, LEFT(b.trx_query,80) AS blocking_query FROM performance_schema.data_lock_waits w JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_engine_transaction_id JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_engine_transaction_id;","what":"어떤 트랜잭션이 어떤 트랜잭션에 의해 블로킹되는지 관련 쿼리 및 스레드 ID와 함께 보여 준다.","when":"락 대기와 데드락으로 인한 성능 저하를 진단하기 위해.","caution":"performance_schema.data_lock_waits는 MySQL 8.0에서 도입되었다. 이전 버전은 information_schema.innodb_lock_waits를 사용한다. 블로킹 스레드를 중지하려면 KILL blocking_thread;를 사용한다.","since":"MySQL 8.0","note":"performance_schema.data_lock_waits는 MySQL 8.0에서 도입되었다. MySQL 5.7 이하에서는 같은 정보에 information_schema.innodb_lock_waits를 사용했다.","tags":["blocking","lock","data-lock-waits","deadlock"]}]},"recipes/mysql-ext.json":{"engineId":"mysql","engine":"MySQL","order":2,"recipes":[{"id":"replica-applier-health","task":"replica 지연 및 applier worker 오류 모니터링(performance_schema)","category":"복제","command":"-- Receiver (I/O) thread: connection state and last error\nSELECT CHANNEL_NAME, SERVICE_STATE, LAST_ERROR_NUMBER,\n       LEFT(LAST_ERROR_MESSAGE, 100) AS last_error,\n       LAST_QUEUED_TRANSACTION, LAST_HEARTBEAT_TIMESTAMP\nFROM performance_schema.replication_connection_status;\n\n-- Applier workers: error, applied GTID and real lag\nSELECT CHANNEL_NAME, WORKER_ID, SERVICE_STATE, LAST_ERROR_NUMBER,\n       LEFT(LAST_ERROR_MESSAGE, 120) AS last_error,\n       LAST_APPLIED_TRANSACTION,\n       APPLYING_TRANSACTION,\n       IF(APPLYING_TRANSACTION = '', 0,\n          TIMESTAMPDIFF(SECOND, APPLYING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP, NOW(6))) AS current_lag_sec,\n       TIMESTAMPDIFF(MICROSECOND, LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP,\n                     LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP) / 1000000 AS last_trx_lag_sec\nFROM performance_schema.replication_applier_status_by_worker\nORDER BY CHANNEL_NAME, WORKER_ID;","what":"SHOW REPLICA STATUS가 한 행으로 요약하는 내용을 채널별·worker별로 세분화한다: receiver 스레드 상태, 각 worker의 마지막 오류, 적용 완료 / 적용 중인 GTID, 원본 커밋 타임스탬프로 계산한 실제 지연.","when":"Seconds_Behind_Source를 신뢰하기 어려울 때(0과 큰 값 사이를 오가거나 NULL), 멀티스레드 replica에서 어느 worker가 멈췄는지 찾을 때, 모니터링에 정확한 지연 지표를 제공할 때.","caution":"current_lag_sec는 트랜잭션이 적용 중일 때만 의미가 있으며 idle replica는 0을 반환한다. receiver도 뒤처졌는지 보려면 LAST_QUEUED_TRANSACTION을 source의 gtid_executed와 비교한다. 원본 커밋 타임스탬프는 source 시계를 기준으로 하므로 서버 간 시간 차이(NTP)가 결과를 왜곡한다. Seconds_Behind_Source는 대형 트랜잭션 시작 시 0 근처에 머물다가 끝날 때 급증하므로, 장시간 트랜잭션이 있으면 이 지표는 오해를 일으킨다.","note":"ORIGINAL_COMMIT_TIMESTAMP / END_APPLY_TIMESTAMP 컬럼은 8.0.2에서 도입되었으며 전체 체인(source → 중간 → replica)을 포함한다. 5.7에는 Seconds_Behind_Master만 있다.","since":"MySQL 8.0","tags":["replication","lag","applier","worker","performance_schema","delay"]},{"id":"replica-parallel-applier","task":"멀티스레드 replica 튜닝(replica_parallel_workers)","category":"복제","command":"-- Replica: stop the applier, set worker count and commit order\nSTOP REPLICA SQL_THREAD;\nSET PERSIST replica_parallel_workers = 8;\nSET PERSIST replica_preserve_commit_order = ON;\nSTART REPLICA SQL_THREAD;\nSELECT COUNT(*) AS workers FROM performance_schema.replication_applier_status_by_worker;\n\n-- Source (8.0 only): switch dependency tracking to WRITESET (default in 8.4, variable removed)\nSET PERSIST binlog_transaction_dependency_tracking = WRITESET;","what":"replica에서 트랜잭션을 병렬로 적용하는 worker 수를 설정한다. replica_preserve_commit_order는 커밋 순서를 source와 동일하게 유지한다. source의 WRITESET 의존성 추적은 source에서 순차적으로 커밋되었더라도 서로 다른 행을 건드리는 트랜잭션을 병렬로 적용할 수 있게 한다.","when":"replica가 source의 쓰기 속도를 따라가지 못하고 applier가 사실상 단일 스레드인 상태에서 지연이 쌓일 때.","caution":"새 worker 수는 applier가 재시작될 때 적용된다. CPU 코어보다 많은 worker를 실행하면 조정 오버헤드가 늘어나므로 8–16 범위에서 시작하고 지연과 worker 오류를 관찰한다. 하나의 거대한 트랜잭션이나 PK가 없는 테이블의 대량 DML은 병렬화할 수 없으며, 이것이 대개 지속적인 지연의 실제 원인이다. replica_preserve_commit_order=OFF는 병렬성을 높이지만 replica에서 짧은 순서 뒤바뀐 커밋을 허용하므로 읽기를 처리하는 replica에서는 ON으로 유지한다.","note":"8.0.27부터 기본값은 replica_parallel_workers=4, replica_parallel_type=LOGICAL_CLOCK, replica_preserve_commit_order=ON이다. 8.0.26 이전에는 변수 이름이 slave_parallel_workers / slave_preserve_commit_order였다. binlog_transaction_dependency_tracking은 8.0.35에서 deprecated되고 8.4에서 제거되었으며, 8.4에서는 WRITESET 동작이 기본이다.","since":"MySQL 8.0.26","tags":["replication","parallel","replica_parallel_workers","writeset","lag","mta"]},{"id":"replica-fix-row-conflict","task":"깨진 replica 복구: 1062 / 1032 행 충돌","category":"복제","command":"-- 1) Failing worker, error and GTID\nSELECT WORKER_ID, LAST_ERROR_NUMBER, LEFT(LAST_ERROR_MESSAGE, 250) AS last_error, APPLYING_TRANSACTION\nFROM performance_schema.replication_applier_status_by_worker\nWHERE LAST_ERROR_NUMBER <> 0;\n\n-- 2) Shell: decode the row images of that transaction from the relay log\nmysqlbinlog --base64-output=DECODE-ROWS -vv --include-gtids='af6bc361-c353-11f1-8f12-f69e63ad6c0e:11' /var/lib/mysql/db2-relay-bin.0* | grep '^###'\n\n-- 3a) 1062 Duplicate entry: delete the extra row on the replica without binlogging\nSET SESSION sql_log_bin = 0;\nDELETE FROM shop.orders WHERE id = 20001;\n-- 3b) 1032 Can't find record: insert the missing row from the WHERE (before image) values\nINSERT INTO shop.orders (id, cust, amount, note, created) VALUES (6, 6, 9.00, 'n6', '2026-10-08 20:09:44');\nSET SESSION sql_log_bin = 1;\n\n-- 4) Start the applier and verify\nSTART REPLICA SQL_THREAD;\nSELECT SERVICE_STATE, LAST_ERROR_NUMBER FROM performance_schema.replication_applier_status_by_coordinator;","what":"가장 흔한 두 가지 row 기반 복제 중단(1062 duplicate key, 1032 row not found)을 트랜잭션을 건너뛰지 않고 replica의 행을 source가 기대하는 상태로 맞춰 해결한다. 데이터 일관성이 유지된다.","when":"Replica_SQL_Running = No이고 오류 1062 / 1032일 때. 빈 트랜잭션으로 GTID를 건너뛰는 대신 올바른 복구 방법으로.","caution":"replica에서의 수정은 sql_log_bin=0으로 실행해야 한다. 그렇지 않으면 errant GTID가 생겨 페일오버 시 문제가 된다. super_read_only=ON이면 일시적으로 끄고 나중에 다시 켠다. before-image 값은 binlog_row_image=FULL을 전제로 한다. MINIMAL이면 PK만 기록되므로 나머지 행은 source에서 읽어야 한다. 한 테이블에 충돌이 하나 있으면 대개 더 있으므로 복구 후 pt-table-checksum으로 테이블을 source와 비교한다. relay log 경로: SELECT @@relay_log_basename;","note":"replica_exec_mode=IDEMPOTENT는 이 오류들을 조용히 무시한다. NDB와 특수한 경우를 위한 설정이므로 영구적으로 켜 두어서는 안 된다. 트랜잭션을 정말로 건너뛰어야 한다면 skip-gtid-transaction 레시피를 참고한다.","since":"MySQL 8.0.22","tags":["replication","1062","1032","mysqlbinlog","relay log","broken replica"]},{"id":"gtid-errant-transactions","task":"errant GTID 트랜잭션 찾기 및 무력화","category":"복제","command":"-- 1) On the source: get gtid_executed\nSELECT @@GLOBAL.gtid_executed;\n\n-- 2) On the replica: GTIDs missing on the source (empty result = clean)\nSELECT GTID_SUBTRACT(@@GLOBAL.gtid_executed,\n       'af6bc361-c353-11f1-8f12-f69e63ad6c0e:1-13') AS errant_gtids;\n\n-- 3) If the content is harmless: commit an empty transaction per errant GTID on the source\nSET GTID_NEXT = 'b030e5a6-c353-11f1-a887-523817ec642a:1'; BEGIN; COMMIT;\nSET GTID_NEXT = 'b030e5a6-c353-11f1-a887-523817ec642a:2'; BEGIN; COMMIT;\nSET GTID_NEXT = 'AUTOMATIC';","what":"source에 대응 항목이 없이 replica에서 직접 실행된 트랜잭션(errant)을 GTID 집합 차이로 탐지하고, source에서 빈 트랜잭션으로 표시하여 토폴로지 전체의 GTID 집합을 맞춘다.","when":"페일오버 / 스위치오버 전 정기 점검으로. Orchestrator나 MySQL Shell이 errant GTID 때문에 replica 승격을 거부할 때, 또는 replica가 새 source에 연결할 때 1236으로 실패할 때.","caution":"먼저 replica의 binlog(mysqlbinlog --include-gtids=...)에서 errant 트랜잭션이 무엇을 했는지 확인한다. 데이터를 변경했다면 빈 트랜잭션은 GTID만 덮을 뿐 데이터 차이는 남으므로 replica를 재구축(CLONE)하거나 데이터를 수동으로 맞춘다. source에서 커밋한 빈 트랜잭션은 모든 replica로 전파되며, 이는 의도된 동작이다. 재발을 막기 위해 replica에서 super_read_only=ON을 유지한다.","note":"replica가 승격되면 다른 replica들이 이 GTID를 그 서버에 요청한다. 해당 binlog가 purge되었으면 오류 1236으로 복제가 깨진다. 이 때문에 errant GTID는 페일오버 전까지 보이지 않는 위험이다.","since":"MySQL 5.6","tags":["gtid","errant","gtid_subtract","failover","replication","empty transaction"]},{"id":"replica-rebuild-clone","task":"CLONE plugin으로 replica를 처음부터 재구축","category":"복제","command":"-- Donor (healthy replica or source)\nINSTALL PLUGIN clone SONAME 'mysql_clone.so';\nCREATE USER 'clone_donor'@'%' IDENTIFIED BY 'Cl0ne#Donor';\nGRANT BACKUP_ADMIN ON *.* TO 'clone_donor'@'%';\n\n-- Recipient (replica to rebuild; its data is replaced)\nINSTALL PLUGIN clone SONAME 'mysql_clone.so';\nSET GLOBAL clone_valid_donor_list = 'db1:3306';\nCLONE INSTANCE FROM 'clone_donor'@'db1':3306 IDENTIFIED BY 'Cl0ne#Donor';\n\n-- After the restart: verify and start replication\nSELECT STATE, ERROR_NO, BINLOG_FILE, BINLOG_POSITION, GTID_EXECUTED FROM performance_schema.clone_status;\nSELECT STAGE, STATE, ROUND(DATA / 1024 / 1024) AS data_mb FROM performance_schema.clone_progress;\nCHANGE REPLICATION SOURCE TO SOURCE_HOST = 'db1', SOURCE_PORT = 3306, SOURCE_USER = 'repl', SOURCE_PASSWORD = 'Repl#2024', SOURCE_AUTO_POSITION = 1, SOURCE_SSL = 1;\nSTART REPLICA;","what":"donor의 InnoDB 데이터를 네트워크로 물리 복사하고, recipient를 재시작하며, GTID 상태를 이어받는다. 별도의 백업, 전송, 복원 단계 없이 일관된 replica를 만든다.","when":"손상된 replica, errant GTID가 있는 replica, binlog가 purge될 만큼 크게 뒤처진 replica를 재구축하거나 새 replica를 추가하기 위해.","caution":"recipient의 모든 데이터(사용자 포함)가 donor의 사본으로 교체되며 root 비밀번호도 donor의 것이 된다. donor와 recipient는 같은 릴리스 시리즈, 플랫폼, plugin 집합이 필요하다(8.0.37 이전에는 정확히 같은 point release가 필요했다). 명령을 실행하는 recipient 사용자에게 CLONE_ADMIN이 필요하다. clone이 끝나면 mysqld가 스스로 재시작한다. systemd / mysqld_safe 같은 supervisor가 없으면 3707 'Restart server failed (mysqld is not managed by supervisor process)'를 반환한다. 이는 실패가 아니며 clone은 완료된 것이므로 서버를 수동으로 시작한다. donor에는 IO / 네트워크 부하가 크므로 clone_max_concurrency와 clone_max_data_bandwidth로 제한한다. GTID가 꺼져 있으면 clone_status의 BINLOG_FILE / BINLOG_POSITION을 SOURCE_LOG_FILE / SOURCE_LOG_POS로 사용한다.","note":"InnoDB 데이터만 clone된다. MyISAM과 CSV 테이블은 비어 있는 상태로 도착한다. InnoDB Cluster에서 addInstance(..., {recoveryMethod: 'clone'})는 같은 메커니즘을 사용한다.","since":"MySQL 8.0.17","tags":["clone","rebuild","replica","provisioning","gtid","reseed"]},{"id":"delayed-replica-recover","task":"지연 replica로 잘못된 DROP / DELETE 복구(SOURCE_DELAY + UNTIL)","category":"복제","command":"-- Setup: the replica applies events one hour behind\nSTOP REPLICA;\nCHANGE REPLICATION SOURCE TO SOURCE_DELAY = 3600;\nSTART REPLICA;\n\n-- During the incident: stop the applier (the receiver keeps fetching binlogs)\nSTOP REPLICA SQL_THREAD;\n\n-- Find the bad transaction's GTID in the source binlog, apply up to just before it\nCHANGE REPLICATION SOURCE TO SOURCE_DELAY = 0;\nSTART REPLICA SQL_THREAD UNTIL SQL_BEFORE_GTIDS = 'af6bc361-c353-11f1-8f12-f69e63ad6c0e:17';\nSELECT SERVICE_STATE FROM performance_schema.replication_applier_status_by_coordinator;","what":"replica가 이벤트를 고정된 시간만큼 늦게 적용하게 한다. 잘못된 DROP / DELETE가 source에 발생하면 replica를 해당 트랜잭션 직전까지 진행시켜 손실된 데이터를 복구한다.","when":"백업 복원에 몇 시간이 걸리는 대형 데이터베이스에서 인적 오류에 대비한 빠른 복구 계층으로.","caution":"coordinator가 OFF이면 데이터는 잘못된 작업 직전 상태다. 영향받은 테이블을 mysqldump로 덤프하여 source에 로드한다. 이후 replica를 다시 지연 모드로 되돌린다. 잘못된 트랜잭션까지 적용하면 여기서도 데이터가 사라진다. 지연 replica는 페일오버 후보가 되어서는 안 된다(Orchestrator / Router / ProxySQL에서 제외). 지연 기간 내내 relay log가 쌓이므로 디스크 크기를 그에 맞게 산정한다.","note":"8.0.23 이전에는 CHANGE MASTER TO MASTER_DELAY를 사용한다. 지연은 직속 source의 커밋 시점부터 측정되므로 체인 내 지연된 중간 replica의 지연은 누적된다. SQL_BEFORE_GTIDS는 지정한 GTID 직전에 멈추고, SQL_AFTER_GTIDS는 그 GTID까지 적용한다. 8.2 이전(8.0 포함)에는 멀티스레드 applier와 함께 SQL_AFTER_GTIDS를 사용하면 경고가 발생하고 replica가 단일 스레드 모드로 전환된다.","since":"MySQL 8.0.23","tags":["delayed replica","source_delay","until","sql_before_gtids","recovery","drop table"]},{"id":"mysqldump-consistent","task":"mysqldump로 GTID 정보를 포함한 일관된 논리 백업 수행","category":"백업 및 PITR","command":"mysqldump -h db1 -u backup -p \\\n  --single-transaction --source-data=2 --set-gtid-purged=ON \\\n  --routines --events --triggers --hex-blob \\\n  --all-databases | gzip > /backup/full_$(date +%F_%H%M).sql.gz\n\n# Restore: GTID sets must not overlap on the target\nzcat /backup/full_2026-10-08_0200.sql.gz | mysql -h db2 -u root -p","what":"InnoDB 테이블을 단일 일관 스냅샷 안에서 덤프하고, 맨 위에 binlog 파일 / 위치(주석)와 SET @@GLOBAL.GTID_PURGED 문을 기록하여 덤프를 replica 초기화나 binlog 기반 PITR의 시작점으로 사용할 수 있게 한다.","when":"수십 GB 이하의 데이터베이스, 스키마 / 테이블 이동, 버전 간 논리 마이그레이션용.","caution":"--single-transaction은 InnoDB에서만 일관성이 있으며 MyISAM 테이블은 일관성이 없는 상태로 덤프된다. 덤프 중 테이블에 DDL(ALTER / RENAME / TRUNCATE)이 실행되면 덤프가 실패하거나 테이블이 누락된다. --source-data / --set-gtid-purged는 시작 시 짧은 global read lock을 잡는다(RELOAD 권한 필요). 그 순간 장시간 쿼리가 실행 중이면 락이 대기하는 동안 모든 쓰기가 멈춘다. 부분 덤프(--databases)도 모든 GTID를 기록하므로 일부만 다른 서버로 옮길 때는 --set-gtid-purged=OFF를 사용한다. 덤프는 단일 스레드이므로 대용량 데이터에는 MySQL Shell dump나 XtraBackup을 권장한다.","note":"8.0.26에서 --source-data가 --master-data를 대체했다. mysqlpump는 8.0.34에서 deprecated되고 8.4에서 제거되었다. 병렬 논리 백업에는 MySQL Shell util.dumpInstance를 사용한다.","since":"MySQL 8.0.26","tags":["mysqldump","backup","single-transaction","gtid","logical backup","dump"]},{"id":"mysqlsh-dump-load","task":"MySQL Shell로 병렬 덤프 / 로드(util.dumpInstance, loadDump)","category":"백업 및 PITR","command":"# Parallel, zstd-compressed, consistent instance dump\nmysqlsh backup@db1:3306 -- util dump-instance /backup/dump_20261008 --threads=8 --compression=zstd --consistent=true\n# Single schema\nmysqlsh backup@db1:3306 -- util dump-schemas shop --outputUrl=/backup/shop_20261008 --threads=8\n\n# Load: local_infile must be temporarily ON on the target\nmysql -h db2 -u root -p -e \"SET GLOBAL local_infile = ON\"\nmysqlsh root@db2:3306 -- util load-dump /backup/dump_20261008 --threads=8 --skipBinlog=false\nmysql -h db2 -u root -p -e \"SET GLOBAL local_infile = OFF\"","what":"테이블을 chunk로 나누어 여러 스레드로 덤프 / 로드하며, 덤프와 복원 모두 mysqldump보다 몇 배 빠르다. 덤프 디렉터리는 GTID와 binlog 좌표를 @.json에 저장한다.","when":"수백 GB 규모의 논리 백업, 업그레이드 중 논리 마이그레이션, 온프레미스 → 클라우드(RDS, HeatWave) 이전용.","caution":"--consistent=true는 짧은 global read lock / backup lock을 잡으며 장시간 쿼리가 실행 중이면 쓰기가 대기한다. loadDump는 LOAD DATA LOCAL을 사용하므로 대상에 local_infile=ON이 필요하며 이후에는 꺼야 한다. 중단된 로드는 멈춘 지점부터 재개된다(progress 파일). 처음부터 다시 하려면 --resetProgress를 사용한다. 사용자 계정은 기본적으로 로드되지 않는다(--loadUsers=true). --skipBinlog=true는 로드 중 binlog 기록을 끄므로 대상에 replica가 있으면 false로 둔다.","note":"물리 백업이 아니다. PITR에는 덤프의 @.json에 있는 gtidExecuted 값부터 binlog를 적용한다. ocimds / compatibility 옵션은 HeatWave / RDS 제약을 준수하는 덤프를 생성한다.","since":"MySQL Shell 8.0.21","tags":["mysqlsh","dumpinstance","loaddump","parallel","logical backup","migration"]},{"id":"xtrabackup-full-incremental","task":"XtraBackup으로 full + incremental 물리 백업 및 복원","category":"백업 및 PITR","command":"# Full backup\nxtrabackup --backup --host=127.0.0.1 --user=bkp --password='Bkp#2024' --target-dir=/backup/full --parallel=4\n# Incremental (based on the previous backup)\nxtrabackup --backup --host=127.0.0.1 --user=bkp --password='Bkp#2024' --target-dir=/backup/inc1 --incremental-basedir=/backup/full\n\n# Prepare: full and intermediate incrementals with --apply-log-only, final step without it\nxtrabackup --prepare --apply-log-only --target-dir=/backup/full\nxtrabackup --prepare --apply-log-only --target-dir=/backup/full --incremental-dir=/backup/inc1\nxtrabackup --prepare --target-dir=/backup/full\n\n# Restore: mysqld stopped and datadir empty\nsystemctl stop mysqld\nxtrabackup --copy-back --target-dir=/backup/full --datadir=/var/lib/mysql\nchown -R mysql:mysql /var/lib/mysql\nsystemctl start mysqld\ncat /backup/full/xtrabackup_binlog_info","what":"서버가 실행 중인 동안 InnoDB 파일과 redo log를 복사하여 hot 물리 백업을 수행한다. incremental 백업은 이전 백업 이후 변경된 페이지만 포함한다. prepare 단계는 redo를 적용하여 백업을 일관된 상태로 만든다.","when":"빠른 복원(논리 방식보다 몇 배 빠름)이 필요한 대형 데이터베이스, replica 초기화, 일일 full + 시간별 incremental 전략용.","caution":"XtraBackup 버전은 서버 릴리스 시리즈와 일치해야 한다(8.0 서버 → XtraBackup 8.0, 8.4 → 8.4). 서버가 XtraBackup 빌드보다 새로운 redo / data dictionary 형식을 사용하면 백업이 거부된다. prepare하지 않은 백업은 일관성이 없다. 중간 단계에서 --apply-log-only를 잊으면 이후 incremental을 사용할 수 없다. 복원 시 datadir은 비어 있어야 한다. --move-back은 디스크를 절약하지만 백업을 소모한다. 백업 사용자: BACKUP_ADMIN, PROCESS, RELOAD, LOCK TABLES, REPLICATION CLIENT 및 performance_schema.log_status, keyring_component_status, replication_group_members에 대한 SELECT.","note":"xtrabackup_binlog_info에는 백업의 binlog 파일 / 위치와 GTID 집합이 들어 있으며 PITR은 그 지점부터 binlog를 재생한다. 다른 호스트로 스트리밍하려면 --stream=xbstream | ssh host 'xbstream -x -C /backup'을 사용한다.","since":"XtraBackup 8.0","tags":["xtrabackup","physical backup","incremental","prepare","restore","percona"]},{"id":"pitr-binlog-replay","task":"binlog를 이용한 특정 시점 복구: 문제 트랜잭션 찾기 및 건너뛰기","category":"백업 및 PITR","command":"# 1) Find the bad event's GTID and position (narrow by time range)\nmysqlbinlog --base64-output=DECODE-ROWS -vv --start-datetime='2026-10-08 20:00:00' --stop-datetime='2026-10-08 20:30:00' /var/lib/mysql/binlog.000003 | grep -B12 -iE '^(DROP|TRUNCATE)' | grep -E 'GTID_NEXT|^# at|^#[0-9]{6}|^DROP|^TRUNCATE'\n\n# 2) Restore the backup on a separate server (the dump sets GTID_PURGED)\nzcat /backup/full_2026-10-08_0200.sql.gz | mysql -h restore-db -u root -p\n\n# 3a) Replay all binlogs skipping only the bad transaction\nmysqlbinlog --exclude-gtids='af6bc361-c353-11f1-8f12-f69e63ad6c0e:23' /binlogs/binlog.000003 /binlogs/binlog.000004 | mysql -h restore-db -u root -p\n# 3b) Or stop right before the bad event's GTID event\nmysqlbinlog --stop-position=537205 /binlogs/binlog.000003 | mysql -h restore-db -u root -p","what":"백업을 복원한 후 binlog를 순서대로 재생하여 데이터베이스를 원하는 시점으로 만든다. GTID가 켜져 있으면 이미 백업에 있는 트랜잭션은 자동으로 건너뛴다. --exclude-gtids는 문제 작업만 제거하고 그 이후의 모든 변경을 유지한다.","when":"잘못된 DROP / DELETE / UPDATE 후 백업과 사고 사이의 변경을 잃지 않고 복구하기 위해.","caution":"운영 환경이 아닌 별도 서버에 복원하고 검증한 후 영향받은 테이블만 옮긴다. --start/--stop-datetime은 mysqlbinlog를 실행하는 머신의 시간대로 해석된다. --stop-position은 마지막으로 지정한 파일에만 적용되므로 여러 파일이면 앞의 파일들은 별도 명령으로 전부 재생한다. --exclude-gtids는 독립적인 작업에만 안전하다. 이후 트랜잭션이 삭제된 테이블에 쓰면 그것들도 실패한다. 필요한 binlog가 source에서 purge되면 안 되므로 binlog 보존 기간을 최소 한 번의 백업 주기보다 길게 유지한다.","note":"mysqlbinlog와 mysql을 모든 파일을 지정한 단일 명령의 단일 파이프로 실행한다. 파일을 하나씩 재생하면 임시 테이블과 파일에 걸친 트랜잭션이 깨진다. binlog 집합이 매우 크면 대안으로 binlog를 복원 서버의 relay log로 배치하고 (병렬) SQL 스레드로 적용할 수 있다.","since":"MySQL 8.0","tags":["pitr","mysqlbinlog","binlog","point in time","exclude-gtids","recovery"]},{"id":"innodb-cluster-status","task":"InnoDB Cluster / Group Replication 상태 확인","category":"InnoDB Cluster","command":"# Shell: topology, primary, fault tolerance, lag\nmysqlsh icadmin@db1:3306 -- cluster status --extended=1\nmysqlsh icadmin@db1:3306 -- cluster list-routers\n\n-- SQL: group members and queues from any member\nSELECT m.MEMBER_HOST, m.MEMBER_PORT, m.MEMBER_STATE, m.MEMBER_ROLE, m.MEMBER_VERSION,\n       s.COUNT_TRANSACTIONS_IN_QUEUE AS certify_queue,\n       s.COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE AS apply_queue,\n       s.COUNT_CONFLICTS_DETECTED AS conflicts\nFROM performance_schema.replication_group_members m\nJOIN performance_schema.replication_group_member_stats s ON s.MEMBER_ID = m.MEMBER_ID\nORDER BY m.MEMBER_ROLE, m.MEMBER_HOST;","what":"클러스터 전체 상태(OK / OK_NO_TOLERANCE / NO_QUORUM), primary, 각 멤버의 상태, certification / applier 대기열을 보여 준다. 대기열로 어느 멤버가 뒤처졌는지와 flow control 위험을 알 수 있다.","when":"모든 작업 전후, 알림 발생 시, 쓰기 성능 저하 시, 멤버가 RECOVERING / ERROR / UNREACHABLE을 보일 때.","caution":"statusText는 'can tolerate up to ONE failure'(멤버 3개)여야 한다. NO_QUORUM이면 쓰기가 멈춘다. 다른 파티션이 다운된 것이 확실할 때만 forceQuorumUsingPartitionOf를 사용한다. 그렇지 않으면 split-brain이 발생한다. apply_queue가 계속 늘어나는 멤버는 flow control을 트리거하여 primary의 쓰기를 느리게 한다. RECOVERING이 길면 clone / incremental recovery가 실행 중인 것이다. ERROR이면 error log를 확인하고 cluster.rejoinInstance()를 시도한다. 비밀번호의 특수 문자(#, @)는 URI에서 URL 인코딩해야 한다(# → %23).","note":"MEMBER_COMMUNICATION_STACK은 8.0.27에서 도입되었다. MYSQL 스택은 GR 트래픽을 일반 TLS와 사용자 인증으로 3306을 통해 전달한다(XCOM은 별도 포트, 보통 33061 사용). 8.4에서 group_replication_consistency 기본값은 BEFORE_ON_PRIMARY_FAILOVER, exit_state_action 기본값은 OFFLINE_MODE이다.","since":"MySQL 8.0","tags":["innodb cluster","group replication","mysqlsh","status","flow control","quorum"]},{"id":"innodb-cluster-switch-primary","task":"InnoDB Cluster에서 계획된 primary 스위치오버","category":"InnoDB Cluster","command":"# Shell: planned switchover\nmysqlsh icadmin@db1:3306 -- cluster set-primary-instance db2:3306\n\n-- SQL alternative: target member's server_uuid, 30 s timeout for open transactions\nSELECT MEMBER_HOST, MEMBER_ID, MEMBER_ROLE FROM performance_schema.replication_group_members;\nSELECT group_replication_set_as_primary('3f06e651-c356-11f1-8aac-927a9034ef99', 30);","what":"single-primary 모드에서 primary 역할을 다른 멤버로 제어된 방식으로 옮긴다. 이전 primary는 super_read_only가 되고, 새 primary는 밀린 트랜잭션을 적용한 후 쓰기를 연다. MySQL Router는 새 primary를 자동으로 인식한다.","when":"primary의 유지보수, OS 패치, 업그레이드 전과 하드웨어 이전 시.","caution":"timeout이 만료되면 이전 primary의 열린 트랜잭션은 종료된다. timeout이 없으면 전환이 이 트랜잭션들을 기다리므로 더 오래 걸린다. 애플리케이션이 Router를 통해 연결하지 않으면 직접 리디렉션해야 한다. BEFORE_ON_PRIMARY_FAILOVER에서는 새 primary가 밀린 트랜잭션을 모두 처리할 때까지 쓰기가 대기하므로 먼저 cluster status에서 apply_queue가 비어 있는지 확인한다. InnoDB Cluster에서는 메타데이터와 일관성이 유지되는 Shell 명령을 권장한다.","note":"group_replication_set_as_primary는 8.0.13에서 도입되었고 timeout 파라미터는 8.0.29에서 추가되었다. 계획되지 않은 페일오버에서는 GR이 새 primary를 스스로 선출한다(group_replication_member_weight로 우선순위 설정).","since":"MySQL 8.0.13","tags":["innodb cluster","switchover","primary","group replication","set_as_primary","maintenance"]},{"id":"innodb-cluster-reboot-outage","task":"전체 장애 후 클러스터 복구(rebootClusterFromCompleteOutage)","category":"InnoDB Cluster","command":"# Are all members OFFLINE? (on every node)\nmysql -h db1 -u icadmin -p -e \"SELECT MEMBER_HOST, MEMBER_STATE FROM performance_schema.replication_group_members\"\n# Dry run first: shows the most up-to-date member and the planned steps\nmysqlsh icadmin@db1:3306 --js -e \"dba.rebootClusterFromCompleteOutage('prodCluster', {dryRun: true})\"\nmysqlsh icadmin@db1:3306 --js -e \"dba.rebootClusterFromCompleteOutage('prodCluster')\"\nmysqlsh icadmin@db1:3306 -- cluster status","what":"정전이나 모든 노드의 동시 재시작으로 quorum이 없을 때 그룹을 다시 bootstrap한다. GTID 집합이 가장 큰 멤버를 선택하고 나머지를 재합류시킨다.","when":"cluster status가 'Cluster is not reachable'을 보고하고 모든 멤버가 OFFLINE으로 표시될 때.","caution":"모든 멤버가 OFFLINE일 때만 사용한다. 한 멤버라도 ONLINE이면 그룹이 살아 있는 것이므로 나머지는 rejoinInstance로 합류시킨다. 실행 전에 모든 노드를 시작한다. 접근할 수 없는 멤버가 더 최신이면 그 트랜잭션은 손실된다. force: true는 누락된 멤버를 무시하므로 최후의 수단이다. primary: 'host:port' 옵션은 특정 멤버를 primary로 강제한다.","note":"dryRun, primary, force 옵션은 Shell 8.0.31에서 도입되었다. 이전 버전은 대화형 프롬프트로 동작했다. group_replication_start_on_boot=ON이어도 전체 장애 후에는 그룹이 스스로 형성되지 않으므로 bootstrap은 한곳에서 의도적으로 수행해야 한다.","since":"MySQL Shell 8.0.31","tags":["innodb cluster","outage","reboot","bootstrap","mysqlsh","disaster"]},{"id":"mysql-router-bootstrap","task":"InnoDB Cluster에 대해 MySQL Router bootstrap","category":"Router 및 Proxy","command":"# Generates config from metadata and creates the router account; --user is required when run as root\nmysqlrouter --bootstrap icadmin@db1:3306 --directory /etc/mysqlrouter/prod --account routeruser --account-create if-not-exists --name router01 --conf-bind-address 0.0.0.0 --user mysqlrouter\n/etc/mysqlrouter/prod/start.sh\n# Ports: 6446 RW, 6447 RO, 6450 RW split (classic); 6448 / 6449 X protocol\nmysql -h app-host -P 6446 -u app -p -e \"SELECT @@hostname\"\nmysql -h app-host -P 6447 -u app -p -e \"SELECT @@hostname\"\nmysqlsh icadmin@db1:3306 -- cluster list-routers","what":"Router를 클러스터 메타데이터에 등록하고 계정과 구성을 생성한다. 애플리케이션은 RW 포트로 현재 primary에, RO 포트로 secondary에 round-robin으로 접근한다.","when":"InnoDB Cluster / ClusterSet / ReplicaSet 앞에 투명한 페일오버 계층을 둘 때. 일반적으로 애플리케이션 서버마다 Router 하나.","caution":"Router는 stateless다. 애플리케이션 옆에서 실행하면 단일 장애 지점을 피할 수 있지만 중앙 Router 하나로는 그렇지 않다. bootstrap 계정은 bootstrap 중에만 사용되며 Router는 자체 계정(routeruser)으로 메타데이터를 읽는다. 토폴로지 변경은 재bootstrap 없이 metadata cache로 인식한다. RW split 포트(6450)는 트랜잭션 내 읽기를 primary로, 나머지를 secondary로 보내므로 read-your-writes를 기대하는 애플리케이션에 주의한다. Router 버전은 클러스터의 MySQL 서버보다 오래되어서는 안 된다.","note":"읽기/쓰기 분리 포트(6450)는 8.2에서 도입되었다. 페일오버 시 기존 연결은 끊어지므로 애플리케이션에 재연결(retry) 로직이 있어야 한다.","since":"MySQL Router 8.0","tags":["mysql router","bootstrap","innodb cluster","failover","read write split","6446"]},{"id":"proxysql-rw-split","task":"ProxySQL로 읽기/쓰기 분리 및 replica 상태 점검","category":"Router 및 Proxy","command":"-- On MySQL (source): monitor user\nCREATE USER 'proxysql_mon'@'%' IDENTIFIED BY 'M0n#itor2024';\nGRANT USAGE, REPLICATION CLIENT ON *.* TO 'proxysql_mon'@'%';\n\n-- ProxySQL admin: mysql -u admin -p -h 127.0.0.1 -P 6032\nUPDATE global_variables SET variable_value = 'proxysql_mon' WHERE variable_name = 'mysql-monitor_username';\nUPDATE global_variables SET variable_value = 'M0n#itor2024' WHERE variable_name = 'mysql-monitor_password';\nLOAD MYSQL VARIABLES TO RUNTIME; SAVE MYSQL VARIABLES TO DISK;\nINSERT INTO mysql_replication_hostgroups (writer_hostgroup, reader_hostgroup, check_type, comment) VALUES (10, 20, 'read_only', 'shop');\nINSERT INTO mysql_servers (hostgroup_id, hostname, port, max_replication_lag) VALUES (10, 'db1', 3306, 0), (20, 'db2', 3306, 30), (20, 'db3', 3306, 30);\nLOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;\nINSERT INTO mysql_users (username, password, default_hostgroup, transaction_persistent) VALUES ('app', 'App#Pass2024', 10, 1);\nLOAD MYSQL USERS TO RUNTIME; SAVE MYSQL USERS TO DISK;\nINSERT INTO mysql_query_rules (rule_id, active, match_digest, destination_hostgroup, apply) VALUES (100, 1, '^SELECT .* FOR (UPDATE|SHARE)', 10, 1), (200, 1, '^SELECT ', 20, 1);\nLOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK;\n\n-- Verify\nSELECT hostgroup_id, hostname, status FROM runtime_mysql_servers;\nSELECT hostgroup, srv_host, status, ConnUsed, Queries, Latency_us FROM stats_mysql_connection_pool;\nSELECT hostgroup, substr(digest_text, 1, 60) AS digest, count_star, sum_time / 1000 AS sum_ms FROM stats_mysql_query_digest ORDER BY sum_time DESC LIMIT 10;","what":"read_only 플래그를 기준으로 writer(10)와 reader(20) hostgroup을 자동 관리한다. SELECT는 replica로, 잠금 읽기와 쓰기는 source로 라우팅하며, max_replication_lag를 초과한 replica는 일시적으로 제외한다(SHUNNED).","when":"애플리케이션을 건드리지 않고 비동기 복제 토폴로지에서 읽기 부하를 분산하기 위해. 커넥션 풀링과 쿼리별 라우팅 / 캐싱 / 방화벽용.","caution":"구성은 3개 계층(MEMORY → RUNTIME → DISK)이다. LOAD ... TO RUNTIME 없이는 아무것도 바뀌지 않고, SAVE ... TO DISK 없이는 재시작 시 사라진다. read_only 점검에는 replica의 super_read_only=ON이 필요하다. 그렇지 않으면 replica가 writer hostgroup으로 옮겨진다. 모든 SELECT를 replica로 보내면 지연 때문에 read-after-write 오류가 발생한다. stats_mysql_query_digest를 기반으로 무거운 / 보고용 쿼리에만 규칙을 작성하는 것이 더 안전하다. 관리 인터페이스는 SQLite이므로 LEFT() 같은 MySQL 함수가 없다. substr()을 사용한다. 기본 admin:admin 계정을 변경한다(admin-admin_credentials).","note":"max_replication_lag 점검에는 monitor 사용자에게 REPLICATION CLIENT가 필요하다. Group Replication / InnoDB Cluster에는 mysql_group_replication_hostgroups 테이블을 사용한다.","since":"ProxySQL 2.0","tags":["proxysql","read write split","hostgroup","query rules","connection pool","replication lag"]},{"id":"online-ddl-instant-inplace","task":"Online DDL: ALGORITHM=INSTANT / INPLACE, LOCK=NONE 및 진행 상황 추적","category":"온라인 작업","command":"-- Always state ALGORITHM/LOCK: unsupported operations fail immediately instead of silently falling back to COPY\nALTER TABLE shop.orders ADD COLUMN status TINYINT NOT NULL DEFAULT 0, ALGORITHM=INSTANT;\nALTER TABLE shop.orders RENAME COLUMN note TO remark, ALGORITHM=INSTANT;\nALTER TABLE shop.orders ADD INDEX ix_status (status), ALGORITHM=INPLACE, LOCK=NONE;\nALTER TABLE shop.orders MODIFY remark VARCHAR(200), ALGORITHM=INPLACE, LOCK=NONE;\n\n-- INSTANT row version counter (INSTANT is refused at 64, a rebuild resets it)\nSELECT NAME, TOTAL_ROW_VERSIONS FROM information_schema.INNODB_TABLES WHERE NAME = 'shop/orders';\n\n-- Progress of a long ALTER (enable the instruments once)\nUPDATE performance_schema.setup_instruments SET ENABLED = 'YES', TIMED = 'YES' WHERE NAME LIKE 'stage/innodb/alter%';\nUPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME LIKE 'events_stages_%';\nSELECT t.PROCESSLIST_ID, s.EVENT_NAME, s.WORK_COMPLETED, s.WORK_ESTIMATED,\n       ROUND(100 * s.WORK_COMPLETED / NULLIF(s.WORK_ESTIMATED, 0), 1) AS pct_done\nFROM performance_schema.events_stages_current s\nJOIN performance_schema.threads t ON t.THREAD_ID = s.THREAD_ID\nWHERE s.EVENT_NAME LIKE 'stage/innodb/alter%';","what":"INSTANT는 data dictionary만 변경하므로 테이블 크기와 관계없이 즉시 끝난다. INPLACE + LOCK=NONE은 동시 읽기와 쓰기를 허용하면서 테이블을 제자리에서 처리한다. 마지막 쿼리는 실행 중인 ALTER가 어느 단계에 있는지와 진행률을 보여 준다.","when":"운영 테이블의 스키마 변경 전. 어떤 작업을 다운타임 없이 실행할 수 있는지 알고 긴 ALTER가 언제 끝날지 추정하기 위해.","caution":"INSTANT와 INPLACE도 시작과 끝에 짧은 배타 metadata lock을 잡는다. 장시간 트랜잭션이 테이블을 잡고 있으면 ALTER가 대기하고 그 뒤의 모든 쿼리가 대기열에 쌓인다(metadata lock 진단 참고). 세션에서 lock_wait_timeout을 낮춘다(SET SESSION lock_wait_timeout = 5). INPLACE 중 들어오는 DML은 innodb_online_alter_log_max_size까지 기록되며, 이를 초과하면 마지막에 ALTER가 롤백된다. 컬럼 타입 변경, charset 변환, PK 변경은 COPY가 필요하며 쓰기를 블로킹한다. 대형 테이블에는 gh-ost / pt-online-schema-change를 사용한다. replica에서는 ALTER가 같은 시간 동안 단일 스레드로 실행되므로 대형 INPLACE ALTER는 replica 지연을 일으키지만 INSTANT는 그렇지 않다.","note":"INSTANT ADD COLUMN은 8.0.12에서 마지막 컬럼에 대해서만 도입되었고, 8.0.29부터 임의 위치에 컬럼을 추가할 수 있으며 INSTANT DROP COLUMN이 지원된다. VARCHAR 확장은 길이 바이트 수가 바뀌지 않을 때(255바이트 경계를 넘지 않을 때) 메타데이터만 변경하며, INSTANT가 아닌 INPLACE로 즉시 완료된다. 인덱스가 있는 컬럼은 INSTANT로 삭제할 수 없으므로 먼저 인덱스를 삭제한다.","since":"MySQL 8.0.29","tags":["online ddl","instant","inplace","alter table","lock=none","progress"]},{"id":"pt-online-schema-change","task":"pt-online-schema-change로 대형 테이블에 블로킹 없는 ALTER","category":"온라인 작업","command":"# First --dry-run, then the same command with --execute; throttles on replica lag\npt-online-schema-change --alter \"ADD COLUMN region CHAR(2) NULL, ADD INDEX ix_region (region)\" \\\n  D=shop,t=orders,h=db1,u=osc,p='Osc#2024',s=1 \\\n  --max-lag=5 --check-replica-lag h=db2,u=osc,p='Osc#2024',s=1 \\\n  --max-load Threads_running=50 --critical-load Threads_running=200 \\\n  --set-vars lock_wait_timeout=5 --noversion-check --dry-run","what":"새 구조로 빈 사본을 만들고, 행을 chunk 단위로 복사하며, 복사 중 변경을 트리거로 반영하고, 마지막에 원자적 RENAME으로 테이블을 교체한다. 부하나 replica 지연이 임계값을 넘으면 스스로 속도를 줄이거나 일시 중지한다.","when":"COPY가 필요하거나(컬럼 타입, charset, PK 변경) 긴 replica 지연을 일으킬 ALTER용.","caution":"테이블에 PK나 unique key가 필요하다. 트리거는 마이그레이션 중 모든 쓰기에 오버헤드를 추가하므로 쓰기가 매우 많은 테이블에서는 gh-ost의 영향이 더 적다. 자식 테이블이 foreign key로 이 테이블을 참조하면 --alter-foreign-keys-method=auto가 필요하다. caching_sha2_password 계정은 DSN에 s=1(TLS)을 추가한다. 그렇지 않으면 replica 연결이 'Authentication requires secure connection'으로 실패한다. --check-replica-lag DSN은 기본 DSN의 user / password를 상속하지 않으므로 직접 지정한다. 교체 시 짧은 metadata lock을 잡는다. lock_wait_timeout을 낮게 유지하며, 락을 얻지 못하면 재시도한다.","note":"replica는 기본적으로 processlist를 통해 IP로 탐지된다. 해당 IP에 접근할 수 없으면 --recursion-method=dsn=D=percona,t=dsns로 DSN 테이블을 사용한다. 3.7에서 --check-slave-lag는 --check-replica-lag로 이름이 바뀌었다(이전 이름은 deprecated).","since":"Percona Toolkit 3.7","tags":["pt-online-schema-change","pt-osc","percona toolkit","alter","trigger","online schema change"]},{"id":"gh-ost-migration","task":"gh-ost로 트리거 없는 온라인 스키마 변경","category":"온라인 작업","command":"# Connects to a replica and reads its binlog; postpones cut-over until the flag file is removed\ngh-ost --host=db2 --port=3306 --user=ghost --password='Gh0st#2024' \\\n  --database=shop --table=orders \\\n  --alter=\"ADD COLUMN channel VARCHAR(20) NULL, ADD INDEX ix_channel (channel)\" \\\n  --max-load=Threads_running=50 --critical-load=Threads_running=200 --max-lag-millis=1500 \\\n  --exact-rowcount --concurrent-rowcount --default-retries=120 \\\n  --postpone-cut-over-flag-file=/tmp/ghost.postpone --panic-flag-file=/tmp/ghost.panic \\\n  --serve-socket-file=/tmp/gh-ost.shop.orders.sock --ssl --ssl-allow-insecure --execute\n# While running: status / throttle / cut-over\necho status | nc -U /tmp/gh-ost.shop.orders.sock\necho throttle | nc -U /tmp/gh-ost.shop.orders.sock\nrm /tmp/ghost.postpone","what":"ghost 테이블을 만들고 행을 복사하며, 동시 변경은 트리거 대신 binlog를 읽어 적용한다. throttling, 일시 중지, cut-over 시점은 socket이나 flag 파일로 런타임에 제어한다.","when":"트리거 오버헤드를 허용할 수 없는 쓰기가 많은 대형 테이블에서, cut-over 시점(예: 트래픽이 적은 시간대)을 직접 선택하고 싶을 때.","caution":"binlog_format=ROW와 binlog_row_image=FULL이 필요하며, 연결하는 replica에는 log_replica_updates=ON이 필요하다. 사용자에게 SUPER 또는 REPLICATION CLIENT / REPLICATION SLAVE와 테이블에 대한 ALL이 필요하다. foreign key가 있는 테이블(부모 또는 자식)은 지원되지 않는다. cut-over 후 이전 테이블은 _<table>_del로 남으므로 검증 후 수동으로 삭제한다(--ok-to-drop-table은 자동으로 삭제한다). 1.1.8 이전 gh-ost 릴리스는 SHOW SLAVE STATUS를 사용하므로 MySQL 8.4에서 오류 1064로 멈춘다. 8.4 지원은 1.1.8에서 추가되었다. gh-ost는 --postpone-cut-over-flag-file에 지정한 파일을 직접 생성하며, 파일이 제거될 때까지 cut-over가 일어나지 않는다.","note":"--test-on-replica 및 --migrate-on-replica 옵션으로 먼저 replica에서만 변경을 시도하고 검증할 수 있다. 스로틀 사유와 ETA는 'status' 명령 출력에 표시된다.","tags":["gh-ost","online schema change","binlog","cut-over","alter","triggerless"]},{"id":"ps-top-digests","task":"가장 비용이 큰 쿼리 찾기 (performance_schema digest)","category":"성능","command":"SELECT SCHEMA_NAME AS db,\n       LEFT(DIGEST_TEXT, 80) AS query,\n       COUNT_STAR AS calls,\n       ROUND(SUM_TIMER_WAIT / 1e12, 2) AS total_sec,\n       ROUND(AVG_TIMER_WAIT / 1e9, 2) AS avg_ms,\n       ROUND(QUANTILE_95 / 1e9, 2) AS p95_ms,\n       ROUND(100 * SUM_TIMER_WAIT / SUM(SUM_TIMER_WAIT) OVER (), 1) AS pct_total,\n       ROUND(SUM_ROWS_EXAMINED / NULLIF(COUNT_STAR, 0)) AS rows_exam_per_call,\n       ROUND(SUM_ROWS_SENT / NULLIF(COUNT_STAR, 0)) AS rows_sent_per_call,\n       SUM_NO_INDEX_USED AS no_index,\n       SUM_CREATED_TMP_DISK_TABLES AS tmp_disk,\n       LAST_SEEN\nFROM performance_schema.events_statements_summary_by_digest\nWHERE SCHEMA_NAME IS NOT NULL\nORDER BY SUM_TIMER_WAIT DESC\nLIMIT 20;\n\n-- Server-wide top wait events\nSELECT events, total, total_latency, avg_latency FROM sys.waits_global_by_latency LIMIT 10;","what":"정규화된 쿼리를 총 시간 기준으로 순위화하고 호출 횟수, 평균 및 p95 지연 시간, 전체 대비 비율, 호출당 검사/전송 행 수, 인덱스 없이 실행된 횟수, 디스크로 넘친 임시 테이블을 보여 준다. 두 번째 쿼리는 시간이 어떤 대기(IO, 락, redo)에 소비되는지 보여 준다.","when":"CPU/IO 부하가 오르거나 사용자가 '데이터베이스가 느리다'고 보고할 때 가장 먼저 확인한다. slow log가 꺼져 있어도 동작한다.","caution":"값은 마지막 재시작 또는 TRUNCATE TABLE performance_schema.events_statements_summary_by_digest 이후 누적값이다. 현재 활동을 보려면 스냅샷을 두 번 떠서 차이를 비교한다. 테이블은 performance_schema_digests_size 행으로 제한되며, 가득 차면 새 쿼리는 DIGEST가 NULL인 단일 행으로 집계된다. DIGEST_TEXT는 performance_schema_max_digest_length (1024) 바이트에서 잘린다. 시간 단위는 피코초다. rows_exam_per_call / rows_sent_per_call 비율이 높으면 인덱스 누락의 가장 명확한 신호다.","note":"QUANTILE_95 / QUANTILE_99 및 QUERY_SAMPLE_TEXT 컬럼은 8.0에서 도입되었다. 같은 데이터를 읽기 쉬운 단위로 sys.statement_analysis 및 sys.statements_with_full_table_scans 뷰에서 볼 수 있다.","since":"MySQL 8.0","tags":["performance_schema","digest","top sql","slow query","sys","wait"]},{"id":"sys-session-activity","task":"지금 실행 중인 작업: 활성 세션, 지연 시간, 락, 트랜잭션 (sys.session)","category":"성능","command":"SELECT conn_id, user, db, command, state, time,\n       LEFT(current_statement, 100) AS current_statement,\n       statement_latency, progress, lock_latency,\n       rows_examined, full_scan, tmp_disk_tables,\n       trx_state, trx_latency, last_wait\nFROM sys.session\nWHERE command <> 'Sleep' OR trx_state = 'ACTIVE'\nORDER BY time DESC\nLIMIT 25;","what":"SHOW PROCESSLIST에 performance_schema 데이터를 더해 실행 중인 문장의 지연 시간, 진행률, 락 대기 시간, 검사 행 수, 풀 스캔/디스크 임시 테이블 정보, 열린 트랜잭션 상태를 한 행에 보여 준다. Sleep 상태이지만 트랜잭션을 열어 둔 세션도 나열한다.","when":"장애 발생 후 처음 30초 동안: 어떤 쿼리가 얼마나 오래 실행 중인지, 누가 락을 기다리는지, 어떤 유휴 세션이 트랜잭션을 열어 두고 있는지 확인한다.","caution":"sys.session은 performance_schema에 대해 여러 조인을 수행하므로 연결이 수천 개면 쿼리 자체가 몇 초 걸릴 수 있다. 빈번한 모니터링에는 x$ 뷰와 LIMIT을 사용한다. progress는 stage instrument가 활성화되어 있고 ALTER처럼 진행률을 보고하는 작업에서만 채워진다. trx_state=ACTIVE인 Sleep 세션은 autocommit=0을 쓰는 애플리케이션/커넥션 풀이 잊어버린 트랜잭션이며, 메타데이터 락과 purge 문제의 가장 흔한 원인이다.","note":"sys.processlist는 백그라운드 스레드를 포함해 같은 데이터를 반환한다. 종료하려면: KILL QUERY conn_id (문장만) 또는 KILL conn_id (연결, 트랜잭션은 롤백됨).","since":"MySQL 8.0","tags":["sys.session","processlist","active sessions","incident","trx","session"]},{"id":"metadata-lock-diagnose","task":"'Waiting for table metadata lock' 진단 및 실제 블로커 찾기","category":"성능","command":"-- Waiter -> blocker pairs; filter out the ALTER's own SHARED_UPGRADABLE entry\nSELECT object_schema, object_name, waiting_pid, LEFT(waiting_query, 60) AS waiting_query,\n       waiting_lock_type, blocking_pid, blocking_lock_type, sql_kill_blocking_connection\nFROM sys.schema_table_lock_waits\nWHERE blocking_lock_type <> 'SHARED_UPGRADABLE';\n\n-- All MDLs on the table, owner age and open transaction\nSELECT ml.OBJECT_SCHEMA, ml.OBJECT_NAME, ml.LOCK_TYPE, ml.LOCK_DURATION, ml.LOCK_STATUS,\n       t.PROCESSLIST_ID AS conn_id, t.PROCESSLIST_COMMAND AS command, t.PROCESSLIST_TIME AS sec,\n       LEFT(t.PROCESSLIST_INFO, 60) AS query, trx.trx_started\nFROM performance_schema.metadata_locks ml\nJOIN performance_schema.threads t ON t.THREAD_ID = ml.OWNER_THREAD_ID\nLEFT JOIN information_schema.INNODB_TRX trx ON trx.trx_mysql_thread_id = t.PROCESSLIST_ID\nWHERE ml.OBJECT_TYPE = 'TABLE' AND ml.OBJECT_SCHEMA NOT IN ('performance_schema', 'mysql')\nORDER BY ml.OBJECT_NAME, ml.LOCK_STATUS DESC;","what":"ALTER / TRUNCATE / RENAME 같은 DDL이 어느 세션의 메타데이터 락을 기다리는지, 그리고 DDL 뒤에 대기 중인 쿼리를 보여 준다. 블로커는 보통 테이블을 읽고 트랜잭션을 닫지 않은 Sleep 상태 세션이다.","when":"ALTER 시작 후 해당 테이블의 모든 쿼리가 'Waiting for table metadata lock'에 쌓일 때.","caution":"대기 중인 DDL은 그 뒤에 도착한 SELECT도 막는다. 장애 중에는 빠르게 결정한다: 블로킹 세션을 KILL하거나 ALTER를 취소(KILL QUERY)한다. 블로커의 PROCESSLIST_INFO는 NULL(유휴)일 수 있으므로 마지막 문장은 performance_schema.events_statements_history 또는 sys.session.last_statement로 확인한다. 예방: ALTER가 빨리 포기하도록 SET SESSION lock_wait_timeout = 5 이후 DDL을 실행하고, 애플리케이션에서 긴 열린 트랜잭션을 피한다.","note":"wait/lock/metadata/sql/mdl instrument는 8.0에서 기본 활성화되어 있으며 5.7에서는 setup_instruments에서 활성화해야 한다. sys.schema_table_lock_waits는 ALTER 자신의 SHARED_UPGRADABLE 락도 블로커로 나열한다. 실제 원인은 GRANTED 상태의 SHARED_READ / SHARED_WRITE를 보유한 세션이다.","since":"MySQL 8.0","tags":["metadata lock","mdl","alter","waiting for table metadata lock","lock","blocking"]},{"id":"innodb-deadlock-analysis","task":"InnoDB 데드락 분석 및 전체 로깅","category":"성능","command":"-- Latest deadlock: LATEST DETECTED DEADLOCK section (only the most recent is kept)\nSHOW ENGINE INNODB STATUS\\G\n-- Total deadlock counter\nSELECT NAME, COUNT FROM information_schema.INNODB_METRICS WHERE NAME = 'lock_deadlocks';\n-- Write every deadlock to the error log (logged at Note level, needs verbosity 3)\nSET PERSIST innodb_print_all_deadlocks = ON;\nSET PERSIST log_error_verbosity = 3;","what":"데드락에 관련된 두 트랜잭션, 각자 보유하고 대기하는 락(인덱스, lock_mode, 레코드), InnoDB가 롤백한 쪽을 보여 준다. 카운터는 빈도를 추적하고 오류 로그는 과거 데드락을 보관한다.","when":"애플리케이션이 1213 'Deadlock found when trying to get lock' 오류를 받거나 데드락 알림이 증가할 때.","caution":"LATEST DETECTED DEADLOCK은 마지막 이벤트만 보관하며 재시작 시 사라진다. 'HOLDS THE LOCK(S)' / 'WAITING FOR THIS LOCK TO BE GRANTED' 줄의 인덱스 이름과 lock_mode(X, S, gap, insert intention)가 원인을 설명하고, 'WE ROLL BACK TRANSACTION (n)'이 희생자를 보여 준다. 1213 발생 시 애플리케이션은 트랜잭션 전체를 재시도해야 한다. 근본 해결: 일관된 순서로 행에 접근하고, 트랜잭션을 짧게 유지하며, 적절한 인덱스로 gap 락을 줄이고 필요한 곳에 READ COMMITTED를 사용한다. log_error_verbosity=3은 모든 Note 메시지도 활성화하므로 오류 로그 증가를 주시한다. Innodb_deadlocks 상태 변수는 MySQL Community에 없으므로 카운터는 INNODB_METRICS를 사용한다.","note":"동시성이 매우 높으면 데드락 감지 자체가 CPU 병목이 될 수 있다. innodb_deadlock_detect=OFF는 낮은 innodb_lock_wait_timeout과 함께일 때만 고려한다. 락 대기 타임아웃(1205)은 데드락이 아니므로 blocking-locks 레시피로 블로커를 찾는다.","since":"MySQL 8.0","tags":["deadlock","innodb","1213","innodb status","lock","error log"]},{"id":"long-trx-history-list","task":"장기 실행 트랜잭션과 history list length (purge 지연)","category":"성능","command":"-- Undo records waiting for purge (millions = trouble)\nSELECT NAME, COUNT AS history_list_length\nFROM information_schema.INNODB_METRICS WHERE NAME = 'trx_rseg_history_len';\n\n-- Oldest transactions, their owner and last SQL\nSELECT trx.trx_id, trx.trx_mysql_thread_id AS conn_id, trx.trx_state,\n       TIMESTAMPDIFF(SECOND, trx.trx_started, NOW()) AS age_sec,\n       trx.trx_rows_modified, trx.trx_rows_locked, trx.trx_isolation_level,\n       p.USER, p.HOST, p.COMMAND, p.TIME AS state_sec,\n       LEFT(COALESCE(trx.trx_query, esc.SQL_TEXT), 80) AS current_or_last_sql\nFROM information_schema.INNODB_TRX trx\nJOIN performance_schema.processlist p ON p.ID = trx.trx_mysql_thread_id\nLEFT JOIN performance_schema.threads th ON th.PROCESSLIST_ID = trx.trx_mysql_thread_id\nLEFT JOIN performance_schema.events_statements_current esc ON esc.THREAD_ID = th.THREAD_ID\nORDER BY trx.trx_started\nLIMIT 10;","what":"purge가 얼마나 뒤처졌는지(history list length)와 이를 붙잡고 있는 가장 오래된 트랜잭션의 소유자, 경과 시간, 수정한 행 수, 마지막 실행 SQL을 보여 준다.","when":"undo 테이블스페이스가 커질 때, 같은 쿼리가 시간이 지날수록 느려질 때(오래된 행 버전을 스캔), 또는 디스크 공간 알림 시.","caution":"가장 흔한 원인은 트랜잭션을 연 채 Sleep 상태인 세션이거나 REPEATABLE READ에서 잊힌 consistent snapshot(예: 방치된 mysqldump/리포트)이다. KILL 이후 롤백은 트랜잭션이 변경한 행만큼 시간이 걸린다. trx_rows_modified가 큰 트랜잭션을 종료하면 길고 IO가 많은 롤백이 발생하며, 재시작해도 단축되지 않는다. purge가 따라잡으면 undo는 자동으로 truncate된다(innodb_undo_log_truncate=ON).","note":"performance_schema.processlist는 8.0.22에서 도입되었으며 deprecated된 information_schema.PROCESSLIST를 대체한다. SHOW ENGINE INNODB STATUS의 'History list length'도 같은 값을 보여 준다.","since":"MySQL 8.0.22","tags":["history list length","purge","undo","long transaction","innodb_trx","idle in transaction"]},{"id":"slow-log-pt-query-digest","task":"짧은 전체 slow log를 수집해 pt-query-digest로 분석","category":"성능","command":"-- Temporary full capture (e.g. 10 minutes); only new connections are affected\nSET GLOBAL slow_query_log_file = '/var/lib/mysql/slow.log';\nSET GLOBAL log_slow_extra = ON;\nSET GLOBAL long_query_time = 0;\nSET GLOBAL slow_query_log = ON;\n-- ... then return to the normal threshold\nSET GLOBAL long_query_time = 1;\n\n# Shell: top 10 queries by total time\npt-query-digest --limit=10 --order-by=Query_time:sum --noversion-check /var/lib/mysql/slow.log > /tmp/digest.txt\npt-query-digest --since='2026-10-08 09:00:00' --until='2026-10-08 10:00:00' --filter '$event->{db} && $event->{db} eq \"shop\"' --report-format=profile --noversion-check /var/lib/mysql/slow.log","what":"long_query_time=0으로 짧은 구간 동안 모든 쿼리를 수집한다. pt-query-digest는 이를 fingerprint별로 그룹화하고 총/평균/p95 시간, 검사 행 수, 샘플 쿼리가 포함된 순위 리포트를 만든다.","when":"performance_schema digest로 부족할 때: 전체 문장 텍스트와 실제 파라미터를 이용한 분석, 배포 전후 비교, 다른 호스트에서의 오프라인 검토.","caution":"long_query_time은 세션 시작 시 전역 값에서 복사된다. 풀에 있는 기존 연결은 재연결하기 전까지 새 임계값을 보지 못한다. long_query_time=0은 바쁜 서버에서 초당 수천 건을 기록하므로 디스크 공간과 IO를 주시하고, 구간을 짧게 유지하며 반드시 원복한다. 분석용 파일은 운영 서버 밖으로 복사한다. pt-query-digest를 실행하는 사용자는 파일 읽기 권한이 필요하다(mode 640, mysql:mysql).","note":"log_slow_extra (8.0.14)는 Bytes_sent, Read_first / Read_next, Created_tmp_disk_tables 같은 추가 필드를 기록하며 pt-query-digest가 이를 리포트에 표시한다. 영구 활성화/비활성화는 enable-general-slow-log 레시피를 참고한다.","since":"MySQL 8.0.14","tags":["slow log","pt-query-digest","long_query_time","percona toolkit","query analysis","slow query"]},{"id":"explain-analyze-optimizer-trace","task":"EXPLAIN ANALYZE와 optimizer trace로 잘못된 실행 계획 디버깅","category":"성능","command":"-- Plan + actual time and rows (the query actually runs)\nEXPLAIN ANALYZE\nSELECT region, COUNT(*) FROM shop.big WHERE id < 20000 GROUP BY region;\n\n-- Why the optimizer chose this plan: optimizer trace\nSET SESSION optimizer_trace = 'enabled=on', SESSION optimizer_trace_max_mem_size = 1048576;\nSELECT * FROM shop.big WHERE region = 'TR' AND id > 100 LIMIT 5;\nSELECT JSON_PRETTY(JSON_EXTRACT(TRACE, '$**.considered_execution_plans')) AS plans,\n       MISSING_BYTES_BEYOND_MAX_MEM_SIZE\nFROM information_schema.OPTIMIZER_TRACE;\nSET SESSION optimizer_trace = 'enabled=off';","what":"EXPLAIN ANALYZE는 각 계획 단계별로 추정값(cost, rows)과 실제값(actual time, rows, loops)을 나란히 보여 준다. optimizer trace는 검토된 접근 경로, 비용, 각 경로가 선택 또는 거부된 이유를 보여 준다.","when":"쿼리가 갑자기 느려졌을 때(실행 계획 변경), 옵티마이저가 잘못된 인덱스를 고를 때, 또는 추정 행 수와 실제 행 수 불일치의 원인을 찾을 때.","caution":"EXPLAIN ANALYZE는 쿼리를 실제로 실행하므로 운영 환경의 무거운 쿼리에는 주의한다. SELECT, TABLE, 다중 테이블 UPDATE / DELETE에서 동작한다. 추정 행 수와 실제 행 수가 10배 이상 차이 나면 통계/히스토그램 문제(ANALYZE TABLE, 히스토그램)를 가리킨다. nested loop의 내부 쪽에서 actual rows는 루프당 값이며 총합 = rows × loops다. MISSING_BYTES_BEYOND_MAX_MEM_SIZE > 0이면 trace가 잘린 것이므로 optimizer_trace_max_mem_size를 늘린다. trace는 같은 세션의 마지막 문장만 보관한다.","note":"EXPLAIN FORMAT=TREE는 8.0.16, EXPLAIN ANALYZE는 8.0.18에서 도입되었다. 실행 계획 고정에는 optimizer hint(/*+ INDEX(t ix) */, JOIN_ORDER)가 FORCE INDEX보다 세밀한 제어를 제공한다.","since":"MySQL 8.0.18","tags":["explain analyze","optimizer trace","execution plan","query tuning","index","plan"]},{"id":"histogram-statistics","task":"인덱스가 없는 컬럼에 대한 히스토그램 생성 및 관리","category":"인덱스 및 통계","command":"ANALYZE TABLE shop.big UPDATE HISTOGRAM ON status WITH 64 BUCKETS;\n-- 8.4+: histogram refreshed automatically as data changes\nANALYZE TABLE shop.big UPDATE HISTOGRAM ON status WITH 64 BUCKETS AUTO UPDATE;\n\nSELECT SCHEMA_NAME, TABLE_NAME, COLUMN_NAME,\n       HISTOGRAM->>'$.\"histogram-type\"' AS type,\n       JSON_LENGTH(HISTOGRAM->'$.buckets') AS buckets,\n       HISTOGRAM->>'$.\"last-updated\"' AS last_updated,\n       HISTOGRAM->>'$.\"sampling-rate\"' AS sampling_rate\nFROM information_schema.COLUMN_STATISTICS;\n\nANALYZE TABLE shop.big DROP HISTOGRAM ON status;","what":"컬럼의 값 분포(singleton 또는 equi-height)를 데이터 딕셔너리에 저장한다. 옵티마이저가 인덱스 없는 컬럼 필터의 선택도를 정확히 추정하여 조인 순서와 실행 계획 품질이 개선된다.","when":"인덱스 추가가 바람직하지 않은 편향된 컬럼(예: status, type)에서 EXPLAIN ANALYZE가 추정 행 수와 실제 행 수의 큰 차이를 보일 때.","caution":"인덱스가 있는 컬럼에서는 옵티마이저가 index dive를 사용하므로 히스토그램은 보통 효과가 없다(eq_range_index_dive_limit 초과 시 제외). 기본 MANUAL 모드에서는 데이터가 바뀌어도 히스토그램이 갱신되지 않으므로 ANALYZE ... UPDATE HISTOGRAM을 스케줄링한다. 큰 테이블은 histogram_generation_max_mem_size에 따라 샘플링된다(sampling-rate < 1). ANALYZE TABLE은 짧은 메타데이터 락을 잡으며, 히스토그램 갱신은 테이블 통계(인덱스 카디널리티)를 바꾸지 않는다.","note":"히스토그램은 8.0에서 도입되었다. AUTO UPDATE 옵션은 8.4에서 추가되었으며 자동 통계 재계산과 함께 갱신된다. 최대 버킷 수는 1024이며, distinct 값이 버킷 수보다 적으면 singleton 히스토그램이 생성된다.","since":"MySQL 8.0","tags":["histogram","statistics","optimizer","analyze table","selectivity","cardinality"]},{"id":"invisible-redundant-index","task":"미사용/중복 인덱스 찾기, invisible 처리, 안전하게 삭제","category":"인덱스 및 통계","command":"-- Candidate 1: indexes unused since the last restart\nSELECT * FROM sys.schema_unused_indexes WHERE object_schema = 'shop';\n-- Candidate 2: redundant indexes that are a left prefix of another\nSELECT table_name, redundant_index_name, redundant_index_columns, dominant_index_name, sql_drop_index\nFROM sys.schema_redundant_indexes WHERE table_schema = 'shop';\n\n-- Make it invisible before dropping; revert instantly if anything regresses\nALTER TABLE shop.big ALTER INDEX ix_region INVISIBLE;\nSELECT INDEX_NAME, IS_VISIBLE FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'shop' AND TABLE_NAME = 'big' AND SEQ_IN_INDEX = 1;\n-- Try the invisible index for a single query\nEXPLAIN SELECT /*+ SET_VAR(optimizer_switch = 'use_invisible_indexes=on') */ COUNT(*) FROM shop.big WHERE region = 'TR';\nALTER TABLE shop.big ALTER INDEX ix_region VISIBLE;\n-- After a few business cycles (incl. month-end) without issues\nALTER TABLE shop.big DROP INDEX ix_region, ALGORITHM=INPLACE, LOCK=NONE;","what":"사용되지 않는 인덱스와 다른 인덱스에 포함되는 인덱스를 나열한다. 삭제 전에 인덱스를 invisible로 만들면 해당 인덱스 없이 운영 동작을 테스트하고 필요 시 즉시(재구축 없이) 되돌릴 수 있다.","when":"쓰기 비용, buffer pool 부담, 디스크 사용량을 줄이기 위해 인덱스를 정리할 때.","caution":"schema_unused_indexes는 마지막 재시작 이후 수집된 통계에만 의존하므로 월간 리포트처럼 드문 쿼리는 나타나지 않을 수 있다. replica가 서로 다른 워크로드를 처리한다면 서버별로 따로 확인한다. invisible 인덱스도 DML 시 유지되지만(쓰기 비용 유지) 옵티마이저는 무시한다. unique 인덱스는 invisible 상태에서도 유일성을 강제한다. PRIMARY KEY는 invisible로 만들 수 없다. FORCE INDEX / USE INDEX에 해당 인덱스를 명시한 쿼리는 invisible이 되면 실패한다.","note":"인덱스 가시성 전환은 메타데이터만 변경하며 즉시 완료된다. 중복 목록은 컬럼 순서를 기준으로 한다. 인덱스 (a)는 (a,b)에 포함되지만 (b)는 포함되지 않는다.","since":"MySQL 8.0","tags":["invisible index","unused index","redundant index","sys","drop index","index cleanup"]},{"id":"buffer-pool-sizing","task":"buffer pool 크기는 충분한가: 적중률, wait_free, 온라인 크기 조정","category":"메모리","command":"SELECT ROUND(@@innodb_buffer_pool_size / 1024 / 1024) AS bp_mb,\n       @@innodb_buffer_pool_chunk_size / 1024 / 1024 AS chunk_mb,\n       @@innodb_buffer_pool_instances AS instances;\nSELECT\n  ROUND(100 * (1 - (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads')\n                 / NULLIF((SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests'), 0)), 3) AS hit_pct,\n  (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_free') AS pages_free,\n  (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty') AS pages_dirty,\n  (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_wait_free') AS wait_free;\n\n-- Online resize and status tracking\nSET PERSIST innodb_buffer_pool_size = 8589934592;\nSELECT VARIABLE_NAME, VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME LIKE 'Innodb_buffer_pool_resize_status%';","what":"buffer pool 크기, 캐시 적중률, free/dirty 페이지 수, free 페이지 대기(wait_free) 카운터를 보여 준다. 재시작 없이 크기를 조정하고 진행 상황을 추적한다.","when":"읽기 IO가 높을 때, RAM 추가/제거 후, 또는 용량 계획 시.","caution":"적중률만으로는 오해할 수 있다. 99%라도 초당 수천 건의 디스크 읽기를 의미할 수 있다. Innodb_buffer_pool_reads의 초당 증가량을 본다. wait_free > 0이면 buffer pool이 너무 작거나 page cleaner가 따라가지 못하는 것이다. 새 크기는 innodb_buffer_pool_chunk_size × instances의 배수로 반올림된다. 크기 조정 중 짧은 정체가 발생할 수 있으며 축소 시 더 심하므로 피크 시간대를 피한다. 전용 서버에서는 RAM의 60–75%로 시작한다. 연결별 메모리, OS 캐시, 다른 프로세스를 위한 여유를 남긴다. 스와핑은 작은 buffer pool보다 항상 나쁘다.","note":"Innodb_buffer_pool_resize_status_code / _progress는 8.0.31에서 도입되었다. innodb_dedicated_server=ON은 RAM을 기준으로 buffer pool과 redo 용량을 자동으로 설정한다. sys.innodb_buffer_stats_by_table은 테이블별 buffer pool 사용량을 보여 주며, INNODB_BUFFER_PAGE를 스캔하므로 큰 buffer pool에서는 무겁다.","since":"MySQL 8.0","tags":["buffer pool","innodb","memory","hit ratio","resize","sizing"]},{"id":"partition-maintenance","task":"RANGE 파티션 유지보수: 미래 파티션 추가, 오래된 파티션 아카이브/삭제","category":"파티셔닝","command":"-- Current partitions\nSELECT PARTITION_NAME, PARTITION_DESCRIPTION, TABLE_ROWS\nFROM information_schema.PARTITIONS\nWHERE TABLE_SCHEMA = 'shop' AND TABLE_NAME = 'events_log' ORDER BY PARTITION_ORDINAL_POSITION;\n\n-- Add a new month by splitting the empty pmax\nALTER TABLE shop.events_log REORGANIZE PARTITION pmax INTO (\n  PARTITION p2026_11 VALUES LESS THAN ('2026-12-01'),\n  PARTITION pmax VALUES LESS THAN (MAXVALUE));\n\n-- Move the old month into an archive table (instant), then drop the empty partition\nCREATE TABLE shop.events_log_2026_08 LIKE shop.events_log;\nALTER TABLE shop.events_log_2026_08 REMOVE PARTITIONING;\nALTER TABLE shop.events_log EXCHANGE PARTITION p2026_08 WITH TABLE shop.events_log_2026_08;\nALTER TABLE shop.events_log DROP PARTITION p2026_08;\n\n-- Pruning check: the partitions column should list a single partition\nEXPLAIN SELECT * FROM shop.events_log WHERE created >= '2026-10-01' AND created < '2026-11-01';","what":"날짜 기반 RANGE 파티션 테이블에 다음 기간의 파티션을 추가하고, EXCHANGE로 오래된 기간을 밀리초 만에 별도 테이블로 옮기며, DELETE보다 훨씬 저렴하게 파티션을 삭제한다.","when":"로그, 이벤트, 감사 추적처럼 시간이 지나며 커지는 테이블의 월 단위 보존 및 아카이빙.","caution":"파티션 컬럼은 모든 unique key(PK 포함)에 포함되어야 한다. REORGANIZE PARTITION pmax는 pmax의 데이터를 다시 쓰므로 pmax를 비워 두고 파티션을 미리(예: 3개월) 만든다. DROP PARTITION은 되돌릴 수 없고 행 단위로 로깅되지 않으며 replica에서는 DDL로 실행된다. EXCHANGE 대상은 구조가 같고 파티션이 없어야 하며, 범위 밖의 행은 오류를 일으킨다(WITHOUT VALIDATION은 빠르지만 잘못된 데이터가 들어올 수 있음). 파티션 DDL은 테이블 수준 메타데이터 락을 잡는다. pruning을 위해 WHERE에서 파티션 컬럼을 함수로 감싸지 말고 사용한다.","note":"8.0에서 파티셔닝은 InnoDB(및 NDB)에서만 지원된다. 미래 파티션 추가/오래된 파티션 삭제는 보통 Event Scheduler 또는 cron으로 자동화한다. ALTER TABLE ... TRUNCATE PARTITION은 데이터를 지우지만 파티션은 유지한다.","since":"MySQL 8.0","tags":["partition","range","reorganize","exchange partition","retention","archive"]},{"id":"roles-least-privilege","task":"롤로 최소 권한 모델 구축 (CREATE ROLE, DEFAULT ROLE)","category":"보안","command":"CREATE ROLE IF NOT EXISTS 'app_read', 'app_write';\nGRANT SELECT ON shop.* TO 'app_read';\nGRANT INSERT, UPDATE, DELETE ON shop.* TO 'app_write';\n\n-- Application account: network range, TLS required, roles active by default\nCREATE USER 'svc_app'@'10.0.%' IDENTIFIED BY 'S3cure#App2024' DEFAULT ROLE 'app_read', 'app_write' REQUIRE SSL;\nCREATE USER 'ana'@'%' IDENTIFIED BY 'An4#Report2024';\nGRANT 'app_read' TO 'ana'@'%';\nSET DEFAULT ROLE ALL TO 'ana'@'%';\n\n-- Show grants with roles expanded\nSHOW GRANTS FOR 'svc_app'@'10.0.%' USING 'app_read', 'app_write';\nSELECT FROM_USER, FROM_HOST, TO_USER, TO_HOST FROM mysql.role_edges;","what":"개별 사용자 대신 롤에 권한을 부여하고 계정은 롤 멤버십으로 권한을 얻는다. 변경이 한 곳에서 이루어지고 감사와 퇴사자 처리가 쉬워진다.","when":"새 애플리케이션/팀 계정을 만들 때, 그리고 권한이 얽힌 레거시 서버를 정리할 때.","caution":"기본 롤이 아닌 롤은 세션에서 활성화되지 않으며(CURRENT_ROLE() NONE) 사용자에게 권한이 없는 것처럼 보인다. SET DEFAULT ROLE ALL 또는 activate_all_roles_on_login=ON을 사용한다. 롤도 계정(잠김, 비밀번호 없음)이며 mysql.user에 나타난다. 애플리케이션 계정에는 '%' 대신 네트워크 범위를 사용한다. 애플리케이션 계정에 ALL, SUPER, FILE, PROCESS, CONNECTION_ADMIN을 절대 부여하지 않는다. 롤에서 권한을 회수하면 활성 세션에도 영향을 준다.","note":"mandatory_roles는 모든 계정에 롤을 자동으로 할당한다. CREATE USER ... DEFAULT ROLE은 롤을 부여하는 동시에 기본 롤로 설정한다.","since":"MySQL 8.0","tags":["role","grant","least privilege","default role","security","privilege"]},{"id":"password-policy-lockout","task":"비밀번호 정책, 로그인 실패 잠금, 무중단 비밀번호 교체","category":"보안","command":"-- Password strength policy (component)\nINSTALL COMPONENT 'file://component_validate_password';\nSET PERSIST validate_password.policy = 'MEDIUM';\nSET PERSIST validate_password.length = 12;\nSELECT VALIDATE_PASSWORD_STRENGTH('Summer2024') AS score;\n\n-- Failed-login lockout, password lifetime, history\nALTER USER 'ana'@'%' FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LOCK_TIME 1\n  PASSWORD EXPIRE INTERVAL 90 DAY PASSWORD HISTORY 5 PASSWORD REUSE INTERVAL 365 DAY;\n\n-- Zero-downtime rotation: dual password\nALTER USER 'svc_app'@'10.0.%' IDENTIFIED BY 'N3w#AppSecret2025' RETAIN CURRENT PASSWORD;\n-- Once every application uses the new password\nALTER USER 'svc_app'@'10.0.%' DISCARD OLD PASSWORD;","what":"약한 비밀번호를 거부하는 정책을 활성화하고, 연속 로그인 실패 시 계정을 일시 잠그며, 비밀번호 수명과 재사용 규칙을 설정한다. dual password를 사용하면 애플리케이션을 하나씩 갱신하는 동안 서비스 계정 비밀번호를 무중단으로 변경한다.","when":"보안 감사/컴플라이언스(PCI, ISO 27001) 및 서비스 계정 비밀번호 교체 시.","caution":"정책은 새로 설정/변경된 비밀번호에만 적용되며 기존의 약한 비밀번호는 감지하지 않는다. 서비스 계정에서 FAILED_LOGIN_ATTEMPTS는 잘못 설정된 클라이언트 하나가 계정을 잠가 장애를 일으킬 수 있으므로 사람 계정에 사용한다. PASSWORD_LOCK_TIME 단위는 일이다(UNBOUNDED: 수동 해제 시까지). 해제는 ALTER USER ... ACCOUNT UNLOCK으로 한다. PASSWORD EXPIRE는 서비스 계정에서 예기치 않은 장애를 일으키므로 PASSWORD EXPIRE NEVER와 dual password 교체를 권장한다.","note":"dual password는 8.0.14, FAILED_LOGIN_ATTEMPTS / PASSWORD_LOCK_TIME은 8.0.19에서 도입되었다. 5.7의 validate_password 플러그인은 8.0에서 컴포넌트로 대체되었으며 변수 이름에 점을 사용한다(validate_password.policy).","since":"MySQL 8.0.19","tags":["password policy","validate_password","failed_login_attempts","dual password","security","password"]},{"id":"require-tls-transport","task":"TLS 강제, 비TLS 연결 찾기, 재시작 없이 인증서 갱신","category":"보안","command":"SELECT @@require_secure_transport, @@tls_version, @@ssl_ca, @@ssl_cert;\nSELECT * FROM performance_schema.tls_channel_status\nWHERE PROPERTY IN ('Enabled', 'Current_tls_version', 'Current_tls_cert', 'Ssl_server_not_after');\n\n-- Active connections and their TLS versions (empty = no TLS)\nSELECT t.PROCESSLIST_USER AS user, t.PROCESSLIST_HOST AS host, t.CONNECTION_TYPE, sv.VARIABLE_VALUE AS tls_version\nFROM performance_schema.threads t\nLEFT JOIN performance_schema.status_by_thread sv ON sv.THREAD_ID = t.THREAD_ID AND sv.VARIABLE_NAME = 'Ssl_version'\nWHERE t.TYPE = 'FOREGROUND' AND t.PROCESSLIST_USER IS NOT NULL;\n\nSET PERSIST require_secure_transport = ON;\n-- After copying the new certificate files\nALTER INSTANCE RELOAD TLS;","what":"서버 TLS 설정, 인증서 만료일, 각 연결의 TLS 사용 여부를 보여 준다. 암호화되지 않은 TCP 연결을 완전히 금지하고 서버를 멈추지 않고 인증서를 갱신한다.","when":"보안 감사/컴플라이언스, 인증서 만료 전, 암호화 없이 접속하는 레거시 클라이언트를 추적할 때.","caution":"require_secure_transport=ON은 Unix socket 및 shared memory 연결에 영향을 주지 않으며 이들은 안전한 것으로 간주된다. 활성화 전에 TLS 없이 접속하는 클라이언트를 찾아야 하며, 그렇지 않으면 3159 오류로 실패한다. ALTER INSTANCE RELOAD TLS는 기존 연결이 아닌 새 연결에만 적용된다. 인증서 파일이 유효하지 않으면 오류를 내고 기존 인증서를 유지한다. 자동 생성된 self-signed 인증서는 VERIFY_CA / VERIFY_IDENTITY를 사용하는 클라이언트가 검증할 수 없으므로 운영 환경에서는 자체 CA가 서명한 인증서를 사용한다.","note":"ALTER INSTANCE RELOAD TLS는 8.0.16, performance_schema.tls_channel_status는 8.0.21에서 도입되었다. 계정별 강제에는 ALTER USER ... REQUIRE SSL (또는 REQUIRE X509)을 사용한다.","since":"MySQL 8.0.21","tags":["tls","ssl","require_secure_transport","certificate","encryption","cert"]},{"id":"risky-accounts-audit","task":"위험 계정 감사: 빈 비밀번호, '%' 호스트, SUPER, mysql_native_password","category":"보안","command":"SELECT user, host, plugin,\n       CONCAT_WS(', ',\n         IF(authentication_string = '' AND account_locked = 'N', 'EMPTY PASSWORD', NULL),\n         IF(host = '%', 'ANY HOST', NULL),\n         IF(plugin = 'mysql_native_password', 'NATIVE AUTH', NULL),\n         IF(Super_priv = 'Y', 'SUPER', NULL),\n         IF(Grant_priv = 'Y', 'GRANT OPTION', NULL),\n         IF(File_priv = 'Y', 'FILE', NULL),\n         IF(ssl_type = '', 'NO TLS REQUIRED', NULL),\n         IF(password_expired = 'Y', 'EXPIRED', NULL),\n         IF(account_locked = 'Y', 'LOCKED', NULL)) AS findings,\n       password_last_changed\nFROM mysql.user\nWHERE user NOT IN ('mysql.sys', 'mysql.session', 'mysql.infoschema')\nORDER BY (Super_priv = 'Y') DESC, user;\n\n-- Accounts holding powerful dynamic privileges\nSELECT GRANTEE, PRIVILEGE_TYPE FROM information_schema.USER_PRIVILEGES\nWHERE PRIVILEGE_TYPE IN ('SUPER', 'SYSTEM_VARIABLES_ADMIN', 'CONNECTION_ADMIN', 'BINLOG_ADMIN', 'REPLICATION_SLAVE_ADMIN')\nORDER BY GRANTEE;","what":"계정별 보안 발견 사항을 한 행에 나열한다: 비밀번호 없음, 어디서나 접속 가능, 레거시 인증 플러그인, SUPER / GRANT / FILE 권한, TLS 요구 없음. 두 번째 쿼리는 위험한 동적 권한을 보여 준다.","when":"정기 보안 감사, 새로 인수한 서버, 8.4 업그레이드 전.","caution":"쿼리는 mysql.user를 직접 읽으므로 해당 테이블에 대한 SELECT 권한이 필요하다. 롤 계정은 비밀번호 없이 잠겨 있으므로 EMPTY PASSWORD는 잠기지 않은 계정에 대해서만 보고된다. Super_priv는 정적 SUPER 권한이며, 8.0부터 동적 권한으로 분리되었으므로 두 번째 쿼리도 확인한다. NATIVE AUTH로 표시된 계정은 8.4 이전에 caching_sha2_password로 전환해야 한다: ALTER USER 'u'@'h' IDENTIFIED WITH caching_sha2_password BY '...'; 오래된 클라이언트/커넥터는 caching_sha2_password를 지원하지 않을 수 있으므로 먼저 테스트한다.","note":"mysql_native_password는 8.0.34에서 deprecated되었고, 8.4에서는 기본 비활성화되었으며(mysql_native_password=ON으로 활성화 가능) 9.0에서 제거되었다.","since":"MySQL 8.0","tags":["security audit","mysql.user","mysql_native_password","super","privileges","audit"]},{"id":"upgrade-checker-84","task":"8.0 → 8.4 LTS 업그레이드 사전 점검 (util.checkForServerUpgrade)","category":"업그레이드","command":"# State the target version explicitly; the Shell must be the same version as the target or newer\nmysqlsh root@db1:3306 -- util check-for-server-upgrade --target-version=8.4.10 --output-format=TEXT\n# JSON output for automation, including a my.cnf check\nmysqlsh root@db1:3306 -- util check-for-server-upgrade --target-version=8.4.10 --output-format=JSON --config-path=/etc/my.cnf > /tmp/upgrade_check.json","what":"현재 서버를 대상 버전과 비교 점검한다: 제거된 변수와 권한, 기본값이 바뀐 파라미터, 레거시 인증 방식, 잘못된 컬럼 정의, 외래 키 및 파티션 문제. 오류 수준 발견 사항은 업그레이드를 차단한다.","when":"모든 메이저/LTS 업그레이드 전. 발견 사항을 수정한 후 다시 실행한다.","caution":"경고도 검토한다: 8.4에서는 innodb_io_capacity (10000), innodb_log_buffer_size (64 MB), innodb_adaptive_hash_index (OFF), innodb_change_buffering (none), innodb_buffer_pool_instances, group_replication_consistency 같은 기본값이 바뀌어 성능에 영향을 줄 수 있다. 현재 동작을 유지하려는 값은 my.cnf에 고정한다. SET_USER_ID 같은 제거된 권한은 업그레이드 중 삭제된다. 먼저 replica를 업그레이드해 부하 상태에서 관찰한 뒤 switchover한다. 8.4에서 8.0으로의 바이너리 다운그레이드는 지원되지 않으므로 대비책은 논리 백업 또는 이전 버전에 남겨 둔 replica다.","note":"8.4에서 제거됨: START / STOP SLAVE, SHOW SLAVE STATUS, CHANGE MASTER TO 같은 이전 복제 구문, default_authentication_plugin (authentication_policy로 대체), binlog_transaction_dependency_tracking, mysqlpump. mysql_native_password는 기본 비활성화된다.","since":"MySQL Shell 8.0","tags":["upgrade","8.4","checkforserverupgrade","mysqlsh","lts","version upgrade"]},{"id":"charset-utf8mb4-convert","task":"latin1 / utf8mb3 테이블을 utf8mb4로 변환","category":"유지보수","command":"-- Columns that are not utf8mb4\nSELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME, COLUMN_TYPE\nFROM information_schema.COLUMNS\nWHERE CHARACTER_SET_NAME IS NOT NULL AND CHARACTER_SET_NAME <> 'utf8mb4'\n  AND TABLE_SCHEMA NOT IN ('mysql', 'sys', 'information_schema', 'performance_schema')\nORDER BY TABLE_SCHEMA, TABLE_NAME;\n\n-- Generate conversion DDL for tables whose default charset is not utf8mb4\nSELECT CONCAT('ALTER TABLE `', t.TABLE_SCHEMA, '`.`', t.TABLE_NAME,\n              '` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, ALGORITHM=COPY, LOCK=SHARED;') AS ddl\nFROM information_schema.TABLES t\nJOIN information_schema.COLLATIONS c ON c.COLLATION_NAME = t.TABLE_COLLATION\nWHERE c.CHARACTER_SET_NAME <> 'utf8mb4' AND t.TABLE_TYPE = 'BASE TABLE'\n  AND t.TABLE_SCHEMA NOT IN ('mysql', 'sys', 'information_schema', 'performance_schema');\n\n-- Schema default and a single table\nALTER DATABASE legacy CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;\nALTER TABLE legacy.cust CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, ALGORITHM=COPY, LOCK=SHARED;","what":"utf8mb4가 아닌 컬럼과 테이블을 찾아 변환 DDL을 생성한다. CONVERT TO는 테이블 기본값과 모든 문자 컬럼을 함께 바꿔 이모지/4바이트 문자를 사용할 수 있게 한다.","when":"5.x에서 마이그레이션된 스키마, 'Incorrect string value' (1366) 오류, collation이 다른 컬럼 간 조인이 인덱스를 사용하지 못할 때.","caution":"CONVERT TO는 COPY로 테이블을 다시 쓰며 그동안 쓰기를 차단한다(INPLACE 미지원). 큰 테이블에서는 같은 --alter를 pt-online-schema-change / gh-ost로 실행한다. utf8mb4는 문자당 4바이트를 사용하므로 인덱스가 있는 VARCHAR(255)는 1020바이트가 된다. 한도는 DYNAMIC / COMPRESSED 행 포맷에서 3072바이트, COMPACT / REDUNDANT에서 767바이트이며 이를 넘으면 1071 오류가 발생한다. VARCHAR / TEXT 컬럼은 같은 문자 용량을 유지하기 위해 필요 시 더 큰 타입으로 승격된다(TEXT → MEDIUMTEXT). collation 변경은 정렬과 비교에 영향을 준다. utf8mb4_0900_ai_ci는 악센트와 대소문자를 구분하지 않으므로 이전에 달랐던 값이 unique 인덱스에서 충돌할 수 있다(1062). 조인되는 테이블은 함께 변환한다. 클라이언트 연결도 utf8mb4를 사용해야 한다.","note":"utf8 (utf8mb3)은 8.0에서 deprecated되었으며 8.0.30부터 SHOW CREATE TABLE에 utf8mb3로 표시된다. 터키어 collation 규칙(점 있는 I와 점 없는 I)에는 utf8mb4_tr_0900_ai_ci를 사용한다. 8.0 기본값은 utf8mb4 / utf8mb4_0900_ai_ci다.","since":"MySQL 8.0","tags":["utf8mb4","character set","collation","convert","emoji","charset"]}]},"recipes/mssql.json":{"engineId":"mssql","engine":"SQL Server","order":4,"recipes":[{"id":"backup-log","task":"트랜잭션 로그 백업 수행","category":"백업 및 로그","command":"BACKUP LOG [Customer]\nTO DISK = N'D:\\Backup\\Customer_log_20260706_1200.trn'\nWITH COMPRESSION, CHECKSUM, STATS = 10;","what":"트랜잭션 로그 백업을 수행하고 (FULL/BULK_LOGGED 복구 모델에서) 로그의 사용된 부분을 truncate하여 재사용할 수 있게 한다.","when":"FULL 복구 모델에서 정기적으로(예: 15분마다), 시점 복구 체인을 위해, 그리고 로그 파일이 커질 때.","caution":"FULL/BULK_LOGGED 복구 모델에서만 의미가 있다(SIMPLE에서는 로그가 자동 truncate됨). 로그 체인이 끊기지 않도록 먼저 FULL 백업이 최소 하나 있어야 한다. 백업 파일의 삭제/덮어쓰기 전략을 올바르게 설정한다.","tags":["backup log","log backup","transaction log","trn","recovery","backup"]},{"id":"backup-full-diff","task":"FULL 및 DIFFERENTIAL 백업 수행","category":"백업 및 로그","command":"-- Full backup:\nBACKUP DATABASE [Customer]\nTO DISK = N'D:\\Backup\\Customer_full_20260706.bak'\nWITH COMPRESSION, CHECKSUM, INIT, STATS = 10;\n\n-- Differential backup:\nBACKUP DATABASE [Customer]\nTO DISK = N'D:\\Backup\\Customer_diff_20260706.bak'\nWITH DIFFERENTIAL, COMPRESSION, CHECKSUM, INIT, STATS = 10;","what":"FULL은 데이터베이스 전체를 백업하고, DIFFERENTIAL은 마지막 FULL 이후 변경된 익스텐트를 백업한다.","when":"FULL: 일일/주간 기본 백업. DIFF: FULL 사이의 빠른 중간 백업.","caution":"DIFF를 복원하려면 기준 FULL 백업이 필요하다. INIT은 기존 파일을 덮어쓰므로 파일 이름이 잘못되면 백업을 잃는다. CHECKSUM은 손상을 조기에 발견한다.","tags":["backup","full","differential","differential backup","bak","backup"]},{"id":"log-reuse-wait","task":"로그 파일이 커진 이유 / truncate되지 않는 이유","category":"백업 및 로그","command":"SELECT name,\n       recovery_model_desc,\n       log_reuse_wait_desc\nFROM   sys.databases\nWHERE  name = N'Customer';\n\n-- Log space usage for all databases:\nSELECT DB_NAME(database_id) AS db, name, type_desc,\n       size*8/1024        AS size_mb,\n       CAST(FILEPROPERTY(name,'SpaceUsed') AS BIGINT)*8/1024 AS used_mb\nFROM   sys.master_files\nWHERE  type_desc = 'LOG';","what":"log_reuse_wait_desc 컬럼은 트랜잭션 로그를 truncate할 수 없는 이유를 알려준다(예: LOG_BACKUP, ACTIVE_TRANSACTION, REPLICATION, AVAILABILITY_REPLICA).","when":"로그 파일이 계속 커지거나 로그가 가득 찼을 때(9002 오류) 근본 원인을 찾기 위해.","caution":"LOG_BACKUP이 표시되면 해결책은 파일 축소가 아니라 로그 백업이다. 조회 전용이며 위험 없음.","tags":["log_reuse_wait_desc","log growth","sys.databases","log backup","recovery","diagnostics"]},{"id":"shrink-log","task":"로그 파일 축소","category":"백업 및 로그","command":"-- First take a log backup so the log can be truncated (FULL model):\nBACKUP LOG [Customer] TO DISK = N'D:\\Backup\\Customer_log_pre_shrink.trn' WITH COMPRESSION;\n\n-- Find the logical name of the log file:\nSELECT name, type_desc FROM sys.database_files WHERE type_desc = 'LOG';\n\n-- Shrink (target 2048 MB):\nUSE [Customer];\nDBCC SHRINKFILE (N'Customer_log', 2048);","what":"트랜잭션 로그 파일의 물리적 크기를 줄이고 공간을 운영체제에 반환한다.","when":"일회성 비정상 증가(대형 트랜잭션/인덱스 재구축) 후 로그가 계속 비대한 상태로 남아 있을 때.","caution":"위험/주의: 정기적인 축소는 나쁜 관행이다. 로그가 다시 커지는 동안 성능이 떨어지고 VLF 단편화가 증가한다. 먼저 로그 백업을 수행해야 크기가 줄어든다. 데이터 파일이 아니라 불필요하게 커진 로그 파일만 축소한다. 이후 한 번에 적절한 크기로 다시 늘린다.","tags":["shrink","dbcc shrinkfile","shrink log","log","vlf","caution"]},{"id":"vlf-count","task":"VLF (Virtual Log File) 수 확인","category":"백업 및 로그","command":"-- SQL Server 2016 SP2 / 2017 and later:\nSELECT DB_NAME(database_id) AS db,\n       COUNT(*)             AS vlf_count,\n       SUM(CASE WHEN vlf_active = 1 THEN 1 ELSE 0 END) AS active_vlf\nFROM   sys.dm_db_log_info(DB_ID(N'Customer'))\nGROUP BY database_id;\n\n-- For older versions: DBCC LOGINFO('Customer'); (row count = VLF count)","what":"트랜잭션 로그 내부의 가상 로그 파일(VLF) 수를 반환한다. VLF가 너무 많으면 시작/복구/로그 백업 성능이 저하된다.","when":"로그가 느리거나 데이터베이스 시작이 오래 걸릴 때, 로그 증가 설정을 검토할 때.","caution":"VLF가 수백/수천 개면 경고 신호다. 해결책은 로그를 축소한 뒤 적절히 큰 단위(예: 8GB씩)로 다시 늘리는 것이다. 조회 전용이며 위험 없음.","since":"SQL Server 2016 SP2","note":"sys.dm_db_log_info DMV는 SQL Server 2016 SP2 및 2017에서 도입되었다. 그 이전에는 문서화되지 않은 DBCC LOGINFO 출력의 행 수로 VLF를 셌다.","tags":["vlf","dm_db_log_info","dbcc loginfo","log","virtual log file","performance"]},{"id":"open-transaction","task":"가장 오래된 열린 트랜잭션 찾기 (ACTIVE_TRANSACTION)","category":"백업 및 로그","command":"USE [Customer];\nDBCC OPENTRAN;\n\n-- Detailed: show long-running open transactions and their owners:\nSELECT s.session_id, s.login_name, s.host_name, s.program_name,\n       t.transaction_id,\n       DATEDIFF(SECOND, t.transaction_begin_time, GETDATE()) AS duration_sec,\n       t.transaction_begin_time\nFROM   sys.dm_tran_active_transactions t\n  JOIN sys.dm_tran_session_transactions st ON st.transaction_id = t.transaction_id\n  JOIN sys.dm_exec_sessions s ON s.session_id = st.session_id\nORDER BY t.transaction_begin_time;","what":"가장 오래된 열린(커밋/롤백되지 않은) 트랜잭션과 이를 보유한 세션을 보여 준다. 열린 트랜잭션은 로그 truncate를 막는다(LOG_REUSE_WAIT = ACTIVE_TRANSACTION).","when":"로그 백업을 해도 로그가 줄지 않고 log_reuse_wait_desc = ACTIVE_TRANSACTION일 때 근본 원인을 찾기 위해.","caution":"긴 열린 트랜잭션을 보유한 세션을 KILL하기 전에 업무 담당자와 확인한다. 롤백이 오래 걸릴 수 있다. 조회 전용이며 위험 없음.","tags":["dbcc opentran","open transaction","active_transaction","dm_tran_active_transactions","log","diagnostics"]},{"id":"add-data-file","task":"데이터베이스에 데이터 파일 추가","category":"스토리지","command":"ALTER DATABASE [Customer]\nADD FILE (\n  NAME = N'Customer_data2',\n  FILENAME = N'E:\\Data\\Customer_data2.ndf',\n  SIZE = 4GB,\n  FILEGROWTH = 512MB,\n  MAXSIZE = 100GB\n) TO FILEGROUP [PRIMARY];","what":"기존 filegroup에 새 데이터 파일(.ndf)을 추가해 용량을 늘리고 I/O를 분산한다.","when":"디스크가 가득 찼을 때, 또는 여러 디스크에 I/O를 분산하려 할 때.","caution":"새 파일로의 쓰기는 proportional fill로 균형이 맞춰지며 가장 빈 파일이 먼저 채워진다. FILEGROWTH는 비율이 아닌 고정 MB로 지정한다. MAXSIZE를 설정하면 통제되지 않는 증가를 막는다.","tags":["add file","add data file","ndf","alter database","filegroup","storage"]},{"id":"add-filegroup","task":"filegroup 추가","category":"스토리지","command":"-- 1) Create the filegroup:\nALTER DATABASE [Customer] ADD FILEGROUP [FG_Archive];\n\n-- 2) Add a file to it:\nALTER DATABASE [Customer]\nADD FILE (\n  NAME = N'Customer_archive1',\n  FILENAME = N'F:\\Data\\Customer_archive1.ndf',\n  SIZE = 8GB, FILEGROWTH = 1GB\n) TO FILEGROUP [FG_Archive];\n\n-- (Optional) make it the default filegroup:\n-- ALTER DATABASE [Customer] MODIFY FILEGROUP [FG_Archive] DEFAULT;","what":"파일을 포함한 새 filegroup을 생성한다. 테이블/인덱스를 별도의 디스크/filegroup에 배치할 수 있다.","when":"아카이브/콜드 데이터 분리, filegroup 단위 백업/복원, I/O 분리를 위해.","caution":"빈 filegroup은 백업/사용할 수 없으므로 파일을 최소 하나 추가한다. 기본값을 변경하면 새 객체가 배치되는 위치에 영향을 준다.","tags":["filegroup","filegroup","add filegroup","alter database","ndf","storage"]},{"id":"modify-file-growth","task":"파일 크기 / 자동 증가 변경","category":"스토리지","command":"-- Grow (SIZE only grows; shrinking requires shrink, not a resize):\nALTER DATABASE [Customer]\nMODIFY FILE (NAME = N'Customer_data', SIZE = 20GB);\n\n-- Set autogrowth (fixed MB recommended):\nALTER DATABASE [Customer]\nMODIFY FILE (NAME = N'Customer_data', FILEGROWTH = 1GB, MAXSIZE = 200GB);\n\n-- Disable autogrowth:\nALTER DATABASE [Customer]\nMODIFY FILE (NAME = N'Customer_log', FILEGROWTH = 0);","what":"데이터/로그 파일을 미리 늘리거나 자동 증가 단위/상한을 설정한다.","when":"통제되지 않는 작은 증가 단위를 바로잡거나, 공간을 미리 할당하거나, MAXSIZE를 설정하기 위해.","caution":"비율 기반 자동 증가는 큰 파일에서 매우 큰 증가 단위를 만들므로 고정 MB를 사용한다. Instant File Initialization이 없으면 데이터 파일 증가가 블로킹된다. SIZE로는 파일을 줄일 수 없다(shrink 필요).","tags":["modify file","autogrowth","filegrowth","size","alter database","storage"]},{"id":"move-file","task":"데이터/로그 파일을 다른 위치로 이동","category":"스토리지","command":"-- 1) Tell SQL Server the new path (nothing is moved yet):\nALTER DATABASE [Customer]\nMODIFY FILE (NAME = N'Customer_data',\n             FILENAME = N'G:\\Data\\Customer_data.mdf');\n\n-- 2) Take the database offline:\nALTER DATABASE [Customer] SET OFFLINE WITH ROLLBACK IMMEDIATE;\n\n-- 3) Physically copy/move the file to the new path AT THE OPERATING SYSTEM level (G:\\Data\\...).\n\n-- 4) Bring it online:\nALTER DATABASE [Customer] SET ONLINE;","what":"데이터베이스 파일의 물리적 위치를 변경한다: 메타데이터에 새 경로를 정의하고, 데이터베이스가 오프라인인 동안 파일을 물리적으로 옮긴 뒤 데이터베이스를 온라인으로 전환한다.","when":"디스크를 교체하거나 공간 확보를 위해 다른 드라이브로 옮길 때.","caution":"OFFLINE + ROLLBACK IMMEDIATE는 모든 활성 연결을 끊으므로 다운타임을 계획한다. OS에서 파일을 새 경로로 실제로 복사하기 전에 데이터베이스를 ONLINE으로 전환하면 데이터베이스가 SUSPECT가 된다. tempdb는 다른 방법이 필요하다(MODIFY FILE + SQL Server 서비스 재시작).","tags":["move file","move file","modify file","offline","online","mdf","ldf","storage"]},{"id":"add-tempdb-file","task":"tempdb에 데이터 파일 추가","category":"스토리지","command":"-- Show current tempdb files:\nSELECT name, physical_name, size*8/1024 AS size_mb, growth\nFROM   sys.master_files WHERE database_id = DB_ID(N'tempdb');\n\n-- Add a new file (up to the number of CPU cores, equally sized):\nALTER DATABASE [tempdb]\nADD FILE (\n  NAME = N'tempdev2',\n  FILENAME = N'T:\\tempdb\\tempdb_mssql_2.ndf',\n  SIZE = 8GB, FILEGROWTH = 1GB\n);","what":"tempdb에 데이터 파일을 추가해 할당(PFS/GAM/SGAM) 경합을 줄이고 I/O를 분산한다.","when":"tempdb 경합(PAGELATCH_UP 대기)이 있거나 tempdb 사용량이 많은 시스템에서.","caution":"모든 tempdb 데이터 파일은 크기와 증가 단위가 같아야 한다(proportional fill을 위해). 추가된 파일은 재시작 없이 즉시 사용된다. 기존 파일 크기가 다르면 먼저 모두 맞춘다. 그렇지 않으면 새(빈) 파일이 불균형한 부하를 받는다. 파일 수는 보통 코어 수에 맞춘다(최대 8). SQL Server 2016+ 설치 시 기본으로 이렇게 구성된다.","tags":["tempdb","add file","tempdb file","ndf","contention","pagelatch","storage"]},{"id":"index-rebuild-reorg","task":"인덱스 rebuild / reorganize","category":"유지보수","command":"-- Rebuild (high fragmentation > 30%). ONLINE requires Enterprise:\nALTER INDEX IX_Customer_Email ON dbo.Customer\n  REBUILD WITH (ONLINE = ON, MAXDOP = 4, SORT_IN_TEMPDB = ON);\n\n-- All indexes of a table:\nALTER INDEX ALL ON dbo.Customer REBUILD WITH (FILLFACTOR = 90);\n\n-- Reorganize (moderate fragmentation 5-30%, always online, can be stopped):\nALTER INDEX IX_Customer_Email ON dbo.Customer REORGANIZE;","what":"REBUILD는 인덱스를 처음부터 다시 만들고(단편화 완전 제거, 통계 갱신), REORGANIZE는 리프 수준을 제자리에서 압축한다(가볍고 비블로킹).","when":"단편화 5-30%는 REORGANIZE, 30% 초과는 REBUILD. 큰 테이블은 유지보수 시간대에 수행한다.","caution":"ONLINE = ON은 Enterprise/Developer에서만 사용할 수 있으며 Standard에서는 OFF를 사용한다(테이블이 잠김). REBUILD는 로그를 많이 생성하고 공간이 필요하다. MAXDOP와 SORT_IN_TEMPDB는 리소스 사용량에 영향을 준다.","tags":["index","rebuild","reorganize","alter index","fragmentation","maintenance","online"]},{"id":"update-statistics","task":"통계 갱신","category":"유지보수","command":"-- Single table, full scan:\nUPDATE STATISTICS dbo.Customer WITH FULLSCAN;\n\n-- Specific statistic, with sampling:\nUPDATE STATISTICS dbo.Customer IX_Customer_Email WITH SAMPLE 30 PERCENT;\n\n-- Bulk for the whole database (user tables), sampled:\nEXEC sys.sp_updatestats;","what":"옵티마이저가 사용하는 컬럼/인덱스 통계를 갱신하여 올바른 실행 계획과 카디널리티 추정을 보장한다.","when":"대량 데이터 변경/적재 후, 실행 계획 회귀 시. 인덱스 rebuild는 이미 통계를 완전히 갱신한다(별도 불필요).","caution":"FULLSCAN은 정확하지만 비용이 크며 큰 테이블에서 I/O 부하를 만든다. sp_updatestats는 변경된 통계만 기본 샘플링으로 갱신한다. 인덱스 REBUILD 후 같은 인덱스에 UPDATE STATISTICS를 반복하는 것은 불필요하다.","tags":["update statistics","statistics","sp_updatestats","fullscan","optimizer","maintenance"]},{"id":"dbcc-checkdb","task":"무결성 점검 (DBCC CHECKDB)","category":"유지보수","command":"-- Full integrity check:\nDBCC CHECKDB (N'Customer') WITH NO_INFOMSGS, ALL_ERRORMSGS;\n\n-- Lighter on large databases (physical consistency):\nDBCC CHECKDB (N'Customer') WITH PHYSICAL_ONLY, NO_INFOMSGS;","what":"데이터베이스의 물리적/논리적 무결성(페이지 구조, 할당, 인덱스 일관성)을 점검하고 손상을 보고한다.","when":"정기 유지보수(주간), 하드웨어/IO 문제 의심 시, 823/824 오류 후.","caution":"I/O와 tempdb를 많이 사용하므로 유지보수 시간대에 실행한다. 오류가 발견되면 REPAIR_ALLOW_DATA_LOSS는 최후의 수단이어야 하며 데이터 손실을 일으킨다. 먼저 정상 백업에서 복원하는 것을 고려한다. PHYSICAL_ONLY는 더 빠르지만 논리 점검을 건너뛴다.","tags":["dbcc checkdb","integrity","corruption","corruption","integrity","maintenance"]},{"id":"index-fragmentation","task":"인덱스 단편화 확인","category":"유지보수","command":"SELECT OBJECT_SCHEMA_NAME(ips.object_id) AS schema_name,\n       OBJECT_NAME(ips.object_id)        AS table_name,\n       i.name                            AS index_name,\n       ips.index_type_desc,\n       ROUND(ips.avg_fragmentation_in_percent,1) AS frag_pct,\n       ips.page_count\nFROM   sys.dm_db_index_physical_stats(DB_ID(N'Customer'), NULL, NULL, NULL, 'LIMITED') ips\n  JOIN sys.indexes i\n    ON i.object_id = ips.object_id AND i.index_id = ips.index_id\nWHERE  ips.avg_fragmentation_in_percent > 10\n  AND  ips.page_count > 1000\nORDER BY ips.avg_fragmentation_in_percent DESC;","what":"인덱스의 단편화 비율과 페이지 수를 나열한다. rebuild/reorganize 결정의 근거가 된다.","when":"유지보수 전에 어떤 인덱스를 처리할지 결정하기 위해.","caution":"작은 인덱스(< 1000 페이지)에서는 단편화가 무의미하므로 필터링된다. 'DETAILED' 모드는 더 정확하지만 비용이 매우 크며 보통 'LIMITED'로 충분하다. 조회 전용이며 위험 없음.","tags":["fragmentation","dm_db_index_physical_stats","index","frag","maintenance","diagnostics"]},{"id":"kill-spid","task":"세션 종료 (SPID)","category":"세션","command":"-- First find the offending SPID:\nSELECT s.session_id, s.login_name, s.host_name, s.program_name,\n       r.status, r.command, r.wait_type, r.blocking_session_id,\n       DB_NAME(r.database_id) AS db\nFROM   sys.dm_exec_sessions s\n  LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id\nWHERE  s.is_user_process = 1;\n\n-- Kill (example SPID 63):\nKILL 63;\n\n-- Show rollback progress:\nKILL 63 WITH STATUSONLY;","what":"지정한 session_id (SPID)를 종료하며 열린 트랜잭션은 롤백된다.","when":"락을 보유하거나, 폭주/장기 쿼리를 실행하거나, 연결을 해제하지 않는 세션을 중지하기 위해.","caution":"큰 열린 트랜잭션에서는 롤백이 오래 걸릴 수 있으며 취소할 수 없다. KILL WITH STATUSONLY로 진행 상황을 모니터링한다. 시스템 SPID(is_user_process=0)는 종료하지 않는다. 올바른 SPID인지 확인한다.","tags":["kill","spid","kill session","dm_exec_sessions","dm_exec_requests","session"]},{"id":"blocking-chain","task":"블로킹(락) 체인 찾기","category":"세션","command":"SELECT r.session_id            AS waiting_spid,\n       r.blocking_session_id  AS blocking_spid,\n       r.wait_type,\n       r.wait_time / 1000.0   AS wait_sec,\n       r.status, r.command,\n       DB_NAME(r.database_id) AS db,\n       t.text                 AS waiting_query\nFROM   sys.dm_exec_requests r\n  CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t\nWHERE  r.blocking_session_id <> 0\nORDER BY r.wait_time DESC;\n\n-- To find the head (root) blocker of the chain, look for the SPID whose\n-- own blocking_session_id is not set (it is not blocked itself).","what":"대기 중인 세션과 이를 블로킹하는 세션을 대기 시간 및 실행 쿼리와 함께 나열하여 락 체인을 보이게 한다.","when":"\"애플리케이션이 멈춘다 / 타임아웃된다\"는 불만이 있을 때 락 경합의 원인을 찾기 위해.","caution":"루트 블로커(누구에게도 블로킹되지 않지만 다른 세션을 블로킹하는 SPID)를 종료하기 전에 업무 담당자와 확인한다. 조회 전용이며 위험 없음.","tags":["blocking","lock","lock","dm_exec_requests","blocking_session_id","session","chain"]},{"id":"active-queries","task":"활성 쿼리와 비용 확인","category":"세션","command":"SELECT r.session_id,\n       r.status,\n       r.command,\n       DB_NAME(r.database_id) AS db,\n       r.cpu_time,\n       r.total_elapsed_time / 1000 AS elapsed_sec,\n       r.reads, r.writes, r.logical_reads,\n       r.wait_type,\n       SUBSTRING(t.text,\n         (r.statement_start_offset/2)+1,\n         ((CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(t.text)\n            ELSE r.statement_end_offset END - r.statement_start_offset)/2)+1) AS running_statement\nFROM   sys.dm_exec_requests r\n  JOIN sys.dm_exec_sessions s ON s.session_id = r.session_id\n  CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t\nWHERE  s.is_user_process = 1 AND r.session_id <> @@SPID\nORDER BY r.total_elapsed_time DESC;","what":"현재 실행 중인 사용자 쿼리를 CPU, 경과 시간, 읽기/쓰기, 정확한 문장 텍스트와 함께 보여 준다.","when":"실시간 성능 저하 중 어떤 쿼리가 리소스를 소비하는지 찾기 위해.","caution":"statement_start/end_offset은 배치 전체가 아니라 실행 중인 정확한 문장을 추출한다. 조회 전용이며 위험 없음.","tags":["active queries","active query","dm_exec_requests","dm_exec_sql_text","cpu","session"]},{"id":"sp-configure-memory","task":"sp_configure로 설정 (max server memory)","category":"구성","command":"-- Make advanced options visible:\nEXEC sys.sp_configure 'show advanced options', 1;\nRECONFIGURE;\n\n-- Set max server memory (MB). Example: allow 24 GB:\nEXEC sys.sp_configure 'max server memory (MB)', 24576;\nRECONFIGURE;\n\n-- Verify:\nEXEC sys.sp_configure 'max server memory (MB)';","what":"SQL Server가 buffer pool에 사용할 수 있는 최대 메모리를 제한하여 운영체제와 다른 구성 요소를 위한 RAM을 남긴다.","when":"서버의 OS/다른 서비스가 메모리 부족을 겪거나 RAM이 새로 추가되었을 때.","caution":"운영체제, SSAS/SSIS, CLR, 스레드 스택을 위한 RAM을 충분히 남긴다(OS용으로 보통 최소 4-6 GB). 너무 낮게 설정하면 SQL Server 성능이 크게 저하된다. RECONFIGURE 없이는 변경이 적용되지 않는다. 이 설정은 동적이다(재시작 불필요).","tags":["sp_configure","max server memory","reconfigure","memory","configuration","memory"]},{"id":"reconfigure-general","task":"일반 sp_configure 설정 및 RECONFIGURE","category":"구성","command":"EXEC sys.sp_configure 'show advanced options', 1;\nRECONFIGURE;\n\n-- Example: cost threshold for parallelism and MAXDOP:\nEXEC sys.sp_configure 'cost threshold for parallelism', 50;\nEXEC sys.sp_configure 'max degree of parallelism', 4;\nRECONFIGURE;\n\n-- Show all configured/running values:\nSELECT name, value AS configured, value_in_use AS running, is_dynamic, is_advanced\nFROM   sys.configurations\nORDER BY name;","what":"서버 수준 구성 값을 설정하며 RECONFIGURE가 이를 실행 값에 적용한다.","when":"MAXDOP, 병렬 처리 임계값, optimize for ad hoc workloads 같은 서버 설정을 변경할 때.","caution":"is_dynamic = 0인 설정은 SQL Server 재시작이 필요하다. RECONFIGURE WITH OVERRIDE는 일부 설정에서 검증을 건너뛰므로 주의해서 사용한다. 변경 전에 value_in_use를 기록해 둔다.","tags":["sp_configure","reconfigure","maxdop","cost threshold","sys.configurations","configuration"]},{"id":"ag-failover","task":"Availability Group 페일오버","category":"구성","command":"-- Run on the TARGET (secondary) REPLICA - planned, no data loss (synchronous):\nALTER AVAILABILITY GROUP [AG_Customer] FAILOVER;\n\n-- Forced (asynchronous replica / primary unreachable, DATA LOSS risk):\n-- ALTER AVAILABILITY GROUP [AG_Customer] FORCE_FAILOVER_ALLOW_DATA_LOSS;\n\n-- Check status:\nSELECT ag.name, ar.replica_server_name,\n       rs.role_desc, rs.synchronization_health_desc, ar.availability_mode_desc\nFROM   sys.availability_groups ag\n  JOIN sys.availability_replicas ar ON ar.group_id = ag.group_id\n  JOIN sys.dm_hadr_availability_replica_states rs ON rs.replica_id = ar.replica_id;","what":"Availability Group의 primary 역할을 다른 replica로 옮긴다. 동기 replica로의 계획된 페일오버는 데이터 손실 없이 이루어진다.","when":"유지보수/패치를 위한 역할 이전, primary 노드 장애 시 페일오버.","caution":"일반 FAILOVER 명령은 역할을 넘겨받을 SECONDARY replica에서 실행한다. FORCE_FAILOVER_ALLOW_DATA_LOSS는 primary에 접근할 수 없거나 비동기 replica에서만 사용하며 데이터 손실을 일으킬 수 있다. 동기화가 정상이 아니면 매우 주의한다.","tags":["availability group","ag","failover","hadr","failover","alwayson","configuration"]},{"id":"change-recovery-model","task":"데이터베이스 복구 모델 변경","category":"구성","command":"-- Show the current model:\nSELECT name, recovery_model_desc FROM sys.databases WHERE name = N'Customer';\n\n-- FULL recovery (for point-in-time recovery):\nALTER DATABASE [Customer] SET RECOVERY FULL;\n\n-- SIMPLE recovery (log truncated automatically, no log backups needed):\n-- ALTER DATABASE [Customer] SET RECOVERY SIMPLE;\n\n-- BULK_LOGGED (temporary, for large bulk operations):\n-- ALTER DATABASE [Customer] SET RECOVERY BULK_LOGGED;","what":"데이터베이스 복구 모델을 변경한다. 로그 관리 방식과 사용 가능한 복구 옵션이 결정된다.","when":"FULL: 시점 복구/로그 백업/AG가 필요할 때. SIMPLE: 복구 시점이 필요 없는 시스템(dev/test, DW).","caution":"FULL로 전환한 후 로그 체인을 시작하기 위해 FULL 백업을 수행하고 정기 로그 백업을 설정한다. 그렇지 않으면 로그가 계속 커진다. FULL에서 SIMPLE로 전환하면 로그 체인이 끊어져 시점 복구를 잃는다. AG 멤버는 FULL 복구 모델이어야 한다.","tags":["recovery model","recovery full","simple","bulk_logged","alter database","configuration"]}]},"recipes/mssql-ext.json":{"engineId":"mssql","engine":"SQL Server","order":2,"recipes":[{"id":"ag-health-lag","task":"Availability Group 상태와 지연 모니터링 (log send / redo queue, RPO)","category":"Always On 및 HA","command":"-- Replica health (run on the primary to see all replicas):\nSELECT ag.name AS ag_name, ar.replica_server_name, ars.role_desc,\n       ar.availability_mode_desc, ar.failover_mode_desc,\n       ars.connected_state_desc, ars.synchronization_health_desc,\n       ars.operational_state_desc, ars.last_connect_error_description\nFROM   sys.availability_groups ag\n  JOIN sys.availability_replicas ar                ON ar.group_id = ag.group_id\n  JOIN sys.dm_hadr_availability_replica_states ars ON ars.replica_id = ar.replica_id\nORDER BY ag.name, ars.role_desc;\n\n-- Per-database lag: log send queue (RPO), redo queue (RTO):\nSELECT ag.name AS ag_name, ar.replica_server_name, DB_NAME(drs.database_id) AS db,\n       drs.is_primary_replica, drs.synchronization_state_desc, drs.synchronization_health_desc,\n       drs.is_suspended, drs.suspend_reason_desc,\n       drs.log_send_queue_size  AS log_send_queue_kb,\n       drs.log_send_rate        AS log_send_kb_s,\n       drs.redo_queue_size      AS redo_queue_kb,\n       drs.redo_rate            AS redo_kb_s,\n       CAST(drs.redo_queue_size * 1.0 / NULLIF(drs.redo_rate, 0) AS DECIMAL(18,1)) AS est_redo_catchup_s,\n       drs.last_commit_time,\n       DATEDIFF(SECOND, drs.last_commit_time, pri.last_commit_time) AS data_loss_window_s,\n       drs.secondary_lag_seconds\nFROM   sys.dm_hadr_database_replica_states drs\n  JOIN sys.availability_replicas ar ON ar.replica_id = drs.replica_id\n  JOIN sys.availability_groups ag   ON ag.group_id = drs.group_id\n  LEFT JOIN sys.dm_hadr_database_replica_states pri\n         ON pri.group_database_id = drs.group_database_id AND pri.is_primary_replica = 1\nORDER BY ag.name, db, drs.is_primary_replica DESC;\n\n-- WSFC quorum state:\nSELECT cluster_name, quorum_type_desc, quorum_state_desc FROM sys.dm_hadr_cluster;\nSELECT member_name, member_type_desc, member_state_desc, number_of_quorum_votes\nFROM   sys.dm_hadr_cluster_members;","what":"replica 연결 및 동기화 상태를 보여 준다. 데이터베이스별로 log send queue, redo queue, 전송/redo 속도, 예상 redo 따라잡기 시간, primary와의 마지막 커밋 차이(잠재적 데이터 손실 구간)를 보여 주며 WSFC quorum 상태도 반환한다.","when":"AG 대시보드에 NOT_HEALTHY / NOT SYNCHRONIZING 알림이 뜰 때, 계획된 페일오버 전 RPO/RTO를 확인할 때, 또는 사용자가 읽기 가능 secondary에서 오래된 데이터를 보고할 때.","caution":"증가하는 log_send_queue는 primary 장애 시 손실되는 로그 양(RPO)이고, 증가하는 redo_queue는 페일오버 후 데이터베이스가 온라인이 되기까지 걸리는 시간(RTO)이다. secondary에서는 로컬 replica만 보이므로 전체 상황은 primary에서 실행한다. data_loss_window_s는 쓰기 트래픽이 있을 때만 의미가 있다. 읽기 전용 쿼리이며 안전함.","since":"2016","note":"secondary_lag_seconds는 2016+ 컬럼이다. redo queue가 계속 증가하면 secondary의 redo 스레드가 블로킹되었는지(긴 읽기 쿼리, DDL) 확인하고 REDO_THREAD_PENDING_WORK / PARALLEL_REDO_* 대기를 찾는다.","tags":["availability group","ag","log send queue","redo queue","synchronization","rpo"],"untested":true},{"id":"ag-forced-failover-dr","task":"강제 페일오버 (FORCE_FAILOVER_ALLOW_DATA_LOSS) 및 페일오버 후 복구","category":"Always On 및 HA","command":"-- 1) On each candidate secondary: pick the most current one (highest end_of_log_lsn)\nSELECT ag.name AS ag_name, DB_NAME(drs.database_id) AS db,\n       drs.synchronization_state_desc, drs.end_of_log_lsn, drs.last_commit_time\nFROM   sys.dm_hadr_database_replica_states drs\n  JOIN sys.availability_groups ag ON ag.group_id = drs.group_id\nWHERE  drs.is_local = 1;\n\n-- 2) On the chosen replica (while the primary is unreachable):\nALTER AVAILABILITY GROUP [AG_Musteri] FORCE_FAILOVER_ALLOW_DATA_LOSS;\n\n-- 3) On the other secondaries, resume the suspended data movement:\nALTER DATABASE [Musteri] SET HADR RESUME;\n\n-- 4) When the old primary returns (now a SUSPENDED secondary):\n--    take a snapshot first to inspect the lost transactions, then resume\nCREATE DATABASE [Musteri_lost_snap]\n  ON (NAME = N'Musteri', FILENAME = N'D:\\Data\\Musteri_lost_snap.ss')\n  AS SNAPSHOT OF [Musteri];\nALTER DATABASE [Musteri] SET HADR RESUME;","what":"primary를 완전히 잃었을 때 데이터 손실을 감수하고 DR replica를 primary로 만든다. 이후 다른 replica에서 중단된 데이터 이동을 재개하고, 이전 primary가 돌아오면 새 primary에 도달하지 못한 트랜잭션을 검사하기 위해 스냅샷을 만든다.","when":"primary 데이터 센터가 다운되었거나, 비동기 replica만 남았거나, 동기 replica가 SYNCHRONIZED가 아닌데도 페일오버가 필요할 때.","caution":"데이터 손실: 마지막 hardened LSN 이후의 트랜잭션이 손실된다. 먼저 업무 측에서 RPO 승인을 받는다. 강제 페일오버 후 모든 secondary 데이터베이스는 SUSPENDED 상태가 되며, HADR RESUME 전까지 로그가 truncate되지 않고 계속 커진다. WSFC quorum을 잃었다면 먼저 forced quorum으로 클러스터를 시작한다(PowerShell: Start-ClusterNode -FixQuorum). 그렇지 않으면 명령이 실패한다. 이전 primary에서 HADR RESUME을 실행하면 손실된 트랜잭션이 영구 폐기되므로 스냅샷을 만들기 전에는 재개하지 않는다.","note":"데이터 손실 없는 계획된 페일오버에는 ag-failover 레시피의 FAILOVER 명령을 사용한다. CLUSTER_TYPE = NONE / EXTERNAL인 AG도 FORCE_FAILOVER_ALLOW_DATA_LOSS를 사용한다. EXTERNAL(Pacemaker)에서는 클러스터 관리자가 페일오버를 수행한다. 스냅샷에는 원본의 모든 논리 데이터 파일이 필요하므로 다중 파일 데이터베이스는 데이터 파일마다 ON 항목을 하나씩 추가한다.","tags":["force failover","allow data loss","dr","availability group","hadr resume","data loss"],"untested":true},{"id":"ag-add-db-autoseed","task":"AG에 데이터베이스 추가 (automatic seeding) 및 seeding 진행 모니터링","category":"Always On 및 HA","command":"-- Prerequisite (primary): FULL recovery and at least one FULL backup\nSELECT name, recovery_model_desc FROM sys.databases WHERE name = N'Musteri';\nBACKUP DATABASE [Musteri] TO DISK = N'D:\\Backup\\Musteri_pre_ag.bak' WITH COMPRESSION, CHECKSUM, INIT;\n\n-- 1) Primary: switch the replica to automatic seeding\nALTER AVAILABILITY GROUP [AG_Musteri]\n  MODIFY REPLICA ON N'SQL02' WITH (SEEDING_MODE = AUTOMATIC);\n\n-- 2) On each secondary: allow the AG to create databases\nALTER AVAILABILITY GROUP [AG_Musteri] GRANT CREATE ANY DATABASE;\n\n-- 3) Primary: add the database\nALTER AVAILABILITY GROUP [AG_Musteri] ADD DATABASE [Musteri];\n\n-- 4) Progress (primary or secondary):\nSELECT local_database_name, role_desc, internal_state_desc,\n       transferred_size_bytes / 1048576         AS transferred_mb,\n       database_size_bytes / 1048576            AS db_mb,\n       transfer_rate_bytes_per_second / 1048576 AS mb_s,\n       estimate_time_complete_utc, failure_message\nFROM   sys.dm_hadr_physical_seeding_stats;\n\n-- 5) Seeding history and failure reason:\nSELECT s.start_time, s.completion_time, adc.database_name, ar.replica_server_name,\n       s.current_state, s.failure_state_desc, s.error_code, s.number_of_attempts\nFROM   sys.dm_hadr_automatic_seeding s\n  JOIN sys.availability_databases_cluster adc ON adc.group_database_id = s.ag_db_id\n  JOIN sys.availability_replicas ar           ON ar.replica_id = s.ag_remote_replica_id\nORDER BY s.start_time DESC;","what":"수동 백업/복원 없이 엔드포인트(VDI)를 통해 데이터베이스를 secondary로 스트리밍하여 AG에 추가한다. seeding 처리량, 예상 완료 시점, 실패 사유를 보여 준다.","when":"AG에 새 데이터베이스를 추가할 때, 새 replica를 추가할 때, 또는 손상된 secondary 데이터베이스를 재구축할 때.","caution":"seeding은 엔드포인트 링크를 포화시키며 기본적으로 압축되지 않는다. 매우 큰 데이터베이스나 약한 WAN에서는 수동 백업/복원 + JOIN이 더 예측 가능하다. seeding 중에는 primary 로그가 truncate되지 않는다(log_reuse_wait_desc = AVAILABILITY_REPLICA). 2016에서는 secondary가 primary와 같은 데이터/로그 폴더 경로를 가져야 한다. 2017+에서는 primary 파일이 인스턴스 기본 경로에 있으면 secondary는 자신의 기본 경로를 사용한다. TDE 데이터베이스는 먼저 모든 secondary에 인증서를 복원해야 한다. secondary에 GRANT CREATE ANY DATABASE가 없으면 seeding이 대기 상태로 남는다.","since":"2016","note":"seeding 압축은 trace flag 9567로 활성화한다(CPU 비용 발생). seeding 시작, 완료, 실패는 SQL Server 오류 로그에 기록된다. 상세 추적에는 hadr_automatic_seeding_* 및 hadr_physical_seeding_* 이벤트로 별도의 Extended Events 세션을 만든다. SEEDING_MODE = MANUAL로 되돌린다.","tags":["automatic seeding","availability group","add database","seeding_mode","dm_hadr_physical_seeding_stats","ag"],"untested":true},{"id":"ag-listener-routing","task":"리스너, IP, 읽기 전용 라우팅 구성 확인","category":"Always On 및 HA","command":"-- Listener, port and IP states:\nSELECT ag.name AS ag_name, l.dns_name, l.port, l.is_conformant,\n       ip.ip_address, ip.ip_subnet_mask, ip.state_desc\nFROM   sys.availability_group_listeners l\n  JOIN sys.availability_groups ag ON ag.group_id = l.group_id\n  LEFT JOIN sys.availability_group_listener_ip_addresses ip ON ip.listener_id = l.listener_id;\n\n-- Read-only routing list: where read intent goes when X is primary:\nSELECT ag.name AS ag_name, pr.replica_server_name AS when_primary_is,\n       rl.routing_priority, sr.replica_server_name AS route_to,\n       sr.read_only_routing_url, sr.secondary_role_allow_connections_desc\nFROM   sys.availability_read_only_routing_lists rl\n  JOIN sys.availability_replicas pr ON pr.replica_id = rl.replica_id\n  JOIN sys.availability_replicas sr ON sr.replica_id = rl.read_only_replica_id\n  JOIN sys.availability_groups ag   ON ag.group_id = pr.group_id\nORDER BY ag.name, when_primary_is, rl.routing_priority;\n\n-- Replica endpoint and connection settings:\nSELECT replica_server_name, endpoint_url, read_only_routing_url,\n       secondary_role_allow_connections_desc, seeding_mode_desc, session_timeout\nFROM   sys.availability_replicas;\n\n-- Routing test from the command line (-d is required with -K ReadOnly):\n-- sqlcmd -S tcp:AGL-Musteri,1433 -d Musteri -E -M -K ReadOnly -Q \"SELECT @@SERVERNAME\"","what":"리스너 DNS 이름, 포트, 각 서브넷 IP의 클러스터 상태를 보여 준다. 읽기 전용 라우팅 목록의 각 primary 시나리오에서 read-intent 연결이 어떤 replica와 URL로 라우팅되는지 보여 준다.","when":"페일오버 후 애플리케이션이 연결하지 못할 때, ApplicationIntent=ReadOnly 연결이 primary로 갈 때, 또는 새 서브넷/replica가 추가되었을 때.","caution":"읽기 전용 라우팅은 ApplicationIntent=ReadOnly와 명시적 데이터베이스로 리스너를 통해 연결한 경우에만 동작한다. 인스턴스 이름으로 직접 연결하면 라우팅되지 않는다. read_only_routing_url이 비어 있는 replica는 라우팅 대상이 될 수 없다. 다중 서브넷 리스너에서 MultiSubnetFailover=True가 없는 클라이언트는 페일오버 후 20-30초 연결 타임아웃을 겪는다. 읽기 전용 쿼리이며 안전함.","note":"2016+ 부하 분산에서는 라우팅 목록에 중첩 괄호를 사용한다: READ_ONLY_ROUTING_LIST = (('SQL02','SQL03'),'SQL01'). 리스너 IP가 OFFLINE으로 표시되면 클러스터 측 리소스 종속성을 확인한다(Failover Cluster Manager / Get-ClusterResource).","tags":["listener","read-only routing","applicationintent","multisubnetfailover","availability group","connection"],"untested":true},{"id":"logshipping-monitor","task":"로그 전달(log shipping) 상태와 지연 모니터링","category":"Always On 및 HA","command":"-- Summary (on the primary, secondary or monitor server):\nEXEC master.sys.sp_help_log_shipping_monitor;\n\n-- PRIMARY: when the last log backup was taken:\nSELECT p.primary_server, p.primary_database,\n       p.last_backup_date, p.last_backup_file,\n       DATEDIFF(MINUTE, p.last_backup_date, GETDATE()) AS backup_age_min,\n       p.backup_threshold                              AS threshold_min\nFROM   msdb.dbo.log_shipping_monitor_primary p;\n\n-- SECONDARY: copy and restore latency:\nSELECT s.secondary_server, s.secondary_database, s.primary_server,\n       s.last_copied_date, s.last_copied_file,\n       s.last_restored_date, s.last_restored_file,\n       s.last_restored_latency                           AS restore_latency_min,\n       DATEDIFF(MINUTE, s.last_restored_date, GETDATE()) AS since_last_restore_min,\n       s.restore_threshold                               AS threshold_min\nFROM   msdb.dbo.log_shipping_monitor_secondary s;\n\n-- Errors in the last 24 hours:\nSELECT e.database_name, e.agent_type, e.log_time, e.message\nFROM   msdb.dbo.log_shipping_monitor_error_detail e\nWHERE  e.log_time >= DATEADD(HOUR, -24, GETDATE())\nORDER BY e.log_time DESC;\n\n-- Secondary DB mode (RESTORING = NORECOVERY, is_in_standby = STANDBY):\nSELECT name, state_desc, is_in_standby FROM sys.databases WHERE name = N'Musteri';","what":"primary의 마지막 로그 백업 경과 시간, secondary의 마지막 복사/복원 파일 및 복원 지연을 임계값과 함께 보여 주고, 최근 24시간의 백업/복사/복원 작업 오류를 보여 준다.","when":"log shipping 알림 작업이 실행될 때(오류 14420/14421), DR 테스트 전, 또는 secondary가 얼마나 뒤처졌는지 보고할 때.","caution":"모니터 테이블에는 서버가 보는 쪽만 있다: primary 테이블은 primary/monitor에, secondary 테이블은 secondary/monitor에 채워진다. 수동(log shipping이 아닌) 로그 백업은 체인을 끊어 복원 작업이 계속 실패하므로 임시 로그 백업에는 COPY_ONLY를 사용한다. STANDBY 모드에서는 각 복원 전에 사용자 연결이 끊긴다. 읽기 전용 쿼리이며 안전함.","note":"날짜 컬럼은 서버 로컬 시간이다. 시간대가 다른 서버를 비교할 때는 *_utc 컬럼을 사용한다. 복원 작업이 \"skipping log backup file\"을 기록하면 해당 파일은 이미 적용된 것이다. 실제 공백을 찾으려면 secondary에서 RESTORE HEADERONLY로 FirstLSN/LastLSN 순서를 확인한다.","tags":["log shipping","sp_help_log_shipping_monitor","restore latency","dr","secondary","latency"],"untested":true},{"id":"backup-copy-only","task":"체인을 끊지 않는 임시 백업 수행 (COPY_ONLY, 스트라이핑, 처리량)","category":"백업 및 로그","command":"-- Ad-hoc FULL that does not reset the differential base:\nBACKUP DATABASE [Musteri]\nTO DISK = N'D:\\Backup\\Musteri_copyonly_20261008.bak'\nWITH COPY_ONLY, COMPRESSION, CHECKSUM, INIT, STATS = 10;\n\n-- Ad-hoc log backup that does not truncate the log:\nBACKUP LOG [Musteri]\nTO DISK = N'D:\\Backup\\Musteri_copyonly_20261008.trn'\nWITH COPY_ONLY, COMPRESSION, CHECKSUM, INIT;\n\n-- Large DB: stripe across files and tune buffers:\nBACKUP DATABASE [Musteri]\nTO DISK = N'D:\\Backup\\Musteri_s1.bak', DISK = N'E:\\Backup\\Musteri_s2.bak'\nWITH COPY_ONLY, COMPRESSION, CHECKSUM, INIT,\n     MAXTRANSFERSIZE = 4194304, BUFFERCOUNT = 64, STATS = 5;","what":"정규 백업 체인(differential 기준 및 로그 체인)에 영향을 주지 않는 독립 FULL 또는 로그 백업을 수행한다. 큰 데이터베이스는 여러 파일로 스트라이핑하고 전송 크기를 키우면 백업 시간이 단축된다.","when":"test/dev에 사본을 넘길 때, 위험한 변경 전, 감사용 백업을 요청받았을 때 등 예약된 백업 전략을 건드리지 않고 백업이 필요할 때.","caution":"COPY_ONLY 없는 임시 FULL은 이후 DIFF 백업의 기준이 된다. 해당 파일이 삭제되면 DIFF는 쓸모없어진다. COPY_ONLY는 DIFFERENTIAL과 함께 사용하면 효과가 없다. 스트라이핑된 백업을 복원하려면 모든 파일이 필요하며 하나라도 잃으면 전체 백업을 사용할 수 없다. BUFFERCOUNT x MAXTRANSFERSIZE만큼의 메모리가 buffer pool 밖에 할당되므로(예시에서 256 MB) 메모리 압박이 있는 서버에서는 낮춘다.","note":"TDE 데이터베이스의 백업 압축에는 65536보다 큰 MAXTRANSFERSIZE가 필요하다(2016+). 서버 기본값: sp_configure 'backup compression default' 및 'backup checksum default'. Intel QAT 하드웨어가 있는 2022에서는 WITH COMPRESSION (ALGORITHM = QAT_DEFLATE)을 사용할 수 있다.","tags":["copy_only","ad-hoc backup","stripe","maxtransfersize","buffercount","backup"]},{"id":"backup-to-url","task":"URL로 백업 (Azure Blob SAS / S3 호환 스토리지)","category":"백업 및 로그","command":"-- A) Azure Blob: SAS credential (named after the container URL, SECRET without a leading '?')\nCREATE CREDENTIAL [https://dbabackup.blob.core.windows.net/sql]\nWITH IDENTITY = N'SHARED ACCESS SIGNATURE',\n     SECRET   = N'sv=2022-11-02&ss=b&srt=co&sp=rwdlac&se=2027-01-01T00:00:00Z&sig=XXXX';\n\nBACKUP DATABASE [Musteri]\nTO URL = N'https://dbabackup.blob.core.windows.net/sql/Musteri_full_1.bak',\n   URL = N'https://dbabackup.blob.core.windows.net/sql/Musteri_full_2.bak'\nWITH COMPRESSION, CHECKSUM, MAXTRANSFERSIZE = 4194304, BLOCKSIZE = 65536, STATS = 10;\n\n-- B) S3-compatible storage (2022+):\nCREATE CREDENTIAL [s3://s3.eu-central-1.amazonaws.com/dba-backup/sql]\nWITH IDENTITY = N'S3 Access Key',\n     SECRET   = N'<AccessKeyID>:<SecretKeyID>';\n\nBACKUP DATABASE [Musteri]\nTO URL = N's3://s3.eu-central-1.amazonaws.com/dba-backup/sql/Musteri_full.bak'\nWITH COMPRESSION, CHECKSUM, FORMAT, STATS = 10;\n\n-- Verify (uses the same credential):\nRESTORE VERIFYONLY\nFROM URL = N's3://s3.eu-central-1.amazonaws.com/dba-backup/sql/Musteri_full.bak'\nWITH CHECKSUM;","what":"백업을 오브젝트 스토리지에 직접 기록한다: SAS 자격 증명으로 Azure Blob에 block blob으로, 또는 2022에서는 access key 자격 증명으로 S3 API 호환 스토리지(AWS S3, MinIO 등)에 기록한다. 자격 증명은 URL 접두사 일치로 자동 선택된다.","when":"오프사이트 백업 요구 사항, 클라우드 마이그레이션/하이브리드 DR 시나리오, 또는 로컬 백업 스토리지가 부족할 때.","caution":"Azure에서 단일 block blob은 최대 50,000 블록을 가지며 4 MB MAXTRANSFERSIZE에서는 파일당 약 195 GB이므로 큰 데이터베이스는 여러 URL로 스트라이핑한다. S3에서 파일은 최대 10,000 파트를 가지므로 큰 데이터베이스는 MAXTRANSFERSIZE를 늘리고(최대 20 MB, COMPRESSION 필요) 여러 URL(최대 64)로 스트라이핑한다. S3는 INIT/append를 지원하지 않으며 FORMAT은 같은 이름의 기존 백업을 덮어쓴다. SAS가 만료되면 백업 작업이 오류로 실패하므로 만료일을 일정에 기록한다. 자격 증명 secret은 master에 저장되며 CONTROL SERVER 권한을 가진 principal이 사용할 수 있다.","since":"2022","note":"SAS를 사용한 block blob 백업은 2016+, S3 지원은 2022+이다. 레거시 스토리지 계정 키 방식(IDENTITY = 계정 이름)은 page blob을 만들며 권장되지 않는다. S3의 경우 엔드포인트 TLS 인증서를 서버가 신뢰해야 한다. MinIO 같은 온프레미스 솔루션은 포트를 URL에 넣는다(s3://minio.local:9000/bucket/...).","tags":["backup to url","azure blob","s3","credential","sas","cloud backup"],"untested":true},{"id":"backup-history-msdb","task":"msdb의 백업 이력 및 누락 백업 점검","category":"백업 및 로그","command":"-- Last FULL / DIFF / LOG per database (never backed up first):\nSELECT d.name AS db,\n       d.recovery_model_desc,\n       MAX(CASE WHEN b.type = 'D' THEN b.backup_finish_date END) AS last_full,\n       MAX(CASE WHEN b.type = 'I' THEN b.backup_finish_date END) AS last_diff,\n       MAX(CASE WHEN b.type = 'L' THEN b.backup_finish_date END) AS last_log,\n       DATEDIFF(MINUTE, MAX(CASE WHEN b.type = 'L' THEN b.backup_finish_date END), GETDATE()) AS log_age_min\nFROM   sys.databases d\n  LEFT JOIN msdb.dbo.backupset b\n         ON b.database_name = d.name AND b.is_copy_only = 0\nWHERE  d.name <> N'tempdb'\nGROUP BY d.name, d.recovery_model_desc\nORDER BY last_full;\n\n-- Chain for one DB over the last 7 days (LSN, size, file):\nSELECT b.backup_start_date, b.backup_finish_date, b.type, b.is_copy_only,\n       b.first_lsn, b.last_lsn, b.database_backup_lsn,\n       CAST(b.backup_size / 1048576.0 AS DECIMAL(12,1))            AS size_mb,\n       CAST(b.compressed_backup_size / 1048576.0 AS DECIMAL(12,1)) AS compressed_mb,\n       b.has_backup_checksums, mf.physical_device_name\nFROM   msdb.dbo.backupset b\n  JOIN msdb.dbo.backupmediafamily mf ON mf.media_set_id = b.media_set_id\nWHERE  b.database_name = N'Musteri'\n  AND  b.backup_start_date >= DATEADD(DAY, -7, GETDATE())\nORDER BY b.backup_start_date;","what":"모든 데이터베이스의 마지막 FULL/DIFF/LOG 백업 시각과 로그 백업 경과 시간을 나열한다. 단일 데이터베이스에 대해서는 LSN 순서, 압축 크기, checksum 플래그, 파일 경로를 포함한 백업 체인을 보여 준다.","when":"아침 점검에서, 복원에 필요한 파일을 찾을 때, 감사에서 \"마지막 백업은 언제였나\"에 답할 때, 또는 log-full 알림 중 로그 백업이 실행 중인지 확인할 때.","caution":"msdb는 이 인스턴스가 수행한 백업만 알고 있다. 다른 서버나 다른 AG replica에서 수행한 백업은 보이지 않는다(AG에서는 모든 replica를 확인). 로그 백업의 first_lsn은 이전 백업의 last_lsn과 같아야 하며, 차이가 있으면 체인이 끊어진 것이다. FULL 복구 모델에서 last_log가 NULL이면 로그가 한 번도 백업되지 않은 것이며 계속 커진다. 읽기 전용 쿼리이며 안전함.","note":"msdb 이력은 무한히 커지며 백업 작업을 느리게 하므로 EXEC msdb.dbo.sp_delete_backuphistory @oldest_date = '2026-01-01';로 정기적으로 정리한다. type 값: D = FULL, I = DIFF, L = LOG, F = 파일/filegroup.","tags":["backupset","msdb","backup history","last backup","lsn","backup chain"]},{"id":"backup-restore-progress","task":"백업 / 복원 / CHECKDB 진행률 및 예상 완료 시간 확인","category":"백업 및 로그","command":"SELECT r.session_id, r.command, DB_NAME(r.database_id) AS db,\n       CAST(r.percent_complete AS DECIMAL(5,1))                     AS pct,\n       DATEADD(SECOND, r.estimated_completion_time / 1000, GETDATE()) AS eta,\n       r.total_elapsed_time / 60000                                 AS elapsed_min,\n       r.wait_type, r.wait_time, r.blocking_session_id\nFROM   sys.dm_exec_requests r\nWHERE  r.percent_complete > 0;\n\n-- Restore phases and throughput are in the error log:\nEXEC sys.xp_readerrorlog 0, 1, N'Restore';","what":"percent_complete를 보고하는 장기 작업(BACKUP, RESTORE, DBCC CHECKDB/SHRINK, ALTER INDEX REORGANIZE, KILL 후 롤백)의 진행률, 예상 완료 시각, 현재 대기를 보여 준다.","when":"야간 백업이 업무 시간까지 이어질 때, 대규모 복원 중 \"언제 끝나나\"에 답할 때, 또는 종료된 세션의 롤백을 추적할 때.","caution":"추정치는 선형 진행을 가정한다. 복원의 redo/undo 단계와 로그 zero-initialization 중에는 진행률이 오래 멈춰 있을 수 있다. 로그 파일은 instant file initialization의 혜택을 받지 못하므로(2022에서 64 MB까지의 증가는 예외) 로그가 큰 복원은 첫 단계에서 오래 걸린다. 읽기 전용 쿼리이며 안전함.","note":"wait_type이 주로 BACKUPIO / ASYNC_IO_COMPLETION이면 병목은 대상/원본 디스크다. BACKUPTHREAD나 CPU가 높으면 압축 비용을 가리킨다. 복원의 긴 zeroing 단계를 오류 로그에서 자세히 보려면 trace flag 3004 + 3605를 사용한다.","tags":["percent_complete","backup progress","restore progress","eta","dm_exec_requests","progress"]},{"id":"tail-log-backup","task":"복원 전 tail-log 백업 수행 (NORECOVERY / NO_TRUNCATE)","category":"복원 및 복구","command":"-- A) Healthy DB: back up the tail and put the DB into RESTORING (no new writes):\nBACKUP LOG [Musteri]\nTO DISK = N'D:\\Backup\\Musteri_tail.trn'\nWITH NORECOVERY, CHECKSUM, INIT;\n\n-- B) Damaged DB / data file lost, log file intact:\n-- BACKUP LOG [Musteri]\n-- TO DISK = N'D:\\Backup\\Musteri_tail_damaged.trn'\n-- WITH NO_TRUNCATE, CONTINUE_AFTER_ERROR, INIT;\n\nSELECT name, state_desc FROM sys.databases WHERE name = N'Musteri';","what":"마지막 로그 백업 이후 기록된 로그 부분(tail)을 백업하여 복원 체인이 장애 시점까지 도달하고 마지막 몇 분의 트랜잭션을 잃지 않게 한다.","when":"같은 데이터베이스 위에 복원하기 전, 데이터 파일 디스크 장애 시, 또는 마이그레이션의 최종 전환 시.","caution":"NORECOVERY는 데이터베이스를 RESTORING 상태로 만들고 사용자 접근을 종료하므로 잘못된 데이터베이스에서 실행하지 않는다. tail-log 백업 없이 WITH REPLACE로 복원하면 마지막 로그 백업 이후의 트랜잭션이 영구 폐기된다. NO_TRUNCATE로 수행한 백업은 메타데이터가 불완전할 수 있으므로 복원 후 DBCC CHECKDB를 실행한다. SIMPLE 복구 모델에서는 tail-log 백업이 불가능하다.","note":"복원 없이 데이터베이스를 RESTORING에서 되돌리려면: RESTORE DATABASE [Musteri] WITH RECOVERY;. NO_TRUNCATE는 COPY_ONLY + CONTINUE_AFTER_ERROR와 같으며 데이터베이스를 열 수 없어도 로그 읽기를 시도한다.","tags":["tail-log","backup log","norecovery","no_truncate","restore","recovery"]},{"id":"restore-pitr-stopat","task":"특정 시점으로 복원 (point-in-time restore, STOPAT)","category":"복원 및 복구","command":"-- Restore under a side name, keep the original intact:\nRESTORE DATABASE [Musteri_PITR]\nFROM DISK = N'D:\\Backup\\Musteri_full_20261008.bak'\nWITH MOVE N'Musteri'     TO N'D:\\Data\\Musteri_PITR.mdf',\n     MOVE N'Musteri_log' TO N'D:\\Log\\Musteri_PITR_log.ldf',\n     NORECOVERY, STATS = 10;\n\nRESTORE DATABASE [Musteri_PITR]\nFROM DISK = N'D:\\Backup\\Musteri_diff_20261008.bak' WITH NORECOVERY;\n\n-- All log backups in order, each with the same STOPAT:\nRESTORE LOG [Musteri_PITR] FROM DISK = N'D:\\Backup\\Musteri_log_1200.trn'\nWITH NORECOVERY, STOPAT = '2026-10-08T14:29:00';\nRESTORE LOG [Musteri_PITR] FROM DISK = N'D:\\Backup\\Musteri_log_1500.trn'\nWITH NORECOVERY, STOPAT = '2026-10-08T14:29:00';\n\nRESTORE DATABASE [Musteri_PITR] WITH RECOVERY;\n\n-- Alternative: step forward with STANDBY and read data at each point:\n-- RESTORE LOG [Musteri_PITR] FROM DISK = N'...' WITH STANDBY = N'D:\\Backup\\Musteri_PITR_undo.dat', STOPAT = '...';","what":"FULL + (선택) DIFF + 로그 백업을 WITH NORECOVERY로 적용하고 지정한 시각에서 로그 재생을 멈춘다. 잘못된 작업(잘못된 DELETE/UPDATE, DROP) 직전 상태로 데이터베이스를 되돌린다.","when":"실수로 삭제/수정된 데이터를 복구하거나, 특정 시점의 데이터를 검사하거나, 감사를 위해.","caution":"원본 위에 덮어쓰지 말고 다른 이름으로 복원한 뒤 필요한 행을 다시 복사한다. 덮어써야 한다면 먼저 tail-log 백업을 수행한다. STOPAT은 서버 로컬 시간이다. 복구 모델은 FULL이어야 한다. STOPAT 시각을 포함한 로그 백업에 BULK_LOGGED의 최소 로깅 작업이 있으면 해당 백업 내부에서 멈출 수 없다. RESTORE ... WITH RECOVERY 이후에는 로그를 더 적용할 수 없다.","note":"모든 로그 복원에 STOPAT을 지정하는 것이 권장 패턴이며, 이전 파일에서 나오는 \"contains records that were logged before the designated point in time\" 안내 메시지는 정상이다. 이름 있는 마크(BEGIN TRAN ... WITH MARK)에서 멈추려면 STOPATMARK / STOPBEFOREMARK를 사용한다. 정확한 시각을 찾으려면 fn_dump_dblog 또는 애플리케이션 로그를 사용한다.","tags":["stopat","point-in-time","restore log","norecovery","standby","rollback to time"]},{"id":"restore-verify-header","task":"백업 파일 검사 및 검증 (VERIFYONLY, HEADERONLY, FILELISTONLY)","category":"복원 및 복구","command":"-- Readability and page checksum verification:\nRESTORE VERIFYONLY FROM DISK = N'D:\\Backup\\Musteri_full_20261008.bak' WITH CHECKSUM;\n\n-- Backup sets in the file (type, LSN, date, version, compression):\nRESTORE HEADERONLY FROM DISK = N'D:\\Backup\\Musteri_full_20261008.bak';\n\n-- Logical file names and sizes (needed for MOVE):\nRESTORE FILELISTONLY FROM DISK = N'D:\\Backup\\Musteri_full_20261008.bak';\n\n-- Media info (stripe count: FamilyCount):\nRESTORE LABELONLY FROM DISK = N'D:\\Backup\\Musteri_full_20261008.bak';","what":"복원하지 않고 백업이 읽을 수 있는지와 페이지 checksum이 유효한지 검증한다. 내부의 백업 세트(Position, BackupType, FirstLSN/LastLSN, DatabaseVersion), 논리 파일 이름, 스트라이프 정보를 보여 준다.","when":"다른 팀에서 받은 백업을 복원하기 전, MOVE용 파일 이름을 알아낼 때, 백업 작업 후 빠른 점검으로, 또는 파일에 여러 세트가 있을 때 올바른 FILE = n을 고를 때.","caution":"VERIFYONLY는 데이터베이스가 복원 가능하고 일관적임을 보장하지 않는다. 백업 구조와 (WITH CHECKSUM으로 수행했다면) 페이지 checksum만 확인한다. 실제 보증을 위해 정기적인 테스트 복원 + DBCC CHECKDB를 실행한다. 더 새로운 버전의 백업은 이전 버전에 복원할 수 없다(DatabaseVersion 확인). 읽기 작업이며 안전함.","note":"HEADERONLY에 HasBackupChecksums = 0이 표시되면 CHECKSUM 없이 수행된 백업이므로 VERIFYONLY WITH CHECKSUM으로 페이지를 검증할 수 없다. INIT 없이 기록된 파일에는 여러 세트가 누적되므로 복원 시 WITH FILE = <Position>을 지정한다.","tags":["restore verifyonly","headeronly","filelistonly","labelonly","backup verification","checksum"]},{"id":"page-restore-suspect","task":"손상된 페이지 찾기 (suspect_pages) 및 단일 페이지 복원","category":"복원 및 복구","command":"-- Pages that hit 823/824 or checksum errors:\nSELECT DB_NAME(sp.database_id) AS db, sp.file_id, sp.page_id,\n       CASE sp.event_type WHEN 1 THEN '823/824 error' WHEN 2 THEN 'bad checksum'\n            WHEN 3 THEN 'torn page'     WHEN 4 THEN 'restored'\n            WHEN 5 THEN 'repaired'      WHEN 7 THEN 'deallocated by DBCC' END AS event_desc,\n       sp.error_count, sp.last_update_date\nFROM   msdb.dbo.suspect_pages sp\nORDER BY sp.last_update_date DESC;\n\n-- Page restore (example 1:336): FULL + later logs + a new log backup\nRESTORE DATABASE [Musteri] PAGE = '1:336'\nFROM DISK = N'D:\\Backup\\Musteri_full_20261008.bak' WITH NORECOVERY;\nRESTORE LOG [Musteri] FROM DISK = N'D:\\Backup\\Musteri_log_1200.trn' WITH NORECOVERY;\nBACKUP LOG [Musteri] TO DISK = N'D:\\Backup\\Musteri_log_pagetail.trn' WITH INIT;\nRESTORE LOG [Musteri] FROM DISK = N'D:\\Backup\\Musteri_log_pagetail.trn' WITH RECOVERY;\n\n-- Verify:\nDBCC CHECKDB (N'Musteri') WITH NO_INFOMSGS, ALL_ERRORMSGS;","what":"msdb.dbo.suspect_pages에서 I/O(823/824), checksum, torn page 오류가 발생한 페이지를 나열한다. 데이터베이스 전체를 복원하지 않고 손상된 개별 페이지를 FULL 백업에서 가져와 로그로 roll forward하여 복구한다.","when":"오류 로그에 823/824 오류가 있을 때, CHECKDB의 일관성 오류가 몇 페이지에 한정될 때, 또는 사용자 쿼리가 \"incorrect checksum\"으로 실패할 때.","caution":"FULL 또는 BULK_LOGGED 복구 모델과 끊기지 않은 로그 체인이 필요하다. 온라인 페이지 복원은 Enterprise/Developer 전용이며 다른 에디션에서는 복원 중 데이터베이스가 오프라인이 된다. 할당 페이지(GAM/SGAM/PFS), 파일 헤더, boot page (1:9)는 이 방법으로 복원할 수 없다. 823/824는 보통 스토리지 문제를 의미하므로 디스크/컨트롤러/SAN을 확인하지 않고 종결하면 재발한다. 손상 페이지가 많으면 데이터베이스 전체 복원이 더 빠르다.","note":"suspect_pages는 최대 1,000행을 보관하며 가득 차면 새 오류가 기록되지 않으므로 오래된 행(event_type 4/5/7)을 삭제한다. 여러 페이지는 쉼표로 구분한다: PAGE = '1:336, 1:337'. AG에서는 primary의 손상 페이지가 automatic page repair(sys.dm_hadr_auto_page_repair)로 secondary에서 자동 복구된다.","tags":["suspect_pages","page restore","823","824","corruption","bad page"]},{"id":"top-queries-cpu","task":"plan cache에서 가장 비용이 큰 쿼리 찾기 (CPU, 읽기, 소요 시간)","category":"성능","command":"SELECT TOP (20)\n       qs.query_hash,\n       SUM(qs.execution_count)                                       AS execs,\n       SUM(qs.total_worker_time) / 1000                              AS total_cpu_ms,\n       SUM(qs.total_worker_time) / SUM(qs.execution_count) / 1000    AS avg_cpu_ms,\n       SUM(qs.total_elapsed_time) / SUM(qs.execution_count) / 1000   AS avg_ms,\n       SUM(qs.total_logical_reads) / SUM(qs.execution_count)         AS avg_reads,\n       SUM(qs.total_grant_kb) / SUM(qs.execution_count)              AS avg_grant_kb,\n       COUNT(DISTINCT qs.query_plan_hash)                            AS plan_count,\n       MAX(qs.last_execution_time)                                   AS last_exec,\n       MIN(SUBSTRING(st.text, qs.statement_start_offset / 2 + 1,\n           ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)\n             ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2) + 1)) AS statement_text,\n       MIN(DB_NAME(pa.dbid))                                         AS db\nFROM   sys.dm_exec_query_stats qs\n  CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st\n  OUTER APPLY (SELECT CONVERT(int, value) AS dbid FROM sys.dm_exec_plan_attributes(qs.plan_handle)\n               WHERE attribute = N'dbid') pa\nGROUP BY qs.query_hash\nORDER BY total_cpu_ms DESC;\n\n-- To rank by reads: ORDER BY SUM(qs.total_logical_reads) DESC\n-- Plan: SELECT query_plan FROM sys.dm_exec_query_plan(<plan_handle>);","what":"plan cache의 문장을 query_hash로 그룹화하고(리터럴만 다른 ad-hoc 쿼리는 한 행으로 합쳐짐) 총/평균 CPU, 소요 시간, 논리 읽기, memory grant, 실행 계획 수를 반환한다.","when":"CPU가 포화될 때, \"서버가 느리다\"는 불만이 있을 때, Query Store가 없는 데이터베이스, 또는 모든 데이터베이스에 걸친 서버 전체 관점이 필요할 때.","caution":"캐시에 남아 있는 실행 계획만 보인다: 재시작, DBCC FREEPROCCACHE, 메모리 압박, RECOMPILE로 계획이 제거된 쿼리는 누락된다. 지속적이고 시간 구간별 분석에는 Query Store를 사용한다. plan_count > 1은 파라미터 민감도 또는 다른 SET 옵션을 시사한다. plan cache가 매우 크면 쿼리에 몇 초가 걸릴 수 있다. 안전함.","since":"2016","note":"total_grant_kb는 2016+ 컬럼이다. 저장 프로시저별 합계는 sys.dm_exec_procedure_stats를 사용한다.","tags":["dm_exec_query_stats","top queries","cpu","query_hash","plan cache","expensive query"]},{"id":"wait-stats-top","task":"대기 통계: 서버가 무엇을 기다리는가","category":"성능","command":"WITH w AS (\n    SELECT wait_type, waiting_tasks_count,\n           wait_time_ms / 1000.0                         AS wait_s,\n           (wait_time_ms - signal_wait_time_ms) / 1000.0 AS resource_s,\n           signal_wait_time_ms / 1000.0                  AS signal_s\n    FROM   sys.dm_os_wait_stats\n    WHERE  waiting_tasks_count > 0\n      AND  wait_type NOT LIKE N'SLEEP[_]%'\n      AND  wait_type NOT IN (\n           N'BROKER_EVENTHANDLER', N'BROKER_RECEIVE_WAITFOR', N'BROKER_TASK_STOP', N'BROKER_TO_FLUSH',\n           N'BROKER_TRANSMITTER', N'CHECKPOINT_QUEUE', N'CLR_AUTO_EVENT', N'CLR_MANUAL_EVENT',\n           N'DIRTY_PAGE_POLL', N'DISPATCHER_QUEUE_SEMAPHORE', N'FT_IFTS_SCHEDULER_IDLE_WAIT',\n           N'HADR_FILESTREAM_IOMGR_IOCOMPLETION', N'HADR_WORK_QUEUE', N'LAZYWRITER_SLEEP',\n           N'LOGMGR_QUEUE', N'ONDEMAND_TASK_QUEUE', N'PVS_PREALLOCATE', N'REQUEST_FOR_DEADLOCK_SEARCH',\n           N'SNI_HTTP_ACCEPT', N'SOS_WORK_DISPATCHER', N'SP_SERVER_DIAGNOSTICS_SLEEP',\n           N'SQLTRACE_BUFFER_FLUSH', N'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', N'WAITFOR',\n           N'XE_DISPATCHER_WAIT', N'XE_TIMER_EVENT', N'XE_LIVE_TARGET_TVF', N'PREEMPTIVE_XE_DISPATCHER',\n           N'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', N'QDS_ASYNC_QUEUE', N'QDS_SHUTDOWN_QUEUE',\n           N'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP', N'PWAIT_ALL_COMPONENTS_INITIALIZED',\n           N'PWAIT_EXTENSIBILITY_CLEANUP_TASK', N'STARTUP_DEPENDENCY_MANAGER', N'WAIT_XTP_CKPT_CLOSE')\n)\nSELECT TOP (20) wait_type, waiting_tasks_count,\n       CAST(wait_s AS DECIMAL(18,1))                                AS wait_s,\n       CAST(100.0 * wait_s / SUM(wait_s) OVER () AS DECIMAL(5,1))   AS pct,\n       CAST(resource_s AS DECIMAL(18,1))                            AS resource_s,\n       CAST(signal_s AS DECIMAL(18,1))                              AS signal_s,\n       CAST(1000.0 * wait_s / waiting_tasks_count AS DECIMAL(18,2)) AS avg_wait_ms\nFROM   w\nORDER BY wait_s DESC;\n\n-- Values accumulate since:\nSELECT sqlserver_start_time FROM sys.dm_os_sys_info;\n\n-- User requests waiting right now:\nSELECT wt.session_id, wt.wait_type, wt.wait_duration_ms, wt.resource_description, wt.blocking_session_id\nFROM   sys.dm_os_waiting_tasks wt\n  JOIN sys.dm_exec_sessions s ON s.session_id = wt.session_id\nWHERE  s.is_user_process = 1;","what":"무해한 백그라운드 대기를 제외하고 서버 전체 대기 시간을 대기 유형별로 비율 및 resource 대 signal(CPU 큐) 비중과 함께 분해한다. 지금 대기 중인 사용자 요청도 보여 준다.","when":"병목(I/O, CPU, 락, 로그, 메모리, 병렬 처리)을 분류하는 성능 조사의 첫 단계로, 그리고 전후 baseline 비교를 위해.","caution":"값은 재시작 이후 누적값이다. 현재 문제는 스냅샷을 두 번 떠서 차이를 비교한다. DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR)는 서버 전체 카운터를 초기화하고 모니터링 도구를 망가뜨리므로 피한다. 해석: PAGEIOLATCH_* = 디스크 읽기, WRITELOG = 로그 디스크, LCK_M_* = 블로킹, CXPACKET/CXCONSUMER = 병렬 처리, RESOURCE_SEMAPHORE = memory grant, SOS_SCHEDULER_YIELD + 높은 signal = CPU 압박. 읽기 전용 쿼리이며 안전함.","note":"세션별 대기는 sys.dm_exec_session_wait_stats (2016+), Query Store의 쿼리별 대기 범주는 sys.query_store_wait_stats (2017+)를 사용한다.","tags":["wait stats","dm_os_wait_stats","waits","pageiolatch","writelog","performance"]},{"id":"deadlock-system-health","task":"system_health 세션에서 과거 데드락 읽기 (deadlock graph)","category":"성능","command":"SET QUOTED_IDENTIFIER ON;  -- required for XML methods (sqlcmd/Agent default is OFF)\nWITH dl AS (\n    SELECT CAST(event_data AS XML) AS x\n    FROM   sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL)\n    WHERE  object_name = N'xml_deadlock_report'\n)\nSELECT TOP (20)\n       x.value('(event/@timestamp)[1]', 'datetime2')                                          AS utc_time,\n       x.value('(event/data/value/deadlock/victim-list/victimProcess/@id)[1]', 'varchar(50)')  AS victim,\n       x.value('count(event/data/value/deadlock/process-list/process)', 'int')                 AS process_count,\n       x.value('(event/data/value/deadlock/process-list/process/@currentdbname)[1]', 'sysname') AS db,\n       x.query('event/data/value/deadlock')                                                    AS deadlock_graph\nFROM   dl\nORDER BY utc_time DESC;","what":"system_health Extended Events 세션(기본 활성화)의 event_file 대상에서 xml_deadlock_report 이벤트를 읽는다. 각 데드락에 대해 시각(UTC), 희생 프로세스, 참여자 수, 데이터베이스, 전체 deadlock graph XML을 반환한다.","when":"애플리케이션이 1205 오류를 보고할 때, 데드락 빈도와 관련 객체를 분석할 때, trace flag 1222가 없는 서버에서.","caution":"system_health 파일은 순환되므로 바쁜 서버에서는 오래된 데드락이 사라질 수 있다. 영구 보관에는 전용 XE 세션을 설정한다. 많은 파일을 읽으면 CPU 비용이 들므로 업무 시간에는 TOP을 유지한다. 이벤트는 지연되어 기록되므로 새 데드락은 몇 초 후에 나타난다. 읽기 전용이며 안전함.","note":"deadlock_graph 값을 클릭해 .xdl로 저장하면 SSMS에서 그래픽으로 열 수 있다. system_health event_file 기본값은 2016 SP2 CU12 / 2017 CU20 / 2019 CU3 이후(Standard/Enterprise)에서 10 x 100 MB, 이전 빌드 및 다른 에디션에서 4 x 5 MB다. 그래프에서는 inputbuf, waitresource (KEY/PAGE/OBJECT), lockMode가 핵심 필드다. 대부분의 데드락은 누락된 인덱스 추가나 일관된 접근 순서로 해결된다.","tags":["deadlock","system_health","xml_deadlock_report","extended events","1205","deadlock graph"]},{"id":"xe-long-query-session","task":"Extended Events 세션 생성: 느린 쿼리를 파일로 수집하고 읽기","category":"성능","command":"SET QUOTED_IDENTIFIER ON;\nCREATE EVENT SESSION [dba_long_queries] ON SERVER\nADD EVENT sqlserver.sql_statement_completed (\n    ACTION (sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.database_name,\n            sqlserver.session_id, sqlserver.sql_text, sqlserver.username, sqlserver.query_hash)\n    WHERE (duration >= 1000000 AND sqlserver.is_system = 0)),   -- 1 s (microseconds)\nADD EVENT sqlserver.rpc_completed (\n    ACTION (sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.database_name,\n            sqlserver.session_id, sqlserver.sql_text, sqlserver.username, sqlserver.query_hash)\n    WHERE (duration >= 1000000 AND sqlserver.is_system = 0))\nADD TARGET package0.event_file (SET filename = N'D:\\XE\\dba_long_queries.xel',\n                                    max_file_size = 256, max_rollover_files = 4)\nWITH (MAX_MEMORY = 8 MB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,\n      MAX_DISPATCH_LATENCY = 15 SECONDS, STARTUP_STATE = OFF);\n\nALTER EVENT SESSION [dba_long_queries] ON SERVER STATE = START;\n\n-- Read:\nSELECT x.value('(event/@name)[1]', 'sysname')                                AS event_name,\n       x.value('(event/@timestamp)[1]', 'datetime2')                         AS utc_time,\n       x.value('(event/data[@name=\"duration\"]/value)[1]', 'bigint') / 1000   AS duration_ms,\n       x.value('(event/data[@name=\"cpu_time\"]/value)[1]', 'bigint') / 1000   AS cpu_ms,\n       x.value('(event/data[@name=\"logical_reads\"]/value)[1]', 'bigint')     AS logical_reads,\n       x.value('(event/action[@name=\"database_name\"]/value)[1]', 'sysname')  AS db,\n       x.value('(event/action[@name=\"client_app_name\"]/value)[1]', 'nvarchar(256)') AS app,\n       x.value('(event/action[@name=\"sql_text\"]/value)[1]', 'nvarchar(max)') AS sql_text\nFROM  (SELECT CAST(event_data AS XML) AS x\n       FROM sys.fn_xe_file_target_read_file(N'D:\\XE\\dba_long_queries*.xel', NULL, NULL, NULL)) t\nORDER BY duration_ms DESC;\n\n-- When done:\n-- ALTER EVENT SESSION [dba_long_queries] ON SERVER STATE = STOP;\n-- DROP EVENT SESSION [dba_long_queries] ON SERVER;","what":"1초보다 긴 문장과 RPC 호출을 애플리케이션, 호스트, 사용자, 데이터베이스, SQL 텍스트와 함께 디스크에 기록하는 경량 Extended Events 세션을 생성한다. .xel 파일을 T-SQL로 행으로 변환한다.","when":"간헐적 성능 저하 불만이 있을 때, Query Store가 보여 주지 않는 클라이언트 정보(app/host)가 필요할 때, SQL Trace/Profiler의 대체 수단으로.","caution":"매우 낮은 임계값(예: 0)은 바쁜 서버에서 파일을 빠르게 채우고 오버헤드를 추가하므로 사후가 아니라 WHERE predicate에서 필터링한다. sql_text action은 대량에서 비용이 크다. 폴더는 SQL Server 서비스 계정이 쓸 수 있어야 한다. duration과 cpu_time 단위는 마이크로초다. 끝나면 세션을 중지/삭제한다. STARTUP_STATE = OFF이면 재시작 후 시작되지 않는다.","note":"한 데이터베이스로 좁히려면 WHERE 절에 sqlserver.database_name = N'Musteri'를 추가한다. 빠르게 보려면 SSMS > Management > Extended Events > Watch Live Data를 사용한다.","tags":["extended events","xe","event session","slow query","sql_statement_completed","event_file"]},{"id":"tempdb-contention","task":"tempdb 경합과 공간 사용 진단","category":"성능","command":"-- 1) Current tempdb page latch waits (2:1:1 PFS, 2:1:2 GAM, 2:1:3 SGAM):\nSELECT wt.session_id, wt.wait_type, wt.wait_duration_ms, wt.resource_description\nFROM   sys.dm_os_waiting_tasks wt\nWHERE  wt.wait_type LIKE N'PAGELATCH[_]%'\n  AND  wt.resource_description LIKE N'2:%';\n\n-- 2) Sessions using the most tempdb (MB):\nSELECT TOP (20) su.session_id, s.login_name, s.program_name,\n       (su.user_objects_alloc_page_count - su.user_objects_dealloc_page_count) * 8 / 1024         AS user_obj_mb,\n       (su.internal_objects_alloc_page_count - su.internal_objects_dealloc_page_count) * 8 / 1024 AS internal_obj_mb\nFROM   sys.dm_db_session_space_usage su\n  JOIN sys.dm_exec_sessions s ON s.session_id = su.session_id\nWHERE  s.is_user_process = 1\nORDER BY su.user_objects_alloc_page_count + su.internal_objects_alloc_page_count\n       - su.user_objects_dealloc_page_count - su.internal_objects_dealloc_page_count DESC;\n\n-- 3) Overall breakdown and version store:\nSELECT SUM(unallocated_extent_page_count) * 8 / 1024       AS free_mb,\n       SUM(user_object_reserved_page_count) * 8 / 1024     AS user_obj_mb,\n       SUM(internal_object_reserved_page_count) * 8 / 1024 AS internal_obj_mb,\n       SUM(version_store_reserved_page_count) * 8 / 1024   AS version_store_mb\nFROM   tempdb.sys.dm_db_file_space_usage;\n\nSELECT DB_NAME(database_id) AS db, reserved_page_count * 8 / 1024 AS version_store_mb\nFROM   sys.dm_tran_version_store_space_usage\nORDER BY reserved_page_count DESC;\n\n-- 4) File count / equal sizing and memory-optimized metadata:\nSELECT name, type_desc, size * 8 / 1024 AS size_mb, growth * 8 / 1024 AS growth_mb, is_percent_growth\nFROM   tempdb.sys.database_files;\nSELECT SERVERPROPERTY('IsTempdbMetadataMemoryOptimized') AS tempdb_metadata_memopt;","what":"tempdb 할당 페이지(PFS/GAM/SGAM)와 시스템 테이블의 PAGELATCH 대기, tempdb를 가장 많이 쓰는 세션(임시 테이블 대 sort/hash spill), version store를 키우는 데이터베이스, 파일 구성을 보여 준다.","when":"임시 테이블 워크로드가 많아 PAGELATCH_UP/EX 대기가 증가할 때, tempdb가 가득 찰 때(1105/9002), 또는 version store가 커질 때.","caution":"페이지 2:1:1, 2:1:2, 2:1:3 및 8088의 배수인 PFS 페이지는 할당 경합을 의미한다. 해결책은 크기와 증가 단위가 같은 여러 데이터 파일이다(8개까지 코어당 하나, 이후 4개씩 추가). 시스템 테이블 페이지 경합에는 2019+의 memory-optimized tempdb metadata를 사용한다(재시작 필요). version store가 커지면 가장 오래된 열린 스냅샷 트랜잭션을 찾는다. tempdb 축소는 해결책이 아니다. 읽기 전용 쿼리이며 안전함.","note":"memory-optimized metadata: ALTER SERVER CONFIGURATION SET MEMORY_OPTIMIZED TEMPDB_METADATA = ON; + 재시작 (2019+). 2022는 latch-free GAM/SGAM 업데이트를 추가해 경합을 크게 줄였다. version store를 붙잡고 있는 트랜잭션: sys.dm_tran_active_snapshot_database_transactions ORDER BY elapsed_time_seconds DESC.","tags":["tempdb","pagelatch","pfs","gam","version store","contention"]},{"id":"qs-enable-config","task":"Query Store 활성화, 권장 설정 적용, 상태 확인","category":"Query Store","command":"ALTER DATABASE [Musteri] SET QUERY_STORE = ON;\nALTER DATABASE [Musteri] SET QUERY_STORE (\n    OPERATION_MODE = READ_WRITE,\n    QUERY_CAPTURE_MODE = AUTO,\n    MAX_STORAGE_SIZE_MB = 2048,\n    INTERVAL_LENGTH_MINUTES = 15,\n    DATA_FLUSH_INTERVAL_SECONDS = 900,\n    CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),\n    SIZE_BASED_CLEANUP_MODE = AUTO,\n    MAX_PLANS_PER_QUERY = 200,\n    WAIT_STATS_CAPTURE_MODE = ON);\n\n-- State: if actual_state dropped to READ_ONLY, check readonly_reason\nUSE [Musteri];\nSELECT actual_state_desc, desired_state_desc, readonly_reason,\n       current_storage_size_mb, max_storage_size_mb,\n       query_capture_mode_desc, size_based_cleanup_mode_desc, wait_stats_capture_mode_desc\nFROM   sys.database_query_store_options;","what":"데이터베이스에서 Query Store를 활성화한다. 쿼리 텍스트, 실행 계획, 런타임 및 대기 통계를 시간 구간별로 저장하기 시작한다. 옵션은 저장 한도, 정리, 캡처 모드를 정의한다.","when":"새 데이터베이스가 운영에 들어갈 때, 버전/호환성 수준 업그레이드 전(baseline 확보), 또는 실행 계획 회귀가 의심될 때.","caution":"MAX_STORAGE_SIZE_MB에 도달하면 Query Store는 조용히 READ_ONLY(readonly_reason = 65536)로 전환되어 수집을 멈추므로 current_storage_size_mb를 모니터링한다. QUERY_CAPTURE_MODE = ALL은 ad-hoc 워크로드가 많을 때 저장 공간을 빠르게 채우고 오버헤드를 추가한다. 매우 바쁜 OLTP 시스템에서는 QDS_* 대기가 나타날 수 있다. OFF로 전환하면 수집된 데이터는 유지되고 CLEAR는 삭제한다.","since":"2016","note":"2022에서는 새로 생성된 데이터베이스의 Query Store가 기본으로 READ_WRITE로 켜져 있다. 2019+ QUERY_CAPTURE_MODE = CUSTOM은 임계값(EXECUTION_COUNT, TOTAL_COMPILE_CPU_TIME_MS, TOTAL_EXECUTION_CPU_TIME_MS)을 받는다. 읽기 가능 secondary용 Query Store(ALTER DATABASE ... FOR SECONDARY SET QUERY_STORE = ON)는 2022에서 제한된 preview로 trace flag 12606으로만 활성화되며 운영 환경에서 지원되지 않는다. 2025는 trace flag가 필요 없다(여전히 preview).","tags":["query store","enable","max_storage_size_mb","query_capture_mode","read_only","qds"]},{"id":"qs-regressed-queries","task":"Query Store로 실행 계획 회귀 찾기 (같은 쿼리, 다른 계획)","category":"Query Store","command":"USE [Musteri];\n-- Queries with more than one plan in the last 24 hours, per-plan performance:\nWITH plan_stats AS (\n    SELECT p.query_id, p.plan_id, p.is_forced_plan,\n           SUM(rs.count_executions) AS execs,\n           SUM(rs.avg_duration * rs.count_executions) / SUM(rs.count_executions) / 1000.0     AS avg_ms,\n           SUM(rs.avg_cpu_time * rs.count_executions) / SUM(rs.count_executions) / 1000.0     AS avg_cpu_ms,\n           SUM(rs.avg_logical_io_reads * rs.count_executions) / SUM(rs.count_executions)      AS avg_reads,\n           MAX(rs.last_execution_time) AS last_exec\n    FROM   sys.query_store_plan p\n      JOIN sys.query_store_runtime_stats rs ON rs.plan_id = p.plan_id\n      JOIN sys.query_store_runtime_stats_interval i\n        ON i.runtime_stats_interval_id = rs.runtime_stats_interval_id\n    WHERE  i.start_time >= DATEADD(HOUR, -24, SYSUTCDATETIME())\n    GROUP BY p.query_id, p.plan_id, p.is_forced_plan\n)\nSELECT ps.query_id, ps.plan_id, ps.is_forced_plan, ps.execs,\n       CAST(ps.avg_ms AS DECIMAL(18,2))     AS avg_ms,\n       CAST(ps.avg_cpu_ms AS DECIMAL(18,2)) AS avg_cpu_ms,\n       CAST(ps.avg_reads AS BIGINT)         AS avg_reads,\n       CAST(ps.avg_ms / NULLIF(MIN(ps.avg_ms) OVER (PARTITION BY ps.query_id), 0) AS DECIMAL(18,1)) AS x_slower_than_best,\n       ps.last_exec,\n       OBJECT_NAME(q.object_id)     AS module,\n       LEFT(qt.query_sql_text, 120) AS query_text\nFROM   plan_stats ps\n  JOIN sys.query_store_query q       ON q.query_id = ps.query_id\n  JOIN sys.query_store_query_text qt ON qt.query_text_id = q.query_text_id\nWHERE  ps.query_id IN (SELECT query_id FROM plan_stats GROUP BY query_id HAVING COUNT(*) > 1)\nORDER BY ps.query_id, ps.avg_ms;","what":"최근 24시간 동안 둘 이상의 실행 계획으로 실행된 쿼리를 찾는다. 각 계획의 실행 횟수, 가중 평균 소요 시간/CPU/읽기, 최적 계획 대비 몇 배 느린지를 나란히 보여 준다.","when":"쿼리가 \"어제는 빨랐는데 오늘은 느릴\" 때, 통계 갱신, 인덱스 변경, 호환성 수준 업그레이드 또는 패치 후.","caution":"평균 소요 시간은 데이터 분포에도 좌우되므로 다른 파라미터 값으로 실행된 계획을 비교할 때는 avg_reads도 본다(parameter sniffing). 시간 구간은 UTC다. 실행 횟수가 적은 계획은 통계적으로 신뢰도가 낮다. 읽기 전용 쿼리이며 안전함.","since":"2016","note":"SSMS > Query Store > Regressed Queries가 같은 작업을 그래픽으로 수행한다. 호환성 수준 160인 2022에서는 Parameter Sensitive Plan (PSP) 최적화가 한 문장에 대해 여러 variant query_id를 만든다. sys.query_store_query_variant (parent_query_id)로 부모와 매핑한다. 2017+ automatic tuning (FORCE_LAST_GOOD_PLAN)은 이런 회귀를 자동으로 바로잡을 수 있다.","tags":["query store","plan regression","regressed queries","parameter sniffing","query_store_runtime_stats","plan"]},{"id":"qs-force-plan-hints","task":"Query Store로 좋은 실행 계획 강제 또는 힌트 부착","category":"Query Store","command":"USE [Musteri];\n-- Force the good plan (query_id / plan_id from the regression query):\nEXEC sys.sp_query_store_force_plan @query_id = 42, @plan_id = 7;\n\n-- Forced plans and failure reason:\nSELECT p.query_id, p.plan_id, p.is_forced_plan, p.plan_forcing_type_desc,\n       p.force_failure_count, p.last_force_failure_reason_desc\nFROM   sys.query_store_plan p\nWHERE  p.is_forced_plan = 1;\n\n-- Undo:\nEXEC sys.sp_query_store_unforce_plan @query_id = 42, @plan_id = 7;\n\n-- 2022+: attach / remove a hint without changing code:\nEXEC sys.sp_query_store_set_hints @query_id = 42, @query_hints = N'OPTION (RECOMPILE)';\nSELECT query_id, query_hint_text, last_query_hint_failure_reason_desc\nFROM   sys.query_store_query_hints;\nEXEC sys.sp_query_store_clear_hints @query_id = 42;","what":"선택한 쿼리에 특정 Query Store 실행 계획을 강제하여 옵티마이저가 매 컴파일마다 그 계획을 사용하게 한다. 2022에서는 대신 애플리케이션 코드를 바꾸지 않고 RECOMPILE, MAXDOP, OPTIMIZE FOR 같은 힌트를 부착할 수 있다.","when":"운영 환경에서 실행 계획 회귀를 긴급히 수정해야 할 때, 영구 해결책(인덱스, 쿼리 재작성, 통계)이 준비될 때까지.","caution":"강제된 계획은 데이터가 늘면서 성능이 저하될 수 있으며 영구 해결책이 아니므로 정기적으로 검토한다. 계획이 사용하는 인덱스가 삭제/이름 변경되면 강제가 실패하고(last_force_failure_reason_desc = NO_INDEX 등) 쿼리는 정상적으로 컴파일된다. 강제 후 \"사실상 동일한\" 계획이 새 plan_id로 나타날 수 있으며 이는 정상이다. 강제는 Query Store가 READ_ONLY여도 동작하지만 복원 또는 AG 페일오버 후에는 확인한다.","since":"2016","note":"Query Store 힌트는 2022+이다(Azure SQL에서는 더 일찍 제공). 힌트는 plan guide를 대체한다. 힌트를 적용할 수 없으면 쿼리는 힌트 없이 실행되고 사유는 last_query_hint_failure_reason_desc에 나타난다. 읽기 가능 secondary에서의 계획 강제(@replica_group_id)에는 secondary replica용 Query Store가 필요하며, 2022에서는 trace flag 12606으로 활성화되는 제한된 preview다.","tags":["force plan","sp_query_store_force_plan","query store hints","plan forcing","regression","recompile"]},{"id":"missing-index-dmv","task":"DMV에서 누락 인덱스 제안 읽기 (주의 사항 포함)","category":"유지보수","command":"USE [Musteri];\nSELECT TOP (25)\n       mid.statement                                   AS table_name,\n       mid.equality_columns, mid.inequality_columns, mid.included_columns,\n       migs.user_seeks, migs.user_scans,\n       CAST(migs.avg_total_user_cost AS DECIMAL(18,2)) AS avg_query_cost,\n       migs.avg_user_impact                            AS est_impact_pct,\n       CAST(migs.user_seeks * migs.avg_total_user_cost * migs.avg_user_impact / 100.0 AS DECIMAL(18,0)) AS score,\n       migs.last_user_seek,\n       N'CREATE INDEX [IX_' + OBJECT_NAME(mid.object_id, mid.database_id) + N'_missing_'\n         + CAST(mid.index_handle AS NVARCHAR(10)) + N'] ON ' + mid.statement + N' ('\n         + ISNULL(mid.equality_columns, N'')\n         + CASE WHEN mid.equality_columns IS NOT NULL AND mid.inequality_columns IS NOT NULL THEN N', ' ELSE N'' END\n         + ISNULL(mid.inequality_columns, N'') + N')'\n         + ISNULL(N' INCLUDE (' + mid.included_columns + N')', N'') AS create_stmt_draft\nFROM   sys.dm_db_missing_index_details mid\n  JOIN sys.dm_db_missing_index_groups mig       ON mig.index_handle = mid.index_handle\n  JOIN sys.dm_db_missing_index_group_stats migs ON migs.group_handle = mig.index_group_handle\nWHERE  mid.database_id = DB_ID()\nORDER BY score DESC;\n\n-- 2019+: the query text behind each suggestion:\nSELECT TOP (25) mid.statement AS table_name, misq.group_handle, misq.user_seeks,\n       SUBSTRING(st.text, misq.last_statement_start_offset / 2 + 1, 300) AS statement_text\nFROM   sys.dm_db_missing_index_group_stats_query misq\n  JOIN sys.dm_db_missing_index_groups mig  ON mig.index_group_handle = misq.group_handle\n  JOIN sys.dm_db_missing_index_details mid ON mid.index_handle = mig.index_handle\n  CROSS APPLY sys.dm_exec_sql_text(misq.last_sql_handle) st\nWHERE  mid.database_id = DB_ID()\nORDER BY misq.user_seeks DESC;","what":"옵티마이저가 컴파일 시 기록한 누락 인덱스 제안을 사용량, 예상 비용, 효과로 점수화하고 CREATE INDEX 초안 문장을 생성한다. 2019+에서는 각 제안 뒤의 쿼리도 보여 준다.","when":"새 애플리케이션 릴리스 후, 읽기 중심 쿼리의 인덱스 필요성을 빠르게 훑어볼 때, 또는 인덱스 설계 검토의 시작 목록으로.","caution":"제안을 맹목적으로 적용하지 않는다: 옵티마이저는 기존 인덱스를 무시하고, 거의 중복되는(겹치는) 제안과 지나치게 넓은 INCLUDE 목록을 자주 만들며, 컬럼을 선택도가 아니라 등호/부등호 기준으로 정렬한다. 새 인덱스마다 쓰기와 저장 비용이 들므로 먼저 기존 인덱스 확장을 고려한다. DMV는 재시작, 데이터베이스 오프라인, 테이블의 인덱스 변경 시 초기화된다. 읽기 전용 쿼리이며 안전함.","note":"제안된 인덱스를 만들기 전에 테이블의 기존 인덱스(sp_helpindex 또는 sys.index_columns)와 비교하고 미사용 인덱스 레시피와 함께 검토한다. 실행 계획 XML의 MissingIndex 요소가 쿼리별로 같은 정보를 제공한다.","tags":["missing index","missing index dmv","dm_db_missing_index_details","index suggestion","create index","tuning"]},{"id":"unused-indexes","task":"미사용 / 쓰기 전용 인덱스 찾기","category":"유지보수","command":"USE [Musteri];\nSELECT OBJECT_SCHEMA_NAME(i.object_id) AS schema_name,\n       OBJECT_NAME(i.object_id)        AS table_name,\n       i.name                          AS index_name,\n       ISNULL(us.user_seeks, 0) + ISNULL(us.user_scans, 0) + ISNULL(us.user_lookups, 0) AS reads,\n       ISNULL(us.user_updates, 0)      AS writes,\n       ps.used_mb,\n       us.last_user_seek, us.last_user_scan,\n       N'DROP INDEX ' + QUOTENAME(i.name) + N' ON ' + QUOTENAME(OBJECT_SCHEMA_NAME(i.object_id))\n         + N'.' + QUOTENAME(OBJECT_NAME(i.object_id)) + N';' AS drop_stmt_draft\nFROM   sys.indexes i\n  LEFT JOIN sys.dm_db_index_usage_stats us\n         ON us.database_id = DB_ID() AND us.object_id = i.object_id AND us.index_id = i.index_id\n  CROSS APPLY (SELECT CAST(SUM(p.used_page_count) * 8 / 1024.0 AS DECIMAL(18,1)) AS used_mb\n               FROM sys.dm_db_partition_stats p\n               WHERE p.object_id = i.object_id AND p.index_id = i.index_id) ps\nWHERE  OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1\n  AND  i.type_desc = N'NONCLUSTERED'\n  AND  i.is_primary_key = 0 AND i.is_unique_constraint = 0 AND i.is_unique = 0\n  AND  ISNULL(us.user_seeks, 0) + ISNULL(us.user_scans, 0) + ISNULL(us.user_lookups, 0) = 0\nORDER BY writes DESC, ps.used_mb DESC;\n\n-- Usage counters since:\nSELECT sqlserver_start_time FROM sys.dm_os_sys_info;","what":"재시작 이후 읽히지 않았지만(seek/scan/lookup = 0) 여전히 쓰기 비용이 드는 nonclustered 인덱스를 크기와 함께 나열하고 DROP INDEX 초안 문장을 생성한다.","when":"쓰기 중심 테이블에서 INSERT/UPDATE가 느릴 때, 저장 공간과 유지보수(rebuild, CHECKDB) 시간을 줄일 때, 또는 인덱스 통합 전.","caution":"카운터는 재시작, 데이터베이스 오프라인/detach, (일부 오래된 2012/2014 빌드에서는) 인덱스 rebuild 시 초기화된다. 월말/연말 리포트에서만 쓰이는 인덱스는 짧은 구간에서는 \"미사용\"으로 보인다. 삭제 전에 최소 한 번의 전체 업무 주기 동안 관찰하고, AG에서는 읽기 가능 secondary의 사용량을 따로 확인한다(카운터는 replica별). unique 인덱스는 읽히지 않아도 제약을 강제하므로 제외된다. 읽기 전용 쿼리이며 안전함.","note":"삭제 전에 ALTER INDEX ... DISABLE로 비활성화하면 롤백이 쉽다(정의가 남아 있고 REBUILD로 복구). Query Store나 plan cache에서 인덱스 이름을 검색하면 힌트로 참조되는지 알 수 있다.","tags":["unused index","write-only index","dm_db_index_usage_stats","drop index","write overhead","index"]},{"id":"stats-health","task":"통계 상태: 언제 갱신되었고 몇 행이 변경되었나","category":"유지보수","command":"USE [Musteri];\nSELECT OBJECT_SCHEMA_NAME(s.object_id) AS schema_name,\n       OBJECT_NAME(s.object_id)        AS table_name,\n       s.name                          AS stats_name,\n       s.auto_created, s.has_filter, s.no_recompute,\n       sp.last_updated,\n       sp.rows, sp.rows_sampled,\n       CAST(100.0 * sp.rows_sampled / NULLIF(sp.rows, 0) AS DECIMAL(5,1))         AS sample_pct,\n       sp.modification_counter,\n       CAST(100.0 * sp.modification_counter / NULLIF(sp.rows, 0) AS DECIMAL(9,1)) AS modified_pct,\n       CAST(CASE WHEN 500 + 0.20 * sp.rows < SQRT(1000.0 * sp.rows) THEN 500 + 0.20 * sp.rows\n                 ELSE SQRT(1000.0 * sp.rows) END AS BIGINT)                       AS auto_update_threshold,\n       sp.persisted_sample_percent\nFROM   sys.stats s\n  CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp\nWHERE  OBJECTPROPERTY(s.object_id, 'IsUserTable') = 1\nORDER BY sp.modification_counter DESC;\n\nSELECT name, is_auto_create_stats_on, is_auto_update_stats_on, is_auto_update_stats_async_on\nFROM   sys.databases\nWHERE  name = DB_NAME();","what":"각 통계의 마지막 갱신 시각, 샘플링 비율, 마지막 갱신 이후 수정된 행 수, auto update가 실행되는 임계값(compat 130+의 동적 임계값)을 보여 준다.","when":"추정 행 수와 실제 행 수의 차이가 큰 실행 계획, 대량 데이터 적재 후, 또는 통계 유지보수 작업이 효과적인지 확인할 때.","caution":"임계값 공식은 compat level 130+에 적용된다(MIN(500 + 20%, SQRT(1000 x rows))). 낮은 compat level은 20% + 500 규칙을 사용한다. 한 번도 갱신되지 않은 통계(빈 테이블)는 NULL을 반환한다. sample_pct가 낮은 편향 컬럼은 FULLSCAN 또는 PERSIST_SAMPLE_PERCENT가 필요할 수 있다. 읽기 전용 쿼리이며 안전함.","since":"2016","note":"persisted_sample_percent는 UPDATE STATISTICS ... WITH PERSIST_SAMPLE_PERCENT = ON (2016 SP1 CU4 / 2017 CU1+)을 반영한다. 갱신 명령은 update-statistics 레시피를 참고한다.","tags":["statistics","stats health","dm_db_stats_properties","modification_counter","auto update","cardinality"]},{"id":"checkdb-strategy","task":"DBCC CHECKDB 전략: 마지막 정상 점검, 작업 분할, 결과 해석","category":"유지보수","command":"-- Last successful CHECKDB (1900-01-01 = never):\nSELECT name, DATABASEPROPERTYEX(name, 'LastGoodCheckDbTime') AS last_good_checkdb\nFROM   sys.databases\nWHERE  state_desc = N'ONLINE' AND name <> N'tempdb'\nORDER BY last_good_checkdb;\n\n-- Estimated tempdb requirement:\nDBCC CHECKDB (N'Musteri') WITH ESTIMATEONLY;\n\n-- Split a large DB across days: allocation + catalog + table by table CHECKTABLE\nUSE [Musteri];\nDBCC CHECKALLOC (N'Musteri') WITH NO_INFOMSGS;\nDBCC CHECKCATALOG (N'Musteri') WITH NO_INFOMSGS;\nDBCC CHECKTABLE (N'dbo.Siparis') WITH NO_INFOMSGS, ALL_ERRORMSGS;\n\n-- Get results as rows (error rows with object and page):\nDBCC CHECKDB (N'Musteri') WITH TABLERESULTS, NO_INFOMSGS, ALL_ERRORMSGS, MAXDOP = 4;","what":"모든 데이터베이스의 마지막 성공 CHECKDB 시각을 보여 주고, 필요한 tempdb를 추정하며, 큰 데이터베이스의 점검을 CHECKALLOC + CHECKCATALOG + CHECKTABLE 조각으로 나눌 수 있게 하고, 결과를 행(객체, 인덱스, 페이지)으로 반환한다.","when":"유지보수를 감사할 때, 유지보수 시간대에 들어가지 않는 매우 큰 데이터베이스, 또는 823/824 후 손상 범위를 파악할 때.","caution":"결과의 \"repair_allow_data_loss is the minimum repair level\" 문구는 복구 시 데이터가 삭제됨을 의미하므로 먼저 정상 백업에서 복원(또는 페이지 복원)을 고려한다. nonclustered 인덱스(index_id > 1)만 영향을 받았다면 인덱스를 삭제 후 재생성하여 데이터 손실 없이 해결된다. 손상을 발견한 후에는 디스크 서브시스템을 조사한다. PHYSICAL_ONLY는 논리 점검을 건너뛰므로 전체 CHECKDB도 주기적으로 필요하다. CHECKTABLE로 분할할 때 CHECKALLOC/CHECKCATALOG도 실행하지 않으면 점검 범위가 불완전하다.","note":"DATABASEPROPERTYEX(..., 'LastGoodCheckDbTime')는 2016 SP2, 2017 CU9, 2019+에서 사용할 수 있다. 이전 빌드에서는 DBCC DBINFO WITH TABLERESULTS의 dbi_dbccLastKnownGood을 읽는다. 부하를 분산하려면 백업에서 복원한 사본(예: 테스트 서버)에서 CHECKDB를 실행해도 운영 데이터베이스가 검증된다. MAXDOP 옵션은 2014 SP2 / 2016+이다.","tags":["dbcc checkdb","lastgoodcheckdbtime","checktable","corruption","repair_allow_data_loss","integrity"]},{"id":"resource-governor","task":"Resource Governor로 리포팅 부하 제한 (pool, workload group, classifier)","category":"구성","command":"USE master;\n-- 1) Pool and workload group:\nCREATE RESOURCE POOL [pool_report]\nWITH (MIN_CPU_PERCENT = 0, MAX_CPU_PERCENT = 30, CAP_CPU_PERCENT = 40,\n      MIN_MEMORY_PERCENT = 0, MAX_MEMORY_PERCENT = 25);\nCREATE WORKLOAD GROUP [wg_report]\nWITH (IMPORTANCE = LOW, REQUEST_MAX_MEMORY_GRANT_PERCENT = 15,\n      MAX_DOP = 2, REQUEST_MAX_CPU_TIME_SEC = 600)\nUSING [pool_report];\nGO\n-- 2) Classifier function (in master, SCHEMABINDING required):\nCREATE FUNCTION dbo.fn_rg_classifier()\nRETURNS sysname\nWITH SCHEMABINDING\nAS\nBEGIN\n    DECLARE @grp sysname = N'default';\n    IF SUSER_SNAME() = N'rapor_user' OR APP_NAME() LIKE N'Power BI%'\n        SET @grp = N'wg_report';\n    RETURN @grp;\nEND;\nGO\n-- 3) Enable:\nALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = dbo.fn_rg_classifier);\nALTER RESOURCE GOVERNOR RECONFIGURE;\n\n-- 4) Which session landed in which group:\nSELECT s.session_id, s.login_name, s.program_name, wg.name AS workload_group, rp.name AS pool\nFROM   sys.dm_exec_sessions s\n  JOIN sys.dm_resource_governor_workload_groups wg ON wg.group_id = s.group_id\n  JOIN sys.dm_resource_governor_resource_pools rp  ON rp.pool_id = wg.pool_id\nWHERE  s.is_user_process = 1;\n\n-- Rollback:\n-- ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = NULL); ALTER RESOURCE GOVERNOR DISABLE;","what":"특정 로그인 또는 애플리케이션의 세션을 연결 시점에 별도 workload group으로 보낸다. 해당 그룹의 CPU, memory grant, DOP, 요청당 CPU 시간을 제한하여 OLTP 워크로드를 보호한다.","when":"리포팅/BI 쿼리가 OLTP를 고갈시킬 때, ETL이 업무 시간 부하와 겹칠 때, 또는 멀티테넌트 서버의 리소스 공유를 위해.","caution":"classifier 함수는 모든 새 연결에서 실행된다. 함수가 느리거나 실패하면 모든 로그인이 느려지거나 실패한다(함수 안에서 쿼리나 테이블 접근 금지). 오동작하면 DAC로 접속해 CLASSIFIER_FUNCTION = NULL로 설정한다. 분류는 연결 시점에만 일어나며 기존 세션에는 영향이 없다. CAP_CPU_PERCENT는 CPU가 유휴일 때도 적용되는 하드 상한이다. MAX_CPU_PERCENT는 CPU 경합 시에만 적용된다. Resource Governor는 Enterprise/Developer 기능이다.","note":"REQUEST_MAX_CPU_TIME_SEC 초과는 기본적으로 이벤트(cpu_threshold_exceeded)만 발생시킨다. trace flag 2422 (2016 SP2 / 2017 CU3+)를 사용하면 요청이 중단된다. I/O 제한에는 pool의 MAX_IOPS_PER_VOLUME (2014+)을 사용한다. 통계: sys.dm_resource_governor_resource_pools / _workload_groups.","tags":["resource governor","resource pool","workload group","classifier","cpu limit","resource management"]},{"id":"tde-enable","task":"TDE 설정: master key, 인증서, 인증서 백업, 암호화 진행률","category":"보안","command":"USE master;\nIF NOT EXISTS (SELECT 1 FROM sys.symmetric_keys WHERE name = N'##MS_DatabaseMasterKey##')\n    CREATE MASTER KEY ENCRYPTION BY PASSWORD = N'<GucluParola_1>';\nCREATE CERTIFICATE [TDE_Cert_2026]\n    WITH SUBJECT = N'TDE certificate', EXPIRY_DATE = '20361231';\n\n-- Back it up NOW: without cert + private key the backups cannot be restored anywhere\nBACKUP CERTIFICATE [TDE_Cert_2026]\n    TO FILE = N'D:\\Keys\\TDE_Cert_2026.cer'\n    WITH PRIVATE KEY (FILE = N'D:\\Keys\\TDE_Cert_2026.pvk',\n                      ENCRYPTION BY PASSWORD = N'<GucluParola_2>');\n\nUSE [Musteri];\nCREATE DATABASE ENCRYPTION KEY\n    WITH ALGORITHM = AES_256\n    ENCRYPTION BY SERVER CERTIFICATE [TDE_Cert_2026];\nALTER DATABASE [Musteri] SET ENCRYPTION ON;\n\n-- Progress and certificate backup date:\nSELECT DB_NAME(dek.database_id) AS db, dek.encryption_state_desc, dek.percent_complete,\n       dek.encryption_scan_state_desc, c.name AS cert_name, c.expiry_date, c.pvt_key_last_backup_date\nFROM   sys.dm_database_encryption_keys dek\n  LEFT JOIN master.sys.certificates c ON c.thumbprint = dek.encryptor_thumbprint;\n\n-- On the target server (before restore / AG secondary):\n-- CREATE CERTIFICATE [TDE_Cert_2026] FROM FILE = N'D:\\Keys\\TDE_Cert_2026.cer'\n--   WITH PRIVATE KEY (FILE = N'D:\\Keys\\TDE_Cert_2026.pvk', DECRYPTION BY PASSWORD = N'<GucluParola_2>');","what":"master에 database master key와 인증서를 만들고, 인증서를 private key와 함께 백업하며, AES-256 database encryption key를 생성해 데이터 및 로그 파일을 저장 시 암호화한다. 백그라운드 암호화 스캔의 진행 상황을 보여 준다.","when":"GDPR/PCI 같은 저장 데이터 암호화 요구 사항, 그리고 백업 파일 및 MDF/LDF 파일 도난 대비.","caution":"인증서 + private key 백업이나 그 비밀번호를 잃으면 TDE 데이터베이스와 모든 백업을 영구히 읽을 수 없다. 백업은 서버와 떨어진 안전한 보관소에 둔다. 어떤 데이터베이스든 TDE를 활성화하면 tempdb도 암호화된다(서버 전체에 작은 CPU 비용). 암호화 스캔은 I/O가 많다. 2019+에서는 ALTER DATABASE ... SET ENCRYPTION SUSPEND/RESUME으로 일시 중지할 수 있다. AG/log shipping에서는 먼저 모든 대상 서버에 인증서를 설치해야 한다. TDE는 메모리나 네트워크상의 데이터를 암호화하지 않는다.","note":"인증서 만료는 기존 TDE를 멈추지 않지만, 교체하려면 새 인증서를 만들고 ALTER DATABASE ENCRYPTION KEY ENCRYPTION BY SERVER CERTIFICATE [new]로 다시 암호화한다(데이터가 아니라 DEK만 재암호화됨). encryption_state_desc와 encryption_scan_state_desc는 2019+ 컬럼이며, 이전 버전에서는 encryption_state (3 = 암호화됨, 2 = 진행 중)를 사용한다. TDE는 2019부터 Standard 에디션에서 사용할 수 있다.","tags":["tde","transparent data encryption","certificate","backup certificate","encryption","dek"]},{"id":"sql-server-audit","task":"SQL Server Audit 설정: 로그인, 권한 변경, 테이블 접근","category":"보안","command":"USE master;\nCREATE SERVER AUDIT [Audit_DBA]\nTO FILE (FILEPATH = N'D:\\Audit\\', MAXSIZE = 256 MB, MAX_ROLLOVER_FILES = 20, RESERVE_DISK_SPACE = OFF)\nWITH (QUEUE_DELAY = 1000, ON_FAILURE = CONTINUE);\nALTER SERVER AUDIT [Audit_DBA] WITH (STATE = ON);\n\n-- Server-wide (covers all databases):\nCREATE SERVER AUDIT SPECIFICATION [AuditSpec_Server]\nFOR SERVER AUDIT [Audit_DBA]\n    ADD (FAILED_LOGIN_GROUP),\n    ADD (SERVER_ROLE_MEMBER_CHANGE_GROUP),\n    ADD (DATABASE_ROLE_MEMBER_CHANGE_GROUP),\n    ADD (SERVER_PRINCIPAL_CHANGE_GROUP),\n    ADD (DATABASE_PRINCIPAL_CHANGE_GROUP),\n    ADD (SERVER_PERMISSION_CHANGE_GROUP),\n    ADD (DATABASE_PERMISSION_CHANGE_GROUP),\n    ADD (AUDIT_CHANGE_GROUP)\nWITH (STATE = ON);\n\n-- Database: access to a sensitive table:\nUSE [Musteri];\nCREATE DATABASE AUDIT SPECIFICATION [AuditSpec_Musteri]\nFOR SERVER AUDIT [Audit_DBA]\n    ADD (SELECT, INSERT, UPDATE, DELETE ON OBJECT::[dbo].[Siparis] BY [public])\nWITH (STATE = ON);\n\n-- Read:\nSELECT event_time, action_id, succeeded, server_principal_name, database_name,\n       schema_name, object_name, LEFT(statement, 200) AS stmt, client_ip, application_name\nFROM   sys.fn_get_audit_file(N'D:\\Audit\\Audit_DBA_*.sqlaudit', DEFAULT, DEFAULT)\nWHERE  event_time >= DATEADD(HOUR, -24, SYSUTCDATETIME())\nORDER BY event_time DESC;","what":"파일에 기록하는 server audit를 생성한다. 서버 전체의 로그인 실패, 롤 멤버십, principal 및 권한 변경과 데이터베이스 수준에서 선택한 테이블에 대한 SELECT/DML 접근을 기록하고, T-SQL로 기록을 읽는다.","when":"감사/컴플라이언스 요구 사항(GDPR, PCI, SOX), \"누가 이 테이블을 읽거나 변경했나\" 질문, 또는 권한 상승이 의심될 때.","caution":"바쁜 테이블의 SELECT 감사는 기록을 매우 빠르게 생성하고 성능 비용이 들므로 객체와 principal로 범위를 좁힌다. ON_FAILURE = SHUTDOWN은 audit를 기록할 수 없을 때 서버를 중지하므로 의무 사항일 때만 사용한다. audit 폴더는 SQL Server 서비스 계정이 쓸 수 있어야 하고 DBA가 아닌 사용자가 삭제할 수 없어야 한다. event_time은 UTC다. database audit specification은 데이터베이스와 함께 복원되지만 server audit GUID가 일치하지 않으면 고아 상태로 남는다.","note":"database audit specification은 2016 SP1부터 Standard 에디션에서 사용할 수 있다. audit 자체에 대한 변경(끄기 포함)이 기록되도록 항상 AUDIT_CHANGE_GROUP을 포함한다.","tags":["sql server audit","server audit","audit specification","fn_get_audit_file","auditing","compliance"]},{"id":"orphaned-users-fix","task":"고아 사용자 찾기 및 로그인에 재매핑 (복원 / AG 후)","category":"보안","command":"USE [Musteri];\n-- Database users without a matching login:\nSELECT dp.name AS user_name, dp.type_desc, dp.authentication_type_desc, dp.sid, dp.create_date\nFROM   sys.database_principals dp\n  LEFT JOIN sys.server_principals sp ON sp.sid = dp.sid\nWHERE  sp.sid IS NULL\n  AND  dp.type IN ('S', 'U', 'G')\n  AND  dp.authentication_type_desc IN (N'INSTANCE', N'WINDOWS')\n  AND  dp.principal_id > 4;\n\n-- If a login with the same name exists, remap the SID:\nALTER USER [app_user] WITH LOGIN = [app_user];\n\n-- Permanent fix: generate CREATE LOGIN with SID + password hash on the source, run it on the target:\nSELECT N'CREATE LOGIN ' + QUOTENAME(sp.name)\n     + N' WITH PASSWORD = ' + CONVERT(NVARCHAR(MAX), LOGINPROPERTY(sp.name, 'PasswordHash'), 1)\n     + N' HASHED, SID = ' + CONVERT(NVARCHAR(MAX), sp.sid, 1)\n     + N', CHECK_POLICY = OFF;' AS create_login_stmt\nFROM   sys.server_principals sp\nWHERE  sp.type = 'S' AND sp.name = N'app_user';","what":"SID가 어떤 서버 로그인과도 일치하지 않는 데이터베이스 사용자(고아)를 찾는다. 사용자를 기존 로그인에 재매핑하고, 다른 서버에 같은 SID와 비밀번호 해시로 로그인을 다시 만드는 스크립트를 생성한다.","when":"다른 서버에서 데이터베이스를 복원/attach한 후, AG/log shipping 페일오버 후 애플리케이션이 \"Login failed for user\"를 받을 때, 또는 서버 마이그레이션 중.","caution":"ALTER USER ... WITH LOGIN은 사용자의 SID를 로그인의 SID로 바꾼다. AG에서는 이 변경이 primary에서 secondary로 전파되지만, secondary 서버의 로그인 SID가 다르면 다음 페일오버에서 문제가 재발한다. 근본 해결책은 모든 replica에 같은 SID로 로그인을 만드는 것이다. 비밀번호 해시는 민감 정보이므로 스크립트를 안전하게 보관한다. contained database 사용자(authentication_type = DATABASE)는 로그인이 필요 없으며 나열되지 않는다.","note":"sp_change_users_login은 deprecated되었으므로 ALTER USER WITH LOGIN을 사용한다. 고아 dbo(데이터베이스 소유자 로그인 누락)에는 ALTER AUTHORIZATION ON DATABASE::[Musteri] TO [sa];를 사용한다. 대량 로그인 마이그레이션에는 PowerShell dbatools를 사용한다: Copy-DbaLogin -Source SQL01 -Destination SQL02.","tags":["orphaned user","alter user with login","sid","login failed","after restore","sp_change_users_login"]},{"id":"permission-audit","task":"로그인 및 권한 감사: sysadmin 멤버, 위험한 권한, 약한 비밀번호","category":"보안","command":"-- 1) Server role memberships:\nSELECT r.name AS server_role, m.name AS member, m.type_desc, m.is_disabled, m.create_date\nFROM   sys.server_role_members rm\n  JOIN sys.server_principals r ON r.principal_id = rm.role_principal_id\n  JOIN sys.server_principals m ON m.principal_id = rm.member_principal_id\nORDER BY r.name, m.name;\n\n-- 2) Risky server permissions:\nSELECT pr.name AS grantee, pr.type_desc, pe.permission_name, pe.state_desc\nFROM   sys.server_permissions pe\n  JOIN sys.server_principals pr ON pr.principal_id = pe.grantee_principal_id\nWHERE  pe.permission_name IN (N'CONTROL SERVER', N'IMPERSONATE ANY LOGIN', N'ALTER ANY LOGIN',\n                              N'ALTER ANY SERVER ROLE', N'ALTER SETTINGS', N'IMPERSONATE')\n   OR  pe.state_desc = N'GRANT_WITH_GRANT_OPTION';\n\n-- 3) SQL login health (policy, lockout, password = login name):\nSELECT sl.name, sl.is_disabled, sl.is_policy_checked, sl.is_expiration_checked,\n       LOGINPROPERTY(sl.name, 'PasswordLastSetTime') AS pwd_last_set,\n       LOGINPROPERTY(sl.name, 'IsLocked')            AS is_locked,\n       LOGINPROPERTY(sl.name, 'BadPasswordCount')    AS bad_pwd_count,\n       PWDCOMPARE(sl.name, sl.password_hash)         AS pwd_equals_name,\n       PWDCOMPARE(N'', sl.password_hash)             AS pwd_empty\nFROM   sys.sql_logins sl;\n\n-- 4) Database role memberships and explicit object permissions:\nUSE [Musteri];\nSELECT r.name AS db_role, m.name AS member, m.type_desc\nFROM   sys.database_role_members rm\n  JOIN sys.database_principals r ON r.principal_id = rm.role_principal_id\n  JOIN sys.database_principals m ON m.principal_id = rm.member_principal_id\nWHERE  m.name <> N'dbo'\nORDER BY r.name, m.name;\n\nSELECT pr.name AS grantee, pe.class_desc, pe.permission_name, pe.state_desc,\n       OBJECT_SCHEMA_NAME(pe.major_id) AS schema_name,\n       CASE pe.class WHEN 1 THEN OBJECT_NAME(pe.major_id) WHEN 3 THEN SCHEMA_NAME(pe.major_id) END AS object_name\nFROM   sys.database_permissions pe\n  JOIN sys.database_principals pr ON pr.principal_id = pe.grantee_principal_id\nWHERE  (pr.name NOT IN (N'public', N'dbo') AND NOT (pe.class = 0 AND pe.permission_name = N'CONNECT'))\n   OR  (pr.name = N'public' AND OBJECT_SCHEMA_NAME(pe.major_id) <> N'sys')\nORDER BY pr.name, pe.class_desc;","what":"서버 롤 멤버십, CONTROL SERVER/IMPERSONATE 같은 위험한 권한, 비밀번호 정책이 꺼져 있거나 잠겨 있거나 비밀번호가 로그인 이름과 같거나 비어 있는 SQL 로그인을 나열한다. 데이터베이스에서는 롤 멤버십과 시스템 외 객체에 대한 명시적 권한을 나열한다.","when":"정기 접근 검토, 감사 전, 팀원 퇴사 후, 또는 보안 사고가 의심될 때.","caution":"Windows 그룹을 통해 부여된 권한은 그룹 이름으로 표시된다. 구성원을 보려면 EXEC xp_logininfo N'DOMAIN\\Group', 'members';를 사용한다. 권한을 회수하기 전에 애플리케이션 의존성을 확인한다. PWDCOMPARE는 SQL 로그인에서만 동작하며 CONTROL SERVER가 필요하다. 읽기 전용 쿼리이며 안전함.","note":"principal의 유효 권한: EXECUTE AS USER = N'app_user'; SELECT * FROM fn_my_permissions(NULL, 'DATABASE'); REVERT;. 2022의 새로운 세분화 롤(##MS_ServerStateReader##, ##MS_DefinitionReader## 등)은 sysadmin 없이 모니터링 권한을 부여한다.","tags":["permission audit","sysadmin","server_role_members","database_permissions","pwdcompare","security"]},{"id":"ledger-tables","task":"ledger 테이블 생성, 변경 이력 조회, 무결성 검증","category":"보안","command":"USE [Musteri];\n-- Ledger verification requires snapshot isolation:\nALTER DATABASE [Musteri] SET ALLOW_SNAPSHOT_ISOLATION ON;\n\n-- Updatable ledger table (history table maintained automatically):\nCREATE TABLE dbo.Hesap (\n    HesapId int           NOT NULL PRIMARY KEY,\n    Bakiye  decimal(18,2) NOT NULL\n)\nWITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Hesap_History), LEDGER = ON);\n\n-- Append-only ledger (UPDATE/DELETE are blocked):\nCREATE TABLE dbo.IslemLog (Id bigint IDENTITY, Mesaj nvarchar(400))\nWITH (LEDGER = ON (APPEND_ONLY = ON));\n\n-- Who changed what, and when:\nSELECT t.commit_time, t.principal_name, l.HesapId, l.Bakiye, l.ledger_operation_type_desc\nFROM   dbo.Hesap_Ledger l\n  JOIN sys.database_ledger_transactions t ON t.transaction_id = l.ledger_transaction_id\nORDER BY t.commit_time DESC;\n\n-- Take a digest (store it somewhere trusted outside) and verify:\nEXEC sys.sp_generate_database_ledger_digest;\nEXEC sys.sp_verify_database_ledger\n     N'{\"database_name\":\"Musteri\",\"block_id\":0,\"hash\":\"0x...\",\"last_transaction_commit_time\":\"...\",\"digest_time\":\"...\"}';","what":"행 변경을 암호화 해시 체인에 기록하는 ledger 테이블을 생성한다. _Ledger 뷰는 각 INSERT/UPDATE/DELETE를 누가 언제 했는지 보여 주며, 저장된 digest로 검증하면 데이터가 사후에(DBA를 포함해) 변경되지 않았음을 증명할 수 있다.","when":"불변성을 증명해야 하는 데이터: 금융 기록, 감사 추적, 계약/승인 기록.","caution":"ledger는 테이블 생성 후 끌 수 없다. ledger 테이블은 일반 테이블로 변환할 수 없으며, 삭제된 ledger 테이블은 dropped ledger table로 보존된다. updatable ledger는 변경마다 이력 행을 기록하므로 저장 공간이 늘어난다. digest를 데이터베이스 밖의 불변 스토리지(WORM, Azure immutable blob)에 저장하지 않으면 검증은 증거 가치가 없다. sp_verify_database_ledger는 snapshot isolation 없이 37498 오류로 실패한다.","since":"2022","note":"sp_generate_database_ledger_digest는 JSON을 반환한다. sp_verify_database_ledger는 단일 digest 또는 JSON 배열을 받으며 @table_name으로 단일 테이블을 검증할 수 있다. 자동 digest 저장: ALTER DATABASE SCOPED CONFIGURATION SET LEDGER_DIGEST_STORAGE_ENDPOINT = '<blob url>'. CREATE DATABASE ... WITH LEDGER = ON은 모든 테이블을 기본적으로 ledger 테이블로 만든다.","tags":["ledger","append-only","sp_verify_database_ledger","digest","immutable","tamper-evident"]},{"id":"agent-job-failures","task":"SQL Agent 작업 실패 이력 및 마지막 실행 상태","category":"SQL Agent","command":"-- Job steps that failed / were cancelled in the last 24 hours:\nSELECT j.name AS job_name, h.step_id, h.step_name,\n       msdb.dbo.agent_datetime(h.run_date, h.run_time) AS run_at,\n       STUFF(STUFF(RIGHT('000000' + CAST(h.run_duration AS VARCHAR(6)), 6), 3, 0, ':'), 6, 0, ':') AS duration_hhmmss,\n       CASE h.run_status WHEN 0 THEN 'Failed' WHEN 1 THEN 'Succeeded' WHEN 2 THEN 'Retry'\n                         WHEN 3 THEN 'Canceled' WHEN 4 THEN 'In progress' END AS status,\n       h.sql_message_id, h.sql_severity,\n       LEFT(h.message, 500) AS message\nFROM   msdb.dbo.sysjobhistory h\n  JOIN msdb.dbo.sysjobs j ON j.job_id = h.job_id\nWHERE  h.run_status IN (0, 3)\n  AND  msdb.dbo.agent_datetime(h.run_date, h.run_time) >= DATEADD(HOUR, -24, GETDATE())\nORDER BY run_at DESC;\n\n-- Each job's last outcome, running now, next run:\nSELECT j.name, j.enabled,\n       CASE lr.run_status WHEN 0 THEN 'Failed' WHEN 1 THEN 'Succeeded' WHEN 3 THEN 'Canceled' END AS last_outcome,\n       lr.run_at AS last_run_at,\n       CASE WHEN a.start_execution_date IS NOT NULL AND a.stop_execution_date IS NULL THEN 'RUNNING' END AS running_now,\n       a.next_scheduled_run_date\nFROM   msdb.dbo.sysjobs j\n  LEFT JOIN msdb.dbo.sysjobactivity a\n         ON a.job_id = j.job_id AND a.session_id = (SELECT MAX(session_id) FROM msdb.dbo.syssessions)\n  OUTER APPLY (SELECT TOP (1) h.run_status, msdb.dbo.agent_datetime(h.run_date, h.run_time) AS run_at\n               FROM msdb.dbo.sysjobhistory h\n               WHERE h.job_id = j.job_id AND h.step_id = 0\n               ORDER BY h.instance_id DESC) lr\nORDER BY j.name;\n\n-- Is the Agent service running:\nSELECT servicename, status_desc, startup_type_desc FROM sys.dm_server_services;","what":"최근 24시간 동안 실패하거나 취소된 작업 단계를 오류 메시지와 함께 보여 준다. 각 작업의 마지막 결과, 현재 실행 여부, 다음 예약 실행, Agent 서비스 상태를 보여 준다.","when":"아침 점검에서, 백업/유지보수 작업 알림 시, \"야간 작업이 실행되었나\" 확인할 때, 또는 페일오버 후 작업이 올바른 replica에서 실행되는지 확인할 때.","caution":"msdb의 작업 이력은 제한되어 있다(기본 전체 1,000행, 작업당 100행). 자주 실행되는 작업이 오래된 실패를 빠르게 밀어내므로 Agent 속성에서 한도를 늘린다. message 컬럼은 4,000자에서 잘리므로 긴 출력은 작업 단계에 출력 파일을 지정한다. Agent 서비스가 중지되면 예약 작업이 실행되지 않으며 어떤 작업도 오류를 보고하지 않는다. 읽기 전용 쿼리이며 안전함.","note":"msdb.dbo.agent_datetime은 문서화되지 않았지만 널리 쓰인다. AG에서는 작업이 모든 replica에 있어야 하며 첫 단계에서 sys.fn_hadr_is_primary_replica(N'Musteri')로 역할을 확인한다. dbatools: Get-DbaAgentJobHistory -SqlInstance SQL01 -StartDate (Get-Date).AddDays(-1) -OutcomeType Failed.","tags":["sql agent","job history","sysjobhistory","job failure","msdb","scheduled job"]}]},"recipes/mongodb.json":{"engineId":"mongodb","engine":"MongoDB","order":5,"recipes":[{"id":"rs-add-member","task":"replica set에 멤버 추가","category":"Replica Set","command":"rs.add({ host: \"mongo2:27017\", priority: 1, votes: 1 })","what":"기존 replica set에 새 멤버(secondary)를 추가하고 초기 동기화를 시작한다.","when":"읽기 용량을 늘리거나 이중화를 위해 새 노드를 추가할 때.","caution":"새 멤버는 초기 동기화 중 primary에서 데이터를 가져오므로 데이터가 크면 네트워크와 디스크 부하가 발생한다.","tags":["replica","rs.add","member","member","add"]},{"id":"rs-remove-member","task":"replica set에서 멤버 제거","category":"Replica Set","command":"rs.remove(\"mongo2:27017\")","what":"지정한 호스트를 replica set 구성에서 제거한다.","when":"노드를 서비스에서 제외하거나 하드웨어를 교체할 때.","caution":"primary를 제거하지 않는다. 과반수를 깨는 방식으로 멤버를 제거하면 set이 읽기와 쓰기에 사용할 수 없게 될 수 있다.","tags":["replica","rs.remove","member","member","remove"]},{"id":"rs-step-down","task":"primary 강등 (stepDown)","category":"Replica Set","command":"rs.stepDown(60)","what":"현재 primary를 지정한 초 동안 secondary로 강등하고 새 선출을 트리거한다.","when":"계획된 유지보수 전에 primary 역할을 다른 노드로 넘기기 위해.","caution":"stepDown 중 짧은 쓰기 중단이 발생하므로 애플리케이션은 재연결해야 한다.","tags":["replica","stepdown","primary","failover","step down"]},{"id":"rs-set-priority","task":"멤버 우선순위 변경","category":"Replica Set","command":"cfg = rs.conf(); cfg.members[1].priority = 2; rs.reconfig(cfg)","what":"선출 시 지정한 멤버가 primary가 될 우선순위를 변경한다.","when":"특정 노드를 선호 primary로 두려 할 때.","caution":"reconfig는 선출을 트리거해 짧은 primary 변경/쓰기 중단을 일으킬 수 있다.","tags":["replica","priority","reconfig","priority","election"]},{"id":"rs-status","task":"replica set 상태 확인","category":"Replica Set","command":"rs.status()","what":"set 멤버의 상태, 역할, 복제 지연, 건강 상태를 보여 준다.","when":"복제 상태와 지연을 확인할 때.","caution":"읽기 전용 명령이며 위험 없음.","tags":["replica","status","status","health","rs.status"]},{"id":"rs-hidden-member","task":"hidden / non-voting 멤버 구성","category":"Replica Set","command":"cfg = rs.conf(); cfg.members[2].hidden = true; cfg.members[2].priority = 0; cfg.members[2].votes = 0; rs.reconfig(cfg)","what":"멤버를 hidden(애플리케이션에 보이지 않음), non-voting, priority 0으로 만들어 백업/분석용으로만 사용한다.","when":"백업이나 분석 쿼리를 격리하려 할 때.","caution":"hidden 멤버는 priority가 0이어야 하며 그렇지 않으면 reconfig가 실패한다.","tags":["replica","hidden","votes","hidden","non-voting"]},{"id":"rs-resync","task":"멤버를 처음부터 재동기화","category":"Replica Set","command":"// On the SECONDARY to be resynced (NEVER on the PRIMARY):\ndb.getSiblingDB(\"admin\").shutdownServer()\n// At the OS level: empty the dbPath contents (or move them elsewhere as a backup)\n// and restart mongod with the same configuration; the member starts an initial sync automatically.\n// Monitor progress (stateStr: STARTUP2 -> SECONDARY):\nrs.status()","what":"MongoDB에는 별도의 resync 명령이 없다. secondary를 중지하고 데이터 디렉터리를 비운 뒤 재시작하면 primary(또는 sync source)에서 새 초기 동기화가 시작된다.","when":"멤버가 너무 뒤처져 oplog window 밖으로 벗어났거나(RECOVERING / too stale) 데이터가 불일치하게 되었을 때.","caution":"멤버의 모든 로컬 데이터가 삭제되고 다시 가져와진다. secondary에서만 수행하고 과반수가 계속 동작하는지 확인한다. 동기화 시간과 네트워크/디스크 부하가 크다. 매우 큰 데이터 세트는 정상 멤버의 파일 시스템 스냅샷으로 시딩하는 것이 더 빠르다.","tags":["replica","resync","initialsync","sync","resync"]},{"id":"sh-add-shard","task":"클러스터에 샤드 추가","category":"샤딩","command":"sh.addShard(\"shardRS/mongoShard1:27018\")","what":"sharded cluster에 새 shard(replica set)를 추가한다.","when":"저장 용량이나 처리량을 수평으로 확장할 때.","caution":"샤드를 추가하면 balancer가 chunk 마이그레이션을 시작하며 추가 IO 부하가 발생한다.","tags":["sharding","addshard","shard","add","cluster"]},{"id":"sh-shard-collection","task":"컬렉션 샤딩","category":"샤딩","command":"sh.shardCollection(\"shop.customer\", { customerId: \"hashed\" })","what":"지정한 컬렉션을 주어진 샤드 키로 샤딩한다.","when":"커지는 컬렉션을 여러 샤드에 분산하려 할 때.","caution":"샤드 키는 5.0+에서 reshardCollection으로 변경할 수 있지만(4.4+에서는 refineCollectionShardKey로 확장 가능) 모든 데이터를 재분배하므로 비용이 크다. 잘못된 key 선택은 불균등한 분산으로 이어진다.","tags":["sharding","shardcollection","key","customer","shard key"]},{"id":"sh-start-balancer","task":"balancer 시작","category":"샤딩","command":"sh.startBalancer()","what":"chunk를 분산하는 balancer 프로세스를 활성화한다.","when":"유지보수 후 밸런싱을 다시 활성화하려 할 때.","caution":"balancer는 chunk 마이그레이션 중 IO와 네트워크 부하를 만든다. 피크 시간대 밖에 balancing window를 정의한다.","tags":["sharding","balancer","start","start","rebalance"]},{"id":"sh-stop-balancer","task":"balancer 중지","category":"샤딩","command":"sh.stopBalancer()","what":"chunk 마이그레이션을 수행하는 balancer를 비활성화한다.","when":"백업, 유지보수, 대량 데이터 적재 전에 마이그레이션을 중지하기 위해.","caution":"오래 꺼 두면 샤드가 불균형해지므로 이후 다시 활성화하는 것을 잊지 않는다.","tags":["sharding","balancer","stop","stop","maintenance"]},{"id":"sh-status","task":"샤딩 상태 확인","category":"샤딩","command":"sh.status()","what":"샤드, 데이터베이스, 샤딩된 컬렉션, chunk 분포를 요약한다.","when":"클러스터의 분산 상태와 balancer 상태를 점검할 때.","caution":"읽기 전용 명령이며 위험 없음.","tags":["sharding","status","status","chunk","sh.status"]},{"id":"sh-add-zone","task":"zone(tag) 추가 및 범위 할당","category":"샤딩","command":"sh.addShardToZone(\"shard0000\", \"TR\"); sh.updateZoneKeyRange(\"shop.customer\", { region: \"TR\" }, { region: \"TR~\" }, \"TR\")","what":"샤드를 zone에 할당하고 특정 key 범위를 해당 zone에 바인딩한다(zone sharding).","when":"지역이나 하드웨어에 따라 데이터를 특정 샤드에 두려 할 때.","caution":"zone 범위는 샤드 키와 일치해야 한다. 잘못된 범위는 chunk 마이그레이션을 막는다.","tags":["sharding","zone","tag","region","range"]},{"id":"sh-move-chunk","task":"chunk 수동 이동","category":"샤딩","command":"sh.moveChunk(\"shop.customer\", { customerId: 1000 }, \"shard0001\")","what":"지정한 key를 포함한 chunk를 대상 샤드로 수동 이동한다.","when":"불균등한 분산을 바로잡거나 특정 데이터를 배치하기 위해.","caution":"수동 이동은 balancer와 충돌할 수 있다. 큰 chunk 마이그레이션은 IO 부하와 짧은 락을 만든다.","tags":["sharding","movechunk","chunk","move","manual"]},{"id":"compact-collection","task":"컬렉션 compact","category":"유지보수","command":"db.runCommand({ compact: \"customer\" })","what":"컬렉션과 인덱스를 디스크에 다시 써서 여유 공간을 회수한다.","when":"대량 삭제/수정 후 디스크 공간을 회수하려 할 때.","caution":"4.4+에서 compact는 읽기/쓰기를 차단하지 않으며 같은 컬렉션의 일부 DDL 작업(drop, createIndex, dropIndex 등)만 차단한다. 그래도 IO가 많고 항상 운영체제에 공간을 반환하지는 않는다. 가능하면 secondary에서 유지보수 시간대에 실행한다.","tags":["compact","storage","maintenance","disk","storage"]},{"id":"create-index-background","task":"인덱스 생성 (비블로킹)","category":"유지보수","command":"db.customer.createIndex({ email: 1 }, { name: \"idx_email\" })","what":"지정한 필드에 인덱스를 생성한다. MongoDB 4.2부터 인덱스 빌드는 기본적으로 쓰기를 차단하지 않는다.","when":"쿼리 성능 향상을 위해 새 인덱스를 추가할 때.","caution":"큰 컬렉션에서 인덱스 빌드는 CPU와 IO를 소비하며 primary 부하가 증가할 수 있다.","since":"MongoDB 4.2","note":"MongoDB 4.2는 새로운 인덱스 빌드 프로세스를 도입했으며 인덱스 생성은 더 이상 기본적으로 쓰기를 차단하지 않는다. 따라서 이전 { background: true } 옵션은 불필요하며 4.2부터 무시된다.","tags":["index","createindex","background","create","performance"]},{"id":"collection-stats","task":"인덱스 및 컬렉션 크기 확인","category":"유지보수","command":"db.customer.stats({ indexDetails: true })","what":"컬렉션 크기, 저장 크기, 각 인덱스 크기를 보고한다.","when":"저장 공간 사용량과 인덱스 비용을 분석할 때.","caution":"읽기 전용 명령이며 위험 없음.","tags":["stats","index","size","collection","storage"]},{"id":"log-rotate","task":"로그 파일 교체(rotate)","category":"유지보수","command":"db.adminCommand({ logRotate: 1 })","what":"현재 로그 파일을 닫고 새 파일을 연다(log rotation).","when":"로그 파일이 커졌을 때 또는 아카이빙 전.","caution":"OS 로그 rotation과 함께 사용하면 충돌하지 않도록 구성을 확인한다.","tags":["log","logrotate","rotate","maintenance","archive"]},{"id":"current-op-long","task":"장기 실행 작업 찾기","category":"유지보수","command":"db.currentOp({ \"active\": true, \"secs_running\": { $gt: 5 } })","what":"5초 넘게 실행 중인 활성 작업을 나열한다.","when":"성능 저하나 멈춤 중 문제 쿼리를 식별할 때.","caution":"읽기 전용 명령이며 위험 없음.","tags":["currentop","long","long-running","operation","diagnostics"]},{"id":"kill-op","task":"작업 종료","category":"유지보수","command":"db.killOp(12345)","what":"지정한 opid로 실행 중인 작업을 종료한다.","when":"쿼리가 시스템을 멈추게 하거나 지나치게 오래 실행될 때.","caution":"쓰기 작업을 종료하면 부분 변경이 남을 수 있다. 시스템/복제 작업은 종료하지 않는다.","tags":["killop","kill","operation","kill","diagnostics"]},{"id":"set-parameter","task":"런타임 파라미터 변경","category":"파라미터","command":"db.adminCommand({ setParameter: 1, cursorTimeoutMillis: 600000 })","what":"서버를 재시작하지 않고 런타임 파라미터를 변경한다.","when":"서버 동작을 일시적으로 조정하려 할 때.","caution":"변경은 영구적이지 않으며 재시작하면 구성 파일의 값으로 돌아간다. 잘못된 값은 성능을 저하시킬 수 있다.","tags":["setparameter","parameter","runtime","setting","admin"]},{"id":"set-profiling","task":"profiling 수준 설정","category":"파라미터","command":"db.setProfilingLevel(1, { slowms: 100 })","what":"느린 쿼리(100 ms 초과)를 system.profile 컬렉션에 기록한다(level 1).","when":"느린 쿼리를 진단하려 할 때.","caution":"level 2는 모든 작업을 기록하여 상당한 오버헤드를 추가하며 profiling 데이터가 디스크 공간을 차지한다.","tags":["profiling","slowms","profiler","slow","diagnostics"]}]},"recipes/mongodb-ext.json":{"engineId":"mongodb","engine":"MongoDB","order":2,"recipes":[{"id":"rs-oplog-window","task":"oplog window와 복제 지연 측정","category":"Replica Set","command":"// Oplog size and the time span it covers (oplog window):\nrs.printReplicationInfo()\n// Lag of each secondary relative to the primary:\nrs.printSecondaryReplicationInfo()\n// Lag in seconds and sync source, for monitoring/scripts:\nconst s = rs.status(); const p = s.members.find(m => m.stateStr === \"PRIMARY\");\ns.members.filter(m => m.stateStr === \"SECONDARY\").map(m => ({ host: m.name, lagSec: (p.optimeDate - m.optimeDate) / 1000, syncSource: m.syncSourceHost }))\n// Window (hours):\ndb.getReplicationInfo().timeDiffHours","what":"oplog가 몇 시간 분량의 쓰기를 보관하는지(oplog window)와 각 secondary가 primary보다 몇 초 뒤처졌는지를 sync source와 함께 보여 준다.","when":"복제 지연 알림 시, 계획된 유지보수 전 멤버가 얼마나 오래 다운될 수 있는지 계산할 때, 대량 쓰기 작업 후.","caution":"멤버가 oplog window보다 오래 다운되면 'too stale' 상태가 되어 초기 동기화가 필요하다. 대량 update/delete 작업은 몇 시간 안에 window를 줄일 수 있으므로 피크 부하 시의 값으로 판단한다. 지연 계산은 optimeDate 차이를 기준으로 한다. primary에 쓰기가 없으면 지연이 높아 보이지 않지만 window도 줄지 않는다.","since":"MongoDB 4.4","note":"rs.printSlaveReplicationInfo()는 4.4에서 rs.printSecondaryReplicationInfo()로 이름이 바뀌었다. window가 너무 작으면 replSetResizeOplog로 oplog를 키운다(rs-oplog-resize 참고). delayed 멤버는 secondaryDelaySecs(4.4에서는 slaveDelay)만큼 뒤처져 보이므로 지연 알림에서 제외한다.","tags":["oplog","oplog window","replication lag","delay","secondary","printReplicationInfo"],"untested":true},{"id":"rs-oplog-resize","task":"oplog 온라인 크기 조정 (replSetResizeOplog)","category":"Replica Set","command":"// Current oplog size (MB):\ndb.getSiblingDB(\"local\").oplog.rs.stats().maxSize / 1024 / 1024\n// Resize the oplog to 50 GB (in MB; run on each member separately, secondaries first):\ndb.adminCommand({ replSetResizeOplog: 1, size: 51200 })\n// Minimum retention period (hours), independent of size:\ndb.adminCommand({ replSetResizeOplog: 1, minRetentionHours: 48 })\n// Verify the result:\nrs.printReplicationInfo()","what":"실행 중인 멤버의 oplog를 재시작 없이 늘리거나 줄인다. minRetentionHours는 크기 한도를 넘더라도 최소 기간 동안 oplog 항목을 유지한다.","when":"oplog window가 유지보수나 초기 동기화 시간을 감당하지 못하거나, 대량 쓰기 작업이 window를 줄일 때.","caution":"명령은 실행한 멤버에만 적용되며 복제되지 않으므로 모든 멤버에 적용한다. sync source 역할을 하는 멤버에서도 window가 충분해야 한다. 축소해도 디스크 공간이 OS에 즉시 반환되지 않으며 이를 위해서는 oplog.rs에 compact가 필요하다. minRetentionHours를 쓰면 쓰기량에 따라 oplog가 size를 넘어 커질 수 있으므로 디스크를 그에 맞게 계획한다.","since":"MongoDB 4.4","note":"새 크기는 재시작 후에도 유지되지만 명령으로 설정한 minRetentionHours는 유지되지 않으므로 storage.oplogMinRetentionHours도 설정한다. size는 최소 990 MB여야 한다. 구성의 replication.oplogSizeMB 변경은 oplog가 처음 생성될 때만 적용된다.","tags":["oplog","resize","replSetResizeOplog","minRetentionHours","size","oplog window"],"untested":true},{"id":"rs-force-reconfig","task":"과반수 상실 후 replica set 강제 재구성 (force reconfig)","category":"Replica Set","command":"// On a surviving member with the most recent optime (runs as a secondary):\nrs.status().members.map(m => ({ host: m.name, state: m.stateStr, health: m.health, optime: m.optimeDate }))\ncfg = rs.conf()\ncfg.members.map((m, i) => ({ i: i, host: m.host }))\n// Keep only the reachable members (example: members 0 and 2):\ncfg.members = [cfg.members[0], cfg.members[2]]\nrs.reconfig(cfg, { force: true })\n// Verify the new primary election:\nrs.status().members.map(m => ({ host: m.name, state: m.stateStr }))","what":"투표 멤버의 과반수를 영구적으로 잃었을 때(예: 데이터 센터 전체 소실), 남은 멤버로 새 구성을 만들어 set이 다시 primary를 선출할 수 있게 한다.","when":"재해 시나리오에서만: 과반수가 돌아오지 않고 set이 읽기 전용으로 멈춘 경우.","caution":"최후의 수단이다. 잃었던 멤버가 이후 이전 구성으로 돌아오면 split-brain과 rollback이 발생할 수 있으므로 네트워크에서 격리하고, 복귀시킬 때는 데이터를 지우고 초기 동기화로 추가한다. optime이 가장 최신인 멤버를 고른다. 그렇지 않으면 다른 멤버에서 majority-commit되지 않은 쓰기가 rollback된다. 일상적인 멤버 변경에는 force를 사용하지 않는다.","since":"MongoDB 4.4","note":"force reconfig는 config 버전을 큰 무작위 값만큼 올리며 secondary에서 실행할 수 있는 유일한 reconfig 형태다. 4.4+에서 일반 reconfig는 한 번에 최대 하나의 투표 멤버만 추가 또는 제거할 수 있으나 force는 이 제한을 받지 않는다.","tags":["force reconfig","majority","disaster","recovery","split-brain","rs.reconfig"],"untested":true},{"id":"rs-rolling-maintenance","task":"replica set 롤링 유지보수 (재시작, OS 패치, 마이너 업그레이드)","category":"Replica Set","command":"// 1) Before starting: all members healthy, low lag, and an oplog window longer than the maintenance\nrs.printSecondaryReplicationInfo()\n// 2) Clean shutdown on the SECONDARY under maintenance (then OS/binary work, restart with the same config):\ndb.getSiblingDB(\"admin\").shutdownServer({ timeoutSecs: 60 })\n// 3) Wait until the member is SECONDARY and lag has closed, then move to the next secondary:\nrs.status().members.map(m => ({ host: m.name, state: m.stateStr, health: m.health }))\n// 4) When all secondaries are done, on the PRIMARY: hand over to a secondary that catches up within 30 s\nrs.stepDown(120, 30)\n// 5) The old primary is now a SECONDARY: apply steps 2 and 3 to it","what":"secondary를 먼저, primary를 마지막으로 하여 멤버를 하나씩 유지보수에 들이는 순서를 제공한다. set을 서비스에서 빼지 않고 재시작, OS 패치, 바이너리 업데이트를 수행한다.","when":"계획된 OS/커널 업데이트, 마이너 버전 업그레이드, mongod 파라미터 변경, 디스크/하드웨어 유지보수 시.","caution":"한 번에 둘 이상의 멤버를 종료하지 않는다. 3멤버 set에서 두 멤버를 잃으면 과반수를 잃는다. 각 멤버 후 SECONDARY 상태와 지연 해소를 확인한다. stepDown은 짧은 쓰기 중단을 일으키므로 드라이버가 retryable writes로 재연결하는지 확인한다. arbiter가 있으면 같은 순서로 처리한다.","since":"MongoDB 4.4","note":"rs.stepDown(stepDownSecs, secondaryCatchUpPeriodSecs): 두 번째 파라미터는 primary가 secondary의 따라잡기를 기다리는 시간이다. 따라잡는 멤버가 없으면 stepDown이 실패하고 primary가 유지된다. 유지보수 중 특정 멤버가 primary가 되지 않게 하려면 해당 멤버에서 rs.freeze(seconds)를 실행한다.","tags":["rolling","maintenance","restart","stepDown","upgrade","patching"],"untested":true},{"id":"sh-balancer-window","task":"balancer 상태, 활성 window, 컬렉션별 밸런싱","category":"샤딩","command":"// Is the balancer enabled, is it running a round right now:\nsh.getBalancerState()\ndb.adminCommand({ balancerStatus: 1 })\n// Run the balancer only between 01:00 and 05:00 (on mongos):\ndb.getSiblingDB(\"config\").settings.updateOne({ _id: \"balancer\" }, { $set: { activeWindow: { start: \"01:00\", stop: \"05:00\" } } }, { upsert: true })\n// Remove the window:\ndb.getSiblingDB(\"config\").settings.updateOne({ _id: \"balancer\" }, { $unset: { activeWindow: true } })\n// Disable / enable balancing for a single collection:\nsh.disableBalancing(\"shop.musteri\")\nsh.enableBalancing(\"shop.musteri\")\n// Whether the collection is balanced (4.4+):\ndb.adminCommand({ balancerCollectionStatus: \"shop.musteri\" })","what":"balancer가 활성화되어 있고 라운드를 실행 중인지 보여 주고, chunk 마이그레이션을 시간 window로 제한하며, 단일 컬렉션의 밸런싱을 비활성화 또는 활성화한다.","when":"chunk 마이그레이션이 주간 성능을 해칠 때, 백업 window 중, 또는 한 컬렉션의 마이그레이션 문제를 격리할 때.","caution":"너무 좁은 window는 데이터 증가를 따라가지 못해 샤드가 불균형해지므로 balancerCollectionStatus로 정기적으로 균형을 확인한다. window가 닫히기 전에 시작된 마이그레이션은 끝날 때까지 실행된다. 컬렉션별 밸런싱을 비활성화한 채 두면 그 컬렉션은 단일 샤드에서 계속 커진다.","since":"MongoDB 6.0","note":"activeWindow 시각은 config server replica set primary의 로컬 시간대로 평가된다. 6.0.3+에서 balancer는 chunk 수가 아니라 샤드 간 데이터 크기 차이로 판단하므로 데이터가 균형 상태여도 sh.status()의 chunk 수는 불균등해 보일 수 있다.","tags":["balancer","activeWindow","window","migration","sharding","balancerCollectionStatus"],"untested":true},{"id":"sh-jumbo-chunks","task":"jumbo chunk 찾기, 분할, 플래그 해제","category":"샤딩","command":"// Chunks flagged as jumbo (5.0+ config.chunks links to the collection by uuid):\nconst uuid = db.getSiblingDB(\"config\").collections.findOne({ _id: \"shop.musteri\" }).uuid;\ndb.getSiblingDB(\"config\").chunks.find({ uuid: uuid, jumbo: true }, { min: 1, max: 1, shard: 1 })\n// If the chunk can be split, split it at the median:\nsh.splitFind(\"shop.musteri\", { musteriId: 1000 })\n// Last resort for an indivisible chunk, clear the flag manually (a successful split already clears it):\ndb.adminCommand({ clearJumboFlag: \"shop.musteri\", find: { musteriId: 1000 } })\n// If a single key value is too large, refine the key (4.4+):\ndb.adminCommand({ refineCollectionShardKey: \"shop.musteri\", key: { musteriId: 1, siparisId: 1 } })","what":"balancer가 이동하지 못해 jumbo로 표시된 chunk를 나열하고, 분할 가능한 것은 분할한 뒤 jumbo 플래그를 해제한다. 단일 값으로 된 거대한 chunk를 위한 샤드 키 refine 방법을 보여 준다.","when":"한 샤드가 다른 샤드보다 눈에 띄게 큰 상태로 유지될 때, sh.status() 출력에 jumbo가 나타날 때, 또는 balancer가 마이그레이션 오류를 보고할 때.","caution":"모든 문서가 같은 샤드 키 값을 가진 chunk는 분할할 수 없다. 지속적인 해결책은 refineCollectionShardKey 또는 reshardCollection으로 샤드 키를 바꾸는 것뿐이다. refineCollectionShardKey는 새 key를 지원하는 인덱스가 미리 있어야 한다. 분할 없이 jumbo 플래그만 해제하면 balancer가 같은 chunk를 다시 시도하고 또 실패한다.","since":"MongoDB 5.0","note":"카디널리티가 낮거나 한 값에 집중된 shard key(예: status, country)가 jumbo chunk의 가장 흔한 원인이다. 5.0 이전에는 config.chunks 문서를 ns 필드로 조회한다. clearJumboFlag는 4.2.3/4.0.15에서 도입되었다.","tags":["jumbo","chunk","split","clearJumboFlag","shard key","refineCollectionShardKey"],"untested":true},{"id":"sh-shard-key-check","task":"샤드 키 선택과 데이터 분포 점검 (getShardDistribution, analyzeShardKey)","category":"샤딩","command":"// Data and document distribution of the collection across shards:\ndb.getSiblingDB(\"shop\").musteri.getShardDistribution()\n// Cardinality, frequency and monotonicity analysis of the current or candidate shard key:\ndb.adminCommand({ analyzeShardKey: \"shop.musteri\", key: { musteriId: 1 }, keyCharacteristics: true, readWriteDistribution: false })\n// For read/write distribution, enable query sampling first (10 samples per second):\ndb.adminCommand({ configureQueryAnalyzer: \"shop.musteri\", mode: \"full\", samplesPerSecond: 10 })\ndb.adminCommand({ analyzeShardKey: \"shop.musteri\", key: { musteriId: 1 }, keyCharacteristics: false, readWriteDistribution: true })\n// Turn sampling off when done:\ndb.adminCommand({ configureQueryAnalyzer: \"shop.musteri\", mode: \"off\" })","what":"컬렉션 데이터가 각 샤드에 몇 퍼센트 있는지 보여 준다. analyzeShardKey는 현재 또는 후보 샤드 키의 카디널리티, 최빈값, 단조성을 보고하고, 샘플링된 쿼리가 단일 샤드를 대상으로 하는지 모든 샤드를 대상으로 하는지(scatter-gather) 보고한다.","when":"새 컬렉션을 샤딩하기 전, hot shard나 불균등한 증가가 발견될 때, reshardCollection을 결정하기 전.","caution":"analyzeShardKey는 key를 지원하는 인덱스가 필요하고 큰 컬렉션에서는 문서를 샘플링하므로 피크 시간에 읽기 부하를 추가한다. query sampling이 켜져 있는 동안 샘플은 config.sampledQueries에 기록되므로 분석이 끝나면 끈다. 단조 증가 key(ObjectId, timestamp)는 모든 insert를 한 샤드에 몰리게 한다.","since":"MongoDB 7.0","note":"analyzeShardKey와 configureQueryAnalyzer는 7.0에서 도입되었으며 replica set에서도 동작한다. 좋은 샤드 키는 카디널리티가 높고, 빈도가 낮으며, 단조적이지 않고, 빈번한 쿼리의 필터에 나타난다. hashed key는 분산 문제를 해결하지만 범위 쿼리를 scatter-gather로 만든다.","tags":["shard key","analyzeShardKey","getShardDistribution","cardinality","distribution","hot shard"],"untested":true},{"id":"explain-execstats","task":"실제 실행 통계로 쿼리 실행 계획 검사 (explain executionStats)","category":"성능","command":"db.musteri.find({ sehir: \"Ankara\", durum: \"aktif\" }).sort({ kayitTarihi: -1 }).explain(\"executionStats\")\n// Summary: winning plan, returned documents, examined keys/documents and time\nconst e = db.musteri.find({ sehir: \"Ankara\", durum: \"aktif\" }).sort({ kayitTarihi: -1 }).explain(\"executionStats\");\n({ plan: e.queryPlanner.winningPlan, n: e.executionStats.nReturned, keys: e.executionStats.totalKeysExamined, docs: e.executionStats.totalDocsExamined, ms: e.executionStats.executionTimeMillis })\n// For an aggregation:\ndb.musteri.explain(\"executionStats\").aggregate([{ $match: { sehir: \"Ankara\" } }, { $group: { _id: \"$durum\", n: { $sum: 1 } } }])","what":"쿼리를 실행하고 채택된 계획(IXSCAN/COLLSCAN, SORT 단계), 반환 문서 수, 검사한 인덱스 key와 문서 수, 소요 시간을 보여 준다.","when":"profiler나 로그에서 느리게 나타나는 쿼리를 분석할 때, 그리고 새 인덱스가 실제로 사용되는지 확인할 때.","caution":"executionStats는 쿼리를 실제로 실행하므로 피크 시간에 무거운 쿼리를 explain하면 부하도 추가된다. 쓰기 명령(update/delete/findAndModify)에서 explain은 데이터를 변경하지 않지만 실행 계획은 실행한다. totalDocsExamined/nReturned 비율이 높거나 계획에 in-memory SORT 단계가 있으면 인덱스가 적합하지 않은 것이다.","since":"MongoDB 4.4","note":"복합 인덱스 설계 시 ESR 규칙을 적용한다: Equality 필드 먼저, 그다음 Sort, 그다음 Range. 거부된 계획도 보려면 explain(\"allPlansExecution\")을 사용한다. 7.0+에서 쿼리가 Slot-Based Engine으로 실행되면 winningPlan에 queryPlan 및 slotBasedPlan 필드가 포함된다.","tags":["explain","executionStats","IXSCAN","COLLSCAN","plan","index"]},{"id":"profiler-analyze","task":"profiler 데이터에서 가장 느린 쿼리와 query shape 추출","category":"성능","command":"// Recent slow operations (in the db where profiling is on; see set-profiling):\ndb.system.profile.find({ millis: { $gte: 100 } }, { ts: 1, ns: 1, op: 1, millis: 1, planSummary: 1, keysExamined: 1, docsExamined: 1, nreturned: 1, appName: 1 }).sort({ ts: -1 }).limit(20)\n// Total time per query shape (those consuming the most time):\ndb.system.profile.aggregate([\n  { $group: { _id: { ns: \"$ns\", plan: \"$planSummary\", shape: \"$queryHash\" }, n: { $sum: 1 }, totalMs: { $sum: \"$millis\" }, maxMs: { $max: \"$millis\" }, avgDocs: { $avg: \"$docsExamined\" } } },\n  { $sort: { totalMs: -1 } },\n  { $limit: 10 }\n])\n// Queries not using an index (COLLSCAN):\ndb.system.profile.find({ planSummary: \"COLLSCAN\" }, { ns: 1, millis: 1, command: 1 }).sort({ millis: -1 }).limit(10)","what":"system.profile 컬렉션의 기록에서 최근 느린 작업, query shape(queryHash)별 총 시간, 인덱스를 사용하지 않는 COLLSCAN 쿼리를 나열한다.","when":"성능 저하 불만 시, 어떤 쿼리가 서버 시간을 소비하는지 찾고 인덱스 필요성을 판단하기 위해.","caution":"system.profile은 기본적으로 1 MB capped collection이며 데이터베이스별로 존재한다. 바쁜 시스템에서는 몇 분 만에 기록이 순환되므로 profiling이 켜져 있는 동안이나 직후에 분석한다. profiling level 2 또는 slowms 0은 모든 작업을 기록하여 상당한 오버헤드를 추가한다. 기록은 실행된 멤버에만 보관되며 복제되지 않는다.","since":"MongoDB 4.4","note":"profiling이 꺼져 있어도 slowms(기본 100 ms)를 넘는 작업은 mongod 로그에 'Slow query'로 기록된다. 로그의 attr.durationMillis와 attr.planSummary로 같은 진단을 할 수 있다. 8.0+에서는 query shape에 대한 planCacheShapeHash 필드도 기록된다.","tags":["profiler","system.profile","slow query","top queries","COLLSCAN","queryHash"]},{"id":"index-usage-hide","task":"미사용 인덱스 찾기 및 삭제 전 숨기기 (hideIndex)","category":"성능","command":"// Usage counter per index (on this member, since the last restart):\ndb.musteri.aggregate([{ $indexStats: {} }, { $project: { name: 1, ops: \"$accesses.ops\", since: \"$accesses.since\" } }, { $sort: { ops: 1 } }])\n// Hide it from the planner before dropping (the index keeps being maintained):\ndb.musteri.hideIndex(\"idx_email\")\n// Revert immediately if problems appear:\ndb.musteri.unhideIndex(\"idx_email\")\n// If the observation period passes without issues:\ndb.musteri.dropIndex(\"idx_email\")","what":"$indexStats는 각 인덱스가 몇 번 사용되었는지 보여 준다. hideIndex는 인덱스를 삭제하지 않고 쿼리 플래너에서 숨겨 제거 효과를 되돌릴 수 있는 방식으로 테스트하게 한다.","when":"쓰기 오버헤드와 저장 공간을 늘리는 불필요한 인덱스를 정리할 때.","caution":"카운터는 멤버별이며 재시작 시 초기화된다. 읽기가 secondary로 간다면 모든 멤버를 확인하고 충분히 긴 기간(월말 리포트 포함) 동안 관찰한다. ops가 0인 unique 또는 TTL 인덱스도 여전히 필요할 수 있다: unique 제약과 TTL 삭제는 카운터에 나타나지 않는다. hidden 인덱스도 쓰기 시 유지되며, hidden unique 인덱스는 여전히 제약을 강제한다.","since":"MongoDB 4.4","note":"hideIndex는 featureCompatibilityVersion 4.4 이상이 필요하다. _id 인덱스는 숨길 수 없다. unhideIndex로 다시 보이게 하는 데 재구축이 필요 없으며 즉시 적용된다.","tags":["indexStats","hideIndex","unused index","index cleanup","dropIndex","index"]},{"id":"connections-by-app","task":"애플리케이션 및 클라이언트별 연결 수 집계 (connection storm 진단)","category":"성능","command":"// Total, available and rejected connection counts:\ndb.serverStatus().connections\n// Count connections by application name and client IP:\ndb.getSiblingDB(\"admin\").aggregate([\n  { $currentOp: { allUsers: true, idleConnections: true } },\n  { $group: { _id: { app: \"$appName\", client: { $arrayElemAt: [{ $split: [\"$client\", \":\"] }, 0] } }, conns: { $sum: 1 }, active: { $sum: { $cond: [\"$active\", 1, 0] } } } },\n  { $sort: { conns: -1 } },\n  { $limit: 20 }\n])","what":"서버의 총 연결 수와 사용 가능 연결 수를 보여 준 뒤, 열린 연결을 appName과 클라이언트 IP별로 활성 연결 수와 함께 그룹화한다.","when":"연결 수가 갑자기 급증할 때, 'connection refused' 또는 pool timeout 오류 시, 어떤 애플리케이션 서버가 연결을 누수하는지 찾을 때.","caution":"idleConnections: true는 모든 유휴 연결도 반환하므로 연결이 수천 개인 시스템에서는 출력이 크다. 그래서 $group으로 요약하는 것이 중요하다. client가 null인 행은 내부 스레드다. 연결 하나는 약 1 MB의 메모리를 소비하며 connection storm은 WiredTiger 캐시 밖의 메모리도 증가시킨다.","since":"MongoDB 4.4","note":"지속적인 해결책은 보통 드라이버 측에 있다: maxPoolSize, maxIdleTimeMS, 애플리케이션 인스턴스 수. 애플리케이션이 appName을 설정하지 않으면 구분하기 어려우므로 연결 문자열에 appName=<service>를 요청한다. net.maxIncomingConnections와 OS ulimit -n 값이 하드 상한을 정한다.","tags":["connections","connection count","currentOp","appName","connection storm","pool"]},{"id":"index-build-monitor","task":"진행 중인 인덱스 빌드 모니터링 및 중단","category":"유지보수","command":"// Index builds in progress and their completion percentage:\ndb.getSiblingDB(\"admin\").aggregate([\n  { $currentOp: { allUsers: true } },\n  { $match: { $or: [{ \"command.createIndexes\": { $exists: true } }, { desc: /^IndexBuildsCoordinator/ }] } },\n  { $project: { opid: 1, desc: 1, ns: 1, secs_running: 1, msg: 1, progress: 1, \"command.indexes.name\": 1 } }\n])\n// Abort the build (on the primary; in a replica set it stops on all members):\ndb.musteri.dropIndex(\"idx_email\")","what":"실행 중인 인덱스 빌드를 스캔한 문서 수와 진행률(progress.done / progress.total)과 함께 나열하고, 필요하면 dropIndex로 빌드를 중단한다.","when":"큰 컬렉션에서 시작한 createIndex가 언제 끝날지 추정하거나, 피크 시간에 부하를 만드는 빌드를 중지하기 위해.","caution":"인덱스 빌드는 killOp가 아니라 dropIndex/dropIndexes로 중단한다. replica set에서는 primary에서 실행해야 중단이 모든 멤버로 전파된다. 빌드 중에는 컬렉션의 일부 DDL 작업이 대기한다. 빌드 끝에 짧은 배타 락이 잡히며, 쓰기가 매우 많으면 그 순간 지연이 생길 수 있다.","since":"MongoDB 4.4","note":"4.4+ replica set에서 인덱스 빌드는 데이터를 가진 모든 멤버에서 동시에 실행되며 commitQuorum(기본 votingMembers)이 충족되면 커밋된다. secondary가 뒤처지면 primary에서도 빌드가 끝나지 않는다. 5.0+에서는 정상 종료로 중단된 빌드가 재시작 후 중단된 지점부터 재개된다.","tags":["index build","createIndex","progress","monitor","dropIndex","commitQuorum"]},{"id":"rolling-index-build","task":"replica set 롤링 인덱스 빌드 (멤버별)","category":"유지보수","command":"// 1) Shut down a SECONDARY; comment out replication.replSetName and start it standalone on another port:\n//    mongod --config /etc/mongod.conf --port 27217 --setParameter disableLogicalSessionCacheRefresh=true\n// 2) Build the index on the standalone member:\ndb.getSiblingDB(\"shop\").musteri.createIndex({ email: 1 }, { name: \"idx_email\" })\n// 3) Restart with the normal configuration; wait until it is SECONDARY and lag has closed:\nrs.printSecondaryReplicationInfo()\n// 4) When all secondaries are done, step down the primary and apply steps 1-3 to the old primary:\nrs.stepDown(120, 30)","what":"멤버를 일시적으로 set 밖에서 standalone으로 실행하여 각 멤버에 따로 인덱스를 빌드한다. primary는 빌드 부하를 전혀 지지 않는다.","when":"매우 큰 컬렉션에서 기본 분산 빌드가 primary와 복제 지연에 주는 영향을 받아들일 수 없을 때.","caution":"각 멤버의 유지보수 시간은 oplog window보다 짧아야 한다. 그렇지 않으면 멤버가 돌아올 때 too stale 상태가 된다. 롤링 방식으로 unique 인덱스를 빌드할 때는 빌드 전체 동안 컬렉션 쓰기를 중지한다. 그렇지 않으면 일부 멤버에 위반 문서가 생길 수 있다. 애플리케이션이 접속하지 않도록 standalone 멤버는 다른 포트로 시작한다.","since":"MongoDB 4.4","note":"4.4+에서는 기본 빌드가 이미 비블로킹이며 모든 멤버에서 동시에 실행된다. 롤링 방식은 그 부하를 받아들일 수 없을 때만 필요하다. sharded cluster에서는 balancer를 먼저 중지한 뒤 각 샤드 replica set마다 따로 절차를 수행한다.","tags":["rolling index","index build","standalone","replica","createIndex","maintenance"],"untested":true},{"id":"ttl-index","task":"TTL 인덱스 생성, 만료 시간 변경, TTL 삭제 모니터링","category":"유지보수","command":"// Automatically delete documents lastSeen + 3600 s later:\ndb.oturum.createIndex({ lastSeen: 1 }, { expireAfterSeconds: 3600, name: \"ttl_lastSeen\" })\n// Change the expiry without rebuilding the index:\ndb.runCommand({ collMod: \"oturum\", index: { name: \"ttl_lastSeen\", expireAfterSeconds: 7200 } })\n// TTL indexes on the collection and their expiry:\ndb.oturum.getIndexes().filter(i => i.expireAfterSeconds !== undefined)\n// TTL monitor statistics (deleted documents, passes):\ndb.serverStatus().metrics.ttl","what":"날짜 필드 기준으로 정해진 시간이 지나면 문서를 자동 삭제하는 TTL 인덱스를 생성하고, 재구축 없이 collMod로 만료 시간을 변경하며, TTL monitor가 삭제한 문서 수를 보여 준다.","when":"애플리케이션에 삭제 작업을 작성하지 않고 세션, 로그, 임시 토큰 같은 기한부 데이터를 정리하고, 커지는 컬렉션이 줄지 않는 이유를 조사하기 위해.","caution":"TTL monitor는 기본 60초마다 실행되므로 삭제는 즉시 일어나지 않는다. 필드는 Date 타입(또는 Date 배열)이어야 하며, 문자열 날짜나 필드가 없는 문서는 삭제되지 않는다. 만료 시간을 줄이면 많은 문서가 한 번에 삭제되어 쓰기 부하와 복제 지연이 발생한다. 삭제는 primary에서만 일어나며 oplog에 기록된다.","since":"MongoDB 5.1","note":"TTL 인덱스는 단일 필드여야 한다(복합 TTL 미지원). 기존 비TTL 단일 필드 인덱스에 collMod로 expireAfterSeconds를 추가하는 것은 5.1+에서 가능하며, 기존 TTL 만료 시간 변경은 이전 버전에서도 동작한다. 특정 시각에 삭제하려면 expireAfterSeconds: 0을 사용하고 삭제 시각을 필드에 저장한다.","tags":["ttl","expireAfterSeconds","collMod","auto delete","index","metrics.ttl"]},{"id":"wt-cache-pressure","task":"WiredTiger 캐시 압박 측정 (채움률, dirty 비율, 애플리케이션 eviction)","category":"성능","command":"const c = db.serverStatus().wiredTiger.cache;\nconst max = c[\"maximum bytes configured\"];\n({\n  maxGB: (max / 1024 ** 3).toFixed(1),\n  usedPct: (100 * c[\"bytes currently in the cache\"] / max).toFixed(1),\n  dirtyPct: (100 * c[\"tracked dirty bytes in the cache\"] / max).toFixed(1),\n  appThreadEvictedPages: c[\"pages evicted by application threads\"],\n  pagesReadIntoCache: c[\"pages read into cache\"]\n})\n// Read/write ticket usage and queue:\ndb.serverStatus().wiredTiger.concurrentTransactions","what":"WiredTiger 캐시 크기, 채움 및 dirty 비율, 애플리케이션 스레드가 강제로 evict한 페이지 수, 읽기/쓰기 ticket 상태를 보여 준다.","when":"디스크 읽기가 증가하거나, 지연이 갑자기 튀거나, 쓰기 처리량이 떨어질 때. 캐시 크기를 산정할 때.","caution":"채움이 80%(eviction_target)를 넘으면 백그라운드 eviction이 시작되고, 95%(eviction_trigger)에서는 애플리케이션 스레드도 eviction에 참여해 쿼리가 느려진다. dirty 데이터는 5%가 목표, 20%가 트리거이며 계속 20% 근처라면 디스크 쓰기 용량이 부족한 것이다. 증가하는 pages evicted by application threads 카운터는 사용자가 직접 체감하는 지연이다. 값은 누적 카운터이므로 두 샘플의 차이를 본다.","since":"MongoDB 7.0","note":"기본 캐시 크기는 (RAM - 1 GB) / 2와 256 MB 중 큰 값이며 컨테이너에서는 cgroup 한도로 계산된다. storage.wiredTiger.engineConfig.cacheSizeGB를 늘리면 OS page cache가 줄어든다. RAM의 절반을 넘겨도 도움이 되는 경우는 드물다. 7.0에서는 ticket 수가 동적으로 조정되며 8.0+에서는 ticket 지표가 serverStatus().queues.execution에도 나타난다.","tags":["wiredtiger","cache","eviction","dirty","tickets","memory"]},{"id":"dump-restore-archive","task":"mongodump/mongorestore로 아카이브 백업 및 단일 컬렉션 복원","category":"백업","command":"# Single database, one compressed archive file (password is prompted):\nmongodump --uri=\"mongodb://admin@mongo1:27017/?authSource=admin\" --db=shop --gzip --archive=/backup/shop_$(date +%F).archive.gz\n# Dry run before restoring the archive under a different name:\nmongorestore --uri=\"mongodb://admin@mongo1:27017/?authSource=admin\" --gzip --archive=/backup/shop_2026-10-08.archive.gz --nsFrom=\"shop.*\" --nsTo=\"shop_restore.*\" --dryRun\n# Restore a single collection from the archive under a new name:\nmongorestore --uri=\"mongodb://admin@mongo1:27017/?authSource=admin\" --gzip --archive=/backup/shop_2026-10-08.archive.gz --nsInclude=\"shop.musteri\" --nsFrom=\"shop.*\" --nsTo=\"shop_restore.*\" --numInsertionWorkersPerCollection=4\n# Restore under the same name, dropping the existing collection:\nmongorestore --uri=\"mongodb://admin@mongo1:27017/?authSource=admin\" --gzip --archive=/backup/shop_2026-10-08.archive.gz --nsInclude=\"shop.musteri\" --drop","what":"데이터베이스를 단일 gzip 아카이브 파일로 백업하고, 아카이브에서 데이터베이스 전체 또는 단일 컬렉션을 선택적으로 다른 이름(nsFrom/nsTo)으로 복원한다.","when":"논리 백업, 환경 간 데이터 이동, 또는 실수로 삭제되거나 손상된 단일 컬렉션 복구에.","caution":"mongodump는 논리 백업이다: 큰 데이터 세트에서는 느리고 복원 중 인덱스가 재구축된다. --oplog 없이 수행한 덤프는 운영 중인 시스템에서 시점 일관성이 없다. --drop은 복원 시작 전에 대상 컬렉션을 삭제하므로 먼저 nsTo로 다른 이름으로 복원해 확인한다. 복원은 primary에 쓰므로 복제 지연을 일으킬 수 있다.","since":"MongoDB 4.4","note":"4.4부터 데이터베이스 도구(mongodump/mongorestore)는 서버와 별도로 버전이 관리된다(100.x). 백업 중 primary의 읽기 부하를 덜려면 URI에 readPreference=secondary를 추가한다. 사용자와 롤도 옮기려면 --restoreDbUsersAndRoles와 함께 전체 인스턴스 덤프를 사용하거나 admin 데이터베이스를 따로 백업한다.","tags":["mongodump","mongorestore","backup","archive","nsFrom","restore"]},{"id":"pitr-oplog-replay","task":"oplog replay로 시점 복원 (잘못된 drop/delete 이전으로 되돌리기)","category":"백업","command":"# 1) Full backup + oplog written during the backup (whole instance, from a replica set member):\nmongodump --uri=\"mongodb://admin@mongo2:27017/?authSource=admin&readPreference=secondary\" --oplog --gzip --out=/backup/full\n# 2) Find the time of the bad operation (mongosh): db.getSiblingDB(\"local\").oplog.rs.find({ \"o.drop\": \"musteri\" }).sort({ $natural: -1 }).limit(1)\n# 3) Dump the oplog after the backup (t: start time of the full backup, epoch seconds):\nmongodump --uri=\"mongodb://admin@mongo2:27017/?authSource=admin\" --db=local --collection=oplog.rs --query='{\"ts\":{\"$gte\":{\"$timestamp\":{\"t\":1791400000,\"i\":1}}}}' --out=/backup/oplog\n# 4) Restore the full backup together with its own oplog:\nmongorestore --uri=\"mongodb://admin@target:27017/?authSource=admin\" --gzip --oplogReplay /backup/full\n# 5) Replay the oplog up to the bad operation (oplogLimit is exclusive):\nmkdir /backup/pitr && mv /backup/oplog/local/oplog.rs.bson /backup/pitr/oplog.bson\nmongorestore --uri=\"mongodb://admin@target:27017/?authSource=admin\" --oplogReplay --oplogLimit=1791403600:1 /backup/pitr","what":"--oplog로 전체 백업을 일관성 있게 만든 다음, 백업 이후 누적된 oplog를 잘못된 작업(drop, 대량 삭제)의 timestamp까지 재생하여 데이터를 그 시점으로 되돌린다.","when":"컬렉션이 실수로 삭제되었거나 잘못된 대량 update/delete가 실행되어 손상 직전의 마지막 상태가 필요할 때.","caution":"운영 시스템이 아닌 별도 대상에 복원하고, 검증 후 필요한 데이터를 옮긴다. 백업 시작부터 잘못된 작업까지의 oplog가 아직 oplog window 안에 있어야 한다. window 밖으로 밀려난 작업은 복구할 수 없다. --oplog는 전체 인스턴스 덤프에서만 동작한다(--db와 함께 사용할 수 없음). sharded cluster에서는 mongodump로 일관된 PITR이 불가능하므로 파일 시스템 스냅샷이나 백업 제품이 필요하다.","since":"MongoDB 4.4","note":"--oplogLimit=<seconds>:<ordinal>은 지정한 timestamp와 그 이후의 항목을 적용하지 않으므로 잘못된 작업의 ts를 그대로 넘기면 해당 작업이 제외된다. ts 값은 oplog에 Timestamp(t, i)로 나타난다. window가 닫히기 전에 잘못된 작업 직후 oplog를 덤프한다.","tags":["pitr","point in time","oplogReplay","oplogLimit","rollback","mongorestore"],"untested":true},{"id":"user-role-admin","task":"사용자 정의 롤 정의, 사용자 생성, 롤 부여/회수, 비밀번호 변경","category":"보안","command":"use admin\ndb.createRole({\n  role: \"opsRead\",\n  privileges: [\n    { resource: { db: \"shop\", collection: \"\" }, actions: [\"find\", \"listCollections\", \"listIndexes\", \"collStats\", \"dbStats\"] },\n    { resource: { cluster: true }, actions: [\"serverStatus\", \"inprog\"] }\n  ],\n  roles: []\n})\ndb.createUser({ user: \"shop_app\", pwd: passwordPrompt(), roles: [{ role: \"readWrite\", db: \"shop\" }, { role: \"opsRead\", db: \"admin\" }] })\ndb.grantRolesToUser(\"shop_app\", [{ role: \"clusterMonitor\", db: \"admin\" }])\ndb.revokeRolesFromUser(\"shop_app\", [{ role: \"clusterMonitor\", db: \"admin\" }])\ndb.changeUserPassword(\"shop_app\", passwordPrompt())\ndb.getUser(\"shop_app\", { showPrivileges: true })\ndb.runCommand({ usersInfo: { forAllDBs: true } })","what":"특정 데이터베이스 및 클러스터 action을 가진 사용자 정의 롤을 정의하고, 그 롤과 내장 롤로 사용자를 만들며, 롤을 부여하고 회수하고, 비밀번호를 변경하고, 모든 데이터베이스의 사용자를 나열한다.","when":"최소 권한으로 애플리케이션 및 모니터링 계정을 만들 때, 접근 검토나 비밀번호 교체 중.","caution":"사용자가 정의된 데이터베이스(여기서는 admin)가 authSource로 사용되며 애플리케이션의 연결 문자열과 일치해야 한다. 애플리케이션 계정에 root/dbOwner 같은 넓은 롤을 부여하지 않는다. 롤 변경은 기존 세션에 즉시 적용된다. 비밀번호 변경은 열린 연결을 끊지 않으며 새 연결에 적용된다.","since":"MongoDB 4.4","note":"passwordPrompt()는 셸 이력이나 프로세스 목록에 남기지 않고 비밀번호를 묻는다. replica set과 sharded cluster에서 사용자는 primary/mongos를 통해 관리되며 복제된다. 사용자의 유효 권한을 보려면 getUser에서 showPrivileges: true를 사용한다.","tags":["createUser","createRole","grantRolesToUser","privileges","rbac","password"]},{"id":"fcv-upgrade","task":"featureCompatibilityVersion (fCV) 및 메이저 업그레이드 순서 확인","category":"업그레이드","command":"// 1) Current fCV (must be the same on all members; targetVersion appears if a transition is incomplete):\ndb.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })\n// 2) Upgrade binaries in a rolling fashion (secondaries -> stepDown -> old primary); fCV stays old\ndb.version()\n// 3) After the observation period, raise fCV on the primary (on mongos in a sharded cluster):\ndb.adminCommand({ setFeatureCompatibilityVersion: \"7.0\", confirm: true })\n// 4) If a rollback is needed, fCV is lowered BEFORE the binaries:\ndb.adminCommand({ setFeatureCompatibilityVersion: \"6.0\", confirm: true })","what":"실행 중인 fCV를 보여 주고, 별도 단계인 바이너리 업그레이드와 fCV 업그레이드를 수행하는 순서를 제공한다.","when":"메이저 버전 업그레이드(예: 6.0 -> 7.0)를 계획할 때, 업그레이드 후 새 기능을 사용할 수 없을 때, 또는 다음 메이저로 넘어가기 전.","caution":"메이저 버전은 건너뛸 수 없다: 5.0 -> 7.0은 먼저 6.0으로 옮기고 fCV를 6.0으로 설정한다. fCV를 올리면 데이터 파일이 새 형식으로 바뀔 수 있어 바이너리 다운그레이드가 어려워지므로 새 버전이 며칠간 문제없이 동작한 뒤에만 fCV를 올린다. 7.0+ Community에서는 바이너리 다운그레이드가 공식 지원되지 않으므로 대비책은 백업을 기반으로 한다. 업그레이드 전에 드라이버 호환성을 확인한다.","since":"MongoDB 7.0","note":"7.0부터 setFeatureCompatibilityVersion에는 confirm: true가 필요하다. fCV 전환이 실패하면 getParameter 출력에 targetVersion/previousVersion이 나타나며, 같은 명령을 다시 실행해 완료한다. fCV 값은 replica set 내에서 복제된다.","tags":["fcv","featureCompatibilityVersion","upgrade","major upgrade","major","downgrade"]}]},"recipes/redis.json":{"engineId":"redis","engine":"Redis","order":6,"recipes":[{"id":"bgrewriteaof","task":"AOF 파일 재작성 (compact)","category":"영속성","command":"BGREWRITEAOF","what":"백그라운드에서 AOF 파일을 더 압축된 형태로 재작성한다.","when":"AOF 파일이 비대해졌을 때, 재시작 시간을 줄이기 위해.","caution":"fork 중 메모리와 IO 사용량이 증가하므로 피크 시간에는 주의한다.","tags":["aof","rewrite","persistence","persistence"]},{"id":"bgsave","task":"백그라운드에서 RDB 스냅샷 생성","category":"영속성","command":"BGSAVE","what":"fork된 자식 프로세스를 사용해 백그라운드에서 RDB 스냅샷을 디스크에 기록한다.","when":"백업을 하거나 스냅샷을 만들려 할 때.","caution":"fork 시점에 copy-on-write로 인해 메모리 사용량이 일시적으로 증가할 수 있다.","tags":["rdb","bgsave","snapshot","persistence","backup"]},{"id":"save","task":"동기 RDB 스냅샷 생성","category":"영속성","command":"SAVE","what":"메인 스레드에서 동기적으로 RDB 스냅샷을 디스크에 기록한다.","when":"클라이언트를 서비스하지 않는 유지보수/종료 직전에만.","caution":"SAVE는 서버 전체를 블로킹하여 끝날 때까지 어떤 명령도 처리되지 않으므로 운영 환경에서는 BGSAVE를 사용한다.","tags":["rdb","save","snapshot","blocking","persistence"]},{"id":"lastsave","task":"마지막 성공 스냅샷 시각 확인","category":"영속성","command":"LASTSAVE","what":"마지막으로 성공한 RDB 저장의 Unix timestamp를 반환한다.","when":"BGSAVE가 완료되었는지 확인하기 위해.","caution":"읽기 전용 명령이며 위험 없음.","tags":["lastsave","rdb","timestamp","check","persistence"]},{"id":"config-rewrite","task":"실행 중인 구성을 디스크에 기록","category":"영속성","command":"CONFIG REWRITE","what":"런타임에 CONFIG SET으로 변경한 내용을 redis.conf 파일에 저장한다.","when":"런타임 변경이 재시작 후에도 유지되게 하려 할 때.","caution":"기존 redis.conf 파일을 덮어쓰므로 파일이 정의되어 있고 쓰기 가능해야 한다.","tags":["config","rewrite","persistent","conf","setting"]},{"id":"config-appendonly","task":"AOF 활성화 또는 비활성화","category":"영속성","command":"CONFIG SET appendonly yes","what":"런타임에 AOF 영속성을 활성화(yes)하거나 비활성화(no)한다.","when":"재시작 없이 AOF 영속성을 켜거나 끄려 할 때.","caution":"AOF를 비활성화하면 내구성 보장이 약해진다. 활성화하면 초기 AOF 재작성이 IO 부하를 만든다.","tags":["aof","appendonly","config","persistence","setting"]},{"id":"config-save-rdb","task":"RDB 스냅샷 규칙 설정","category":"영속성","command":"CONFIG SET save \"900 1 300 10 60 10000\"","what":"자동 RDB 스냅샷을 트리거하는 규칙(초/변경 수)을 설정한다.","when":"자동 스냅샷 빈도를 변경하려 할 때.","caution":"빈 값(\"\")을 넘기면 자동 RDB 저장이 완전히 비활성화되어 영속성을 잃을 수 있다.","tags":["rdb","save","config","snapshot","setting"]},{"id":"replicaof","task":"노드를 replica로 만들기","category":"복제","command":"REPLICAOF redis-master 6379","what":"이 노드를 지정한 호스트와 포트의 master에 대한 replica로 만들고 동기화를 시작한다.","when":"새 읽기 replica를 추가하거나 역할을 할당할 때.","caution":"replica가 되는 노드의 기존 데이터는 폐기되고 master에서 전체 동기화를 받는다.","since":"Redis 5.0","note":"REPLICAOF 명령은 Redis 5.0에서 SLAVEOF를 대체했다. 이전 SLAVEOF 명령도 별칭으로 계속 동작한다.","tags":["replicaof","replica","replication","master","sync"]},{"id":"replicaof-no-one","task":"replica를 master로 승격","category":"복제","command":"REPLICAOF NO ONE","what":"노드의 replica 연결을 끊어 독립 master로 만든다.","when":"페일오버 시나리오에서 replica를 승격하려 할 때.","caution":"승격 후 이전 master와 데이터 분기(split-brain)가 없도록 토폴로지를 신중하게 관리한다.","since":"Redis 5.0","note":"REPLICAOF NO ONE은 Redis 5.0에서 SLAVEOF NO ONE을 대체했다. 이전 SLAVEOF NO ONE도 별칭으로 계속 동작한다.","tags":["replicaof","promote","master","failover","replication"]},{"id":"cluster-failover","task":"수동 페일오버 수행","category":"클러스터","command":"CLUSTER FAILOVER","what":"replica에서 실행하면 해당 replica를 master로 승격하는 통제된 페일오버를 시작한다.","when":"유지보수 전에 master 역할을 replica에 넘기려 할 때.","caution":"이 명령은 master 자신이 아니라 대상 replica에서 실행해야 한다.","tags":["cluster","failover","promote","maintenance","replication"]},{"id":"cluster-meet","task":"클러스터에 노드 추가","category":"클러스터","command":"CLUSTER MEET 10.0.0.12 6379","what":"지정한 IP와 포트의 노드를 클러스터 gossip 네트워크에 참여시킨다.","when":"클러스터에 새 노드를 추가할 때(확장).","caution":"노드 추가 후 slot 할당(ADDSLOTS)이나 reshard를 하지 않으면 데이터를 받지 않는다.","tags":["cluster","meet","node","add","node"]},{"id":"cluster-addslots","task":"클러스터 slot 할당","category":"클러스터","command":"CLUSTER ADDSLOTS 0 1 2 3 4 5","what":"지정한 hash slot을 이 master 노드에 할당한다.","when":"새 master 노드를 수동으로 구성하거나 slot 분포를 바로잡을 때.","caution":"같은 slot을 둘 이상의 노드에 할당할 수 없다. 16384개 slot이 모두 할당되지 않으면 클러스터가 down 상태가 된다.","tags":["cluster","addslots","slot","assign","node"]},{"id":"info-replication","task":"복제 상태 확인","category":"클러스터","command":"INFO replication","what":"역할(master/replica), 연결된 replica, 복제 offset 정보를 보여 준다.","when":"복제 지연과 링크 상태를 확인할 때.","caution":"읽기 전용 명령이며 위험 없음.","tags":["info","replication","status","replication","lag"]},{"id":"cluster-info","task":"클러스터 상태 확인","category":"클러스터","command":"CLUSTER INFO","what":"전체 클러스터 상태(ok/fail), 할당된 slot 수, 알려진 노드 수를 보여 준다.","when":"클러스터 상태와 slot 무결성을 확인할 때.","caution":"읽기 전용 명령이며 위험 없음.","tags":["cluster","info","status","slot","health"]},{"id":"config-maxmemory","task":"maxmemory 및 정책 설정","category":"메모리","command":"CONFIG SET maxmemory 4gb maxmemory-policy allkeys-lru","what":"메모리 한도와 한도 초과 시 적용되는 eviction 정책을 설정한다. Redis 7.0+는 단일 CONFIG SET에 여러 파라미터를 받으며, 이전 버전에서는 CONFIG SET 명령 두 개를 따로 실행한다.","when":"메모리 사용량을 제한하고 eviction 동작을 정의하려 할 때.","caution":"noeviction 정책에서는 한도에 도달하면 쓰기가 실패한다. 잘못된 한도는 데이터 손실/다운타임으로 이어질 수 있다.","tags":["maxmemory","policy","memory","eviction","config"]},{"id":"memory-doctor","task":"메모리 분석","category":"메모리","command":"MEMORY DOCTOR","what":"메모리 사용량을 분석하고 가능한 문제에 대해 사람이 읽기 쉬운 리포트를 만든다.","when":"높은 메모리 사용량이나 단편화가 의심될 때 진단용으로.","caution":"읽기 전용 분석이다. 자세한 내용은 MEMORY STATS 명령도 사용할 수 있다.","tags":["memory","doctor","memory","analysis","diagnostics"]},{"id":"slowlog-get","task":"느린 명령 로그 확인","category":"유지보수","command":"SLOWLOG GET 10","what":"slowlog에서 최근 느린 명령 10건(소요 시간, 시각, 인자)을 반환한다.","when":"느린 명령을 식별하고 성능 병목을 조사할 때.","caution":"읽기 전용 명령이다. 항목을 지우려면 SLOWLOG RESET을 사용한다.","tags":["slowlog","get","slow","diagnostics","performance"]},{"id":"slowlog-config","task":"느린 명령 임계값 설정","category":"유지보수","command":"CONFIG SET slowlog-log-slower-than 10000","what":"명령이 slowlog에 기록되는 소요 시간 임계값을 마이크로초 단위로 설정한다(10000 = 10 ms).","when":"slowlog 민감도를 조정하려 할 때.","caution":"0으로 설정하면 모든 명령이 기록되어 오버헤드가 추가된다. -1은 slowlog를 비활성화한다.","tags":["slowlog","threshold","threshold","config","slow"]},{"id":"slowlog-reset","task":"느린 명령 로그 초기화","category":"유지보수","command":"SLOWLOG RESET","what":"slowlog의 모든 항목을 지운다.","when":"새 관찰 구간을 시작하기 위해 항목을 지울 때.","caution":"기존 느린 명령 이력이 영구 삭제된다.","tags":["slowlog","reset","reset","clear","maintenance"]},{"id":"client-kill","task":"클라이언트 연결 종료","category":"유지보수","command":"CLIENT KILL ADDR 10.0.0.5:53210","what":"지정한 주소의 클라이언트 연결을 종료한다.","when":"멈추거나 오동작하는 클라이언트를 종료하려 할 때.","caution":"연결이 즉시 끊기며 진행 중인 작업이 미완료로 남을 수 있다. 올바른 주소를 골랐는지 확인한다.","tags":["client","kill","client","kill","maintenance"]},{"id":"client-list","task":"연결된 클라이언트 목록","category":"유지보수","command":"CLIENT LIST","what":"연결된 모든 클라이언트를 주소, 유휴 시간, 마지막 명령 같은 세부 정보와 함께 나열한다.","when":"연결을 점검하거나 문제 클라이언트를 찾을 때.","caution":"읽기 전용 명령이며 위험 없음.","tags":["client","list","client","list","connection"]},{"id":"config-get","task":"파라미터 값 확인","category":"런타임","command":"CONFIG GET maxmemory-policy","what":"하나 이상의 구성 파라미터의 현재 값을 반환한다(glob 패턴 지원).","when":"설정의 런타임 값을 확인할 때.","caution":"읽기 전용 명령이며 위험 없음.","tags":["config","get","parameter","view","runtime"]},{"id":"config-set","task":"파라미터 값 변경","category":"런타임","command":"CONFIG SET appendfsync everysec","what":"재시작 없이 런타임에 구성 파라미터를 변경한다.","when":"서버 동작을 즉시 조정하려 할 때.","caution":"변경은 일시적이며 영구화하려면 CONFIG REWRITE를 실행한다. 잘못된 값은 성능/내구성을 저하시킬 수 있다.","tags":["config","set","parameter","runtime","setting"]}]},"recipes/redis-ext.json":{"engineId":"redis","engine":"Redis","order":2,"recipes":[{"id":"mem-fragmentation","task":"메모리 사용량과 단편화 분석, active defrag 활성화","category":"메모리","command":"INFO memory\n# Fields to check: used_memory_human, used_memory_rss_human, mem_fragmentation_ratio, mem_fragmentation_bytes, allocator_frag_ratio\nMEMORY STATS\n# If fragmentation is high and persistent, enable active defrag (requires jemalloc):\nCONFIG SET activedefrag yes\nCONFIG SET active-defrag-ignore-bytes 100mb\nCONFIG SET active-defrag-threshold-lower 10\n# Ask the allocator to return free pages to the OS:\nMEMORY PURGE","what":"Redis가 데이터에 할당한 메모리(used_memory)와 OS가 보는 RSS의 차이, 단편화 비율, 메모리 내역을 보여 준다. 필요하면 런타임에 active defrag를 활성화한다.","when":"RSS가 데이터셋보다 확연히 클 때, 대량 삭제/만료 후 메모리가 줄지 않을 때, 또는 OOM 위험이 나타날 때.","caution":"mem_fragmentation_ratio는 작은 데이터셋에서 오해를 일으킨다(예: 2 MB 데이터에서 4.0). 절대값은 mem_fragmentation_bytes를 본다. 비율이 1 미만이면 단편화가 아니라 스와핑을 의미하며 지연을 일으킨다. active defrag는 CPU를 소비하므로 active-defrag-cycle-max로 제한한다. 영구화하려면 CONFIG REWRITE 또는 redis.conf가 필요하다.","since":"Redis 4.0","note":"active defrag는 Redis에 포함된 jemalloc으로 컴파일된 빌드에서만 동작한다. allocator_frag_ratio는 defrag로 해결할 수 있는 내부 단편화를, allocator_rss_ratio는 allocator가 OS에 반환하지 않은 공간을 보여 준다. 크기가 다양한 값을 자주 갱신하는 워크로드에서 단편화가 더 빨리 증가한다.","tags":["memory","ram","fragmentation","activedefrag","rss","info memory"]},{"id":"bigkeys-memkeys","task":"큰 key 찾기 (--bigkeys, --memkeys, MEMORY USAGE)","category":"메모리","command":"# Largest keys per type by element count:\nredis-cli -h redis1 -p 6379 --bigkeys -i 0.01\n# By memory size:\nredis-cli -h redis1 -p 6379 --memkeys -i 0.01\n# Size and length together, top 20 keys (redis-cli 7.4+):\nredis-cli -h redis1 -p 6379 --keystats --top 20 -i 0.01\n# Memory usage (SAMPLES 0: all elements) and encoding of a single suspicious key:\nredis-cli -h redis1 -p 6379 MEMORY USAGE kullanici:1001:sepet SAMPLES 0\nredis-cli -h redis1 -p 6379 OBJECT ENCODING kullanici:1001:sepet","what":"SCAN으로 keyspace를 스캔하여 타입별로 요소가 가장 많은 key와 메모리를 가장 많이 쓰는 key를 보고한다. 의심 key의 실제 메모리 소비와 encoding을 보여 준다.","when":"메모리가 예기치 않게 증가할 때, 단일 명령(HGETALL, SMEMBERS, DEL)이 지연을 일으킬 때, 또는 클러스터의 한 노드가 다른 노드보다 메모리를 더 쓸 때.","caution":"스캔은 SCAN을 사용하여 서버를 블로킹하지 않지만 keyspace 전체를 순회한다. -i 0.01은 SCAN 호출 100회마다 대기하여 부하를 줄인다. 큰 key를 DEL로 삭제하면 메인 스레드가 블로킹되므로 UNLINK를 사용한다. SAMPLES 0인 MEMORY USAGE는 큰 컬렉션에서 O(N)이다. replica에서 실행하면 master에 부하를 주지 않는다.","since":"Redis 6.0","note":"--bigkeys는 요소 수, --memkeys는 MEMORY USAGE를 기준으로 하므로 list/hash 같은 타입에서는 서로 다른 key를 지목할 수 있다. 작은 hash/zset/list는 listpack encoding을 사용해 메모리를 훨씬 적게 쓰며, hash-max-listpack-entries 같은 임계값을 넘으면 hashtable로 변환된다. 클러스터에서는 각 master에서 따로 실행한다.","tags":["bigkeys","memkeys","big key","memory usage","keystats","large key"]},{"id":"hotkeys-lfu","task":"hot key 찾기 (--hotkeys, OBJECT FREQ)","category":"성능","command":"# Requires an LFU-based policy; note the current policy:\nCONFIG GET maxmemory-policy\nCONFIG SET maxmemory-policy allkeys-lfu\n# Hottest keys by access frequency:\nredis-cli -h redis1 -p 6379 --hotkeys -i 0.01\n# LFU counter of a single key:\nOBJECT FREQ urun:populer:liste\n# Revert to the previous policy when the analysis is done:\nCONFIG SET maxmemory-policy noeviction","what":"LFU 카운터를 사용해 가장 자주 접근되는 key를 나열하고, 단일 key의 접근 빈도 카운터를 보여 준다.","when":"한 노드의 CPU가 100%에 가까워질 때, 클러스터의 한 slot/노드가 다른 곳보다 훨씬 많은 트래픽을 받을 때, 또는 단일 key로 네트워크 대역폭이 포화될 때.","caution":"--hotkeys는 maxmemory-policy가 *-lfu일 때만 동작한다. 정책을 바꾸면 eviction 동작도 바뀐다: maxmemory가 설정되어 있고 이전 정책이 volatile-* 또는 noeviction이었다면 allkeys-lfu는 TTL 없는 key도 evict할 수 있다. 분석은 짧게 하고 정책을 원복한다. LFU 카운터는 로그 스케일이며 시간이 지나면 감소하므로 절대 접근 횟수가 아니다.","since":"Redis 4.0","note":"정책 변경 후 LFU 카운터가 의미를 갖기까지 어느 정도의 트래픽이 필요하다. 정책을 바꿀 수 없다면 짧은 MONITOR 샘플이 대안이지만, MONITOR는 바쁜 시스템에서 처리량을 크게 떨어뜨린다. 읽기가 많은 hot key에는 client-side caching(CLIENT TRACKING)이나 key 분할이 지속적인 해결책이다.","tags":["hotkeys","hot keys","lfu","object freq","hot key","cpu"]},{"id":"scan-unlink-pattern","task":"KEYS 없이 패턴에 맞는 key 찾기 및 안전하게 삭제 (SCAN + UNLINK)","category":"유지보수","command":"# Cursor-based scan instead of KEYS (call again with the returned cursor until it returns 0):\nSCAN 0 MATCH oturum:* COUNT 1000 TYPE string\n# Number of keys matching the pattern:\nredis-cli -h redis1 -p 6379 --scan --pattern 'oturum:*' --count 1000 | wc -l\n# Bulk delete in batches of 500, freeing memory in the background:\nredis-cli -h redis1 -p 6379 --scan --pattern 'oturum:*' --count 1000 | xargs -r -n 500 redis-cli -h redis1 -p 6379 UNLINK","what":"서버를 블로킹하지 않고 커서로 keyspace를 스캔하여 패턴과 타입에 맞는 key를 세고, UNLINK로 일괄 삭제하여 백그라운드에서 메모리를 해제한다.","when":"이전 애플리케이션 버전이 남긴 key를 정리할 때, prefix 아래에 key가 몇 개 있는지 알아낼 때, 또는 운영 환경에서 KEYS가 필요해 보일 때.","caution":"KEYS는 keyspace 전체를 한 번에 반환하며 데이터가 크면 서버를 몇 초간 블로킹하므로 운영 환경에서 사용하지 않는다. SCAN은 스캔 중 추가/삭제된 key에 대해 완전한 보장을 하지 않으며 같은 key를 여러 번 반환할 수 있다. 공백이나 따옴표가 포함된 key 이름은 xargs에서 깨진다. 삭제 전에 개수와 몇 개의 샘플을 확인한다. 되돌릴 수 없다.","since":"Redis 4.0","note":"SCAN의 TYPE 옵션은 6.0에서 도입되었다. COUNT는 힌트일 뿐 반환되는 요소 수를 보장하지 않는다. 클러스터에서 --scan은 연결된 노드만 스캔하므로 각 master에서 실행한다. UNLINK는 lazyfree로 별도 스레드에서 큰 값을 해제하며, 작은 key에서는 DEL과 비용이 같다.","tags":["scan","unlink","keys","bulk delete","pattern","match"]},{"id":"latency-diagnosis","task":"지연 원인 진단 (LATENCY DOCTOR, histogram, commandstats)","category":"성능","command":"# Enable the latency monitor (record events above 100 ms):\nCONFIG SET latency-monitor-threshold 100\nLATENCY LATEST\nLATENCY HISTORY command\nLATENCY DOCTOR\n# Per-command latency distribution and percentiles:\nLATENCY HISTOGRAM GET SET HGETALL\nINFO latencystats\n# Most expensive commands by total CPU time:\nredis-cli -h redis1 -p 6379 INFO commandstats | sort -t= -k3 -rn | head\n# On the server host, baseline latency outside Redis (VM/steal detection, 30 s):\nredis-cli --intrinsic-latency 30\nLATENCY RESET","what":"latency monitor는 fork, AOF fsync, expire cycle, 느린 명령 같은 이벤트의 지연 스파이크를 기록한다. 명령별 지연 백분위수와 총 CPU 시간 기준 가장 비용이 큰 명령을 보여 주며, 호스트 수준의 기준 지연을 측정한다.","when":"애플리케이션에서 p99 지연 스파이크가 보이고 SLOWLOG가 깨끗한데도 성능 저하 불만이 계속될 때.","caution":"latency-monitor-threshold 기본값은 0(꺼짐)이며 진단 후 이전 값으로 되돌린다. SLOWLOG는 명령 실행 시간만 측정하며 네트워크나 큐 대기는 보지 못한다. LATENCY 이벤트는 fork나 fsync 같은 서버 내부 정체도 포착한다. --intrinsic-latency는 CPU를 소비하며 Redis 서버를 호스팅하는 머신에서 실행해야 한다.","since":"Redis 7.0","note":"LATENCY HISTOGRAM과 INFO latencystats는 7.0에서 도입되었다(latency-tracking은 기본 활성화). LATENCY HISTORY에서 fork 이벤트가 지배적이면 데이터셋 크기와 THP(transparent huge pages)를 본다. aof-fsync-always/aof-write 이벤트가 많으면 디스크 지연을 본다. commandstats에서 usec_per_call이 높은 명령은 O(N) 사용을 나타낸다.","tags":["latency","slowness","latency doctor","histogram","commandstats","intrinsic-latency"]},{"id":"eviction-oom-diag","task":"eviction, 만료, OOM 오류 진단","category":"메모리","command":"INFO stats\n# Fields to check: evicted_keys, expired_keys, expired_stale_perc, keyspace_hits, keyspace_misses, total_eviction_exceeded_time (7.0+)\nINFO keyspace\nCONFIG GET maxmemory\nCONFIG GET maxmemory-policy\nINFO memory\n# The value that triggers eviction: used_memory - mem_not_counted_for_evict, compared with maxmemory","what":"evict된 key와 만료된 key 수, 캐시 적중률, TTL이 있는 key 비율, maxmemory 대비 eviction을 트리거하는 메모리 값을 보여 준다.","when":"애플리케이션이 'OOM command not allowed when used memory > maxmemory'를 받을 때, 캐시 적중률이 떨어질 때, 또는 key가 예기치 않게 사라진다는 보고가 있을 때.","caution":"noeviction 정책에서는 maxmemory에 도달하면 쓰기 명령이 OOM 오류를 받고 읽기는 계속 동작한다. volatile-* 정책에서는 TTL 없는 key가 evict되지 않으며, TTL이 있는 key가 없으면 noeviction처럼 동작한다(INFO keyspace의 expires 확인). maxmemory를 물리 RAM에 너무 가깝게 설정하지 않는다: 복제 버퍼와 fork 중 copy-on-write를 위한 여유를 남긴다.","since":"Redis 6.0","note":"적중률 = keyspace_hits / (keyspace_hits + keyspace_misses). 적중률이 떨어지면서 evicted_keys가 꾸준히 증가하면 메모리가 부족한 것이다. expired_stale_perc가 높으면 아직 제거되지 않은 만료 key 비율이 높은 것이며, active-expire-effort로 expire cycle을 빠르게 할 수 있다. replica 버퍼와 AOF 버퍼는 mem_not_counted_for_evict에 포함된다.","tags":["eviction","oom","maxmemory","expire","hit ratio","evicted_keys"]},{"id":"client-memory","task":"메모리를 부풀리는 연결 찾기 (output buffer, query buffer, maxmemory-clients)","category":"메모리","command":"INFO clients\n# Top 10 connections by tot-mem (tot-mem, omem, qbuf, id, addr, user, cmd, age):\nredis-cli -h redis1 -p 6379 CLIENT LIST | awk '{for(i=1;i<=NF;i++){split($i,a,\"=\"); f[a[1]]=a[2]} print f[\"tot-mem\"], f[\"omem\"], f[\"qbuf\"], f[\"id\"], f[\"addr\"], f[\"user\"], f[\"cmd\"], f[\"age\"]}' | sort -rn | head -10\nCLIENT LIST TYPE pubsub\nCONFIG GET client-output-buffer-limit\n# Upper limit for the total memory of all clients (largest clients are closed when exceeded):\nCONFIG SET maxmemory-clients 5%","what":"연결을 총 메모리(tot-mem), output buffer(omem), query buffer(qbuf) 기준으로 정렬한다. pub/sub 연결과 버퍼 한도를 보여 주며, 클라이언트 메모리 전체 상한을 설정한다.","when":"데이터 증가 없이 used_memory가 증가할 때, mem_clients_normal이 높게 보일 때, 또는 소비가 느린 pub/sub/MONITOR 클라이언트가 메모리를 채울 때.","caution":"큰 omem은 보통 결과를 느리게 읽는 클라이언트를 가리킨다(큰 KEYS/HGETALL 응답, 느린 pub/sub 구독자, 열어 둔 MONITOR). maxmemory-clients를 초과하면 메모리를 가장 많이 가진 클라이언트가 경고 없이 닫힌다. replica 및 master 링크는 제외되지만 애플리케이션 연결은 끊길 수 있다. 중요한 연결에는 CLIENT NO-EVICT on을 사용한다.","since":"Redis 7.0","note":"maxmemory-clients는 7.0에서 도입되었으며 기본값은 0(무제한)이다. client-output-buffer-limit은 pubsub와 replica 클래스에 hard/soft 한도를 적용하며, normal 클라이언트는 기본적으로 무제한이다. 문제 연결은 CLIENT KILL ID <id>로 닫는다(client-kill 참고).","tags":["client list","omem","output buffer","maxmemory-clients","connections","pubsub"]},{"id":"persistence-health","task":"RDB/AOF 오류 상태, fork 시간, MISCONF 원인 진단","category":"영속성","command":"INFO persistence\n# Critical fields: rdb_last_bgsave_status, aof_last_bgrewrite_status, aof_last_write_status, rdb_changes_since_last_save, aof_delayed_fsync\nINFO stats\n# latest_fork_usec: duration of the last fork (microseconds), total_forks\nCONFIG GET stop-writes-on-bgsave-error\nCONFIG GET dir\nCONFIG GET appendfsync","what":"마지막 RDB 스냅샷과 AOF 재작성/쓰기의 성공 여부, 마지막 저장 이후 변경된 key 수, 지연된 fsync 횟수, fork 소요 시간을 보여 준다.","when":"애플리케이션이 'MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk'를 받을 때, BGSAVE/BGREWRITEAOF가 실패할 때, 또는 fork 관련 지연이 보일 때.","caution":"stop-writes-on-bgsave-error yes(기본값)에서는 BGSAVE 실패 후 모든 쓰기가 거부된다. 'no'로 설정해 오류를 숨기면 데이터 손실 위험이 가려지므로 먼저 원인을 해결한다: 디스크 가득 참, dir에 쓰기 권한 없음, 또는 fork가 메모리를 얻지 못함(vm.overcommit_memory=0). 큰 데이터셋에서 fork 시간은 수백 밀리초에 이를 수 있으며 그동안 Redis는 응답하지 않는다.","since":"Redis 7.0","note":"Linux에서 vm.overcommit_memory=1과 transparent huge pages 비활성화가 Redis의 공식 권장 사항이다. aof_delayed_fsync가 증가하면 디스크가 fsync를 따라가지 못하는 것이다(appendfsync everysec). 오류 세부 정보는 Redis 로그에 있다('Background saving error', 'Can't save in background: fork').","tags":["misconf","bgsave","aof","fork","persistence","durability"]},{"id":"aof-repair","task":"손상된 AOF로 시작되지 않는 Redis 복구 (redis-check-aof, redis-check-rdb)","category":"영속성","command":"# With Redis stopped, copy the files first:\ncp -a /var/lib/redis/appendonlydir /var/lib/redis/appendonlydir.bak\n# Check (7.0+ multi-part AOF: pass the manifest file):\nredis-check-aof /var/lib/redis/appendonlydir/appendonly.aof.manifest\n# Repair by truncating everything after the first corrupt point (asks for confirmation):\nredis-check-aof --fix /var/lib/redis/appendonlydir/appendonly.aof.manifest\n# Verify the RDB file:\nredis-check-rdb /var/lib/redis/dump.rdb","what":"AOF 파일(base + incr + manifest)과 RDB 파일을 검증하고, AOF의 불완전하거나 손상된 끝부분을 잘라 Redis가 다시 시작되게 한다.","when":"정전, 디스크 가득 참, 갑작스러운 종료 후 Redis가 'Bad file format reading the append only file' 또는 'Unexpected EOF'로 시작되지 않을 때.","caution":"--fix는 첫 손상 지점부터 파일 끝까지 모두 삭제한다. 손상이 파일 중간에 있으면 이후의 모든 쓰기를 잃는다. 확인 전에 출력에서 몇 바이트가 잘리는지(ok_up_to, diff) 확인한다. 불완전한 마지막 명령(예: 큰 HSET)은 통째로 버려진다. 항상 먼저 사본을 만든다.","since":"Redis 7.0","note":"aof-load-truncated yes(기본값)에서는 끝부분만 잘린 AOF가 시작 시 자동으로 정리된다. redis-check-aof는 보통 손상이 파일 중간에 있을 때 필요하다. 7.0+ multi-part AOF에서는 파일이 appenddirname 디렉터리에 있으며 base 파일은 RDB 형식일 수 있다. 손상된 RDB에 대해 redis-check-rdb는 진단만 하고 복구하지 않는다.","tags":["aof","redis-check-aof","corrupted","corrupt","repair","redis-check-rdb"]},{"id":"replication-backlog","task":"복제 끊김과 끝없는 full resync 루프 해결 (backlog, replica buffer)","category":"복제","command":"INFO replication\n# Master: connected_slaves, slaveN:...,state=online,offset=,lag=  Replica: master_link_status, master_last_io_seconds_ago, master_sync_in_progress\nINFO stats\n# sync_full / sync_partial_ok / sync_partial_err: repeated full syncs = backlog too small\nCONFIG GET repl-backlog-size\nCONFIG SET repl-backlog-size 256mb\n# If the replica buffer exceeds its limit during a full sync, the link drops and sync starts over:\nCONFIG GET client-output-buffer-limit\nCONFIG SET client-output-buffer-limit \"replica 1gb 512mb 120\"","what":"master와 replica의 링크 상태, offset 차이, partial/full sync 카운터를 보여 준다. 짧은 끊김이 full sync로 바뀌지 않도록 replication backlog를 키우고, full sync가 중간에 끊기지 않도록 replica output buffer 한도를 키운다.","when":"replica가 'MASTER <-> REPLICA sync started'에서 계속 반복될 때, sync_full 카운터가 증가할 때, 또는 master 로그에 'scheduled to be closed ASAP for overcoming of output buffer limits'가 나타날 때.","caution":"full sync마다 master에서 fork + RDB 생성이 일어나므로 루프는 master 메모리와 디스크에 부담을 준다. repl-backlog-size는 master 메모리에서 할당된다. replica buffer 한도를 올리면 full sync 중 master가 그만큼 추가 메모리를 쓸 수 있으므로 maxmemory 계산에 포함한다. 변경을 영구화하려면 CONFIG REWRITE 또는 redis.conf가 필요하다.","since":"Redis 4.0","note":"backlog 크기 대략 산정: master의 초당 쓰기량(master_repl_offset 증가량) x 허용할 끊김 시간. client-output-buffer-limit에서 'replica' 클래스 이름은 5.0부터 'slave' 대신 사용할 수 있으며, CONFIG GET 출력에는 여전히 slave로 보일 수 있다. 디스크가 느리면 repl-diskless-sync yes(7.0+ 기본값)가 RDB를 디스크에 쓰지 않고 소켓으로 스트리밍한다.","tags":["replication","backlog","full sync","psync","replica","output buffer"]},{"id":"sentinel-status","task":"Sentinel 상태, 현재 master, quorum 확인","category":"Sentinel","command":"redis-cli -p 26379 SENTINEL MASTERS\nredis-cli -p 26379 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster\nredis-cli -p 26379 SENTINEL REPLICAS mymaster\nredis-cli -p 26379 SENTINEL SENTINELS mymaster\n# Are there enough sentinels (quorum and majority) for a failover:\nredis-cli -p 26379 SENTINEL CKQUORUM mymaster\nredis-cli -p 26379 INFO sentinel","what":"Sentinel이 모니터링하는 master, 현재 master 주소, 알려진 replica와 다른 sentinel을 나열하고, 페일오버에 필요한 quorum과 과반수가 확보되는지 확인한다.","when":"애플리케이션이 잘못된 노드에 연결할 때, 페일오버 후 토폴로지를 확인할 때, 그리고 모든 유지보수 전.","caution":"각 sentinel은 자신의 관점을 보고한다. GET-MASTER-ADDR-BY-NAME은 모든 sentinel에서 같은 주소를 반환해야 하며, 그렇지 않으면 네트워크 분할이나 구성 불일치가 있는 것이다. flags 필드의 s_down/o_down이나 replica의 disconnected는 긴급 조사가 필요하다. Sentinel 포트(26379)는 방화벽에서 sentinel과 클라이언트 사이에만 열어 둔다.","since":"Redis 5.0","note":"SENTINEL REPLICAS는 5.0에서 SENTINEL SLAVES를 대체했다(이전 이름도 계속 동작). sentinel 수는 홀수이고 최소 3개여야 하며 서로 다른 장애 도메인(호스트/랙/AZ)에 분산한다. Redis가 ACL/비밀번호를 사용하면 sentinel.conf에 sentinel auth-user/auth-pass를 설정해야 한다.","tags":["sentinel","master","quorum","ckquorum","topology","failover"],"untested":true},{"id":"sentinel-failover","task":"Sentinel로 수동 페일오버 수행 및 토폴로지 정리","category":"Sentinel","command":"# First check quorum and an eligible replica:\nredis-cli -p 26379 SENTINEL CKQUORUM mymaster\nredis-cli -p 26379 SENTINEL REPLICAS mymaster\n# Manual failover without waiting for agreement from other sentinels:\nredis-cli -p 26379 SENTINEL FAILOVER mymaster\n# Verify the new master (must be the same on all sentinels):\nredis-cli -p 26379 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster\n# If old/removed replicas remain listed, reset sentinels one at a time, 30 s apart:\nredis-cli -p 26379 SENTINEL RESET mymaster","what":"master가 정상이어도 가장 적합한 replica에 master 역할을 넘긴다. 이후 새 master를 확인하고 더 이상 존재하지 않는 replica를 sentinel 메모리에서 지운다.","when":"master 호스트의 계획된 유지보수 전, 페일오버 훈련 중, 또는 master를 다른 데이터 센터로 옮길 때.","caution":"페일오버 중 몇 초간 쓰기 중단이 있으며, master에 기록되었지만 아직 복제되지 않은 데이터는 손실될 수 있다. replica-priority 0인 replica는 선택되지 않으며 적격 replica가 없으면 명령이 오류를 반환한다. 모든 sentinel에서 동시에 SENTINEL RESET을 실행하면 토폴로지 정보가 일시적으로 사라지므로 각각 간격을 두고 따로 실행한다. 클라이언트가 Sentinel에 master 주소를 묻는 드라이버를 사용하는지 확인한다.","since":"Redis 5.0","note":"SENTINEL FAILOVER는 다른 sentinel의 동의를 구하지 않지만 새 config epoch는 전파한다. 선택 순서: replica-priority(낮은 것 우선), 복제 offset(큰 것 우선), run ID. 페일오버 후 이전 master가 돌아오면 Sentinel이 이를 새 master의 replica로 자동 재구성한다.","tags":["sentinel","failover","manual failover","master","reset","switchover"],"untested":true},{"id":"cluster-check-fix","task":"클러스터 상태 점검 및 slot 불일치 복구 (--cluster check/fix)","category":"클러스터","command":"redis-cli -h node1 -p 6379 CLUSTER INFO\nredis-cli -h node1 -p 6379 CLUSTER NODES\nredis-cli -h node1 -p 6379 CLUSTER SHARDS\n# Slot coverage, open (migrating/importing) slots and node consistency:\nredis-cli --cluster check node1:6379\n# Repair if there are open or unowned slots:\nredis-cli --cluster fix node1:6379\n# Which slot and node a key lives on (-c: follow redirects):\nredis-cli -h node1 -p 6379 CLUSTER KEYSLOT kullanici:1001\nredis-cli -c -h node1 -p 6379 GET kullanici:1001","what":"모든 노드가 같은 slot map을 보는지, 16384개 slot이 모두 할당되었는지, 미완료(migrating/importing) slot이 없는지 점검한다. 발견된 문제를 수정하고 key가 어느 slot/노드에 있는지 보여 준다.","when":"cluster_state:fail이 나타날 때, 클라이언트가 CLUSTERDOWN이나 끊임없는 MOVED/ASK 오류를 받을 때, 그리고 reshard가 중단된 후.","caution":"--cluster fix는 소유자 없는 slot을 임의의 노드에 할당하고 열린 slot의 key를 이동할 수 있으므로 먼저 check 출력을 검토한다. master와 모든 replica를 잃으면 해당 slot의 데이터는 사라지며, fix는 slot을 빈 상태로 다른 노드에 할당할 뿐이다. cluster-require-full-coverage yes(기본값)에서는 소유자 없는 slot 하나만 있어도 클러스터 전체의 쓰기가 중지된다.","since":"Redis 7.0","note":"CLUSTER SHARDS는 7.0에서 도입되었으며 CLUSTER SLOTS를 대체한다. 인증이 활성화되어 있으면 --cluster 명령에 -a <password> 또는 --user/--pass를 추가한다. CLUSTER NODES 출력의 fail/pfail 플래그는 다른 노드가 해당 노드를 접근 불가로 본다는 뜻이다.","tags":["cluster","check","fix","slot","clusterdown","cluster nodes"],"untested":true},{"id":"cluster-reshard","task":"클러스터 노드 추가, slot 이동, 노드 제거 (add-node, reshard, rebalance)","category":"클러스터","command":"# Add the new master to the cluster (joins empty, with no slots):\nredis-cli --cluster add-node node7:6379 node1:6379\n# Attach a new replica to a specific master:\nredis-cli --cluster add-node node8:6379 node1:6379 --cluster-slave --cluster-master-id <master-node-id>\n# Move 1000 slots from all masters to the new node:\nredis-cli --cluster reshard node1:6379 --cluster-from all --cluster-to <node7-id> --cluster-slots 1000 --cluster-yes\n# Or rebalance automatically (give slots to empty masters too):\nredis-cli --cluster rebalance node1:6379 --cluster-use-empty-masters\n# Remove a node whose slots have been emptied:\nredis-cli --cluster del-node node1:6379 <node-id>","what":"클러스터에 새 master 또는 replica를 추가하고, 노드 간에 slot과 key를 온라인으로 이동하며, slot 분포를 자동으로 재조정하고, 비워진 노드를 클러스터에서 제거한다.","when":"메모리/처리량을 수평 확장할 때, hot 노드의 부하를 분산할 때, 또는 노드를 폐기할 때.","caution":"slot 마이그레이션은 MIGRATE로 key를 하나씩 전송한다. 큰 key가 이동하는 동안 두 노드가 잠시 블로킹될 수 있다. 마이그레이션 중 클라이언트는 ASK 리다이렉트를 받으므로 cluster-aware 클라이언트가 필요하다. reshard가 중단되면 slot이 migrating/importing 상태로 남으므로 --cluster fix로 완료한다. 아직 slot을 소유한 master를 del-node로 제거하지 않는다. 먼저 slot을 옮긴다.","since":"Redis 5.0","note":"노드 ID는 CLUSTER NODES 또는 CLUSTER MYID에서 얻는다. --cluster-pipeline은 MIGRATE 호출당 이동할 key 수를 정한다(기본 10). --cluster rebalance에 --cluster-weight를 주면 노드에 다른 가중치를 줄 수 있다. redis-cli --cluster 명령은 5.0에서 redis-trib.rb를 대체했다.","tags":["cluster","reshard","rebalance","add-node","slot","del-node"],"untested":true},{"id":"acl-users","task":"ACL 사용자 생성, 권한 테스트, 영구화","category":"보안","command":"# Application user: only app:* keys and channels, dangerous commands excluded:\nACL SETUSER app_rw on >S3cret-Change ~app:* resetchannels &app:* +@read +@write -@dangerous\n# Monitoring user: no key access, only specific commands:\nACL SETUSER monitor on >M0n-Secret resetkeys resetchannels -@all +info +ping +client|list +slowlog|get +latency +config|get\n# Test permissions without running the command:\nACL DRYRUN app_rw GET app:1001\nACL GETUSER app_rw\nACL LIST\n# Make it persistent: ACL SAVE if aclfile is set, otherwise CONFIG REWRITE into redis.conf\nACL SAVE","what":"key prefix, pub/sub 채널, 명령 범주로 권한을 준 애플리케이션 및 모니터링 사용자를 만든다. 명령을 실행하지 않고 DRYRUN으로 허용 여부를 테스트하고, ACL을 디스크에 기록한다.","when":"단일 공유 비밀번호(requirepass)에서 사용자별 접근으로 전환할 때, 애플리케이션 간 key를 격리할 때, 또는 모니터링 도구용 읽기 전용 계정을 만들 때.","caution":"규칙 순서가 중요하다: resetchannels는 앞에 주어진 &channel 규칙을 모두 제거한다. -@dangerous는 KEYS, FLUSHALL, CONFIG, DEBUG 같은 명령도 차단하므로 애플리케이션이 사용하는 명령을 DRYRUN으로 확인한다. default 사용자가 nopass와 +@all로 활성 상태로 남아 있으면 ACL은 보호 효과가 없으므로 admin 사용자를 만든 후 ACL SETUSER default off를 실행한다. redis-cli에서 --user/--pass로 AUTH가 실패하면 연결은 default 사용자로 계속되고 명령이 그대로 실행되므로 권한 테스트에는 DRYRUN을 사용한다.","since":"Redis 7.0","note":"ACL은 6.0에서 도입되었다. ACL DRYRUN, selector, 서브커맨드 규칙(client|list)은 7.0에서 추가되었다. aclfile과 redis.conf의 user 줄은 함께 사용할 수 없다. 비밀번호는 SHA-256 해시로 저장되며 ACL GETUSER는 평문 비밀번호를 보여 주지 않는다.","tags":["acl","setuser","users","permissions","dryrun","security"]},{"id":"acl-log-denied","task":"NOPERM 및 WRONGPASS 오류 조사 (ACL LOG)","category":"보안","command":"# Last 10 permission denials and failed logins (reason: auth/command/key/channel):\nACL LOG 10\n# User of this connection:\nACL WHOAMI\n# Cumulative denial counters:\nINFO stats\n# acl_access_denied_auth, acl_access_denied_cmd, acl_access_denied_key, acl_access_denied_channel\nACL LOG RESET","what":"거부된 명령, key, 채널 접근과 실패한 AUTH 시도를 사용자 이름, 객체, 반복 횟수, 클라이언트 정보(IP, 마지막 명령)와 함께 나열하고 누적 거부 카운터를 보여 준다.","when":"배포 후 애플리케이션이 NOPERM 또는 WRONGPASS를 받을 때, 강화된 ACL 규칙으로 깨진 명령을 찾을 때, 또는 비밀번호 추측을 탐지할 때.","caution":"ACL LOG는 메모리에 있으며 재시작 시 지워지고 acllog-max-len(기본 128) 항목으로 제한된다. 같은 오류가 반복되면 새 행 대신 count가 증가한다. client-info 필드에는 클라이언트 IP와 사용 중인 사용자가 들어 있으므로 초기화 전에 필요한 내용을 기록한다.","since":"Redis 7.0","note":"acl_access_denied_* 카운터는 7.2에서 INFO stats에 추가되었다. 모니터링에 연결해 증가 시 알림을 받는다. entry-id와 timestamp-created/last-updated 필드는 7.2에서 도입되었다. 잘못된 비밀번호 시도는 reason=auth, 명령 권한 누락은 reason=command로 나타난다.","tags":["acl log","noperm","wrongpass","access denied","auth","security"]},{"id":"keyspace-notifications","task":"keyspace notification으로 key 삭제 및 만료 이벤트 모니터링","category":"유지보수","command":"CONFIG GET notify-keyspace-events\n# E: keyevent channel, x: expire events, g: generic commands (DEL, EXPIRE, RENAME):\nCONFIG SET notify-keyspace-events Exg\n# Listen to expire and del events in db 0:\nredis-cli -h redis1 -p 6379 --csv PSUBSCRIBE '__keyevent@0__:expired' '__keyevent@0__:del'\n# Turn off:\nCONFIG SET notify-keyspace-events \"\"","what":"key에 대한 이벤트(expire, del, set 등)를 pub/sub 채널로 발행하여 어떤 key가 언제 삭제 또는 만료되었는지 실시간으로 볼 수 있게 한다.","when":"key가 '저절로 사라질' 때 어떤 명령이나 만료가 제거하는지 증명하거나, 만료 이벤트에 의존하는 워크플로(세션 종료)에 공급하기 위해.","caution":"이벤트는 CPU와 네트워크 부하를 추가한다. 'A'(모든 클래스)는 바쁜 시스템에서 특히 비용이 크므로 필요한 클래스만 활성화하고 이후 끈다. pub/sub는 fire-and-forget이다: 연결되지 않은 구독자는 이벤트를 놓치며 아무것도 저장되지 않는다. expired 이벤트는 TTL 시점이 아니라 key가 실제로 삭제될 때(접근 시 또는 백그라운드 expire cycle에 의해) 생성된다.","since":"Redis 2.8","note":"eviction으로 제거된 key에는 e(evicted) 클래스를 추가한다. 클러스터에서는 key를 가진 노드에서만 이벤트가 발행되므로 각 master를 따로 구독한다. 영구 설정은 redis.conf에 notify-keyspace-events를 설정한다.","tags":["keyspace notifications","notify-keyspace-events","expired","pubsub","events","psubscribe"]}]},"recipes/elasticsearch.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":7,"recipes":[{"id":"flush","task":"인덱스 flush","category":"인덱스 수명 주기","command":"POST /customer/_flush","what":"Lucene commit을 수행한다: 메모리의 인덱싱 데이터를 segment로 디스크에 영구 기록하고 더 이상 필요 없는 translog 항목을 정리한다.","when":"유지보수나 재시작 전에 translog를 줄이기 위해.","caution":"보통 자동으로 수행된다. 수동 flush는 IO를 약간 증가시킨다.","tags":["flush","translog","index","customer","maintenance"]},{"id":"force-merge","task":"인덱스 segment 병합 (force merge)","category":"인덱스 수명 주기","command":"POST /customer/_forcemerge?max_num_segments=1","what":"인덱스 segment를 병합하여 segment 수를 줄이고 읽기 성능을 높인다.","when":"더 이상 쓰기가 없는(읽기 전용) 인덱스에서만, 쿼리 성능 향상을 위해.","caution":"IO와 CPU를 많이 소비하므로 활발히 쓰이는 인덱스에서는 절대 실행하지 않는다. 오래 걸릴 수 있으며 취소할 수 없다.","tags":["forcemerge","segment","merge","merge segments","performance"]},{"id":"rollover","task":"alias를 통한 rollover","category":"인덱스 수명 주기","command":"POST /customer-alias/_rollover\n{\n  \"conditions\": {\n    \"max_age\": \"7d\",\n    \"max_docs\": 100000000,\n    \"max_primary_shard_size\": \"50gb\"\n  }\n}","what":"alias가 가리키는 쓰기 인덱스가 조건을 충족하면 새 인덱스를 만들고 alias가 이를 가리키게 한다.","when":"시계열 데이터의 인덱스 크기를 관리하기 위해(ILM의 대안/보완).","caution":"alias는 is_write_index로 단일 쓰기 인덱스를 가져야 한다. 잘못된 구성은 rollover를 막는다.","tags":["rollover","alias","index","time series","ilm"]},{"id":"set-refresh-interval","task":"refresh 간격 변경","category":"인덱스 수명 주기","command":"PUT /customer/_settings\n{\n  \"index\": { \"refresh_interval\": \"30s\" }\n}","what":"인덱스가 새 문서를 검색 가능하게 만드는 빈도(refresh)를 설정한다.","when":"대량 적재 중 인덱싱 속도를 높이기 위해 간격을 늘릴 때.","caution":"-1은 refresh를 완전히 비활성화하여 문서가 검색 가능해지지 않는다. 적재 후 적절한 값으로 되돌린다.","tags":["refresh","interval","settings","bulk","refresh interval"]},{"id":"set-replicas","task":"replica 수 변경","category":"인덱스 수명 주기","command":"PUT /customer/_settings\n{\n  \"index\": { \"number_of_replicas\": 2 }\n}","what":"인덱스의 replica 샤드 수를 동적으로 변경한다.","when":"이중화나 읽기 용량을 늘리거나 줄이기 위해.","caution":"replica를 추가하면 추가 디스크 사용량과 네트워크 트래픽(recovery)이 발생한다. 0으로 줄이면 이중화가 사라진다.","tags":["replica","settings","number_of_replicas","redundancy","capacity"]},{"id":"open-close-index","task":"인덱스 열기 또는 닫기","category":"인덱스 수명 주기","command":"POST /customer/_close\nPOST /customer/_open","what":"인덱스를 읽기/쓰기에 대해 닫거나(리소스 해제) 다시 연다.","when":"드물게 사용되는 인덱스의 리소스 사용량을 줄이거나 특정 설정을 변경하기 위해.","caution":"닫힌 인덱스는 쿼리하거나 쓸 수 없으며, 닫혀 있는 동안 스냅샷을 통하지 않고는 접근할 수 없다.","tags":["open","close","index","open index","close index"]},{"id":"node-join-note","task":"클러스터에 노드 추가","category":"클러스터/노드","command":"GET /_cat/nodes?v","what":"새 노드를 elasticsearch.yml에 같은 cluster.name과 올바른 discovery.seed_hosts로 시작하면 충분하다. 이 명령은 노드가 합류했는지 확인한다.","when":"클러스터에 새 노드를 추가할 때 멤버십을 확인하기 위해.","caution":"7.0+에는 minimum_master_nodes가 없으며 voting configuration은 자동으로 관리된다. master-eligible 노드는 한 번에 하나씩 추가/제거하고, 절반 이상을 한꺼번에 제거하지 않는다. master-eligible 노드를 둘 이상 영구 제거할 때는 먼저 POST _cluster/voting_config_exclusions?node_names=...를 사용한다.","tags":["node","join","cluster","cat","add node"]},{"id":"node-decommission","task":"노드 비우기 (decommission)","category":"클러스터/노드","command":"PUT /_cluster/settings\n{\n  \"persistent\": {\n    \"cluster.routing.allocation.exclude._name\": \"node-3\"\n  }\n}","what":"지정한 노드의 샤드를 다른 노드로 옮겨 안전하게 비운다.","when":"노드를 서비스에서 제외하기 전에 데이터를 옮기기 위해.","caution":"샤드 재배치는 네트워크/IO 부하가 크므로 남은 노드에 충분한 디스크와 용량이 있는지 확인한다. 노드 제거 후 null로 설정을 제거한다. transient 설정은 7.16부터 deprecated되었으므로 persistent를 사용한다.","tags":["decommission","exclude","allocation","drain","node"]},{"id":"reroute-shard","task":"샤드 수동 이동 (reroute)","category":"클러스터/노드","command":"POST /_cluster/reroute\n{\n  \"commands\": [\n    {\n      \"move\": {\n        \"index\": \"customer\",\n        \"shard\": 0,\n        \"from_node\": \"node-1\",\n        \"to_node\": \"node-2\"\n      }\n    }\n  ]\n}","what":"특정 샤드를 한 노드에서 다른 노드로 수동 이동한다.","when":"불균형한 샤드 분포를 바로잡거나 특정 노드를 비우기 위해.","caution":"자동 rebalancing과 충돌할 수 있다. 큰 샤드 이동은 네트워크와 IO 부하를 만든다.","tags":["reroute","shard","move","relocate","cluster"]},{"id":"allocation-enable","task":"샤드 allocation 활성화 또는 비활성화","category":"클러스터/노드","command":"PUT /_cluster/settings\n{\n  \"persistent\": {\n    \"cluster.routing.allocation.enable\": \"primaries\"\n  }\n}","what":"클러스터 전체의 샤드 allocation을 제한하거나(primaries: primary만 할당, none: 할당 없음) 기본값(all / null)으로 되돌린다.","when":"롤링 재시작 전에 불필요한 replica 재배치를 막기 위해(공식 권장값은 \"primaries\").","caution":"이후 null(또는 \"all\")로 설정을 되돌리는 것을 잊지 않는다. 제한된 상태로 남으면 replica가 unassigned로 남는다. transient 설정은 deprecated되었으므로 persistent를 사용한다.","tags":["allocation","enable","restart","shard","cluster"]},{"id":"cluster-settings","task":"클러스터 설정 변경","category":"클러스터/노드","command":"PUT /_cluster/settings\n{\n  \"persistent\": {\n    \"indices.recovery.max_bytes_per_sec\": \"100mb\"\n  }\n}","what":"persistent 또는 transient 클러스터 전체 설정을 변경한다.","when":"recovery 속도 같은 클러스터 동작을 조정할 때.","caution":"persistent 설정은 재시작 후에도 유지되고 transient 설정은 사라지며 7.16부터 deprecated되었으므로 사용하지 않는다. 설정을 기본값으로 되돌리려면 값을 null로 설정한다. 잘못된 값은 클러스터 안정성에 영향을 준다.","tags":["cluster","settings","persistent","transient","setting"]},{"id":"snapshot-repo","task":"스냅샷 리포지토리 등록","category":"스냅샷","command":"PUT /_snapshot/backup_repo\n{\n  \"type\": \"fs\",\n  \"settings\": {\n    \"location\": \"/mnt/es_backups\"\n  }\n}","what":"스냅샷을 저장할 repository를 등록한다.","when":"백업 인프라를 처음 구축할 때.","caution":"fs repository의 경우 경로가 path.repo에 정의되어 있고 모든 master 및 data 노드에서 접근 가능해야 한다.","tags":["snapshot","repository","repo","backup","register"]},{"id":"snapshot-create","task":"스냅샷 생성","category":"스냅샷","command":"PUT /_snapshot/backup_repo/snap1?wait_for_completion=false\n{\n  \"indices\": \"customer\",\n  \"include_global_state\": false\n}","what":"지정한 인덱스의 시점 백업(스냅샷)을 repository에 생성한다.","when":"정기 백업용, 또는 큰 변경 전 안전 백업으로.","caution":"wait_for_completion=true는 큰 인덱스에서 HTTP 연결을 오래 대기시키므로 큰 스냅샷에는 false를 사용한다.","tags":["snapshot","create","backup","backup snapshot","take"]},{"id":"snapshot-restore","task":"스냅샷에서 복원","category":"스냅샷","command":"POST /_snapshot/backup_repo/snap1/_restore\n{\n  \"indices\": \"customer\",\n  \"rename_pattern\": \"customer\",\n  \"rename_replacement\": \"customer_restored\"\n}","what":"스냅샷에서 인덱스를 클러스터로 복원한다(선택적으로 이름 변경).","when":"데이터 손실 후 복구할 때, 또는 백업을 테스트 환경으로 옮길 때.","caution":"같은 이름으로 복원하려면 대상 인덱스가 닫혀 있거나 삭제되어 있어야 한다. 열린 인덱스 위에는 복원할 수 없다.","tags":["snapshot","restore","restore snapshot","recovery","backup"]},{"id":"cache-clear","task":"캐시 비우기","category":"유지보수","command":"POST /customer/_cache/clear","what":"인덱스의 query, request, fielddata 캐시를 비운다.","when":"메모리 압박을 줄이거나 fielddata 문제를 해결하기 위해.","caution":"캐시를 비운 후 다시 워밍될 때까지 첫 쿼리가 느리게 실행된다.","tags":["cache","clear","clear cache","fielddata","maintenance"]},{"id":"pending-tasks","task":"대기 중인 클러스터 작업 확인","category":"유지보수","command":"GET /_cluster/pending_tasks","what":"master 노드에서 처리를 기다리는 클러스터 수준 작업을 나열한다.","when":"클러스터가 느려지거나 업데이트가 멈출 때 master 부하를 진단하기 위해.","caution":"읽기 전용 명령이며 위험 없음.","tags":["pending","tasks","master","diagnostics","pending tasks"]},{"id":"hot-threads","task":"hot threads 분석","category":"유지보수","command":"GET /_nodes/hot_threads","what":"노드에서 CPU를 가장 많이 소비하는 스레드의 스택 트레이스를 보여 준다.","when":"높은 CPU 사용량의 원인을 진단할 때.","caution":"읽기 전용 명령이다. 출력이 길 수 있으므로 thread_type 파라미터로 범위를 좁힌다.","tags":["hotthreads","cpu","diagnostics","thread","performance"]},{"id":"cat-shards","task":"샤드 분포 확인","category":"유지보수","command":"GET /_cat/shards?v&s=state","what":"모든 샤드를 인덱스, 유형(primary/replica), 상태, 위치한 노드와 함께 나열한다.","when":"샤드 배치와 unassigned shard를 검토할 때.","caution":"읽기 전용 명령이며 위험 없음.","tags":["cat","shards","distribution","state","shard"]},{"id":"allocation-explain","task":"샤드가 unassigned인 이유 찾기","category":"유지보수","command":"GET /_cluster/allocation/explain\n{\n  \"index\": \"customer\",\n  \"shard\": 0,\n  \"primary\": true\n}","what":"특정 샤드가 할당되지 못한(unassigned) 이유를 자세히 설명한다.","when":"yellow/red 클러스터에서 unassigned shard의 원인을 찾을 때.","caution":"읽기 전용 진단 명령이며 위험 없음.","tags":["allocation","explain","unassigned","diagnostics","shard"]},{"id":"cluster-health","task":"클러스터 상태 확인","category":"유지보수","command":"GET /_cluster/health","what":"전체 클러스터 상태(green/yellow/red)와 활성 및 unassigned shard 수를 보여 준다.","when":"클러스터 상태를 빠르게 확인할 때.","caution":"읽기 전용 명령이며 위험 없음.","tags":["cluster","health","health check","status","green"]},{"id":"refresh-index","task":"인덱스 수동 refresh","category":"인덱스 수명 주기","command":"POST /customer/_refresh","what":"인덱스의 대기 중인 변경을 즉시 검색 가능하게 만든다.","when":"대량 적재 직후 데이터를 바로 쿼리할 수 있게 하려 할 때.","caution":"잦은 수동 refresh는 작은 segment를 많이 만들어 성능을 저하시킬 수 있다.","tags":["refresh","index","search","segment","customer"]}]},"recipes/elasticsearch-ext.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":2,"recipes":[{"id":"red-yellow-triage","task":"red/yellow 클러스터 분류 흐름 (어떤 인덱스, 어떤 샤드, 왜)","category":"클러스터/노드","command":"GET _cluster/health?filter_path=status,unassigned_shards,initializing_shards,delayed_unassigned_shards,number_of_pending_tasks\nGET _health_report?verbose=false\nGET _cat/indices?v&health=red&h=health,index,pri,rep,docs.count,store.size\nGET _cat/indices?v&health=yellow&h=health,index,pri,rep\nGET _cat/shards?v&h=index,shard,prirep,state,unassigned.reason,unassigned.for,node&s=state\n# Without a body, the call explains the first unassigned shard:\nGET _cluster/allocation/explain","what":"클러스터 상태에서 문제 인덱스로, 다시 unassigned shard와 할당되지 않는 이유(unassigned.reason, allocation explain)로 내려가는 순서화된 분류 흐름을 제공한다. _health_report는 어떤 지표(디스크, 샤드 가용성, master)가 비정상인지 요약한다.","when":"클러스터가 red 또는 yellow로 바뀔 때 첫 대응으로.","caution":"Red: 최소 하나의 primary 샤드가 unassigned이다. 해당 인덱스로의 쓰기와 그 샤드에 닿는 검색이 실패한다. Yellow: 모든 primary는 있지만 replica가 누락되었다. 데이터는 접근 가능하지만 이중화가 없다. 단일 노드 클러스터에서는 replicas > 0인 인덱스가 영구히 yellow로 남는다(same_shard 결정). 최근에 노드가 떠났다면 delayed_unassigned_shards는 index.unassigned.node_left.delayed_timeout(기본 1m)이 만료될 때까지 의도적으로 보류된다.","since":"Elasticsearch 8.7","note":"_health_report는 8.7에서 도입되었으며 이전 버전에서는 처음 네 단계를 사용한다. unassigned.reason 값: NODE_LEFT(노드 이탈), ALLOCATION_FAILED(재시도 한도 도달, retry-failed-allocation 참고), INDEX_CREATED, CLUSTER_RECOVERED. 특정 샤드는 index/shard/primary body와 함께 allocation-explain 레시피를 사용한다.","tags":["red","yellow","cluster health","unassigned","health report","triage"]},{"id":"retry-failed-allocation","task":"재시도 한도에 도달한 샤드 재할당 (reroute retry_failed)","category":"클러스터/노드","command":"# Attempt count and last failure details:\nGET _cluster/allocation/explain?filter_path=index,shard,unassigned_info.reason,unassigned_info.failed_allocation_attempts,unassigned_info.details,allocate_explanation\n# After fixing the cause, retry shards that hit the limit:\nPOST _cluster/reroute?retry_failed=true&metric=none\n# Recovery progress:\nGET _cat/recovery?v&active_only=true&h=index,shard,stage,source_node,target_node,bytes_percent,translog_ops_percent","what":"연속 실패 후 할당이 중단된 shard(ALLOCATION_FAILED, max_retry)의 실패 세부 정보를 보여 주고 할당을 다시 트리거하며 recovery 진행 상황을 모니터링한다.","when":"allocation explain에 'shard has exceeded the maximum number of retries'가 표시될 때. 디스크 가득 참, 손상된 translog, 일시적 네트워크 문제가 해결된 후.","caution":"원인을 고치지 않고 retry_failed를 실행하면 샤드가 5번 더 시도한 뒤 다시 실패하므로 먼저 unassigned_info.details의 오류를 해결한다. 많은 샤드가 한꺼번에 recovery에 들어가면 네트워크와 디스크 부하가 증가하며, cluster.routing.allocation.node_concurrent_recoveries와 indices.recovery.max_bytes_per_sec로 제한된다.","since":"Elasticsearch 7.0","note":"재시도 한도는 index.allocation.max_retries(기본 5)다. Elasticsearch도 주기적으로 재시도하지만 retry_failed는 대기를 없앤다. metric=none은 응답의 큰 cluster state를 생략한다.","tags":["reroute","retry_failed","allocation failed","max_retries","unassigned","recovery"]},{"id":"allocate-stale-primary","task":"최후의 수단으로 잃어버린 primary 샤드 할당 (allocate_stale_primary)","category":"클러스터/노드","command":"# Which nodes hold copies of the primary (allocation id, store errors):\nGET _shard_stores/musteri?status=red\n# Last resort: promote a stale copy to primary; data loss is accepted:\nPOST _cluster/reroute\n{\n  \"commands\": [\n    {\n      \"allocate_stale_primary\": {\n        \"index\": \"musteri\",\n        \"shard\": 0,\n        \"node\": \"node-2\",\n        \"accept_data_loss\": true\n      }\n    }\n  ]\n}","what":"최신 primary 사본을 가진 노드를 영구히 잃었을 때 다른 노드의 오래된(in-sync가 아닌) 사본을 primary로 지정하여 인덱스를 red에서 벗어나게 한다.","when":"primary를 호스팅하던 노드를 복구할 수 없고 스냅샷 복원이 불가능하거나 더 오래 걸릴 때만.","caution":"오래된 사본에는 최신 쓰기가 없으므로 그 사본 이후의 모든 쓰기가 영구 손실된다. 잃었던 노드가 나중에 돌아오면 그 노드의 더 최신 사본은 삭제된다. 먼저 노드 복구와 스냅샷 복원을 고려한다. allocate_empty_primary는 샤드를 완전히 빈 상태로 만들어 해당 샤드의 모든 데이터를 삭제하므로 사본이 하나도 남지 않았을 때만 사용한다.","since":"Elasticsearch 7.0","note":"_shard_stores 출력에서 allocation 유형은 'primary', 'replica', 'unused'로 나타난다. store_exception이 있는 사본은 선택하지 않는다. 먼저 ?dry_run=true&explain=true로 명령을 실행해 결정을 확인할 수 있다. 작업 후 애플리케이션 측에서 reindex하여 데이터 공백을 메운다.","tags":["allocate_stale_primary","red","primary","data loss","shard_stores","reroute"],"untested":true},{"id":"disk-watermark","task":"디스크 watermark 관리 및 flood-stage 쓰기 차단 해제","category":"클러스터/노드","command":"GET _cat/allocation?v&h=node,shards,disk.indices,disk.used,disk.avail,disk.percent&s=disk.percent:desc\nGET _cluster/settings?include_defaults=true&filter_path=*.cluster.routing.allocation.disk\n# Adjust thresholds temporarily (null returns to the default):\nPUT _cluster/settings\n{\n  \"persistent\": {\n    \"cluster.routing.allocation.disk.watermark.low\": \"85%\",\n    \"cluster.routing.allocation.disk.watermark.high\": \"90%\",\n    \"cluster.routing.allocation.disk.watermark.flood_stage\": \"95%\"\n  }\n}\n# After freeing space, remove the flood-stage write block:\nPUT _all/_settings\n{\n  \"index.blocks.read_only_allow_delete\": null\n}","what":"노드별 디스크 사용량과 실제 적용되는 watermark 값을 보여 준다. low/high/flood_stage 임계값을 조정하고 flood stage에서 인덱스에 걸린 read_only_allow_delete 차단을 해제한다.","when":"쓰기가 'TOO_MANY_REQUESTS/12/disk usage exceeded flood-stage watermark, index has read-only-allow-delete block'으로 거부될 때, 새 샤드가 할당되지 않을 때, 또는 가득 찬 노드에서 샤드가 빠져나가기 시작할 때.","caution":"low를 넘으면 노드에 새 샤드가 할당되지 않고, high를 넘으면 샤드가 노드에서 옮겨지며, flood_stage를 넘으면 그 노드에 샤드가 있는 인덱스가 읽기 전용이 된다. 임계값을 올리는 것은 시간을 벌 뿐이며, 지속적인 해결책은 오래된 인덱스 삭제, replica 축소, 디스크/노드 추가다. 디스크가 완전히 차면 샤드가 손상될 수 있다.","since":"Elasticsearch 8.5","note":"7.4+에서는 디스크 사용량이 high watermark 아래로 내려가면 차단이 자동 해제되며, 수동 해제는 대기 시간을 줄인다. 8.5+에서 큰 디스크는 비율 대신 절대 여유 공간으로 max_headroom(low 200GB, high 150GB, flood 100GB)을 사용한다. 이 기본 headroom은 watermark가 기본값으로 남아 있을 때만 적용되므로 위처럼 비율을 명시하면 비활성화된다. watermark는 단일 data 노드 클러스터에도 적용된다.","tags":["disk watermark","flood_stage","read_only_allow_delete","disk full","allocation","cat allocation"]},{"id":"rolling-restart","task":"롤링 재시작 / 롤링 업그레이드 (노드별, 불필요한 샤드 이동 없이)","category":"클러스터/노드","command":"# 1) Disable replica allocation (only primaries are allocated):\nPUT _cluster/settings\n{\n  \"persistent\": { \"cluster.routing.allocation.enable\": \"primaries\" }\n}\n# 2) Optional: stop indexing and flush to speed up recovery:\nPOST _flush\n# 3) Stop one node, do the maintenance, start it; verify it joined:\nGET _cat/nodes?v&h=name,version,node.role,master,heap.percent\n# 4) Re-enable allocation:\nPUT _cluster/settings\n{\n  \"persistent\": { \"cluster.routing.allocation.enable\": null }\n}\n# 5) Wait for green before moving to the next node:\nGET _cluster/health?wait_for_status=green&timeout=10m","what":"노드가 다운된 동안 replica가 다른 노드로 다시 복사되는 것을 막으면서 노드를 하나씩 재시작하거나 업그레이드하는 순서를 제공한다. 노드가 돌아오면 샤드는 로컬 사본에서 빠르게 복구된다.","when":"OS 패치, JVM/heap 변경, elasticsearch.yml 변경, 마이너/메이저 버전 업그레이드를 위해 노드를 순차적으로 재시작할 때.","caution":"green을 기다리지 않고 다음 노드로 넘어가지 않는다. 두 노드가 동시에 다운되면 샤드의 모든 사본이 사라질 수 있다. master-eligible 노드는 마지막에 하나씩 재시작한다. 과반수를 잃으면 클러스터가 master를 선출할 수 없다. allocation.enable을 다시 활성화하는 것을 잊으면 replica가 unassigned로 남는다. 업그레이드 중에는 data 노드를 먼저(frozen/cold 먼저, hot 마지막), 그다음 master를 업그레이드한다.","since":"Elasticsearch 7.16","note":"ML job이 있으면 시작 시 POST _ml/set_upgrade_mode?enabled=true, 끝에 enabled=false를 실행한다. synced flush는 8.0에서 제거되었으며 일반 _flush로 충분하다. transient 설정은 7.16부터 deprecated되었으므로 persistent를 사용한다. 짧은 재시작에는 allocation을 비활성화하는 대신 index.unassigned.node_left.delayed_timeout을 늘리는 방법도 동작한다.","tags":["rolling restart","rolling upgrade","allocation","primaries","maintenance","upgrade"],"untested":true},{"id":"shard-sizing","task":"샤드 크기와 샤드 수 감사 (oversharding / 과대 샤드 탐지)","category":"유지보수","command":"# Largest shards (GB):\nGET _cat/shards?v&h=index,shard,prirep,state,docs,store,node&s=store:desc&bytes=gb\n# Indices by primary size:\nGET _cat/indices?v&h=index,pri,rep,docs.count,pri.store.size,store.size&s=pri.store.size:desc&bytes=gb\n# Shard count and disk per node:\nGET _cat/allocation?v&h=node,shards,disk.indices,disk.percent&s=shards:desc\nGET _cluster/stats?filter_path=indices.shards.total,indices.shards.primaries,nodes.count.data\nGET _cluster/settings?include_defaults=true&filter_path=*.cluster.max_shards_per_node","what":"가장 큰 샤드, 인덱스의 primary 크기, 노드별 샤드 수, 전체 샤드 수, cluster.max_shards_per_node 한도를 나열한다.","when":"용량 계획 중, recovery 시간이 길어지거나 heap 압박이 증가할 때, 그리고 'this action would add [x] 샤드, but this cluster currently has [y]/[z] maximum normal shards open' 오류 시.","caution":"아주 작은 샤드가 많으면(oversharding) master와 heap에 부담을 주고, 아주 큰 샤드는 recovery와 rebalancing을 몇 시간으로 늘린다. primary 샤드 수는 인덱스 생성 후 바꿀 수 없으며, 바로잡으려면 shrink, split 또는 reindex가 필요하다. max_shards_per_node를 올리면 한도는 사라지지만 문제는 해결되지 않는다.","since":"Elasticsearch 8.3","note":"Elastic의 일반 지침: shard당 10-50 GB, 최대 약 2억 문서. 8.3+에서는 이전의 'heap GB당 20 shard' 규칙이 매핑된 필드 수 기반의 heap 추정으로 대체되었다. 시계열에서는 rollover의 max_primary_shard_size로 샤드 크기를 고정한다.","tags":["shard size","oversharding","cat shards","max_shards_per_node","capacity","sizing"]},{"id":"shrink-index","task":"샤드가 너무 많은 인덱스 shrink","category":"인덱스 수명 주기","command":"# 1) Block writes on the source and gather all shard copies on one node:\nPUT log-2026.09/_settings\n{\n  \"index.number_of_replicas\": 0,\n  \"index.routing.allocation.require._name\": \"node-1\",\n  \"index.blocks.write\": true\n}\nGET _cluster/health/log-2026.09?wait_for_no_relocating_shards=true&timeout=30m\n# 2) Create a new index with fewer shards (the target must be a factor of the source):\nPOST log-2026.09/_shrink/log-2026.09-shrunk\n{\n  \"settings\": {\n    \"index.number_of_shards\": 1,\n    \"index.number_of_replicas\": 1,\n    \"index.routing.allocation.require._name\": null,\n    \"index.blocks.write\": null,\n    \"index.codec\": \"best_compression\"\n  }\n}","what":"더 이상 쓰기가 없는 인덱스의 segment를 새 인덱스로 hard-link하여 primary 샤드 수를 줄인다. heap과 샤드 수 오버헤드를 낮춘다.","when":"일일 인덱스가 필요 이상의 샤드로 생성되었을 때, 샤드 수 한도에 가까울 때, 또는 오래된 인덱스를 warm tier로 옮기기 전.","caution":"shrink 중 원본 인덱스는 쓰기가 닫히므로 더 이상 쓰기가 없는 인덱스에만 적용한다. 대상 샤드 수는 원본 수의 약수여야 한다(8 -> 4, 2 또는 1). 모든 사본이 모이는 노드에는 원본 인덱스만큼의 여유 디스크가 필요하다. replicas 0이면 원본 샤드의 사본이 하나뿐이므로 작업이 끝나고 새 인덱스가 green이 될 때까지 원본을 삭제하지 않는다.","since":"Elasticsearch 7.0","note":"shrink 후 원본을 삭제하거나 alias를 새 인덱스로 전환한다. 같은 노드가 같은 파일 시스템에서 파일을 hard-link할 수 있어 작업은 보통 빠르며, 그렇지 않으면 파일이 복사된다. 샤드 수를 늘리려면 _split을 사용하고, ILM으로 자동화하려면 warm 단계의 shrink action을 사용한다.","tags":["shrink","shard count","oversharding","index","blocks.write","warm"]},{"id":"ilm-policy-bootstrap","task":"ILM 정책 정의, 템플릿으로 연결, rollover 체인 부트스트랩","category":"인덱스 수명 주기","command":"PUT _ilm/policy/log-policy\n{\n  \"policy\": {\n    \"phases\": {\n      \"hot\": { \"actions\": { \"rollover\": { \"max_primary_shard_size\": \"50gb\", \"max_age\": \"1d\" }, \"set_priority\": { \"priority\": 100 } } },\n      \"warm\": { \"min_age\": \"7d\", \"actions\": { \"shrink\": { \"number_of_shards\": 1 }, \"forcemerge\": { \"max_num_segments\": 1 }, \"set_priority\": { \"priority\": 50 } } },\n      \"delete\": { \"min_age\": \"30d\", \"actions\": { \"delete\": {} } }\n    }\n  }\n}\nPUT _index_template/log\n{\n  \"index_patterns\": [\"log-*\"],\n  \"priority\": 500,\n  \"template\": {\n    \"settings\": {\n      \"number_of_shards\": 1,\n      \"number_of_replicas\": 1,\n      \"index.lifecycle.name\": \"log-policy\",\n      \"index.lifecycle.rollover_alias\": \"log\"\n    }\n  }\n}\n# Create the first index with the write alias (its name must end with a number such as -000001):\nPUT log-000001\n{\n  \"aliases\": { \"log\": { \"is_write_index\": true } }\n}","what":"hot(rollover), warm(shrink + force merge), delete 단계를 가진 ILM 정책을 만들고, 템플릿을 통해 새 인덱스에 정책과 rollover alias를 적용하며, 쓰기 alias와 함께 체인의 첫 인덱스를 생성한다.","when":"로그나 메트릭 같은 시계열 데이터의 인덱스 크기, 보존, 삭제를 자동화할 때.","caution":"warm 및 delete 단계의 min_age는 인덱스 생성이 아니라 rollover 시점부터 계산된다. 정책 변경은 기존 인덱스의 현재 단계가 완료된 후에만 적용된다. warm 단계의 shrink와 forcemerge는 IO가 많으며, force merge가 끝날 때까지 인덱스에 쓰지 않아야 한다. ILM은 기본 10분마다 실행되므로 조건이 즉시 평가되지 않는다.","since":"Elasticsearch 7.8","note":"새 프로젝트에서는 data stream이 같은 일을 더 간단히 한다: 인덱스 템플릿에 \"data_stream\": {}를 추가하면 rollover alias나 부트스트랩 인덱스가 필요 없다. rollover 조건은 refresh된 통계를 기준으로 평가된다. composable index template(_index_template)은 7.8에서 도입되었으며 레거시 _template API는 deprecated되었다.","tags":["ilm","lifecycle","rollover","policy","index template","retention"]},{"id":"ilm-explain-retry","task":"멈춘 ILM 단계 진단 및 재시도 (_ilm/explain, _ilm/retry)","category":"인덱스 수명 주기","command":"# Indices waiting in an error step:\nGET log-*/_ilm/explain?only_errors=true\n# Phase/action/step state and failure reason for one index:\nGET log-000003/_ilm/explain?filter_path=indices.*.phase,indices.*.action,indices.*.step,indices.*.failed_step,indices.*.step_info,indices.*.is_auto_retryable_error\n# After fixing the cause (example: missing rollover alias setting):\nPUT log-000003/_settings\n{\n  \"index.lifecycle.rollover_alias\": \"log\"\n}\nPOST log-000003/_ilm/retry\n# Is ILM running (if STOPPED, POST _ilm/start):\nGET _ilm/status","what":"ERROR 단계에서 대기 중인 인덱스, 멈춘 phase/action/step, 실패 사유(step_info)를 보여 주고, 원인을 해결한 후 실패한 단계를 다시 실행한다.","when":"rollover가 일어나지 않을 때, 오래된 인덱스가 삭제되지 않을 때, 또는 ILM이 제거했어야 할 인덱스로 디스크가 찰 때.","caution":"_ilm/retry는 ERROR 단계의 인덱스에서만 동작하며, 그렇지 않으면 'has not encountered an error'를 반환한다. 흔한 원인: rollover_alias 설정 누락 또는 alias의 is_write_index가 다른 인덱스에 있음, shrink를 위한 단일 노드 디스크 부족, allocation 규칙(_tier_preference)을 충족할 수 없음. 정책을 삭제해도 인덱스에서 ILM 관리가 제거되지 않으므로 먼저 인덱스에서 정책 설정을 제거한다.","since":"Elasticsearch 7.0","note":"is_auto_retryable_error가 true인 오류는 ILM이 스스로 재시도한다(failed_step_retry_count 증가). ILM 점검 간격은 indices.lifecycle.poll_interval(기본 10m)로 설정한다. 인덱스를 ILM에서 빼려면 POST <index>/_ilm/remove를 사용한다.","tags":["ilm","explain","retry","error step","rollover","lifecycle"]},{"id":"index-template-component","task":"component template으로 인덱스 템플릿 구성 및 결과 시뮬레이션","category":"인덱스 수명 주기","command":"PUT _component_template/app-mappings\n{\n  \"template\": {\n    \"mappings\": {\n      \"dynamic\": \"strict\",\n      \"properties\": {\n        \"@timestamp\": { \"type\": \"date\" },\n        \"level\": { \"type\": \"keyword\" },\n        \"msg\": { \"type\": \"text\" }\n      }\n    }\n  }\n}\nPUT _index_template/app-events\n{\n  \"index_patterns\": [\"app-events-*\"],\n  \"priority\": 500,\n  \"composed_of\": [\"app-mappings\"],\n  \"template\": { \"settings\": { \"number_of_shards\": 1, \"refresh_interval\": \"5s\" } }\n}\n# See the final settings and mapping before the index is created:\nPOST _index_template/_simulate_index/app-events-000001\nGET _cat/templates?v&s=name","what":"재사용 가능한 매핑 component template을 정의하고 이를 인덱스 템플릿으로 조합하며, 인덱스를 만들지 않고 주어진 인덱스 이름에 적용될 최종 설정과 매핑을 보여 준다.","when":"새 인덱스가 잘못된 매핑이나 샤드 수로 생성될 때(dynamic mapping 폭증, keyword 대신 text), 그리고 템플릿 변경을 배포하기 전.","caution":"템플릿은 새로 생성되는 인덱스에만 적용되며 기존 인덱스의 매핑은 바뀌지 않는다(reindex-alias-swap 참고). 두 템플릿이 같은 패턴에 일치하면 priority가 가장 높은 하나만 사용되며 병합되지 않는다. dynamic: strict는 정의되지 않은 필드가 포함된 문서를 거부하므로 애플리케이션이 새 필드를 보내면 인덱싱이 실패한다.","since":"Elasticsearch 7.8","note":"_simulate_index 출력의 overlapping 필드는 같은 패턴에 일치하지만 무시되는 다른 템플릿을 보여 준다. 내장 템플릿(logs-*-*, metrics-*-*)과 충돌하지 않도록 priority를 200보다 높게 유지한다. composable 템플릿은 레거시 _template API보다 우선한다.","tags":["index template","component template","simulate","mapping","priority","dynamic strict"]},{"id":"reindex-alias-swap","task":"무중단 매핑 변경 (reindex + 원자적 alias 전환)","category":"인덱스 수명 주기","command":"# 1) Target index with the new mapping (refresh off and replicas 0 for loading):\nPUT musteri-v2\n{\n  \"settings\": { \"number_of_shards\": 1, \"number_of_replicas\": 0, \"refresh_interval\": \"-1\" },\n  \"mappings\": { \"properties\": { \"kod\": { \"type\": \"keyword\" }, \"adet\": { \"type\": \"long\" } } }\n}\n# 2) Reindex in the background (tracked with the returned task id):\nPOST _reindex?wait_for_completion=false&slices=auto\n{\n  \"source\": { \"index\": \"musteri-v1\", \"size\": 1000 },\n  \"dest\": { \"index\": \"musteri-v2\", \"op_type\": \"create\" },\n  \"conflicts\": \"proceed\"\n}\nGET _tasks/<task_id>\n# 3) Restore settings and compare document counts:\nPUT musteri-v2/_settings\n{\n  \"index\": { \"refresh_interval\": null, \"number_of_replicas\": 1 }\n}\nPOST musteri-v2/_refresh\nGET _cat/indices/musteri-v1,musteri-v2?v&h=index,docs.count,pri.store.size\n# 4) Switch the alias to the new index in one atomic operation:\nPOST _aliases\n{\n  \"actions\": [\n    { \"remove\": { \"index\": \"musteri-v1\", \"alias\": \"musteri\" } },\n    { \"add\": { \"index\": \"musteri-v2\", \"alias\": \"musteri\", \"is_write_index\": true } }\n  ]\n}","what":"새 매핑으로 인덱스를 만들고, reindex로 백그라운드에서 데이터를 복사하고, 문서 수를 비교한 뒤, 애플리케이션이 사용하는 alias를 한 번의 원자적 작업으로 새 인덱스로 전환한다.","when":"기존 필드의 타입을 바꿔야 하거나(text -> keyword, integer -> long), analyzer를 바꿔야 하거나, 샤드 수를 다시 정해야 할 때. 이런 작업은 기존 인덱스에서 제자리로 할 수 없다.","caution":"애플리케이션은 인덱스 이름이 아니라 alias로 연결해야 한다. 그렇지 않으면 전환에 배포가 필요하다. reindex 중 원본에 새로 들어온 쓰기는 대상에 도달하지 않는다: 쓰기를 중지하거나, 전환 직전에 op_type create + conflicts proceed로 reindex를 다시 실행해 공백을 메운다(이 방법으로 update와 delete는 옮겨지지 않음). reindex는 원본에 읽기 부하, 대상에 쓰기 부하를 만들므로 requests_per_second로 조절한다.","since":"Elasticsearch 7.0","note":"refresh_interval이 -1인 동안 적재된 문서는 refresh 전까지 개수에 나타나지 않으므로 비교 전에 _refresh를 실행한다. _cat/count에 쉼표로 구분된 여러 인덱스를 주면 합계를 반환하므로 인덱스별 비교에는 _cat/indices를 사용한다. 실행 중인 reindex 속도는 POST _reindex/<task_id>/_rethrottle?requests_per_second=500으로 바꿀 수 있다. wait_for_completion=false는 결과를 .tasks 인덱스에 기록하므로 GET _tasks/<task_id>로 response.failures를 확인한다.","tags":["reindex","alias","mapping change","zero downtime","_aliases","no downtime"]},{"id":"tasks-cancel","task":"장기 실행 search 및 reindex 작업 찾기, 취소, 조절","category":"유지보수","command":"# Longest running tasks:\nGET _cat/tasks?v&detailed&h=action,task_id,parent_task_id,running_time,node,description&s=running_time:desc\n# Search, reindex and by-query tasks, grouped by parent:\nGET _tasks?actions=*search*,*reindex*,*byquery*&detailed=true&group_by=parents\n# Cancel a single task:\nPOST _tasks/<task_id>/_cancel\n# Cancel all search tasks (with care):\nPOST _tasks/_cancel?actions=*search*\n# Slow down a running reindex/update_by_query:\nPOST _reindex/<task_id>/_rethrottle?requests_per_second=500","what":"클러스터에서 실행 중인 작업을 소요 시간 순으로 정렬하고, search와 bulk 작업을 쿼리 설명과 함께 보여 준다. 선택한 작업이나 모든 search를 취소하고 reindex/by-query 작업을 조절한다.","when":"search thread pool이 가득 찰 때, CPU가 갑자기 오를 때, 대시보드가 무거운 aggregation으로 클러스터에 부담을 줄 때, 또는 update_by_query가 운영에 영향을 줄 때.","caution":"취소는 협조적이다: 작업은 다음 체크포인트에서 멈추며 일부 단계(예: force merge)는 취소할 수 없다. 모든 search를 취소하면 사용자에게 오류가 반환된다. 취소된 reindex/by-query는 지금까지의 변경을 롤백하지 않으므로 대상에 부분 데이터가 남는다. 부모 작업이 취소되면 자식 작업도 취소된다.","since":"Elasticsearch 7.0","note":"detailed 파라미터는 search 작업의 쿼리 body를 description 필드에 기록한다. 지속적인 보호를 위해 search.default_search_timeout, search 요청의 timeout, 큰 aggregation에 대한 search.max_buckets를 사용한다. 7.x+에서는 HTTP 연결이 닫힌 search가 자동으로 취소된다.","tags":["tasks","cancel","kill query","rethrottle","long query","search"]},{"id":"search-slowlog","task":"search 및 indexing slow log 활성화 (인덱스별 임계값)","category":"성능","command":"PUT musteri/_settings\n{\n  \"index.search.slowlog.threshold.query.warn\": \"2s\",\n  \"index.search.slowlog.threshold.query.info\": \"500ms\",\n  \"index.search.slowlog.threshold.fetch.warn\": \"1s\",\n  \"index.indexing.slowlog.threshold.index.warn\": \"1s\",\n  \"index.indexing.slowlog.source\": \"200\"\n}\nGET musteri/_settings/index.*slowlog*?flat_settings=true\n# Set thresholds to null to turn it off:\nPUT musteri/_settings\n{\n  \"index.search.slowlog.threshold.query.warn\": null,\n  \"index.search.slowlog.threshold.query.info\": null,\n  \"index.search.slowlog.threshold.fetch.warn\": null,\n  \"index.indexing.slowlog.threshold.index.warn\": null,\n  \"index.indexing.slowlog.source\": null\n}","what":"지정한 인덱스에 대해 query 및 fetch 단계와 indexing에서 임계값을 넘는 요청을 쿼리 body와 함께 slow log에 기록한다.","when":"어떤 쿼리가 느린 search를 일으키는지 찾거나, 특정 대시보드나 클라이언트의 무거운 쿼리를 잡기 위해.","caution":"임계값은 샤드별로 측정된다: shard가 많은 인덱스에서는 각 샤드가 자신의 줄을 기록할 수 있다. 매우 낮은 임계값(0ms)은 로그량과 IO를 크게 늘리므로 진단 후 null로 끈다. indexing.slowlog.source는 문서 source를 로그에 기록하므로 민감 데이터가 있으면 false로 설정한다. 설정은 지정한 인덱스에만 적용되므로 새 인덱스를 위해 템플릿에 추가한다.","since":"Elasticsearch 8.0","note":"slow log 파일은 logs 디렉터리에 <cluster>_index_search_slowlog.json 및 <cluster>_index_indexing_slowlog.json으로 기록되며, log4j2가 콘솔로 라우팅되면 stdout으로 간다. index.search.slowlog.level 설정은 8.0에서 제거되었으며 수준은 임계값(warn/info/debug/trace)으로 결정된다. 8.14+에서는 index.search.slowlog.include.user: true가 요청한 사용자도 로그에 기록한다. 단일 쿼리를 자세히 검사하려면 search 요청에 \"profile\": true를 추가한다.","tags":["slowlog","slow log","slow search","search","indexing","threshold"]},{"id":"rejections-heap-breaker","task":"429 거부, heap, circuit breaker 압박 진단","category":"성능","command":"# Thread pool queue and rejection counts (source of 429):\nGET _cat/thread_pool/write,search,get?v&h=node_name,name,active,queue,rejected,completed,size,queue_size&s=rejected:desc\nGET _cat/nodes?v&h=name,node.role,heap.percent,ram.percent,cpu,load_1m,disk.used_percent,master&s=heap.percent:desc\n# Circuit breaker trips and parent breaker usage:\nGET _nodes/stats/breaker?filter_path=nodes.*.name,nodes.*.breakers.*.tripped,nodes.*.breakers.parent\nGET _nodes/stats/jvm?filter_path=nodes.*.name,nodes.*.jvm.mem.heap_used_percent,nodes.*.jvm.gc.collectors.old\n# Fielddata memory usage (per field):\nGET _nodes/stats/indices/fielddata?fields=*&filter_path=nodes.*.indices.fielddata","what":"write/search thread pool 큐가 가득 차서 거부된 요청, 노드별 heap/CPU 상태, circuit breaker 발동, old GC 횟수, fielddata 메모리 사용량을 보여 준다.","when":"클라이언트가 429 Too Many Requests 또는 es_rejected_execution_exception을 받을 때, CircuitBreakingException ([parent] Data too large)이 나타날 때, 또는 노드에 긴 GC 정지가 있을 때.","caution":"rejected 카운터는 노드 시작 이후 누적값이므로 두 샘플 간 증가량을 본다. queue_size를 늘리면 거부가 가려지지만 heap과 지연이 증가한다. 실제 해결책은 bulk 크기/동시성 축소 또는 용량 추가다. heap이 85% 이상을 유지하고 old GC가 자주 실행되면 노드가 OOM에 가까워진 것이다. text 필드에 fielddata를 활성화하면 heap을 빠르게 소비한다.","since":"Elasticsearch 7.0","note":"parent breaker 기본값은 heap의 95%다(real memory circuit breaker, 7.0+). write 거부는 보통 단일 hot 노드에 집중되므로 샤드 분포와 라우팅(_routing)을 확인한다. fielddata를 일시적으로 해제하려면 POST _cache/clear?fielddata=true를 사용한다(cache-clear 참고).","tags":["429","rejected","thread pool","circuit breaker","heap","fielddata"]},{"id":"slm-snapshot-monitor","task":"SLM으로 스냅샷과 보존 자동화, 스냅샷 진행 모니터링","category":"스냅샷","command":"# Take a snapshot every night at 01:30, keep for 30 days (at least 7, at most 60 snapshots):\nPUT _slm/policy/gece-yedek\n{\n  \"schedule\": \"0 30 1 * * ?\",\n  \"name\": \"<gece-{now/d}>\",\n  \"repository\": \"yedek_repo\",\n  \"config\": { \"indices\": [\"*\"], \"include_global_state\": true },\n  \"retention\": { \"expire_after\": \"30d\", \"min_count\": 7, \"max_count\": 60 }\n}\n# Run the policy now and see success/failure statistics:\nPOST _slm/policy/gece-yedek/_execute\nGET _slm/stats\n# Snapshot in progress and per-shard progress:\nGET _snapshot/yedek_repo/_current\nGET _snapshot/yedek_repo/_status\nGET _cat/snapshots/yedek_repo?v&s=end_epoch:desc\n# Verify that the repository is writable from all nodes:\nPOST _snapshot/yedek_repo/_verify","what":"cron 일정으로 스냅샷을 만들고 보존 규칙에 따라 오래된 스냅샷을 삭제하는 SLM 정책을 정의한다. 실행 중인 스냅샷의 샤드별 진행 상황, 과거 스냅샷, repository 접근성을 보여 준다.","when":"정기 백업을 설정할 때, 야간 스냅샷 실패 알림 시, 또는 오래 걸리는 스냅샷이 언제 끝날지 추정할 때.","caution":"일정은 UTC로 평가된다. 둘 이상의 클러스터가 같은 repository에 쓰거나 다른 클러스터에 repository가 쓰기 가능으로 등록되어 있으면 손상될 수 있으므로 다른 클러스터에서는 readonly: true를 사용한다. 보존 규칙은 SLM이 만든 스냅샷만 삭제한다. include_global_state true는 복원 시 템플릿과 persistent 클러스터 설정도 복원할 수 있다.","since":"Elasticsearch 7.5","note":"스냅샷은 증분 방식이다: 이전 스냅샷에 없는 segment만 복사되므로 잦은 스냅샷이 repository에서 비례하는 공간을 차지하지 않는다. SLM은 7.4, 보존은 7.5에서 도입되었다. repository 이름 없이 _status는 클러스터에서 실행 중인 모든 스냅샷을 반환하며, 과거 스냅샷 이름을 주면 repository에서 읽으므로 느릴 수 있다.","tags":["slm","snapshot","retention","backup","schedule","snapshot status"],"untested":true},{"id":"security-user-role","task":"롤과 native 사용자 생성, 비밀번호 변경, API key 발급","category":"보안","command":"POST _security/role/log_okuma\n{\n  \"cluster\": [\"monitor\"],\n  \"indices\": [\n    { \"names\": [\"log-*\"], \"privileges\": [\"read\", \"view_index_metadata\"] }\n  ]\n}\nPOST _security/user/rapor_kullanici\n{\n  \"password\": \"Degistir-123!\",\n  \"roles\": [\"log_okuma\"],\n  \"full_name\": \"Rapor Servisi\"\n}\nGET _security/user/rapor_kullanici\n# User making this request and its roles:\nGET _security/_authenticate\nPOST _security/user/rapor_kullanici/_password\n{\n  \"password\": \"Yeni-Sifre-456!\"\n}\n# Time-limited API key with restricted privileges:\nPOST _security/api_key\n{\n  \"name\": \"rapor-key\",\n  \"expiration\": \"30d\",\n  \"role_descriptors\": { \"log_okuma\": { \"indices\": [ { \"names\": [\"log-*\"], \"privileges\": [\"read\"] } ] } }\n}\n# If the elastic user's password is lost (shell on a node):\n# bin/elasticsearch-reset-password -u elastic -i","what":"인덱스 패턴에 대한 읽기 권한을 가진 롤과 그 롤을 가진 native realm 사용자를 만들고, 비밀번호를 변경하고, 호출자의 신원을 확인하며, 권한을 좁힌 기한부 API key를 발급한다.","when":"최소 권한으로 리포팅, 애플리케이션, 모니터링 계정을 만들 때, 비밀번호 교체 중, 그리고 서비스를 사용자에서 API key로 옮길 때.","caution":"애플리케이션에서 elastic superuser를 사용하지 않는다. API key의 권한은 생성한 사용자의 권한을 넘을 수 없으며 role_descriptors는 권한을 좁히기만 한다. API key는 생성 시점 소유자 권한의 스냅샷으로 제한되므로 이후 롤 변경은 기존 key에 영향을 주지 않는다. 비밀번호 변경 후 이전 비밀번호를 쓰는 서비스는 401을 받으므로 전환을 계획한다. API key 응답의 encoded 값은 한 번만 반환되므로 저장해 둔다.","since":"Elasticsearch 8.0","note":"8.0+에서는 보안이 기본 활성화되어 있으며 elastic 비밀번호는 첫 시작 시 생성된다. elasticsearch-reset-password 도구는 8.0에서 도입되었다. API key는 GET _security/api_key?name=rapor-key로 나열하고 DELETE _security/api_key로 폐기한다. LDAP/AD/SAML realm 사용자의 롤 매핑은 _security/role_mapping으로 한다.","tags":["security","users","roles","api key","password","rbac"],"untested":true}]},"solver/slow-query.json":{"id":"slow-query","title":"느린 쿼리: 심층 진단","icon":"🐢","category":"성능","intro":"쿼리 하나 또는 시스템 전체가 느리다. 단계적으로 범위를 좁힌다: 각 노드에서 점검을 실행하고 결과에 따라 분기를 선택한다. 끝에 근본 원인 + 해결책에 도달한다.","start":"scope","nodes":{"scope":{"kind":"step","title":"무엇이 느린가?","detail":"특정 쿼리 하나 또는 몇 개가 느린가, 아니면 시스템 전체인가? 이에 따라 조사가 '쿼리 중심' 또는 '리소스/병목 중심' 중 하나로 나뉜다.","choices":[{"label":"특정 쿼리 하나 또는 몇 개가 느리다","next":"running"},{"label":"시스템 전체가 느리다","next":"system"}]},"running":{"kind":"step","title":"쿼리가 지금 실행 중인가?","detail":"지금 실행 중/멈춘 쿼리인가, 아니면 '대체로 느린' 쿼리인가? 활성 세션을 본다.","command":{"oracle":"SELECT sid, serial#, sql_id, last_call_et AS running_sec, event, blocking_session\nFROM v$session WHERE status='ACTIVE' AND type='USER' ORDER BY last_call_et DESC;","postgres":"SELECT pid, now()-query_start AS dur, state, wait_event, query\nFROM pg_stat_activity WHERE state='active' ORDER BY query_start;","mysql":"SELECT id, time, state, info FROM information_schema.processlist WHERE command='Query' ORDER BY time DESC;","mssql":"SELECT session_id, total_elapsed_time/1000 AS sec, status, wait_type, blocking_session_id\nFROM sys.dm_exec_requests WHERE session_id IN (SELECT session_id FROM sys.dm_exec_sessions WHERE is_user_process = 1) ORDER BY total_elapsed_time DESC;"},"choices":[{"label":"지금 실행 중 / 멈춤","next":"blocked"},{"label":"대체로 느림. 실행 계획을 보고 싶다","next":"plan"}]},"blocked":{"kind":"step","title":"쿼리가 무언가를 기다리고 있는가?","detail":"활성 상태인데 진행되지 않는다면 대기 중일 가능성이 높다. blocking_session / wait_event가 채워져 있는가?","command":{"oracle":"SELECT sid, blocking_session, event, seconds_in_wait\nFROM v$session WHERE sid=&target_sid;","postgres":"SELECT pid, pg_blocking_pids(pid) AS blocked_by, wait_event_type, wait_event\nFROM pg_stat_activity WHERE pid=&target_pid;","mssql":"SELECT session_id, blocking_session_id, wait_type, wait_resource\nFROM sys.dm_exec_requests WHERE session_id=&target;"},"choices":[{"label":"다른 세션에 의해 블로킹됨","next":"sol_blocking"},{"label":"리소스 대기 (IO / 로그 / latch)","next":"wait"},{"label":"대기 없음. 활발히 CPU를 소비 중","next":"plan"}]},"wait":{"kind":"step","title":"어떤 대기가 지배적인가?","detail":"지배적인 대기 이벤트가 병목의 유형을 알려준다.","command":{"oracle":"SELECT event, total_waits, time_waited FROM v$session_event WHERE sid=&target_sid ORDER BY time_waited DESC FETCH FIRST 5 ROWS ONLY;","postgres":"SELECT wait_event_type, wait_event, count(*) FROM pg_stat_activity WHERE state='active' GROUP BY 1,2 ORDER BY 3 DESC;","mssql":"SELECT TOP 5 wait_type, waiting_tasks_count, wait_time_ms FROM sys.dm_os_wait_stats ORDER BY wait_time_ms DESC;"},"choices":[{"label":"IO 대기 (db file read / PAGEIOLATCH)","next":"sol_io"},{"label":"로그 / 커밋 대기 (log file sync / WRITELOG)","next":"sol_log"},{"label":"락 / enqueue (enq: TX / LCK)","next":"sol_blocking"}]},"plan":{"kind":"step","title":"실행 계획 확인 (EXPLAIN)","detail":"실행 계획을 가져온다. full table scan, 예상치 못한 조인 순서, 추정 행 수와 실제 행 수의 차이가 있는가?","command":{"oracle":"SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('&sql_id', NULL, 'ALLSTATS LAST'));","postgres":"EXPLAIN (ANALYZE, BUFFERS) <query>;","mysql":"EXPLAIN ANALYZE <query>;","mssql":"SET STATISTICS PROFILE ON; <query>; SET STATISTICS PROFILE OFF;","mongodb":"db.collection.find({...}).explain('executionStats')  // COLLSCAN or IXSCAN?"},"choices":[{"label":"full scan / 인덱스 누락","next":"sol_index"},{"label":"어제는 빨랐다. 실행 계획이 갑자기 바뀌었다","next":"sol_planchange"},{"label":"너무 많은 행을 처리 / N+1","next":"sol_rewrite"},{"label":"실행 계획은 좋아 보이지만 느리다","next":"stats"}]},"stats":{"kind":"step","title":"통계가 최신인가?","detail":"추정 행 수와 실제 행 수(E-rows vs A-rows)의 큰 차이는 오래된 통계의 신호다.","command":{"oracle":"SELECT table_name, num_rows, last_analyzed FROM dba_tables WHERE table_name='&TABLE_NAME';","postgres":"SELECT relname, last_analyze, last_autoanalyze, n_live_tup FROM pg_stat_user_tables WHERE relname='&table_name';","mysql":"SELECT table_name, table_rows, update_time FROM information_schema.tables WHERE table_name='&table_name';","mssql":"SELECT name, STATS_DATE(object_id, stats_id) AS last_updated FROM sys.stats WHERE object_id=OBJECT_ID('&table_name');"},"choices":[{"label":"통계가 오래되었다","next":"sol_stats"},{"label":"통계는 최신이지만 여전히 느리다","next":"sol_deep"}]},"system":{"kind":"step","title":"시스템 전체: 병목은 어디인가?","detail":"쿼리 하나가 아니라 모든 것이 느리다면 리소스 병목을 찾는다: CPU, IO, 메모리, 또는 광범위한 블로킹인가?","command":{"oracle":"SELECT metric_name, value FROM v$sysmetric WHERE metric_name IN ('Host CPU Utilization (%)','Average Active Sessions') AND intsize_csec=(SELECT MAX(intsize_csec) FROM v$sysmetric);","postgres":"SELECT state, wait_event_type, count(*) FROM pg_stat_activity GROUP BY 1,2 ORDER BY 3 DESC;","mssql":"SELECT TOP 5 wait_type, wait_time_ms FROM sys.dm_os_wait_stats ORDER BY wait_time_ms DESC;"},"choices":[{"label":"CPU가 100%에 가까움","next":"sol_cpu"},{"label":"높은 IO 대기","next":"sol_io"},{"label":"메모리 압박 / 낮은 캐시 적중률","next":"sol_mem"},{"label":"블로킹이 많음","next":"sol_blocking"}]},"sol_blocking":{"kind":"solution","title":"근본 원인: 블로킹","detail":"쿼리가 다른 세션이 보유한 락을 기다리고 있다. 체인의 맨 앞 세션(루트 블로커)을 찾아 해결한다.","steps":["블로킹 체인의 맨 앞(자신은 대기하지 않지만 다른 세션을 기다리게 하는 세션)을 찾는다.","루트 블로커가 무엇을 하는지 파악한다: 커밋되지 않은 긴 트랜잭션인가, 'idle in transaction'인가?","애플리케이션이 commit/rollback할 수 있다면 그렇게 하게 한다. 그럴 수 없고 작업이 멈춰 있다면 루트 블로커를 종료한다.","근본 원인을 해결한다: 누락된 인덱스가 광범위한 락을 일으킬 수 있다. 트랜잭션을 짧게 하고 격리 수준을 검토한다."],"related":["blocking-sessions","kill-session"],"playbook":"blocking"},"sol_io":{"kind":"solution","title":"근본 원인: IO 병목","detail":"쿼리가 디스크에서 많이 읽는다. 보통 full scan(인덱스 누락)이나 메모리 부족(buffer cache) 때문이다.","steps":["full scan을 일으키는 필터에 적합한 인덱스를 추가한다(실행 계획이 index scan, IXSCAN으로 바뀌어야 함).","buffer cache / buffer pool 적중률을 확인하고 낮으면 메모리를 늘린다.","불필요한 컬럼/행 읽기를 줄인다(SELECT * 대신 필요한 컬럼, 페이지네이션).","스토리지가 실제로 느리다면(높은 IO 지연) 디스크 계층을 개선한다."],"related":["cache-hit-ratio","create-index"]},"sol_log":{"kind":"solution","title":"근본 원인: 커밋 / 로그 병목","detail":"'log file sync' / 'WRITELOG'가 지배적이면 커밋이 redo/WAL 쓰기를 기다리고 있는 것이다.","steps":["잦은 단일 행 커밋을 배치 커밋으로 바꾸고 불필요한 커밋을 줄인다.","redo/WAL 디스크를 빠르게 한다(별도의 빠른 디스크). 로그 파일 크기/개수를 검토한다.","synchronous_commit / commit_write 설정을 워크로드와 내구성 trade-off에 비추어 평가한다."],"related":["show-config-param"]},"sol_index":{"kind":"solution","title":"근본 원인: 인덱스 누락","detail":"실행 계획이 full scan을 한다. 필터/조인 컬럼에 적합한 인덱스를 두면 크게 빨라진다.","steps":["WHERE/JOIN 컬럼에 적합한 인덱스(필요하면 복합)를 온라인으로 추가한다.","인덱스 추가 후 실행 계획을 다시 확인해 index scan(IXSCAN)으로 바뀌었는지 검증한다.","쓰기 부하가 크면 인덱스 비용을 따져 보고 불필요한 인덱스를 삭제한다.","선택도가 낮은 컬럼에서는 인덱스가 도움이 되지 않으므로 선택도를 확인한다."],"related":["create-index","explain-plan"]},"sol_planchange":{"kind":"solution","title":"근본 원인: 실행 계획 회귀","detail":"쿼리가 어제는 빨랐지만 오늘은 아니다. 실행 계획이 바뀌었다. 보통 오래된 통계나 파라미터 변경 때문이다.","steps":["먼저 새 통계를 수집한다(오래된 통계가 가장 흔한 원인).","좋은 실행 계획을 고정한다: Oracle SQL Plan Baseline / 힌트, MSSQL Query Store 'force plan', PG 설정.","최근 업그레이드/패치/파라미터 변경이 있었다면 검토한다(옵티마이저 동작)."],"related":["update-statistics"]},"sol_rewrite":{"kind":"solution","title":"근본 원인: 쿼리 설계","detail":"실행 계획은 합리적이지만 쿼리 자체가 너무 많은 일을 한다(너무 많은 행, N+1, 불필요한 조인).","steps":["결과 집합을 줄인다: 페이지네이션(LIMIT/OFFSET 또는 keyset), 조기 필터링.","N+1 패턴을 단일 배치 쿼리로 바꾸고 불필요한 조인/서브쿼리를 단순화한다.","반복되는 무거운 리포트에는 materialized view / 요약 테이블을 고려한다."],"related":["explain-plan","materialized-view"]},"sol_stats":{"kind":"solution","title":"근본 원인: 오래된 통계","detail":"통계가 최신이 아니어서 옵티마이저가 잘못 추정한다.","steps":["영향받은 테이블의 통계를 수동으로 수집한다.","자동 수집 작업(autovacuum/auto stats/gather job)이 실행 중인지 확인한다.","갑작스러운 대량 데이터 적재 후 워크플로에 통계 수집을 추가한다."],"related":["update-statistics"]},"sol_deep":{"kind":"solution","title":"심층 분석","detail":"실행 계획과 통계에 문제가 없는데도 느리다면 더 깊이 파고든다.","steps":["세션 수준 trace를 활성화한다: Oracle 10046, PG auto_explain, MySQL profiling, MSSQL Extended Events.","병렬 처리, memory grant, sort/hash spill(tempdb/temp)을 검사한다.","하드웨어/OS 계층(CPU steal, IO 지연, 네트워크)을 배제한다.","연결 수와 동시성으로 인한 경합을 평가한다."],"related":["session-trace","top-wait-events"]},"sol_cpu":{"kind":"solution","title":"근본 원인: CPU 병목","detail":"시스템 전체가 CPU에 묶여 있다. 보통 실행 계획이 나쁜 소수의 쿼리가 CPU를 소비한다.","steps":["CPU를 가장 많이 쓰는 상위 10개 쿼리를 찾아 실행 계획(인덱스/통계)을 수정한다.","폭주 쿼리를 제한한다(Resource Manager / Resource Governor).","사용률이 계속 높으면 확장한다(CPU 추가, 읽기를 read replica로 분산)."],"related":["top-slow-queries"],"playbook":"high-cpu"},"sol_mem":{"kind":"solution","title":"근본 원인: 메모리 압박","detail":"낮은 캐시 적중률 및/또는 스와핑: 메모리가 부족하거나 잘못 할당되어 있다.","steps":["buffer pool / SGA와 세션 메모리(work_mem/PGA)의 크기를 올바르게 정한다(도구 > 메모리 계산기).","메모리를 소비하는 폭주 쿼리를 찾아 수정한다.","시스템이 스와핑 중이면 스왑에서 벗어나는 것이 우선이다. 필요하면 RAM을 추가한다."],"related":["cache-hit-ratio","show-config-param"],"playbook":"memory-pressure"}}},"solver/deadlock.json":{"id":"deadlock","title":"데드락 / 락 충돌 (ORA-00060, 1213, 40P01)","icon":"♻️","category":"락","intro":"데드락이 발생했다(Oracle ORA-00060, MySQL 1213, PG 40P01, MSSQL 1205). 그래프를 수집하고, 패턴을 파악하고, 재발을 막는다. 데드락은 설계로 해결한다 — 목표는 빈도를 줄이는 것이다.","start":"capture","nodes":{"capture":{"kind":"step","title":"먼저 deadlock graph를 수집한다","detail":"어떤 두 개(또는 그 이상)의 세션이, 어떤 리소스에서, 어떤 락 모드로 충돌했는가? 이것을 보지 않고 맹목적인 수정을 적용하지 않는다.","command":{"oracle":"-- Check the trace file referenced in the alert log (deadlock graph). For history:\nSELECT * FROM dba_hist_active_sess_history WHERE event LIKE 'enq: TX%' ORDER BY sample_time DESC FETCH FIRST 20 ROWS ONLY;","mysql":"SHOW ENGINE INNODB STATUS\\G  -- 'LATEST DETECTED DEADLOCK' section","postgres":"-- Deadlock errors are always written to postgresql.log as ERROR (log_lock_waits not required); log_lock_waits=on also logs lock waits longer than deadlock_timeout.","mssql":"SELECT XEvent.query('.') FROM (SELECT CAST(target_data AS XML) td FROM sys.dm_xe_session_targets t JOIN sys.dm_xe_sessions s ON s.address=t.event_session_address WHERE s.name='system_health') x CROSS APPLY td.nodes('//event[@name=\"xml_deadlock_report\"]') AS q(XEvent);"},"choices":[{"label":"그래프 확보 — 두 트랜잭션이 리소스를 반대 순서로 잠근다","next":"sol_order"},{"label":"많은 트랜잭션이 단일 행/테이블에 몰린다(hotspot)","next":"sol_hotspot"},{"label":"넓은 범위 락이 있다(많은 행이 잠김)","next":"index_check"}]},"index_check":{"kind":"step","title":"락을 거는 쿼리가 인덱스를 사용하는가?","detail":"인덱스가 없으면 UPDATE/DELETE가 WHERE 절을 만족시키기 위해 많은 행(심지어 테이블 전체)을 잠근다 → 데드락의 원인. 실행 계획을 확인한다.","command":{"mysql":"EXPLAIN <the UPDATE/DELETE involved in the deadlock>;  -- type=ALL means a full scan, no index","postgres":"EXPLAIN <query>;  -- Seq Scan or Index Scan?","oracle":"SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('&sql_id'));"},"choices":[{"label":"인덱스 없음 / full scan 수행","next":"sol_index"},{"label":"인덱스는 있지만 여전히 충돌한다","next":"sol_order"}]},"sol_order":{"kind":"solution","title":"근본 원인: 락 획득 순서 불일치","detail":"데드락의 가장 흔한 원인: 두 트랜잭션이 서로 다른 순서로 리소스에 접근한다. A→B와 B→A가 동시에 일어나면 데드락이 된다.","steps":["모든 코드 경로에서 리소스에 같은 순서로 접근한다(예: 항상 가장 낮은 id → 가장 높은 id).","트랜잭션을 짧게 유지한다. 사용자 상호작용/긴 작업은 트랜잭션 밖으로 옮긴다.","필요하면 결정적인 순서로 락을 미리 잡는다(정렬된 SELECT ... FOR UPDATE).","애플리케이션에 데드락 희생자에 대한 자동 재시도(retry + backoff)를 추가한다 — 필수다."],"related":["blocking-sessions","open-transactions"]},"sol_index":{"kind":"solution","title":"근본 원인: 인덱스 누락 → 넓은 락","detail":"인덱스 없는 UPDATE/DELETE는 필요 이상으로 많은 행을 잠가 데드락 가능성을 크게 높인다.","steps":["WHERE 절의 컬럼에 인덱스를 추가하면 락이 영향받는 행으로 한정된다.","실행 계획을 다시 확인해 full scan(Seq Scan / type=ALL)이 인덱스 접근으로 바뀌었는지 검증한다.","대량 업데이트는 작은 배치(chunk)로 실행한다."],"related":["create-index","list-indexes"]},"sol_hotspot":{"kind":"solution","title":"근본 원인: hotspot","detail":"많은 트랜잭션이 카운터 행/key 같은 단일 리소스에 동시에 몰린다.","steps":["hot 행을 분산한다: sequence/identity를 사용하고 카운터를 분할한다(sharded counter).","트랜잭션 시간을 줄여 락을 보유하는 시간을 단축한다.","가능하면 낙관적 락(version/timestamp)이나 큐 패턴을 사용한다."],"related":["sequence","open-transactions"]}}},"solver/disk-full.json":{"id":"disk-full","title":"디스크 / 테이블스페이스 가득 참","icon":"💽","category":"용량","intro":"'unable to extend', 'disk full', 'could not extend' 또는 'log full'이 발생했다. 당황하지 않는다: 먼저 무엇이 가득 찼는지 찾고, 안전하게 공간을 회수한 다음, 영구적으로 해결한다.","start":"what","nodes":{"what":{"kind":"step","title":"무엇이 가득 찼는가?","detail":"엉뚱한 곳을 정리해도 소용없다. 먼저 사용량을 측정한다: 데이터 파일인가, 트랜잭션 로그인가, archive/redo인가, temp인가, OS 디스크인가?","command":{"oracle":"SELECT d.tablespace_name, ROUND((1-(d.alloc-NVL(f.free,0))/d.maxb)*100,1) AS pct_free\nFROM (SELECT tablespace_name, SUM(bytes) alloc, SUM(GREATEST(bytes,maxbytes)) maxb FROM dba_data_files GROUP BY tablespace_name) d LEFT JOIN (SELECT tablespace_name, SUM(bytes) free FROM dba_free_space GROUP BY tablespace_name) f ON f.tablespace_name = d.tablespace_name ORDER BY pct_free;","postgres":"SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database ORDER BY 2 DESC;  -- OS disk: df -h","mssql":"SELECT name, size/128.0 AS size_mb, size/128.0 - CAST(FILEPROPERTY(name,'SpaceUsed') AS int)/128.0 AS free_mb, type_desc\nFROM sys.database_files;","mysql":"SELECT table_schema, ROUND(SUM(data_length+index_length)/1048576,1) AS mb FROM information_schema.tables GROUP BY table_schema ORDER BY mb DESC;"},"choices":[{"label":"데이터 파일 / 테이블스페이스가 가득 참","next":"data"},{"label":"트랜잭션 로그 / archive-redo가 가득 참","next":"log"},{"label":"temp / undo가 가득 참","next":"sol_temp"},{"label":"OS 디스크가 가득 참 (DB가 아님)","next":"sol_os"}]},"data":{"kind":"step","title":"가장 큰 소비자는 무엇인가?","detail":"대부분 폭주한 단일 테이블/segment가 원인이다. 가장 큰 것들을 찾는다.","command":{"oracle":"SELECT segment_name, segment_type, ROUND(bytes/1048576,1) AS mb FROM dba_segments ORDER BY bytes DESC FETCH FIRST 10 ROWS ONLY;","postgres":"SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) FROM pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;","mssql":"SELECT TOP 10 OBJECT_NAME(object_id) AS table_name, SUM(reserved_page_count)*8/1024 AS mb FROM sys.dm_db_partition_stats GROUP BY object_id ORDER BY mb DESC;"},"choices":[{"label":"예상치 못하게 큰/폭주한 테이블이 있다","next":"sol_runaway"},{"label":"정상 증가이며 공간이 실제로 부족하다","next":"sol_extend"},{"label":"dead row / bloat 과다","next":"sol_reclaim"}]},"log":{"kind":"step","title":"로그가 왜 커지는가?","detail":"가장 오래된 활성 트랜잭션/백업이 완료될 때까지 로그/아카이브를 정리할 수 없다. 왜 쌓였는가?","command":{"mssql":"SELECT name, log_reuse_wait_desc FROM sys.databases WHERE name=DB_NAME();  -- why can it not be truncated?","postgres":"SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained FROM pg_replication_slots ORDER BY 3 DESC;","oracle":"SELECT * FROM v$flash_recovery_area_usage;  -- FRA (archive) usage"},"choices":[{"label":"MSSQL: 로그 백업이 수행되지 않음 (LOG_BACKUP)","next":"sol_logbackup"},{"label":"PG: 멈춘 replication slot이 WAL을 붙잡고 있음","next":"sol_slot"},{"label":"Oracle: 아카이브 로그 영역(FRA)이 가득 참","next":"sol_archive"},{"label":"긴 열린 트랜잭션이 로그를 붙잡고 있음","next":"sol_longtxn"}]},"sol_runaway":{"kind":"solution","title":"근본 원인: 폭주한 테이블/segment","detail":"단일 객체가 예기치 않게 커졌다(통제되지 않는 insert, 정리되지 않는 큐/로그 테이블).","steps":["객체가 실제로 필요한지 확인한다. 로그/큐 테이블이면 보존 정책을 적용한다.","불필요한 오래된 데이터를 partition drop / TRUNCATE로 정리한다(백업/승인 후).","애플리케이션에서 통제되지 않는 증가를 멈추고 증가 알림을 설정한다."],"related":["largest-tables","create-partition"]},"sol_extend":{"kind":"solution","title":"근본 원인: 실제 용량 필요","detail":"정상 증가이며 공간을 추가해야 한다.","steps":["데이터 파일에 공간을 추가하거나 AUTOEXTEND를 활성화한다(Oracle: ALTER DATABASE DATAFILE ... AUTOEXTEND ON MAXSIZE).","필요하면 새 데이터 파일/테이블스페이스를 추가하거나 디스크를 늘린다.","더 이상 놀라지 않도록 사용량 80%/90% 알림을 설정한다."],"related":["free-space"]},"sol_reclaim":{"kind":"solution","title":"근본 원인: bloat / dead row","detail":"삭제/수정된 행의 공간이 회수되지 않았다.","steps":["공간을 회수한다: PG VACUUM(필요하면 pg_repack), Oracle SHRINK SPACE, MySQL OPTIMIZE TABLE, Mongo compact.","autovacuum/자동 유지보수 설정을 더 공격적으로 만든다.","VACUUM FULL / OPTIMIZE는 테이블을 잠그므로 피크 시간 밖에 실행한다."],"related":["reclaim-space","free-space"]},"sol_temp":{"kind":"solution","title":"근본 원인: temp / undo 가득 참","detail":"큰 sort/hash나 긴 열린 트랜잭션이 임시 공간을 채웠다.","steps":["temp를 폭증시킨 쿼리(큰 sort/hash spill)를 찾아 최적화한다.","긴 열린 트랜잭션을 찾는다. 그것이 undo를 붙잡고 있다(Oracle).","필요하면 temp/undo 공간을 늘리고 보존 설정을 검토한다."],"related":["open-transactions","reclaim-space"]},"sol_os":{"kind":"solution","title":"근본 원인: OS 디스크 가득 참","detail":"문제는 DB가 아니라 운영체제 디스크에 있다. 로그, core dump, 오래된 백업, trace 파일이 공간을 차지하고 있을 수 있다.","steps":["df -h로 가득 찬 파일 시스템을 찾고 가장 큰 디렉터리를 스캔한다(du -sh *).","오래된 로그/trace/core dump/백업 파일을 정리한다(승인 후).","DB 진단 디렉터리(Oracle ADR, PG 로그)에 rotation을 적용한다."]},"sol_logbackup":{"kind":"solution","title":"근본 원인: 로그 백업 미수행 (SQL Server)","detail":"FULL 복구 모델에서 로그 백업을 하지 않으면 로그가 무한히 커진다.","steps":["로그 백업(BACKUP LOG)을 수행한다 — 로그를 재사용할 수 있게 된다.","복구 모델이 실제로 SIMPLE이어야 한다면 변경한다.","꼭 필요할 때만 로그 파일을 축소하고(DBCC SHRINKFILE) 정기 로그 백업 작업을 설정한다."],"related":["last-backup-status"],"playbook":"log-growth"},"sol_slot":{"kind":"solution","title":"근본 원인: 방치된 replication slot (PG)","detail":"멈춘(비활성) slot이 WAL 삭제를 막아 pg_wal을 부풀린다.","steps":["slot이 실제로 사용 중인지 확인한다.","불필요하면 삭제한다: SELECT pg_drop_replication_slot('slot_name');","slot이 진행되도록 활성 replica를 수정하고 slot 지연 알림을 설정한다."],"related":["free-space"],"playbook":"log-growth"},"sol_archive":{"kind":"solution","title":"근본 원인: 아카이브 로그 영역(FRA) 가득 참 (Oracle)","detail":"아카이브 로그가 쌓여 FRA가 가득 찼다. archiver가 멈추면 데이터베이스가 쓰기를 받지 못할 수 있다.","steps":["이미 백업된 오래된 아카이브 로그를 RMAN으로 삭제한다(백업 없이 절대 삭제하지 않는다).","필요하면 FRA 크기(db_recovery_file_dest_size)를 늘린다.","아카이브 로그 정리/백업 작업이 정기적으로 실행되도록 설정한다."],"related":["last-backup-status"]},"sol_longtxn":{"kind":"solution","title":"근본 원인: 긴 열린 트랜잭션","detail":"가장 오래된 활성 트랜잭션이 로그/undo 회수를 막고 있다.","steps":["가장 오래된 열린 트랜잭션을 찾아 commit/rollback하게 한다(또는 소유자와 이야기한다).","세션을 'idle in transaction'으로 남기는 애플리케이션 동작을 고치고 타임아웃을 설정한다.","긴 리포트는 별도 replica/시간대로 옮긴다."],"related":["open-transactions"],"playbook":"log-growth"}}},"solver/connection-storm.json":{"id":"connection-storm","title":"커넥션 폭주 (too many connections)","icon":"🌊","category":"용량","intro":"'too many connections', 'remaining connection slots reserved' 또는 ORA-00018이 발생해 새 연결이 거부되고 있다. 원인을 찾고, 지금 압박을 완화하고, 근본적으로 해결한다.","start":"measure","nodes":{"measure":{"kind":"step","title":"상황 측정: 연결이 몇 개이고 그중 활성은 몇 개인가?","detail":"먼저 전체 연결 수, 한도, 활성/유휴 비율을 본다.","command":{"oracle":"SELECT status, COUNT(*) FROM v$session WHERE type='USER' GROUP BY status;\nSELECT name, value FROM v$parameter WHERE name IN ('processes','sessions');","postgres":"SELECT state, count(*) FROM pg_stat_activity GROUP BY state;\nSHOW max_connections;","mysql":"SHOW STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';","mssql":"SELECT status, COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process=1 GROUP BY status;","redis":"INFO clients"},"choices":[{"label":"대부분의 연결이 idle / idle in transaction이다","next":"sol_idle"},{"label":"대부분 활성이다. 원인을 찾아야 한다","next":"who"}]},"who":{"kind":"step","title":"누가/어디서? 연결을 출처별로 그룹화한다","detail":"보통 단일 애플리케이션 서버의 풀이 폭증했거나 연결 누수가 있다. 호스트/사용자/애플리케이션별 분포를 본다.","command":{"postgres":"SELECT client_addr, usename, application_name, count(*) FROM pg_stat_activity GROUP BY 1,2,3 ORDER BY 4 DESC;","mysql":"SELECT host, user, count(*) FROM information_schema.processlist GROUP BY host, user ORDER BY 3 DESC;","mssql":"SELECT host_name, program_name, login_name, COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process=1 GROUP BY host_name, program_name, login_name ORDER BY 4 DESC;","oracle":"SELECT machine, program, COUNT(*) FROM v$session WHERE type='USER' GROUP BY machine, program ORDER BY 3 DESC;"},"choices":[{"label":"단일 애플리케이션/호스트가 연결을 폭증시켰다(풀)","next":"sol_pool"},{"label":"부하가 실제로 증가했다. 모두 정상적이고 활성이다","next":"sol_scale"}]},"sol_idle":{"kind":"solution","title":"근본 원인: 유휴 연결 누적 / 누수","detail":"연결이 열리고 닫히지 않거나 'idle in transaction'으로 남아 slot을 헛되이 점유한다.","steps":["오래된 idle / idle-in-transaction 연결을 정리한다. 관리자용으로 예약된 slot을 사용한다.","서버 측 타임아웃을 설정한다: PG idle_in_transaction_session_timeout, MySQL wait_timeout, Oracle profile idle_time.","애플리케이션이 연결(과 트랜잭션)을 닫도록 하고, 누수를 근원에서 고친다."],"related":["open-transactions","kill-session","connection-count"],"playbook":"connection-storm"},"sol_pool":{"kind":"solution","title":"근본 원인: 애플리케이션 풀 폭증","detail":"애플리케이션 서버의 connection pool이 무제한/너무 크거나 누수가 있다. 그것이 연결을 만들어 내고 있다.","steps":["즉시: 문제 애플리케이션의 유휴 연결을 종료하고 풀을 재시작한다.","애플리케이션 측에 적절한 풀 크기를 설정한다(도구 > Connection Pool 계산기).","서버와 DB 사이에 pooler를 둔다(PgBouncer / ProxySQL / Oracle DRCP / Connection Manager (CMAN)).","max_connections를 맹목적으로 늘리지 않는다. 메모리를 소비하고 문제를 미룰 뿐이다."],"related":["connection-count","kill-session"],"playbook":"connection-storm"},"sol_scale":{"kind":"solution","title":"근본 원인: 실제 부하 증가","detail":"연결은 정상적이고 활성이다. 용량이 실제로 부족하다.","steps":["connection pooler로 다중화하여 DB에 도달하는 연결 수를 줄인다.","읽기 부하를 read replica로 분산한다.","필요하면 메모리 예산에 맞춰 통제된 방식으로 max_connections를 늘리고 서버 메모리도 확장한다."],"related":["connection-count"],"playbook":"connection-storm"}}},"solver/replication-lag.json":{"id":"replication-lag","title":"복제 지연 / 중지","icon":"🔁","category":"복제","intro":"standby/replica가 primary보다 뒤처지거나 복제가 완전히 멈췄다. 이 트리는 지연 유형(증가, 일정, 중지)을 단계적으로 좁혀 apply 오류, 단일 스레드 병목, recovery conflict, 손상 같은 근본 원인에 도달한다.","start":"root","nodes":{"root":{"kind":"step","title":"지연이 어떻게 움직이는가?","detail":"먼저 상황을 측정한다. replica 스트림과 지연 지표를 본다: 지연이 시간에 따라 증가하는가, 일정 수준에 머무는가, 아니면 apply가 완전히 멈췄는가? 이에 따라 조사가 세 가지 주요 분기로 나뉜다.","command":{"postgres":"SELECT client_addr, state, write_lag, flush_lag, replay_lag,\n       pg_wal_lsn_diff(sent_lsn, replay_lsn) AS bytes_behind\nFROM pg_stat_replication;","mysql":"SHOW REPLICA STATUS\\G","oracle":"SELECT name, value, unit FROM v$dataguard_stats WHERE name IN ('apply lag','transport lag');","mssql":"SELECT database_id, is_local, synchronization_state_desc, redo_queue_size, log_send_queue_size\nFROM sys.dm_hadr_database_replica_states;","redis":"INFO replication","elasticsearch":"GET _cluster/health"},"choices":[{"label":"apply가 완전히 멈춤 / 오류가 있음","next":"stopped"},{"label":"지연이 계속 증가함 (따라잡지 못함)","next":"growing"},{"label":"지연은 일정하지만 긴 쿼리가 의심됨","next":"standby_query"},{"label":"크게 뒤처짐 / 불일치로 보임","next":"sol_rebuild"}]},"stopped":{"kind":"step","title":"apply 오류 메시지는 무엇인가?","detail":"복제가 멈췄다면 보통 적용 중인 이벤트에 오류가 있다. 오류 메시지를 본다: 대상에 행/테이블이 없는가, primary key 충돌인가, 연결/네트워크 끊김인가?","command":{"mysql":"SHOW REPLICA STATUS\\G  -- Last_Error, Last_SQL_Error, Replica_IO_Running, Replica_SQL_Running","postgres":"SELECT status, last_msg_receipt_time, latest_end_lsn, slot_name\nFROM pg_stat_wal_receiver;","oracle":"SELECT process, status, sequence#, block# FROM v$managed_standby WHERE process LIKE 'MRP%';","elasticsearch":"GET _cat/recovery?v&active_only=true"},"choices":[{"label":"충돌/누락된 행 또는 테이블 오류","next":"sol_apply_error"},{"label":"네트워크/연결 끊김, IO 스레드 중지","next":"sol_connection"}]},"growing":{"kind":"step","title":"왜 따라잡지 못하는가?","detail":"지연이 계속 증가하면 replica가 primary의 쓰기 속도를 따라가지 못하는 것이다. 단일 스레드 apply가 병목인가, replica의 디스크/IO가 느린가? apply worker 상태와 IO 대기를 본다.","command":{"mysql":"SHOW REPLICA STATUS\\G  -- Seconds_Behind_Source and apply thread state\nSELECT * FROM performance_schema.replication_applier_status_by_worker;","postgres":"SELECT pid, wait_event_type, wait_event, state\nFROM pg_stat_activity WHERE backend_type='startup';","mssql":"SELECT database_id, redo_queue_size, redo_rate, log_send_queue_size\nFROM sys.dm_hadr_database_replica_states;"},"choices":[{"label":"단일 스레드 apply + 많은 쓰기 부하","next":"sol_parallel"},{"label":"replica의 디스크/IO가 느리다","next":"sol_io"}]},"standby_query":{"kind":"step","title":"standby에 긴 쿼리가 있는가?","detail":"읽기 전용 standby에서 실행되는 긴 쿼리는 apply(recovery)를 막을 수 있다. PostgreSQL에서는 이렇게 recovery conflict가 발생한다. standby의 활성 쿼리와 conflict 카운터를 본다.","command":{"postgres":"SELECT now()-query_start AS dur, pid, state, query\nFROM pg_stat_activity WHERE state='active' ORDER BY query_start;\nSELECT confl_snapshot, confl_lock, confl_bufferpin FROM pg_stat_database_conflicts WHERE datname=current_database();"},"choices":[{"label":"예, 긴 쿼리가 apply를 막고 있다","next":"sol_recovery_conflict"},{"label":"아니요, 쿼리는 없지만 여전히 뒤처져 있다","next":"sol_parallel"}]},"sol_apply_error":{"kind":"solution","title":"근본 원인: apply 오류 (데이터 충돌)","detail":"replica가 적용할 수 없는 이벤트에 멈춰 있다: 대상에 없는 행, primary key 충돌, 또는 누락된 테이블. 맹목적으로 건너뛰지 말고 불일치의 원인을 해결한다.","steps":["오류와 멈춘 이벤트를 검사한다: 어떤 테이블의 어떤 작업(delete/update/insert)이 실패했는가?","근본 원인을 찾는다: 대상에 수동으로 기록된 데이터, 누락된 DDL(테이블/컬럼), 또는 손상된 binlog/WAL 항목.","스키마를 맞춘다: 누락된 테이블/컬럼을 추가하고, 충돌하는 행을 primary와 비교해 수정한다.","이벤트를 건너뛰어야 한다면(예: MySQL의 단일 이벤트) 문서화하고 이후 replica의 데이터 일관성을 검증한다.","재발 방지: 대상에 대한 직접 쓰기를 막고 DDL은 primary를 통해 실행한다."],"related":["replication-status"],"playbook":"replication-lag"},"sol_connection":{"kind":"solution","title":"근본 원인: 연결 / 네트워크 끊김","detail":"수신(IO/receiver) 스레드가 primary에 연결하지 못한다. 네트워크, 인증, 또는 누적/삭제된 로그 segment가 문제일 수 있다.","steps":["primary와 replica 간 네트워크 접근과 복제 사용자의 권한을 확인한다.","필요한 WAL/binlog segment가 primary에 아직 있는지 확인한다(retention/expire).","PostgreSQL에서는 replication slot이 존재하고 활성인지 확인한다. slot이 없으면 WAL이 일찍 삭제되었을 수 있다.","연결을 재시작한다. 로그가 다시 흐르기 시작하면 지연이 해소되어야 한다."],"related":["replication-status","list-replicas"],"playbook":"replication-lag"},"sol_parallel":{"kind":"solution","title":"근본 원인: 단일 스레드 apply 병목","detail":"primary는 병렬로 쓰는데 replica는 변경을 단일 순서로 적용하여 따라가지 못한다. apply를 병렬화한다.","steps":["병렬 apply 활성화: MySQL에서는 replica_parallel_workers + replica_parallel_type=LOGICAL_CLOCK.","PostgreSQL 물리 복제에서 apply는 단일 프로세스이므로 큰 트랜잭션을 나누어 primary의 생성 속도를 줄인다.","무거운 배치 작업을 트래픽이 적은 시간대로 옮기거나 분할한다.","변경 후 지연 지표를 모니터링한다. 0으로 줄어들어야 한다."],"related":["replication-status"],"playbook":"replication-lag"},"sol_io":{"kind":"solution","title":"근본 원인: replica IO 병목","detail":"apply 스레드가 CPU가 아니라 디스크를 기다리고 있다. replica의 스토리지가 primary의 쓰기 속도를 따라가지 못한다.","steps":["replica 디스크의 IO 지연/포화도를 측정하고 primary와 비교한다.","replica의 하드웨어가 더 약하다면(느린 디스크, 적은 RAM) primary에 맞춘다. 비대칭 하드웨어는 흔한 원인이다.","redo/WAL apply를 위한 디스크를 빠르게 하고 buffer/cache 크기를 충분히 정한다.","개선되지 않으면 쓰기 부하를 줄이거나 읽기 트래픽을 다른 replica로 분산한다."],"related":["replication-status"],"playbook":"replication-lag"},"sol_recovery_conflict":{"kind":"solution","title":"근본 원인: standby recovery conflict","detail":"standby의 긴 읽기 전용 쿼리가 primary의 변경 적용을 막고 있다(vacuum/락 충돌). PostgreSQL은 apply를 지연하거나 쿼리를 취소하여 이를 해결한다.","steps":["블로킹하는 긴 쿼리를 찾는다. 정말 필요하면 끝날 때까지 기다리고, 그렇지 않으면 종료한다.","hot_standby_feedback=on으로 primary가 standby 쿼리를 존중하게 한다(primary의 bloat가 대가).","또는 max_standby_streaming_delay를 워크로드에 맞게 조정한다(지연 허용과 쿼리 취소 사이의 균형).","긴 리포트를 apply를 막지 않는 별도 replica나 다른 시간대로 옮긴다."],"related":["replication-status","open-transactions"],"playbook":"replication-lag"},"sol_rebuild":{"kind":"solution","title":"근본 원인: replica가 너무 뒤처짐 / 손상 - 재구축","detail":"replica가 복구하기에 너무 뒤처졌거나, 필요한 로그가 삭제되었거나, 데이터가 불일치한다. 가장 안전한 방법은 새 백업에서 재구축하는 것이다.","steps":["primary에서 현재 base backup을 만든다: PG pg_basebackup, MySQL clone/xtrabackup, Oracle RMAN duplicate.","백업을 replica에 복원하고 복제 구성(slot/사용자/위치)을 다시 설정한다.","복제를 시작하고 초기 동기화(catch-up) 진행 상황을 모니터링한다.","지연이 0이 된 후 데이터 일관성과 읽기 트래픽을 확인하고 서비스에 투입한다."],"related":["replication-status","list-replicas","last-backup-status"],"playbook":"replication-lag"}}},"solver/ora-01555.json":{"id":"ora-01555","title":"ORA-01555: Snapshot too old (Oracle)","icon":"⏳","category":"Oracle","intro":"Oracle 'ORA-01555: snapshot too old' 오류는 쿼리가 일관된 읽기에 필요한 이전(undo) 데이터가 이미 덮어써졌음을 나타낸다. 이 트리는 오류의 원인(긴 쿼리 + UNDO 부족, 높은 undo 회전율, 또는 LOB)을 좁혀 올바른 해결책으로 안내한다.","start":"root","nodes":{"root":{"kind":"step","title":"오류는 어디서 발생하는가?","detail":"먼저 오류의 맥락을 파악한다. 전형적인 ORA-01555가 장기 실행 SELECT/리포트 중에 발생하는가, 아니면 LOB(CLOB/BLOB) 컬럼에 접근할 때 발생하는가? LOB 관련 ORA-01555는 다른 메커니즘(PCTVERSION/RETENTION)이며 따로 해결한다.","command":{"oracle":"SELECT sql_id, elapsed_time/1e6 AS sec, sql_text\nFROM v$sql ORDER BY elapsed_time DESC FETCH FIRST 10 ROWS ONLY;"},"choices":[{"label":"장기 실행 쿼리/리포트 중","next":"undo_size"},{"label":"LOB 컬럼에 접근할 때","next":"sol_lob"}]},"undo_size":{"kind":"step","title":"UNDO는 충분한가, retention은 적절한가?","detail":"긴 쿼리는 시작 이후 변경된 블록의 이전 이미지를 UNDO에서 읽는다. UNDO 테이블스페이스가 작거나 undo_retention이 쿼리 시간보다 짧으면 이전 undo가 덮어써진다. UNDO 사용량과 retention을 측정한다.","command":{"oracle":"SHOW PARAMETER undo_retention;\nSELECT tablespace_name, SUM(bytes)/1024/1024 AS mb, status\nFROM dba_undo_extents GROUP BY tablespace_name, status;\nSELECT MAX(maxquerylen) AS longest_query_sec, MAX(tuned_undoretention) AS tuned_retention\nFROM v$undostat;"},"choices":[{"label":"UNDO가 작음 / retention이 쿼리보다 짧음","next":"sol_undo"},{"label":"UNDO는 충분하지만 커밋이 매우 잦음","next":"sol_commits"},{"label":"쿼리가 지나치게 오래 실행됨","next":"sol_query"}]},"sol_undo":{"kind":"solution","title":"근본 원인: UNDO / retention 부족","detail":"retention 기간이 만료되었거나 테이블스페이스가 가득 차서 긴 쿼리에 필요한 이전 undo가 덮어써졌다. UNDO를 늘리고 쿼리 시간을 감당하도록 retention을 설정한다.","steps":["가장 긴 쿼리 시간(v$undostat.maxquerylen)을 기준으로 undo_retention을 그보다 높게 올린다: ALTER SYSTEM SET undo_retention=10800;","retention을 수용할 만큼 UNDO 테이블스페이스를 늘리거나 autoextend를 활성화한다: ALTER DATABASE DATAFILE '...' AUTOEXTEND ON MAXSIZE ...;","중요한 긴 쿼리에 대해 보장한다: ALTER TABLESPACE undotbs1 RETENTION GUARANTEE;  (주의: UNDO가 가득 차면 DML이 실패할 수 있음).","변경 후 같은 리포트를 다시 실행해 ORA-01555가 사라졌는지 확인한다."],"related":["show-config-param"],"playbook":"ora-01555"},"sol_commits":{"kind":"solution","title":"근본 원인: 높은 undo 회전율 (잦은 커밋)","detail":"매우 잦은 커밋을 동반한 대량 DML은 undo 블록을 빠르게 재활용하여 긴 쿼리에 필요한 이전 이미지를 지운다. 'commit cleanout' 효과가 이를 악화시킨다.","steps":["불필요하게 잦은 커밋을 줄인다: 루프 안에서 행마다 커밋하지 말고 배치 커밋을 사용한다.","undo 회전을 늦추기 위해 무거운 DML 워크로드를 긴 리포트와 같은 시간에 예약하지 않는다.","undo_retention을 늘리고 필요하면 RETENTION GUARANTEE로 이전 undo를 보호한다.","리포트를 쓰기가 많은 시간대 밖으로 옮기거나 일관된 읽기 시점에 고정한다."],"related":["open-transactions","show-config-param"],"playbook":"ora-01555"},"sol_query":{"kind":"solution","title":"근본 원인: 지나치게 긴 쿼리 실행 시간","detail":"쿼리가 너무 오래 실행되어 합리적인 undo_retention으로도 충분하지 않다. 영구 해결책은 쿼리를 빠르게 하거나 워크로드를 분산하는 것이다.","steps":["쿼리를 최적화한다: 누락된 인덱스, 나쁜 실행 계획, 불필요한 full scan을 고쳐 실행 시간을 줄인다.","큰 작업을 (날짜/범위별로) 나누어 각 조각이 undo window 안에 들어가게 한다.","무거운 리포트를 standby/replica나 별도의 읽기 시간대로 옮겨 활성 DML과 충돌하지 않게 한다.","실행 시간이 수용 가능해지면 그에 맞게 undo_retention을 설정한다."],"related":["explain-plan"],"playbook":"ora-01555"},"sol_lob":{"kind":"solution","title":"근본 원인: LOB retention / PCTVERSION","detail":"LOB을 읽는 중의 ORA-01555는 이전 LOB 버전을 보관하는 공간(RETENTION 또는 PCTVERSION)이 부족함을 나타낸다. LOB에는 별도의 버전 관리 메커니즘이 있다.","steps":["LOB 컬럼의 현재 설정 확인: SELECT retention, pctversion FROM dba_lobs WHERE table_name='...' AND column_name='...';","AUTO/MANUAL undo 모드에 따라 적절한 설정을 올린다: ALTER TABLE t MODIFY LOB (my_column) (RETENTION);  또는 MANUAL 모드에서는 PCTVERSION을 늘린다.","RETENTION을 사용하는 LOB은 데이터베이스의 undo_retention 값을 기준으로 하므로 이 값도 충분히 유지한다.","LOB에 접근하는 긴 작업을 단축하고, 변경 후 오류를 다시 테스트한다."],"related":["show-config-param"],"playbook":"ora-01555"}}},"solver/backup-restore.json":{"id":"backup-restore","title":"백업 및 복원 결정","icon":"💾","category":"백업 및 복구","intro":"백업으로 해야 할 일(백업 수행, 복원, 특정 시점 복구 - PITR, 백업 검증)을 단계적으로 안내하고 각 데이터베이스에 맞는 명령과 절차로 이끄는 결정 트리.","start":"root","nodes":{"root":{"kind":"step","title":"무엇을 하려는가?","detail":"목표를 선택한다. 새 백업을 수행하는가, 기존 백업을 복원하는가, 특정 시각(장애 직전)으로 돌아가는가, 아니면 보유한 백업이 정상인지 검증하는가?","choices":[{"label":"백업을 수행하려 한다","next":"take"},{"label":"백업을 복원하려 한다","next":"restore"},{"label":"특정 시점으로 복구 (PITR)","next":"pitr"},{"label":"백업을 검증하려 한다","next":"verify"}]},"take":{"kind":"step","title":"어떤 유형의 백업인가?","detail":"논리 백업(dump)은 이식성이 좋고 선택적 복원에 적합하지만 큰 데이터에서는 느리다. 물리 백업(파일/블록 수준)은 큰 데이터베이스와 빠른 복구에 적합하다. 요구 사항에 따라 선택한다.","choices":[{"label":"논리 백업 (dump / export)","next":"sol_logical_backup"},{"label":"물리 / 전체 백업","next":"sol_physical_backup"}]},"sol_logical_backup":{"kind":"solution","title":"논리 백업(dump) 수행","detail":"데이터를 SQL/BSON 문으로 내보낸다. 버전/플랫폼 간 이동과 단일 테이블/컬렉션 복원에 적합하다.","steps":["PostgreSQL: pg_dump -Fc -d mydb -f backup.dump  (클러스터 전체는 pg_dumpall).","Oracle: expdp app_user/password directory=DP_DIR dumpfile=backup.dmp schemas=APP_SCHEMA logfile=exp.log","MySQL/MariaDB: mysqldump --single-transaction --routines --triggers mydb > backup.sql","MongoDB: mongodump --db mydb --out /backup/dir","Redis: BGSAVE로 백그라운드에서 RDB 스냅샷을 만든다(dump.rdb).","Elasticsearch: 먼저 스냅샷 리포지토리를 등록한 뒤 PUT _snapshot/my_repo/snap1?wait_for_completion=true","백업을 안전한 위치로 옮기고 크기/완료 상태를 기록한다."],"related":["logical-dump","last-backup-status"],"playbook":"backup-restore"},"sol_physical_backup":{"kind":"solution","title":"물리 / 전체 백업 수행","detail":"데이터 파일의 블록/파일 수준 사본이다. 큰 데이터베이스에서 빠르며 PITR과 전체 복구의 기반이다.","steps":["Oracle: RMAN> BACKUP DATABASE PLUS ARCHIVELOG;  (아카이브 로그 포함).","PostgreSQL: pg_basebackup -D /backup/base -Fp -Xs -P  (WAL 아카이빙 활성화 상태).","SQL Server: BACKUP DATABASE [mydb] TO DISK='D:\\backup\\db.bak' WITH CHECKSUM, COMPRESSION;","MySQL/MariaDB: xtrabackup --backup --target-dir=/backup/base  (일관된 물리 사본).","MongoDB: 파일 시스템 스냅샷을 만들거나 replica set 멤버에서 일관된 스냅샷을 만든다.","백업을 오프라인/원격 위치로 복사하고 마지막 백업 상태를 기록한다."],"related":["last-backup-status"],"playbook":"backup-restore"},"restore":{"kind":"solution","title":"백업 복원","detail":"보유한 가장 적합한 백업을 대상에 복원한다. 먼저 백업의 무결성과 timestamp를 확인한 뒤 엔진에 맞는 명령으로 적재한다.","steps":["복원할 백업의 날짜와 정상 여부를 확인한다(데이터 손실 구간을 파악한다).","PostgreSQL: pg_restore -d new_db backup.dump   (SQL dump는 psql -d new_db -f backup.sql).","Oracle: RMAN> RESTORE DATABASE; RECOVER DATABASE; ALTER DATABASE OPEN;  (논리 import는 impdp).","MySQL/MariaDB: mysql mydb < backup.sql   (물리: xtrabackup --copy-back).","SQL Server: RESTORE DATABASE [mydb] FROM DISK='D:\\backup\\db.bak' WITH RECOVERY, REPLACE;","MongoDB: mongorestore --db mydb /backup/dir","복원 후 행 수/애플리케이션 점검으로 데이터를 검증한다."],"related":["restore-dump","last-backup-status"],"playbook":"backup-restore"},"pitr":{"kind":"solution","title":"특정 시점으로 복구 (PITR)","detail":"실수로 인한 삭제/손상 직전으로 초 단위 정밀도로 돌아가려 한다. 전체 백업 + 그 시점까지의 트랜잭션 로그(아카이브 로그 / WAL / binlog)가 필요하다.","steps":["목표 시각을 정확히 정하고(사고 직전) 필요한 로그가 있는지 확인한다.","Oracle: RMAN> RESTORE DATABASE; RECOVER DATABASE UNTIL TIME \"TO_DATE('2026-07-04 10:00:00','YYYY-MM-DD HH24:MI:SS')\"; ALTER DATABASE OPEN RESETLOGS;","PostgreSQL: base backup을 복원한 뒤 recovery_target_time='2026-07-04 10:00:00'을 설정하고 그 시점까지 WAL을 적용한다.","SQL Server: RESTORE ... WITH NORECOVERY; 이후 RESTORE LOG ... WITH STOPAT='2026-07-04 10:00:00', RECOVERY;","MySQL/MariaDB: 전체 백업을 복원한 뒤 mysqlbinlog --stop-datetime='2026-07-04 10:00:00'으로 그 시점까지 binlog를 적용한다.","오픈 후 데이터가 예상한 시점에 있는지 확인한다."],"related":["restore-dump","last-backup-status"],"playbook":"backup-restore"},"verify":{"kind":"solution","title":"백업 검증","detail":"백업은 복원 가능함이 증명되어야 비로소 백업이다. 무결성과 실제 복원 가능성을 정기적으로 테스트한다.","steps":["Oracle: RMAN> RESTORE DATABASE VALIDATE;  블록 일관성은 VALIDATE CHECK LOGICAL DATABASE;","SQL Server: 백업을 WITH CHECKSUM으로 수행하고 RESTORE VERIFYONLY ... WITH CHECKSUM을 실행한다(백업 파일만 점검). 실제 증명을 위해 백업을 테스트 서버에 복원하고 그곳에서 DBCC CHECKDB([mydb]) WITH NO_INFOMSGS;를 실행한다(CHECKDB는 백업 파일이 아니라 운영/복원된 데이터베이스를 점검한다).","PostgreSQL: 백업을 별도/임시 서버에 실제로 복원하고 pg_restore가 오류 없이 완료되는지 확인한다.","MySQL/MongoDB: dump를 테스트 환경에 복원하고 행/컬렉션 수와 샘플 데이터를 비교한다.","정기적인 '복원 훈련'을 예약한다 — 백업만으로는 충분하지 않으며 복원도 증명해야 한다.","결과와 마지막 검증 성공 날짜를 기록한다."],"related":["last-backup-status","restore-dump"],"playbook":"backup-restore"}}},"solver/upgrade-wizard.json":{"id":"upgrade-wizard","title":"버전 업그레이드 마법사","icon":"⬆️","category":"수명 주기","intro":"패치인가 메이저인가, 제자리인가 마이그레이션인가? 업그레이드를 단계적으로 계획한다. 각 분기는 사전 점검, 방법, 업그레이드 후 작업을 다룬다. 운영 환경을 건드리기 전에 여기서 리허설한다.","start":"kind","nodes":{"kind":{"kind":"step","title":"어떤 종류의 업그레이드를 하는가?","detail":"패치/마이너 버전(같은 메이저 버전 내, 예: 16.2 -> 16.4, 또는 SQL Server CU)은 위험이 낮고 보통 in-place로 수행한다. 메이저 버전(예: 15 -> 16)은 데이터 형식 변경이 포함될 수 있어 더 신중한 계획이 필요하다.","command":{"oracle":"SELECT * FROM v$version;\nSELECT patch_id, action, status FROM dba_registry_sqlpatch ORDER BY action_time DESC;","postgres":"SELECT version();","mysql":"SELECT VERSION();","mssql":"SELECT SERVERPROPERTY('ProductVersion') AS v, SERVERPROPERTY('ProductUpdateLevel') AS cu;","elasticsearch":"GET /"},"choices":[{"label":"마이너 버전 / 패치 (같은 메이저 버전, CU)","next":"patch_prep"},{"label":"메이저 버전 (메이저 버전 변경)","next":"major_method"},{"label":"차이를 모르겠다. 설명해 달라","next":"explain"}]},"explain":{"kind":"step","title":"패치인가 메이저인가?","detail":"패치: 메이저 버전이 같다(16.2 -> 16.4). 데이터 파일 형식이 바뀌지 않으며 보통 바이너리 교체 + 재시작이다. 메이저: 메이저 버전이 바뀐다(15 -> 16). 카탈로그/형식이 바뀔 수 있어 pg_upgrade / AutoUpgrade / dump-restore가 필요하며 애플리케이션 호환성을 테스트해야 한다.","choices":[{"label":"패치다 (같은 메이저 버전)","next":"patch_prep"},{"label":"메이저다 (메이저 버전이 바뀜)","next":"major_method"}]},"patch_prep":{"kind":"step","title":"백업과 롤백 계획이 준비되었는가?","detail":"패치는 위험이 낮지만 모든 업그레이드의 철칙은 먼저 전체 백업(가능하면 스냅샷도)이다. 문제가 생기면 돌아갈 지점이 필요하다.","choices":[{"label":"예, 백업 + 롤백 준비 완료","next":"sol_patch"},{"label":"아니요, 아직 아님","next":"sol_backupfirst"}]},"sol_backupfirst":{"kind":"solution","title":"먼저 백업 + 롤백 계획","detail":"업그레이드를 시작하기 전에 안전망을 마련한다.","steps":["전체 백업을 수행한다(가능하면 스토리지 스냅샷 / Oracle guaranteed restore point도).","롤백 단계를 적어 둔다: 문제가 생기면 어떻게 이전 버전으로 돌아가는가?","유지보수 시간대와 커뮤니케이션 계획을 확정한 뒤 패치 단계로 넘어간다."],"related":["logical-dump","last-backup-status"]},"sol_patch":{"kind":"solution","title":"패치 적용","detail":"같은 메이저 버전 내 패치다. 클러스터에서 롤링 방식(replica부터)으로 적용하면 다운타임이 줄어든다.","steps":["Oracle: opatch로 새 RU를 설치한 뒤 'datapatch -verbose'로 SQL 변경을 적용한다.","PostgreSQL: 마이너 업그레이드 = 새 바이너리를 설치하고 서비스를 재시작한다(initdb/dump 불필요).","MySQL: 패키지를 업데이트한다. 8.0에서는 서버가 시작 시 업그레이드를 자동으로 수행한다.","SQL Server: 해당 CU/GDR 설치 프로그램을 실행한 뒤 재시작한다.","MongoDB/Redis/Elasticsearch: 롤링 재시작. secondary 노드 먼저, primary는 마지막.","이후 버전과 애플리케이션을 확인한다. 오류가 보이면 롤백 계획을 실행한다."],"related":["apply-patch","check-version"],"playbook":"upgrade-rollback"},"major_method":{"kind":"step","title":"메이저 업그레이드 방법","detail":"두 가지 주요 경로: (1) 제자리: 기존 서버에서 변환한다(pg_upgrade, Oracle AutoUpgrade/DBUA, MySQL in-place). 빠르지만 롤백이 어렵다. (2) 마이그레이션: 새 버전으로 새 서버를 구축하고, 데이터를 옮긴 뒤(dump/restore 또는 복제), 전환한다. 더 안전하며 다운타임이 최소화된다.","choices":[{"label":"먼저 준비/호환성 점검을 실행하겠다","next":"precheck"},{"label":"제자리","next":"sol_inplace"},{"label":"새 서버로 마이그레이션 (최소 다운타임)","next":"sol_migrate"}]},"precheck":{"kind":"step","title":"호환성 / 준비 상태 점검","detail":"업그레이드 전에 대상 버전의 경고를 수집한다: 제거된(deprecated) 기능, 유효하지 않은 객체, 호환성 수준.","command":{"oracle":"-- AutoUpgrade analyze mode: java -jar autoupgrade.jar -config cfg -mode analyze","postgres":"-- pg_upgrade --check   (with the old and new binary paths)","mysql":"-- MySQL Shell: util.checkForServerUpgrade('root@localhost:3306', {targetVersion: '8.4.0'})","mssql":"-- Compatibility report with Data Migration Assistant (DMA)","elasticsearch":"GET _migration/deprecations"},"choices":[{"label":"출력 깨끗함, 계속","next":"major_method"},{"label":"경고/비호환이 있다","next":"sol_fixwarn"}]},"sol_fixwarn":{"kind":"solution","title":"먼저 경고를 해결한다","detail":"업그레이드 전에 리포트의 모든 경고를 해결한다. 그렇지 않으면 업그레이드가 중간에 실패하거나 애플리케이션이 깨질 수 있다.","steps":["제거된(deprecated) 기능/구문을 사용하는 객체와 쿼리를 수정한다.","유효하지 않은 객체를 컴파일/수정하고 누락된 종속성을 해결한다.","대상 버전에 맞는 호환성 수준 / feature version 단계를 계획한다.","경고가 해소되면 사전 점검을 다시 실행하고 방법 선택으로 돌아간다."],"related":["check-upgrade-readiness","set-compatibility"]},"sol_inplace":{"kind":"solution","title":"제자리 메이저 업그레이드","detail":"기존 서버에서 변환한다. 빠르지만 롤백은 보통 백업에서 복원하는 것을 의미하므로 스냅샷이 필수다.","steps":["전체 백업 + 스냅샷/guaranteed restore point를 만든다(유일한 복귀 수단).","Oracle: AutoUpgrade(deploy 모드) 또는 DBUA. PostgreSQL: pg_upgrade(--link는 빠르지만 되돌릴 수 없음). MySQL: 새 바이너리 + 자동 in-place 업그레이드. SQL Server: setup /ACTION=upgrade.","업그레이드 후 작업을 실행한다(아래 관련 항목 참고).","실제 부하에서 애플리케이션을 검증한다. 실행 계획 회귀가 발생하면 옵티마이저 동작을 고정한다."],"related":["upgrade-major","post-upgrade-tasks"],"playbook":"upgrade-rollback"},"sol_migrate":{"kind":"solution","title":"마이그레이션으로 업그레이드 (최소 다운타임)","detail":"새 버전으로 새 서버를 구축하고 데이터를 옮긴 뒤 짧은 cutover로 전환한다. 가장 안전한 방법이며 기존 시스템은 그대로 남는다.","steps":["대상 버전으로 새 서버를 구축하고 스키마를 준비한다.","dump/restore 또는 복제로 초기 적재를 전송하고, 이후 지속적 복제(CDC / logical replication)로 동기화를 유지한다.","짧은 유지보수 시간대에: 쓰기를 중지하고, 남은 delta를 기다린 뒤, 애플리케이션이 새 서버를 가리키게 한다(cutover).","마이그레이션을 검증한다(행 수/checksum). 문제가 있으면 즉시 기존 서버로 되돌린다."],"related":["setup-cdc-replication","verify-migration","export-for-migration"]}}},"solver/migration-wizard.json":{"id":"migration-wizard","title":"마이그레이션 마법사 (데이터/스키마 마이그레이션)","icon":"🚚","category":"수명 주기","intro":"데이터/스키마를 마이그레이션한다. 동종인가 이기종인가, 다운타임 허용 범위는 어떤가? 평가 → 마이그레이션 → 검증 단계로 계획한다. 먼저 여기서 cutover를 리허설한다.","start":"kind","nodes":{"kind":{"kind":"step","title":"무엇에서 무엇으로 마이그레이션하는가?","detail":"동종: 같은 데이터베이스 엔진(예: Oracle 19 → Oracle 23, 또는 PostgreSQL에서 다른 서버로). 이기종: 다른 엔진(예: Oracle → PostgreSQL, MySQL → SQL Server). 이기종 마이그레이션은 훨씬 많은 호환성 작업(데이터 타입, 함수, PL/SQL)이 필요하다.","choices":[{"label":"같은 엔진, 새 서버/버전 (동종)","next":"downtime"},{"label":"다른 엔진으로 (이기종)","next":"hetero"},{"label":"계획 단계다. 어떻게 접근해야 하는가?","next":"downtime"}]},"downtime":{"kind":"step","title":"다운타임 허용 범위는 어떤가?","detail":"이에 따라 마이그레이션 방법이 결정된다. 유지보수 시간대가 있다면 가장 간단한 경로는 dump/restore다. 거의 무중단이 필요하다면 초기 데이터를 적재하고, 지속적 복제(CDC)로 동기화를 유지하며, 짧은 cutover를 수행한다.","choices":[{"label":"유지보수 시간대가 있다 (다운타임 허용)","next":"sol_offline"},{"label":"거의 무중단이 필요하다","next":"sol_online"}]},"hetero":{"kind":"step","title":"스키마와 타입 호환성을 평가했는가?","detail":"이기종 마이그레이션에서 실제 작업은 데이터 복사가 아니라 차이 해소다: 데이터 타입(VARCHAR2↔TEXT), 함수(NVL↔COALESCE), auto-increment 값, PL/SQL ↔ PL/pgSQL, 제거되거나 다른 기능.","choices":[{"label":"스키마/타입 차이를 매핑했다","next":"sol_hetero"},{"label":"아직 평가하지 않았다","next":"sol_assess"}]},"sol_assess":{"kind":"solution","title":"먼저 호환성 평가","detail":"마이그레이션 전에 원본과 대상의 차이를 매핑한다. 이 단계를 건너뛰면 마이그레이션이 중간에 멈춘다.","steps":["스키마 diff를 만든다: 테이블, 제약 조건, 인덱스, 뷰, 프로시저.","데이터 타입과 함수를 매핑한다(DBAtlas > 타입 & 함수 모듈이 바로 이를 위해 만들어졌다).","엔진 고유 기능(identity/sequence, 파티셔닝, PL/SQL, 트리거)을 대상에서 어떻게 처리할지 결정한다.","변환 도구(ora2pg, pgloader, AWS SCT/DMS)를 선택하고 작은 샘플로 리허설한다."],"related":["compare-schema","heterogeneous-migration"]},"sol_offline":{"kind":"solution","title":"오프라인 마이그레이션 (dump / restore)","detail":"가장 간단하고 신뢰할 수 있는 동종 방법: 일관된 백업을 만들고, 대상에 적재하고, 검증하고, 전환한다.","steps":["원본에서 일관된 export를 수행한다(pg_dump -Fc, expdp FLASHBACK_SCN, mysqldump --single-transaction, BACKUP, mongodump --oplog).","대상에 적재한다(pg_restore -j, impdp, mysql <, RESTORE, mongorestore).","마이그레이션을 검증한다: 원본과 대상의 행 수와 핵심 checksum이 일치하는가?","유지보수 시간대에 애플리케이션이 대상을 가리키게 한다(cutover). 문제가 생기면 원본으로 되돌린다."],"related":["export-for-migration","import-for-migration","verify-migration"]},"sol_online":{"kind":"solution","title":"최소 다운타임 (CDC / 복제)","detail":"초기 데이터를 적재한 뒤 지속적 복제로 대상을 원본과 동기화한다. 다운타임은 짧은 cutover 동안에만 발생한다.","steps":["dump/restore 또는 스냅샷으로 초기(baseline) 데이터를 적재한다.","지속적 복제를 설정한다: PG logical replication(PUBLICATION/SUBSCRIPTION), MySQL binlog 복제, Oracle GoldenGate, MSSQL transactional replication/CDC, Mongo change streams/mongosync.","대상이 원본을 따라잡아 지연이 ~0이 될 때까지 기다리며 모니터링한다.","짧은 cutover: 원본 쓰기를 중지하고, 남은 delta를 기다린 뒤, 애플리케이션이 대상을 가리키게 하고 검증한다."],"related":["setup-cdc-replication","verify-migration"]},"sol_hetero":{"kind":"solution","title":"이기종 마이그레이션 (다른 엔진으로)","detail":"스키마를 변환하고, 데이터를 옮기고, 로직을 조정하고, 검증한다. 단계적으로 진행하며 모든 것을 한 번에 마이그레이션하지 않는다.","steps":["스키마 변환: 도구(ora2pg, pgloader, AWS SCT)나 수작업으로 한다. 타입과 제약 조건을 대상에 맞게 조정한다.","데이터 이동: bulk load 또는 도구를 사용한다. 큰 테이블은 파티션/chunk로 나눈다.","서버 측 로직 조정: PL/SQL → PL/pgSQL, 함수/트리거 대응물.","철저히 검증한다: 행 수, 샘플 데이터, 핵심 쿼리 결과. 대상에서 애플리케이션을 테스트한다.","다운타임을 줄여야 한다면 변환 후 CDC 계층으로 대상을 동기화한 뒤 cutover한다."],"related":["heterogeneous-migration","verify-migration","setup-cdc-replication"]}}},"solver/high-cpu.json":{"id":"high-cpu","title":"높은 CPU","icon":"🔥","category":"성능","intro":"서버 CPU가 포화 상태다. 먼저 실제로 데이터베이스가 소비하는지 확인한 뒤 분기를 따라 근본 원인으로 간다: 실행 계획이 나쁜 쿼리, 실행 계획 회귀, 과도한 동시성, 또는 DB가 아닌 프로세스.","start":"root","nodes":{"root":{"kind":"step","title":"실제로 데이터베이스가 CPU를 소비하는가?","detail":"운영체제 측에서 어떤 프로세스가 CPU를 소비하는지 확인한다. 부하 대부분이 DB 프로세스에 있는가, 아니면 이웃 프로세스(백업, 백신, 다른 애플리케이션)에 있는가? 이에 따라 조사가 둘로 나뉜다.","command":{"oracle":"-- OS side: top -o %CPU  (or: top -c)\n-- DB-side verification:\nSELECT ROUND(value,1) AS host_cpu_util_pct FROM v$sysmetric WHERE metric_name='Host CPU Utilization (%)' AND rownum=1;","postgres":"-- OS: top -c  /  ps -eo pcpu,pid,cmd --sort=-pcpu | head\n-- DB active session count:\nSELECT count(*) FROM pg_stat_activity WHERE state='active';","mysql":"-- OS: top -c  /  mysqladmin processlist\nSHOW GLOBAL STATUS LIKE 'Threads_running';","mssql":"-- OS: Task Manager / typeperf\nSELECT record_id, SQLProcessUtilization AS sql_cpu_pct, (100 - SystemIdle - SQLProcessUtilization) AS other_cpu_pct FROM (SELECT record.value('(./Record/@id)[1]','int') AS record_id, record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]','int') AS SystemIdle, record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]','int') AS SQLProcessUtilization FROM (SELECT CONVERT(xml, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type='RING_BUFFER_SCHEDULER_MONITOR') AS x) AS y ORDER BY record_id DESC;","mongodb":"// OS: top -c  |  db.serverStatus().metrics\ndb.serverStatus().opcounters","redis":"INFO cpu","elasticsearch":"GET _nodes/stats/os,process"},"choices":[{"label":"예, DB 프로세스가 CPU를 소비한다","next":"db_confirm"},{"label":"아니요, 이웃 프로세스 / 백업 작업이 소비한다","next":"sol_os"}]},"db_confirm":{"kind":"step","title":"부하가 어떻게 나타났는가?","detail":"DB가 CPU를 소비한다면 패턴을 파악한다: 무거운 쿼리 몇 개가 지배적인가, 변경 후 문제가 갑자기 시작되었는가, 아니면 높은 동시성/연결 경합이 있는가?","choices":[{"label":"무거운 쿼리 몇 개가 CPU를 소비한다","next":"topcpu"},{"label":"갑자기 시작되었다. 최근 배포 / 파라미터 변경이 있었다","next":"sol_regression"},{"label":"연결 과다 / 동시성 경합","next":"sol_concurrency"}]},"topcpu":{"kind":"step","title":"CPU를 가장 많이 소비하는 쿼리 찾기","detail":"총 CPU 소비 기준 상위 쿼리를 가져온다. 그다음 각 쿼리가 호출당 무거운지(나쁜 실행 계획), 호출당 가볍지만 매우 자주 호출되는지 판단한다.","command":{"oracle":"SELECT sql_id, executions, ROUND(cpu_time/1e6,1) AS cpu_sec, ROUND(cpu_time/GREATEST(executions,1)/1000,2) AS cpu_ms_per_exec, sql_text\nFROM v$sql ORDER BY cpu_time DESC FETCH FIRST 10 ROWS ONLY;","postgres":"SELECT queryid, calls, ROUND(total_exec_time::numeric,1) AS total_ms, ROUND(mean_exec_time::numeric,2) AS mean_ms, query\nFROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;","mysql":"SELECT DIGEST_TEXT, COUNT_STAR AS calls, ROUND(SUM_CPU_TIME/1e12,2) AS cpu_sec, ROUND(SUM_TIMER_WAIT/1e12,2) AS total_sec\nFROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;","mssql":"SELECT TOP 10 qs.execution_count, qs.total_worker_time/1000 AS cpu_ms, qs.total_worker_time/qs.execution_count/1000 AS cpu_ms_per_exec, SUBSTRING(st.text,(qs.statement_start_offset/2)+1,50) AS stmt\nFROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st ORDER BY qs.total_worker_time DESC;","mongodb":"db.setProfilingLevel(1, { slowms: 100 });  // then: db.system.profile.find().sort({ millis: -1 }).limit(10)"},"choices":[{"label":"호출당 무겁다. 나쁜 실행 계획 / full scan","next":"sol_query"},{"label":"호출당 가볍지만 매우 자주 호출된다","next":"sol_frequency"}]},"sol_query":{"kind":"solution","title":"근본 원인: 실행 계획이 나쁜 쿼리가 CPU를 소비함","detail":"몇몇 쿼리가 full scan이나 나쁜 조인 순서 때문에 호출당 과도한 CPU를 소비한다. 이들의 실행 계획을 고치는 것이 핵심 해결책이다.","steps":["CPU 상위 쿼리의 실행 계획(EXPLAIN)을 확인한다. full scan, 나쁜 조인 순서, 추정 행 수와 실제 행 수의 차이를 찾는다.","필터/조인 컬럼에 적합한 인덱스를 추가하고 실행 계획이 index scan(IXSCAN)으로 바뀌는지 확인한다.","옵티마이저가 잘못 추정하면 새 통계를 수집한다.","폭주 쿼리를 제한하고(Oracle Resource Manager / MSSQL Resource Governor) 필요하면 쿼리를 재작성한다."],"related":["top-slow-queries","explain-plan","create-index","update-statistics"],"playbook":"high-cpu"},"sol_frequency":{"kind":"solution","title":"근본 원인: 과도한 호출 빈도","detail":"쿼리 하나하나는 가볍지만 초당 수천 번 호출되어 총 CPU 비용이 여기서 나온다. 보통 N+1 패턴이나 캐시되지 않은 hot path다.","steps":["가장 자주 호출되는 문장(calls / execution_count)을 식별하고 호출하는 애플리케이션 경로를 찾는다.","N+1 패턴을 배치 쿼리로 바꾸고, 애플리케이션/결과 캐시로 반복 읽기를 줄인다.","prepared statement를 사용해 반복 파싱(hard parse) 비용을 줄인다.","자주 실행되고 결과가 바뀌지 않는 무거운 쿼리에는 materialized view / 요약 테이블을 고려한다."],"related":["top-slow-queries","explain-plan"],"playbook":"high-cpu"},"sol_regression":{"kind":"solution","title":"근본 원인: 실행 계획 회귀 / 오래된 통계","detail":"어제는 시스템이 정상이었다. 배포, 파라미터 변경, 데이터 적재 후 CPU가 급증했다. 옵티마이저가 더 나쁜 실행 계획으로 바뀌었을 수 있다.","steps":["변경 사항을 파악한다: 최근 배포, 업그레이드/패치, 파라미터 변경, 대량 데이터 적재.","영향받은 테이블의 새 통계를 수집한다(오래된 통계가 가장 흔한 원인).","좋은 실행 계획을 고정한다: Oracle SQL Plan Baseline / 힌트, MSSQL Query Store 'force plan'.","변경이 파라미터에서 왔다면 되돌리고 통제된 방식으로 다시 평가한다."],"related":["update-statistics","explain-plan","show-config-param"],"playbook":"high-cpu"},"sol_concurrency":{"kind":"solution","title":"근본 원인: 높은 동시성 / 연결 경합","detail":"개별 쿼리는 수용 가능하지만 너무 많은 세션이 동시에 실행된다. CPU 코어보다 많은 활성 세션은 context switching과 latch/spin 경합을 일으킨다.","steps":["활성 세션 수를 CPU 코어 수와 비교한다. 활성 세션이 코어보다 많으면 run queue가 쌓인다.","애플리케이션과 DB 사이에 connection pooler(PgBouncer / ProxySQL)를 두고 적절한 풀 크기를 설정한다.","병렬 처리가 과도하면 병렬도를 제한한다(Oracle parallel_degree_limit / parallel_max_servers, MSSQL MAXDOP).","읽기 부하를 read replica로 분산하여 primary 노드의 CPU 부담을 덜어 준다."],"related":["connection-count","active-sessions"],"playbook":"connection-storm"},"sol_os":{"kind":"solution","title":"근본 원인: DB가 아닌 프로세스가 CPU를 소비함","detail":"데이터베이스가 아니라 이웃 프로세스가 CPU를 소비한다: 같은 머신의 백업/압축 작업, 백신 스캔, log shipper, 또는 다른 애플리케이션.","steps":["OS 도구(top / Task Manager)로 원인 프로세스를 정확히 찾는다.","백업, 통계 수집, 유지보수 작업이라면 피크 시간 밖으로 일정을 옮긴다.","데이터 디렉터리를 백신/스캔 프로세스의 제외 목록에 추가한다.","충돌이 지속적이라면 DB를 자체 리소스를 가진 별도 서버로 옮긴다."],"related":["os-resource"],"playbook":"high-cpu"}}},"solver/memory-pressure.json":{"id":"memory-pressure","title":"메모리 압박 / OOM","icon":"🧠","category":"성능","intro":"서버 메모리가 바닥나고 있거나, 시스템이 스와핑 중이거나, OOM killer가 동작 중이다. 먼저 누가 메모리를 소비하는지 판단한다: 할당 메모리(buffer pool/SGA), 세션 메모리(work_mem/PGA), OS 전반, 또는 Redis 고유 상황인가?","start":"root","nodes":{"root":{"kind":"step","title":"누가 메모리를 소비하는가?","detail":"메모리 압박의 원인을 계층별로 구분한다. 할당된(고정) 메모리가 너무 큰가, 세션별 메모리가 폭증하는가, DB가 아닌 프로세스가 차지하는가, 아니면 Redis 자체의 메모리 관리 문제인가?","command":{"oracle":"SELECT component, ROUND(current_size/1024/1024) AS mb FROM v$memory_dynamic_components WHERE current_size>0 ORDER BY current_size DESC;","postgres":"SHOW shared_buffers; SHOW work_mem; SHOW effective_cache_size;\nSELECT count(*) FROM pg_stat_activity WHERE state='active';","mysql":"SHOW VARIABLES LIKE 'innodb_buffer_pool_size';\nSHOW VARIABLES LIKE 'sort_buffer_size'; SHOW VARIABLES LIKE 'join_buffer_size';\nSHOW STATUS LIKE 'Threads_connected';","mssql":"SELECT physical_memory_in_use_kb/1024 AS used_mb, large_page_allocations_kb/1024 AS large_mb FROM sys.dm_os_process_memory;\nSELECT cntr_value/1024 AS target_mb FROM sys.dm_os_performance_counters WHERE counter_name='Target Server Memory (KB)';","mongodb":"db.serverStatus().wiredTiger.cache","redis":"INFO memory","elasticsearch":"GET _nodes/stats/jvm,os"},"choices":[{"label":"할당 메모리(buffer pool / SGA)가 지배적이다","next":"allocated"},{"label":"세션 메모리(work_mem / PGA)가 폭증한다","next":"sol_session"},{"label":"DB가 아닌 프로세스 / OS 전반","next":"sol_os"},{"label":"Redis 고유 (maxmemory / eviction / 단편화)","next":"sol_redis"}]},"allocated":{"kind":"step","title":"설정된 총 메모리가 물리 RAM을 초과하는가?","detail":"고정 할당 메모리가 크다면 두 가지 가능성이 있다: 설정된 총합(buffer pool + 세션 + OS)이 물리 RAM을 초과하거나(over-commit), 크기는 합리적이지만 캐시 적중률이 여전히 낮은 경우다. 설정값을 합산해 RAM과 비교한다.","command":{"oracle":"SHOW PARAMETER sga_target;  SHOW PARAMETER pga_aggregate_target;\nSELECT ROUND(value/1024/1024/1024,1) AS host_ram_gb FROM v$osstat WHERE stat_name='PHYSICAL_MEMORY_BYTES';","postgres":"SELECT name, setting, unit FROM pg_settings WHERE name IN ('shared_buffers','work_mem','maintenance_work_mem','max_connections');","mysql":"SELECT ROUND(@@innodb_buffer_pool_size/1024/1024/1024,1) AS pool_gb, ROUND((@@sort_buffer_size+@@join_buffer_size)/1024/1024,1) AS per_conn_mb, @@max_connections AS max_conn;","mssql":"SELECT value_in_use AS max_server_mem_mb FROM sys.configurations WHERE name='max server memory (MB)';\nSELECT total_physical_memory_kb/1024 AS ram_mb FROM sys.dm_os_sys_memory;"},"choices":[{"label":"예, 설정 총합이 RAM을 초과한다 (over-commit)","next":"sol_overcommit"},{"label":"아니요, 크기는 합리적이지만 캐시 적중률이 낮다","next":"sol_cache"}]},"sol_overcommit":{"kind":"solution","title":"근본 원인: 메모리 over-commit","detail":"buffer pool/SGA + 세션 메모리 + OS 요구량의 합이 물리 RAM을 초과한다. 시스템이 스와핑 중이거나 OOM killer가 프로세스를 종료하고 있다.","steps":["메모리 예산을 다시 짠다: OS 몫을 남겨 둔 뒤 할당 메모리 크기를 올바르게 정한다(도구 > 메모리 계산기).","buffer pool/SGA 목표와 세션 메모리 상한을 낮춰 RAM 한도 안에 맞춘다.","변경을 통제된 방식으로 적용하고 스왑 사용과 OOM 이벤트가 멈췄는지 확인한다.","용량이 지속적으로 부족하면 RAM을 추가하거나 부하를 분산한다."],"related":["show-config-param","cache-hit-ratio"],"playbook":"memory-pressure"},"sol_cache":{"kind":"solution","title":"근본 원인: 캐시 부족 (낮은 캐시 적중률)","detail":"할당 메모리가 RAM을 초과하지는 않지만 working set에 비해 작다. buffer cache 적중률이 낮고 DB가 계속 디스크에서 읽는다.","steps":["buffer cache / buffer pool 적중률을 측정한다. 지속적으로 낮다면(예: 90% 미만) 캐시가 너무 작은 것이다.","RAM 여유가 있으면 buffer pool/SGA를 통제된 방식으로 늘리고 적중률을 다시 측정한다.","hot 데이터가 실제로 크다면 불필요한 읽기를 줄인다: 인덱스 추가, SELECT * 대신 필요한 컬럼만 선택, 페이지네이션.","늘리는 것이 RAM에 부담이 되면 세션 메모리와 할당 메모리의 균형을 다시 맞춘다."],"related":["cache-hit-ratio","show-config-param"],"playbook":"memory-pressure"},"sol_session":{"kind":"solution","title":"근본 원인: 세션 메모리 폭증","detail":"work_mem/PGA 같은 세션별 메모리가 높은 동시성과 곱해져 RAM을 고갈시킨다. 각 sort/hash 작업은 이 메모리를 따로 할당할 수 있다.","steps":["최악의 경우를 추정한다: 세션 메모리 × 동시 세션 수 × 작업 수. 이를 RAM과 비교한다.","전역 세션 메모리 설정(work_mem / sort_buffer_size / PGA target)을 낮춘다.","정말로 큰 sort/hash가 필요한 무거운 쿼리에 대해서만 세션 수준에서 설정을 올린다.","커넥션 풀러로 동시성을 제한해 전체 메모리 요구량을 줄인다."],"related":["show-config-param","connection-count"],"playbook":"memory-pressure"},"sol_redis":{"kind":"solution","title":"근본 원인: Redis 메모리 관리","detail":"Redis에서 used_memory가 maxmemory에 도달했거나, eviction이 동작 중이거나, 단편화가 높다(used_memory_rss >> used_memory). 정책과 단편화를 함께 해결해야 한다.","steps":["INFO memory로 used_memory, maxmemory, mem_fragmentation_ratio를 확인한다.","용도에 맞는 eviction 정책을 설정한다: CONFIG SET maxmemory-policy allkeys-lru(캐시) 또는 noeviction(영구 데이터).","TTL을 사용해 만료된 키가 정리되도록 하고, 큰 키(BIG KEYS)를 줄인다.","단편화가 높으면 active defrag를 활성화(CONFIG SET activedefrag yes)하거나 적절한 유지보수 시간대에 재시작한다."],"related":["show-config-param"],"playbook":"memory-pressure"},"sol_os":{"kind":"solution","title":"근본 원인: DB 외 메모리 소비","detail":"메모리를 소비하는 것은 데이터베이스가 아니라 OS 전체 또는 인접 프로세스다: 다른 애플리케이션, 파일 시스템 캐시 압박, 메모리 누수가 있는 보조 프로세스 등.","steps":["OS 도구(top / free / Task Manager)로 메모리를 가장 많이 소비하는 프로세스를 찾는다.","원인이 DB 외 프로세스라면 해당 프로세스를 옮기거나, 제한(cgroup / job object)하거나, 메모리 누수를 수정한다.","OOM killer가 DB 프로세스를 종료하고 있다면 OOM 설정을 검토하고 DB용 메모리를 보장한다.","충돌이 지속되면 DB를 전용 메모리를 가진 별도 서버로 옮긴다."],"related":["os-resource"],"playbook":"memory-pressure"}}},"solver/install-setup.json":{"id":"install-setup","title":"신규 설치 및 보안 기준선","icon":"🛠️","category":"수명 주기","intro":"새로 설치한 데이터베이스를 운영 환경에 맞게 준비하면서 건너뛴 단계는 가장 비용이 큰 장애로 이어진다. 현재 단계를 선택하면 초기 구성, 보안 강화, 내구성, 모니터링에 대한 순서별 체크리스트를 제공한다.","start":"root","nodes":{"root":{"kind":"step","title":"현재 어느 단계인가?","detail":"신규 설치를 운영 환경에 맞게 준비하는 과정은 초기 구성, 보안 강화, 내구성(백업/복제), 모니터링의 네 단계로 이루어진다. 지금 어느 단계를 진행 중인가? 모든 단계를 순서대로 진행할 것을 권장한다.","command":{"oracle":"SELECT banner_full FROM v$version; SHOW PARAMETER compatible;","postgres":"SELECT version(); SHOW server_encoding; SHOW data_directory;","mysql":"SELECT VERSION(); SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'datadir';","mssql":"SELECT @@VERSION; SELECT SERVERPROPERTY('Collation') AS collation;","mongodb":"db.version(); db.serverStatus().host","redis":"INFO server","elasticsearch":"GET /"},"choices":[{"label":"설치 후 초기 구성","next":"firstconfig"},{"label":"보안 강화","next":"sol_security"},{"label":"백업 및 내구성 설정","next":"durability"},{"label":"모니터링 및 알림 설정","next":"sol_monitoring"}]},"firstconfig":{"kind":"step","title":"초기 구성에서 우선순위는 무엇인가?","detail":"초기 구성은 두 가지 주요 주제로 나뉜다: 리소스/파라미터 사이징(메모리, 연결)과 영구적인 기반(문자 집합/콜레이션, 데이터 디렉터리/파일 배치). 이는 나중에 변경하기 가장 어려운 결정이다.","command":{"postgres":"SELECT name, setting, unit FROM pg_settings WHERE name IN ('shared_buffers','work_mem','max_connections','wal_level');","oracle":"SHOW PARAMETER sga_target; SHOW PARAMETER pga_aggregate_target; SHOW PARAMETER processes;","mysql":"SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW VARIABLES LIKE 'max_connections';","mssql":"SELECT name, value_in_use FROM sys.configurations WHERE name IN ('max server memory (MB)','max degree of parallelism');"},"choices":[{"label":"메모리 / 파라미터 사이징","next":"sol_sizing"},{"label":"문자 집합, 콜레이션 및 파일 배치","next":"sol_layout"}]},"durability":{"kind":"step","title":"내구성의 어느 부분을 설정하는가?","detail":"내구성은 두 가지 축으로 이루어진다: 검증된 백업 전략(첫 백업 + 복구 테스트)과 연속성(아카이브/로그 모드, 복제). 테스트하지 않고 '백업이 있다'고 말하는 것이 가장 흔한 오해다.","command":{"postgres":"SHOW archive_mode; SHOW wal_level;","oracle":"ARCHIVE LOG LIST; SELECT log_mode FROM v$database;","mysql":"SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'server_id';","mssql":"SELECT name, recovery_model_desc FROM sys.databases;"},"choices":[{"label":"백업 전략 + 첫 백업 + 복구 테스트","next":"sol_backup"},{"label":"아카이브/로그 모드 + 복제","next":"sol_replication"}]},"sol_sizing":{"kind":"solution","title":"초기 구성: 메모리 및 파라미터 사이징","detail":"대부분의 엔진은 기본값이 보수적이다. 서버의 RAM과 CPU에 맞게 사이징하되 오버커밋은 피한다.","steps":["전체 RAM을 배분한다: OS 몫을 남기고 buffer pool/SGA를 주 캐시로 설정한다.","세션 메모리(work_mem/PGA)는 예상 동시성을 기준으로 보수적으로 설정하고 전체 RAM을 초과하지 않게 한다.","max_connections는 실제 필요량에 맞추고, 높은 동시성에는 커넥션 풀러를 계획한다.","엔진별 핵심 파라미터(WAL/redo, checkpoint, 병렬 처리)를 검토하고 변경 사항을 문서화한다."],"related":["show-config-param","connect-cli"]},"sol_layout":{"kind":"solution","title":"초기 구성: 문자 집합, 콜레이션 및 파일 배치","detail":"문자 집합/콜레이션과 데이터 디렉터리 배치는 설치 시점에 결정된다. 나중에 변경하려면 대개 재설치나 마이그레이션이 필요하다.","steps":["서버 수준에서 Unicode 문자 집합(예: UTF-8 / AL32UTF8)을 선택하고 일관된 콜레이션을 설정한다.","데이터, 로그/WAL, 백업 디렉터리를 별도 디스크/볼륨에 배치해 IO 경합과 단일 장애 지점을 줄인다.","파일 시스템과 권한을 강화한다: 데이터 디렉터리는 DB 사용자만 읽을 수 있어야 한다.","나중에 추가되는 노드가 동일하게 맞출 수 있도록 배치 및 문자 집합 결정을 문서화한다."],"related":["show-config-param","connect-cli"]},"sol_security":{"kind":"solution","title":"보안 강화","detail":"신규 설치에는 기본 계정, 느슨한 네트워크 접근, 열린 포트가 존재한다. 운영 환경 투입 전에 공격 표면을 줄이는 것은 필수다.","steps":["기본/샘플 계정을 제거하거나 잠그고, 사용하지 않는 관리 계정을 비활성화한다.","강력한 비밀번호 정책을 적용하고 모든 기본 비밀번호를 변경한다.","최소 권한: 각 애플리케이션에 필요한 권한만 부여하고 광범위한 롤은 피한다.","TLS를 활성화하고, 리스너/포트를 신뢰할 수 있는 네트워크로 제한하며, 감사 로깅을 활성화한다."],"related":["create-user-grant","change-password","enable-audit"]},"sol_backup":{"kind":"solution","title":"내구성: 백업 전략 및 복구 테스트","detail":"백업의 가치는 복구할 수 있는 능력에 달려 있다. 첫 백업을 수행하고, 연속성을 구성하며, 반드시 복구 테스트를 수행한다.","steps":["복구 목표(RPO/RTO)를 정의하고 전체 + 증분 백업 일정을 구성한다.","첫 전체 백업을 수행하고, 백업은 DB 서버와 분리된 위치에 보관한다.","백업을 별도 환경에 복구해 테스트한다. 무결성이 입증되지 않은 백업은 백업으로 간주하지 않는다.","백업 작업을 스케줄링하고, 출력을 모니터링에 연동하며, 마지막 백업 상태를 정기적으로 확인한다."],"related":["logical-dump","restore-dump","last-backup-status"]},"sol_replication":{"kind":"solution","title":"내구성: 아카이브/로그 모드 및 복제","detail":"특정 시점 복구와 고가용성을 위해 아카이브/로그 모드를 활성화하고 최소 하나의 복제본을 두어야 한다.","steps":["아카이브/로그 모드를 활성화한다(PG archive_mode/wal_level, Oracle ARCHIVELOG, MySQL binlog, MSSQL FULL recovery).","최소 하나의 복제본(standby/replica)을 구성하고 초기 동기화를 완료한다.","복제 지연을 모니터링하고 지연 알림을 정의한다.","통제된 페일오버/스위치오버 훈련을 실행해 페일오버를 검증한다."],"related":["replication-status","list-replicas"],"playbook":"replication-lag"},"sol_monitoring":{"kind":"solution","title":"모니터링 및 알림 설정","detail":"운영 환경 투입 전 마지막 단계: 사용자보다 먼저 문제를 파악하는 것. 적절한 임계값으로 헬스 체크와 알림을 구성한다.","steps":["기본 헬스 체크를 수집한다: 캐시 적중률, 활성/대기 세션, 가장 느린 쿼리.","용량 알림을 정의한다: 디스크 사용량, 한도에 근접한 연결 수, 복제 지연.","알림을 채널(이메일/채팅)로 전달하고 임계값을 실제 부하에 맞게 보정한다.","백업 성공 여부와 오류 로그도 모니터링에 추가해 조용한 실패를 잡아낸다."],"related":["cache-hit-ratio","connection-count","free-space"]}}},"solver/corruption-recovery.json":{"id":"corruption-recovery","title":"데이터 손상 복구","icon":"🧯","category":"복구","intro":"데이터 손상(블록/페이지 체크섬 오류, 읽을 수 없는 테이블, 손상된 RDB/AOF)에 대한 단계별 의사결정 트리: 먼저 손상 범위를 확인한 뒤 블록 수준 복구, 백업에서의 전체 복구/PITR, 최후 수단인 복구 도구 중 올바르게 선택한다.","start":"root","nodes":{"root":{"kind":"step","title":"손상 범위는 어느 정도인가?","detail":"먼저 무결성 검사를 실행해 손상이 어디까지 퍼졌는지 파악한다. 일부 블록/페이지만 손상되었는가, 단일 객체(테이블/인덱스)가 영향을 받았는가, 아니면 손상이 광범위한가? 아래 명령으로 스캔해 범위를 파악한 뒤 적절한 분기를 선택한다.","command":{"oracle":"RMAN> BACKUP VALIDATE CHECK LOGICAL DATABASE;  -- then: SELECT file#, block#, blocks, corruption_type FROM v$database_block_corruption;","postgres":"pg_amcheck --all --heapallindexed --progress   # single table: pg_amcheck -d mydb -t app_schema.my_table","mysql":"CHECK TABLE app_schema.my_table EXTENDED;   -- for InnoDB also: SHOW ENGINE INNODB STATUS\\G","mssql":"DBCC CHECKDB (N'mydb') WITH NO_INFOMSGS, ALL_ERRORMSGS;","mongodb":"db.my_table.validate({ full: true })","redis":"redis-check-rdb /var/lib/redis/dump.rdb   # AOF (7+, read-only check): redis-check-aof /var/lib/redis/appendonlydir/appendonly.aof.manifest  (--fix modifies the files; only after taking a copy)","elasticsearch":"GET _cluster/health?level=shards   # for a corrupted shard: GET _cat/shards?v&h=index,shard,state,unassigned.reason"},"choices":[{"label":"일부 블록 / 페이지만 손상됨","next":"block_level"},{"label":"단일 객체(테이블/인덱스)가 영향을 받음","next":"restore"},{"label":"손상이 광범위함 / 다수 객체","next":"restore"},{"label":"검사 도구가 동작하지 않고 DB가 열리지 않음","next":"last_resort"},{"label":"손상이 반복됨(근본 원인 의심)","next":"hardware"}]},"block_level":{"kind":"solution","title":"근본 원인: 제한적인 블록/페이지 손상 → 대상 복구","detail":"일부 블록/페이지만 손상되었다면 데이터베이스 전체를 복구할 필요가 없다. 엔진의 블록/페이지 수준 복구 기능으로 손상된 부분만 백업/로그에서 복구해 데이터 손실을 최소화한다.","steps":["Oracle: v$database_block_corruption에서 손상 블록 목록을 검토한 후(목록이 오래되었다면 먼저 RMAN> VALIDATE DATABASE; 실행) RMAN> RECOVER DATAFILE 5 BLOCK 1234;로 단일 블록을, RMAN> RECOVER CORRUPTION LIST;로 목록의 모든 블록을 복구한다.","SQL Server: 단일 페이지 복구 — RESTORE DATABASE [db] PAGE='5:1234' FROM DISK='D:\\backup\\db.bak' WITH NORECOVERY;  이후의 로그 백업을 WITH NORECOVERY로 적용하고 마지막 백업은 WITH RECOVERY로 적용한다.","PostgreSQL: 자동 블록 수준 복구 기능은 없다. 영향을 받은 테이블을 백업에서 부분 복구하거나, 데이터 추출 목적으로만 zero_damaged_pages=on을 일시적으로 활성화한다(손상된 행은 손실됨).","MySQL/InnoDB: 단일 페이지 복구 기능은 없다. 영향을 받은 테이블을 마지막 정상 백업에서 복구하고 필요하면 PITR로 롤포워드한다.","복구 후 검사 명령(VALIDATE / DBCC CHECKDB / pg_amcheck)을 다시 실행해 손상이 남아 있지 않은지 확인한다.","복구된 블록의 업무 데이터를 애플리케이션 측에서 샘플링해 검증한다."],"related":["restore-dump","last-backup-status"],"playbook":"corruption"},"restore":{"kind":"solution","title":"근본 원인: 객체 수준/광범위한 손상 → 백업에서 전체 복구 또는 PITR","detail":"손상이 하나 이상의 객체로 퍼졌다면 가장 안전한 방법은 정상 백업에서 복구하고 가능하면 손상 시점까지 롤포워드(PITR)하는 것이다. 이렇게 하면 복구 도구로 인한 데이터 손실을 피할 수 있다.","steps":["손상이 시작된 대략적인 시점(alert log / error log)을 파악해 PITR 목표를 그 직전으로 설정한다.","최신 정상 전체 백업과 그 시점까지의 트랜잭션 로그(아카이브 로그 / WAL / binlog)를 검증한다.","Oracle: RMAN> RESTORE DATABASE; RECOVER DATABASE UNTIL TIME \"TO_DATE('2026-07-04 09:59:00','YYYY-MM-DD HH24:MI:SS')\"; ALTER DATABASE OPEN RESETLOGS;","SQL Server: RESTORE DATABASE [db] FROM DISK='...' WITH NORECOVERY; RESTORE LOG [db] FROM DISK='...' WITH STOPAT='2026-07-04 09:59:00', RECOVERY;","PostgreSQL: base backup을 복구하고 recovery_target_time='2026-07-04 09:59:00'을 설정한 뒤 그 시점까지 WAL을 적용한다.","MySQL/MariaDB: 전체 백업을 복구한 뒤 mysqlbinlog --stop-datetime='2026-07-04 09:59:00'으로 그 시점까지 binlog를 적용한다.","MongoDB: 정상 복제본/스냅샷에서 mongorestore로 복구한다. Elasticsearch: POST _snapshot/my_repo/snap1/_restore로 손상된 인덱스를 복구한다.","오픈 후 무결성 검사를 다시 실행하고 행 수/애플리케이션 검사로 데이터를 검증한다."],"related":["restore-dump","logical-dump","last-backup-status"],"playbook":"corruption"},"last_resort":{"kind":"step","title":"백업이 없음 / DB가 열리지 않음 — 최후 수단으로 넘어갈 것인가?","detail":"복구 도구(REPAIR_ALLOW_DATA_LOSS, innodb_force_recovery, --fix)는 데이터 손실을 유발할 수 있으며 무결성 제약 조건을 깨뜨릴 수 있다. 사용 가능한 백업이 없고 데이터베이스를 열 수 없을 때만 쓰는 최후 수단이다. 먼저 사용 가능한 백업이 정말 없는지 확인한다.","choices":[{"label":"정상 백업을 찾음 — 복원 진행","next":"restore"},{"label":"백업이 전혀 없다, 복구 도구가 유일한 방법이다","next":"repair"}]},"repair":{"kind":"solution","title":"최후 수단: 데이터 손실 위험이 있는 복구 도구","detail":"이 도구들은 손상된 행/페이지를 폐기해 데이터베이스를 열 수 있게 만든다. 백업이 없고 기존 파일의 사본을 먼저 확보한 경우에만 사용한다. 목표: 데이터를 내보낼 수 있을 정도로만 연 뒤 깨끗한 사본으로 옮기는 것.","steps":["먼저 기존 데이터 파일의 원본 사본을 확보한다(시도 과정에서 상황이 악화될 수 있다).","SQL Server: DBCC CHECKDB (N'db', REPAIR_ALLOW_DATA_LOSS) WITH ALL_ERRORMSGS;  (데이터베이스는 SINGLE_USER 모드여야 하며, 이 명령은 데이터 손실을 유발할 수 있다).","MySQL/InnoDB: my.cnf의 [mysqld] 아래에 innodb_force_recovery=1을 설정해 시작한다. 열리지 않으면 2, 3...(최대 6)으로 높인다. 열리면 즉시 mysqldump로 데이터를 내보내고, 깨끗한 설치본에 가져온 뒤 설정을 제거한다.","PostgreSQL: 최후 수단으로 WAL이 손상된 경우에만 pg_resetwal(데이터 손실 위험 높음)을 사용한다. 이후 pg_dump로 데이터를 추출해 새 클러스터에 로드한다.","Oracle: 백업이 없으면 DBMS_REPAIR / 이벤트 기반 복구를 Oracle Support와 함께 검토한다. 'skip corrupt'로 블록을 건너뛸 수 있다(데이터 손실).","MongoDB: mongod --repair 또는 WiredTiger salvage로 연 뒤 데이터를 새 replica set으로 옮긴다.","복구 후에는 반드시 깨끗한 설치본으로 옮기고, 무결성 검사를 다시 실행하며, 손실된 데이터를 보고한다."],"related":["logical-dump","restore-dump"],"playbook":"corruption"},"hardware":{"kind":"solution","title":"근본 원인: 하드웨어/인프라 → 원인을 해결하고 체크섬을 활성화","detail":"손상이 반복된다면 실제 원인은 디스크, 컨트롤러, 메모리(RAM) 또는 불안정한 스토리지 계층일 가능성이 높다. 데이터 복구만으로는 충분하지 않으며, 원인을 해결하지 않으면 손상이 재발한다. 조기 탐지를 위해 체크섬/무결성 검증도 활성화한다.","steps":["디스크 상태를 확인한다: SMART 값(smartctl -a /dev/sdX), RAID 컨트롤러 로그, 스토리지 알림.","메모리 테스트를 실행한다(memtest86+ 또는 ECC 오류 카운터). 단일 비트 오류는 손상의 대표적인 원인이다.","OS/파일 시스템 및 하드웨어 오류 로그(dmesg, Event Viewer, 스토리지 어레이 알림)를 검토한다.","PostgreSQL: initdb --data-checksums(또는 pg_checksums --enable)로 체크섬이 적용된 클러스터를 생성해 디스크 기록 시점에 손상을 잡아낸다.","Oracle: DB_BLOCK_CHECKSUM=FULL과 DB_BLOCK_CHECKING=FULL로 블록 검증을 활성화하고 DB_LOST_WRITE_PROTECT=TYPICAL을 추가한다.","SQL Server: 데이터베이스가 PAGE_VERIFY CHECKSUM을 사용하는지 확인한다(ALTER DATABASE [db] SET PAGE_VERIFY CHECKSUM).","MySQL/InnoDB: innodb_checksum_algorithm=crc32와 필요 시 innodb_doublewrite=ON으로 부분 쓰기를 방지한다.","원인을 해결한 뒤 최신 정상 백업을 깨끗한 사본으로 복구하고 모니터링을 강화한다."],"related":["restore-dump","last-backup-status"],"playbook":"corruption"}}},"solver/failover-drill.json":{"id":"failover-drill","title":"페일오버 훈련 / 계획된 역할 전환","icon":"🔀","category":"복제","intro":"역할 변경 유형(계획된 스위치오버, 긴급 페일오버, 운영 환경을 건드리지 않는 훈련)을 식별하고 각 시나리오에 맞는 올바른 명령 순서와 검증 단계를 제공하는 의사결정 트리. 목표: 계획된 전환은 데이터 손실 없이, 긴급 시에는 최소 손실로, 훈련에서는 다운타임 없이 전환할 준비를 갖추는 것.","start":"root","nodes":{"root":{"kind":"step","title":"무엇을 하려는가?","detail":"역할 변경의 상황을 선택한다. primary가 정상인 상태에서 계획된 전환(유지보수/마이그레이션)을 하는가, primary가 실제로 중단되어 긴급 전환이 필요한가, 아니면 운영 환경을 건드리지 않고 훈련/준비 상태 검증을 하는가?","choices":[{"label":"계획된 스위치오버(primary 정상)","next":"planned"},{"label":"긴급 페일오버(primary 중단)","next":"emergency_check"},{"label":"훈련 / 준비 상태 테스트(운영 환경 미변경)","next":"drill"}]},"planned":{"kind":"step","title":"계획된 전환 — 먼저 준비되었는가?","detail":"계획된 스위치오버의 목표는 데이터 손실 제로다. 이를 위해 전환 전에 복제본이 primary를 따라잡았고(지연 ~0) 정상인지 확인해야 한다. 아래 명령으로 지연을 확인하고, 지연이 0에 가까우면 전환 단계로 진행한다.","command":{"oracle":"SELECT thread#, MAX(sequence#) FROM v$archived_log GROUP BY thread#;   -- DG: SELECT name, value FROM v$dataguard_stats WHERE name LIKE '%lag%';","postgres":"SELECT client_addr, state, replay_lag, write_lag FROM pg_stat_replication;","mysql":"SHOW REPLICA STATUS\\G   -- check Seconds_Behind_Source and Replica_SQL_Running","mssql":"SELECT ag.name, drs.synchronization_state_desc, drs.log_send_queue_size, drs.redo_queue_size FROM sys.dm_hadr_database_replica_states drs JOIN sys.availability_groups ag ON ag.group_id = drs.group_id;","mongodb":"rs.printSecondaryReplicationInfo()","redis":"redis-cli INFO replication   # check the difference between master_repl_offset and the slave offset","elasticsearch":"GET _cat/recovery?v&active_only=true"},"choices":[{"label":"지연이 0에 가깝다, 전환 준비 완료","next":"sol_planned"},{"label":"지연이 큼 — 먼저 동기화를 기다림","next":"drill"}]},"sol_planned":{"kind":"solution","title":"계획된 스위치오버(데이터 손실 없는 통제된 전환)","detail":"primary와 standby가 정상이고 동기화된 상태에서 통제된 방식으로 역할을 교체한다. 순서가 중요하다: 쓰기를 중지하고, 남은 변경 사항이 standby에 도달했는지 확인한 뒤, 역할을 넘기고 애플리케이션을 새 primary로 전환한다.","steps":["유지보수 시간대를 공지하고 애플리케이션의 읽기와 쓰기를 통제된 방식으로 보류한다.","Oracle Data Guard (12c+): SQL> ALTER DATABASE SWITCHOVER TO standby_db VERIFY; 이후 SQL> ALTER DATABASE SWITCHOVER TO standby_db;  (Broker 사용 시: DGMGRL> SWITCHOVER TO 'standby_db'; 11g 이하: COMMIT TO SWITCHOVER TO PHYSICAL STANDBY).","PostgreSQL: primary를 정상 종료(pg_ctl stop -m fast)하고, SELECT pg_promote();로 standby를 승격한 뒤, pg_rewind로 이전 primary를 새 standby로 다시 연결한다.","SQL Server AG: 동기 복제본에서 ALTER AVAILABILITY GROUP [ag] FAILOVER;  (데이터 손실 없는 계획된 페일오버, SYNCHRONOUS_COMMIT 필요).","MongoDB: rs.stepDown(60)으로 현재 primary를 60 s 동안 강제로 물러나게 한다. 가장 최신 상태의 secondary가 자동으로 선출되어 primary가 된다.","Redis: 새 master 후보에서 REPLICAOF NO ONE; 실행 후 다른 노드에서 REPLICAOF new_master port; 실행(Sentinel 사용 시 SENTINEL FAILOVER master_name).","애플리케이션 연결 문자열 / 라우팅을 새 primary로 지정한다(VIP, 서비스 이름 또는 연결 문자열).","검증: 새 primary가 쓰기를 수락하고, 이전 primary가 지연 ~0으로 standby로서 스트리밍 중이며, 애플리케이션에 오류가 없는지 확인한다."],"related":["promote-failover","replication-status","list-replicas"],"playbook":"replication-lag"},"emergency_check":{"kind":"step","title":"긴급 전환 — primary가 정말 중단되었는가?","detail":"페일오버를 강제하기 전에 primary에 정말 접근할 수 없는지 확인하고 split-brain 위험을 평가한다. primary가 실제로 살아 있다면(예: 일시적인 네트워크 장애) 강제 전환은 쓰기 가능한 두 개의 별도 사본(split-brain)을 만든다. 또한 지연을 비교해 가장 최신 상태의 standby를 선택한다.","command":{"oracle":"-- try to reach the primary: tnsping primary_db; and on the standby: SELECT thread#, MAX(sequence#) FROM v$archived_log GROUP BY thread#;","postgres":"SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();   -- choose the highest LSN among the standbys","mysql":"SHOW REPLICA STATUS\\G   -- choose the candidate with the largest Exec_Source_Log_Pos","mssql":"SELECT drs.replica_id, drs.last_hardened_lsn FROM sys.dm_hadr_database_replica_states drs;","mongodb":"rs.status()   -- the member with the highest optime is the primary candidate","redis":"redis-cli INFO replication   # choose the one with the highest slave_repl_offset","elasticsearch":"GET _cat/nodes?v   # verify whether the master node is reachable"},"choices":[{"label":"예, primary가 중단됨 — 전환을 강제한다","next":"emergency"},{"label":"확실하지 않음 / primary가 복귀할 수 있음","next":"planned"}]},"emergency":{"kind":"solution","title":"긴급 페일오버(primary 중단, 데이터 손실 위험이 있는 전환)","detail":"primary에 접근할 수 없고 복귀를 기다릴 수 없다. standby를 강제로 승격해야 하며, 비동기 복제에서는 마지막 트랜잭션 일부가 손실될 수 있다. 우선순위는 서비스 복구이며, 이후 손실 구간을 파악하고 이전 primary를 안전하게 복귀시킨다.","steps":["primary가 정말 중단되었는지 확인한다(split-brain 방지를 위해 이전 primary를 네트워크에서 격리하거나 종료 — fencing/STONITH).","가장 최신 상태의 standby를 선택한다: 데이터 손실이 가장 적은 standby를 승격하도록 실행 전에 지연/오프셋을 비교한다.","Oracle Data Guard: DGMGRL> FAILOVER TO 'standby_db';  (또는 SQL> ALTER DATABASE ACTIVATE STANDBY DATABASE;).","PostgreSQL: SELECT pg_promote();로 standby를 즉시 승격한다. 이전 primary가 복귀하면 pg_rewind 없이 primary로 열지 않는다(split-brain 위험).","SQL Server AG: ALTER AVAILABILITY GROUP [ag] FORCE_FAILOVER_ALLOW_DATA_LOSS;  (비동기 복제본에서는 데이터 손실이 발생할 수 있다).","MongoDB: 과반수가 남아 있지 않으면 rs.reconfig(cfg, {force:true})로 남은 멤버를 재구성한다.","Redis: Sentinel이 없으면 새 master 후보에서 REPLICAOF NO ONE을 실행하고 나머지를 그 노드에 연결한다. 이전 master가 자동으로 다시 합류하지 않도록 한다.","애플리케이션을 새 primary로 전환한 뒤, 손실된 데이터 구간(손실된 트랜잭션)을 파악해 보고하고 이전 primary를 새 standby로 깨끗하게 재구축한다."],"related":["promote-failover","replication-status","list-replicas"],"playbook":"db-down"},"drill":{"kind":"solution","title":"훈련 / 준비 상태 테스트(운영 환경을 건드리지 않고 검증)","detail":"실제 전환 없이 페일오버 준비 상태를 입증한다. 운영 환경에 영향을 주지 않고 복제본 상태, 허용 가능한 지연, 애플리케이션 전환 가능 여부를 검증한다. 선택적으로 격리된 사본에서 실제 스위치오버를 리허설한다.","steps":["지연을 측정하고 임계값 미만인지 확인한다(PG: pg_stat_replication.replay_lag; MySQL: Seconds_Behind_Source; Oracle: v$dataguard_stats; Mongo: rs.printSecondaryReplicationInfo()).","복제본 상태를 검증한다: 모든 standby가 스트리밍 중이고 예상한 멤버에서 동기 모드가 활성화되어 있는지 확인한다(SQL Server: synchronization_state_desc='SYNCHRONIZED').","읽기/역할 테스트: standby에서 읽기 전용 쿼리를 실행해 지연 없이 응답하는지 확인한다(PG hot standby, Oracle Active Data Guard, read replica).","애플리케이션 라우팅 리허설: 테스트 환경에서 연결 문자열/서비스 이름을 새 primary로 지정하고 애플리케이션이 문제없이 연결되는지 확인한다(운영 트래픽은 건드리지 않는다).","격리된 사본에서 실제 스위치오버를 처음부터 끝까지 리허설하고 전환 시간(RTO)을 측정한다.","페일백 단계도 리허설한다: pg_rewind / DG reinstate / AG resume으로 이전 primary를 재구축한다.","발견 사항, 소요 시간, 미비점을 기록하고 런북을 갱신하며 훈련을 정기적으로 일정에 반영한다."],"related":["replication-status","list-replicas","promote-failover"],"playbook":"replication-lag"}}},"solver/security-incident.json":{"id":"security-incident","title":"보안 사고 / 의심스러운 접근","icon":"🚨","category":"보안","intro":"의심스러운 접근, 예상치 못한 권한 변경 또는 데이터 유출 가능성에 대해, 증상에 따라 올바른 대응 단계(계정 잠금, 과도한 권한 회수, 연결 차단, 범위 파악)와 사고 후 영구적인 보안 강화로 안내하는 의사결정 트리.","start":"root","nodes":{"root":{"kind":"step","title":"무엇을 발견했는가?","detail":"사고 유형을 선택한다. 증상이 의심스러운 로그인 또는 로그인 실패 급증인가, 예상치 못한 권한(GRANT/롤) 변경인가, 데이터 유출 가능성 / 비정상적인 내보내기인가, 아니면 사고가 종료되어 영구적인 보안 강화를 하는가? 첫 관찰 내용을 바탕으로 가장 적절한 분기를 선택한다. 먼저 사고 시각과 영향을 받은 계정/리소스를 기록한다.","choices":[{"label":"의심스러운 로그인 / 로그인 실패 급증","next":"identity"},{"label":"예상치 못한 권한(GRANT/롤) 변경","next":"privilege"},{"label":"데이터 유출 / 비정상적인 내보내기","next":"data"},{"label":"사고 후 영구 조치(보안 강화)","next":"harden"}]},"identity":{"kind":"step","title":"계정 사고: 어떤 증상인가?","detail":"다수의 로그인 실패는 brute-force / credential stuffing 공격을 의미한다. 반면 성공했지만 예상치 못한 로그인(비정상적인 IP/시간대)은 계정이 탈취되었을 수 있음을 나타낸다. 두 경우 모두 먼저 증거를 수집한 뒤 대응한다.","command":{"oracle":"SELECT username, os_username, userhost, timestamp, returncode FROM dba_audit_session WHERE returncode <> 0 ORDER BY timestamp DESC;","postgres":"-- failed logins in the log file: grep 'password authentication failed' postgresql.log | awk '{print $NF}' | sort | uniq -c | sort -rn","mysql":"SELECT * FROM performance_schema.host_cache ORDER BY COUNT_AUTHENTICATION_ERRORS DESC;   -- also 'Access denied' in the error log","mssql":"-- failed logins in the error log: EXEC xp_readerrorlog 0,1,N'Login failed';  also SELECT login_name, COUNT(*) FROM sys.dm_exec_sessions GROUP BY login_name;","mongodb":"-- audit log: grep 'authenticate' audit.json | grep '\"result\":18'  (18 = AuthenticationFailed)","redis":"ACL LOG   # shows failed authentication/command attempts","elasticsearch":"GET .security-audit-*/_search { \"query\": { \"match\": { \"event.action\": \"authentication_failed\" } } }"},"choices":[{"label":"로그인 실패 급증(brute force)","next":"id_bruteforce"},{"label":"성공했지만 예상치 못한 로그인(탈취)","next":"id_compromised"}]},"id_bruteforce":{"kind":"solution","title":"Brute force → 출처를 차단하고 계정을 보호","detail":"로그인 실패가 급증하는 경우 아직 접근에 성공하지 않았을 수 있다. 목표는 출처를 차단하고, 잠금/지연으로 계정을 보호하며, 침입 성공 가능성을 배제하는 것이다.","steps":["공격 IP를 식별하고(위 쿼리) 네트워크 수준에서 차단한다: firewall / pg_hba.conf reject / listener ACL / security group.","대상 계정을 일시적으로 잠근다: Oracle: ALTER USER target_user ACCOUNT LOCK;  PostgreSQL: ALTER ROLE target_user NOLOGIN;  SQL Server: ALTER LOGIN [target_user] DISABLE;  MySQL: ALTER USER 'target_user'@'%' ACCOUNT LOCK;  Redis: ACL SETUSER target_user off.","로그인 시도 제한 / 잠금 정책을 적용한다: Oracle: 프로파일의 FAILED_LOGIN_ATTEMPTS; PostgreSQL: auth_delay 모듈(shared_preload_libraries; 잠금 대신 실패한 시도를 지연시킴); MySQL: FAILED_LOGIN_ATTEMPTS ... PASSWORD_LOCK_TIME.","대상 계정의 비밀번호는 어쨌든 강력한 값으로 재설정한다(시도가 성공했을 수 있다).","시도가 성공했는지 확인한다: 동일 IP에서의 성공한 로그인을 확인하고, 있다면 탈취 분기도 따른다.","타임라인과 함께 사고를 기록하고 영구 조치를 위해 보안 강화 단계(루트의 '사고 후 영구 조치')를 따른다."],"related":["change-password","list-users"],"playbook":"harden-sql"},"id_compromised":{"kind":"solution","title":"탈취된 계정 → 중지, 조사, 자격 증명 교체","detail":"예상치 못한 로그인 성공은 계정이 탈취되었음을 나타낸다. 먼저 계정과 세션을 중지하고, 수행한 작업을 조사한 뒤, 비밀번호를 재설정하고 접근 출처를 좁힌다.","steps":["계정을 즉시 잠그고(위의 LOCK/DISABLE 명령) 활성 세션을 종료한다: Oracle: ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE;  PostgreSQL: SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename='app_user';  SQL Server: KILL <spid>;  MySQL: KILL <id>;  Redis: CLIENT KILL.","계정이 세션에서 수행한 작업을 조사한다: 감사 추적/로그에서 실행한 쿼리, 접근한 테이블, GRANT/CREATE 작업을 확인한다.","비밀번호를 강력한 값으로 재설정하고 관련된 모든 서비스/애플리케이션 자격 증명을 교체한다.","공격자가 생성했을 수 있는 새 계정이나 권한 변경을 찾는다(권한 분기도 따른다).","접근 출처를 좁힌다: 계정이 알려진 IP에서만 연결할 수 있도록 허용한다(pg_hba.conf / listener ACL / firewall).","영구 조치를 위해 보안 강화 단계(루트의 '사고 후 영구 조치')를 따른다."],"related":["change-password","list-users","show-privileges"],"playbook":"harden-sql"},"privilege":{"kind":"solution","title":"권한 상승 → 감사 및 과도한 권한 회수","detail":"예상치 못한 GRANT, 롤 할당 또는 새로운 고권한 계정은 탈취의 2단계(지속성 확보/권한 상승)일 수 있다. 최근 권한 변경을 감사하고, 정당하지 않은 권한을 회수하며, 최소 권한 원칙으로 되돌린다.","steps":["최근 권한 변경을 감사한다: Oracle: SELECT * FROM dba_audit_trail WHERE action_name IN ('GRANT','REVOKE','CREATE USER') ORDER BY timestamp DESC;  SQL Server: default trace / Server Audit의 GRANT 이벤트 및 SELECT * FROM sys.server_permissions;  MySQL: 감사 로그의 GRANT 항목 및 SELECT * FROM mysql.user;  MongoDB: db.getRoles({showPrivileges:true}).","현재 권한을 조회한다: Oracle: SELECT grantee, privilege FROM dba_sys_privs WHERE grantee='APP_USER';  PostgreSQL: SELECT * FROM information_schema.role_table_grants WHERE grantee='app_user';  MySQL: SHOW GRANTS FOR 'app_user'@'%';","정당하지 않은 과도한 권한을 회수한다: Oracle: REVOKE DBA FROM app_user;  PostgreSQL: REVOKE ALL ON ... FROM app_user; ALTER ROLE app_user NOSUPERUSER;  SQL Server: ALTER SERVER ROLE [sysadmin] DROP MEMBER [app_user];  MySQL: REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app_user'@'%';","공격자가 생성했을 수 있는 숨겨진/백도어 계정을 찾아 제거한다(예상치 못한 CREATE USER 기록).","PUBLIC 또는 광범위한 롤에 부여된 과도한 권한을 검토하고 축소한다.","변경 사항이 정당한 변경 요청과 일치하는지 확인한다. 일치하지 않는 모든 항목은 사고의 일부로 취급하고 보안 강화 단계를 따른다."],"related":["show-privileges","list-users","create-user-grant"],"playbook":"harden-sql"},"data":{"kind":"solution","title":"데이터 유출 → 접근 차단 및 범위 파악","detail":"비정상적으로 큰 쿼리/내보내기나 예상치 못한 외부 연결은 데이터 유출을 의미한다. 먼저 위험한 활성 연결을 차단한 뒤, 감사 추적/로그로 범위를 파악한다: 어떤 데이터에, 누가, 언제 접근했는가.","steps":["활성 연결 출처를 검토한다: PostgreSQL: SELECT pid, usename, client_addr, state, query FROM pg_stat_activity WHERE client_addr IS NOT NULL;  Oracle: SELECT sid, serial#, username, machine, program FROM v$session;  SQL Server: SELECT * FROM sys.dm_exec_connections;  MongoDB: db.currentOp(true);  Redis: CLIENT LIST.","의심스러운/외부 연결을 즉시 차단하고 해당 계정을 잠근다(KILL/LOCK 명령).","네트워크 수준에서 출처를 차단한다: firewall / pg_hba.conf / listener ACL로 의심스러운 IP 대역을 차단한다.","감사 로그를 검토한다: 어떤 테이블이 SELECT/EXPORT되었는지, 행 수, 시간 구간을 파악한다(Oracle unified_audit_trail; SQL Server sys.fn_get_audit_file; PostgreSQL pgaudit 로그; MongoDB auditLog).","내보내기 흔적을 찾는다: 대용량 결과 집합, COPY / SELECT ... INTO OUTFILE, mongoexport, 대량 읽기 패턴.","영향을 받은 데이터 범위(어떤 테이블, 어떤 개인/기밀 필드)와 예상 데이터 양을 문서화하고, 법적/신고 의무를 평가한 뒤 보안 강화 단계를 따른다."],"related":["show-privileges","list-users"],"playbook":"harden-sql"},"harden":{"kind":"solution","title":"사고 후 영구적인 보안 강화","detail":"긴급 대응 후 재발 방지를 위한 영구 통제를 적용한다: 감사 활성화, 최소 권한 적용, 네트워크 접근 제한, 사고의 완전한 기록.","steps":["감사를 활성화한다: 중요 이벤트(로그인, GRANT/REVOKE, 민감 테이블 접근)를 감사 추적에 기록한다. 자세한 내용은 '감사 설정' 트리를 따른다.","최소 권한: 각 계정에 필요한 권한만 부여하고, 공유/서비스 계정을 분리하며, 사용하지 않는 계정을 비활성화한다.","비밀번호/계정 정책: 강력한 비밀번호, 정기적인 교체, 가능한 경우 다중 인증, 중앙 집중식 인증(LDAP/Kerberos).","네트워크 제한: 데이터베이스를 애플리케이션 서버에만 개방하고(firewall, security group, pg_hba.conf, listener ACL) 인터넷 직접 접근을 차단한다.","암호화: 전송 중 TLS를 활성화하고 필요한 경우 저장 데이터 암호화를 적용한다.","모니터링/알림: 로그인 실패 급증, 새 GRANT, 비정상적인 데이터 양에 대한 알림을 설정한다.","사고 기록: 타임라인, 영향을 받은 자산, 수행한 조치, 교훈을 보고하고 런북을 갱신한다."],"related":["list-users","show-privileges","change-password"]}}},"solver/audit-setup.json":{"id":"audit-setup","title":"감사 설정","icon":"🧾","category":"보안","intro":"감사 대상(로그인/세션, DDL/스키마 변경, 데이터 접근, 권한 변경)에 따라 해당 범위의 감사를 올바른 데이터베이스 엔진에서 활성화하고, 기록 조회 및 보존/순환 단계를 제공하는 의사결정 트리.","start":"root","nodes":{"root":{"kind":"step","title":"무엇을 감사하려 하는가?","detail":"감사 범위를 선택한다. 모든 것을 감사하면 노이즈와 비용이 발생하므로 필요에 맞게 좁힌다. 로그인/세션 이벤트, DDL/스키마 변경, 데이터 접근(DML/SELECT), 권한 변경(GRANT/REVOKE) 중 무엇을 추적하려 하는가?","choices":[{"label":"로그인 / 세션(login, connect)","next":"login"},{"label":"DDL / 스키마 변경(CREATE/ALTER/DROP)","next":"ddl"},{"label":"데이터 접근(DML / SELECT)","next":"dml"},{"label":"권한 변경(GRANT / REVOKE)","next":"grants"},{"label":"감사 기록 조회 및 보존 / 순환","next":"retention"}]},"login":{"kind":"solution","title":"로그인 / 세션 감사","detail":"성공 및 실패한 로그인을 기록한다: 누가, 어디서, 언제 연결했는가. 무단 접근과 brute-force 시도를 탐지하는 기반이다. 엔진별로 해당 정책을 활성화한 뒤 기록을 조회하고 보존한다.","steps":["Oracle (Unified Auditing): CREATE AUDIT POLICY login_pol ACTIONS LOGON, LOGOFF;  AUDIT POLICY login_pol;  (실패 기록: AUDIT POLICY login_pol WHENEVER NOT SUCCESSFUL;).","PostgreSQL: postgresql.conf에서 log_connections=on과 log_disconnections=on을 설정하고 reload한다. pgaudit 없이도 연결이 로그에 기록된다.","MySQL/MariaDB: audit_log 플러그인을 설치하고(INSTALL PLUGIN audit_log ...) SET GLOBAL audit_log_policy='LOGINS';를 실행한다(MariaDB: server_audit_events='CONNECT').","SQL Server: Server Audit를 생성하고(CREATE SERVER AUDIT ...), Server Audit Specification에 FAILED_LOGIN_GROUP과 SUCCESSFUL_LOGIN_GROUP을 추가한 뒤 둘 다 ENABLE한다.","MongoDB: --auditDestination file --auditFilter '{ atype: { $in: [\"authenticate\", \"logout\"] } }'로 시작한다(Enterprise/Percona).","Redis: ACL LOG로 실패한 인증/명령 시도를 추적한다(자세한 내용은 'notify'가 아니라 ACL log를 사용).","Elasticsearch: elasticsearch.yml에서 xpack.security.audit.enabled: true를 설정하고 include 목록에 authentication_success, authentication_failed를 추가한다.","기록을 조회하고 보존/순환하려면 루트에서 '감사 기록 조회 및 보존 / 순환' 단계를 따른다."],"related":["list-users","change-password"]},"ddl":{"kind":"solution","title":"DDL / 스키마 변경 감사","detail":"CREATE/ALTER/DROP 같은 스키마 변경을 기록한다. 무단 객체 변경, 숨겨진 테이블/백도어, 스키마 드리프트를 탐지하는 데 사용한다.","steps":["Oracle: CREATE AUDIT POLICY ddl_pol ACTIONS CREATE TABLE, ALTER TABLE, DROP TABLE, CREATE USER, ALTER USER;  AUDIT POLICY ddl_pol;","PostgreSQL (pgaudit): shared_preload_libraries='pgaudit'로 로드한 뒤 ALTER SYSTEM SET pgaudit.log='ddl'; SELECT pg_reload_conf();","MySQL/MariaDB: audit_log로 SET GLOBAL audit_log_policy='ALL'; 설정 후 로그에서 DDL 문을 필터링한다(MariaDB: server_audit_events='QUERY_DDL').","SQL Server: Database Audit Specification에 SCHEMA_OBJECT_CHANGE_GROUP(및 DATABASE_OBJECT_CHANGE_GROUP)을 추가하고 ENABLE한다.","MongoDB: --auditFilter '{ atype: { $in: [\"createCollection\",\"dropCollection\",\"createIndex\",\"dropIndex\",\"createDatabase\",\"dropDatabase\"] } }'.","Elasticsearch: index_creation/index_deletion 관련 이벤트를 감사 include 목록에 추가한다. 스키마(mapping) 변경은 _mapping 호출을 요청으로 기록한다.","기록을 조회하고 보존/순환하려면 루트에서 '감사 기록 조회 및 보존 / 순환' 단계를 따른다."],"related":["show-privileges"]},"dml":{"kind":"solution","title":"데이터 접근(DML / SELECT) 감사","detail":"민감 테이블에 대한 읽기/쓰기 접근(SELECT/INSERT/UPDATE/DELETE)을 기록한다. 대량으로 생성될 수 있으므로 감사를 민감 객체로 제한한다. 유출 탐지와 컴플라이언스(KVKK/GDPR)에 필수적이다.","steps":["범위를 좁힌다: 전체 데이터가 아니라 민감한 테이블/컬럼만 감사한다(노이즈와 비용 통제).","Oracle: CREATE AUDIT POLICY dml_pol ACTIONS SELECT, INSERT, UPDATE, DELETE ON HR.SALARIES;  AUDIT POLICY dml_pol;","PostgreSQL (pgaudit): 광범위한 로깅에는 ALTER SYSTEM SET pgaudit.log='read,write';를 사용하고, 대상 객체 수준 감사에는 관련 테이블에만 감사 롤 권한을 부여하고 pgaudit.role을 설정한다.","MySQL/MariaDB: audit_log 필터로 관련 테이블/사용자만 기록한다(audit_log_filter / MariaDB server_audit_events='QUERY_DML').","SQL Server: Database Audit Specification에 특정 테이블과 principal(예: public)에 대한 SELECT, INSERT, UPDATE, DELETE 작업을 추가하고 ENABLE한다.","MongoDB: --auditFilter '{ atype: \"authCheck\", \"param.command\": { $in: [\"find\",\"insert\",\"update\",\"delete\"] }, \"param.ns\": \"db.sensitive\" }' (볼륨에 주의).","Elasticsearch: 감사 include 목록에 access_granted/access_denied를 추가하고 emit_request_body로 쿼리가 대상으로 하는 인덱스를 확인한다(민감 데이터의 경우 본문 로깅에 주의).","기록을 조회하고 보존/순환하려면 루트에서 '감사 기록 조회 및 보존 / 순환' 단계를 따른다."],"related":["show-privileges","list-users"]},"grants":{"kind":"solution","title":"권한 변경(GRANT / REVOKE) 감사","detail":"권한 및 롤 변경(GRANT/REVOKE, 롤 할당, 사용자 생성)을 기록한다. 권한 상승과 백도어 계정을 탐지하는 기반이며, 대개 볼륨은 적지만 가치가 높은 감사다.","steps":["Oracle: CREATE AUDIT POLICY priv_pol ACTIONS GRANT, REVOKE, CREATE USER, ALTER USER, CREATE ROLE;  AUDIT POLICY priv_pol;","PostgreSQL (pgaudit): ALTER SYSTEM SET pgaudit.log='role'; SELECT pg_reload_conf();  (GRANT/REVOKE/CREATE ROLE 이벤트를 기록).","MySQL/MariaDB: audit_log(정책 'ALL' 또는 DDL)로 GRANT/REVOKE/CREATE USER 문을 기록하고 필터에 ACL 문을 유지한다.","SQL Server: Server Audit Specification에 SERVER_PERMISSION_CHANGE_GROUP, SERVER_ROLE_MEMBER_CHANGE_GROUP, DATABASE_PERMISSION_CHANGE_GROUP을 추가하고 ENABLE한다.","MongoDB: --auditFilter '{ atype: { $in: [\"grantRolesToUser\",\"revokeRolesFromUser\",\"createUser\",\"dropUser\",\"createRole\"] } }'.","Elasticsearch: 롤/사용자 변경의 경우 보안 관련 요청 이벤트(realm/role API 호출)를 감사 추적에 기록한다.","기록을 조회하고 보존/순환하려면 루트에서 '감사 기록 조회 및 보존 / 순환' 단계를 따른다."],"related":["show-privileges","create-user-grant","list-users"]},"retention":{"kind":"solution","title":"감사 기록 조회 및 보존 / 순환","detail":"감사는 기록을 조회하고 보존할 때만 의미가 있다. 여기에서 기록 조회 방법, 보관 위치, 증가량을 통제하는 보존/순환 정책을 정의한다.","steps":["기록 조회: Oracle: SELECT event_timestamp, dbusername, action_name, object_name, sql_text FROM unified_audit_trail ORDER BY event_timestamp DESC;  PostgreSQL: pgaudit 출력은 서버 로그 파일(log_directory)에 기록된다. grep으로 'AUDIT:' 행을 필터링한다.","SQL Server: SELECT * FROM sys.fn_get_audit_file('D:\\audit\\*.sqlaudit', DEFAULT, DEFAULT);  MySQL: audit_log 파일(JSON/XML) 또는 MariaDB의 server_audit.log를 읽는다. MongoDB: audit.json을 파싱한다. Elasticsearch: .security-audit-* 인덱스를 조회한다.","보존 기간을 설정하고(컴플라이언스 요구 사항에 따라, 예: 90일 / 1년) 정책에 명시한다.","순환/정리: Oracle: DBMS_AUDIT_MGMT.SET_LAST_ARCHIVE_TIMESTAMP와 CLEAN_AUDIT_TRAIL로 오래된 기록을 아카이브/삭제한다. PostgreSQL/MySQL/MongoDB 파일 로그는 logrotate로 크기/시간 기준 순환한다.","Elasticsearch: 감사 인덱스에 대한 ILM 정책(rollover + delete)을 정의한다.","감사 기록을 DBA와 분리된 접근 권한을 가진 변경 불가(WORM) 위치로 전송하고(직무 분리) 접근을 제한한다.","스토리지 증가를 모니터링한다: 감사가 디스크를 채우거나 성능에 과도한 영향을 주지 않는지 주기적으로 확인한다."],"related":["list-users","show-privileges"]}}},"solver/decide-ha.json":{"id":"decide-ha","title":"결정: 어떤 HA/DR 솔루션인가?","icon":"🧭","category":"의사결정 가이드","intro":"고가용성 및 재해 복구 솔루션 선택은 기술이 아니라 업무 요구 사항(RTO/RPO), 지리적 분산 필요성, 비용 간의 균형에서 시작한다. 이 트리는 다운타임과 데이터 손실 허용 범위를 출발점으로 동기 복제본, 비동기 standby, 자동 페일오버 클러스터, 백업+PITR 중 올바른 접근 방식을 제시한다.","start":"root","nodes":{"root":{"kind":"step","title":"데이터 손실 허용 범위(RPO)는 어느 정도인가?","detail":"가장 엄격한 제약 조건부터 시작한다: 재해 시 몇 초 분량의 데이터 손실을 감수할 수 있는가? 데이터 손실 제로(RPO=0)가 요구되면 동기 복제가 필수이고, 몇 초/몇 분이 허용되면 비동기 솔루션과 더 저렴한 접근 방식이 가능하다. RPO 목표는 대개 다른 모든 결정을 좌우한다.","choices":[{"label":"데이터 손실 제로가 필수(RPO = 0)","next":"rpo_zero"},{"label":"몇 초/몇 분의 데이터 손실은 허용 가능","next":"rpo_soft"}]},"rpo_zero":{"kind":"step","title":"자동의 빠른 인계(낮은 RTO)도 필요한가?","detail":"데이터 손실 제로를 원한다. 그렇다면 primary가 중단되었을 때 얼마나 빨리 복구되어야 하는가? 수 초 내 자동 페일오버가 필요한가, 아니면 짧은 수동 개입 시간이 허용되는가? 이에 따라 단순 동기 복제본과 완전 자동화 클러스터가 구분된다.","choices":[{"label":"수 초 내 자동 페일오버가 필요","next":"sol_cluster"},{"label":"수동/계획된 인계로 충분, 손실 제로가 우선","next":"sol_sync"}]},"rpo_soft":{"kind":"step","title":"다른 리전/지역이 필요한가(DR)?","detail":"일부 데이터 손실이 허용된다면 핵심 질문은 복원력의 범위다: 데이터 센터/리전 전체 손실에 대비해야 하는가, 아니면 같은 리전 내 단일 노드 장애 대비로 충분한가? 지리적 분산이 필요하면 비동기 standby가 우선 고려되고, 그렇지 않으면 비용 우선 옵션을 검토한다.","choices":[{"label":"예, 지리적 재해에 대비한 별도 리전이 필수","next":"sol_async"},{"label":"아니요, 예산과 단순성이 우선","next":"budget"}]},"budget":{"kind":"step","title":"긴 복구 시간(높은 RTO)을 수용할 수 있는가?","detail":"단일 리전에 머무르며 일부 데이터 손실을 허용한다. 최저 비용을 목표로 하는가, 아니면 장애 시 빠르게 인계하는 standby 노드가 필요한가? 복구에 몇 시간이 걸려도 된다면 백업+PITR만으로 가장 경제적이며, 그렇지 않으면 warm standby가 필요하다.","choices":[{"label":"예, 복구에 몇 시간이 걸려도 됨(최저 비용)","next":"sol_backup"},{"label":"아니요, 몇 분 내 인계하는 warm standby가 필요","next":"sol_async"}]},"sol_sync":{"kind":"solution","title":"권장: 동기 복제본(데이터 손실 제로)","detail":"primary는 최소 하나의 standby가 확인 응답할 때까지 커밋을 완료하지 않는다. 따라서 primary를 잃어도 확인된 트랜잭션은 손실되지 않는다(RPO=0). 저지연 네트워크를 갖춘 단일 리전 내에서 이상적인 솔루션이다.","steps":["이유: 확인된 트랜잭션이 standby에 기록될 때까지 커밋이 완료되지 않으므로 데이터 손실은 수학적으로 제로다.","적용 시점: 금융/핵심 OLTP, 동일 리전(동일/인접 가용 영역), 저 RTT 네트워크를 사용할 수 있는 경우.","예: PostgreSQL synchronous_commit + synchronous_standby_names, MySQL Group Replication / semisync, Oracle Data Guard SYNC (Maximum Protection/Availability).","주의: 동기 확인 응답은 모든 커밋에 네트워크 지연을 추가한다. standby가 느려지거나 연결이 끊기고 동기 사본이 하나뿐이면 primary도 쓰기를 받지 못할 수 있다. 최소 두 개의 standby 또는 적절한 대체 방안으로 설계한다.","주의: 원격/고지연 리전으로 동기 복제하지 않는다. 쓰기 성능이 급락한다. 지리적 DR에는 비동기 복제를 사용한다."],"related":["replication-status","promote-failover"]},"sol_async":{"kind":"solution","title":"권장: 비동기 standby(지리적 DR)","detail":"standby는 primary의 커밋을 지연시키지 않고 변경 사항을 스트림으로 적용한다. 쓰기 성능을 해치지 않고 다른 리전에 사본을 유지하는 표준 방법이며, 대신 작은 데이터 손실 구간(복제 지연만큼)을 감수한다.","steps":["이유: primary가 standby 확인 응답을 기다리지 않으므로 원격 리전으로의 복제가 쓰기 지연을 추가하지 않는다.","적용 시점: 리전/데이터 센터 수준의 재해 복구가 필요하고 초 단위 데이터 손실이 허용되는 경우.","예: PostgreSQL streaming replication (async), MySQL 비동기 복제, Oracle Data Guard ASYNC (Maximum Performance).","주의: 페일오버 시 지연만큼의 데이터를 잃을 수 있다. 복제 지연을 지속적으로 모니터링하고 임계값 알림을 설정한다.","주의: 인계는 대개 수동 또는 외부 오케스트레이션으로 수행된다. 명확한 페일오버 절차와 정기 훈련이 필수다."],"related":["replication-status","promote-failover"]},"sol_cluster":{"kind":"solution","title":"권장: 자동 페일오버 클러스터","detail":"헬스 모니터링, 리더 선출, 자동 인계를 결합한 클러스터 솔루션으로, 낮은 RTO(수 초 내 자동 페일오버)와 동기 사본을 통한 낮은/제로 RPO를 모두 목표로 한다. 운영 부담은 크지만 사람의 개입이 최소화된다.","steps":["이유: 클러스터 매니저가 primary 장애를 자동으로 감지하고 정상 사본을 승격해 트래픽을 전환한다. 한밤중 수동 개입이 필요 없다.","적용 시점: 낮은 RTO와 낮은 RPO가 모두 필수이고 운영팀이 추가 복잡성을 관리할 수 있는 경우.","예: PostgreSQL용 Patroni/etcd, MySQL InnoDB Cluster (Group Replication + MySQL Router), SQL Server Always On Availability Groups, Oracle RAC + Fast-Start Failover를 사용하는 Data Guard(자동 페일오버에는 FSFO와 observer가 필요).","주의: split-brain 위험은 실재한다. 적절한 fencing/quorum(홀수 노드, 신뢰할 수 있는 분산 저장소) 없이 배포하지 않는다.","주의: 클러스터 자체가 새로운 장애 지점이 된다. 페일오버를 정기적으로 리허설하고 실제 조건에서 자동화를 검증한다."],"related":["promote-failover","replication-status"]},"sol_backup":{"kind":"solution","title":"권장: 백업 + PITR(가장 경제적)","detail":"실행 중인 standby 대신 정기 백업과 WAL/아카이브 로그를 통한 특정 시점 복구(PITR)만 유지한다. 비용이 가장 낮은 솔루션이며, 대가로 복구에 몇 시간이 걸릴 수 있다(높은 RTO).","steps":["이유: 상시 가동되는 두 번째 노드의 비용이 없다. 백업 스토리지와 아카이브 로그만 유지한다.","적용 시점: 다운타임 허용 범위가 큰 비핵심 시스템, 개발/테스트 환경, 예산과 단순성이 우선인 경우.","예: 물리 백업 + 아카이브 로그를 이용한 PITR(PG base backup + WAL, Oracle RMAN + archivelog, MySQL xtrabackup + binlog).","주의: RTO가 높다 — 복구 + 로그 적용 시간은 데이터 크기에 비례해 증가한다. 측정해서 허용 시간 내인지 확인한다.","주의: 테스트하지 않은 백업은 백업이 아니다. 복구 훈련과 백업 검증을 정기적으로 수행한다."],"related":["last-backup-status","logical-dump"]}}},"solver/decide-isolation.json":{"id":"decide-isolation","title":"결정: 어떤 격리 수준인가?","icon":"🧭","category":"의사결정 가이드","intro":"트랜잭션 격리 수준은 동시성과 일관성 간의 균형을 결정한다. 격리 수준이 높을수록 이상 현상은 줄지만 락/충돌과 재시도가 늘어난다. 이 트리는 허용할 수 있는 읽기 이상 현상(dirty, non-repeatable, phantom)을 기준으로 Read Committed, Repeatable Read/Snapshot, Serializable 중 선택을 돕는다.","start":"root","nodes":{"root":{"kind":"step","title":"dirty read를 허용할 수 있는가?","detail":"가장 기본적인 이상 현상부터 시작한다: 트랜잭션이 아직 커밋되지 않은(롤백될 수 있는) 데이터를 읽을 수 있어야 하는가? 이를 원하는 실제 애플리케이션은 거의 없으며, 대부분의 최신 엔진은 기본값으로 이미 dirty read를 방지한다. 대략적인 분석용 집계와 최대 속도가 필요한 경우에만 완화한다.","choices":[{"label":"아니요, 커밋된 데이터만 읽어야 함","next":"committed"},{"label":"예, 대략적인 읽기로 충분함(드묾)","next":"sol_read_uncommitted"}]},"committed":{"kind":"step","title":"같은 트랜잭션 내에서 다시 읽을 때 같은 결과가 보여야 하는가?","detail":"dirty read를 배제했다면 다음 질문: 트랜잭션 내에서 같은 행을 두 번 읽을 때, 그 사이 다른 세션의 커밋으로 다른 값이 보이거나(non-repeatable read) 새 행이 나타나는 것(phantom)이 문제가 되는가? 보고서/계산 일관성이 필요한 로직은 대개 일관된 스냅샷이 필요하다.","choices":[{"label":"아니요, 각 문장이 최신 커밋 데이터를 보는 것으로 충분함","next":"sol_read_committed"},{"label":"예, 트랜잭션 전체에 일관된 스냅샷이 필요함","next":"consistent"}]},"consistent":{"kind":"step","title":"동시 트랜잭션이 논리적으로 완전히 순서화되어야 하는가?","detail":"일관된 읽기를 원한다. 그것으로 충분한가, 아니면 동시 트랜잭션이 하나씩 순차 실행된 것과 같은 결과를 내야 하는가(serializable)? write skew 같은 미묘한 이상 현상(예: 두 트랜잭션이 서로 다른 행을 읽고 서로를 무효화하는 방식으로 쓰는 경우)을 허용할 수 있다면 Snapshot으로 충분하고, 그렇지 않으면 완전한 직렬화가 필요하다.","choices":[{"label":"아니요, 일관된 스냅샷으로 충분함","next":"sol_repeatable_read"},{"label":"예, 어떤 동시성 이상 현상도 허용 불가","next":"sol_serializable"}]},"sol_read_uncommitted":{"kind":"solution","title":"권장: Read Uncommitted(특수한 경우만)","detail":"가장 낮은 격리 수준으로 dirty read를 허용한다. 락 대기가 거의 없이 읽지만 롤백될 수 있는 데이터를 볼 수 있다. 범용 애플리케이션에는 권장하지 않는다.","steps":["이유: 읽는 쪽이 쓰는 쪽의 락을 기다리지 않는다. 동시성은 최대지만 일관성은 보장되지 않는다.","적용 시점: 정확도가 중요하지 않은 대략적인 지표/진행률 집계 같은 경우에만.","주의: PostgreSQL에서는 이 수준이 Read Committed처럼 동작하며, Oracle은 Read Uncommitted를 아예 제공하지 않는다. 실제 dirty read는 주로 락 기반 SQL Server(NOLOCK)에서 발생한다.","주의: '속도를 위한 NOLOCK'을 습관화하지 않는다. 잘못된/중복된/누락된 행을 읽을 위험이 있다."]},"sol_read_committed":{"kind":"solution","title":"권장: Read Committed(기본 OLTP)","detail":"각 문장은 해당 시점에 커밋된 최신 데이터를 보며 dirty read가 방지된다. 대부분의 엔진에서 기본값이며 일반적인 OLTP 워크로드에 최적의 동시성/일관성 균형을 제공한다.","steps":["이유: 트랜잭션이 불필요하게 오래 락을 유지하도록 강제하지 않으면서 dirty read를 방지하고, 높은 동시성을 제공한다.","적용 시점: 짧고 목적이 분명한 OLTP 트랜잭션, 대다수 웹 애플리케이션에 적합한 기본값.","예: PostgreSQL, SQL Server, Oracle의 기본값은 Read Committed다(Oracle에서는 문장 수준 스냅샷으로 일관성을 제공).","주의: 같은 트랜잭션 내 두 번의 읽기가 서로 다른 결과를 반환할 수 있다(non-repeatable/phantom). 일관된 보고가 필요한 로직의 기반으로 삼지 않는다.","주의: MySQL/InnoDB의 기본값은 Repeatable Read다. 마이그레이션 시 엔진 간 차이에 유의한다."]},"sol_repeatable_read":{"kind":"solution","title":"권장: Repeatable Read / Snapshot(일관된 읽기)","detail":"트랜잭션은 시작 시점(또는 첫 읽기 시점)에 고정된 일관된 스냅샷에서 동작하며, 트랜잭션 동안 읽기 결과가 바뀌지 않는다. MVCC에서는 읽기가 쓰기를, 쓰기가 읽기를 블록하지 않는다.","steps":["이유: 트랜잭션 전체에 고정된 뷰를 제공해 non-repeatable read(MVCC 엔진에서는 실질적으로 phantom도)를 방지하고, 읽기 중심의 보고서/계산 로직을 일관되게 만든다.","적용 시점: 한 트랜잭션에서 같은 데이터를 여러 번 읽고 일관된 결과가 필요한 보고서, 대사(reconciliation), 배치 계산.","예: MySQL/InnoDB 기본값 Repeatable Read, SQL Server SNAPSHOT(row versioning), PostgreSQL REPEATABLE READ, Oracle SERIALIZABLE은 실제로는 스냅샷 시맨틱이다.","주의: write skew 같은 직렬화 이상 현상은 방지하지 않는다. 업무 규칙상 필요하면 Serializable로 올린다.","주의: Snapshot/row versioning은 tempdb 또는 undo/version store 증가를 유발할 수 있다. 긴 트랜잭션은 오래된 버전을 비대하게 만든다."]},"sol_serializable":{"kind":"solution","title":"권장: Serializable(완전한 격리)","detail":"가장 높은 격리 수준: 동시 트랜잭션이 직렬로(하나씩 순차적으로) 실행된 것과 같은 결과를 낸다. 모든 읽기 이상 현상과 write skew를 방지하며, 대가로 충돌이 늘고 재시도가 필요하다.","steps":["이유: 결과가 항상 유효한 직렬 실행과 동등함을 보장하며 모든 동시성 이상 현상을 제거한다.","적용 시점: 정확성이 핵심인 불변 조건을 가진 로직: 잔액/재고 확인, 이중 예약이 절대 발생해서는 안 되는 경우.","예: PostgreSQL SSI (Serializable Snapshot Isolation), SQL Server SERIALIZABLE(range lock). Oracle에서 진정한 직렬화는 애플리케이션 수준에서 처리한다.","주의: 직렬화 실패(예: PG의 40001)는 예상된 동작이다. 애플리케이션에서 트랜잭션을 자동으로 재실행하는 재시도 로직이 필수다.","주의: 충돌이 많은 부하에서는 처리량이 떨어질 수 있다. 모든 곳이 아니라 정확성이 핵심인 트랜잭션에만 적용한다."]}}},"solver/decide-sqlnosql.json":{"id":"decide-sqlnosql","title":"결정: SQL인가 NoSQL인가(어떤 데이터베이스 계열인가)?","icon":"🧭","category":"의사결정 가이드","intro":"올바른 데이터베이스 계열 선택은 데이터 모델의 형태, 일관성과 확장성의 우선순위, 쿼리 복잡도로 결정된다. 'NoSQL이 더 현대적'이라는 식의 일반화는 오해를 낳는다. 각 계열은 특정 접근 패턴을 위해 설계되었다. 이 트리는 데이터 구조에서 출발해 관계형, 문서형, 키-값, 검색 엔진 중 올바른 도구로 안내하며, 필요 시 폴리글랏 접근 방식을 상기시킨다.","start":"root","nodes":{"root":{"kind":"step","title":"데이터의 성격과 지배적인 접근 패턴은 무엇인가?","detail":"가장 결정적인 질문부터 시작한다: 데이터가 강한 관계를 가진 구조화된 레코드인가, 유연/가변 스키마의 문서인가, 단순 키-값 조회인가, 아니면 텍스트 검색 같은 특수한 요구인가? 데이터의 형태와 조회 방식이 계열을 대부분 결정한다.","choices":[{"label":"관계형, 구조화된 레코드와 복잡한 쿼리","next":"relational_check"},{"label":"유연/가변 스키마의 중첩 문서","next":"sol_document"},{"label":"단순 키-값, 매우 낮은 지연/캐싱","next":"sol_keyvalue"},{"label":"전문(full-text) / 패싯 검색, 로그 분석","next":"sol_search"}]},"relational_check":{"kind":"step","title":"강한 일관성(ACID)과 수평 확장 중 무엇이 우선인가?","detail":"데이터는 관계형으로 보인다. 그렇다면 우선순위는 무엇인가? 다중 테이블 조인, 트랜잭션 무결성, 강한 일관성(ACID)이 필수인가, 아니면 단일 노드에 담기지 않는 대용량을 수평 확장하는 것인가? 관계형 모델은 대부분의 워크로드에 올바른 기본값이며, 확장 규모가 실제로 단일 노드를 넘어설 때만 한계에 부딪힌다.","choices":[{"label":"ACID, 조인, 복잡한 쿼리 무결성이 우선","next":"sol_relational"},{"label":"무엇보다 확장성, 약한 관계/조인","next":"sol_document"}]},"sol_relational":{"kind":"solution","title":"권장: 관계형 데이터베이스(RDBMS)","detail":"강한 관계를 가진 구조화된 데이터와 트랜잭션 무결성이 필요한 워크로드에 올바른 기본값이다. ACID 보장, 성숙한 SQL 쿼리 엔진, 조인을 통해 복잡한 질의에 안정적으로 응답한다.","steps":["이유: ACID 트랜잭션, 외래 키 무결성, 강한 일관성과 함께 여러 테이블을 조인하는 복잡한 쿼리를 최적화하는 성숙한 옵티마이저를 제공한다.","적용 시점: 금융, 주문/재고, ERP/CRM처럼 관계와 일관성이 핵심인 OLTP 및 보고 워크로드.","예: PostgreSQL, MySQL, Oracle, SQL Server. 시계열 요구에는 PostgreSQL + TimescaleDB 같은 확장.","주의: 스키마 변경(마이그레이션)에는 규율이 필요하다. 스키마 변경이 잦고 예측하기 어렵다면 문서 모델을 검토한다.","주의: 단일 노드의 쓰기/데이터 한계에 도달하면 읽기 복제본, 파티셔닝, 필요 시 샤딩으로 확장한다."],"related":["create-partition"]},"sol_document":{"kind":"solution","title":"권장: 문서 데이터베이스","detail":"자주 변경되는 스키마를 가진 유연한 중첩 데이터를 자연스러운 형태(JSON 유사 문서)로 저장한다. 주로 단일 문서 단위로 읽기/쓰기하고, 조인이 적으며, 수평 확장이 필요한 애플리케이션에 적합하다.","steps":["이유: 스키마가 유연하다. 문서는 필드를 추가하거나 제거할 수 있고, 중첩 구조를 한 번의 읽기로 가져오며, 수평 확장(샤딩)에 적합하다.","적용 시점: 카탈로그, 사용자 프로필, 콘텐츠/CMS, 이벤트 기록처럼 문서 중심이고 구조가 가변적이며 빠르게 진화하는 데이터 모델.","예: MongoDB, Couchbase. 관계형 진영에서는 JSONB(PostgreSQL)가 제한적으로 유사한 유연성을 제공한다.","주의: 복잡한 다중 문서 조인과 보고 기능이 약하다. 관계가 지배적이라면 관계형 모델이 더 적합하다.","주의: 일관성과 스키마 검증의 책임이 상당 부분 애플리케이션으로 넘어간다. 코드에서 데이터 품질을 보호한다."]},"sol_keyvalue":{"kind":"solution","title":"권장: 키-값 저장소 / 캐시","detail":"키 기반의 매우 빠른 읽기/쓰기가 필요하고 복잡한 쿼리가 필요 없는 워크로드를 위해 설계되었다. 대개 메모리에서 동작하며 마이크로초/밀리초 지연을 제공한다. 캐싱, 세션, 카운터 같은 작업에 이상적이다.","steps":["이유: 단순 키 접근에 대해 극히 낮은 지연과 높은 처리량을 제공하며, 인메모리 구조로 관계형 데이터베이스의 부하를 덜어준다.","적용 시점: 캐시 계층, 세션 저장소, rate-limit 카운터, 리더보드, 임시 큐/pub-sub.","예: Redis, Memcached. Redis는 lists/sets/sorted sets 같은 풍부한 자료구조와 영속성 옵션도 제공한다.","주의: 대개 단일 원본 데이터 저장소가 아니다. 원본 데이터는 내구성 있는 데이터베이스에 두고 파생/캐시 데이터를 여기에 저장한다.","주의: 메모리는 한정적이고 비싸다. eviction 정책, TTL, 영속성/내구성 설정을 업무 요구에 맞게 계획한다."]},"sol_search":{"kind":"solution","title":"권장: 검색 / 분석 엔진","detail":"전문 검색, 관련도 순위, 패싯 필터링, 대용량 로그/텍스트 분석에 특화된 역색인(inverted index) 기반 엔진이다. 관계형 LIKE 쿼리로는 불가능한 검색 경험을 제공한다.","steps":["이유: 역색인과 스코어링으로 전문 검색, 자동 완성, 패싯, 집계를 고성능으로 수행한다.","적용 시점: 상품/사이트 검색, 로그 및 관측성 분석, 관련도 순위 검색 경험이 필요한 애플리케이션.","예: Elasticsearch/OpenSearch, Apache Solr. 중간 규모의 전문 검색에는 PostgreSQL tsvector로 충분할 수 있다.","주의: 대개 주 원본 시스템이 아니다. 주 데이터는 관계형/문서 DB에 두고 거기서 검색 인덱스로 공급한다(CDC/sync).","주의: 실시간 강한 일관성 보장이 약하다(near-real-time 인덱싱). 설계 시 인덱싱 지연을 고려한다."],"related":["setup-cdc-replication"]}}},"solver/decide-shard-replica.json":{"id":"decide-shard-replica","title":"결정: 샤딩인가 복제인가(확장)?","icon":"🧭","category":"의사결정 가이드","intro":"데이터베이스 확장 결정은 병목이 읽기인지 쓰기인지, 데이터가 단일 노드에 담기는지에 따라 달라진다. 잘못된 전략은 불필요한 복잡성이나 해결되지 않은 병목을 낳는다. 이 트리는 먼저 가장 저렴하고 단순한 옵션(수직 확장, 캐싱)을 상기시킨 뒤 읽기 복제본과 샤딩 중 올바른 선택을 돕는다.","start":"root","nodes":{"root":{"kind":"step","title":"가장 단순한 방법인 수직 확장과 쿼리 튜닝을 먼저 시도했는가?","detail":"분산 아키텍처로 넘어가기 전에 가장 저렴한 개선부터 취한다: 하드웨어(CPU/RAM/IOPS) 증설, 누락된 인덱스 추가, 비용이 큰 쿼리 수정으로 대부분의 병목이 해결된다. 이것들이 소진되었을 때만 복잡성을 더한다.","choices":[{"label":"아니요 / 아직 한계에 도달하지 않음","next":"sol_vertical"},{"label":"예, 수직 확장과 튜닝으로는 부족함","next":"bottleneck"}]},"bottleneck":{"kind":"step","title":"병목은 읽기인가, 아니면 쓰기/데이터 양인가?","detail":"확장 문제의 원인을 분리한다: 많은 읽기 쿼리가 단일 노드를 압박하는가(읽기 중심), 아니면 쓰기 속도나 전체 데이터 크기가 단일 노드의 용량을 넘는가(쓰기/용량 중심)? 이 구분이 복제본과 샤딩의 근본적인 차이다.","choices":[{"label":"읽기 트래픽이 지배적, 쓰기는 단일 노드로 충분","next":"read_path"},{"label":"쓰기 속도 또는 데이터 양이 단일 노드를 초과","next":"sol_sharding"}]},"read_path":{"kind":"step","title":"읽기가 반복적/캐시 가능한가, 아니면 최신이고 다양한가?","detail":"읽기 병목을 한 단계 더 나눈다: 같은 결과를 반복해서 읽는가(캐시가 매우 효과적), 아니면 다양하고 광범위한 최신 데이터를 요청하는가(추가 읽기 용량 필요)? 이에 따라 캐시 계층과 읽기 복제본 중 선택이 결정된다.","choices":[{"label":"같은 데이터를 반복해서 읽음, 약간의 지연된 데이터는 허용","next":"sol_cache"},{"label":"다양한/최신 읽기, 순수 읽기 용량이 필요","next":"sol_read_replica"}]},"sol_vertical":{"kind":"solution","title":"권장: 수직 확장 + 쿼리 튜닝 우선","detail":"분산 아키텍처로 넘어가기 전 첫 번째이자 가장 저렴한 방법이다. 하드웨어 증설과 쿼리/인덱스 수정으로 운영 복잡성을 더하지 않고 대부분의 확장 문제를 해결한다.","steps":["이유: 복잡성과 위험이 가장 낮다. 단일 노드에 머무르면 트랜잭션/일관성 모델이 바뀌지 않고 애플리케이션 코드를 분할할 필요도 없다.","적용 시점: 아직 단일 노드의 하드웨어 한계에 도달하지 않았거나, 병목이 실제로는 잘못된 쿼리/누락된 인덱스에서 오는 경우.","단계: 느린 쿼리를 프로파일링하고, 누락된 인덱스를 추가하며, 실행 계획/통계를 수정한 뒤 CPU/RAM/IOPS를 증설한다.","주의: 수직 확장에는 한계가 있고 비용이 기하급수적으로 증가한다. 단일 노드는 여전히 단일 장애 지점이다(HA용 복제본은 별도로 구성).","주의: 한계에 가까워지면 다음 단계(복제본/샤딩)를 미리 계획한다. 서두른 마이그레이션은 위험하다."]},"sol_read_replica":{"kind":"solution","title":"권장: 읽기 복제본(읽기 확장)","detail":"쓰기는 단일 노드(primary)에 유지하고 읽기 트래픽은 하나 이상의 복제본으로 분산한다. 읽기 중심 워크로드에서 순수 읽기 용량과 복원력을 높이는 표준 방법이다.","steps":["이유: 읽기 쿼리가 여러 사본에 분산된다. primary는 쓰기만 처리하고 읽기 처리량은 거의 선형으로 증가한다.","적용 시점: 읽기/쓰기 비율이 높은 워크로드, 보고 및 분석 쿼리를 primary에서 분리할 때, 동시에 HA standby를 확보할 때.","예: PostgreSQL streaming 복제본, MySQL 복제본 + 프록시 라우팅, 애플리케이션의 읽기/쓰기 분리(read-write split).","주의: 비동기 복제본에서는 복제 지연으로 read-your-writes 문제가 발생한다(방금 쓴 내용을 즉시 읽을 수 없음). 중요한 읽기는 primary로 라우팅한다.","주의: 복제본은 쓰기 병목을 해결하지 않는다. 단일 쓰기 노드가 여전히 한계다 — 쓰기도 병목이라면 샤딩이 필요하다."],"related":["replication-status"]},"sol_cache":{"kind":"solution","title":"권장: 캐시 계층","detail":"자주 반복해서 읽는 결과를 빠른 인메모리 계층에 유지하면 데이터베이스 부하가 크게 줄어든다. 적절한 패턴을 사용하면 데이터베이스에 가지 않고 응답해 지연과 부하를 모두 낮춘다.","steps":["이유: 반복 읽기가 데이터베이스에 도달하지 않고 메모리 속도로 응답된다. 효과 대비 비용이 가장 좋은 읽기 확장 방법이다.","적용 시점: 같은 데이터를 여러 번 읽고 약간 지연된 데이터가 허용되는 경우. 예: 상품 상세, 설정, 참조 데이터.","예: Redis/Memcached를 이용한 cache-aside 패턴. 애플리케이션은 먼저 캐시를 조회하고 miss 시 데이터베이스로 간다.","주의: 캐시 무효화는 어렵다. TTL과 쓰기 시 갱신/삭제 전략을 명시적으로 설계한다.","주의: 강한 일관성이 필요한 읽기(금액/재고)에는 오래된 캐시에 의존하지 않는다. 해당 읽기는 원본에서 제공한다."]},"sol_sharding":{"kind":"solution","title":"권장: 샤딩(쓰기/데이터 확장)","detail":"데이터를 샤드 키로 분할해 여러 독립 노드에 분산한다. 각 노드는 데이터 일부와 그에 대한 쓰기를 담당한다. 쓰기 속도와 전체 데이터 양을 확장하는 방법이지만 상당한 복잡성이 추가된다.","steps":["이유: 쓰기와 스토리지가 여러 노드에 분산된다. 단일 쓰기 노드 및 단일 노드 데이터 한계가 사라지고 수평 확장이 가능해진다.","적용 시점: 쓰기 처리량이나 데이터 크기가 단일 노드(및 읽기 복제본)로 더 이상 감당되지 않고 다른 선택지가 남지 않은 경우.","예: 애플리케이션 계층의 샤드 라우팅, Vitess (MySQL), Citus (PostgreSQL), 문서 DB의 내장 샤딩.","주의: 샤드 키 선택은 핵심적이며 되돌리기 어렵다. 잘못된 키는 핫스팟과 불균등한 분산을 만든다. 접근 패턴에 맞는 높은 카디널리티의 키를 선택한다.","주의: 샤드 간 조인, 트랜잭션, 고유성 보장이 어려워진다. 애플리케이션 복잡성, 재분산(resharding), 운영 부담이 크게 증가한다."],"related":["create-partition"]}}},"solver/decide-backup.json":{"id":"decide-backup","title":"결정: 어떤 백업 전략인가?","icon":"🧭","category":"의사결정 가이드","intro":"백업 전략은 세 가지 요소의 균형으로 선택한다: 허용 가능한 데이터 손실(RPO), 데이터 크기, 복구 속도(RTO). 모든 시나리오에 맞는 단일 방법은 없다. 이 트리는 RPO 목표에서 출발해 논리 덤프, 물리 백업 + PITR, 스냅샷 중 선택을 안내하며, 3-2-1과 검증은 타협할 수 없음을 상기시킨다.","start":"root","nodes":{"root":{"kind":"step","title":"데이터 손실 목표(RPO)는 무엇인가?","detail":"가장 결정적인 제약 조건부터 시작한다: 재해 시 몇 분/몇 시간 분량의 데이터 손실을 감수할 수 있는가? 거의 제로에 가까운 데이터 손실이 요구되면 연속 로그 아카이빙(PITR)이 필수이고, 일일/주간 백업 수준이 허용되면 더 단순한 방법으로 충분하다. RPO가 전략의 틀을 정한다.","choices":[{"label":"거의 제로 — 임의 시점으로 복구할 수 있어야 함","next":"sol_physical_pitr"},{"label":"일일/주기적 백업(몇 시간의 손실) 허용","next":"size"},{"label":"먼저 일반적인 모범 사례(3-2-1 / 검증)를 알려 달라","next":"sol_321"}]},"size":{"kind":"step","title":"데이터 크기와 이식성 요구 사항은 무엇인가?","detail":"주기적 백업으로 충분하다면 다음 질문은 규모와 목적이다: 데이터베이스가 소/중규모이고 백업을 다른 환경/버전으로 이식하고 싶은가, 아니면 데이터가 크고 몇 분 내 복구가 주된 관심사인가? 이에 따라 논리 덤프와 스냅샷 중 선택이 결정된다.","choices":[{"label":"소/중규모, 이식 가능하고 버전 간 호환","next":"sol_logical"},{"label":"대용량 데이터, 복구가 매우 빨라야 함(낮은 RTO)","next":"sol_snapshot"}]},"sol_logical":{"kind":"solution","title":"권장: 논리 덤프","detail":"데이터를 논리 수준(스키마 정의 + 행)으로 내보낸다. 출력은 SQL/텍스트(pg_dump plain 형식, mysqldump) 또는 도구별 바이너리 덤프(pg_dump -Fc, Oracle Data Pump)다. 소/중규모 데이터베이스, 마이그레이션, 버전 업그레이드, 선택한 테이블/객체의 복구에 유연하고 이식성 있는 방법이다.","steps":["이유: 출력은 이식 가능하며(텍스트 형식이면 읽기도 가능) 버전/엔진 간 이동과 단일 테이블/객체 복구에 적합하다.","적용 시점: 소/중규모 데이터, 버전 업그레이드/마이그레이션, 개발 환경 복사, 논리(행 수준) 복구가 필요한 경우.","예: PostgreSQL pg_dump/pg_dumpall, MySQL mysqldump, Oracle Data Pump(expdp/impdp; 바이너리 .dmp 생성, DDL을 텍스트로 보려면 impdp SQLFILE= 사용).","주의: 복구 시 데이터를 다시 처리하므로 대용량 데이터베이스에서는 백업과 복구 모두 매우 느리다(높은 RTO).","주의: 단독으로는 낮은 RPO를 제공하지 않는다. 특정 시점 복구에는 물리 백업 + 로그 아카이브가 필요하다. 일관된 스냅샷을 위해 적절한 일관성 옵션을 사용한다."],"related":["logical-dump","last-backup-status"]},"sol_physical_pitr":{"kind":"solution","title":"권장: 물리 백업 + WAL/아카이브 로그(PITR)","detail":"정기적으로 물리 base backup을 수행하고 트랜잭션 로그(WAL/redo/binlog)를 연속 아카이빙한다. 이를 통해 임의 시점으로 Point-in-Time Recovery를 수행하고 데이터 손실을 거의 제로로 줄일 수 있다.","steps":["이유: 연속 로그 아카이빙 덕분에 마지막 base backup 이후의 어떤 트랜잭션까지든 롤포워드해 원하는 시점(예: 잘못된 명령 직전)으로 복구할 수 있다 — 가장 낮은 RPO.","적용 시점: 대용량의 핵심 운영 데이터베이스, 낮은 RPO와 합리적인 RTO가 필요한 시스템.","예: PostgreSQL base backup + WAL archive (pgBackRest/barman), Oracle RMAN + archivelog, MySQL 물리 백업 + binlog (xtrabackup).","주의: 구성과 운영이 더 복잡하다. 아카이브 로그가 중단이나 누락 없이 흐르는지 지속적으로 모니터링한다 — 하나의 누락이 PITR 체인을 끊는다.","주의: 로그 아카이브 저장소는 계속 증가한다. 보존 정책과 스토리지 계획이 없으면 디스크가 가득 찬다."],"related":["last-backup-status","logical-dump"]},"sol_snapshot":{"kind":"solution","title":"권장: 스냅샷","detail":"스토리지 또는 가상화 계층에서 데이터의 블록 수준 스냅샷을 생성한다. 매우 큰 데이터베이스에서도 백업과 복구가 매우 빠르지만, 인프라에 의존하며 신중한 사용이 필요하다.","steps":["이유: 블록 수준 사본이 거의 즉시 생성되고 복구가 매우 빠르다. 대용량 데이터에서 가장 낮은 RTO를 제공한다.","적용 시점: 테라바이트급 데이터, 빠른 복구가 우선이며 스토리지/클라우드 계층이 스냅샷을 지원하는 경우.","예: SAN/LVM 스냅샷, 클라우드 디스크 스냅샷(EBS 등), ZFS/파일 시스템 스냅샷. 데이터베이스 엔진과 일관되게 트리거한다.","주의: 스냅샷은 스토리지 시스템에 의존하며 대개 같은 인프라에 존재한다. 단독으로는 3-2-1을 충족하지 않으므로 오프사이트로 복사한다.","주의: 애플리케이션 일관성을 확보하려면 스냅샷 시점에 데이터베이스가 적절한 모드(fsfreeze/backup mode/hot backup)여야 한다. 그렇지 않으면 'crash-consistent' 백업이 된다."],"related":["last-backup-status"]},"sol_321":{"kind":"solution","title":"권장: 3-2-1 규칙 + 오프사이트 + 필수 검증","detail":"어떤 방법을 선택하든 이 원칙은 전략의 상위에 있어야 한다. 백업이 존재하는 것만으로는 부족하며, 다른 매체와 다른 위치에 있고 정기적으로 테스트되어야 한다. 위의 모든 옵션을 보완하는 프레임워크다.","steps":["3-2-1: 최소 3개의 사본을 2가지 다른 매체/유형에 보관하고, 1개 사본은 오프사이트에 둔다.","오프사이트가 필요한 이유: 화재, 홍수, 랜섬웨어, 리전 손실로 한 위치의 모든 사본이 동시에 파괴될 수 있다. 최소 하나의 사본은 격리/원격지에 있어야 한다.","검증은 필수: 테스트하지 않은 백업은 백업이 아니다. 실제 복구 훈련을 정기적으로 수행하고 복구 시간(RTO)을 측정한다.","불변성: 랜섬웨어에 대비해 삭제/수정 불가(immutable/WORM) 또는 air-gapped 사본을 유지한다.","주의: 백업 무결성(checksum)과 복구 가능성을 자동으로 모니터링하고, 실패/오래된 백업에 대한 알림을 설정한다. 백업 자체도 암호화한다."],"related":["last-backup-status"]}}},"solver/decide-index.json":{"id":"decide-index","title":"결정: 어떤 인덱스 유형인가?","icon":"🧭","category":"의사결정 가이드","intro":"올바른 인덱스 유형은 쿼리의 접근 패턴(동등, 범위, 텍스트/JSON 검색), 컬럼의 카디널리티, 읽기/쓰기 균형에 따라 달라진다. 잘못된 인덱스는 공간을 낭비하고 쓰기도 느리게 만든다. 이 트리는 쿼리 형태에서 출발해 B-tree, Hash, Bitmap, GIN/GiST, 부분, 커버링 인덱스 중 선택을 안내하며, 복합 인덱스에서는 컬럼 순서가 핵심임을 상기시킨다.","start":"root","nodes":{"root":{"kind":"step","title":"쿼리가 이 컬럼을 어떻게 사용하는가?","detail":"접근 패턴은 인덱스 선택에서 가장 결정적인 요소다. 컬럼을 '='로만 비교하는가, 범위/정렬(>, <, BETWEEN, ORDER BY)을 수행하는가, 아니면 전문, JSON 필드, 기하 데이터/배열을 검색하는가? 여기서 시작하며, 요구가 명확해지기 전에는 인덱스 유형을 선택하지 않는다.","choices":[{"label":"동등과 범위/정렬 혼합(=, >, <, BETWEEN, ORDER BY)","next":"cardinality"},{"label":"동등(=)만, 범위나 정렬 없음","next":"sol_hash"},{"label":"전문, JSON/JSONB 필드, 배열 또는 기하 검색","next":"sol_gin_gist"},{"label":"쿼리가 항상 같은 작은 행 부분집합을 대상으로 함(예: WHERE status='active')","next":"sol_partial"}]},"cardinality":{"kind":"step","title":"컬럼의 카디널리티(고유 값 수)는 어느 정도인가?","detail":"범위/정렬이 필요하면 기본값은 B-tree다. 다만 읽기 중심 데이터 웨어하우스에서 고유 값이 매우 적은 컬럼(낮은 카디널리티: 성별, 상태, 예/아니요)에는 Bitmap 인덱스가 더 나을 수 있다. 컬럼에 고유 값이 많은가, 아니면 반복되는 소수의 값인가?","choices":[{"label":"높은 카디널리티(고유 값이 많음) 또는 OLTP 워크로드","next":"coverage"},{"label":"낮은 카디널리티이며 읽기 중심 / 데이터 웨어하우스(DWH)","next":"sol_bitmap"}]},"coverage":{"kind":"step","title":"쿼리가 인덱스에 있는 컬럼만 읽는가?","detail":"접근 패턴은 B-tree에 적합하다. 마지막 최적화: 쿼리가 반환하고 필터링하는 모든 컬럼이 인덱스에 있으면 엔진은 테이블을 방문하지 않고 인덱스만으로 응답할 수 있다(index-only / covering scan). 쿼리가 몇 개의 컬럼만 읽는가, 아니면 행의 대부분이 필요한가?","choices":[{"label":"예, 몇 개 컬럼만; 테이블 방문 없이 응답 가능","next":"sol_covering"},{"label":"아니요, 표준 접근으로 충분함","next":"sol_btree"}]},"sol_btree":{"kind":"solution","title":"권장: B-tree(기본)","detail":"B-tree는 동등, 범위, 정렬(ORDER BY), LIKE 'abc%' 접두사 매칭, MIN/MAX 등 폭넓은 접근 패턴을 지원하는 균형 트리 구조다. 대부분의 관계형 엔진에서 기본 인덱스 유형이며, 확신이 없을 때 올바른 출발점이다.","steps":["이유: 하나의 구조로 동등과 범위/정렬을 모두 효율적으로 처리한다. 가장 범용적인 인덱스다.","적용 시점: OLTP 쿼리, WHERE + 범위 + ORDER BY 조합, 기본/고유 키.","복합 인덱스에서는 컬럼 순서가 핵심이다: 동등 조건으로 사용되는 선택도가 가장 높은 컬럼을 앞에 두고 범위 컬럼은 마지막에 둔다. (a,b) 인덱스는 'a' 또는 'a,b' 필터에 도움이 되지만, 'b'만의 필터에는 대개 도움이 되지 않는다(leftmost-prefix 규칙).","예: PostgreSQL/Oracle/MySQL(InnoDB)/MSSQL 모두의 기본 B-tree/B+tree 인덱스.","주의: 모든 인덱스는 쓰기(INSERT/UPDATE/DELETE) 비용과 디스크 공간을 늘린다. 불필요하거나 중복된 인덱스를 유지하지 않는다."],"related":["slow-query"]},"sol_hash":{"kind":"solution","title":"권장: Hash 인덱스(동등 조건 전용)","detail":"Hash 인덱스는 컬럼 값의 해시를 저장하며 정확한 동등('=') 조회에만 매우 빠르다. 범위, 정렬, 접두사 검색을 지원하지 않으므로 사용 범위가 좁다.","steps":["이유: 동등 조회에 평균 상수 시간(O(1)) 접근을 제공한다. 메모리에 들어가는 매우 큰 동등 조회 워크로드에서 유리할 수 있다.","적용 시점: 컬럼이 '='로만 사용되고 범위/정렬이 전혀 필요 없는 경우.","엔진별 차이: PostgreSQL에는 hash 인덱스가 있지만 실제로는 대부분 B-tree로 충분하고 더 유연하다. MySQL(InnoDB)은 hash 인덱싱을 adaptive hash index로 내부에서만 관리하며, MEMORY 엔진은 hash를 명시적으로 사용한다. Oracle에서는 cluster/hash cluster 맥락에서 등장한다.","주의: 범위, BETWEEN, ORDER BY, LIKE 'abc%'는 지원되지 않는다. 이런 요구가 생기면 B-tree를 선택한다.","주의: 기본 B-tree로 대개 충분하므로 측정 없이 hash로 전환하지 않는다."],"related":["slow-query"]},"sol_bitmap":{"kind":"solution","title":"권장: Bitmap 인덱스(낮은 카디널리티 / DWH)","detail":"Bitmap 인덱스는 각 고유 값마다 비트 배열을 유지하며, 여러 낮은 카디널리티 조건을 비트 단위 AND/OR로 매우 빠르게 결합한다. 데이터 웨어하우스와 보고처럼 읽기 중심 워크로드에 강하다.","steps":["이유: 고유 값이 적은 컬럼에서 공간을 매우 적게 차지하며, 여러 조건(WHERE gender=.. AND region=..)을 bitmap 연산으로 효율적으로 결합한다.","적용 시점: 낮은 카디널리티 컬럼, 스타 스키마 쿼리, 데이터 웨어하우스/보고, 읽기 중심 워크로드.","엔진별 차이: Oracle의 대표적인 강점이다. PostgreSQL에는 영구 bitmap 인덱스가 없지만 옵티마이저가 런타임에 'bitmap heap scan'을 만들 수 있다. MySQL/MSSQL에는 영구 bitmap 인덱스 자체가 없다.","주의: 자주 갱신되는(OLTP) 테이블에는 사용하지 않는다. bitmap 락이 행 수준 동시성을 크게 떨어뜨린다.","주의: 높은 카디널리티 컬럼에서는 이점이 사라진다. 그런 경우 B-tree를 사용한다."],"related":["slow-query"]},"sol_gin_gist":{"kind":"solution","title":"권장: GIN / GiST(텍스트, JSON, 배열, 기하)","detail":"PostgreSQL에는 표준 B-tree가 처리할 수 없는 접근 유형을 위한 특수 인덱스 계열이 있다. GIN은 하나의 값 안에 있는 여러 구성 요소(단어, JSONB 키, 배열 요소)를 가리키고, GiST는 기하/범위/최근접 이웃 같은 쿼리에 적합하다.","steps":["이유: 이러한 데이터 유형에서 '포함/겹침/근접' 같은 쿼리는 B-tree로 효율적으로 처리할 수 없다. GIN/GiST가 이를 가속한다.","GIN 적용 시점: 전문 검색(tsvector), JSONB 필드/키 조회(@>, ?), 배열 포함 쿼리. 읽기가 많고 쓰기가 적은 경우.","GiST 적용 시점: 기하/지리 공간(PostGIS), 범위 유형, 최근접 이웃(KNN) 쿼리.","엔진별 차이: GIN/GiST는 PostgreSQL 전용이다. MySQL의 FULLTEXT 및 SPATIAL (R-tree) 인덱스, Oracle의 Oracle Text와 Spatial, MSSQL의 Full-Text Index가 유사한 목적을 수행한다.","주의: GIN은 쓰기 비용이 크고 생성이 느리다(fastupdate로 완화 가능). 쓰기가 많은 테이블에서는 비용을 측정한다."],"related":["slow-query"]},"sol_partial":{"kind":"solution","title":"권장: 부분 / 필터링된 인덱스","detail":"부분 인덱스는 테이블 전체가 아니라 조건(WHERE ...)을 만족하는 행만 인덱싱한다. 쿼리가 항상 같은 작은 부분집합을 대상으로 하면 인덱스가 훨씬 작고 빨라지며 쓰기 비용도 줄어든다.","steps":["이유: 관련 행만 인덱싱하므로 공간과 유지 비용이 모두 줄고, 자주 조회되는 '핫' 부분집합이 훨씬 빨라진다.","적용 시점: WHERE status='active', deleted_at IS NULL, priority='high' 같은 안정적이고 좁은 조건.","엔진별 차이: PostgreSQL은 'partial index'(CREATE INDEX ... WHERE ...)로, MSSQL은 'filtered index'로 직접 지원한다. Oracle에서는 함수 기반/조건부 인덱스 패턴으로 동등한 효과를 낸다. MySQL(InnoDB)은 직접 지원하지 않는다.","주의: 인덱스의 WHERE 조건이 쿼리 조건과 일치해야 한다. 그렇지 않으면 옵티마이저가 인덱스를 사용할 수 없다.","주의: 부분집합이 커지면 이점이 줄어든다. 조건이 안정적인지 확인한다."],"related":["slow-query"]},"sol_covering":{"kind":"solution","title":"권장: 커버링(INCLUDE) 인덱스","detail":"커버링 인덱스는 쿼리에 필요한 모든 컬럼을 포함해 엔진이 테이블(heap)을 전혀 방문하지 않고 인덱스만으로 응답하게 한다(index-only scan). 적은 컬럼을 반환하는 자주 실행되는 쿼리에서 큰 속도 향상을 제공한다.","steps":["이유: 테이블 접근(heap fetch)이 제거되어 디스크 I/O와 지연이 눈에 띄게 줄어든다.","적용 시점: WHERE + SELECT 컬럼이 명확하고 결과 집합이 좁은, 자주 실행되는 쿼리.","엔진별 차이: MSSQL과 PostgreSQL은 'INCLUDE'로 키가 아닌 컬럼을 인덱스 리프에 추가한다. MySQL(InnoDB)에서는 보조 인덱스가 이미 기본 키를 포함하므로 복합 인덱스로 커버링을 구현한다. Oracle에서는 필요한 모든 컬럼이 인덱스에 있을 때 인덱스만으로 접근한다.","주의: 추가/넓은 INCLUDE 컬럼은 인덱스를 비대하게 하고 쓰기를 느리게 한다. 꼭 필요한 컬럼만 추가한다.","주의: PostgreSQL에서 index-only scan에는 최신 visibility map이 필요하다. 정기적인 VACUUM이 필수다."],"related":["slow-query"]}}},"solver/decide-partition.json":{"id":"decide-partition","title":"결정: 어떤 파티셔닝 전략인가?","icon":"🧭","category":"의사결정 가이드","intro":"파티셔닝은 큰 테이블을 하나의 논리적 이름 아래 여러 물리적 조각으로 나누어 오래된 데이터 삭제를 쉽게 하고, 쿼리가 관련 조각만 보도록 한다(partition pruning). 그러나 작은 테이블에서는 복잡성만 늘린다. 이 트리는 먼저 파티셔닝이 정말 필요한지 묻고, 접근 및 분포 패턴에 따라 Range, List, Hash, Composite 중 선택을 안내한다.","start":"root","nodes":{"root":{"kind":"step","title":"테이블이 파티셔닝이 필요할 만큼 정말 큰가?","detail":"파티셔닝은 성능 마법이 아니라 규모와 유지보수(수명 주기) 도구다. 테이블이 충분히 크지 않거나 쿼리가 이미 인덱스로 잘 동작한다면 파티셔닝은 실행 계획 복잡성과 관리 부담만 가져온다. 테이블이 수천만 행 이상/수백 GB에 이르는가, 아니면 아직 소/중규모인가?","choices":[{"label":"예, 매우 크고/또는 오래된 데이터를 일괄 삭제/아카이브해야 함","next":"access"},{"label":"아니요, 소/중규모 테이블이거나 확실하지 않음","next":"sol_no_partition"}]},"access":{"kind":"step","title":"데이터에 어떻게 접근하며, 어떻게 분할해야 하는가?","detail":"파티션 키는 대부분의 쿼리가 필터링하는 컬럼이어야 한다. 그래야만 pruning이 동작한다. 지배적인 접근 패턴은 무엇인가? 날짜/순차 범위(최근 30일)인가, 특정 카테고리/리전 값인가, 아니면 명확한 필터 없이 부하를 균등하게 분산하려는가?","choices":[{"label":"날짜/시퀀스 번호 같은 순차 범위(최근 X일, 월별)","next":"sol_range"},{"label":"카테고리/리전/테넌트 같은 이산 값 집합","next":"sol_list"},{"label":"명확한 범위/카테고리 없음, 부하 균등 분산이 목표","next":"sol_hash"},{"label":"날짜와 대용량 모두, 두 차원으로 분할 필요","next":"sol_composite"}]},"sol_no_partition":{"kind":"solution","title":"권장: (아직) 파티셔닝하지 않는다","detail":"소/중규모 테이블에서 파티셔닝은 이점이 없다. 잘 선택한 인덱스와 일반적인 유지보수로 충분하다. 성급한 파티셔닝은 쿼리 실행 계획을 복잡하게 하고, 잘못된 키로 pruning을 놓치며, 운영 부담을 더한다.","steps":["이유: 작은 테이블에서는 전체 스캔이나 인덱스 접근이 이미 빠르다. 파티셔닝은 추가 관리와 실행 계획 복잡성만 더한다.","이 상태를 유지할 시점: 수백만 행 미만, 오래된 데이터를 일괄 삭제할 필요 없음, 쿼리가 인덱스로 잘 동작함.","대안: 올바른 인덱스, 필요 시 아카이브 테이블, 정기적인 통계/VACUUM/ANALYZE.","주의: '커질 것'이라는 가정으로 미리 파티셔닝을 구성하지 않는다. 증가가 입증되고 올바른 키가 명확해지면 전환한다.","엔진별 차이: 모든 엔진에 유효한 일반 원칙이다. PostgreSQL declarative partitioning, MySQL PARTITION BY, Oracle 및 MSSQL 파티셔닝은 나중에 쉽게 추가할 수 있다."],"related":[]},"sol_range":{"kind":"solution","title":"권장: Range 파티셔닝","detail":"Range 파티셔닝은 컬럼의 순차 범위(예: 날짜 범위: 월/분기마다 파티션 하나)로 행을 분할한다. 시계열과 계속 증가하는 테이블에 가장 일반적이고 강력한 전략이다.","steps":["이유: 쿼리가 날짜/범위로 필터링하면 엔진은 관련 파티션만 읽는다(partition pruning). 오래된 데이터는 단일 명령(partition DROP)으로 즉시 삭제된다.","적용 시점: 시계열, 로그, 주문/거래 이력, '최근 N일' 쿼리와 시간 기반 아카이빙.","파티션 키: 대다수 쿼리의 WHERE 절에 있는 날짜/순차 컬럼을 선택한다. 그렇지 않으면 pruning이 동작하지 않는다.","예: PostgreSQL RANGE 파티셔닝, Oracle range/interval 파티셔닝(새 파티션 자동 생성), MySQL PARTITION BY RANGE(날짜에는 대개 RANGE COLUMNS), MSSQL partition function/scheme.","주의: 향후 범위에 대한 새 파티션 생성을 자동화한다(Oracle interval 또는 스케줄 작업). 데이터가 쌓인 'default/maxvalue' 파티션은 핫스팟이 된다."],"related":[]},"sol_list":{"kind":"solution","title":"권장: List 파티셔닝","detail":"List 파티셔닝은 컬럼의 이산적이고 알려진 값(예: region = 'TR','EU','US' 또는 tenant ID)으로 행을 그룹화한다. 자연스럽고 유한한 카테고리가 있으면 접근과 관리가 명확해진다.","steps":["이유: 쿼리가 특정 카테고리 값으로 필터링하면 해당 파티션만 읽는다. 카테고리별 유지보수/삭제가 쉬워진다.","적용 시점: 리전/국가, 상품 유형, 상태, 멀티 테넌트 분리처럼 이산적이고 비교적 안정적인 값 집합.","파티션 키: 값 집합이 유한하고 예측 가능해야 한다. 새 값이 자주 추가된다면 List 대신 Hash를 고려한다.","예: PostgreSQL LIST 파티셔닝, Oracle list 파티셔닝, MySQL PARTITION BY LIST, 적절한 경계를 가진 MSSQL partition scheme.","주의: 정의되지 않은/새 값을 위한 'default' 파티션을 유지한다. 그렇지 않으면 행을 삽입할 수 없고 오류가 발생한다."],"related":[]},"sol_hash":{"kind":"solution","title":"권장: Hash 파티셔닝(균등 분산)","detail":"Hash 파티셔닝은 키 컬럼의 해시를 기준으로 미리 정한 수의 파티션에 행을 비교적 균등하게 분산한다. 자연스러운 범위/카테고리가 없을 때 핫스팟을 피하고 부하를 균형 있게 하는 데 사용한다.","steps":["이유: 행이 균등하게 분산된다. 단일 파티션이나 기본 키 끝부분에 몰리는 쓰기 핫스팟이 줄어든다.","적용 시점: 쓰기량이 많고, 자연스러운 범위/카테고리가 없으며, I/O와 크기를 균등하게 하는 것이 목표인 경우(예: 높은 카디널리티의 id).","파티션 키: 높은 카디널리티와 균등한 분포를 가진 컬럼(대개 기본/고유 키)을 선택한다.","예: PostgreSQL HASH 파티셔닝, Oracle hash 파티셔닝, MySQL PARTITION BY HASH/KEY.","주의: 범위 쿼리에는 pruning을 제공하지 않는다(해싱이 순서를 없앰). '최근 N일' 같은 쿼리는 모든 파티션을 스캔한다. 나중에 파티션 수를 변경하려면 재분산이 필요하다."],"related":[]},"sol_composite":{"kind":"solution","title":"권장: Composite(다단계) 파티셔닝","detail":"Composite 파티셔닝은 두 가지 전략을 결합한다: 먼저 상위 수준에서 분할한 뒤 각 파티션을 서브파티션으로 나눈다(예: Range-by-Hash 또는 Range-by-List). 시간 기반 수명 주기와 균등 분산/카테고리 분리가 동시에 필요할 때 사용한다.","steps":["이유: 단일 전략으로 충분하지 않을 때 두 요구를 함께 충족한다. 예를 들어 월별로 분할하고(아카이빙) 월 내에서는 hash로 분산한다(핫스팟 방지).","적용 시점: 매우 큰 시계열 + 높은 동시 쓰기, 또는 날짜 + 리전/테넌트의 이중 접근 패턴.","예: Oracle composite(range-hash, range-list, list-hash 등). PostgreSQL에서는 파티션을 다시 파티셔닝하는 방식(partition of partition)으로 수동 구성한다. MySQL 서브파티션(RANGE/LIST + HASH/KEY).","주의: 관리와 스키마 복잡성이 가장 높은 옵션이다. 두 차원 모두 입증된 요구일 때만 선택한다.","주의: 상위/하위 키를 잘못 선택하면 pruning을 잃는다. 두 수준 모두 쿼리 패턴과 일치하는지 검증한다."],"related":[]}}},"solver/decide-pooling.json":{"id":"decide-pooling","title":"결정: 커넥션 풀링을 어떻게 구성하는가?","icon":"🧭","category":"의사결정 가이드","intro":"모든 데이터베이스 연결은 메모리와 스케줄링 비용을 수반한다. 연결 수가 통제 없이 증가하면 서버가 느려지거나 다운될 수 있다. 커넥션 풀은 소수의 영구 연결을 재사용해 이를 해결한다. 이 트리는 풀의 위치, 사용할 모드(session/transaction/statement), 적정 크기 결정을 안내하며, 서버리스 환경에서 외부 풀러가 필수인 이유를 강조한다.","start":"root","nodes":{"root":{"kind":"step","title":"애플리케이션 아키텍처는 어떻게 연결하는가?","detail":"풀링 결정은 아키텍처에서 시작한다. 단일 장기 실행 애플리케이션 프로세스(전통적 서버)인가, 다수의 독립 프로세스/인스턴스(마이크로서비스, 다수의 pod)인가, 아니면 요청마다 생성되고 사라지는 서버리스/Lambda 함수인가? 각각 다른 풀 배치가 필요하다.","choices":[{"label":"하나/소수의 장기 실행 애플리케이션 프로세스","next":"sol_app_pool"},{"label":"다수의 프로세스/인스턴스/pod가 같은 DB에 연결","next":"middleware"},{"label":"서버리스 / Lambda / 단기 실행 함수","next":"sol_serverless"},{"label":"먼저 올바른 풀 크기 선택 방법을 알려 달라","next":"sol_size"}]},"middleware":{"kind":"step","title":"미들웨어 풀러 모드는 어떻게 선택해야 하는가?","detail":"클라이언트가 많으면 미들웨어 풀러(PgBouncer, ProxySQL)가 DB의 연결 수를 낮게 유지한다. 그러나 풀 모드는 애플리케이션이 사용하는 기능에 따라 달라진다: 세션에 묶인 상태(prepared statement, 임시 테이블, SET, advisory lock, 세션 수준 기능)에 의존하는가, 아니면 모든 작업이 단일 트랜잭션 안에서 완료되는가?","choices":[{"label":"세션 상태(SET, 임시 테이블, 세션 prepared statement)에 의존함","next":"sol_session_mode"},{"label":"트랜잭션이 짧고 독립적이며 세션 상태를 유지하지 않음","next":"sol_txn_mode"}]},"sol_app_pool":{"kind":"solution","title":"권장: 애플리케이션 내 커넥션 풀","detail":"하나/소수의 장기 실행 프로세스에는 애플리케이션 내 풀 라이브러리(HikariCP, Go database/sql의 내장 풀, .NET connection pooling)로 대부분의 요구를 충족한다. 연결을 한 번 열고 반복해서 재사용한다.","steps":["이유: 연결 열기/닫기 비용이 사라지고, 제한되고 예측 가능한 연결 수가 DB를 보호한다.","적용 시점: 전통적인 모놀리스나 소수의 애플리케이션 인스턴스, 전체 연결 수요가 DB 용량 미만인 경우.","예: Java HikariCP, .NET SqlConnection/Npgsql pooling, Node pg-pool, Python SQLAlchemy pool.","사이징: 코어/디스크 수를 기준으로 풀 크기를 정한다(다음 결정). '많을수록 좋다'는 적용되지 않는다.","주의: 각 애플리케이션 인스턴스는 자체 풀을 연다. 인스턴스 수 × 풀 크기가 DB max_connections를 초과해서는 안 된다. 초과하면 미들웨어 풀러로 전환한다."],"related":["connection-storm"]},"sol_size":{"kind":"solution","title":"권장: 풀 크기를 올바르게 설정","detail":"풀은 너무 작아도(요청 대기) 너무 커도 안 된다. 연결이 너무 많으면 CPU 컨텍스트 스위칭과 락 경합이 발생해 처리량이 떨어진다. 크기는 무한정 늘리는 숫자가 아니라 코어와 I/O에 기반한 작은 숫자다.","steps":["시작 공식: 일반적인 출발점은 풀 크기 ≈ (코어 수 × 2) + 유효 스핀들 수다. SSD/다수 코어 환경에서는 '코어 × 2-4' 정도가 좋은 출발점이다.","작게 유지하는 이유: DB의 실제 병렬성은 코어 수로 제한된다. 추가 연결은 대기열에 쌓일 뿐이며 컨텍스트 스위칭/락 경합으로 전체를 느리게 만든다.","규칙: 수백 개의 동시 클라이언트가 있을 때 해결책은 풀을 키우는 것이 아니라 작은 풀을 빠르게 회전시키는 것이다(짧은 트랜잭션 + 대기열).","전체 한도: (애플리케이션 인스턴스 수 × 풀 크기) < DB max_connections. superuser 및 유지보수 연결을 위한 여유를 남긴다.","주의: 공식으로 시작하되 항상 측정한다. 대기 시간, 활성 연결, 대기열 깊이 지표를 기준으로 조정한다."],"related":["connection-storm"]},"sol_session_mode":{"kind":"solution","title":"권장: Session 풀 모드","detail":"Session 모드에서는 클라이언트 연결이 세션 종료 시까지 백엔드 서버 연결에 고정된다. 세션 수준 상태(SET, 임시 테이블, advisory lock, 세션 prepared statement)가 유지되지만 연결 공유는 최소화된다.","steps":["이유: 세션 내내 상태 유지가 필요한 애플리케이션에 유일하게 안전한 모드다. DB에 직접 연결하는 것과 동작이 같다.","적용 시점: SET 세션 변수, 임시 테이블, LISTEN/NOTIFY, 세션 수준 prepared statement, advisory lock을 사용하는 애플리케이션.","예: PgBouncer pool_mode=session, ProxySQL에서 세션에 민감한 트래픽을 별도로 라우팅.","주의: 연결 공유가 적어 클라이언트가 많으면 이점이 줄어든다. 가능하면 애플리케이션에서 세션 상태를 제거하고 transaction 모드로 전환하는 것을 목표로 한다.","엔진별 차이: PostgreSQL에는 PgBouncer. MySQL 측에서는 ProxySQL이 유사한 동작을 다른 방식으로 처리하며 세션 변수에 주의가 필요하다."],"related":["connection-storm"]},"sol_txn_mode":{"kind":"solution","title":"권장: Transaction 풀 모드","detail":"Transaction 모드에서는 서버 연결이 트랜잭션 동안만 클라이언트에 할당되고 트랜잭션이 끝나면 풀로 반환된다. 따라서 수천 개의 클라이언트가 DB의 매우 적은 실제 연결로 작업할 수 있다. 대규모 환경에서 가장 일반적인 모드다.","steps":["이유: 연결이 트랜잭션 단위로 넘겨지므로 공유율이 가장 높다. 적은 DB 연결로 많은 클라이언트를 감당한다.","적용 시점: 짧고 독립적인 트랜잭션, 세션 상태를 유지하지 않는(또는 유지하지 않도록 만든) 마이크로서비스/다중 pod 아키텍처.","예: PgBouncer pool_mode=transaction(가장 일반적인 구성), ProxySQL의 기본 multiplexing 동작.","주의: 세션에 걸친 기능이 깨진다: 세션 수준 prepared statement, 임시 테이블, SET, advisory lock은 안전하지 않다. 프로토콜 수준 named prepared statement의 경우 애플리케이션/풀러 호환성을 검증한다.","Statement 모드: 더 공격적인 (statement) 모드는 문장마다 연결을 넘긴다. 트랜잭션이 없는 매우 특수한 읽기 전용 시나리오에서만 고려하며 제약이 매우 크다."],"related":["connection-storm"]},"sol_serverless":{"kind":"solution","title":"권장: 서버리스에는 외부 풀러가 필수","detail":"서버리스/Lambda 함수는 빠르게 확장되며 각 인스턴스가 자체 연결을 연다. 인스턴스가 단기 실행이고 아무것도 공유하지 않으므로 애플리케이션 내 풀은 도움이 되지 않는다. 수백/수천 개의 인스턴스가 갑자기 생성되면 DB 연결 한도가 초과된다. 해결책은 DB 앞단의 외부 커넥션 풀이다.","steps":["이유: 단기 실행되고 공유되지 않는 함수 인스턴스에서는 애플리케이션 내 풀이 유지될 수 없다. scale-out 시 연결 폭주가 발생한다.","적용 시점: AWS Lambda, Cloud Functions, Azure Functions처럼 요청마다 인스턴스를 생성하는 환경.","예: 별도 계층의 PgBouncer/ProxySQL 또는 관리형 풀러(예: AWS RDS Proxy, PostgreSQL용 Data API/외부 풀러 서비스).","모드: 대개 transaction 풀 모드를 선호한다. 함수 코드에서 세션 상태를 제거한다.","주의: 함수별 동시성 한도(reserved concurrency)도 설정한다. 풀러와 DB max_connections를 함께 사이징하지 않으면 풀러 자체가 병목이 된다."],"related":["connection-storm"]}}},"solver/decide-cache.json":{"id":"decide-cache","title":"결정: 어떤 캐싱 전략인가?","icon":"🧭","category":"의사결정 가이드","intro":"캐시는 자주 읽는 데이터를 데이터베이스 앞단의 빠른 계층(예: Redis)에 유지해 지연과 DB 부하를 낮춘다. 그러나 모든 데이터가 캐싱에 적합한 것은 아니며, 잘못된 무효화는 오래된/잘못된 데이터의 위험을 만든다. 이 트리는 먼저 캐시가 정말 필요한지 확인하고, 읽기/쓰기 패턴에 따라 cache-aside, read-through, write-through, write-behind 중 선택을 도우며, stampede 방지도 다룬다.","start":"root","nodes":{"root":{"kind":"step","title":"워크로드는 어떤 형태인가: 읽기 중심인가, 쓰기 중심인가?","detail":"캐시는 같은 데이터를 여러 번 읽고(읽기 중심) 자주 변경되지 않을 때 가장 효과가 크다. 먼저 전체 패턴을 파악한다: 데이터를 많이 읽고 드물게 변경하는가, 계속 갱신하는가, 아니면 캐시를 추가할 가치가 있는지 확실하지 않은가?","choices":[{"label":"매우 읽기 중심, 데이터 변경은 비교적 드묾","next":"read_pattern"},{"label":"쓰기도 많음, 쓴 데이터를 즉시/자주 읽음","next":"write_pattern"},{"label":"확실하지 않음, 캐시가 정말 필요한가?","next":"sol_no_cache"}]},"read_pattern":{"kind":"step","title":"캐시 채우기는 누가 담당해야 하는가?","detail":"읽기 중심 시나리오에서 핵심 선택은 누가 캐시를 채우는가이다. 애플리케이션이 캐시를 명시적으로 관리하고 직접 DB로 폴백해야 하는가(높은 유연성), 아니면 캐시 계층이 miss 시 백엔드 DB에서 데이터를 가져와 투명하게 반환해야 하는가?","choices":[{"label":"애플리케이션이 명시적으로 관리, 유연성과 통제를 원함","next":"sol_cache_aside"},{"label":"캐시 계층이 miss 시 DB에서 가져옴(투명)","next":"sol_read_through"}]},"write_pattern":{"kind":"step","title":"쓰기 시 일관성과 쓰기 속도 중 무엇이 우선인가?","detail":"쓰기가 많다면 쓴 데이터가 캐시와 DB 간에 얼마나 일관될지 결정해야 한다. 일관성을 보장하기 위해 모든 쓰기에서 캐시와 DB를 함께 갱신해야 하는가(쓰기가 다소 느림), 아니면 쓰기를 먼저 캐시에 보내고 백그라운드에서 비동기로 DB에 반영해야 하는가(쓰기는 매우 빠르지만 손실 위험)?","choices":[{"label":"일관성이 중요, 읽기는 항상 최신이어야 함","next":"sol_write_through"},{"label":"쓰기 속도가 핵심, 약간 지연된 일관성과 손실 위험은 허용","next":"sol_write_behind"}]},"sol_no_cache":{"kind":"solution","title":"권장: 지금은 캐시를 추가하지 않는다","detail":"캐시는 공짜가 아니다: 일관성 문제(오래된 데이터), 추가 인프라, 무효화 복잡성을 가져온다. 쿼리가 이미 빠르거나 요청마다 데이터가 바뀐다면 캐시는 이점 없이 장애 지점만 늘린다.","steps":["이유: 캐시는 일관성, 무효화, 운영 부담을 더한다. 효과가 없으면 순손실이다.","필요 없는 경우: 인덱스 덕분에 쿼리가 이미 빠르거나, 데이터가 읽을 때마다 다르거나/사용자별이라 재사용되지 않거나, 쓰기가 읽기보다 훨씬 많은 경우.","대안: 먼저 인덱스/쿼리 최적화, materialized view 또는 DB 자체의 buffer/plan cache.","주의: '만일을 대비해' 캐시를 추가하지 않는다. 측정된 읽기 병목과 재사용되는 데이터 없이 추가하지 않는다.","다음 단계: 실제 읽기 핫스팟이 입증되면 cache-aside부터 시작한다."],"related":["slow-query","high-cpu"]},"sol_cache_aside":{"kind":"solution","title":"권장: cache-aside(lazy loading)","detail":"cache-aside에서는 애플리케이션이 먼저 캐시를 확인한다. 데이터가 없으면(miss) DB에서 읽어 캐시에 쓰고 반환한다. 가장 일반적이고 유연한 패턴이며, 실제로 요청된 데이터만 캐시에 채워진다.","steps":["이유: 단순하고 유연하며 복원력이 있다. 캐시가 장애를 일으켜도 애플리케이션은 DB로 폴백해 계속 동작한다. 요청된 데이터만 캐싱된다.","적용 시점: 범용 읽기 가속, 읽기가 많고 쓰기가 보통인 경우, Redis/Memcached의 전형적인 사용 방식.","무효화: 쓰기 시 관련 키를 갱신하거나 삭제한다(write-invalidate). 모든 키에 적절한 TTL을 부여해 오래된 데이터의 범위를 제한한다.","예: Redis GET → 없으면 DB 쿼리 → SET key value EX <ttl>.","주의: 첫 요청은 항상 miss다(cold start). 같은 키를 다수의 클라이언트가 동시에 요청하면 cache stampede가 발생할 수 있다 — stampede 방지(다음 항목)를 참고한다.","Stampede: 인기 키가 만료되면 수천 개의 요청이 동시에 DB로 몰린다. lock/single-flight, 조기 갱신 또는 jitter를 둔 TTL로 방지한다."],"related":["high-cpu","slow-query"]},"sol_read_through":{"kind":"solution","title":"권장: read-through 캐시","detail":"read-through에서는 애플리케이션이 캐시와만 통신한다. miss 시 캐시 계층이 직접 DB에서 데이터를 가져와 채운다(provider/loader를 통해). 애플리케이션 코드가 단순해지고 채우기 로직이 한곳에 모인다.","steps":["이유: 채우기 로직이 캐시 계층에 집중된다. 애플리케이션 코드에서 'miss를 곳곳에서 처리하는' 반복이 사라진다.","적용 시점: 일관된 캐시 접근 계층(라이브러리/서비스)을 사용할 수 있는 읽기 중심 워크로드.","예: 캐시 라이브러리/클라이언트(예: loader가 정의된 로컬 캐시 또는 Redis 앞단의 접근 계층)가 miss 시 DB에서 로드한다.","무효화: TTL과 쓰기 시 무효화는 여전히 필요하다. read-through만으로는 최신성이 보장되지 않는다.","주의: cache-aside와 매우 유사하며 차이는 '누가 채우는가'이다. 여기서도 stampede 위험이 있으므로 같은 대책을 적용한다.","엔진 참고: Redis는 자체적으로 read-through를 수행하지 않는다. 이 패턴은 애플리케이션/라이브러리 계층이 제공한다."],"related":["slow-query"]},"sol_write_through":{"kind":"solution","title":"권장: write-through 캐시","detail":"write-through에서는 모든 쓰기가 캐시와 DB 양쪽에(동기적으로) 기록되어 둘이 함께 갱신된다. 따라서 캐시가 DB보다 뒤처지지 않고 읽기는 항상 최신 데이터를 본다.","steps":["이유: 캐시와 DB가 일관되게 유지된다. 쓴 데이터를 바로 읽어도 오래된 데이터를 읽을 위험이 없다.","적용 시점: 쓴 데이터를 즉시 자주 읽으며 강한 읽기 일관성이 필요한 시나리오.","예: 쓰기 시 애플리케이션이 먼저 DB를 갱신한 뒤 즉시 캐시 키를 새 값으로 설정(또는 삭제)한다.","주의: 모든 쓰기가 두 대상으로 가므로 쓰기 지연이 늘어난다. 읽히지 않을 데이터까지 불필요하게 캐싱될 수 있다. 두 쓰기는 원자적이지 않아(하나는 성공, 다른 하나는 실패) 불일치가 생길 수 있다 — 오류 시 캐시 항목을 삭제한다.","이중 쓰기 함정: DB와 캐시를 각각 쓰는 대신 'DB에 쓰고 캐시를 삭제(무효화)'하는 방식이 대개 더 안전하다."],"related":[]},"sol_write_behind":{"kind":"solution","title":"권장: write-behind(write-back) 캐시","detail":"write-behind에서는 쓰기가 먼저 캐시로 가고 사용자에게 즉시 반환된다. DB 쓰기는 백그라운드에서 비동기로(배치/지연) 수행된다. 쓰기 속도는 가장 빠르지만 내구성과 일관성 위험이 따른다.","steps":["이유: 쓰기 지연이 크게 줄고, 쓰기를 배치로 묶어 DB로 효율적으로 전송할 수 있다. 쓰기 중심 부하에서 DB 부담이 줄어든다.","적용 시점: 카운터, 지표, 활동 피드처럼 쓰기량이 매우 많고 약간 지연된 일관성이 허용되는 경우.","예: Redis에 쓰기 → 큐/백그라운드 워커가 주기적으로 DB에 배치 반영.","주의: DB에 쓰기 전에 캐시 노드가 다운되면 데이터가 손실된다. 내구성이 필요한 핵심 데이터에는 사용하지 않거나 내구성 있는 큐/AOF로 보호한다.","주의: DB는 한동안 오래된 상태로 남는다. DB를 직접 읽는 다른 시스템은 오래된 데이터를 본다. 일관성 지연과 손실 구간을 의도적으로 수용해야 한다."],"related":[]}}},"solver/decide-datatype.json":{"id":"decide-datatype","title":"결정: 올바른 데이터 유형 선택","icon":"🧭","category":"의사결정 가이드","intro":"데이터 유형 선택은 나중에 고치기 가장 비용이 큰 결정 중 하나다: 잘못된 유형은 잘못된 결과(예: 금액에 float), 공간 낭비, 느린 쿼리를 낳는다. 이 트리는 숫자, 텍스트, 날짜/시간, 키(PK), 반정형(JSON), 카테고리(ENUM) 필드에 맞는 유형을 안내하며, 금액과 문자 집합 같은 대표적인 함정을 짚는다.","start":"root","nodes":{"root":{"kind":"step","title":"어떤 종류의 데이터를 저장하는가?","detail":"올바른 유형은 데이터의 성격으로 결정된다. 필드가 어느 범주에 속하는가: 숫자, 텍스트, 날짜/시간, 테이블의 기본 키, 유연/반정형(JSON) 데이터, 아니면 제한된 카테고리 집합인가?","choices":[{"label":"숫자 값(수량, 금액, 측정값, 카운터)","next":"numeric"},{"label":"텍스트(이름, 설명, 코드)","next":"sol_text"},{"label":"날짜 및/또는 시간","next":"sol_datetime"},{"label":"기본 키(PK)를 설계 중","next":"sol_pk"},{"label":"유연/중첩/가변 필드(반정형 데이터)","next":"sol_json"},{"label":"제한되고 알려진 카테고리 집합(상태, 유형, 우선순위)","next":"sol_enum"}]},"numeric":{"kind":"step","title":"숫자가 정수인가 소수인가 — 그리고 금액인가?","detail":"숫자에서 가장 중요한 구분은 정확성이다. 값이 정수(수량, ID, 카운터)인가, 소수인가? 소수라면 금액/재무 금액인가? 이진 부동소수점(FLOAT/DOUBLE)은 반올림 오차를 만들므로 금액이나 정확성이 필요한 곳에는 절대 사용하지 않는다.","choices":[{"label":"정수(수량, 카운터, id)","next":"sol_integer"},{"label":"금액 또는 정확한 소수(가격, 잔액, 비율)","next":"sol_decimal"}]},"sol_integer":{"kind":"solution","title":"권장: 정수(SMALLINT / INT / BIGINT)","detail":"정수에는 필요한 범위를 포괄하는 가장 작은 유형을 선택한다. 불필요하게 큰 유형은 공간과 인덱스 크기를 낭비한다. 그러나 카운터/PK처럼 증가할 수 있는 필드에는 오버플로를 막기 위해 충분한 폭이 필수다.","steps":["이유: 올바른 폭은 저장 공간과 인덱스/메모리 사용량을 모두 최적화하며 산술 연산이 빠르다.","SMALLINT/INT 적용 시점: 제한된 범위(나이, 수량, 작은 카운터)에는 SMALLINT/INT로 충분하다.","BIGINT 적용 시점: 계속 증가하는 ID/카운터와 행 수가 수십억에 이를 수 있는 PK에는 BIGINT를 선택한다 — INT 오버플로는 운영 장애의 심각한 원인이다.","엔진별 차이: MySQL에서 INT UNSIGNED는 범위를 두 배로 늘린다. PostgreSQL에는 UNSIGNED가 없으므로 BIGINT로 전환한다. Oracle NUMBER(p), MSSQL INT/BIGINT.","주의: '커질 경우를 대비해' 모든 곳에 BIGINT를 쓰지 않는다. 다만 증가하는 PK에는 처음부터 BIGINT를 쓰는 것이 저렴한 보험이다."],"related":[]},"sol_decimal":{"kind":"solution","title":"권장: NUMERIC / DECIMAL — 금액에 FLOAT는 절대 금지","detail":"금액과 정확한 소수에는 고정소수점(정확한) NUMERIC/DECIMAL을 사용한다. FLOAT/DOUBLE은 이진 부동소수점이라 0.1 같은 값을 정확히 저장할 수 없다. 덧셈/곱셈에서 센트 단위 오차, 잘못된 잔액, 감사 문제가 발생한다.","steps":["이유: NUMERIC/DECIMAL은 값을 십진수로 정확히 저장한다. 금액 연산이 반올림 오차 없이 올바른 결과를 낸다.","적용 시점: 가격, 잔액, 세금, 비율, 이자처럼 정확성이 필요한 모든 필드. 적절한 scale을 지정한다: DECIMAL(precision, scale), 예: 금액에는 DECIMAL(19,4).","대안: 일부 시스템은 금액을 최소 단위(센트)로 BIGINT에 저장한다. 이 방법도 완전한 정확성을 제공한다.","엔진별 차이: PostgreSQL NUMERIC, MySQL DECIMAL, Oracle NUMBER(p,s), MSSQL DECIMAL/MONEY(MONEY의 제약에 유의).","주의: FLOAT/DOUBLE은 과학적/측정된 근삿값과 정확성보다 성능이 중요한 경우에만 사용한다 — 금액에는 절대 사용하지 않는다."],"related":[]},"sol_text":{"kind":"solution","title":"권장: 텍스트 — VARCHAR vs TEXT vs CHAR","detail":"텍스트에는 가변 길이에 VARCHAR/TEXT를, 정말 고정된 길이에 CHAR를 사용한다. 최신 엔진에서 VARCHAR와 TEXT의 저장 차이는 대부분 무시할 수준이다. 실제 결정은 의미상의 한도(길이 제약)를 둘지 여부다.","steps":["VARCHAR(n): 길이가 가변적이고 합리적인 상한이 있으면(예: 이메일, 이름) VARCHAR(n)을 사용한다 — 한도는 데이터 검증 도구다.","TEXT: 길이를 예측할 수 없는 긴 자유 텍스트(설명, 본문)에는 TEXT를 사용한다.","CHAR(n): 정말 고정된 길이(예: 국가 코드 'TR', 항상 2자)에만 사용한다. 짧은 텍스트에서 CHAR는 뒤쪽을 공백으로 채우며 대개 불필요하다.","엔진별 차이: PostgreSQL에서 VARCHAR와 TEXT는 성능이 같고 한도는 제약 조건일 뿐이다. MySQL에서 VARCHAR는 행 내부에 저장되고 TEXT는 별도로 저장될 수 있으며 인덱싱에 접두사 길이 제한이 있다. Oracle에서는 VARCHAR2를 사용한다.","Charset/collation: 이모지와 완전한 Unicode를 위해 MySQL에서는 항상 utf8mb4를 사용한다(구 'utf8'은 3바이트라 이모지가 깨짐). 정렬/비교 요구에 따라 콜레이션(예: utf8mb4_0900_ai_ci)을 선택한다. PostgreSQL에서는 일반적으로 UTF-8 데이터베이스 + 적절한 콜레이션."],"related":[]},"sol_datetime":{"kind":"solution","title":"권장: 날짜/시간 — 시간대를 포함한 TIMESTAMP","detail":"시점(이벤트 시각)을 저장한다면 시간대를 인식하는 유형을 선택한다. 시간대 없이 저장하면 리전/일광 절약 시간 변경 시 계산과 비교가 잘못된다.","steps":["이유: 시간대 인식 유형은 시점을 UTC로 정규화해 일관된 비교와 변환을 제공한다. 글로벌 시스템에 필수다.","TIMESTAMPTZ 적용 시점: PostgreSQL에서 이벤트/트랜잭션 시점에는 TIMESTAMP WITH TIME ZONE(timestamptz)을 사용한다. UTC로 저장하고 표시할 때 사용자 시간대로 변환한다.","날짜 또는 시간만: 달력 날짜에는 DATE, 시간대와 무관한 시각에는 TIME, 기간에는 INTERVAL(지원하는 엔진에서).","엔진별 차이: PostgreSQL timestamptz(실제로는 UTC로 저장). MySQL에서 TIMESTAMP는 시간대에 따라 변환되지만 범위가 제한된다(2038). DATETIME은 변환되지 않으므로 애플리케이션에서 UTC 저장 규칙을 강제한다. Oracle TIMESTAMP WITH TIME ZONE, MSSQL DATETIMEOFFSET.","주의: 시간을 텍스트(VARCHAR)나 epoch INT로 저장하면 정렬, 범위, 시간대 연산이 깨진다. 실제 날짜/시간 유형을 사용한다."],"related":[]},"sol_pk":{"kind":"solution","title":"권장: 기본 키 — BIGINT vs UUID","detail":"PK에는 두 가지 주요 옵션이 있다: 증가하는 숫자(BIGINT identity/sequence) 또는 UUID. 숫자 PK는 작고 인덱스 친화적이지만 예측 가능하고 중앙에서 생성해야 한다. UUID는 분산 생성이 가능하지만 더 크고 무작위 순서가 인덱스에 악영향을 줄 수 있다.","steps":["BIGINT(증가형): 가장 작고 빠른 인덱스. 순차 삽입은 B-tree에 이상적이다. 단일 DB/중앙 생성의 기본 선택이며, 처음부터 INT 대신 BIGINT를 쓰면 오버플로에 안전하다.","UUID: 클라이언트/분산 시스템에서 충돌 없이 생성할 수 있고, ID가 노출되어도 순번이 드러나지 않는다. 다만 16바이트(2×BIGINT)를 차지한다.","UUID 순서가 중요하다: 완전 무작위 UUID(v4)는 순차적이지 않아 인덱스에 흩어진 삽입(page split, 낮은 캐시 적중률)을 유발한다. 시간 순서 UUID(v7/ULID)는 이를 대부분 해소한다.","엔진별 차이: PostgreSQL에는 네이티브 uuid 유형이 있다. MySQL에서는 UUID를 (텍스트 CHAR(36) 대신) BINARY(16)으로 저장하고 가능하면 순서대로 생성한다. MSSQL uniqueidentifier + NEWSEQUENTIALID, Oracle RAW(16).","주의: UUID를 VARCHAR/CHAR(36) 텍스트로 저장하면 공간과 인덱스 크기가 대략 2~3배가 된다. 바이너리로 저장한다."],"related":[]},"sol_json":{"kind":"solution","title":"권장: JSON / JSONB — 적절히 사용","detail":"JSON 유형은 유연한 스키마, 중첩 또는 희소 필드에 강력하지만 관계형 규율(제약 조건, 인덱스, 타입 안전성)을 잃을 수 있다. 규칙: 고정적이고 자주 조회되는 필드는 일반 컬럼으로 옮기고, 정말 가변적인 부분만 JSON에 둔다.","steps":["이유: 가변/선택 필드를 단일 컬럼에 유연하게 저장하며, 스키마 마이그레이션 없이 필드를 추가할 수 있다.","적용 시점: 외부 API 페이로드, 사용자 정의 필드, 상품 속성처럼 예측할 수 없는 구조.","엔진별 차이: PostgreSQL에서는 JSONB(바이너리, 인덱싱 가능, GIN으로 @>/? 쿼리)를 사용한다 — 일반 JSON보다 선호한다. MySQL의 JSON 유형도 바이너리이며 generated column + 인덱스로 조회한다. Oracle 21c+에는 JSON 유형/OSON이 있다. 2025 이전 MSSQL은 JSON을 NVARCHAR에 보관하고 함수로 처리하며, SQL Server 2025와 Azure SQL에는 네이티브(바이너리) json 유형이 있다.","주의: 자주 필터링하는 JSON 필드는 인덱싱한다(PostgreSQL GIN, MySQL generated column 인덱스). 그렇지 않으면 모든 쿼리가 문서 전체를 스캔한다.","주의: 모든 것을 JSON에 넣지 않는다 — 관계형 무결성, 외래 키, 타입 지정을 잃는다. 쿼리/제약 조건이 필요한 필드는 실제 컬럼이어야 한다."],"related":["decide-sqlnosql"]},"sol_enum":{"kind":"solution","title":"권장: ENUM vs 룩업(참조) 테이블","detail":"제한된 카테고리 집합에는 두 가지 옵션이 있다: 엔진의 ENUM 유형, 또는 룩업 테이블 + 외래 키. ENUM은 간결하고 단순하지만 값 추가에 스키마 변경이 필요하다. 룩업 테이블은 유연하며 추가 속성(설명, 활성 플래그, 순서)을 가질 수 있다.","steps":["ENUM 적용 시점: 값이 매우 안정적이고 거의 바뀌지 않으며(예: 성별, 요일) 추가 속성이 필요 없는 경우. 간결하고 읽기 쉽다.","룩업 테이블 적용 시점: 값이 시간에 따라 바뀔 수 있거나, 번역/설명/순서/활성 플래그 같은 추가 필드가 필요하거나, 보고서에서 조인하는 경우 — 더 유연하다.","엔진별 차이: PostgreSQL과 MySQL은 네이티브 ENUM 유형을 제공한다(PostgreSQL에서 ENUM에 값을 추가하려면 ALTER TYPE이 필요하고, 정렬은 정의 순서를 따른다). Oracle과 MSSQL에는 내장 ENUM이 없으므로 CHECK 제약 조건이나 룩업 테이블을 사용한다.","주의: ENUM에 값을 추가/제거하는 것은 스키마 마이그레이션이며 큰 테이블에서 락/비용을 유발할 수 있다. 자주 바뀌는 집합에는 룩업 테이블을 선호한다.","대안: 작고 안정적인 집합에는 자유 텍스트 + CHECK 제약 조건도 실용적인 절충안이다."],"related":[]}}},"solver/decide-capacity.json":{"id":"decide-capacity","title":"결정: 서버 사이징","icon":"🧭","category":"의사결정 가이드","intro":"올바른 사이징은 워크로드 프로필(OLTP, 데이터 웨어하우스, 혼합)에서 시작하며 실제 병목(CPU, I/O, RAM)을 찾아야 한다. 황금률: 자주 접근하는 데이터 집합(working set)은 RAM에 들어가야 한다. 먼저 측정한 뒤 확장하고, 갑작스러운 부하에 대비해 여유를 남긴다. 이 트리는 워크로드를 식별하고 병목에 따라 리소스와 확장 방향(수직/수평)을 선택하도록 돕는다.","start":"root","nodes":{"root":{"kind":"step","title":"워크로드 프로필은 무엇인가?","detail":"OLTP와 분석 부하는 완전히 다른 리소스 프로필을 요구하므로 사이징은 워크로드에서 시작한다. 시스템의 주된 작업은 무엇인가: 작고 빠른 다수의 트랜잭션(OLTP)인가, 대규모 스캔과 집계를 수행하는 보고/분석(DWH)인가, 아니면 둘의 혼합인가? 확실하지 않다면 먼저 측정해야 한다.","choices":[{"label":"OLTP — 작고 짧은 다수의 트랜잭션(웹/애플리케이션)","next":"bottleneck"},{"label":"분석 / 데이터 웨어하우스(DWH) — 대규모 스캔과 집계","next":"sol_dwh"},{"label":"혼합 워크로드이거나 확실하지 않음","next":"sol_measure"},{"label":"병목은 알고 있다. 수직 확장과 수평 확장 중 무엇을 해야 하는가?","next":"sol_scale"}]},"bottleneck":{"kind":"step","title":"병목은 어디인가: RAM, I/O, CPU?","detail":"OLTP에서는 무작정 리소스를 추가하는 대신 실제 병목을 찾는다. 지표를 확인한다: 자주 접근하는 데이터가 RAM에 들어가지 않아 디스크 읽기가 많은가(RAM/I/O), 디스크 지연/IOPS가 부족한가(I/O), 아니면 데이터는 이미 메모리에 있지만 CPU가 포화 상태인가(CPU)? 잘못된 리소스를 확장하면 비용과 시간을 낭비한다.","choices":[{"label":"자주 접근하는 데이터가 RAM에 들어가지 않음, 디스크 읽기가 많음","next":"sol_ram"},{"label":"데이터는 메모리에 있지만 디스크 지연/IOPS가 부족함(쓰기 중심)","next":"sol_io"},{"label":"데이터는 메모리에 있고 디스크도 정상이지만 CPU가 계속 포화 상태임","next":"sol_cpu"}]},"sol_measure":{"kind":"solution","title":"권장: 먼저 측정한 뒤 사이징","detail":"추측에 기반한 하드웨어 구매는 거의 항상 잘못된 크기로 이어진다. 워크로드 프로필과 병목은 실제 지표에서 드러난다. 사이징은 추측이 아니라 측정 기반의 반복 작업이다.","steps":["이유: OLTP인지 DWH인지, 병목이 CPU, I/O, RAM 중 무엇인지는 실제 부하에서 측정해야만 명확해진다. 추측은 비싼 실수다.","측정 항목: CPU 사용률과 run queue, 메모리/캐시 적중률(buffer cache hit), 디스크 IOPS와 지연, 활성 연결과 대기 이벤트.","엔진 도구: PostgreSQL pg_stat_* 및 대기 이벤트, Oracle AWR/ASH, MySQL Performance Schema, MSSQL DMV 및 wait stats.","접근 방식: 대표 부하로 테스트하고, 가장 부족한 리소스를 찾아 그것만 확장한 뒤 다시 측정한다. 효과를 분리할 수 있도록 한 번에 하나의 리소스만 변경한다.","다음 단계: 프로필이 명확해지면 해당 결정(OLTP 병목, DWH 또는 수평/수직 확장)으로 넘어간다."],"related":["high-cpu","memory-pressure","slow-query"]},"sol_ram":{"kind":"solution","title":"권장: RAM 증설 — working set이 메모리에 들어가야 한다","detail":"가장 중요한 OLTP 규칙: 자주 접근하는 데이터와 인덱스(working set)는 메모리(buffer/cache)에 들어가야 한다. 그렇지 않으면 모든 쿼리가 디스크로 가서 지연이 폭증하고 I/O가 병목이 된다. RAM은 대개 투자 대비 효과가 가장 큰 항목이다.","steps":["이유: 메모리 읽기는 디스크보다 수백~수천 배 빠르다. working set이 RAM에 들어가면 캐시 적중률이 오르고 디스크 I/O가 급감한다.","적용 시점: 낮은 캐시 적중률, 빈번한 'disk read' 대기 이벤트, 사용 가능한 RAM보다 큰 활성 데이터 집합.","방법: DB 메모리 영역을 올바르게 사이징한다 — PostgreSQL shared_buffers + OS page cache, MySQL innodb_buffer_pool_size(대개 RAM의 대부분), Oracle SGA/buffer cache, MSSQL max server memory.","목표: 이상적으로는 working set + 인덱스가 메모리에 들어가야 한다. 전부 들어가지 않는다면 최소한 가장 자주 접근하는 '핫' 부분은 들어가야 한다.","주의: 물리 RAM 전부를 buffer pool에 할당하지 않는다. OS, 연결별 메모리, 기타 프로세스를 위한 여유를 남긴다. 그렇지 않으면 스와핑으로 전체가 느려진다."],"related":["memory-pressure","slow-query"]},"sol_io":{"kind":"solution","title":"권장: I/O 계층 강화(IOPS/지연)","detail":"데이터가 메모리에 있어도 쓰기 중심 부하(commit, WAL/redo/binlog, checkpoint)에서는 디스크 속도가 결정적이다. 여기서 문제는 용량이 아니라 IOPS와 지연이다. 잘못된 디스크 등급은 빠른 CPU/RAM에도 불구하고 시스템을 마비시킨다.","steps":["이유: 내구성을 위해 모든 커밋은 로그를 디스크에 기록한다. IOPS가 낮고 지연이 큰 디스크는 트랜잭션 처리량을 직접 제한한다.","적용 시점: 높은 디스크 지연, 긴 'log/WAL write' 대기, checkpoint 중 병목, 쓰기 집약적 OLTP.","방법: NVMe/SSD를 사용한다. 트랜잭션 로그를 데이터 파일과 분리된 빠른 디스크로 옮긴다. 클라우드에서는 충분한 프로비저닝 IOPS/처리량을 가진 디스크 등급(예: provisioned IOPS)을 선택한다.","목표: IOPS와 지연 목표를 모두 설정한다 — IOPS뿐 아니라 p99 지연도 중요하다. RAID/쓰기 캐시(BBU)로 쓰기 지연을 줄일 수 있다.","주의: 클라우드에서는 디스크 처리량과 IOPS가 인스턴스 크기에도 좌우되는 경우가 많다. 네트워크 연결 디스크의 지연은 로컬 NVMe보다 크다."],"related":["disk-full","slow-query"]},"sol_cpu":{"kind":"solution","title":"권장: CPU 병목 해결(먼저 쿼리, 그다음 코어)","detail":"데이터가 메모리에 있고 디스크가 정상인데 CPU가 포화라면 근본 원인은 대개 비효율적인 쿼리(나쁜 실행 계획, 누락된 인덱스, 다수의 작은 쿼리) 또는 과도한 동시성이다. 코어를 추가하기 전에 부하를 줄이는 것이 대개 더 저렴하고 효과적이다.","steps":["이유: CPU를 가장 많이 소비하는 것은 전체 스캔, 정렬/해시, 실행 계획 컴파일, 과도한 연결의 컨텍스트 스위칭이다. 이를 고치는 것이 코어보다 저렴하다.","먼저 최적화: 가장 비용이 큰 쿼리를 찾고(pg_stat_statements / AWR / Performance Schema), 누락된 인덱스를 추가하며, N+1 쿼리를 줄이고, 커넥션 풀을 축소한다.","그다음 확장: 쿼리가 정상인데도 CPU가 포화라면 코어 수를 늘린다(수직) — 특히 동시성이 높은 OLTP에서.","엔진 참고: 연결이 많을수록 컨텍스트 스위칭도 많아진다. 빠르게 회전하는 작은 풀이 더 많은 코어보다 나은 경우가 많다(커넥션 풀 결정 참고).","주의: 라이선스가 코어 단위(Oracle/MSSQL)라면 코어 추가는 비싸다. 쿼리 최적화를 먼저 하면 비용이 줄어든다."],"related":["high-cpu","slow-query"]},"sol_dwh":{"kind":"solution","title":"권장: 분석/DWH — 처리량, 병렬성, 수평 확장","detail":"데이터 웨어하우스 워크로드는 큰 테이블을 스캔하고 집계하는 소수의 무거운 쿼리로 구성된다. 여기서 우선순위는 낮은 지연이 아니라 높은 스캔 처리량, 병렬성, 그리고 대개 수평 확장 가능한 아키텍처다.","steps":["이유: 쿼리가 테라바이트를 스캔한다. 개별 트랜잭션의 지연이 아니라 디스크/메모리 대역폭과 병렬로 동작하는 코어가 결정적이다.","방법: 높은 I/O 처리량의 스토리지, 충분한 RAM(집계/hash join용), 다수의 코어와 병렬 쿼리. 대규모에서는 컬럼형 저장소나 MPP 아키텍처가 매우 효율적이다.","수평 확장: 대규모 DWH에서는 단일 머신을 키우는 것보다 분산/MPP(예: 샤딩된 분석 엔진)가 대개 더 잘 확장된다. 파티셔닝 + partition pruning으로 스캔을 줄인다.","엔진 참고: OLTP 설정을 그대로 가져오지 않는다 — DWH에는 더 큰 work_mem/정렬 영역, 더 높은 병렬도, 큰 블록/readahead를 선호한다.","주의: 같은 서버에서 OLTP와 DWH를 혼용하지 않는다. 무거운 보고서가 OLTP를 압박한다. 가능하면 분석을 별도 복제본/데이터 웨어하우스로 옮긴다."],"related":["decide-shard-replica","decide-partition"]},"sol_scale":{"kind":"solution","title":"일반 원칙: 수직 vs 수평 + 여유 + 먼저 측정","detail":"어떤 리소스를 확장하느냐만큼 어떻게 확장하느냐도 중요하다. 수직(더 큰 머신)은 단순하지만 한계와 단일 장애 지점이 있고, 수평(더 많은 머신)은 확장되지만 복잡성을 더한다. 어떤 경우든 갑작스러운 부하에 대비해 여유를 남기고 측정으로 결정을 검증한다.","steps":["수직(scale-up): 단일 머신의 리소스(CPU/RAM/IO)를 늘린다 — 가장 단순한 옵션이며 대부분의 OLTP에서 첫 단계다. 그러나 물리적/비용 한계와 단일 장애 지점이 있다.","수평(scale-out): 읽기 복제본(읽기 부하용) 또는 샤딩(쓰기/데이터 양용)을 추가한다 — 거의 무제한 확장이 가능하지만 일관성과 운영 복잡성이 따른다.","여유: 리소스를 100% 기준으로 계획하지 않는다. 일반 피크 부하에서 ~30%의 여유를 남겨 갑작스러운 급증, 유지보수, 증가를 수용한다.","먼저 측정: 확장 전에 측정으로 병목을 확인하고, 가장 부족한 리소스를 대상으로 하며, 변경 후 다시 측정한다.","주의: 수평 확장이 모든 문제를 해결하지는 않는다 — 분산 트랜잭션과 조인은 비싸다. 수직 확장과 최적화가 소진된 후에만 전환한다."],"related":["decide-shard-replica","decide-ha"]}}},"solver/slow-write.json":{"id":"slow-write","title":"느린 쓰기(INSERT/UPDATE 지연)","icon":"🐢","category":"성능","intro":"INSERT/UPDATE/DELETE 작업이 예상보다 느리다. 먼저 쓰기가 무엇을 기다리는지(commit/redo, 락, 디스크, 행 단위 오버헤드) 파악한 뒤 단계적으로 근본 원인을 좁혀 간다.","start":"root","nodes":{"root":{"kind":"step","title":"쓰기는 무엇을 기다리는가?","detail":"대기 이벤트를 보고 병목을 분류한다: commit/redo(WAL) 동기화, 락, 디스크 쓰기 지연, 행 단위 인덱스/트리거 오버헤드 중 무엇이 지배적인가?","command":{"oracle":"SELECT event, total_waits, ROUND(time_waited/100,1) AS sec_waited\nFROM v$system_event\nWHERE event IN ('log file sync','log file parallel write','enq: TX - row lock contention','db file sequential read')\nORDER BY time_waited DESC;","postgres":"SELECT wait_event_type, wait_event, count(*) AS sessions\nFROM pg_stat_activity\nWHERE state='active'\nGROUP BY 1,2 ORDER BY 3 DESC;","mysql":"SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_waits';\nSHOW GLOBAL STATUS LIKE 'Innodb_os_log_pending_fsyncs';\nSHOW ENGINE INNODB STATUS\\G","mssql":"SELECT TOP 10 wait_type, waiting_tasks_count, wait_time_ms\nFROM sys.dm_os_wait_stats\nWHERE wait_type IN ('WRITELOG','PAGEIOLATCH_EX','LCK_M_X','LCK_M_U')\nORDER BY wait_time_ms DESC;"},"choices":[{"label":"commit / redo 대기(WAL sync)","next":"commit"},{"label":"락 대기","next":"lock"},{"label":"행 단위 오버헤드 의심(인덱스/트리거)","next":"overhead"},{"label":"디스크 쓰기 지연이 큼","next":"sol_disk"}]},"commit":{"kind":"step","title":"커밋 패턴인가, fsync인가?","detail":"행마다 별도 커밋(autocommit)을 하면 행 수만큼 redo/로그 동기화가 발생한다. 반대로 커밋 수는 적절한데 각 커밋이 fsync를 기다린다면 문제는 로그 디스크 지연이다.","command":{"oracle":"SELECT name, value FROM v$sysstat WHERE name IN ('user commits','redo writes','redo size');\nSELECT ROUND(time_waited_micro/GREATEST(total_waits,1)/1000,2) AS ms_per_sync FROM v$system_event WHERE event='log file sync';","postgres":"SHOW synchronous_commit;\nSELECT wal_records, wal_fpi, wal_bytes, wal_buffers_full FROM pg_stat_wal; -- wal_sync/wal_sync_time PG14-17; PG18+: pg_stat_io (object = 'wal')","mysql":"SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';\nSHOW VARIABLES LIKE 'sync_binlog';\nSHOW GLOBAL STATUS LIKE 'Com_commit';","mssql":"SELECT io_stall_write_ms, num_of_writes, io_stall_write_ms/NULLIF(num_of_writes,0) AS ms_per_write\nFROM sys.dm_io_virtual_file_stats(DB_ID(), 2);"},"choices":[{"label":"Autocommit / 행마다 커밋","next":"sol_batch"},{"label":"커밋이 로그 동기화(fsync)를 기다림","next":"sol_disk"}]},"lock":{"kind":"step","title":"누가 락을 보유하고 있는가?","detail":"쓰기가 락을 기다린다면: 다른 세션이 같은 행/인덱스를 보유하고 있거나, 다수의 세션이 같은 '핫' 행에 쓰려 하고 있을 수 있다.","command":{"oracle":"SELECT s.sid, s.serial#, s.username, s.blocking_session, s.event, s.seconds_in_wait\nFROM v$session s WHERE s.blocking_session IS NOT NULL;","postgres":"SELECT pid, pg_blocking_pids(pid) AS blocked_by, wait_event, left(query,60) AS query\nFROM pg_stat_activity\nWHERE cardinality(pg_blocking_pids(pid)) > 0;","mysql":"SELECT requesting_engine_transaction_id AS waiter, blocking_engine_transaction_id AS blocker,\n       object_name, lock_type, lock_mode\nFROM performance_schema.data_lock_waits w\nJOIN performance_schema.data_locks l ON l.engine_lock_id = w.blocking_engine_lock_id;","mssql":"SELECT session_id, blocking_session_id, wait_type, wait_time, wait_resource\nFROM sys.dm_exec_requests\nWHERE blocking_session_id <> 0;"},"choices":[{"label":"다른 세션이 락을 보유 중","next":"sol_lock"},{"label":"다수의 세션이 같은 핫 행에 씀","next":"sol_hotrow"}]},"overhead":{"kind":"step","title":"행 단위 오버헤드는 어디서 오는가?","detail":"모든 INSERT/UPDATE는 테이블의 모든 인덱스도 갱신하고 트리거/제약 조건을 실행한다. 인덱스가 과도하거나 트리거가 무거우면 쓰기가 크게 느려진다.","command":{"oracle":"SELECT index_name, uniqueness FROM user_indexes WHERE table_name='T';\nSELECT trigger_name, triggering_event FROM user_triggers WHERE table_name='T';","postgres":"SELECT indexname, indexdef FROM pg_indexes WHERE tablename='t';\nSELECT tgname FROM pg_trigger WHERE tgrelid='t'::regclass AND NOT tgisinternal;","mysql":"SHOW INDEX FROM t;\nSHOW TRIGGERS LIKE 't';","mssql":"SELECT name, type_desc FROM sys.indexes WHERE object_id=OBJECT_ID('dbo.t');\nSELECT name, is_disabled FROM sys.triggers WHERE parent_id=OBJECT_ID('dbo.t');"},"choices":[{"label":"테이블에 인덱스가 너무 많음 / 불필요한 인덱스","next":"sol_index"},{"label":"무거운 트리거 또는 제약 조건 비용","next":"sol_trigger"}]},"sol_batch":{"kind":"solution","title":"근본 원인: autocommit / 행마다 커밋","detail":"행마다 별도 커밋을 하면 행 수만큼 redo/로그 동기화가 반복된다. 작업을 묶으면 커밋 수(따라서 fsync 수)를 크게 줄일 수 있다.","steps":["애플리케이션에서 autocommit을 끄고 N행마다(예: 500-5000) 커밋하는 배치 커밋을 적용한다.","단일 행 INSERT 대신 다중 행 INSERT / 대량 적재 경로를 사용한다(Postgres COPY, Oracle bulk insert, MySQL LOAD DATA, MSSQL BULK INSERT).","prepared statement를 사용해 반복 파싱(hard parse) 비용을 줄인다.","변경 후 'log file sync' / WRITELOG 대기가 줄었는지 확인한다."],"related":["active-sessions"]},"sol_disk":{"kind":"solution","title":"근본 원인: 느린 redo/로그 디스크(fsync 지연)","detail":"모든 커밋은 redo/WAL 레코드가 내구성 있는 스토리지에 기록될 때까지(fsync) 기다린다. 로그 디스크가 느리면(높은 쓰기 지연, 부족한 IOPS) 모든 커밋이 그 지연을 떠안는다.","steps":["로그/redo 파일을 지연이 낮은 전용 디스크(가급적 NVMe SSD)로 옮기고 데이터 파일과 물리적으로 분리한다.","스토리지 지연(파일 I/O 통계)을 측정한다. 쓰기당 ms가 높다면 하드웨어/RAID 쓰기 캐시를 검토한다.","가상/클라우드 환경에서는 프로비저닝 IOPS와 처리량 한도를 높인다.","내구성 요구 사항이 허용한다면 group commit / commit delay 설정(예: Postgres commit_delay)을 고려한다. 단, 데이터 손실 위험을 따져 본다."],"related":[]},"sol_lock":{"kind":"solution","title":"근본 원인: 다른 세션이 락을 보유 중","detail":"쓰기가 다른 세션이 보유한 행/인덱스 락을 기다리고 있다. 블로커는 대개 오래 실행 중인 트랜잭션이거나 커밋을 잊은 트랜잭션이다.","steps":["블로커 세션을 식별하고 무엇을 하는지 확인한다(긴 배치인가, idle in transaction인가?).","블로커가 이유 없이 열려 있거나 길다면 애플리케이션의 commit/rollback 패턴을 수정한다.","긴급 시 블로커를 통제된 방식으로 종료한다(kill session / pg_terminate_backend / KILL).","트랜잭션을 짧게 유지한다. 트랜잭션 내에서 사용자 상호작용을 기다리지 않는다."],"related":["blocking-sessions","current-locks"]},"sol_hotrow":{"kind":"solution","title":"근본 원인: 핫 행 경합","detail":"다수의 세션이 같은 행(예: 카운터, 시퀀스 번호, 잔액)에 쓰려 한다. 모두 같은 락을 기다리며 쓰기가 직렬화된다.","steps":["핫 카운터 패턴은 값을 병렬 버킷으로 분할하고(sharded counter) 읽을 때 합산한다.","시퀀스/ID 생성을 중앙의 단일 행에서 sequence/identity 메커니즘으로 옮긴다.","갱신 빈도를 줄인다: 누적 합계를 애플리케이션에서 버퍼링하고 주기적으로 기록한다.","작업 순서를 표준화하고 락 유지 시간을 줄여 경합을 낮춘다."],"related":["current-locks"]},"sol_index":{"kind":"solution","title":"근본 원인: 과도한 인덱스 오버헤드","detail":"테이블의 모든 인덱스는 쓰기마다 갱신된다. 인덱스가 많거나 사용되지 않는 인덱스가 있으면 INSERT/UPDATE가 직접 느려진다.","steps":["사용되지 않거나 중복된 인덱스를 식별하고(사용 통계) 안전하게 삭제한다.","넓은 복합 인덱스를 단순화하고, 같은 선행 컬럼을 반복하는 인덱스를 병합한다.","대규모 대량 적재에서는 인덱스를 일시적으로 비활성화하고 적재 후 재구축하는 것을 고려한다.","변경 후 쓰기 지연을 다시 측정해 개선을 확인한다."],"related":["slow-queries"]},"sol_trigger":{"kind":"solution","title":"근본 원인: 무거운 트리거 / 제약 조건","detail":"쓰기마다 실행되는 트리거나 검증 중인 제약 조건(특히 인덱스 없는 외래 키 검사)은 모든 행에 추가 비용을 더한다.","steps":["트리거 본문의 무거운 작업(예: 동기식 로그 기록)을 비동기/큐 기반으로 바꾼다.","참조하는(자식) 외래 키 컬럼에 인덱스를 생성한다. 그렇지 않으면 부모에 대한 모든 DELETE/UPDATE가 자식 테이블을 전체 스캔한다.","가능하면 행 수준 트리거를 문장 수준으로 바꾼다.","불필요한 트리거 로직이나 업무 규칙이 된 로직은 애플리케이션 계층으로 옮긴다."],"related":[]}}},"solver/io-bottleneck.json":{"id":"io-bottleneck","title":"높은 IO wait / 디스크 지연","icon":"💽","category":"성능","intro":"시스템이 디스크 IO를 기다리고 있다. 먼저 부하가 읽기 중심인지 쓰기 중심인지 구분한 뒤 원인을 좁혀 간다: 부족한 buffer cache, 전체 테이블 스캔, checkpoint storm, IOPS 한도, 임시 공간으로의 spill.","start":"root","nodes":{"root":{"kind":"step","title":"IO는 읽기 중심인가, 쓰기 중심인가?","detail":"파일 IO 통계로 대기가 읽기 측(디스크에서 데이터 읽기)인지 쓰기 측(redo/checkpoint/데이터 쓰기)인지 판단한다. 이에 따라 트리가 둘로 나뉜다.","command":{"oracle":"SELECT function_name, small_read_reqs+large_read_reqs AS reads, small_write_reqs+large_write_reqs AS writes,\n       small_read_megabytes+large_read_megabytes+small_write_megabytes+large_write_megabytes AS mb\nFROM v$iostat_function ORDER BY mb DESC;","postgres":"SELECT sum(blks_read) AS reads, sum(blks_hit) AS hits FROM pg_stat_database;\nSELECT buffers_checkpoint, buffers_clean, buffers_backend FROM pg_stat_bgwriter;","mysql":"SHOW GLOBAL STATUS LIKE 'Innodb_data_read';\nSHOW GLOBAL STATUS LIKE 'Innodb_data_written';\nSHOW GLOBAL STATUS LIKE 'Innodb_data_reads';","mssql":"SELECT DB_NAME(database_id) AS db, io_stall_read_ms, io_stall_write_ms, num_of_reads, num_of_writes\nFROM sys.dm_io_virtual_file_stats(NULL, NULL)\nORDER BY io_stall_read_ms + io_stall_write_ms DESC;"},"choices":[{"label":"주로 읽기 IO","next":"read"},{"label":"주로 쓰기 IO","next":"write"}]},"read":{"kind":"step","title":"읽기 IO가 왜 높은가?","detail":"과도한 물리 읽기에는 세 가지 주요 원인이 있다: buffer cache가 너무 작음(working set이 RAM에 들어가지 않음), 쿼리가 전체 테이블 스캔을 수행함, 대규모 정렬/해시 작업이 임시 공간으로 spill됨.","command":{"oracle":"SELECT ROUND((1-(pr.value/(dbg.value+cg.value)))*100,2) AS buffer_hit_pct\nFROM v$sysstat pr, v$sysstat dbg, v$sysstat cg\nWHERE pr.name='physical reads' AND dbg.name='db block gets' AND cg.name='consistent gets';\nSELECT sql_id, disk_reads, executions FROM v$sql ORDER BY disk_reads DESC FETCH FIRST 10 ROWS ONLY;","postgres":"SELECT ROUND(sum(blks_hit)*100.0/nullif(sum(blks_hit)+sum(blks_read),0),2) AS cache_hit_pct FROM pg_stat_database;\nSELECT query, shared_blks_read, temp_blks_read FROM pg_stat_statements ORDER BY shared_blks_read DESC LIMIT 10;","mysql":"SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';\nSHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';\nSHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';","mssql":"SELECT CAST(100.0 * a.cntr_value / NULLIF(b.cntr_value, 0) AS decimal(5,2)) AS buffer_cache_hit_ratio\nFROM sys.dm_os_performance_counters a\nJOIN sys.dm_os_performance_counters b ON b.object_name = a.object_name AND b.counter_name = 'Buffer cache hit ratio base'\nWHERE a.counter_name = 'Buffer cache hit ratio' AND a.object_name LIKE '%Buffer Manager%';"},"choices":[{"label":"buffer cache 적중률이 낮고 working set이 들어가지 않음","next":"sol_cache"},{"label":"쿼리가 전체 테이블 스캔을 수행함","next":"sol_scan"},{"label":"대규모 정렬/해시가 임시 공간으로 spill됨","next":"sol_spill"}]},"write":{"kind":"step","title":"쓰기 IO가 왜 높은가?","detail":"쓰기 측 병목은 주기적인 checkpoint/flush가 짧은 시간 동안 디스크를 포화시키는 경우(checkpoint storm)이거나, 스토리지가 부하를 감당할 IOPS/처리량이 부족한 경우다.","command":{"oracle":"SELECT event, total_waits, ROUND(time_waited/100,1) AS sec\nFROM v$system_event\nWHERE event IN ('log file parallel write','db file parallel write','checkpoint completed')\nORDER BY time_waited DESC;","postgres":"-- PG17+:\nSELECT num_timed, num_requested, buffers_written FROM pg_stat_checkpointer;\n-- PG16 and earlier:\nSELECT checkpoints_timed, checkpoints_req, buffers_checkpoint, maxwritten_clean\nFROM pg_stat_bgwriter;\nSHOW checkpoint_completion_target;","mysql":"SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';\nSHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';\nSHOW ENGINE INNODB STATUS\\G","mssql":"SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name='Checkpoint pages/sec';\nSELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name='Page writes/sec';"},"choices":[{"label":"갑작스러운 checkpoint/flush 파동(checkpoint storm)","next":"sol_checkpoint"},{"label":"스토리지가 지속 부하를 따라가지 못함(IOPS 한도)","next":"sol_iops"}]},"sol_cache":{"kind":"solution","title":"근본 원인: buffer cache 부족","detail":"자주 접근하는 데이터 집합(working set)이 buffer cache에 들어가지 않아 블록을 디스크에서 계속 다시 읽는다. 적중률은 낮고 물리 읽기는 많다.","steps":["buffer/cache 크기를 늘린다: Oracle db_cache_size/SGA, Postgres shared_buffers, MySQL innodb_buffer_pool_size, MSSQL max server memory.","변경 전후의 적중률과 물리 읽기를 비교한다.","RAM이 물리적으로 부족하면 서버 메모리를 증설하거나 아카이빙/파티셔닝으로 working set을 줄인다.","실제 병목이 단일 스캔 패턴에서 온다면 cache-hit 트리의 스캔 분기로 넘어간다."],"related":["cache-hit-ratio"]},"sol_scan":{"kind":"solution","title":"근본 원인: 전체 테이블 스캔","detail":"적절한 인덱스가 없어 쿼리가 큰 테이블을 처음부터 끝까지 스캔한다. 이로 인해 물리 읽기가 많아지고 쓸모없는 블록이 캐시를 오염시킨다.","steps":["물리 읽기가 가장 많은 쿼리의 실행 계획(EXPLAIN)을 확인하고 전체 스캔을 찾는다.","필터 및 조인 컬럼에 적절한 인덱스를 추가하고 실행 계획이 인덱스 접근으로 바뀌는지 확인한다.","매우 큰 테이블은 날짜/범위 기반 파티셔닝으로 스캔하는 데이터를 줄인다.","불필요한 'SELECT *' 대신 필요한 컬럼만 선택해 읽는 블록 수를 줄인다."],"related":["slow-queries"]},"sol_spill":{"kind":"solution","title":"근본 원인: 임시 공간으로 spill되는 정렬/해시","detail":"대규모 sort, hash join, group by 작업이 작업 메모리에 들어가지 않아 임시 공간(temp/tempdb)에 기록되며 대량의 읽기/쓰기 IO를 발생시킨다.","steps":["작업 메모리 설정을 높인다: Oracle pga_aggregate_target, Postgres work_mem, MySQL sort_buffer_size/join_buffer_size, MSSQL memory grant.","작업량을 줄인다: 필터를 일찍 적용하고, 불필요한 정렬을 제거하며, 조인 순서를 개선한다.","자주 쓰는 정렬/그룹화에 적절한 인덱스를 추가해 디스크로의 spill을 방지한다.","임시 공간을 빠른 디스크로 옮긴다. 문제가 지속되면 undo/temp 압박 트리로 더 깊이 조사한다."],"related":["slow-queries"]},"sol_checkpoint":{"kind":"solution","title":"근본 원인: checkpoint storm","detail":"dirty page가 오랫동안 쌓였다가 짧은 간격에 대량으로 디스크에 기록된다. 이 갑작스러운 쓰기 파동이 주기적으로 IO를 포화시킨다.","steps":["checkpoint를 시간에 걸쳐 분산한다: Postgres에서는 checkpoint_completion_target을 높이고 checkpoint_timeout을 조정한다.","MySQL에서는 스토리지에 맞게 innodb_io_capacity / innodb_io_capacity_max를 설정하고 adaptive flushing을 튜닝한다.","MSSQL에서는 indirect checkpoint(target_recovery_time)로 갑작스러운 파동을 평탄화한다.","Oracle에서는 fast_start_mttr_target으로 checkpoint 빈도를 조정하고 redo log 파일을 키운다."],"related":[]},"sol_iops":{"kind":"solution","title":"근본 원인: IOPS / 처리량 부족","detail":"스토리지가 워크로드에 필요한 읽기+쓰기 요청을 처리할 IOPS나 대역폭이 부족하다. 큐 깊이와 지연이 지속적으로 높다.","steps":["디스크 지연(요청당 ms)을 측정하고 하드웨어/클라우드 IOPS 한도와 비교한다.","더 빠른 스토리지(HDD→SSD/NVMe)로 옮기거나 클라우드에서 프로비저닝 IOPS/처리량 등급을 높인다.","데이터, redo/로그, 임시 공간을 서로 다른 디스크에 분산해 경합을 줄인다.","영구적인 해결책으로 전체 IO 수요를 줄인다(인덱싱, 아카이빙, 쿼리 최적화)."],"related":[]}}},"solver/undo-temp-pressure.json":{"id":"undo-temp-pressure","title":"Undo / TempDB / 임시 공간 압박","icon":"🧯","category":"용량","intro":"undo/rollback 공간, 임시(temp) 공간 또는 TempDB가 압박을 받고 있다. 압박이 어디에 있는지 파악하고 대규모 정렬/해시, 긴 쿼리/트랜잭션, 공간 누수 같은 근본 원인으로 좁혀 간다.","start":"root","nodes":{"root":{"kind":"step","title":"압박은 어디인가: undo, temp, 아니면 tempdb 경합?","detail":"공간 사용량을 보고 압박의 원인을 분류한다: undo/rollback 테이블스페이스가 차고 있는가, 일반 임시(temp) 공간인가, 아니면 MSSQL에서 TempDB 페이지 할당 latch 경합인가?","command":{"oracle":"SELECT tablespace_name, ROUND(used_percent,1) AS used_pct\nFROM dba_tablespace_usage_metrics\nWHERE tablespace_name LIKE '%UNDO%' OR tablespace_name LIKE 'TEMP%';","postgres":"SELECT datname, temp_files, pg_size_pretty(temp_bytes) AS temp_written\nFROM pg_stat_database ORDER BY temp_bytes DESC;","mysql":"SELECT * FROM information_schema.INNODB_TEMP_TABLE_INFO;\nSHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';","mssql":"SELECT SUM(user_object_reserved_page_count)*8/1024 AS user_obj_mb,\n       SUM(internal_object_reserved_page_count)*8/1024 AS internal_obj_mb,\n       SUM(version_store_reserved_page_count)*8/1024 AS version_store_mb\nFROM sys.dm_db_file_space_usage;"},"choices":[{"label":"Undo/rollback 공간이 차고 있음(ORA-01555 / ORA-30036)","next":"undo"},{"label":"임시(temp) 공간이 차고 있음(sort/hash spill)","next":"temp"},{"label":"TempDB 할당 latch 경합(PFS/GAM/SGAM)","next":"tempdb"}]},"undo":{"kind":"step","title":"무엇이 undo를 소비하는가?","detail":"Undo 압박은 대개 두 가지 중 하나다: 긴 쿼리가 시작 시점 기준의 일관된 읽기를 위해 오래된 undo를 필요로 하거나(ORA-01555), 길거나 열린 트랜잭션이 undo를 너무 많이 생성해 공간을 채우고 ORA-30036으로 이어진다.","command":{"oracle":"SELECT s.sid, s.username, t.used_ublk*8/1024 AS undo_mb, s.status, t.start_time\nFROM v$transaction t JOIN v$session s ON s.taddr=t.addr\nORDER BY undo_mb DESC;\nSELECT MAX(maxquerylen) AS longest_query_sec, MAX(tuned_undoretention) AS tuned_retention FROM v$undostat;","postgres":"SELECT pid, now()-xact_start AS xact_age, state, left(query,60) AS query\nFROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start LIMIT 10;"},"choices":[{"label":"긴 쿼리가 오래된 undo를 필요로 함(ORA-01555)","next":"sol_undo_query"},{"label":"길거나 열린 트랜잭션이 undo를 축적 중(ORA-30036)","next":"sol_undo_tx"}]},"temp":{"kind":"step","title":"무엇이 임시 공간을 채우고 있는가?","detail":"Temp 압박은 대규모 정렬/해시 작업이 디스크로 spill되는 경우(정상이지만 거대함)이거나 단일 세션이 temp를 계속 보유하는 경우(열린 커서, 누수, 끝나지 않는 쿼리)에서 온다.","command":{"oracle":"SELECT s.sid, s.username, u.tablespace, u.blocks*8/1024 AS temp_mb, u.sqladdr\nFROM v$tempseg_usage u JOIN v$session s ON s.saddr=u.session_addr\nORDER BY temp_mb DESC;","postgres":"SELECT query, temp_blks_written, temp_blks_read\nFROM pg_stat_statements ORDER BY temp_blks_written DESC LIMIT 10;","mysql":"SELECT event_name, current_number_of_bytes_used\nFROM performance_schema.memory_summary_global_by_event_name\nWHERE event_name LIKE '%temptable%' OR event_name LIKE '%sort%';","mssql":"SELECT session_id, request_id, SUM(internal_objects_alloc_page_count)*8/1024 AS internal_mb\nFROM sys.dm_db_task_space_usage GROUP BY session_id, request_id ORDER BY internal_mb DESC;"},"choices":[{"label":"대규모 정렬/해시 작업이 디스크로 spill됨","next":"sol_temp_sort"},{"label":"단일 세션이 temp를 보유 중(누수/열린 커서)","next":"sol_temp_leak"}]},"tempdb":{"kind":"step","title":"TempDB 경합은 할당 문제인가, 용량 문제인가?","detail":"MSSQL TempDB에서는 두 가지 다른 문제가 발생한다: PFS/GAM/SGAM 페이지의 PAGELATCH 경합(다수의 작은 임시 객체), 또는 대규모 정렬/version store가 용량으로 TempDB를 채우는 경우.","command":{"mssql":"SELECT session_id, wait_type, wait_duration_ms, resource_description\nFROM sys.dm_os_waiting_tasks\nWHERE wait_type LIKE 'PAGELATCH%' AND resource_description LIKE '2:%';\nSELECT COUNT(*) AS tempdb_data_files FROM sys.master_files WHERE database_id=2 AND type=0;"},"choices":[{"label":"PFS/GAM/SGAM latch 경합","next":"sol_tempdb_latch"},{"label":"대규모 정렬/version store가 TempDB를 채움","next":"sol_temp_sort"}]},"sol_undo_query":{"kind":"solution","title":"근본 원인: 긴 쿼리 + undo 부족(ORA-01555)","detail":"긴 쿼리가 일관된 읽기에 필요로 하는 오래된 undo가 보존 기간 만료 또는 공간 부족으로 덮어써졌다.","steps":["undo_retention을 가장 긴 쿼리 소요 시간(v$undostat.maxquerylen)보다 큰 값으로 높인다.","보존 기간을 공간으로 뒷받침할 수 있도록 UNDO 테이블스페이스를 키우거나 autoextend를 활성화한다.","핵심 보고서의 경우 ALTER TABLESPACE undotbs RETENTION GUARANTEE로 오래된 undo를 보호한다(공간이 차면 DML이 실패할 수 있으므로 주의).","영구적인 해결을 위해 긴 쿼리를 최적화하거나 standby/replica로 옮긴다."],"related":["slow-queries"]},"sol_undo_tx":{"kind":"solution","title":"근본 원인: 길거나 열린 트랜잭션이 undo를 채움(ORA-30036)","detail":"단일 거대 DML이나 커밋이 지연된 열린 트랜잭션이 undo를 너무 많이 생성했다. UNDO 테이블스페이스가 확장되지 못해 ORA-30036이 발생했다.","steps":["undo를 가장 많이 보유한 활성 트랜잭션(v$transaction.used_ublk)을 식별하고 소유자를 찾는다.","단일 거대 DML을 날짜/범위 단위로 나누어 중간 커밋과 함께 실행한다.","커밋을 잊은 / idle-in-transaction 세션을 애플리케이션 측에서 수정한다. 필요하면 통제된 방식으로 종료한다.","UNDO 테이블스페이스에 임시로 공간을 추가한다. 영구적으로는 워크로드에 맞게 보존 기간과 공간 크기를 계획한다."],"related":["active-sessions"]},"sol_temp_sort":{"kind":"solution","title":"근본 원인: 대규모 정렬/해시가 임시 공간을 채움","detail":"메모리에 들어가지 않는 대규모 sort/hash/group by 작업이 임시 공간으로 spill되어 공간을 채울 수 있다.","steps":["작업 메모리를 늘린다: Oracle pga_aggregate_target, Postgres work_mem, MySQL sort_buffer_size, MSSQL memory grant.","쿼리를 개선한다: 불필요한 정렬을 제거하고, 필터를 일찍 적용하며, 정렬/그룹 컬럼에 인덱스를 추가한다.","매우 큰 작업은 청크로 나눈다. 모두가 동시에 temp를 보유하지 않도록 동시성을 제한한다.","임시 공간을 키우고 빠른 디스크로 옮긴다. MSSQL TempDB는 코어 수에 맞춰 여러 파일로 구성한다."],"related":["slow-queries"]},"sol_temp_leak":{"kind":"solution","title":"근본 원인: 세션이 임시 공간을 보유 중(누수)","detail":"닫히지 않은 커서, 끝나지 않는 쿼리, 열린 임시 테이블 때문에 단일 세션이 임시 공간을 해제하지 않는다.","steps":["temp를 가장 많이 보유한 세션과 실행 중인 SQL을 식별한다.","애플리케이션에서 커서/결과 집합과 임시 테이블이 제대로 닫히는지 확인한다.","멈추었거나 지나치게 긴 세션을 통제된 방식으로 종료하고 공간이 반환되는지 확인한다.","global temporary table은 세션/트랜잭션 종료 시 자동으로 정리되도록 정의한다."],"related":["active-sessions"]},"sol_tempdb_latch":{"kind":"solution","title":"근본 원인: TempDB PFS/GAM/SGAM latch 경합","detail":"다수의 세션이 동시에 작은 임시 객체를 생성하면 TempDB의 할당 맵 페이지(PFS/GAM/SGAM)에서 PAGELATCH 경합이 발생한다.","steps":["TempDB 데이터 파일 수를 CPU 코어 수에 비례하게 같은 크기로 구성한다(예: 4-8개 파일, 모두 같은 크기/autogrowth).","최신 SQL Server에서는 할당 경합을 줄이는 옵션(memory-optimized tempdb metadata)을 활성화한다.","임시 테이블 생성을 줄인다: 불필요한 #temp 테이블을 테이블 변수 / 단일 쿼리로 대체한다.","변경 후 PAGELATCH 대기 시간이 줄었는지 확인한다."],"related":["current-locks"]}}},"solver/long-transaction.json":{"id":"long-transaction","title":"장기 실행 / 열린 트랜잭션","icon":"⏱️","category":"락","intro":"장기 실행 중이거나 열린 트랜잭션이 락, undo, bloat를 축적하고 있다. 문제의 트랜잭션을 찾아 유형(idle in transaction, 긴 배치, 영향)을 파악하고 안전하게 해결한다.","start":"root","nodes":{"root":{"kind":"step","title":"가장 오래된 열린 트랜잭션은 무엇인가?","detail":"열린 트랜잭션을 시작 시각 순으로 조회한다. 가장 오래된 것이 대개 문제의 원인이다. 상태를 확인한다: 실제로 실행 중인가, 아니면 커밋을 기다리는 'idle in transaction'인가?","command":{"oracle":"SELECT s.sid, s.serial#, s.username, s.status, t.start_time, t.used_ublk*8/1024 AS undo_mb\nFROM v$transaction t JOIN v$session s ON s.taddr=t.addr\nORDER BY t.start_time;","postgres":"SELECT pid, state, now()-xact_start AS xact_age, now()-state_change AS since_change, left(query,60) AS query\nFROM pg_stat_activity\nWHERE xact_start IS NOT NULL ORDER BY xact_start LIMIT 20;","mysql":"SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND,trx_started,NOW()) AS secs, trx_mysql_thread_id, trx_rows_locked\nFROM information_schema.INNODB_TRX ORDER BY trx_started;","mssql":"SELECT s.session_id, s.login_name, t.transaction_begin_time,\n       DATEDIFF(second,t.transaction_begin_time,GETDATE()) AS secs\nFROM sys.dm_tran_active_transactions t\nJOIN sys.dm_tran_session_transactions st ON st.transaction_id=t.transaction_id\nJOIN sys.dm_exec_sessions s ON s.session_id=st.session_id\nORDER BY t.transaction_begin_time;"},"choices":[{"label":"idle in transaction(커밋을 잊고 유휴 상태)","next":"idle"},{"label":"활성 상태이지만 매우 오래 실행되는 배치/DML","next":"batch"},{"label":"트랜잭션이 계속되는 동안 락/bloat/undo가 축적됨","next":"impact"}]},"idle":{"kind":"step","title":"idle in transaction은 왜 발생하는가?","detail":"세션이 트랜잭션을 열었지만 쿼리를 실행하지 않고 유휴 상태로 있다. 즉 commit/rollback을 잊었다. 이로 인해 락과 undo가 불필요하게 유지된다. 애플리케이션 수정으로 충분한가, 아니면 긴급히 종료해야 하는가?","command":{"postgres":"SELECT pid, now()-state_change AS idle_age, left(query,80) AS last_query\nFROM pg_stat_activity WHERE state='idle in transaction' ORDER BY state_change;","mysql":"SELECT trx.trx_mysql_thread_id, trx.trx_started, p.time AS idle_secs, p.command\nFROM information_schema.INNODB_TRX trx\nJOIN information_schema.PROCESSLIST p ON p.id=trx.trx_mysql_thread_id\nWHERE p.command='Sleep' ORDER BY trx.trx_started;","oracle":"SELECT sid, serial#, status, last_call_et AS idle_secs\nFROM v$session WHERE status='INACTIVE' AND type='USER' AND last_call_et>300;","mssql":"SELECT s.session_id, s.status, DATEDIFF(second,s.last_request_end_time,GETDATE()) AS idle_secs\nFROM sys.dm_exec_sessions s\nWHERE s.session_id IN (SELECT session_id FROM sys.dm_tran_session_transactions)\n  AND s.status='sleeping';"},"choices":[{"label":"애플리케이션/커넥션 풀이 커밋하지 않음(영구 해결)","next":"sol_idle"},{"label":"지금 바로 종료해야 함","next":"sol_kill"}]},"batch":{"kind":"step","title":"활성 상태의 긴 트랜잭션은 무엇을 하고 있는가?","detail":"세션은 실제로 실행 중(active)이다. 단일 거대 DML/보고서인가, 아니면 다른 세션을 기다리게 해서 긴급히 끊어야 하는 블로커인가?","command":{"oracle":"SELECT s.sid, s.serial#, sq.sql_text, s.last_call_et AS running_secs\nFROM v$session s JOIN v$sql sq ON sq.sql_id=s.sql_id\nWHERE s.status='ACTIVE' AND s.type='USER' ORDER BY s.last_call_et DESC;","postgres":"SELECT pid, now()-query_start AS run_time, left(query,80) AS query\nFROM pg_stat_activity WHERE state='active' ORDER BY query_start LIMIT 10;","mysql":"SELECT id, time AS run_secs, state, LEFT(info,80) AS query\nFROM information_schema.PROCESSLIST WHERE command<>'Sleep' ORDER BY time DESC;","mssql":"SELECT r.session_id, r.status, r.total_elapsed_time/1000 AS secs, SUBSTRING(t.text,1,80) AS query\nFROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t\nORDER BY r.total_elapsed_time DESC;"},"choices":[{"label":"단일 거대 DML/보고서, 분할이 필요함","next":"sol_batch"},{"label":"다른 세션을 기다리게 함, 긴급히 종료","next":"sol_kill"}]},"impact":{"kind":"step","title":"긴 트랜잭션은 무엇을 축적하고 있는가?","detail":"긴 트랜잭션의 부작용을 파악한다: 주로 다른 세션을 기다리게 하는 락 적체인가, 아니면 오래된 행 버전을 유지해 정리(VACUUM/undo)를 막아 생기는 bloat인가?","command":{"postgres":"SELECT pid, pg_blocking_pids(pid) AS blocked_by FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid))>0;\nSELECT max(age(backend_xmin)) AS max_xmin_age FROM pg_stat_activity WHERE backend_xmin IS NOT NULL;","oracle":"SELECT blocking_session, sid, serial#, seconds_in_wait, event\nFROM v$session WHERE blocking_session IS NOT NULL;","mssql":"SELECT session_id, blocking_session_id, wait_type, wait_time\nFROM sys.dm_exec_requests WHERE blocking_session_id<>0;"},"choices":[{"label":"락 적체로 다른 세션이 대기 중","next":"sol_lock_backlog"},{"label":"오래된 버전 누적이 정리를 막음(undo/bloat)","next":"sol_bloat"}]},"sol_idle":{"kind":"solution","title":"근본 원인: 애플리케이션이 커밋하지 않음(idle in transaction)","detail":"커넥션 풀이나 애플리케이션이 트랜잭션을 연 뒤 commit/rollback 없이 연결을 유휴 상태로 유지한다. 이로 인해 락과 undo가 불필요하게 유지된다.","steps":["애플리케이션 코드에서 트랜잭션을 여는 모든 경로에 commit/rollback을 보장한다. 트랜잭션 내에서 사용자 입력이나 긴 IO를 기다리지 않는다.","유휴 트랜잭션을 자동으로 끊는다: Postgres idle_in_transaction_session_timeout, MySQL 연결/timeout 설정, 애플리케이션 풀 maxLifetime.","커넥션 풀을 'autocommit 활성, 짧은 트랜잭션' 원칙에 맞춰 구성한다.","모니터링을 구성한다: 긴 idle-in-transaction 세션에 대해 알림을 발생시킨다."],"related":["active-sessions","blocking-sessions"]},"sol_kill":{"kind":"solution","title":"해결: 문제 세션을 안전하게 종료","detail":"블로킹/장기 세션이 다른 세션을 기다리게 한다면 통제된 방식으로 종료해야 한다. 종료는 롤백을 유발하며 공간/시간이 필요할 수 있으므로 주의한다.","steps":["중요한 작업을 끊지 않도록 먼저 세션의 정체(어떤 사용자/애플리케이션, 어떤 작업)를 확인한다.","종료: Oracle ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE; Postgres SELECT pg_terminate_backend(pid); MySQL KILL thread_id; MSSQL KILL session_id;","대규모 쓰기 트랜잭션을 종료했다면 롤백(undo/로그 되돌리기)이 완료될 때까지 기다린다.","종료 후 락과 대기 세션이 해소되었는지 확인하고, 근본 원인(idle/배치)을 영구적으로 수정한다."],"related":["blocking-sessions"]},"sol_batch":{"kind":"solution","title":"근본 원인: 단일 거대 DML/보고서","detail":"단일 트랜잭션에서 수백만 행을 처리하는 DML이나 매우 긴 보고서는 실행 시간 내내 락과 undo/bloat를 축적한다.","steps":["대규모 DML을 날짜/범위/키 단위 청크로 나누고 각 청크 후에 커밋한다.","무거운 보고서는 피크 시간 이후로 옮기거나 standby/읽기 복제본으로 오프로드한다.","긴 쿼리를 최적화해 전체 소요 시간(따라서 락/undo 보유 시간)을 줄인다.","배치 작업을 관측 가능하게 한다: 진행 상황을 기록하고 중단된 지점부터 재개할 수 있게 한다."],"related":["slow-queries"]},"sol_lock_backlog":{"kind":"solution","title":"근본 원인: 락 적체 대기열","detail":"보유한 락 때문에 긴 트랜잭션 뒤에 대기열이 생겼다. 다수의 세션이 같은 리소스를 기다리고 있다.","steps":["락 체인의 선두 세션(head blocker)을 식별한다. 대기열을 해소할 수 있는 유일한 노드다.","head blocker를 짧게 끝내거나(commit/rollback) 통제된 방식으로 종료하고 대기열이 줄어드는지 지켜본다.","락 유지 시간을 줄이려면 트랜잭션을 작게 만들고 일관된 접근 순서를 정의한다.","적절한 경우 더 약한 락이 필요한 방식(행 수준, 적절한 격리 수준)으로 전환한다."],"related":["blocking-sessions","current-locks"]},"sol_bloat":{"kind":"solution","title":"근본 원인: 오래된 버전 누적(undo/bloat)","detail":"장기 실행 트랜잭션이 그보다 앞선 오래된 행 버전의 정리를 막는다. Postgres에서는 테이블/인덱스 bloat로, Oracle에서는 undo 압박으로 나타난다.","steps":["가장 오래된 xmin/트랜잭션을 보유한 세션을 찾는다(Postgres backend_xmin, Oracle의 가장 오래된 활성 트랜잭션).","이 트랜잭션을 짧게 하거나 종료해 정리(VACUUM / undo 재사용)가 다시 진행되도록 한다.","Postgres에서는 트랜잭션 종료 후 bloat된 테이블에 VACUUM을 실행한다(필요 시 VACUUM FULL/pg_repack).","트랜잭션을 짧게 유지하는 애플리케이션 패턴을 영구적으로 도입하고 autovacuum을 더 적극적으로 설정한다."],"related":[]}}},"solver/plan-regression.json":{"id":"plan-regression","title":"쿼리 실행 계획 회귀(갑자기 느려짐)","icon":"📉","category":"성능","intro":"어제 빨랐던 쿼리가 오늘 갑자기 느리다. 대개 옵티마이저가 더 나쁜 실행 계획으로 바꾼 것이다. 오래된 통계, 파라미터 민감도, 데이터 양 증가, 갑작스러운 실행 계획 변경 중 근본 원인을 찾아 실행 계획을 안정화한다.","start":"root","nodes":{"root":{"kind":"step","title":"실행 계획이 정말 바뀌었는가, 언제인가?","detail":"같은 쿼리에 여러 실행 계획(plan_hash/plan_generation)이 있는가, 느려지기 시작한 시점은 언제인가? 최근 배포, 업그레이드, 파라미터 변경, 대량 데이터 적재와 일치하는가?","command":{"oracle":"SELECT sql_id, plan_hash_value, executions, ROUND(elapsed_time/GREATEST(executions,1)/1e6,3) AS sec_per_exec, first_load_time\nFROM v$sql WHERE sql_id='&sqlid' ORDER BY first_load_time;","postgres":"EXPLAIN (ANALYZE, BUFFERS) SELECT /* your query */ 1;\n-- Trend: SELECT query, calls, mean_exec_time FROM pg_stat_statements WHERE query LIKE '%...%';","mysql":"EXPLAIN FORMAT=JSON SELECT /* your query */ 1;\nSELECT DIGEST_TEXT, COUNT_STAR, AVG_TIMER_WAIT/1e9 AS avg_ms\nFROM performance_schema.events_statements_summary_by_digest WHERE DIGEST_TEXT LIKE '%...%';","mssql":"SELECT qs.plan_generation_num, qs.execution_count, qs.total_worker_time/qs.execution_count AS avg_cpu_us, qs.creation_time\nFROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) t\nWHERE t.text LIKE '%...%';"},"choices":[{"label":"통계가 오래되었을 수 있음","next":"stats"},{"label":"같은 쿼리가 파라미터에 따라 다르게 동작함(sniffing/peeking)","next":"sniff"},{"label":"데이터 양이 크게 증가함","next":"sol_datagrowth"},{"label":"업그레이드/flush 후 실행 계획이 갑자기 바뀜","next":"sol_planflip"}]},"stats":{"kind":"step","title":"통계가 최신인가?","detail":"옵티마이저는 통계를 기반으로 결정한다. 마지막 analyze 날짜가 오래되었거나 변경 카운터가 높다면 추정치가 실제로 어긋났을 수 있으며, 이는 나쁜 실행 계획으로 이어진다.","command":{"oracle":"SELECT table_name, num_rows, last_analyzed, stale_stats\nFROM dba_tab_statistics WHERE table_name='T';","postgres":"SELECT relname, n_live_tup, n_mod_since_analyze, last_analyze, last_autoanalyze\nFROM pg_stat_user_tables WHERE relname='t';","mysql":"SELECT table_name, update_time, table_rows FROM information_schema.tables WHERE table_name='t';\nSHOW INDEX FROM t;","mssql":"SELECT s.name, STATS_DATE(s.object_id,s.stats_id) AS last_updated, p.modification_counter, p.rows\nFROM sys.stats s CROSS APPLY sys.dm_db_stats_properties(s.object_id,s.stats_id) p\nWHERE s.object_id=OBJECT_ID('dbo.t');"},"choices":[{"label":"통계가 오래되었거나 없음","next":"sol_stats"},{"label":"통계는 최신이지만 실행 계획이 여전히 파라미터에 민감함","next":"sniff"}]},"sniff":{"kind":"step","title":"파라미터 민감도가 있는가?","detail":"쿼리는 첫 실행 시의 파라미터 값을 기준으로 실행 계획을 선택하고 캐싱한다(MSSQL parameter sniffing / Oracle bind peeking). 그 실행 계획은 분포가 치우친 다른 값에 대해 나쁠 수 있다.","command":{"oracle":"SELECT sql_id, child_number, plan_hash_value, is_bind_sensitive, is_bind_aware\nFROM v$sql WHERE sql_id='&sqlid';","mssql":"SELECT qs.query_hash, qs.execution_count, qs.total_worker_time/qs.execution_count AS avg_cpu_us\nFROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) t\nWHERE t.text LIKE '%...%';","postgres":"-- For generic vs custom plan behavior:\nPREPARE p AS SELECT /* your query */ 1;\nEXPLAIN EXECUTE p;"},"choices":[{"label":"예, 파라미터에 따라 실행 계획이 좋고 나쁨을 오감","next":"sol_sniff"},{"label":"올바른 실행 계획을 알고 있으며 영구적으로 고정하고 싶음","next":"sol_forceplan"}]},"sol_stats":{"kind":"solution","title":"근본 원인: 오래된 / 누락된 통계","detail":"실제와 맞지 않는 오래된 통계를 바탕으로 옵티마이저가 행 수를 잘못 추정해 나쁜 실행 계획을 선택했다. 실행 계획 회귀의 가장 흔한 원인이다.","steps":["영향을 받은 테이블과 인덱스의 통계를 새로 수집한다: Oracle DBMS_STATS.GATHER_TABLE_STATS, Postgres ANALYZE, MySQL ANALYZE TABLE, MSSQL UPDATE STATISTICS ... WITH FULLSCAN.","분포가 치우친 컬럼에 히스토그램/확장 통계를 생성한다.","통계 수집 후 EXPLAIN으로 실행 계획을 확인하고 좋은 실행 계획으로 돌아왔는지 확인한다.","재발 방지를 위해 자동 통계 수집(autovacuum/auto update stats)을 적절한 임계값으로 활성화해 둔다."],"related":["slow-queries"]},"sol_sniff":{"kind":"solution","title":"근본 원인: 파라미터 민감도(sniffing / bind peeking)","detail":"캐시된 실행 계획은 처음 들어온 파라미터 값에 맞춰 선택되었으며 분포가 치우친 다른 값에는 나쁘다. 목표는 값에 맞는 실행 계획이 선택되도록 하는 것이다.","steps":["MSSQL에서는 핵심 쿼리에 OPTION (RECOMPILE) 또는 OPTIMIZE FOR로 대상 동작을 정의한다. 자주 바뀌는 값에는 recompile을 선호한다.","Oracle에서는 adaptive cursor sharing / bind-aware 동작을 사용한다. 필요하면 값 범위별로 쿼리를 분리한다.","Postgres에서는 generic vs custom plan 선택을 관찰한다. 필요하면 plan_cache_mode로 유도한다.","단일하고 정확한 영구 실행 계획이 필요하면 실행 계획 고정 솔루션으로 넘어간다."],"related":["slow-queries"]},"sol_forceplan":{"kind":"solution","title":"해결: 올바른 실행 계획 고정(baseline / force plan)","detail":"검증된 좋은 실행 계획이 있다면 옵티마이저가 벗어나지 않도록 고정해 회귀를 영구적으로 막는다.","steps":["Oracle: 좋은 실행 계획에 대해 SQL Plan Baseline을 생성하거나(DBMS_SPM) SQL Plan Management를 통해 수락한다.","MSSQL: Query Store에서 좋은 실행 계획을 선택하고 sp_query_store_force_plan으로 강제한다.","MySQL/Postgres: 옵티마이저 힌트(예: index hint) 또는 pg_hint_plan으로 실행 계획을 유도한다.","고정 후 성능을 모니터링한다. 데이터 패턴이 근본적으로 바뀌면 baseline을 재평가한다."],"related":[]},"sol_datagrowth":{"kind":"solution","title":"근본 원인: 데이터 양이 증가해 실행 계획이 더 이상 맞지 않음","detail":"테이블이 커지면서 예전에 이상적이던 실행 계획(예: nested loop)이 이제는 잘 확장되지 않는다. 옵티마이저에는 증가를 정확히 반영하는 통계와 접근 경로가 필요하다.","steps":["옵티마이저가 새로운 행 수를 볼 수 있도록 최신 통계를 수집한다.","증가하는 테이블에 적절한 인덱스를 추가하고, 이제 잘 확장되지 않는 접근 경로(예: full scan/nested loop)를 수정한다.","매우 큰 테이블은 날짜/범위 기반 파티셔닝으로 스캔 데이터를 제한한다.","증가에 맞게 쿼리를 재작성한다(조기 필터링, 페이지네이션, 요약/materialized view)."],"related":["slow-queries"]},"sol_planflip":{"kind":"solution","title":"근본 원인: 갑작스러운 실행 계획 변경(업그레이드 / 캐시 flush)","detail":"버전 업그레이드, 파라미터 변경, plan cache 비우기 후 실행 계획이 갑자기 바뀌었다. 옵티마이저 동작이나 환경 조건이 달라졌다.","steps":["변경의 계기를 식별한다: 업그레이드/패치, 옵티마이저 파라미터, 데이터 적재, 재시작/캐시 flush.","파라미터가 원인이라면 변경을 되돌리고 통제된 방식으로 재평가한다.","옵티마이저가 다시 벗어나지 않도록 검증된 좋은 실행 계획을 고정한다(baseline / force plan).","업그레이드 후 회귀의 경우 옵티마이저 호환성/준비 설정을 검토한다."],"related":[]}}},"solver/replica-resync.json":{"id":"replica-resync","title":"복제본 손상 / 재동기화 필요","icon":"🔗","category":"복제","intro":"복제본/standby와 primary 간 동기화가 깨졌다. 복제가 중지되었는지, 얼마나 뒤처졌는지, 위치/GTID 체인이 깨졌는지 파악한 뒤 오류 건너뛰기, 대기, 재구축(reseed) 중 올바른 경로를 선택한다. split-brain 경고를 포함한다.","start":"root","nodes":{"root":{"kind":"step","title":"복제의 전반적인 상태는 어떠한가?","detail":"먼저 복제 상태를 확인한다: 적용 스레드가 실행 중인가, 지연은 얼마나 큰가, 오류가 있는가? 또한 두 노드가 모두 쓰기를 받고 있지 않은지(split-brain) 확인한다.","command":{"mysql":"SHOW REPLICA STATUS\\G\n-- Check: Replica_IO_Running, Replica_SQL_Running, Seconds_Behind_Source, Last_Error, Retrieved/Executed_Gtid_Set","postgres":"-- On the primary:\nSELECT client_addr, state, sent_lsn, replay_lsn, pg_wal_lsn_diff(sent_lsn,replay_lsn) AS lag_bytes FROM pg_stat_replication;\n-- On the replica:\nSELECT status, last_msg_receipt_time, latest_end_lsn FROM pg_stat_wal_receiver;","oracle":"SELECT name, value, unit, time_computed FROM v$dataguard_stats WHERE name IN ('transport lag','apply lag');\nSELECT dest_id, status, error, gap_status FROM v$archive_dest_status WHERE status<>'INACTIVE';","mssql":"SELECT ag.name AS ag, drs.synchronization_state_desc, drs.synchronization_health_desc,\n       drs.log_send_queue_size, drs.redo_queue_size\nFROM sys.dm_hadr_database_replica_states drs\nJOIN sys.availability_groups ag ON ag.group_id=drs.group_id;"},"choices":[{"label":"복제가 중지됨 / 실패 중","next":"error"},{"label":"실행 중이지만 크게 뒤처짐(큰 지연)","next":"lag"},{"label":"GTID/위치가 깨지고 불일치 — 재구축","next":"sol_reseed"},{"label":"두 노드가 모두 primary로 쓰기 중(split-brain 의심)","next":"sol_splitbrain"}]},"error":{"kind":"step","title":"오류를 복구할 수 있는가, 재구축이 필요한가?","detail":"적용 오류를 검토한다. 일부 오류(duplicate key, missing row)는 단일 이벤트를 안전하게 건너뛰어 해결할 수 있다. 그러나 데이터 불일치/손상이 있다면 유일하게 올바른 방법은 복제본을 재구축하는 것이다.","command":{"mysql":"SHOW REPLICA STATUS\\G\n-- Check: Last_SQL_Errno, Last_SQL_Error, Last_IO_Errno, Last_IO_Error","postgres":"-- Examine the replica logs (PostgreSQL log): recovery / WAL apply error messages.\nSELECT status, last_msg_receipt_time FROM pg_stat_wal_receiver;","mssql":"SELECT * FROM sys.dm_hadr_automatic_seeding;\nSELECT database_id, synchronization_state_desc, is_suspended, suspend_reason_desc\nFROM sys.dm_hadr_database_replica_states WHERE is_local=1;"},"choices":[{"label":"단일/소수의 적용 오류(건너뛰기 가능)","next":"sol_skiperror"},{"label":"복구 불가능한 불일치 / 손상","next":"sol_reseed"}]},"lag":{"kind":"step","title":"지연은 일시적인가, 지속적인가?","detail":"시간에 따른 지연 추세를 확인한다. 일시적인 부하 급증 후 지연이 줄어드는가(따라잡는가), 아니면 계속 증가해 복제본이 primary를 따라잡지 못하는가?","command":{"mysql":"SHOW REPLICA STATUS\\G\n-- Measure Seconds_Behind_Source several times: is it decreasing or increasing?","postgres":"-- Measure repeatedly on the replica:\nSELECT now()-pg_last_xact_replay_timestamp() AS replay_lag;","oracle":"SELECT name, value, time_computed FROM v$dataguard_stats WHERE name='apply lag';","mssql":"SELECT drs.redo_queue_size, drs.redo_rate, drs.log_send_queue_size\nFROM sys.dm_hadr_database_replica_states drs WHERE drs.is_local=0;"},"choices":[{"label":"일시적 부하 급증, 지연이 줄어드는 중","next":"sol_lag_wait"},{"label":"계속 증가, 따라잡지 못함","next":"sol_lag_perm"}]},"sol_skiperror":{"kind":"solution","title":"해결: 적용 오류를 신중하게 건너뛰기","detail":"단일 적용 오류(예: duplicate key)로 복제가 중지되었고 데이터가 실제로 일관됨을 확신한다면 해당 이벤트를 건너뛰고 스트림을 계속할 수 있다. 이는 최후 수단이며, 무작정 건너뛰면 조용한 데이터 불일치가 생긴다.","steps":["먼저 오류가 발생한 이유를 이해하고, 영향을 받은 행을 양쪽에서 비교해 데이터 일관성을 확인한다.","GTID를 사용하지 않는 MySQL에서 단일 이벤트 건너뛰기: STOP REPLICA; SET GLOBAL sql_replica_skip_counter=1; START REPLICA; (8.0.26 이전: sql_slave_skip_counter; 신중하게).","GTID를 사용하는 경우 실패한 트랜잭션을 건너뛰려면 해당 GTID를 빈 트랜잭션으로 표시한 뒤 복제본을 시작한다.","건너뛴 후 양쪽 간 행 수준 일관성 검사(checksum)를 실행한다. 불일치가 있으면 reseed로 넘어간다."],"related":["replication-status"]},"sol_reseed":{"kind":"solution","title":"해결: 복제본 재구축(reseed / rebuild)","detail":"위치/GTID 체인이 깨졌거나, 데이터가 어긋났거나, 복구 불가능한 오류가 있다면 유일하게 신뢰할 수 있는 방법은 primary에서 새로 받은 사본으로 복제본을 재구축하는 것이다.","steps":["primary에서 일관된 base backup을 수행한다: MySQL xtrabackup/mysqldump 또는 CLONE, Postgres pg_basebackup, Oracle RMAN duplicate, MSSQL full+log backup.","복제본의 이전 상태를 정리하고 백업을 복구한다(Postgres pg_basebackup -R, MSSQL RESTORE ... WITH NORECOVERY).","올바른 위치/GTID에서 복제를 시작한다: MySQL CHANGE REPLICATION SOURCE TO ...; START REPLICA; MSSQL은 데이터베이스를 AG에 다시 추가한다(automatic seeding).","따라잡는 과정을 모니터링한다. 지연이 0이 되면 checksum으로 양쪽 일관성을 확인한다."],"related":["replication-status"]},"sol_lag_wait":{"kind":"solution","title":"근본 원인: 일시적 부하 급증(지연이 줄어드는 중)","detail":"복제는 정상이다. primary의 일시적인 쓰기 폭증이나 네트워크 변동으로 지연이 생겼지만 복제본이 따라잡고 있다.","steps":["여러 번 측정해 지연 추세가 실제로 감소하는지 확인한다.","부하 구간이 끝날 때까지 필요하면 지연에 민감한 읽기만 primary로 라우팅한다.","재발한다면 쓰기 폭증(배치 작업, ETL)을 시간에 걸쳐 분산하거나 피크 시간 외로 옮긴다.","일시적 지연이 지속적으로 바뀌기 전에 파악할 수 있도록 네트워크/적용 지연을 모니터링에 추가한다."],"related":["replication-status"]},"sol_lag_perm":{"kind":"solution","title":"근본 원인: 복제본이 primary를 따라가지 못함","detail":"적용(apply/redo) 측이 primary가 생성하는 변경 속도를 따라가지 못해 지연이 계속 증가한다. 대개 단일 스레드 적용, 느린 복제본 하드웨어 또는 네트워크 병목이 원인이다.","steps":["적용 병목을 찾는다: 단일 스레드 적용인가, 복제본의 디스크/CPU인가, 아니면 네트워크 대역폭이 제한하는가?","병렬 적용을 활성화/증가한다: MySQL에서는 다수의 replica worker(replica_parallel_workers). Postgres/Oracle에서는 적용 리소스를 검토한다.","복제본 하드웨어를 최소한 primary 수준으로 맞춘다(특히 디스크 IOPS).","복제본의 불필요한 무거운 읽기 쿼리를 줄여 적용에 리소스를 남긴다. 그래도 따라잡지 못하면 reseed 후 재평가한다."],"related":["replication-status"]},"sol_splitbrain":{"kind":"solution","title":"경고: split-brain(두 개의 primary) — 데이터 손실 위험","detail":"두 노드가 모두 primary 역할로 쓰기를 받으면 양쪽에 충돌하는 변경이 쌓인다. 가장 위험한 상황이며 데이터 손실로 이어질 수 있다. 쓰기를 단일 노드로 줄이는 것이 최우선이다.","steps":["즉시 쓰기를 단일 노드로 통합한다: 애플리케이션 트래픽을 엄격히 하나의 primary로 라우팅하고, 다른 노드는 읽기 전용으로 만들거나 격리한다.","기준(올바른) 노드를 식별한다: 업무 관점에서 어느 쪽 데이터를 유효한 것으로 볼지 결정한다.","양쪽에서 충돌하는(분기된) 변경을 비교한다. 탈락한 쪽의 중요한 변경은 수동으로 조정한다.","기준 노드를 원본으로 하여 다른 노드를 reseed로 재구축한다. 이후 이중 primary를 방지하는 fencing/quorum 메커니즘을 구성한다."],"related":["replication-status"]}}},"solver/cache-hit-low.json":{"id":"cache-hit-low","title":"낮은 캐시 적중률(buffer/cache hit)","icon":"🎯","category":"성능","intro":"캐시 적중률이 낮아 데이터가 계속 디스크(또는 원본)에서 읽힌다. 문제가 DB buffer/shared cache에 있는지 Redis/애플리케이션 캐시에 있는지 파악한 뒤 측정하고 수정한다.","start":"root","nodes":{"root":{"kind":"step","title":"어떤 캐시의 적중률이 낮은가?","detail":"적중률을 측정하고 문제를 분리한다: 데이터베이스 buffer/shared cache가 낮은가(물리 읽기가 많음), 아니면 Redis/애플리케이션 캐시에서 miss와 eviction이 많은가?","command":{"oracle":"SELECT ROUND((1-(pr.value/(dbg.value+cg.value)))*100,2) AS buffer_hit_pct\nFROM v$sysstat pr, v$sysstat dbg, v$sysstat cg\nWHERE pr.name='physical reads' AND dbg.name='db block gets' AND cg.name='consistent gets';","postgres":"SELECT ROUND(sum(blks_hit)*100.0/nullif(sum(blks_hit)+sum(blks_read),0),2) AS cache_hit_pct\nFROM pg_stat_database;","mysql":"SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';\nSHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';","mssql":"SELECT cntr_value AS buffer_cache_hit_ratio\nFROM sys.dm_os_performance_counters\nWHERE counter_name='Buffer cache hit ratio' AND object_name LIKE '%Buffer Manager%';","redis":"INFO stats\n# Check the keyspace_hits / keyspace_misses ratio and evicted_keys in INFO memory"},"choices":[{"label":"DB buffer/shared cache 적중률이 낮음","next":"dbcache"},{"label":"Redis/애플리케이션 캐시에서 miss / eviction이 많음","next":"rediscache"}]},"dbcache":{"kind":"step","title":"왜 디스크에서 읽는가: 크기인가, 스캔 패턴인가?","detail":"낮은 buffer 적중률에는 두 가지 대표 원인이 있다: 자주 접근하는 데이터(working set)가 메모리에 들어가지 않거나(캐시가 너무 작음), 대규모 전체 테이블 스캔이 일회용 블록으로 캐시를 오염시켜 유용한 페이지를 밀어낸다.","command":{"oracle":"SELECT component, current_size/1024/1024 AS mb FROM v$sga_dynamic_components WHERE component LIKE '%buffer cache%';\nSELECT sql_id, disk_reads, executions FROM v$sql ORDER BY disk_reads DESC FETCH FIRST 10 ROWS ONLY;","postgres":"SHOW shared_buffers;\nSELECT pg_size_pretty(pg_database_size(current_database())) AS db_size;\nSELECT query, shared_blks_read FROM pg_stat_statements ORDER BY shared_blks_read DESC LIMIT 10;","mysql":"SHOW VARIABLES LIKE 'innodb_buffer_pool_size';\nSELECT ROUND(SUM(data_length+index_length)/1024/1024) AS data_mb FROM information_schema.tables;","mssql":"SELECT physical_memory_kb/1024 AS server_ram_mb FROM sys.dm_os_sys_info;\nSELECT COUNT(*)*8/1024 AS buffer_pool_mb FROM sys.dm_os_buffer_descriptors;"},"choices":[{"label":"working set이 RAM에 들어가지 않음(캐시가 너무 작음)","next":"sol_sizing"},{"label":"전체 테이블 스캔이 캐시를 오염시킴","next":"sol_scan"}]},"rediscache":{"kind":"step","title":"Redis miss의 원인: maxmemory eviction인가, TTL/키 관리인가?","detail":"Redis 적중률이 낮다면 메모리가 maxmemory에 도달해 키가 eviction되고 있거나, 키의 TTL 관리/캐싱 패턴이 약하다(콜드 키, 낮은 재사용).","command":{"redis":"INFO memory\n# used_memory, maxmemory, maxmemory_policy, evicted_keys\nCONFIG GET maxmemory-policy\nINFO keyspace\n# Key count and expires (keys with TTL) ratio per db"},"choices":[{"label":"maxmemory 가득 참, 높은 eviction","next":"sol_maxmem"},{"label":"약한 TTL/키 패턴(낮은 재사용)","next":"sol_ttl"}]},"sol_sizing":{"kind":"solution","title":"근본 원인: working set에 비해 캐시가 너무 작음","detail":"자주 접근하는 데이터가 할당된 buffer/cache보다 커서 계속 밀려나고 디스크에서 다시 읽힌다. 적중률은 낮고 물리 읽기는 많다.","steps":["working set을 포괄하도록 캐시를 키운다: Oracle db_cache_size/SGA, Postgres shared_buffers, MySQL innodb_buffer_pool_size, MSSQL max server memory.","물리 RAM이 부족하면 서버 메모리를 증설한다. 이는 DB 확장의 핵심 리소스다.","working set을 줄인다: 오래된 데이터를 아카이브/파티셔닝하고 불필요한 컬럼/인덱스를 정리한다.","변경 후 적중률과 물리 읽기를 비교해 개선을 확인한다."],"related":["cache-hit-ratio"]},"sol_scan":{"kind":"solution","title":"근본 원인: 스캔 패턴이 캐시를 오염시킴","detail":"대규모 전체 테이블 스캔이 한 번 사용되고 버려지는 블록으로 캐시를 채워 핫 페이지를 밀어낸다. 이로 인해 적중률이 떨어진다.","steps":["물리 읽기가 가장 많은 쿼리의 실행 계획을 확인하고 전체 스캔을 인덱스 접근으로 바꾼다(적절한 인덱스 추가).","'SELECT *' 대신 필요한 컬럼만 선택해 스캔하는 블록 양을 줄인다.","큰 테이블은 파티셔닝으로 스캔을 관련 범위로 좁힌다.","무거운 분석/스캔 작업을 OLTP 캐시와 분리한다(읽기 복제본 또는 별도 시간대)."],"related":["slow-queries","cache-hit-ratio"]},"sol_maxmem":{"kind":"solution","title":"근본 원인: Redis maxmemory eviction","detail":"Redis 메모리가 maxmemory 한도에 도달해 새 키를 위한 공간을 만들려고 오래된 키를 eviction한다(evicted_keys가 계속 증가). 이로 인해 아직 필요한 항목이 사라지고 적중률이 떨어진다.","steps":["maxmemory를 실제 working set에 맞게 높이거나 데이터 집합을 줄인다.","접근 패턴에 맞는 eviction 정책을 선택한다(예: LRU/LFU: allkeys-lfu는 핫/콜드 구분에 효과적인 경우가 많다).","크거나 거의 사용되지 않는 구조를 정리한다. 값을 줄이고, 불필요한 필드를 제거하며, 적절한 자료형을 사용한다.","요구가 지속적이라면 Redis를 확장하고(RAM 증설 또는 cluster/sharding) evicted_keys 모니터링을 시작한다."],"related":[]},"sol_ttl":{"kind":"solution","title":"근본 원인: 약한 TTL / 캐싱 패턴","detail":"키에 TTL이 전혀 없어 메모리가 콜드 데이터로 가득 차거나, 캐싱 패턴의 재사용률이 낮아 대부분의 요청이 miss로 끝난다.","steps":["캐시된 키에 적절한 TTL을 부여해 식어 가는 데이터가 스스로 만료되도록 한다.","자주 읽는(핫) 데이터를 캐싱하고, 거의 사용되지 않는 데이터는 캐싱하지 않는다(적중되지 않는 항목으로 캐시를 채우지 않는다).","cache-aside 패턴에서 miss 후 채우기를 올바르게 구현하고, 'thundering herd'에 대비해 locking/요청 중복 제거를 사용한다.","keyspace_hits/misses 비율을 모니터링하고 데이터에 기반해 TTL과 키 설계를 개선한다."],"related":[]}}},"solver/arch-oltp.json":{"id":"arch-oltp","title":"참조 아키텍처: 3계층 OLTP 애플리케이션","icon":"🏛️","category":"참조 아키텍처","intro":"전형적인 업무 애플리케이션(주문, 결제, 재고)은 짧고 단일 행이며 일관성이 핵심인 다수의 트랜잭션(OLTP)을 실행한다. 이 트리는 부하 프로필 식별에서 출발해 주 관계형 데이터베이스, 커넥션 풀러, 읽기 복제본, 캐시 계층, 백업 + PITR, 모니터링을 언제 도입할지 단계별로 보여 준다. 목표는 '처음부터 모든 것을 추가'하는 것이 아니라 각 병목이 입증될 때 올바른 구성 요소를 추가하는 것이다.","start":"root","nodes":{"root":{"kind":"step","title":"부하 프로필은 무엇인가: 트랜잭션 구성과 일관성 요구 사항은?","detail":"OLTP 아키텍처의 첫 결정은 워크로드가 정말 트랜잭션 중심인지(ACID가 필요한 짧은 쓰기와 읽기의 혼합) 여부다. 데이터가 본질적으로 관계형이고 일관성이 핵심인가(금액, 재고, 주문), 워크로드가 대부분 읽기인가, 아니면 부하가 아직 매우 낮은가?","choices":[{"label":"짧은 트랜잭션, 강한 일관성(금액/재고/주문)","next":"primary_db"},{"label":"읽기가 압도적이고 쓰기는 적으며 트래픽이 증가 중","next":"scale_reads"},{"label":"부하가 매우 낮음, 가장 단순한 구성으로 충분한가?","next":"sol_single_node"}]},"primary_db":{"kind":"step","title":"주 데이터베이스 엔진을 선택했는가, 확장이 걱정되는가?","detail":"트랜잭션 핵심에는 관계형 엔진이 올바른 기반이다(PostgreSQL, MySQL/InnoDB, Oracle, SQL Server). 엔진 선택이 명확해지면 다음 질문은 확장이다: 지금은 단일 인스턴스로 충분한가, 아니면 연결 수와 읽기 부하가 증가하고 있는가?","choices":[{"label":"엔진은 정해졌고, 지금은 단일 인스턴스로 충분함","next":"sol_single_node"},{"label":"연결 수가 급증했고 리소스가 부족함","next":"sol_pooler"},{"label":"읽기 부하가 증가해 주 쓰기 인스턴스를 압박함","next":"scale_reads"}]},"scale_reads":{"kind":"step","title":"읽기를 어떻게 확장할 것인가: 복제본인가, 캐시인가?","detail":"읽기 부하가 주 인스턴스를 압박한다면 상호 보완적인 두 가지 옵션이 있다. 보고서/목록처럼 약간 지연된 데이터를 허용하는 읽기는 읽기 복제본으로 라우팅할 수 있고, 같은 '핫' 데이터가 반복해서 요청된다면 마이크로초 지연의 캐시(Redis)가 DB 부담을 덜어 준다.","choices":[{"label":"보고서/목록 읽기가 많음, 약간의 지연은 허용","next":"sol_read_replica"},{"label":"같은 인기 데이터를 반복해서 읽음","next":"sol_cache"}]},"sol_single_node":{"kind":"solution","title":"권장: 단일 primary + 풀 + 백업으로 시작","detail":"초기에는 아키텍처를 단순하게 유지한다: 단일 관계형 primary 인스턴스, 그 앞단의 커넥션 풀, 정기 백업으로 충분하다. 측정된 병목이 요구할 때만 복잡성을 더한다.","steps":["주 DB: PostgreSQL이나 MySQL/InnoDB 같은 ACID 호환 관계형 엔진. 스키마, 제약 조건, 인덱스를 올바르게 구성한다.","커넥션 풀: 처음부터 애플리케이션 측 풀(예: HikariCP)이나 서버 측 PgBouncer를 둔다. 연결 폭주를 방지한다.","백업 + PITR: 일일 전체 백업 + 특정 시점 복구를 위한 WAL/binlog 아카이빙. 백업은 별도 스토리지에 보관한다.","주의: 초기에 복제본/샤딩/캐시를 추가하지 않는다. 측정되지 않은 문제를 해결하려 하면 장애 지점만 커진다.","모니터링: 처음부터 지연, 오류율, 연결 수 지표를 수집한다. 증가 신호를 놓치지 않는다."],"related":["decide-pooling","decide-backup"]},"sol_pooler":{"kind":"solution","title":"권장: 커넥션 풀러 추가","detail":"OLTP에서 모든 애플리케이션 인스턴스가 자체 연결을 열면 주 DB는 수천 개의 연결에서 메모리와 스케줄러 오버헤드로 마비된다. 중간에 풀러를 두면 소수의 실제 DB 연결을 다수의 애플리케이션 요청이 공유한다.","steps":["PostgreSQL: PgBouncer(transaction pooling 모드)는 다수의 클라이언트를 소수의 백엔드 연결로 줄인다.","MySQL: ProxySQL도 마찬가지로 연결 multiplexing과 라우팅을 제공한다.","풀 크기: 일반적으로 CPU 코어 수에 비례한다. 수십 개의 백엔드 연결로 충분하다. '많을수록 좋다'는 적용되지 않는다.","주의: transaction pooling에서는 세션 수준 기능(prepared statement, 임시 테이블, 세션 변수)이 제한된다. 애플리케이션을 이에 맞게 작성한다.","다음 단계: 풀도 포화된다면 문제는 연결이 아니라 쿼리/트랜잭션 소요 시간이다. 느린 쿼리와 긴 트랜잭션을 찾아낸다."],"related":["decide-pooling","connection-storm"]},"sol_read_replica":{"kind":"solution","title":"권장: 읽기 복제본 추가","detail":"쓰기는 primary에 유지하고, 읽기 전용 트래픽(보고서, 목록, 검색)은 primary에서 공급받는 하나 이상의 비동기 복제본으로 라우팅한다. 이로써 primary는 쓰기와 일관성이 핵심인 읽기에 집중할 수 있다.","steps":["토폴로지: primary -> 하나 이상의 비동기 복제본. 애플리케이션이 읽기/쓰기를 분리해 읽기를 복제본으로 보낸다.","엔진: PostgreSQL streaming replication, MySQL 복제본 또는 관리형 읽기 복제본.","적용 시점: 보고서/분석/목록처럼 약간의 지연을 허용하는 읽기, primary의 CPU/IO 부담을 덜고 싶을 때.","주의: 복제 지연은 오래된 데이터 읽기를 유발한다. '방금 쓴 내용을 즉시 읽어야 하는' 흐름은 primary에서 제공한다(read-your-writes).","다음 단계: 복제본으로도 부족하면 핫 데이터용 캐시나 쓰기 측 샤딩을 고려한다."],"related":["replication-lag","decide-shard-replica"]},"sol_cache":{"kind":"solution","title":"권장: 캐시 계층 추가(Redis)","detail":"같은 인기 데이터(세션, 카탈로그, 프로필, 페이지 조각)를 반복해서 읽는다면 DB 앞단에 인메모리 캐시를 두어 대부분의 읽기를 DB에 도달하지 않고 처리한다. 지연이 줄고 주 DB 부담이 덜어진다.","steps":["엔진: Redis(또는 Memcached)를 이용한 cache-aside 패턴. 애플리케이션은 먼저 캐시를 확인하고, miss 시 DB에서 가져와 캐시에 쓴다.","TTL + 무효화: 모든 키에 적절한 TTL을 부여한다. 쓰기 시 관련 키를 삭제/갱신해 오래된 데이터가 남지 않게 한다.","적용 시점: 읽기가 많고, 데이터 변경이 비교적 드물며, 같은 키가 반복해서 요청되는 경우.","주의: cache stampede. 인기 키가 만료되면 수천 개의 요청이 동시에 DB로 몰릴 수 있다. locking/single-flight와 jitter를 둔 TTL을 사용한다.","주의: 캐시를 '원본 데이터'로 취급하지 않는다. 영구적인 원본은 항상 DB에 있으며, 캐시가 장애를 일으켜도 애플리케이션은 DB로 폴백해 계속 동작해야 한다."],"related":["decide-cache","cache-hit-low"]}}},"solver/arch-dwh.json":{"id":"arch-dwh","title":"참조 아키텍처: 데이터 웨어하우스 / 분석","icon":"🏛️","category":"참조 아키텍처","intro":"분석 쿼리(보고, 집계, 광범위한 스캔)는 OLTP 트랜잭션과 근본적으로 다르다: 수는 적지만 매우 넓고 오래 실행되는 쿼리다. 이 트리는 분석을 OLTP에서 분리하는 결정에서 출발해 적재 방식(ETL vs ELT), 컬럼형/columnstore 저장소, 스타 스키마 모델링, 파티셔닝 + 압축, materialized view를 이용한 사전 계산 같은 구성 요소를 단계별로 다루며, 동시성과 처리량 간의 트레이드오프를 함께 따진다.","start":"root","nodes":{"root":{"kind":"step","title":"분석 워크로드가 OLTP와 같은 데이터베이스에서 실행되는가?","detail":"가장 흔한 실수는 운영 중인 트랜잭션 DB에서 무거운 보고서를 실행해 OLTP 지연을 악화시키는 것이다. 첫 결정은 분리다: 분석이 아직 OLTP 인스턴스에서 실행되는가, 별도 분석 환경을 구축 중인가, 아니면 데이터를 그곳으로 옮기는 방법을 결정하는 중인가?","choices":[{"label":"보고서가 아직 운영 OLTP에서 실행되어 OLTP를 느리게 함","next":"sol_separate"},{"label":"별도 웨어하우스가 있음, 데이터를 어떻게 적재해야 하는가?","next":"load_method"},{"label":"웨어하우스가 있지만 쿼리가 여전히 느림, 어떻게 모델링해야 하는가?","next":"model"}]},"load_method":{"kind":"step","title":"변환은 어디서 할 것인가: 적재 전인가, 적재 후인가?","detail":"원본에서 웨어하우스로 데이터를 옮길 때, 적재 전에 별도 처리 계층에서 정제/변환할 것인가(ETL), 아니면 원시 데이터를 먼저 웨어하우스에 적재하고 강력한 웨어하우스 엔진 내부에서 SQL로 변환할 것인가(ELT)? 결정은 웨어하우스의 처리 능력과 스키마 유연성 요구에 달려 있다.","choices":[{"label":"변환이 복잡하거나 외부에서 수행됨, 적재 전에 처리","next":"sol_etl"},{"label":"웨어하우스가 강력함, 원시 데이터를 적재하고 내부에서 SQL로 변환","next":"sol_elt"}]},"model":{"kind":"step","title":"쿼리 병목은 저장 구조에 있는가, 데이터 모델에 있는가?","detail":"분석이 느리다면 두 가지 주요 수단이 있다. 광범위한 스캔은 행 기반 저장소에서 행 전체를 읽는다. 컬럼형 저장소는 필요한 컬럼만 읽고 압축률도 높다. 모델 측면에서는 과도한 정규화로 조인이 많아지면 쿼리가 느려진다. 스타 스키마(fact + dimension)는 분석을 단순화한다.","choices":[{"label":"광범위한 집계 스캔이 느림, 읽는 컬럼은 적음","next":"sol_columnar"},{"label":"조인이 너무 많음, 복잡한 정규화 스키마","next":"sol_star"},{"label":"같은 무거운 보고서가 같은 결과를 반복해서 생성함","next":"sol_matview"}]},"sol_separate":{"kind":"solution","title":"권장: 분석을 OLTP에서 분리","detail":"트랜잭션 워크로드와 분석 워크로드는 같은 엔진에서 서로를 압박한다: 긴 보고서가 락/IO를 붙잡아 짧은 트랜잭션을 지연시킨다. 분석을 별도 대상(읽기 복제본 또는 별도 분석 웨어하우스)으로 옮기면 양쪽 모두 부담이 줄어든다.","steps":["첫 단계: 가벼운 보고서는 OLTP 읽기 복제본으로, 무거운 분석은 별도 분석 엔진으로 라우팅한다.","분석 엔진: 컬럼형/분석 엔진(예: columnstore를 사용하는 SQL Server, ClickHouse 또는 컬럼형 웨어하우스)은 광범위한 스캔에서 행 기반 저장소보다 몇 배 빠르다.","동시성 vs 처리량: OLTP는 다수의 작은 트랜잭션(높은 동시성)에, 웨어하우스는 소수의 거대한 쿼리(높은 처리량)에 맞춰 튜닝된다. 같은 서버에서 둘 다 최적화할 수는 없다.","주의: 웨어하우스는 '운영 환경의 실시간 사본'이 아니다. 주기적으로 적재되어 지연은 있지만 분석용으로 정리된 데이터다.","다음 단계: 분리 후 적재 방식(ETL/ELT)과 모델(스타 스키마)을 결정한다."],"related":["decide-sqlnosql","slow-query"]},"sol_etl":{"kind":"solution","title":"권장: ETL(적재 전 변환)","detail":"ETL에서는 원본에서 데이터를 추출(Extract)하고, 별도 처리 계층에서 정제 및 변환(Transform)한 뒤에야 웨어하우스에 적재(Load)한다. 정제되고 모델에 맞는 데이터만 웨어하우스에 들어간다.","steps":["적용 시점: 변환이 복잡하거나, 여러 원본을 결합해야 하거나, 원시/민감 데이터가 웨어하우스에 절대 들어가지 않기를 원하는 경우.","흐름: 원본(OLTP/파일/API) → 변환 계층 → 웨어하우스로 배치 적재.","장점: 웨어하우스 저장소를 깨끗하게 유지한다. 민감 데이터를 적재 전에 마스킹할 수 있다.","단점: 변환이 별도 시스템의 성능에 좌우되며, 웨어하우스의 SQL 유연성을 활용할 수 없다.","주의: 배치 시간대(예: 야간)에 적재한다. 적재 중 무거운 쿼리와 충돌하지 않도록 계획한다."],"related":["decide-capacity"]},"sol_elt":{"kind":"solution","title":"권장: ELT(웨어하우스 내부 변환)","detail":"ELT에서는 원시 데이터를 먼저 웨어하우스에 적재하고 웨어하우스 자체의 강력한 SQL 엔진 내부에서 변환한다. 최신 컬럼형 웨어하우스는 처리 능력이 높으므로 변환을 데이터로 가져가는 대신 데이터를 한 번 옮기고 내부에서 처리한다.","steps":["적용 시점: 웨어하우스 엔진이 강력하고 확장 가능하며, 원시 데이터도 보관하고 싶은 경우(감사/재처리).","흐름: 원본 → 원시 계층(staging) → SQL/변환 뷰 → 모델링된 fact/dimension 계층.","장점: 하나의 엔진으로 유연하다. 원시 데이터가 보관되므로 나중에 변환을 바꾸고 재생성할 수 있다.","단점: 원시(민감할 수 있는) 데이터가 웨어하우스에 들어간다. 접근 제어와 마스킹이 필수다.","주의: 계층별로 모델링한다(raw → staging → mart). 재실행이 안전하도록 변환을 멱등적으로 작성한다."],"related":["decide-capacity"]},"sol_columnar":{"kind":"solution","title":"권장: 컬럼형 / columnstore 저장소 + 압축","detail":"분석 쿼리는 대개 모든 행에서 소수의 컬럼을 스캔한다. 컬럼형 저장소는 데이터를 컬럼 단위로 보관하므로 필요한 컬럼만 읽고, 유사한 값이 인접해 압축률이 매우 높으며, IO가 크게 줄어든다.","steps":["엔진/기능: SQL Server columnstore index, ClickHouse 또는 컬럼형 웨어하우스. PostgreSQL 측의 컬럼형 확장.","압축: 컬럼형 구조는 높은 압축률을 제공한다. 디스크 사용량과 읽는 바이트가 줄고 스캔이 빨라진다.","파티셔닝: 큰 fact 테이블을 날짜/범위로 파티셔닝해 partition pruning이 관련 없는 파티션을 완전히 건너뛰게 한다.","주의: 컬럼형은 다수의 단일 행 갱신/포인트 읽기에 적합하지 않다. OLTP를 옮기지 않는다.","동시성 vs 처리량: 컬럼형은 수천 개의 작은 동시 쿼리가 아니라 소수의 거대한 스캔 쿼리에서 강점을 발휘한다."],"related":["decide-partition","io-bottleneck"]},"sol_star":{"kind":"solution","title":"권장: 스타 스키마(fact + dimension)","detail":"분석 모델에서는 측정값을 담은 fact 테이블이 중심에 있고 설명용 dimension 테이블이 둘러싼다. 과도하게 정규화된 스키마에 비해 조인 수가 줄고, 쿼리가 단순해지며, 엔진이 더 나은 실행 계획을 만든다.","steps":["구조: 중심에 fact(측정값 + dimension 키), 주변에 dimension(시간, 상품, 고객)이 별 모양을 이룬다.","적용 시점: 보고서가 많은 테이블을 조인하고 정규화된 스키마가 조인으로 넘쳐 나는 경우.","비정규화: 분석에서 의도적인 비정규화는 일반적이다. 일관성은 적재 과정(ETL/ELT)에서 보장한다.","다음 단계: 자주 실행되는 무거운 집계는 materialized view로 사전 계산한다.","주의: OLTP를 스타 스키마로 바꾸지 않는다. 이 모델은 트랜잭션 쓰기가 아니라 분석을 위한 것이다."],"related":["decide-index"]},"sol_matview":{"kind":"solution","title":"권장: materialized view로 사전 계산","detail":"같은 무거운 집계(일일 매출, 월간 요약)가 반복 실행되어 같은 결과를 낸다면 결과를 materialized view에 물리적으로 저장하고 보고서가 사전 계산된 결과를 읽게 한다. 무거운 계산은 한 번만 실행되고 쿼리는 즉시 반환된다.","steps":["적용 시점: 특정 요약 쿼리가 같은 큰 테이블을 자주 반복해서 스캔하는 경우.","갱신: materialized view를 일정에 따라 갱신한다(예: 야간 배치 또는 증분). 최신성 요구에 맞게 빈도를 정한다.","엔진: PostgreSQL materialized view (REFRESH), Oracle materialized view(query rewrite까지 지원), SQL Server indexed view.","주의: 뷰에는 최신성 구간이 따른다. 실시간 데이터가 필요한 경우에는 라이브 쿼리가 필수다.","다음 단계: 갱신 비용이 크다면 증분 갱신이나 파티션 기반 갱신을 고려한다."],"related":["decide-partition"]}}},"solver/arch-microservice.json":{"id":"arch-microservice","title":"참조 아키텍처: 마이크로서비스 데이터 계층","icon":"🏛️","category":"참조 아키텍처","intro":"마이크로서비스에서 가장 중요한 결정은 데이터 소유권이다: 각 서비스는 자신의 데이터를 소유해야 하며(database-per-service) 다른 서비스의 테이블에 직접 접근해서는 안 된다. 이 트리는 공유 DB 안티패턴, 분산 트랜잭션 없이 서비스 간 일관성을 확보하는 방법(saga + outbox), CDC를 통한 데이터 동기화, 서비스 간 쿼리(API composition vs CQRS read model)를 단계별로 다룬다. 목표는 서비스를 진정으로 독립적이고 개별 배포 가능하게 유지하는 것이다.","start":"root","nodes":{"root":{"kind":"step","title":"서비스는 데이터를 어떻게 소유하는가: 공유 DB인가, 서비스별 DB인가?","detail":"데이터 격리는 마이크로서비스 아키텍처의 근간이다. 서비스들이 단일 공유 데이터베이스를 사용하는가(숨은 의존성), 각 서비스가 자체 데이터베이스를 가지는가(올바른 패턴), 아니면 실제 과제가 서비스 간 일관성/쿼리인가? 먼저 현재 상태를 파악한다.","choices":[{"label":"모든 서비스가 단일 공유 DB를 사용함","next":"sol_db_per_service"},{"label":"각 서비스가 자체 DB를 가지지만 작업에 서비스 간 일관성이 필요함","next":"consistency"},{"label":"각 서비스가 자체 DB를 가지지만 결합 쿼리/보고서가 필요함","next":"query"}]},"consistency":{"kind":"step","title":"서비스 간 워크플로를 어떻게 일관되게 유지할 것인가?","detail":"여러 서비스에 걸친 작업(주문 생성 → 결제 → 재고 차감)에서 전통적인 분산 트랜잭션(2PC)은 취약하고 확장되지 않는다. 대신 흐름을 로컬 트랜잭션 + 이벤트 단계(saga)로 나누고 이벤트를 안정적으로 발행한다(outbox). 어떤 문제를 다루고 있는가?","choices":[{"label":"다중 서비스 워크플로를 원자적으로 유지하려 함(2PC?)","next":"sol_saga"},{"label":"로컬 쓰기와 이벤트 발행을 일관되게 만들 수 없음(dual write)","next":"sol_outbox"}]},"query":{"kind":"step","title":"여러 서비스의 데이터를 보여 주는 쿼리를 어떻게 제공할 것인가?","detail":"데이터가 여러 서비스로 분리되면 단일 조인으로 통합 화면을 만들 수 없다. 두 가지 주요 방식이 있다: 요청 시점에 관련 서비스의 API를 호출해 결과를 병합하는 방식(API composition), 또는 이벤트로 갱신되는 사전 조인된 읽기 모델을 유지하는 방식(CQRS). 쿼리 빈도와 복잡도에 따라 선택한다.","choices":[{"label":"단순하고 드문 조인. 몇 개 서비스를 호출해 병합하면 충분","next":"sol_api_composition"},{"label":"복잡하거나 잦은 통합 쿼리. 미리 준비된 읽기 모델이 필요","next":"sol_cqrs"}]},"sol_db_per_service":{"kind":"solution","title":"권장: Database-per-service(공유 DB 분리)","detail":"하나의 데이터베이스를 공유하는 서비스는 숨겨진 위험한 의존성이다: 한 서비스의 스키마 변경이 다른 서비스를 깨뜨리고, 테이블 소유권이 불분명해지며, 독립 배포가 불가능해진다. 각 서비스는 자신의 데이터를 소유해야 한다.","steps":["원칙: 각 서비스는 자신의 데이터베이스만 읽고 쓴다. 다른 서비스의 테이블에 직접 접근할 수 없으며, 해당 서비스의 API를 통해서만 데이터를 요청한다.","엔진 독립성: 각 서비스는 요구에 맞는 엔진을 선택할 수 있다(예: 주문에는 PostgreSQL, 카탈로그에는 MongoDB, 검색에는 Elasticsearch, 장바구니에는 Redis).","안티패턴: '공유 데이터베이스': 공통 테이블, 교차 조인, 단일 스키마. 이는 마이크로서비스가 아니라 분산 모놀리스다.","분해: 먼저 논리적 분리(별도 스키마/사용자), 그다음 물리적 분리. 테이블 간 조인을 제거한다.","주의: 분리 후에는 일관성(saga/outbox)과 결합 쿼리(composition/CQRS) 문제가 생긴다. 이는 이 전환의 자연스러운 비용이다."],"related":["decide-isolation","decide-sqlnosql"]},"sol_saga":{"kind":"solution","title":"권장: Saga 패턴(분산 트랜잭션 회피)","detail":"여러 서비스에 걸친 작업을 단일 분산 트랜잭션(2PC)으로 원자화하려 하지 않는다. 2PC는 락을 유지하며 단일 지점 취약성과 낮은 가용성을 가져온다. 대신 흐름을 각 서비스의 로컬 트랜잭션인 단계들로 나누고, 실패 시 보상 단계로 되돌린다.","steps":["원칙: 워크플로를 로컬 트랜잭션 + 이벤트의 체인으로 모델링한다. 각 단계가 다음 단계를 트리거한다.","보상: 단계가 실패하면 이전 단계를 되돌리는 보상 작업을 실행한다(예: 결제 실패 시 주문 취소).","코레오그래피 vs 오케스트레이션: 단순한 흐름에서는 서비스가 이벤트로 서로를 트리거하고(choreography), 복잡한 흐름에서는 중앙 오케스트레이터가 단계를 관리한다.","주의: saga는 원자적이지 않으며 최종 일관성(eventual consistency)을 제공한다. 중간 상태가 외부에 보일 수 있으므로 업무 규칙을 이에 맞게 설계한다.","주의: 대규모 마이크로서비스에서 2PC/XA를 사용하지 않는다. 블로킹과 락 유지 시간 때문에 시스템이 멈춘다."],"related":["decide-isolation","long-transaction"]},"sol_outbox":{"kind":"solution","title":"권장: Transactional outbox(dual-write 함정)","detail":"서비스가 자체 DB에 쓰면서 이벤트도 발행하려 할 때 두 작업을 따로 수행하면 'dual write' 함정에 빠진다: 하나는 성공하고 다른 하나는 실패하면 시스템이 불일치 상태로 남는다. 해결책은 업무 데이터와 같은 로컬 트랜잭션 안에서 이벤트를 outbox 테이블에 쓰고, 별도 프로세스로 발행하는 것이다.","steps":["원칙: 업무 레코드 + 이벤트 레코드를 하나의 로컬 트랜잭션에서 outbox 테이블에 쓴다. 둘 다 수행되거나 둘 다 수행되지 않는다(원자적).","발행: 별도 relay/CDC 프로세스가 outbox 행을 읽어 메시지 브로커(예: Kafka)에 발행하고 표시한다.","CDC 사용 시: Debezium 같은 CDC 도구가 DB 로그(WAL/binlog)를 읽어 outbox 변경 사항을 Kafka로 안정적으로 옮긴다.","멱등성: 컨슈머는 같은 이벤트를 여러 번 받을 수 있다. 이벤트 id를 사용해 멱등적으로 처리한다(at-least-once 전달을 가정).","주의: 'DB에 먼저 쓰고 그다음 Kafka에 발행'하는 방식을 직접 사용하지 않는다. 발행 시점의 장애가 이벤트 손실을 유발한다."],"related":["replication-lag"]},"sol_api_composition":{"kind":"solution","title":"권장: API composition(요청 시점 병합)","detail":"결합 데이터가 필요한 곳에서 호출자(예: gateway/BFF)가 관련 서비스의 API를 호출하고 결과를 메모리에서 병합한다. 소수 서비스에 걸친 단순 조인에 가장 간단한 솔루션이며, 추가 데이터 사본을 유지하지 않는다.","steps":["원칙: composer(API gateway/BFF)가 service-A와 service-B를 호출해 결과를 병합하고 반환한다.","적용 시점: 조인이 단순하고, 소수 서비스에 걸치며, 쿼리 빈도가 보통인 경우. 추가 read model의 유지 부담을 원하지 않는 경우.","장점: 추가 데이터 저장소나 동기화가 없다. 각 서비스가 자신의 최신 데이터를 반환한다.","주의: 다수 서비스나 대규모 데이터 집합에 걸친 조인에서는 지연이 누적되고 메모리 내 조인 비용이 크다.","다음 단계: 조인이 복잡해지거나 매우 빈번해지면 CQRS read model로 전환한다."],"related":[]},"sol_cqrs":{"kind":"solution","title":"권장: CQRS read model(이벤트로 공급)","detail":"복잡하고 빈번한 결합 쿼리에는 서비스가 발행하는 이벤트로 공급되는, 미리 조인되고 쿼리에 최적화된 read model(materialized view/비정규화 사본)을 유지한다. 쓰기 측은 서비스에 남고, 읽기 측은 준비된 데이터를 한곳에서 빠르게 제공한다.","steps":["원칙: 서비스가 상태 변경을 이벤트로 발행하고, projection이 이 이벤트를 수신해 read model을 갱신한다.","저장소 선택: 쿼리 유형에 따라 read model 저장소를 선택한다: 검색에는 Elasticsearch, 유연한 문서에는 MongoDB, 빠른 키 접근에는 Redis, 관계형 보고서에는 PostgreSQL.","적용 시점: 결합 쿼리가 복잡/빈번하고, 낮은 지연이 필요하며, API composition 비용이 커진 경우.","주의: read model은 최종 일관성을 가진다. 이벤트가 처리될 때까지 약간 뒤처진다.","멱등성: projection은 같은 이벤트를 두 번 볼 수 있다. 이벤트 id로 멱등적으로 갱신하고 필요하면 순서/버전을 추적한다."],"related":["decide-cache"]}}},"solver/arch-eventdriven.json":{"id":"arch-eventdriven","title":"참조 아키텍처: 이벤트 기반 / CQRS","icon":"🏛️","category":"참조 아키텍처","intro":"이벤트 기반 아키텍처에서는 서비스가 서로를 직접 호출하는 대신 이벤트 로그(예: Kafka)에 이벤트를 발행하고, 컨슈머가 이를 비동기로 처리한다. 이 트리는 이벤트 백본 구축에서 출발해 event sourcing(이벤트로부터 상태 도출), CQRS를 통한 command/query 분리, read model projection, 멱등성, 최종 일관성 허용, 'exactly-once 전달'의 오류를 단계별로 다룬다. 목표는 일관성 트레이드오프를 잘 이해한, 느슨하게 결합되고 확장 가능한 시스템이다.","start":"root","nodes":{"root":{"kind":"step","title":"이벤트 기반 아키텍처에서 어떤 결정을 앞두고 있는가?","detail":"이벤트 기반 시스템의 기반은 비동기 이벤트로 서비스를 연결하는 내구성 있는 이벤트 로그다. 아직 백본을 구축 중인가, 이벤트로부터 상태를 도출하고 싶은가(event sourcing), 아니면 읽기와 쓰기 경로를 분리하려는가(CQRS)?","choices":[{"label":"서비스를 연결하는 이벤트 백본을 구축 중","next":"sol_event_log"},{"label":"이벤트 이력으로부터 상태를 도출하고 싶음","next":"sol_event_sourcing"},{"label":"읽기와 쓰기 경로를 분리하고 싶음(CQRS)","next":"cqrs_split"}]},"cqrs_split":{"kind":"step","title":"CQRS에서 읽기 측을 어떻게 구축하며, 일관성 허용 범위는 어느 정도인가?","detail":"CQRS에서는 command(쓰기)와 query(읽기) 측이 분리된다. 쓰기 이벤트가 read model에 공급하는 projection을 갱신한다. 핵심 질문: read model이 쓰기보다 뒤처지는 것(최종 일관성)을 허용할 수 있는가, 아니면 모든 이벤트가 정확히 한 번 처리되기를 기대하는가?","choices":[{"label":"projection을 통해 이벤트로 read model을 공급할 것임","next":"sol_read_model"},{"label":"읽기는 최신이어야 함, 지연은 허용 불가","next":"sol_consistency"},{"label":"이벤트가 '정확히 한 번' 처리되기를 원함","next":"sol_idempotency"}]},"sol_event_log":{"kind":"solution","title":"권장: 내구성 있는 이벤트 로그 백본","detail":"이벤트 기반 아키텍처의 중심에는 프로듀서가 쓰고 컨슈머가 각자의 속도로 읽는, 내구성 있고 순서가 보장되는 이벤트 로그가 있다. 이 백본은 서비스를 시간과 규모 측면에서 분리한다.","steps":["엔진: Apache Kafka(또는 유사한 로그 기반 플랫폼): 파티션되고, 영속적이며, 재생 가능한 이벤트 로그.","파티션과 순서: 순서는 파티션 내에서만 보장된다. 같은 엔티티의 이벤트를 같은 키로 라우팅해 순서를 유지한다.","컨슈머 독립성: 각 컨슈머 그룹은 자체 offset을 유지한다. 프로듀서에 영향을 주지 않고 다시 읽거나 되감을 수 있다.","보존: 새 컨슈머와 재처리가 이력을 읽을 수 있도록 로그를 충분히 오래 보존한다.","주의: Kafka는 큐가 아니라 재생 가능한 로그다. at-least-once 전달을 가정하고 멱등적인 컨슈머를 작성한다."],"related":[]},"sol_event_sourcing":{"kind":"solution","title":"권장: Event sourcing(이벤트로부터 상태 도출)","detail":"event sourcing에서는 엔티티의 현재 상태가 아니라 엔티티에 적용된 모든 (불변) 변경 이벤트를 저장한다. 현재 상태는 이 이벤트를 순서대로 적용해 도출한다. 완전한 감사 이력과 과거 임의 시점으로 돌아갈 수 있는 능력을 제공한다.","steps":["원칙: 이벤트 스트림이 원본 데이터다. 상태는 처음부터(또는 스냅샷 + 이후 이벤트) 이벤트를 재생해 재구성한다.","스냅샷: 긴 스트림에서는 매번 처음부터 재생하지 않도록 주기적으로 스냅샷을 생성하고 이후 이벤트를 그 위에 적용한다.","장점: 완전한 감사 추적, 시간 흐름에 따른 '어떻게 이 상태에 이르렀는가'에 대한 답, 다양한 read model 도출 능력.","주의: 이벤트 스키마는 진화한다. 이벤트는 영구히 남으므로 오래된 이벤트를 읽으려면 버전 관리/upcasting이 필요하다.","주의: 모든 시나리오에 필요하지는 않다. 현재 상태로 충분하다면 전통적인 CRUD가 더 단순하다. 대개 CQRS와 함께 사용한다."],"related":[]},"sol_read_model":{"kind":"solution","title":"권장: 읽기 모델 projection","detail":"CQRS에서는 쓰기 측이 이벤트를 생성하고, projection이 이 이벤트를 수신해 쿼리에 최적화된 read model(비정규화 뷰)을 갱신한다. 읽기는 쓰기 경로와 완전히 분리된 이 준비된 모델에서 제공되며, 양측은 독립적으로 확장된다.","steps":["원칙: 이벤트 → projection → 읽기 저장소. 읽기 쿼리는 이 비정규화 모델에만 접근한다.","저장소 선택: 쿼리 유형별로: 검색에는 Elasticsearch, 유연한 문서에는 MongoDB, 빠른 접근에는 Redis, 관계형에는 PostgreSQL.","재구축 가능성: read model은 이벤트로부터 도출되므로 삭제한 뒤 로그를 다시 읽어 처음부터 재구축할 수 있다.","멱등성: projection은 같은 이벤트를 여러 번 볼 수 있다. 이벤트 id/버전을 사용해 멱등적으로 갱신한다.","주의: read model은 최종 일관성을 가진다. 이벤트가 처리될 때까지 쓰기보다 뒤처지므로 UI를 이에 맞게 설계한다."],"related":["decide-cache"]},"sol_consistency":{"kind":"solution","title":"권장: 최종 일관성 허용을 전제로 설계","detail":"이벤트 기반/CQRS 시스템에서 read model은 쓰기보다 약간 뒤처진다(최종 일관성). 이를 '제거'하려 하기보다 허용하도록 설계하고, 엄격한 즉시 일관성이 필요한 소수의 흐름은 따로 둔다.","steps":["원칙: 대부분의 업무 흐름은 짧은 지연이 있는 일관성을 허용한다. 사용자에게 '요청 접수, 처리 중' 피드백을 제공한다.","Read-your-writes: 사용자가 자신의 쓰기를 즉시 확인해야 한다면 해당 읽기는 쓰기 측에서/동기적으로 제공한다.","엄격한 일관성이 필요한 핵심: 금액 잔액처럼 절대적인 즉시 일관성이 필요한 작업은 단일 서비스/DB 내에서 ACID 트랜잭션으로 처리한다. 이벤트로 분산하지 않는다.","주의: 최종 일관성을 숨기지 않는다. 화면과 업무 규칙에서 중간/일시적 상태를 명시적으로 처리한다.","주의: '시스템 전체를 강한 일관성으로 만든다'는 목표는 이벤트 기반 아키텍처의 이점(확장성, 느슨한 결합)을 없앤다."],"related":["decide-ha","replication-lag"]},"sol_idempotency":{"kind":"solution","title":"권장: 멱등성(exactly-once의 오류)","detail":"분산 메시징에서 종단 간 'exactly-once 전달'은 실제로는 오류다. 네트워크와 재시도 때문에 메시지 중복은 피할 수 없다. 올바른 목표는 멱등적 처리다: 같은 이벤트를 두 번 처리해도 한 번 처리한 것과 같은 결과가 나와야 한다.","steps":["원칙: at-least-once 전달을 수용하고, 컨슈머에서 중복이 해가 되지 않도록 만든다.","방법: 모든 이벤트에 고유 id를 부여한다. 처리한 id를 기록하고 다시 도착하면 건너뛰거나(dedup), 본질적으로 멱등적인 연산(upsert, set)을 사용한다.","Exactly-once 시맨틱: Kafka의 'exactly-once' 기능은 Kafka 자체 트랜잭션 경계 내에서만 적용된다. 외부 DB에 대한 부수 효과는 멱등성 없이 보장되지 않는다.","순서: 같은 엔티티의 이벤트를 같은 파티션 키로 라우팅해 순서 뒤바뀜을 피한다.","주의: 'exactly-once 전달을 설정했으니 dedup은 필요 없다'는 가정은 운영 환경에서 불일치를 낳는다. 항상 멱등적으로 처리한다."],"related":[]}}},"solver/arch-multiregion.json":{"id":"arch-multiregion","title":"참조 아키텍처: 멀티 리전 / 글로벌","icon":"🏛️","category":"참조 아키텍처","intro":"사용자가 지리적으로 분산되어 있거나 리전 재해에 대한 복원력이 필요하면 데이터가 여러 리전으로 퍼진다. 이 트리는 읽기 지역성(리전마다 읽기 복제본), 쓰기 토폴로지(single-leader vs multi-leader vs quorum), 데이터 상주/컴플라이언스(GDPR), 충돌 해결, RPO/RTO와 지연 간의 트레이드오프를 단계별로 다룬다. 기본 사실: 물리 법칙 때문에 낮은 지연, 강한 일관성, 멀티 리전 쓰기를 동시에 완전히 달성할 수는 없다. 의도적인 트레이드오프가 필수다.","start":"root","nodes":{"root":{"kind":"step","title":"멀티 리전으로 가는 이유는 무엇인가: 읽기 지역성, 쓰기/복원력, 아니면 컴플라이언스?","detail":"멀티 리전 아키텍처는 비용이 크고 복잡하므로 동기를 명확히 한다. 멀리 있는 사용자의 읽기 지연을 낮추고 싶은가, 리전 간 쓰기/재해 복원력이 필요한가, 아니면 실제 요구 사항이 데이터를 특정 국가에 두는 것(법적 상주)인가?","choices":[{"label":"멀리 있는 사용자를 위한 빠른 읽기, 쓰기는 한 리전에 머물러도 됨","next":"sol_read_locality"},{"label":"리전 간 쓰기 / 리전 손실에 대한 복원력이 필요함","next":"write_topology"},{"label":"데이터가 법적으로 특정 국가/리전에 있어야 함","next":"sol_data_residency"}]},"write_topology":{"kind":"step","title":"쓰기 토폴로지는 무엇이어야 하는가: single-leader, multi-leader, quorum?","detail":"리전 간 쓰기에서 핵심 결정은 리더십 모델이다. 모든 쓰기가 단일 리전으로 가는가(단순하고 일관되지만 원격 쓰기가 느림), 모든 리전이 쓸 수 있는가(쓰기 지연은 낮지만 충돌 발생), 아니면 과반수 리전의 확인 응답으로 쓰기를 수락하는가(quorum: 일관되지만 지연 추가)?","choices":[{"label":"한 리전이 쓰고 나머지는 읽음(단순하고 일관됨)","next":"sol_single_leader"},{"label":"모든 리전이 로컬로 쓸 수 있음(낮은 쓰기 지연)","next":"sol_multi_leader"},{"label":"과반수 확인 응답으로 쓰기, 일관성이 우선","next":"sol_quorum"}]},"sol_read_locality":{"kind":"solution","title":"권장: 리전별 읽기 복제본(읽기 지역성)","detail":"쓰기는 단일 주 리전에 유지하고, 각 사용자 리전에 그 리전에서 공급받는 읽기 복제본을 둔다. 사용자는 가장 가까운 복제본에서 읽어 읽기 지연이 줄어든다. 쓰기는 여전히 단일 중심으로 가므로 일관성 모델이 단순하게 유지된다.","steps":["토폴로지: 하나의 주 리전(쓰기) → 각 사용자 리전의 비동기 읽기 복제본.","엔진: PostgreSQL streaming replication, MySQL 리전 간 복제본 또는 관리형 글로벌 읽기 복제본.","라우팅: 애플리케이션이 읽기는 가장 가까운 복제본으로, 쓰기는 주 리전으로 라우팅한다(geo DNS / smart router).","주의: 리전 간 복제 지연은 오래된 데이터 읽기를 유발한다. read-your-writes가 필요한 흐름은 primary에서 읽게 한다.","RPO/RTO: 주 리전이 다운되면 비동기 복제본을 일부 데이터 손실(RPO > 0)과 함께 승격할 수 있다. 페일오버 계획을 미리 준비한다."],"related":["replication-lag","decide-shard-replica"]},"sol_single_leader":{"kind":"solution","title":"권장: 단일 리더 쓰기","detail":"모든 쓰기는 단일 리더 리전으로 가고, 다른 리전은 그 리더에서 비동기로 공급받는 읽기 복제본이다. 가장 단순하고 일관성이 강한 모델이며, 쓰기 지점이 하나이므로 충돌이 없다. 대가는 원격 리전에서의 높은 쓰기 지연이다.","steps":["원칙: 단일 쓰기 권한 → 충돌 없음, 단순한 일관성 모델.","적용 시점: 쓰기량을 한 리전에서 감당할 수 있고 원격 쓰기 지연을 허용할 수 있는 경우.","페일오버: 리더 리전이 다운되면 복제본을 리더로 승격한다. 자동/통제된 페일오버와 split-brain 방지가 필수다.","RPO/RTO: 비동기 복제에서는 페일오버 시 소량의 데이터가 손실될 수 있다(RPO). 동기 복제는 RPO=0을 제공하지만 쓰기 지연을 늘린다.","주의: 원격 사용자의 쓰기는 느리다. 쓰기 지역성이 핵심이라면 multi-leader나 quorum을 검토한다."],"related":["decide-ha","failover-drill"]},"sol_multi_leader":{"kind":"solution","title":"권장: multi-leader 쓰기 + 충돌 해결","detail":"모든 리전이 로컬로 쓸 수 있어 쓰기 지연이 낮고, 한 리전이 단절되어도 다른 리전은 계속 쓴다. 대가는 크다: 두 리전이 같은 레코드를 서로 다르게 갱신할 수 있으며 충돌 해결을 설계해야 한다.","steps":["원칙: 모든 리전이 리더다. 쓰기는 다른 리전으로 비동기 전파되어 병합된다.","충돌 해결: Last-write-wins(단순하지만 데이터 손실 위험), 애플리케이션별 병합, 또는 CRDT 같은 충돌 없는 데이터 유형.","엔진 예: Multi-master/multi-leader 구성(예: PostgreSQL용 EDB Postgres Distributed (BDR)/pgEdge, Redis Enterprise Active-Active (CRDT), CouchDB). Cassandra는 multi-leader가 아니라 leaderless이며, LOCAL_ONE/LOCAL_QUORUM으로 모든 리전에서 로컬 쓰기를 허용해 유사한 결과를 내고 last-write-wins(타임스탬프)로 충돌을 해결한다.","적용 시점: 강한 일관성보다 로컬 쓰기 지연과 리전 독립성이 더 중요한 경우.","주의: 처음부터 충돌 해결을 설계한다. '충돌은 일어나지 않을 것'이라는 가정은 조용한 데이터 손실을 낳는다. 고유 ID 생성 시 리전 간 충돌에 유의한다."],"related":["decide-ha"]},"sol_quorum":{"kind":"solution","title":"권장: quorum 기반 쓰기(과반수 확인 응답)","detail":"쓰기는 리전/복제본의 과반수(quorum)가 확인 응답하면 수락된다. 읽기도 과반수로 가면 최신 값이 보장된다. 리전 손실에 강하고 일관성이 강하며, 대가는 모든 쓰기가 과반수에 도달하는 데 드는 지연이다.","steps":["원칙: W + R > N 규칙에 따른 과반수 확인 응답. 소수 리전이 다운되어도 시스템은 일관되게 계속 쓴다.","엔진 예: 합의 기반 분산 DB(예: Raft/Paxos를 사용하는 시스템), Cassandra의 QUORUM 일관성 수준.","적용 시점: 리전 재해 복원력과 강한 일관성이 모두 필요하고 더 높은 쓰기 지연이 허용되는 경우.","RPO/RTO: 과반수가 내구성 있게 기록하므로 RPO≈0. 리전 손실 후 과반수가 자동으로 재구성되면 RTO도 낮을 수 있다.","주의: 지리적으로 먼 리전 간 quorum은 빛의 속도에 따른 지연으로 모든 쓰기에 수십 ms를 추가한다. 리전 수와 배치를 신중하게 선택한다."],"related":["decide-ha","decide-shard-replica"]},"sol_data_residency":{"kind":"solution","title":"권장: 데이터 상주 / 컴플라이언스(GDPR)","detail":"일부 데이터(개인, 의료, 금융)는 법적으로 특정 국가/리전의 경계 내에 있어야 한다(예: GDPR). 이 경우 아키텍처를 '모든 곳에 복사'가 아니라 데이터가 지리적으로 고정되도록 설계한다.","steps":["원칙: 개인/규제 대상 데이터는 발생한 리전에 보관한다. 리전 외부로의 복제를 제한하거나 금지한다.","리전 파티셔닝: 사용자를 등록된 리전으로 라우팅한다(geo-partitioning). 각 리전은 해당 거주자의 데이터를 보유한다.","글로벌 vs 로컬 데이터: 카탈로그 같은 글로벌 데이터는 모든 곳에 복제할 수 있고, 개인 데이터는 리전에 머문다.","주의: 백업과 읽기 복제본도 데이터를 이동시킨다. 백업 저장소 리전과 DR 리전도 컴플라이언스 범위에 포함된다.","주의: '낮은 지연을 위해 모든 곳에 복사하자'는 결정은 규제 대상 데이터에 대해 법 위반이 될 수 있다. 처음부터 상주 제약을 아키텍처에 반영한다."],"related":["security-incident","audit-setup"]}}},"solver/arch-highscale.json":{"id":"arch-highscale","title":"참조 아키텍처: 대규모 / 고트래픽","icon":"🏛️","category":"참조 아키텍처","intro":"트래픽이 증가할 때 데이터베이스를 보호하는 올바른 순서가 있다: 먼저 단순하고 저렴한 수단, 그다음 복잡한 수단. 이 트리는 수직 확장 → 읽기 복제본 → 샤딩 순서, 캐싱 패턴(cache-aside, write-back), 큐를 이용한 버퍼링 + backpressure, rate limiting, bulkhead/circuit breaker를 통한 데이터베이스 격리, 핫스팟 방지를 단계별로 다룬다. 핵심 원칙: 샤딩처럼 되돌리기 어려운 결정은 마지막으로 미루고 저렴한 개선을 먼저 소진한다.","start":"root","nodes":{"root":{"kind":"step","title":"병목은 어디인가: 단일 인스턴스가 포화되었는가, 읽기나 쓰기가 많은가, 아니면 트래픽이 급증하는가?","detail":"확장 문제를 올바른 곳에서 해결하려면 먼저 병목을 식별한다. 단일 인스턴스가 CPU/메모리에서 포화되었는가, 읽기가 지배적인가, 쓰기가 한계에 이르렀는가, 아니면 갑작스러운 트래픽 급증이 데이터베이스를 압도하는가? 각각 다른 구성 요소를 가리킨다.","choices":[{"label":"단일 인스턴스가 포화됨, 아직 단순한 수단을 시도하지 않음","next":"scale_path"},{"label":"읽기가 매우 많고 증가 중","next":"sol_cache"},{"label":"갑작스러운 트래픽 급증 / 폭발적 부하가 DB를 압도함","next":"protect"}]},"scale_path":{"kind":"step","title":"어느 확장 단계에 있는가: 수직, 읽기 복제본, 샤딩?","detail":"확장에는 올바른 순서가 있다. 수직 확장(더 강력한 하드웨어)이 가장 단순한 첫 단계이고, 그다음 읽기를 복제본에 분산하며, 단일 primary가 더 이상 쓰기를 감당하지 못할 때만 수평 분할(샤딩)로 넘어간다. 지금 어디에 있는가?","choices":[{"label":"아직 하드웨어를 증설하지 않았거나 기본 튜닝을 소진하지 않음","next":"sol_vertical"},{"label":"쓰기는 단일 인스턴스로 충분하지만 읽기 부하를 분산해야 함","next":"sol_read_replica"},{"label":"단일 primary가 더 이상 쓰기 부하를 감당하지 못함","next":"sol_sharding"}]},"protect":{"kind":"step","title":"데이터베이스를 부하로부터 어떻게 보호할 것인가: 버퍼링, 제한, 격리?","detail":"확장이 항상 DB를 키우는 것은 아니다. 많은 경우 DB를 과부하로부터 보호하는 것이다. 갑작스러운 쓰기 급증을 큐로 평탄화할 것인가, 들어오는 요청 속도를 제한할 것인가(rate limiting), 아니면 단일 의존성이 시스템 전체를 다운시키지 않도록 할 것인가(bulkhead/circuit breaker)?","choices":[{"label":"갑작스러운 쓰기 급증을 평탄화하고 싶음","next":"sol_queue"},{"label":"들어오는 요청 속도를 제한/공정하게 분배하고 싶음","next":"sol_ratelimit"},{"label":"장애가 난 하나의 의존성이 시스템 전체를 다운시키지 않기를 원함","next":"sol_bulkhead"}]},"sol_vertical":{"kind":"solution","title":"권장: 수직 확장과 기본 튜닝 우선","detail":"가장 저렴하고 위험이 적은 단계는 수직 확장이다: CPU/RAM/더 빠른 디스크를 추가하고 기본 사항(인덱스, 커넥션 풀링, 메모리 파라미터)을 바로잡는다. 샤딩 같은 복잡한 결정 전에 이 개선들을 소진한다.","steps":["첫 단계: 하드웨어(CPU/RAM/NVMe)를 증설하고 buffer/cache 메모리 파라미터를 워크로드에 맞게 튜닝한다.","저렴한 개선: 누락된 인덱스를 추가하고, 무거운 쿼리를 수정하며, 앞단에 커넥션 풀러(PgBouncer/ProxySQL)를 둔다. 대부분의 '확장' 문제는 실제로 튜닝 문제다.","충분한 시점: 증설한 단일 인스턴스가 목표 지연/처리량을 충족하면 멈춘다. 복잡성을 더 추가하지 않는다.","주의: 수직 확장에는 물리적·비용 한계가 있다. 한계에 가까워지면 읽기 복제본과 캐싱으로 넘어간다.","다음 단계: 읽기가 지배적이면 복제본 + 캐시, 쓰기가 지배적이고 한계를 넘었다면 샤딩."],"related":["decide-capacity","high-cpu"]},"sol_read_replica":{"kind":"solution","title":"권장: 읽기 복제본으로 읽기 분산","detail":"쓰기는 단일 primary에 유지하고 읽기 전용 트래픽은 하나 이상의 복제본에 분산한다. 샤딩보다 훨씬 단순하며 대부분의 읽기 중심 워크로드를 해결한다. primary는 쓰기와 일관성이 핵심인 읽기만 담당한다.","steps":["토폴로지: primary(쓰기) → 다수의 비동기 읽기 복제본. 로드 밸런서를 통해 읽기를 복제본에 분산한다.","엔진: PostgreSQL streaming replication, MySQL 복제본, 관리형 읽기 복제본.","적용 시점: 읽기/쓰기 비율이 읽기 쪽으로 크게 치우쳐 있고 쓰기는 여전히 단일 인스턴스로 감당 가능한 경우.","주의: 복제 지연은 오래된 데이터 읽기를 유발한다. read-your-writes가 필요한 읽기는 primary에서 제공한다.","다음 단계: 복제본으로도 부족하거나 실제 병목이 쓰기라면 캐시를 추가한 뒤 샤딩을 검토한다."],"related":["replication-lag","decide-shard-replica"]},"sol_sharding":{"kind":"solution","title":"권장: 최후 수단으로서의 샤딩(수평 분할)","detail":"단일 primary가 쓰기 부하를 감당하지 못하면 데이터를 키 기준으로 여러 독립 조각(샤드)으로 분할한다. 각 샤드가 쓰기의 일부를 담당한다. 강력하지만 되돌리기 어렵고, 샤드 간 쿼리와 트랜잭션을 복잡하게 하므로 마지막으로 미룬다.","steps":["샤드 키: 부하와 데이터를 균등하게 분산하고 대부분의 쿼리가 단일 샤드에 도달하게 하는 키를 선택한다(예: customer_id).","방법: Hash 기반(균등 분산) 또는 range 기반(범위 쿼리가 쉽지만 핫스팟 위험) 분할.","엔진 예: MongoDB sharding, Citus (PostgreSQL), Vitess (MySQL) 또는 애플리케이션 수준 샤딩.","주의: 샤드 간 조인/트랜잭션은 비싸고 복잡하다. 대부분의 트랜잭션이 단일 샤드 내에서 완료되도록 스키마를 설계한다.","주의: 잘못된 키는 핫스팟(한 샤드에 부하 집중)을 만든다. 증가하는 id/날짜 같은 단조 증가 키를 피하고, resharding이 어렵다는 점을 기억한다."],"related":["decide-shard-replica","decide-partition"]},"sol_cache":{"kind":"solution","title":"권장: 캐싱 패턴(cache-aside + write-back)","detail":"읽기 중심 고트래픽에서는 DB 앞단에 인메모리 캐시(Redis)를 두어 대부분의 읽기를 DB에 도달하지 않고 처리한다. 대량 쓰기에서는 write-back이 쓰기를 버퍼링해 DB에 배치로 반영하므로 primary 부담이 줄어든다.","steps":["Cache-aside: 애플리케이션이 먼저 Redis를 확인하고, miss 시 DB에서 가져와 캐시에 쓴다. 이를 통해 읽기 부하 대부분을 DB에서 덜어 낸다.","Write-back(write-behind): 카운터/지표처럼 쓰기량이 매우 많은 경우 먼저 캐시에 쓰고 백그라운드에서 DB에 배치로 반영한다.","TTL + 무효화: 키에 적절한 TTL을 부여한다. 쓰기 시 관련 키를 갱신/삭제해 오래된 데이터가 남지 않게 한다.","주의: Cache stampede: 인기 키가 만료되면 수천 개의 요청이 동시에 DB로 몰린다. locking/single-flight, 조기 갱신, jitter를 둔 TTL을 사용한다.","주의: write-back에서는 DB에 쓰기 전에 캐시 노드가 다운되면 데이터가 손실된다. 핵심 데이터는 내구성 있는 큐/AOF로 보호한다."],"related":["decide-cache","cache-hit-low"]},"sol_queue":{"kind":"solution","title":"권장: 큐를 이용한 버퍼링(backpressure)","detail":"갑작스러운 쓰기 급증이 DB를 직접 강타하지 않도록 중간에 내구성 있는 큐(예: Kafka/메시지 큐)를 두고 DB가 감당할 수 있는 속도로 쓰기를 소비한다. 큐가 급증을 흡수하며, 시스템은 동기적으로 실패하는 대신 통제된 지연으로 동작한다(backpressure).","steps":["원칙: 프로듀서는 이벤트를 큐에 쓰고, 컨슈머는 감당 가능한 속도로(배치일 수 있음) DB에 쓴다.","엔진: Kafka 또는 메시지 큐. 피크 트래픽으로부터 DB를 격리하는 버퍼 역할을 한다.","Backpressure: 큐가 계속 커지면 프로듀서를 늦추거나 신호를 보낸다. 무한정 쌓이게 두지 않는다. 그렇지 않으면 지연이 폭증한다.","적용 시점: 쓰기 트래픽이 매우 폭발적이고 즉각적인 동기 처리가 필수가 아닌 경우(비동기 처리 허용).","주의: 큐는 비동기성을 도입하며 결과는 최종 일관성을 가진다. 컨슈머를 멱등적으로 만든다(메시지가 재전달될 수 있음)."],"related":["connection-storm"]},"sol_ratelimit":{"kind":"solution","title":"권장: rate limiting(요청 속도 제한)","detail":"출처별로 들어오는 요청을 제한하면(rate limiting) 남용과 단일 클라이언트의 리소스 독점을 모두 방지한다. DB에 도달하는 부하에 상한을 두어 시스템을 예측 가능하고 공정하게 유지한다.","steps":["배치: DB 앞단(API gateway / 애플리케이션 엣지)에서 제한을 적용한다. 수락된 요청만 DB에 도달해야 한다.","방법: token bucket / sliding window로 클라이언트별 또는 사용자별 속도를 제한한다. 초과 시 429를 반환한다.","공정한 분배: 쿼터를 사용해 'noisy neighbor'가 용량 전체를 차지하지 못하게 한다.","적용 시점: 트래픽이 갑자기 급증할 수 있거나 남용/자동화 클라이언트의 위험이 있는 경우.","주의: rate limiting은 확장이 아니라 보호 수단이다. 실제 용량 요구가 있다면 실제 병목(수직/복제본/샤드)도 해결한다."],"related":["connection-storm"]},"sol_bulkhead":{"kind":"solution","title":"권장: bulkhead + circuit breaker(격리)","detail":"의존성(예: 느려진 DB나 외부 서비스)이 시스템 전체를 다운시키지 않도록 리소스를 구획(bulkhead)으로 나누고, 계속 실패하는 의존성에 대한 호출을 일시적으로 차단한다(circuit breaker). 이렇게 하면 부분 장애가 시스템 전체로 번지지 않는다.","steps":["Bulkhead: 작업 종류별로 별도의 커넥션 풀/스레드를 할당한다. 하나의 느린 경로가 풀 전체를 소진해 다른 작업을 고갈시켜서는 안 된다.","Circuit breaker: 의존성이 오류/지연 임계값을 넘으면 회로를 '열고' 한동안 빠르게 실패시킨다. 이후 시험 호출(half-open)로 복구 여부를 확인한다.","타임아웃: 모든 DB/서비스 호출에 적절한 타임아웃을 둔다. 멈춘 호출이 풀을 잠가서는 안 된다.","적용 시점: 의존성이 많은 시스템에서 한 구성 요소의 지연이 연쇄 장애로 이어질 위험이 있는 경우.","주의: 회로가 열렸을 때를 위한 단계적 기능 축소(graceful degradation)를 설계한다: 요청 전체를 거부하는 대신 캐시의 오래된 데이터나 부분 결과를 반환한다."],"related":["connection-storm","high-cpu"]}}},"types/types-functions.json":{"schemaVersion":1,"sections":[{"id":"datatypes","title":"데이터 유형","icon":"🔤","rows":[{"id":"var-text","concept":"가변 길이 텍스트","engines":{"oracle":"VARCHAR2(n)","postgres":"VARCHAR(n) / TEXT","mysql":"VARCHAR(n)","mssql":"NVARCHAR(n)","mongodb":"string","redis":"string (value)","elasticsearch":"text / keyword"},"note":"ES에서 text는 전문 검색에, keyword는 정확한 일치/집계에 사용한다."},{"id":"fixed-text","concept":"고정 길이 텍스트","engines":{"oracle":"CHAR(n)","postgres":"CHAR(n)","mysql":"CHAR(n)","mssql":"NCHAR(n)","mongodb":"string","redis":"string","elasticsearch":"keyword"},"note":"문서/키-값 시스템에는 고정 길이 개념이 없으며 값은 문자열로 저장된다."},{"id":"large-text","concept":"대용량 텍스트(CLOB/TEXT)","engines":{"oracle":"CLOB","postgres":"TEXT","mysql":"LONGTEXT / TEXT","mssql":"NVARCHAR(MAX)","mongodb":"string","redis":"string (up to 512 MB)","elasticsearch":"text"}},{"id":"int32","concept":"정수(32비트)","engines":{"oracle":"NUMBER(10)","postgres":"INTEGER / INT4","mysql":"INT","mssql":"INT","mongodb":"int (NumberInt / int32)","redis":"string (interpreted as an integer)","elasticsearch":"integer"},"note":"Oracle에는 별도의 INT 유형이 없으며 NUMBER를 사용한다."},{"id":"int64","concept":"큰 정수(64비트)","engines":{"oracle":"NUMBER(19)","postgres":"BIGINT / INT8","mysql":"BIGINT","mssql":"BIGINT","mongodb":"long (NumberLong / int64)","redis":"string (INCR 64-bit integer)","elasticsearch":"long"}},{"id":"decimal","concept":"소수/정확한 숫자(NUMERIC)","engines":{"oracle":"NUMBER(p,s)","postgres":"NUMERIC(p,s) / DECIMAL","mysql":"DECIMAL(p,s)","mssql":"DECIMAL(p,s) / NUMERIC","mongodb":"decimal (Decimal128)","redis":"none, stored as a string","elasticsearch":"scaled_float"},"note":"금액처럼 정확한 값에는 float보다 이 유형들을 선호한다."},{"id":"float","concept":"부동소수점(FLOAT/DOUBLE)","engines":{"oracle":"BINARY_DOUBLE / FLOAT","postgres":"REAL / DOUBLE PRECISION","mysql":"FLOAT / DOUBLE","mssql":"FLOAT / REAL","mongodb":"double","redis":"none, string (INCRBYFLOAT)","elasticsearch":"float / double"}},{"id":"boolean","concept":"논리(BOOLEAN)","engines":{"oracle":"NUMBER(1) / CHAR(1) (BOOLEAN in 23c)","postgres":"BOOLEAN","mysql":"TINYINT(1) / BOOL","mssql":"BIT","mongodb":"bool","redis":"none, 0/1 string or bit","elasticsearch":"boolean"},"note":"Oracle SQL의 BOOLEAN 컬럼 유형은 23c에서야 도입되었다. 그 이전에는 NUMBER/CHAR로 대체한다."},{"id":"date","concept":"날짜","engines":{"oracle":"DATE (includes time)","postgres":"DATE","mysql":"DATE","mssql":"DATE","mongodb":"Date (ISODate)","redis":"none (epoch string/number)","elasticsearch":"date (with format)"},"note":"Oracle DATE 유형은 날짜와 시간을 모두 담는다. 날짜만 담는 유형이 아니다."},{"id":"datetime","concept":"날짜-시간(timestamp/datetime)","engines":{"oracle":"TIMESTAMP","postgres":"TIMESTAMP","mysql":"DATETIME / TIMESTAMP","mssql":"DATETIME2","mongodb":"Date (ISODate)","redis":"none (epoch string/number)","elasticsearch":"date"}},{"id":"datetime-tz","concept":"시간대를 포함한 날짜-시간","engines":{"oracle":"TIMESTAMP WITH TIME ZONE","postgres":"TIMESTAMPTZ","mysql":"TIMESTAMP (stored as UTC)","mssql":"DATETIMEOFFSET","mongodb":"Date (stored as UTC)","redis":"none (epoch UTC)","elasticsearch":"date (UTC, time zone via format)"},"note":"Mongo와 ES는 날짜를 UTC로 저장하며 시간대는 표시할 때 적용된다."},{"id":"binary","concept":"바이너리 데이터(BLOB/binary)","engines":{"oracle":"BLOB / RAW","postgres":"BYTEA","mysql":"BLOB / VARBINARY","mssql":"VARBINARY(MAX)","mongodb":"BinData (binary)","redis":"string (binary-safe)","elasticsearch":"binary (base64, not searchable)"},"note":"Redis 문자열은 binary-safe이며 임의의 바이트 시퀀스를 담을 수 있다."},{"id":"uuid","concept":"UUID/GUID","engines":{"oracle":"RAW(16) / SYS_GUID()","postgres":"UUID","mysql":"BINARY(16) / CHAR(36)","mssql":"UNIQUEIDENTIFIER","mongodb":"UUID (BinData subtype 4) / ObjectId","redis":"none, stored as a string","elasticsearch":"keyword"},"note":"Mongo의 기본 _id는 ObjectId다. UUID는 BinData로도 저장할 수 있다."},{"id":"json","concept":"JSON","engines":{"oracle":"JSON (21c+) / CLOB","postgres":"JSON / JSONB","mysql":"JSON","mssql":"JSON (2025) / NVARCHAR","mongodb":"object (native BSON)","redis":"not native / RedisJSON module","elasticsearch":"object / nested / flattened"},"note":"Postgres에서는 조회/인덱싱에 JSONB를 선호한다. MongoDB는 원래 문서 기반이다."},{"id":"auto-increment","concept":"자동 증가(identity/serial)","engines":{"oracle":"GENERATED AS IDENTITY / SEQUENCE","postgres":"SERIAL / GENERATED ... IDENTITY","mysql":"AUTO_INCREMENT","mssql":"IDENTITY(1,1)","mongodb":"none (ObjectId or a counter pattern)","redis":"counter with INCR","elasticsearch":"none (automatic _id)"}},{"id":"enum","concept":"Enum/고정 목록","engines":{"oracle":"via a CHECK constraint","postgres":"ENUM (CREATE TYPE)","mysql":"ENUM(...)","mssql":"via a CHECK constraint","mongodb":"none (validation with JSON Schema enum)","redis":"none, stored as a string","elasticsearch":"keyword"}},{"id":"array","concept":"배열","engines":{"oracle":"VARRAY / nested table / collection","postgres":"ARRAY (e.g. INTEGER[])","mysql":"none (via JSON array)","mssql":"none (JSON / table variable)","mongodb":"array (native)","redis":"List / Set (native)","elasticsearch":"any field can be an array (native)"},"note":"ES에서는 모든 필드가 암묵적으로 배열을 담을 수 있으며 별도의 배열 유형이 없다."},{"id":"ip-address","concept":"IP 주소","engines":{"oracle":"VARCHAR2 / UTL_INADDR","postgres":"INET / CIDR","mysql":"VARBINARY(16) / INET6_ATON","mssql":"none, via VARCHAR","mongodb":"none, stored as a string","redis":"none, stored as a string","elasticsearch":"ip"},"note":"Postgres와 ES의 네이티브 IP 유형은 범위/서브넷 쿼리를 지원한다."},{"id":"money","concept":"금액","engines":{"oracle":"NUMBER(p,2)","postgres":"MONEY / NUMERIC(p,2)","mysql":"DECIMAL(p,2)","mssql":"MONEY / DECIMAL(p,2)","mongodb":"decimal (Decimal128)","redis":"none, an integer in minor units (cents) is recommended","elasticsearch":"scaled_float"},"note":"반올림 오차를 피하기 위해 금액은 float가 아니라 DECIMAL/Decimal128로 저장한다."},{"id":"spatial","concept":"지리/공간","engines":{"oracle":"SDO_GEOMETRY (Oracle Spatial)","postgres":"GEOMETRY / GEOGRAPHY (PostGIS)","mysql":"GEOMETRY / POINT","mssql":"GEOGRAPHY / GEOMETRY","mongodb":"GeoJSON object (2dsphere index)","redis":"Geo (GEOADD, sorted set based)","elasticsearch":"geo_point / geo_shape"}}]},{"id":"functions","title":"공통 함수","icon":"🧩","rows":[{"id":"coalesce","concept":"NULL이면 기본값 반환(COALESCE/NVL)","engines":{"oracle":"NVL(x, y) / COALESCE","postgres":"COALESCE(x, y)","mysql":"COALESCE(x, y) / IFNULL","mssql":"COALESCE(x, y) / ISNULL","mongodb":"$ifNull: [x, y]","redis":"none (application side)","elasticsearch":"?.field / painless ternary"}},{"id":"nullif-replace","concept":"NULL이면 대체(ISNULL/IFNULL)","engines":{"oracle":"NVL(x, y)","postgres":"COALESCE(x, y)","mysql":"IFNULL(x, y)","mssql":"ISNULL(x, y)","mongodb":"$ifNull: [x, y]","redis":"none","elasticsearch":"painless: doc[f].size()==0 ? y : x"}},{"id":"now","concept":"현재 날짜-시간(NOW/SYSDATE)","engines":{"oracle":"SYSDATE / SYSTIMESTAMP","postgres":"NOW() / CURRENT_TIMESTAMP","mysql":"NOW()","mssql":"GETDATE() / SYSDATETIME()","mongodb":"$$NOW / new Date()","redis":"TIME command","elasticsearch":"now (date math)"}},{"id":"current-date","concept":"현재 날짜","engines":{"oracle":"TRUNC(SYSDATE) / CURRENT_DATE","postgres":"CURRENT_DATE","mysql":"CURDATE()","mssql":"CAST(GETDATE() AS DATE)","mongodb":"$dateTrunc / new Date()","redis":"TIME (epoch)","elasticsearch":"now/d"}},{"id":"concat","concept":"텍스트 연결(concat)","engines":{"oracle":"a || b / CONCAT","postgres":"a || b / CONCAT","mysql":"CONCAT(a, b)","mssql":"CONCAT(a, b) / a + b","mongodb":"$concat: [a, b]","redis":"APPEND","elasticsearch":"painless: a + b"},"note":"MySQL에서 ||는 기본적으로 OR을 의미한다. 연결에는 CONCAT을 사용한다."},{"id":"substring","concept":"부분 문자열","engines":{"oracle":"SUBSTR(s, p, n)","postgres":"SUBSTRING(s, p, n)","mysql":"SUBSTRING(s, p, n)","mssql":"SUBSTRING(s, p, n)","mongodb":"$substrCP: [s, p, n]","redis":"GETRANGE key start end","elasticsearch":"painless: s.substring(p, e)"}},{"id":"length","concept":"문자열 길이","engines":{"oracle":"LENGTH(s)","postgres":"LENGTH(s) / CHAR_LENGTH","mysql":"CHAR_LENGTH(s)","mssql":"LEN(s)","mongodb":"$strLenCP: s","redis":"STRLEN key","elasticsearch":"painless: s.length()"}},{"id":"upper-lower","concept":"대문자/소문자","engines":{"oracle":"UPPER(s) / LOWER(s)","postgres":"UPPER(s) / LOWER(s)","mysql":"UPPER(s) / LOWER(s)","mssql":"UPPER(s) / LOWER(s)","mongodb":"$toUpper / $toLower","redis":"none (application side)","elasticsearch":"painless: s.toUpperCase() / lowercase analyzer"}},{"id":"trim","concept":"공백 제거(TRIM)","engines":{"oracle":"TRIM(s) / LTRIM / RTRIM","postgres":"TRIM(s)","mysql":"TRIM(s)","mssql":"TRIM(s)","mongodb":"$trim: { input: s }","redis":"none","elasticsearch":"painless: s.trim() / trim token filter"}},{"id":"replace","concept":"텍스트 치환(REPLACE)","engines":{"oracle":"REPLACE(s, a, b)","postgres":"REPLACE(s, a, b)","mysql":"REPLACE(s, a, b)","mssql":"REPLACE(s, a, b)","mongodb":"$replaceAll / $replaceOne","redis":"SETRANGE (positional) / none","elasticsearch":"painless: s.replace(a, b)"}},{"id":"cast","concept":"유형 변환(CAST/CONVERT)","engines":{"oracle":"CAST(x AS t) / TO_NUMBER","postgres":"CAST(x AS t) / x::t","mysql":"CAST(x AS t) / CONVERT","mssql":"CAST(x AS t) / CONVERT","mongodb":"$convert / $toInt, $toString","redis":"none (everything is a string)","elasticsearch":"painless: Integer.parseInt(...) / runtime field"}},{"id":"date-to-char","concept":"날짜를 텍스트로 서식화(TO_CHAR/FORMAT)","engines":{"oracle":"TO_CHAR(d, 'YYYY-MM-DD')","postgres":"TO_CHAR(d, 'YYYY-MM-DD')","mysql":"DATE_FORMAT(d, '%Y-%m-%d')","mssql":"FORMAT(d, 'yyyy-MM-dd') / CONVERT","mongodb":"$dateToString: { format, date }","redis":"none","elasticsearch":"date field format / painless SimpleDateFormat"}},{"id":"parse-date","concept":"텍스트를 날짜로 변환(TO_DATE/PARSE)","engines":{"oracle":"TO_DATE(s, 'YYYY-MM-DD')","postgres":"TO_DATE(s, 'YYYY-MM-DD')","mysql":"STR_TO_DATE(s, '%Y-%m-%d')","mssql":"CONVERT(date, s) / PARSE","mongodb":"$dateFromString: { dateString: s }","redis":"none","elasticsearch":"automatic parsing via date mapping"}},{"id":"date-add","concept":"날짜에 더하기(interval/DATEADD)","engines":{"oracle":"d + n / ADD_MONTHS / INTERVAL","postgres":"d + INTERVAL '1 day'","mysql":"DATE_ADD(d, INTERVAL 1 DAY)","mssql":"DATEADD(day, 1, d)","mongodb":"$dateAdd: { startDate, unit, amount }","redis":"none (add to the epoch)","elasticsearch":"date math: now+1d"}},{"id":"date-diff","concept":"두 날짜 간 차이(DATEDIFF)","engines":{"oracle":"d1 - d2 (days) / MONTHS_BETWEEN","postgres":"d1 - d2 / AGE(d1, d2)","mysql":"DATEDIFF(d1, d2)","mssql":"DATEDIFF(day, d2, d1)","mongodb":"$dateDiff: { startDate, endDate, unit }","redis":"none (epoch difference)","elasticsearch":"painless: ChronoUnit.DAYS.between(...)"}},{"id":"extract","concept":"날짜 부분 추출(EXTRACT)","engines":{"oracle":"EXTRACT(YEAR FROM d)","postgres":"EXTRACT(YEAR FROM d) / DATE_PART","mysql":"EXTRACT(YEAR FROM d) / YEAR(d)","mssql":"DATEPART(year, d) / YEAR(d)","mongodb":"$year / $month / $dayOfMonth","redis":"none","elasticsearch":"painless: doc[f].value.year"}},{"id":"round","concept":"반올림(ROUND)","engines":{"oracle":"ROUND(x, n)","postgres":"ROUND(x, n)","mysql":"ROUND(x, n)","mssql":"ROUND(x, n)","mongodb":"$round: [x, n]","redis":"none","elasticsearch":"painless: Math.round(x)"}},{"id":"mod","concept":"나머지(Modulo)","engines":{"oracle":"MOD(a, b)","postgres":"MOD(a, b) / a % b","mysql":"MOD(a, b) / a % b","mssql":"a % b","mongodb":"$mod: [a, b]","redis":"none","elasticsearch":"painless: a % b"}},{"id":"case","concept":"조건(CASE/DECODE)","engines":{"oracle":"CASE ... / DECODE(x, ...)","postgres":"CASE WHEN ... END","mysql":"CASE WHEN ... END / IF()","mssql":"CASE WHEN ... END / IIF()","mongodb":"$cond / $switch","redis":"none","elasticsearch":"painless: if/else"}},{"id":"greatest-least","concept":"최대/최소(GREATEST/LEAST)","engines":{"oracle":"GREATEST(...) / LEAST(...)","postgres":"GREATEST(...) / LEAST(...)","mysql":"GREATEST(...) / LEAST(...)","mssql":"GREATEST(...) / LEAST(...) (2022+)","mongodb":"$max / $min (expression)","redis":"none","elasticsearch":"painless: Math.max / Math.min"}},{"id":"string-agg","concept":"그룹 내 연결(STRING_AGG/GROUP_CONCAT/LISTAGG)","engines":{"oracle":"LISTAGG(x, ',')","postgres":"STRING_AGG(x, ',')","mysql":"GROUP_CONCAT(x)","mssql":"STRING_AGG(x, ',')","mongodb":"$push + $reduce / $concatArrays","redis":"none","elasticsearch":"terms agg / scripted_metric"}},{"id":"row-number","concept":"행 번호(ROW_NUMBER)","engines":{"oracle":"ROW_NUMBER() OVER (...)","postgres":"ROW_NUMBER() OVER (...)","mysql":"ROW_NUMBER() OVER (...)","mssql":"ROW_NUMBER() OVER (...)","mongodb":"$setWindowFields: { $documentNumber }","redis":"none","elasticsearch":"none (top_hits / bucket_sort)"}},{"id":"limit","concept":"처음 N개 행(LIMIT/TOP/ROWNUM)","engines":{"oracle":"FETCH FIRST n ROWS ONLY / ROWNUM","postgres":"LIMIT n","mysql":"LIMIT n","mssql":"TOP n / OFFSET-FETCH","mongodb":"$limit: n / .limit(n)","redis":"LRANGE key 0 n-1","elasticsearch":"size: n"}},{"id":"current-user","concept":"현재 사용자","engines":{"oracle":"USER / SYS_CONTEXT","postgres":"CURRENT_USER","mysql":"CURRENT_USER() / USER()","mssql":"CURRENT_USER / SUSER_SNAME()","mongodb":"connectionStatus / $$USER (none, application side)","redis":"ACL WHOAMI","elasticsearch":"_security/_authenticate API"}},{"id":"gen-uuid","concept":"UUID 생성","engines":{"oracle":"SYS_GUID()","postgres":"gen_random_uuid()","mysql":"UUID()","mssql":"NEWID()","mongodb":"UUID() / ObjectId()","redis":"none (application side)","elasticsearch":"automatic _id / application side"}},{"id":"json-extract","concept":"JSON 필드 추출","engines":{"oracle":"JSON_VALUE(j, '$.a')","postgres":"j->>'a' / j #>> '{a}'","mysql":"JSON_EXTRACT(j, '$.a') / j->>'$.a'","mssql":"JSON_VALUE(j, '$.a')","mongodb":"doc.a (native access)","redis":"JSON.GET key $.a (RedisJSON)","elasticsearch":"doc['a'] / field access (native)"}},{"id":"regexp","concept":"정규식 일치(REGEXP)","engines":{"oracle":"REGEXP_LIKE(s, p)","postgres":"s ~ p / REGEXP_MATCHES","mysql":"s REGEXP p / REGEXP_LIKE","mssql":"REGEXP_LIKE (2025+) / before: LIKE / CLR","mongodb":"$regex / $regexMatch","redis":"none (SCAN MATCH glob pattern)","elasticsearch":"regexp query"},"note":"SQL Server 2025(및 Azure SQL)에 REGEXP_LIKE, REGEXP_REPLACE, REGEXP_SUBSTR 같은 네이티브 정규식 함수가 추가되었다(REGEXP_LIKE는 호환성 수준 170 필요). 이전 버전에서는 LIKE 또는 CLR 함수가 필요하다."}]}]},"reference/params-oracle.json":{"engineId":"oracle","engine":"Oracle","order":1,"params":[{"name":"sga_target","default":"0","recommended":"전용 서버에서 RAM의 ~60%","unit":"bytes (K/M/G)","dynamic":"dynamic","desc":"Automatic Shared Memory Management(ASMM)를 위한 전체 SGA 크기를 설정한다. 하위 풀을 자동으로 배분한다.","tags":["memory","memory","sga"]},{"name":"sga_max_size","default":"Same as sga_target","recommended":"sga_target과 같거나 약간 높게","unit":"bytes (K/M/G)","dynamic":"restart","desc":"런타임에 SGA가 커질 수 있는 상한을 설정한다. 값을 늘리려면 인스턴스 재시작이 필요하다.","tags":["memory","memory","sga"]},{"name":"pga_aggregate_target","default":"10M or 20% of the SGA","recommended":"RAM의 ~20%","unit":"bytes (K/M/G)","dynamic":"dynamic","desc":"모든 서버 프로세스가 사용하는 PGA 메모리의 전체 목표치를 설정한다. 정렬 및 해시 작업에 영향을 준다.","tags":["memory","memory","pga","sort"]},{"name":"pga_aggregate_limit","default":"Greatest of: 2G, 200% of pga_aggregate_target, 3M x processes (not exceeding 120% of physical RAM minus SGA)","recommended":"대개 기본값으로 충분함","unit":"bytes (K/M/G)","dynamic":"dynamic","desc":"인스턴스 전체 PGA 메모리의 하드 상한. 초과하면 가장 많이 소비하는 세션의 호출이 중단된다(SYS 및 백그라운드 프로세스 제외). Oracle 12c에서 도입되었다.","tags":["memory","memory","pga","limit"]},{"name":"memory_target","default":"0","recommended":"0(SGA/PGA 개별 관리를 선호)","unit":"bytes (K/M/G)","dynamic":"dynamic","desc":"Automatic Memory Management(AMM)로 SGA와 PGA를 단일 목표치로 통합한다. 대규모 시스템에서는 권장하지 않는다.","tags":["memory","memory","amm"]},{"name":"db_cache_size","default":"0 (automatic)","recommended":"sga_target을 사용하는 경우 0으로 둔다","unit":"bytes (K/M/G)","dynamic":"dynamic","desc":"기본 블록 크기에 대한 버퍼 캐시 크기를 설정한다. 데이터 블록을 메모리에 유지한다.","tags":["memory","memory","cache","buffer"]},{"name":"shared_pool_size","default":"0 (automatic)","recommended":"sga_target을 사용하는 경우 0으로 둔다","unit":"bytes (K/M/G)","dynamic":"dynamic","desc":"SQL/PLSQL 파싱 결과, 딕셔너리 캐시, 라이브러리 캐시를 보관하는 공유 풀의 크기.","tags":["memory","memory","shared","pool"]},{"name":"processes","default":"100 / 300 (depending on version)","recommended":"예상 동시 접속 수에 따라 늘린다","unit":"integer","dynamic":"restart","desc":"인스턴스에 접속할 수 있는 운영체제 프로세스의 최대 수. 변경 시 재시작이 필요하다.","tags":["connection","connection","process","process"]},{"name":"sessions","default":"Derived from processes (1.5x+22)","recommended":"기본 산출값으로 대개 충분하다","unit":"integer","dynamic":"restart","desc":"동시에 존재할 수 있는 최대 세션 수. processes 값에서 자동으로 산출된다.","tags":["connection","connection","session","session"]},{"name":"open_cursors","default":"50","recommended":"애플리케이션 요구에 따라 300-1000","unit":"integer","dynamic":"dynamic","desc":"한 세션이 동시에 열어 둘 수 있는 커서의 최대 수를 설정한다.","tags":["cursor","cursor","session","session"]},{"name":"cursor_sharing","default":"EXACT","recommended":"EXACT (바인드 변수 사용)","unit":"EXACT | FORCE","dynamic":"dynamic","desc":"리터럴이 포함된 SQL 문을 바인드 변수로 공유할지 결정한다. FORCE는 리터럴을 치환한다.","tags":["cursor","cursor","sql","parse"]},{"name":"optimizer_mode","default":"ALL_ROWS","recommended":"ALL_ROWS","unit":"ALL_ROWS | FIRST_ROWS_n","dynamic":"dynamic","desc":"비용 기반 옵티마이저의 목표를 설정한다. 전체 처리량과 첫 행 응답 시간 중 무엇을 우선할지 정한다.","tags":["optimizer","optimize","plan","sql"]},{"name":"undo_retention","default":"900","recommended":"가장 긴 쿼리/flashback 시간 기준","unit":"seconds","dynamic":"dynamic","desc":"커밋된 undo 데이터를 가능한 한 보존할 시간을 설정한다. ORA-01555 위험에 영향을 준다.","tags":["undo","rollback","retention","flashback"]},{"name":"undo_tablespace","default":"UNDOTBS1","recommended":"충분한 크기의 전용 undo 테이블스페이스","unit":"string (tablespace name)","dynamic":"dynamic","desc":"자동 undo 관리에서 사용하는 활성 undo 테이블스페이스를 설정한다. 온라인으로 변경할 수 있다.","tags":["undo","rollback","tablespace"]},{"name":"db_recovery_file_dest_size","default":"0","recommended":"archivelog + 백업 + flashback 로그를 담을 만큼 충분히 크게","unit":"bytes (K/M/G)","dynamic":"dynamic","desc":"Fast Recovery Area(FRA)의 최대 디스크 크기를 설정한다. 아카이브 로그와 백업을 보관한다.","tags":["recovery","recovery","fra","archive"]},{"name":"db_writer_processes","default":"1 or based on CPU count","recommended":"약 CPU/8 (쓰기 부하가 높으면 늘린다)","unit":"integer","dynamic":"restart","desc":"더티 블록을 디스크에 기록하는 database writer(DBWn) 프로세스 수를 설정한다. 쓰기 부하를 병렬화한다.","tags":["io","write","dbwr","writer"]},{"name":"parallel_max_servers","default":"parallel_threads_per_cpu x cpu_count x concurrent_parallel_users (1/2/4) x 5; capped at processes - 15","recommended":"워크로드와 CPU 수에 따라 설정","unit":"integer","dynamic":"dynamic","desc":"인스턴스에서 동시에 실행될 수 있는 병렬 실행 서버(PX)의 최대 수를 제한한다.","tags":["parallel","parallel","px","query"]},{"name":"statistics_level","default":"TYPICAL","recommended":"TYPICAL","unit":"BASIC | TYPICAL | ALL","dynamic":"dynamic","desc":"AWR/ADDM 및 자동 통계 수집 수준을 설정한다. BASIC은 많은 진단 기능을 비활성화한다.","tags":["statistics","statistics","awr","monitoring"]},{"name":"deferred_segment_creation","default":"TRUE","recommended":"TRUE (빈 테이블의 공간 절약)","unit":"TRUE | FALSE","dynamic":"dynamic","desc":"빈 테이블/인덱스의 첫 세그먼트 생성을 첫 행이 삽입될 때까지 지연한다. 공간을 절약한다.","tags":["segment","deferred-segment","space","storage"]},{"name":"audit_trail","default":"NONE (DB in databases created with DBCA)","recommended":"DB 또는 unified auditing 사용","unit":"NONE | DB | DB,EXTENDED | OS | XML","dynamic":"restart","desc":"기존 감사 레코드를 어디에 어떻게 기록할지 결정한다. 변경 시 재시작이 필요하다.","tags":["audit","audit","security","security"]},{"name":"compatible","default":"Installed version (e.g., 19.0.0)","recommended":"새 버전 기능이 안정된 후 올린다","unit":"version string","dynamic":"restart","desc":"활성화할 버전 기능을 결정한다. 한 번 올리면 낮출 수 없으며 재시작이 필요하다.","tags":["compatible","compatibility","version","version"]},{"name":"db_block_size","default":"8192","recommended":"8K (OLTP). 생성 시에만 설정","unit":"bytes","dynamic":"restart","desc":"데이터베이스의 표준 블록 크기를 설정한다. 데이터베이스 생성 시에만 설정되며 이후 변경할 수 없다.","tags":["block","block","storage","io"]},{"name":"filesystemio_options","default":"none (platform-dependent)","recommended":"SETALL (Direct + Async I/O)","unit":"none | asynch | directio | setall","dynamic":"restart","desc":"파일 시스템상 데이터파일에 대한 비동기 I/O 및 direct I/O 사용을 제어한다.","tags":["io","io","async","directio"]},{"name":"log_archive_max_processes","default":"4","recommended":"아카이브 대상 수에 따라 늘린다","unit":"integer","dynamic":"dynamic","desc":"redo 로그를 아카이브하는 ARCn 프로세스의 최대 수를 설정한다. 아카이브 지연을 줄이기 위해 늘린다.","tags":["archive","archive","redo","log"]}]},"reference/params-postgres.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"params":[{"name":"shared_buffers","default":"128MB","recommended":"RAM의 ~25%","unit":"bytes (kB/MB/GB)","dynamic":"restart","desc":"PostgreSQL이 데이터 블록 캐싱을 위해 할당하는 공유 메모리 양. 변경 시 재시작이 필요하다.","tags":["memory","memory","cache","buffer"]},{"name":"effective_cache_size","default":"4GB","recommended":"RAM의 ~50-75%","unit":"bytes (kB/MB/GB)","dynamic":"dynamic","desc":"OS + PostgreSQL 캐시의 추정 총 크기를 플래너에 알린다. 인덱스 사용에 영향을 준다.","tags":["memory","memory","planner","cache"]},{"name":"work_mem","default":"4MB","recommended":"16-64MB (접속 수에 유의)","unit":"bytes (kB/MB/GB)","dynamic":"dynamic","desc":"정렬 및 해시 연산이 디스크로 넘치기 전에 사용할 수 있는 메모리. 쿼리 플랜 노드마다 할당된다.","tags":["memory","memory","sort","query"]},{"name":"maintenance_work_mem","default":"64MB","recommended":"256MB-1GB","unit":"bytes (kB/MB/GB)","dynamic":"dynamic","desc":"VACUUM, CREATE INDEX, ALTER TABLE 등 유지보수 작업이 사용할 수 있는 최대 메모리를 설정한다.","tags":["memory","memory","vacuum","maintenance"]},{"name":"max_connections","default":"100","recommended":"100-200 (그 이상은 풀러 사용)","unit":"integer","dynamic":"restart","desc":"허용되는 동시 클라이언트 접속의 최대 수. 변경 시 재시작이 필요하다.","tags":["connection","connection","limit"]},{"name":"wal_buffers","default":"-1 (1/32 of shared_buffers)","recommended":"16MB","unit":"bytes (kB/MB/GB)","dynamic":"restart","desc":"디스크에 기록되기 전 WAL 레코드를 보관하는 공유 메모리 버퍼의 크기를 설정한다. -1(자동) 값은 최소 64kB, 최대 WAL 세그먼트 1개(보통 16MB)로 제한된다.","tags":["wal","memory","memory","buffer"]},{"name":"max_wal_size","default":"1GB","recommended":"워크로드에 따라 4-16GB","unit":"bytes (MB/GB)","dynamic":"dynamic","desc":"자동 체크포인트가 발생하기 전 WAL이 커질 수 있는 소프트 상한을 설정한다.","tags":["wal","checkpoint","disk"]},{"name":"min_wal_size","default":"80MB","recommended":"1-2GB","unit":"bytes (MB/GB)","dynamic":"dynamic","desc":"재활용을 위해 WAL 파일을 유지하는 하한. 잦은 재할당을 줄인다.","tags":["wal","checkpoint","disk"]},{"name":"checkpoint_completion_target","default":"0.9","recommended":"0.9","unit":"ratio (0-1)","dynamic":"dynamic","desc":"체크포인트 쓰기를 분산시킬 체크포인트 간격의 비율을 설정한다. I/O 피크를 완화한다. 기본값은 PG14에서 0.9가 되었으며 이전에는 0.5였다.","tags":["checkpoint","io","wal"]},{"name":"checkpoint_timeout","default":"5min","recommended":"15-30min","unit":"time (s/min)","dynamic":"dynamic","desc":"자동 체크포인트 사이의 최대 시간을 설정한다. 늘리면 쓰기 부하는 줄지만 복구 시간이 길어진다.","tags":["checkpoint","wal","recovery"]},{"name":"random_page_cost","default":"4.0","recommended":"SSD에서는 1.1","unit":"decimal","dynamic":"dynamic","desc":"랜덤 디스크 접근에 대한 플래너의 상대 비용을 설정한다. SSD에서 낮추면 인덱스 스캔이 유도된다.","tags":["planner","optimize","io","index"]},{"name":"effective_io_concurrency","default":"1","recommended":"SSD에서는 200","unit":"integer","dynamic":"dynamic","desc":"디스크가 동시에 처리할 수 있는 I/O 작업의 추정 수. bitmap 스캔의 프리페치에 영향을 준다. 기본값은 PG17 이하에서 1, PG18에서 16이다.","tags":["io","gc","ssd","prefetch"]},{"name":"default_statistics_target","default":"100","recommended":"100 (대형 테이블은 컬럼별로 올린다)","unit":"integer","dynamic":"dynamic","desc":"ANALYZE가 수집하는 통계의 상세 수준을 설정한다. 올리면 플랜 정확도가 높아지고 analyze 시간이 늘어난다.","tags":["statistics","statistics","analyze","planner"]},{"name":"autovacuum","default":"on","recommended":"on","unit":"on | off","dynamic":"dynamic","desc":"dead tuple을 제거하고 통계를 수집하는 자동 vacuum 프로세스를 활성화한다. 비활성화하면 bloat가 발생한다.","tags":["autovacuum","vacuum","maintenance","bloat"]},{"name":"autovacuum_vacuum_scale_factor","default":"0.2","recommended":"대형 테이블에서 0.05-0.1","unit":"ratio (0-1)","dynamic":"dynamic","desc":"테이블이 vacuum되기 전 변경되어야 하는 행 비율을 설정한다. 낮추면 대형 테이블을 더 자주 정리한다.","tags":["autovacuum","vacuum","bloat","maintenance"]},{"name":"autovacuum_max_workers","default":"3","recommended":"3-6 (테이블이 많은 시스템에서 올린다)","unit":"integer","dynamic":"restart","desc":"동시에 실행 가능한 autovacuum 워커 프로세스의 최대 수를 설정한다. PG17 이하에서는 변경 시 재시작이 필요하다. PG18에서는 autovacuum_worker_slots 한도까지 reload로 변경할 수 있다.","tags":["autovacuum","vacuum","worker","maintenance"]},{"name":"synchronous_commit","default":"on","recommended":"on (중요 데이터), off (성능 우선)","unit":"on | off | local | remote_write | remote_apply","dynamic":"dynamic","desc":"커밋이 WAL의 디스크 기록을 기다릴지 결정한다. off는 크래시 시 소량의 데이터 손실 위험이 있다.","tags":["wal","commit","durability","performance"]},{"name":"wal_level","default":"replica","recommended":"replica (복제/PITR용)","unit":"minimal | replica | logical","dynamic":"restart","desc":"WAL에 기록되는 정보량을 결정한다. 복제 및 논리 복제에 필요하며 변경 시 재시작이 필요하다.","tags":["wal","replication","replication","pitr"]},{"name":"max_wal_senders","default":"10","recommended":"standby 수 + 백업 기준","unit":"integer","dynamic":"restart","desc":"WAL을 동시에 스트리밍할 수 있는 프로세스의 최대 수를 설정한다(standby 및 백업용).","tags":["wal","replication","replication","standby"]},{"name":"wal_keep_size","default":"0","recommended":"standby 지연을 커버할 만큼","unit":"bytes (MB/GB)","dynamic":"dynamic","desc":"standby를 위해 pg_wal에 유지하는 WAL의 최소량. replication slot이 없을 때 지연된 standby를 보호한다. PG13에서 도입되었으며 이전에는 wal_keep_segments(세그먼트 수)를 사용했다.","tags":["wal","replication","replication","standby"]},{"name":"log_min_duration_statement","default":"-1 (disabled)","recommended":"느린 쿼리 포착을 위해 1000ms","unit":"milliseconds","dynamic":"dynamic","desc":"지정 시간보다 오래 실행된 문을 로그에 기록한다. 느린 쿼리 탐지에 사용하며 -1은 비활성화한다.","tags":["logging","log","slow-query","slow-query"]},{"name":"idle_in_transaction_session_timeout","default":"0 (disabled)","recommended":"락 보유 방지를 위해 60000ms","unit":"milliseconds","dynamic":"dynamic","desc":"열린 트랜잭션 안에서 지정 시간 동안 유휴 상태인 세션을 종료한다. 락 및 bloat 위험을 줄인다.","tags":["timeout","transaction","transaction","lock"]},{"name":"statement_timeout","default":"0 (disabled)","recommended":"애플리케이션에 따라 다름 (OLTP는 낮게)","unit":"milliseconds","dynamic":"dynamic","desc":"지정 시간을 초과한 문을 자동으로 취소한다. 폭주 쿼리가 리소스를 소모하는 것을 막는다.","tags":["timeout","query","query","limit"]},{"name":"shared_preload_libraries","default":"'' (empty)","recommended":"pg_stat_statements (모니터링용)","unit":"string (comma-separated list)","dynamic":"restart","desc":"서버 시작 시 로드되는 공유 라이브러리를 지정한다. pg_stat_statements 같은 확장을 여기서 설정한다.","tags":["extension","extension","preload","monitoring"]},{"name":"max_parallel_workers_per_gather","default":"2","recommended":"2-4 (CPU 수에 따라)","unit":"integer","dynamic":"dynamic","desc":"단일 Gather/GatherMerge 노드가 시작할 수 있는 병렬 워커의 최대 수를 설정한다. 0은 병렬 처리를 비활성화한다.","tags":["parallel","parallel","worker","query"]}]},"reference/params-mysql.json":{"engineId":"mysql","engine":"MySQL","order":3,"params":[{"name":"innodb_buffer_pool_size","default":"128MB","recommended":"전용 서버에서 RAM의 ~70-80%","unit":"bytes (M/G)","dynamic":"dynamic","desc":"InnoDB가 데이터와 인덱스를 캐싱하는 주 메모리 풀. 8.0에서는 온라인으로 변경할 수 있으나 큰 변경은 주의가 필요하다.","tags":["memory","memory sizing","innodb","buffer"]},{"name":"innodb_buffer_pool_instances","default":"1 (or 8 if size>1GB)","recommended":"대형 풀에서는 8","unit":"integer","dynamic":"restart","desc":"buffer pool을 나누는 인스턴스 수를 설정한다. 동시성 환경에서 mutex 경합을 줄이며 변경 시 재시작이 필요하다.","tags":["memory","memory sizing","innodb","concurrency"]},{"name":"innodb_redo_log_capacity","default":"100MB (8.0.30+)","recommended":"1-8GB (쓰기 부하에 따라, 피크 시 redo 약 1시간분)","unit":"bytes (M/G)","dynamic":"dynamic","desc":"InnoDB redo 로그가 차지하는 총 디스크 공간을 설정한다. 늘리면 체크포인트 압박과 피크 쓰기 부하가 완화된다. MySQL 8.0.30에서 도입되었으며 SET GLOBAL/SET PERSIST로 재시작 없이 변경할 수 있다. 기존 innodb_log_file_size(기본 48MB)와 innodb_log_files_in_group은 8.0.30부터 deprecated이며, 8.0.30 이전에는 이 두 값으로 redo 크기를 설정하고 재시작이 필요하다.","tags":["innodb","redo","log","io"]},{"name":"innodb_flush_log_at_trx_commit","default":"1","recommended":"1 (완전한 ACID), 2 (성능)","unit":"0 | 1 | 2","dynamic":"dynamic","desc":"커밋마다 redo 로그를 기록하고 디스크에 동기화할지 제어한다. 1은 완전한 내구성을 제공하며 2/0은 손실 위험이 있다.","tags":["innodb","commit","durability","acid"]},{"name":"innodb_flush_method","default":"8.0: fsync (Linux) / 8.4: O_DIRECT (if supported); Windows: unbuffered","recommended":"O_DIRECT (Linux)","unit":"fsync | O_DIRECT | O_DIRECT_NO_FSYNC","dynamic":"restart","desc":"InnoDB가 데이터 및 로그 파일을 디스크에 플러시하는 방식을 정의한다. O_DIRECT는 이중 버퍼링을 피하며 변경 시 재시작이 필요하다.","tags":["innodb","io","flush","gc"]},{"name":"innodb_io_capacity","default":"8.0: 200 / 8.4: 10000","recommended":"스토리지의 실제 IOPS 기준 (8.0에서 SSD는 1000-2000)","unit":"integer (IOPS)","dynamic":"dynamic","desc":"InnoDB 백그라운드 작업(flush, merge)이 사용하는 추정 디스크 IOPS 용량을 설정한다. MySQL 8.4에서 기본값이 innodb_io_capacity=10000, innodb_io_capacity_max=20000으로 상향되었다(8.0에서는 200/2000).","tags":["innodb","io","iops","ssd"]},{"name":"innodb_read_io_threads","default":"8.0: 4 / 8.4: half the number of logical CPUs (minimum 4)","recommended":"4-8 (읽기 부하에 따라)","unit":"integer","dynamic":"restart","desc":"읽기 I/O 요청을 처리하는 InnoDB 백그라운드 스레드 수를 설정한다. 변경 시 재시작이 필요하다.","tags":["innodb","io","thread","read"]},{"name":"innodb_write_io_threads","default":"4","recommended":"4-8 (쓰기 부하에 따라)","unit":"integer","dynamic":"restart","desc":"쓰기 I/O 요청을 처리하는 InnoDB 백그라운드 스레드 수를 설정한다. 변경 시 재시작이 필요하다.","tags":["innodb","io","thread","write"]},{"name":"max_connections","default":"151","recommended":"워크로드에 따라 200-500","unit":"integer","dynamic":"dynamic","desc":"허용되는 동시 클라이언트 접속의 최대 수를 설정한다. 온라인으로 변경할 수 있다.","tags":["connection","connections","limit"]},{"name":"thread_cache_size","default":"-1 (autosized)","recommended":"자동. 수명이 짧은 접속이 많으면 늘린다","unit":"integer","dynamic":"dynamic","desc":"재사용을 위해 캐시에 유지할 스레드 수를 설정한다. 새 접속 시 스레드 생성을 줄인다.","tags":["connection","connections","thread","cache"]},{"name":"table_open_cache","default":"4000","recommended":"열린 테이블 수에 따라 4000-8000","unit":"integer","dynamic":"dynamic","desc":"모든 스레드의 열린 테이블 핸들에 대한 캐시 크기를 설정한다. 테이블을 다시 여는 오버헤드를 줄인다.","tags":["cache","table","table cache","open"]},{"name":"tmp_table_size","default":"16MB","recommended":"64-256MB","unit":"bytes (M/G)","dynamic":"dynamic","desc":"인메모리 임시 테이블이 디스크 테이블로 변환되기 전의 최대 크기를 설정한다. max_heap_table_size와 함께 동작한다.","tags":["memory","memory sizing","temp","temporary table"]},{"name":"max_heap_table_size","default":"16MB","recommended":"tmp_table_size와 동일","unit":"bytes (M/G)","dynamic":"dynamic","desc":"MEMORY 엔진 테이블 및 인메모리 임시 테이블이 커질 수 있는 최대 크기를 설정한다.","tags":["memory","memory sizing","temp","heap"]},{"name":"sort_buffer_size","default":"256KB","recommended":"256KB-2MB (접속당 비용에 유의)","unit":"bytes (K/M)","dynamic":"dynamic","desc":"ORDER BY 및 GROUP BY 정렬을 위해 세션마다 할당되는 버퍼. 너무 크게 하면 메모리를 소모한다.","tags":["memory","memory sizing","sort","buffer"]},{"name":"join_buffer_size","default":"256KB","recommended":"256KB-1MB (인덱스로 해결하는 것을 우선)","unit":"bytes (K/M)","dynamic":"dynamic","desc":"인덱스를 사용할 수 없는 조인을 위해 세션마다 할당되는 버퍼. 근본적인 해결책은 적절한 인덱스 추가다.","tags":["memory","memory sizing","join","buffer"]},{"name":"sync_binlog","default":"1","recommended":"1 (완전한 안전성), 고부하 시 100-1000","unit":"integer","dynamic":"dynamic","desc":"몇 번의 커밋마다 바이너리 로그를 디스크에 동기화할지 설정한다. 1이 가장 안전하며 큰 값은 내구성 대신 성능을 얻는다.","tags":["binlog","replication","durability","commit"]},{"name":"binlog_expire_logs_seconds","default":"2592000 (30 days)","recommended":"백업/복제 윈도에 맞춰","unit":"seconds","dynamic":"dynamic","desc":"바이너리 로그 파일을 자동 삭제 전까지 보관할 기간을 설정한다. 디스크 사용량을 제어한다.","tags":["binlog","log","retention","replication"]},{"name":"long_query_time","default":"10","recommended":"1 (느린 쿼리 포착)","unit":"seconds","dynamic":"dynamic","desc":"쿼리가 slow query log에 기록되기 위해 초과해야 하는 시간을 설정한다. 소수 값을 허용한다.","tags":["slow-query","slow-query-log","log","performance"]},{"name":"slow_query_log","default":"OFF","recommended":"ON (진단용)","unit":"ON | OFF","dynamic":"dynamic","desc":"long_query_time을 초과한 쿼리를 slow query log에 기록할지 제어한다. 성능 분석 시 활성화한다.","tags":["slow-query","slow-query-log","log","monitoring"]},{"name":"wait_timeout","default":"28800 (8 hours)","recommended":"애플리케이션 풀에 맞춰 (예: 600)","unit":"seconds","dynamic":"dynamic","desc":"비대화형 접속이 닫히기 전까지 유휴 상태로 있을 수 있는 최대 시간을 설정한다. 유휴 접속을 정리한다.","tags":["timeout","connection","connections","idle"]},{"name":"max_allowed_packet","default":"64MB","recommended":"대형 BLOB/행에는 64-256MB","unit":"bytes (M/G)","dynamic":"dynamic","desc":"단일 패킷 또는 생성/중간 행의 최대 크기를 설정한다. 대형 BLOB 전송 시 늘린다.","tags":["packet","packet size","blob","network"]},{"name":"innodb_lock_wait_timeout","default":"50","recommended":"OLTP에서는 10-50","unit":"seconds","dynamic":"dynamic","desc":"InnoDB 트랜잭션이 타임아웃되기 전 행 락을 기다리는 시간을 설정한다.","tags":["innodb","lock","lock wait","timeout"]},{"name":"character_set_server","default":"utf8mb4","recommended":"utf8mb4","unit":"text (character set)","dynamic":"dynamic","desc":"서버의 기본 문자 집합을 설정한다. utf8mb4는 완전한 Unicode 지원(이모지 포함)을 제공한다.","tags":["charset","character-set","unicode","utf8mb4"]}]},"reference/params-mssql.json":{"engineId":"mssql","engine":"SQL Server","order":4,"params":[{"name":"max server memory (MB)","default":"2147483647","recommended":"총 RAM에서 OS 및 기타 서비스 몫(~10-25%)을 뺀 값","unit":"MB","dynamic":"dynamic","desc":"SQL Server buffer pool 및 기타 메모리 소비자가 사용할 수 있는 최대 메모리 양을 설정한다. 기본값은 사실상 무제한이며, 서버에서 다른 서비스가 실행되면 운영체제에 충분한 메모리를 남기도록 낮춰야 한다. sp_configure로 설정하며 RECONFIGURE로 즉시 적용된다.","tags":["memory","memory limit","buffer pool"]},{"name":"min server memory (MB)","default":"0","recommended":"중요 인스턴스에서 max 대비 합리적인 하한","unit":"MB","dynamic":"dynamic","desc":"SQL Server가 한 번 할당한 후 운영체제에 반환하지 않는 최소 메모리 양. 메모리 압박 시 SQL Server가 메모리를 과도하게 해제하는 것을 막는 데 사용한다. sp_configure로 설정하며 RECONFIGURE로 적용된다.","tags":["memory","memory limit"]},{"name":"max degree of parallelism","default":"0","recommended":"대개 8 또는 NUMA 노드당 물리 코어 수","unit":"count","dynamic":"dynamic","desc":"단일 쿼리가 병렬 실행에 사용할 수 있는 프로세서(코어)의 최대 수를 제한한다. 0은 모든 코어 사용을 의미하며, OLTP 워크로드에서 과도한 병렬 처리를 피하기 위해 제한한다. sp_configure + RECONFIGURE로 즉시 적용된다.","tags":["parallelism","parallel query","maxdop","cpu"]},{"name":"cost threshold for parallelism","default":"5","recommended":"최신 하드웨어에서 25-50","unit":"cost","dynamic":"dynamic","desc":"쿼리 플랜이 병렬로 실행되기 위해 도달해야 하는 최소 추정 비용 임계값. 기본값 5는 매우 낮아 작은 쿼리도 불필요하게 병렬화되며, 올리면 서버 전체의 병렬 처리 동작이 개선된다. sp_configure + RECONFIGURE로 즉시 적용된다.","tags":["parallelism","parallel query","cpu"]},{"name":"max worker threads","default":"0","recommended":"0 (SQL Server가 계산하도록 둔다)","unit":"count","dynamic":"dynamic","desc":"SQL Server가 클라이언트 요청 처리를 위해 생성할 수 있는 워커 스레드의 최대 수를 설정한다. 0은 프로세서 수와 아키텍처에 따라 자동 계산한다. 고급 옵션이며 sp_configure + RECONFIGURE로 재시작 없이 적용된다(sys.configurations.is_dynamic = 1).","tags":["threads","worker","cpu","concurrency"]},{"name":"optimize for ad hoc workloads","default":"0","recommended":"1 (on)","unit":"0/1","dynamic":"dynamic","desc":"한 번만 실행되는 ad hoc 쿼리에 대해 첫 실행 시 전체 플랜 대신 작은 컴파일 플랜 스텁을 캐싱한다. 일회성 플랜으로 plan cache가 차는 것을 막고 메모리 사용을 개선한다. sp_configure + RECONFIGURE로 즉시 적용된다.","tags":["plan cache","memory","memory usage"]},{"name":"backup compression default","default":"0","recommended":"1 (on)","unit":"0/1","dynamic":"dynamic","desc":"새 백업을 기본적으로 압축한다. 압축은 약간의 CPU 비용으로 백업 크기와 백업 시간을 크게 줄인다. sp_configure + RECONFIGURE로 즉시 적용된다.","tags":["backup","backup size","compression","backup compression"]},{"name":"fill factor (%)","default":"0","recommended":"0 또는 90-100 (워크로드에 따라)","unit":"%","dynamic":"restart","desc":"인덱스 생성 또는 재구성(REBUILD) 시 리프 수준 페이지를 채우는 비율을 백분율로 설정한다. 0과 100은 페이지를 가득 채우며, 낮은 값은 페이지 분할을 줄이지만 디스크 및 메모리 사용량을 늘린다. 서버 수준 기본값은 sp_configure로 변경하지만 서비스를 재시작해야 적용된다(is_dynamic = 0). 인덱스별 FILLFACTOR 옵션은 즉시 사용할 수 있다.","tags":["index","page split","storage","disk usage"]},{"name":"recovery interval (min)","default":"0","recommended":"0 (자동)","unit":"minutes","dynamic":"dynamic","desc":"목표 최대 복구 시간 기준으로 자동 체크포인트 빈도를 설정한다. 0은 SQL Server가 데이터베이스별 목표 복구 시간을 자동 관리하게 한다. sp_configure + RECONFIGURE로 즉시 적용된다.","tags":["checkpoint","recovery","crash recovery"]},{"name":"remote query timeout (s)","default":"600","recommended":"워크로드에 따라 600 또는 0 (무제한)","unit":"seconds","dynamic":"dynamic","desc":"서버가 원격 쿼리(linked server 경유)의 응답을 타임아웃 전까지 기다리는 시간. 0은 무한 대기를 의미한다. sp_configure + RECONFIGURE로 즉시 적용된다.","tags":["timeout","remote","linked server"]},{"name":"network packet size (B)","default":"4096","recommended":"4096 (특수한 경우 외에는 변경하지 않는다)","unit":"bytes","dynamic":"dynamic","desc":"네트워크 데이터 전송에 사용하는 패킷 크기. 매우 큰 데이터를 전송하는 특수 대량 작업 외에는 기본값을 권장하며, 잘못 설정하면 성능이 저하될 수 있다. sp_configure + RECONFIGURE로 적용된다.","tags":["network","network packet","packet"]},{"name":"database compatibility level (ALTER DATABASE)","default":"Depends on server version","recommended":"테스트를 마친 최신 수준","unit":"level","dynamic":"dynamic","desc":"데이터베이스가 어떤 SQL Server 버전의 동작과 쿼리 옵티마이저 모델로 실행될지 결정한다. ALTER DATABASE ... SET COMPATIBILITY_LEVEL로 데이터베이스별로 설정하며 즉시 적용된다. 카디널리티 추정 등 옵티마이저 기능에 영향을 준다.","tags":["database","compatibility level","optimizer","compatibility"]},{"name":"MAXDOP (database scoped configuration)","default":"0 (inherits the server setting)","recommended":"데이터베이스 워크로드에 맞는 값","unit":"count","dynamic":"dynamic","desc":"서버 전체 max degree of parallelism 설정을 데이터베이스 수준에서 재정의한다. ALTER DATABASE SCOPED CONFIGURATION SET MAXDOP로 설정하며 즉시 적용된다. 데이터베이스마다 다른 병렬 처리 동작을 허용한다.","tags":["parallelism","parallel query","maxdop","database scoped"]},{"name":"tempdb 파일 수 (권장)","default":"SQL Server 2016+ setup: equal to the number of logical cores, at most 8 (1 before)","recommended":"논리 코어 수와 동일, 최대 8 (필요 시 4개 단위로 늘린다)","unit":"count","dynamic":"dynamic","desc":"tempdb 데이터 파일 수는 할당 페이지 경합을 줄이는 데 중요하다. 일반적인 권장은 코어 수(최대 8)까지 동일 크기의 파일을 사용하는 것이다. ALTER DATABASE tempdb ADD FILE로 추가한 파일은 재시작 없이 즉시 사용되지만, 기존 파일이 가득 찬 경우 비례 채우기 균형을 위해 모든 파일을 동일 크기로 맞춘다. tempdb 파일 제거(REMOVE FILE)는 파일이 비워지지 않아 재시작 후에야 가능한 경우가 많다.","tags":["tempdb","storage","data files","contention"]},{"name":"Trace Flag 1117 / 1118 (참고)","default":"Default behavior for tempdb in SQL 2016+","recommended":"SQL 2016+에서는 대개 불필요, 이전 버전에서는 on","unit":"flag","dynamic":"restart","desc":"TF 1117은 파일 그룹의 모든 파일이 함께 증가하도록 강제하고, TF 1118은 혼합 익스텐트 대신 단일 익스텐트 할당을 강제한다. SQL Server 2016부터 tempdb에서는 이 동작이 기본이므로 플래그는 대개 불필요하다. 시작 플래그로 추가하면 재시작이 필요하다.","tags":["trace flag","tempdb","note"]},{"name":"Trace Flag 3226","default":"Off","recommended":"On (성공한 백업 메시지 억제)","unit":"flag","dynamic":"dynamic","desc":"성공한 백업 작업이 SQL Server 오류 로그와 Windows 이벤트 로그에 기록되지 않게 한다. 로그 백업이 잦은 시스템에서 로그가 불필요한 메시지로 차는 것(log spam)을 막는다. DBCC TRACEON(3226, -1)으로 런타임에 즉시 활성화할 수 있으며, 재시작 후에도 유지하려면 시작 매개변수로 -T3226도 추가한다.","tags":["trace flag","backup","backup messages","log"]},{"name":"lock pages in memory (Windows 정책 참고)","default":"Off","recommended":"특정 시나리오에서 서비스 계정에 부여","unit":"policy","dynamic":"restart","desc":"SQL Server 서비스 계정이 자신의 메모리 페이지가 운영체제에 의해 디스크로 페이징되는 것을 막을 수 있게 하는 Windows 사용자 권한 정책. 메모리 압박 시 성능 변동을 줄일 수 있으나 신중하게 구성해야 한다. 정책 변경 시 서비스 재시작이 필요하다.","tags":["memory","memory paging","windows","policy","note"]}]},"reference/params-mongodb.json":{"engineId":"mongodb","engine":"MongoDB","order":5,"params":[{"name":"storage.wiredTiger.engineConfig.cacheSizeGB","default":"(RAM - 1GB) * 0.5","recommended":"기본값 유지 또는 가용 RAM의 ~50%","unit":"GB","dynamic":"restart","desc":"WiredTiger 스토리지 엔진의 내부 캐시에 할당하는 메모리 양을 기가바이트 단위로 설정한다. 기본적으로 (총 RAM - 1GB)의 절반을 사용한다. 값을 수동으로 설정하는 것은 특수한 경우에만 필요하며 mongod 재시작이 필요하다.","tags":["memory","memory","cache","wiredtiger"]},{"name":"net.maxIncomingConnections","default":"1000000 (effectively limited by OS limits)","recommended":"워크로드와 운영체제 ulimit에 따라","unit":"count","dynamic":"restart","desc":"mongod 또는 mongos 인스턴스가 받아들이는 동시 수신 접속의 최대 수를 제한한다. 값이 운영체제의 파일 디스크립터/접속 한도(ulimit)보다 높으면 효과가 없으며 실제 상한은 OS가 결정한다(db.serverStatus().connections의 current + available로 확인 가능). 변경은 설정 파일에서 하며 재시작이 필요하다.","tags":["network","network","connections","connection"]},{"name":"operationProfiling.slowOpThresholdMs","default":"100","recommended":"워크로드에 따라 100 이하","unit":"ms","dynamic":"dynamic","desc":"작업이 느린 것으로 간주되어 profiler와 로그에 기록되는 시간 임계값을 밀리초 단위로 설정한다. 이 임계값을 초과한 작업이 기록되어 진단이 쉬워진다. db.setProfilingLevel()로 런타임에 즉시 변경할 수 있다.","tags":["profiling","slow query","slow query","monitoring"]},{"name":"replication.oplogSizeMB","default":"~5% of disk size (max 50GB)","recommended":"유지보수 윈도를 커버할 만큼 크게","unit":"MB","dynamic":"dynamic","desc":"replica set 멤버에서 작업을 저장하는 oplog 컬렉션의 최대 크기를 메가바이트 단위로 설정한다. oplog가 충분히 크면 지연된 멤버가 전체 resync 없이 따라잡을 수 있다. 실행 중인 replica set에서 replSetResizeOplog 명령으로 즉시 변경할 수 있다.","tags":["replication","oplog","replica set"]},{"name":"security.authorization","default":"disabled","recommended":"enabled (역할 기반 접근 제어)","unit":"enabled/disabled","dynamic":"restart","desc":"역할 기반 접근 제어(RBAC)를 활성화한다. 활성화하면 클라이언트는 인증과 권한 부여를 받아야 한다. 운영 환경에서는 항상 활성화해야 한다. 설정 파일에서 설정하며 재시작이 필요하다.","tags":["security","security","authorization","authorization","rbac"]},{"name":"net.compression.compressors","default":"snappy,zstd,zlib","recommended":"snappy,zstd (기본값으로 충분)","unit":"list","dynamic":"restart","desc":"클라이언트 및 클러스터 내부 네트워크 트래픽에 사용할 수 있는 압축 알고리즘과 그 우선순위를 설정한다. 약간의 CPU 비용으로 네트워크 대역폭 사용량을 줄인다. 설정 파일에서 설정하며 재시작이 필요하다.","tags":["network","network","compression","compression"]},{"name":"storage.wiredTiger.collectionConfig.blockCompressor","default":"snappy","recommended":"snappy (속도) 또는 zstd (높은 압축률)","unit":"algorithm","dynamic":"restart","desc":"WiredTiger가 컬렉션 데이터를 디스크에 기록할 때 사용하는 블록 압축 알고리즘을 설정한다. snappy는 속도를 우선하고 zlib와 zstd는 더 높은 압축률을 제공한다. 설정 이후 생성된 컬렉션에만 적용되며 재시작이 필요하다.","tags":["storage","storage","compression","compression","wiredtiger"]},{"name":"storage.directoryPerDB","default":"false","recommended":"디스크를 분리하려면 true","unit":"true/false","dynamic":"restart","desc":"각 데이터베이스의 데이터를 별도 하위 디렉터리에 저장하여 데이터베이스마다 다른 디스크 볼륨에 둘 수 있게 한다. 빈 데이터 디렉터리에서만 활성화할 수 있다. 변경은 설정 파일에서 하며 재시작이 필요하다.","tags":["storage","storage","directory"]},{"name":"setParameter.cursorTimeoutMillis","default":"600000","recommended":"워크로드에 따라 기본값(10분)","unit":"ms","dynamic":"dynamic","desc":"유휴 커서를 서버가 자동으로 닫기 전까지 기다리는 시간을 밀리초 단위로 설정한다. 방치된 커서가 리소스를 소모하는 것을 막는다. db.adminCommand({setParameter:1})로 런타임에 즉시 변경할 수 있다.","tags":["cursor","timeout","setparameter"]},{"name":"setParameter.transactionLifetimeLimitSeconds","default":"60","recommended":"긴 트랜잭션이 필요하지 않으면 기본값","unit":"seconds","dynamic":"dynamic","desc":"다중 문서 트랜잭션이 자동 중단되기 전까지 열려 있을 수 있는 최대 시간을 초 단위로 설정한다. 장시간 실행 트랜잭션이 리소스와 WiredTiger 캐시를 점유하는 것을 막는다. setParameter로 즉시 변경할 수 있다.","tags":["transaction","transaction","setparameter"]},{"name":"setParameter.maxTransactionLockRequestTimeoutMillis","default":"5","recommended":"락 경합에 따라 기본값","unit":"ms","dynamic":"dynamic","desc":"트랜잭션이 필요한 락을 획득하기 위해 타임아웃 전까지 기다리는 최대 시간을 밀리초 단위로 설정한다. 락 대기 동작을 조정하면 트랜잭션이 너무 오래 차단되는 것을 막는다. setParameter로 즉시 변경할 수 있다.","tags":["transaction","lock","lock","setparameter"]},{"name":"net.tls.mode","default":"disabled","recommended":"requireTLS (운영 환경)","unit":"mode","dynamic":"restart","desc":"서버의 TLS/SSL 암호화 접속 사용을 결정한다. requireTLS는 모든 접속의 암호화를 강제하며 preferTLS와 allowTLS는 전환 기간을 위한 중간 모드를 제공한다. 인증서 구성과 함께 사용하며 재시작이 필요하다.","tags":["security","security","tls","ssl","encryption"]},{"name":"storage.syncPeriodSecs","default":"60","recommended":"60 (기본값을 변경하지 않는다)","unit":"seconds","dynamic":"dynamic","desc":"WiredTiger가 체크포인트를 수행하는 빈도, 즉 메모리 데이터를 디스크에 플러시하는 간격을 초 단위로 설정한다. 기본값이 대부분의 워크로드에 적합하며 변경은 일반적으로 권장하지 않는다. 설정 파일에서 설정하며, 런타임에는 db.adminCommand({ setParameter: 1, syncdelay: <seconds> })로 재시작 없이 변경할 수도 있다.","tags":["storage","storage","checkpoint","sync"]},{"name":"replication.enableMajorityReadConcern","default":"true","recommended":"true (5.0+에서는 변경 불가)","unit":"true/false","dynamic":"restart","desc":"majority read concern 수준의 사용을 허용한다. 이 수준은 과반수 멤버에 기록되어 롤백되지 않을 데이터를 읽도록 보장한다. MongoDB 5.0부터 이 설정은 변경할 수 없으며 항상 true이다. 이전 버전(예: PSA 아키텍처)에서는 false로 설정할 수 있었다. 설정 파일에서 설정하며 재시작이 필요하다.","tags":["replication","read concern","replica set"]},{"name":"processManagement.fork","default":"false","recommended":"systemd 사용 시 false, 수동 데몬은 true","unit":"true/false","dynamic":"restart","desc":"mongod 프로세스를 fork하여 데몬으로 백그라운드에서 실행하게 한다. systemd 등 서비스 관리자를 사용하면 프로세스 관리를 서비스 관리자가 담당하므로 대개 false로 둔다. 설정 파일에서 설정하며 시작 동작에 영향을 준다.","tags":["process","process","daemon","fork"]}]},"reference/params-redis.json":{"engineId":"redis","engine":"Redis","order":6,"params":[{"name":"maxmemory","default":"0 (unlimited)","recommended":"물리 RAM의 ~70-75%","unit":"bytes","dynamic":"dynamic","desc":"Redis가 데이터에 사용할 수 있는 최대 메모리 양을 설정한다. 0은 무제한 사용을 의미하며 메모리 부족 상황을 초래할 수 있으므로 한도 설정을 강력히 권장한다. CONFIG SET maxmemory로 즉시 변경할 수 있다.","tags":["memory","memory"]},{"name":"maxmemory-policy","default":"noeviction","recommended":"캐시는 allkeys-lru, 그 외에는 noeviction","unit":"policy","dynamic":"dynamic","desc":"maxmemory 한도에 도달했을 때 Redis가 어떤 키를 어떻게 축출할지 결정하는 정책. noeviction은 쓰기 오류를 반환하고 allkeys-lru는 가장 오래 사용되지 않은 키를 제거한다. CONFIG SET으로 즉시 변경할 수 있다.","tags":["memory","memory","eviction","cache"]},{"name":"maxclients","default":"10000","recommended":"워크로드와 OS ulimit 기준","unit":"count","dynamic":"dynamic","desc":"허용되는 동시 클라이언트 접속의 최대 수를 설정한다. 운영체제의 파일 디스크립터 한도가 이 값을 제한할 수 있다. CONFIG SET maxclients로 즉시 변경할 수 있다.","tags":["connections","connection","clients"]},{"name":"appendonly","default":"no","recommended":"영속성이 필요하면 yes","unit":"yes/no","dynamic":"dynamic","desc":"AOF(Append Only File) 영속성 메커니즘을 활성화한다. 활성화하면 모든 쓰기 작업이 로그 파일에 추가된다. RDB 스냅샷보다 데이터 손실 위험이 적다. CONFIG SET appendonly yes로 즉시 활성화할 수 있다.","tags":["persistence","persistence","aof"]},{"name":"appendfsync","default":"everysec","recommended":"everysec (균형)","unit":"policy","dynamic":"dynamic","desc":"AOF 파일을 디스크에 동기화(fsync)하는 빈도를 설정한다. always는 쓰기마다 동기화하여 가장 안전하지만 느리고, everysec는 초당 1회 동기화하여 성능과 내구성의 균형을 맞추며, no는 동기화를 운영체제에 맡긴다. CONFIG SET으로 즉시 변경할 수 있다.","tags":["persistence","persistence","aof","fsync"]},{"name":"save","default":"3600 1 300 100 60 10000","recommended":"워크로드에 따라 다름 또는 비활성화 (AOF만 사용)","unit":"rule","dynamic":"dynamic","desc":"RDB 스냅샷을 디스크에 저장하는 조건(지정 시간 내 지정 변경 횟수)을 정의한다. 빈 값을 설정하면 RDB 스냅샷이 완전히 비활성화된다. CONFIG SET save로 즉시 변경할 수 있다.","tags":["persistence","persistence","rdb","snapshot"]},{"name":"tcp-keepalive","default":"300","recommended":"300 (기본값으로 충분)","unit":"seconds","dynamic":"dynamic","desc":"Redis가 클라이언트 접속에 TCP keepalive ACK를 보내는 간격을 초 단위로 설정한다. 끊어진 접속을 탐지해 닫고 half-open 접속을 정리한다. CONFIG SET으로 즉시 변경할 수 있다.","tags":["network","network","tcp","keepalive"]},{"name":"timeout","default":"0","recommended":"0 (접속을 닫지 않음) 또는 적절한 유휴 시간","unit":"seconds","dynamic":"dynamic","desc":"클라이언트 접속이 유휴 상태로 있을 수 있는 최대 시간을 초 단위로 설정하며 초과하면 접속을 닫는다. 0은 클라이언트가 무기한 유휴 상태로 있을 수 있음을 의미한다. CONFIG SET timeout으로 즉시 변경할 수 있다.","tags":["connections","connection","timeout","idle"]},{"name":"hz","default":"10","recommended":"10 (바쁜 시스템에서는 최대 100)","unit":"Hz","dynamic":"dynamic","desc":"Redis가 백그라운드 작업(키 만료, 클라이언트 타임아웃 확인 등)을 초당 몇 번 실행할지 설정한다. 값이 높을수록 정밀하게 제어되지만 CPU 사용량이 늘어난다. CONFIG SET hz로 즉시 변경할 수 있다.","tags":["performance","cpu","background"]},{"name":"activedefrag","default":"no","recommended":"메모리 단편화가 높으면 yes","unit":"yes/no","dynamic":"dynamic","desc":"능동 메모리 조각 모음을 활성화한다. 실행 중 데이터를 재배치하여 메모리 단편화를 줄인다. 단편화가 높은 장기 실행 인스턴스에 유용하다. CONFIG SET activedefrag yes로 즉시 활성화할 수 있다.","tags":["memory","memory","defragmentation"]},{"name":"maxmemory-samples","default":"5","recommended":"5 (더 정확한 축출을 위해 올릴 수 있음)","unit":"count","dynamic":"dynamic","desc":"LRU 및 LFU 축출 알고리즘이 키를 선택할 때 샘플링하는 키 수를 설정한다. 값이 높을수록 실제 LRU/LFU 동작에 가까워지지만 CPU를 약간 더 소모한다. CONFIG SET으로 즉시 변경할 수 있다.","tags":["memory","memory","eviction","lru"]},{"name":"io-threads","default":"1","recommended":"멀티코어 서버에서 2-4 (코어 수에 따라)","unit":"count","dynamic":"restart","desc":"클라이언트 소켓 쓰기 및 읽기에 사용하는 I/O 스레드 수를 설정한다. 네트워크 트래픽이 많은 멀티코어 시스템에서 처리량을 높일 수 있다. 이 설정은 설정 파일에서만 변경할 수 있으며 재시작이 필요하다.","tags":["performance","threads","io","cpu"]},{"name":"repl-backlog-size","default":"1mb","recommended":"쓰기 속도와 장애 시간에 따라 더 크게","unit":"bytes","dynamic":"dynamic","desc":"replica 접속이 끊겼을 때 부분 재동기화를 위해 primary에 유지하는 복제 버퍼의 크기를 설정한다. 버퍼가 충분히 크면 짧은 장애 후 replica가 전체 동기화 없이 따라잡을 수 있다. CONFIG SET으로 즉시 변경할 수 있다.","tags":["replication","replication","backlog"]},{"name":"repl-diskless-sync","default":"yes","recommended":"yes (빠른 네트워크에서)","unit":"yes/no","dynamic":"dynamic","desc":"전체 동기화 시 RDB 데이터를 디스크에 기록하지 않고 소켓을 통해 replica에 직접 전송한다. 디스크 I/O 병목이 있는 시스템에서 동기화를 빠르게 한다. CONFIG SET repl-diskless-sync로 즉시 변경할 수 있다.","tags":["replication","replication","diskless","sync"]},{"name":"requirepass","default":"(empty)","recommended":"강력한 비밀번호 설정","unit":"password","dynamic":"dynamic","desc":"클라이언트가 명령 실행 전에 AUTH로 인증해야 하는 비밀번호를 설정한다. Redis에 대한 무단 접근을 막기 위해 강력한 비밀번호를 설정해야 한다. CONFIG SET requirepass로 즉시 변경할 수 있다.","tags":["security","security","authentication","password"]},{"name":"cluster-enabled","default":"no","recommended":"수평 확장이 필요하면 yes","unit":"yes/no","dynamic":"restart","desc":"Redis Cluster 모드를 활성화한다. 활성화하면 데이터가 hash slot을 통해 여러 노드에 분산된다. 수평 확장과 자동 데이터 분할에 사용한다. 이 설정은 설정 파일에서만 변경할 수 있으며 재시작이 필요하다.","tags":["cluster","cluster","scaling","scaling"]},{"name":"cluster-node-timeout","default":"15000","recommended":"15000 또는 네트워크 지연에 맞춘 값","unit":"ms","dynamic":"dynamic","desc":"클러스터 모드에서 노드가 응답하지 않아 도달 불가(fail)로 간주되기까지의 최대 시간을 밀리초 단위로 설정한다. 페일오버 동작과 클러스터 안정성에 직접 영향을 준다. CONFIG SET으로 즉시 변경할 수 있다.","tags":["cluster","cluster","timeout","failover"]},{"name":"aof-use-rdb-preamble","default":"yes","recommended":"yes (빠른 재로드)","unit":"yes/no","dynamic":"dynamic","desc":"AOF rewrite 시 파일 앞부분에 압축된 RDB 스냅샷을 쓰고 이어서 증분 AOF 명령을 쓰는 하이브리드 영속성 형식을 활성화한다. 재시작 시 AOF 로드 시간이 단축된다. CONFIG SET으로 즉시 변경할 수 있다.","tags":["persistence","persistence","aof","rdb"]}]},"reference/params-elasticsearch.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":7,"params":[{"name":"-Xms / -Xmx (JVM heap)","default":"Automatic depending on version (half of RAM)","recommended":"물리 RAM의 50%, 최대 ~30-31GB (Xms=Xmx)","unit":"GB","dynamic":"restart","desc":"JVM heap의 초기(-Xms) 및 최대(-Xmx) 크기를 설정하며 두 값은 같게 설정해야 한다. 전체 RAM의 절반이나 압축 객체 포인터가 적용되지 않게 되는 ~32GB 한도를 넘지 않는 것을 권장한다. jvm.options에서 설정하며 노드 재시작이 필요하다.","tags":["memory","memory","jvm","heap"]},{"name":"indices.memory.index_buffer_size","default":"10%","recommended":"10% (인덱싱 부하가 높으면 늘릴 수 있음)","unit":"% / bytes","dynamic":"restart","desc":"인덱싱 중 새 문서를 보관하는 메모리 버퍼에 할당하는 heap 비율을 설정한다. 이 버퍼를 늘리면 쓰기 중심 워크로드의 인덱싱 처리량이 개선될 수 있다. elasticsearch.yml에서 노드별로 설정하며 재시작이 필요하다.","tags":["memory","memory","indexing","buffer"]},{"name":"thread_pool.write.queue_size","default":"10000","recommended":"10000 (큐 거부가 발생하면 신중하게 늘린다)","unit":"count","dynamic":"restart","desc":"index, delete, bulk 쓰기 작업용 스레드 풀의 큐 길이를 설정한다. 큐가 가득 차면 요청이 거부된다. 값을 너무 높이면 메모리 압박이 생길 수 있다. elasticsearch.yml에서 설정하며 재시작이 필요하다.","tags":["thread pool","write","queue","queue"]},{"name":"thread_pool.search.queue_size","default":"1000","recommended":"1000 (검색 거부가 발생하면 신중하게 늘린다)","unit":"count","dynamic":"restart","desc":"검색 작업용 스레드 풀의 큐 길이를 설정한다. 동시 검색이 풀 용량을 초과하면 요청이 이 큐에서 대기하며 큐가 가득 차면 거부된다. elasticsearch.yml에서 설정하며 재시작이 필요하다.","tags":["thread pool","search","search","queue"]},{"name":"indices.fielddata.cache.size","default":"unbounded","recommended":"20-40% 같은 한도 (메모리 보호용)","unit":"% / bytes","dynamic":"restart","desc":"text 필드의 정렬 및 집계에 사용하는 fielddata 캐시의 최대 크기를 설정한다. 기본값은 무제한이므로 메모리 고갈을 막기 위해 한도 설정을 권장한다. elasticsearch.yml에서 설정하며 재시작이 필요하다.","tags":["memory","memory","fielddata","cache"]},{"name":"index.number_of_shards","default":"1","recommended":"데이터 양 기준, 샤드당 10-50GB 목표","unit":"count","dynamic":"restart","desc":"인덱스를 나누는 primary 샤드 수를 설정한다. 정적 인덱스 설정이며(재시작이 아니라 인덱스 생성 시에만 지정): 기존 인덱스에서 직접 변경할 수 없고, 새 인덱스 + _reindex로 새 인덱스를 만들거나 _split(배수로 증가) / _shrink(약수로 감소) API로 변경한다. 샤드가 너무 많으면 클러스터 오버헤드가 늘고 너무 적으면 확장이 제한된다.","tags":["index","index","shard","scaling"]},{"name":"index.number_of_replicas","default":"1","recommended":"운영 환경에서 최소 1 (고가용성용)","unit":"count","dynamic":"dynamic","desc":"각 primary 샤드에 대해 유지하는 replica 샤드 수를 설정한다. replica는 고가용성과 읽기 성능을 모두 제공한다. 이 설정은 _settings API로 운영 중인 인덱스에서 즉시 변경할 수 있다.","tags":["index","index","replica","availability"]},{"name":"index.refresh_interval","default":"1s","recommended":"대량 인덱싱 시 30s 또는 -1 (임시)","unit":"duration","dynamic":"dynamic","desc":"새로 인덱싱된 문서를 검색에 보이게 하는 refresh 작업의 실행 빈도를 설정한다. 값을 늘리거나 대량 로드 중 -1로 설정하면 인덱싱 처리량이 크게 개선된다. _settings API로 즉시 변경할 수 있다.","tags":["index","index","refresh","indexing"]},{"name":"indices.breaker.total.limit","default":"95%","recommended":"95% (기본값, 신중하게 변경)","unit":"%","dynamic":"dynamic","desc":"모든 circuit breaker가 합쳐서 사용할 수 있는 heap의 최대 비율을 설정하여 노드가 OutOfMemory로 다운되는 것을 막는다. _cluster/settings API로 클러스터 전체에서 즉시 변경할 수 있다.","tags":["memory","memory","circuit breaker","stability"]},{"name":"cluster.routing.allocation.disk.watermark.low","default":"85%","recommended":"85% (기본값으로 충분)","unit":"% / bytes","dynamic":"dynamic","desc":"노드의 디스크 사용량이 이 임계값을 초과하면 Elasticsearch는 해당 노드에 새 샤드 할당을 중단한다(기존 샤드는 유지). 디스크 가득 참을 막기 위한 첫 번째 경고 수준이다. _cluster/settings API로 즉시 변경할 수 있다.","tags":["disk","allocation","watermark","storage"]},{"name":"cluster.routing.allocation.disk.watermark.high","default":"90%","recommended":"90% (기본값으로 충분)","unit":"% / bytes","dynamic":"dynamic","desc":"노드의 디스크 사용량이 이 임계값을 초과하면 Elasticsearch는 이 노드의 샤드를 다른 노드로 재배치하기 시작한다. 디스크가 차고 있는 노드의 부담을 덜어 준다. _cluster/settings API로 즉시 변경할 수 있다.","tags":["disk","allocation","watermark","storage"]},{"name":"cluster.routing.allocation.disk.watermark.flood_stage","default":"95%","recommended":"95% (기본값으로 충분)","unit":"% / bytes","dynamic":"dynamic","desc":"디스크 사용량이 이 위험 임계값을 초과하면 해당 노드에 샤드가 있는 인덱스가 읽기 전용(read-only-allow-delete) 모드가 되어 쓰기가 차단된다. 디스크가 완전히 차기 전에 데이터 손상을 막는다. 7.4+에서는 디스크 사용량이 high watermark 아래로 떨어지면 차단이 자동 해제된다. 8.x의 대용량 디스크에서는 비율보다 max_headroom(기본 100GB 여유)이 기준이 된다. _cluster/settings API로 즉시 변경할 수 있다.","tags":["disk","allocation","watermark","read-only"]},{"name":"index.translog.durability","default":"request","recommended":"request (내구성) 또는 async (성능)","unit":"mode","dynamic":"dynamic","desc":"트랜잭션 로그(translog)를 디스크에 동기화(fsync)하는 시점을 설정한다. request는 요청마다 동기화하여 데이터 손실을 최소화하고, async는 주기적으로 동기화하여 성능을 높이지만 크래시 시 데이터 손실 위험이 있다. _settings API로 즉시 변경할 수 있다.","tags":["translog","durability","durability","indexing"]},{"name":"indices.recovery.max_bytes_per_sec","default":"40mb","recommended":"네트워크 및 디스크 용량에 따라 늘릴 수 있음","unit":"bytes/s","dynamic":"dynamic","desc":"샤드 복구 중 노드당 최대 데이터 전송 속도를 설정한다. 값을 늘리면 복구가 빨라지지만 일반 워크로드의 네트워크 및 디스크 리소스를 소모할 수 있다. _cluster/settings API로 즉시 변경할 수 있다.","tags":["recovery","recovery","throttle","network"]},{"name":"action.destructive_requires_name","default":"true","recommended":"true (와일드카드 파괴적 작업 차단)","unit":"true/false","dynamic":"dynamic","desc":"와일드카드(*)나 _all로 인덱스를 삭제하는 등의 파괴적 작업을 차단하고 인덱스 이름을 명시적으로 지정하도록 요구한다. 실수로 모든 인덱스를 삭제하는 것을 막는 중요한 안전 설정이다. _cluster/settings API로 즉시 변경할 수 있다.","tags":["security","security","destructive","safety"]},{"name":"discovery.seed_hosts","default":"(empty / 127.0.0.1)","recommended":"클러스터 내 master-eligible 노드의 주소","unit":"list","dynamic":"restart","desc":"노드가 클러스터에 합류하기 위해 접촉하는 seed host(master-eligible 노드) 주소 목록을 설정한다. 클러스터 탐색과 노드 간 상호 발견에 필요하다. elasticsearch.yml에서 설정하며 재시작이 필요하다.","tags":["discovery","cluster","cluster","network"]}]},"reference/waits-oracle.json":{"engineId":"oracle","engine":"Oracle","order":1,"waits":[{"name":"log file sync","kind":"wait","meaning":"세션이 COMMIT(또는 ROLLBACK)을 실행했고 LGWR 프로세스가 redo 레코드를 online redo log 파일에 기록하고 확인 응답하기를 기다리고 있다.","causes":"느린 redo log I/O, 매우 잦은 소규모 커밋, 과부하된 LGWR 프로세스, CPU 부족으로 지연된 LGWR, 느린 디스크에 있는 redo log.","action":"커밋 빈도를 줄이고(배치 커밋), redo log 파일을 빠르고 지연이 낮은 디스크(SSD)로 옮기고, 불필요한 커밋을 제거하고, 손실을 허용할 수 있는 작업에만 COMMIT NOWAIT/BATCH를 고려하고(경고: 내구성이 낮아지며 커밋된 것으로 보이는 트랜잭션이 크래시 시 손실될 수 있다), log writer I/O 지연(log file parallel write)을 측정한다.","tags":["commit","redo","io","lgwr"]},{"name":"db file sequential read","kind":"wait","meaning":"세션이 단일 블록 물리 읽기를 수행 중이며, 주로 인덱스 접근이나 ROWID에 의한 테이블 블록 읽기 중에 블록이 디스크에서 buffer cache로 올라오기를 기다리고 있다.","causes":"인덱스 스캔, 비효율적인 접근 계획, 작은 buffer cache, 느린 디스크 서브시스템, 높은 물리 읽기로 인해 캐시에서 찾지 못한 블록.","action":"SQL을 튜닝하여 불필요한 읽기를 줄이고, 적절한 인덱스를 추가하고, buffer cache 크기를 늘리고, I/O 서브시스템을 개선하고, AWR/ASH로 읽기 상위 쿼리를 식별한다.","tags":["io","index","single-block","read"]},{"name":"db file scattered read","kind":"wait","meaning":"세션이 멀티 블록 물리 읽기를 수행 중이며, 주로 full table scan 또는 fast full index scan 중에 여러 블록이 디스크에서 읽히기를 기다리고 있다.","causes":"full table scan, 누락되었거나 사용되지 않는 인덱스, 비효율적인 실행 계획, 대형 테이블 스캔, 부정확한 옵티마이저 통계.","action":"인덱스를 추가하여 불필요한 full scan을 막고, 실행 계획을 검토하고, 통계를 갱신하고, db_file_multiblock_read_count 설정을 확인하고, 실제로 필요한 full scan은 병렬화를 고려한다.","tags":["io","full-scan","multi-block","read"]},{"name":"direct path read","kind":"wait","meaning":"세션이 buffer cache를 우회하여 블록을 PGA로 직접 읽고 있다. 주로 병렬 쿼리, 대형 테이블 스캔, LOB 읽기 중에 나타난다.","causes":"병렬 full table scan, serial direct path read 결정(adaptive direct read), 대형 테이블 스캔, 임시 테이블스페이스(temp) 읽기, LOB 접근.","action":"쿼리 튜닝으로 대형 스캔의 필요성을 줄이고, PGA 크기를 평가하고, temp 사용량이 높으면 sort/hash 작업을 최적화하고, I/O 대역폭을 늘린다.","tags":["io","direct-path","parallel","read"]},{"name":"direct path write","kind":"wait","meaning":"세션이 buffer cache를 거치지 않고 블록을 디스크에 직접 기록하고 있다. 주로 direct-path insert, sort spill, LOB 쓰기 중에 나타난다.","causes":"direct-path INSERT(APPEND) 작업, 임시 테이블스페이스로의 sort/hash spill, 병렬 DML, LOB 쓰기, 느린 temp 또는 데이터 디스크.","action":"sort/hash 작업용 PGA를 늘려 temp spill을 줄이고, temp 테이블스페이스를 빠른 디스크에 두고, 불필요한 direct-path 로드를 검토하고, I/O 성능을 개선한다.","tags":["io","direct-path","temp","write"]},{"name":"buffer busy waits","kind":"wait","meaning":"세션이 buffer cache의 블록에 접근하려 하지만 다른 세션이 호환되지 않는 모드로 해당 블록을 사용 중이므로 해제되기를 기다리고 있다.","causes":"동일 블록에 대한 높은 동시 접근(hot block), 불충분한 freelist/ASSM, 순차 인덱스의 우측 블록 경합(right-hand index growth), DML 집중도가 높은 세그먼트.","action":"hot block을 분산하고(hash 파티셔닝, reverse key index), ASSM을 사용하고, freelist/freelist group을 늘리고, 순차 키를 생성하는 테이블에서 sequence 캐싱과 분산을 개선한다.","tags":["concurrency","buffer","hot-block","contention"]},{"name":"read by other session","kind":"wait","meaning":"이 세션이 필요로 하는 블록을 다른 세션이 이미 디스크에서 읽고 있으며, 이 세션은 같은 블록의 해당 읽기가 완료되기를 기다린다.","causes":"동일 hot block에 대한 동시 물리 읽기 요청, 작은 buffer cache, 느린 I/O, 인기 있는 인덱스 또는 테이블 블록에 동시에 접근하는 많은 세션.","action":"buffer cache를 늘려 물리 읽기를 줄이고, hot segment를 식별하고 접근 계획을 개선하고, I/O 지연을 낮추고, 불필요한 full scan을 제거한다.","tags":["io","concurrency","read","hot-block"]},{"name":"latch: cache buffers chains","kind":"wait","meaning":"세션이 buffer cache hash chain을 보호하는 latch를 획득하기 위해 기다리고 있다. buffer cache에서 블록을 찾는 동안 직렬화가 발생한다.","causes":"과도한 논리 I/O를 발생시키는 비효율적인 SQL, hot block에 대한 높은 동시 접근, nested loop를 통한 동일 블록 반복 접근, 매우 인기 있는 인덱스 root/branch 블록.","action":"SQL을 튜닝하여 논리 읽기를 줄이고, 불필요한 nested loop 접근을 제거하고, hot block을 분산하고, AWR/ASH로 과도한 buffer get을 발생시키는 쿼리를 찾아 수정한다.","tags":["latch","concurrency","logical-io","buffer"]},{"name":"cursor: pin S wait on X","kind":"wait","meaning":"세션이 mutex로 커서를 공유(S) 모드로 pin하려 하지만 다른 세션이 같은 커서를 배타(X) 모드로 보유하고 있어 기다리고 있다.","causes":"높은 hard/soft parse 비율, 매우 자주 파싱되는 인기 SQL, 바인드 변수를 사용하지 않는 리터럴 쿼리, child cursor 증식, 같은 SQL을 동시에 파싱하는 많은 세션.","action":"바인드 변수를 사용하여 파싱 부하를 줄이고, 애플리케이션에서 커서를 캐싱하고(session_cached_cursors), 불필요한 재파싱을 줄이고, version count가 높은 SQL을 수정한다.","tags":["mutex","parse","cursor","concurrency"]},{"name":"library cache lock","kind":"wait","meaning":"세션이 library cache에 있는 객체(패키지, 뷰, 테이블 정의)의 핸들에 대한 락을 획득하기 위해 기다리고 있다. 주로 객체 정의에 대한 동시 접근/변경 중에 나타난다.","causes":"사용 중인 객체에 대한 DDL, 계속 재컴파일되는 패키지/뷰, invalid 객체, 높은 동시 접속과 파싱, 권한(grant) 변경.","action":"DDL은 비피크 시간에 실행하고, 불필요한 컴파일을 막고, invalid 객체를 수정하고, 애플리케이션 측의 과도한 connect/parse 루프를 줄인다.","tags":["library-cache","concurrency","ddl","parse"]},{"name":"library cache: mutex X","kind":"wait","meaning":"세션이 library cache의 객체를 배타(X) 모드로 보호하기 위해 mutex를 획득하려고 기다리고 있다. 주로 높은 파싱과 객체 핸들 경합 상황에서 나타난다.","causes":"과도한 파싱(hard/soft), 바인드 없는 다양한 리터럴 SQL, 높은 child cursor 수, 잦은 객체 무효화, 같은 객체에 동시 접근하는 많은 세션.","action":"바인드 변수와 커서 캐싱으로 파싱을 줄이고, SQL 리터럴을 표준화하고, 높은 version count 문제를 해결하고, 애플리케이션 접속 동작을 개선한다.","tags":["mutex","library-cache","parse","concurrency"]},{"name":"enq: TX - row lock contention","kind":"wait","meaning":"세션이 다른 트랜잭션이 락을 건 행을 수정하려 하며, 그 트랜잭션이 commit/rollback하여 행 락을 해제하기를 기다리고 있다.","causes":"같은 행에 대한 동시 UPDATE/DELETE, 커밋하지 않는 장시간 트랜잭션, 잘못 설계된 애플리케이션 흐름, unique/foreign key를 통한 간접 행 락, 멈춘 세션.","action":"트랜잭션을 짧게 유지하고 즉시 커밋하고, 경합이 생기는 접근 패턴을 재설계하고, 필요하면 SELECT FOR UPDATE로 순서 있는 접근을 강제하고, ASH로 블로킹 세션을 찾아 해결한다.","tags":["lock","transaction","row-lock","contention"]},{"name":"enq: TX - allocate ITL entry","kind":"wait","meaning":"세션이 블록에서 작업하려면 ITL(Interested Transaction List) 슬롯이 필요하지만 블록에 빈 ITL 슬롯이 남아 있지 않아 기다려야 한다.","causes":"낮은 INITRANS 설정, 작은 블록 크기, 같은 블록 내 많은 동시 트랜잭션, 높은 DML을 받는 조밀하게 채워진 블록.","action":"테이블/인덱스의 INITRANS를 늘리고, PCTFREE를 올려 블록당 행 수를 줄이고, hot block을 분산하고, 필요하면 객체를 재구성한다.","tags":["lock","transaction","itl","block"]},{"name":"gc buffer busy acquire","kind":"wait","meaning":"RAC 환경에서 세션이 global cache를 통해 블록을 얻으려 하지만, 같은 인스턴스의 다른 세션이 이미 같은 블록에 대한 global 요청을 시작했으므로 기다리고 있다.","causes":"RAC에서 인스턴스 간 hot block 공유, 불충분한 interconnect 성능, 순차 인덱스의 우측 경합, 잘못 분할된 워크로드, 높은 global cache 트래픽.","action":"워크로드를 인스턴스 간에 합리적으로 분할하고(application affinity/서비스), hot segment를 분산하고(reverse key/파티션), interconnect 지연을 개선하고, AWR RAC 섹션과 GV$ 뷰로 gc 경합을 분석한다.","tags":["rac","global-cache","concurrency","interconnect"]},{"name":"gc cr block busy","kind":"wait","meaning":"RAC 환경에서 세션이 다른 인스턴스에 일관된 읽기(CR)용 블록을 요청했으며, 그 인스턴스가 블록을 준비해 interconnect로 전송하기를 기다리고 있다.","causes":"인스턴스 간 hot block 공유, 느리거나 포화된 interconnect, 높은 redo/commit 트래픽, 잘못 분할된 워크로드, 과도한 global cache CR 요청.","action":"워크로드 affinity를 개선하고, interconnect 대역폭과 지연을 최적화하고, hot block을 줄이고, commit 및 DML 패턴을 검토하고, RAC를 튜닝한다.","tags":["rac","global-cache","cr","interconnect"]},{"name":"log file switch (checkpoint incomplete)","kind":"wait","meaning":"세션이 새 redo log로 전환하려 하지만 다음 로그 파일의 체크포인트가 아직 완료되지 않아 LGWR가 그 로그를 재사용할 수 없고 모든 redo 생성이 멈춘다.","causes":"너무 작거나 적은 redo log 파일, 느린 DBWR/체크포인트 I/O, 높은 redo 생성 속도, 불충분한 로그 그룹 수.","action":"redo log 파일을 키우고 그룹을 추가하고, DBWR/체크포인트 I/O 성능을 개선하고, redo 생성을 줄이는 애플리케이션 최적화를 하고, 로그 스위치 빈도를 모니터링한다.","tags":["redo","checkpoint","log-switch","io"]},{"name":"free buffer waits","kind":"wait","meaning":"세션이 새 블록을 읽기 위해 buffer cache에서 clean/free 버퍼를 찾고 있지만, DBWR가 dirty 버퍼를 충분히 빠르게 디스크에 기록하지 못해 free 버퍼를 기다리고 있다.","causes":"느린 DBWR 쓰기 I/O, 작은 buffer cache, 높은 DML로 인한 빠른 dirty 버퍼 생성, 불충분한 체크포인트 속도, 포화된 디스크 서브시스템.","action":"데이터파일 쓰기 I/O를 빠르게 하고, buffer cache 크기를 늘리고, DBWR 프로세스 수(db_writer_processes)를 평가하고, DML 집중도와 체크포인트 설정을 검토한다.","tags":["buffer","dbwr","io","checkpoint"]},{"name":"버퍼 캐시 히트율","kind":"metric","meaning":"요청된 블록 중 디스크에 가지 않고 buffer cache에서 제공된 비율. 논리 읽기 중 몇 퍼센트가 물리 읽기를 필요로 하지 않았는지 보여 준다.","causes":"낮은 비율은 주로 작은 buffer cache, 비효율적인 full scan, 콜드 캐시, 높은 물리 읽기 때문이다. 이 값만으로는 오해의 소지가 있으며 높은 비율이 나쁜 SQL을 감출 수도 있다.","action":"비율 자체를 목표로 삼지 않는다. 대기 이벤트 및 물리 읽기 추세와 함께 평가한다. 비효율적인 SQL을 튜닝하고 불필요한 논리 읽기를 줄이며 워크로드에 맞게 캐시 크기를 정한다.","tags":["metric","buffer-cache","ratio","memory"]},{"name":"평균 활성 세션 수","kind":"metric","meaning":"구간 동안 동시에 활성 상태인(CPU에서 실행 중이거나 이벤트를 대기 중인) 세션의 평균 수. 데이터베이스의 전체 부하와 병목 강도를 요약한다.","causes":"높은 AAS는 CPU 포화, I/O 대기, lock/latch 경합, 동시성 증가 때문일 수 있다. CPU 코어 수에 근접하거나 초과하는 값은 리소스 병목을 가리킨다.","action":"AAS를 CPU 코어 수와 비교하고, ASH로 지배적인 wait class(CPU/IO/concurrency)를 찾고, 가장 많은 부하를 발생시키는 SQL과 세션을 튜닝하고, 리소스 또는 동시성 문제를 해결한다.","tags":["metric","load","ash","sessions"]}]},"reference/waits-postgres.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"waits":[{"name":"LWLock:WALWrite","kind":"wait","meaning":"세션이 WAL 버퍼를 디스크에 기록하는 데 필요한 lightweight lock을 기다리고 있다. 주로 커밋 중 WAL 쓰기의 직렬화로 발생한다.","causes":"느린 WAL 디스크 I/O, 매우 잦은/소규모 커밋, 높은 쓰기 부하, 불충분한 WAL 쓰기 대역폭, 동시에 WAL을 기록하는 많은 backend.","action":"WAL을 빠르고 지연이 낮은 디스크로 옮기고, 커밋을 그룹화하고(synchronous_commit, commit_delay 평가), wal_buffers를 늘리고, 불필요한 쓰기 작업을 줄인다.","tags":["wal","lwlock","commit","io"]},{"name":"LWLock:BufferContent","kind":"wait","meaning":"세션이 shared buffer pool에 있는 버퍼의 페이지 내용을 보호하는 lightweight lock을 기다리고 있다. 페이지에 대한 동시 접근으로 인한 경합을 나타낸다.","causes":"같은 페이지에 대한 높은 동시 읽기/쓰기(hot page), 순차 인덱스의 우측 끝 경합, 업데이트가 많은 작은 테이블, 높은 동시성.","action":"hot page를 분산하고(fillfactor 설정, 파티셔닝), 순차 키 경합을 줄이고, 업데이트가 많은 행을 재구성하고, 동시 접근 패턴을 검토한다.","tags":["lwlock","buffer","concurrency","contention"]},{"name":"Lock:transactionid","kind":"wait","meaning":"세션이 수정하려는 행에 락을 건 다른 트랜잭션이 종료(commit/rollback)되기를 기다리고 있다. 락 대기는 대상 트랜잭션의 ID에 걸린다.","causes":"같은 행에 대한 동시 UPDATE/DELETE, 커밋하지 않는 장시간 트랜잭션, idle-in-transaction 세션, 경합을 유발하도록 잘못 설계된 흐름.","action":"트랜잭션을 짧게 유지하고 즉시 커밋하고, idle-in-transaction 세션을 제한하고(idle_in_transaction_session_timeout), 경합이 생기는 접근 패턴을 재설계하고, pg_stat_activity로 블로킹 쿼리를 찾는다.","tags":["lock","transaction","row-lock","contention"]},{"name":"Lock:tuple","kind":"wait","meaning":"세션이 특정 tuple(행)에 대한 락을 획득하기 위해 기다리고 있다. 여러 세션이 같은 행을 업데이트/락하기 위해 줄을 설 때 나타난다.","causes":"같은 행에 대한 동시 SELECT FOR UPDATE/UPDATE, hot row 경합(예: 카운터 행), 오래 보유된 행 락, 높은 동시성.","action":"hot row 업데이트를 분산하거나 큐잉하고, 트랜잭션 시간을 단축하고, 카운터 같은 경합 패턴을 재설계하고(예: 배치 업데이트), FOR UPDATE SKIP LOCKED를 고려한다.","tags":["lock","tuple","row-lock","contention"]},{"name":"Lock:relation","kind":"wait","meaning":"세션이 테이블/인덱스(relation)에 대한 테이블 수준 락을 획득하기 위해 기다리고 있다. 주로 DDL과 DML의 호환되지 않는 락 모드 간 경합에서 나타난다.","causes":"쿼리 실행 중 ALTER/DROP/TRUNCATE 같은 DDL, 강한 락이 필요한 VACUUM FULL, 호환되지 않는 모드로 같은 테이블에 동시 접근, 장시간 실행 쿼리.","action":"DDL은 한가한 시간에 짧은 lock_timeout으로 실행하고, VACUUM FULL 대신 정기 autovacuum을 사용하고, 긴 쿼리를 모니터링하고, 테이블 접근을 직렬화하는 작업을 검토한다.","tags":["lock","relation","ddl","table-lock"]},{"name":"IO:DataFileRead","kind":"wait","meaning":"세션이 데이터 파일 페이지가 디스크에서 shared buffers로 읽히기를 기다리고 있다. 요청한 페이지가 캐시에 없을 때 발생하는 물리 읽기 대기다.","causes":"작은 shared_buffers, 비효율적인 실행 계획과 대형 스캔, 콜드 캐시, 느린 디스크 서브시스템, 불충분한 인덱스 사용.","action":"shared_buffers와 OS 페이지 캐시를 워크로드에 맞게 조정하고, 비효율적인 쿼리와 누락된 인덱스를 수정하고, I/O 성능을 개선하고, effective_cache_size를 올바르게 설정한다.","tags":["io","read","buffer","disk"]},{"name":"IO:WALSync","kind":"wait","meaning":"세션이 WAL이 디스크에 영속적으로 기록(fsync/동기화)되기를 기다리고 있다. 커밋 내구성을 위해 WAL이 물리적으로 플러시되기를 기다린다.","causes":"느린 스토리지 fsync 지연, 매우 잦은 커밋, 저성능 WAL 디스크, write-back 캐시가 없는 디스크, 높은 쓰기 부하.","action":"WAL 디스크를 fsync 지연이 낮은 빠른 스토리지로 옮기고, 커밋을 그룹화하고(commit_delay/commit_siblings), 비즈니스 요구에 비추어 synchronous_commit을 평가하고, 스토리지 계층을 개선한다.","tags":["wal","io","fsync","commit"]},{"name":"IO:DataFileWrite","kind":"wait","meaning":"세션이 dirty 데이터 페이지가 디스크에 기록되기를 기다리고 있다. 주로 backend가 buffer pool에서 clean 페이지를 찾기 위해 직접 기록할 때 나타난다.","causes":"느린 데이터 디스크 쓰기 I/O, 체크포인트 혼잡, 불충분한 background writer, 작은 buffer pool, 높은 쓰기 부하로 backend가 직접 기록.","action":"체크포인트를 평활화하고(checkpoint_completion_target, max_wal_size), background writer를 튜닝하고(bgwriter_lru_maxpages), 디스크 쓰기 성능을 개선하고, shared_buffers를 검토한다.","tags":["io","write","checkpoint","bgwriter"]},{"name":"Client:ClientRead","kind":"wait","meaning":"backend가 클라이언트의 다음 명령/데이터를 기다리고 있다. 서버가 유휴 상태로 클라이언트의 데이터 전송을 기다릴 때 나타난다.","causes":"느린 네트워크, idle-in-transaction 세션, 데이터를 느리게 송수신하는 클라이언트 애플리케이션, 느리게 소비되는 대형 결과 집합, 커넥션 풀 문제.","action":"애플리케이션 측 및 네트워크 지연을 조사하고, idle-in-transaction 세션을 제한하고, 결과를 효율적으로 소비하고(예: cursor/batch), 커넥션 풀을 개선한다. 이 대기는 대개 데이터베이스 병목이 아니다.","tags":["client","network","idle","application"]},{"name":"IPC:ProcArrayGroupUpdate","kind":"wait","meaning":"세션이 그룹 리더 backend가 ProcArray에 자신의 트랜잭션 종료를 그룹으로 표시하기를 기다리고 있다. 커밋 중 ProcArray 업데이트의 직렬화다.","causes":"매우 높은 커밋/트랜잭션 비율, 과도한 동시 단기 트랜잭션, 높은 접속 수, 높은 snapshot/ProcArray 접근.","action":"트랜잭션 비율과 동시 접속 수를 관리하고(커넥션 풀), 불필요한 단기 트랜잭션을 줄이고, 매우 높은 동시성에서 접속 수를 제한하고, 워크로드를 분산한다.","tags":["ipc","commit","procarray","concurrency"]},{"name":"LWLock:LockManager","kind":"wait","meaning":"세션이 heavyweight lock 테이블을 보호하는 lock manager lightweight lock을 기다리고 있다. 락 획득/해제 작업의 직렬화로 발생한다.","causes":"많은 테이블/파티션을 건드리는 쿼리(fast-path lock 한도 초과), 높은 동시성, 과도하게 파티셔닝된 테이블, 많은 락을 획득/해제하는 워크로드.","action":"쿼리당 건드리는 파티션/테이블 수를 줄이고(partition pruning), 과도한 파티셔닝을 검토하고, 커넥션 풀로 동시성을 관리하고, 불필요한 락을 잡는 작업을 단순화한다.","tags":["lwlock","lock-manager","concurrency","partitioning"]},{"name":"Timeout:VacuumDelay","kind":"wait","meaning":"vacuum/autovacuum 프로세스가 비용 기반 vacuum 지연으로 의도적으로 일시 정지되어 있다. I/O 부하를 제한하기 위한 의도적인 대기다.","causes":"vacuum_cost_delay/autovacuum_vacuum_cost_delay 설정에 따른 스로틀링, 낮은 cost_limit, 많은 테이블을 정리하는 vacuum, 보수적인 autovacuum 설정.","action":"vacuum이 너무 느려 bloat/dead tuple이 쌓이면 cost_limit을 올리고 cost_delay를 낮추고, autovacuum 워커 수와 임계값을 조정하고, I/O 용량과 균형을 맞춘다.","tags":["vacuum","timeout","throttle","maintenance"]},{"name":"캐시 히트율","kind":"metric","meaning":"디스크에 가지 않고 shared buffers에서 제공된 블록 요청의 비율. pg_stat_database의 blks_hit와 blks_read로 계산한다.","causes":"낮은 비율은 주로 작은 shared_buffers, 비효율적인 대형 스캔, 콜드 캐시, 메모리 부족을 나타낸다. OS 페이지 캐시도 읽기를 처리하므로 비율만으로는 오해의 소지가 있다.","action":"비율을 대기 이벤트 및 I/O 추세와 함께 해석하고, shared_buffers와 effective_cache_size를 조정하고, 비효율적인 쿼리를 수정한다. 지속적으로 낮으면 메모리와 실행 계획을 조사한다.","tags":["metric","cache","ratio","memory"]},{"name":"복제 지연","kind":"metric","meaning":"standby/replica 서버가 primary가 생성한 WAL 적용에서 얼마나 뒤처졌는지를 나타낸다. 바이트(LSN 차이) 또는 지연 초로 측정한다.","causes":"느린 네트워크, standby의 높은 쿼리 부하(특히 replay 충돌), 느린 standby 디스크 I/O, 높은 primary 쓰기 속도, 복구를 지연시키는 장시간 읽기 쿼리.","action":"pg_stat_replication으로 write/flush/replay lag를 모니터링하고, 네트워크와 standby I/O 성능을 개선하고, 충돌하는 긴 쿼리를 관리하고(hot_standby_feedback/max_standby_streaming_delay), 동기 복제의 필요성을 검토한다.","tags":["metric","replication","lag","standby"]}]},"reference/waits-mysql.json":{"engineId":"mysql","engine":"MySQL","order":3,"waits":[{"name":"wait/synch/mutex/innodb/buf_pool_LRU_list_mutex","kind":"wait","meaning":"스레드가 InnoDB buffer pool LRU 리스트를 보호하는 mutex를 기다리고 있다. 페이지 읽기/축출 중 직렬화를 나타낸다. 8.0에서 기존 단일 buf_pool_mutex가 분할되었으며 관련된 다른 계측 항목은 buf_pool_free_list_mutex, buf_pool_flush_state_mutex, buf_pool_zip_mutex이다.","causes":"높은 동시성에서의 많은 buffer pool 접근, 단일 buffer pool 인스턴스, 잦은 페이지 읽기/축출, 작은 buffer pool.","action":"innodb_buffer_pool_instances를 늘려 mutex를 분할하고, innodb_buffer_pool_size를 working set에 맞추고, 쿼리를 튜닝하여 물리 읽기를 줄이고, 동시성을 관리한다.","tags":["innodb","mutex","buffer-pool","concurrency"]},{"name":"wait/io/file/innodb/innodb_data_file","kind":"wait","meaning":"스레드가 InnoDB 데이터 파일(ibd/ibdata)에 대한 물리 파일 I/O(읽기/쓰기)의 완료를 기다리고 있다.","causes":"느린 디스크 서브시스템, 작은 buffer pool로 인한 많은 물리 읽기, 대형 테이블 스캔, 체크포인트/flush 압박, 불충분한 I/O 용량.","action":"buffer pool을 키우고, 비효율적인 쿼리와 누락된 인덱스를 수정하고, 빠른 스토리지(SSD/NVMe)로 옮기고, innodb_io_capacity를 스토리지에 맞추고, flush 동작을 검토한다.","tags":["innodb","io","datafile","disk"]},{"name":"wait/lock/table/sql/handler","kind":"wait","meaning":"스레드가 SQL 계층의 테이블 수준 handler lock(테이블 락)을 획득하기 위해 기다리고 있다. 호환되지 않는 모드로 테이블에 동시 접근할 때 나타난다.","causes":"테이블 락이 필요한 작업, MyISAM 같은 테이블 수준 락 스토리지 엔진, LOCK TABLES 문, DDL과 DML의 경합, 장시간 실행 쿼리.","action":"행 수준 락을 쓰는 InnoDB를 사용하고, LOCK TABLES 사용을 줄이고, DDL은 활동이 적을 때 실행하고, 긴 쿼리를 최적화하고, 테이블 접근을 직렬화하는 패턴을 검토한다.","tags":["lock","table-lock","sql-layer","contention"]},{"name":"wait/synch/mutex/sql/LOCK_open","kind":"wait","meaning":"스레드가 열린 테이블 정의/객체 딕셔너리를 보호하는 LOCK_open mutex를 기다리고 있다. 테이블 open/close 작업의 직렬화다.","causes":"많은 서로 다른 테이블에 대한 지속적인 접근, 작은 table_open_cache/table_definition_cache, 잦은 테이블 열기와 닫기, 높은 동시성, 너무 많은 테이블/파티션.","action":"table_open_cache와 table_open_cache_instances를 늘리고, table_definition_cache를 조정하고, 과도한 테이블/파티션 수를 검토하고, 불필요하게 잦은 테이블 열기를 줄인다.","tags":["mutex","table-cache","sql-layer","concurrency"]},{"name":"행 락 대기","kind":"wait","meaning":"트랜잭션이 다른 트랜잭션이 보유한 InnoDB 행 락이 해제되기를 기다리고 있다. 행 수준 락 경합을 나타낸다.","causes":"같은 행에 대한 동시 UPDATE/DELETE, 커밋하지 않는 장시간 트랜잭션, 누락된 인덱스로 인한 넓은 락 범위(gap lock), hot row 경합, 데드락 순환.","action":"트랜잭션을 짧게 유지하고 즉시 커밋하고, 적절한 인덱스를 추가하여 락이 걸리는 행 범위를 좁히고, 경합 패턴을 재설계하고, 데드락을 모니터링하고(SHOW ENGINE INNODB STATUS), 격리 수준을 검토한다.","tags":["innodb","row-lock","transaction","contention"]},{"name":"로그 파일 대기","kind":"wait","meaning":"스레드가 InnoDB redo log 버퍼에 공간이 확보되거나 redo log가 디스크에 기록되기를 기다리고 있다. 주로 로그 버퍼 flush/체크포인트 압박 상황에서 나타난다.","causes":"작은 redo log(8.0.30+ innodb_redo_log_capacity, 이전에는 innodb_log_file_size × innodb_log_files_in_group), 느린 로그 디스크, 높은 쓰기 속도, 잦은 flush(잦은 커밋과 innodb_flush_log_at_trx_commit=1), 불충분한 로그 버퍼.","action":"redo 용량(8.0.30+ innodb_redo_log_capacity, 동적. 이전에는 innodb_log_file_size, 재시작 필요)과 innodb_log_buffer_size를 늘리고, redo log를 빠른 디스크로 옮기고, 커밋 패턴을 검토하고, 내구성 요구에 비추어 flush 설정을 평가한다.","tags":["innodb","redo","log","io"]},{"name":"wait/io/file/innodb/innodb_log_file","kind":"wait","meaning":"스레드가 InnoDB redo log 파일에 대한 물리 I/O(쓰기 및 fsync)의 완료를 기다리고 있다. 커밋 내구성을 위해 redo가 디스크에 도달하기를 기다린다.","causes":"느린 스토리지 fsync 지연, innodb_flush_log_at_trx_commit=1에서의 매우 잦은 커밋, 저성능 로그 디스크, 높은 쓰기 부하.","action":"redo log 디스크를 지연이 낮은 빠른 스토리지로 옮기고, 가능하면 커밋을 그룹화하고, 비즈니스 요구에 비추어 innodb_flush_log_at_trx_commit을 평가하고, 스토리지 계층을 개선한다.","tags":["innodb","redo","fsync","commit"]},{"name":"wait/synch/mutex/innodb/log_writer_mutex","kind":"wait","meaning":"스레드가 InnoDB redo log writer 스레드의 구조를 보호하는 mutex를 기다리고 있다. 8.0에서 redo 서브시스템이 재설계되어 기존 단일 log_sys_mutex 대신 log_writer_mutex, log_flusher_mutex, log_checkpointer_mutex 같은 별도 계측 항목이 있다.","causes":"매우 높은 쓰기/커밋 비율, 많은 redo 생성, log writer/flusher 스레드 경합, 느린 로그 디스크, 작은 로그 버퍼.","action":"쓰기 부하와 커밋 빈도의 균형을 맞추고, redo 생성을 줄이는 배치 작업을 고려하고, 로그 버퍼를 조정하고, 사용 중인 MySQL 버전의 redo log 확장성 개선을 활용한다.","tags":["innodb","mutex","redo","concurrency"]},{"name":"Innodb_buffer_pool_wait_free","kind":"metric","meaning":"스레드가 buffer pool에서 free 페이지를 찾지 못해 free 페이지를 얻기 위해 dirty 페이지의 flush를 기다려야 했던 횟수. 꾸준히 증가하면 buffer pool 압박을 나타낸다.","causes":"작은 buffer pool, 느린 flush/쓰기 I/O, 낮은 innodb_io_capacity, 높은 쓰기 부하로 인한 빠른 dirty 페이지 생성, 체크포인트 지연.","action":"innodb_buffer_pool_size를 늘리고, innodb_io_capacity/io_capacity_max를 스토리지에 맞추고, 쓰기 I/O 성능을 개선하고, dirty 페이지 비율(innodb_max_dirty_pages_pct)을 검토한다.","tags":["metric","innodb","buffer-pool","flush"]},{"name":"Threads_running","kind":"metric","meaning":"현재 유휴 상태가 아니며 쿼리를 실행 중인 스레드 수. 서버의 순간 동시 실행과 예상 경합 강도를 보여 준다.","causes":"쿼리 급증, 느린 쿼리의 누적, lock/mutex 경합으로 멈춘 스레드, CPU 포화, thundering herd 효과.","action":"Threads_running이 급증하면 느리거나 차단된 쿼리를 찾고(SHOW PROCESSLIST), 쿼리를 튜닝하고, 커넥션 풀과 thread pool로 동시성을 제한하고, 경합 원인을 제거한다.","tags":["metric","concurrency","threads","load"]},{"name":"Innodb_row_lock_time_avg","kind":"metric","meaning":"행 락 획득을 기다리는 데 걸린 평균 시간(밀리초). 행 수준 락 경합의 심각도를 보여 준다.","causes":"같은 행에 대한 높은 동시 접근, 긴 트랜잭션, 누락된 인덱스로 인한 넓은 락 범위, 데드락/lock wait timeout에 근접하는 대기.","action":"트랜잭션 시간을 단축하고, 적절한 인덱스로 락 범위를 좁히고, 경합이 생기는 접근 패턴을 재설계하고, innodb_lock_wait_timeout과 격리 수준을 검토한다.","tags":["metric","innodb","row-lock","contention"]},{"name":"history list length","kind":"metric","meaning":"InnoDB undo log에서 아직 purge되지 않은(이전 행 버전을 보유한) 트랜잭션 수. MVCC를 위해 유지되는 이전 버전의 누적을 보여 준다.","causes":"장시간 열린 트랜잭션(특히 긴 읽기 트랜잭션), 느린 purge 스레드, 높은 DML 비율, 커밋되지 않은/유휴 트랜잭션.","action":"장시간 실행 및 유휴 트랜잭션을 식별해 종료하고, purge 스레드 수(innodb_purge_threads)를 조정하고, 애플리케이션에서 트랜잭션을 짧게 유지한다. HLL이 높으면 성능 및 bloat 문제가 생긴다.","tags":["metric","innodb","undo","mvcc"]},{"name":"Innodb_data_reads","kind":"metric","meaning":"InnoDB가 디스크에서 수행한 물리 읽기 요청의 총수. buffer pool이 처리하지 못해 디스크로 간 읽기 부하를 보여 준다.","causes":"작은 buffer pool, 콜드 캐시, 비효율적인 쿼리와 대형 스캔, 누락된 인덱스, 메모리보다 큰 working set.","action":"buffer pool 크기를 늘리고, 비효율적인 쿼리와 누락된 인덱스를 수정하고, 물리 읽기 추세를 buffer pool 히트율과 함께 모니터링하고, I/O 성능을 개선한다.","tags":["metric","innodb","io","read"]},{"name":"Innodb_buffer_pool_read_requests","kind":"metric","meaning":"InnoDB buffer pool에서 처리된 논리 읽기 요청의 총수. Innodb_buffer_pool_reads(buffer pool에서 찾지 못해 디스크에서 읽은 페이지 수)와 함께 buffer pool 히트율 계산에 사용한다.","causes":"값이 높으면 쿼리 활동이 활발함을 나타낸다. 이 값 대비 Innodb_buffer_pool_reads의 비율이 높으면(히트율이 낮으면) 메모리 부족이나 비효율적인 접근이 있다.","action":"buffer pool 히트율(1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)을 모니터링한다. 낮으면 buffer pool을 키우고 쿼리를 최적화하며, 과도한 논리 읽기를 발생시키는 비효율적인 쿼리를 튜닝한다.","tags":["metric","innodb","buffer-pool","logical-io"]}]},"reference/waits-mssql.json":{"engineId":"mssql","engine":"SQL Server","order":4,"waits":[{"name":"CXPACKET","kind":"wait","meaning":"병렬 쿼리 실행 중 스레드가 동기화하는 동안 발생하는 대기. 병렬 플랜 연산자 간 작업 분배/정합이 대기 중일 때 나타난다.","causes":"높은 병렬도, 불균등한 작업 분배(skew), 무제한/높은 MAXDOP(0)와 낮은 cost threshold for parallelism(기본 5), 누락된 인덱스로 인한 대형 병렬 스캔, 비효율적인 플랜.","action":"cost threshold for parallelism과 MAXDOP를 워크로드에 맞게 조정하고, 누락된 인덱스와 통계를 수정하고, CXPACKET 자체를 문제로 보지 않는다. CXCONSUMER로 실제 대기를 분리하고 비효율적인 병렬 플랜을 대상으로 한다.","tags":["parallelism","cxpacket","query","cpu"]},{"name":"CXCONSUMER","kind":"wait","meaning":"병렬 플랜에서 consumer 스레드가 producer 스레드로부터 데이터가 도착하기를 기다리는 것. 대개 무해한 병렬 처리 대기다.","causes":"정상적인 병렬 실행 중 대기하는 consumer 스레드, 불균등한 데이터 흐름, producer 측 지연, 비효율적인 병렬 플랜.","action":"CXCONSUMER는 대부분 무해하며 그 자체로는 조치가 필요 없다. 실제 병목을 찾으려면 producer 측의 기저 대기 유형(IO, LCK 등)과 플랜 효율에 집중한다.","tags":["parallelism","cxconsumer","query","benign"]},{"name":"PAGEIOLATCH_SH","kind":"wait","meaning":"스레드가 데이터 페이지를 디스크에서 buffer pool로 읽는 동안 공유(SH) 모드로 보호하기 위한 latch를 기다리고 있다. 읽기에 대한 물리 I/O 대기다.","causes":"느린 디스크 I/O, 메모리 부족(작은 buffer pool), 대형 스캔과 누락된 인덱스, 콜드 캐시, 높은 물리 읽기 부하.","action":"메모리(max server memory)와 buffer pool을 평가하고, 비효율적인 쿼리와 누락된 인덱스를 수정하고, 스토리지 I/O 지연을 개선하고, 가장 많이 읽는 쿼리를 식별해 튜닝한다.","tags":["io","pageiolatch","read","buffer-pool"]},{"name":"PAGEIOLATCH_EX","kind":"wait","meaning":"스레드가 디스크에서 읽히는 데이터 페이지를 배타(EX) 모드로 보호하기 위한 latch를 기다리고 있다. 주로 수정을 위해 읽힐 페이지의 물리 I/O를 기다린다.","causes":"느린 디스크 I/O, 높은 쓰기 워크로드, 메모리 부족, 체크포인트/lazy writer 압박, 대량 데이터 변경 작업.","action":"스토리지 쓰기/읽기 성능을 개선하고, buffer pool 메모리를 늘리고, 쓰기가 많은 작업을 배치화하고 효율화하고, tempdb와 데이터 디스크 I/O를 분리해 측정한다.","tags":["io","pageiolatch","write","buffer-pool"]},{"name":"PAGELATCH_UP","kind":"wait","meaning":"스레드가 이미 메모리에 있는 페이지를 업데이트(UP) 모드로 보호하기 위한 latch를 기다리고 있다. 물리 I/O가 아니라 메모리 내 페이지에 대한 동시 접근 경합이다.","causes":"메모리 내 hot page 경합, 특히 tempdb 시스템 페이지(PFS/GAM/SGAM) 경합, 높은 동시 tempdb 객체 생성, 마지막 페이지(hot page) insert 경합.","action":"tempdb를 코어 수에 맞춘 동일 크기의 데이터 파일로 분할하고, tempdb 경합을 줄이고, hot page 패턴을 분산하고, 불필요한 tempdb 사용(spill 등)을 제거한다.","tags":["pagelatch","tempdb","concurrency","contention"]},{"name":"PAGELATCH_EX","kind":"wait","meaning":"스레드가 메모리 내 페이지를 배타(EX) 모드로 보호하기 위한 latch를 기다리고 있다. 주로 오름차순(순차) 키를 가진 테이블의 마지막 페이지 insert 경합에서 나타난다.","causes":"순차/오름차순 키 인덱스의 마지막 페이지 insert 경합, tempdb 시스템 페이지 경합, hot page에 대한 높은 동시 쓰기, 높은 insert 비율.","action":"마지막 페이지 경합에는 OPTIMIZE_FOR_SEQUENTIAL_KEY = ON(SQL Server 2019+) 또는 키 분산(hash/GUID/reverse index)을 사용하고, tempdb를 여러 파일로 분할하고, 대량 insert 패턴을 재설계한다.","tags":["pagelatch","last-page-insert","concurrency","contention"]},{"name":"LCK_M_X","kind":"wait","meaning":"스레드가 리소스(행/페이지/테이블)에 대한 배타 락을 획득하기 위해 기다리고 있다. 주로 데이터 수정(UPDATE/DELETE/INSERT)을 위해 다른 락의 해제를 기다린다.","causes":"장시간 트랜잭션, 같은 리소스에 대한 동시 변경, lock escalation, 누락된 인덱스로 인한 넓은 락 범위, 블로킹 세션.","action":"트랜잭션을 짧게 유지하고 즉시 커밋하고, 적절한 인덱스로 락 범위를 좁히고, 블로킹 쿼리를 찾고(sys.dm_exec_requests/blocking), lock escalation과 격리 수준을 검토하고, 필요하면 RCSI를 고려한다.","tags":["lock","exclusive","blocking","transaction"]},{"name":"LCK_M_U","kind":"wait","meaning":"스레드가 리소스에 대한 update 락을 획득하기 위해 기다리고 있다. UPDATE가 스캔하며 행을 읽고 수정을 준비하는 동안 다른 락의 해제를 기다린다.","causes":"같은 행에 대한 동시 업데이트, 긴 트랜잭션, 누락된 인덱스로 인해 많은 행이 스캔되고 락이 걸림, 블로킹 세션, 락 경합.","action":"UPDATE 문에 적절한 인덱스를 추가하여 스캔/락 대상 행 수를 줄이고, 트랜잭션 시간을 단축하고, 블로킹 체인을 분석하고, 격리 수준과 RCSI 옵션을 평가한다.","tags":["lock","update-lock","blocking","transaction"]},{"name":"WRITELOG","kind":"wait","meaning":"스레드가 트랜잭션 로그 레코드가 디스크에 기록(log flush)되기를 기다리고 있다. 주로 커밋 시 로그가 영속화되기를 기다린다.","causes":"느린 로그 디스크 I/O, 매우 잦은/소규모 커밋, 높은 쓰기 부하, 로그 디스크와 데이터 디스크 간 경합, 동기 Always On 복제의 영향.","action":"트랜잭션 로그를 빠르고 지연이 낮은 디스크로 옮기고, 커밋을 배치화하고, 불필요하게 잦은 커밋을 줄이고, delayed durability를 고려하고(적절한 경우), 로그 I/O 지연을 측정한다.","tags":["log","writelog","commit","io"]},{"name":"ASYNC_NETWORK_IO","kind":"wait","meaning":"SQL Server는 결과를 보낼 준비가 되었지만 클라이언트가 데이터를 충분히 빠르게 소비하지 않아 네트워크/클라이언트 측을 기다리고 있다. 대개 애플리케이션 측 병목이다.","causes":"결과를 느리게 처리하는 클라이언트 애플리케이션(행 단위 소비), 대형 결과 집합, 느린 네트워크, 애플리케이션의 RBAR 처리, 불충분한 클라이언트 리소스.","action":"애플리케이션에서 결과를 빠르게 일괄 소비하고, 불필요하게 큰 결과 집합을 줄이고(필요한 컬럼/행만), 네트워크 지연을 확인한다. 이는 대개 SQL Server가 아니라 클라이언트/네트워크 문제다.","tags":["network","client","application","async"]},{"name":"SOS_SCHEDULER_YIELD","kind":"wait","meaning":"스레드가 CPU 시간 할당량(quantum)을 다 쓰고 스케줄러를 자발적으로 양보할 때 발생하는 대기. 주로 CPU 압박이나 CPU 집약적 처리를 나타낸다.","causes":"CPU 집약적 쿼리, 높은 동시성과 CPU 포화, spinlock 경합, 많은 논리 읽기를 수행하는 비효율적인 쿼리.","action":"CPU를 소모하는 비효율적인 쿼리(누락된 인덱스, 나쁜 플랜)를 찾아 튜닝하고, 논리 읽기를 줄이고, CPU 용량과 동시성을 평가하고, 계속 높으면 하드웨어/플랜 병목을 조사한다.","tags":["cpu","scheduler","cpu-bound","yield"]},{"name":"RESOURCE_SEMAPHORE","kind":"wait","meaning":"쿼리가 실행에 필요한 memory grant를 기다리고 있다. 충분한 쿼리 메모리가 확보될 때까지 실행을 시작할 수 없다.","causes":"과도한 메모리를 요청하는 쿼리(대형 sort/hash), 잘못된(과대) 카디널리티 추정, 불충분한 서버 메모리, 메모리 집약적인 많은 동시 쿼리, 오래된 통계.","action":"통계를 갱신하여 과도한 memory grant 추정을 바로잡고, 대형 sort/hash가 필요한 쿼리를 최적화하고(인덱스, 필터), Resource Governor와 max server memory 설정을 검토하고, 메모리 집약적인 동시 부하를 제한한다.","tags":["memory","memory-grant","query","resource"]},{"name":"THREADPOOL","kind":"wait","meaning":"새 작업을 실행할 worker thread가 없어 thread pool의 free 스레드를 기다리는 작업. 심각한 worker thread 고갈을 나타내는 중대한 대기 유형이다.","causes":"긴 블로킹에 붙잡힌 스레드, 매우 높은 동시 접속, 불충분한 max worker threads, 높은 병렬 처리, 데드락/긴 락 체인.","action":"블로킹 체인을 긴급히 해소하고, 커넥션 풀로 동시 접속을 관리하고, 오래 보유된 락을 제거하고, max worker threads 설정을 검토한다. 근본 원인을 찾으려면 DAC로 접속한다.","tags":["threadpool","worker-thread","blocking","critical"]},{"name":"HADR_SYNC_COMMIT","kind":"wait","meaning":"Always On 동기 replica에서 primary가 트랜잭션을 커밋하기 전에 로그 레코드가 secondary replica에 harden되기를 기다리는 것. 동기 커밋 지연이다.","causes":"secondary replica까지의 느리거나 지연이 큰 네트워크, secondary의 느린 로그 디스크 I/O, 과부하된 secondary, 지리적으로 먼 replica, 높은 쓰기 속도.","action":"replica 간 네트워크 지연과 대역폭을 개선하고, secondary 로그 디스크 성능을 높이고, secondary 부하를 분산하고, 지연이 중요하면 동기/비동기 replica 토폴로지를 검토한다.","tags":["alwayson","replication","sync-commit","hadr"]},{"name":"Page Life Expectancy","kind":"metric","meaning":"데이터 페이지가 (메모리에서 축출되지 않고) buffer pool에 머무는 평균 초를 나타내는 지표. 메모리 압박의 중요한 지표다.","causes":"PLE가 낮거나 빠르게 떨어지면 메모리 압박, 비효율적인 대형 스캔, 메모리보다 큰 working set, 메모리 부족을 의미한다. NUMA 노드도 개별적으로 평가해야 한다.","action":"고정 임계값 대신 PLE 추세와 급락을 보고, buffer pool 메모리(max server memory)를 평가하고, 메모리를 많이 쓰는 비효율적인 쿼리를 튜닝하고, NUMA 노드별 PLE를 모니터링한다.","tags":["metric","memory","buffer-pool","ple"]},{"name":"버퍼 캐시 적중률","kind":"metric","meaning":"디스크에 가지 않고 buffer cache에서 충족된 데이터 페이지 요청의 비율. 메모리에서 읽힌 페이지의 백분율을 보여 준다.","causes":"최신 시스템에서는 이 비율이 대개 매우 높으며(read-ahead 덕분), 낮은 값은 상당한 메모리 압박을 나타낸다. 높은 비율만으로 좋은 성능이 보장되지는 않는다.","action":"이 지표를 단독으로 해석하지 말고 PLE, PAGEIOLATCH 대기, 물리 읽기 추세와 함께 본다. 메모리 압박 징후가 있으면 buffer pool 메모리를 늘리고 비효율적인 쿼리를 수정한다.","tags":["metric","memory","buffer-pool","ratio"]}]},"reference/waits-mongodb.json":{"engineId":"mongodb","engine":"MongoDB","order":5,"waits":[{"name":"WiredTiger 캐시 사용률 %","kind":"metric","meaning":"WiredTiger 스토리지 엔진에 할당된 내부 캐시가 얼마나 사용 중인지 나타낸다. 데이터를 메모리에 유지하는 압박을 보여 준다(기본 목표 임계값은 약 80%).","causes":"캐시보다 큰 working set, 작은 캐시, 대형 스캔, 많은 읽기/쓰기, 많은 열린 트랜잭션.","action":"WiredTiger 캐시 크기(storage.wiredTiger.engineConfig.cacheSizeGB)와 RAM을 평가하고, working set을 줄이는 인덱스를 추가하고, 사용량이 80% 목표와 95% 축출 임계값에 근접하면 축출 압박과 지연을 모니터링한다.","tags":["metric","wiredtiger","cache","memory"]},{"name":"WiredTiger 캐시 dirty %","kind":"metric","meaning":"WiredTiger 캐시에서 아직 디스크에 기록되지 않은(dirty) 데이터의 백분율. 쓰기 압박과 체크포인트/축출 부하를 보여 준다(dirty 축출 임계값은 대개 약 20%).","causes":"높은 쓰기 부하, 느린 디스크 I/O, 대형/장시간 쓰기, 체크포인트 압박, 쓰기 속도를 따라가지 못하는 캐시.","action":"dirty 비율이 축출 임계값(20%)에 근접하면 디스크 쓰기 성능을 개선하고, 쓰기 부하를 분산하고, 캐시 크기를 평가하고, 긴 쓰기와 체크포인트 간격을 검토한다.","tags":["metric","wiredtiger","cache","write"]},{"name":"사용 가능한 티켓 (read/write)","kind":"metric","meaning":"WiredTiger가 동시 읽기 및 쓰기 작업에 부여하는 동시성 티켓의 남은 수. 0으로 떨어지면 새 작업이 티켓을 얻기 위해 큐에 대기한다.","causes":"티켓을 오래 보유하는 느린 작업, 디스크 I/O 병목, 락/경합, 급격한 부하 급증, 장시간 실행 쿼리로 인한 티켓 고갈.","action":"사용 가능한 티켓이 0에 근접하고 큐가 생기면 느린 쿼리를 최적화하고(인덱스 추가), 디스크 I/O를 개선하고, 부하를 분산한다. 티켓 고갈은 대개 더 깊은 I/O/경합 병목의 증상이다.","tags":["metric","wiredtiger","concurrency","tickets"]},{"name":"글로벌 락 큐","kind":"metric","meaning":"global/database/collection 락을 기다리는 읽기(readers) 및 쓰기(writers) 작업의 큐 길이. 락 경합 상황에서 대기 중인 작업 수를 보여 준다.","causes":"오래 보유된 락, 높은 쓰기 경합, 장시간 실행 작업, DDL 작업과 인덱스 빌드 시작/종료 시 잡는 짧은 배타 collection 락(4.2+ hybrid build. foreground build는 더 이상 없음), 느린 I/O로 오래 보유된 락.","action":"큐가 지속적으로 차 있으면 장시간 실행 작업을 찾아 최적화하고(currentOp), 인덱스 빌드와 기타 DDL을 비피크 시간에 예약하고, I/O를 개선하고, 경합하는 쓰기 패턴을 검토한다.","tags":["metric","lock","queue","concurrency"]},{"name":"복제 지연 (secondary)","kind":"metric","meaning":"secondary 멤버가 primary의 oplog 적용에서 얼마나(초) 뒤처졌는지 나타낸다. replica set 멤버 간 데이터 지연을 보여 준다.","causes":"느린 네트워크, secondary의 느린 디스크 I/O, secondary의 높은 읽기 부하, 높은 primary 쓰기 속도, 불충분한 secondary 하드웨어, 장시간 실행 작업.","action":"rs.printSecondaryReplicationInfo로 지연을 모니터링하고, 네트워크와 secondary I/O 성능을 개선하고, secondary 부하를 분산하고, oplog 크기를 충분히 유지하고, write concern 요구 사항을 검토한다.","tags":["metric","replication","lag","secondary"]},{"name":"oplog 윈도","kind":"metric","meaning":"oplog에 보관된 가장 오래된 작업과 가장 최신 작업 사이의 시간 범위(시간). secondary가 동기화를 잃지 않고 뒤처질 수 있는 시간을 보여 준다.","causes":"작은 oplog 크기, 갑작스러운 높은 쓰기 속도, 오랫동안 오프라인인 secondary, 복제 부하 증가.","action":"oplog 윈도가 줄었으면 oplog 크기를 늘리고, secondary 지연을 윈도 이하로 유지하고, 쓰기 부하와 백업/동기화 요구 사항을 검토한다. 윈도를 놓치면 전체 resync가 필요하다.","tags":["metric","replication","oplog","window"]},{"name":"페이지 폴트","kind":"metric","meaning":"운영체제가 메모리에 없는 데이터 페이지를 디스크에서 가져와야 하는 페이지 폴트(특히 hard fault) 수. 데이터가 메모리에서 디스크로 넘치는 것을 보여 준다.","causes":"RAM보다 큰 working set, 메모리 부족, 대형 스캔, 캐시 압박, 다른 프로세스의 메모리 소비.","action":"서버 메모리/working set을 평가하고, 인덱스를 추가하여 스캔 데이터를 줄이고, WiredTiger 캐시와 OS 메모리의 균형을 맞추고, hard page fault 증가를 I/O 지연과 함께 모니터링한다.","tags":["metric","memory","page-fault","io"]},{"name":"현재 접속 수 (connections current)","kind":"metric","meaning":"현재 서버에 열려 있는 클라이언트 접속 수. 커넥션 풀 사용량과 최대치에 얼마나 근접했는지 보여 준다.","causes":"잘못 구성된 커넥션 풀, 접속 누수, 클라이언트 급증, 오래 보유된 접속, 마이크로서비스의 과도한 접속.","action":"접속 수를 서버 한도(maxIncomingConnections)와 비교하고, 애플리케이션 측에서 커넥션 풀 크기를 올바르게 정하고, 접속 누수를 수정하고 급증을 모니터링한다.","tags":["metric","connections","pool","capacity"]},{"name":"scanAndOrder / COLLSCAN 비율","kind":"metric","meaning":"인덱스 없이 컬렉션을 전체 스캔(COLLSCAN)하거나 메모리에서 정렬(scanAndOrder)하는 쿼리의 비율. 인덱스 비효율의 지표다.","causes":"누락되었거나 부적합한 인덱스, 쿼리와 맞지 않는 정렬 키, 필터 없는 광범위한 쿼리, 나쁜 쿼리 패턴.","action":"느린 쿼리와 그 플랜을 검토하고(explain), 쿼리 및 정렬 패턴에 맞는 복합 인덱스를 추가하고, COLLSCAN과 scanAndOrder를 발생시키는 쿼리를 수정하고, examined/returned 문서 비율을 모니터링한다.","tags":["metric","index","collscan","query"]},{"name":"대기 중인 작업","kind":"metric","meaning":"락이나 리소스를 기다리느라 큐에 들어간(아직 활성 실행되지 못한) 읽기 및 쓰기 작업 수. 시스템이 수요를 감당하지 못함을 보여 준다.","causes":"락 경합, 티켓 고갈, I/O 병목, 장시간 실행 쿼리, 급격한 부하 급증, 느린 디스크.","action":"큐가 커지면 활성 및 장시간 실행 작업을 조사해 최적화하고(currentOp), 인덱스와 I/O를 개선하고, 부하를 분산하고, 티켓 및 락 경합 지표와 함께 근본 원인을 찾는다.","tags":["metric","queue","concurrency","load"]}]},"reference/waits-redis.json":{"engineId":"redis","engine":"Redis","order":6,"waits":[{"name":"keyspace 히트율","kind":"metric","meaning":"keyspace_hits와 keyspace_misses로 계산한, 메모리에서 찾은 키 조회의 비율. Redis가 캐시로서 얼마나 효과적으로 동작하는지 보여 준다.","causes":"낮은 비율은 메모리 부족으로 인한 축출, 짧은 TTL, 콜드 캐시, 캐싱된 적 없는 고유 키의 잦은 조회, 나쁜 키 설계를 나타낸다.","action":"히트율을 hits/(hits+misses)로 모니터링한다. 낮으면 메모리/축출 정책과 TTL을 검토하고, 자주 접근하는 데이터를 미리 적재하고, 키 설계와 캐싱 전략을 개선한다.","tags":["metric","cache","hit-ratio","keyspace"]},{"name":"evicted_keys","kind":"metric","meaning":"maxmemory 한도에 도달했을 때 축출 정책에 의해 메모리에서 제거(삭제)된 키 수. 메모리 압박의 직접적인 지표다.","causes":"maxmemory 한도 초과, 메모리 부족, TTL 없는 키의 누적, 메모리보다 큰 데이터 양, 부적합한 축출 정책.","action":"축출이 꾸준히 증가하면 메모리를 추가하거나 데이터 양을 줄이고, 워크로드에 맞게 maxmemory-policy(예: allkeys-lru/volatile-lru)를 선택하고, TTL 사용과 big key를 검토한다.","tags":["metric","memory","eviction","maxmemory"]},{"name":"blocked_clients","kind":"metric","meaning":"BLPOP, BRPOP, WAIT, XREAD BLOCK 같은 블로킹 명령으로 이벤트/데이터를 기다리는 클라이언트 수. 큐 consumer 패턴에서는 정상이지만 비정상적인 증가는 경고 신호다.","causes":"블로킹 list/stream 명령을 사용하는 consumer, producer 측에서 데이터가 오지 않음, WAIT로 replica 확인 응답 대기, consumer-producer 불균형.","action":"차단된 클라이언트 수를 예상 consumer 모델과 비교한다. 비정상적으로 증가하면 producer 측과 복제 확인 응답을 조사하고 큐 소비 속도의 균형을 맞춘다.","tags":["metric","clients","blocking","queue"]},{"name":"mem_fragmentation_ratio","kind":"metric","meaning":"운영체제가 Redis에 할당한 물리 메모리(RSS)와 Redis가 실제 사용하는 메모리의 비율. 메모리 단편화나 스와핑을 보여 준다.","causes":"1보다 분명히 큰 값은 메모리 단편화를, 1보다 작은 값은 Redis가 (디스크로) 스왑되었음을 나타낸다. 가변 크기 키와 삭제/삽입 반복은 단편화를 늘린다.","action":"단편화가 높으면 activedefrag 활성화나 재시작을 고려한다. 1보다 작으면 스와핑을 조사하고 메모리를 추가한다. allocator(jemalloc) 동작과 big key 패턴을 검토한다.","tags":["metric","memory","fragmentation","swap"]},{"name":"instantaneous_ops_per_sec","kind":"metric","meaning":"Redis가 현재 초당 처리 중인 명령 수(순간 처리량). 워크로드 강도와 급격한 트래픽 변화를 보여 준다.","causes":"급격한 트래픽 급증, 배치 작업 부하, 비효율적인 클라이언트 접근 패턴, 개별 명령 대신 pipelining을 사용하지 않음, 높은 읽기/쓰기 버스트.","action":"처리량을 지연 및 CPU와 함께 모니터링하고, 급락/급증을 트래픽 및 느린 명령과 연관 짓고, pipelining/batching으로 효율을 높이고, 용량을 계획한다.","tags":["metric","throughput","ops","load"]},{"name":"rdb_last_bgsave_status","kind":"metric","meaning":"마지막 RDB 백그라운드 저장(BGSAVE)의 성공 상태. 영속성 스냅샷이 성공적으로 생성되었는지 보여 준다.","causes":"실패(err) 상태는 디스크 가득 참/공간 부족, fork에 필요한 메모리 부족, 디스크 I/O 오류, 권한 문제, 과도한 부하 때문일 수 있다.","action":"상태가 'err'이면 디스크 공간, fork에 필요한 메모리(overcommit 설정), 디스크 상태를 확인하고 로그를 검토한다. 실패한 BGSAVE는 데이터 손실 위험을 만들므로 영속성 구성을 검토한다.","tags":["metric","persistence","rdb","bgsave"]},{"name":"master_link_status","kind":"metric","meaning":"replica의 master에 대한 복제 링크 상태(up/down). replica와 master 간 접속이 정상인지 보여 준다.","causes":"'down' 상태는 네트워크 장애, master 도달 불가, 복제 동기화 오류, 인증 문제, master 과부하 때문일 수 있다.","action":"상태가 'down'이면 네트워크 연결, master 도달 가능성, auth 설정을 확인하고, replica 로그를 검토하고, master_repl_offset 지연을 모니터링하고, 동기화 완료 여부를 확인한다.","tags":["metric","replication","master-link","availability"]},{"name":"지연 (slowlog)","kind":"metric","meaning":"slowlog와 latency 모니터링으로 탐지한, 설정 임계값을 초과한 느린 명령의 지연. 단일 스레드 Redis에서 한 명령이 다른 명령을 차단할 위험을 보여 준다.","causes":"시간 복잡도가 높은 명령(KEYS, 대형 SORT, 대형 컬렉션의 SMEMBERS/HGETALL), big key, 느린 Lua 스크립트, fork/영속성 일시 정지, 스와핑.","action":"SLOWLOG 및 LATENCY 명령으로 느린 명령을 찾고, O(N) 명령(KEYS 대신 SCAN)과 big key를 제거하고, 스크립트를 최적화하고, fork/AOF/RDB로 인한 일시 정지를 모니터링한다.","tags":["metric","latency","slowlog","performance"]},{"name":"expired_keys","kind":"metric","meaning":"TTL이 만료되어 삭제된 키의 총수. TTL 기반 만료 동작과 메모리 회수의 지표다.","causes":"TTL이 있는 키의 만료는 정상이다. 급격한 변화는 TTL 정책 변경, 대량 키 만료, 예상치 못한 삭제 패턴을 나타낼 수 있다.","action":"expired와 evicted를 구분하고(expired는 정상적인 TTL 동작, evicted는 메모리 압박), 급격한 만료 파동이 캐시 미스와 부하에 미치는 영향을 모니터링하고, TTL 분포를 분산한다.","tags":["metric","ttl","expiration","keyspace"]},{"name":"connected_clients vs maxclients","kind":"metric","meaning":"현재 접속된 클라이언트 수가 설정된 maxclients 한도에 얼마나 근접했는지 나타낸다. 접속 용량 고갈 위험을 보여 준다.","causes":"접속 누수, 잘못 구성된 커넥션 풀, 클라이언트 급증, 오래 보유된 유휴 접속, 많은 마이크로서비스 접속.","action":"접속된 클라이언트를 maxclients와 비교한다. 한도에 근접하면 커넥션 풀 크기를 올바르게 정하고, 접속 누수를 수정하고, 유휴 접속 타임아웃을 설정하고, 필요하면 maxclients를 늘린다.","tags":["metric","connections","maxclients","capacity"]}]},"reference/waits-elasticsearch.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":7,"waits":[{"name":"JVM heap 사용률 %","kind":"metric","meaning":"노드의 JVM heap 사용 백분율. heap이 높고 지속적으로 가득 차 있으면 메모리 압박과 과도한 GC 위험을 의미한다.","causes":"과도한 fielddata/집계 메모리, 너무 많은 샤드, 높은 bulk/쿼리 부하, 대형 매핑, 작은 heap, 메모리 누수 패턴.","action":"heap 사용량을 모니터링하고(특히 GC 후에도 75% 이상이면 개입), 샤드 수를 줄이고, 무거운 집계와 fielddata 사용을 제한하고, heap을 RAM의 절반 이하이자 32GB 미만으로 유지한다.","tags":["metric","jvm","heap","memory"]},{"name":"GC 시간/빈도","kind":"metric","meaning":"JVM garbage collection의 시간과 빈도. 길거나 잦은 GC 일시 정지는 노드를 무응답 상태로 만들고 성능을 저하시킨다.","causes":"높은 heap 압박, 대량의 old generation(old gen) 누적, 무거운 집계/fielddata, 작은 heap, 너무 많은 샤드, 장시간 실행되는 대형 쿼리.","action":"긴 일시 정지(특히 old GC)의 근본 원인인 heap 압박을 제거하고, 메모리를 많이 쓰는 쿼리를 최적화하고, 샤드 수를 줄인다. 잦고 긴 GC는 노드 불안정의 조기 경고다.","tags":["metric","jvm","gc","pause"]},{"name":"thread_pool write 거부","kind":"metric","meaning":"write(indexing/bulk) 스레드 풀 큐가 가득 차 거부된 인덱싱 요청 수. 노드가 쓰기 부하를 감당하지 못함을 보여 준다.","causes":"과도한 bulk/인덱싱 속도, 느린 디스크 I/O, bulk 대신 많은 소규모 요청, 불충분한 노드 용량, refresh 압박.","action":"인덱싱 요청을 적절한 크기의 bulk로 보내고, 쓰기 속도를 노드 용량에 맞게 조절하고(backoff), 디스크 I/O를 개선하고, refresh_interval을 늘리고, 용량을 확장한다.","tags":["metric","thread-pool","indexing","rejection"]},{"name":"thread_pool search 거부","kind":"metric","meaning":"search 스레드 풀 큐가 가득 차 거부된 검색 요청 수. 노드가 쿼리 부하를 감당하지 못함을 보여 준다.","causes":"과도한 동시 쿼리, 무겁거나 느린 집계, 너무 많은 샤드 접근, 비효율적인 쿼리, 불충분한 노드/CPU 용량.","action":"클라이언트 측에서 쿼리 동시성을 제한하고, 무거운 쿼리와 집계를 최적화하고, 불필요한 샤드 fan-out을 줄이고(routing/filtering), 노드를 추가하고 replica 샤드 수를 늘리고(검색은 replica 샤드에서도 처리됨), 용량을 확장한다.","tags":["metric","thread-pool","search","rejection"]},{"name":"인덱싱/검색 지연","kind":"metric","meaning":"인덱싱 및 검색 작업의 평균 지연. index_time_in_millis와 query_time_in_millis 지표에서 산출하며 사용자가 체감하는 응답 시간을 반영한다.","causes":"느린 디스크 I/O, 무거운 쿼리/집계, GC 일시 정지, 많은 샤드 수, refresh/merge 압박, 불충분한 리소스.","action":"slow log로 느린 쿼리를 찾아 최적화하고, 인덱싱을 위해 bulk와 refresh_interval을 조정하고, 디스크 및 heap 성능을 개선하고, 지연을 스레드 풀 거부 및 GC와 함께 해석한다.","tags":["metric","latency","indexing","search"]},{"name":"refresh/flush 시간","kind":"metric","meaning":"새 문서를 검색 가능하게 하는 refresh와 translog를 Lucene에 영속화하는 flush에 소요된 시간. 인덱싱 오버헤드를 보여 준다.","causes":"너무 잦은 refresh(낮은 refresh_interval), 느린 디스크 I/O, 많은 인덱싱, 큰 translog, 많은 소형 세그먼트.","action":"대량 인덱싱 중에는 refresh_interval을 늘리거나(예: 30s) 일시적으로 비활성화하고, 디스크 I/O를 개선하고, bulk 크기를 조정하고, 세그먼트 merge 동작을 검토한다.","tags":["metric","refresh","flush","indexing"]},{"name":"미할당 샤드","kind":"metric","meaning":"어떤 노드에도 할당되지 않은 샤드 수. 미할당 primary는 데이터에 접근할 수 없음(red health)을, 미할당 replica는 내구성/용량 위험을 의미한다.","causes":"노드 장애/이탈, 디스크 watermark 초과, 노드 수 대비 너무 높은 replica 설정, 할당 규칙/필터, 샤드 복구 중 지연.","action":"cluster allocation explain API로 원인을 찾고, 장애 노드를 복구하고, 디스크 공간/watermark 문제를 해결하고, replica 수와 할당 규칙을 검토한다. red 상태를 우선 처리한다.","tags":["metric","shards","allocation","cluster-health"]},{"name":"디스크 watermark %","kind":"metric","meaning":"Elasticsearch 샤드 배치에 영향을 주는 low/high/flood-stage 임계값(기본 85%/90%/95%) 대비 노드 디스크 사용량.","causes":"디스크 가득 참, 데이터 증가, 불충분한 스토리지, 불균형한 샤드 분포, 정리되지 않는 오래된 인덱스.","action":"low watermark에서는 노드에 새 샤드가 할당되지 않고, high watermark에서는 샤드가 다른 노드로 재배치되며, flood-stage에서는 인덱스가 읽기 전용이 된다. 디스크 공간을 확보하고(오래된 인덱스 삭제/ILM 사용), 스토리지를 늘리고, 샤드 분포의 균형을 맞추고 watermark 임계값을 모니터링한다.","tags":["metric","disk","watermark","storage"]},{"name":"fielddata/query cache 축출","kind":"metric","meaning":"메모리 한도 때문에 fielddata 캐시와 query(filter) 캐시에서 축출된 항목 수. 캐시 비효율과 메모리 압박을 나타낸다.","causes":"높은 fielddata 사용(text 필드의 정렬/집계), 작은 캐시 한도, 매우 다양한 쿼리 필터, 메모리 압박, 재사용되지 않는 필터.","action":"text 필드에는 fielddata 대신 keyword/doc_values를 사용하고, 무거운 fielddata 집계를 제한하고, 캐시 크기를 검토한다. 잦은 축출은 heap 압박과 함께 평가해야 한다.","tags":["metric","cache","fielddata","eviction"]},{"name":"circuit breaker 작동","kind":"metric","meaning":"circuit breaker(parent/fielddata/request breaker)가 요청이 메모리를 초과할 것으로 예측해 거부한 횟수. 노드를 OOM에서 보호하는 안전 메커니즘이다.","causes":"과도한 메모리를 요청하는 집계/쿼리, 높은 fielddata 사용, 대형 bulk 요청, 전반적인 heap 압박, 매우 큰 결과 집합.","action":"circuit breaker 작동을 heap 압박의 징후로 보고, 메모리를 많이 쓰는 쿼리를 최적화하고, fielddata/집계 사용을 줄이고, bulk 크기를 낮추고, heap과 샤드 수를 검토한다.","tags":["metric","circuit-breaker","memory","oom"]},{"name":"pending tasks","kind":"metric","meaning":"master 노드의 처리를 기다리는 cluster state 업데이트 작업 큐. master가 클러스터 관리 부하를 따라가지 못함을 보여 준다.","causes":"많은 인덱스/샤드, 잦은 mapping/settings 변경, 불충분한 master 노드 리소스, 큰 cluster state, 네트워크/master 선출 문제.","action":"큐가 지속적으로 차 있으면 샤드/인덱스 수를 줄이고, 잦은 동적 mapping 업데이트를 제한하고, 별도의 전용 master 노드를 사용하고, cluster state 크기와 master 리소스를 검토한다.","tags":["metric","master","cluster-state","queue"]}]},"reference/alerts-oracle.json":{"engineId":"oracle","engine":"Oracle Database","order":1,"alerts":[{"metric":"버퍼 캐시 적중률","warn":"< 95%","crit":"< 90%","tool":"V$SYSSTAT / V$BUFFER_POOL_STATISTICS","note":"buffer cache 히트율은 디스크에 가지 않고 메모리에서 읽은 데이터 블록의 백분율을 보여 준다. 값이 낮으면 물리 읽기 증가와 작은 SGA를 나타낸다. 이 값만으로는 오해의 소지가 있으므로 항상 AWR 보고서의 물리 읽기와 함께 평가한다.","tags":["memory","cache","sga"]},{"metric":"테이블스페이스 사용률 (%used)","warn":"> 85%","crit":"> 95%","tool":"DBA_TABLESPACE_USAGE_METRICS","note":"테이블스페이스 사용률이 100%에 도달하면 새 익스텐트를 할당할 수 없어 ORA-01653 같은 오류로 작업이 중단된다. autoextend가 활성화되어 있어도 디스크 한도를 주시한다. DBA_TABLESPACE_USAGE_METRICS 뷰는 autoextensible 파일과 고정 크기 파일 모두에 대해 정확한 백분율을 제공한다.","tags":["disk","tablespace","capacity"]},{"metric":"PGA 메모리 사용량","warn":"> 90% pga_aggregate_target","crit":"> 100% (over-alloc)","tool":"V$PGASTAT / V$PGA_TARGET_ADVICE","note":"PGA 목표를 초과하면 sort 및 hash 작업이 디스크(temp)로 넘치고 쿼리가 느려진다. over allocation count가 꾸준히 증가하면 목표가 부족하다는 뜻이다. 서버 메모리에 비추어 pga_aggregate_target 설정을 검토한다.","tags":["memory","pga","temp"]},{"metric":"TEMP 테이블스페이스 사용량","warn":"> 80%","crit":"> 95%","tool":"V$TEMPSEG_USAGE / V$SORT_SEGMENT / DBA_TEMP_FREE_SPACE","note":"TEMP 공간은 대형 정렬, hash join, global temporary table 작업에 사용된다. 가득 차면 ORA-01652가 발생하고 쿼리가 실패한다. 갑작스러운 가득 참은 대개 나쁜 플랜을 가진 단일 쿼리나 너무 많은 데이터를 정렬하는 쿼리에서 비롯된다.","tags":["temp","disk","sort"]},{"metric":"평균 활성 세션 수 (AAS)","warn":"> CPU 코어 수","crit":"> CPU 코어 수의 2배","tool":"V$ACTIVE_SESSION_HISTORY / OEM","note":"Average Active Sessions는 데이터베이스의 현재 부하를 하나의 숫자로 요약한다. 값이 CPU 코어 수를 초과하면 세션이 CPU나 대기 이벤트를 위해 줄을 서고 있다. 코어 수의 배수로 유지되면 병목 분석이 필요하다.","tags":["cpu","load","session"]},{"metric":"Data Guard 적용 지연 (apply lag)","warn":"> 60 s","crit":"> 300 s","tool":"V$DATAGUARD_STATS (apply lag)","note":"apply lag는 standby 데이터베이스가 primary보다 얼마나 뒤처졌는지를 나타낸다. apply lag가 높으면 페일오버 시 적용할 redo 양이 늘어 복구 시간(RTO)이 길어진다. redo가 standby에 도달했다면 데이터는 손실되지 않는다. 잠재적 데이터 손실(RPO)은 transport lag가 결정한다. 네트워크와 redo 적용 중 어느 쪽이 느린지 판단하려면 transport lag와 함께 검토한다.","tags":["replication","dataguard","lag"]},{"metric":"Fast Recovery Area (FRA) 사용량","warn":"> 85%","crit":"> 95%","tool":"V$RECOVERY_FILE_DEST / V$RECOVERY_AREA_USAGE","note":"FRA가 가득 차면 archivelog 생성이 멈추고 데이터베이스 전체가 정지한다. 운영 시스템에서 가장 흔한 다운타임 원인 중 하나다. RMAN 삭제 정책과 db_recovery_file_dest_size 값을 정기적으로 확인한다.","tags":["disk","fra","archivelog"]},{"metric":"블로킹 세션 수","warn":">= 1 (> 30 s)","crit":">= 1 (> 120 s)","tool":"V$LOCK / V$SESSION (blocking_session)","note":"한 세션이 보유한 락이 다른 세션을 기다리게 하면 애플리케이션 전반에 연쇄적인 지연이 발생한다. 오래 지속되는 블로킹은 대개 커밋되지 않은 트랜잭션이나 애플리케이션 버그에서 비롯된다. 블로킹 체인의 루트 세션을 찾아 필요하면 종료한다.","tags":["lock","blocking","concurrency"]},{"metric":"invalid 객체 수","warn":"> 0","crit":"> 50","tool":"DBA_OBJECTS (status='INVALID')","note":"invalid 패키지, 뷰, 트리거는 런타임에 예기치 않은 오류를 일으킬 수 있다. 주로 DDL 변경 후에 나타난다. UTL_RECOMP.RECOMP_PARALLEL 또는 utlrp.sql로 재컴파일하여 해결하고, 계속 invalid 상태인 객체는 근본 원인을 조사한다.","tags":["object","compile","maintenance"]},{"metric":"가장 오래 실행 중인 쿼리","warn":"> 5 min","crit":"> 30 min","tool":"V$SQL / V$SESSION_LONGOPS","note":"장시간 실행되는 단일 쿼리가 리소스를 독점하여 다른 작업도 느려질 수 있다. 배치 작업을 제외하면 OLTP 시스템에서 몇 분씩 실행되는 쿼리는 대개 누락된 인덱스나 나쁜 플랜에서 비롯된다. SQL Monitor 보고서로 플랜과 실제 행 수를 조사한다.","tags":["query","performance","sql"]},{"metric":"alert log의 ORA 오류","warn":">= 1 (비중대)","crit":"ORA-00600 / ORA-07445 / ORA-04031","tool":"alert log / V$DIAG_ALERT_EXT","note":"alert log는 데이터베이스의 상태 일지다. ORA-00600과 ORA-07445는 데이터 손상을 나타낼 수 있는 내부 오류이고, ORA-04031은 shared pool 메모리 고갈을 나타낸다. 이 오류가 나타나면 즉시 조사하고 필요하면 Oracle Support와 공유한다.","tags":["error","alertlog","monitoring"]},{"metric":"redo log 스위치 빈도","warn":"> 6 / hour","crit":"> 12 / hour","tool":"V$LOG_HISTORY","note":"매우 잦은 redo log 스위치는 로그 파일이 너무 작고 체크포인트 압박이 있음을 나타낸다. 이상적으로는 피크 시간대에 15-20분마다 스위치가 일어나야 한다. 과도하게 잦은 스위치는 IO와 아카이브 부하를 늘리므로 redo log 그룹 확대를 고려한다.","tags":["redo","io","checkpoint"]},{"metric":"마지막 성공 RMAN 백업 경과 시간","warn":"> 24 hours","crit":"> 48 hours","tool":"V$RMAN_BACKUP_JOB_DETAILS","note":"백업 경과 시간은 복구 가능한 가장 최근 시점을 결정하며 데이터 손실 위험을 직접 반영한다. 실패하거나 누락된 백업을 알아차리지 못하면 재해 시 되돌릴 방법이 없다. 시점 복구 능력을 보장하기 위해 archivelog 백업도 별도로 모니터링한다.","tags":["backup","rman","recovery"]},{"metric":"디스크 IO 지연 (db file sequential read)","warn":"> 10 ms","crit":"> 20 ms","tool":"V$SYSTEM_EVENT / AWR","note":"단일 블록 읽기의 평균 대기 시간은 스토리지 계층의 성능을 보여 준다. 최신 flash/SSD 시스템에서 이 값은 몇 밀리초여야 한다. 지속적으로 높은 지연은 스토리지 병목이나 과도한 물리 읽기를 나타내므로 SGA와 스토리지 계층을 함께 평가한다.","tags":["io","disk","latency"]}]},"reference/alerts-postgres.json":{"engineId":"postgres","engine":"PostgreSQL","order":2,"alerts":[{"metric":"캐시 히트율","warn":"< 99%","crit":"< 95%","tool":"pg_stat_database / postgres_exporter","note":"캐시 히트율은 shared_buffers에서만 제공된 블록의 백분율이다. 운영체제 캐시가 처리한 읽기도 blks_read로 집계되므로 비율이 실제 디스크 읽기를 실제보다 나쁘게 보이게 할 수 있다. 정상적인 PostgreSQL OLTP 시스템에서는 이 비율이 99% 이상이어야 한다. 값이 낮으면 메모리 부족이나 콜드 캐시를 나타내며, blks_hit와 blks_read로 계산한다.","tags":["memory","cache"]},{"metric":"접속 수 (connections vs max_connections)","warn":"> 80%","crit":"> 90%","tool":"pg_stat_activity / max_connections","note":"활성 접속이 max_connections 한도에 근접하면 새 접속이 실패한다. 접속마다 메모리를 소비하므로 접속 수가 많으면 메모리 압박도 생긴다. PgBouncer 같은 커넥션 풀러로 backend 수를 낮게 유지하는 것을 권장한다.","tags":["connection","limit","pool"]},{"metric":"복제 지연","warn":"> 10 MB","crit":"> 100 MB","tool":"pg_stat_replication (replay_lag / lsn diff)","note":"복제 지연은 standby가 primary보다 뒤처진 WAL의 양(바이트)이다. 지연이 크면 읽기 replica의 데이터가 오래되고 페일오버 시 데이터 손실 위험이 있다. 바이트 단위 LSN 차이를 replay_lag 시간과 함께 모니터링한다.","tags":["replication","lag","standby"]},{"metric":"가장 오래된 트랜잭션 경과 시간","warn":"> 5 min","crit":"> 30 min","tool":"pg_stat_activity (xact_start)","note":"장시간 열린 트랜잭션은 VACUUM이 dead tuple을 제거하지 못하게 하여 테이블 bloat를 일으킨다. 'idle in transaction' 상태의 세션도 불필요하게 락을 보유한다. 그런 세션을 식별하고 idle_in_transaction_session_timeout으로 제한한다.","tags":["query","vacuum","transaction"]},{"metric":"데드락","warn":"> 1 / min","crit":"> 5 / min","tool":"pg_stat_database (deadlocks)","note":"데드락은 두 트랜잭션이 서로의 락을 기다릴 때 발생하며 PostgreSQL은 하나를 취소하여 해결한다. 가끔 발생하는 데드락은 있을 수 있지만 잦으면 애플리케이션의 락 순서가 일관되지 않음을 나타낸다. log_lock_waits를 활성화하여 어떤 쿼리가 충돌하는지 확인한다.","tags":["lock","deadlock","concurrency"]},{"metric":"임시 파일 사용량","warn":"> 1 GB / hour","crit":"> 10 GB / hour","tool":"pg_stat_database (temp_bytes)","note":"임시 파일은 work_mem에 들어가지 않는 sort 및 hash 작업이 디스크로 넘칠 때 생성된다. temp 사용량이 높으면 지연과 디스크 압박이 모두 발생한다. 자주 넘치는 쿼리는 work_mem 증가나 쿼리 최적화를 고려한다.","tags":["temp","disk","work_mem"]},{"metric":"autovacuum 지연 (dead tuple)","warn":"> 10% dead tuples","crit":"> 20% dead tuples","tool":"pg_stat_user_tables (n_dead_tup)","note":"dead tuple 비율이 상승하면 autovacuum이 따라가지 못하고 테이블 bloat가 커지고 있다는 뜻이다. bloat된 테이블은 공간을 차지하고 스캔도 느리게 한다. 바쁜 테이블에서는 autovacuum_vacuum_scale_factor를 낮춰 정리가 더 자주 실행되게 한다.","tags":["vacuum","bloat","maintenance"]},{"metric":"디스크 사용량 (데이터 디렉터리)","warn":"> 85%","crit":"> 95%","tool":"df / node_exporter","note":"PostgreSQL은 디스크가 가득 차기 전에 스스로를 보호하는 메커니즘을 제공하지 않는다. 데이터 디스크가 가득 차면 쓰기가 'could not extend file' / 53100으로 실패하고, WAL(pg_wal) 디스크가 가득 차면 서버가 PANIC으로 종료된다. WAL과 임시 파일은 급격한 증가를 일으킬 수 있으므로 조기 대응이 중요하다.","tags":["disk","capacity"]},{"metric":"WAL 보존 (replication slot 지연)","warn":"> 1 GB","crit":"> 10 GB","tool":"pg_replication_slots (restart_lsn)","note":"사용되지 않거나 지연된 replication slot은 WAL 파일 제거를 막아 pg_wal 디렉터리를 비대하게 만든다. 이는 디스크 가득 참으로 끝날 수 있다. 죽은 slot을 식별해 삭제하고 max_slot_wal_keep_size로 상한을 설정한다.","tags":["wal","slot","disk"]},{"metric":"체크포인트 빈도 (requested checkpoint)","warn":"> 10% requested ratio","crit":"> 30% requested ratio","tool":"PG16 이하: pg_stat_bgwriter (checkpoints_req) / PG17+: pg_stat_checkpointer (num_requested)","note":"timed 대비 requested 체크포인트 비율이 높아지면 max_wal_size가 너무 작다는 뜻이다. 잦은 체크포인트는 IO 급증과 성능 저하를 일으킨다. 부하를 분산하도록 max_wal_size와 checkpoint_completion_target 설정을 검토한다.","tags":["checkpoint","wal","io"]},{"metric":"마지막 성공 백업 경과 시간","warn":"> 24 hours","crit":"> 48 hours","tool":"pgBackRest / barman info","note":"백업 경과 시간은 되돌아갈 수 있는 최신 시점을 결정하며 RPO를 직접 나타낸다. 시점 복구에는 끊김 없는 백업 체인과 WAL 아카이브가 필요하다. 백업은 생성만으로 끝나지 않으며 정기적인 복원 테스트로 유효성을 검증해야 한다.","tags":["backup","recovery","rpo"]},{"metric":"디스크 IO 지연","warn":"> 10 ms","crit":"> 20 ms","tool":"node_exporter / iostat","note":"스토리지 읽기/쓰기 지연은 쿼리 시간에 직접 영향을 준다. 최신 SSD 시스템에서 이 값은 몇 밀리초여야 한다. 지속적으로 높은 지연은 디스크 병목이나 과도한 물리 읽기를 나타내므로 shared_buffers와 스토리지 계층을 함께 평가한다.","tags":["io","disk","latency"]},{"metric":"트랜잭션 ID wraparound (age)","warn":"> 10억","crit":"> 15억","tool":"pg_database (age(datfrozenxid))","note":"트랜잭션 ID age가 20억에 근접하면 PostgreSQL은 데이터 손실을 막기 위해 모든 쓰기를 중단한다(wraparound 보호). 가장 위험한 PostgreSQL 장애 중 하나다. datfrozenxid age를 모니터링하고 autovacuum이 freeze 작업에서 뒤처지지 않는지 확인한다.","tags":["wraparound","vacuum","critical"]}]},"reference/alerts-mysql.json":{"engineId":"mysql","engine":"MySQL","order":3,"alerts":[{"metric":"InnoDB buffer pool 적중률","warn":"< 99%","crit":"< 95%","tool":"SHOW ENGINE INNODB STATUS / mysqld_exporter","note":"buffer pool 히트율은 디스크에 가지 않고 메모리에서 읽은 데이터 및 인덱스 페이지의 백분율이다. 값이 낮으면 innodb_buffer_pool_size가 working set에 비해 너무 작다는 뜻이다. 전용 서버에서는 일반적으로 메모리의 ~70-80%를 buffer pool에 할당한다(공유 서버에서는 더 적게).","tags":["memory","cache","innodb"]},{"metric":"접속 수 (Threads_connected vs max_connections)","warn":"> 80%","crit":"> 90%","tool":"SHOW STATUS / max_connections","note":"접속 수가 max_connections 한도에 근접하면 새 접속이 'Too many connections'로 거부된다. Max_used_connections는 과거 최대치를 보여 준다. 애플리케이션 측 커넥션 풀로 동시 접속을 통제한다.","tags":["connections","limit","thread"]},{"metric":"replica 지연 (Seconds_Behind_Source)","warn":"> 30 s","crit":"> 300 s","tool":"SHOW REPLICA STATUS","note":"Seconds_Behind_Source는 replica가 source보다 몇 초 뒤처졌는지 나타낸다. 지연이 크면 읽기 replica의 데이터가 오래되고 페일오버 위험이 있다. 단일 스레드 적용에서는 대형 트랜잭션 중 값이 오해를 줄 수 있으므로 GTID 차이와 함께 확인한다.","tags":["replication","lag","replica"]},{"metric":"InnoDB 행 락 대기 시간","warn":"avg > 1 s","crit":"avg > 5 s","tool":"SHOW STATUS (Innodb_row_lock_time_avg)","note":"평균 행 락 대기 시간은 트랜잭션이 서로를 얼마나 기다리게 하는지 보여 준다. 값이 높으면 충돌이 심한 업데이트나 장시간 트랜잭션을 가리킨다. Innodb_row_lock_waits와 함께 검토하고 performance_schema에서 충돌하는 쿼리를 찾는다.","tags":["lock","blocking","innodb"]},{"metric":"History list length (undo 이력 길이)","warn":"> 100000","crit":"> 1000000","tool":"SHOW ENGINE INNODB STATUS","note":"history list length는 purge되지 않은 undo 레코드 수이며 장시간 열린 트랜잭션 때문에 증가한다. 값이 높으면 purge가 뒤처지고 MVCC로 인해 읽기 성능이 저하되고 있다는 뜻이다. 목록이 줄어들 수 있도록 장시간 트랜잭션을 찾아 종료한다.","tags":["undo","transaction","mvcc"]},{"metric":"실패한 접속 (Aborted_connects)","warn":"> 5 / min","crit":"> 20 / min","tool":"SHOW STATUS (Aborted_connects)","note":"Aborted_connects는 인증이나 핸드셰이크 중 실패한 접속을 집계한다. 급증은 잘못된 비밀번호, 네트워크 문제, 공격 시도 가능성을 나타낼 수 있다. 접속이 어디서 끊기는지 판단하려면 Aborted_clients와 함께 검토한다.","tags":["connections","error","security"]},{"metric":"디스크에 기록된 임시 테이블","warn":"> 25% ratio","crit":"> 50% ratio","tool":"SHOW STATUS (Created_tmp_disk_tables)","note":"인메모리 임시 테이블이 디스크로 넘치는 비율은 쿼리가 tmp_table_size 한도를 초과함을 보여 준다. 디스크 임시 테이블은 쿼리를 눈에 띄게 느리게 한다. GROUP BY와 BLOB/TEXT 컬럼이 있는 쿼리를 최적화하고 tmp_table_size와 max_heap_table_size 증가를 고려한다.","tags":["temp","disk","query"]},{"metric":"느린 쿼리","warn":"> 5 / min","crit":"> 20 / min","tool":"slow query log / Slow_queries status","note":"long_query_time 임계값을 초과한 쿼리는 slow query log에 기록되며 성능 문제의 주요 원인이다. 느린 쿼리가 잦으면 누락된 인덱스나 잘못 작성된 쿼리를 가리킨다. pt-query-digest로 로그를 분석하고 가장 비용이 큰 쿼리를 우선 처리한다.","tags":["query","performance","slowlog"]},{"metric":"CPU 사용률","warn":"> 80%","crit":"> 90%","tool":"node_exporter / top","note":"지속적으로 높은 CPU 사용률은 무거운 쿼리, 누락된 인덱스, 너무 많은 접속에서 비롯될 수 있다. 짧은 피크는 정상이지만 지속적인 포화는 쿼리 대기열을 만든다. performance_schema나 slow log로 CPU를 가장 많이 쓰는 쿼리를 식별한다.","tags":["cpu","load"]},{"metric":"디스크 사용량 (데이터 디렉터리)","warn":"> 85%","crit":"> 95%","tool":"df / node_exporter","note":"데이터 디렉터리가 가득 차면 MySQL이 쓰기를 할 수 없고 테이블 오류가 발생한다. binlog 파일, undo 테이블스페이스, 임시 테이블스페이스(ibtmp1)의 증가는 디스크를 갑자기 채울 수 있으며, redo log는 고정 크기다(8.0.30+ #innodb_redo, innodb_redo_log_capacity). binlog_expire_logs_seconds로 오래된 binlog가 purge되는지 확인한다.","tags":["disk","capacity","binlog"]},{"metric":"가장 오래 실행 중인 쿼리","warn":"> 60 s","crit":"> 300 s","tool":"SHOW PROCESSLIST / information_schema.PROCESSLIST","note":"장시간 실행되는 단일 쿼리가 락을 보유하고 리소스를 소모하여 다른 트랜잭션에 영향을 줄 수 있다. OLTP 시스템에서 몇 분씩 실행되는 쿼리는 대개 나쁜 플랜에서 비롯된다. EXPLAIN으로 플랜을 조사하고 필요하면 쿼리를 kill한 뒤 최적화한다.","tags":["query","performance"]},{"metric":"마지막 성공 백업 경과 시간","warn":"> 24 hours","crit":"> 48 hours","tool":"xtrabackup / mysqldump 작업 로그","note":"백업 경과 시간은 되돌아갈 수 있는 최신 시점, 즉 RPO를 결정한다. binlog 백업과 함께하면 시점 복구가 가능하다. 백업 생성만으로는 충분하지 않으며 정기적인 복원 테스트로 유효성을 검증한다.","tags":["backup","recovery","rpo"]},{"metric":"디스크 IO 지연","warn":"> 10 ms","crit":"> 20 ms","tool":"iostat / node_exporter","note":"스토리지 지연은 쿼리 시간으로 직결되며 특히 buffer pool에 들어가지 않는 워크로드에서 중요하다. 최신 SSD 시스템에서는 몇 밀리초가 예상된다. 지연이 높으면 디스크 병목이나 과도한 물리 읽기를 나타내므로 buffer pool 크기도 검토한다.","tags":["io","disk","latency"]}]},"reference/alerts-mssql.json":{"engineId":"mssql","engine":"SQL Server","order":4,"alerts":[{"metric":"Page Life Expectancy (PLE)","warn":"< 900 s","crit":"< 300 s","tool":"sys.dm_os_performance_counters","note":"Page Life Expectancy는 데이터 페이지가 buffer pool에서 축출되기 전에 메모리에 머물 수 있는 추정 시간을 초 단위로 보여 준다. 값이 낮으면 메모리 압박과 페이지가 캐시에서 빠르게 밀려남을 나타낸다. NUMA 노드별로 따로 모니터링하고 급락은 메모리 부족으로 간주한다.","tags":["memory","cache","ple"]},{"metric":"버퍼 캐시 적중률","warn":"< 98%","crit":"< 95%","tool":"sys.dm_os_performance_counters","note":"buffer cache 히트율은 디스크에 가지 않고 메모리에서 읽은 페이지의 백분율이다. SQL Server에서는 이 값이 대개 매우 높으므로 PLE와 함께 평가한다. 단독으로는 오해의 소지가 있으므로 메모리 병목 분석 시에는 PLE를 주 지표로 사용한다.","tags":["memory","cache"]},{"metric":"차단된 프로세스","warn":">= 1 (> 5 s)","crit":">= 1 (> 30 s)","tool":"sys.dm_exec_requests (blocking_session_id)","note":"한 세션이 보유한 락이 다른 세션을 기다리게 하면 블로킹 체인이 형성되어 애플리케이션 전반의 지연으로 이어진다. 오래 지속되는 블로킹은 대개 커밋되지 않은 트랜잭션이나 부적절한 격리 수준에서 비롯된다. blocked process report로 head blocker를 식별한다.","tags":["lock","blocking","concurrency"]},{"metric":"트랜잭션 로그 사용률 (%used)","warn":"> 75%","crit":"> 90%","tool":"sys.dm_db_log_space_usage / DBCC SQLPERF(LOGSPACE)","note":"트랜잭션 로그가 가득 차면 데이터베이스가 쓰기를 할 수 없고 오류 9002로 중단된다. FULL 복구 모델에서는 로그 백업을 하지 않으면 로그가 계속 커진다. 로그 백업 빈도와 VLF 수를 모니터링하여 로그가 truncate되고 재사용되는지 확인한다.","tags":["log","disk","recovery"]},{"metric":"tempdb 사용률 (%used)","warn":"> 80%","crit":"> 95%","tool":"sys.dm_db_file_space_usage","note":"tempdb는 임시 테이블, 정렬, version store, hash 작업에 공유된다. 가득 차면 서버 전체의 쿼리가 실패한다. 급격한 증가는 대개 대형 쿼리나 장시간 스냅샷 트랜잭션에서 비롯되므로 version store 크기를 별도로 모니터링한다.","tags":["tempdb","disk","sort"]},{"metric":"가장 오래 실행 중인 쿼리","warn":"> 60 s","crit":"> 300 s","tool":"sys.dm_exec_requests / Query Store","note":"장시간 실행되는 단일 쿼리가 락을 보유하고 리소스를 소모하여 다른 작업에 영향을 줄 수 있다. OLTP 시스템에서 몇 분씩 실행되는 쿼리는 대개 누락된 인덱스, parameter sniffing, 나쁜 플랜에서 비롯된다. Query Store로 플랜 회귀를 조사한다.","tags":["query","performance"]},{"metric":"Availability Group redo 큐","warn":"> 1 MB","crit":"> 100 MB","tool":"sys.dm_hadr_database_replica_states","note":"redo queue는 secondary replica에서 아직 적용을 기다리는 로그 양을 보여 준다. redo queue가 크면 페일오버 후 복구 시간(RTO)이 길어지고 읽기 가능 replica의 데이터가 오래된다. 지연이 언제 해소될지 예측하려면 redo_rate와 함께 조사한다.","tags":["replication","ag","lag"]},{"metric":"데드락 비율","warn":"> 1 / min","crit":"> 5 / min","tool":"system_health XEvent / Performance counters","note":"데드락은 두 트랜잭션이 서로의 락을 기다릴 때 발생하며 SQL Server는 하나를 victim으로 선택해 해결한다. 가끔 발생하는 데드락은 있을 수 있지만 잦으면 락 순서가 일관되지 않음을 나타낸다. system_health extended event 세션에서 deadlock graph를 추출하고 충돌하는 쿼리를 수정한다.","tags":["lock","deadlock","concurrency"]},{"metric":"worker thread 사용량","warn":"> 80%","crit":"> 90%","tool":"sys.dm_os_schedulers / max worker threads","note":"worker thread pool이 고갈되면 새 요청이 THREADPOOL 대기에서 줄을 서고 서버가 무응답 상태가 될 수 있다. 대개 장시간 블로킹이나 과도한 동시 접속이 원인이다. 블로킹 체인을 해소하여 스레드 압박을 줄인다.","tags":["cpu","thread","load"]},{"metric":"CPU 사용률","warn":"> 80%","crit":"> 90%","tool":"sys.dm_os_ring_buffers / Performance Monitor","note":"지속적으로 높은 CPU는 무거운 쿼리, 누락된 인덱스, 나쁜 플랜에서 비롯될 수 있다. 짧은 피크는 정상이지만 지속적인 포화는 쿼리 대기열을 만든다. sys.dm_exec_query_stats로 CPU를 가장 많이 쓰는 쿼리를 식별한다.","tags":["cpu","load"]},{"metric":"마지막 성공 백업 경과 시간","warn":"> 24 hours (full)","crit":"> 48 hours (full)","tool":"msdb.dbo.backupset","note":"백업 경과 시간은 되돌아갈 수 있는 최신 시점과 RPO를 결정한다. FULL 복구 모델에서는 로그 백업으로 시점 복구가 가능하다. 복구 가능성을 보장하기 위해 full, differential, log 백업 체인을 모니터링하고 정기적인 복원 테스트를 수행한다.","tags":["backup","recovery","rpo"]},{"metric":"디스크 IO 지연 (avg read/write)","warn":"> 15 ms","crit":"> 30 ms","tool":"sys.dm_io_virtual_file_stats","note":"파일별 평균 읽기/쓰기 지연은 스토리지 성능을 보여 준다. 데이터 파일은 약 15 ms, 로그 파일은 그보다 낮은 값이 예상된다. 지속적으로 높은 지연은 스토리지 병목을 나타내므로 sys.dm_io_virtual_file_stats로 가장 느린 파일을 식별한다.","tags":["io","disk","latency"]}]},"reference/alerts-mongodb.json":{"engineId":"mongodb","engine":"MongoDB","order":5,"alerts":[{"metric":"WiredTiger 캐시 dirty 비율","warn":"> 5%","crit":"> 20%","tool":"db.serverStatus().wiredTiger.cache","note":"dirty 캐시 비율은 WiredTiger 캐시에서 아직 디스크에 기록되지 않은 수정 페이지의 백분율이다. 20%를 초과하면 축출 압박과 쓰기 병목을 의미하며, MongoDB는 애플리케이션 스레드에 축출을 강제한다. 캐시 크기와 디스크 쓰기 처리량을 함께 평가한다.","tags":["memory","cache","wiredtiger"]},{"metric":"WiredTiger 캐시 사용량 (used %)","warn":"> 80%","crit":"> 95%","tool":"db.serverStatus().wiredTiger.cache","note":"캐시 사용량이 80% 축출 목표를 초과하면 WiredTiger가 페이지를 적극적으로 축출하기 시작하여 성능이 떨어진다. working set이 캐시에 들어가지 않으면 물리 읽기가 늘어난다. wiredTigerCacheSizeGB 설정과 서버 메모리를 검토한다.","tags":["memory","cache","wiredtiger"]},{"metric":"사용 가능한 티켓 (read/write tickets)","warn":"available/totalTickets < 20% 또는 queueLength > 0 (지속)","crit":"available = 0이고 queueLength가 계속 증가","tool":"db.serverStatus().wiredTiger.concurrentTransactions","note":"WiredTiger는 티켓 시스템으로 동시 읽기 및 쓰기 작업을 제한한다. 7.0 이전에는 기본값이 고정 128 티켓이었고, 7.0+에서는 티켓 수가 동적으로 조정되므로(storageEngineConcurrencyAdjustmentAlgorithm: throughputProbing) 절대 수 대신 available/totalTickets 비율과 queueLength를 모니터링한다. 티켓이 고갈되어 작업이 대기열에 쌓이는 것은 심각한 병목의 징후이며, 대개 느린 쿼리나 디스크 I/O 포화가 원인이다.","tags":["concurrency","ticket","bottleneck"]},{"metric":"복제 지연","warn":"> 10 s","crit":"> 60 s","tool":"rs.printSecondaryReplicationInfo()","note":"복제 지연은 secondary 멤버가 primary보다 몇 초 뒤처졌는지 나타낸다. 지연이 크면 read preference에 따라 오래된 데이터를 읽게 되고 페일오버 시 데이터 손실 위험이 있다. 대개 secondary의 디스크 I/O 또는 네트워크 병목이나 장시간 실행 작업이 원인이다.","tags":["replication","lag","replicaset"]},{"metric":"접속 사용률 (connections %)","warn":"> 80%","crit":"> 90%","tool":"db.serverStatus().connections","note":"활성 접속이 설정된 상한에 근접하면 새 접속이 거부된다. 접속마다 메모리와 스레드를 소비하므로 접속 수가 많으면 리소스 압박도 생긴다. 애플리케이션 측에서 커넥션 풀 크기를 올바르게 정하여 connection storm을 방지한다.","tags":["connection","limit","pool"]},{"metric":"페이지 폴트 비율","warn":"> 100 / s","crit":"> 1000 / s","tool":"db.serverStatus() / extra_info page_faults","note":"페이지 폴트는 데이터가 메모리에 없어 디스크에서 가져와야 할 때 발생한다. 폴트 비율이 높으면 working set이 메모리에 들어가지 않고 시스템이 디스크 의존적으로 동작함을 보여 준다. 지속적으로 높으면 메모리 증설이나 쿼리/인덱스 최적화가 필요하다.","tags":["memory","disk","pagefault"]},{"metric":"대기 중인 작업","warn":"> 10","crit":"> 50","tool":"db.serverStatus().globalLock.currentQueue","note":"globalLock currentQueue는 락이나 리소스를 기다리며 큐에 들어간 읽기/쓰기 수를 보여 준다. 큐가 지속적으로 차 있으면 서버가 유입 부하를 따라가지 못함을 나타낸다. 병목의 원인을 판단하려면 티켓 고갈 및 디스크 I/O와 함께 조사한다.","tags":["concurrency","queue","lock"]},{"metric":"oplog 윈도","warn":"< 24 hours","crit":"< 6 hours","tool":"rs.printReplicationInfo()","note":"oplog 윈도는 oplog가 커버하는 시간 범위로, secondary가 접속이 끊긴 채로 얼마나 있어도 재동기화할 수 있는지 결정한다. 윈도가 좁아지면 뒤처진 멤버는 전체 resync가 필요하다. 쓰기 부하가 늘면 윈도가 짧아지므로 그에 맞게 oplog 크기를 늘린다.","tags":["oplog","replication","recovery"]},{"metric":"가장 오래 실행 중인 쿼리","warn":"> 30 s","crit":"> 120 s","tool":"db.currentOp() / slow query log","note":"장시간 실행 쿼리는 리소스를 소모하고 락을 보유하여 다른 작업에 영향을 준다. 대개 누락된 인덱스 때문에 컬렉션 스캔(COLLSCAN)을 하는 쿼리에서 비롯된다. profiler와 currentOp로 느린 작업을 찾고 적절한 인덱스를 추가한다.","tags":["query","performance","index"]},{"metric":"CPU 사용률","warn":"> 80%","crit":"> 90%","tool":"node_exporter / mongostat","note":"지속적으로 높은 CPU는 인덱스 없는 쿼리, 무거운 aggregation 작업, 과도한 접속에서 비롯될 수 있다. 짧은 피크는 정상이지만 지속적인 포화는 작업 대기열을 만든다. profiler로 CPU를 가장 많이 쓰는 쿼리를 식별해 최적화한다.","tags":["cpu","load"]},{"metric":"디스크 사용량 (dbPath)","warn":"> 85%","crit":"> 95%","tool":"df / node_exporter","note":"데이터 디렉터리가 가득 차면 MongoDB가 쓰기를 할 수 없고 손상으로 이어질 수 있다. WiredTiger 압축이 도움이 되지만 journal과 증가하는 컬렉션이 디스크를 빠르게 채울 수 있다. 용량 계획을 세우고 TTL 인덱스로 불필요한 데이터를 자동 삭제한다.","tags":["disk","capacity"]},{"metric":"마지막 성공 백업 경과 시간","warn":"> 24 hours","crit":"> 48 hours","tool":"mongodump / Ops Manager / 스냅샷 기록","note":"백업 경과 시간은 되돌아갈 수 있는 최신 시점과 RPO를 결정한다. oplog와 결합하면 시점 복구가 가능하다. 백업 생성만으로는 충분하지 않으며 정기적인 복원 테스트로 검증하고, sharded cluster에서는 일관된 스냅샷이 생성되는지 확인한다.","tags":["backup","recovery","rpo"]}]},"reference/alerts-redis.json":{"engineId":"redis","engine":"Redis","order":6,"alerts":[{"metric":"메모리 사용량 (used_memory vs maxmemory)","warn":"> 80%","crit":"> 95%","tool":"INFO memory / redis_exporter","note":"used_memory 대 maxmemory 비율은 Redis 메모리가 얼마나 사용 중인지 보여 준다. 한도에 도달하면 maxmemory-policy에 따라 키가 축출되거나 쓰기가 거부된다. 예기치 않은 OOM과 축출 파동을 막기 위해 메모리 증가를 추적한다.","tags":["memory","maxmemory","capacity"]},{"metric":"keyspace 히트율","warn":"< 90%","crit":"< 80%","tool":"INFO stats (keyspace_hits/misses)","note":"히트율은 요청된 키 중 캐시에서 찾은 비율이며 keyspace_hits와 keyspace_misses로 계산한다. 비율이 낮으면 비효율적인 캐시 사용, 잘못된 TTL, 콜드 캐시를 가리킨다. 캐시 워크로드에서는 이 비율이 가능한 한 높아야 한다.","tags":["cache","hit","performance"]},{"metric":"축출된 키(evicted_keys)","warn":"> 1 / s","crit":"> 100 / s","tool":"INFO stats (evicted_keys)","note":"evicted_keys는 메모리가 가득 차서 maxmemory-policy에 따라 제거된 키 수다. 지속적인 축출은 메모리가 부족하고 중요한 데이터가 손실될 수 있음을 보여 준다. 메모리를 증설하거나 TTL 전략 또는 데이터 모델을 재검토하여 축출을 줄인다.","tags":["memory","eviction","maxmemory"]},{"metric":"차단된 클라이언트 (blocked_clients)","warn":"> 50","crit":"> 200","tool":"INFO clients (blocked_clients)","note":"blocked_clients는 BLPOP/BRPOP/WAIT 같은 블로킹 명령으로 대기 중인 클라이언트 수다. 예상보다 높은 값은 consumer가 따라가지 못하거나 큐 적체가 늘고 있음을 가리킨다. 큐 기반 워크로드에서는 producer/consumer 균형을 확인한다.","tags":["client","blocking","queue"]},{"metric":"메모리 단편화 비율 (mem_fragmentation_ratio)","warn":"> 1.5","crit":"> 2.0","tool":"INFO memory","note":"단편화 비율은 운영체제로부터 얻은 메모리를 Redis가 사용하는 메모리로 나눈 값이다. 1.5 초과는 과도한 단편화를, 1.0 미만은 메모리가 swap으로 넘치고 있음을 나타내며 매우 위험하다. activedefrag를 활성화하거나 재시작하여 단편화를 줄일 수 있다.","tags":["memory","fragmentation"]},{"metric":"master 링크 상태 (master_link_status)","warn":"down (< 10 s)","crit":"down (> 30 s)","tool":"INFO replication (master_link_status)","note":"master_link_status는 replica의 master 접속 상태(up/down)를 보여 준다. 'down'은 복제가 끊겨 replica가 오래된 데이터를 제공하고 있음을 의미한다. 장시간 끊기면 전체 resync가 필요하므로 네트워크와 master 상태를 즉시 확인한다.","tags":["replication","master","connection"]},{"metric":"복제 지연 (master_repl_offset 차이)","warn":"> 1 MB","crit":"> 10 MB","tool":"INFO replication (offset diff)","note":"master와 replica offset의 차이는 replica가 얼마나 뒤처졌는지 바이트 단위로 보여 준다. 차이가 크면 페일오버 시 데이터 손실 위험과 읽기 replica의 불일치를 의미한다. 네트워크 지연과 replica 부하를 조사하여 차이가 줄어드는지 확인한다.","tags":["replication","lag","offset"]},{"metric":"마지막 RDB bgsave 상태 (rdb_last_bgsave_status)","warn":"err (마지막 시도)","crit":"err (> 24 hours)","tool":"INFO persistence","note":"rdb_last_bgsave_status는 마지막 백그라운드 스냅샷이 성공(ok)했는지 보여 준다. bgsave 실패는 영속성이 동작하지 않아 재시작 시 데이터 손실 위험이 있음을 의미한다. 대개 디스크 가득 참이나 fork에 필요한 메모리 부족이 원인이다.","tags":["backup","persistence","rdb"]},{"metric":"접속 사용률 (connected_clients)","warn":"> 80% maxclients","crit":"> 90% maxclients","tool":"INFO clients / maxclients","note":"connected_clients가 maxclients 한도에 근접하면 새 접속이 거부된다. 접속 누수나 잘못 구성된 풀이 급증을 일으킬 수 있다. 애플리케이션 측에서 커넥션 풀을 올바르게 구성하고 유휴 접속을 닫는다.","tags":["connection","limit","client"]},{"metric":"순간 명령 지연","warn":"> 1 ms","crit":"> 10 ms","tool":"redis-cli --latency / LATENCY DOCTOR","note":"Redis는 단일 스레드이므로 명령 지연은 대개 마이크로초 범위다. 지연 증가는 느린 명령(KEYS, 대형 SORT), fork 중 블로킹, swap 사용에서 비롯될 수 있다. slowlog와 LATENCY 명령으로 원인을 찾는다.","tags":["latency","performance","slowlog"]},{"metric":"CPU 사용률","warn":"> 80% (단일 코어)","crit":"> 90% (단일 코어)","tool":"top / redis_exporter","note":"Redis 메인 스레드는 단일 코어를 사용하므로 한 코어가 포화되면 서버 전체가 느려진다. 높은 CPU는 비용이 큰 명령, 너무 많은 접속, Lua 스크립트에서 비롯될 수 있다. SLOWLOG로 비용이 큰 명령을 찾고 필요하면 워크로드를 여러 인스턴스로 분산한다.","tags":["cpu","load"]},{"metric":"거부된 접속 (rejected_connections)","warn":"> 0","crit":"> 10 / min","tool":"INFO stats (rejected_connections)","note":"rejected_connections는 maxclients 한도에 도달하여 거부된 접속 수다. 0보다 크면 클라이언트가 서비스를 받지 못하고 애플리케이션에서 오류가 발생하고 있다는 뜻이다. maxclients 값과 접속 누수를 즉시 조사한다.","tags":["connection","error","limit"]}]},"reference/alerts-elasticsearch.json":{"engineId":"elasticsearch","engine":"Elasticsearch","order":7,"alerts":[{"metric":"JVM heap 사용량","warn":"> 75%","crit":"> 85%","tool":"_nodes/stats/jvm / elasticsearch_exporter","note":"JVM heap 사용량은 노드의 메모리 압박을 보여 준다. 75%를 지속적으로 넘으면 잦은 GC와 OutOfMemory 가능성을 의미한다. heap이 85%를 넘으면 노드가 무응답 상태가 되거나 클러스터에서 이탈할 수 있다. heap은 서버 메모리의 약 절반, 최대 30-31 GB로 정한다.","tags":["memory","jvm","heap"]},{"metric":"미할당 샤드 수 (unassigned shards)","warn":">= 1 (replica)","crit":">= 1 (primary)","tool":"_cluster/health / _cat/shards","note":"미할당 샤드는 데이터 일부에 접근할 수 없거나 복제되지 않았음을 의미한다. 미할당 primary 샤드는 데이터 손실 위험을, 미할당 replica는 이중화 상실을 의미하며 클러스터가 yellow/red가 된다. _cluster/allocation/explain으로 원인을 찾는다.","tags":["shard","cluster","health"]},{"metric":"디스크 watermark 사용률","warn":"> 85% (low)","crit":"> 90% (high) / > 95% (flood_stage)","tool":"_nodes/stats/fs / _cat/allocation","note":"Elasticsearch는 디스크 사용량에 따라 단계적으로 조치한다. 85% low watermark에서는 노드에 새 샤드 할당을 중단하고, 90% high watermark에서는 기존 샤드를 다른 노드로 재배치하며, 95% flood_stage에서는 인덱스에 read_only_allow_delete 차단을 건다. 8.x의 대용량 디스크에서는 비율보다 max_headroom 값(low 200GB / high 150GB / flood 100GB 여유)이 우선한다. 7.4+에서는 디스크 사용량이 high watermark 아래로 떨어지면 차단이 자동 해제되지만, watermark를 초과하기 전에 디스크를 확보한다.","tags":["disk","watermark","capacity"]},{"metric":"스레드 풀 거부 작업 (rejected)","warn":"> 0 (write/search)","crit":"> 100 / min","tool":"_cat/thread_pool / _nodes/stats/thread_pool","note":"스레드 풀 큐가 가득 차면 들어오는 요청이 거부된다. write 풀의 거부는 인덱싱 손실을, search 풀의 거부는 쿼리 실패를 의미한다. 지속적인 거부는 노드가 유입 부하를 따라가지 못함을 나타낸다. bulk 크기와 동시성을 줄여 부하의 균형을 맞춘다.","tags":["threadpool","rejected","load"]},{"metric":"GC 일시 정지 시간 (old gen GC pause)","warn":"> 1 s","crit":"> 5 s","tool":"GC 로그 / _nodes/stats/jvm","note":"긴 GC 일시 정지 동안 노드는 응답할 수 없고 master와의 연결을 잃을 수 있으며, 이는 샤드 재할당을 유발한다. 잦고 긴 old generation GC는 heap 압박의 증상이다. heap 크기와 메모리를 많이 쓰는 aggregation 및 fielddata 사용을 검토한다.","tags":["jvm","gc","pause"]},{"metric":"인덱싱 지연","warn":"> 50 ms","crit":"> 200 ms","tool":"_nodes/stats/indices (index_time/index_total)","note":"평균 인덱싱 시간은 문서 쓰기 성능을 보여 주며 index_time_in_millis를 index_total로 나누어 계산한다. 값이 상승하면 디스크 IO 병목, 무거운 analysis chain, refresh 압박을 나타낸다. bulk 크기와 refresh_interval 설정을 최적화한다.","tags":["indexing","latency","performance"]},{"metric":"검색 지연","warn":"> 100 ms","crit":"> 500 ms","tool":"_nodes/stats/indices (query_time/query_total)","note":"평균 쿼리 시간은 사용자가 체감하는 검색 성능을 직접 반영하며 query_time_in_millis를 query_total로 나누어 계산한다. 값 상승은 무거운 집계, 너무 많은 샤드, 콜드 캐시에서 비롯될 수 있다. slow log를 활성화하여 비용이 큰 쿼리를 찾는다.","tags":["search","latency","performance"]},{"metric":"대기 중인 클러스터 작업 (pending tasks)","warn":"> 10","crit":"> 100","tool":"_cluster/pending_tasks","note":"pending tasks는 master 노드가 아직 적용하지 않은 cluster state 변경(mapping, 샤드 할당 등)이다. 계속 쌓이면 master가 과부하이거나 cluster state가 너무 커졌음을 나타낸다. 과도한 샤드 수를 줄여 master의 부담을 덜어 준다.","tags":["cluster","master","queue"]},{"metric":"circuit breaker 작동","warn":"> 0 (tripped)","crit":"> 10 / min","tool":"_nodes/stats/breakers","note":"circuit breaker는 작업이 과도한 메모리를 소비해 노드를 OutOfMemory로 몰아넣는 것을 막기 위해 요청을 취소한다. 잦은 작동은 무거운 집계나 대형 요청으로 인한 메모리 압박을 나타낸다. tripped 횟수를 모니터링하고 문제 쿼리를 가볍게 한다.","tags":["memory","breaker","jvm"]},{"metric":"클러스터 상태 (cluster status)","warn":"yellow","crit":"red","tool":"_cluster/health","note":"클러스터 상태는 green(모든 샤드 할당), yellow(replica 누락), red(primary 누락, 데이터 접근 불가)일 수 있다. red는 일부 데이터가 손실되었거나 접근할 수 없으므로 즉각적인 조치가 필요하다. yellow는 이중화 상실을 나타내며 무시해서는 안 된다.","tags":["cluster","health","shard"]},{"metric":"CPU 사용률","warn":"> 80%","crit":"> 90%","tool":"_nodes/stats/os / node_exporter","note":"지속적으로 높은 CPU는 무거운 쿼리, 과도한 인덱싱, merge 작업에서 비롯될 수 있다. 짧은 피크는 정상이지만 지속적인 포화는 쿼리 및 인덱싱 대기열을 만든다. hot threads API(_nodes/hot_threads)로 CPU를 가장 많이 쓰는 작업을 식별한다.","tags":["cpu","load"]},{"metric":"마지막 성공 스냅샷 경과 시간","warn":"> 24 hours","crit":"> 48 hours","tool":"_snapshot/_status / SLM","note":"스냅샷 경과 시간은 되돌아갈 수 있는 최신 시점과 RPO를 결정한다. Snapshot Lifecycle Management(SLM)로 자동 정기 백업을 보장해야 한다. 실패하거나 누락된 스냅샷을 알아차리지 못하면 재해 시 데이터를 복구할 수 없으므로 스냅샷 상태를 정기적으로 확인한다.","tags":["backup","snapshot","rpo"]}]},"cloud/cloud-ops-1.json":{"category":"연결 · 백업 · 복제","categoryId":"cloud-core","order":1,"providers":[{"id":"aws-rds","name":"AWS RDS","color":"#B85C00"},{"id":"aws-aurora","name":"Aurora","color":"#7B3FE4"},{"id":"gcp-cloudsql","name":"Cloud SQL","color":"#C5372C"},{"id":"azure-sql","name":"Azure SQL","color":"#0067B8"},{"id":"mongodb-atlas","name":"Atlas","color":"#00843D"}],"concepts":[{"id":"list-instances","title":"인스턴스 / 데이터베이스 목록","aliases":["list","instances","list all"],"providers":{"aws-rds":{"command":"aws rds describe-db-instances --query 'DBInstances[].DBInstanceIdentifier'","note":"모든 RDS 인스턴스의 식별자를 나열한다. 리전을 지정하려면 --region을 추가한다.","danger":"safe"},"aws-aurora":{"command":"aws rds describe-db-clusters --query 'DBClusters[].DBClusterIdentifier'","note":"Aurora 클러스터 식별자를 나열한다. 클러스터 멤버는 DBClusterMembers 필드에서 확인한다.","danger":"safe"},"gcp-cloudsql":{"command":"gcloud sql instances list","note":"프로젝트의 모든 Cloud SQL 인스턴스를 나열한다. 프로젝트를 지정하려면 --project를 추가한다.","danger":"safe"},"azure-sql":{"command":"az sql db list -g <rg> -s <server>","note":"지정한 리소스 그룹(-g)과 논리 서버(-s)의 데이터베이스를 나열한다. 서버 목록은 'az sql server list'로 확인한다.","danger":"safe"},"mongodb-atlas":{"command":"atlas clusters list","note":"Atlas 프로젝트의 모든 클러스터를 나열한다. 프로젝트를 지정하려면 --projectId를 추가한다.","danger":"safe"}}},{"id":"connect","title":"인스턴스에 접속 (endpoint 확인 + 접속)","aliases":["connect","endpoint","connection string","connect to"],"providers":{"aws-rds":{"command":"aws rds describe-db-instances --db-instance-identifier <id> --query 'DBInstances[0].Endpoint.Address'","note":"인스턴스 접속 주소(host)를 반환한다. 포트는 Endpoint.Port에서 확인하고 psql/mysql 클라이언트로 접속한다.","danger":"safe"},"aws-aurora":{"command":"aws rds describe-db-clusters --db-cluster-identifier <id> --query 'DBClusters[0].Endpoint'","note":"클러스터 writer endpoint를 반환한다. 읽기 전용 접근에는 ReaderEndpoint 필드를 사용한다.","danger":"safe"},"gcp-cloudsql":{"command":"gcloud sql instances describe <instance> --format='value(ipAddresses[].ipAddress)'","note":"인스턴스 IP 주소를 반환한다. 보안 접속에는 Cloud SQL Auth Proxy를 권장한다.","danger":"safe"},"azure-sql":{"command":"az sql db show-connection-string -s <server> -n <db> -c ado.net","note":"지정한 클라이언트 유형(-c: ado.net, jdbc, odbc, php 등)의 connection string을 생성한다. user/password는 직접 채운다.","danger":"safe"},"mongodb-atlas":{"command":"atlas clusters connectionStrings describe <clusterName>","note":"클러스터의 standard 및 SRV connection string을 반환한다. IP access list 항목과 데이터베이스 사용자가 필요하다.","danger":"safe"}}},{"id":"take-snapshot","title":"수동 백업 / 스냅샷 생성","aliases":["snapshot","backup","backup"],"providers":{"aws-rds":{"command":"aws rds create-db-snapshot --db-instance-identifier <id> --db-snapshot-identifier <snap-id>","note":"인스턴스의 수동 스냅샷을 생성한다(수동 삭제 전까지 보존). 잠시 I/O가 중단될 수 있다.","danger":"caution"},"aws-aurora":{"command":"aws rds create-db-cluster-snapshot --db-cluster-identifier <id> --db-cluster-snapshot-identifier <snap-id>","note":"Aurora 클러스터의 수동 스냅샷을 생성한다. Aurora 백업은 연속적이므로 이는 고정된 사본이다.","danger":"caution"},"gcp-cloudsql":{"command":"gcloud sql backups create --instance=<instance>","note":"인스턴스의 온디맨드 백업을 생성한다. 작업 중 잠시 성능에 영향이 있을 수 있다.","danger":"caution"},"azure-sql":{"command":"az sql db export -s <server> -n <db> -u <user> -p <pwd> --admin-user <adm> --admin-password <pwd> --storage-key <key> --storage-key-type StorageAccessKey --storage-uri https://<acct>.blob.core.windows.net/<container>/<file>.bacpac","note":"Azure SQL은 자동으로 백업된다. 이동 가능한 수동 백업이 필요하면 데이터베이스를 BACPAC으로 export한다. 시점 복원은 별도 기능이다.","danger":"caution"},"mongodb-atlas":{"command":"atlas backups snapshots create <clusterName>","note":"클러스터의 온디맨드 백업 스냅샷을 시작한다. Cloud Backup이 활성화되어 있어야 한다.","danger":"caution"}}},{"id":"restore-pitr","title":"특정 시점으로 복원 (point-in-time restore)","aliases":["pitr","restore","point-in-time","roll back"],"providers":{"aws-rds":{"command":"aws rds restore-db-instance-to-point-in-time --source-db-instance-identifier <src> --target-db-instance-identifier <new> --restore-time 2026-07-03T10:00:00Z","note":"소스 인스턴스로부터 지정 시점의 새 인스턴스를 생성한다. 기존 인스턴스를 덮어쓰지 않는다. 최신 시점으로 복원하려면 --use-latest-restorable-time을 사용한다.","danger":"caution"},"aws-aurora":{"command":"aws rds restore-db-cluster-to-point-in-time --source-db-cluster-identifier <src> --db-cluster-identifier <new> --restore-to-time 2026-07-03T10:00:00Z","note":"Aurora 클러스터를 지정 시점으로 새 클러스터로 복원한다. 이후 create-db-instance로 인스턴스를 추가해야 한다.","danger":"caution"},"gcp-cloudsql":{"command":"gcloud sql instances clone <src> <new> --point-in-time '2026-07-03T10:00:00Z'","note":"소스 인스턴스의 지정 시점 클론을 새 인스턴스로 생성한다. 소스에서 Binary/WAL 로깅이 활성화되어 있어야 한다.","danger":"caution"},"azure-sql":{"command":"az sql db restore -g <rg> -s <server> -n <db> --dest-name <new-db> --time '2026-07-03T10:00:00'","note":"데이터베이스를 지정 시점으로 새 데이터베이스로 복원한다. 소스 데이터베이스는 보존된다. 시간은 UTC다.","danger":"caution"},"mongodb-atlas":{"command":"atlas backups restores start pointInTime --clusterName <cluster> --pointInTimeUTCMillis <ms> --targetClusterName <target> --targetProjectId <projId>","note":"Continuous Cloud Backup을 사용하여 지정 UTC 시점으로 복원을 시작한다. 대상 클러스터를 덮어쓰므로 주의해서 진행한다.","danger":"danger"}}},{"id":"read-replica","title":"읽기 replica 생성","aliases":["replica","read replica","read replica"],"providers":{"aws-rds":{"command":"aws rds create-db-instance-read-replica --db-instance-identifier <replica-id> --source-db-instance-identifier <src>","note":"소스 인스턴스로부터 비동기 읽기 replica를 생성한다. 리전 간 복사에는 --source-region을 사용한다.","danger":"caution"},"aws-aurora":{"command":"aws rds create-db-instance --db-instance-identifier <reader-id> --db-cluster-identifier <cluster> --engine aurora-postgresql --db-instance-class db.r6g.large","note":"Aurora에서 reader는 클러스터에 추가된 인스턴스이며 클러스터 스토리지를 공유한다. engine 값은 클러스터와 맞춘다(aurora-mysql / aurora-postgresql).","danger":"caution"},"gcp-cloudsql":{"command":"gcloud sql instances create <replica> --master-instance-name=<src> --region=<region>","note":"primary(master) 인스턴스로부터 읽기 replica를 생성한다. 소스에서 바이너리 로깅이 활성화되어 있어야 한다.","danger":"caution"},"azure-sql":{"command":"az sql db replica create -g <rg> -s <server> -n <db> --partner-server <target-server>","note":"다른 서버에 데이터베이스의 active geo-replication 읽기 가능 secondary를 생성한다. 대상 서버가 존재해야 한다.","danger":"caution"},"mongodb-atlas":{"command":"atlas clusters update <clusterName> --file cluster.json","note":"Atlas 클러스터는 이미 복제된 replica set이다. 추가 읽기 용량이 필요하면 replicationSpecs 아래에 readOnly 또는 analytics 노드를 추가하고 업데이트한다.","danger":"caution"}}},{"id":"failover","title":"페일오버","aliases":["failover","fail over","switch to standby"],"providers":{"aws-rds":{"command":"aws rds reboot-db-instance --db-instance-identifier <id> --force-failover","note":"Multi-AZ 인스턴스에서 standby로의 페일오버를 유발하는 재부팅을 강제한다. 짧은 다운타임이 예상된다.","danger":"danger"},"aws-aurora":{"command":"aws rds failover-db-cluster --db-cluster-identifier <cluster> --target-db-instance-identifier <reader>","note":"Aurora 클러스터의 reader를 새 writer로 승격한다. 대상을 지정하지 않으면 적절한 replica가 선택된다. 짧은 다운타임이 예상된다.","danger":"danger"},"gcp-cloudsql":{"command":"gcloud sql instances failover <instance>","note":"HA(고가용성) 인스턴스를 standby 영역으로 페일오버한다. HA가 활성화된 인스턴스에서만 동작하며 다운타임이 발생한다.","danger":"danger"},"azure-sql":{"command":"az sql failover-group set-primary -g <rg> -s <secondary-server> -n <fog-name>","note":"auto-failover group에서 primary 역할을 secondary 서버로 이동한다(계획된 페일오버). --allow-data-loss로 강제할 수 있으나 데이터 손실 위험이 있다.","danger":"danger"},"mongodb-atlas":{"command":"atlas clusters failover <clusterName>","note":"Atlas는 자동으로 페일오버한다. 이 명령은 테스트용 수동 페일오버를 유발한다. primary 노드가 바뀌므로 애플리케이션이 재접속해야 한다.","danger":"danger"}}}]},"cloud/cloud-ops-2.json":{"category":"구성 · 확장 · 모니터링","categoryId":"cloud-config","order":2,"providers":[{"id":"aws-rds","name":"AWS RDS","color":"#B85C00"},{"id":"aws-aurora","name":"Aurora","color":"#7B3FE4"},{"id":"gcp-cloudsql","name":"Cloud SQL","color":"#C5372C"},{"id":"azure-sql","name":"Azure SQL","color":"#0067B8"},{"id":"mongodb-atlas","name":"Atlas","color":"#00843D"}],"concepts":[{"id":"scale-instance","title":"인스턴스 크기 변경 (수직 확장)","aliases":["scale","size","resize","scale up","vertical"],"providers":{"aws-rds":{"command":"aws rds modify-db-instance --db-instance-identifier <id> --db-instance-class db.r6g.xlarge --apply-immediately","note":"--apply-immediately는 변경을 즉시 적용하고 인스턴스를 재시작한다(짧은 다운타임). 이 플래그가 없으면 다음 유지보수 윈도에 적용된다. Multi-AZ 인스턴스에서는 다운타임이 페일오버 시간 정도로 줄어든다.","danger":"caution"},"aws-aurora":{"command":"aws rds modify-db-instance --db-instance-identifier <writer-or-reader-id> --db-instance-class db.r6g.2xlarge --apply-immediately","note":"Aurora에서는 각 인스턴스(writer와 reader)를 따로 확장하며, 클러스터가 아니라 인스턴스 크기가 바뀐다. 다운타임을 최소화하려면 reader를 먼저 확장한 뒤 페일오버하여 writer를 교체한다. Serverless v2에서는 min/max ACU로 확장한다.","danger":"caution"},"gcp-cloudsql":{"command":"gcloud sql instances patch <id> --tier db-custom-4-16384","note":"커스텀 머신 유형은 db-custom-CPU-MEMORY 형식으로 지정한다(예: 4 vCPU, 16384 MB RAM). 변경 시 인스턴스가 재시작되어 짧은 다운타임이 발생한다. 사전 정의된 tier(예: db-n1-standard-4)도 사용할 수 있다.","danger":"caution"},"azure-sql":{"command":"az sql db update -g <rg> -s <server> -n <db> --service-objective S3","note":"DTU 모델에서는 S3/P2 같은 service objective를, vCore 모델에서는 --edition GeneralPurpose --family Gen5 --capacity 4를 사용한다. 확장은 온라인으로 수행되며 작업 종료 무렵 몇 초간 접속이 끊길 수 있다.","danger":"caution"},"mongodb-atlas":{"command":"atlas clusters update <name> --tier M30","note":"Atlas는 replica set 노드를 rolling 방식으로 크기 변경하므로 다운타임이 없다. --projectId 플래그나 기본 프로젝트가 필요하다. 다운그레이드 시에는 디스크와 메모리 한도에 유의한다.","danger":"caution"}}},{"id":"parameter-group","title":"파라미터 변경 (parameter group / flags / server config)","aliases":["parameter","parameter group","flags","config","setting"],"providers":{"aws-rds":{"command":"aws rds modify-db-parameter-group --db-parameter-group-name <pg> --parameters \"ParameterName=max_connections,ParameterValue=500,ApplyMethod=pending-reboot\"","note":"동적 파라미터에는 ApplyMethod=immediate를, 정적 파라미터에는 pending-reboot를 사용한다. parameter group이 인스턴스에 연결되어 있어야 하며 정적 파라미터 변경은 재부팅 후 적용된다. 그룹 생성에는 create-db-parameter-group을 사용한다.","danger":"caution"},"aws-aurora":{"command":"aws rds modify-db-cluster-parameter-group --db-cluster-parameter-group-name <cpg> --parameters \"ParameterName=innodb_lock_wait_timeout,ParameterValue=100,ApplyMethod=immediate\"","note":"Aurora에서 클러스터 전체 파라미터는 DB cluster parameter group으로, 인스턴스별 파라미터는 DB parameter group(modify-db-parameter-group)으로 관리한다. 정적 파라미터는 클러스터 전체의 재부팅이 필요하다.","danger":"caution"},"gcp-cloudsql":{"command":"gcloud sql instances patch <id> --database-flags max_connections=500,log_min_duration_statement=1000","note":"--database-flags는 모든 플래그를 한 번에 설정한다. 목록에 없는 기존 플래그는 초기화되므로 모두 함께 지정한다. 일부 플래그는 인스턴스 재시작이 필요하다. 현재 플래그는 'gcloud sql instances describe <id>'로 확인한다.","danger":"caution"},"azure-sql":{"command":"az sql db configure -g <rg> -s <server> -n <db> ...   # DB-level settings via T-SQL: ALTER DATABASE SCOPED CONFIGURATION SET MAXDOP = 4;","note":"Azure SQL에서는 기존 server config 대신 주로 T-SQL로 설정을 적용한다: ALTER DATABASE SCOPED CONFIGURATION ... 서버 수준의 sp_configure는 제한적이다. elastic pool/데이터베이스 옵션에는 'az sql db update' 플래그를 사용한다.","danger":"caution"},"mongodb-atlas":{"command":"atlas clusters advancedSettings update <name> --oplogMinRetentionHours 24 --oplogSizeMB 20480","note":"Atlas에서는 저수준 MongoDB 프로세스 인수를 'advancedSettings'(oplog, 전문 검색, 인덱스 깊이 등)로 제한적으로만 설정할 수 있다. 대부분의 서버 파라미터는 Atlas가 관리하며 직접 변경할 수 없다.","danger":"caution"}}},{"id":"monitoring","title":"메트릭 및 성능 확인","aliases":["monitoring","metric","observability","performance","insights"],"providers":{"aws-rds":{"command":"aws cloudwatch get-metric-statistics --namespace AWS/RDS --metric-name CPUUtilization --dimensions Name=DBInstanceIdentifier,Value=<id> --start-time 2026-07-05T00:00:00Z --end-time 2026-07-05T01:00:00Z --period 300 --statistics Average","note":"기본 메트릭(CPU, 접속, IOPS, 지연)은 CloudWatch AWS/RDS 네임스페이스에 있다. 쿼리 수준 분석에는 Performance Insights를 활성화한다: 'aws pi get-resource-metrics --service-type RDS ...' 또는 콘솔의 Performance Insights 대시보드를 사용한다.","danger":"safe"},"aws-aurora":{"command":"aws cloudwatch get-metric-statistics --namespace AWS/RDS --metric-name AuroraReplicaLag --dimensions Name=DBInstanceIdentifier,Value=<reader-id> --start-time 2026-07-05T00:00:00Z --end-time 2026-07-05T01:00:00Z --period 60 --statistics Maximum","note":"Aurora 전용 메트릭(AuroraReplicaLag, VolumeReadIOPs, CommitLatency, buffer cache hit)도 AWS/RDS 네임스페이스에 있다. Aurora에는 Performance Insights를 권장하며 top SQL과 대기 이벤트 분석을 제공한다.","danger":"safe"},"gcp-cloudsql":{"command":"gcloud monitoring time-series list --filter='metric.type=\"cloudsql.googleapis.com/database/cpu/utilization\" AND resource.labels.database_id=\"<project>:<id>\"' --interval-start-time=2026-07-05T00:00:00Z --interval-end-time=2026-07-05T01:00:00Z","note":"Cloud SQL 메트릭은 Cloud Monitoring의 cloudsql.googleapis.com/... 아래에 있다. 쿼리 수준 분석에는 Cloud SQL Query Insights(콘솔 또는 API)를 활성화한다. 가장 느린 쿼리와 대기 이벤트를 보여 준다.","danger":"safe"},"azure-sql":{"command":"az monitor metrics list --resource <db-resource-id> --metric \"cpu_percent\" --interval PT5M --start-time 2026-07-05T00:00:00Z --end-time 2026-07-05T01:00:00Z","note":"cpu_percent, dtu_consumption_percent, storage_percent, connection_successful 같은 메트릭은 Azure Monitor에서 제공된다. 쿼리 성능에는 Query Performance Insight와 Intelligent Insights(Azure SQL Analytics) 대시보드를 사용한다.","danger":"safe"},"mongodb-atlas":{"command":"atlas metrics processes <host>:<port> --granularity PT5M --period P1D --type SYSTEM_CPU_USER,QUERY_TARGETING_SCANNED_OBJECTS_PER_RETURNED","note":"프로세스 수준 측정값은 'atlas metrics processes'로, 디스크 측정값은 'atlas metrics disks'로 가져온다. 느린 쿼리 권장 사항에는 Performance Advisor를 사용한다: 'atlas performanceAdvisor slowQueryLogs list' 및 'atlas performanceAdvisor suggestedIndexes list'.","danger":"safe"}}},{"id":"maintenance-window","title":"유지보수 윈도 및 업데이트 관리","aliases":["maintenance","maintenance window","window","update","patch"],"providers":{"aws-rds":{"command":"aws rds modify-db-instance --db-instance-identifier <id> --preferred-maintenance-window Sun:03:00-Sun:04:00   # apply pending maintenance now: aws rds apply-pending-maintenance-action --resource-identifier <arn> --apply-action system-update --opt-in-type immediate","note":"윈도는 UTC 기준 ddd:hh24:mi-ddd:hh24:mi 형식이며 최소 30분이어야 한다. 보류 중인 유지보수는 'aws rds describe-pending-maintenance-actions'로 나열하고 apply-pending-maintenance-action으로 즉시 또는 다음 윈도에 적용한다.","danger":"caution"},"aws-aurora":{"command":"aws rds modify-db-cluster --db-cluster-identifier <cluster> --preferred-maintenance-window Sun:03:00-Sun:04:00","note":"Aurora에서 클러스터 수준 유지보수 윈도는 modify-db-cluster로, 인스턴스 수준 윈도는 modify-db-instance로 설정한다. 보류 중인 엔진/OS 업데이트는 apply-pending-maintenance-action으로 관리하며 엔진 버전 업그레이드에는 다운타임이 필요할 수 있다.","danger":"caution"},"gcp-cloudsql":{"command":"gcloud sql instances patch <id> --maintenance-window-day SUN --maintenance-window-hour 3 --maintenance-release-channel production","note":"요일(MON..SUN), 시간(0-23, 인스턴스 로컬/UTC 시간), 릴리스 채널(production 또는 preview)을 설정한다. --deny-maintenance-period-* 플래그로 유지보수를 수행하지 않는 기간도 정의할 수 있다.","danger":"caution"},"azure-sql":{"command":"az sql db update -g <rg> -s <server> -n <db> --maint-config-id SQL_Default   # or custom: /subscriptions/<sub>/providers/Microsoft.Maintenance/publicMaintenanceConfigurations/SQL_<Region>_DB_1","note":"Azure SQL 유지보수 윈도는 사전 정의된 Maintenance Configuration(기본값 대신 평일/주말 저녁 윈도)을 할당하여 관리한다. 구성 목록은 'az maintenance public-configuration list'로 확인한다.","danger":"caution"},"mongodb-atlas":{"command":"atlas maintenanceWindows update --dayOfWeek 1 --hourOfDay 3 --projectId <projectId>   # defer: atlas maintenanceWindows defer","note":"dayOfWeek 1=일요일 ... 7=토요일, hourOfDay 0-23 (UTC). 보류 중인 유지보수를 1주 연기하려면 'atlas maintenanceWindows defer'를, 즉시 시작하려면 'atlas maintenanceWindows start'를 사용한다.","danger":"caution"}}},{"id":"encryption","title":"암호화(at-rest) 상태 확인/구성","aliases":["encryption","encryption at rest","at-rest","kms","tde","cmek"],"providers":{"aws-rds":{"command":"aws rds describe-db-instances --db-instance-identifier <id> --query 'DBInstances[0].[StorageEncrypted,KmsKeyId]'   # at creation: aws rds create-db-instance ... --storage-encrypted --kms-key-id <kms-arn>","note":"RDS at-rest 암호화는 인스턴스 생성 시에만 활성화할 수 있다. 기존 비암호화 인스턴스에 적용하려면 스냅샷의 암호화 사본을 만들어 그로부터 복원한다. 키는 AWS KMS가 관리한다.","danger":"caution"},"aws-aurora":{"command":"aws rds describe-db-clusters --db-cluster-identifier <cluster> --query 'DBClusters[0].[StorageEncrypted,KmsKeyId]'   # at creation: aws rds create-db-cluster ... --storage-encrypted --kms-key-id <kms-arn>","note":"Aurora에서 암호화는 클러스터 수준에서 생성 시에만 설정한다. 기존 비암호화 클러스터를 암호화하려면 스냅샷을 암호화 사본으로 복사해 복원한다. 모든 인스턴스와 백업은 같은 KMS 키를 사용한다.","danger":"caution"},"gcp-cloudsql":{"command":"gcloud sql instances create <id> --disk-encryption-key projects/<project>/locations/<region>/keyRings/<ring>/cryptoKeys/<key>   # to view: gcloud sql instances describe <id> --format='value(diskEncryptionConfiguration.kmsKeyName)'","note":"Cloud SQL은 기본적으로 Google 관리 키로 암호화된다. 고객 관리 키(CMEK)는 인스턴스 생성 시에만 --disk-encryption-key로 지정할 수 있다. Cloud SQL 서비스 계정에 키에 대한 encrypt/decrypt 권한을 부여해야 한다.","danger":"caution"},"azure-sql":{"command":"az sql db tde set -g <rg> -s <server> -d <db> --status Enabled   # or T-SQL: ALTER DATABASE <db> SET ENCRYPTION ON;   # status: az sql db tde show -g <rg> -s <server> -d <db>","note":"Azure SQL에서 at-rest 암호화는 Transparent Data Encryption(TDE)으로 제공되며 새 데이터베이스에서 기본 활성화된다. 자체 키를 사용하려면 서버 수준에서 'az sql server tde-key set'(Azure Key Vault / BYOK)을 구성한다.","danger":"caution"},"mongodb-atlas":{"command":"atlas clusters describe <name> --output json | jq '.encryptionAtRestProvider'   # enable BYOK: atlas encryptionAtRest enable AWS --awsKmsMasterKeyID <keyId> --awsKmsRegion <region> --awsKmsRoleID <roleId>","note":"Atlas에서 데이터는 기본적으로 at-rest 암호화된다. 추가로 AWS KMS / Azure Key Vault / GCP KMS를 통한 고객 키(BYOK) envelope 암호화를 'atlas encryptionAtRest'로 프로젝트 전체에 활성화하고 클러스터에 적용한다.","danger":"caution"}}},{"id":"storage-autoscale","title":"스토리지 확장 / 자동 확장 활성화","aliases":["storage","disk space","autoscale","grow","disk"],"providers":{"aws-rds":{"command":"aws rds modify-db-instance --db-instance-identifier <id> --allocated-storage 200 --max-allocated-storage 1000 --apply-immediately","note":"--allocated-storage는 현재 크기를, --max-allocated-storage는 스토리지 자동 확장의 상한을 설정한다. 스토리지 확장은 온라인으로 수행되지만 스토리지 변경 간에 최소 6시간을 기다려야 하며 스토리지는 축소할 수 없다.","danger":"caution"},"aws-aurora":{"command":"# Aurora storage is automatic; no manual setting required\naws rds describe-db-clusters --db-cluster-identifier <cluster> --query 'DBClusters[0].AllocatedStorage'","note":"Aurora 스토리지는 완전 자동으로 확장되며(10 GB 단위, 엔진에 따라 최대 128 TB) 사용에 따라 커진다. --allocated-storage / --max-allocated-storage는 설정하지 않는다. 사용량만 모니터링하면 되며 크기 관리는 필요 없다.","danger":"safe"},"gcp-cloudsql":{"command":"gcloud sql instances patch <id> --storage-auto-increase --storage-size 200GB   # upper limit: --storage-auto-increase-limit 1000","note":"--storage-auto-increase는 디스크가 가득 차갈 때 자동 증가를 활성화하고, --storage-auto-increase-limit은 상한을 설정한다(0 = 무제한). 스토리지는 늘릴 수만 있고 줄일 수 없으므로 초기 크기와 한도를 신중하게 정한다.","danger":"caution"},"azure-sql":{"command":"az sql db update -g <rg> -s <server> -n <db> --max-size 500GB","note":"Azure SQL에서 최대 데이터베이스 크기는 --max-size로 설정하며 데이터베이스는 할당된 공간 내에서 자동으로 커진다. 최대 크기는 선택한 서비스 tier/vCore 한도에 따라 다르다. Hyperscale tier에서는 스토리지가 자동으로 거의 무제한 확장된다.","danger":"caution"},"mongodb-atlas":{"command":"atlas clusters update <name> --diskSizeGB 200   # for autoscaling, diskGBEnabled=true via extended configuration (advancedSettings / API)","note":"디스크 크기는 --diskSizeGB로 늘린다. Atlas에서 스토리지 자동 확장(Auto-scale storage)은 M10 이상 클러스터에서 기본 활성화되며 디스크가 90% 차면 자동으로 커진다. 활성화/비활성화는 클러스터 구성(autoScaling.diskGBEnabled)에서 관리한다.","danger":"caution"}}}]},"guide/best-practices.json":{"topics":[{"id":"indexing-strategy","title":"인덱싱 전략","category":"성능","summary":"인덱스는 읽기 쿼리를 빠르게 하지만 쓰기와 스토리지 비용을 더한다. 필요와 측정에 근거하여 추가한다.","points":["WHERE, JOIN, ORDER BY 컬럼을 대상으로 한다. 복합 인덱스에서는 선택도가 가장 높은 컬럼을 맨 앞에 둔다(leftmost-prefix 규칙).","커버링 인덱스를 사용한다: 쿼리에 필요한 모든 컬럼을 인덱스에 포함하여 테이블 접근(lookup)을 없앤다.","사용되지 않거나 중복된 인덱스(다른 인덱스의 중복 또는 prefix)를 식별해 삭제한다. 모든 인덱스는 INSERT/UPDATE/DELETE를 느리게 한다.","카디널리티가 낮은 컬럼(예: 성별, boolean)에 단독 B-tree 인덱스를 피한다. 필요하면 partial/filtered 인덱스를 사용한다.","EXPLAIN/EXPLAIN ANALYZE로 인덱스가 실제로 사용되는지 확인한다. 함수나 형 변환은 인덱스를 무력화한다(WHERE UPPER(name)처럼)."],"level":"junior"},{"id":"normalization","title":"정규화 vs 비정규화","category":"설계","summary":"정규화는 데이터 무결성과 중복 없음을 보장하며, 비정규화는 읽기 성능을 위한 의도적인 트레이드오프다.","points":["OLTP 시스템에서는 기본적으로 3NF를 목표로 한다. 데이터 중복과 갱신 이상을 방지한다.","측정된 성능 병목이 있을 때만 비정규화하고 그 근거를 남긴다.","비정규화된 데이터의 일관성은 트리거, materialized view, 애플리케이션 계층으로 보장한다.","리포팅/분석 워크로드에는 별도의 비정규화 스키마(star schema)나 읽기 replica를 고려한다.","섣부른 비정규화는 기술 부채다. 먼저 올바른 인덱스와 쿼리로 문제를 해결해 본다."],"level":"senior"},{"id":"data-types","title":"적절한 데이터 타입 선택","category":"설계","summary":"정확하고 가능한 한 작은 데이터 타입은 스토리지, 인덱스 크기, 성능을 직접 개선한다.","points":["요구를 충족하는 가장 작은 타입을 선택한다: 나이는 SMALLINT, 금액은 DECIMAL/NUMERIC(FLOAT는 절대 사용하지 않음).","시간 데이터는 항상 time zone과 함께 저장한다(TIMESTAMPTZ / UTC). 로컬 시간 저장을 피한다.","고정된 제한 값 집합에는 ENUM이나 lookup 테이블을 사용한다. 자유 텍스트를 허용하지 말고 제약을 건다.","가변 길이 텍스트는 불필요하게 큰 VARCHAR(255) 대신 실제 필요에 맞게 크기를 정하거나 TEXT를 사용한다.","UUID를 PK로 사용하면 인덱스 단편화에 유의한다. 순차 UUID(v7)나 BIGINT identity를 고려한다.","NULL 의미를 의도적으로 정한다. '알 수 없음'과 '비어 있음'의 차이를 설계에서 명확히 한다."],"level":"junior"},{"id":"naming-conventions","title":"명명 규칙","category":"설계","summary":"일관된 명명은 가독성, 유지보수, 팀 커뮤니케이션을 쉽게 한다. 처음부터 표준을 정한다.","points":["하나의 규칙을 정해 모든 곳에 적용한다: snake_case는 널리 쓰이며 대소문자 구분 문제에 강하다.","테이블 이름의 단수/복수를 정한다. 의미 있고 축약하지 않은 컬럼 이름을 선호한다.","foreign key는 출처가 분명하게 명명하고(예: customer_id) 인덱스/제약에는 표준 접두사(fk_, ix_, uq_)를 붙인다.","예약어(order, user, group)를 테이블/컬럼 이름으로 사용하지 않는다.","boolean 컬럼은 is_/has_ 접두사로 명확히 한다. 날짜 컬럼에는 _at, _date 같은 일관된 접미사를 붙인다."],"level":"junior"},{"id":"partitioning-strategy","title":"파티셔닝 전략","category":"성능","summary":"파티셔닝은 대형 테이블을 관리 가능한 조각으로 나눈다. 유지보수, purge, 쿼리 성능을 개선한다.","points":["파티션 키는 쿼리 및 아카이브 패턴에 따라 선택한다. 시계열에는 날짜 기반 RANGE 파티셔닝이 가장 일반적이다.","partition pruning이 동작하는지 확인한다: 쿼리의 WHERE 절에 파티션 키가 포함되어야 하며, 그렇지 않으면 모든 파티션을 스캔한다.","오래된 데이터를 삭제하는 대신 파티션 DROP/DETACH로 즉시, 거의 락 없이 제거한다.","과도한 파티션 수는 플래닝 및 메타데이터 비용을 늘린다. 적절한 단위(월별/주별)를 선택한다.","향후 파티션을 자동으로 생성하는 유지보수 작업을 설정한다. 일치하는 파티션이 없는 쓰기로 인한 오류를 방지한다."],"level":"senior"},{"id":"connection-pooling","title":"커넥션 풀링","category":"운영","summary":"커넥션 풀은 비용이 큰 접속 수립 비용을 없애고 데이터베이스를 접속 폭주로부터 보호한다.","points":["요청마다 새 접속을 열지 않는다. 애플리케이션이나 중간 계층(PgBouncer, HikariCP)에서 풀을 사용한다.","풀 크기는 CPU 코어와 워크로드에 맞춘다. 너무 큰 풀은 데이터베이스를 질식시킨다(대개 작은 풀이 더 낫다).","접속 수명, idle 및 acquire timeout 값을 의도적으로 설정한다. 누수 탐지를 활성화한다.","데이터베이스 max_connections 한도에 맞춰 풀을 계획한다. 여러 애플리케이션 인스턴스의 합계를 고려한다.","짧은 트랜잭션을 사용하여 접속이 빠르게 풀로 반환되게 한다. 트랜잭션 안에서 외부 I/O를 기다리지 않는다."],"level":"senior"},{"id":"transaction-design","title":"트랜잭션 설계 (짧고 원자적으로)","category":"성능","summary":"트랜잭션은 짧고 원자적이며 하나의 논리적 작업 단위를 다뤄야 한다. 이렇게 하면 락 보유 시간이 최소화된다.","points":["트랜잭션은 가능한 한 짧게 유지한다. 사용자 상호작용, 외부 API 호출, 긴 계산을 트랜잭션 안에 넣지 않는다.","함께 성공하거나 실패해야 하는 작업만 하나의 트랜잭션으로 묶는다.","락 충돌을 줄이기 위해 행은 항상 같은 순서로 업데이트한다(데드락 방지의 기본).","불필요하게 넓은 락을 피한다. 읽기 전용 작업을 불필요한 트랜잭션 블록에 넣지 않는다.","오류 시 명확한 롤백 경로를 정의한다. 부분 커밋이나 열린 트랜잭션을 남기지 않는다.","긴 대량 작업은 배치로 나누어 커밋한다. 하나의 거대한 트랜잭션은 WAL/undo bloat를 일으킨다."],"level":"senior"},{"id":"isolation-levels","title":"격리 수준 선택","category":"성능","summary":"격리 수준은 일관성과 동시성의 균형을 정한다. 비즈니스 규칙에 따라 의도적으로 선택한다.","points":["이상 현상 유형을 알아 둔다: dirty read, non-repeatable read, phantom read. 어떤 수준이 무엇을 막는지 이해한다.","대부분의 OLTP에는 Read Committed로 충분하다. 정말 필요할 때만 더 높은 수준을 사용한다.","중요한 금액/재고 작업에는 Serializable이나 명시적 락(SELECT ... FOR UPDATE)을 사용한다.","Serializable에서는 serialization failure에 대한 자동 재시도 로직을 추가한다.","MVCC 엔진과 락 기반 엔진의 동작 차이를 알아 둔다. 같은 수준이라도 엔진마다 보장이 다를 수 있다."],"level":"expert"},{"id":"deadlock-prevention","title":"데드락 방지","category":"성능","summary":"데드락은 트랜잭션이 서로의 리소스를 기다릴 때 발생하며 설계로 상당 부분 방지할 수 있다.","points":["리소스/행에는 항상 일관된 동일 순서로 접근한다. 순서 차이가 데드락의 가장 흔한 원인이다.","트랜잭션을 짧게 유지하고 락 수를 줄인다. 불필요하게 넓은 업데이트를 피한다.","필요하면 조기 락(SELECT ... FOR UPDATE)으로 충돌을 예측 가능하게 만든다.","애플리케이션에서 데드락 오류를 잡아 트랜잭션을 자동 재시도한다. 데드락을 완전히 없애는 것은 대개 불가능하다.","데드락 로그/그래프를 정기적으로 검토한다. 반복되는 패턴은 인덱스나 접근 순서를 바꿔 제거한다."],"level":"expert"},{"id":"sharding-strategy","title":"샤딩 / 수평 확장 전략","category":"설계","summary":"샤딩은 데이터를 여러 노드에 분산하여 단일 서버의 한계를 극복한다. 복잡성이 크게 늘어나므로 최후의 수단이어야 한다.","points":["먼저 수직 확장, 인덱싱, 캐싱, 읽기 replica를 모두 시도한다. 정말 필요할 때만 샤딩한다.","샤드 키를 신중하게 선택한다: cross-shard 쿼리가 적고 분포가 고르며 hotspot을 만들지 않아야 한다.","재분배(resharding)에는 consistent hashing이나 가상 샤드를 사용한다. 처음부터 성장을 계획한다.","cross-shard JOIN과 분산 트랜잭션은 비용이 크다. 이를 최소화하도록 스키마를 설계한다.","전역 고유 ID 생성(snowflake 방식 순차 ID 등)과 샤드 라우팅 계층을 해결한다."],"level":"expert"},{"id":"backup-strategy","title":"백업 전략 (3-2-1)","category":"백업 및 HA","summary":"견고한 백업 전략은 재해에 대한 최후의 방어선이다. 테스트되지 않은 백업은 백업으로 치지 않는다.","points":["3-2-1 규칙을 적용한다: 사본 3개, 서로 다른 매체 2개, 사본 1개는 외부(offsite/다른 리전)에 둔다.","full + incremental 백업을 WAL/binlog 아카이빙과 결합하여 시점 복구(PITR) 능력을 확보한다.","백업을 실제로 복원하여 정기적으로 테스트한다. 테스트되지 않은 백업은 백업이 아니다.","백업은 저장 시와 전송 시 모두 암호화한다. 접근을 제한하고 보존 기간을 정책으로 정한다.","랜섬웨어에 대비해 immutable 또는 air-gapped 사본을 유지한다."],"level":"junior"},{"id":"hadr-design","title":"HA/DR 설계와 RPO/RTO","category":"백업 및 HA","summary":"고가용성 및 재해 복구 설계는 비즈니스 목표를 RPO와 RTO 지표로 정의하는 데서 시작한다.","points":["RPO(허용 가능한 데이터 손실)와 RTO(허용 가능한 다운타임) 목표를 비즈니스 부서와 함께 정의한다.","동기 복제는 RPO를 거의 0으로 만들지만 지연을 더하고, 비동기 복제는 성능이 좋지만 데이터 손실 위험이 있다.","자동 페일오버를 구성하고 정기적인 페일오버 훈련으로 실제 동작을 검증한다.","split-brain을 막기 위해 quorum/witness 메커니즘을 사용한다. 두 노드가 모두 master가 되는 일이 없게 한다.","DR 환경은 물리적으로 다른 리전에 둔다. 단일 리전/데이터 센터 장애에 견딜 수 있어야 한다."],"level":"expert"},{"id":"capacity-planning","title":"용량 계획과 성장","category":"운영","summary":"용량 계획은 성장 추세에 앞서 리소스를 확보하여 예기치 않은 장애를 방지한다.","points":["데이터 증가율, IOPS, CPU, 메모리, 접속 사용량을 시간에 따라 측정하고 추세를 도출한다.","디스크 사용량에 조기 임계값 알림(예: 75%/85%)을 설정한다. 공간이 바닥나기 전에 조치한다.","예측에 근거하여 성장을 계획한다. 피크 부하와 계절적 변동을 고려한다.","접속 수, 테이블/인덱스 bloat, 아카이빙 필요성을 정기적으로 검토한다.","병목에 도달하기 전에 확장 경로(수직, replica, 파티셔닝/샤딩)를 결정한다."],"level":"senior"},{"id":"migration-strategy","title":"마이그레이션 전략 (최소 다운타임)","category":"운영","summary":"스키마 및 데이터 마이그레이션은 다운타임을 최소화하고 가능하면 온라인으로 수행하는 되돌릴 수 있는 계획을 따라야 한다.","points":["모든 마이그레이션을 되돌릴 수 있고 idempotent하게 설계한다. 롤백 계획 없이 운영 환경에 적용하지 않는다.","대형 테이블에서 블로킹 DDL을 피한다. 온라인 스키마 변경 도구를 사용한다(PG: pg-osc/pgroll, MySQL: gh-ost, pt-osc).","하위/상위 호환 변경에는 expand-contract 패턴을 적용한다: 먼저 추가하고, 배포한 뒤, 이전 것을 제거한다.","대량 데이터는 작은 배치로 옮긴다. 하나의 거대한 UPDATE로 락과 WAL bloat를 만들지 않는다.","실제와 같은 데이터 양의 운영 유사 환경에서 마이그레이션을 먼저 리허설하고 소요 시간을 측정한다.","cutover 시점, 검증 단계, 롤백 결정을 사전에 문서화된 계획으로 확정한다."],"level":"senior"},{"id":"schema-versioning","title":"스키마 버전 관리 (Flyway/Liquibase)","category":"운영","summary":"스키마 변경은 코드와 함께 관리되고 버전이 붙으며 자동으로 적용되는 마이그레이션으로 관리해야 한다. 수동 개입은 오류를 낳기 쉽다.","points":["모든 스키마 변경을 Flyway/Liquibase 같은 도구로 버전 관리하고 소스 관리에 보관한다.","운영 환경에서 DDL을 수동으로 실행하지 않는다. 모든 변경은 리뷰된 마이그레이션 파일을 거쳐야 한다.","마이그레이션은 순서가 있고 고유하게 유지한다. 적용된 마이그레이션 파일은 절대 수정하지 말고 새 마이그레이션으로 고친다.","CI/CD 파이프라인에서 마이그레이션을 자동으로 적용하고 검증한다. 환경 간 스키마 일관성을 보장한다.","애플리케이션 코드와 스키마 버전을 보조를 맞춰 진행한다. 하위 호환 전환에는 expand-contract 패턴을 채택한다."],"level":"senior"},{"id":"least-privilege","title":"최소 권한 원칙","category":"보안","summary":"모든 사용자와 애플리케이션은 업무에 필요한 권한만 가져야 한다. 과도한 권한은 공격 표면을 넓힌다.","points":["애플리케이션에는 별도의 제한된 권한 계정을 사용한다. superuser/admin으로 접속하지 않는다.","권한은 사용자에게 직접이 아니라 역할에 부여한다. 접근은 역할 멤버십으로 관리한다.","필요한 테이블과 작업(SELECT/INSERT/UPDATE)만 허용한다. 광범위한 GRANT ALL을 피한다.","권한을 주기적으로 검토하고 사용하지 않는 권한을 회수한다. 직원/서비스가 떠나면 즉시 접근을 제거한다.","비밀 정보(비밀번호, connection string)를 코드에 넣지 않는다. secrets manager(vault)를 사용하고 정기적으로 교체한다."],"level":"junior"},{"id":"encryption-basics","title":"암호화 기본 (저장 데이터/전송 구간)","category":"보안","summary":"데이터는 저장 시와 전송 시 모두 암호화해야 한다. 암호화는 컴플라이언스와 데이터 유출에 대한 기본 보호 수단이다.","points":["전송 중에는 TLS를 강제한다. 암호화되지 않은 접속을 거부하고 인증서 검증을 활성화한다.","at-rest 암호화(TDE 또는 디스크/볼륨 암호화)를 적용하여 도난당한 디스크/백업을 읽을 수 없게 한다.","키 관리를 중요하게 다룬다: 키는 별도의 KMS/HSM에 보관하고 정기적으로 교체한다.","신용카드 번호, 국가 신분증 번호 같은 매우 민감한 필드는 애플리케이션 계층에서 추가로 암호화한다(field-level).","백업도 암호화한다. 운영 환경의 보호를 백업과 replica까지 확장한다.","테스트/개발 환경에서는 민감한 데이터를 마스킹하거나 익명화한다(data masking)."],"level":"senior"},{"id":"monitoring-basics","title":"모니터링 기본 (golden signals)","category":"운영","summary":"효과적인 모니터링은 올바른 지표를 수집하고 의미 있는 임계값에 알림을 거는 것이다. 사용자가 알아차리기 전에 문제를 포착한다.","points":["golden signals를 모니터링한다: 지연, 트래픽, 오류율, 포화도.","데이터베이스 전용 지표를 추가한다: 느린 쿼리, 락/대기 시간, 접속 수, 캐시 히트율, 복제 지연.","알림은 증상이 아니라 영향과 조치 가능한 임계값에 연결한다. 시끄러운 알림은 alert fatigue를 일으킨다.","slow query log와 쿼리 통계(pg_stat_statements 등)를 항상 활성화해 두고 정기적으로 검토한다.","지표를 이력으로 보존한다. 추세와 용량 계획을 위한 baseline을 만든다.","남은 디스크 공간, 백업 성공 같은 중요한 조건에는 반드시 알림을 건다."],"level":"junior"},{"id":"query-design","title":"쿼리 설계와 N+1 방지","category":"성능","summary":"효율적인 쿼리는 최소한의 왕복으로 필요한 데이터만 가져온다. N+1은 가장 흔하고 가장 교묘한 성능 버그다.","points":["SELECT * 대신 필요한 컬럼만 선택한다. 네트워크, 메모리, 커버링 인덱스 활용이 개선된다.","N+1 문제를 방지한다: 루프 안의 단일 쿼리 대신 JOIN이나 배치(IN) 쿼리를 사용한다. ORM에서는 eager loading을 활성화한다.","대형 결과 집합은 페이지로 나눈다. 대용량 데이터에서는 OFFSET보다 keyset(seek) 페이지네이션을 선호한다.","비즈니스 로직은 가능한 한 쿼리에 맡긴다. 테이블 전체를 가져와 애플리케이션에서 필터링/집계하지 않는다.","EXPLAIN ANALYZE로 실제 플랜과 행 추정치를 조사한다. full table scan과 잘못된 추정을 제거한다.","자주 실행되는 비용이 큰 집계 쿼리는 materialized view나 미리 계산된 요약 테이블로 뒷받침한다."],"level":"junior"},{"id":"large-table-management","title":"대형 테이블 관리 (아카이빙/파티셔닝)","category":"성능","summary":"계속 커지는 테이블은 시간이 지날수록 쿼리, 유지보수, 백업을 느리게 한다. 아카이빙과 파티셔닝으로 통제한다.","points":["hot(활성) 데이터와 cold(이력) 데이터를 분리한다. 오래된 데이터는 아카이브 테이블이나 저렴한 스토리지로 옮긴다.","테이블을 날짜/범위로 파티셔닝한다. 오래된 데이터는 DELETE 대신 파티션 DROP으로 즉시 제거한다.","대량 삭제/업데이트는 작은 배치로 수행한다. 하나의 거대한 DELETE는 락, WAL bloat, 테이블 bloat를 만든다.","bloat와 테이블/인덱스 크기를 정기적으로 모니터링한다. 필요하면 온라인 재구성(VACUUM/reorg)을 적용한다.","자주 실행되는 쿼리에 맞는 인덱스를 유지한다. 대형 테이블에서 불필요한 인덱스를 유지하는 비용은 매우 크다."],"level":"senior"},{"id":"caching-strategy","title":"캐싱 전략","category":"성능","summary":"캐시는 반복 읽기를 데이터베이스에서 떼어 내 지연과 부하를 낮춘다. 진짜 어려움은 무효화(invalidation)다.","points":["캐시는 비용이 크고 자주 읽히며 거의 바뀌지 않는 데이터에 사용한다. 모든 것을 캐싱하려 하지 않는다.","무효화 전략을 선택한다: TTL, 쓰기 시 갱신(write-through), 쓰기 시 삭제(write-invalidate).","오래된 데이터 허용 범위를 비즈니스 규칙에 따라 정의한다. 엄격한 일관성이 필요한 곳에서는 캐시에 의존하지 않는다.","cache stampede(많은 요청이 빈 캐시를 동시에 채우는 현상)를 락이나 request coalescing으로 방지한다.","캐시 히트율을 모니터링한다. 히트율이 낮으면 캐시가 가치를 제공하지 못하고 있다는 뜻이다.","캐시는 가속 계층이지 영속 상태가 아니다. 캐시가 사라져도 시스템은 올바르게 동작해야 한다."],"level":"senior"},{"id":"statistics-maintenance","title":"통계와 유지보수 규율","category":"운영","summary":"최신 통계와 정기적인 유지보수는 쿼리 플래너가 올바른 결정을 내리고 성능이 시간이 지나도 저하되지 않게 한다.","points":["통계를 최신으로 유지한다(ANALYZE / auto-analyze). 오래된 통계는 플래너를 잘못된 플랜으로 이끈다.","VACUUM/정리를 정기적으로 실행하여 dead row와 bloat를 제거한다. MVCC 엔진에서는 매우 중요하다.","인덱스 단편화를 모니터링하고 필요하면 온라인으로 재구성한다(REINDEX/rebuild).","대량 로드 후에는 통계를 수동으로 갱신한다. 플래너가 이전 분포로 동작하지 않게 한다.","유지보수 작업은 트래픽이 적은 시간대에 예약하고 소요 시간/영향을 모니터링하며 자동화한다.","플랜 회귀(갑자기 느려진 쿼리)를 주시한다. 가장 먼저 확인할 것은 통계의 최신성이다."],"level":"senior"}]},"guide/best-practices-2.json":{"topics":[{"id":"iac","title":"인프라를 코드로 관리 (IaC)","category":"운영","summary":"데이터베이스 서버, 네트워크, 구성은 수작업이 아니라 버전 관리되는 코드로 정의해야 한다. 그래야 환경을 재현, 감사, 되돌릴 수 있다.","points":["서버, parameter group, 네트워크, 사용자 정의를 Terraform/Pulumi/CloudFormation 같은 도구로 코드화하여 소스 관리에 둔다.","수동 콘솔 변경(click-ops)을 금지한다. drift detection을 실행하여 코드와 어긋난 리소스를 바로잡는다.","환경(dev/stage/prod) 간 차이는 변수로 관리한다. 운영 유사 환경은 같은 모듈로 구축한다.","state 파일은 원격에 두고 락을 건다(예: S3 + DynamoDB lock). 비밀 정보가 state에 평문으로 새지 않게 한다.","변경은 plan/apply 흐름으로 검토한다. 운영 환경 앞에 승인 게이트를 둔다."],"level":"senior"},{"id":"automation-idempotency","title":"자동화와 멱등성","category":"운영","summary":"운영 작업은 수동으로 반복하는 명령에서 몇 번 실행해도 같은 결과를 내는 idempotent 자동화로 전환해야 한다.","points":["모든 스크립트를 idempotent하게 작성하여 다시 실행해도 실패하거나 상태를 망가뜨리지 않게 한다(예: CREATE 대신 CREATE IF NOT EXISTS).","반복되는 유지보수 작업(백업, 정리, 통계)은 예약되고 로그가 남는 자동화에 연결한다. 수동 실행에 맡기지 않는다.","스크립트에 명시적인 사전 조건 검사와 안전한 종료를 넣는다. 중간에 멈춘 작업을 다시 실행하면 멈춘 지점부터 완료되어야 한다.","자동화는 코드로 실행한다(Ansible/스크립트 + CI). 운영 환경에서 누군가 터미널에 명령을 입력하는 데 의존하지 않는다.","중요한 작업에는 dry-run 지원을 제공한다. 적용 전에 영향을 보여 준다."],"level":"senior"},{"id":"observability","title":"관측 가능성 (지표/로그/트레이스)","category":"운영","summary":"관측 가능성은 지표, 로그, 분산 트레이스를 결합하여 '무엇이 일어났는가'에서 '왜 일어났는가'로 나아가게 한다.","points":["세 가지 신호를 모두 함께 수집한다: 지표(추세), 로그(맥락), 트레이스(요청 경로). 어느 하나만으로는 근본 원인이 드러나지 않는다.","쿼리를 end-to-end로 추적한다: 애플리케이션 트레이스를 데이터베이스 지연과 연관 짓는다(OpenTelemetry 같은 표준 사용).","correlation/trace id가 포함된 구조화(JSON) 로그를 출력한다. 자유 텍스트 로그는 검색을 어렵게 한다.","high-cardinality 레이블을 피한다. 카디널리티 폭발과 비용을 통제한다.","SLO를 정의하고 error budget을 추적한다. 관측 가능성을 비즈니스 목표에 연결한다."],"level":"senior"},{"id":"alerting-strategy","title":"알림 전략 (저소음, 조치 가능)","category":"운영","summary":"좋은 알림 전략은 실제로 조치 가능한 상황에서만 사람을 깨운다. 소음은 alert fatigue와 번아웃을 일으킨다.","points":["증상이 아니라 영향과 사용자 경험(SLO 위반)에 알림을 건다. 지표가 흔들릴 때마다 호출하지 않는다.","모든 알림에는 명확한 담당자, 조건, runbook이 있어야 한다. 정의된 조치가 없는 알림은 가치가 없다.","임계값에 hysteresis와 지속 시간(for: 5m)을 사용한다. 순간적인 급증으로 알림이 발생하지 않게 한다.","호출(paging)과 통지(ticket/email)를 구분한다. 즉각 개입이 필요한 것에만 호출한다.","알림을 정기적으로 검토한다. 계속 울려서 무시되는 알림은 고치거나 제거한다.","백업 실패, 디스크 가득 참, 복제 중단 같은 조용한 재해는 반드시 다룬다."],"level":"senior"},{"id":"dr-testing","title":"DR/페일오버 훈련 규율","category":"백업 및 HA","summary":"재해 복구 계획은 정기적이고 현실적인 훈련으로 검증될 때만 신뢰할 수 있다. 테스트되지 않은 계획은 가정일 뿐이다.","points":["페일오버 및 복원 훈련을 일정에 넣고 주기적으로 실행한다. 1년에 한 번 검토하는 서류상 계획으로는 충분하지 않다.","훈련마다 실제 RTO와 RPO를 측정한다. 목표와의 차이를 보고하고 개선한다.","계획된 페일오버를 실제 트래픽이 있는 운영 유사 환경에서 리허설한다. standby를 시작하고 멈추는 데 그치지 않는다.","리전 손실, 디스크 장애, 데이터 손상 같은 시나리오를 game day로 시뮬레이션한다.","훈련 후마다 교훈을 runbook에 기록한다. 감으로 기억하는 단계를 자동화로 바꾼다."],"level":"expert"},{"id":"blue-green-deploy","title":"Blue-green / 무중단 배포","category":"운영","summary":"blue-green 및 canary 배포는 새 버전을 이전 버전과 나란히 실행하고 트래픽을 점진적으로 옮겨 다운타임과 위험을 최소화한다.","points":["두 개의 운영 환경(blue/green)을 유지한다. 유휴 환경에 새 버전을 배포하고 검증한 뒤 트래픽을 한 번에 전환한다.","문제가 생기면 즉시 트래픽을 이전 환경으로 되돌린다. 롤백에 재배포가 필요해서는 안 된다.","canary에서는 트래픽의 작은 비율을 새 버전으로 보내고 지표를 지켜보며 점진적으로 늘린다.","데이터베이스 스키마를 배포와 분리한다: 두 코드 버전이 모두 같은 스키마로 동작해야 한다(expand-contract).","health check와 자동 롤백 임계값을 미리 정의한다. 사람의 수동 결정을 기다리는 시간을 줄인다."],"level":"senior"},{"id":"expand-contract","title":"하위 호환 스키마 변경 (expand-contract)","category":"설계","summary":"expand-contract 패턴은 스키마 변경을 추가(expand), 이전, 마지막으로 이전 것 제거(contract)로 나누어 무중단 전환을 가능하게 한다.","points":["먼저 확장한다: 새 컬럼/테이블을 이전 것을 깨뜨리지 않고 하위 호환 방식(nullable 또는 기본값 포함)으로 추가한다.","dual-write 또는 dual-read를 사용하여 애플리케이션이 이전 스키마와 새 스키마 모두에서 동작하도록 배포한다.","데이터를 백그라운드에서 작은 배치로 새 구조로 이전하고 검증한다. 하나의 거대한 마이그레이션으로 락을 걸지 않는다.","모든 인스턴스가 새 버전으로 옮겨 가고 검증이 끝난 뒤에만 축소한다: 이전 컬럼/코드를 제거한다.","각 단계를 별도 배포로 수행한다. '추가와 이전 것 삭제를 동시에' 하지 않는다. 그러면 롤백이 불가능해진다.","NOT NULL 등의 제약은 데이터가 일관된 상태가 된 뒤 마지막에 추가한다."],"level":"expert"},{"id":"secrets-management","title":"비밀 관리 (비밀번호/키)","category":"보안","summary":"데이터베이스 비밀번호, API 키, connection string은 코드나 구성에 넣어서는 안 된다. 중앙 secrets manager로 안전하게 저장하고 교체해야 한다.","points":["비밀 정보를 코드, 저장소, env 파일, 이미지에 넣지 않는다. Vault, AWS Secrets Manager, KMS 같은 secret store를 사용한다.","비밀 정보는 런타임에 애플리케이션에 주입한다. 디스크에 평문으로 남기지 않는다.","비밀번호와 키를 정기적으로 교체한다. 가능하면 수명이 짧고 자동 생성되는 자격 증명을 사용한다.","접근은 최소 권한과 감사 로그로 부여한다. 누가 언제 어떤 비밀 정보에 접근했는지 추적할 수 있어야 한다.","코드베이스에서 유출된 비밀 정보를 스캔한다(git-secrets, trufflehog). 유출된 비밀 정보는 즉시 폐기하고 교체한다.","사람의 접근은 개인적이고 영구적인 것이 아니라 임시적이고 승인 기반(just-in-time)으로 한다."],"level":"senior"},{"id":"cost-optimization","title":"클라우드 비용 최적화","category":"운영","summary":"클라우드 데이터베이스 비용은 right-sizing, 스토리지 계층화, 미사용 리소스 정리로 성능을 희생하지 않고 크게 줄일 수 있다.","points":["실제 사용량 지표에 근거하여 인스턴스 크기를 정한다. 사용률이 지속적으로 낮은 서버는 축소한다(right-sizing).","예측 가능한 기본 부하에는 reserved capacity/savings plan을 사용한다. 변동 부하는 온디맨드로 둔다.","cold 데이터는 더 저렴한 스토리지 계층으로 옮긴다. hot 데이터와 드물게 접근하는 데이터를 분리한다.","미사용 리소스를 정리한다: 고아 스냅샷, 유휴 replica, 방치된 테스트 데이터베이스는 비용 누수다.","태그로 비용을 담당자에게 할당하고 예산 알림을 설정한다. 예상치 못한 비용을 월말이 아니라 즉시 파악한다.","백업 보존 기간과 replica 수를 비즈니스 요구에 비추어 검토한다. 과잉은 위험이자 비용이다."],"level":"senior"},{"id":"multi-tenancy","title":"멀티테넌트 설계","category":"설계","summary":"멀티테넌트 시스템에서 격리 모델(공유 테이블, 별도 스키마, 별도 데이터베이스)은 보안, 비용, 확장성 간의 의도적인 트레이드오프다.","points":["격리 모델을 선택한다: 공유 테이블(tenant_id), 테넌트별 스키마, 테넌트별 데이터베이스. 각각 비용/격리 균형이 다르다.","공유 모델에서는 모든 쿼리에 tenant_id 필터를 적용한다. row-level security(RLS)로 엔진 수준에서 유출이 없음을 보장한다.","관련 인덱스에서 tenant_id를 맨 앞에 둔다. 테넌트 간 스캔과 hot spot을 방지한다.","noisy neighbor 효과에 대비해 리소스 한도와 할당량을 적용한다. 한 테넌트가 다른 테넌트를 굶겨서는 안 된다.","대형 또는 프리미엄 테넌트를 별도 스키마/데이터베이스로 옮기는 경로(tenant migration)를 처음부터 설계한다.","테넌트별 백업, 삭제, 데이터 이동성을 고려한다. KVKK/GDPR 삭제 요청을 이행할 수 있어야 한다."],"level":"expert"},{"id":"time-series-design","title":"시계열 데이터 설계","category":"설계","summary":"시계열 데이터는 높은 쓰기량과 시간 기반 쿼리 패턴 때문에 특수하다. 날짜 기반 파티셔닝, rollup, 보존 정책으로 설계해야 한다.","points":["timestamp 기반 RANGE 파티셔닝으로 데이터를 나눈다. 오래된 데이터는 파티션 DROP으로 저렴하게 제거한다.","원시 데이터를 그대로 쌓는 대신 일정 간격의 요약(rollup/continuous aggregate)을 생성한다. 쿼리는 요약에서 빠르게 실행되게 한다.","인덱스에서 시간과 태그(tag/source) 구성 요소를 앞에 둔다. 전형적인 쿼리 패턴은 '특정 소스, 특정 범위'다.","적절한 도구를 선택한다: 대용량에서는 TimescaleDB, ClickHouse 같은 시계열 엔진이 관계형보다 훨씬 효율적일 수 있다.","보존 정책을 처음부터 정의한다. 원시 데이터는 짧게, 집계 데이터는 길게 보관한다.","시간은 항상 UTC로 저장한다. 로컬 시간과 일광 절약 시간 전환은 쿼리 계층에서 처리한다."],"level":"senior"},{"id":"event-sourcing","title":"이벤트 소싱 / CQRS 기본","category":"설계","summary":"event sourcing은 상태가 아니라 상태를 바꾸는 이벤트를 기록한다. CQRS는 읽기와 쓰기 모델을 분리하여 각각 독립적으로 확장하게 한다.","points":["현재 상태를 덮어쓰는 대신 불변 이벤트 스트림을 쌓는다. 현재 상태는 이벤트를 재생하여 도출한다.","CQRS에서는 쓰기 모델(command)과 읽기 모델(query)을 분리한다. 읽기 측은 비정규화된 projection으로 빠르게 한다.","이벤트 스키마에 버전을 붙이고 하위 호환 방식으로 발전시킨다. 이전 이벤트는 절대 삭제하지 말고 upcasting으로 새 버전으로 변환한다.","읽기 projection은 eventually consistent임을 받아들이고 설계에서 지연을 고려한다.","긴 이벤트 스트림을 매번 처음부터 재생하는 비용을 피하기 위해 스냅샷을 생성한다.","이 패턴은 모든 곳이 아니라 감사 추적, 복잡한 비즈니스 규칙, 시간에 따른 이력이 필요한 도메인에 사용한다. 그에 따르는 복잡성을 과소평가하지 않는다."],"level":"expert"},{"id":"data-retention","title":"데이터 보존 및 아카이빙 정책","category":"운영","summary":"모든 데이터를 영원히 보관하지는 않는다. 법적, 비즈니스, 비용 요건에 근거한 보존 및 아카이빙 정책이 컴플라이언스와 성능을 함께 제공한다.","points":["각 데이터 유형의 보존 기간을 비즈니스 및 법적(KVKK/GDPR) 요건에 따라 정의한다. 막연한 '혹시 모르니 보관'은 정책이 아니다.","기간이 지난 데이터는 자동으로 아카이브하거나 삭제한다. 잊히기 쉬운 수동 정리에 맡기면 테이블이 비대해진다.","hot 데이터는 빠른 스토리지에, 아카이브는 저렴한/cold 스토리지에 둔다. 접근 빈도에 따라 계층화한다.","개인 데이터 삭제/익명화 요청을 처리하는 메커니즘을 처음부터 설계한다(삭제권).","아카이브된 데이터도 필요할 때 다시 가져오고 조회할 수 있게 유지한다. 아카이브가 접근 불가를 의미해서는 안 된다.","삭제는 감사 로그에 기록한다. 무엇을 언제 왜 삭제했는지 추적할 수 있어야 한다."],"level":"senior"},{"id":"change-management","title":"변경 관리와 운영 안전","category":"운영","summary":"운영 데이터베이스에 대한 모든 변경은 리뷰되고 되돌릴 수 있으며 추적 가능해야 한다. 통제되지 않은 수동 개입이 다운타임의 가장 흔한 원인이다.","points":["운영 환경에서 ad-hoc DDL/DML을 직접 실행하지 않는다. 모든 변경은 동료 리뷰를 거친 마이그레이션이나 스크립트를 통해야 한다.","변경은 롤백 계획을 준비한 상태로 트래픽이 적은 시간대에 적용한다. 계획 없이 운영 환경을 건드리지 않는다.","위험한 명령은 실제 데이터 양의 운영 유사 환경에서 먼저 리허설한다. 소요 시간과 락 영향을 측정한다.","운영 접근을 제한하고 감사한다: 누가 언제 어떤 명령을 실행했는지 로그로 남겨야 한다(just-in-time access).","삭제/업데이트 전에 WHERE 절과 영향받는 행 수를 확인한다. 트랜잭션 안에서 실행하고 먼저 SELECT로 확인한다.","변경은 일정 및 커뮤니케이션과 연결한다. 공지 없는 운영 변경으로 다른 팀을 놀라게 하지 않는다."],"level":"senior"}]},"guide/best-practices-3.json":{"topics":[{"id":"plan-reading-expert","title":"실행 계획 읽는 법","category":"성능","summary":"실행 계획은 옵티마이저의 추정과 선택한 물리 연산자를 보여 준다. 병목은 추정 행 수와 실제 행 수의 차이에 숨어 있다.","points":["접근 방식을 구분한다: Full/Table Scan은 대형 테이블의 광범위한 읽기용, Index Seek/Range Scan은 선택도 높은 조건용, Index Full Scan은 정렬된 읽기용이다. Rows Removed by Filter나 predicate/access 구분은 선택도가 어디서 손실되는지 보여 준다.","조인 방식을 워크로드에 맞춘다: Nested Loop는 작은 외부 집합 + 인덱스가 있는 내부 집합에, Hash Join은 대형 동등 조인에, Merge Join은 미리 정렬된 두 집합에 적합하다. 잘못된 선택은 대개 나쁜 카디널리티 추정의 결과다.","카디널리티 추정 오류를 찾는다: EXPLAIN ANALYZE(PG), DBMS_XPLAN ALLSTATS LAST(Oracle), SET STATISTICS PROFILE(MSSQL)로 추정 행과 실제 행을 비교한다. 10배 이상 차이는 플랜 선택이 근본적으로 잘못되었을 수 있음을 시사한다.","플랜은 실제 비용으로 읽는다: 시간/논리 읽기(logical reads, buffers)를 가장 많이 소모하는 연산자를 찾는다. 옵티마이저의 'cost' 단위는 추정치이지 실제 병목이 아니다.","spill/temp 공간 경고를 포착한다: 메모리에 들어가지 않아 디스크로 넘치는 Hash/Sort 연산자(PG 'Sort Method: external merge', 플랜에 SpillToTempDb가 있는 MSSQL Sort/Hash Warning)는 work_mem/정렬 영역 부족을 나타낸다.","연산자 트리는 안쪽에서 바깥쪽으로, 아래에서 위로 읽는다: 안쪽 노드가 먼저 실행되고 행이 위로 흐른다. 연산자별 총비용은 loops에 actual time을 곱해 계산한다."],"level":"expert"},{"id":"parameter-sniffing-deep","title":"파라미터 스니핑과 bind peeking","category":"성능","summary":"옵티마이저는 첫 호출의 파라미터 값을 기준으로 플랜을 만들어 캐싱한다. 분포가 다른 이후 값이 그 플랜을 물려받으면 성능이 극적으로 달라질 수 있다.","points":["증상을 인식한다: 같은 쿼리가 어떤 파라미터에서는 빠르고 다른 파라미터에서는 매우 느리다. 캐싱된 플랜이 비전형적인 '첫' 값에 맞춰 컴파일되었음을 보여 준다.","분포가 치우친 컬럼에서 위험이 크다. 히스토그램은 옵티마이저를 값에 민감하게 만들지만 단일 캐시 플랜은 값에 둔감하게 유지하며, 이 상충이 문제의 원인이다.","MSSQL에서 OPTION (RECOMPILE)은 실행할 때마다 플랜을 만들고, OPTIMIZE FOR UNKNOWN은 평균 밀도에 근거한 균형 잡힌 플랜을 선택하며, OPTIMIZE FOR (@p = value)는 전형적인 값을 고정한다.","Oracle에서는 adaptive cursor sharing과 bind-aware cursor가 치우친 값에 대해 다른 플랜을 만든다. PG에서는 custom 대 generic 플랜 선택을 plan_cache_mode로 제어한다.","영구적인 해결을 위해 값별로 플랜을 분리한다: 문장 분할, filtered/partial 인덱스, 매우 비전형적인 값을 위한 전용 접근 경로를 설계한다.","OLTP에서 과도한 RECOMPILE은 CPU와 컴파일(hard parse) 비용을 더한다. 자주 실행되는 쿼리에서는 측정하고 균형을 유지한다."],"level":"expert"},{"id":"plan-stability","title":"실행 계획 안정성: baseline, 힌트, plan guide","category":"성능","summary":"엔진별 고정 메커니즘을 사용하여 중요 쿼리의 검증된 플랜을 고정하고 통계/버전 변경 시 예기치 않은 플랜 회귀를 방지한다.","points":["Oracle에서는 SQL Plan Baselines(SPM)로 승인된 플랜을 고정한다. 새 플랜은 'accepted'될 때까지 후보로만 수집되며 evolve로 통제된 방식으로 검증된다.","MSSQL에서는 Query Store로 플랜 회귀를 탐지하고 'Force Plan'으로 검증된 좋은 플랜을 강제한다. Automatic Plan Correction으로 이를 자동화할 수 있다.","PostgreSQL에는 내장 플랜 고정 기능이 없다. 안정성은 pg_hint_plan 확장이나 쿼리 재작성, extended statistics, 인덱스 설계로 확보한다.","hint는 근거와 주석을 갖춘 최후의 수단으로 사용한다. hint는 물리 스키마와 데이터 분포를 전제하며 그것이 바뀌면 해가 된다.","baseline/강제 플랜 목록을 주기적으로 감사한다. 고정된 플랜은 데이터가 커지면서 최적성을 잃을 수 있으므로 '안정적이지만 느린' 함정에 빠지지 않는다.","버전 업그레이드 전에 중요 쿼리의 플랜을 캡처한다. 옵티마이저 동작 변경에 대비한 안전망으로 baseline을 준비한다."],"level":"expert"},{"id":"latch-mutex-contention","title":"래치, 뮤텍스, 스핀락 경합","category":"성능","summary":"latch/mutex는 메모리 내 구조를 보호하는 경량 락이다. 높은 동시성에서 이에 대한 경합은 spinlock 루프에서 CPU를 소모하고 확장을 멈추게 한다.","points":["락과 latch를 구분한다: 락은 트랜잭션 동안 논리 데이터(행/테이블)를 보호하고, latch는 마이크로초 동안 물리 메모리 구조(버퍼, hash chain)를 보호한다. 진단 방법이 다르다.","Oracle에서 'cache buffers chains' latch 경합은 대개 hot block을 가리킨다. 자주 읽히는 작은 테이블이나 단조 증가 인덱스의 선두 끝(right-hand index)이 흔한 원인이다.","순차 증가 키(identity/sequence)를 사용한 insert는 마지막 페이지/블록 경합(hot page, PAGELATCH)을 일으킨다. hash 파티셔닝, reverse-key 인덱스, 무작위화된 키로 분산한다.","MSSQL에서는 spinlock 통계와 PAGELATCH_* 대기를 모니터링한다. tempdb 할당 경합에는 여러 데이터 파일과 올바른 tempdb 구성을 사용한다.","spinlock 경합은 높은 CPU와 낮은 처리량으로 나타난다. 해결책은 코드/아키텍처 수준에서 경합을 줄이는 것이며, 하드웨어 추가는 종종 상황을 악화시킨다.","NUMA 인식과 접속 수 제한(풀링)은 latch 경합을 줄인다. 과도한 병렬 처리는 latch 압박을 늘릴 수 있다."],"level":"expert"},{"id":"wait-event-methodology","title":"대기 이벤트 분석 방법론","category":"성능","summary":"성능은 추측이 아니라 시스템이 어디서 시간을 대기에 쓰는지 측정하여 관리한다. 하향식 대기 분석은 병목을 체계적으로 좁혀 가는 과학적 방법이다.","points":["DB time을 구성 요소로 나눈다: CPU에 쓴 시간과 대기 시간을 분리한다. 가장 큰 구성 요소부터 시작하고 미세 최적화로 빠지지 않는다.","상위 대기 이벤트를 찾는다: Oracle은 ASH/AWR과 'Top Wait Events', PG는 pg_stat_activity + pg_wait_sampling, MSSQL은 sys.dm_os_wait_stats와 Query Store를 사용한다.","wait class를 해석한다: I/O 대기는 스토리지/플랜을, 락 대기는 동시성 설계를, latch 대기는 hot spot을, 네트워크/커밋 대기는 구성 문제를 가리킨다.","세션/쿼리 수준까지 파고든다: 총 대기에서 시작해 가장 많이 기여하는 SQL로, 다시 그 SQL의 플랜으로 내려가 원인을 분리한다(drill-down).","측정을 워크로드 구간에 연결한다: 두 시점 사이의 누적 카운터 차이(delta)를 구한다. 합계는 오해를 낳으며 비율과 시간 구간이 중요하다.","idle/무해한 대기는 노이즈로 걸러 낸다. 모든 대기가 문제는 아니며 중요한 것은 사용자 응답 시간에 기여하는 foreground 대기다."],"level":"expert"},{"id":"plan-regression-detection","title":"실행 계획 회귀 탐지","category":"성능","summary":"갑자기 느려진 쿼리는 대개 코드 변경이 아니라 플랜 변경 때문이다. 회귀를 선제적으로 탐지하는 것이 사후 대응보다 저렴하다.","points":["플랜 이력을 보관하고 비교한다: MSSQL Query Store와 Oracle AWR/SQL Monitor로 같은 sql_id/query_hash의 플랜 변경과 시간/리소스 추세를 추적한다.","회귀의 계기를 식별한다: 통계 갱신, 데이터 양 증가, 버전 업그레이드, 파라미터 변경, 삭제된 인덱스가 가장 흔한 원인이다.","자동 알림을 설정한다: plan hash가 바뀌고 평균 시간/논리 읽기가 임계값을 넘으면 알림을 발생시킨다. 누적이 아니라 실행당 지표를 추적한다.","빠른 롤백 경로를 준비한다: Force Plan(MSSQL)이나 SQL Plan Baseline(Oracle)으로 검증된 좋은 플랜을 일시 고정하고 근본 원인은 나중에 해결한다.","통계로 인한 회귀에는 잠긴/현재 통계와 히스토그램의 최신성을 조사한다. 자동 통계 수집이 중요한 시간대에 예약되어 있을 수 있다.","업그레이드 전에 회귀 테스트를 실행한다: 대표 워크로드를 이전 및 새 옵티마이저에 재생하고 플랜 차이를 보고한다(Oracle SPA와 유사한 접근)."],"level":"expert"},{"id":"mvcc-vacuum-internals","title":"MVCC와 vacuum/undo 내부 구조","category":"내부 구조","summary":"MVCC는 이전 행 버전을 유지하여 reader-writer 블로킹을 방지한다. 이 버전을 어떻게 어디에 보관하는지(undo 대 테이블 내부)가 유지보수 전략을 결정한다.","points":["PostgreSQL에서는 이전 버전이 dead tuple로 테이블 자체에 남는다. VACUUM이 이를 정리하며, 그렇지 않으면 bloat와 인덱스 증가가 발생한다.","Oracle과 MySQL/InnoDB는 이전 버전을 undo/rollback segment에 보관한다. 장시간 읽기가 undo를 소진하면 'ORA-01555 snapshot too old' 같은 오류가 나타난다.","autovacuum을 워크로드에 맞게 조정한다: 업데이트/삭제가 많은 테이블에서는 scale factor를 낮추고 cost delay를 줄인다. 공격적인 테이블에는 테이블별 설정을 준다.","visibility map을 건강하게 유지한다. index-only scan과 frozen tuple 추적이 이에 의존하며, 방치하면 성능이 저하된다.","트랜잭션 ID wraparound 위험을 모니터링한다: PG에서 datfrozenxid/age 지표를 추적하고 공격적/anti-wraparound vacuum이 업무 시간에 시작되지 않게 한다.","HOT(Heap-Only Tuple) 업데이트를 유도한다: 인덱스 컬럼을 변경하지 않는 업데이트는 인덱스를 키우지 않고 제자리에서 수행된다. 적절한 fillfactor가 이를 뒷받침한다."],"level":"expert"},{"id":"wal-redo-log-mechanics","title":"WAL/redo/트랜잭션 로그 메커니즘","category":"내부 구조","summary":"내구성은 데이터 페이지가 디스크에 기록되기 전에 변경을 로그에 기록하여 달성한다. 로그 스트림의 속도와 크기가 내구성과 쓰기 성능을 모두 결정한다.","points":["write-ahead 원칙을 이해한다: 커밋이 반환되려면 관련 로그 레코드가 영속 스토리지에 있기만 하면 된다. dirty 데이터 페이지는 나중에 지연 기록된다.","group commit과 로그 flush 동작을 조정한다: PG에서는 commit_delay/synchronous_commit으로, Oracle에서는 log writer 동작으로 fsync 빈도와 워크로드의 내구성 요구 사이의 균형을 맞춘다.","로그 생성 속도(WAL/redo generation)를 모니터링한다. 급격한 증가는 대량 업데이트, 체크포인트 후 full-page image(full_page_writes/FPI), 인덱스 유지보수를 가리키며 DR 지연을 늘린다.","로그는 복구뿐 아니라 복제와 CDC의 원천이기도 하다. 논리 복제와 WAL shipping이 이 스트림에서 공급되므로 아카이브/정리 정책을 그에 맞게 설계한다.","로그 디스크를 데이터 디스크와 분리하고 지연이 낮은 스토리지에 둔다. 로그 쓰기는 순차적이며 지연에 민감하고 커밋 처리량이 이에 직접 의존한다.","통제되지 않는 로그 증가를 막는다: MSSQL에서는 올바른 복구 모델과 정기 로그 백업을 사용하고, PG에서는 아카이빙/replication slot이 뒤처지지 않는지 확인한다."],"level":"expert"},{"id":"buffer-pool-management","title":"버퍼/shared pool 관리와 축출","category":"내부 구조","summary":"buffer pool은 hot 데이터를 메모리에 유지하여 디스크 I/O를 줄인다. 어떤 페이지를 언제 축출할지 결정하는 알고리즘이 히트율과 안정성을 좌우한다.","points":["buffer cache 히트율만 목표로 삼지 않는다. 높은 비율도 물리 읽기의 지연을 감출 수 있으므로 절대 I/O와 대기 시간도 함께 본다.","축출 알고리즘을 이해한다: 순차 스캔으로 오염되는 순수 LRU에 대응하여 PG는 clock-sweep을, Oracle은 touch-count/LRU 변형을, InnoDB는 midpoint-insertion LRU를 사용한다.","일회성 대형 스캔이 hot 데이터를 풀에서 밀어내는 것(cache pollution)을 막는다. 별도의 buffer pool이나 스캔 친화적 접근 전략으로 보호한다.","dirty 페이지 비율과 쓰기 압박을 관리한다: dirty 페이지가 너무 많으면 체크포인트 시 I/O 급증이 생기고, 너무 공격적인 쓰기는 불필요한 I/O를 만든다. 균형을 측정한다.","메모리는 working set에 맞춰 정한다. 목표는 전체 데이터가 아니라 활발히 접근되는 집합을 커버하는 것이며 과도한 크기는 효과가 체감한다.","Oracle shared pool에서는 library/dictionary cache 경합과 잦은 hard parse를 모니터링한다. 리터럴 SQL 대신 바인드 변수를 사용하면 shared pool 압박이 줄어든다."],"level":"expert"},{"id":"checkpoint-tuning","title":"체크포인트 튜닝","category":"내부 구조","summary":"체크포인트는 dirty 페이지를 디스크에 기록하여 복구 시간을 단축한다. 너무 잦은 체크포인트는 I/O 피크를, 너무 드문 체크포인트는 긴 복구를 만들며 이 트레이드오프를 조정해야 한다.","points":["복구 시간과 런타임 I/O의 균형을 맞춘다: 드문 체크포인트는 쓰기 부하가 적지만 크래시 복구가 길고, 잦은 체크포인트는 그 반대다.","PG에서는 checkpoint_timeout과 max_wal_size로 체크포인트를 시간적으로 분산한다. checkpoint_completion_target으로 쓰기를 간격 전체에 분배하여 I/O 피크를 평활화한다.","MSSQL indirect checkpoint(target recovery time)는 지속적인 백그라운드 쓰기를 제공한다. 급격한 대규모 체크포인트 파동을 막아 지연을 예측 가능하게 한다.","체크포인트 빈도를 로그/WAL 생성 속도와 연관 짓는다. 쓰기가 많은 기간에는 시간 트리거가 아니라 크기 트리거가 작동하는지 모니터링한다.","full_page_writes(PG)는 체크포인트 후 첫 변경 시 전체 페이지를 로그에 기록한다. 체크포인트가 너무 잦으면 WAL 양이 급증하여 DR 및 디스크 비용이 늘어난다.","'checkpoint sink' 증상을 포착한다: 체크포인트 중 쓰기 지연과 fsync 대기의 상승. 스토리지 큐와 분산을 검토한다."],"level":"expert"},{"id":"statistics-strategy","title":"통계 수집 전략","category":"내부 구조","summary":"옵티마이저 결정의 품질은 통계의 최신성과 해상도에 달려 있다. 샘플링 비율, 히스토그램, extended statistics가 카디널리티 추정을 결정한다.","points":["샘플링 비율은 데이터 크기와 치우침에 따라 선택한다: 너무 작은 샘플은 치우친 분포에서 잘못된 추정을 만들고, 너무 큰 샘플은 불필요한 수집 비용을 만든다.","히스토그램으로 치우침을 모델링한다: PG default_statistics_target, Oracle FREQUENCY/HYBRID 히스토그램, MSSQL 통계 히스토그램은 빈번하고 불균형한 값을 옵티마이저에 드러낸다.","extended statistics로 컬럼 간 상관관계를 포착한다: PG CREATE STATISTICS(ndistinct/dependencies)와 Oracle extended stats는 상호 의존 컬럼의 추정 오류를 바로잡는다.","수집 일정을 워크로드와 분리한다: 자동 통계/gather 작업이 중요한 시간대에 실행되어 I/O와 플랜 불안정을 일으키지 않게 한다.","빠르게 커지는 테이블에서 오래된 통계의 위험을 관리한다. 임계값 기반 자동 갱신은 늦을 수 있으므로 중요 테이블에는 수동/예약 수집을 추가한다.","통계 변경이 플랜 회귀를 일으킬 수 있음을 알아 둔다. 중요한 환경에서는 통계를 잠그거나(locked stats) pending publish로 통제된 방식으로 게시한다."],"level":"expert"},{"id":"partition-pruning-join","title":"파티션 프루닝과 partition-wise 조인","category":"확장/아키텍처","summary":"파티셔닝은 테이블을 나누는 데 그치지 않는다. 진짜 이점은 쿼리가 관련 없는 파티션을 전혀 읽지 않고(pruning) 조인이 파티션별로 병렬 실행되는 것이다.","points":["파티션 키는 가장 빈번하고 선택도 높은 필터를 기준으로 선택한다. pruning은 쿼리 조건에 파티션 키가 포함될 때만 동작하며, 그렇지 않으면 모든 파티션을 스캔한다.","정적 pruning과 동적 pruning을 구분한다: 상수 조건은 컴파일 시점에, 바인드/조인 값은 실행 시점에(runtime pruning) 파티션을 제거한다. 그에 따라 플랜을 확인한다.","partition-wise join을 위해 두 테이블을 같은 키와 호환되는 방식으로 파티셔닝한다. 옵티마이저는 대형 조인을 작은 파티션별 조인으로 줄이고 병렬화한다.","파티션 수와 단위의 균형을 맞춘다: 너무 많으면 메타데이터 및 플래닝 비용이, 너무 적으면 약한 pruning이 생긴다. 접근 패턴에 맞게 정한다.","수명 주기 관리를 파티셔닝과 결합한다: 오래된 파티션을 DETACH/DROP하거나 아카이브한다. DELETE 대신 즉시, 적은 로깅으로 정리할 수 있다.","키를 가리는 함수/형 변환이나 표현식은 pruning을 깨뜨린다는 점을 기억한다. EXPLAIN으로 스캔된 파티션 수를 확인한다."],"level":"expert"},{"id":"shard-key-selection","title":"샤드 키 선택과 hot spot","category":"확장/아키텍처","summary":"샤드 키는 분산 시스템의 운명을 결정한다. 잘못 선택하면 부하가 단일 노드에 몰리고(hot shard) 확장이 멈추며 cross-shard 쿼리가 비싸진다.","points":["부하를 고르게 분산하는 카디널리티 높은 키를 선택한다. 카디널리티가 낮거나 시간적/단조 증가하는 키는 부하를 단일 샤드에 몰아 hot spot을 만든다.","가장 빈번한 쿼리가 단일 샤드에서 해결되도록 키를 설계한다. cross-shard(scatter-gather) 쿼리는 지연과 조정 비용을 배가한다.","hash 샤딩은 부하를 고르게 하지만 범위 쿼리를 흩어 놓는다. range 샤딩은 범위 쿼리에 좋지만 hot spot 위험을 높인다. 접근 패턴에 따라 선택한다.","consistent hashing으로 노드 추가/제거 시 재분배를 최소화한다. 고정 modulo hash는 노드 수가 바뀌면 거의 모든 데이터를 옮긴다.","resharding 계획을 처음부터 설계한다. 나중에 키를 바꾸는 것은 비용이 크고 위험하므로 성장 시나리오를 미리 모델링한다.","테넌트/고객 단위로 나눌 때 거대 테넌트(whale)가 단일 샤드를 채우지 않도록 전용 샤드나 하위 분할(split) 전략을 준비한다."],"level":"expert"},{"id":"read-write-splitting","title":"읽기-쓰기 분리와 replica 라우팅","category":"확장/아키텍처","summary":"읽기를 replica로 보내면 쓰기 primary의 부담이 줄지만, 복제 지연이 일관성 보장을 깨뜨릴 수 있으므로 라우팅은 의도적으로 해야 한다.","points":["읽기 부하를 replica에 분산할 때는 복제 지연을 지속적으로 모니터링한다. 오래된 replica는 사용자에게 이전 데이터를 보여 줄 수 있다.","read-your-writes(쓴 사용자가 즉시 읽음)가 필요한 흐름은 primary로 보내거나 causal read/token으로 최신 replica를 보장한다.","라우팅은 애플리케이션이 아니라 인프라 계층에서 한다: proxy/router로 투명하게 분산한다(MySQL은 ProxySQL, PG는 PgBouncer가 읽기/쓰기를 분리하지 않으므로 Pgpool-II나 별도 읽기/쓰기 endpoint를 둔 HAProxy, MSSQL은 application intent를 통한 read-only routing).","지연 임계값을 초과하면 replica를 풀에서 제외한다. 지연이 큰 replica로 읽기를 보내면 '확장'이라는 착각 속에 데이터 불일치가 생긴다.","분석/리포팅 같은 무겁고 지연을 허용하는 읽기는 전용 replica로 격리한다. OLTP replica를 그 부하로부터 보호한다.","페일오버 시나리오에서 읽기 라우팅이 자동으로 갱신되는지 테스트한다. 새 primary를 모르는 router는 split-brain이나 다운타임을 일으킨다."],"level":"expert"},{"id":"connection-pool-sizing","title":"커넥션 풀 크기 계산","category":"확장/아키텍처","summary":"풀 크기는 직관이 아니라 Little's law와 코어 수에 근거한 계산으로 정한다. 접속이 너무 많으면 경합과 context switching으로 처리량이 떨어진다.","points":["Little's law를 적용한다: 필요한 동시성 ≈ 요청 속도(처리량) × 평균 처리 시간. 목표 동시성은 추측이 아니라 측정값에서 도출한다.","작은 풀이 대개 더 빠르다: 활성 접속이 물리 코어/디스크 병렬성을 넘으면 CPU context switching과 latch 경합이 처리량을 떨어뜨린다.","대략적인 출발점으로 코어 수에 가까운 핵심 접속 풀을 사용하고(예: PgBouncer 뒤에 소수의 실제 접속) 부하 상태에서 측정하며 조정한다.","애플리케이션 측 풀은 데이터베이스 max_connections와 함께 계획한다. 애플리케이션 인스턴스 N개 × 풀 크기가 데이터베이스 한도를 넘어서는 안 된다.","PG처럼 접속당 프로세스 비용이 큰 엔진에서는 PgBouncer/transaction pooling을 사용한다. 모든 접속은 메모리와 스케줄링 비용을 낳는다.","풀 포화도, 큐 대기 시간, 타임아웃을 모니터링한다. 계속 커지는 큐는 병목이나 크기 부족의 신호이므로 맹목적으로 풀을 키우지 않는다."],"level":"expert"},{"id":"lock-escalation-deadlock","title":"락 에스컬레이션과 데드락 방지 패턴","category":"신뢰성","summary":"많은 행 락은 테이블 락으로 escalation되어 동시성을 무너뜨릴 수 있다. 데드락은 일관되지 않은 락 순서의 피할 수 있는 결과다.","points":["lock escalation을 유발하는 대량 작업을 나눈다(batch/chunk). 단일 트랜잭션에서 수백만 행에 락을 걸면 escalation과 블로킹이 발생한다.","데드락 방지의 기본 패턴은 일관된 락 순서다: 모든 코드 경로가 같은 순서로(예: primary key 순서) 리소스에 락을 걸어야 한다.","트랜잭션을 짧게 유지하고 하나의 원자적 작업 단위만 수행한다. 사용자 상호작용, 원격 호출, 긴 계산을 포함한 트랜잭션은 락을 불필요하게 오래 보유한다.","격리 수준을 의도적으로 선택한다: 불필요하게 높은 격리는 과도한 락을, 불필요하게 낮은 격리는 이상 현상을 일으킨다. MVCC 엔진에서는 읽기가 이미 대부분의 락을 피한다.","deadlock graph/trace를 수집한다(MSSQL deadlock graph, PG log_lock_waits, Oracle trace). victim을 로그로 남기고 반복 패턴의 근본 원인을 추적한다.","재시도 로직을 구현한다: 데드락 victim 트랜잭션을 idempotent하게 재시도한다. 설계상 피할 수 없는 경합에 대한 안전망이다."],"level":"expert"},{"id":"long-transaction-bloat","title":"긴 트랜잭션과 bloat/undo 관리","category":"신뢰성","summary":"열린 채로 남은 긴 트랜잭션은 MVCC 엔진에서 이전 버전 정리를 막아 bloat, undo 압박, 성능 붕괴의 연쇄를 시작한다.","points":["장시간 실행/열린 트랜잭션을 모니터링하고 알림을 건다: PG의 idle in transaction과 Oracle에서 활성 undo를 보유한 긴 쿼리는 정리(vacuum/undo 회수)를 멈춘다.","PG에서는 단일 긴 트랜잭션이 VACUUM의 dead tuple 제거를 막는다. 테이블과 인덱스가 비대해지며 '좀비' 세션 하나가 데이터베이스 전체를 느리게 할 수 있다.","idle_in_transaction_session_timeout과 statement_timeout으로 멈춘 트랜잭션을 자동 종료한다. 애플리케이션 누수에 대한 방어선을 구축한다.","Oracle/InnoDB에서는 긴 읽기가 undo를 소진하여 'snapshot too old'를 발생시킨다. undo 공간/보존 기간을 워크로드의 가장 긴 쿼리에 맞춘다.","배치 작업은 짧은 커밋 간격으로 나눈다. 하나의 거대한 트랜잭션은 undo/WAL을 쌓고 롤백 시간을 위험할 만큼 늘린다.","기존 bloat를 측정하고 회수한다: pgstattuple로 bloat를 측정하고 VACUUM FULL/pg_repack이나 온라인 재구성으로 공간과 인덱스 효율을 되찾는다."],"level":"expert"},{"id":"dr-game-day","title":"DR 훈련(game day)과 RPO/RTO 검증","category":"신뢰성","summary":"테스트되지 않은 백업 및 페일오버 계획은 가정일 뿐이다. DR 능력은 현실적인 훈련에서 RPO/RTO를 측정해야만 입증된다.","points":["RPO와 RTO를 비즈니스 요구에서 도출하고 실제로 측정한다: 백업/복제 지연이 RPO를, 복구 + 검증 시간이 RTO를 결정한다. 목표와 실제를 비교한다.","백업을 정기적이고 자동으로 복원한다: 한 번도 복원해 보지 않은 백업은 슈뢰딩거의 백업이다. 복원 테스트 없이는 백업이 있다는 주장은 검증되지 않은 것이다.","운영에 가까운 환경에서 현실적인 장애 시나리오(primary 손실, 리전 장애, 데이터 손상)로 game day 훈련을 계획한다.","페일오버와 failback 절차를 end-to-end로 실행한다. DNS/접속 라우팅, 애플리케이션 재접속, replica 승격을 포함한다.","시점 복구(PITR) 능력을 입증한다: 논리적 손상 시나리오에서 로그 아카이브 + full 백업으로 특정 시점으로 돌아갈 수 있는지 테스트한다.","훈련마다 측정된 RTO/RPO, 예상 밖의 사항, 조치 항목을 산출한다. runbook을 갱신하고 다음 훈련에서 개선을 확인한다."],"level":"expert"},{"id":"observability-slo","title":"관측 가능성과 SLO/golden signals","category":"신뢰성","summary":"비즈니스 영향 기준으로 데이터베이스를 관리하려면 golden signals(지연, 트래픽, 오류, 포화도)를 SLO에 연결하고 error budget으로 우선순위를 정한다.","points":["네 가지 golden signals를 모니터링한다: 지연(latency), 트래픽(throughput), 오류율(errors), 포화도(saturation). 근본 원인까지 파고들지 않고도 상태를 요약한다.","지연은 평균이 아니라 백분위(p95/p99)로 측정한다. 평균은 tail latency와 최악의 사용자 경험을 감춘다.","SLO와 error budget을 정의한다. 모든 저수준 지표 변동이 아니라 증상(SLO 위반)에 알림을 걸어 alert fatigue를 방지한다.","리소스에 USE(Utilization, Saturation, Errors) 방법을 적용한다: CPU, 메모리, 디스크 I/O, 커넥션 풀 포화도를 각각 관찰한다.","계층 간 모니터링을 연관 짓는다: 쿼리 -> 대기 이벤트 -> 리소스 포화 -> 비즈니스 지표 체인을 하나의 시간 축에서 본다.","metrics + logs + traces를 함께 구성한다. 지표만으로는 '무엇'에 답하고 로그/트레이스가 '왜'에 답하며, 이들이 없으면 MTTR이 늘어난다."],"level":"expert"},{"id":"least-privilege-roles","title":"최소 권한과 롤 설계","category":"보안","summary":"각 계정이 업무에 필요한 최소 권한만 갖게 하면 침해 시 피해 범위가 좁아진다. 역할은 권한을 직접 부여하는 대신 관리 가능한 추상화를 제공한다.","points":["권한은 사용자가 아니라 역할에 부여한다. 역할을 기능별(reader, writer, DDL, 운영)로 설계하고 사용자를 역할에 할당하며 직접 grant를 피한다.","애플리케이션 계정에는 필요한 객체 및 작업 권한만 준다. 운영 애플리케이션 계정에 DDL, SUPERUSER/DBA, 광범위한 스키마 권한이 있어서는 안 된다.","사람 관리자의 접근을 개인화하고 감사한다: 공유 관리자 계정 대신 개인 ID + 필요 시 권한 상승(JIT/break-glass) 접근을 사용한다.","PUBLIC/기본 광범위 권한을 회수한다. default privileges와 스키마 수준 통제를 구성하여 새 객체에 기본 권한이 새지 않게 한다.","권한 있는 계정을 정기적으로 검토한다(access recertification). 사용하지 않는 계정, 퇴사자 계정, 과도한 권한 계정을 주기적으로 정리한다.","직무 분리를 강제한다. 같은 사람이 운영 환경을 변경하고 감사 추적도 지울 수 있어서는 안 되므로 감사 기록을 별도 권한으로 보호한다."],"level":"expert"},{"id":"encryption-decision","title":"암호화 결정: 저장 데이터, 전송 구간, TDE","category":"보안","summary":"암호화는 위협 모델에 따라 선택한다. 각 계층은 서로 다른 공격을 막으며, 키 관리가 암호화 알고리즘보다 더 중요한 경우가 많다.","points":["위협을 명확히 한다: at-rest(디스크/백업 도난), in-transit(네트워크 도청), 애플리케이션 수준(DBA를 포함해 누구에게도 노출되지 않음)은 서로 다른 보호다. 어떤 것이 필요한지 결정한다.","in-transit에는 클라이언트-서버 접속에 TLS를 강제한다. 인증서 검증을 비활성화하지 않는다. 중간자(MITM) 공격에 노출된다.","at-rest에는 TDE와 파일 시스템/디스크 암호화 옵션을 비교한다. TDE는 데이터파일과 백업을 보호하지만 메모리 내 데이터나 권한 있는 DBA 접근은 보호하지 않는다.","가장 민감한 필드에는 애플리케이션/컬럼 수준 암호화나 Always Encrypted 같은 클라이언트 측 방식을 사용하여 DBA도 평문을 볼 수 없게 한다.","키 관리를 중요하게 다룬다: 키는 데이터와 분리된 KMS/HSM에 저장하고 교체와 접근 제어를 계획한다. 키가 유출되면 암호화는 무의미하다.","성능 및 기능 영향을 측정한다: 암호화된 컬럼에서는 인덱싱/검색이 제한되고 TDE는 CPU 비용을 더한다. 측정된 트레이드오프로 결정한다."],"level":"expert"},{"id":"audit-strategy","title":"감사 전략","category":"보안","summary":"감사 추적은 보안 사건과 컴플라이언스 요건의 증거를 제공한다. 무엇을 기록하고 추적 기록을 어디로 보내며 얼마나 보관하는지가 노이즈와 증거의 균형을 정한다.","points":["무엇을 감사할지 위협과 컴플라이언스에서 도출한다: 권한 있는 작업, 로그인/로그인 실패, 스키마(DDL) 변경, 민감 데이터 접근을 우선한다.","모든 것을 기록하는 함정에 빠지지 않는다. 과도한 감사는 성능을 저하시키고 신호를 노이즈에 묻히게 하므로 중요한 이벤트를 선택적으로, 목적을 갖고 대상으로 한다.","감사 추적은 운영 DBA가 수정할 수 없는 별도의 안전한 대상(외부 로그 수집기/SIEM)에 기록한다. 직무 분리로 추적 기록 조작을 방지한다.","엔진 기능을 활용한다: Oracle Unified/Fine-Grained Audit, MSSQL SQL Audit, PG pgAudit로 상세 수준을 요구에 맞게 조정한다.","보존 기간은 컴플라이언스 요건(KVKK/GDPR/PCI-DSS 등)에 따라 정하고 아카이브/정리 정책을 자동화한다. 추적 기록의 무결성을 보존한다.","추적 기록을 수집만 하지 말고 정기적으로 검토하고 알림과 연결한다. 감사는 죽은 아카이브가 아니라 이상을 표시하고 조치로 이어질 때만 가치를 만든다."],"level":"expert"}]},"guide/best-practices-4.json":{"topics":[{"id":"oracle-rac-cache-fusion","title":"Oracle RAC: Cache Fusion과 interconnect","category":"Oracle","summary":"RAC에서는 여러 인스턴스가 같은 데이터베이스를 공유한다. Cache Fusion은 디스크 대신 interconnect를 통해 노드 간에 블록을 전송하여 일관성을 유지한다.","points":["인스턴스가 다른 인스턴스의 buffer cache에 있는 블록을 필요로 하면 Cache Fusion은 private interconnect를 통해 RAM에서 RAM으로 전송한다. 디스크에 기록했다가 다시 읽지 않는다(disk ping).","Global Cache Service(GCS/LMS 프로세스)는 블록 소유권과 모드(shared/exclusive)를 관리하고, Global Enqueue Service(GES)는 락과 enqueue를 관리한다.","모든 블록에는 master 노드가 있다. 블록 위치는 Global Resource Directory에서 추적되며 접근 강도에 따라 DRM(Dynamic Resource Mastering)으로 remaster될 수 있다.","interconnect에는 지연이 낮은 전용 네트워크가 필요하다. Jumbo Frames(MTU 9000)와 필요하면 RDMA/RoCE 또는 InfiniBand를 사용하며 public 네트워크와 절대 공유하지 않는다.","'gc buffer busy' 및 'gc cr/current block busy' 대기 이벤트는 hot block 경합을 나타낸다. sequence CACHE 증가, reverse key 인덱스, 파티셔닝으로 부하를 분산하는 것이 도움이 될 수 있다.","gc blocks lost 통계가 계속 증가하면 interconnect에서 패킷 손실이 있는 것이다. NIC, 스위치 버퍼, 소켓 버퍼 크기(rmem/wmem)를 검토한다."],"level":"expert"},{"id":"oracle-rac-node-eviction","title":"Oracle RAC: 노드 축출(node eviction)과 split-brain","category":"Oracle","summary":"Clusterware는 split-brain을 막기 위해 응답하지 않는 노드를 클러스터에서 축출한다. network heartbeat, disk heartbeat, voting disk 과반수를 근거로 결정한다.","points":["CSSD는 두 개의 별도 heartbeat를 모니터링한다: interconnect를 통한 network heartbeat(misscount, 기본 30 s)와 voting disk를 통한 disk heartbeat(disktimeout, 기본 200 s).","split-brain 시 살아남는 하위 클러스터(cohort)는 voting disk 과반수로 결정된다. 따라서 voting disk 수는 홀수(3 또는 5)여야 하며 서로 다른 failure group에 있어야 한다.","eviction은 대개 노드 재부팅이나 재부팅 없는 fencing(스택만 재시작)으로 수행된다. 목적은 장애 노드가 공유 스토리지에 쓰지 못하게 하는 것이다.","흔한 eviction 원인: interconnect 손실, 고부하에서 CPU를 받지 못한 CSSD, voting disk I/O 지연, OCR/voting disk 접근 문제.","근본 원인 분석을 위해 ocssd.log, CSSD trace, alert log, OS 메시지를 CHM/oclumon(Cluster Health Monitor) 데이터와 함께 검토한다."],"level":"expert"},{"id":"oracle-asm-redundancy-rebalance","title":"Oracle ASM: disk group 이중화와 rebalance","category":"Oracle","summary":"ASM은 disk group 내 디스크에 데이터를 스트라이핑하고 failure group별로 미러링하여 보호한다. 디스크 추가/제거 시 익스텐트를 자동으로 rebalance한다.","points":["redundancy 수준: EXTERNAL(미러링 없음, 보호는 스토리지 어레이에 맡김), NORMAL(사본 2개, 디스크 1개 손실 허용), HIGH(사본 3개, 동시 2개 손실 허용).","ASM은 디스크가 아니라 failure group을 미러링한다. primary 익스텐트는 한 failure group에, 미러 사본은 다른 failure group에 있으므로 failure group 전체가 장애가 나도 데이터가 손실되지 않는다.","디스크를 추가하거나 제거하면 익스텐트를 재분배하는 rebalance가 시작된다. ASM_POWER_LIMIT(0-1024)이 속도와 리소스 소비의 균형을 정한다.","AU(Allocation Unit) 크기는 disk group 생성 시 정해진다. 대형 데이터베이스에서는 4 MB AU가 I/O 효율을 높인다.","voting disk와 OCR은 normal/high redundancy disk group에 저장할 수 있다. quorum failure group은 voting disk에만 사용되며 데이터 익스텐트를 보관하지 않는다."],"level":"expert"},{"id":"oracle-data-guard-modes","title":"Oracle Data Guard: standby 유형과 보호 모드","category":"Oracle","summary":"Data Guard는 redo를 standby로 전송하여 재해 복구를 제공한다. physical/logical standby 선택과 세 가지 보호 모드가 데이터 손실과 가용성의 균형을 정한다.","points":["physical standby는 primary와 블록 단위로 동일한 사본이다(Redo Apply/MRP). Active Data Guard를 사용하면 open 상태에서 읽기 전용으로 조회할 수 있다. logical standby는 redo를 SQL로 변환하여 적용하며(SQL Apply) 다른 구조/인덱스를 둘 수 있다.","Maximum Performance(기본값): redo는 비동기(ASYNC)로 전송되며 primary는 확인 응답을 기다리지 않는다. 성능 영향은 없지만 데이터 손실 위험이 약간 있다.","Maximum Availability: redo는 동기(SYNC/AFFIRM)로 전송된다. standby에 연결할 수 없으면 primary는 중단되지 않고 MAX PERFORMANCE 동작으로 전환되며, standby가 복귀하면 재동기화한다.","Maximum Protection: 최소 하나의 standby가 확인 응답할 때까지 primary는 커밋하지 않는다. 데이터 손실 제로를 보장하지만 모든 standby를 잃으면 primary가 종료된다.","Fast-Start Failover(FSFO)는 observer 프로세스를 사용해 primary 장애 시 자동으로 페일오버한다. FastStartFailoverThreshold와 동기 전송으로 RPO=0을 목표로 한다.","standby의 적용 지연은 v$dataguard_stats와 'apply lag'/'transport lag' 지표로 모니터링한다. real-time apply는 redo가 아카이브되기 전에 적용한다."],"level":"expert"},{"id":"oracle-exadata-offload","title":"Oracle Exadata: Smart Scan과 storage index","category":"Oracle","summary":"Exadata에서는 스토리지 셀이 쿼리 처리의 일부를 맡아(offload) 관련 데이터만 DB 노드로 반환한다. Smart Scan, storage index, HCC가 I/O를 크게 줄인다.","points":["full scan 쿼리에서 Smart Scan은 스토리지 셀에서 조건절 필터링과 컬럼 프로젝션을 수행한다. 블록이 아니라 필요한 행/컬럼만 DB 노드로 반환되므로 인터커넥트 트래픽이 줄어든다.","Smart Scan은 direct path read에서만 동작하며, buffer cache에서 읽은 블록이나 일부 작업에는 적용되지 않는다. 효과는 'cell physical IO bytes eligible for predicate offload'와 'cell physical IO interconnect bytes returned by smart scan' 통계로 측정한다('cell physical IO bytes saved by storage index'는 storage index 절감분만 보여 준다).","storage index는 1 MB 스토리지 영역마다 컬럼의 min/max 값을 메모리에 유지한다. 쿼리 조건이 범위를 벗어나면 해당 영역은 읽지 않고 건너뛴다(네거티브 인덱스처럼 동작).","storage index는 영구적이지 않다. 메모리에 존재하며 셀이 재시작되면 다시 구축된다. 물리적으로 정렬된 데이터(clustering)는 건너뛰기 비율을 높인다.","HCC(Hybrid Columnar Compression)는 Compression Unit 안에서 데이터를 컬럼 단위로 압축한다. QUERY LOW/HIGH는 조회가 많은 데이터용이고, ARCHIVE LOW/HIGH는 거의 접근하지 않는 아카이브 데이터용으로 높은 CPU 비용으로 매우 높은 압축률을 제공한다."],"level":"expert"},{"id":"oracle-awr-ash-analysis","title":"Oracle AWR과 ASH를 이용한 성능 분석","category":"Oracle","summary":"AWR은 주기적으로 성능 스냅샷을 수집하고, ASH는 매초 활성 세션을 샘플링한다. 둘을 함께 사용하면 대기 이벤트 관점에서 '시간이 어디에 쓰였는가'에 답할 수 있다.","points":["기본적으로 AWR은 60분마다 스냅샷을 생성하고 8일간 보관한다. 두 스냅샷 사이의 AWR 보고서는 어떤 SQL과 대기 이벤트가 DB Time을 소비했는지 보여 준다(Diagnostics Pack 라이선스 필요).","분석은 항상 'Top 10 Foreground Events by Total Wait Time'과 DB Time에서 시작한다. 개별 통계에 빠지지 말고 그곳에서 CPU vs I/O vs 동시성 균형을 확인한다.","ASH는 초당 한 번 v$active_session_history를 샘플링하여 짧고 갑작스러운 문제(spike)를 포착한다. AWR이 놓치는 순간적인 lock/latch 폭주를 SQL_ID와 blocking session별로 드러낸다.","Average Active Sessions(AAS = DB Time / 경과 시간)는 시스템 부하를 하나의 숫자로 요약한다. AAS가 CPU 코어 수를 넘으면 포화 상태를 의미한다.","SQL 튜닝 시 AWR의 'SQL ordered by Elapsed Time/Gets' 섹션에서 가장 비용이 큰 SQL_ID를 찾고, ASH로 해당 SQL이 어떤 단계에서 대기하는지 확인한 뒤 SQL Monitor 보고서로 계획을 줄 단위로 분석한다."],"level":"expert"},{"id":"oracle-scn-read-consistency","title":"Oracle SCN과 읽기 일관성","category":"Oracle","summary":"System Change Number(SCN)는 데이터베이스의 논리적 시계다. Oracle은 쿼리가 시작된 SCN 시점 기준으로 undo에서 일관된 이미지(읽기 일관성)를 구성하여 읽기와 쓰기를 분리한다.","points":["SCN은 커밋마다 단조 증가하며 redo/undo, 컨트롤 파일, 블록 헤더에 기록된다. 블록의 마지막 변경 SCN이 쿼리 SCN보다 크면 해당 블록은 '롤백'된다.","읽기 일관성을 위해 Oracle은 현재 블록의 CR(Consistent Read) 사본을 만들고 undo를 사용해 쿼리 시작 SCN으로 롤백한다. 그 결과 '읽기는 쓰기를 막지 않고, 쓰기는 읽기를 막지 않는다'.","undo가 부족하거나 undo_retention을 초과하면 장시간 실행 쿼리에서 'ORA-01555 snapshot too old'가 발생한다. 해결책은 undo 테이블스페이스 확장, retention 조정 또는 쿼리 단축이다.","문장 수준 일관성은 SQL 문마다 별도의 SCN을 사용한다. SERIALIZABLE 또는 읽기 전용 트랜잭션은 트랜잭션 시작 시점의 단일 SCN을 모든 문장에 유지한다.","RAC에서 SCN은 모든 인스턴스 간에 동기화된다(Lamport 또는 broadcast-on-commit). 전역 일관성을 위해 SCN 증가는 노드 간에 조정된다."],"level":"expert"},{"id":"postgres-patroni-failover","title":"PostgreSQL: Patroni를 이용한 자동 페일오버","category":"PostgreSQL","summary":"Patroni는 PostgreSQL 클러스터 상태를 분산 합의 저장소(DCS)에 유지하여 리더 선출과 자동 페일오버를 제공한다. watchdog으로 split-brain을 방지한다.","points":["클러스터 상태와 리더 락은 etcd/Consul/ZooKeeper 같은 DCS에 저장된다. 리더는 TTL이 있는 락을 주기적으로 갱신하며, 갱신하지 못하면 락이 만료되고 새 리더 선출이 시작된다.","페일오버 시 Patroni는 가장 최신 상태의 replica(LSN이 가장 높은 것)를 선택한다. 새 리더가 승격되고 다른 replica는 새 리더에 다시 연결된다(reinitialize/rewind).","watchdog(softdog)은 이전 리더가 DCS와 연결을 잃으면 머신을 리셋하는 하드웨어 수준의 안전장치로, 두 노드가 동시에 리더가 되는 것(split-brain)을 방지한다.","maximum_lag_on_failover는 크게 뒤처진 replica가 리더가 되어 데이터 손실을 일으키는 것을 막는다. synchronous_mode로 RPO=0을 목표로 할 수 있지만 쓰기 지연이 증가한다.","클라이언트는 노드에 직접 연결하지 말고 Patroni REST 헬스 체크를 사용하는 HAProxy 또는 VIP를 통해 연결해야 한다. 그러면 리더가 바뀔 때 트래픽이 자동으로 새 리더로 라우팅된다.","DCS 과반수(quorum)는 필수다. 3개 또는 5개 멤버의 etcd 클러스터를 사용한다. DCS에 연결할 수 없으면 Patroni는 안전하게 동작하여 현재 리더를 읽기 전용으로 강등할 수 있다."],"level":"expert"},{"id":"postgres-logical-replication-upgrade","title":"PostgreSQL: 논리 복제를 이용한 무중단 메이저 업그레이드","category":"PostgreSQL","summary":"논리 복제는 버전 간에 행 변경을 전달할 수 있으므로 거의 무중단으로 메이저 버전 업그레이드를 가능하게 한다. 구 버전과 신 버전이 한동안 병행 운영된다.","points":["논리 복제는 WAL의 변경 사항을 논리적으로 디코딩한다(INSERT/UPDATE/DELETE). 물리 복제와 달리 서로 다른 메이저 버전과 플랫폼 간에도 동작한다.","흐름: 신 버전에 빈 스키마를 만들고, 구 버전에 PUBLICATION을, 신 버전에 SUBSCRIPTION을 정의한다. 먼저 테이블이 복사되고(초기 동기화) 이후 실시간 변경이 흐르기 시작한다.","모든 테이블에는 REPLICA IDENTITY가 있어야 한다. primary key가 없는 테이블에서 UPDATE/DELETE를 복제하려면 REPLICA IDENTITY FULL이 필요하며, 그렇지 않으면 오류가 발생한다.","DDL 변경은 자동으로 복제되지 않으며 시퀀스 현재 값도 전달되지 않는다. 전환 시 신 버전에서 시퀀스를 수동으로 앞당기지 않으면 키 충돌이 발생한다.","전환 시 구 버전에 쓰는 애플리케이션을 잠시 중지하고, 지연이 0이 되면(pg_stat_replication으로 확인) 애플리케이션을 신 버전으로 전환한다. 필요하면 빠른 롤백을 위해 역방향 subscription을 준비해 둔다.","제약 사항: large object와 일부 DDL은 복제되지 않는다. wal_level=logical과 충분한 max_replication_slots/max_wal_senders를 설정해야 한다."],"level":"expert"},{"id":"postgres-pgvector-similarity","title":"PostgreSQL: pgvector를 이용한 유사도 검색","category":"PostgreSQL","summary":"pgvector는 벡터 임베딩을 저장하고 근사 최근접 이웃(ANN) 검색을 수행한다. HNSW와 IVFFlat 인덱스가 정확도-속도-메모리 간 트레이드오프를 결정한다.","points":["용도에 따라 거리 연산자를 선택한다: <-> L2(유클리드), <=> 코사인, <#> 내적. 인덱스는 쿼리와 같은 operator class를 사용해야 하며, 그렇지 않으면 사용되지 않는다.","HNSW는 다층 그래프를 구축한다. 높은 recall과 빠른 쿼리를 제공하지만 메모리를 더 많이 쓰고 구축이 느리다. m(연결 수)과 ef_construction은 구축 품질을, ef_search는 쿼리 시 정확도를 조정한다.","IVFFlat은 벡터를 lists 개의 클러스터로 분할하고 쿼리마다 probes 개의 클러스터만 스캔한다. 메모리 사용이 적지만 인덱스는 데이터 적재 후에 생성해야 한다(빈 테이블에서는 클러스터가 의미 없음).","IVFFlat은 100만 행까지 lists ≈ rows/1000, 100만 행 초과 시 ≈ sqrt(rows)로 시작한다. probes를 늘리면 recall이 높아지지만 쿼리가 느려지므로 측정을 통해 이 트레이드오프를 조정한다.","ANN은 근사 결과를 반환한다. 100% 정확성이 필요하면 인덱스 없이 정확한 스캔이 필요하다. 필터(WHERE) 검색에서는 인덱스 recall이 떨어질 수 있으므로 필요하면 iterative scan이나 부분 인덱스를 고려한다.","벡터 차원과 행 수가 메모리 요구량을 결정한다. 구축 중에는 maintenance_work_mem을 충분히 크게 유지한다. 그렇지 않으면 HNSW 구축이 디스크로 넘쳐 매우 느려진다."],"level":"expert"},{"id":"postgres-citus-distributed","title":"PostgreSQL: Citus를 이용한 분산 확장","category":"PostgreSQL","summary":"Citus는 분산 컬럼 기준으로 테이블을 샤드로 나누어 worker 노드에 분산함으로써 PostgreSQL을 수평 확장한다. 분산 컬럼 선택이 성능을 결정한다.","points":["coordinator 노드는 쿼리를 받아 분산 컬럼을 기준으로 관련 샤드에 라우팅한다. worker 노드는 샤드를 저장하고 병렬로 처리한다. 샤드는 표준 PostgreSQL 테이블이다(해시 범위로 분할).","분산 컬럼은 대부분의 쿼리와 JOIN의 공통 필터여야 한다(예: tenant_id). 같은 컬럼 값을 가진 행은 같은 샤드에 위치하므로 JOIN과 foreign key가 같은 노드 안에서 로컬로 유지된다(colocation).","colocation은 매우 중요하다. 함께 조인되는 테이블이 같은 분산 컬럼과 샤드 수로 분산되어 있으면 노드 간 데이터 셔플(repartition) 없이 실행된다.","모든 샤드와 자주 조인되는 작은 테이블은 reference table로 만든다. 모든 worker에 전체 복사되어 분산 JOIN의 네트워크 비용을 없앤다.","분산 컬럼을 포함하지 않는 쿼리는 모든 샤드로 fan-out되어 비용이 크다. 멀티테넌트 SaaS에서는 tenant_id로 필터링하면 쿼리가 단일 샤드로 축소된다(router execution)."],"level":"expert"},{"id":"postgres-timescaledb-hypertable","title":"PostgreSQL: TimescaleDB hypertable과 continuous aggregate","category":"PostgreSQL","summary":"TimescaleDB는 시계열 데이터를 시간 구간 chunk로 자동 분할하는 hypertable과 미리 계산된 continuous aggregate로 확장한다.","points":["hypertable은 외부에서 하나의 테이블처럼 보이지만 내부적으로는 시간(및 선택적으로 공간) 컬럼 기준으로 chunk로 분할된다. 쿼리는 해당 시간 범위의 chunk만 접근한다(chunk exclusion).","chunk_time_interval은 가장 활발한 chunk가 메모리(shared_buffers)에 들어가도록 설정한다. 너무 작으면 chunk/계획 수립 오버헤드가 과도해지고, 너무 크면 exclusion 효과가 약해진다.","continuous aggregate는 증분 갱신되는 materialized view다. 변경된 time bucket만 재계산하는 refresh policy에 의해 백그라운드에서 최신 상태로 유지된다.","real-time aggregation을 사용하면 continuous aggregate가 구체화된 과거 데이터와 원본 테이블의 최신 데이터를 결합하여 쿼리 시점에 최신 결과를 반환한다.","오래된 chunk는 컬럼형 압축으로 크게 줄일 수 있다. retention policy는 drop_chunks로 일정 기간보다 오래된 chunk를 자동 삭제한다(행 단위 DELETE보다 훨씬 저렴)."],"level":"expert"},{"id":"postgres-vacuum-xid-wraparound","title":"PostgreSQL: VACUUM과 XID wraparound","category":"PostgreSQL","summary":"MVCC는 dead row 버전을 남긴다. VACUUM은 이를 정리하고 행을 'frozen'으로 표시하여 32비트 트랜잭션 ID(XID)의 wraparound로 인한 데이터 손실을 방지한다.","points":["PostgreSQL은 순환하는 32비트 XID를 사용하며 모든 행 버전에 xmin이 기록된다. 충분히 오래된 행을 freeze하지 않으면 XID wraparound 시 '과거' 트랜잭션이 '미래'에 있는 것처럼 보여 행이 갑자기 보이지 않게 될 수 있다.","autovacuum은 두 가지 작업을 한다: 일반 정리(dead tuple, bloat)와 freeze. autovacuum_freeze_max_age에 근접한 테이블에는 autovacuum이 비활성화되어 있어도 필수 'anti-wraparound' vacuum이 실행된다.","wraparound가 가까워지면 경고가 로그에 기록된다. 한계에 매우 근접하면 데이터베이스는 데이터 손실을 막기 위해 새 쓰기를 거부하고 읽기 전용이 되며, 해결책은 공격적인 VACUUM(FREEZE) 실행이다.","가장 오래된 미동결 XID는 pg_database.datfrozenxid와 age()로 모니터링한다. 장시간 열린 트랜잭션(idle in transaction)과 사용하지 않는 replication slot은 VACUUM 진행을 막아 bloat와 wraparound 위험을 높인다.","쓰기 부하가 높으면 autovacuum이 뒤처져 bloat가 발생한다. autovacuum_vacuum_cost_limit을 높이고 naptime을 낮추며, 크고 활발한 테이블에는 테이블별 autovacuum 설정을 적용한다.","VACUUM은 일반적으로 공간을 운영체제에 반환하지 않고 재사용 가능으로 표시한다. 물리적 축소에는 VACUUM FULL(테이블 락) 또는 pg_repack(온라인)이 필요하다."],"level":"expert"},{"id":"mysql-innodb-cluster","title":"MySQL: Group Replication과 InnoDB Cluster","category":"MySQL","summary":"InnoDB Cluster는 Group Replication 기반의 과반수(quorum) 클러스터로, MySQL Router를 통해 애플리케이션에 단일 연결 지점을 제공하여 고가용성을 구현한다.","points":["Group Replication은 Paxos 기반 그룹 통신 계층을 통해 트랜잭션을 그룹에 전파한다. 트랜잭션은 그룹 과반수(quorum)가 동의한 후에만 커밋되며, 이를 통해 자동 페일오버와 일관성을 제공한다.","single-primary 모드(기본값)는 한 노드에서만 쓰기를 허용하며, primary 장애 시 그룹이 자동으로 새 primary를 선출한다. multi-primary 모드에서는 모든 노드가 쓰기를 받지만 충돌(certification) 처리가 더 복잡해진다.","MySQL Router는 클러스터 메타데이터를 읽어 애플리케이션에 두 개의 포트를 제공한다: 쓰기는 현재 primary(6446), 읽기는 replica(6447)로 간다. 페일오버 시 애플리케이션 연결을 바꾸지 않고 라우팅이 갱신된다.","과반수 상실(예: 3개 중 2개 노드 손실) 시 그룹이 차단된다. split-brain을 막기 위해 클러스터는 새 쓰기를 받지 않으며, 복구는 group_replication_force_members로 수동 수행한다.","Group Replication에는 행 기반 binlog(ROW 형식), GTID, InnoDB가 필요하다. 모든 테이블에 primary key가 필수이며, 없으면 노드가 클러스터에 참여할 수 없다.","새로 참여하는 노드는 먼저 기존 donor로부터 상태 전송으로 동기화한 뒤(distributed recovery: clone plugin 또는 binlog) 실시간 스트림으로 전환한다."],"level":"expert"},{"id":"mysql-semisync-replication","title":"MySQL: semi-sync 복제","category":"MySQL","summary":"반동기(semi-synchronous) 복제는 primary가 커밋을 확인하기 전에 최소 하나의 replica가 트랜잭션을 relay log에 수신할 때까지 기다리게 하여 데이터 손실을 줄인다.","points":["비동기 복제에서 primary는 replica를 기다리지 않고 커밋을 확인한다. primary가 중단되면 아직 전송되지 않은 트랜잭션은 손실된다. semi-sync는 최소 N개 replica의 수신 확인을 기다린다(rpl_semi_sync_source_wait_for_replica_count).","AFTER_SYNC(기본값, 권장): primary는 먼저 replica 확인을 받은 뒤 클라이언트에 커밋을 반환한다. 이를 통해 클라이언트가 '성공'으로 본 모든 트랜잭션이 최소 하나의 replica에 존재함을 보장한다.","AFTER_COMMIT 모드에서는 primary가 먼저 로컬 커밋한 후 replica를 기다린다. replica가 지연되면 다른 세션이 아직 복제되지 않은 데이터를 볼 수 있다(phantom read와 유사).","rpl_semi_sync_source_timeout을 초과하면 primary는 중단되지 않고 자동으로 비동기로 전환된다. replica가 복귀하면 semi-sync로 돌아가므로 '보장'은 조건부다.","semi-sync는 'replica에 적용됨'이 아니라 '수신됨'을 보장한다. 엄격한 RPO=0에는 Group Replication이나 외부 합의 계층이 더 적합하다."],"level":"expert"},{"id":"mysql-undo-purge","title":"MySQL InnoDB: undo 테이블스페이스와 purge","category":"MySQL","summary":"InnoDB는 MVCC를 위해 이전 행 버전을 undo log에 유지한다. purge 스레드는 더 이상 어떤 읽기에도 필요 없는 undo 레코드와 dead 버전을 정리한다.","points":["모든 변경은 롤백과 일관된 읽기를 위한 undo log 레코드를 생성한다. 장시간 트랜잭션은 오래된 read view를 유지하여 purge 진행을 막고 undo를 증가시킨다(history list length 증가).","history list length(SHOW ENGINE INNODB STATUS)가 계속 증가하면 purge가 따라가지 못하는 것이다. 주원인은 대개 장시간 열린 트랜잭션이나 활성 상태로 유지되는 오래된 읽기다(예: mysqldump --single-transaction).","innodb_purge_threads는 purge 병렬도를 제어하고, innodb_max_purge_lag는 purge가 크게 뒤처질 때 DML에 인위적 지연을 추가하여 부하가 purge를 압도하지 않도록 한다.","MySQL 8에서는 undo가 별도의 undo 테이블스페이스에 유지되며 커진 undo 파일은 자동 truncate(innodb_undo_log_truncate)로 줄일 수 있다. 이전 버전에서는 undo가 시스템 테이블스페이스에 있어 줄어들지 않았다.","비대해진 undo는 일관된 읽기가 더 긴 버전 체인을 따라가게 만든다(I/O와 CPU). 짧은 트랜잭션과 autocommit 규율로 undo 부담을 최소화한다."],"level":"expert"},{"id":"mysql-buffer-pool-lru","title":"MySQL InnoDB: buffer pool 인스턴스와 LRU","category":"MySQL","summary":"InnoDB buffer pool은 데이터와 인덱스 페이지를 메모리에 유지한다. midpoint insertion LRU 알고리즘은 테이블 스캔이 hot 데이터를 메모리에서 밀어내지 않도록 한다.","points":["전용 데이터베이스 서버에서 buffer pool은 보통 RAM의 ~70-80%로 설정한다(innodb_buffer_pool_size, 공유 서버에서는 더 낮게). 목표는 자주 접근하는 working set을 메모리에 유지하고 디스크 I/O를 최소화하는 것이다.","LRU 리스트는 'young'(hot)과 'old'(cold) 하위 리스트로 나뉜다. 새로 읽은 페이지는 리스트 맨 앞이 아니라 old 영역의 앞(midpoint, ~37%)에 삽입되므로 일회성 스캔이 hot 페이지를 밀어내지 않는다.","old 영역의 페이지는 innodb_old_blocks_time이 지난 후 다시 접근될 때만 young으로 승격된다. 이를 통해 대규모 full scan이 buffer pool을 오염시키는 것을 막는다.","innodb_buffer_pool_instances는 pool을 여러 개의 독립 인스턴스로 나눈다. 인스턴스마다 자체 mutex가 있으므로 높은 동시성에서 buffer pool mutex 경합이 줄어든다(큰 pool에서 의미 있음).","buffer pool hit ratio와 free/dirty 페이지 균형을 모니터링한다. dirty 페이지가 많고 flush가 공격적이면 'checkpoint storm'이 발생할 수 있으며, innodb_io_capacity로 스토리지에 맞게 flush 속도를 설정한다."],"level":"expert"},{"id":"sqlserver-always-on-ag","title":"SQL Server: Always On Availability Groups","category":"SQL Server","summary":"Availability Group은 데이터베이스 그룹을 동기/비동기 replica로 복제한다. WSFC 위에서 자동 페일오버, 읽기 가능 replica, read-only routing을 제공한다.","points":["synchronous-commit replica에서는 primary가 커밋 전에 secondary가 로그 레코드를 harden할 때까지 기다린다. RPO=0을 제공하지만 replica가 지연되면 primary의 커밋 지연이 증가한다.","asynchronous-commit replica는 primary를 기다리게 하지 않는다. 원격 재해 복구(DR) 사이트에 적합하지만 페일오버 시 일부 데이터가 손실될 수 있으며(RPO>0) 강제 수동 페일오버(FORCE_FAILOVER_ALLOW_DATA_LOSS)만 지원된다.","자동 페일오버는 WSFC quorum이 유지되는 상태에서 synchronous-commit 및 'automatic failover' 모드인 replica 쌍 사이에서만 발생한다. 헬스 체크는 sp_server_diagnostics와 database health detection으로 수행한다.","읽기 가능 secondary replica는 primary의 보고서 부하를 분산한다. read-only routing 덕분에 ApplicationIntent=ReadOnly로 연결하는 클라이언트는 listener를 통해 읽기 가능 replica로 자동 라우팅된다.","읽기 가능 replica의 쿼리는 자동으로 snapshot isolation을 사용한다. 이를 위해 primary에서 변경된 행에 14바이트 버전 태그가 추가되고 secondary의 tempdb version store에 부하가 걸린다. 또한 redo 적용이 지연되면 replica의 데이터가 오래된 상태가 될 수 있다.","AG listener는 가상 네트워크 이름 + IP로 클라이언트에 단일 연결 지점을 제공한다. 페일오버 시 listener가 새 primary로 이동하며 애플리케이션은 연결 문자열을 바꾸지 않고 계속 동작한다."],"level":"expert"},{"id":"sqlserver-columnstore","title":"SQL Server: columnstore 인덱스와 batch mode","category":"SQL Server","summary":"columnstore 인덱스는 데이터를 높은 압축률로 컬럼 단위 저장한다. batch mode 처리와 segment elimination으로 분석 쿼리를 크게 가속한다.","points":["데이터는 rowgroup(~100만 행) 안에서 컬럼 세그먼트로 나뉘고 각 세그먼트가 압축된다. 쿼리에 필요한 컬럼만 읽으므로 I/O가 크게 줄어든다.","각 세그먼트는 min/max 메타데이터를 가진다. 쿼리 조건이 세그먼트의 값 범위를 벗어나면 해당 세그먼트는 읽지 않고 건너뛰므로(segment elimination) 데이터를 정렬된 순서로 적재하는 것이 매우 중요하다.","batch mode는 행 단위가 아니라 ~900개 행 그룹 단위로 처리하여 CPU 사이클당 처리량을 높인다. 집계와 조인에서 row mode보다 몇 배 빠르다(SQL 2019+에서는 rowstore에서도 batch mode 가능).","clustered columnstore는 대규모 분석/DW 팩트 테이블에 이상적이다. OLTP + 분석 혼합 워크로드에서는 rowstore 테이블에 nonclustered columnstore를 두어 실시간 운영 분석(HTAP)을 구현할 수 있다.","아직 압축되지 않은 hot 행은 deltastore에 행 단위로 유지되다가 가득 차면 tuple mover가 압축한다. 소규모 적재가 많으면 단편화가 발생하므로 대량 적재를 우선한다."],"level":"expert"},{"id":"sqlserver-inmemory-oltp","title":"SQL Server: In-Memory OLTP(Hekaton)","category":"SQL Server","summary":"In-Memory OLTP는 memory-optimized 테이블을 lock/latch-free 구조와 natively compiled 프로시저로 처리하여 고동시성 OLTP 워크로드의 병목을 제거한다.","points":["memory-optimized 테이블은 전체가 메모리에 유지되며 모든 행 버전도 메모리에 존재한다. 동시성은 lock-free 낙관적 MVCC로 처리되어 기존의 latch 및 락 경합이 사라진다.","지속성을 위해 변경 사항은 여전히 로그에 기록되고 checkpoint 파일(data/delta)로 flush된다. SCHEMA_ONLY 테이블은 로깅되지 않으며 재시작 시 비워진다(예: 임시 staging/세션 데이터에 이상적).","natively compiled 저장 프로시저는 T-SQL을 기계어(DLL)로 컴파일하여 해석 오버헤드를 없앤다. memory-optimized 테이블에만 접근할 수 있으며 제한된 T-SQL 하위 집합을 지원한다.","동시성이 낙관적이므로 충돌은 실행 시점에 감지된다. write-write 충돌은 오류와 함께 트랜잭션을 롤백하므로(재시도 필요) 애플리케이션에 재시도 로직이 있어야 한다.","적합한 시나리오: 매우 높은 insert 속도(예: IoT/텔레메트리 수집), lock/latch 병목이 있는 hot 테이블, tempdb를 사용하는 임시 테이블. 모든 데이터가 메모리에 들어가야 하며 메모리 압박을 면밀히 모니터링해야 한다."],"level":"expert"},{"id":"sqlserver-query-store","title":"SQL Server: Query Store를 이용한 계획 관리","category":"SQL Server","summary":"Query Store는 쿼리 텍스트, 실행 계획, 런타임 통계를 영구 저장한다. 계획 회귀를 감지하고 특정 계획을 강제할 수 있다.","points":["Query Store는 각 쿼리가 시간에 따라 사용한 모든 계획과 계획별 duration/CPU/read 지표를 데이터베이스 안에 보관한다. '이 쿼리가 어제는 빨랐는데 오늘은 왜 느린가'에 이력 데이터로 답한다.","계획 회귀(새로 컴파일된 계획이 이전보다 나쁜 경우)는 'Regressed Queries'와 'Top Resource Consumers' 보고서로 찾는다. sys.sp_query_store_force_plan으로 알려진 좋은 계획을 강제하여 안정성을 회복한다.","SQL Server 2022의 Query Store hints를 사용하면 계획을 강제하거나 쿼리 텍스트를 바꾸지 않고 힌트(예: RECOMPILE, MAXDOP)를 적용할 수 있다. 서드파티 애플리케이션 쿼리 튜닝에 강력하다.","자동 계획 수정(automatic tuning, FORCE_LAST_GOOD_PLAN)은 회귀를 감지하여 이전의 좋은 계획을 자동으로 강제하고, 개선이 없으면 되돌린다. 운영 환경에서는 감독하에 활성화한다.","캡처 모드(ALL/AUTO/CUSTOM)와 저장 크기를 올바르게 설정한다. AUTO는 드물거나 중요하지 않은 쿼리를 걸러 오버헤드와 공간을 줄이며, 가득 찬 Query Store는 읽기 전용 모드로 전환되어 데이터 수집을 멈춘다."],"level":"expert"},{"id":"mongodb-sharding-deep","title":"MongoDB: 샤딩, chunk와 balancer","category":"MongoDB","summary":"MongoDB는 샤드 키 기준으로 데이터를 chunk로 나누어 샤드에 분산한다. balancer가 chunk를 균등화하며, 샤드 키 선택이 분산과 쿼리 라우팅을 결정한다.","points":["데이터는 샤드 키 범위별 chunk로 나뉘어 샤드에 분산된다. mongos 라우터는 샤드 키를 보고 쿼리를 관련 샤드로만 라우팅하고(targeted), 그렇지 않으면 모든 샤드로 브로드캐스트한다(scatter-gather).","좋은 샤드 키는 카디널리티가 높고 빈도가 낮으며 단조 증가하지 않는다. 단조 증가 키(예: timestamp/ObjectId)는 모든 새 쓰기를 단일 샤드에 몰리게 하며(hotspot), hashed sharding은 이를 분산한다.","샤드 간 chunk 수가 불균형해지면 balancer가 백그라운드에서 chunk를 마이그레이션한다. 마이그레이션 중 원본과 대상 간 일관성은 유지되지만 I/O 비용이 발생하며 유지보수 시간대로 스케줄링할 수 있다.","jumbo chunk는 최대 chunk 크기를 초과했지만 샤드 키 카디널리티가 낮아 분할할 수 없는 chunk다. 마이그레이션할 수 없어 불균형을 만들며, 해결책은 더 선택적인(복합) 샤드 키다.","zone sharding(tag-aware)은 특정 샤드 키 범위를 특정 샤드에 고정한다. 지역이나 하드웨어 기준으로 데이터 배치를 제어하는 데 사용한다(예: EU 데이터를 EU 리전의 샤드에 배치).","최신 버전은 chunk 수 대신 크기 기반 balancing과 자동 defragmentation을 도입했지만 원칙은 같다: targeted 쿼리를 위해 모든 읽기/쓰기에 샤드 키가 있어야 한다."],"level":"expert"},{"id":"mongodb-oplog-election","title":"MongoDB: replica set oplog와 선출","category":"MongoDB","summary":"replica set에서 primary는 쓰기 작업을 oplog에 기록하고 secondary가 이를 적용한다. primary 장애 시 Raft와 유사한 선출로 새 primary가 선택된다.","points":["oplog는 primary의 모든 변경을 멱등(안전하게 재적용 가능) 형태로 기록하는 capped collection이다. secondary는 oplog를 tail하여 적용하며 동기화를 유지한다.","oplog 크기가 복제 윈도를 결정한다. secondary가 oplog가 포괄하는 기간보다 오래 오프라인 상태이면 'stale'이 되어 전체 재동기화(initial sync)가 필요하므로 oplog는 워크로드에 맞게 크기를 정해야 한다.","primary 장애 시 secondary들은 Raft와 유사한 프로토콜로 투표한다. 과반수 표를 얻은 후보가 primary가 되므로 과반수 유지를 위해 투표 수를 홀수로 유지한다(필요시 arbiter 사용).","writeConcern 'majority'는 쓰기가 노드 과반수에 도달했음을 보장하므로 페일오버 시 롤백되지 않는다(롤백 보호). w:1은 더 빠르지만 primary 장애 시 쓰기가 손실될 수 있다.","readConcern과 read preference는 읽기 최신성을 제어한다. secondary에서 읽으면 부하가 분산되지만 복제 지연으로 오래된 데이터가 반환될 수 있으며, 'majority'/'linearizable'은 더 강한 보장을 제공한다.","네트워크 분할 후 이전 primary가 소수 측에 남으면 secondary로 강등된다. 새 primary에 다시 합류할 때 커밋되지 않은 쓰기를 rollback 파일로 옮긴다(majority write concern으로 방지 가능)."],"level":"expert"},{"id":"mongodb-wiredtiger-checkpoint","title":"MongoDB: WiredTiger checkpoint와 journal","category":"MongoDB","summary":"WiredTiger는 주기적인 checkpoint와 write-ahead journal로 지속성을 제공한다. 문서 수준 동시성과 압축으로 높은 처리량을 제공한다.","points":["기본적으로 WiredTiger는 약 60초마다 checkpoint를 수행하여 일관된 메모리 스냅샷을 디스크에 기록한다. checkpoint 사이에는 모든 쓰기를 journal(WAL)에 기록하여 지속성을 유지한다.","journal이 없으면 장애 시 마지막 checkpoint 이후의 쓰기가 손실된다. journal이 있으면 복구 시 마지막 checkpoint를 로드하고 이후 작업을 journal에서 재실행한다.","문서 수준 동시성 제어는 같은 컬렉션의 서로 다른 문서를 동시에 갱신할 수 있게 한다. MVCC 스냅샷이 일관된 읽기를 제공한다.","WiredTiger 캐시(기본적으로 RAM의 ~50%)는 자주 접근하는 데이터와 인덱스를 보관한다. working set이 캐시에 들어가지 않으면 eviction과 디스크 I/O가 증가하며, 캐시 포화는 성능을 심각하게 저하시킨다.","스토리지 압축 기본값은 snappy(속도/압축률 균형)다. zstd/zlib은 더 높은 압축률을 주지만 CPU를 더 사용한다. 인덱스는 prefix compression으로 추가로 줄어든다."],"level":"expert"},{"id":"redis-cluster-hashslot","title":"Redis Cluster: hash slot과 resharding","category":"Redis","summary":"Redis Cluster는 키 공간을 16384개의 hash slot으로 나누어 노드에 분산함으로써 수평 확장한다. 클라이언트는 MOVED/ASK 리다이렉션으로 올바른 노드를 찾는다.","points":["각 키는 CRC16(key) mod 16384로 slot에 매핑된다. 각 master는 특정 slot 범위를 소유하므로 클러스터는 중앙 라우터 없이 데이터를 분산한다.","클라이언트가 잘못된 노드로 가면 노드는 MOVED로 slot의 영구 소유자를 알려주고 클라이언트는 slot 맵을 갱신한다. resharding 중에는 마이그레이션 중인 slot에 임시 ASK 리다이렉션이 사용된다.","resharding은 slot(과 그 안의 키)을 한 노드에서 다른 노드로 온라인 이동한다. 마이그레이션은 키 단위로 수행되며 클러스터는 그동안 계속 서비스한다.","다중 키 작업(MSET, 트랜잭션, Lua)은 같은 slot의 키에서만 동작한다. 관련 키를 같은 slot으로 묶으려면 hash tag({...})를 사용한다. 예: user:{123}:name, user:{123}:email.","고가용성을 위해 각 master에 최소 하나의 replica를 둔다. master가 장애 나면 해당 replica가 승격된다. 클러스터는 master 과반수로 결정하며, 과반수를 잃으면 쓰기가 중단된다.","할당되지 않았거나 master가 없는 slot이 하나라도 있으면 클러스터는 모든 쓰기를 거부한다(cluster-require-full-coverage가 켜진 경우). 16384개 slot 전체가 할당되어야 한다."],"level":"expert"},{"id":"redis-replication-psync","title":"Redis: 복제와 PSYNC","category":"Redis","summary":"Redis 비동기 복제에서 master는 데이터 스냅샷을 replica에 전송한 후 이후 명령을 스트리밍한다. PSYNC는 짧은 연결 끊김 후 부분 재동기화를 수행한다.","points":["초기 동기화 시 master는 RDB 스냅샷을 생성해 replica에 보낸다(full resync). 그동안 들어오는 쓰기는 replication backlog 버퍼에 쌓고 RDB 전송 후 스트리밍한다.","PSYNC는 replica의 replication offset과 master replication ID를 추적한다. 짧은 끊김 후 놓친 데이터가 아직 backlog 버퍼에 있으면 누락된 부분만 전송하여(partial resync) 비용이 큰 full resync를 피한다.","repl-backlog-size는 얼마나 긴 끊김을 견딜 수 있는지 결정한다. 쓰기 속도가 높은데 backlog가 작으면 끊김 시 full resync가 강제되어 master의 RDB 생성과 네트워크 비용이 발생한다.","복제는 비동기다: master는 replica를 기다리지 않고 쓰기를 확인하므로 master가 갑자기 중단되면 최신 쓰기가 손실될 수 있다. WAIT 명령으로 지정한 수의 replica가 수신할 때까지 기다릴 수 있지만 진정한 동기 보장은 아니다.","diskless replication(repl-diskless-sync)을 사용하면 master가 RDB를 디스크에 쓰지 않고 소켓으로 직접 스트리밍할 수 있다. 디스크는 느리지만 네트워크가 빠른 환경에서 초기 동기화를 가속한다."],"level":"expert"},{"id":"redis-memory-fragmentation","title":"Redis: 메모리 단편화와 jemalloc","category":"Redis","summary":"Redis는 기본적으로 jemalloc을 사용한다. 가변 크기 할당과 해제는 시간이 지나며 메모리 단편화를 일으켜 RSS가 실제 사용 데이터보다 크게 보이게 한다.","points":["mem_fragmentation_ratio = used_memory_rss / used_memory. 1에 가까우면 이상적이고, 1보다 눈에 띄게 크면(예: >1.5) 단편화를, 1보다 작으면 데이터가 디스크로 swap되고 있음(나쁨)을 나타낸다.","단편화는 크기가 다른 키가 계속 생성·삭제되어 jemalloc arena에 사용할 수 없는 틈이 남을 때 발생한다. 큰 값의 잦은 변경은 이를 악화시킨다.","active defragmentation(activedefrag yes)은 jemalloc의 도움으로 실행 중 메모리 블록을 압축하여 단편화를 줄인다. 오버헤드를 제한하도록 임계값(active-defrag-ignore-bytes, -threshold-lower/upper)을 조정한다.","maxmemory와 maxmemory-policy를 올바르게 설정한다. jemalloc이 실제로 할당한 RSS는 maxmemory를 초과할 수 있으므로 운영체제 OOM을 피하려면 maxmemory를 RAM보다 낮게 유지한다.","used_memory는 낮은데 RSS가 계속 높으면 재시작(또는 페일오버로 재시작한 노드)이 대개 단편화를 초기화하는 근본 해결책이다. active defrag는 대부분의 경우 재시작 필요성을 줄인다."],"level":"expert"},{"id":"elasticsearch-shard-sizing","title":"Elasticsearch: 샤드 크기 산정","category":"Elasticsearch","summary":"각 샤드는 Lucene 인덱스이며 고정된 리소스 비용을 가진다. 작은 샤드가 너무 많거나(oversharding) 샤드가 지나치게 크면 성능과 안정성이 저하된다.","points":["경험 법칙: shard는 10-50 GB 범위로 유지한다. 매우 큰 샤드는 복구와 리밸런싱을 느리게 하고, 매우 작은 shard(oversharding)는 cluster state와 heap에 불필요한 부하를 준다.","모든 샤드, 특히 매핑된 모든 필드는 heap을 소비한다. 기존의 \"heap GB당 ~20 shard\" 규칙은 8.3부터 더 이상 적용되지 않는다(샤드당 heap 오버헤드가 크게 감소). 현재 가이드는 data 노드에서 인덱스별 매핑 필드당 ~1 KB heap과 오버헤드를 확보하고, 노드당 샤드 수를 cluster.max_shards_per_node(기본값 1000) 미만으로 유지하는 것이다. 샤드가 너무 많으면 여전히 cluster state가 커지고 조정 오버헤드가 증가한다.","쿼리 지연은 샤드 수와 관련된다: 쿼리는 모든 관련 샤드로 fan-out되고 결과가 병합된다. 필요 이상으로 샤드가 많으면 조정 오버헤드로 작은 쿼리도 느려진다.","시계열/로그 데이터에는 거대한 단일 인덱스 대신 시간 기반 인덱스(data stream)를 사용한다. 이렇게 하면 오래된 데이터를 인덱스 단위로 이동/삭제(ILM)하는 비용이 낮아지고 샤드를 관리하기 쉬워진다.","primary 샤드 수는 인덱스 생성 시 고정되며 이후 변경할 수 없다(reindex/split/shrink로만 가능). 따라서 성장 예측에 따라 처음부터 올바르게 계획해야 하며, replica 수는 동적으로 조정할 수 있다."],"level":"expert"},{"id":"elasticsearch-ilm-tiers","title":"Elasticsearch: ILM과 hot-warm-cold-frozen 티어","category":"Elasticsearch","summary":"Index Lifecycle Management는 수명 주기 단계에 따라 인덱스를 비용/성능 티어 간에 자동으로 이동시켜 시계열 데이터를 경제적으로 관리한다.","points":["ILM 정책은 인덱스를 단계별로 이동시킨다: hot(활발한 쓰기 + 잦은 쿼리, 빠른 SSD), warm(쓰기 완료, 가끔 쿼리), cold(드문 쿼리, 저렴한 하드웨어), frozen(매우 드묾, searchable snapshot을 통해 오브젝트 스토리지에서 제공).","rollover는 hot 인덱스가 지정한 크기/기간/문서 수에 도달하면 새 인덱스를 연다. 이를 통해 단일 인덱스가 무한히 커지지 않고 각 단계가 인덱스 단위로 관리된다.","frozen 티어는 searchable snapshot을 통해 데이터를 오브젝트 스토리지(S3/blob)에 유지하고 필요할 때만 일부를 메모리로 가져온다. 디스크 비용을 크게 줄이지만 쿼리 지연이 증가한다.","warm/cold로 이동할 때 force merge, shrink, replica 축소로 스토리지와 heap을 최적화한다. 이 단계에서는 데이터가 더 이상 변경되지 않으므로 세그먼트를 하나로 병합할 수 있다.","data 티어를 노드 역할(data_hot/data_warm/data_cold/data_frozen)에 매핑한다. ILM은 샤드 allocation filter를 사용해 적합한 하드웨어의 노드에 인덱스를 배치한다."],"level":"expert"},{"id":"elasticsearch-segment-merge","title":"Elasticsearch: 세그먼트 병합과 force merge","category":"Elasticsearch","summary":"Lucene은 refresh마다 새로운 불변 세그먼트를 생성한다. 백그라운드 병합이 세그먼트를 합치고 제거하며, force merge는 신중하게 사용해야 하는 수동 작업이다.","points":["refresh(기본 1 s)마다 새 세그먼트가 생성되고 삭제는 세그먼트에 'tombstone'으로 표시된다. 작은 세그먼트가 많으면 읽기가 느려지므로 백그라운드 병합이 이를 합친다.","삭제/갱신된 문서는 병합 시에만 디스크에서 물리적으로 제거된다. 갱신 부하가 높으면 세그먼트에 'deleted docs'가 쌓여 디스크/메모리 사용량이 부풀어 오른다.","더 이상 쓰기가 없는(읽기 전용) 인덱스에서는 1개 세그먼트로 force merge하면 쿼리 속도와 디스크 효율이 향상된다. 이는 ILM warm 단계의 전형적인 단계다.","활발히 쓰기가 일어나는 인덱스에서는 절대 force merge를 실행하지 않는다. 생성된 매우 큰 세그먼트는 일반 병합 주기에 들어가지 않아 이후 삭제가 정리되지 않으며, 오랫동안 I/O와 heap을 압박한다.","대량 적재 중 refresh_interval을 일시적으로 늘리면(예: -1 또는 30s) 더 적고 큰 세그먼트가 생성되어 병합 부하가 줄고 쓰기 처리량이 향상된다."],"level":"expert"},{"id":"elasticsearch-node-roles","title":"Elasticsearch: 노드 역할과 클러스터 토폴로지","category":"Elasticsearch","summary":"Elasticsearch 노드는 master, data, coordinating, ingest 같은 역할을 맡는다. 대규모 클러스터에서 역할을 분리하면 안정성과 확장성이 확보된다.","points":["master-eligible 노드는 cluster state(인덱스, 샤드 배치, 매핑)를 관리한다. 안정적인 선출을 위해 홀수(보통 3개)의 전용 master 노드를 사용하고 split-brain 방지를 위해 quorum(과반수)에 의존한다.","data 노드는 샤드를 호스팅하고 인덱싱과 쿼리 I/O를 처리한다. CPU/RAM/디스크를 가장 많이 소비하며 data 티어(hot/warm/cold/frozen)로 나눌 수 있다.","coordinating-only 노드는 다른 역할 없이 요청을 관련 샤드로 분배하고 결과를 병합한다(scatter-gather). 무거운 검색/집계 병합을 맡아 data 노드를 보호하며 로드 밸런서 역할을 한다.","ingest 노드는 인덱싱 전에 pipeline을 통해 데이터를 처리한다(변환, 보강). 무거운 ingest pipeline을 전용 ingest 노드로 옮기면 data 노드의 쿼리 성능이 보호된다.","master 노드에 무거운 쿼리/데이터 부하를 함께 맡기지 않는다. 부하로 master가 느려지면 cluster state 갱신이 지연되어 클러스터 전체가 불안정해진다(샤드 할당 지연, 선출)."],"level":"expert"}]},"guide/glossary.json":{"terms":[{"term":"ACID","full":"Atomicity, Consistency, Isolation, Durability","definition":"트랜잭션을 신뢰할 수 있게 하는 네 가지 핵심 속성: Atomicity(원자성, 전부 아니면 전무), Consistency(일관성, 규칙 유지), Isolation(격리성, 동시 트랜잭션이 서로를 손상시키지 않음), Durability(지속성, 커밋된 데이터는 손실되지 않음). 관계형 데이터베이스의 트랜잭션 보장이다.","tags":["transaction","transaction","acid","consistency","consistency"]},{"term":"BASE","full":"Basically Available, Soft state, Eventually consistent","definition":"분산 및 NoSQL 시스템에서 선호되는 ACID의 대안인 완화된 일관성 접근 방식. 시스템은 항상 가용한 것을 우선하며 데이터가 시간이 지나면서 일관성을 갖추는 것(eventual consistency)을 수용한다. 엄격한 일관성을 확장성과 맞바꾼다.","tags":["base","distributed","distributed","eventual-consistency","nosql"]},{"term":"MVCC","full":"Multi-Version Concurrency Control","definition":"같은 행의 여러 버전을 유지하여 읽기와 쓰기가 서로를 잠그지 않고 동작하게 하는 동시성 기법. 각 트랜잭션은 자신의 데이터 스냅샷을 보므로 읽기가 쓰기를, 쓰기가 읽기를 막지 않는다. PostgreSQL, Oracle 등에서 사용된다.","tags":["concurrency","concurrency","lock","mvcc","snapshot"]},{"term":"WAL","full":"Write-Ahead Log","definition":"변경이 데이터 파일에 기록되기 전에 로그 레코드를 디스크에 영구 기록하는 원칙. 이를 통해 장애 복구와 지속성이 보장된다. PostgreSQL에서는 WAL, SQL Server에서는 transaction log라고 한다.","tags":["wal","log","recovery","recovery","durability"]},{"term":"Redo 로그 (Redo Log)","full":"","definition":"데이터베이스의 변경 사항을 기록하고 장애로 손실된 작업을 재적용(redo)하는 데 사용하는 로그. 복구 시 커밋되었지만 아직 디스크에 기록되지 않은 변경을 롤포워드한다. Oracle의 redo log는 일반적인 WAL 개념에 해당한다.","tags":["redo","log","recovery","recovery","oracle"]},{"term":"Undo / 롤백 세그먼트 (Rollback Segment)","full":"","definition":"트랜잭션이 변경한 데이터의 이전 이미지를 저장하는 영역. 두 가지 목적이 있다: 롤백으로 변경을 취소하는 것과 MVCC에서 읽기에 이전 버전을 제공하는 것. Oracle에서는 undo/rollback segment, 다른 시스템에서는 undo/version store라고 한다.","tags":["undo","rollback","rollback","mvcc","segment"]},{"term":"버퍼 풀 / 버퍼 캐시 (Buffer Pool / Buffer Cache)","full":"","definition":"자주 사용하는 데이터와 인덱스 페이지를 보관하는 메모리 캐시. 디스크 읽기는 비용이 크므로 데이터베이스 성능은 이곳의 적중률(cache hit ratio)에 크게 좌우된다. MySQL InnoDB에서는 buffer pool, SQL Server에서는 buffer cache라고 한다.","tags":["buffer-pool","buffer-cache","cache","cache","memory"]},{"term":"더티 페이지 (Dirty Page)","full":"","definition":"메모리에서 수정되었지만 아직 디스크에 기록되지 않은 데이터 페이지. 지속성은 WAL로 보호되며, 페이지 자체는 checkpoint나 background writer가 디스크로 flush할 때까지 dirty 상태로 남는다. dirty 페이지가 너무 많으면 복구 시간이 길어진다.","tags":["dirty-page","dirty-page","memory","checkpoint","io"]},{"term":"LSN","full":"Log Sequence Number","definition":"WAL/transaction log의 모든 레코드에 부여되는 계속 증가하는 고유 일련번호. 페이지가 어느 로그 레코드까지 최신인지, 복구를 어디서부터 이어갈지 판단하는 데 사용된다. 복제 진행 상황 추적에도 쓰인다.","tags":["lsn","log","sequence-number","recovery","replication"]},{"term":"SCN","full":"System Change Number","definition":"Oracle에서 데이터베이스의 모든 커밋 시점에 부여되는 단조 증가 논리 타임스탬프. 읽기 일관성, MVCC, 복구 중 이벤트 순서 결정에 사용된다. Oracle판 LSN으로 볼 수 있다.","tags":["scn","oracle","timestamp","read-consistency","commit"]},{"term":"격리 수준 (Isolation Level)","full":"격리 수준 (Isolation Level)","definition":"동시 트랜잭션이 서로의 변경을 어느 정도까지 볼 수 있는지 결정하는 설정. 수준은 Read Uncommitted, Read Committed, Repeatable Read, Serializable이며 수준이 높을수록 일관성은 높아지고 동시성은 낮아진다. dirty/non-repeatable/phantom read 이상 현상을 제어한다.","tags":["isolation","isolation-level","transaction","concurrency","concurrency"]},{"term":"더티 리드 (Dirty Read)","full":"","definition":"다른 트랜잭션이 아직 커밋하지 않은(롤백할 수도 있는) 변경을 읽는 것. 읽은 데이터가 무효일 수 있으므로 일관성 이상 현상이다. Read Uncommitted 격리 수준에서만 발생할 수 있다.","tags":["dirty-read","anomaly","isolation","transaction","consistency"]},{"term":"반복 불가능한 읽기 (Non-repeatable Read)","full":"","definition":"같은 트랜잭션 안에서 같은 행을 두 번 읽을 때 그 사이 다른 트랜잭션이 커밋하여 다른 값이 보이는 것. Read Committed에서 발생할 수 있으며 Repeatable Read 이상에서 방지된다.","tags":["non-repeatable-read","anomaly","isolation","transaction","non-repeatable"]},{"term":"팬텀 리드 (Phantom Read)","full":"","definition":"같은 트랜잭션 안에서 같은 조건으로 쿼리를 다시 실행할 때 중간의 insert/delete로 인해 행 집합이 바뀌는 것(새 'phantom' 행 출현). SQL 표준상 Serializable에서 방지되며, PostgreSQL은 Repeatable Read에서도 방지한다.","tags":["phantom-read","phantom","anomaly","serializable","isolation"]},{"term":"데드락 (Deadlock)","full":"","definition":"둘 이상의 트랜잭션이 각자 상대가 보유한 락을 기다려 어느 쪽도 진행할 수 없는 상황. 데이터베이스가 이를 감지하고 대개 가장 비용이 적은 트랜잭션을 'victim'으로 선택해 롤백하여 순환을 끊는다. 항상 같은 순서로 락을 획득하면 위험이 줄어든다.","tags":["deadlock","deadlock","lock","lock","concurrency"]},{"term":"락 에스컬레이션 (Lock Escalation)","full":"","definition":"트랜잭션이 많은 행/페이지 락을 보유할 때 데이터베이스가 이를 하나의 더 큰 락(예: 테이블 락)으로 변환하는 것. 락 관리 메모리를 줄이지만 동시성을 낮추고 블로킹을 증가시킬 수 있다. 특히 SQL Server에서 볼 수 있다.","tags":["lock-escalation","lock","lock","concurrency","sql-server"]},{"term":"래치 (Latch)","full":"","definition":"메모리 내 구조(예: buffer pool 페이지)를 매우 짧은 시간 동안 보호하는 가볍고 저비용인 동시성 잠금. 트랜잭션 락과 달리 논리 데이터가 아닌 물리적 메모리 구조를 보호하며 매우 짧게만 유지된다.","tags":["latch","lock","memory","concurrency","internal"]},{"term":"B-Tree 인덱스","full":"Balanced Tree Index","definition":"데이터를 정렬된 균형 트리 구조로 유지하는 가장 일반적인 인덱스 유형. 등치 및 범위 쿼리, 정렬, 접두사 검색에 효율적이다. 대부분의 데이터베이스에서 기본 인덱스 유형이다.","tags":["btree","index","index","search","range"]},{"term":"해시 인덱스 (Hash Index)","full":"","definition":"키를 해시 함수에 통과시켜 위치를 찾는 인덱스 유형. 정확한 등치(=) 조회에서만 매우 빠르며 범위 쿼리나 정렬에는 사용할 수 없다. 예를 들어 PostgreSQL과 MySQL Memory 테이블에서 사용할 수 있다.","tags":["hash-index","index","index","equality","search"]},{"term":"비트맵 인덱스 (Bitmap Index)","full":"","definition":"고유 값마다 비트 배열을 유지하는 인덱스로, 카디널리티가 낮은(고유 값이 적은) 컬럼에 적합하다. 여러 조건을 AND/OR로 빠르게 결합할 수 있다. 데이터 웨어하우스/OLAP 워크로드에 효과적이지만 쓰기가 많으면 비효율적이다.","tags":["bitmap-index","index","index","olap","cardinality"]},{"term":"커버링 인덱스 (Covering Index)","full":"","definition":"쿼리에 필요한 모든 컬럼을 포함하는 인덱스로, 테이블(heap/clustered)을 방문하지 않고 인덱스만으로 쿼리를 처리한다. 비용이 큰 행 조회를 없애 성능을 높인다. INCLUDE 컬럼으로 생성할 수 있다.","tags":["covering-index","index","index","performance","lookup"]},{"term":"클러스터형 인덱스 (Clustered Index)","full":"","definition":"테이블 행을 인덱스 키 순서로 물리적으로 정렬해 유지하는 인덱스로, 테이블 자체가 그 순서로 저장된다. 테이블당 하나만 가질 수 있다. 범위 쿼리에 빠르며, InnoDB에서 primary key는 항상 clustered다.","tags":["clustered-index","index","index","physical","primary-key"]},{"term":"카디널리티 (Cardinality)","full":"","definition":"컬럼의 고유 값 개수. 카디널리티가 높으면(예: email) 서로 다른 값이 많고, 낮으면(예: 성별) 적다. 옵티마이저가 인덱스를 선택하고 계획 비용을 추정할 때 중요한 척도다.","tags":["cardinality","cardinality","statistics","optimizer","distinct"]},{"term":"선택도 (Selectivity)","full":"","definition":"필터 조건이 전체 행 중 얼마나 작은 비율을 반환하는지 나타낸다. 선택도가 높으면(적은 행) 인덱스 사용이 유리하고, 낮으면(많은 행) full scan이 더 저렴할 수 있다. 옵티마이저가 계획 선택 시 사용한다.","tags":["selectivity","selectivity","optimizer","index","statistics"]},{"term":"실행 계획 (Execution Plan)","full":"","definition":"옵티마이저가 쿼리 실행을 위해 선택한 단계(스캔 유형, 조인 방식, 정렬 등)를 보여 주는 로드맵. EXPLAIN/EXPLAIN ANALYZE로 분석하면 성능 문제의 근본 원인이 드러난다. 예상 비용과 실제 비용 비교가 중요하다.","tags":["execution-plan","query-plan","explain","optimizer","performance"]},{"term":"비용 기반 옵티마이저 (Cost-Based Optimizer)","full":"CBO","definition":"통계를 기반으로 가능한 실행 계획의 비용을 추정하여 가장 저렴한 계획을 선택하는 쿼리 옵티마이저. 올바른 판단에는 최신 통계가 필수이며, 오래된 통계는 나쁜 계획으로 이어진다. 현대 데이터베이스의 표준 방식이다.","tags":["cbo","optimizer","cost","statistics","query-plan"]},{"term":"통계","full":"통계","definition":"옵티마이저가 행 수, 카디널리티, 값 분포를 추정할 수 있도록 테이블/인덱스에 대해 수집되는 메타데이터. 최신 통계는 좋은 쿼리 계획을 의미하며, 오래되면 ANALYZE/UPDATE STATISTICS로 갱신해야 한다. 성능 저하의 가장 흔한 원인 중 하나다.","tags":["statistics","statistics","optimizer","analyze","query-plan"]},{"term":"히스토그램 (Histogram)","full":"","definition":"컬럼 값의 분포를 범위(bucket)로 나누어 요약하는 통계 구조. 특히 편향된 분포에서 옵티마이저가 행 수를 더 정확하게 추정하게 한다. 통계의 구성 요소다.","tags":["histogram","statistics","statistics","optimizer","distribution"]},{"term":"파티션 (Partition)","full":"","definition":"큰 테이블을 논리적으로는 하나의 테이블로 유지하면서 물리적으로 더 작은 조각(예: 날짜나 범위 기준)으로 나누는 기법. 쿼리는 관련 조각만 스캔하며(partition pruning) 유지보수와 아카이빙이 쉬워진다. 샤딩과의 차이: 같은 서버/인스턴스 안에 머문다.","tags":["partition","partitioning","scaling","pruning","big-data"]},{"term":"샤딩","full":"","definition":"샤드 키에 따라 데이터를 여러 개의 별도 서버/데이터베이스에 수평 분산하는 방법. 단일 머신의 한계를 넘는 수평 확장을 제공하지만 쿼리와 트랜잭션 관리가 복잡해진다. partitioning과의 차이: 데이터가 서로 다른 노드에 분산된다.","tags":["sharding","scaling","distributed","distributed","horizontal-scaling"]},{"term":"복제 (동기/비동기)","full":"","definition":"primary 노드의 데이터를 하나 이상의 복사본(replica)으로 복제하는 것. 동기 복제에서는 커밋이 replica의 확인을 기다리고(데이터 손실 없음, 지연 증가), 비동기 복제에서는 기다리지 않는다(빠르지만 replica가 뒤처지면 데이터 손실 가능). HA와 읽기 확장에 사용된다.","tags":["replication","replication","synchronous","asynchronous","ha"]},{"term":"논리 복제 vs 물리 복제","full":"","definition":"물리 복제는 디스크 블록/WAL 수준에서 정확한 사본을 만든다(같은 버전, 데이터베이스 전체). 논리 복제는 행/SQL 수준에서 변경을 전송하며, 선택적 테이블 복제와 버전 간 및 서로 다른 스키마 간 전송이 가능하다. CDC와 업그레이드 시나리오에는 논리 복제가 선호된다.","tags":["logical-replication","physical-replication","replication","cdc","wal"]},{"term":"페일오버","full":"","definition":"primary 노드 장애 시 서비스를 자동 또는 수동으로 standby 노드로 넘기는 것. 다운타임을 최소화하며, RPO는 손실 가능한 데이터 양을, RTO는 복구 소요 시간을 결정한다. 계획되지 않은 장애 시 작동한다.","tags":["failover","ha","high-availability","standby","rpo"]},{"term":"스위치오버 (Switchover)","full":"","definition":"primary와 standby 역할을 계획적으로 통제하여 교환하는 것(예: 유지보수 목적). failover와의 차이: 장애가 아닌 계획된 작업이며 대개 데이터 손실 없이 수행된다.","tags":["switchover","planned","ha","maintenance","standby"]},{"term":"RPO","full":"Recovery Point Objective","definition":"재해 후 허용 가능한 최대 데이터 손실량(시간으로 표현). 예를 들어 RPO가 5분이면 최대 5분의 데이터 손실을 허용한다. 이 목표를 충족하도록 백업 빈도와 복제 방식을 선택한다.","tags":["rpo","data-loss","dr","disaster-recovery","backup"]},{"term":"RTO","full":"Recovery Time Objective","definition":"장애 후 시스템이 다시 운영 가능해지기까지의 최대 목표 시간. 예를 들어 RTO가 1시간이면 서비스는 늦어도 1시간 안에 복구되어야 한다. HA/DR 아키텍처와 자동화 수준은 이 목표에 맞춰 설계한다.","tags":["rto","recovery-time","dr","disaster-recovery","ha"]},{"term":"HA","full":"High Availability","definition":"장애에도 불구하고 시스템이 중단 없이 또는 최소한의 중단으로 서비스를 유지하도록 하는 설계 목표. 이중화, 자동 페일오버, 헬스 체크로 달성한다. 보통 가동률(예: 99.99%)로 표현한다.","tags":["ha","high-availability","high-availability","failover","redundancy"]},{"term":"DR","full":"Disaster Recovery","definition":"화재, 홍수, 리전 전체 장애 같은 대형 재해 후 대개 지리적으로 분리된 리전에서 시스템과 데이터를 복구하기 위한 계획과 절차. RPO와 RTO 목표로 측정한다. HA는 일상적 장애에, DR은 대형 재해에 초점을 둔다.","tags":["dr","disaster-recovery","disaster-recovery","rpo","rto"]},{"term":"쿼럼 (Quorum)","full":"","definition":"분산 시스템에서 결정(예: 리더 선출이나 쓰기 확인)이 유효하려면 필요한 최소 노드 과반수(보통 N/2 + 1). split-brain을 방지하고 일관성을 유지하는 데 사용된다. 클러스터 크기가 대부분 홀수인 이유다.","tags":["quorum","majority","distributed","distributed","split-brain"]},{"term":"스플릿 브레인 (Split-Brain)","full":"","definition":"네트워크 분할 후 클러스터의 양쪽이 모두 자신을 primary로 여기고 독립적으로 쓰기를 계속하는 상황으로, 데이터 충돌과 손상을 초래한다. quorum과 fencing 메커니즘으로 방지한다. HA 클러스터에서 가장 위험한 장애 시나리오 중 하나다.","tags":["split-brain","distributed","quorum","ha","consistency"]},{"term":"TDE","full":"Transparent Data Encryption","definition":"데이터베이스 파일(데이터와 로그)을 디스크에서 암호화된 상태로 유지하는, 애플리케이션에 투명한 암호화 방식. 디스크나 백업 파일이 도난당해도 데이터를 보호한다(data at rest). 네트워크상의 데이터(in transit)는 보호하지 않으며, 이를 위해서는 TLS가 필요하다.","tags":["tde","encryption","encryption","security","security"]},{"term":"RBAC","full":"Role-Based Access Control","definition":"권한을 사용자에게 직접 부여하지 않고 역할에 부여하며 사용자를 역할의 멤버로 만드는 접근 제어 모델. 권한 관리를 단순화하고 최소 권한 원칙 적용을 쉽게 한다. 사용자 수가 늘어나도 관리를 확장 가능하게 유지한다.","tags":["rbac","role","privilege","security","security"]},{"term":"커넥션 풀 (Connection Pool)","full":"","definition":"애플리케이션의 데이터베이스 연결을 반복적으로 열고 닫는 대신 미리 연결을 만들어 재사용하는 풀. 연결 설정 비용을 낮추고 리소스 소비를 제한하며 부하를 제어한다. 과도한 연결을 막아 데이터베이스를 보호한다.","tags":["connection-pool","connection-pool","performance","resource","application"]},{"term":"프리페어드 스테이트먼트 (Prepared Statement)","full":"","definition":"SQL 문을 한 번 파싱하고 계획을 수립한 뒤 다른 파라미터 값으로 반복 실행하는 구조. 계획 재사용으로 성능을 높이고 SQL injection에 대한 보안을 제공한다. bind variable과 함께 사용된다.","tags":["prepared-statement","bind","security","sql-injection","performance"]},{"term":"바인드 변수 (Bind Variable)","full":"","definition":"값을 SQL 텍스트에 직접 넣지 않고 placeholder를 통해 전달하는 파라미터. 같은 쿼리가 값마다 다시 계획되는 것을 막아 plan cache를 효율적으로 사용하게 하며 SQL injection을 방지한다. 리터럴 값이 자주 바뀌는 쿼리에서 매우 중요하다.","tags":["bind-variable","parameter","prepared-statement","plan-cache","security"]},{"term":"파라미터 스니핑 (Parameter Sniffing)","full":"","definition":"옵티마이저가 prepared/파라미터화된 쿼리의 계획을 첫 실행 시의 파라미터 값 기준으로 생성하고 캐시하는 것. 그 값이 비전형적이면 이후 다른 값에서 성능이 나쁜 계획이 고착될 수 있다. 해결책으로는 계획 재컴파일(recompile)과 옵티마이저 힌트가 있다.","tags":["parameter-sniffing","plan-cache","optimizer","performance","sql-server"]},{"term":"VACUUM / Bloat (팽창)","full":"","definition":"MVCC로 인해 삭제/갱신된 행의 dead 버전이 테이블에 쌓이며, 이렇게 부풀어 오르는 것을 bloat라고 한다. VACUUM은 dead 행의 공간을 회수하고 통계를 갱신한다(PostgreSQL). 정기적으로 실행하지 않으면 디스크가 부풀고 성능이 떨어진다.","tags":["vacuum","bloat","bloated","mvcc","postgresql"]},{"term":"컴팩션 (Compaction)","full":"","definition":"LSM-tree 기반 저장소(예: Cassandra, RocksDB)에서 여러 개의 작은 데이터 파일을 병합하고 dead/오래된 레코드를 제거하는 과정. 읽기 성능을 높이고 디스크 공간을 회수한다. LSM 세계의 VACUUM에 해당한다.","tags":["compaction","lsm-tree","merge","cassandra","rocksdb"]},{"term":"Oplog","full":"Operations Log","definition":"MongoDB에서 primary에 적용된 모든 쓰기 작업을 순서대로 기록하는 특수한 고정 크기(capped) 컬렉션. replica set 멤버는 이를 읽어 primary와 동기화를 유지한다. CDC와 복제의 기반이다.","tags":["oplog","mongodb","replication","cdc","log"]},{"term":"Binlog","full":"Binary Log","definition":"MySQL에서 데이터를 변경하는 모든 작업(또는 행 변경)을 기록하는 바이너리 로그. 복제, 시점 복구(point-in-time recovery), CDC에 사용된다. row, statement 또는 mixed 형식으로 유지할 수 있다.","tags":["binlog","mysql","replication","cdc","recovery"]},{"term":"CDC","full":"Change Data Capture","definition":"데이터베이스의 insert/update/delete 변경을 포착하여 다른 시스템(데이터 웨어하우스, 검색 엔진, 메시지 큐)으로 스트리밍하는 기법. 대개 트랜잭션 로그(binlog, WAL, oplog)를 읽어 낮은 오버헤드로 동작한다. 실시간 데이터 통합의 기반이다.","tags":["cdc","change-data-capture","data-streaming","binlog","integration"]},{"term":"구체화된 뷰 (Materialized View)","full":"","definition":"쿼리 결과를 디스크에 물리적으로 저장하는 뷰로, 일반 뷰와 달리 쿼리마다 다시 계산되지 않는다. 비용이 큰 집계와 조인을 가속하지만 데이터가 오래되므로 refresh로 갱신해야 한다. OLAP/보고서에서 흔히 사용된다.","tags":["materialized-view","view","cache","olap","reporting"]},{"term":"외래 키 (Foreign Key)","full":"","definition":"한 테이블의 컬럼이 다른 테이블의 primary/후보 키를 참조하게 하는 참조 무결성 제약. 존재하지 않는 행에 대한 참조를 막고 관계를 유지한다. ON DELETE/UPDATE CASCADE 같은 규칙이 종속 행의 동작을 정의한다.","tags":["foreign-key","foreign-key","integrity","relationship","constraint"]},{"term":"정규화 (Normalization)","full":"정규화 (Normalization)","definition":"중복을 줄이고 불일치 위험을 낮추기 위해 데이터를 관련 테이블로 분리하는 과정(1NF, 2NF, 3NF 같은 정규형). 갱신 이상을 방지하며 OLTP 시스템에서 선호된다. 대신 쿼리에 더 많은 조인이 필요할 수 있다.","tags":["normalization","normalization","design","oltp","redundancy"]},{"term":"비정규화 (Denormalization)","full":"비정규화 (Denormalization)","definition":"조인 수를 줄이고 읽기 성능을 높이기 위해 의도적으로 데이터를 중복시키는 기법. 쿼리를 가속하지만 데이터 중복과 갱신 복잡성을 초래한다. 보고서/OLAP 및 읽기 위주 워크로드에서 선호된다.","tags":["denormalization","denormalization","performance","olap","read"]},{"term":"OLTP","full":"Online Transaction Processing","definition":"짧고 빈번한 동시 트랜잭션(insert/update/delete, 단일 행 읽기)을 대량으로 처리하는 시스템 유형. 예: 은행, 전자상거래. 낮은 지연과 높은 동시성이 필요하며 대개 정규화하여 설계한다. OLAP에 대응하는 운영 시스템이다.","tags":["oltp","transaction","transaction","operational","normalization"]},{"term":"OLAP","full":"Online Analytical Processing","definition":"대규모 데이터 세트에 대해 복잡한 집계·분석 쿼리를 실행하는 시스템 유형. 예: 보고서, 비즈니스 인텔리전스. 적지만 무거운 쿼리가 실행되며 대개 비정규화·컬럼형으로 설계한다. OLTP에 대응하는 분석 시스템이다.","tags":["olap","analytics","reporting","data-warehouse","columnar"]},{"term":"ETL","full":"Extract, Transform, Load","definition":"소스에서 데이터를 추출(Extract)하고 비즈니스 규칙에 따라 변환(Transform)한 뒤 대상 시스템, 대개 데이터 웨어하우스에 적재(Load)하는 과정. 적재 후 대상에서 변환하는 변형을 ELT라고 한다. 데이터 통합의 고전적 방식이다.","tags":["etl","data-integration","data-warehouse","elt","transformation"]},{"term":"핫/콜드 데이터 (Hot/Cold Data)","full":"","definition":"자주 접근하는 최신 데이터를 'hot', 거의 접근하지 않는 오래된/아카이브 데이터를 'cold'라고 한다. hot 데이터는 빠르고 비싼 스토리지에, cold 데이터는 저렴하고 느린 스토리지에 둔다(data tiering). 비용과 성능의 균형을 맞추는 전략이다.","tags":["hot-data","cold-data","hot-cold","archive","tiering"]},{"term":"인덱스 단편화 (Index Fragmentation)","full":"","definition":"시간이 지나며 insert/delete/update로 인해 인덱스 페이지의 논리적 순서가 물리적 순서와 어긋나고 페이지 충전율이 떨어지는 현상. 불필요한 I/O와 공간 낭비를 일으켜 성능을 저하시킨다. REBUILD 또는 REORGANIZE로 해결한다.","tags":["index-fragmentation","fragmentation","index","maintenance","performance"]},{"term":"2단계 커밋 (Two-Phase Commit)","full":"2PC","definition":"여러 데이터베이스/리소스에 걸친 분산 트랜잭션에 원자적 커밋을 제공하는 프로토콜. 두 단계로 동작한다: 먼저 coordinator가 모든 참여자에게 준비 여부를 묻고(prepare), 모두 동의하면 커밋하며 하나라도 거부하면 모두 롤백한다. 느리고 coordinator 장애에 취약하다.","tags":["two-phase-commit","2pc","distributed","distributed","atomic"]}]},"guide/glossary-2.json":{"terms":[{"term":"LSM 트리 (LSM Tree)","full":"Log-Structured Merge Tree","definition":"쓰기를 먼저 메모리 구조(memtable)에 모은 뒤 정렬된 블록으로 디스크에 flush하는 저장 구조. 랜덤 쓰기를 순차 쓰기로 바꿔 높은 쓰기 처리량을 제공하며, 읽기는 여러 레벨을 스캔한다. RocksDB, Cassandra, LevelDB의 기반이다.","tags":["storage","storage engine","lsm","write"]},{"term":"B+ 트리 (B+ Tree)","full":"","definition":"모든 데이터를 리프 노드에 두고 내부 노드는 라우팅 키만 갖는 균형 트리 구조. 연결된 리프가 범위 스캔을 가속한다. 관계형 데이터베이스 인덱스에서 가장 일반적인 구조다.","tags":["index","index structure","storage","storage engine","btree"]},{"term":"블룸 필터 (Bloom Filter)","full":"","definition":"적은 메모리로 원소가 집합에 있는지 확인하는 확률적 자료 구조. \"확실히 없음\" 또는 \"아마 있음\"으로 답하며, false positive는 가능하지만 false negative는 없다. LSM 기반 엔진에서 불필요한 디스크 읽기를 피하는 데 사용된다.","tags":["storage","storage engine","probabilistic","filter","performance"]},{"term":"일관된 해싱 (Consistent Hashing)","full":"","definition":"노드를 추가하거나 제거할 때 키의 일부만 이동하도록 보장하는 분산 방식. 키와 노드를 가상 링 위에 배치한다. 분산 캐시와 NoSQL 시스템의 데이터 분산에 사용된다.","tags":["distributed systems","distributed","hashing","sharding","scaling"]},{"term":"벡터 클록 (Vector Clock)","full":"","definition":"분산 시스템에서 이벤트의 인과 순서를 추적하기 위해 노드별 카운터를 유지하는 구조. 두 갱신이 서로에게서 파생되었는지 동시(충돌)인지 판별한다. 충돌 감지와 eventual consistency에 사용된다.","tags":["distributed systems","distributed","clock","causality","consistency"]},{"term":"가십 프로토콜 (Gossip Protocol)","full":"","definition":"각 노드가 주기적으로 무작위로 선택한 몇몇 peer에게 자신의 상태를 전파하여 정보가 전염병처럼 클러스터 전체로 퍼지는 통신 방식. 중앙 coordinator가 필요 없고 확장 가능하며 장애에 강하다. Cassandra와 Consul의 멤버십 관리에 사용된다.","tags":["distributed systems","distributed","gossip","cluster","membership"]},{"term":"Raft","full":"","definition":"Paxos보다 이해하기 쉽도록 설계된 합의 알고리즘. 리더를 선출하고, 모든 쓰기를 리더를 통해 복제되는 로그에 추가하며, 과반수 확인으로 영구화한다. etcd와 Consul이 사용한다.","tags":["distributed systems","distributed","consensus","raft","replication"]},{"term":"Paxos","full":"","definition":"신뢰할 수 없는 네트워크에서 과반수가 단일 값에 합의하도록 보장하는 고전적 합의 알고리즘. 정확하지만 구현이 복잡하다고 여겨지며 많은 분산 시스템의 이론적 기반을 이룬다. Google Chubby 같은 시스템에서 사용된다.","tags":["distributed systems","distributed","consensus","paxos","agreement"]},{"term":"리더 선출 (Leader Election)","full":"","definition":"분산 클러스터에서 특정 작업을 조정할 단일 노드(리더)를 선택하는 과정. 현재 리더가 장애 나면 새 리더가 자동으로 선출된다. split-brain 방지를 위해 대개 과반수(quorum) 승인이 필요하다.","tags":["distributed systems","distributed","leader","election","coordination"]},{"term":"쿼럼 쓰기/읽기 (Quorum Write/Read)","full":"","definition":"읽기 또는 쓰기가 유효한 것으로 간주되기 위해 필요한 최소 노드 수. W + R > N이 성립하면 최신 데이터에 도달함이 보장된다. 일관성과 가용성 간 트레이드오프를 조정할 수 있게 한다.","tags":["distributed systems","distributed","quorum","consistency","replication"]},{"term":"WAL 전송 (WAL Shipping)","full":"Write-Ahead Log Shipping","definition":"primary 서버가 생성한 WAL 레코드를 standby 서버로 보내 그곳에서 재실행하는 것. 이를 통해 standby는 primary 데이터베이스의 거의 최신 사본을 유지한다. PostgreSQL streaming replication의 기반이다.","tags":["replication","replication","wal","high-availability","standby"]},{"term":"로그 전달 (Log Shipping)","full":"","definition":"트랜잭션 로그 백업을 주기적으로 standby 서버에 복사하고 복원하는 방식으로 동작하는 단순한 복제 방법. 스트리밍보다 지연이 크지만 설정이 쉽다. SQL Server에서 재해 복구용으로 흔히 사용된다.","tags":["replication","replication","log","backup","standby"]},{"term":"스냅샷 격리 (Snapshot Isolation)","full":"","definition":"각 트랜잭션이 시작 시점 기준의 일관된 데이터 스냅샷을 보는 격리 수준. 읽기는 쓰기를 막지 않지만 write skew 같은 일부 이상 현상에 노출된다. MVCC와 함께 구현된다.","tags":["isolation level","isolation","mvcc","snapshot","concurrency"]},{"term":"직렬화 가능 스냅샷 격리 (SSI)","full":"Serializable Snapshot Isolation","definition":"위험한 read-write 의존성을 추적하여 snapshot isolation 위에서 진정한 직렬화 가능성을 제공하는 방법. 충돌이 감지되면 트랜잭션 중 하나가 롤백된다. PostgreSQL의 SERIALIZABLE 수준에서 사용된다.","tags":["isolation level","isolation","serializable","ssi","concurrency"]},{"term":"갭 락 (Gap Lock)","full":"","definition":"인덱스 레코드 사이의 간격(아직 존재하지 않는 범위)에 거는 락으로, 그 범위에 새 행이 삽입되는 것을 막는다. phantom read 방지에 사용된다. InnoDB의 REPEATABLE READ 수준에서 작동한다.","tags":["locking","lock","innodb","phantom","concurrency"]},{"term":"넥스트 키 락 (Next-Key Lock)","full":"","definition":"인덱스 레코드에 대한 락과 그 앞 간격에 대한 락(gap lock)의 조합. 기존 행과 범위를 모두 보호하여 phantom insert를 방지한다. InnoDB의 기본 행 잠금 동작이다.","tags":["locking","lock","innodb","gap","concurrency"]},{"term":"낙관적 락 (Optimistic Locking)","full":"","definition":"충돌이 드물다고 가정하고 락 없이 작업하며 커밋 시점에만 데이터 변경 여부를 확인하는 방법. 대개 버전 번호나 타임스탬프로 구현한다. 충돌이 발견되면 작업을 재시도한다.","tags":["locking","lock","optimistic","concurrency","concurrency"]},{"term":"비관적 락 (Pessimistic Locking)","full":"","definition":"충돌이 발생할 것으로 가정하고 데이터를 읽거나 수정할 때 즉시 락을 거는 방법. 다른 트랜잭션은 락이 해제될 때까지 기다린다. 경합이 높은 환경에서 데이터 무결성을 보호하지만 동시성을 낮춘다.","tags":["locking","lock","pessimistic","concurrency","concurrency"]},{"term":"멱등성 (Idempotency)","full":"","definition":"작업을 한 번 실행하든 여러 번 실행하든 같은 결과를 내는 속성. 네트워크 오류 후 안전한 재시도를 가능하게 한다. 분산 시스템에서 중복 요청이 부작용을 일으키는 것을 막는다.","tags":["distributed systems","distributed","idempotent","retry","reliability"]},{"term":"서킷 브레이커 (Circuit Breaker)","full":"","definition":"계속 실패하는 서비스에 대한 호출을 일시적으로 차단하여 시스템을 보호하는 디자인 패턴. 회로가 \"열림\" 상태인 동안 요청은 즉시 거부되며, 일정 시간 후 시험 호출이 이루어진다. 연쇄 장애를 방지한다.","tags":["distributed systems","distributed","fault tolerance","resilience","pattern"]},{"term":"Saga 패턴","full":"","definition":"여러 서비스에 걸친 장기 실행 트랜잭션을 각 단계를 로컬 트랜잭션으로 만들고 실패 시 보상 트랜잭션을 사용하여 관리하는 패턴. 분산 환경에서 ACID 대신 eventual consistency를 제공한다. 마이크로서비스 아키텍처에서 흔히 사용된다.","tags":["distributed systems","distributed","saga","microservices","pattern"]},{"term":"Outbox 패턴","full":"","definition":"데이터베이스 갱신과 메시지 발행을 원자적으로 만들기 위해 같은 트랜잭션에서 메시지를 \"outbox\" 테이블에 기록하는 패턴. 별도 프로세스가 이 테이블을 읽어 메시지를 발행한다. 데이터와 이벤트 발행 간의 불일치를 방지한다.","tags":["distributed systems","distributed","outbox","messaging","pattern"]},{"term":"팬아웃 (Fan-out)","full":"","definition":"하나의 작업을 여러 하위 작업이나 대상으로 분배하는 것. 예: 게시물을 수천 명의 팔로워 피드에 기록. 쓰기 시점(write fan-out) 또는 읽기 시점(read fan-out)에 수행할 수 있다. 확장에서 중요한 설계 결정이다.","tags":["distributed systems","distributed","scaling","fanout","architecture"]},{"term":"핫스팟 (Hotspot)","full":"","definition":"특정 노드, 파티션 또는 키에 부하가 과도하게 집중되는 현상. 이 지점이 병목이 되어 시스템 전체의 성능을 제한한다. 잘못 선택한 sharding key가 흔한 원인이다.","tags":["distributed systems","distributed","hotspot","performance","sharding"]},{"term":"스큐 (데이터 불균형)","full":"","definition":"데이터나 부하가 파티션 간에 고르지 않게 분포하는 것. 일부 파티션은 넘치고 다른 파티션은 비어 병렬 효율이 떨어진다. 키 선택이나 salting으로 균형을 맞춘다.","tags":["distributed systems","distributed","skew","imbalance","sharding"]},{"term":"부분 인덱스 (Partial Index)","full":"","definition":"WHERE 조건을 만족하는 테이블 행만 포함하는 인덱스. 인덱스 크기를 줄이고 유지보수 비용을 낮추며 자주 조회되는 하위 집합을 가속한다. 예를 들어 활성 행만 인덱싱하는 데 사용된다.","tags":["index","index type","postgresql","partial","performance"]},{"term":"표현식 인덱스 (Expression Index)","full":"","definition":"컬럼 대신 식이나 함수의 결과에 구축하는 인덱스. 예를 들어 LOWER(email)에 구축하면 대소문자 구분 없는 검색이 빨라진다. 쿼리는 같은 식을 사용해야 한다.","tags":["index","index type","expression","function","performance"]},{"term":"GIN 인덱스","full":"Generalized Inverted Index","definition":"값 안의 여러 구성 요소를 인덱싱하는 역 인덱스 유형. 배열, JSONB, 전문 검색처럼 한 필드에 많은 키가 있는 경우에 이상적이다. PostgreSQL에서 사용된다.","tags":["index","index type","postgresql","gin","jsonb"]},{"term":"GiST 인덱스","full":"Generalized Search Tree","definition":"트리 기반의 확장 가능한 인덱스 프레임워크로, 데이터 유형별 맞춤 검색 전략을 정의할 수 있다. 지리공간(geometry), 범위, 최근접 이웃 쿼리에 적합하다. PostGIS의 기반이다.","tags":["index","index type","postgresql","gist","geospatial"]},{"term":"TOAST","full":"The Oversized-Attribute Storage Technique","definition":"한 페이지에 들어가지 않는 큰 필드 값을 압축하여 별도 영역에 저장하는 PostgreSQL의 기법. 큰 텍스트와 JSONB 값이 투명하게 분할된다. 메인 테이블을 작게 유지하여 성능을 보존한다.","tags":["storage","storage engine","postgresql","toast","large-values"]},{"term":"인덱스 전용 스캔 (Index-Only Scan)","full":"","definition":"필요한 모든 데이터가 인덱스에 있어 테이블을 전혀 읽지 않고 인덱스만으로 결과를 만드는 스캔. 디스크 접근을 크게 줄인다. PostgreSQL에서는 visibility map과 함께 동작한다.","tags":["index","index scan","scan","performance","postgresql"]},{"term":"HTAP","full":"Hybrid Transactional/Analytical Processing","definition":"트랜잭션(OLTP)과 분석(OLAP) 워크로드를 단일 시스템에서 함께 실행하는 방식. 분석을 위해 데이터를 별도 저장소로 옮기는 지연(ETL)을 없앤다. 대개 행 저장소와 컬럼 저장소를 결합한다.","tags":["analytics","oltp","olap","htap","architecture"]},{"term":"샤딩 키 (Sharding Key)","full":"","definition":"행이 어느 샤드에 위치할지 결정하는 컬럼(들). 잘 선택한 키는 부하를 고르게 분산하고 hotspot을 방지한다. 잘못 선택하면 데이터 불균형과 비용이 큰 cross-shard 쿼리로 이어진다.","tags":["distributed systems","distributed","sharding","key","scaling"]},{"term":"리밸런싱 (Rebalancing)","full":"","definition":"노드가 추가·제거되거나 부하가 불균형해질 때 데이터를 파티션 간에 재분배하는 것. 목표는 부하를 균등화하고 리소스를 효율적으로 사용하는 것이다. 실행 중 네트워크와 I/O 비용이 발생한다.","tags":["distributed systems","distributed","rebalancing","scaling","sharding"]},{"term":"펜싱 (Fencing)","full":"","definition":"오래되었거나 고립된 노드가 공유 리소스에 쓰지 못하도록 막아 데이터 손상을 방지하는 메커니즘. 대개 증가하는 \"fencing token\"을 사용해 오래된 요청을 거부한다. split-brain의 영향을 제한한다.","tags":["distributed systems","distributed","fencing","safety","split-brain"]},{"term":"워터마크 (Watermark)","full":"","definition":"스트림 처리 시스템에서 특정 시점까지의 모든 이벤트가 도착한 것으로 간주됨을 나타내는 진행 표시. 늦게 도착한 이벤트를 처리하고 윈도 계산을 언제 닫을지 결정하는 데 사용된다.","tags":["stream processing","streaming","watermark","event time","event"]},{"term":"툼스톤 (Tombstone)","full":"","definition":"레코드가 삭제되었음을 나타내기 위해 기록하는 특수 표시. 데이터가 즉시 물리적으로 삭제되지 않는 LSM 및 분산 시스템에서 삭제가 모든 replica로 전파되도록 보장한다. 실제 정리는 compaction 시 이루어진다.","tags":["storage","storage engine","lsm","tombstone","delete"]},{"term":"컴팩션 (LSM)","full":"","definition":"LSM 트리에서 디스크의 정렬된 파일(SSTable)을 병합하고 다시 쓰는 것. 오래된 버전과 tombstone을 제거하여 읽기 성능과 디스크 사용을 개선한다. 백그라운드에서 실행되며 쓰기 부하를 발생시킨다.","tags":["storage","storage engine","lsm","compaction","maintenance"]},{"term":"읽기 복구 (Read Repair)","full":"","definition":"읽기 중 replica 간 불일치가 감지되면 최신 값을 뒤처진 노드에 기록하여 복구하는 기법. 시스템은 일반 읽기 트래픽을 통해 스스로 일관성을 회복한다. Cassandra 같은 시스템에서 사용된다.","tags":["distributed systems","distributed","repair","consistency","replication"]},{"term":"힌티드 핸드오프 (Hinted Handoff)","full":"","definition":"대상 노드에 일시적으로 연결할 수 없을 때 쓰기를 다른 노드에 \"hint\"로 저장했다가 노드가 복귀하면 전달하는 것. 일시적 장애 중 쓰기가 손실되지 않도록 한다. eventual consistency를 지원한다.","tags":["distributed systems","distributed","handoff","availability","replication"]},{"term":"최종 일관성 (Eventual Consistency)","full":"","definition":"새 갱신이 멈추면 모든 replica가 결국 같은 값으로 수렴함을 보장하는 약한 일관성 모델. 짧은 불일치를 허용하지만 높은 가용성과 낮은 지연을 제공한다. 많은 NoSQL 시스템의 기본값이다.","tags":["distributed systems","distributed","consistency","eventual","nosql"]},{"term":"강한 일관성 (Strong Consistency)","full":"","definition":"쓰기가 완료되면 모든 읽기가 즉시 최신 값을 보는 일관성 모델. 사용이 직관적이지만 조정이 필요하므로 지연과 가용성 비용이 크다. 금융 시스템에서 선호된다.","tags":["distributed systems","distributed","consistency","strong","strong consistency"]},{"term":"선형화 가능성 (Linearizability)","full":"","definition":"각 작업이 시작과 종료 사이의 한 순간에 적용되는 것처럼 보이고 모든 작업이 단일 실시간 순서에 들어맞음을 보장하는 가장 강한 일관성 수준. 시스템이 마치 단일 사본인 것처럼 동작한다. 구현 비용이 크다.","tags":["distributed systems","distributed","consistency","linearizability","strong consistency"]},{"term":"2단계 락킹 (Two-Phase Locking, 2PL)","full":"Two-Phase Locking","definition":"트랜잭션이 먼저 락 획득만 하고(growing phase) 그 후 락 해제만 하는(shrinking phase) 2단계 잠금 규칙. 이 규율이 직렬화 가능성을 보장한다. 엄격한 형태(Strict 2PL)에서는 커밋까지 락을 유지한다.","tags":["locking","lock","2pl","serializable","concurrency"]},{"term":"데드락 탐지 (Deadlock Detection)","full":"","definition":"둘 이상의 트랜잭션이 서로 보유한 락을 기다리는 데드락을 감지하는 메커니즘. 대개 wait-for 그래프에서 순환을 찾아 발견하며, 하나의 트랜잭션을 victim으로 선택해 롤백한다. 그러면 시스템이 다시 진행된다.","tags":["locking","lock","deadlock","deadlock detection","concurrency"]}]},"guide/glossary-3.json":{"terms":[{"term":"쓰기 증폭 (Write Amplification)","full":"쓰기 증폭 (Write Amplification)","definition":"하나의 논리적 쓰기가 스토리지 계층에서 훨씬 많은 물리적 쓰기를 유발하는 현상(LSM compaction, 페이지 재기록, WAL, SSD 블록 소거). 값이 높으면 디스크가 마모되고 쓰기 처리량이 줄어든다.","tags":["storage","lsm","performance","ssd"]},{"term":"읽기 증폭 (Read Amplification)","full":"읽기 증폭 (Read Amplification)","definition":"하나의 논리적 읽기가 실제로는 여러 번의 물리적/내부 읽기를 필요로 하는 현상. LSM 트리에서 여러 레벨에 걸쳐 키를 찾는 것이 전형적인 예이며, bloom filter와 compaction이 이를 줄인다.","tags":["storage","lsm","performance"]},{"term":"LSM-tree","full":"Log-Structured Merge Tree","definition":"쓰기를 먼저 메모리 구조(memtable)에 흡수하고, 정렬된 블록(SSTable)으로 디스크에 flush한 뒤 백그라운드에서 병합하는(compaction) 자료 구조. 쓰기 위주 워크로드에 이상적이며 RocksDB와 Cassandra가 사용한다.","tags":["storage","lsm","write-heavy"]},{"term":"B+tree","full":"B+ 트리 (B+ Tree)","definition":"관계형 데이터베이스 인덱스의 고전적 구조. 모든 데이터는 리프 노드에 있고 리프는 연결 리스트로 이어진다. 포인트 쿼리와 범위 쿼리에 균형 잡힌 읽기 성능을 제공한다.","tags":["index","btree","storage"]},{"term":"Fill Factor (채우기 비율)","full":"Fill Factor (채우기 비율)","definition":"인덱스/테이블 페이지를 처음에 얼마나 채울지 지정하는 비율. 여유 공간을 남기면 이후 update/insert 시 page split이 줄어든다. PG에서는 fillfactor, MSSQL에서는 fill factor.","tags":["index","storage","tuning"]},{"term":"HOT 업데이트 (HOT Update)","full":"Heap-Only Tuple Update","definition":"PostgreSQL에서 인덱스 컬럼을 변경하지 않는 update는 새 행 버전을 같은 페이지에 만들고 인덱스를 갱신하지 않는다. 인덱스 bloat를 줄이고 update 비용을 낮춘다.","tags":["postgres","mvcc","update","performance"]},{"term":"Bloat (팽창)","full":"Table/Index Bloat","definition":"MVCC 엔진에서 정리되지 않은 dead 행 버전이나 빈 페이지 때문에 테이블/인덱스가 필요 이상의 공간을 차지하는 현상. 스캔 비용을 높이며, VACUUM/reorg로 공간을 회수한다.","tags":["mvcc","postgres","maintenance","bloat"]},{"term":"Autovacuum","full":"Automatic VACUUM","definition":"dead tuple 정리, 통계 수집, 트랜잭션 ID freeze를 자동으로 수행하는 PostgreSQL 백그라운드 프로세스. 임계값과 비용 파라미터로 워크로드에 맞게 조정한다.","tags":["postgres","vacuum","maintenance"]},{"term":"동결 / 랩어라운드 (Freeze / Wraparound)","full":"Transaction ID Freeze / Wraparound","definition":"PostgreSQL에서 32비트 트랜잭션 ID가 순환(wraparound)하지 않도록 오래된 행을 'frozen'으로 표시하는 것. 방치하면 데이터베이스는 데이터 손실을 막기 위해 쓰기를 받지 않는다.","tags":["postgres","mvcc","vacuum","wraparound"]},{"term":"체크포인트 (Checkpoint)","full":"체크포인트 (Checkpoint)","definition":"메모리의 dirty 페이지를 디스크에 기록하고 복구가 시작될 지점을 기록하는 이벤트. 장애 복구 시간을 단축하며, 빈도는 I/O 피크와 복구 시간 간 트레이드오프다.","tags":["recovery","wal","internals","checkpoint"]},{"term":"퍼지 체크포인트 (Fuzzy Checkpoint)","full":"퍼지 체크포인트 (Fuzzy Checkpoint)","definition":"시스템을 멈추지 않고 dirty 페이지를 백그라운드에서 시간에 걸쳐 분산하여 계속 기록하는 checkpoint 유형. 갑작스러운 I/O 급증(checkpoint spike)을 피해 응답 시간을 예측 가능하게 유지한다.","tags":["recovery","checkpoint","internals"]},{"term":"Redo","full":"Redo Log / Roll Forward","definition":"복구 시 트랜잭션이 수행한 변경을 재적용(롤포워드)하기 위해 유지하는 기록. 장애 전 디스크에 도달하지 못한 커밋된 변경을 복원한다.","tags":["recovery","oracle","wal","redo"]},{"term":"Undo","full":"Undo Log / Rollback","definition":"트랜잭션을 롤백하고 MVCC를 위한 이전 행 버전을 제공하기 위해 유지하는 기록. Oracle/InnoDB에서는 이전 버전을 undo segment에서 읽는다.","tags":["mvcc","oracle","innodb","undo","rollback"]},{"term":"MVCC 스냅샷","full":"MVCC 스냅샷","definition":"트랜잭션이 일관된 읽기를 보도록 어떤 행 버전이 보이는지 결정하는 논리적 시점 뷰. 트랜잭션 시작 시 또는 문장 시작 시(격리 수준에 따라) 생성된다.","tags":["mvcc","concurrency","isolation","snapshot"]},{"term":"격리 이상 현상 (Isolation Anomaly)","full":"Isolation Anomalies","definition":"격리가 불충분할 때 동시 트랜잭션이 만들어내는 불일치: dirty read(커밋되지 않은 데이터 읽기), non-repeatable read(같은 행이 변경됨), phantom(새 행 출현), write skew(독립적으로 보이는 쓰기가 함께 규칙을 위반).","tags":["isolation","concurrency","anomaly","acid"]},{"term":"2PL","full":"Two-Phase Locking","definition":"락으로 직렬화 가능성을 보장하는 프로토콜: 트랜잭션은 먼저 락 획득만 하고(growing phase), 특정 시점 이후에는 해제만 한다(shrinking phase). Strict 2PL은 커밋까지 락을 유지한다.","tags":["concurrency","locking","serializable","2pl"]},{"term":"MVTO","full":"Multi-Version Timestamp Ordering","definition":"트랜잭션에 타임스탬프를 부여하고 그에 따라 다중 버전 데이터의 읽기/쓰기 순서를 정하는 동시성 제어 방법. 락 대신 타임스탬프로 충돌을 해결한다.","tags":["concurrency","mvcc","timestamp","protocol"]},{"term":"뮤텍스 (Mutex)","full":"Mutual Exclusion","definition":"한 번에 하나의 스레드만 리소스에 접근하도록 보장하는 상호 배제 기본 요소. 데이터베이스에서는 공유 메모리 구조를 보호하는 latch와 유사한 역할을 한다.","tags":["concurrency","internals","mutex"]},{"term":"스핀락 (Spinlock)","full":"스핀락 (Spinlock)","definition":"락이 해제될 때까지 스레드를 잠재우지 않고 루프(busy-wait)시키는 매우 짧은 수명의 락. 매우 짧은 임계 구역에서는 저렴하지만 경합 시 CPU를 소모한다.","tags":["concurrency","internals","spinlock","cpu"]},{"term":"버퍼 풀 (Buffer Pool)","full":"버퍼 풀 / 버퍼 캐시 (Buffer Pool / Buffer Cache)","definition":"자주 접근하는 데이터 페이지를 메모리에 유지하여 디스크 I/O를 줄이는 캐시. 교체 알고리즘(LRU, clock-sweep)이 어떤 페이지를 내보낼지 결정하며, 크기는 working set에 맞춰 설정한다.","tags":["memory","cache","internals","buffer"]},{"term":"워킹 셋 (Working Set)","full":"워킹 셋 (Working Set)","definition":"워크로드가 일정 시간 동안 활발히 접근하는 데이터 집합. buffer pool이 이 집합을 담을 만큼 크면 물리적 I/O가 최소화된다. 전체 데이터 크기와는 다르다.","tags":["memory","cache","sizing","performance"]},{"term":"스큐 (Skew)","full":"Data Skew","definition":"값이 고르게 분포하지 않고 일부 값이 훨씬 자주 나타나는 상황. 카디널리티 오추정, parameter sniffing 문제, 샤딩 hot spot의 주요 원인이며 histogram으로 처리한다.","tags":["statistics","distribution","skew","performance"]},{"term":"플랜 캐시 (Plan Cache)","full":"플랜 캐시 (Plan Cache)","definition":"컴파일된 실행 계획을 재사용하기 위해 저장하는 메모리 영역. hard parse 비용을 피하지만 parameter sniffing과 오래된 계획 위험을 수반한다. MSSQL에서는 plan cache, Oracle에서는 library cache.","tags":["optimizer","plan-cache","parsing"]},{"term":"하드/소프트 파싱 (Hard/Soft Parse)","full":"Hard Parse / Soft Parse","definition":"Hard parse: SQL을 처음부터 파싱·최적화하고 계획을 생성한다(비용 큼). Soft parse: 캐시에서 기존 계획을 찾아 재사용한다(저렴). bind variable은 soft parse 비율을 높인다.","tags":["parsing","oracle","performance","plan-cache"]},{"term":"커넥션 폭주 (Connection Storming)","full":"Connection Storm","definition":"짧은 시간 안에 대량의 새 데이터베이스 연결이 동시에 열리는 현상. 연결 설정 비용과 메모리 압박으로 데이터베이스를 압도하며, 대개 pool 고갈이나 재연결 폭주에서 비롯된다.","tags":["connections","pooling","overload"]},{"term":"썬더링 허드 (Thundering Herd)","full":"Thundering Herd Problem","definition":"어떤 이벤트(캐시 만료, 락 해제, 서비스 복귀) 후 많은 클라이언트가 동시에 같은 리소스에 몰리는 현상. backpressure, jitter, single-flight 패턴으로 완화한다.","tags":["concurrency","cache","overload","resilience"]},{"term":"백프레셔 (Backpressure)","full":"백프레셔 (Backpressure)","definition":"시스템이 감당할 수 없는 부하를 받아들이는 대신 생산자를 늦추거나 거부하는 것(큐 한도, rate limit). 과부하로 붕괴하는 대신 점진적 성능 저하(graceful degradation)를 가능하게 한다.","tags":["resilience","flow-control","overload"]},{"term":"리스 (Lease)","full":"리스 (Lease)","definition":"노드에 일정 기간 부여되며 만료되면 자동으로 무효가 되는 리더십/리소스 권한. 시간 제한이 있으므로 멈춘 노드가 영원히 리더로 남는 것을 방지한다.","tags":["distributed","leader-election","lease"]},{"term":"자기 쓰기 읽기 (Read-Your-Writes)","full":"Read-Your-Writes Consistency","definition":"사용자가 바로 다음 읽기에서 항상 자신의 변경을 보도록 보장하는 세션 일관성. 지연이 있는 replica로 라우팅할 때는 특수 라우팅/토큰으로 구현한다.","tags":["distributed","consistency","session","replication"]},{"term":"인과적 일관성 (Causal Consistency)","full":"인과적 일관성 (Causal Consistency)","definition":"인과적으로 관련된 작업이 모든 노드에서 같은 순서로 보이도록 보장하는, eventual보다 강한 일관성 모델. 관련 없는(동시) 작업은 서로 다른 순서로 보일 수 있다.","tags":["distributed","consistency","causal"]},{"term":"CAP","full":"Consistency, Availability, Partition tolerance","definition":"네트워크 분할 중 분산 시스템은 일관성과 가용성 중 하나만 선택할 수 있다는 정리. 분할이 없으면 이 트레이드오프는 필수가 아니다.","tags":["distributed","cap","consistency","theory"]},{"term":"PACELC","full":"PACELC Theorem","definition":"CAP을 확장한 모델: 분할(P) 시에는 Availability와 Consistency 사이에서, 그렇지 않으면(E) Latency와 Consistency 사이에서 선택한다. 정상 운영 중의 지연-일관성 트레이드오프도 다룬다.","tags":["distributed","cap","pacelc","latency","theory"]},{"term":"핫 파티션 (Hot Partition)","full":"Hot Partition / Hot Spot","definition":"단일 파티션/shard에 부하가 불균형하게 쌓이는 현상. 카디널리티가 낮거나 시간 기반 또는 단조 증가하는 샤드 키에서 비롯되며, 해당 노드를 병목으로 만들고 확장을 멈추게 한다.","tags":["distributed","sharding","hot-spot","skew"]},{"term":"컬럼스토어 (Columnstore)","full":"Column-Oriented Storage","definition":"데이터를 행이 아닌 컬럼 단위로 저장하는 형식. 분석 쿼리는 필요한 컬럼만 읽으며, 높은 압축률과 벡터화 실행을 제공한다. OLAP에 이상적이다.","tags":["olap","columnar","analytics","compression"]},{"term":"벡터화 실행 (Vectorized Execution)","full":"벡터화 실행 (Vectorized Execution)","definition":"쿼리 연산자가 행 단위가 아니라 컬럼 값 블록(batch) 단위로 동작하는 것. CPU 캐시와 SIMD를 더 잘 활용하여 분석 쿼리 처리량을 크게 높인다.","tags":["olap","execution","performance","columnar"]},{"term":"존 맵 / Min-Max (Zone Map)","full":"Zone Map / Min-Max Index","definition":"각 데이터 블록의 컬럼 최솟값과 최댓값을 저장하는 가벼운 메타데이터. 쿼리 조건과 일치할 수 없는 블록을 읽지 않고 건너뛰어(block skipping) 스캔 비용을 줄인다.","tags":["olap","index","pruning","columnar"]},{"term":"논리 복제","full":"논리 복제","definition":"물리 블록 대신 행 수준 변경(논리 이벤트)을 publish/subscribe하는 복제. 선택적 테이블, 버전 간, 양방향 시나리오를 지원하며 PG의 publication/subscription이 이를 제공한다.","tags":["replication","postgres","logical","cdc"]},{"term":"GTID","full":"Global Transaction Identifier","definition":"MySQL 복제에서 모든 트랜잭션에 부여되는 전역 고유 식별자. 파일/오프셋 대신 트랜잭션 ID로 replica 위치를 추적하여 페일오버와 재구성을 안전하고 간단하게 한다.","tags":["mysql","replication","gtid","failover"]},{"term":"하이 워터 마크 (High Water Mark)","full":"하이 워터 마크 (High Water Mark)","definition":"세그먼트/테이블에서 지금까지 사용된 가장 높은 블록 경계. Oracle에서 full table scan은 이 경계까지 읽는다. 대량 삭제 후에도 내려가지 않으므로 빈 블록 스캔과 bloat로 이어질 수 있다.","tags":["oracle","storage","internals","scan"]}]},"playbooks/playbooks.json":{"schemaVersion":1,"playbooks":[{"id":"db-slow","title":"데이터베이스가 느림","icon":"🐢","symptom":"애플리케이션 전반의 느려짐. 쿼리가 평소보다 훨씬 오래 걸리고 사용자 불만이 제기된다.","steps":[{"title":"먼저 정확히 무엇이 느린지 명확히 한다","detail":"전부 느린가, 특정 쿼리/화면만 느린가? 언제 시작되었으며 배포/패치/데이터 증가와 시기가 겹치는가? 시스템 전체인가, 단일 DB인가? 이를 통해 조사를 CPU, I/O, 락, 계획 변경 중 하나의 축으로 좁힌다."},{"title":"활성 세션과 대기 이벤트를 확인한다","detail":"지금 무엇이 실행 중이고 무엇이 대기 중인가? 지배적인 wait event(I/O, 락, 로그)가 병목을 가리킨다.","engines":{"oracle":"SELECT sid, username, status, event, sql_id FROM v$session WHERE status='ACTIVE' AND type='USER';","postgres":"SELECT pid, state, wait_event_type, wait_event, query FROM pg_stat_activity WHERE state='active';","mysql":"SELECT id, user, command, time, state, info FROM information_schema.processlist WHERE command<>'Sleep' ORDER BY time DESC;","mssql":"SELECT session_id, status, wait_type, wait_time, blocking_session_id FROM sys.dm_exec_requests WHERE session_id IN (SELECT session_id FROM sys.dm_exec_sessions WHERE is_user_process = 1);","mongodb":"db.currentOp({ active: true, secs_running: { $gt: 1 } })","redis":"SLOWLOG GET 10"}},{"title":"블로킹을 확인한다","detail":"느려짐의 가장 흔한 원인은 한 세션이 다른 세션을 막는 것이다. 블로킹 세션을 찾아 체인의 시작점을 식별한다.","engines":{"oracle":"SELECT blocking_session, sid, seconds_in_wait, event FROM v$session WHERE blocking_session IS NOT NULL;","postgres":"SELECT pid, pg_blocking_pids(pid) AS blocked_by, query FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid))>0;","mysql":"SELECT * FROM sys.innodb_lock_waits\\G","mssql":"SELECT session_id, blocking_session_id, wait_type FROM sys.dm_exec_requests WHERE blocking_session_id<>0;"}},{"title":"가장 비용이 큰 쿼리를 추출한다","detail":"리소스를 가장 많이 소비하는 상위 10개 쿼리를 찾는다. 대개 문제의 90%는 소수의 쿼리에서 비롯된다.","engines":{"oracle":"SELECT sql_id, elapsed_time/1e6 sec, executions, sql_text FROM v$sql ORDER BY elapsed_time DESC FETCH FIRST 10 ROWS ONLY;","postgres":"SELECT calls, total_exec_time, mean_exec_time, query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;","mysql":"SELECT digest_text, count_star, sum_timer_wait/1e12 sec FROM performance_schema.events_statements_summary_by_digest ORDER BY sum_timer_wait DESC LIMIT 10;","mssql":"SELECT TOP 10 total_elapsed_time/execution_count avg_us, execution_count, SUBSTRING(t.text,1,200) FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) t ORDER BY total_elapsed_time DESC;"}},{"title":"의심 쿼리의 계획을 분석한다","detail":"느린 쿼리의 EXPLAIN을 확인한다. full table scan, 누락된 인덱스, 잘못된 조인 순서, 오래된 통계를 찾는다. 계획이 갑자기 바뀌었다면 통계 수집을 고려한다.","engines":{"oracle":"SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('&sql_id', NULL, 'ALLSTATS LAST'));","postgres":"EXPLAIN (ANALYZE, BUFFERS) <query>;","mysql":"EXPLAIN ANALYZE <query>;","mssql":"SET STATISTICS PROFILE ON; <query>; SET STATISTICS PROFILE OFF;","mongodb":"db.my_collection.find({...}).explain('executionStats')  // COLLSCAN or IXSCAN?"}},{"title":"시스템 리소스를 확인한다","detail":"OS 수준에서 CPU, I/O 대기(iowait), 메모리, 디스크 사용량을 확인한다. cache hit ratio를 확인하고, 낮으면 메모리 병목일 수 있다. 문제가 DB 외부에 있을 수도 있다(디스크 가득 참, noisy neighbor VM)."},{"title":"결정: 임시 조치인가 근본 해결인가","detail":"즉각적인 완화: 폭주 쿼리 종료, 누락된 인덱스 추가, 통계 수집. 근본 해결: 쿼리/인덱스 재설계, 메모리 설정 수정, 계획 안정성 확보(baseline/hint)."}],"related":["active-sessions","blocking-sessions","top-slow-queries","explain-plan","cache-hit-ratio"]},{"id":"disk-full","title":"디스크 / 테이블스페이스 가득 참","icon":"💽","symptom":"'unable to extend', 'disk full', 'log file full' 오류. 쓰기가 멈추고 있다.","steps":[{"title":"당황하지 말고 먼저 무엇이 가득 찼는지 찾는다","detail":"datafile인가, 트랜잭션 로그인가, archive/redo 영역인가, temp인가, OS 디스크인가? 엉뚱한 곳을 정리하면 도움이 되지 않는다. OS 수준에서는 df -h로, DB 내부에서는 공간 뷰로 디스크를 확인한다."},{"title":"사용량을 측정한다","detail":"어떤 테이블스페이스/파일/디렉터리가 몇 퍼센트 가득 찼는가?","engines":{"oracle":"SELECT d.tablespace_name, ROUND((1-(d.alloc-NVL(f.free,0))/d.maxb)*100,1) pct_free FROM (SELECT tablespace_name, SUM(bytes) alloc, SUM(GREATEST(bytes,maxbytes)) maxb FROM dba_data_files GROUP BY tablespace_name) d LEFT JOIN (SELECT tablespace_name, SUM(bytes) free FROM dba_free_space GROUP BY tablespace_name) f ON f.tablespace_name = d.tablespace_name;","postgres":"SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database ORDER BY 2 DESC; -- use OS df for the disk","mysql":"SELECT table_schema, ROUND(SUM(data_length+index_length)/1048576,1) mb FROM information_schema.tables GROUP BY table_schema ORDER BY mb DESC;","mssql":"SELECT name, size/128.0 size_mb, size/128.0 - CAST(FILEPROPERTY(name,'SpaceUsed') AS int)/128.0 free_mb FROM sys.database_files;","redis":"INFO memory  # used_memory vs maxmemory"}},{"title":"가장 큰 소비자를 찾는다","detail":"가장 큰 테이블/세그먼트, 가장 큰 로그/아카이브 파일, 불필요한 오래된 백업을 식별한다. 대부분 하나의 폭주 테이블이나 쌓인 아카이브 로그가 원인이다.","engines":{"oracle":"SELECT segment_name, bytes/1048576 mb FROM dba_segments ORDER BY bytes DESC FETCH FIRST 10 ROWS ONLY;","postgres":"SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) FROM pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;","mssql":"SELECT TOP 10 OBJECT_NAME(object_id), SUM(reserved_page_count)*8/1024 mb FROM sys.dm_db_partition_stats GROUP BY object_id ORDER BY mb DESC;"}},{"title":"공간을 안전하게 회수한다(신속)","detail":"로그/아카이브: 백업한 후 오래된 아카이브 로그/백업을 정리한다(Oracle: RMAN delete archivelog, MSSQL: 로그 백업 후 로그 shrink). Redis: maxmemory-policy와 eviction. 운영 중인 datafile을 절대 수동으로 삭제하지 않는다. 백업 없이 아카이브 로그를 절대 삭제하지 않는다."},{"title":"용량을 영구적으로 늘린다","detail":"datafile에 공간을 추가하거나 autoextend를 활성화하고(Oracle: ALTER DATABASE DATAFILE ... AUTOEXTEND ON MAXSIZE), 디스크를 늘리고, 테이블스페이스를 추가한다. 근본 원인을 해결한다: 로그 로테이션, 아카이브 정리 작업, retention 정책."},{"title":"재발을 방지한다","detail":"디스크 사용량 알림(80%/90% 임계값)을 설정하고, 아카이브/로그 정리를 자동화하며, retention 정책을 명확히 정의한다. 이 플레이북을 다시 열 일이 없도록 한다."}],"related":["free-space","largest-tables","database-size","reclaim-space","last-backup-status"]},{"id":"blocking","title":"블로킹 및 락 위기","icon":"🔒","symptom":"쿼리가 멈추고 'lock wait timeout'이 발생하며, 많은 세션이 대기 중이지만 CPU는 낮다.","steps":[{"title":"블로킹 체인의 시작점(root blocker)을 찾는다","detail":"누가 누구를 막고 있는지 파악한다. 진짜 원인은 자신은 아무것도 기다리지 않으면서 다른 세션을 기다리게 하는 세션이다. 체인 맨 위의 세션에 집중한다.","engines":{"oracle":"SELECT blocking_session, sid, serial#, username, seconds_in_wait, sql_id FROM v$session WHERE blocking_session IS NOT NULL ORDER BY seconds_in_wait DESC;","postgres":"SELECT pid, usename, pg_blocking_pids(pid) blocked_by, state, query FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid))>0;","mysql":"SELECT waiting_pid, waiting_query, blocking_pid, blocking_query FROM sys.innodb_lock_waits;","mssql":"SELECT session_id, blocking_session_id, wait_type, wait_resource FROM sys.dm_exec_requests WHERE blocking_session_id<>0;","mongodb":"db.currentOp({ waitingForLock: true })"}},{"title":"root blocker가 무엇을 하는지 파악한다","detail":"열려 있지만 커밋되지 않은 트랜잭션인가? 애플리케이션이 'idle in transaction' 상태로 두었는가? 긴 배치인가? 무작정 종료하기 전에 무엇인지 파악한다. 곧 끝날 수도 있다.","engines":{"oracle":"SELECT s.sid, s.prev_sql_id, t.start_time, t.used_ublk FROM v$transaction t JOIN v$session s ON t.ses_addr=s.saddr;","postgres":"SELECT pid, state, xact_start, now()-xact_start age, query FROM pg_stat_activity WHERE pid=<root_pid>;"}},{"title":"결정: 대기, 애플리케이션에서 해결, 또는 종료","detail":"곧 끝나면 기다린다. 애플리케이션이 커밋/롤백할 수 있으면 그렇게 하도록 한다. 그렇지 않고 업무가 멈췄다면 root blocker를 종료한다. 종료 전에 영향(롤백 시간, 데이터)을 고려한다."},{"title":"필요하면 root blocker를 종료한다","detail":"체인 맨 앞의 세션만 종료한다. 그 아래 세션들은 자동으로 해제된다.","engines":{"oracle":"ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE;","postgres":"SELECT pg_terminate_backend(<root_pid>);","mysql":"KILL <root_id>;","mssql":"KILL <root_session_id>;","mongodb":"db.killOp(<opid>)"}},{"title":"근본 원인을 해결한다","detail":"누락된 인덱스가 넓은 락을 유발할 수 있고, 애플리케이션이 트랜잭션을 너무 오래 열어 둘 수 있으며, 격리 수준이 잘못되었을 수 있다. 인덱스를 추가하고, 트랜잭션을 짧게 하고, 'idle in transaction' timeout을 설정하며, 적절한 격리 수준을 선택한다."}],"related":["blocking-sessions","kill-session","open-transactions"]},{"id":"connection-storm","title":"커넥션 폭주 (too many connections)","icon":"🌊","symptom":"'too many connections', 'ORA-00018', 'remaining connection slots reserved' — 새 연결이 거부되고 있다.","steps":[{"title":"현재 상태를 측정한다","detail":"연결이 몇 개이고 한도는 얼마이며, 활성과 유휴는 각각 몇 개인가?","engines":{"oracle":"SELECT COUNT(*) total, SUM(CASE WHEN status='ACTIVE' THEN 1 ELSE 0 END) active FROM v$session WHERE type='USER';","postgres":"SELECT count(*), state FROM pg_stat_activity WHERE backend_type = 'client backend' GROUP BY state;","mysql":"SHOW STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';","mssql":"SELECT COUNT(*), status FROM sys.dm_exec_sessions WHERE is_user_process=1 GROUP BY status;","redis":"INFO clients"}},{"title":"원인을 식별한다: 누가/어디서","detail":"어떤 애플리케이션/호스트/사용자가 연결을 열고 있는가? 대개 한 애플리케이션 서버의 pool이 폭주했거나 연결 누수(idle in transaction 세션 다수)가 있다.","engines":{"postgres":"SELECT client_addr, usename, state, count(*) FROM pg_stat_activity WHERE backend_type = 'client backend' GROUP BY 1,2,3 ORDER BY 4 DESC;","mysql":"SELECT host, user, count(*) FROM information_schema.processlist GROUP BY host, user ORDER BY 3 DESC;","mssql":"SELECT host_name, program_name, login_name, COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process=1 GROUP BY host_name, program_name, login_name ORDER BY 4 DESC;"}},{"title":"즉시 여유를 확보한다","detail":"장시간 idle/idle-in-transaction 연결을 정리한다. 필요하면 문제 애플리케이션의 연결을 일괄 종료한다. 관리자 로그인용으로 예약된 슬롯을 사용한다(PG superuser_reserved_connections)."},{"title":"올바른 해결책: connection pooling","detail":"max_connections를 무작정 올리면 메모리를 소모하고 문제를 미룰 뿐이다. 올바른 해결책은 애플리케이션 측 connection pool(HikariCP, PgBouncer, ProxySQL)과 적절한 pool 크기다. pool 크기는 보통 (코어*2 + 디스크 수) 정도다 — Tools > Connection Pool 계산기 참조."},{"title":"누수와 재발을 방지한다","detail":"애플리케이션이 연결을 닫는지 확인한다(finally/using). idle_in_transaction_session_timeout과 wait_timeout을 설정한다. 연결 수에 대한 알림을 설정한다."}],"related":["connection-count","kill-session","open-transactions"]},{"id":"replication-lag","title":"복제 지연 / 중지","icon":"🔁","symptom":"standby/replica가 뒤처지거나 완전히 멈췄으며, 읽기 replica가 오래된 데이터를 보여 준다.","steps":[{"title":"지연이 얼마나 크며 증가 중인가 안정적인가","detail":"지연은 초/바이트로 얼마인가? 일정한 차이인가, 계속 증가하는가? 증가한다면 replica가 적용을 따라가지 못하는 것이고, 일정하다면 일시적인 정체가 있었던 것이다.","engines":{"oracle":"SELECT name, value, unit FROM v$dataguard_stats WHERE name LIKE '%lag%';","postgres":"SELECT client_addr, state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;","mysql":"SHOW REPLICA STATUS\\G  -- Seconds_Behind_Source","mssql":"SELECT ar.replica_server_name, drs.redo_queue_size, drs.log_send_queue_size FROM sys.dm_hadr_database_replica_states drs JOIN sys.availability_replicas ar ON drs.replica_id=ar.replica_id;","redis":"INFO replication  # master_repl_offset vs slave offset"}},{"title":"복제가 실행 중인가, 오류가 있는가","detail":"IO/SQL 스레드 상태, apply 프로세스는 어떠하며 연결이 끊겼는가? 오류(예: 충돌 행, 누락 테이블)가 있으면 적용이 멈춘 것이므로 오류 메시지를 읽는다.","engines":{"mysql":"SHOW REPLICA STATUS\\G  -- Replica_IO_Running / Replica_SQL_Running / Last_Error","postgres":"SELECT status, last_msg_receipt_time, latest_end_lsn FROM pg_stat_wal_receiver;  -- on the standby","oracle":"SELECT process, status, sequence#, thread# FROM v$managed_standby WHERE process LIKE 'MRP%' OR process LIKE 'RFS%';"}},{"title":"병목을 찾는다","detail":"왜 따라가지 못하는가? 단일 스레드 적용과 무거운 쓰기 부하(PG/MySQL은 대부분 직렬 적용), standby의 I/O 병목, 네트워크, 또는 적용을 막는 긴 쿼리(PG hot_standby_feedback / recovery conflict)인가?"},{"title":"지연을 해소한다","detail":"적용을 가속한다: 병렬 적용 활성화(MySQL replica_parallel_workers — 8.0.26 이전에는 slave_parallel_workers, PG에는 없지만 recovery 설정이 있음), standby I/O 개선, 적용을 막는 쿼리 해결. 오류로 멈췄다면 오류를 수정하고(충돌 행을 수동 해결) 복제를 재개한다."},{"title":"깨졌다면 재구축한다","detail":"replica가 크게 뒤처졌거나 깨졌다면 재동기화가 가장 깔끔하다: 새 백업으로 standby를 재구축한다(PG pg_basebackup, MySQL clone/xtrabackup, Oracle RMAN duplicate). 중요: 재구축 전에 원인을 찾지 않으면 다시 발생한다."}],"related":["replication-status","list-replicas","promote-failover"]},{"id":"high-cpu","title":"높은 CPU","icon":"🔥","symptom":"서버 CPU 90% 이상. 시스템 전체가 느리고 load average가 높다.","steps":[{"title":"실제로 DB가 CPU를 소비하는지 확인한다","detail":"OS 수준에서 top/mpstat으로 CPU를 소비하는 프로세스가 정말 DB인지 확인한다. 때로는 인접 프로세스나 백업/백신이 소비자다. iowait가 높으면 문제는 CPU가 아니라 디스크다."},{"title":"CPU를 가장 많이 쓰는 쿼리를 찾는다","detail":"CPU 시간 기준으로 정렬한 상위 쿼리를 추출한다. 대개 몇몇 쿼리(나쁜 계획, full scan, 누락된 인덱스)가 CPU를 소모한다.","engines":{"oracle":"SELECT sql_id, cpu_time/1e6 cpu_sec, executions, sql_text FROM v$sql ORDER BY cpu_time DESC FETCH FIRST 10 ROWS ONLY;","postgres":"SELECT query, calls, total_exec_time, mean_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;","mysql":"SELECT digest_text, count_star, sum_timer_wait/1e12 sec FROM performance_schema.events_statements_summary_by_digest ORDER BY sum_timer_wait DESC LIMIT 10;","mssql":"SELECT TOP 10 total_worker_time/execution_count avg_cpu, execution_count, SUBSTRING(t.text,1,200) FROM sys.dm_exec_query_stats q CROSS APPLY sys.dm_exec_sql_text(q.sql_handle) t ORDER BY total_worker_time DESC;"}},{"title":"계획/인덱스 문제를 확인한다","detail":"문제 쿼리의 계획을 확인한다. full table scan, nested loop 폭증, hash join spill, 또는 parameter sniffing/오래된 통계로 인한 나쁜 계획이 CPU를 소모한다. 인덱스를 추가하거나 통계를 수집한다."},{"title":"즉시 완화한다","detail":"폭주 쿼리를 종료하고 필요하면 Resource Manager/Resource Governor로 제한한다. 갑자기 시작된 부하라면 최근 배포/파라미터 변경을 검토한다."},{"title":"영구적으로 해결한다","detail":"쿼리/인덱스를 수정하고, 계획 안정성을 확보하며, 통계 수집 작업을 스케줄링한다. CPU가 항상 100%라면 확장(CPU 추가, 읽기를 read replica로 분산)이 필요할 수 있다."}],"related":["top-slow-queries","explain-plan","active-sessions","update-statistics"]},{"id":"memory-pressure","title":"메모리 압박 / OOM","icon":"🧠","symptom":"OOM killer가 프로세스를 종료하고, 'out of memory', 낮은 cache hit ratio, 과도한 swap, Redis 'OOM command not allowed'가 발생한다.","steps":[{"title":"누가 메모리를 소비하는가","detail":"DB에 할당된 메모리(buffer pool/SGA)인가, 세션별 메모리(PGA/sort/work_mem)인가, OS 전체인가? OS 수준에서는 free/vmstat으로, DB 내부에서는 메모리 뷰로 확인한다.","engines":{"oracle":"SELECT component, current_size/1048576 mb FROM v$memory_dynamic_components WHERE current_size>0;","postgres":"SHOW shared_buffers; SHOW work_mem; -- work_mem * connections * number of sorts can blow up in total","mysql":"SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW STATUS LIKE 'Innodb_buffer_pool%';","redis":"INFO memory  # used_memory, maxmemory, mem_fragmentation_ratio"}},{"title":"설정 문제와 부하 문제를 구분한다","detail":"메모리 설정의 합계가 물리 RAM을 초과하는가(over-commit)? shared_buffers + (work_mem * 동시성)을 계산한다. 아니면 일시적인 부하(거대한 sort/hash, 많은 연결)인가?"},{"title":"Redis 특유: eviction과 단편화","detail":"used_memory가 maxmemory에 도달했다면 maxmemory-policy를 확인한다(noeviction이면 쓰기가 거부됨). mem_fragmentation_ratio가 높으면 MEMORY PURGE / activedefrag를 사용한다. --bigkeys로 큰 키를 찾는다.","engines":{"redis":"CONFIG GET maxmemory-policy\nMEMORY DOCTOR\nredis-cli --bigkeys"}},{"title":"즉시 완화한다","detail":"메모리를 잡아먹는 폭주 쿼리/세션을 종료하고, work_mem 같은 세션 메모리를 일시적으로 낮추며, 불필요한 연결을 끊는다. 시스템이 swap 중이라면 swap에서 벗어나는 것이 최우선이다."},{"title":"크기를 올바르게 산정한다","detail":"Tools > Memory/Buffer Pool 계산기로 RAM을 올바르게 배분한다. 일반 규칙: 전용 서버에서 buffer pool/SGA는 RAM의 ~70-80%. 세션 메모리 총합에 동시성을 곱한 값이 RAM을 넘지 않게 한다. 필요하면 RAM을 추가한다."}],"related":["cache-hit-ratio","show-config-param","reclaim-space"]},{"id":"log-growth","title":"트랜잭션 로그 / undo 비대화","icon":"📈","symptom":"트랜잭션 로그 파일이나 undo/redo 영역이 빠르게 커진다. 'log full', 'ORA-01555 snapshot too old', 'unable to extend undo'.","steps":[{"title":"로그를 키우는 열린 트랜잭션을 찾는다","detail":"가장 오래된 활성 트랜잭션이 끝날 때까지 log/undo를 정리할 수 없다. 하나의 장시간 열린 트랜잭션이 로그 전체를 붙잡고 있을 수 있다.","engines":{"oracle":"SELECT s.sid, s.username, t.start_time, t.used_ublk FROM v$transaction t JOIN v$session s ON t.ses_addr=s.saddr ORDER BY t.start_time;","postgres":"SELECT pid, state, xact_start, now()-xact_start age, query FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start;","mysql":"SELECT trx_id, trx_started, trx_mysql_thread_id FROM information_schema.innodb_trx ORDER BY trx_started;","mssql":"DBCC OPENTRAN;"}},{"title":"엔진별 근본 원인을 이해한다","detail":"MSSQL: FULL 복구 모델에서 로그 백업을 하지 않으면 로그가 무한히 커진다. Oracle: ORA-01555 = 부족한 undo retention/크기 + 긴 쿼리. PG: replication slot이 진행하지 않으면 WAL이 쌓이고, 긴 트랜잭션은 VACUUM을 막는다."},{"title":"MSSQL 로그에 대한 올바른 조치","detail":"로그 백업(BACKUP LOG)을 수행한다 — 이렇게 하면 로그를 재사용할 수 있다. 복구 모델이 정말 SIMPLE이어야 한다면 변경한다. 그 후 필요하면(필요할 때만) 로그를 shrink한다.","engines":{"mssql":"BACKUP LOG [DB] TO DISK='C:\\backup\\DB_log.trn';\n-- Then, if needed: DBCC SHRINKFILE (N'DB_log', 512);"}},{"title":"PG WAL / replication slot 확인","detail":"진행하지 않는(inactive) replication slot은 WAL 누적을 일으킨다. 삭제하기 전에 slot의 소유자(slot_type, database, plugin)를 확인한다: 일시적으로 연결이 끊긴 standby이거나 논리 subscriber/CDC(예: Debezium)일 수 있다. slot을 삭제하면 연결된 standby는 재구축해야 하고 subscriber/CDC 측은 재동기화해야 한다. 정말 사용하지 않을 때만 삭제한다. 지속적인 보호를 위해 max_slot_wal_keep_size(PG13+)로 상한을 설정한다.","engines":{"postgres":"SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) retained FROM pg_replication_slots ORDER BY 3 DESC;\n-- If unneeded: SELECT pg_drop_replication_slot('slot_name');"}},{"title":"undo/retention을 조정하고 재발을 방지한다","detail":"Oracle: UNDO 테이블스페이스를 늘리고, 긴 쿼리에 맞춰 undo_retention을 설정한다(또는 RETENTION GUARANTEE). 애플리케이션에서 장시간 열린 트랜잭션을 방지한다. 로그 백업 작업이 실행되는지 모니터링한다."}],"related":["open-transactions","reclaim-space","last-backup-status"]},{"id":"deadlocks","title":"데드락 분석","icon":"♻️","symptom":"'deadlock detected', 'ORA-00060', 'chosen as deadlock victim' — 트랜잭션이 서로를 잠그고 하나가 중단된다.","steps":[{"title":"데드락은 정상이지만 빈번해서는 안 된다","detail":"DB는 데드락을 감지하고 victim을 선택해 해결한다 — 설계된 동작이다. 목표는 빈도를 줄이는 것이다. 애플리케이션은 victim 트랜잭션을 재시도해야 한다. 먼저 deadlock graph를 확보한다."},{"title":"deadlock graph/상세 정보를 확보한다","detail":"어떤 두(또는 그 이상의) 세션이 어떤 리소스와 어떤 락 모드로 충돌했는가?","engines":{"oracle":"-- Check the trace file referenced in the alert log (deadlock graph). Also:\nSELECT * FROM dba_hist_active_sess_history WHERE event LIKE 'enq: TX%' ORDER BY sample_time DESC;","postgres":"-- log_lock_waits=on; deadlock details are in postgresql.log. Check the deadlock_timeout setting.","mysql":"SHOW ENGINE INNODB STATUS\\G  -- LATEST DETECTED DEADLOCK section","mssql":"-- Deadlock XML in the system_health Extended Events session (ring_buffer, recent events):\nSELECT x.ev.value('@timestamp','datetime2') AS event_time, x.ev.query('.') AS deadlock_xml\nFROM (SELECT CAST(t.target_data AS XML) td FROM sys.dm_xe_session_targets t\n      JOIN sys.dm_xe_sessions s ON s.address = t.event_session_address\n      WHERE s.name = N'system_health' AND t.target_name = N'ring_buffer') r\nCROSS APPLY r.td.nodes('//RingBufferTarget/event[@name=\"xml_deadlock_report\"]') AS x(ev);\n-- For longer history, the event_file target:\n-- SELECT CAST(event_data AS XML) FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL) WHERE object_name = N'xml_deadlock_report';"}},{"title":"충돌 패턴을 해소한다","detail":"가장 흔한 원인: 두 트랜잭션이 리소스에 반대 순서로 접근하는 것. 모든 코드 경로에서 같은 순서로 리소스에 접근한다. 두 번째로 흔한 원인: 누락된 인덱스로 인한 넓은 범위 락 — 필요한 인덱스를 추가한다."},{"title":"락 지속 시간과 범위를 줄인다","detail":"트랜잭션을 짧게 유지한다(트랜잭션 안에 사용자 상호작용을 포함하지 않는다). 적절한 격리 수준을 선택한다. 필요하면 행 락을 일찍 획득한다(결정적 순서의 SELECT ... FOR UPDATE)."},{"title":"애플리케이션 재시도는 필수다","detail":"데드락을 완전히 없앨 수는 없다. victim 트랜잭션에 대해 자동 재시도(exponential backoff)를 구현한다. 이렇게 하면 사용자가 오류를 보지 않는다."}],"related":["blocking-sessions","open-transactions","list-indexes"]},{"id":"db-down","title":"데이터베이스가 열리지 않음 / 접속 불가","icon":"🚨","symptom":"서비스에 연결할 수 없고, 'connection refused', 인스턴스가 시작되지 않으며, listener가 응답하지 않는다.","steps":[{"title":"어디서 끊기는가: 네트워크, 서비스, 인스턴스","detail":"서버에 도달할 수 있는가(ping/ssh)? DB 프로세스/서비스가 실행 중인가(ps/systemctl/services.msc)? 포트가 listening 상태인가(ss -ltnp / telnet host port)? 이로써 문제를 네트워크 / 서비스 / 데이터베이스 인스턴스 중 한 계층으로 좁힌다."},{"title":"서비스와 listener를 확인한다","detail":"DB 프로세스가 올라와 있지 않으면 시작을 시도한다. 올라와 있는데 연결이 실패하면 listener/포트와 인증 계층을 확인한다.","engines":{"oracle":"-- OS: lsnrctl status  (listener)\n-- SQL*Plus: STARTUP;  (if the instance is down)\nSELECT status FROM v$instance;","postgres":"-- OS: systemctl status postgresql ; pg_isready -h host\n-- Logs: the last lines tell why it did not start","mysql":"-- OS: systemctl status mysqld ; mysqladmin ping","mssql":"-- Services / SQL Server Configuration Manager; SQL Server Error Log","redis":"redis-cli ping  # expect PONG"}},{"title":"로그를 읽는다 — 답은 거기에 있다","detail":"DB가 열리지 않으면 이유가 로그에 기록된다: alert log(Oracle), postgresql.log / startup, mysqld error log, SQL Server ERRORLOG, redis log. 디스크 가득 참, 손상된 파일, 권한, 포트 충돌, 누락 파일 — 모두 여기에 나타난다."},{"title":"흔한 원인을 처리한다: 디스크, 권한, 포트, 손상","detail":"디스크가 100% 찼는가(가장 흔한 원인)? 데이터 디렉터리 권한이 깨졌는가? 다른 프로세스가 포트를 점유했는가? (장애 후) 복구가 필요한가? 컨트롤 파일/redo log가 손상되었는가? 로그 메시지에 따라 각각을 처리한다."},{"title":"복구하고 장애 기록을 남긴다","detail":"crash recovery는 대개 자동으로 완료되므로 기다린다. 손상이 있으면 백업/PITR로 복구한다(데이터 손상 플레이북 참조). 이후 근본 원인 분석(RCA)을 작성한다: 무슨 일이 왜 일어났고 재발을 어떻게 막을지."}],"related":["version-uptime","connect-cli","last-backup-status"]},{"id":"corruption","title":"데이터 손상","icon":"🧯","symptom":"읽기 중 'ORA-01578', 'page corruption', 'checksum mismatch', 'MISCONF', I/O 일관성 오류.","steps":[{"title":"침착하게 확산을 막는다","detail":"당황한 개입은 상황을 악화시킨다. 먼저 범위를 파악하고 가능하면 영향받은 객체에 대한 쓰기를 중지한다. 하드웨어(디스크/컨트롤러) 관련이라면 OS/하드웨어 로그도 확인한다 — DB를 고쳐도 디스크는 고쳐지지 않는다."},{"title":"범위를 파악한다: 얼마나 손상되었는가","detail":"단일 블록/페이지/객체인가, 광범위한가? 무결성 검사를 실행한다.","engines":{"oracle":"RMAN> VALIDATE DATABASE;  -- or: SELECT * FROM v$database_block_corruption;","postgres":"-- With data_checksums enabled it shows up in the logs; check indexes/heap with pg_amcheck:\n-- pg_amcheck --heapallindexed -d mydb","mysql":"CHECK TABLE customer;  -- for InnoDB, innodb_force_recovery is the last resort","mssql":"DBCC CHECKDB ('MyDB') WITH NO_INFOMSGS, ALL_ERRORMSGS;","redis":"redis-check-rdb dump.rdb\n# AOF (Redis 7+ multi-part: under appendonlydir/, pass the manifest file):\nredis-check-aof appendonlydir/appendonly.aof.manifest\n# --fix TRUNCATES everything after the corrupted point (writes in that part are lost); back up the directory first:\n# redis-check-aof --fix appendonlydir/appendonly.aof.manifest"}},{"title":"우선순위: 백업에서 복구","detail":"손상 시 가장 안전한 방법은 정상 백업에서 복원하는 것이다. 블록/페이지 수준 복구가 지원되면 손상된 부분만 복구한다(Oracle RMAN BLOCKRECOVER, MSSQL page restore). 복구 도구(DBCC REPAIR_ALLOW_DATA_LOSS, innodb_force_recovery)는 데이터 손실을 일으킨다 — 최후의 수단이다."},{"title":"무결성을 검증한다","detail":"복구 후 검사(DBCC CHECKDB / RMAN VALIDATE / CHECK TABLE)를 다시 실행하여 깨끗하게 나오는지 확인한다. 애플리케이션 측에서도 데이터 일관성(행 수, 핵심 합계)을 검증한다."},{"title":"근본 원인: 대개 하드웨어","detail":"손상은 대개 디스크/메모리/컨트롤러 장애에서 비롯된다. 하드웨어를 테스트/교체하고, checksum을 활성화하며(data_checksums, PAGE_VERIFY CHECKSUM), 백업 + 검증 절차를 강화한다. 정기적인 DBCC CHECKDB / RMAN VALIDATE를 스케줄링한다."}],"related":["last-backup-status","restore-dump","db-down"]},{"id":"long-running-query","title":"장시간 실행 쿼리 / 작업","icon":"⏱️","symptom":"하나의 쿼리/작업이 몇 시간째 끝나지 않고 실행 중이며, 리소스를 소비하거나 다른 작업에 영향을 준다.","steps":[{"title":"쿼리와 실행 시간을 파악한다","detail":"어떤 쿼리가, 얼마나 오래, 어떤 사용자/애플리케이션에서 실행 중인가?","engines":{"oracle":"SELECT sid, serial#, sql_id, last_call_et/60 running_min, username FROM v$session WHERE status='ACTIVE' AND type='USER' ORDER BY last_call_et DESC;","postgres":"SELECT pid, now()-query_start dur, usename, query FROM pg_stat_activity WHERE state='active' ORDER BY query_start;","mysql":"SELECT id, user, time, state, info FROM information_schema.processlist WHERE command='Query' ORDER BY time DESC;","mssql":"SELECT session_id, total_elapsed_time/1000 sec, status, command FROM sys.dm_exec_requests WHERE session_id IN (SELECT session_id FROM sys.dm_exec_sessions WHERE is_user_process = 1) ORDER BY total_elapsed_time DESC;","mongodb":"db.currentOp({ active: true, secs_running: { $gt: 60 } })"}},{"title":"진행 중인가 멈췄는가","detail":"정말 실행 중인가, 락을 기다리는가? 진행 지표(Oracle v$session_longops, PG pg_stat_progress_* 뷰)를 확인한다. 대기 중이라면 블로킹 플레이북으로 전환한다.","engines":{"oracle":"SELECT sid, opname, sofar, totalwork, ROUND(sofar/NULLIF(totalwork,0)*100,1) pct, time_remaining FROM v$session_longops WHERE sofar<totalwork;","postgres":"SELECT * FROM pg_stat_progress_create_index;  -- or pg_stat_progress_vacuum / _cluster / _analyze"}},{"title":"결정: 대기 또는 취소","detail":"진행 중이고 곧 끝난다면 기다린다(취소 시 롤백 비용으로 더 오래 걸릴 수 있음). 멈췄거나 잘못 실행 중이거나 운영에 영향을 준다면 종료한다. 취소 전에 롤백 시간을 고려한다."},{"title":"필요하면 종료한다","detail":"쿼리만 취소하는 것(세션 유지)을 우선한다. 효과가 없으면 세션을 종료한다.","engines":{"oracle":"ALTER SYSTEM CANCEL SQL 'sid,serial#';  -- 18c+; otherwise KILL SESSION","postgres":"SELECT pg_cancel_backend(pid);  -- otherwise pg_terminate_backend(pid)","mysql":"KILL QUERY <id>;","mssql":"KILL <session_id>;","mongodb":"db.killOp(<opid>)"}},{"title":"영구적으로 해결한다","detail":"쿼리를 최적화하고(인덱스, 계획, 페이지네이션/배치 처리), 무거운 작업은 비피크 시간대로 옮기며, 필요하면 나누어 실행한다. 반복되는 보고서에는 materialized view / read replica를 고려한다."}],"related":["active-sessions","kill-session","explain-plan","top-slow-queries"]},{"id":"xid-wraparound","title":"Postgres XID wraparound / autovacuum 위기","icon":"🧬","symptom":"'database is not accepting commands to avoid wraparound data loss'. autovacuum이 따라가지 못하고 datfrozenxid가 노화되고 있다.","steps":[{"title":"긴급성을 이해한다","detail":"PostgreSQL 트랜잭션 ID는 32비트이며, 소진에 가까워지면 서버는 데이터 손실을 막기 위해 쓰기를 중지한다. 이는 시간에 민감한 심각한 상황이다. 좋은 소식: 해결 가능하며, 필요한 것은 당황이 아니라 규율이다."},{"title":"얼마나 근접했는지 측정한다","detail":"각 데이터베이스의 가장 오래된 미동결 XID의 age를 추출한다. autovacuum_freeze_max_age(기본값 200M)에 대한 근접도가 위험도를 보여 준다.","engines":{"postgres":"SELECT datname, age(datfrozenxid) AS xid_age\nFROM pg_database ORDER BY xid_age DESC;"}},{"title":"가장 오래된 테이블을 찾는다","detail":"문제는 대개 몇몇 크거나 쓰기가 잦은 테이블에서 비롯된다. 가장 오래된 relfrozenxid 값에 집중한다.","engines":{"postgres":"SELECT relname, age(relfrozenxid) AS xid_age\nFROM pg_class WHERE relkind IN ('r','m','t')\nORDER BY xid_age DESC LIMIT 20;"}},{"title":"VACUUM을 막는 요소를 제거한다","detail":"장시간 열린 트랜잭션, 방치된 replication slot, prepared transaction이 정리(freeze)를 막는다. 이를 찾아 해결한다 — 그렇지 않으면 VACUUM을 아무리 오래 실행해도 진행되지 않는다.","engines":{"postgres":"SELECT pid, now()-xact_start AS age, state FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY age DESC;\nSELECT slot_name, active FROM pg_replication_slots WHERE NOT active;\nSELECT * FROM pg_prepared_xacts;"}},{"title":"공격적인 FREEZE를 실행한다","detail":"차단 요소가 사라지면 가장 오래된 테이블부터 freeze를 강제한다. 서버가 새 트랜잭션 ID 할당을 중지했더라도 single-user 모드(postgres --single)로 전환하지 않는다: 현재 PostgreSQL 문서는 이것이 더 이상 필요 없으며 피해야 한다고 명시한다. 일반 모드에서 superuser로 연결해 VACUUM을 실행하면 충분하다. PG14+에서는 wraparound가 가까워지면 VACUUM의 failsafe 메커니즘이 작동한다.","engines":{"postgres":"VACUUM (FREEZE, VERBOSE) oldest_table;\n-- Entire database: vacuumdb --all --freeze --jobs=4"}},{"title":"영구 조치: autovacuum을 강화하고 모니터링한다","detail":"autovacuum_vacuum_cost_limit을 높이고 autovacuum_naptime을 낮추며, 쓰기가 많은 테이블에 테이블별 storage parameter를 설정한다. age(datfrozenxid)에 알림을 설정하여 이 위기를 다시 겪지 않도록 한다."}],"related":["open-transactions","reclaim-space"]},{"id":"tempdb-contention","title":"tempdb 경합(SQL Server)","icon":"🌀","symptom":"PAGELATCH_UP/EX 대기, tempdb 할당 페이지(PFS/GAM/SGAM) 경합, 전반적인 느려짐.","steps":[{"title":"경합을 확인한다","detail":"tempdb 시스템 페이지(예: 2:1:1)에 대한 PAGELATCH 대기는 할당 경합을 나타낸다.","engines":{"mssql":"SELECT session_id, wait_type, wait_duration_ms, resource_description\nFROM sys.dm_os_waiting_tasks\nWHERE wait_type LIKE 'PAGELATCH%' AND resource_description LIKE '2:%'\nORDER BY wait_duration_ms DESC;"}},{"title":"tempdb 사용량을 분석한다","detail":"누가 tempdb를 얼마나 쓰는가? 내부 객체(sort/hash spill), version store, 사용자 객체 중 무엇인가?","engines":{"mssql":"SELECT * FROM sys.dm_db_file_space_usage;\nSELECT TOP 10 session_id, user_objects_alloc_page_count, internal_objects_alloc_page_count\nFROM sys.dm_db_task_space_usage ORDER BY internal_objects_alloc_page_count DESC;"}},{"title":"파일 구성을 수정한다","detail":"tempdb에 CPU 코어 수(최대 8개)만큼 크기가 같은 여러 데이터 파일을 사용한다. SQL 2016+는 설치 시 이를 권장하며, 이전 버전에서는 trace flag 1117/1118을 사용한다. 파일은 빠른 디스크(로컬 NVMe)에 둔다."},{"title":"폭주 쿼리를 찾는다","detail":"큰 sort/hash spill이 있는 쿼리가 tempdb를 폭증시킨다. memory grant가 부족하거나 계획이 나쁜 쿼리를 대상으로 인덱스/계획으로 spill을 줄인다.","engines":{"mssql":"SELECT TOP 10 t.text, mg.requested_memory_kb, mg.granted_memory_kb\nFROM sys.dm_exec_query_memory_grants mg CROSS APPLY sys.dm_exec_sql_text(mg.sql_handle) t\nORDER BY mg.requested_memory_kb DESC;"}},{"title":"다른 데이터베이스의 대응 사례","detail":"같은 '임시 공간 경합'은 Oracle에서는 TEMP 테이블스페이스(v$tempseg_usage / v$sort_usage), PostgreSQL에서는 temp_files와 work_mem 초과다. 근본 해결은 항상 같다: spill하는 쿼리를 수정하고 임시 공간을 빠르게 한다."}],"related":["free-space","top-slow-queries","reclaim-space"]},{"id":"cert-expiry","title":"TLS / 인증서 만료","icon":"🔐","symptom":"'certificate has expired', 'SSL handshake failed', 클라이언트가 갑자기 연결할 수 없다(코드 변경 없음).","steps":[{"title":"어디서 끊기는가: 서버, 클라이언트, CA?","detail":"만료된 것이 서버 인증서인가, 클라이언트 인증서인가, 체인의 CA인가? 증상은 대개 '어제는 됐는데 오늘은 안 되고, 코드는 바뀌지 않았다'이다."},{"title":"만료일을 확인한다","detail":"서버에서 인증서 만료일을 직접 읽는다(데이터베이스와 무관한 빠른 방법).","engines":{"postgres":"openssl s_client -connect host:5432 -starttls postgres </dev/null 2>/dev/null | openssl x509 -noout -dates","mongodb":"openssl s_client -connect host:27017 </dev/null 2>/dev/null | openssl x509 -noout -enddate","redis":"openssl s_client -connect host:6379 </dev/null 2>/dev/null | openssl x509 -noout -enddate"}},{"title":"새 인증서를 설치하고 로드한다","detail":"인증서를 교체하고, 대부분의 경우 서비스를 재시작하지 않고 reload한다.","engines":{"postgres":"-- Renew ssl_cert_file/ssl_key_file, then:\nSELECT pg_reload_conf();","mysql":"ALTER INSTANCE RELOAD TLS;","mssql":"-- SQL Server Configuration Manager > Certificate; a service restart is required","mongodb":"// Renew the net.tls.certificateKeyFile file (at the same path), then load it without a restart (5.0+):\ndb.adminCommand({rotateCertificates: 1})","redis":"CONFIG SET tls-cert-file /path/new.crt\nCONFIG SET tls-key-file /path/new.key","elasticsearch":"# ES watches the certificate/key files (PEM, PKCS#12) defined under xpack.security.*.ssl and\n# reloads them automatically when the file at the SAME path changes (no restart needed).\n# If the path, file name or keystore password changes, a rolling restart is required.\n# Note: POST _nodes/reload_secure_settings is only for reloadable keystore settings, not for TLS certificates.\n# Verify: check the new expiry date with openssl s_client and look in the ES logs for\n# an entry showing the certificate was reloaded."}},{"title":"검증한다","detail":"위의 openssl 명령으로 새 인증서로 TLS handshake가 성공하고 만료일이 연장되었는지 확인한다. 클라이언트로 end-to-end 연결한다."},{"title":"영구 조치: 다시는 없도록","detail":"인증서 만료 30일 전에 알림을 설정하고, 자동 갱신(ACME/Let's Encrypt, HashiCorp Vault, cert-manager)을 사용한다. 갱신 후 reload를 자동화한다."}],"related":["connect-cli"]},{"id":"upgrade-rollback","title":"버전 업그레이드 / 패치 롤백","icon":"⏪","symptom":"업그레이드/패치 후 문제: 성능 회귀, 애플리케이션 비호환, 데이터베이스가 열리지 않음.","steps":[{"title":"먼저 멈춘다: 대체 계획이 있는가?","detail":"운영 업그레이드 전에 전체 백업 + (가능하면) 스토리지 스냅샷을 확보하지 않았다면 가장 안전한 방법은 fix forward다. 대부분의 데이터베이스에서 롤백은 백업에서 복원하는 것을 의미한다. 이 단계는 원래 업그레이드 전에 계획되었어야 한다."},{"title":"문제를 분류한다","detail":"옵티마이저 계획 회귀(쿼리 느려짐)인가, 애플리케이션/드라이버 비호환인가, 열리지 않음/손상인가? 이에 따라 해결 경로가 크게 달라진다."},{"title":"데이터베이스별 롤백 경로","detail":"Oracle: datapatch로 패치 롤백, 또는 Flashback Database(guaranteed restore point를 만든 경우). PostgreSQL: pg_upgrade 후 롤백 = 이전 바이너리 + 이전 데이터 디렉터리 백업. MySQL: in-place 다운그레이드는 일반적으로 지원되지 않는다 — 백업에서 복원한다. SQL Server: 다운그레이드가 없다 — 업그레이드 전 백업에서 복원한다. 모든 경우에 스냅샷/백업이 보험이다."},{"title":"계획 회귀라면: 롤백 없이 해결한다","detail":"대개 버전 전체를 롤백할 필요는 없으며, 계획만 바뀐 것이다.","engines":{"oracle":"ALTER SYSTEM SET optimizer_features_enable='19.1.0';  -- pin to the old behavior\n-- or lock in the good plan with a SQL Plan Baseline","postgres":"ANALYZE;  -- fresh statistics first\n-- if needed, temporarily: steer the plan with settings such as SET enable_nestloop=off;"}},{"title":"영구 조치: 다음번에는","detail":"먼저 실제 워크로드로 staging에서 업그레이드를 테스트하고, canary/rolling 업그레이드를 적용하며, guaranteed restore point / 스냅샷을 확보하고 대체 절차를 문서화한다."}],"related":["last-backup-status","restore-dump","version-uptime"]},{"id":"es-red-cluster","title":"Elasticsearch red/yellow 클러스터 및 unassigned shard","icon":"🟥","symptom":"_cluster/health가 'red' 또는 'yellow'. unassigned shard 발생, 읽기/쓰기가 제한되거나 데이터에 접근할 수 없다.","steps":[{"title":"색상과 원인","detail":"yellow = replica 샤드가 할당되지 않음(데이터는 있으나 내구성 저하). red = 최소 하나의 PRIMARY shard가 없음(해당 데이터 접근 불가). 먼저 어느 쪽이고 왜인지 파악한다.","engines":{"elasticsearch":"GET _cluster/health?level=indices\nGET _cluster/allocation/explain"}},{"title":"unassigned shard와 이유를 나열한다","detail":"unassigned.reason 컬럼이 근본 원인을 알려준다(NODE_LEFT, ALLOCATION_FAILED, DISK 등).","engines":{"elasticsearch":"GET _cat/shards?v&h=index,shard,prirep,state,unassigned.reason&s=state"}},{"title":"흔한 원인을 처리한다","detail":"1) Disk watermark → low를 넘으면 노드가 새 샤드를 받지 않고, flood_stage에서는 인덱스가 읽기 전용이 된다. 2) 노드가 다운되었거나 이탈했다. 3) allocation이 비활성화된 채로 남아 있다. 4) replica 수 > 적격 노드 수."},{"title":"해결한다","detail":"해당 원인에 따라 차단을 해제한다.","engines":{"elasticsearch":"# If disk space was freed, remove the read-only block (7.4+ also lifts it automatically once below the high watermark):\nPUT _all/_settings {\"index.blocks.read_only_allow_delete\": null}\n# If allocation was left restricted, return it to the default (all):\nPUT _cluster/settings {\"persistent\":{\"cluster.routing.allocation.enable\":null}}\n# Retry failed allocations:\nPOST _cluster/reroute?retry_failed=true"}},{"title":"여전히 red이고 primary가 정말 손실되었다면","detail":"영향받은 인덱스를 스냅샷에서 복원하거나 원본 데이터에서 reindex한다(_reindex). 영구 조치: disk watermark 알림, 적절한 replica 수(≥1), 노드 상태 모니터링."}],"related":["replication-status","list-replicas","free-space"]},{"id":"time-skew","title":"Clock skew / NTP 문제","icon":"⏱️","symptom":"클러스터 노드 간 시간 차이. 복제/선출 불일치, 'certificate not yet valid', 로그의 시간 점프.","steps":[{"title":"증상을 알아본다","detail":"clock skew는 은밀하다: Mongo/Redis Sentinel 선출 반복(flapping), TLS 'not yet valid/expired' 오류, Kerberos 오류, 로그에서 타임스탬프가 거꾸로 감. 분산 시스템에서 대부분의 '설명할 수 없는' 이상 현상의 근원이다."},{"title":"차이를 측정한다","detail":"각 노드에서 시간 소스와 offset을 비교한다: `timedatectl`, `chronyc tracking`(또는 `ntpq -p`), 노드 간 `date -u`. 몇 ms/s 차이가 나는가?"},{"title":"NTP/chrony를 수정한다","detail":"chronyd/ntpd가 실행 중인가, 시간 소스에 도달할 수 있는가, 동기화되었는가? 서비스를 시작하고 소스를 수정한다. offset이 크면 신중하게 step한다(갑작스러운 점프는 일부 DB에 영향을 줄 수 있음)."},{"title":"영향받은 구성 요소를 검증한다","detail":"시간을 수정한 후 확인한다: MongoDB replica set 선출이 안정적인가, Redis Sentinel/Cluster가 정상인가, TLS handshake가 성공하는가, 복제 지연 지표가 회복되었는가?"},{"title":"영구 조치","detail":"모든 노드에서 NTP를 필수로 하고 offset을 모니터링한다(알림: >100ms). 가상 머신에서는 host-guest 시간 동기화 설정을 정리한다. 가능하면 단일하고 신뢰할 수 있는 시간 소스 계층을 사용한다."}],"related":["replication-status"]}]},"playbooks/hadr.json":{"schemaVersion":1,"playbooks":[{"id":"hadr-oracle-dataguard","title":"Oracle Data Guard(physical standby) 구성","icon":"🛡️","symptom":"목표: physical standby(Data Guard)로 Oracle의 재해 복구와 고가용성을 구성하고, primary 장애 시 standby로 전환할 수 있게 한다.","steps":[{"title":"primary를 FORCE LOGGING 및 ARCHIVELOG 모드로 설정한다","detail":"standby가 primary와 일관성을 유지하려면 모든 변경이 redo에 기록되어야 한다. FORCE LOGGING은 NOLOGGING 작업도 로깅한다. ARCHIVELOG 모드를 위해 데이터베이스를 MOUNT 상태로 올린다.","engines":{"oracle":"ALTER DATABASE FORCE LOGGING;\n-- For ARCHIVELOG:\nSHUTDOWN IMMEDIATE;\nSTARTUP MOUNT;\nALTER DATABASE ARCHIVELOG;\nALTER DATABASE OPEN;"}},{"title":"standby redo log 파일을 추가한다","detail":"standby redo log(SRL)는 online redo log보다 그룹이 하나 더 많고 크기가 같아야 한다. primary와 standby 양쪽에 생성하며, real-time apply에 필요하다. 예시에서 online redo가 200M이면 SRL도 200M이다.","engines":{"oracle":"ALTER DATABASE ADD STANDBY LOGFILE ('/u01/oradata/srl01.log') SIZE 200M;\nALTER DATABASE ADD STANDBY LOGFILE ('/u01/oradata/srl02.log') SIZE 200M;\nALTER DATABASE ADD STANDBY LOGFILE ('/u01/oradata/srl03.log') SIZE 200M;\nALTER DATABASE ADD STANDBY LOGFILE ('/u01/oradata/srl04.log') SIZE 200M;"}},{"title":"log_archive_dest 및 관련 파라미터를 설정한다","detail":"primary에서 redo를 standby로 전송하도록 dest_2를 정의한다. DB_UNIQUE_NAME, LOG_ARCHIVE_CONFIG(양쪽의 db_unique_name), FAL_SERVER, STANDBY_FILE_MANAGEMENT=AUTO를 설정한다. 이렇게 하면 standby에서 파일이 자동으로 생성된다.","engines":{"oracle":"ALTER SYSTEM SET LOG_ARCHIVE_CONFIG='DG_CONFIG=(orcl_prim,orcl_stby)';\nALTER SYSTEM SET LOG_ARCHIVE_DEST_2='SERVICE=orcl_stby ASYNC VALID_FOR=(ONLINE_LOGFILES,PRIMARY_ROLE) DB_UNIQUE_NAME=orcl_stby';\nALTER SYSTEM SET FAL_SERVER='orcl_stby';  -- for role changes; on the standby FAL_SERVER='orcl_prim'\nALTER SYSTEM SET STANDBY_FILE_MANAGEMENT=AUTO;"}},{"title":"RMAN DUPLICATE FOR STANDBY로 standby를 생성한다","detail":"standby 서버에 password file과 static listener/tnsnames를 구성한 후 RMAN이 primary와 standby(AUXILIARY)에 연결한다. FROM ACTIVE DATABASE는 백업 없이 네트워크로 복사하며, DORECOVER는 redo를 적용한다.","engines":{"oracle":"rman TARGET sys/password@orcl_prim AUXILIARY sys/password@orcl_stby\nRUN {\n  DUPLICATE TARGET DATABASE FOR STANDBY FROM ACTIVE DATABASE\n  DORECOVER\n  NOFILENAMECHECK;\n}"}},{"title":"redo apply(MRP)를 시작하고 확인한다","detail":"standby가 MOUNT 상태일 때 managed recovery를 시작한다. real-time apply에는 USING CURRENT LOGFILE을 사용한다. v$managed_standby로 apply 프로세스(MRP0)가 실행 중인지 확인한다.","engines":{"oracle":"ALTER DATABASE RECOVER MANAGED STANDBY DATABASE USING CURRENT LOGFILE DISCONNECT FROM SESSION;\nSELECT process, status, sequence# FROM v$managed_standby WHERE process LIKE 'MRP%';"}},{"title":"Data Guard Broker를 활성화한다","detail":"broker는 구성을 중앙화하고 failover/switchover를 단일 명령으로 줄인다. 두 인스턴스 모두에 DG_BROKER_START=TRUE를 설정한 후 dgmgrl로 구성을 생성하고 활성화한다. 12c+에서는 broker가 redo 전송을 관리한다: LOG_ARCHIVE_DEST_2를 수동으로 설정했다면 ADD DATABASE가 ORA-16698로 실패하므로 먼저 ALTER SYSTEM SET LOG_ARCHIVE_DEST_2=''로 지운다.","engines":{"oracle":"ALTER SYSTEM SET DG_BROKER_START=TRUE;\n-- In dgmgrl:\nCREATE CONFIGURATION dgconf AS PRIMARY DATABASE IS orcl_prim CONNECT IDENTIFIER IS orcl_prim;\nADD DATABASE orcl_stby AS CONNECT IDENTIFIER IS orcl_stby;\nENABLE CONFIGURATION;"}},{"title":"동기화 차이를 모니터링하고 테스트를 완료한다","detail":"primary와 standby 간 sequence 차이를 지속적으로 모니터링해야 한다. 차이가 커지면 네트워크/적용 병목을 의미한다. dgmgrl SHOW CONFIGURATION이 'SUCCESS'를 보여야 한다. 선택적으로 switchover로 역할 전환을 테스트한다.","engines":{"oracle":"-- dgmgrl (SHOW DATABASE shows the Transport Lag / Apply Lag values):\nSHOW CONFIGURATION;\nSHOW DATABASE orcl_stby;\n-- SQL (standby): SELECT name, value FROM v$dataguard_stats WHERE name LIKE '%lag';"}}],"related":["replication-status","promote-failover","last-backup-status"]},{"id":"hadr-mssql-alwayson","title":"SQL Server Always On Availability Group 구성","icon":"🔁","symptom":"목표: Always On Availability Group(AG)으로 SQL Server의 자동 페일오버와 읽기 가능 secondary replica를 구성한다.","steps":[{"title":"Windows Server Failover Cluster(WSFC)를 구성한다","detail":"Always On은 기반 WSFC에 의존한다. 모든 노드에 Failover Clustering 기능을 설치하고 네트워크와 witness를 구성한다. PowerShell로 클러스터를 생성한다.","engines":{"mssql":"-- PowerShell (the Failover-Clustering feature must be installed on every node):\nNew-Cluster -Name sqlclu -Node node1,node2 -StaticAddress 10.0.0.50\n-- Witness:\nSet-ClusterQuorum -NodeAndFileShareMajority \\\\fs01\\quorum"}},{"title":"각 인스턴스에서 Always On을 활성화한다","detail":"SQL Server Configuration Manager 또는 PowerShell로 Always On Availability Groups 기능을 활성화한다. 그 후 SQL Server 서비스를 재시작한다. 이로써 클러스터 멤버십이 인스턴스에 연결된다.","engines":{"mssql":"-- PowerShell:\nEnable-SqlAlwaysOn -ServerInstance node1\\SQL01 -Force\nEnable-SqlAlwaysOn -ServerInstance node2\\SQL01 -Force"}},{"title":"데이터베이스를 백업하고 secondary에서 WITH NORECOVERY로 복원한다","detail":"AG에 추가할 데이터베이스는 FULL 복구 모델이어야 한다. primary에서 전체 백업과 로그 백업을 수행하고, secondary에서 WITH NORECOVERY로 복원한다(redo를 계속 받을 수 있도록). 이것이 manual seeding이며, 대안은 automatic seeding이다.","engines":{"mssql":"-- On the primary:\nBACKUP DATABASE app TO DISK='\\\\fs01\\bak\\app.bak';\nBACKUP LOG app TO DISK='\\\\fs01\\bak\\app.trn';\n-- On the secondary:\nRESTORE DATABASE app FROM DISK='\\\\fs01\\bak\\app.bak' WITH NORECOVERY;\nRESTORE LOG app FROM DISK='\\\\fs01\\bak\\app.trn' WITH NORECOVERY;"}},{"title":"endpoint를 생성하고 Availability Group을 정의한다","detail":"노드 간 redo 트래픽을 위해 포트 5022에 HADR endpoint를 생성한다. 동기 모드(SYNCHRONOUS_COMMIT)는 데이터 손실 없는 자동 페일오버를 제공하고, 비동기(ASYNCHRONOUS_COMMIT)는 지연이 있지만 원격 DR에서 성능이 좋다.","engines":{"mssql":"CREATE ENDPOINT hadr_endpoint STATE=STARTED\n  AS TCP (LISTENER_PORT=5022)\n  FOR DATA_MIRRORING (ROLE=ALL, ENCRYPTION=REQUIRED ALGORITHM AES);\n\nCREATE AVAILABILITY GROUP appag\n  FOR DATABASE app\n  REPLICA ON\n    'NODE1\\SQL01' WITH (ENDPOINT_URL='TCP://node1:5022', AVAILABILITY_MODE=SYNCHRONOUS_COMMIT, FAILOVER_MODE=AUTOMATIC, SECONDARY_ROLE(ALLOW_CONNECTIONS=ALL)),\n    'NODE2\\SQL01' WITH (ENDPOINT_URL='TCP://node2:5022', AVAILABILITY_MODE=SYNCHRONOUS_COMMIT, FAILOVER_MODE=AUTOMATIC, SECONDARY_ROLE(ALLOW_CONNECTIONS=ALL));"}},{"title":"secondary를 AG에 참여시키고 데이터베이스를 조인한다","detail":"secondary 인스턴스에서 AG에 JOIN한 후 복원한 데이터베이스를 AG에 추가한다. manual seeding을 사용한 경우 이 단계가 필요하며, automatic seeding에서는 primary에서 SET(SEEDING_MODE)로 처리된다.","engines":{"mssql":"-- On the secondary:\nALTER AVAILABILITY GROUP appag JOIN;\nALTER DATABASE app SET HADR AVAILABILITY GROUP = appag;"}},{"title":"Availability Group Listener를 생성한다","detail":"listener는 애플리케이션이 연결하는 가상 이름과 IP다. 페일오버 후 연결은 새 primary로 자동 리디렉션된다. 애플리케이션 연결 문자열에 이 이름과 MultiSubnetFailover=True를 사용한다.","engines":{"mssql":"ALTER AVAILABILITY GROUP appag\n  ADD LISTENER 'appag-listener' (WITH IP ((N'10.0.0.60', N'255.255.255.0')), PORT=1433);"}},{"title":"페일오버를 테스트하고 동기화 상태를 확인한다","detail":"동기화가 'HEALTHY'이면 계획된 페일오버로 역할 전환을 테스트한다. 자동 페일오버에는 동기 모드와 FAILOVER_MODE=AUTOMATIC이 필요하다. sys.dm_hadr_availability_replica_states로 상태를 모니터링한다.","engines":{"mssql":"SELECT ag.name, ars.role_desc, ars.synchronization_health_desc\nFROM sys.dm_hadr_availability_replica_states ars\nJOIN sys.availability_groups ag ON ag.group_id=ars.group_id;\n-- Planned failover (on the node that will become the new primary):\nALTER AVAILABILITY GROUP appag FAILOVER;"}}],"related":["replication-status","promote-failover","list-replicas"]},{"id":"hadr-postgres-streaming","title":"PostgreSQL streaming replication + 자동 페일오버","icon":"🐘","symptom":"목표: streaming replication으로 PostgreSQL의 hot standby를 구성하고 자동 페일오버를 위해 Patroni/repmgr를 추가한다.","steps":[{"title":"primary에서 WAL 및 연결 파라미터를 설정한다","detail":"streaming replication을 위해 wal_level은 최소 'replica'여야 한다. max_wal_senders는 replica 수 + 백업 연결을 감당해야 하며, wal_keep_size나 replication slot으로 WAL의 조기 삭제를 막아야 한다. 변경에는 재시작이 필요하다.","engines":{"postgres":"ALTER SYSTEM SET wal_level = 'replica';\nALTER SYSTEM SET max_wal_senders = 10;\nALTER SYSTEM SET max_replication_slots = 10;\nALTER SYSTEM SET hot_standby = on;\n-- To apply the postgresql.conf change:\n-- systemctl restart postgresql"}},{"title":"복제 역할과 pg_hba 접근을 정의한다","detail":"standby가 연결할 REPLICATION 속성의 역할을 생성한다. pg_hba.conf에 standby IP에서 'replication' 데이터베이스 접근을 허용하는 줄을 추가하고 pg_reload_conf()로 reload한다.","engines":{"postgres":"CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'secret';\n-- Add to pg_hba.conf:\n-- host replication replicator 10.0.0.20/32 scram-sha-256\nSELECT pg_reload_conf();"}},{"title":"primary에 영구 replication slot을 생성한다","detail":"physical replication slot은 standby가 뒤처져도 primary가 필요한 WAL을 삭제하지 않게 한다. slot 관리는 wal_keep_size보다 안전하다. slot 이름은 standby 구성에서 사용한다.","engines":{"postgres":"SELECT pg_create_physical_replication_slot('standby1_slot');\nSELECT slot_name, active, restart_lsn FROM pg_replication_slots;"}},{"title":"standby 서버에서 pg_basebackup으로 base backup을 수행한다","detail":"standby의 데이터 디렉터리를 비운 상태에서 primary로부터 전체 사본을 가져온다. -R 플래그는 standby.signal 파일과 primary_conninfo를 자동으로 기록하며, -C는 slot도 생성할 수 있다. slot 이름은 -S로 전달한다.","engines":{"postgres":"pg_basebackup -h 10.0.0.10 -U replicator -D /var/lib/pgsql/data \\\n  -R -X stream -S standby1_slot -P -v"}},{"title":"primary_conninfo와 hot_standby를 확인하고 standby를 시작한다","detail":"-R이 기록한 postgresql.auto.conf의 primary_conninfo(host, user, slot)를 확인한다. standby.signal 파일의 존재가 standby 모드를 결정한다. hot_standby=on이면 standby가 읽기 전용 쿼리를 받는다. 서비스를 시작한다.","engines":{"postgres":"-- Expected in postgresql.auto.conf:\n-- primary_conninfo = 'host=10.0.0.10 user=replicator password=secret'\n-- primary_slot_name = 'standby1_slot'\n-- systemctl start postgresql\nSELECT pg_is_in_recovery();  -- should return true on the standby"}},{"title":"primary에서 복제 지연을 모니터링한다","detail":"primary에서 pg_stat_replication은 각 standby의 전송/재실행 LSN과 지연을 보여 준다. write/flush/replay lag는 작게 유지되어야 하며, 동기 replica를 사용하면 sync_state가 'sync'다.","engines":{"postgres":"SELECT client_addr, state, sync_state,\n       pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_lag_bytes,\n       write_lag, flush_lag, replay_lag\nFROM pg_stat_replication;"}},{"title":"자동 페일오버를 위해 Patroni 또는 repmgr를 추가한다","detail":"순수 streaming replication에는 수동 promote가 필요하다. 자동 페일오버에는 클러스터 관리자가 필요하다: Patroni(etcd/Consul 기반 리더 선출 + 자동 promote) 또는 repmgr(repmgrd를 이용한 모니터링/페일오버). 애플리케이션 측에는 HAProxy/pgBouncer로 단일 endpoint를 제공한다. 수동 promote가 필요하면: pg_ctl promote. promote 전에 이전 primary가 확실히 중지되었고 애플리케이션이 그곳에 쓸 수 없는지 확인한다(split-brain 위험). 이전 primary를 새 primary의 standby로 재합류시키려면 pg_rewind가 필요하며, 이를 위해 wal_log_hints = on 또는 data checksums가 활성화되어 있어야 한다.","engines":{"postgres":"-- Manual promote (if there is no cluster manager):\npg_ctl promote -D /var/lib/pgsql/data\n-- or via SQL:\nSELECT pg_promote();\n-- With Patroni: patronictl -c /etc/patroni.yml switchover mycluster"}}],"related":["replication-status","list-replicas","promote-failover"]},{"id":"hadr-mysql-innodb-cluster","title":"MySQL InnoDB Cluster(Group Replication) 구성","icon":"🐬","symptom":"목표: InnoDB Cluster(Group Replication + MySQL Shell + MySQL Router)로 MySQL에 자동 페일오버가 있는 고가용성을 구성한다.","steps":[{"title":"사전 요구 사항을 충족한다","detail":"InnoDB Cluster에는 모든 노드에 MySQL 8.0+, 고유한 server_id, GTID 모드, InnoDB 스토리지 엔진이 필요하다. 모든 인스턴스는 네트워크로 서로 도달할 수 있어야 하며 테이블에는 primary key가 있어야 한다. 관리용 clusterAdmin 사용자를 준비한다.","engines":{"mysql":"-- my.cnf on each instance:\n-- server_id unique, gtid_mode=ON, enforce_gtid_consistency=ON\nCREATE USER 'icadmin'@'%' IDENTIFIED BY 'secret';\nGRANT ALL PRIVILEGES ON *.* TO 'icadmin'@'%' WITH GRANT OPTION;"}},{"title":"MySQL Shell로 각 인스턴스를 구성한다","detail":"mysqlsh에서 dba.configureInstance()는 각 노드를 Group Replication에 적합하게 만든다. 누락된 파라미터를 수정하고 필요하면 재시작을 요청한다. 모든 노드에서 반복한다.","engines":{"mysql":"-- mysqlsh:\ndba.configureInstance('icadmin@node1:3306')\ndba.configureInstance('icadmin@node2:3306')\ndba.configureInstance('icadmin@node3:3306')"}},{"title":"클러스터를 생성한다(seed 노드)","detail":"한 노드에 연결하여 dba.createCluster()로 클러스터를 시작한다. 이 노드가 첫 primary가 된다. cluster 객체가 반환되며 이후 추가에 사용된다.","engines":{"mysql":"-- mysqlsh, while connected to node1:\nvar cluster = dba.createCluster('appCluster')"}},{"title":"다른 인스턴스를 클러스터에 추가한다","detail":"cluster.addInstance()로 secondary 노드를 추가한다. recoveryMethod로 clone(물리 복제, 새 노드에 빠름) 또는 incremental(GTID 기반)을 선택한다. 최소 3개 노드가 홀수 과반수와 자동 페일오버를 제공한다.","engines":{"mysql":"-- mysqlsh:\ncluster.addInstance('icadmin@node2:3306', {recoveryMethod: 'clone'})\ncluster.addInstance('icadmin@node3:3306', {recoveryMethod: 'clone'})"}},{"title":"MySQL Router를 bootstrap한다","detail":"MySQL Router는 애플리케이션과 클러스터 사이를 라우팅한다. 읽기/쓰기(6446)와 읽기 전용(6447) 포트를 제공하며 primary가 바뀌면 트래픽을 자동으로 리디렉션한다. bootstrap은 클러스터 메타데이터로부터 구성을 생성한다.","engines":{"mysql":"mysqlrouter --bootstrap icadmin@node1:3306 --user=mysqlrouter --directory /etc/mysqlrouter\n# Then: mysqlrouter -c /etc/mysqlrouter/mysqlrouter.conf &"}},{"title":"클러스터 상태를 확인하고 페일오버를 검증한다","detail":"cluster.status()는 각 노드의 역할(R/W primary, R/O secondary)과 상태(ONLINE)를 보여 준다. clusterName과 topology가 정상이어야 한다. primary를 종료하고 새 primary가 자동 선출되는지 관찰하여 페일오버를 테스트한다.","engines":{"mysql":"-- mysqlsh:\nvar cluster = dba.getCluster('appCluster')\ncluster.status()\n-- Members via SQL:\nSELECT member_host, member_role, member_state FROM performance_schema.replication_group_members;"}}],"related":["replication-status","list-replicas","promote-failover"]},{"id":"hadr-mongodb-replicaset","title":"MongoDB replica set + 샤딩 구성","icon":"🍃","symptom":"목표: MongoDB에 자동 페일오버가 있는 replica set을 구성하고 수평 확장을 위해 sharded cluster로 확장한다.","steps":[{"title":"같은 replSet 이름으로 mongod 프로세스를 시작한다","detail":"각 replica set 멤버를 같은 --replSet 이름과 도달 가능한 --bind_ip로 시작한다. 운영 환경에서는 keyFile 또는 TLS를 이용한 내부 인증을 권장한다. 명확한 과반수를 위해 투표 멤버 수를 홀수로 한다.","engines":{"mongodb":"mongod --replSet rs0 --port 27017 --dbpath /data/n1 --bind_ip localhost,10.0.0.11\nmongod --replSet rs0 --port 27017 --dbpath /data/n2 --bind_ip localhost,10.0.0.12\nmongod --replSet rs0 --port 27017 --dbpath /data/n3 --bind_ip localhost,10.0.0.13"}},{"title":"replica set을 initiate한다","detail":"mongosh로 한 멤버에 연결해 rs.initiate()를 호출하고 모든 멤버를 정의한다. 이로써 첫 선출이 일어나 primary가 선택된다. rs.status()로 PRIMARY/SECONDARY 역할을 확인한다.","engines":{"mongodb":"rs.initiate({\n  _id: 'rs0',\n  members: [\n    { _id: 0, host: '10.0.0.11:27017' },\n    { _id: 1, host: '10.0.0.12:27017' },\n    { _id: 2, host: '10.0.0.13:27017' }\n  ]\n})\nrs.status()"}},{"title":"멤버를 추가하고 우선순위와 필요시 arbiter를 설정한다","detail":"rs.add()로 나중에 멤버를 추가한다. priority는 어떤 멤버가 primary가 되기 쉬운지 결정한다(0 = primary가 되지 않음). arbiter는 투표만 하고 데이터를 갖지 않는다. 데이터 노드 수가 짝수일 때 tie-breaker로 사용하지만 데이터 안전을 위해 실제 멤버가 바람직하다. arbiter 기반 토폴로지(특히 PSA)에서는 데이터 노드가 다운되면 majority write concern을 충족할 수 없어 쓰기가 대기한다. 5.0+에서는 arbiter 추가 전에 setDefaultRWConcern으로 클러스터 전체 기본 write concern을 명시적으로 설정해야 할 수 있다.","engines":{"mongodb":"rs.add({ host: '10.0.0.14:27017', priority: 0.5 })\n// Arbiter only if really needed (a data-bearing member is preferred):\nrs.addArb('10.0.0.15:27017')\n// change priority:\ncfg = rs.conf(); cfg.members[0].priority = 2; rs.reconfig(cfg)"}},{"title":"샤딩을 위한 config server replica set을 구성한다","detail":"sharded cluster의 메타데이터는 config server replica set(--configsvr)에 보관된다. 자체적으로 replica set으로 initiate하며, 고가용성을 위해 3개 멤버를 권장한다.","engines":{"mongodb":"mongod --configsvr --replSet cfgrs --port 27019 --dbpath /data/cfg1 --bind_ip localhost,10.0.0.21\n-- mongosh (on the config server):\nrs.initiate({ _id: 'cfgrs', configsvr: true, members: [\n  { _id: 0, host: '10.0.0.21:27019' },\n  { _id: 1, host: '10.0.0.22:27019' },\n  { _id: 2, host: '10.0.0.23:27019' } ] })"}},{"title":"샤드 replica set과 mongos 라우터를 시작한다","detail":"각 샤드는 --shardsvr로 시작하는 독립 replica set이다(데이터 내구성은 샤드 내부 복제에서 나옴). mongos 라우터는 config server를 가리키도록 시작하며, 애플리케이션은 mongos에 연결한다.","engines":{"mongodb":"mongod --shardsvr --replSet shard1rs --port 27018 --dbpath /data/s1 --bind_ip localhost,10.0.0.31\n# initiate shard1rs with rs.initiate, then mongos:\nmongos --configdb cfgrs/10.0.0.21:27019,10.0.0.22:27019,10.0.0.23:27019 --port 27017 --bind_ip localhost,10.0.0.40"}},{"title":"샤드를 추가하고 컬렉션을 샤드한다","detail":"mongos에 연결해 sh.addShard()로 각 샤드 replica set을 추가한다. 데이터베이스에 샤딩을 활성화하고 sh.shardCollection()으로 샤드 키 기준으로 컬렉션을 분산한다. 키 선택이 매우 중요하다: 카디널리티가 높고 분포가 고르게 해야 한다. sh.status()로 chunk 분포를 모니터링한다.","engines":{"mongodb":"sh.addShard('shard1rs/10.0.0.31:27018')\nsh.addShard('shard2rs/10.0.0.32:27018')\nsh.enableSharding('appdb')\nsh.shardCollection('appdb.events', { customer_id: 'hashed' })\nsh.status()"}}],"related":["replication-status","list-replicas","promote-failover"]},{"id":"hadr-redis-sentinel","title":"Redis Sentinel + Cluster를 이용한 고가용성","icon":"⚡","symptom":"목표: Sentinel로 Redis에 자동 페일오버가 있는 master-replica 구성을 만들고, 수평 확장이 필요하면 Redis Cluster 모드로 전환한다.","steps":[{"title":"master와 replica를 구성한다","detail":"master 하나와 최소 두 개의 replica를 실행한다. replica는 REPLICAOF로 master를 따른다(redis.conf에 'replicaof <host> <port>'로도 작성 가능). 지속성을 위해 AOF 또는 RDB를 활성화하고, requirepass/masterauth로 인증하는 것을 권장한다.","engines":{"redis":"# at runtime on the replica nodes:\nREPLICAOF 10.0.0.10 6379\n# to make it persistent, redis.conf:\n# replicaof 10.0.0.10 6379\n# masterauth secret\nINFO replication"}},{"title":"monitor와 quorum으로 Sentinel을 구성한다","detail":"각 Sentinel 노드의 sentinel.conf에 감시할 master와 quorum을 정의한다. quorum은 master를 'down'으로 판단하는 데 필요한 최소 Sentinel 수다. 운영 환경에서는 3개의 별도 서버에 3개의 Sentinel을 권장한다(split-brain 대비).","engines":{"redis":"# sentinel.conf:\nsentinel monitor mymaster 10.0.0.10 6379 2\nsentinel down-after-milliseconds mymaster 5000\nsentinel failover-timeout mymaster 60000\nsentinel parallel-syncs mymaster 1"}},{"title":"Sentinel 프로세스를 시작한다","detail":"각 노드에서 Sentinel을 시작한다. Sentinel은 서로와 replica를 자동으로 발견하며, 런타임에 구성을 파일에 기록한다. 따라서 sentinel.conf는 쓰기 가능해야 한다.","engines":{"redis":"redis-sentinel /etc/redis/sentinel.conf\n# or:\nredis-server /etc/redis/sentinel.conf --sentinel"}},{"title":"Sentinel로 상태를 조회하고 페일오버를 테스트한다","detail":"SENTINEL 명령으로 현재 master와 replica를 조회한다. 애플리케이션은 Sentinel에 'get-master-addr-by-name'을 요청하여 현재 master를 알아내야 한다. 계획된 페일오버로 자동 전환을 확인한다.","engines":{"redis":"SENTINEL master mymaster\nSENTINEL get-master-addr-by-name mymaster\nSENTINEL replicas mymaster\n# planned switchover:\nSENTINEL failover mymaster"}},{"title":"대안: Redis Cluster로 데이터를 분할한다","detail":"단일 master의 메모리/처리량이 부족하면 Redis Cluster를 사용한다: 데이터는 16384개 hash slot으로 나뉘어 여러 master에 분산되며, 각 master는 자체 replica를 가진다. 애플리케이션은 cluster-aware 클라이언트를 사용해야 한다. redis-cli --cluster create로 구성한다.","engines":{"redis":"redis-cli --cluster create \\\n  10.0.0.11:6379 10.0.0.12:6379 10.0.0.13:6379 \\\n  10.0.0.21:6379 10.0.0.22:6379 10.0.0.23:6379 \\\n  --cluster-replicas 1"}},{"title":"클러스터 상태를 확인한다","detail":"클러스터 생성 후 모든 slot이 할당되고 노드가 연결되었는지 확인한다. cluster_state는 'ok'여야 하며 16384개 slot 전체가 할당된 것으로 보여야 한다. CLUSTER 명령으로 노드와 slot 분포를 모니터링한다.","engines":{"redis":"redis-cli --cluster check 10.0.0.11:6379\nredis-cli -c -h 10.0.0.11 CLUSTER INFO\nredis-cli -c -h 10.0.0.11 CLUSTER NODES"}}],"related":["replication-status","list-replicas","promote-failover"]},{"id":"hadr-elasticsearch-ha","title":"Elasticsearch 복원력: replica + 스냅샷 + CCR","icon":"🔎","symptom":"목표: replica 샤드, 전용 master/data 노드, 스냅샷 백업(SLM), cross-cluster replication(CCR)으로 Elasticsearch의 복원력을 구축한다.","steps":[{"title":"역할 분리: 전용 master 노드와 data 노드","detail":"클러스터 안정성을 위해 투표하는 master-eligible 노드(가급적 3개, 홀수 과반수)를 데이터 워크로드에서 분리한다. elasticsearch.yml의 node.roles로 역할을 지정하고 discovery를 위한 seed host를 정의한다. master 3개가 split-brain을 방지한다.","engines":{"elasticsearch":"# elasticsearch.yml on master nodes:\n# node.roles: [ master ]\n# on data nodes:\n# node.roles: [ data, ingest ]\n# discovery.seed_hosts: [ m1, m2, m3 ]\n# cluster.initial_master_nodes: [ m1, m2, m3 ]"}},{"title":"인덱스 replica 수를 설정한다","detail":"number_of_replicas는 각 primary 샤드에 대해 유지하는 사본 수다. data 노드를 잃어도 데이터를 사용할 수 있도록 최소 1이어야 한다. 운영 중인 인덱스에서 동적으로 변경할 수 있으며, replica 수는 data 노드 수보다 작아야 한다.","engines":{"elasticsearch":"PUT /orders/_settings\n{\n  \"index\": { \"number_of_replicas\": 1 }\n}\n# default via template (composable template; the old _template API is legacy):\n# PUT _index_template/order-tpl { \"index_patterns\": [\"order*\"], \"template\": { \"settings\": { \"number_of_replicas\": 1 } } }"}},{"title":"스냅샷 리포지토리를 정의한다","detail":"백업은 repository에 저장된다: 공유 파일 시스템(fs) 또는 S3/GCS/Azure 플러그인. 클러스터가 삭제되어도 데이터를 복구할 수 있도록 repository는 클러스터 외부의 내구성 있는 스토리지에 있어야 한다.","engines":{"elasticsearch":"PUT /_snapshot/backup_repo\n{\n  \"type\": \"fs\",\n  \"settings\": { \"location\": \"/mnt/es_backups\", \"compress\": true }\n}"}},{"title":"SLM으로 자동 스냅샷을 스케줄링한다","detail":"Snapshot Lifecycle Management(SLM)는 정기적으로 스냅샷을 생성하고 retention 정책을 적용한다. cron 식으로 빈도, 포함할 인덱스, 보관할 스냅샷 수를 정의한다.","engines":{"elasticsearch":"PUT /_slm/policy/daily_backup\n{\n  \"schedule\": \"0 30 1 * * ?\",\n  \"name\": \"<daily-{now/d}>\",\n  \"repository\": \"backup_repo\",\n  \"config\": { \"indices\": [\"*\"] },\n  \"retention\": { \"expire_after\": \"30d\", \"min_count\": 5, \"max_count\": 50 }\n}"}},{"title":"스냅샷과 복원을 검증한다","detail":"SLM 정책을 수동으로 실행해 동작을 확인한다. 스냅샷 상태는 SUCCESS여야 한다. 테스트하지 않은 백업은 백업이 아니므로 복원 테스트를 정기적으로 수행한다.","engines":{"elasticsearch":"POST /_slm/policy/daily_backup/_execute\nGET /_snapshot/backup_repo/_all\n# restore:\nPOST /_snapshot/backup_repo/<snapshot_name>/_restore"}},{"title":"Cross-Cluster Replication(CCR)으로 원격 DR을 구성한다","detail":"리전 간 재해 복구를 위해 두 번째 클러스터(follower)가 원격 primary 클러스터(leader)의 인덱스를 따른다. 먼저 remote cluster를 정의한 뒤 follower 인덱스를 leader에 연결하면 leader의 변경이 자동으로 흐른다. CCR은 Platinum(상용) 기능이다.","engines":{"elasticsearch":"# define the remote cluster on the follower cluster:\nPUT /_cluster/settings\n{ \"persistent\": { \"cluster\": { \"remote\": { \"primary\": { \"seeds\": [\"leader1:9300\"] } } } } }\n# create the follower index:\nPUT /orders-follower/_ccr/follow\n{ \"remote_cluster\": \"primary\", \"leader_index\": \"orders\" }"}}],"related":["replication-status","list-replicas","last-backup-status"]}]},"playbooks/security.json":{"schemaVersion":1,"playbooks":[{"id":"harden-sql","title":"보안 강화: SQL 데이터베이스(Oracle, PostgreSQL, MySQL, SQL Server)","icon":"🔒","symptom":"목표: CIS 방식으로 관계형 데이터베이스를 강화한다 — 기본 계정, 강력한 비밀번호 정책, 최소 권한, 네트워크 제한, TLS, 감사, 암호화(TDE).","steps":[{"title":"기본/불필요한 계정을 제거하거나 잠그고 샘플 스키마를 삭제한다","detail":"설치와 함께 제공되는 기본 계정과 샘플 스키마는 잘 알려진 공격 표면이다. 사용하지 않는 것은 잠그거나 제거하고, 최상위/서비스 계정의 이름을 변경한다.","engines":{"oracle":"-- Lock unused default accounts and expire their passwords:\nALTER USER SCOTT ACCOUNT LOCK PASSWORD EXPIRE;\nALTER USER HR ACCOUNT LOCK PASSWORD EXPIRE;\n-- Audit locked/open accounts:\nSELECT username, account_status FROM dba_users ORDER BY account_status;\n-- Remove sample schemas: $ORACLE_HOME/demo/schema/drop_sch.sql","postgres":"-- Keep the default superuser 'postgres' role; do NOT drop/rename it.\n-- Audit roles:\nSELECT rolname, rolsuper, rolcanlogin FROM pg_roles ORDER BY rolsuper DESC;\n-- Remove unneeded login roles:\nDROP ROLE IF EXISTS test_user;\n-- Drop sample/test databases: DROP DATABASE IF EXISTS test;","mysql":"# Interactive hardening wizard (removes anonymous users, the test DB, remote root):\nmysql_secure_installation\n-- Manual: remove anonymous and test accounts:\nDROP USER IF EXISTS ''@'localhost';\nDROP USER IF EXISTS ''@'%';\nDROP DATABASE IF EXISTS test;\nDELETE FROM mysql.db WHERE Db LIKE 'test%'; FLUSH PRIVILEGES;","mssql":"-- Disable and rename the 'sa' account:\nALTER LOGIN sa DISABLE;\nALTER LOGIN sa WITH NAME = [sa_disabled_acct];\n-- Restrict the guest user — only in USER databases\n-- (guest must stay enabled in master, tempdb and msdb; disabling it in tempdb/msdb causes problems):\nUSE appdb;\nREVOKE CONNECT FROM guest;\n-- Remove sample DBs: DROP DATABASE AdventureWorks;"}},{"title":"강력한 비밀번호 정책과 만료를 적용한다","detail":"약하거나 바뀌지 않는 비밀번호는 가장 흔한 침입 경로다. 로그인 시도 제한, 복잡성, 비밀번호 수명을 적용하고 강력한 해시로 비밀번호를 저장한다.","engines":{"oracle":"-- Attempt limit, lifetime and complexity via a profile:\nCREATE PROFILE app_profile LIMIT\n  FAILED_LOGIN_ATTEMPTS 5\n  PASSWORD_LIFE_TIME 90\n  PASSWORD_REUSE_MAX 10\n  PASSWORD_VERIFY_FUNCTION ora12c_verify_function;\nALTER USER app_user PROFILE app_profile;","postgres":"-- Strong hash algorithm (SCRAM):\nSHOW password_encryption;              -- should be 'scram-sha-256'\nALTER SYSTEM SET password_encryption = 'scram-sha-256';\n-- Load the passwordcheck extension for complexity:\n-- shared_preload_libraries = 'passwordcheck'\n-- Lifetime: ALTER ROLE app_user VALID UNTIL '2026-12-31';","mysql":"-- Install the validate_password component and set the policy:\nINSTALL COMPONENT 'file://component_validate_password';\nSET PERSIST validate_password.policy = 'STRONG';\nSET PERSIST validate_password.length = 14;\n-- Attempt limit and lifetime:\nALTER USER app_user PASSWORD EXPIRE INTERVAL 90 DAY\n  FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LOCK_TIME 1;","mssql":"-- Enforce the Windows password policy and expiration on the login:\nALTER LOGIN app_user WITH CHECK_POLICY = ON, CHECK_EXPIRATION = ON;\n-- Audit policy/expiration status:\nSELECT name, is_policy_checked, is_expiration_checked\nFROM sys.sql_logins;"}},{"title":"최소 권한: PUBLIC/광범위 권한을 회수하고 역할 기반 권한을 부여한다","detail":"모든 사용자(PUBLIC)에게 부여된 광범위 권한은 lateral movement를 쉽게 만든다. 이를 회수하고 역할을 만들어 필요한 것만 부여한다.","engines":{"oracle":"-- Revoke dangerous PUBLIC privileges:\nREVOKE EXECUTE ON UTL_HTTP FROM PUBLIC;\nREVOKE EXECUTE ON UTL_FILE FROM PUBLIC;\nREVOKE EXECUTE ON DBMS_SCHEDULER FROM PUBLIC;\n-- Role-based privileges:\nCREATE ROLE app_read;\nGRANT SELECT ON app.orders TO app_read;\nGRANT app_read TO app_user;","postgres":"-- Revoke the default PUBLIC privileges on the schema:\nREVOKE ALL ON DATABASE appdb FROM PUBLIC;\nREVOKE CREATE ON SCHEMA public FROM PUBLIC;\n-- Role-based, read-only access:\nCREATE ROLE app_read NOLOGIN;\nGRANT USAGE ON SCHEMA app TO app_read;\nGRANT SELECT ON ALL TABLES IN SCHEMA app TO app_read;\nGRANT app_read TO app_user;","mysql":"-- Audit and revoke broad global privileges:\nSHOW GRANTS FOR app_user;\nREVOKE ALL PRIVILEGES, GRANT OPTION FROM app_user;\n-- Role-based, object-level privileges (MySQL 8+):\nCREATE ROLE app_read;\nGRANT SELECT ON appdb.* TO app_read;\nGRANT app_read TO app_user;\nSET DEFAULT ROLE app_read TO app_user;","mssql":"-- Revoke excessive privileges granted to the public role:\nREVOKE SELECT ON SCHEMA::dbo FROM public;\n-- Role-based privileges:\nCREATE ROLE app_read;\nGRANT SELECT ON SCHEMA::app TO app_read;\nALTER ROLE app_read ADD MEMBER app_user;\n-- At server level: limit fixed server roles (sysadmin)."}},{"title":"네트워크/접근을 제한한다 — 알려진 클라이언트만 연결 허용","detail":"데이터베이스가 전 세계에 열려 listen하지 않도록 한다. listen 주소를 제한하고, 호스트/IP 기반 접근 규칙을 적용하며, 방화벽으로 포트를 제한한다.","engines":{"oracle":"# listener.ora / sqlnet.ora — valid node checking:\nTCP.VALIDNODE_CHECKING = YES\nTCP.INVITED_NODES = (10.0.0.5, 10.0.0.6)\nTCP.EXCLUDED_NODES = (0.0.0.0/0)\n# After the change: lsnrctl reload","postgres":"# postgresql.conf — listen only on specific interfaces:\nlisten_addresses = '10.0.0.10,localhost'\n# pg_hba.conf — host/IP + strong authentication:\nhostssl appdb app_user 10.0.0.0/24 scram-sha-256\nhost    all      all       0.0.0.0/0      reject\n# Reload: SELECT pg_reload_conf();","mysql":"# my.cnf — bind only to a specific interface:\n[mysqld]\nbind-address = 10.0.0.10\n-- Host-based user (only from the application server):\nCREATE USER 'app_user'@'10.0.0.5'\n  IDENTIFIED BY '****' REQUIRE SSL;\n-- Never give remote root the '%' host.","mssql":"# Limit the TCP port with the firewall (application subnet only):\nNew-NetFirewallRule -DisplayName 'SQL-1433' -Direction Inbound `\n  -Protocol TCP -LocalPort 1433 -RemoteAddress 10.0.0.0/24 -Action Allow\n-- Audit listening addresses:\nSELECT * FROM sys.dm_tcp_listener_states;\n-- Disable the SQL Browser service if not used."}},{"title":"TLS/암호화 연결을 강제한다","detail":"네트워크 트래픽은 수동 스니핑에 노출된다. 서버 인증서를 구성하고 클라이언트가 암호화하여 연결하도록 강제한다.","engines":{"oracle":"# sqlnet.ora — TCPS/TLS (server certificate in the wallet):\nWALLET_LOCATION = (SOURCE=(METHOD=FILE)(METHOD_DATA=(DIRECTORY=/etc/oracle/wallet)))\nSSL_CLIENT_AUTHENTICATION = FALSE\nSSL_VERSION = 1.2\n# Listen only on TCPS (port 2484) in listener.ora. Note: SQLNET.ENCRYPTION_SERVER is Native Network Encryption, not TLS.","postgres":"# postgresql.conf:\nssl = on\nssl_cert_file = 'server.crt'\nssl_key_file  = 'server.key'\nssl_min_protocol_version = 'TLSv1.2'\n# Use 'hostssl' lines in pg_hba.conf; reject unencrypted connections over plain 'host'.","mysql":"# my.cnf:\n[mysqld]\nrequire_secure_transport = ON\nssl_cert = /etc/mysql/server-cert.pem\nssl_key  = /etc/mysql/server-key.pem\nssl_ca   = /etc/mysql/ca.pem\n-- At user level: ALTER USER app_user REQUIRE SSL;","mssql":"-- Force connection encryption (SQL Server Configuration Manager):\n-- Protocols for MSSQLSERVER > Properties > Force Encryption = Yes\n-- Assign a certificate; then restart the service.\n-- Check the status:\nSELECT session_id, encrypt_option FROM sys.dm_exec_connections;"}},{"title":"감사를 활성화한다 — 누가 무엇을 했는지 보이게 한다","detail":"감사 추적은 권한 있는 접근과 의심스러운 활동을 모니터링하는 데 필수다. 로그인, 권한 변경, 민감 객체 접근을 기록한다.","engines":{"oracle":"-- Create and enable a policy with Unified Auditing:\nCREATE AUDIT POLICY sec_pol\n  ACTIONS LOGON, LOGOFF, ALTER USER, GRANT, CREATE USER, DROP USER;\nAUDIT POLICY sec_pol;\n-- View the records:\nSELECT * FROM unified_audit_trail ORDER BY event_timestamp DESC;","postgres":"# With the pgaudit extension:\n# postgresql.conf:\nshared_preload_libraries = 'pgaudit'\npgaudit.log = 'ddl, role, write'\n-- Create the extension:\nCREATE EXTENSION pgaudit;\n-- log_connections = on and log_disconnections = on should also be enabled.","mysql":"-- Load the Enterprise/Percona audit_log plugin:\nINSTALL PLUGIN audit_log SONAME 'audit_log.so';\nSET PERSIST audit_log_policy = 'ALL';\n-- MariaDB: server_audit plugin\n-- INSTALL SONAME 'server_audit'; SET GLOBAL server_audit_logging=ON;","mssql":"-- Create and start a Server Audit:\nCREATE SERVER AUDIT sec_audit\n  TO FILE (FILEPATH = 'C:\\SQLAudit\\');\nALTER SERVER AUDIT sec_audit WITH (STATE = ON);\nCREATE SERVER AUDIT SPECIFICATION sec_spec\n  FOR SERVER AUDIT sec_audit\n  ADD (FAILED_LOGIN_GROUP), ADD (DATABASE_ROLE_MEMBER_CHANGE_GROUP)\n  WITH (STATE = ON);"}},{"title":"데이터 암호화를 적용한다(at rest / TDE)","detail":"디스크, 백업 또는 파일이 도난당해도 데이터를 읽을 수 없어야 한다. 컬럼/테이블스페이스/데이터베이스 수준에서 저장 데이터 암호화(TDE)를 활성화한다.","engines":{"oracle":"-- Transparent Data Encryption (keystore + tablespace):\nADMINISTER KEY MANAGEMENT CREATE KEYSTORE '/etc/oracle/wallet'\n  IDENTIFIED BY \"WalletPwd1\";\nADMINISTER KEY MANAGEMENT SET KEY IDENTIFIED BY \"WalletPwd1\" WITH BACKUP;\nCREATE TABLESPACE sec_ts DATAFILE '/u01/sec01.dbf' SIZE 100M\n  ENCRYPTION USING 'AES256' DEFAULT STORAGE(ENCRYPT);","postgres":"-- There is no built-in block-level TDE; two common approaches:\n-- 1) Column-level pgcrypto:\nCREATE EXTENSION pgcrypto;\nINSERT INTO t(ssn) VALUES (pgp_sym_encrypt('123-45', 'my_key'));\nSELECT pgp_sym_decrypt(ssn, 'my_key') FROM t;\n-- 2) File system/block encryption (LUKS) or an EDB/Cybertec TDE build.","mysql":"# InnoDB tablespace encryption — keyring COMPONENT (8.0.24+; the keyring_file plugin was removed in 8.4)\n# 1) Global manifest in the mysqld binary directory: mysqld.my\n{ \"components\": \"file://component_keyring_file\" }\n# 2) In plugin_dir: component_keyring_file.cnf\n{ \"path\": \"/var/lib/mysql-keyring/component_keyring_file\", \"read_only\": false }\n# 3) Restart the server; verify:\n-- SELECT * FROM performance_schema.keyring_component_status;\n-- Encrypt the table:\nALTER TABLE app.orders ENCRYPTION = 'Y';\n-- For new tables: SET PERSIST default_table_encryption = ON;\n# Before 8.0.24 (8.0 only): early-plugin-load = keyring_file.so + keyring_file_data\n# WARNING: back up the keyring file SEPARATELY from the data directory and securely;\n# if the keyring is lost, encrypted tables and their backups become permanently unreadable.","mssql":"-- Transparent Data Encryption (DEK + certificate) — the key and certificate must be in master:\nUSE master;\nCREATE MASTER KEY ENCRYPTION BY PASSWORD = '<StrongPassword1>';\nCREATE CERTIFICATE tde_cert WITH SUBJECT = 'TDE Cert';\n-- MANDATORY: back up the certificate with its private key immediately and store it off the server.\n-- Without this backup, if the server is lost the encrypted DB and its backups CANNOT be restored.\nBACKUP CERTIFICATE tde_cert TO FILE = 'D:\\secure\\tde_cert.cer'\n  WITH PRIVATE KEY (FILE = 'D:\\secure\\tde_cert.pvk',\n    ENCRYPTION BY PASSWORD = '<CertBackupPassword1>');\nUSE appdb;\nCREATE DATABASE ENCRYPTION KEY\n  WITH ALGORITHM = AES_256 ENCRYPTION BY SERVER CERTIFICATE tde_cert;\nALTER DATABASE appdb SET ENCRYPTION ON;\n-- Progress: SELECT DB_NAME(database_id), encryption_state, percent_complete FROM sys.dm_database_encryption_keys;"}},{"title":"패치를 최신으로 유지하고 불필요한 기능을 비활성화한다","detail":"알려진 취약점은 가장 많이 악용되는 위험이다. 보안 패치를 정기적으로 적용하고, 사용하지 않는 프로토콜, 플러그인, 기능을 비활성화한다(공격 표면 축소).","engines":{"oracle":"-- Check the installed version and patches:\nSELECT version_full FROM v$instance;\n-- Apply the quarterly Critical Patch Update (CPU): OPatch/opatchauto\n-- Remove unused components (e.g. XML DB, APEX) and\n-- disable the external procedure (extproc) listener.","postgres":"# Check the version and minor patch level:\nSELECT version();\n# Upgrade to the current minor release with the package manager (e.g. latest 16.x).\n# Remove unused extensions: DROP EXTENSION IF EXISTS ...;\n# postgresql.conf: do not load unused language/PL extensions.","mysql":"-- Check the version: SELECT VERSION();\n-- Upgrade to the LTS/security release (8.0.x/8.4 LTS).\n-- Disable local file reading:\nSET PERSIST local_infile = OFF;\n-- Remove unused plugins: UNINSTALL PLUGIN ...;","mssql":"-- Check the product version/patch level:\nSELECT SERVERPROPERTY('ProductVersion'), SERVERPROPERTY('ProductLevel');\n-- Apply the latest Cumulative Update (CU).\n-- Disable unused features (with sp_configure):\nEXEC sp_configure 'xp_cmdshell', 0; RECONFIGURE;\nEXEC sp_configure 'Ole Automation Procedures', 0; RECONFIGURE;"}}],"related":["list-users","create-user-grant","enable-audit","change-password"]},{"id":"harden-nosql","title":"보안 강화: MongoDB / Redis / Elasticsearch","icon":"🛡️","symptom":"목표: CIS 방식으로 NoSQL/데이터 저장소를 강화한다 — 인증, 네트워크 제한, 위험 명령 차단, TLS, 최소 권한 역할, 감사.","steps":[{"title":"인증을 활성화한다 — 인증되지 않은 접근을 차단한다","detail":"이 제품들은 기본적으로 인증이 비활성화된 상태로 제공되는 경우가 많아, 인터넷에 노출된 채 방치되면 침해된다. 인증을 활성화하고 관리자 사용자를 생성한다.","engines":{"mongodb":"// mongod.conf:\nsecurity:\n  authorization: enabled\n// Create an administrator user (first time, via the localhost exception):\nuse admin\ndb.createUser({ user: \"admin\", pwd: passwordPrompt(),\n  roles: [ { role: \"userAdminAnyDatabase\", db: \"admin\" } ] })\n// Then connect with: mongosh -u admin --authenticationDatabase admin","redis":"# redis.conf — ACL (Redis 6+): disable the default user, define named users:\nuser default off\nuser admin on >AdminPassword789! ~* &* +@all\nuser app on >AppPassword456! ~app:* +@read +@write\n# Note: requirepass is a legacy compatibility setting that only sets the 'default' user's password;\n# do not confuse it with ACL users. You can also keep users in an aclfile (persisted with ACL SAVE).","elasticsearch":"# 8.x: security (authentication + TLS) is configured automatically on first start; elasticsearch.yml:\nxpack.security.enabled: true\n# Set/reset the password of the 'elastic' user:\nbin/elasticsearch-reset-password -u elastic\n# (elasticsearch-setup-passwords is deprecated since 8.0)\n# Create separate users with dedicated roles for Kibana/clients instead of 'elastic'."}},{"title":"네트워크를 제한한다 — 필요한 인터페이스와 포트만 연다","detail":"서비스가 0.0.0.0으로 전 세계에 listen하지 않도록 한다. bind 주소를 내부 인터페이스/loopback으로 고정하고 protected mode를 활성화한다.","engines":{"mongodb":"# mongod.conf — internal interface + loopback only:\nnet:\n  bindIp: 127.0.0.1,10.0.0.10\n  port: 27017\n# Never use bindIpAll: true; limit the port with a firewall.","redis":"# redis.conf:\nbind 127.0.0.1 10.0.0.10\nprotected-mode yes\nport 6379\n# protected-mode blocks external access when no password is set;\n# still limit 6379 with a firewall.","elasticsearch":"# elasticsearch.yml — pin the listening interface:\nnetwork.host: 10.0.0.10\nhttp.port: 9200\ntransport.port: 9300\n# Open only the required ports in the firewall;\n# 9300 (transport) must never be exposed externally."}},{"title":"위험한 명령을 제한한다","detail":"명령 하나로 모든 데이터가 지워질 수 있다. 파괴적/관리 명령의 이름을 바꾸거나 제거하고, 와일드카드 대상 지정을 차단한다.","engines":{"mongodb":"// Use role-based control instead of a separate command block:\n// Grant destructive privileges (dropDatabase, dropCollection) only to\n// dbAdmin/clusterAdmin roles; do not grant them to the application user.\ndb.grantRolesToUser(\"app\", [ { role: \"readWrite\", db: \"appdb\" } ])\n// Do NOT grant dbAdmin/root to the application user.","redis":"# For Redis 6+ the preferred way is ACLs (do not give the application user dangerous commands):\n# ACL SETUSER app ... -@dangerous\n# Legacy method — disable/rename commands in redis.conf:\nrename-command FLUSHALL \"\"\nrename-command FLUSHDB \"\"\nrename-command KEYS \"\"\nrename-command DEBUG \"\"\n# Caution: renaming CONFIG can break Sentinel, CONFIG REWRITE and management tools.","elasticsearch":"# elasticsearch.yml — block wildcard/all-index deletion:\naction.destructive_requires_name: true\n# This rejects DELETE /* or DELETE /_all;\n# bulk deletion is not possible without naming the index explicitly."}},{"title":"TLS/암호화 연결을 활성화한다","detail":"자격 증명과 데이터가 개방된 네트워크에서 암호화되지 않은 채 전송되어서는 안 된다. 클라이언트 및 노드 간 트래픽에 TLS를 활성화한다.","engines":{"mongodb":"# mongod.conf — client TLS:\nnet:\n  tls:\n    mode: requireTLS\n    certificateKeyFile: /etc/ssl/mongodb.pem\n    CAFile: /etc/ssl/ca.pem\n# Client: mongosh --tls --tlsCAFile ca.pem","redis":"# redis.conf — enable TLS, close the plain port:\nport 0\ntls-port 6379\ntls-cert-file /etc/redis/redis.crt\ntls-key-file  /etc/redis/redis.key\ntls-ca-cert-file /etc/redis/ca.crt","elasticsearch":"# elasticsearch.yml — HTTP and transport TLS:\nxpack.security.http.ssl.enabled: true\nxpack.security.http.ssl.keystore.path: http.p12\nxpack.security.transport.ssl.enabled: true\nxpack.security.transport.ssl.verification_mode: certificate\nxpack.security.transport.ssl.keystore.path: transport.p12"}},{"title":"최소 권한 역할을 부여한다 — 광범위한 역할을 피한다","detail":"각 사용자에게 업무에 필요한 접근만 준다. 광범위/관리 역할 대신 특정 데이터베이스, 키 패턴, 인덱스로 제한된 세분화된 역할을 사용한다.","engines":{"mongodb":"// Instead of broad readWrite, grant only on the required DB;\n// define a custom role if needed:\ndb.createRole({ role: \"ordersRW\",\n  privileges: [ { resource: { db: \"appdb\", collection: \"orders\" },\n    actions: [ \"find\", \"insert\", \"update\" ] } ], roles: [] })\ndb.grantRolesToUser(\"app\", [ { role: \"ordersRW\", db: \"appdb\" } ])","redis":"# Limit by key pattern and command category with ACLs:\nACL SETUSER app on >AppPassword456! ~app:* +@read +@write -@dangerous\n# Only keys with the 'app:' prefix, read/write; dangerous commands forbidden.\nACL LIST\nACL WHOAMI","elasticsearch":"# Define a fine-grained role (read-only on specific indices):\nPOST /_security/role/orders_read\n{ \"indices\": [ { \"names\": [ \"orders-*\" ],\n    \"privileges\": [ \"read\" ] } ] }\n# Assign it to the user:\nPOST /_security/user/app\n{ \"password\": \"...\", \"roles\": [ \"orders_read\" ] }"}},{"title":"감사/로깅을 활성화한다 — 접근과 변경을 모니터링한다","detail":"무단 접근 시도와 관리 작업은 기록되어야 한다. audit log를 활성화하고 중앙 위치로 전송한다.","engines":{"mongodb":"# Auditing (requires MongoDB Enterprise) — mongod.conf:\nauditLog:\n  destination: file\n  format: JSON\n  path: /var/log/mongodb/audit.json\n# Filter: capture events such as authenticate, createUser, dropDatabase.","redis":"# Redis has no full audit; use ACL LOG to see rejected\n# authentication/command attempts:\nACL LOG\nACL LOG RESET\n# Also: route the loglevel notice and logfile settings to a central collector.","elasticsearch":"# elasticsearch.yml — enable security auditing:\nxpack.security.audit.enabled: true\nxpack.security.audit.logfile.events.include:\n  [ authentication_failed, access_denied, connection_denied ]\n# Records are written to the <cluster>_audit.json file."}}],"related":["list-users","create-user-grant","enable-audit","change-password"]}]},"playbooks/cloud.json":{"schemaVersion":1,"playbooks":[{"id":"cloud-managed-db","title":"클라우드 관리형 데이터베이스(RDS/Aurora/Cloud SQL/Atlas/Azure SQL)","icon":"☁️","symptom":"목표: 클라우드 관리형 데이터베이스와 자체 관리 설치의 핵심 차이를 익히고, 클라우드에서 백업, 파라미터, 페일오버, 모니터링, 업그레이드, 연결이 어떻게 동작하는지 이해한다.","steps":[{"title":"사고방식 전환: 서버가 아니라 서비스에 접근한다","detail":"관리형 데이터베이스에서는 OS, 파일 시스템, 데이터베이스 프로세스에 대한 셸 접근이 없다. SSH 접속, 구성 파일 수동 편집, superuser 권한 획득이 불가능하다. 제공업체의 관리 API/콘솔과 일반 SQL 연결만 주어진다. AWS RDS/Aurora: 관리는 리소스(인스턴스/클러스터) 수준이며 서버는 숨겨져 있다. GCP Cloud SQL: 인스턴스 API만 있고 OS는 없다. MongoDB Atlas: 완전 관리형 클러스터로 mongod에 접근할 수 없다. Azure SQL Database: PaaS로 그 아래의 서버도 OS도 보이지 않는다(Managed Instance는 다소 더 많은 제어를 제공). 따라서 '파일을 본다', '프로세스를 종료한다', 'OS 도구를 실행한다' 같은 자체 관리 습관 대부분이 적용되지 않는다."},{"title":"백업: 자동 스냅샷 + 시점 복구(PITR)","detail":"백업을 직접 스케줄링하지 않으며 제공업체가 자동으로 수행한다. 복구는 대개 '특정 시점으로 복원'(PITR)하거나 스냅샷에서 새 인스턴스를 만드는 방식이며, 기존 인스턴스는 덮어쓰지 않는다. AWS RDS/Aurora: 자동 백업 + retention 기간. 초 단위 정밀도의 PITR 복원은 새 인스턴스/클러스터로 열린다. 수동 스냅샷도 가능하다. GCP Cloud SQL: 자동 백업 + binary log 기반 PITR, 복원은 새 인스턴스로 간다. MongoDB Atlas: 연속 클라우드 백업 + PITR(M10+), 스냅샷에서 복원. Azure SQL: 내장 자동 백업, 1-35일 보관, 임의 시점 PITR 및 삭제된 DB 복원. 공통 규칙: 백업 구성(시간대, 보관 기간)을 확인하고 정기적으로 복원 테스트를 실행한다."},{"title":"파라미터: 구성 파일이 아니라 parameter group / flag","detail":"postgresql.conf나 my.cnf를 직접 편집하는 대신 제공업체의 파라미터 추상화를 사용한다. 일부 파라미터는 잠겨 있고 일부는 재시작이 필요하다(static). AWS RDS/Aurora: DB parameter group(및 cluster parameter group)으로 설정하며, 동적 파라미터는 즉시, static 파라미터는 reboot 시 적용된다. GCP Cloud SQL: 콘솔/gcloud에서 'database flags'를 설정한다. MongoDB Atlas: 제한된 'Additional Settings' + oplog 크기 등, 대부분의 내부 파라미터는 관리형이다. Azure SQL: 대부분의 서버 수준 설정은 관리형이며, SCOPED CONFIGURATION과 서비스 티어 선택으로 동작이 바뀐다. 적용 전에 파라미터가 동적인지 static인지 확인한다."},{"title":"고가용성 / 페일오버: 관리형 Multi-AZ와 replica","detail":"페일오버를 직접 구축하지 않고 체크박스를 선택해 제공업체에 맡긴다. standby와 자동 페일오버는 관리형이며, 연결은 안정적인 endpoint를 통해 새 primary로 리디렉션된다. AWS RDS: Multi-AZ(동기 standby, 자동 페일오버). Aurora: 다중 사본 스토리지 + 자동 페일오버, cluster endpoint가 writer를 가리킨다. GCP Cloud SQL: 클릭 한 번으로 HA 구성(리전, 보조 존에 standby), read replica는 별도로 추가한다. MongoDB Atlas: replica set 내장, 멀티 리전/멀티 클라우드 배포 가능, 페일오버 자동. Azure SQL: 내장 HA(Business Critical의 로컬 replica), zone-redundant 옵션, 리전 간 active geo-replication / failover group. 애플리케이션은 고정 IP가 아니라 항상 endpoint에 연결해야 한다."},{"title":"모니터링: 제공업체의 지표 및 로그 서비스","detail":"OS 도구(top, iostat)를 실행할 수 없으므로 제공업체의 지표 플랫폼으로 모니터링한다. CPU, 연결 수, IOPS, 복제 지연, 스토리지 지표가 그곳에 있다. AWS: CloudWatch 지표 + Enhanced Monitoring + Performance Insights(쿼리 수준 분석). GCP Cloud SQL: Cloud Monitoring(구 Stackdriver) + Query Insights. MongoDB Atlas: 내장 Atlas 지표/차트 + Performance Advisor + 실시간 패널. Azure SQL: Azure Monitor + SQL Analytics + Query Performance Insight. 항상 임계값 알람(스토리지 사용량, CPU, 연결 한도, replica 지연)을 설정한다."},{"title":"버전 업그레이드와 유지보수: 콘솔에서, 유지보수 시간대에","detail":"바이너리를 직접 업데이트하는 대신 대상 버전을 선택한다. 마이너 버전 패치는 대부분 자동이며, 메이저 버전 업그레이드는 계획하여 수행하고 대개 다운타임/HA 페일오버를 수반한다. AWS RDS/Aurora: 엔진 버전 선택 + 자동 마이너 업그레이드 옵션 + Blue/Green Deployments를 이용한 저위험 메이저 업그레이드. GCP Cloud SQL: 유지보수 시간대 + 셀프서비스 메이저 버전 업그레이드. MongoDB Atlas: 클릭 한 번 버전 업그레이드, rolling 방식으로 거의 무중단. Azure SQL: 플랫폼이 자동으로 패치한다(버전 개념은 대부분 추상화됨). 동작은 compatibility level로 제어한다. 항상 테스트/staging 사본에서 먼저 시도하고 유지보수 시간대는 업무 시간 외로 잡는다."},{"title":"연결과 보안: endpoint + IAM/TLS","detail":"연결은 항상 제공업체가 제공하는 DNS endpoint를 통하며 암호화(TLS)된다. 네트워크 접근과 신원은 클라우드 ID 시스템에 연동할 수 있다. AWS RDS/Aurora: 인스턴스/클러스터 endpoint + 네트워크용 Security Group + 선택적 IAM 데이터베이스 인증 + TLS(rds-ca 인증서). GCP Cloud SQL: public IP + Authorized Networks 또는 private IP/VPC, Cloud SQL Auth Proxy, IAM 데이터베이스 인증, TLS. MongoDB Atlas: SRV 연결 문자열(mongodb+srv://), IP access list + private endpoint/peering, SCRAM 또는 IAM/X.509, TLS 필수. Azure SQL: 서버 FQDN endpoint + 방화벽/Private Link + Microsoft Entra ID(구 Azure AD) 인증 + TLS 필수. 규칙: 연결 문자열에는 고정 IP가 아니라 endpoint를 사용하고, TLS를 강제하며, 가능하면 클라우드 IAM ID로 연결한다."}],"related":["last-backup-status","promote-failover","connect-cli"]}]},"playbooks/automation.json":{"schemaVersion":1,"playbooks":[{"id":"auto-ansible","title":"Ansible을 이용한 데이터베이스 자동화","icon":"🤖","symptom":"목표: Ansible로 데이터베이스 설치, 구성, 사용자, 백업 작업을 반복 가능하고 멱등적으로(같은 playbook을 다시 실행해도 시스템이 깨지지 않음) 만든다.","steps":[{"title":"인벤토리와 그룹을 정의한다","detail":"인벤토리에서 서버를 데이터베이스 역할별로 그룹화한다. primary, replica, 백업 서버를 별도 그룹에 둔다. 정적 INI/YAML 인벤토리 또는 클라우드용 동적 인벤토리(aws_ec2, gcp_compute)를 사용한다. 그룹 변수는 group_vars/ 아래에, 호스트별 변수는 host_vars/ 아래에 둔다.","engines":{"postgres":"[pg_primary]\ndb1.example.local\n\n[pg_replica]\ndb2.example.local\n\n[postgres:children]\npg_primary\npg_replica"}},{"title":"올바른 collection을 설치한다(community.postgresql / community.mysql / community.mongodb)","detail":"현재 Ansible core는 데이터베이스 모듈을 별도 collection으로 제공한다. requirements.yml로 사용하는 엔진의 collection을 설치한다. PostgreSQL에는 community.postgresql(postgresql_db, postgresql_user, postgresql_privs), MySQL/MariaDB에는 community.mysql(mysql_db, mysql_user), MongoDB에는 community.mongodb를 사용한다. 대상 호스트에는 해당 Python 클라이언트(psycopg2, PyMySQL, pymongo)가 설치되어 있어야 한다.","engines":{"postgres":"# requirements.yml\ncollections:\n  - name: community.postgresql\n\n# install:\nansible-galaxy collection install -r requirements.yml","mysql":"collections:\n  - name: community.mysql","mongodb":"collections:\n  - name: community.mongodb"}},{"title":"Ansible Vault로 비밀 정보를 암호화한다","detail":"데이터베이스 비밀번호, 복제 비밀, superuser 자격 증명을 평문으로 두지 않고 ansible-vault로 암호화된 변수 파일에 보관한다. 실행 시 --ask-vault-pass 또는 --vault-password-file로 복호화한다. 이렇게 하면 비밀 정보를 Git에 안전하게 저장할 수 있고 playbook은 멱등성을 유지한다.","engines":{"postgres":"ansible-vault create group_vars/postgres/vault.yml\n# contents:\nvault_pg_admin_password: \"S3cret!\"\n\n# run:\nansible-playbook site.yml --ask-vault-pass"}},{"title":"role 구조를 구성한다(roles/postgres, roles/mysql)","detail":"엔진별로 role을 하나씩 만들고 로직을 tasks/, defaults/, templates/, handlers/ 하위 폴더로 나눈다. 이 구조는 재사용성을 높인다: 기본값은 defaults/main.yml, jinja2 구성 템플릿은 templates/, '변경이 있을 때만 재시작' 로직은 handlers/. ansible-galaxy init roles/postgres로 role 골격을 만들 수 있다.","engines":{"postgres":"ansible-galaxy init roles/postgres\n# roles/postgres/\n#   defaults/main.yml\n#   tasks/main.yml\n#   templates/postgresql.conf.j2\n#   handlers/main.yml"}},{"title":"설치 및 구성 task를 멱등적으로 작성한다","detail":"패키지를 설치하고, 데이터 디렉터리를 초기화하며, 템플릿으로 postgresql.conf/my.cnf를 렌더링한다. 구성이 바뀌면 handler로 서비스를 재시작한다. 모듈은 멱등적이다: 이미 설치된 패키지는 재설치되지 않고, 파일이 바뀌지 않으면 handler가 트리거되지 않는다. template 모듈로 구성 템플릿을 렌더링하고 'notify'로 handler에 연결한다.","engines":{"postgres":"- name: postgresql.conf template\n  ansible.builtin.template:\n    src: postgresql.conf.j2\n    dest: /etc/postgresql/16/main/postgresql.conf\n  notify: restart postgresql\n\n# handlers/main.yml\n- name: restart postgresql\n  ansible.builtin.service:\n    name: postgresql\n    state: restarted","mysql":"- name: my.cnf template\n  ansible.builtin.template:\n    src: my.cnf.j2\n    dest: /etc/mysql/my.cnf\n  notify: restart mysql"}},{"title":"데이터베이스 및 사용자 task를 정의한다","detail":"모듈로 데이터베이스, 사용자, 권한을 생성하고 비밀번호는 Vault 변수에서 가져온다. community.postgresql 모듈은 멱등적이다: 사용자/DB가 존재하면 다시 만들지 않고 차이만 적용한다. 권한은 postgresql_privs로 별도 부여하며, MySQL에서는 mysql_user의 priv 필드를 사용한다.","engines":{"postgres":"- name: application database\n  community.postgresql.postgresql_db:\n    name: appdb\n    state: present\n\n- name: application user\n  community.postgresql.postgresql_user:\n    name: app_user\n    password: \"{{ vault_app_password }}\"\n    db: appdb\n    state: present","mysql":"- name: application user\n  community.mysql.mysql_user:\n    name: app_user\n    password: \"{{ vault_app_password }}\"\n    priv: 'appdb.*:ALL'\n    state: present"}},{"title":"백업 task와 스케줄을 추가한다","detail":"템플릿으로 백업 스크립트를 배포하고 ansible.builtin.cron으로 스케줄링한다. 그러면 백업 작업도 코드로 관리된다. 백업 대상 디렉터리, 보관 기간, 스케줄을 변수로 만든다. cron 모듈은 멱등적이며 'name'으로 항목을 식별하므로 재실행해도 중복되지 않는다.","engines":{"postgres":"- name: nightly backup (cron)\n  ansible.builtin.cron:\n    name: \"pg dump nightly\"\n    minute: \"0\"\n    hour: \"2\"\n    job: \"pg_dump -Fc appdb > /backup/appdb_$(date +\\\\%F).dump\""}}],"related":["schedule-job","check-version","logical-dump"]},{"id":"auto-terraform","title":"Terraform을 이용한 데이터베이스 프로비저닝(IaC)","icon":"🏗️","symptom":"목표: Terraform으로 데이터베이스 인프라(인스턴스, parameter group, 네트워크)를 코드로 정의하고(Infrastructure as Code), plan/apply 흐름, remote state, drift 관리를 구성한다.","steps":[{"title":"provider를 선택하고 구성한다(aws / google / azurerm)","detail":"사용하는 클라우드의 provider를 정의하고 버전을 고정한다. required_providers 블록에서 AWS는 hashicorp/aws, GCP는 hashicorp/google, Azure는 hashicorp/azurerm provider를 고정한다. 버전 고정은 팀 전체에서 일관된 동작을 보장한다.","engines":{"postgres":"terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 5.0\"\n    }\n  }\n}\n\nprovider \"aws\" {\n  region = \"eu-central-1\"\n}"}},{"title":"remote state와 locking을 구성한다","detail":"state 파일을 로컬에 두지 말고 팀 작업과 보안을 위해 remote backend를 사용한다. 일반적인 선택은 AWS의 S3 backend + DynamoDB lock 테이블, GCP의 gcs backend, Azure의 azurerm backend(Storage Account)다. remote state는 locking으로 동시 apply를 막고 state를 단일 진실 공급원으로 만든다. state에 비밀번호가 평문으로 남을 수 있으므로 backend는 암호화하고 접근을 제한한다.","engines":{"postgres":"terraform {\n  backend \"s3\" {\n    bucket         = \"company-tfstate\"\n    key            = \"db/terraform.tfstate\"\n    region         = \"eu-central-1\"\n    dynamodb_table = \"tf-lock\"\n    encrypt        = true\n  }\n}"}},{"title":"데이터베이스 리소스를 정의한다","detail":"엔진과 클라우드에 맞는 주 리소스를 작성한다: AWS는 aws_db_instance(Aurora는 aws_rds_cluster), GCP는 google_sql_database_instance, Azure는 azurerm_mssql_database(SQL Server용, azurerm_mssql_server 아래). 여기서 엔진, 버전, 클래스, 스토리지, 백업 시간대를 지정한다.","engines":{"postgres":"resource \"aws_db_instance\" \"pg\" {\n  identifier        = \"app-db\"\n  engine            = \"postgres\"\n  engine_version    = \"16\"\n  instance_class    = \"db.t3.medium\"\n  allocated_storage = 50\n  db_name           = \"appdb\"\n  username          = \"pgadmin\"\n  password          = var.db_password\n  backup_retention_period = 7\n}","mssql":"resource \"azurerm_mssql_database\" \"db\" {\n  name      = \"appdb\"\n  server_id = azurerm_mssql_server.srv.id\n  sku_name  = \"S1\"\n}"}},{"title":"parameter group을 코드로 작성한다","detail":"데이터베이스 설정(max_connections, work_mem, 문자 집합)을 콘솔에서 수동으로 하지 말고 파라미터 리소스로 정의한다. AWS는 aws_db_parameter_group, GCP는 google_sql_database_instance 안의 settings.database_flags, Azure는 server configuration 리소스를 사용한다. 그러면 설정이 버전 관리되고 재현 가능해진다.","engines":{"postgres":"resource \"aws_db_parameter_group\" \"pg\" {\n  name   = \"app-pg16\"\n  family = \"postgres16\"\n\n  parameter {\n    name  = \"max_connections\"\n    value = \"200\"\n  }\n}\n# inside aws_db_instance:\n# parameter_group_name = aws_db_parameter_group.pg.name"}},{"title":"비밀 정보는 변수와 secrets manager로 전달한다","detail":".tf 파일에 비밀번호를 하드코딩하지 말고 sensitive 변수(variable ... sensitive = true)로 정의하며, 값은 TF_VAR_ 환경 변수, secrets manager(AWS Secrets Manager, Vault) 또는 CI secret으로 제공한다. 주의: 값은 state 파일에 기록되므로 remote state는 암호화되어야 한다. 가장 깔끔한 방법은 비밀번호를 Terraform에 전달하지 않고 secrets manager 리소스에서 읽는 것이다.","engines":{"postgres":"variable \"db_password\" {\n  type      = string\n  sensitive = true\n}\n\n# CI/shell:\nexport TF_VAR_db_password=\"$(vault kv get -field=pw secret/db)\""}},{"title":"plan과 apply 흐름을 실행한다","detail":"먼저 terraform init으로 provider/backend를 준비하고, terraform plan으로 무엇이 바뀔지 확인한 뒤, 승인하고 terraform apply로 적용한다. plan 출력을 검토하지 않고 apply하지 않는다. CI에서 안전한 흐름은 'plan'을 PR 코멘트로 게시하고 사람의 승인 후 apply하는 것이다. 파괴적 변경(예: 인스턴스 재생성)은 plan 출력의 -/+ 표시로 잡아낸다.","engines":{"postgres":"terraform init\nterraform plan -out=plan.tfplan\nterraform apply plan.tfplan"}},{"title":"drift를 감지하고 수정한다","detail":"누군가 콘솔에서 수동으로 변경하면 실제 인프라와 state가 어긋난다(drift). '아무것도 바뀌지 않았어야 할 때' terraform plan에 나타나는 차이가 drift를 드러내며, 차이는 terraform refresh/plan으로 확인한다. 변경을 코드에 반영하거나 apply로 인프라를 코드 상태로 되돌린다. 원칙적으로 수동 변경을 금지하고 코드를 단일 진실 공급원으로 삼는다.","engines":{"postgres":"# drift check (when no changes are expected):\nterraform plan\n# expect 'No changes'; any diff means drift\n# bring an existing resource into state with import:\nterraform import aws_db_instance.pg app-db"}}],"related":["check-version","apply-patch","schedule-job"]},{"id":"auto-k8s-operator","title":"Kubernetes 위의 데이터베이스(operator)","icon":"☸️","symptom":"목표: operator로 Kubernetes에서 데이터베이스를 실행한다. 순수 StatefulSet 대신 operator가 제공하는 자동 페일오버, 영구 스토리지, 백업 기능을 사용한다.","steps":[{"title":"왜 operator인가: StatefulSet만으로는 부족하다","detail":"일반 StatefulSet은 안정적인 ID와 PVC를 가진 pod를 제공하지만 페일오버, 복제 구성, 백업, 업그레이드 로직은 제공하지 않는다. operator는 이러한 데이터베이스별 운영 지식을 controller + custom resource(CRD)로 패키징한다. 'Cluster'라는 CR을 작성하면 operator가 그 뒤의 StatefulSet, Service, Secret, 백업 작업을 관리한다. 운영 환경에서는 일반적으로 operator로 데이터베이스를 실행하는 것이 선호된다.","engines":{"postgres":"# operators for PostgreSQL:\n#  - CloudNativePG (CNPG)\n#  - Zalando postgres-operator\n#  - Crunchy PGO"}},{"title":"operator를 선택하고 설치한다","detail":"엔진에 맞는 operator를 선택한다: PostgreSQL에는 CloudNativePG 또는 Zalando postgres-operator, MySQL과 MongoDB에는 Percona operator(Percona Operator for MySQL / for MongoDB)가 일반적이다. Helm 또는 bundle manifest로 operator를 설치한다. 이 단계에서 CRD와 controller pod가 클러스터에 추가된다.","engines":{"postgres":"# CloudNativePG installation\nkubectl apply --server-side -f \\\n  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-1.24.0.yaml","mysql":"# Percona Operator for MySQL (Helm)\nhelm install my-op percona/pxc-operator","mongodb":"# Percona Operator for MongoDB (Helm)\nhelm install psmdb-op percona/psmdb-operator"}},{"title":"스토리지를 계획한다(StorageClass / PVC)","detail":"데이터베이스 상태는 PVC에 저장된다. CR에서 사용할 StorageClass를 지정한다. 지연이 낮은 블록 스토리지, 이상적으로는 로컬에 가까운 SSD를 제공하는 StorageClass를 선택한다. 네트워크 파일 시스템(NFS)은 대부분의 데이터베이스에 권장되지 않는다. 축소는 어려우므로 PVC 크기와 확장 가능성(allowVolumeExpansion)은 처음부터 올바르게 설정한다.","engines":{"postgres":"spec:\n  storage:\n    storageClass: fast-ssd\n    size: 50Gi"}},{"title":"Cluster CR을 적용한다(replica와 자동 페일오버)","detail":"CR에서 인스턴스/replica 수를 지정한다. operator는 동기/비동기 복제를 구성하고, primary가 장애 나면 replica를 승격하여 자동 페일오버를 수행한다. 애플리케이션은 operator가 생성한 '-rw'(read-write) Service에 연결해야 하며, 이 서비스는 항상 현재 primary를 가리킨다. 따라서 페일오버 중에도 연결 대상이 바뀌지 않는다.","engines":{"postgres":"apiVersion: postgresql.cnpg.io/v1\nkind: Cluster\nmetadata:\n  name: app-db\nspec:\n  instances: 3\n  storage:\n    storageClass: fast-ssd\n    size: 50Gi\n# the application connects to: app-db-rw"}},{"title":"CR로 S3에 백업을 기록한다","detail":"operator는 백업도 선언적으로 관리한다: CR에 기본 백업 대상(오브젝트 스토리지/S3)과 WAL 아카이빙을 정의한다. CloudNativePG에서는 spec.backup.barmanObjectStore로 S3 정보를 지정한다(1.26부터 deprecated, 새 배포에는 Barman Cloud Plugin과 ObjectStore 리소스 사용). 연속 WAL 아카이빙 + base backup으로 시점 복구가 가능하다. 자격 증명은 Kubernetes Secret에서 참조한다.","engines":{"postgres":"spec:\n  backup:\n    barmanObjectStore:\n      destinationPath: s3://company-backup/app-db\n      s3Credentials:\n        accessKeyId:\n          name: s3-creds\n          key: ACCESS_KEY_ID\n        secretAccessKey:\n          name: s3-creds\n          key: SECRET_ACCESS_KEY"}},{"title":"예약 백업 및 복원 CR","detail":"일회성 백업에는 Backup CR을, 예약 백업에는 ScheduledBackup CR을 생성하고 cron 식으로 정기 base backup을 수행한다. 복원도 선언적이다: 'bootstrap.recovery'로 기존 오브젝트 스토리지 백업에서 새 Cluster를 bootstrap한다. 복원 훈련을 정기적으로 실행한다. 백업만으로는 부족하며 복구를 테스트해야 한다.","engines":{"postgres":"apiVersion: postgresql.cnpg.io/v1\nkind: ScheduledBackup\nmetadata:\n  name: nightly-backup\nspec:\n  schedule: \"0 0 2 * * *\"\n  cluster:\n    name: app-db"}}],"related":["setup-cdc-replication","logical-dump","restore-dump"]},{"id":"auto-schema-migration","title":"스키마 마이그레이션 CI/CD(Flyway/Liquibase)","icon":"🧬","symptom":"목표: 버전이 지정된 마이그레이션 파일로 데이터베이스 스키마 변경을 관리하고, Flyway 또는 Liquibase로 CI/CD 안에서 자동이지만 통제된 방식으로 적용한다.","steps":[{"title":"버전이 지정된 마이그레이션 파일을 만든다(Vxxx__name.sql)","detail":"모든 스키마 변경을 증가하는 버전 번호가 붙은 이름의 파일에 작성한다. Flyway 규칙은 V<version>__<description>.sql이다(예: V1__initial_schemas.sql, V2__orders_table.sql). 이 파일들은 Git에 보관되며 버전 순서로 적용된다. 적용된 마이그레이션은 추적 테이블(Flyway의 flyway_schema_history)에 기록되므로 각 마이그레이션은 정확히 한 번 실행된다.","engines":{"postgres":"-- V2__orders_table.sql\nCREATE TABLE orders (\n  id       bigserial PRIMARY KEY,\n  amount    numeric(12,2) NOT NULL,\n  created_at   timestamptz DEFAULT now()\n);"}},{"title":"checksum으로 불변성을 유지한다","detail":"Flyway/Liquibase는 적용된 모든 마이그레이션의 checksum을 저장한다. 이미 적용된 파일을 나중에 수정하면 checksum이 일치하지 않아 도구가 실패한다. 규칙: 적용된 마이그레이션은 절대 수정하지 않으며 수정은 새 버전 파일로 한다. 이를 통해 모든 환경(dev/staging/prod)이 같은 이력을 공유함을 보장한다.","engines":{"postgres":"# repair on checksum mismatch (use with care):\nflyway repair\n# the right way: add the change in a new Vx file"}},{"title":"기존 데이터베이스를 baseline한다","detail":"이력이 없는 이미 데이터가 있는 데이터베이스에 마이그레이션 도구를 연결할 때 baseline을 사용한다. 현재 상태를 '시작 버전'으로 간주하며 기존 객체를 다시 만들려 하지 않는다. baselineVersion 이후의 마이그레이션이 적용된다. 처음부터 구축하는 환경에서는 baseline이 필요 없으며 V1부터 마이그레이션이 실행된다.","engines":{"postgres":"flyway baseline -baselineVersion=1 -baselineDescription=\"existing schema\""}},{"title":"CI 파이프라인에서 자동으로 migrate한다","detail":"CI 단계에서 마이그레이션 도구를 실행해 테스트 및 staging 데이터베이스에 변경을 자동 적용한다. migrate 명령은 새로운(적용되지 않은) 파일만 실행하며 멱등적이다. 마이그레이션을 먼저 임시/ephemeral 데이터베이스에 적용해 검증하면 깨진 SQL이 prod에 도달하기 전에 잡을 수 있다. 스키마는 애플리케이션 코드 배포 전에 마이그레이션해야 한다(하위 호환 변경으로).","engines":{"postgres":"# CI step (Flyway)\nflyway -url=jdbc:postgresql://staging/appdb \\\n  -user=ci -password=$PW migrate","mysql":"flyway -url=jdbc:mysql://staging/appdb \\\n  -user=ci -password=$PW migrate"}},{"title":"prod에 수동 승인 게이트를 추가한다","detail":"테스트/staging은 자동이지만 prod migrate는 수동 승인을 요구하도록 한다. CI/CD에 'approval gate'(수동 승인) 단계를 추가한다. 승인 전에 'flyway info' 또는 'liquibase status'로 실행될 마이그레이션을 보여 준다. 문제가 생기면 되돌릴 수 있도록 prod에 적용하기 전에 항상 최신 백업을 확보한다.","engines":{"postgres":"# what will run before approval?\nflyway info\n# liquibase equivalent:\nliquibase status --verbose"}},{"title":"가역성과 롤백 전략을 정의한다","detail":"Flyway Community에는 자동 undo가 없으므로 forward-only 마이그레이션 원칙을 채택한다: 실수를 되돌리는 대신 이를 수정하는 새 마이그레이션을 작성한다. Liquibase changeset은 rollback 블록을 정의할 수 있으며 'liquibase rollback'으로 되돌릴 수 있다. 가장 견고한 전략: 변경을 작고 하위 호환되게 유지하고, 위험한 작업(컬럼 삭제) 전에 백업을 확보하며, 문서화된 롤백 계획을 준비한다.","engines":{"postgres":"-- Liquibase rollback example (inside a changeset)\n--changeset dev:2\nCREATE TABLE orders (...);\n--rollback DROP TABLE orders;"}}],"related":["apply-patch","logical-dump","check-version"]},{"id":"auto-backup","title":"자동 백업(pgBackRest/Barman/Xtrabackup/cron)","icon":"💾","symptom":"목표: 데이터베이스 백업을 자동화한다. 예약된 전체+증분+WAL/binlog 아카이빙, retention, 검증, offsite 사본으로 신뢰할 수 있는 복구 체인을 구축한다.","steps":[{"title":"올바른 백업 도구를 선택한다","detail":"엔진에 맞는 도구를 선택한다: PostgreSQL에는 pgBackRest 또는 Barman(물리 백업 + WAL 아카이브 + PITR), MySQL/MariaDB에는 Percona XtraBackup(온라인 물리 백업), 작은 데이터 세트에는 mysqldump/mydumper(논리). 물리 도구는 대형 데이터베이스에서 빠르고 일관된 백업 + 시점 복구를 제공하며, 논리 덤프는 이식성이 있지만 대용량 데이터에서는 느리다.","engines":{"postgres":"# pgBackRest or Barman","mysql":"# physical: xtrabackup\n# logical: mysqldump / mydumper"}},{"title":"전체 + 증분 + 연속 아카이브 체인을 구축한다","detail":"주간/일일 전체 백업, 그 사이의 증분/차등 백업, 연속 WAL(PostgreSQL) 또는 binlog(MySQL) 아카이빙으로 시점 복구를 구성한다. pgBackRest는 full/diff/incremental 백업 유형과 WAL 아카이브를 관리하며, PostgreSQL에서는 archive_command를 pgBackRest로 지정한다. 증분 백업 + 아카이브로 임의 시점으로 돌아갈 수 있다.","engines":{"postgres":"# postgresql.conf\narchive_mode = on\narchive_command = 'pgbackrest --stanza=appdb archive-push %p'\n\n# full backup\npgbackrest --stanza=appdb --type=full backup\n# incremental\npgbackrest --stanza=appdb --type=incr backup","mysql":"# full\nxtrabackup --backup --target-dir=/backup/full\n# incremental\nxtrabackup --backup --target-dir=/backup/incr1 \\\n  --incremental-basedir=/backup/full"}},{"title":"cron/timer로 백업을 스케줄링한다","detail":"cron 또는 systemd timer로 백업 명령을 정기 실행한다: 예를 들어 일요일 밤 전체 백업, 다른 날 밤 증분 백업. 업무 시간 외로 스케줄링하고 full과 incr 작업이 겹치지 않도록 한다. 출력 로그를 기록하고 실패 시 알림을 발생시킨다.","engines":{"postgres":"# crontab\n0 1 * * 0 pgbackrest --stanza=appdb --type=full backup\n0 1 * * 1-6 pgbackrest --stanza=appdb --type=incr backup"}},{"title":"retention 정책을 정의한다","detail":"백업을 무한히 쌓지 말고 보관할 전체 백업 수와 아카이브 일수를 정해 도구가 오래된 것을 자동으로 만료시키게 한다. pgBackRest에서는 repo1-retention-full과 아카이브 retention 설정으로, Barman에서는 retention_policy로 관리한다. retention은 복구 목표(며칠 전까지 돌아가야 하는지)와 일치해야 한다.","engines":{"postgres":"# pgbackrest.conf\n[global]\nrepo1-retention-full=4\nrepo1-retention-archive=8","mysql":"# purge old backups with cron (example)\nfind /backup -type d -mtime +14 -exec rm -rf {} +"}},{"title":"백업을 offsite / S3로 복사한다","detail":"백업은 데이터베이스와 같은 머신/리전에 남아 있어서는 안 된다. 다른 위치나 오브젝트 스토리지(S3, GCS)로 복사한다. pgBackRest는 S3 호환 스토리지에 직접 기록할 수 있으며, 그렇지 않으면 rsync/rclone으로 offsite 대상에 동기화한다. 재해(로컬 디스크/리전 손실) 시에는 offsite 사본만이 구원이 된다.","engines":{"postgres":"# pgbackrest.conf S3 repository\n[global]\nrepo1-type=s3\nrepo1-s3-bucket=company-backup\nrepo1-s3-region=eu-central-1\nrepo1-s3-endpoint=s3.eu-central-1.amazonaws.com"}},{"title":"백업을 검증한다","detail":"백업이 존재한다고 복원할 수 있는 것은 아니므로 무결성을 정기적으로 검증한다. pgBackRest에서는 'verify' 명령과 아카이브 일관성 검사, XtraBackup에서는 성공적인 prepare(--prepare) 단계가 백업이 정상임을 보여 준다. 매 백업 후 검증을 자동 실행하고 실패 시 알람을 발생시킨다.","engines":{"postgres":"pgbackrest --stanza=appdb verify","mysql":"# make the backup consistent (verification)\nxtrabackup --prepare --target-dir=/backup/full"}},{"title":"정기적으로 복원 훈련을 실행한다","detail":"가장 중요한 단계: 주기적으로 백업을 별도 서버에 실제로 복원하고 데이터베이스가 열리며 데이터가 일관된지 검증한다. 이 훈련으로 복구 시간(RTO)을 측정하고 장애 상황이 아닐 때 절차의 허점을 잡을 수 있다. 테스트하지 않은 백업은 백업이 아니다.","engines":{"postgres":"# restore + PITR on a separate server\npgbackrest --stanza=appdb \\\n  --type=time --target=\"2026-07-01 03:00:00\" restore","mysql":"# copy the prepared backup back to the data directory\nxtrabackup --copy-back --target-dir=/backup/full"}}],"related":["logical-dump","restore-dump","schedule-job"]},{"id":"auto-gitops-bluegreen","title":"GitOps 및 blue-green / 무중단 배포","icon":"🟦","symptom":"목표: 다운타임 없이 스키마와 애플리케이션 변경을 배포한다. expand-contract 마이그레이션, blue-green 환경, feature flag, 자동 롤백으로 무중단에 근접한다.","steps":[{"title":"GitOps: Git을 단일 진실 공급원으로 만든다","detail":"인프라, 스키마 마이그레이션, 애플리케이션 버전을 Git에 정의한다. 배포는 저장소로의 merge로 트리거된다(GitOps). reconciler(Argo CD/Flux 또는 CI/CD)가 원하는 상태를 실제 환경에 적용한다. 그러면 모든 변경은 감사 가능하고 반복 가능하며 필요시 이전 커밋으로 되돌릴 수 있다.","engines":{"postgres":"# repo/\n#   migrations/  (Vxxx__*.sql)\n#   k8s/         (Deployment, Service)\n#   flags/       (feature flag config)"}},{"title":"스키마를 하위 호환되게 변경한다(expand-contract)","detail":"무중단을 위해 스키마를 한 번에 깨지는 방식으로 바꾸지 말고 두 단계로 나눈다. EXPAND: 구 코드와 신 코드가 동시에 실행될 수 있도록 새 구조(새 컬럼/테이블)를 추가한다(nullable 컬럼, 기본값 포함, 이중 쓰기). 애플리케이션이 새 구조로 이전한 후 CONTRACT: 더 이상 쓰지 않는 이전 컬럼/테이블을 별도 마이그레이션에서 삭제한다. 그러면 배포 중 구·신 애플리케이션 버전이 문제없이 나란히 실행된다.","engines":{"postgres":"-- EXPAND (V5): backward-compatible addition\nALTER TABLE customer ADD COLUMN email text;\n\n-- for a while the code writes/reads both columns\n\n-- CONTRACT (V9): drop the old column\nALTER TABLE customer DROP COLUMN mail_old;"}},{"title":"위험한 동작은 feature flag 뒤에 둔다","detail":"새 코드 경로(예: 새 컬럼 읽기)를 feature flag 뒤에서 조건부로 실행한다. 스키마가 준비되면 flag를 켜고, 문제가 생기면 배포를 건드리지 않고 끈다. flag는 코드 배포(deploy)와 기능 활성화(release)를 분리한다. expand 단계는 배포되지만 동작 변경은 통제된 방식으로 활성화되며 빠르게 되돌릴 수 있다.","engines":{"postgres":"-- application layer (pseudocode)\n-- if flag('new_email_field'):\n--     SELECT email FROM customer ...\n-- else:\n--     SELECT mail_old FROM customer ..."}},{"title":"blue-green 환경을 구성한다","detail":"동일한 두 환경을 유지한다: 'blue'(운영)와 'green'(새 버전). 새 애플리케이션 버전을 green에 배포하고 실제 트래픽을 받기 전에 검증한다. 준비되면 라우터/트래픽을 green으로 전환한다. 핵심: 데이터베이스는 대개 공유되므로 스키마는 두 버전 모두와 호환되어야 한다(expand-contract가 이를 보장). 전환 시 연결 대상이 바뀌어도 스키마가 호환되므로 다운타임이 없다.","engines":{"postgres":"# traffic is switched to green (new version)\n# shared DB: schema must be compatible with both blue and green"}},{"title":"모니터링으로 배포를 검증한다(canary/관찰)","detail":"전체 트래픽을 전환하기 전에 오류율, 지연, 데이터베이스 오류/락 수, 복제 지연 같은 지표를 모니터링한다. 선택적으로 트래픽의 일부(canary)를 green으로 보내 관찰한다. 상태 지표가 양호하면 트래픽을 점진적으로 늘리고, 나쁘면 진행하지 않는다. 모니터링은 자동 롤백 결정의 입력이다.","engines":{"postgres":"-- live health queries\nSELECT count(*) FROM pg_stat_activity WHERE wait_event_type='Lock';\nSELECT * FROM pg_stat_replication;"}},{"title":"자동 롤백을 준비한다","detail":"지표가 임계값을 넘으면 배포는 자동으로 롤백되어야 한다: blue-green에서는 트래픽을 빠르게 blue(이전 버전)로 되돌린다. blue가 여전히 실행 중이므로 애플리케이션 롤백은 즉시 이루어진다. 스키마 측에서는 대개 롤백이 필요 없다: expand-contract 덕분에 새 스키마가 이전 코드와도 호환되므로 애플리케이션 롤백만으로 충분하다. 그래서 스키마 변경은 애플리케이션 배포와 독립적이어야 하며 항상 하위 호환되어야 한다.","engines":{"postgres":"# traffic instantly returns to blue (application rollback)\n# schema is not rolled back: expand-contract is compatible with old code\n# if truly needed: write a new corrective migration"}}],"related":["setup-cdc-replication","apply-patch","check-version"]}]},"install/index.json":{"schemaVersion":1,"categories":[{"id":"install","title":"설치"},{"id":"ha","title":"고가용성 & 복제"},{"id":"backup","title":"백업 & 복구"},{"id":"ops","title":"일상 운영 & 장애"},{"id":"pooling","title":"연결 & 로드 밸런싱"},{"id":"patch","title":"패치 & 마이너 업데이트"},{"id":"upgrade","title":"메이저 업그레이드 & 마이그레이션"},{"id":"monitoring","title":"모니터링 & 관리"},{"id":"security","title":"보안 & 하드닝"}],"guides":[{"id":"pg-install","file":"install/guides/pg-install.json","group":"PostgreSQL","engineId":"postgres","category":"install","title":"PostgreSQL 단일 서버 설치(운영 환경용)","summary":"PGDG 저장소에서 단일 서버에 운영 환경용 PostgreSQL을 설치합니다: 별도 데이터 디렉터리, data checksums, RAM/CPU 기준으로 계산한 설정, 안전한 접근(peer + scram-sha-256), 로깅, pg_stat_statements, 방화벽, SELinux. 완료되면 실행 중이며 검증된 클러스터와 백업/모니터링 체크리스트를 얻게 됩니다.","level":"초급","duration":"30–45분","topology":"서버 1대","tags":["postgresql","postgres","설치","install","pgdg","initdb","tuning","rhel","ubuntu"],"order":1,"steps":23,"scenarios":[]},{"id":"pg-streaming","file":"install/guides/pg-streaming.json","group":"PostgreSQL","engineId":"postgres","category":"ha","title":"PostgreSQL streaming replication (primary + standby)","summary":"설치된 primary에 pg_basebackup으로 하나 이상의 hot standby를 추가하고(복제 사용자, pg_hba, replication slot, 비동기 또는 동기 ANY/FIRST 모드, 지연 모니터링), Patroni 없는 이 클러스터의 운영 런북을 생성합니다: 계획된 switchover, 수동 failover(pg_promote), 이전 primary를 pg_rewind로 재","level":"중급","duration":"45–90분","topology":"primary 1대 + standby 1..N대","tags":["postgresql","streaming replication","복제","standby","hot standby","pg_basebackup","replication slot","synchronous","동기 복제","failover","switchover","pg_promote","pg_rewind","ha","cascading","delayed standby","recovery_min_apply_delay","slot 정리","pg_wal 가득 참","synchronous_standby_names","standby 재구축"],"order":2,"steps":24,"scenarios":[{"k":"mode","v":"install","l":"설치: primary에 standby 추가","m":"Standby 추가 / 재구축 streaming replication 구성 standby 추가 hot standby pg_basebackup read replica 읽기 전용 복제본 physical replication 물리 복제 standby.signal primary_conninfo 설치된 primary에 pg_basebackup으로 하나 이상의 hot standby를 추가합니다(slot, 동기/비동기, 모니터링). Primary는 계속 열려 있으며, 초기 복사는 primary에 디스크와 네트워크 부하를 줍니다."},{"k":"mode","v":"switchover","l":"계획된 switchover (역할 전환, 데이터 손실 없음)","m":"역할 전환 switchover planned switchover 역할 전환 계획된 역할 교체 primary 유지보수 standby를 primary로 전환 pg_promote pg_ctl promote 계획된 유지보수: primary를 정상 종료하고 첫 번째 standby를 promote하며, 이전 primary는 standby로 돌아옵니다. 데이터 손실은 없고 쓰기 중단은 보통 1분 미만입니다."},{"k":"mode","v":"failover","l":"수동 failover (primary 장애, standby promote)","m":"역할 전환 could not connect to the primary server Connection refused primary down primary 장애 primary 접근 불가 failover manual failover promote standby pg_promote split-brain could not receive data from WAL stream primary 서버 유실 Primary가 다운되었거나 접근할 수 없음: 이전 primary를 격리(fencing)하고 가장 최신 standby를 수동으로 promote합니다. 비동기 모드에서는 마지막 트랜잭션이 유실될 수 있습니다."},{"k":"mode","v":"rejoin","l":"이전 primary를 pg_rewind로 standby로 재합류","m":"역할 전환 requested timeline is not a child of this server's history target server must be shut down cleanly could not find previous WAL record pg_rewind diverged timeline 이전 primary 재합류 rejoin old primary Failover 후 이전 primary를 pg_rewind로 새 primary와 동기화하여 standby로 재합류시킵니다. 전체 복사보다 훨씬 빠릅니다."},{"k":"mode","v":"cascade","l":"standby에서 WAL을 받는 새 standby 추가 (cascading)","m":"Standby 추가 / 재구축 cascading replication cascade standby 연쇄 복제 standby에서 standby로 원격 데이터센터 DR standby 새 standby가 WAL을 primary 대신 첫 번째 standby에서 받습니다. 원격 데이터센터나 standby가 많은 구성에서 primary의 네트워크/WAL 전송 부하가 줄어듭니다. Primary에는 영향이 없습니다."},{"k":"mode","v":"delayed","l":"지연(delayed) standby 구성","m":"Standby 추가 / 재구축 recovery_min_apply_delay delayed standby 지연 standby 실수로 삭제 DROP TABLE 되돌리기 accidental delete pg_wal_replay_pause WAL을 몇 시간 늦게 적용하는 standby(`recovery_min_apply_delay`): 실수로 삭제된 데이터를 백업에서 복원하지 않고 복구하기 위한 용도입니다. Failover 후보가 아닙니다."},{"k":"mode","v":"rebuild","l":"standby를 처음부터 재구축","m":"Standby 추가 / 재구축 requested WAL segment has already been removed could not receive data from WAL stream database system identifier differs wal_status lost exceeds max_slot_wal_keep_size standby 재구축 standby 손상 re-clone standby could not start WAL streaming wal_removed standby 뒤처짐 Standby가 뒤처졌거나, 필요한 WAL이 삭제되었거나, 손상됨: 데이터 디렉터리를 옆으로 옮기고 primary에서 다시 복사합니다. Primary에는 영향이 없습니다."},{"k":"mode","v":"slotclean","l":"pg_wal 증가, 디스크 가득 참 (사용하지 않는 slot 정리)","m":"유지보수 및 문제 해결 No space left on device could not write to file \"pg_wal pg_wal 가득 참 pg_wal 증가 WAL 비대화 디스크 가득 참 replication slot inactive max_slot_wal_keep_size pg_wal 디렉터리 큼 WAL disk full WAL 누적 원인(비활성 slot, 아카이브 오류, wal_keep_size)을 찾고, 사용하지 않는 replication slot을 삭제하여 WAL 공간을 회수합니다. Primary는 계속 열려 있습니다."},{"k":"mode","v":"syncchg","l":"동기 standby 추가 / 제거 (synchronous_standby_names)","m":"유지보수 및 문제 해결 canceling wait for synchronous replication synchronous_standby_names COMMIT 대기 상태 commit hangs sync standby 동기 standby sync_state potential synchronous_commit 동기 목록을 폼의 모드(ANY/FIRST, k)에 따라 다시 쓰거나 비동기로 되돌립니다. reload로 적용되며 restart는 필요 없습니다. 잘못된 설정은 commit을 대기 상태로 만들 수 있으므로 먼저 점검합니다."}]},{"id":"pg-patroni","file":"install/guides/pg-patroni.json","group":"PostgreSQL","engineId":"postgres","category":"ha","title":"Patroni + etcd + HAProxy 기반 자동 failover 클러스터","summary":"자동 failover를 갖춘 PostgreSQL 클러스터를 구축하고 운영합니다. 각 노드의 PostgreSQL은 Patroni가 관리하고, leader 선출은 3(또는 5)개 멤버로 구성된 etcd에서 이루어지며, 애플리케이션은 HAProxy의 쓰기(5000)·읽기(5001) 포트에 선택적으로 keepalived VIP를 통해 접속합니다. 구축 외에도 운영 중인 클러스터를 위한 시나리오가 있","level":"고급","duration":"2~3시간","topology":"PostgreSQL 3대 + etcd 노드(공용 또는 별도) + HAProxy(+ VIP)","tags":["postgresql","patroni","etcd","haproxy","keepalived","vip","자동 failover","auto failover","switchover","ha","high availability","cluster","클러스터","patronictl","dcs","reinit","pg_rewind","edit-config","pause","resume","etcdctl member","quorum","failsafe","노드 추가","노드 제거","patroni upgrade","클러스터 중지"],"order":3,"steps":40,"scenarios":[{"k":"mode","v":"install","l":"구축: 신규 Patroni 클러스터","m":"구축 patroni 설치 patroni install etcd 설치 haproxy 자동 failover automatic failover high availability 고가용성 HA cluster etcd + Patroni + HAProxy(+ VIP) 클러스터를 처음부터 구축합니다. 서버에 PostgreSQL 클러스터가 있으면 안 됩니다. initdb는 Patroni가 수행합니다(pg-install은 적용하지 않습니다)."},{"k":"mode","v":"ops","l":"운영 runbook: switchover, failover 테스트, 유지 관리 모드, 파라미터 변경","m":"점검 및 훈련 failover 테스트 failover test 장애 훈련 DR drill runbook 새로 구축한 클러스터에서 기본 작업을 순서대로 시험합니다: 상태 확인, switchover, failover 테스트(leader 중지, 짧은 쓰기 중단), 유지 관리 모드, 파라미터 변경. 유지 관리 시간대에 수행하십시오."},{"k":"mode","v":"health","l":"클러스터가 정상인가? (상태 점검)","m":"점검 및 훈련 patronictl list Pending restart Maintenance mode: on start failed patroni health 클러스터 상태 replica lag 복제 지연 timeline 불일치 읽기 전용: 역할, 지연, timeline, REST API, etcd 상태, 슬롯, 대기 중인 재시작 및 유지 관리 모드."},{"k":"mode","v":"switchover","l":"계획된 switchover (leader를 다른 노드로 이동)","m":"역할 전환 candidate name does not match with sync_standby Switchover failed does not exist in cluster or is tagged as nofailover switchover leader 변경 역할 전환 계획된 유지 관리 유지 관리 전에 leader 역할을 통제된 방식으로 replica에 넘깁니다. 몇 초간 쓰기가 중단되며 데이터 손실은 없습니다."},{"k":"mode","v":"rejoin","l":"Failover 후 이전 leader가 재합류하지 않음 (pg_rewind / reinit)","m":"역할 전환 requested timeline is not a child of this server's history could not find previous WAL record pg_rewind: error target server needs to use either data checksums or wal_log_hints start failed diverged 이전 leader failover 이후 pg_rewind: fatal could not find common ancestor timeline history mismatch 장애가 난 이전 leader는 기동되면 replica로 합류해야 합니다. pg_rewind가 실패하면 데이터 디렉터리를 leader로부터 다시 구성합니다."},{"k":"mode","v":"reinit","l":"replica를 처음부터 다시 구성 (reinit)","m":"멤버 유지 관리 requested WAL segment has already been removed could not receive data from WAL stream invalid record length invalid page patronictl reinit replica 손상 replica 지연 rebuild replica replica start failed creating replica replica가 손상되었거나 크게 뒤처졌거나 WAL을 잃은 경우: 데이터 디렉터리를 삭제하고 leader(또는 백업)로부터 다시 구성합니다. leader는 영향을 받지 않습니다."},{"k":"mode","v":"addmember","l":"클러스터에 새 노드 추가","m":"멤버 유지 관리 노드 추가 add member 신규 replica scale out 클러스터 확장 새 노드를 양식의 목록 끝에 추가하십시오. 소프트웨어가 설치되고, 기존 노드의 pg_hba와 HAProxy가 갱신되며, replica로 합류합니다."},{"k":"mode","v":"rmmember","l":"클러스터에서 노드 제거","m":"멤버 유지 관리 노드 제거 remove member decommission 서버 폐기 replica 제거 replica인 노드를 영구적으로 제거합니다. Patroni를 중지하고, DCS 기록과 슬롯을 정리하며, HAProxy에서 제외합니다."},{"k":"mode","v":"editcfg","l":"PostgreSQL 또는 Patroni 설정 변경 (edit-config)","m":"설정 및 유지 관리 edit-config Pending restart postgresql.conf 설정이 사라짐 ALTER SYSTEM 파라미터 변경 max_connections shared_buffers change parameter work_mem 파라미터 사라짐 설정이 적용되지 않음 설정은 DCS에 기록되어 모든 노드에 적용됩니다. 재시작이 필요한 설정은 `restart --pending`(replica부터)으로 반영합니다."},{"k":"mode","v":"pause","l":"유지 관리를 위해 자동 failover 중지 (pause / resume)","m":"설정 및 유지 관리 Maintenance mode: on patronictl pause patronictl resume maintenance mode 유지 관리 모드 자동 failover 끄기 pause 상태에서 Patroni는 PostgreSQL을 건드리지 않고 failover도 하지 않습니다. 네트워크/etcd 유지 관리나 수동 개입 시 사용합니다. 작업이 끝나면 resume합니다."},{"k":"mode","v":"upgrade","l":"Patroni 소프트웨어 업그레이드 (rolling)","m":"설정 및 유지 관리 patroni upgrade patroni 업데이트 patroni 버전 dnf update patroni apt upgrade patroni patroni 3 to 4 클러스터를 pause 상태로 두고 노드별로 Patroni를 업데이트한 뒤 재시작합니다. PostgreSQL은 중지되지 않고 failover도 발생하지 않습니다."},{"k":"mode","v":"stopstart","l":"전체 클러스터 중지 / 기동 (올바른 순서)","m":"설정 및 유지 관리 클러스터 중지 클러스터 기동 shutdown order 전체 중지 계획된 중지 데이터센터 유지 관리 정전 계획된 전체 중지(스토리지, 데이터센터 유지 관리): replica 먼저, 다음 leader, 마지막으로 etcd. 기동은 역순입니다."},{"k":"mode","v":"etcdmember","l":"etcd 멤버 교체 또는 추가 (etcdctl member)","m":"etcd (DCS) etcdserver: unhealthy cluster member has already been bootstrapped Peer URLs already exists member count is unequal etcdctl member etcd member add etcd member remove etcd 멤버 unhealthy 손상된 etcd 멤버를 제거하고 새 서버를 추가합니다(또는 멤버 수를 늘립니다). Patroni의 etcd 목록도 갱신합니다."},{"k":"mode","v":"dcsloss","l":"etcd 과반수 상실: leader가 읽기 전용이 됨","m":"etcd (DCS) Failed to get list of machines DCS is not accessible demoting self etcdserver: request timed out etcdserver: no leader cannot execute INSERT in a read-only transaction quorum failsafe etcd quorum lost leader 없음 no leader etcd quorum이 없으면 Patroni는 leader를 읽기 전용으로 만들고 failover도 일어나지 않습니다. etcd를 복구하거나 단일 멤버에서 다시 구성합니다."}]},{"id":"pg-pgbouncer","file":"install/guides/pg-pgbouncer.json","group":"PostgreSQL","engineId":"postgres","category":"pooling","title":"PgBouncer 커넥션 풀","summary":"PGDG repo에서 PgBouncer를 설치합니다: transaction 또는 session 풀 모드, auth_query(SECURITY DEFINER 함수) 또는 userlist.txt를 이용한 scram-sha-256 인증, 데이터베이스의 max_connections와 CPU 값으로 계산한 풀 크기, systemd와 선택 사항인 so_reuseport 다중 프로세스. 마지막으로 관리 ","level":"중급","duration":"30–45분","topology":"PgBouncer를 데이터베이스 서버 또는 별도 서버에 배치, Patroni에서는 HAProxy/VIP 앞에 배치","tags":["postgresql","pgbouncer","connection pool","커넥션 풀","pooling","transaction pooling","auth_query","userlist","scram-sha-256","so_reuseport","max_client_conn","default_pool_size","patroni","haproxy","vip","failover"],"order":4,"steps":15,"scenarios":[]},{"id":"pg-pgbackrest","file":"install/guides/pg-pgbackrest.json","group":"PostgreSQL","engineId":"postgres","category":"backup","title":"pgBackRest를 이용한 백업과 PITR","summary":"pgBackRest를 이용한 전체 / 차등 / 증분 백업, 연속 WAL 아카이브, 시점 복구(PITR). 단일 서버, 스트리밍 복제(primary + standby) 또는 Patroni 클러스터를 대상으로 하며, 리포지토리는 같은 서버, 별도 백업 서버(SSH) 또는 S3 호환 object storage에 둘 수 있습니다. 암호화, 압축, 보존 정책, cron/systemd 스케줄링, 별도 ","level":"중급","duration":"45–90분","topology":"서버 1대, primary + standby 또는 Patroni 클러스터, 선택적으로 백업 서버","tags":["postgresql","pgbackrest","백업","backup","restore","pitr","wal","archive","s3","minio","암호화","retention","복구","patroni","streaming","standby","cron","backup-standby","systemd timer","delta restore","db-include","target-action","restore point","xid","lsn","clone","복제본 생성","재해","disaster recovery","verify","expire","type=standby","복구 훈련"],"order":10,"steps":28,"scenarios":[{"k":"purpose","v":"kur","l":"pgBackRest 설치 + 첫 백업 + 테스트 restore","m":"설치 stanza-create archive_command archive-push pgbackrest 설치 pgbackrest install 백업 구성 backup setup 백업 일정 full diff incr 예약 백업 ERROR: [087] archive_mode must be enabled WAL 아카이브 구성 첫 백업 pgBackRest가 아직 없는 경우: 소프트웨어, 리포지토리, WAL 아카이브, 첫 백업, 스케줄링, 테스트 restore."},{"k":"purpose","v":"geri","l":"장애 시: 제자리 restore (최신 상태 또는 특정 시점으로)","m":"복원 (같은 서버) recovery_target_time recovery_target_xid recovery_target_lsn pg_create_restore_point PITR point in time recovery 시점 복구 실수로 삭제 DROP TABLE DELETE without WHERE UPDATE without WHERE TRUNCATE restore point restore to time 데이터 되살리기 데이터베이스를 제자리에서, 백업과 아카이브가 포함하는 시점으로 되돌립니다: 최신 시점, 특정 시각, xid, LSN 또는 restore point. 클러스터가 중지되며, 대상 시점 이후의 변경은 사라집니다."},{"k":"purpose","v":"delta","l":"데이터 디렉터리의 파일 손상 또는 삭제 (delta restore로 복구)","m":"복원 (같은 서버) invalid page in block could not read block page verification failed checksum verification failed could not open file \"base/ could not access status of transaction missing chunk number No such file or directory could not read file 손상된 블록 data corruption block corruption 삭제된 데이터 파일 ERROR:  could not read block PANIC:  could not open file PANIC:  could not read file \"global/pg_control\" 서버는"},{"k":"purpose","v":"dbinc","l":"데이터베이스 하나만 복원 (--db-include)","m":"복원 (같은 서버) db-include DROP DATABASE 단일 데이터베이스 single database restore 데이터베이스 삭제됨 테이블 복원 table restore 단일 테이블 restore 클러스터는 계속 운영됩니다: 데이터베이스 하나를 임시 인스턴스(별도 디렉터리와 포트)로 restore한 뒤 pg_dump로 덤프하여 운영 서버에 새 이름으로 적재합니다. 삭제된 테이블 하나를 되살릴 때도 이 카드를 사용합니다(거기서 `pg_dump -t`)."},{"k":"purpose","v":"clone","l":"백업을 다른 서버에 복원 (복제본 / 테스트 / 리포팅)","m":"새 서버: 복제본, 재해, standby repo1-cipher-pass clone 복제본 생성 테스트 환경 테스트 환경 갱신 refresh test staging 새 서버로 restore 리포팅 서버 암호화된 리포지토리 운영 서버는 건드리지 않습니다. 백업을 새 서버에서 엽니다. WAL 아카이브는 비활성화하여 복제본이 운영 리포지토리에 쓰지 않도록 합니다. 암호화된 리포지토리는 키가 필요합니다."},{"k":"purpose","v":"dr","l":"서버 완전 유실: 새 서버에서 백업을 열어 대체","m":"새 서버: 복제본, 재해, standby PANIC: could not locate a valid checkpoint record 재해 disaster recovery DR 서버 유실 디스크 장애 server lost 데이터 디렉터리 유실 새 primary 암호화된 리포지토리 could not open file \"global/pg_control\" 서버가 기동되지 않음 PostgreSQL이 시작되지 않음 디스크 또는 서버 유실: 새 서버를 백업에서 열어 새 primary로 사용하며, WAL 아카이브와 백업은 같은 stanza에서 계속됩니다. 기존 서버는 반드시 중지된 상태로 두어야 합니다."},{"k":"purpose","v":"standby","l":"백업에서 새 standby 구축 (--type=standby)","m":"새 서버: 복제본, 재해, standby type=standby standby 구축 replica 추가 백업에서 standby standby rebuild pg_basebackup 대신 add standby primary에 복사 부하를 주지 않는 새 standby: 파일은 백업에서 가져오고, 부족한 WAL은 아카이브와 스트리밍으로 채웁니다. primary는 계속 운영됩니다."},{"k":"purpose","v":"verify","l":"백업은 정말 온전한가? (info / check / verify와 restore 훈련)","m":"점검과 유지보수 ERROR: [082] ERROR: [028] ERROR: [056] pgbackrest verify pgbackrest check pgbackrest info 백업 검증 backup verification restore test 복구 훈련 마지막 백업 시각 verify command 백업이 온전한지 restore drill 월간 검증 읽기 전용 점검과, 운영 서버를 건드리지 않는 별도 디렉터리로의 restore 훈련. 월간 검증 작업(cron)도 설정합니다."},{"k":"purpose","v":"retention","l":"보존 기간 변경과 오래된 백업 정리 (expire)","m":"점검과 유지보수 No space left on device WARN: option 'repo1-retention-full' is not set repo1-retention-full expire retention 보존 리포지토리 가득 참 백업 디스크 가득 참 오래된 백업 삭제 repo 가득 참 bucket 가득 참 백업 디스크 용량 부족 repo1-retention-full/diff 설정을 변경하고, 먼저 --dry-run으로 삭제될 대상을 보여 준 뒤 expire를 실행합니다. 삭제된 백업은 되돌릴 수 없습니다."}]},{"id":"pg-pitr-basebackup","file":"install/guides/pg-pitr-basebackup.json","group":"PostgreSQL","engineId":"postgres","category":"backup","title":"pg_basebackup + WAL 아카이브로 백업과 PITR (pgBackRest 없이)","summary":"추가 도구 없이 PostgreSQL 자체 도구로 물리 백업을 구성합니다: archive_command로 연속 WAL 아카이브, pg_basebackup으로 야간 base backup, pg_archivecleanup으로 보존 관리. 특정 시점 / 트랜잭션 / restore point로 복구(recovery.signal, restore_command, recovery_target_*), tim","level":"중급","duration":"45–90분","topology":"서버 1대. streaming 또는 Patroni에서는 아카이브 디렉터리를 모든 node가 공유(NFS)","tags":["postgresql","pg_basebackup","archive_command","restore_command","pitr","point in time recovery","recovery.signal","recovery_target_time","pg_archivecleanup","wal 아카이브","timeline","base backup","백업","복구","pg_verifybackup","cron","복원 훈련"],"order":12,"steps":14,"scenarios":[{"k":"op","v":"kur","l":"WAL 아카이브와 야간 base backup 구성","m":"구성 archive_mode archive_command archive_timeout pg_stat_archiver wal_level WAL 아카이브 WAL archiving continuous archiving 연속 아카이빙 pg_basebackup cron base backup 야간 백업 nightly backup wal 아카이브 구성 최초 구성: archive_command로 연속 WAL 아카이브, pg_basebackup 스크립트, /etc/cron.d 예약, 보존과 WAL 정리. archive_mode 때문에 한 번 짧은 재시작이 필요합니다."},{"k":"op","v":"pitr","l":"특정 시점으로 복구 (PITR: 시간, xid, LSN, restore point)","m":"복구 recovery ended before configured recovery target was reached requested recovery stop point is before consistent recovery point PITR point in time recovery point in time recovery.signal restore_command recovery_target_time recovery_target_xid recovery_target_lsn recovery_target_name pg_create_restore_point DROP TABLE TRUNCATE 실수로 삭제 accidental delete 시점 복구 서버 유실 FATAL:  could not locate required"},{"k":"op","v":"drill","l":"복원 훈련 (별도 디렉터리, 운영 환경 무영향)","m":"점검과 유지보수 backup successfully verified pg_verifybackup pg_amcheck restore test restore drill 복원 훈련 백업 검증 backup verify RTO 백업 복원 가능 여부 백업이 실제로 복원되는지 증명합니다(월 1회 권장): 최신 base backup을 검증하고(pg_verifybackup), 별도 디렉터리에 풀어 아카이브와 함께 별도 포트로 시작하며, 소요 시간(RTO)을 측정합니다. 운영 환경은 건드리지 않습니다."},{"k":"op","v":"cleanup","l":"오래된 base backup과 WAL 아카이브 정리","m":"점검과 유지보수 No space left on device could not write to file archive command failed pg_archivecleanup 아카이브 디스크 가득 참 아카이브 가득 참 disk full 오래된 백업 삭제 보존 retention pg_wal 증가 archive_status .ready 아카이브나 백업 디스크가 차거나 보존 정책이 바뀐 경우: 오래된 base backup을 삭제하고, pg_archivecleanup으로 가장 오래된 백업 이전의 WAL을 제거합니다(먼저 dry-run). 데이터베이스는 계속 열려 있습니다."}]},{"id":"pg-logical-repl","file":"install/guides/pg-logical-repl.json","group":"PostgreSQL","engineId":"postgres","category":"ha","title":"PostgreSQL logical replication (publication / subscription)","summary":"두 PostgreSQL cluster 사이의 테이블 단위 logical replication: wal_level=logical, replica identity 점검, publication과 subscription, 초기 동기화, 모니터링, 충돌(conflict) 처리, sequence 수동 동기화. 버전 간 이전(major upgrade, 서버 교체)에서 다운타임을 몇 초로 줄이는 최종 전환","level":"고급","duration":"45–90분 (+ 초기 복사 시간)","topology":"서버 2대: 발행자(publisher) + 구독자(subscriber). 발행자는 단일 서버, streaming replication 또는 Patroni cluster일 수 있음","tags":["postgresql","logical replication","논리 복제","publication","subscription","cdc","서버 이전","migration","업그레이드","충돌","replica identity","sequence","patroni","failover slot","permanent slots","sync_replication_slots"],"order":15,"steps":25,"scenarios":[{"k":"purpose","v":"sync","l":"지속 복제 (리포팅, 연동, 부분 사본)","m":"logical replication publication subscription CREATE SUBSCRIPTION 테이블 복제 부분 복제 선택한 테이블이 다른 cluster로 계속 흘러갑니다(리포팅 서버, 데이터 웨어하우스 적재, 서비스 간 연동). 구독자는 쓰기 가능한 상태로 유지됩니다."},{"k":"purpose","v":"migrate","l":"이전: 새 서버 / 새 major 버전 + 최종 전환(cutover)","m":"무중단 업그레이드 near zero downtime cutover 서버 이관 logical upgrade 초 단위 다운타임으로 새 서버나 새 major 버전으로 전환: 초기 복사와 지속 동기화, 이어서 짧은 쓰기 중지와 최종 전환(cutover)."}]},{"id":"pg-dump-restore","file":"install/guides/pg-dump-restore.json","group":"PostgreSQL","engineId":"postgres","category":"backup","title":"PostgreSQL 논리 백업과 복원 (pg_dump / pg_restore)","summary":"pg_dump, pg_dumpall, pg_restore로 테이블, 스키마, 데이터베이스 또는 전체 cluster 수준의 논리 백업과 복원을 수행합니다. 구조(DDL)만 또는 데이터만 처리, 다른 서버/버전으로 이전, dump에서 단일 테이블 복원, systemd/cron으로 예약 실행되고 검증되며 보존 기간이 있는 백업을 다룹니다. 모든 경로는 행 수 비교 검증으로 끝납니다. Streami","level":"중급","duration":"20–90분 (데이터 크기에 따라)","topology":"서버 1대, streaming replication(primary + standby) 또는 Patroni cluster. 이전 시 소스 + 대상, 원격 예약 백업 시 + 백업 서버","tags":["postgresql","postgres","pg_dump","pg_restore","pg_dumpall","논리 백업","logical backup","export","import","dump","restore","복원","이전","migration","upgrade","globals","systemd timer","cron","pgpass","백업","patroni","streaming replication","replica","standby","haproxy","hot standby","pg_wrapper","cron.d","flock"],"order":15,"steps":9,"scenarios":[{"k":"mode","v":"tables","l":"단일 테이블 export / import","m":"Export / import pg_dump -t 테이블 export 테이블 import table export 단일 테이블 백업 no matching tables were found 선택한 테이블의 구조와 데이터(index, constraint, trigger 포함)를 받아 다른 데이터베이스에 적재합니다."},{"k":"mode","v":"schema","l":"스키마 export / import","m":"Export / import pg_dump -n 스키마 export schema export no matching schemas were found 스키마의 모든 객체(테이블, view, 함수, sequence)를 받습니다."},{"k":"mode","v":"db","l":"단일 데이터베이스 백업 / 복원","m":"Export / import pg_dump pg_restore 데이터베이스 백업 database backup database restore errors ignored on restore 데이터베이스 전체(구조 + 데이터)를 하나의 dump로 받아 같은 데이터베이스 또는 다른 데이터베이스에 복원합니다. 롤(globals)은 포함되지 않습니다."},{"k":"mode","v":"cluster","l":"전체 cluster (롤 + tablespace + 모든 데이터베이스)","m":"Export / import pg_dumpall pg_dumpall -g globals 전체 cluster 백업 롤 백업 pg_dumpall -g로 globals를, 데이터베이스마다 별도의 pg_dump를 받습니다. 복원은 임시 테스트 cluster에서 리허설합니다."},{"k":"mode","v":"ddl","l":"스키마 구조만 (DDL)","m":"구조와 데이터 분리 pg_dump -s --schema-only DDL export 구조만 데이터 없이 테이블, index, view, 함수 정의만 받습니다. 검토, 버전 관리, 빈 환경 구축용입니다."},{"k":"mode","v":"data","l":"데이터만","m":"구조와 데이터 분리 pg_dump -a --data-only 데이터만 --disable-triggers circular foreign-key constraints 대상에 구조가 준비된 상태에서 행(COPY)만 전송합니다."},{"k":"mode","v":"migrate","l":"다른 서버 / 버전으로 이전","m":"이전과 복구 서버 이전 migration dump restore upgrade aborting because of server version mismatch 새 버전으로 이전 대상 서버에서 새 버전의 pg_dump가 네트워크로 소스에 접속하고, pipe 또는 파일로 전송합니다."},{"k":"mode","v":"partial","l":"dump에서 단일 테이블 / 객체 복원","m":"이전과 복구 단일 테이블 복원 pg_restore -L pg_restore --list DROP TABLE 삭제된 테이블 삭제된 행 실수로 삭제 table restore 기존 custom/directory dump에서 목록 파일(-L)로 테이블 하나와 그 index, constraint, 데이터만 복원합니다."},{"k":"mode","v":"sched","l":"예약 백업 (systemd timer / cron)","m":"예약 백업 pg_dump cron systemd timer 예약 백업 scheduled backup 일일 백업 백업 보존 매일 globals + 데이터베이스 백업, 읽기 가능 여부 검증, 보존 기간, 로그와 오류 알림."}]},{"id":"pg-minor-update","file":"install/guides/pg-minor-update.json","group":"PostgreSQL","engineId":"postgres","category":"patch","title":"PostgreSQL 마이너 버전 업데이트(패치)","summary":"같은 메이저 버전 안에서의 마이너 업데이트(예: 17.4 → 17.x)입니다. release notes 확인, 백업, 롤백용 현재 RPM/deb 보관을 포함합니다. 단일 서버에서는 짧은 다운타임으로 진행하고, streaming replication 클러스터에서는 standby를 먼저 업데이트한 뒤 switchover로 primary를 업데이트합니다(rolling). Patroni 클러스터에서","level":"중급","duration":"20–60분","topology":"서버 1대, primary + standby 또는 Patroni 클러스터","tags":["postgresql","minor","patch","마이너 업데이트","update","upgrade","rolling","switchover","dnf","apt","downgrade","rollback","extension","patroni","patronictl","failover"],"order":20,"steps":17,"scenarios":[{"k":"purpose","v":"upd","l":"마이너 업데이트","m":"minor update 마이너 버전 업데이트 patch CVE 보안 업데이트 dnf upgrade postgresql apt upgrade postgresql 같은 메이저 버전 안에서 새 마이너 버전으로 전환(예: 17.4 → 17.6): 보안 및 버그 수정. 단일 서버는 짧은 다운타임, 클러스터는 rolling 방식."},{"k":"purpose","v":"rb","l":"롤백: 이전 마이너 버전으로 되돌리기(downgrade)","m":"downgrade 롤백 rollback 이전 버전으로 되돌리기 undefined symbol 새 마이너 버전에서 버그나 회귀가 발생한 경우, 저장해 둔 이전 버전 RPM/deb로 되돌립니다. 데이터 디렉터리는 변경되지 않습니다."}]},{"id":"pg-major-upgrade","file":"install/guides/pg-major-upgrade.json","group":"PostgreSQL","engineId":"postgres","category":"upgrade","title":"PostgreSQL major upgrade (pg_upgrade / logical replication)","summary":"메이저 버전 업그레이드(예: 16 → 17): 방법 선택(pg_upgrade --link / --copy / --clone 또는 logical replication을 이용한 거의 무중단 이전), pg_upgrade --check, 확장 모듈 호환성, 구성 이전, 통계 재수집, rsync 또는 pg_basebackup을 이용한 standby 재구축, 이전 클러스터 삭제 및 롤백 runbook을","level":"고급","duration":"1–3시간 (+ 데이터 크기에 따라 추가)","topology":"같은 서버에서 in-place(pg_upgrade; 단일 서버, primary + standby 또는 Patroni 클러스터) 또는 새 서버로(logical)","tags":["postgresql","major","upgrade","버전 업그레이드","pg_upgrade","pg_upgradecluster","link","clone","logical replication","이전","migration","rsync","rollback","patroni","patronictl remove","replica","streaming replication"],"order":30,"steps":24,"scenarios":[{"k":"purpose","v":"upg","l":"Major upgrade","m":"major upgrade pg_upgrade pg_upgradecluster 버전 업그레이드 메이저 버전 upgrade postgresql 17 upgrade postgresql 18 메이저 버전 업그레이드(예: 16 → 17): 사전 점검, 백업, pg_upgrade 또는 logical replication, 검증 및 정리."},{"k":"purpose","v":"rb","l":"롤백 (업그레이드 실패 / 중단)","m":"업그레이드 롤백 rollback pg_control.old database files are incompatible with server 업그레이드 실패 업그레이드가 중간에 멈췄거나 새 버전에서 문제가 생겼을 때 이전 버전으로 되돌립니다. --link 방식에서 새 클러스터를 이미 시작했다면 백업으로만 되돌릴 수 있습니다."}]},{"id":"pg-security","file":"install/guides/pg-security.json","group":"PostgreSQL","engineId":"postgres","category":"security","title":"PostgreSQL 보안 강화 (상태 점검, 네트워크, TLS, 인증, 권한, audit, 암호화)","summary":"설치 가이드가 아닙니다. 운영 중인 PostgreSQL 서버의 보안 취약점을 찾아 조치합니다. 먼저 읽기 전용 상태 점검이 발견 사항을 심각도 순으로 나열하고 각 항목이 어느 카드로 이어지는지 알려 줍니다. 그다음 영역을 하나씩 또는 전체를 순서대로 조치합니다: 리슨 주소, 방화벽과 pg_hba, TLS와 클라이언트의 sslmode=verify-full, scram-sha-256과 postg","level":"중급","duration":"5분 (점검) – 3시간 (전체 보안 강화)","topology":"PostgreSQL 단일 서버 + 애플리케이션 서버 (클라이언트 검증), replica/Patroni 환경은 참고 사항에서 다룸","tags":["postgresql","postgres","보안","security","hardening","보안 강화","cis benchmark","pg_hba","tls","ssl","sslmode","verify-full","scram-sha-256","md5","pgaudit","audit","감사","public schema","권한","롤","luks","pgcrypto","암호화","encryption","firewall"],"order":40,"steps":3,"scenarios":[{"k":"scenario","v":"scan","l":"상태 점검: 어떤 취약점이 있는가? (읽기 전용)","m":"시작 보안 점검 security audit hardening check CIS 보안 취약점 취약점 vulnerability 모의해킹 지적 사항 감사 보고서 점검 쿼리 CIS 점검 보안 스캔 security scan 가장 먼저 실행할 스캔: pg_hba, 리슨 주소, TLS, md5 패스워드, 슈퍼유저, PUBLIC 권한, audit, 파일 권한, 방화벽을 확인하며 아무것도 변경하지 않습니다."},{"k":"scenario","v":"all","l":"전체 보안 강화 (모든 영역 순서대로)","m":"시작 hardening 보안 강화 전체 보안 full hardening CIS benchmark 적용 STIG 새로 설치했거나 보안 강화를 한 번도 하지 않은 서버: 모든 영역을 안전한 순서로 적용합니다. PostgreSQL이 한두 번 재시작됩니다 (유지보수 시간 필요)."},{"k":"scenario","v":"net","l":"네트워크와 접속: 누가 어디서 접속할 수 있는가? (listen, 방화벽, pg_hba)","m":"네트워크와 인증 pg_hba.conf 0.0.0.0/0 trust listen_addresses firewall no pg_hba.conf entry pg_hba.conf rejects connection 5432 포트 열림 FATAL: no pg_hba.conf entry for host 28000 Connection refused listen_addresses = * host all all 0.0.0.0/0 외부 접속 접근 제한 PostgreSQL이 모든 인터페이스에서 리슨하고, pg_hba에 0.0.0.0/0 또는 trust가 있으며, 포트가 모두에게 열려 있습니다. listen_addresses를 변경하면 재시작이 필요합니다."},{"k":"scenario","v":"tls","l":"접속 암호화 (TLS, sslmode=verify-full)","m":"네트워크와 인증 ssl tls sslmode verify-full hostssl certificate verify failed SSL off no encryption server.crt 인증서 만료 certificate expired SSL error: certificate verify failed does not match host name root certificate file does not exist server does not support SSL, but SSL was required self-signed certificate 인증서 체인 certificate chain hostnossl ssl = off이거나 클라이언트가 인증서를 검증하지 않습니다. 서버 인증서, hostssl, 클라이언트의 v"},{"k":"scenario","v":"auth","l":"인증과 패스워드 (scram-sha-256, postgres 원격 로그인)","m":"네트워크와 인증 md5 scram-sha-256 password_encryption password authentication failed authentication method 10 not supported postgres 원격 로그인 패스워드 정책 password policy 28P01 FATAL: password authentication failed for user SCRAM authentication requires libpq version 10 or above pass-the-hash VALID UNTIL 패스워드 만료 md5/password 방식, md5 해시 패스워드, postgres 사용자의 네트워크 로그인 가능 상태를 조치합니다. scram-sha-256으로 전환하고 개인 DBA 계정을 만"},{"k":"scenario","v":"priv","l":"최소 권한: 롤, PUBLIC, 애플리케이션 계정","m":"권한과 감사 추적 public schema PUBLIC GRANT REVOKE superuser least privilege 최소 권한 permission denied for schema 롤 설계 DEFAULT PRIVILEGES 42501 permission denied for table must be owner of table pg_read_server_files pg_execute_server_program SECURITY DEFINER REASSIGN OWNED 권한 보고서 애플리케이션 계정이 테이블 소유자이거나 슈퍼유저이고, 누구나 public 스키마에 쓸 수 있습니다. 읽기/쓰기/소유자 롤과 권한 보고서를 만듭니다. 다운타임 없음."},{"k":"scenario","v":"audit","l":"Audit: 누가 무엇을 했는가? (pgaudit, 중앙 로그)","m":"권한과 감사 추적 pgaudit audit 감사 추적 audit log AUDIT: SESSION log_connections syslog SIEM 개인정보 보호법 (PIPA) PCI shared_preload_libraries could not access file \"pgaudit\" pgaudit must be loaded via shared_preload_libraries log_statement 누가 무엇을 했는가 who changed 감사인 또는 보안팀이 DDL과 롤 변경 기록을 요구합니다. pgaudit를 설치하고 로그를 중앙 서버로 전송합니다. 재시작이 한 번 필요합니다."},{"k":"scenario","v":"tde","l":"디스크 데이터 암호화 (LUKS / pgcrypto)","m":"데이터와 백업 TDE transparent data encryption 디스크 암호화 encryption at rest LUKS dm-crypt pgcrypto pg_tde cryptsetup luksFormat pgp_sym_encrypt at-rest encryption 디스크나 백업 도난에 대비한 보호가 필요합니다. PostgreSQL에는 내장 TDE가 없습니다: 디스크 수준의 LUKS (이전 시 다운타임) 또는 컬럼 수준의 pgcrypto를 사용합니다."},{"k":"scenario","v":"backup","l":"백업 보안 (암호화 백업, 권한)","m":"데이터와 백업 백업 암호화 backup encryption repo-cipher-type pgbackrest cipher gpg .pgpass 백업 권한 cipher: none repo-cipher-pass 암호화되지 않은 백업 unencrypted backup 백업 파일이 암호화되어 있지 않거나 누구나 읽을 수 있는 디렉터리에 있습니다. pgBackRest 저장소 암호화 또는 pg_dump + GPG, .pgpass와 디렉터리 권한을 조치합니다. 다운타임 없음."},{"k":"scenario","v":"os","l":"운영체제 측면 (서비스 계정, 권한, core dump, SELinux)","m":"서버와 버전 SELinux permissive chmod 700 data directory has invalid permissions core dump postgres 계정 sudo avc: denied LimitCORE core dumped passwd -l postgres 계정에 사용 가능한 패스워드가 있고, 데이터 디렉터리 권한이 느슨하며, SELinux가 Permissive/Disabled입니다. core dump 설정은 다음 재시작 때 적용됩니다."},{"k":"scenario","v":"patch","l":"패치와 버전: 지원되는 버전인가?","m":"서버와 버전 EOL end of life 지원 종료 마이너 버전 CVE 보안 패치 security release out of support server_version 패치 일정 minor update pgsql-announce 버전, 지원 종료일, 대기 중인 마이너 업데이트를 확인하고 적용은 패치 가이드로 연결합니다. 읽기 전용."}]},{"id":"mysql-install","file":"install/guides/mysql-install.json","group":"MySQL","engineId":"mysql","category":"install","title":"MySQL 단일 서버 설치(운영 환경용)","summary":"MySQL 8.4 LTS(또는 8.0) 단일 서버를 공식 repository에서 처음부터 운영 환경 수준으로 설치합니다. 데이터, binlog, 로그용 별도 디렉터리, RAM/CPU/디스크 유형에 맞춰 계산한 my.cnf, GTID와 binlog 보존 기간, slow query log, SELinux/AppArmor, root 접근 제한, 애플리케이션 사용자를 구성합니다. 결과적으로 검증을 ","level":"초급","duration":"30–45분","topology":"서버 1대","tags":["mysql","설치","install","8.4","lts","innodb","my.cnf","tuning","gtid","binlog","rhel","ubuntu"],"order":1,"steps":24,"scenarios":[]},{"id":"mysql-replication","file":"install/guides/mysql-replication.json","group":"MySQL","engineId":"mysql","category":"ha","title":"MySQL GTID 복제 (async / semi-sync)","summary":"설치된 MySQL 서버로 source 1대 + replica N대의 GTID 기반 복제를 구성합니다: 복제 사용자, clone 플러그인 또는 mysqldump를 이용한 기존 데이터 이전, SOURCE_AUTO_POSITION, TLS, 선택적 semi-sync, super_read_only replica, 지연 모니터링, 수동 failover 절차. 완료하면 쓰기는 source에서, 읽기는 ","level":"중급","duration":"45–90분","topology":"source 1대 + replica 1..5대","tags":["mysql","복제","replication","gtid","semi-sync","semisync","replica","source","clone","failover","read_only"],"order":2,"steps":21,"scenarios":[]},{"id":"mysql-innodb-cluster","file":"install/guides/mysql-innodb-cluster.json","group":"MySQL","engineId":"mysql","category":"ha","title":"MySQL InnoDB Cluster (Group Replication + MySQL Router)","summary":"같은 데이터 센터에 설치된 MySQL 서버 3대(또는 5–9대)로 single-primary InnoDB Cluster를 구성합니다: MySQL Shell AdminAPI를 이용한 dba.configureInstance, createCluster, clone을 이용한 addInstance, MySQL Router bootstrap(6446 RW / 6447 RO), cluster.status","level":"고급","duration":"90–120분","topology":"MySQL node 3–9대 + Router 서버 1대","tags":["mysql","innodb cluster","group replication","mysql shell","mysqlsh","adminapi","mysql router","고가용성","failover","quorum","clone","single-primary"],"order":3,"steps":39,"scenarios":[]},{"id":"mysql-xtrabackup","file":"install/guides/mysql-xtrabackup.json","group":"MySQL","engineId":"mysql","category":"backup","title":"Percona XtraBackup을 이용한 백업, 복원 및 PITR","summary":"Percona XtraBackup을 이용한 물리 백업: 호환되는 XtraBackup 버전 설치, auth_socket 기반 백업 사용자, 압축/암호화 full + incremental 백업 스크립트, systemd timer 또는 cron 스케줄링, binlog 복사, 복원 리허설을 다룹니다. 시나리오 카드에서는 전체 복원, incremental 체인 복원, 특정 시점 복구(PITR), 단일","level":"중급","duration":"45–90분","topology":"서버 1대(단일 서버, source, replica 또는 InnoDB Cluster secondary), 백업 저장소는 로컬 디스크 또는 NFS","tags":["mysql","xtrabackup","percona","백업","backup","restore","incremental","pitr","binlog","복구","zstd","암호화","replica","slave-info","safe-slave-backup","innodb cluster","cron","systemd timer","flock"],"order":4,"steps":26,"scenarios":[{"k":"scenario","v":"setup","l":"백업 구성, 스케줄링 및 복원 테스트(전체 흐름)","m":"구성 및 리허설 xtrabackup 설치 백업 스크립트 backup script systemd timer cron 복원 리허설 xtrabackup 백업 물리 백업 physical backup hot backup incremental backup full backup percona-xtrabackup XtraBackup 설치, 백업 사용자, full + incremental 백업 스크립트, 스케줄링, 복원 리허설(PITR 포함)을 수행합니다. 처음 구성하는 경우 이 항목을 선택합니다. 백업은 서버가 가동 중인 상태에서 수행되므로 다운타임은 없고 디스크/CPU 부하만 늘어납니다."},{"k":"scenario","v":"r_full","l":"전체 복원: 최신 full 백업으로 되돌리기","m":"복원 restore 복원 copy-back datadir 유실 InnoDB: corruption MY-011971 MY-012592 Database page corruption mysqld 기동 안 됨 full restore ERROR 2002 Can't connect to local MySQL server through socket MY-012574 Unable to lock ./ibdata1 datadir 삭제됨 디스크 장애 disk failure 데이터 유실 데이터 디렉터리가 유실/손상되었거나 서버가 기동되지 않으며, 체인의 full 백업으로 되돌리는 것으로 충분한 경우입니다. 복원하는 동안 MySQL은 중지된 상태입니다."},{"k":"scenario","v":"r_inc","l":"full + incremental 체인으로 복원(최신 incremental까지)","m":"복원 incremental restore apply-log-only incremental-dir to_lsn from_lsn This incremental backup seems not to be proper for the target incremental 체인 incremental chain xtrabackup_checkpoints applying incremental 최신 incremental 백업 시점(또는 선택한 incremental 시점)으로 되돌리려는 경우입니다. full 백업 위에 incremental을 `--apply-log-only`로 순서대로 적용합니다. 복원하는 동안 MySQL은 중지된 상태입니다."},{"k":"scenario","v":"r_pitr","l":"특정 시점 복구(PITR): 잘못된 DELETE / DROP 직전으로","m":"시점 복구 PITR point in time 실수로 삭제 DROP TABLE 잘못된 DELETE TRUNCATE mysqlbinlog stop-datetime stop-position exclude-gtids binlog replay 잘못된 UPDATE WHERE 없는 DELETE accidental delete accidental drop DROP DATABASE 되돌리기 undo delete binlog로 복구 실수로 삭제되거나 손상된 데이터: 최신 백업을 복원하고 binlog를 문제 transaction 직전까지(시간, 위치 또는 GTID 기준) 적용합니다. 복원하는 동안 MySQL은 중지되며, 문제 발생 이후의 쓰기도 함께 되돌려집니다. 테이블 하나만 영향을 받았다면 먼저 단일 테이블 시나리오를 검토"},{"k":"scenario","v":"r_table","l":"단일 테이블 복원(--export + IMPORT TABLESPACE)","m":"시점 복구 단일 테이블 single table restore IMPORT TABLESPACE DISCARD TABLESPACE transportable tablespace --export .cfg ERROR 1808 ERROR 1812 Schema mismatch ERROR 1813 Tablespace exists ERROR 1146 Table doesn't exist tablespace is missing 테이블 하나 복원 table restore ibd 파일 테이블 하나의 백업 시점 데이터만 필요한 경우입니다. 서버 전체를 복원하지 않고 해당 테이블을 백업에서 `t_restore`라는 이름으로 가져온 뒤 그 데이터로 수정합니다. 서버는 계속 가동됩니다."},{"k":"scenario","v":"r_replica","l":"백업으로 새 replica 구성","m":"이관 및 복제 새 replica replica 구성 xtrabackup_binlog_info xtrabackup_slave_info gtid_purged slave-info replication from backup 1236 백업으로 replica 구성 seed replica Could not find first log file name in binary log index file purged required binary logs 1236 replica 재구성 새로 구성하거나 재구성할 replica를 네트워크 clone 대신 기존 XtraBackup 백업으로 준비하여 source에 연결하려는 경우입니다(대용량 데이터, source 부하). source는 영향을 받지 않으며, 대상 서버의 데이터는 교체됩니다."},{"k":"scenario","v":"r_other","l":"다른 서버로 백업 복원(이관, 테스트, 복제)","m":"이관 및 복제 다른 서버 테스트 서버 restore test 이관 migration 복제본 clone to test rsync server_uuid 테스트 환경에 로드 백업 검증 restore drill 백업 열기 서버 이관 운영 백업을 테스트/개발 서버에서 열거나, 서버를 이관하거나, 백업이 실제로 복원되는지 별도 서버에서 검증하려는 경우입니다. 원본 서버는 영향을 받지 않습니다."}]},{"id":"mysql-upgrade","file":"install/guides/mysql-upgrade.json","group":"MySQL","engineId":"mysql","category":"upgrade","title":"MySQL 8.0 → 8.4 LTS upgrade (in-place)","summary":"설치된 MySQL 8.0을 같은 서버에서(in-place) 8.4 LTS로 업그레이드합니다: 단일 서버, source + replica(replica 먼저, source는 마지막) 또는 InnoDB Cluster(Router → Shell → metadata → secondary 하나씩 → primary는 마지막). 각 서버에서의 util.checkForServerUpgrade 사전 점검, ","level":"중급","duration":"60–120분(데이터 크기에 따라)","topology":"서버 1대, source + replica 또는 InnoDB Cluster(3대 이상 멤버 + MySQL Router 서버)","tags":["mysql","upgrade","8.0","8.4","lts","업그레이드","checkForServerUpgrade","mysql_native_password","authentication_policy","rollback","복구","innodb cluster","group replication","mysql router","upgradeMetadata","setPrimaryInstance","rolling upgrade"],"order":5,"steps":18,"scenarios":[]},{"id":"mysql-repl-ops","file":"install/guides/mysql-repl-ops.json","group":"MySQL","engineId":"mysql","category":"ops","title":"MySQL 복제 일상 운영과 장애 시나리오 (8.0 / 8.4)","summary":"구성된 source + replica 토폴로지(async / semi-sync, GTID 또는 binlog 위치)의 일상 운영 및 장애 복구 런북입니다: 상태 및 지연 점검, 계획된 switchover, failover, 기존 source 재합류, replica 재구성(clone / XtraBackup / mysqldump), errant GTID, 1062/1032 오류, 지연 replic","level":"고급","duration":"5분(점검) – 2시간(재구성)","topology":"source 1대 + replica 1..5대 (async / semi-sync)","tags":["mysql","복제","replication","gtid","errant gtid","errant transaction","switchover","failover","replica","source","super_read_only","semi-sync","show replica status","seconds_behind_source","복제 지연","lag","1062","1032","1236","duplicate entry","can't find record","gtid_next","sql_replica_skip_counter","source_delay","지연 replica","delayed replica","clone","xtrabackup","mysqldump","purge binary logs","binlog_expire_logs_seconds","wait_for_executed_gtid_set","change replication source to","런북","8.0","8.4"],"order":12,"steps":5,"scenarios":[{"k":"scenario","v":"health","l":"상태 및 지연 점검 (복제가 정상인가?)","m":"모니터링 및 점검 SHOW REPLICA STATUS SHOW SLAVE STATUS Seconds_Behind_Source Seconds_Behind_Master Replica_IO_Running Slave_IO_Running Replica_SQL_Running: No 복제 지연 replication lag replica lag Rpl_semi_sync_source_status 1045 2003 2026 Error connecting to source 복제 상태 Replica_IO_Running: No Replica_IO_Running: Connecting Slave_IO_Running: No Slave_SQL_Running: No Seconds_Behind_Source: NULL Last_IO_Error"},{"k":"scenario","v":"switchover","l":"계획된 switchover (source 역할을 replica로 이관)","m":"역할 전환 switchover 계획된 전환 역할 전환 role change promote replica source 변경 유지보수 계획된 유지보수 graceful takeover graceful-master-takeover source 교체 primary 변경 change primary 새 source source와 replica가 정상이며, 유지보수·하드웨어 교체·업그레이드를 위해 source 역할을 계획적으로 replica에 넘기는 경우입니다. 쓰기가 몇 분간 중단되지만 데이터 손실은 없습니다. 원복은 같은 시나리오를 반대로 수행합니다."},{"k":"scenario","v":"failover","l":"source 유실: 가장 앞선 replica로 failover","m":"역할 전환 failover source 다운 source down master down primary down Error connecting to source Can't connect to MySQL server 2003 2013 Lost connection 가장 앞선 replica most advanced replica Replica_IO_Running: Connecting Slave_IO_Running: Connecting 2005 Unknown MySQL server host error reconnecting to source source 접근 불가 source 종료됨 primary 다운 서버 다운 server crashed source가 다운되었거나 접근할 수 없고 곧 복구되지 않을 상황입니다(rep"},{"k":"scenario","v":"rejoin","l":"failover 후 기존 source를 replica로 재합류","m":"역할 전환 기존 source old source old master rejoin 재합류 errant split brain 1236 기존 primary old primary 기존 master 복귀 split-brain Replica has more GTIDs than the source has Slave has more GTIDs than the master has 재합류 reintegrate failover가 끝났고 기존 source 서버가 다시 살아났습니다. 먼저 쓰기를 막고 binlog를 복사한 뒤 errant transaction을 점검합니다. 깨끗하면 바로 replica가 되고, 그렇지 않으면 새 source에서 재구성합니다. 새 source에는 다운타임이 없습니다."},{"k":"scenario","v":"rebuild","l":"replica를 처음부터 재구성 (clone / XtraBackup / mysqldump)","m":"복구 1236 Cannot replicate because the source purged required binary logs purged required binary logs 재구성 rebuild replica reseed clone instance 새 replica 데이터 불일치 data drift Could not find first log file name in binary log index file the source has purged binary logs containing GTIDs that the replica requires the master has purged binary logs containing GTIDs that the slave requires Got fatal error 1"},{"k":"scenario","v":"errant","l":"Errant GTID: source에 없는 트랜잭션이 replica에 있음","m":"복구 errant gtid errant transaction gtid_subtract extra gtid 초과 gtid Having extra GTID events incompatible GTID set 1236 GTID_SUBTRACT gtid_executed 차이 replica에 기록됨 write on replica super_read_only 꺼짐 Replica has more GTIDs than the source has replica에 직접 기록되었거나(super_read_only가 꺼져 있을 때) 이전 source에서 남은 트랜잭션이 있습니다. failover 시 이 트랜잭션이 다른 replica로 퍼지거나 1236으로 복제를 중단시킵니다. 해결: 빈 트랜잭션, source에 적용, 또는 재구성."},{"k":"scenario","v":"sqlerr","l":"복제 중단: 1062 Duplicate entry / 1032 Can't find record","m":"복구 1062 1032 1452 1146 1007 1008 Duplicate entry Can't find record HA_ERR_KEY_NOT_FOUND HA_ERR_FOUND_DUPP_KEY Could not execute Write_rows event Could not execute Update_rows event Could not execute Delete_rows event Replica_SQL_Running: No Slave_SQL_Running: No Coordinator stopped because there were error(s) in the worker(s) sql_slave_skip_counter sql_replica_skip_counter gtid_next MY-010584 1058"},{"k":"scenario","v":"delayed","l":"지연 replica (SOURCE_DELAY): 구성 또는 삭제된 데이터 복구","m":"일상 운영 SOURCE_DELAY MASTER_DELAY delayed replica 지연 replica 실수로 삭제 DROP TABLE 잘못된 DELETE SQL_Delay SQL_BEFORE_GTIDS SQL_Remaining_Delay 실수로 실행한 DELETE WHERE 없는 DELETE accidental delete accidental drop dropped table 테이블 삭제됨 데이터 복구 UNTIL SQL_BEFORE_GTIDS replica를 의도적으로 N초 뒤처지게 유지하거나(지연 설정 / 해제), 실수로 삭제·변경된 데이터(DROP, WHERE 없는 DELETE/UPDATE)를 지연 replica에 아직 적용되기 전에 되살리려는 경우입니다. source는 계속 운영됩니다."},{"k":"scenario","v":"repoint","l":"replica를 다른 source로 이전 (체인 / 재연결)","m":"일상 운영 repoint change source 체인 복제 chained replication intermediate 중간 replica relay replica CHANGE MASTER TO CHANGE REPLICATION SOURCE TO cascading replication 계단식 복제 replica를 다른 source에 연결 SOURCE_HOST 변경 MASTER_HOST 변경 log_replica_updates replica를 다른 replica 아래(체인 / 중간 replica)로 붙이거나 source로 다시 연결하려는 경우입니다. 데이터는 복사되지 않고 연결만 바뀝니다. 이전하는 replica는 몇 초간 멈춥니다."},{"k":"scenario","v":"binlog","l":"binlog 정리와 보존 기간 (디스크 부족)","m":"일상 운영 PURGE BINARY LOGS PURGE MASTER LOGS binlog_expire_logs_seconds expire_logs_days binlog 디스크 가득 참 No space left on device errno 28 binlog 정리 binlog retention Disk is full writing OS errno 28 1867 was not purged because it was being read 1373 Target log not found in binlog index binlog 증가 binlog_expire_logs_auto_purge binlog_transaction_compression binlog가 디스크를 채우고 있거나 보존 기간을 조정하려는 경우입니다. repli"}]},{"id":"mysql-ic-ops","file":"install/guides/mysql-ic-ops.json","group":"MySQL","engineId":"mysql","category":"ops","title":"MySQL InnoDB Cluster 일상 운영 및 장애 시나리오 (MySQL Shell 8.4)","summary":"설치 가이드가 아닙니다: 이미 운영 중인 InnoDB Cluster(Group Replication + MySQL Router, MySQL Shell AdminAPI로 구성)의 일상 운영과 장애 복구를 위한 시나리오별 runbook입니다. 시나리오 카드를 선택하십시오: 상태 점검, 멤버 추가(clone / incremental)와 제거(force 포함), 장애 멤버 재합류, 클러스터 전체가 ","level":"고급","duration":"5분(점검) – 1시간(멤버 추가)","topology":"MySQL node 3–9대(single-primary) + MySQL Router","tags":["mysql","innodb cluster","group replication","mysql shell","mysqlsh","adminapi","cluster.status","addinstance","removeinstance","rejoininstance","rebootclusterfromcompleteoutage","forcequorumusingpartitionof","setprimaryinstance","setoption","setinstanceoption","rescan","mysql router","bootstrap","quorum","no_quorum","split brain","expelTimeout","memberWeight","autoRejoinTries","exitStateAction","consistency","clone","MISSING","UNREACHABLE","RECOVERING","ERROR","runbook","8.4"],"order":13,"steps":5,"scenarios":[{"k":"scenario","v":"health","l":"클러스터 상태 및 헬스 체크(모두 정상인가?)","m":"모니터링 및 점검 cluster.status status() OK_PARTIAL OK_NO_TOLERANCE OK_NO_TOLERANCE_PARTIAL instanceErrors replicationLag flow control RECOVERING applier queue 클러스터 상태 Cluster is ONLINE and can tolerate can tolerate up to ONE failure 헬스 체크 health check 큐 증가 replication_group_members group_replication_member_stats 클러스터 상태 확인 읽기 전용: `cluster.status({extended: 1})`, 멤버의 역할/상태/버전, applier 큐, Router 목록, error"},{"k":"scenario","v":"add","l":"새 멤버 추가(addInstance: clone / incremental)","m":"멤버 addInstance 멤버 추가 node 추가 add instance recoveryMethod clone incremental 3092 Having extra GTID events MYSQLSH 51166 The target instance must be either cloned or fully provisioned Clone Donor Error 용량 증설 장애 허용 수준 상향 OK_NO_TOLERANCE 클러스터에 새 MySQL node를 추가하거나, 장애 허용 수준을 높이거나(3 → 5), 제거했던 node를 다시 넣으려는 경우입니다. 클러스터는 계속 쓰기 가능합니다. clone 중에는 donor에 디스크/네트워크 부하가 생기고 새 node가 재시작됩니다."},{"k":"scenario","v":"remove","l":"멤버 제거(removeInstance, 필요 시 force)","m":"멤버 removeInstance 멤버 제거 node 제거 remove instance force MISSING decommission Cluster.removeInstance is not reachable and cannot be safely removed node 삭제 축소 서버 반납 node를 클러스터에서 영구히 제거하려는 경우(하드웨어 반납, 축소)이거나, node를 잃어 메타데이터에서 삭제해야 하는 경우(`force`)입니다. Secondary 제거 시에는 중단이 없고, primary를 제거하면 몇 초간 쓰기가 중단됩니다."},{"k":"scenario","v":"rejoin","l":"장애 멤버 재합류(MISSING / ERROR / OFFLINE)","m":"장애 및 복구 rejoinInstance MISSING ERROR OFFLINE UNREACHABLE expelled 그룹에서 추방됨 member was expelled MY-011735 MY-011526 incompatible GTID set (MISSING) This member has more executed transactions than those present in the group has an incompatible GTID set with the seed instance Error connecting to all peers Member join failed group_replication is stopped 멤버 다운 멤버 OFFLINE member expelled auto-rejoin 멤버가"},{"k":"scenario","v":"reboot","l":"클러스터 전체 다운: 재시작(rebootClusterFromCompleteOutage)","m":"장애 및 복구 rebootClusterFromCompleteOutage complete outage 전체 중단 모든 node 다운 정전 Group Replication is not active OFFLINE Unable to find a cluster PRIMARY member The cluster is offline Dba.rebootClusterFromCompleteOutage Dba.getCluster: This function is not available through a session to a standalone instance metadata exists, instance belongs to that metadata, but GR is not active group_replication_boot"},{"k":"scenario","v":"quorum","l":"과반 상실(NO_QUORUM): 남은 멤버로 quorum 강제 구성","m":"장애 및 복구 NO_QUORUM forceQuorumUsingPartitionOf quorum 과반 상실 split-brain split brain UNREACHABLE majority partition 3100 Cluster has no quorum cannot process write transactions split-brain! Instance is not part of the majority group 쓰기 멈춤 writes hang forceQuorum 멤버 대부분이 동시에 사라졌고(예: 3대 중 2대) 남은 멤버는 `NO_QUORUM`으로 쓰기를 멈췄습니다(쓰기가 멈춘 채 대기). 사라진 멤버가 실제로 내려가 있음이 확실하다면 남은 파티션으로 클러스터를 다시 가동합니다. 잘못 사용하면 split-"},{"k":"scenario","v":"switch","l":"계획된 primary 전환(setPrimaryInstance)","m":"역할 및 라우팅 setPrimaryInstance primary 변경 switchover primary 이양 The appointed primary member has a version that is greater group_replication_set_as_primary Cluster.setPrimaryInstance runningTransactionsTimeout 계획된 primary 전환 primary 이동 planned switchover 유지 보수(OS 패치, 하드웨어), 부하 분산, DR 훈련을 위해 primary 역할을 특정 secondary로 계획적으로 넘기려는 경우입니다. 쓰기가 몇 초간 대기하고 열린 트랜잭션은 완료되며, Router가 새 primary로 자동으로 라우팅합니다."},{"k":"scenario","v":"router","l":"Router 추가 또는 재 bootstrap","m":"역할 및 라우팅 mysqlrouter MySQL Router bootstrap --force routerErrors re-bootstrap Router listRouters removeRouterMetadata 6446 6447 previously configured in this host Unable to connect to the metadata server Unable to fetch all configuration options The provided server is currently not in a InnoDB cluster group with quorum mysqlrouter.service failed Router 연결 안 됨 Router 설치 새 애플리케이션 서버에 Router를 설치하거나, "},{"k":"scenario","v":"options","l":"클러스터 옵션 변경(consistency, expelTimeout, memberWeight…)","m":"구성 setOption setInstanceOption consistency BEFORE_ON_PRIMARY_FAILOVER expelTimeout group_replication_member_expel_timeout memberWeight autoRejoinTries exitStateAction OFFLINE_MODE READ_ONLY options() group_replication_consistency group_replication_member_weight group_replication_autorejoin_tries group_replication_exit_state_action 멤버가 자주 추방됨 failover 우선순위 Failover 일관성, 네트워크 불안정 시 멤버 추방까지의 시간, fail"},{"k":"scenario","v":"rescan","l":"메타데이터를 실제 그룹과 동기화(rescan)","m":"구성 rescan metadata is not managed not registered in the metadata newlyDiscoveredInstances unavailableInstances instanceErrors obsolete Use cluster.rescan() Instance is not managed by InnoDB cluster Use cluster.rescan() to repair 메타데이터 불일치 메타데이터 동기화 안 됨 `cluster.status()`가 메타데이터에 없는 멤버나 더 이상 그룹에 없는 오래된 항목을 표시합니다(`START GROUP_REPLICATION`을 수동 실행했거나, 이전 멤버를 제거하지 않았거나, 서버를 재구축한 경우). 중단은 없으며 메타데이터만 갱신합"}]},{"id":"mysql-logical-backup","file":"install/guides/mysql-logical-backup.json","group":"MySQL","engineId":"mysql","category":"backup","title":"MySQL 논리 백업 및 복원 (mysqldump, MySQL Shell)","summary":"mysqldump 또는 MySQL Shell(util.dumpInstance / dumpSchemas / dumpTables + util.loadDump)을 사용한 논리 백업 및 복원입니다: 테이블, 스키마 또는 인스턴스 전체, 구조만 또는 데이터만, 다른 서버로의 마이그레이션, 전체 덤프에서 단일 테이블 추출, systemd/cron으로 예약하고 login-path로 비밀번호 없이 실행하며","level":"중급","duration":"20–90분 (데이터 크기에 따라)","topology":"서버 1대 (마이그레이션 및 인스턴스 복원 시 대상 서버 추가)","tags":["mysql","mysqldump","mysql shell","mysqlsh","dumpInstance","dumpSchemas","dumpTables","loadDump","논리 백업","logical backup","export","import","복원","restore","마이그레이션","migration","login-path","mysql_config_editor","systemd timer","cron","백업","replica","dump-replica","source-data","innodb cluster","cron","systemd timer","flock","login-path"],"order":15,"steps":8,"scenarios":[{"k":"mode","v":"table","l":"테이블 export / import","m":"백업 및 복원 테이블 백업 table export 테이블 복사 copy table mysqldump 테이블 하나 이상의 테이블을 백업하거나 다른 스키마/서버로 복사하거나 복원합니다. 원본은 계속 운영되며 InnoDB 테이블에는 잠금이 걸리지 않습니다."},{"k":"mode","v":"schema","l":"스키마(데이터베이스) 백업 / 복원","m":"백업 및 복원 스키마 백업 데이터베이스 백업 database backup dumpSchemas mysqldump database 데이터베이스(스키마) 전체를 백업하거나 다른 스키마/서버에 로드합니다. 원본은 계속 운영되며 대상의 같은 이름 테이블은 삭제 후 다시 생성됩니다."},{"k":"mode","v":"instance","l":"인스턴스 전체 (사용자 + 모든 스키마)","m":"백업 및 복원 모든 데이터베이스 all databases dumpInstance 사용자 마이그레이션 full logical backup 백업은 이 서버에서 받고 복원은 빈 서버에서 수행합니다: 사용자, 권한, 모든 스키마. 서버 재구축 또는 버전/플랫폼 변경용입니다."},{"k":"mode","v":"ddl","l":"구조만 (DDL)","m":"구조와 데이터 분리 구조만 schema only no-data ddlOnly CREATE TABLE export 테이블, 뷰, 트리거, 프로시저, 이벤트 정의만 포함하며 데이터는 없습니다. 스키마 비교, 빈 환경 구축, 구조의 버전 관리용입니다."},{"k":"mode","v":"data","l":"데이터만","m":"구조와 데이터 분리 데이터만 data only no-create-info dataOnly 데이터 이전 대상에 구조가 이미 있는 상태에서 행만 전송합니다(예: 테스트 데이터 갱신). 로드 중 대상의 트리거가 실행될 수 있으며 가이드에서 이를 확인합니다."},{"k":"mode","v":"migrate","l":"다른 서버로 마이그레이션","m":"마이그레이션 마이그레이션 migration 서버 교체 새 서버로 이전 8.0 to 8.4 migration dump and load HeatWave로 마이그레이션 대상 서버에서 작업하며 원본에는 네트워크를 통해 login-path로 접속합니다. 전환 시점에 애플리케이션은 쓰기를 중지하며, 다운타임은 덤프 + 로드 시간만큼입니다. 서버, 운영체제 또는 버전 변경용입니다."},{"k":"mode","v":"extract","l":"전체 덤프에서 단일 테이블 추출","m":"복구 단일 테이블 복원 덤프에서 테이블 추출 extract table from dump DROP TABLE 되돌리기 삭제된 행 ERROR 1146 Table doesn't exist 전체 덤프는 있는데 테이블 하나만 필요한 경우입니다(삭제된 행, DROP된 테이블). 원래 스키마는 건드리지 않고 별도 스키마로 추출하거나, 삭제된 테이블을 되돌려 놓습니다."},{"k":"mode","v":"sched","l":"예약 백업 (systemd timer / cron)","m":"예약 백업 예약 백업 scheduled backup cron mysqldump systemd timer 자동 백업 야간 백업 매일 정해진 시각에 자동으로 받는 논리 백업입니다: 별도 백업 사용자, login-path, 스크립트, systemd timer 또는 cron, 보존 기간, 오류 알림. 원본은 계속 운영됩니다."}]},{"id":"mysql-security","file":"install/guides/mysql-security.json","group":"MySQL","engineId":"mysql","category":"security","title":"MySQL 보안 강화(상태 점검, 네트워크, TLS, 인증, 권한, audit, 암호화)","summary":"설치 가이드가 아닙니다. 운영 중인 MySQL 8.4 / 8.0 서버의 취약점을 찾아 해소합니다. 먼저 읽기 전용 상태 점검이 취약점을 심각도 순으로 나열하고 각 발견 사항이 어느 카드로 이어지는지 알려 줍니다. 이후 영역을 하나씩 또는 전체를 순서대로 조치합니다: bind_address, firewall, 계정의 host 제한; TLS, require_secure_transport, 클라이","level":"중급","duration":"5분(점검) – 3시간(전체 강화)","topology":"MySQL 서버 1대 + 애플리케이션 서버(클라이언트 검증); replica/InnoDB Cluster는 참고 사항에","tags":["mysql","보안","security","hardening","보안 강화","cis benchmark","tls","ssl","require_secure_transport","verify_identity","caching_sha2_password","mysql_native_password","validate_password","audit_log","audit","percona","tde","keyring","innodb encryption","root","익명 사용자","권한","롤","login-path"],"order":40,"steps":2,"scenarios":[{"k":"scenario","v":"scan","l":"상태 점검: 어떤 취약점이 있는가?(읽기 전용)","m":"시작 보안 점검 security audit hardening check CIS 취약점 보안 취약점 vulnerability 모의해킹 결과 mysql_secure_installation 점검 쿼리 보안 스캔 security scan 치명 가장 먼저 실행할 점검입니다. 익명/비밀번호 없는/원격 root 계정, 모든 곳(%)에서의 접근, 취약한 인증 플러그인, 전역 권한, TLS, audit, 암호화, 파일 권한, firewall을 확인하며 아무것도 변경하지 않습니다."},{"k":"scenario","v":"all","l":"전체 강화(모두 순서대로)","m":"시작 hardening 보안 강화 전체 보안 full hardening CIS benchmark 적용 STIG 새로 설치했거나 한 번도 강화하지 않은 서버: 모든 영역을 안전한 순서로 적용합니다. MySQL을 여러 번 재시작하므로 유지보수 창이 필요합니다."},{"k":"scenario","v":"net","l":"네트워크와 연결: 누가 어디서 접속할 수 있는가?(bind_address, firewall, host 제한)","m":"네트워크와 인증 bind_address 0.0.0.0 '%' host '%' firewall 3306 33060 Host is not allowed to connect ERROR 1130 ERROR 2003 Can't connect to MySQL server skip_name_resolve 외부 접근 접근 제한 MySQL이 모든 인터페이스에서 수신 대기하고, 계정 host가 '%'이며, 포트가 모두에게 열려 있습니다. bind_address를 바꾸면 재시작이 필요합니다."},{"k":"scenario","v":"tls","l":"연결 암호화(TLS, require_secure_transport, VERIFY_IDENTITY)","m":"네트워크와 인증 ssl tls require_secure_transport ssl-mode VERIFY_IDENTITY VERIFY_CA ERROR 3159 Connections using insecure transport are prohibited server-cert.pem 인증서 만료 ERROR 2026 SSL connection error certificate verify failed ERROR 3888 인증서 체인 certificate chain 자동 생성된 자체 서명 인증서를 사용하거나, 암호화되지 않은 연결을 허용하거나, 클라이언트가 인증서를 검증하지 않습니다. ALTER INSTANCE RELOAD TLS로 중단 없이 적용합니다."},{"k":"scenario","v":"auth","l":"인증과 비밀번호(caching_sha2, 비밀번호 정책, root)","m":"네트워크와 인증 mysql_native_password caching_sha2_password validate_password password_history FAILED_LOGIN_ATTEMPTS 원격 root anonymous user 익명 사용자 ERROR 1045 Access denied Plugin 'mysql_native_password' is not loaded ERROR 1524 ERROR 1819 ERROR 3955 ERROR 3118 Account is locked ERROR 2061 Authentication requires secure connection auth_socket mysql_native_password 계정, 비밀번호 정책 없음, 원격 root, 익명 계정, test 데이터베"},{"k":"scenario","v":"priv","l":"최소 권한: 롤과 DDL 권한 없는 애플리케이션 계정","m":"권한과 추적 GRANT REVOKE ALL PRIVILEGES SUPER FILE least privilege 최소 권한 CREATE ROLE ERROR 1142 command denied ERROR 3530 ERROR 1227 you need (at least one of) the SUPER privilege DEFAULT ROLE 권한 보고서 애플리케이션 계정의 CREATE/ALTER/DROP 또는 *.* 권한, FILE/SUPER 권한을 가진 계정; 읽기/쓰기/DDL 롤과 권한 보고서. 중단 없음."},{"k":"scenario","v":"audit","l":"Audit: 누가 무엇을 했는가?","m":"권한과 추적 audit audit_log audit_log_filter 감사 추적 general_log Enterprise Audit Percona audit SIEM 개인정보 보호법 (PIPA) PCI audit_log_filter_set_filter ERROR 1126 MySQL Enterprise 라이선스 누가 무엇을 했나 감사 담당자가 세션과 쿼리 기록을 요구합니다. MySQL Community에는 내장 audit이 없습니다: Enterprise Audit, Percona Server Audit Log Filter, 또는 Community용 제한적 대안. **라이선스:** Enterprise Audit은 MySQL Enterprise 구독에만 포함되며, Percona Audit Log Filter는 "},{"k":"scenario","v":"tde","l":"디스크의 데이터 암호화(InnoDB tablespace encryption, keyring)","m":"데이터와 백업 TDE encryption at rest ENCRYPTION='Y' keyring component_keyring_file ERROR 3185 Can't find master key from keyring binlog_encryption innodb_redo_log_encrypt ALTER INSTANCE ROTATE INNODB MASTER KEY mysqld.my keyring_component_status master key 디스크나 백업 복사본 도난에 대비한 테이블, redo, undo, binlog 암호화. keyring component 설치에 재시작 1회, 큰 테이블은 ALTER 시간이 걸립니다. Community에서도 라이선스 없이 사용할 수 있으며, 암호화/KMS keyring"},{"k":"scenario","v":"backup","l":"백업 보안(암호화된 백업, login-path, 권한)","m":"데이터와 백업 백업 암호화 backup encryption mysqldump gpg xtrabackup --encrypt login-path mysql_config_editor .my.cnf 비밀번호 xbcrypt --encrypt-key-file 암호화되지 않은 백업 unencrypted backup 백업이 암호화되지 않았거나 누구나 읽을 수 있는 디렉터리에 있고, 스크립트에 평문 비밀번호가 있습니다. mysqldump + GPG 또는 XtraBackup --encrypt, 백업 계정과 login-path."},{"k":"scenario","v":"os","l":"운영 체제 측면(서비스 계정, 권한, core dump, SELinux)","m":"서버와 버전 SELinux permissive secure_file_priv local_infile core dump mysql 계정 .mysql_history ERROR 1290 secure-file-priv option ERROR 3948 Loading local data is disabled avc: denied 데이터 디렉터리나 인증서 파일이 다른 사용자에게 열려 있고, secure_file_priv가 비어 있으며, local_infile이 켜져 있고, SELinux가 꺼져 있습니다. 일부 설정은 재시작 시 적용됩니다."},{"k":"scenario","v":"patch","l":"패치와 버전: 지원되는 버전인가?","m":"서버와 버전 EOL end of life 지원 종료 MySQL 8.0 EOL CPU Critical Patch Update CVE 보안 패치 패치 일정 MySQL 8.0 end of life 8.4 LTS 버전, 지원 종료(8.0은 2026년 4월 종료), 대기 중인 업데이트를 확인하고 적용은 업그레이드 가이드로 연결합니다. 읽기 전용."}]},{"id":"mssql-linux-install","file":"install/guides/mssql-linux-install.json","group":"SQL Server","engineId":"mssql","category":"install","title":"SQL Server Linux 설치 (RHEL / Ubuntu, 운영 환경용)","summary":"단일 Linux 서버(RHEL 계열 또는 Ubuntu)에 Microsoft 리포지토리에서 SQL Server 2022 또는 2025를 설치합니다: 에디션 선택, sa 비밀번호의 안전한 입력, mssql-tools18(sqlcmd/bcp), 데이터/로그/tempdb/백업 디렉터리 분리, 메모리와 CPU 기준으로 계산한 memory limit, max server memory, MAXDOP, t","level":"초급","duration":"40–60분","topology":"서버 1대","tags":["sql server","mssql","linux","설치","install","mssql-conf","sqlcmd","tempdb","maxdop","rhel","ubuntu","2022","2025"],"order":1,"steps":22,"scenarios":[]},{"id":"mssql-windows-install","file":"install/guides/mssql-windows-install.json","group":"SQL Server","engineId":"mssql","category":"install","title":"SQL Server 2022 Windows 설치 (unattended, 운영 환경 준비 완료)","summary":"단일 Windows Server에 ConfigurationFile.ini를 사용해 SQL Server 2022를 무인(unattended) 설치합니다. INI는 선택한 값에 따라 생성됩니다(기능, 인스턴스, 서비스 계정/gMSA, collation, 데이터/로그/tempdb/백업 디렉터리, tempdb 파일 수, 메모리, MAXDOP, TCP). 설치 전에는 디스크 포맷, 전원 관리 옵션, ","level":"중급","duration":"45–75분","topology":"Windows Server 1대","tags":["sql server","mssql","windows","설치","install","unattended","configurationfile.ini","setup.exe","gmsa","tempdb","maxdop","ifi","lock pages in memory","2022"],"order":2,"steps":18,"scenarios":[]},{"id":"mssql-ag-windows","file":"install/guides/mssql-ag-windows.json","group":"SQL Server","engineId":"mssql","category":"ha","title":"SQL Server Always On Availability Group (Windows, WSFC)","summary":"SQL Server 2022가 설치된 Windows 서버로 Always On Availability Group을 구성합니다: Windows Server Failover Cluster(WSFC)와 쿼럼 감시(file share / cloud witness), Always On 활성화, 미러링 엔드포인트와 권한, automatic seeding을 사용하는 AG, 리스너, 동기/비동기 복제본, 읽","level":"고급","duration":"90–150분","topology":"Windows 노드 2–9대 (WSFC) + 감시(witness)","tags":["sql server","mssql","always on","가용성 그룹","ag","wsfc","장애 조치 클러스터","리스너","감시","automatic seeding","hadr","windows","고가용성"],"order":3,"steps":25,"scenarios":[]},{"id":"mssql-ag-linux","file":"install/guides/mssql-ag-linux.json","group":"SQL Server","engineId":"mssql","category":"ha","title":"SQL Server Always On Availability Group (Linux, Pacemaker)","summary":"SQL Server 2022가 설치된 Linux 서버 3대(RHEL 9 또는 Ubuntu 22.04)로 Pacemaker가 관리하는 Always On AG를 구성합니다: mssql-server-ha, hadr.hadrenabled, 인증서로 인증하는 mirroring endpoint, CLUSTER_TYPE = EXTERNAL AG(동기 replica 3개 또는 동기 2개 + configur","level":"고급","duration":"120–180분","topology":"Linux node 3대: 동기 replica 3개 또는 동기 2개 + configuration-only replica","tags":["sql server","mssql","always on","availability group","ag","linux","pacemaker","corosync","pcs","stonith","fencing","mssql-server-ha","ocf:mssql:ag","cluster_type external","configuration only","listener","virtual ip","rhel","ubuntu","고가용성"],"order":4,"steps":30,"scenarios":[]},{"id":"mssql-cu-patch","file":"install/guides/mssql-cu-patch.json","group":"SQL Server","engineId":"mssql","category":"patch","title":"SQL Server Cumulative Update (CU) 적용","summary":"설치된 SQL Server 2022/2025 instance에 같은 메이저 버전 내의 Cumulative Update(CU)를 적용합니다: Linux에서는 dnf/apt로, Windows에서는 CU 설치 파일로 자동(/quiet) 설치합니다. 단일 서버 또는 Always On AG에서의 rolling 방식(secondary 먼저, failover, 그다음 이전 primary, Linux에서는","level":"중급","duration":"30–90분","topology":"서버 1대 또는 AG (replica 2대 이상)","tags":["sql server","mssql","cu","cumulative update","patch","업데이트","rolling","availability group","always on","yum","apt","kb"],"order":4,"steps":12,"scenarios":[]},{"id":"mssql-ag-ops","file":"install/guides/mssql-ag-ops.json","group":"SQL Server","engineId":"mssql","category":"ops","title":"SQL Server Always On AG 운영 (Windows 및 Linux)","summary":"설치 가이드가 아닙니다. 설치된 Always On Availability Group의 일상 운영과 장애 시나리오를 다룹니다: 상태 점검, 지연 및 로그 증가 진단, 계획된 failover, 강제 failover(FORCE_FAILOVER_ALLOW_DATA_LOSS)와 그 후속 작업, 데이터 이동 일시 중단/재개, 동기/비동기 모드, 데이터베이스 추가(automatic seeding 또는 b","level":"고급","duration":"15–90분 (시나리오에 따라 다름)","topology":"replica 2–9개로 구성된 AG (Windows WSFC 또는 Linux Pacemaker)","tags":["sql server","mssql","always on","availability group","ag","failover","forced failover","force_failover_allow_data_loss","suspend","resume","automatic seeding","read-only routing","listener","pacemaker","pcs","wsfc","secondary lag","log_send_queue","redo_queue","required_synchronized_secondaries_to_commit"],"order":5,"steps":3,"scenarios":[{"k":"op","v":"health","l":"AG 상태 점검 (dashboard 쿼리)","m":"모니터링 및 진단 NOT SYNCHRONIZING NOT_HEALTHY DISCONNECTED RESOLVING PARTIALLY_HEALTHY AG 상태 availability group health sys.dm_hadr dashboard replica 상태 Msg 35206 connection timeout AG dashboard Show Dashboard 모두 정상인지 확인합니다. replica / 데이터베이스 상태, failover 준비 상태, 클러스터, 최근 역할 변경을 봅니다. 아무것도 변경하지 않습니다."},{"k":"op","v":"lag","l":"secondary가 뒤처짐 / 로그 파일 증가 (지연 진단)","m":"모니터링 및 진단 AVAILABILITY_REPLICA log_reuse_wait log_send_queue redo_queue HADR_SYNC_COMMIT secondary lag secondary_lag_seconds Msg 9002 9002 transaction log is full 지연 redo blocked 로그 증가 send/redo 큐가 커지거나, secondary의 데이터가 오래되었거나, primary의 로그가 곧 가득 차거나(log_reuse_wait = AVAILABILITY_REPLICA), commit이 느린(HADR_SYNC_COMMIT) 경우입니다."},{"k":"op","v":"failover","l":"계획된 failover (switchover, 데이터 손실 없음)","m":"역할 변경 planned failover manual failover switchover ALTER AVAILABILITY GROUP FAILOVER pcs resource move Msg 41142 41142 Msg 19407 역할 변경 primary 이동 Failover Cluster Manager Move 유지보수 failover 유지보수, patch 또는 테스트를 위해 primary를 동기 모드이며 SYNCHRONIZED 상태인 secondary로 옮깁니다. 몇 초간 중단되며 데이터 손실은 없습니다."},{"k":"op","v":"forced","l":"primary 상실: 강제 failover (FORCE_FAILOVER_ALLOW_DATA_LOSS)","m":"역할 변경 FORCE_FAILOVER_ALLOW_DATA_LOSS forced failover 강제 failover quorum lost quorum 상실 FixQuorum forcequorum disaster recovery DR Msg 41131 Msg 35250 사이트 상실 primary down Msg 47104 ForceQuorum Cluster is running in ForceQuorum state RESOLVING_NORMAL primary 장애 데이터 손실 primary(또는 사이트 전체 / 클러스터 quorum)를 잃었고 기다릴 수 없는 경우입니다. 데이터 손실을 감수하고 secondary를 primary로 만든 뒤, 데이터를 비교하고 다른 replica를 재개합니다."},{"k":"op","v":"suspend","l":"데이터 이동 일시 중단 / 재개 (SUSPEND / RESUME)","m":"유지보수 SUSPENDED SET HADR SUSPEND SET HADR RESUME is_suspended suspend_reason data movement 데이터 이동 중단 Msg 35264 35264 Msg 35265 SUSPEND_FROM_REDO SUSPEND_FROM_USER SUSPEND_FROM_PARTNER 일시 중단됨 일시 중단 secondary로의 데이터 전송을 일시적으로 중단하거나(네트워크 유지보수, 대량 작업) SUSPENDED 상태로 멈춰 있는 데이터베이스를 재개합니다."},{"k":"op","v":"mode","l":"동기 / 비동기 모드 변경","m":"유지보수 SYNCHRONOUS_COMMIT ASYNCHRONOUS_COMMIT availability mode REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT required_synchronized_secondaries_to_commit 동기 비동기 Msg 35211 35211 Msg 35212 35212 FAILOVER_MODE replica를 SYNCHRONOUS_COMMIT ↔ ASYNCHRONOUS_COMMIT으로 전환하고(원격 사이트, 느린 네트워크, DR 훈련) commit 보호(REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT)를 설정합니다."},{"k":"op","v":"adddb","l":"AG에 데이터베이스 추가 (automatic seeding 또는 backup/restore)","m":"변경 ADD DATABASE automatic seeding SET HADR AVAILABILITY GROUP Msg 35250 Msg 1478 1478 seeding failed 데이터베이스 추가 Msg 1475 1475 might contain bulk logged changes manual seeding 새 데이터베이스나 기존 데이터베이스를 AG에 추가합니다. 중소 규모 데이터베이스는 automatic seeding으로, 대용량 데이터베이스나 느린 네트워크에서는 백업으로 준비합니다."},{"k":"op","v":"addrep","l":"replica (node) 추가","m":"변경 ADD REPLICA Add-ClusterNode pcs cluster node add 새 replica replica 추가 Msg 41158 41158 Failed to join local availability replica JOIN AG에 새 secondary를 추가합니다. node가 클러스터에 조인하고, endpoint를 준비한 뒤 ADD REPLICA + JOIN을 수행하면 데이터베이스는 seeding으로 들어옵니다."},{"k":"op","v":"removerep","l":"replica (node) 제거","m":"변경 REMOVE REPLICA Remove-ClusterNode pcs cluster node remove replica 제거 decommission Msg 35223 35223 node 제거 영구 폐기하거나 다시 구축할 secondary를 AG와 클러스터에서 제거합니다."},{"k":"op","v":"routing","l":"읽기 라우팅 (read-only routing)","m":"연결 read-only routing READ_ONLY_ROUTING_URL READ_ONLY_ROUTING_LIST ApplicationIntent=ReadOnly readable secondary 읽기 가능한 secondary Msg 978 978 Msg 976 976 not accessible for queries Msg 10786 ApplicationIntent ApplicationIntent=ReadOnly 연결을 listener를 통해 읽기 가능한 secondary로 라우팅합니다(primary의 보고서 부하 분산)."},{"k":"op","v":"listener","l":"listener 만들기 또는 변경 (IP, 포트)","m":"연결 listener ADD LISTENER MODIFY LISTENER MultiSubnetFailover RegisterAllProvidersIP Msg 19471 19471 Msg 41009 virtualip Msg 19456 19456 HostRecordTTL listener 포트 DNS 레코드 listener가 없으면 만들고, 포트를 변경하거나 (Windows) 두 번째 서브넷용 IP를 추가합니다."}]},{"id":"mssql-backup","file":"install/guides/mssql-backup.json","group":"SQL Server","engineId":"mssql","category":"backup","title":"SQL Server 백업 전략, 자동화 및 복원","summary":"설치된 SQL Server instance(Linux 또는 Windows; 단일 instance, FCI 또는 AG replica)의 백업을 구성하고 백업에서 복구합니다. 백업: 복구 모델 선택, 주간 full + 일간 differential + n분마다 로그 백업, COMPRESSION, CHECKSUM, 인증서 기반 암호화, 보존 기간과 일정(SQL Server Agent T-SQL jo","level":"중급","duration":"30–90분","topology":"서버 1대, Failover Cluster Instance 또는 Always On AG replica(다른 서버로 복원 시: 원본 + 대상)","tags":["sql server","mssql","backup","백업","백업 수행","restore","복원","pitr","stopat","point in time","differential","transaction log","copy_only","compression","checksum","encryption","암호화","sql server agent","job","ola hallengren","maintenance solution","retention","verifyonly","headeronly","filelistonly","norecovery","standby","with move","task scheduler","cron","availability group","always on","backup preference","automated_backup_preference","fn_hadr_backup_is_preferred_replica","copy_only","sql agent","sysjobhistory","cron","task scheduler","page restore","suspect_pages","824","823","33111","18456","orphaned user","sp_help_revlogin","master restore","single user","msdb restore","model restore","stopatmark","tail-log","no_truncate","recovery_pending","suspect","log chain","lsn"],"order":15,"steps":11,"scenarios":[{"k":"mode","v":"strategy","l":"백업 전략 구성(예약 백업 + 보존 기간)","m":"백업 백업 계획 backup strategy 로그 파일 증가 LOG_BACKUP Msg 9002 transaction log is full sql agent job Msg 3013 3013 BACKUP DATABASE is terminating abnormally 백업 안 됨 백업 job 새 서버 또는 정기 백업이 없는 데이터베이스: full / differential / 로그 job, 보존 기간과 모니터링을 구성합니다."},{"k":"mode","v":"oneoff","l":"일회성 백업(예: 변경 전 COPY_ONLY)","m":"백업 copy_only 일회성 백업 변경 전 백업 수동 백업 Msg 4214 4214 Msg 4208 4208 BACKUP LOG is not allowed while the recovery model is SIMPLE 변경, patch 또는 upgrade 전이나 테스트 환경 복사용 단일 백업입니다. COPY_ONLY는 체인을 깨뜨리지 않습니다."},{"k":"mode","v":"verify","l":"백업 검증(기록, VERIFYONLY, 테스트 복원)","m":"리허설 및 점검 VERIFYONLY HEADERONLY FILELISTONLY test restore 백업 손상 여부 Msg 3241 Msg 3183 Msg 3203 백업이 실제로 열리는지 입증하기 위한 절차: 백업 기록, VERIFYONLY, HEADERONLY / FILELISTONLY, 임시 데이터베이스에 테스트 복원 + CHECKDB."},{"k":"mode","v":"chain","l":"백업 체인 무결성 점검(LSN 공백, 누락 파일)","m":"리허설 및 점검 Msg 4305 4305 too recent to apply LSN log chain 체인 끊김 broken log chain Msg 3136 last_log_backup_lsn 백업 체인 복원 전이나 정기 점검 시: msdb 기록에서 체인을 추출하여 LSN 공백, 디스크에 없는 파일, 중간에 끼어든 백업을 찾습니다. 아무것도 변경하지 않습니다."},{"k":"mode","v":"restore","l":"백업에서 데이터베이스 복원(체인, STOPAT / STOPATMARK, MOVE / REPLACE, 다른 서버)","m":"복원 및 시점 복구 STOPAT STOPATMARK STOPBEFOREMARK point in time PITR 실수로 삭제 DELETE 되돌리기 WITH MOVE REPLACE tail-log Msg 3154 Msg 3159 Msg 5133 Msg 1834 Msg 3156 The tail of the log for the database Msg 3101 3101 Exclusive access could not be obtained Msg 4335 4335 RESTORE DATABASE 데이터가 삭제되었거나 잘못 업데이트됨, 데이터베이스 복사본이 필요함 또는 다른 서버로 이전 예정이며 instance는 실행 중인 경우입니다. full + differential + 로그 체인, 특정 시점 또는 표시된 트랜잭션"},{"k":"mode","v":"damaged","l":"데이터베이스가 열리지 않음: 데이터 파일 손실 또는 손상(tail-log + 복원)","m":"손실 / 손상 Msg 945 945 RECOVERY_PENDING recovery pending SUSPECT Msg 5120 5120 Msg 17204 Msg 17207 Msg 5181 FCB::Open failed Operating system error 2 mdf 삭제됨 데이터 파일 손실 database cannot be opened Msg 823 NO_TRUNCATE CONTINUE_AFTER_ERROR EMERGENCY 데이터베이스가 RECOVERY_PENDING 또는 SUSPECT이고 .mdf/.ndf를 읽을 수 없지만 instance는 실행 중인 경우: 먼저 NO_TRUNCATE로 tail-log를 백업한 다음 마지막 시점까지 복원합니다. 작업이 끝날 때까지 데이터베이스는 사용할 수 없습니다."},{"k":"mode","v":"page","l":"일부 페이지 손상(Msg 823 / 824): 페이지 복원","m":"손실 / 손상 Msg 824 824 Msg 823 823 Msg 829 829 suspect_pages torn page incorrect checksum 손상된 페이지 page corruption page restore CHECKDB found Msg 8928 Msg 8939 Msg 8976 repair_allow_data_loss 쿼리나 CHECKDB가 특정 페이지에서 오류를 냄(msdb.dbo.suspect_pages): 손상된 페이지만 백업에서 복원합니다. Enterprise/Developer에서는 데이터베이스가 온라인 상태를 유지하고, Standard/Express에서는 잠시 오프라인이 됩니다."},{"k":"mode","v":"master","l":"master 데이터베이스 복원(단일 사용자 모드)","m":"시스템 데이터베이스 master Msg 3108 3108 single user mode single-user 단일 사용자 모드 -m Msg 3417 3417 Msg 17113 Msg 18461 master 손상 force-setup REBUILDDATABASE master 손상 또는 로그인 / linked server / 서버 설정 손실: instance를 단일 사용자 모드(-m)로 시작하고 master를 복원하면 SQL Server가 스스로 종료됩니다. 다운타임이 필요하며, instance가 전혀 시작되지 않으면 먼저 시스템 데이터베이스를 다시 빌드합니다."},{"k":"mode","v":"sysdb","l":"msdb 또는 model 데이터베이스 복원","m":"시스템 데이터베이스 msdb model Msg 3168 3168 job 사라짐 msdb suspect Msg 1807 msdb 손상 model 손상 job, 백업 기록, Database Mail(msdb) 또는 새 데이터베이스 템플릿(model)이 손상 / 손실된 경우: 같은 빌드에서 만든 백업에서 REPLACE로 복원합니다. Agent가 잠시 중지됩니다."},{"k":"mode","v":"orphan","l":"복원 후 사용자가 접속하지 못함(분리된 사용자, 로그인 매핑)","m":"이전 및 접근 Msg 18456 18456 Login failed for user orphaned user orphan 분리된 사용자 sp_help_revlogin sp_change_users_login Msg 15023 15023 Msg 916 Msg 4060 로그인 매핑 SID 데이터베이스를 다른 서버로 옮겼거나 복원했고 애플리케이션에 'Login failed'가 발생하는 경우: 로그인을 SID와 비밀번호 해시 그대로 원본에서 이전하고, 남은 사용자는 로그인에 연결합니다. 데이터베이스는 온라인 상태를 유지합니다."},{"k":"mode","v":"enc","l":"암호화된 백업이 열리지 않음(인증서 없음, Msg 33111)","m":"이전 및 접근 Msg 33111 33111 Cannot find server certificate with thumbprint 암호화된 백업 encrypted backup 인증서 certificate TDE Msg 15581 15581 master key 백업(또는 TDE 데이터베이스의 백업)이 인증서로 암호화되어 있고 이 서버에 인증서가 없는 경우: thumbprint를 찾고 인증서를 private key와 함께 로드한 뒤 백업을 검증합니다. 기존 데이터베이스는 건드리지 않습니다."}]},{"id":"mssql-bcp-export","file":"install/guides/mssql-bcp-export.json","group":"SQL Server","engineId":"mssql","category":"backup","title":"SQL Server 테이블 단위 내보내기/가져오기 (bcp, sqlpackage)","summary":"백업이 아니라 논리적 데이터 이동입니다: bcp로 테이블을 내보내 다른 데이터베이스에 로드(native 또는 문자 형식, 서식 파일, batch, TABLOCK, ID 열, UTF-8), 쿼리 결과를 CSV/텍스트 파일로 저장(queryout), sqlpackage로 스키마 + 데이터 이동(.bacpac Export/Import, 예: Azure SQL Database로 마이그레이션), 스키","level":"중급","duration":"20–60분","topology":"서버 1대 (명령은 SQL Server 자체나 관리 서버에서 실행)","tags":["sql server","mssql","bcp","대량 복사","내보내기","가져오기","queryout","서식 파일","native","csv","utf-8","sqlpackage","bacpac","dacpac","extract","publish","azure sql","데이터 이동","마이그레이션","테이블 복사"],"order":16,"steps":8,"scenarios":[{"k":"op","v":"table","l":"테이블 이동: bcp out → bcp in","m":"데이터 이동 bcp bcp out bcp in 대량 복사 테이블 복사 테이블 이동 Unexpected EOF encountered in BCP data-file Unable to open BCP host data-file 하나 이상의 테이블을 다른 SQL Server 데이터베이스로 복사합니다. 대상에 테이블이 미리 있어야 합니다."},{"k":"op","v":"query","l":"쿼리 결과를 파일로 저장: bcp queryout (CSV/텍스트)","m":"내보내기 queryout csv csv 내보내기 쿼리 결과 파일 저장 export to file 쿼리 결과를 다른 시스템(Excel, ETL, 보고서)에 넘길 CSV/텍스트 파일로 저장합니다."},{"k":"op","v":"bacpac","l":"스키마 + 데이터: sqlpackage Export / Import (.bacpac)","m":"데이터 이동 bacpac sqlpackage SqlPackage Export SqlPackage Import Azure SQL Database 마이그레이션 SQL71564 SQL71501 스키마와 데이터를 파일 하나로 옮깁니다. 특히 Azure SQL Database로 마이그레이션하거나 중소 규모 데이터베이스에 적합합니다."},{"k":"op","v":"dacpac","l":"스키마만: sqlpackage Extract / Script / Publish (.dacpac)","m":"스키마 dacpac Extract Publish 스키마 비교 schema compare BlockOnPossibleDataLoss The schema update is terminating because data loss might occur 스키마만 추출하거나, 대상과 비교해 변경 스크립트를 생성하거나, 빈 스키마를 새 환경에 배포합니다."}]},{"id":"mssql-security","file":"install/guides/mssql-security.json","group":"SQL Server","engineId":"mssql","category":"security","title":"SQL Server 보안 강화 (Linux 및 Windows: 점검, 네트워크, TLS, 인증, 권한, audit, TDE)","summary":"설치 가이드가 아닙니다. 실행 중인 SQL Server 2019 / 2022 / 2025 인스턴스(Linux 또는 Windows)의 보안 취약점을 찾아 조치합니다. 먼저 읽기 전용 보안 점검이 취약점을 심각도 순으로 나열하고, 각 발견 사항이 어느 카드로 이어지는지 알려 줍니다. 그다음 영역을 하나씩 또는 모두 순서대로 조치합니다: 수신 주소와 firewall, Force Encryption","level":"중급","duration":"5분 (점검) – 3시간 (전체 보안 강화)","topology":"단일 SQL Server 인스턴스 (Linux 또는 Windows) + 애플리케이션 서버; Always On AG는 참고 사항에","tags":["sql server","mssql","보안","security","hardening","보안 강화","cis benchmark","stig","force encryption","tls","인증서","sa","check_policy","xp_cmdshell","trustworthy","server audit","audit","tde","transparent data encryption","backup encryption","mssql-conf","windows","linux"],"order":40,"steps":2,"scenarios":[{"k":"scenario","v":"scan","l":"보안 점검: 취약점은 무엇인가? (읽기 전용)","m":"입문 보안 점검 security audit hardening check CIS STIG 보안 취약점 취약점 vulnerability 모의 해킹 결과 SQL Vulnerability Assessment 점검 쿼리 보안 검사 security scan 치명 가장 먼저 실행할 점검: sa, 비밀번호 정책, 빈 비밀번호, sysadmin 멤버, public/guest 권한, xp_cmdshell, 암호화되지 않은 연결, audit, TDE와 인증서 백업, 포트와 firewall. 아무것도 변경하지 않습니다."},{"k":"scenario","v":"all","l":"전체 보안 강화 (모두 순서대로)","m":"입문 hardening 보안 강화 전체 보안 full hardening CIS benchmark 적용 STIG 새로 설치했거나 보안 강화를 한 번도 하지 않은 인스턴스: 모든 영역을 안전한 순서로 적용합니다. SQL Server를 한두 번 재시작합니다(유지보수 시간 필요)."},{"k":"scenario","v":"net","l":"네트워크와 연결: 누가 어디에서 연결할 수 있는가? (수신 대기, firewall, Browser)","m":"네트워크 및 인증 1433 1434 SQL Browser firewall ListenOnAllIPs network.ipaddress Error 53 Error 40 TCP/IP Error 26 Error locating Server/Instance Specified Login timeout expired A network-related or instance-specific error 외부 접근 접근 제한 SQL Server가 모든 인터페이스에서 수신 대기하고, 1433이 모두에게 열려 있으며, SQL Browser가 불필요하게 실행 중입니다. 수신 주소를 변경하면 재시작이 필요합니다."},{"k":"scenario","v":"tls","l":"연결 암호화 (Force Encryption, 인증서, 클라이언트 검증)","m":"네트워크 및 인증 Force Encryption TrustServerCertificate Encrypt=Mandatory certificate chain was issued by an authority that is not trusted SSL Provider error 0x80090325 tlscert 인증서 만료 TDS 8.0 strict The target principal name is incorrect certificate verify failed Encrypt=Strict Force Strict Encryption Unable to load user-specified certificate 인증서 체인 certificate chain 연결이 암호화되지 않았거나 자체 서명된 임시 인증서를 사용하고, "},{"k":"scenario","v":"auth","l":"인증과 비밀번호 (sa, CHECK_POLICY, Windows 인증)","m":"네트워크 및 인증 sa CHECK_POLICY CHECK_EXPIRATION mixed mode Login failed for user Error 18456 Error 18486 account is locked out 비밀번호 정책 password policy PWDCOMPARE Error 18452 Login is from an untrusted domain Error 18470 The account is disabled Msg 15118 Msg 33062 sa_disabled Windows Authentication sa 활성화, SQL 로그인에 비밀번호 정책 없음, 빈/약한 비밀번호. 개인 DBA 계정 생성과 sa 비활성화. 다운타임 없음(Windows 인증 전용으로 전환할 때만 재시작 필요)."},{"k":"scenario","v":"priv","l":"최소 권한과 노출 영역 (역할, public, guest, xp_cmdshell)","m":"권한 및 감사 추적 db_owner sysadmin public guest xp_cmdshell TRUSTWORTHY CONTROL SERVER least privilege 최소 권한 Msg 262 Msg 229 permission was denied Msg 15247 Msg 916 db_ddladmin EXECUTE AS 권한 보고서 애플리케이션 사용자가 db_owner 또는 sysadmin, public/guest에 추가 권한, xp_cmdshell/TRUSTWORTHY 활성화. 역할 설계와 권한 보고서. 다운타임 없음."},{"k":"scenario","v":"audit","l":"Audit: 누가 무엇을 했는가? (Server Audit, Database Audit Specification)","m":"권한 및 감사 추적 SQL Server Audit server audit audit specification fn_get_audit_file C2 audit login auditing 감사 추적 SIEM 개인정보 보호법 (PIPA) PCI sqlaudit Server Audit Specification Database Audit Specification Msg 33072 누가 무엇을 했는지 감사인이 로그인, 권한 변경, 스키마 변경 기록을 요구합니다. 파일(또는 Windows 이벤트 로그)로 기록하는 Server Audit, 중앙 시스템 전송과 보존. 다운타임 없음. 모든 에디션에서 추가 라이선스 불필요(2016 SP1+)."},{"k":"scenario","v":"tde","l":"저장 데이터 암호화 (TDE)","m":"데이터 및 백업 TDE transparent data encryption database encryption key DEK service master key Msg 33111 Cannot find server certificate with thumbprint encryption at rest Msg 15208 ENCRYPTION_IN_PROGRESS percent_complete Msg 15581 인증서 백업 디스크, 데이터 파일 또는 백업이 다른 서버에서 열리는 것에 대비한 TDE: DMK, 인증서, DEK. 온라인으로 수행합니다. 인증서 백업은 필수입니다(분실하면 데이터베이스와 백업을 열 수 없습니다). **라이선스:** SQL Server 2019+ Standard와 Enterprise에서 사용 가능하"},{"k":"scenario","v":"backup","l":"백업 보안 (암호화된 백업, 인증서, 권한)","m":"데이터 및 백업 backup encryption 백업 암호화 WITH ENCRYPTION BACKUP CERTIFICATE 백업 권한 Msg 33111 Msg 15151 RESTORE VERIFYONLY key_algorithm Msg 15240 백업이 암호화되지 않았고, 백업 디렉터리가 모두에게 열려 있으며, 스크립트에 비밀번호가 평문으로 들어 있습니다. BACKUP … WITH ENCRYPTION, 인증서 백업, 디렉터리 권한. 다운타임 없음. 백업 암호화는 Standard와 Enterprise에서 사용 가능하며 Express에는 없습니다."},{"k":"scenario","v":"os","l":"운영 체제 측면 (서비스 계정, 파일 권한, SELinux)","m":"서버 및 버전 service account 서비스 계정 LocalSystem gMSA virtual account icacls SELinux mssql 사용자 NT Service\\MSSQLSERVER mssql-server-selinux unconfined_service_t Configuration Manager Log On 서비스가 LocalSystem 또는 관리자 계정으로 실행되고, 데이터/백업 디렉터리가 모두에게 열려 있으며, SELinux가 비활성화되어 있습니다. 서비스 계정을 변경하면 재시작이 필요합니다."},{"k":"scenario","v":"patch","l":"패치와 버전: 지원되는 버전이며 최신 CU인가?","m":"서버 및 버전 CU cumulative update GDR EOL end of support 지원 종료 CVE 보안 업데이트 ProductUpdateLevel ProductUpdateReference Latest updates 패치 일정 버전, CU 수준, 지원 종료(2016은 2026년 7월에 종료). 적용은 CU 패치 가이드로 연결됩니다. 읽기 전용."}]},{"id":"mongo-replset","file":"install/guides/mongo-replset.json","group":"MongoDB","engineId":"mongodb","category":"ha","title":"MongoDB replica set 설치(멤버 3개, keyfile 및 인증)","summary":"3개 멤버로 구성된 MongoDB 8.0(또는 7.0) replica set을 처음부터 구축합니다: 공식 저장소, XFS 데이터 디렉터리, RAM에 맞춘 WiredTiger cache, keyfile을 이용한 멤버 간 인증, 권한 부여, 관리자 및 애플리케이션 사용자, read/write concern, failover 테스트, 연결 문자열. 완료하면 멤버 하나가 손실되어도 중단 없이 견디고","level":"중급","duration":"60–90분","topology":"서버 3대(primary 1 + secondary 2), 데이터를 가진 멤버는 홀수","tags":["mongodb","mongo","replica set","replset","rs.initiate","keyfile","failover","stepdown","write concern","wiredtiger","고가용성","ha","rhel","ubuntu"],"order":1,"steps":36,"scenarios":[]},{"id":"mongo-sharded","file":"install/guides/mongo-sharded.json","group":"MongoDB","engineId":"mongodb","category":"ha","title":"MongoDB sharded cluster 설치 (config server + shard + mongos)","summary":"데이터나 쓰기 부하가 단일 replica set의 한계를 넘을 때 구성하는 sharded cluster입니다. 3개 멤버의 config server replica set, 2개 또는 3개의 shard replica set, 2개의 mongos router로 구성하며, keyfile 기반 내부 인증, 권한 부여(authorization), sh.addShard, shard key 선택, 컬렉션","level":"고급","duration":"2–3시간","topology":"config server 3대 + shard 멤버 2×3대 + mongos 2대 (최소 11대, 테스트용은 더 적게)","tags":["mongodb","sharding","sharded cluster","mongos","config server","shard key","addShard","balancer","hashed","확장","rhel","ubuntu"],"order":2,"steps":60,"scenarios":[]},{"id":"mongo-upgrade","file":"install/guides/mongo-upgrade.json","group":"MongoDB","engineId":"mongodb","category":"upgrade","title":"MongoDB rolling upgrade (7.0 → 8.0): replica set 및 sharded cluster","summary":"운영 중인 MongoDB 7.0 replica set 또는 sharded cluster를 서비스 중단 없이(rolling) 8.0으로 업그레이드합니다. 사전 점검(버전, FCV, 멤버 상태, 드라이버 호환성), 백업, secondary 먼저, 이어서 stepDown과 기존 primary 순서로 진행하며, sharded cluster에서는 balancer 중지, config server, sh","level":"고급","duration":"60–120분 (데이터 크기와 무관, 멤버당 몇 분)","topology":"Replica set (멤버 3개 이상) 또는 sharded cluster (config server + shard + mongos)","tags":["mongodb","upgrade","업그레이드","rolling upgrade","8.0","7.0","featureCompatibilityVersion","fcv","stepdown","replica set","sharded","balancer","config server","mongos","fcv","featureCompatibilityVersion","rolling upgrade"],"order":3,"steps":30,"scenarios":[]},{"id":"mongo-backup","file":"install/guides/mongo-backup.json","group":"MongoDB","engineId":"mongodb","category":"backup","title":"MongoDB 백업 및 복원 (mongodump, mongorestore, snapshot)","summary":"설치된 MongoDB에 백업 체계를 구성하고, 백업에서 복구하는 방법을 시나리오별로 설명합니다. 백업: mongodump로 단일 collection, 데이터베이스 또는 deployment 전체 백업(--archive --gzip, replica set에서는 --oplog로 일관된 시점 확보, secondary에서 읽기), 자격 증명 파일, systemd timer/cron을 이용한 보존 기간","level":"중급","duration":"45–90분","topology":"백업 서버 또는 멤버 하나 + replica set / 단일 instance / sharded cluster (mongos)","tags":["mongodb","mongo","백업","backup","restore","복원","mongodump","mongorestore","oplog","oplogReplay","nsFrom","nsTo","mongoexport","mongoimport","fsyncLock","lvm snapshot","systemd timer","cron","retention","balancer","activeWindow","config server","cron","systemd timer","flock","oplog","복원 시나리오","point in time recovery","pitr","oplogLimit","nsInclude","dryRun","sharded restore","클론","삭제된 collection","실수로 삭제"],"order":15,"steps":20,"scenarios":[{"k":"scenario","v":"setup","l":"백업 구성: mongodump, 예약 실행, 테스트 복원","m":"백업 mongodump 백업 구성 backup setup systemd timer cron retention mongorestore backup user backup.yaml Failed: can't create session oplog overflow mongodump was unable to capture all new oplog entries 백업 백업 사용자, 자격 증명 파일, 수동 및 예약 mongodump, 이름을 바꿔 수행하는 테스트 복원, 선택적 snapshot 및 mongoexport. 처음 구성하는 경우 여기서 시작하십시오."},{"k":"scenario","v":"r_dry","l":"백업을 실제로 복원할 수 있는가? (--dryRun 및 임시 instance에서 리허설)","m":"리허설 및 점검 dryRun --dryRun restore test 복원 리허설 백업 검증 backup verify gzip: invalid compressed data unexpected EOF archive is corrupt 손상된 아카이브 gzip -t 백업 복원 가능 여부 운영 데이터에 영향 없이 아카이브 무결성, 포함된 namespace와 oplog를 확인하고, 필요하면 임시 mongod에 전체 복원한 뒤 문서 수를 셉니다."},{"k":"scenario","v":"r_full","l":"백업에서 전체 데이터 복원 (전체 복원, --drop)","m":"복원 full restore 전체 복원 데이터베이스 전체 복원 --drop mongorestore --drop 데이터 유실 data loss ransomware 랜섬웨어 전체 데이터 삭제됨 dropDatabase no oplog file to replay oplogReplay applyOps not authorized … applyOps admin.tempusers 데이터 대부분이 유실되거나 손상되어 deployment를 백업 시점으로 되돌립니다. 백업 이후의 쓰기는 유실되며, 복원하는 동안 애플리케이션이 중단됩니다."},{"k":"scenario","v":"r_ns","l":"단일 데이터베이스 또는 collection 복구 (--nsInclude, 새 이름으로 또는 원래 위치에)","m":"복원 --nsInclude --nsFrom --nsTo 단일 collection single collection restore collection 삭제됨 collection dropped drop collection 삭제된 문서 deleted documents deleteMany namespace restore E11000 duplicate key error $merge orders_restored 단일 데이터베이스 복원 collection/데이터베이스 하나가 삭제되거나 손상되었고 나머지는 정상인 경우: 해당 namespace만 백업에서 가져오며, 다른 데이터는 건드리지 않고 애플리케이션도 계속 운영할 수 있습니다."},{"k":"scenario","v":"r_pitr","l":"특정 시점으로 복구: 백업 + oplog (실수로 drop, 잘못된 update/delete)","m":"특정 시점 복구 point in time PITR 특정 시점 복구 --oplogReplay --oplogLimit oplog replay oplog.rs 실수로 삭제 accidental drop 잘못된 update wrong update drop 이전 Timestamp oplog 순환됨 rollback to time 특정 시각으로 복구 잘못된 작업이 백업 이후에 발생했고 그 시점까지의 쓰기도 필요한 경우: 마지막 전체 백업을 복원한 뒤 운영 oplog를 잘못된 작업 직전까지 적용합니다(--oplogReplay --oplogLimit). replica set에만 해당합니다."},{"k":"scenario","v":"r_other","l":"다른 cluster / 서버에 백업 복원 (클론, 테스트 환경, 이전)","m":"이전 및 클론 clone 클론 다른 서버에 복원 restore to another cluster migration 서버 이전 테스트 환경 refresh nsExclude admin.system.users Invalid feature compatibility version cannot use --oplogReplay with excludes specified 테스트 환경으로 복사 원본 deployment는 건드리지 않습니다. 백업을 다른 replica set이나 instance에 복원합니다(테스트/리포팅용 복사본, 서버 이전, 버전 업그레이드). 사용자 이전은 선택 사항입니다."},{"k":"scenario","v":"r_shard","l":"Sharded cluster 복원: mongodump의 한계와 올바른 방법","m":"이전 및 클론 sharded restore sharded cluster restore mongos restore shard key shardCollection config server restore balancer inconsistent 일관성 없는 백업 PBM Percona Backup Ops Manager --oplog mode only supported on full dumps mongos를 통한 백업 백업한 구성이 sharded cluster인 경우: mongos를 통한 논리적 복원(shard key 재생성, balancer) 또는 조정된 snapshot으로 cluster 전체를 물리적으로 재구성합니다."}]},{"id":"mongo-ops","file":"install/guides/mongo-ops.json","group":"MongoDB","engineId":"mongodb","category":"ops","title":"MongoDB 일상 운영 및 장애 시나리오 (replica set 및 sharded cluster)","summary":"설치 가이드가 아닙니다. 이미 설치된 MongoDB replica set과 sharded cluster를 일상적으로 운영하고, 문제가 생겼을 때 복구하기 위한 시나리오 기반 runbook입니다. 상태 점검, 멤버 추가/제거, priority/hidden/delayed 멤버, arbiter를 데이터 멤버로 교체, 뒤처진 멤버 재동기화, 과반수 상실 시 force reconfig, 계획된 ste","level":"고급","duration":"5분(점검) – 수 시간(initial sync, drain, reshard)","topology":"Replica set(멤버 3개 이상) 또는 sharded cluster(config server + shard replica set + mongos)","tags":["mongodb","mongo","replica set","sharded cluster","rs.status","rs.add","rs.remove","rs.reconfig","force reconfig","stepDown","priority","hidden","delayed","secondaryDelaySecs","arbiter","resync","initial sync","too stale","oplog","replSetResizeOplog","FCV","setFeatureCompatibilityVersion","addShard","removeShard","movePrimary","balancer","activeWindow","jumbo","chunk","reshardCollection","shard key","운영","장애"],"order":20,"steps":3,"scenarios":[{"k":"scenario","v":"health","l":"상태 점검: 멤버, 지연, oplog 윈도우(sharded 구성에서는 shard와 balancer 포함)","m":"모니터링 및 점검 rs.status rs.status() replication lag 지연 replLag printSecondaryReplicationInfo sh.status health 상태 점검 NotYetInitialized RECOVERING STARTUP2 UNKNOWN (not reachable/healthy) DOWN ReplicaSetNoPrimary Server selection timed out lastHeartbeatMessage 읽기 전용: 멤버 상태, 복제 지연, oplog 윈도우, 구성. sharded cluster에서는 shard 목록, balancer, chunk 분포도 확인합니다. 무엇을 해야 할지 확실하지 않으면 여기서 시작하십시오."},{"k":"scenario","v":"add","l":"Replica set에 새 멤버 추가 (initial sync)","m":"멤버 관리 rs.add 멤버 추가 add member initial sync 새 secondary newlyAdded STARTUP2 scale out rs.add() Quorum check failed Found two member configurations with same host field votes: 0 새 서버가 준비되었습니다(동일 버전, keyfile, replSetName). priority 0 / votes 0으로 추가한 뒤, initial sync가 끝나고 SECONDARY가 되면 투표권과 priority를 부여합니다. 다운타임은 없습니다."},{"k":"scenario","v":"remove","l":"Replica set에서 멤버 제거","m":"멤버 관리 rs.remove 멤버 제거 remove member decommission 서버 제거 REMOVED Our replica set config is invalid or we are not a member of it rs.remove() 서버를 폐기하거나 영구적으로 잃은 경우입니다. 멤버를 구성에서 제거하고, 남은 투표 수가 과반수를 유지하는지 확인합니다. Primary라면 먼저 stepDown합니다."},{"k":"scenario","v":"member_cfg","l":"멤버 역할 변경: priority, hidden, delayed 멤버","m":"멤버 관리 priority hidden delayed secondaryDelaySecs slaveDelay 숨김 멤버 지연 멤버 votes rs.reconfig priority must be 0 when hidden=true only one voting member can be added or removed at a time rs.reconfig() 멤버가 primary가 될 우선순위를 바꾸거나, 리포팅/백업용 숨김(hidden) 멤버 또는 실수로 인한 삭제에 대비한 지연(delayed) 멤버로 만들거나, 일반 멤버로 되돌립니다."},{"k":"scenario","v":"arbiter","l":"Arbiter를 데이터 멤버로 교체 (PSA → PSS)","m":"멤버 관리 arbiter arbiterOnly PSA rs.addArb majority write concern cache pressure w: majority 대기 Rejecting reconfig where the new config has a PSA topology implicit default write concern majority commit point Primary + secondary + arbiter 구성의 replica set에서는 데이터 멤버 하나가 다운되면 majority 쓰기와 cache 압박이 문제가 됩니다. 새 데이터 멤버를 추가하고 arbiter를 제거합니다."},{"k":"scenario","v":"resync","l":"뒤처지거나 손상된 멤버 재동기화 (resync)","m":"장애 및 복구 too stale too stale to catch up RS102 resync 재동기화 initial sync RECOVERING WiredTiger error WT_PANIC 손상된 데이터 파일 data corruption OplogStartMissing maintenance mode InitialSyncFailure fassert checksum mismatch 멤버가 RECOVERING 상태에 머물며 'too stale to catch up'을 보고하거나 데이터 파일이 손상된 경우입니다. 멤버의 데이터 디렉터리를 비우고 initial sync로 처음부터 다시 동기화합니다. 다른 멤버는 계속 동작합니다."},{"k":"scenario","v":"force","l":"과반수 상실: 남은 멤버로 강제 재구성 (force reconfig)","m":"장애 및 복구 force reconfig force: true no primary primary 없음 과반수 상실 majority lost NotWritablePrimary not master no replset config split brain DR site 사이트 상실 ReplicaSetNoPrimary Server selection timed out rs.reconfig force 멤버 대부분을 영구적으로 잃었고(사이트/서버 상실) 남은 멤버가 primary를 선출하지 못하는 경우입니다. 남은 멤버로 rs.reconfig(cfg, {force: true})를 실행합니다. 최후의 수단이며, 잘못 사용하면 데이터 손실과 split brain 위험이 있습니다."},{"k":"scenario","v":"stepdown","l":"계획된 primary 변경 (rs.stepDown, 특정 멤버를 primary로 지정)","m":"유지보수 stepDown rs.stepDown replSetStepDown primary 변경 switchover 계획된 유지보수 maintenance rs.freeze No electable secondaries caught up InterruptedDueToReplStateChange PrimarySteppedDown primary 전환 Primary 서버에 유지보수/패치가 예정된 경우입니다. Primary가 물러나고 따라잡은 secondary가 primary가 됩니다. 애플리케이션에서 몇 초간 쓰기 오류가 발생할 수 있습니다."},{"k":"scenario","v":"oplog","l":"Oplog 크기 변경 (replSetResizeOplog)","m":"유지보수 oplog oplog window oplog 윈도우 replSetResizeOplog oplogSizeMB oplogMinRetentionHours too stale oplog overflow rs.printReplicationInfo getReplicationInfo log length start to end Oplog 윈도우가 짧은 경우입니다. 유지보수 후 secondary가 'too stale' 상태가 되거나, 백업이 oplog를 따라잡지 못하거나, 시점 복구에 충분하지 않습니다. 각 멤버에서 다운타임 없이 크기를 늘립니다."},{"k":"scenario","v":"fcv","l":"Feature Compatibility Version(FCV) 확인 및 완료","m":"유지보수 FCV featureCompatibilityVersion setFeatureCompatibilityVersion Invalid feature compatibility version IncompatibleServerVersion 업그레이드 후 UPGRADE PROBLEM requires confirm: true getParameter featureCompatibilityVersion 업그레이드 후 FCV가 이전 버전에 머물러 있거나 멤버 간 버전이 다른 경우입니다. 모든 멤버의 바이너리 버전과 FCV를 확인하고 FCV를 새 버전으로 올립니다."},{"k":"scenario","v":"addshard","l":"Cluster에 shard 추가 (sh.addShard)","m":"Sharded cluster addShard sh.addShard shard 추가 add shard scale out 새 shard shardAdded is not running with --shardsvr in use by another shard 용량이 부족한 경우입니다. 새 shard replica set(shardsvr)을 구축해 cluster에 추가하면 balancer가 chunk를 점진적으로 이동합니다."},{"k":"scenario","v":"rmshard","l":"Shard 제거 (removeShard: drain 및 movePrimary)","m":"Sharded cluster removeShard remove shard shard 제거 drain draining movePrimary dbsToMove jumboChunks moveCollection collectionsToMove ShardNotFound draining started successfully Shard를 폐기하는 경우입니다. chunk를 다른 shard로 비우고(drain), 이 shard가 primary shard인 데이터베이스를 이동(movePrimary)한 다음 cluster에서 제거합니다."},{"k":"scenario","v":"balancer","l":"Balancer: 시간 윈도우, 중지 / 시작, 컬렉션 단위 비활성화","m":"Sharded cluster balancer activeWindow stopBalancer startBalancer sh.stopBalancer disableBalancing balancer 윈도우 chunk migration moveChunk isBalancerRunning getBalancerState Balancer timed out Chunk 마이그레이션이 피크 시간에 부하를 주거나, 유지보수/백업 중에 마이그레이션을 멈춰야 하거나, 특정 컬렉션을 밸런싱 대상에서 빼야 하는 경우입니다."},{"k":"scenario","v":"chunks","l":"분포 불균형 또는 jumbo chunk (진단 및 해결)","m":"Sharded cluster jumbo jumbo chunk chunk too big ChunkTooBig 불균형 imbalance hot shard balancerCollectionStatus clearJumboFlag moveChunk failed getShardDistribution low cardinality splitFind Cannot split on the boundary jumboChunks 한 shard가 다른 shard보다 훨씬 많이 차 있거나, balancer가 이동하지 않거나, 'jumbo' 표시된 chunk가 있거나, moveChunk가 실패하는 경우입니다. 분포, jumbo chunk, balancer 오류를 조사합니다."},{"k":"scenario","v":"reshard","l":"Shard key 변경 (reshardCollection)","m":"Sharded cluster reshardCollection reshard shard key 변경 change shard key refineCollectionShardKey abortReshardCollection commitReshardCollection forceRedistribution ReshardCollectionInProgress ReshardCollectionAborted unshardCollection 현재 shard key가 hot shard, jumbo chunk 또는 scatter-gather 쿼리를 유발하는 경우입니다. 컬렉션을 새 shard key로 온라인 재분배합니다(5.0+). 마지막에 쓰기가 약 2초 차단됩니다."},{"k":"mcfg","v":"priority","l":"Priority 변경 (primary 선출 우선순위)","m":"멤버가 primary가 될 우선순위를 바꿉니다(예: 주 데이터 센터의 멤버를 우선, DR 멤버는 0). 선출이 발생할 수 있으며 짧은 쓰기 중단이 생길 수 있습니다."},{"k":"mcfg","v":"hidden","l":"숨김(hidden) 멤버: 애플리케이션에 보이지 않음, 백업/리포팅용","m":"멤버가 애플리케이션에 보이지 않으며(읽기가 라우팅되지 않음) 리포팅, 분석 또는 백업 전용으로 사용됩니다. Priority가 0이 되어 primary가 될 수 없습니다."},{"k":"mcfg","v":"delayed","l":"지연(delayed) 멤버: 실수로 인한 삭제 대비","m":"멤버가 지정한 시간만큼 늦게 oplog를 적용합니다. 실수로 삭제한 데이터가 그 시간 동안 이 멤버에 남아 있습니다. hidden, priority 0, votes 0이 됩니다."},{"k":"mcfg","v":"normal","l":"일반 멤버로 복귀 (priority 1, 표시, 지연 없음, 투표)","m":"이전 역할을 되돌립니다: priority 1, 표시, 지연 없음, 투표."},{"k":"bal_op","v":"window","l":"시간 윈도우 정의 (activeWindow)","m":"마이그레이션을 특정 시간대(예: 야간)에만 수행합니다. 주간에는 부하가 발생하지 않습니다."},{"k":"bal_op","v":"unwindow","l":"시간 윈도우 제거 (항상 실행)","m":"시간 윈도우를 제거합니다. balancer가 필요할 때 언제든 이동합니다(예: 새 shard 추가 후 빠른 균형 조정)."},{"k":"bal_op","v":"stop","l":"Balancer 중지 (유지보수/백업)","m":"백업, 업그레이드, 유지보수 전에 사용합니다. 진행 중인 마이그레이션은 완료되고 새 마이그레이션은 시작되지 않습니다. 작업이 끝나면 반드시 다시 시작하십시오."},{"k":"bal_op","v":"start","l":"Balancer 시작","m":"중지된 balancer를 활성화합니다. 밸런싱이 중단된 지점부터 재개됩니다."},{"k":"bal_op","v":"coll_off","l":"단일 컬렉션 밸런싱 비활성화","m":"한 컬렉션의 chunk만 이동하지 않습니다(의도적 배치, 문제 있는 컬렉션). 나머지 컬렉션은 계속 밸런싱됩니다."},{"k":"bal_op","v":"coll_on","l":"단일 컬렉션 밸런싱 활성화","m":"컬렉션에 대해 비활성화했던 밸런싱을 다시 활성화합니다."}]},{"id":"mongo-security","file":"install/guides/mongo-security.json","group":"MongoDB","engineId":"mongodb","category":"security","title":"MongoDB 보안 강화 (권한 부여, TLS, 역할, 감사)","summary":"운영 중인 MongoDB(단일 서버, 레플리카 세트 또는 샤드 클러스터)를 무중단으로 보안 강화하는 시나리오 기반 runbook입니다. 읽기 전용 보안 점검, keyFile + transitionToAuth를 이용한 롤링 권한 부여, 최소 권한 custom role과 사용자, SCRAM-SHA-256, bindIp와 방화벽, allowTLS → preferTLS → requireTLS 전환,","level":"고급","duration":"10분(점검) – 2시간(전체 보안 강화)","topology":"단일 서버, 3개 이상 멤버의 레플리카 세트 또는 샤드 클러스터","tags":["mongodb 보안","mongodb security","hardening","보안 강화","CIS","authorization","keyFile","transitionToAuth","TLS","requireTLS","x.509","SCRAM-SHA-256","custom role","least privilege","bindIp","audit log","auditLog","encryption at rest","LUKS","Percona"],"order":20,"steps":8,"scenarios":[{"k":"op","v":"audit","l":"보안 상태 점검 (읽기 전용)","m":"점검 보안 점검 security audit security checklist CIS MongoDB hardening 보안 강화 Access control is not enabled for the database 인증 비활성 여부 bindIp 0.0.0.0 exposed mongodb 인터넷에 노출된 mongodb ransom 랜섬웨어 getCmdLineOpts security checklist MongoDB 점검 보고서 아무것도 변경하지 않습니다. 인증 없는 접근, 수신 대기 주소, TLS, 인증 메커니즘, 고권한 사용자, 감사 및 encryption at rest 상태를 OK / WARN / RISK로 보고합니다. 어디서 시작해야 할지 모르면 이 항목을 선택하십시오."},{"k":"op","v":"all","l":"전체 보안 강화 (모두 순서대로)","m":"점검 전체 보안 강화 full hardening CIS benchmark 보안 프로젝트 모의 해킹 결과 점검 결과 점검 → internal auth + 권한 부여(transitionToAuth로 무중단) → 최소 권한 사용자 → SCRAM-SHA-256 → bindIp/방화벽 → TLS(allowTLS → preferTLS → requireTLS) → 감사(Enterprise/Percona) → 최종 점검. 레플리카 세트에서는 롤링으로 진행하며, 위험한 단계마다 확인을 요청합니다."},{"k":"op","v":"auth","l":"권한 부여 비활성: authorization 활성화 (keyFile, 무중단)","m":"인증과 권한 Access control is not enabled for the database Read and write access to data and configuration is unrestricted authorization disabled authorization: enabled 인증 비활성 no authentication transitionToAuth keyFile internal authentication 멤버 간 인증 인증 없는 접근 permissions on /etc/mongod.keyfile are too open Authentication failed __system localhost exception 누구나 사용자 이름과 비밀번호 없이 접속할 수 있습니다. 레플리카 세트와 샤드 클러"},{"k":"op","v":"users","l":"최소 권한 사용자와 custom role (애플리케이션, 백업, 모니터링)","m":"인증과 권한 least privilege 최소 권한 custom role createRole root 사용자 애플리케이션이 root로 접속 not authorized on Unauthorized command find requires authentication requires authentication readWrite clusterMonitor backup 역할 createUser grantRolesToUser monitor_user backup_user 애플리케이션이 root 또는 공용 관리자 계정으로 접속하고 있습니다. 애플리케이션에 자신의 데이터베이스에서 필요한 작업만 허용하는 custom role과 백업·모니터링용 별도 사용자를 생성하고, cron/애플리케이션 측에서 비밀번호를 안전하게 보관하는 방"},{"k":"op","v":"scram","l":"SCRAM-SHA-1 비활성화, SCRAM-SHA-256만 사용","m":"인증과 권한 SCRAM-SHA-1 SCRAM-SHA-256 authenticationMechanisms Received authentication for mechanism SCRAM-SHA-1 which is not enabled Unable to use SCRAM-SHA-256 based authentication for user without any SCRAM-SHA-256 credentials registered MD5 mechanism 기존 사용자의 자격 증명이 SHA-1로 저장되어 있거나 점검에서 SCRAM-SHA-1이 활성화된 것으로 나왔습니다. 사용자를 SHA-256으로 이전한 뒤 롤링 재시작으로 `authenticationMechanisms`를 좁힙니다."},{"k":"op","v":"bind","l":"네트워크 접근 제한 (bindIp 0.0.0.0, 방화벽)","m":"네트워크와 암호화 bindIp 0.0.0.0 bindIpAll bind_ip_all 0.0.0.0:27017 포트 27017 열림 shodan 인터넷에 노출 firewall javascriptEnabled noscripting net.bindIp ufw allow 27017 firewall-cmd 27017 mongod가 모든 인터페이스(0.0.0.0, `bindIpAll`)에서 수신 대기하거나 포트가 모두에게 열려 있습니다. bindIp를 localhost + 서버의 내부 IP로 줄이고, 방화벽은 멤버와 애플리케이션 네트워크만 허용합니다. 롤링, 무중단."},{"k":"op","v":"tls","l":"연결 암호화: TLS (allowTLS → preferTLS → requireTLS)","m":"네트워크와 암호화 TLS SSL net.tls.mode requireTLS preferTLS allowTLS 암호화되지 않은 연결 cleartext no SSL certificate provided by peer SSL peer certificate validation failed certificate verify failed x.509 x509 clusterAuthMode Hostname/IP does not match certificate's altnames self-signed certificate in certificate chain rotateCertificates allowConnectionsWithoutCertificates 애플리케이션과 멤버 사이, 멤버 간 트래픽이 암호화되지 않습니다. 인증"},{"k":"op","v":"auditlog","l":"감사 로그 활성화 (Enterprise / Percona)","m":"감사 추적과 데이터 보호 audit log auditLog 감사 기록 누가 삭제했는지 who dropped Unrecognized option: auditLog unrecognised option '--auditDestination' 개인정보 보호법 (PIPA) PCI DSS 로그 기록 누가 언제 로그인했는지, 사용자/역할 변경과 drop 작업을 기록해야 합니다. MongoDB Enterprise와 Percona Server for MongoDB에서는 `auditLog`로 활성화하며, Community에는 이 기능이 없습니다(대안을 안내합니다)."},{"k":"op","v":"ear","l":"Encryption at rest (디스크의 데이터 암호화)","m":"감사 추적과 데이터 보호 encryption at rest 디스크 암호화 enableEncryption encryptionKeyFile KMIP Vault LUKS dm-crypt 데이터 암호화 Unrecognized option: security.enableEncryption Unable to retrieve key Unable to use encryption at rest cryptsetup luksFormat 디스크나 백업을 도난당해도 데이터를 읽을 수 없어야 합니다. Enterprise/Percona에서는 멤버를 하나씩 initial sync로 다시 구축하여 WiredTiger 암호화를 활성화하고, Community에서는 데이터 디스크를 LUKS로 암호화합니다."}]},{"id":"redis-sentinel","file":"install/guides/redis-sentinel.json","group":"Redis","engineId":"redis","category":"ha","title":"Redis Sentinel 고가용성 구성 (master + replica + Sentinel 3개)","summary":"서버 3대에 Redis 7.2 / 8.2 또는 Valkey 고가용성 환경을 처음부터 구성합니다: master 1대, replica 2대, 그리고 서버마다 Sentinel 1개. RAM 기준 maxmemory, 영속성(RDB/AOF), 비밀번호, 커널 설정, quorum과 타임아웃, 계획된 페일오버 테스트, Sentinel 지원 클라이언트 연결을 단계별로 설정합니다. 완료하면 master가 다","level":"중급","duration":"45–60분","topology":"서버 3대: 각 서버에 Redis(master 1 + replica 2)와 Sentinel","tags":["redis","valkey","센티널","레플리카","복제","페일오버","쿼럼","고가용성","ha","캐시","rhel","ubuntu"],"order":1,"steps":27,"scenarios":[]},{"id":"redis-cluster","file":"install/guides/redis-cluster.json","group":"Redis","engineId":"redis","category":"ha","title":"Redis Cluster 설치 (master 3 + replica 3)","summary":"Redis Cluster를 처음부터 구성합니다(Redis 7.2 / 8.2 또는 Valkey): 16384개 hash slot을 master 3개(또는 그 이상)에 나누고, 각 master에 다른 서버의 replica를 하나씩 둡니다. 서버당 인스턴스 1개(서버 6대) 또는 2개(서버 3대) 배치, 인스턴스별 systemd 서비스, 비밀번호, cluster 생성, 점검, failover 테스","level":"중급","duration":"45–60분","topology":"인스턴스 6개: 서버 6대 × 1 또는 서버 3대 × 2","tags":["redis","valkey","cluster","redis cluster","sharding","hash slot","reshard","failover","replica","확장","rhel","ubuntu"],"order":2,"steps":33,"scenarios":[]},{"id":"redis-backup","file":"install/guides/redis-backup.json","group":"Redis","engineId":"redis","category":"backup","title":"Redis 백업과 복원 (RDB, AOF, replica, cluster)","summary":"설치된 Redis 또는 Valkey(단일 인스턴스, Sentinel을 쓰는 master/replica 또는 Redis Cluster)의 백업 체계를 구성하고 백업에서 복원하는 방법을 설명합니다. RDB(save 규칙, BGSAVE, LASTSAVE, dump.rdb 복사)와 AOF(BGREWRITEAOF, 7.x의 다중 파일 appendonlydir) 백업, replica에서의 백업, Clu","level":"중급","duration":"30–60분","topology":"단일 인스턴스, replica 또는 Redis Cluster 서버 (Redis 7.2 / 8.x, Valkey)","tags":["redis","valkey","백업","backup","restore","복원","rdb","dump.rdb","bgsave","lastsave","aof","appendonly","bgrewriteaof","appendonlydir","redis cluster","systemd timer","cron","retention","cron","systemd timer","flock","REDISCLI_AUTH","sentinel"],"order":15,"steps":19,"scenarios":[]},{"id":"redis-ops","file":"install/guides/redis-ops.json","group":"Redis","engineId":"redis","category":"ops","title":"Redis / Valkey 일상 운영과 장애 시나리오 (Cluster와 Sentinel)","summary":"설치 가이드가 아닙니다: 설치된 Redis Cluster나 Sentinel 기반 master/replica 구성을 일상적으로 관리하고, 문제가 생겼을 때 복구하기 위한 시나리오별 런북입니다(Redis 7.2, Redis 8, Valkey). Cluster: 상태 점검, 노드 추가/제거, rebalance/reshard, 수동 failover(FORCE/TAKEOVER 차이), 장애 노드 교체","level":"고급","duration":"5분(점검) – 1시간(reshard)","topology":"Redis Cluster (master 3대 이상 + replica) 또는 Sentinel (master 1대 + replica + Sentinel 3대)","tags":["redis","valkey","redis cluster","sentinel","redis-cli --cluster","cluster check","add-node","del-node","reshard","rebalance","CLUSTER FAILOVER","FORCE","TAKEOVER","CLUSTER FORGET","cluster fix","SENTINEL FAILOVER","SENTINEL RESET","quorum","CKQUORUM","INFO memory","MEMORY DOCTOR","eviction","maxmemory","bigkeys","memkeys","hotkeys","운영","장애"],"order":20,"steps":3,"scenarios":[{"k":"scenario","v":"c_health","l":"Cluster 상태 점검 (--cluster check, CLUSTER INFO / NODES)","m":"Redis Cluster: 모니터링 cluster_state:fail CLUSTERDOWN CLUSTERDOWN Hash slot not served --cluster check CLUSTER INFO CLUSTER NODES fail? pfail slots not covered Not all 16384 slots are covered [ERR] Nodes don't agree about configuration MOVED ASK CLUSTERDOWN The cluster is down cluster_slots_fail 클러스터 상태 cluster health cluster_state 읽기 전용: 모든 slot이 할당되어 있는지, 노드들의 설정이 일치하는지, 다운(fail)된 노드나 replica 없는 ma"},{"k":"scenario","v":"c_add","l":"Cluster에 노드 추가 (master 또는 replica)","m":"Redis Cluster: 노드 관리 add-node --cluster add-node 노드 추가 add node scale out --cluster-slave --cluster-replica cluster-master-id 새 master 새 replica Node is not empty. Either the node already knows other nodes New node added correctly 용량 증설 새 인스턴스(cluster-enabled yes, 빈 상태)가 준비됨: cluster에 master로 추가되어 slot을 받거나, 특정 master의 replica가 됩니다. 중단 없음."},{"k":"scenario","v":"c_remove","l":"Cluster에서 노드 제거 (slot 비우기 + del-node)","m":"Redis Cluster: 노드 관리 del-node --cluster del-node 노드 제거 remove node scale in slot 비우기 cluster-weight Node … is not empty is not empty! Reshard data away and try again CLUSTER RESET SOFT 노드 퇴역 Master 또는 replica를 퇴역시킴: master의 slot을 다른 master로 옮긴 뒤 노드를 cluster에서 제거합니다. 중단은 없으며, 이동 중에는 ASK 리다이렉션이 발생합니다."},{"k":"scenario","v":"c_reshard","l":"Slot 재분배 (rebalance / reshard)","m":"Redis Cluster: 노드 관리 rebalance reshard --cluster rebalance --cluster reshard slot 분포 불균형 imbalance use-empty-masters MIGRATE importing migrating BUSYKEY IOERR error or timeout reading to target instance has slots in importing state has slots in migrating state Calling MIGRATE No rebalancing needed slot 이동 Master 간 slot이나 메모리가 불균형하거나, 새 master가 slot을 받지 못했거나, 일정 수의 slot을 한 master에서 다른 master로 옮겨야 하"},{"k":"scenario","v":"c_failover","l":"수동 failover: replica를 master로 전환 (CLUSTER FAILOVER, FORCE, TAKEOVER)","m":"Redis Cluster: 장애와 유지보수 CLUSTER FAILOVER failover FORCE TAKEOVER manual failover master 유지보수 replica를 master로 전환 Manual failover timed out ERR Master is down or failed, please use CLUSTER FAILOVER FORCE ERR You should send CLUSTER FAILOVER to a replica master 교체 Master 서버에 유지보수가 예정되어 있거나(계획, 데이터 손실 없음), master에 접근할 수 없는데 자동 failover가 일어나지 않은 경우(FORCE / TAKEOVER, 데이터 손실 위험)."},{"k":"scenario","v":"c_replace","l":"장애 master / 노드 교체 (FORGET, 새 replica, --cluster fix)","m":"Redis Cluster: 장애와 유지보수 CLUSTER FORGET --cluster fix 노드 유실 failed node fail noaddr slots not covered CLUSTERDOWN replace node 장애 master handshake ERR Can't forget my master! CLUSTERDOWN Hash slot not served 서버 유실 디스크 장애 노드의 서버가 영구 유실됨: replica가 master로 승격되었다면 이전 노드를 forget시키고 새 replica를 추가합니다. Replica가 없었다면 slot이 주인 없이 남으므로 --cluster fix로 할당합니다(해당 slot의 데이터는 유실)."},{"k":"scenario","v":"s_health","l":"Sentinel 상태: master, replica, Sentinel, quorum","m":"Sentinel SENTINEL MASTERS SENTINEL master get-master-addr-by-name CKQUORUM +sdown +odown -odown +switch-master NOQUORUM NOGOODSLAVE sentinel 상태 +tilt -failover-abort-not-elected s_down o_down master_link_status:down sentinel health 읽기 전용: Sentinel이 보는 master 주소, replica, 다른 Sentinel, CKQUORUM, 최근 failover 이벤트를 확인합니다. Sentinel 구성에서 무엇을 할지 확실하지 않다면 여기서 시작하십시오."},{"k":"scenario","v":"s_failover","l":"Sentinel로 수동 failover (SENTINEL FAILOVER)","m":"Sentinel SENTINEL FAILOVER sentinel failover switch-master 계획된 failover INPROG NOGOODSLAVE -failover-abort-no-good-slave NOGOODSLAVE No suitable replica to promote sentinel 유지보수 Master 서버에 유지보수 예정: Sentinel이 가장 적합한 replica를 master로 승격하고, 나머지 replica와 클라이언트를 새 master로 전환합니다. 중단은 1–2초이며, master가 동작 중이면 데이터 손실은 없습니다."},{"k":"scenario","v":"s_replace","l":"Sentinel 환경에서 노드 추가 / 제거 / 교체 (SENTINEL RESET)","m":"Sentinel SENTINEL RESET sentinel reset replicaof 새 replica 새 sentinel sentinel 추가 sentinel 제거 이전 replica가 계속 보임 num-other-sentinels known-replica known-sentinel 이전 sentinel이 계속 보임 Replica나 Sentinel을 영구 제거했거나 새로 추가한 경우: 새 replica는 REPLICAOF로 연결하고, Sentinel의 이전 기록은 SENTINEL RESET으로 정리합니다."},{"k":"scenario","v":"s_quorum","l":"Quorum과 Sentinel 설정 변경 (SENTINEL SET)","m":"Sentinel SENTINEL SET quorum down-after-milliseconds failover-timeout parallel-syncs CKQUORUM NOQUORUM failover 너무 늦음 불필요한 failover Sentinel 수가 바뀌었거나 failover가 너무 늦게/너무 일찍 트리거되는 경우: quorum, down-after-milliseconds, failover-timeout, parallel-syncs를 모든 Sentinel에서 변경합니다."},{"k":"scenario","v":"memory","l":"메모리 압박: 사용량, fragmentation, eviction 진단","m":"메모리와 키 OOM command not allowed when used memory > 'maxmemory' OOM maxmemory used_memory evicted_keys eviction maxmemory-policy MEMORY DOCTOR mem_fragmentation_ratio fragmentation activedefrag client-output-buffer-limit 메모리 가득 참 Out of memory: Killed process oom-killer used_memory_rss mem_clients_normal omem used_memory가 maxmemory에 근접, 'OOM command not allowed' 오류, eviction 증가, RSS가 예상보다 크거나 replic"},{"k":"scenario","v":"bigkeys","l":"Big key와 hot key 찾기 (--bigkeys, --memkeys, --hotkeys)","m":"메모리와 키 --bigkeys --memkeys --hotkeys big key 큰 키 hot key 핫 키 MEMORY USAGE UNLINK slowlog latency SCAN HGETALL KEYS * --keystats 지연 급증 latency spike 특정 노드의 메모리나 CPU가 다른 노드보다 높거나, 지연 급증이 있거나, DEL이 오래 걸리는 경우: 가장 크고 가장 많이 접근되는 키를 찾아 안전하게 정리합니다(UNLINK)."},{"k":"add_role","v":"master","l":"Master (slot 할당: 추가 후 rebalance)","m":"빈 master로 합류한 뒤 rebalance로 slot을 받습니다. 용량이 늘어나며, 새 master용 replica도 별도로 추가해야 합니다."},{"k":"add_role","v":"replica","l":"Replica (master의 복제본)","m":"Master의 복제본이 되어 master가 다운되면 역할을 넘겨받습니다. Master 메모리만큼의 데이터가 네트워크로 전송됩니다(전체 동기화)."},{"k":"rs_mode","v":"rebalance","l":"Rebalance: master 간 slot 균등 분배 (빈 master 포함)","m":"모든 master가 같은 몫을 받습니다(slot 없는 새 master 포함). 먼저 시뮬레이션 결과를 보여 줍니다. 새 master 추가 후 가장 일반적인 작업입니다."},{"k":"rs_mode","v":"move","l":"Reshard: 지정한 수의 slot을 한 master에서 다른 master로 이동","m":"지정한 수의 slot을 한 master에서 다른 master로 옮깁니다: 부하가 높거나 큰 master의 부담을 줄이거나, master를 단계적으로 비우기 위한 용도입니다."},{"k":"fo_mode","v":"default","l":"계획 (기본): master와 동기화한 뒤 넘겨받음, 데이터 손실 없음","m":"Master가 동작 중이어야 합니다."},{"k":"fo_mode","v":"force","l":"FORCE: master에 접근 불가, 동기화를 기다리지 않고 넘겨받음","m":"Master의 마지막 쓰기가 유실될 수 있습니다. 여전히 master 과반수의 투표를 거칩니다."},{"k":"fo_mode","v":"takeover","l":"TAKEOVER: master 과반수도 없음, 투표 없이 넘겨받음","m":"최후의 수단: split brain과 epoch 충돌 위험."},{"k":"srep_op","v":"add_replica","l":"새 replica 추가","m":"새 서버의 Redis가 현재 master에 연결되며, Sentinel이 이를 자동으로 발견합니다."},{"k":"srep_op","v":"rm_replica","l":"Replica 영구 제거","m":"Replica를 중지하고 모든 Sentinel에서 기록을 정리합니다."},{"k":"srep_op","v":"add_sentinel","l":"새 Sentinel 추가","m":"새 서버에 Sentinel을 추가합니다(전체 Sentinel 수는 홀수로 유지: 3 → 5)."},{"k":"srep_op","v":"rm_sentinel","l":"Sentinel 영구 제거","m":"Sentinel을 중지하고 다른 Sentinel에서 기록을 정리한 뒤 quorum을 재검토합니다."}]},{"id":"redis-security","file":"install/guides/redis-security.json","group":"Redis","engineId":"redis","category":"security","title":"Redis / Valkey 보안 강화 (ACL, TLS, 위험 명령)","summary":"설치나 일상 운영 가이드가 아닙니다. 실행 중인 Redis 또는 Valkey(단일 서버, replica + Sentinel, Cluster)를 서비스 중단 없이 강화하기 위한 시나리오별 runbook입니다. 읽기 전용 보안 점검, ACL 사용자와 default 사용자 비활성화, requirepass/masterauth, bind와 protected-mode, CONFIG/DEBUG/MODUL","level":"고급","duration":"5분(점검) – 90분(전체 강화)","topology":"단일 서버, master + replica + Sentinel 또는 Redis Cluster","tags":["redis 보안","redis security","valkey","hardening","보안 강화","ACL","ACL SETUSER","aclfile","requirepass","masterauth","protected-mode","bind","rename-command","enable-protected-configs","TLS","tls-replication","sentinel 비밀번호","CIS"],"order":20,"steps":7,"scenarios":[{"k":"op","v":"audit","l":"보안 상태 점검 (읽기 전용)","m":"점검 redis 보안 점검 redis security audit CIS Redis hardening 보안 강화 redis 외부 노출 exposed redis unauthorized redis redis 비밀번호 없음 -NOAUTH crackit redis cryptominer kinsing NOAUTH Authentication required redis 보안 확인 redis pentest 아무것도 변경하지 않습니다. 비밀번호 없는 접근, default 사용자, 수신 주소, protected-mode, 위험 명령, TLS, Sentinel 비밀번호, 파일 권한을 `OK` / `WARN` / `RISK`로 보고합니다. 어디서부터 시작할지 모르겠다면 이것을 선택하십시오."},{"k":"op","v":"all","l":"전체 강화 (모두 순서대로)","m":"점검 전체 보안 강화 full hardening redis 보안 프로젝트 모의 해킹 지적 사항 점검 → ACL 사용자(관리자, 애플리케이션, 복제, Sentinel)와 default 사용자 비활성화 → bind/protected-mode/방화벽 → 위험 명령과 protected config → TLS → 파일 권한 → 최종 점검. replica/Sentinel에서는 rolling으로 적용하며, 위험한 단계는 확인을 요청합니다."},{"k":"op","v":"acl","l":"ACL 사용자 및 default 사용자 비활성화 (Redis 6+)","m":"인증 및 권한 ACL ACL SETUSER aclfile default user default 사용자 NOPERM WRONGPASS invalid username-password pair -NOAUTH Authentication required user default on nopass masteruser least privilege 최소 권한 NOPERM this user has no permissions to run the NOPERM this user has no permissions to access one of the keys used as arguments ACL LOG 사용자별 비밀번호 ACL SAVE 모두가 하나의 비밀번호로(또는 비밀번호 없이) `default` 사용자로 접속하고 있습니다. 관"},{"k":"op","v":"auth","l":"비밀번호 없는 Redis: requirepass / masterauth (긴급 조치)","m":"인증 및 권한 requirepass masterauth auth-pass 비밀번호 없는 redis no password MASTER aborted replication with an error: NOAUTH Unable to AUTH to MASTER -NOAUTH ERR AUTH <password> called without any password configured for the default user Client sent AUTH, but no password is set 비밀번호 설정 비밀번호가 전혀 없고 ACL로 전환할 시간도 없습니다. 모든 node에 공통 비밀번호(`requirepass` + 복제용 `masterauth`, Sentinel에는 `auth-pass`)를 운영 중에 설정합니다. 이후"},{"k":"op","v":"net","l":"네트워크 접근 제한 (bind 0.0.0.0, protected-mode, 방화벽)","m":"네트워크 및 암호화 bind 0.0.0.0 protected-mode no DENIED Redis is running in protected mode 0.0.0.0:6379 6379 포트 열림 shodan 방화벽 DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user *:6379 Could not create server TCP listening socket Redis가 모든 인터페이스에서 수신하고 있거나 `protected-mode no`입니다. bind를 localhost + 내부 IP로 줄이고 protected-mode를 켜며, 방화벽은 node"},{"k":"op","v":"cmds","l":"위험 명령과 CONFIG 악용 (FLUSHALL, CONFIG, DEBUG, MODULE)","m":"네트워크 및 암호화 CONFIG SET dir dbfilename authorized_keys crontab FLUSHALL FLUSHDB DEBUG MODULE LOAD enable-protected-configs enable-debug-command enable-module-command rename-command @dangerous ERR CONFIG SET failed (possibly related to argument 'dir') - can't set protected config ERR DEBUG command not allowed CONFIG SET dbfilename rename-command CONFIG 악성 모듈 SLAVEOF 악용 `CONFIG SET dir/dbfilename`으로 서"},{"k":"op","v":"tls","l":"연결 암호화: TLS (클라이언트, 복제, Sentinel, cluster bus)","m":"네트워크 및 암호화 TLS SSL tls-port tls-replication tls-cluster tls-auth-clients 암호화되지 않은 연결 Error accepting a client connection: error:0A00010B:SSL routines::wrong version number SSL_connect certificate verify failed rediss:// Connection reset by peer tls-cert-file 암호화된 연결 encryption in transit 트래픽이 암호화되지 않았습니다. 기존 포트 옆에 TLS 포트를 운영 중에 열고, 복제와 Sentinel을 TLS로 옮기며, 애플리케이션 전환이 끝나면 평문 포트를 닫습니다(`port 0`)."},{"k":"op","v":"perms","l":"파일 권한 및 실행 사용자","m":"흔적 및 파일 redis.conf 권한 world readable chmod dump.rdb appendonly root로 실행 중 running as root 파일 권한 Permission denied 640 User=redis redis root redis.conf(비밀번호 포함), ACL 파일, RDB/AOF, 로그 파일을 다른 사용자가 읽을 수 있거나 Redis가 root로 실행되고 있습니다. 권한을 640/750으로 조이고 소유자를 서비스 사용자로 지정합니다."}]},{"id":"es-cluster","file":"install/guides/es-cluster.json","group":"Elasticsearch","engineId":"elasticsearch","category":"install","title":"Elasticsearch 클러스터 설치 (3+ 노드, TLS, Kibana)","summary":"3개 이상의 노드로 구성된 Elasticsearch 8.x / 9.x 클러스터를 처음부터 구축합니다: Elastic 저장소, RAM에 맞춘 heap, memory lock, vm.max_map_count, 첫 번째 노드의 자동 보안 구성(TLS + elastic 비밀번호), enrollment token을 이용한 나머지 노드의 합류, 공용 또는 분리된 master/data 역할, Kibana","level":"중급","duration":"60–90분","topology":"3+ Elasticsearch 노드 (+ 선택 사항: Kibana 서버)","tags":["elasticsearch","elastic","kibana","cluster","클러스터","설치","install","enrollment","tls","heap","snapshot","rhel","ubuntu"],"order":1,"steps":54,"scenarios":[]},{"id":"es-rolling-upgrade","file":"install/guides/es-rolling-upgrade.json","group":"Elasticsearch","engineId":"elasticsearch","category":"upgrade","title":"Elasticsearch rolling upgrade (8.x minor 또는 8.19 → 9.x)","summary":"설치 가이드가 아닙니다. 실행 중인 Elasticsearch cluster의 node를 하나씩 업그레이드하여 중단 없이 새 버전으로 옮깁니다(8.x 내 minor 업그레이드 또는 8.19 → 9.x 메이저 업그레이드). 사전 점검(버전, 상태, deprecation API, system feature migration), 업그레이드 전 snapshot, shard allocation을 pri","level":"고급","duration":"node당 15–30분","topology":"node 3개 이상인 기존 cluster (+ Kibana)","tags":["elasticsearch","업그레이드","rolling upgrade","minor","메이저","9.x","8.19","kibana","upgrade assistant","deprecation","스냅샷","롤백","rolling upgrade","node roles","master-eligible","deprecation api","versionlock"],"order":2,"steps":34,"scenarios":[{"k":"path","v":"minor","l":"같은 메이저 버전 내 (예: 8.18 → 8.19, 9.1 → 9.2)","m":"같은 메이저 버전 내 (8.18 → 8.19, 9.1 → 9.2): repository는 바뀌지 않으며 deprecation 점검은 그대로 수행합니다. 중간 minor 버전을 건너뛰는 것은 지원됩니다."},{"k":"path","v":"major","l":"메이저 버전: 8.19.x → 9.x","m":"소스 cluster는 먼저 8.19.x(최신 patch)로 업그레이드되어 있어야 합니다. 사전 점검에는 Upgrade Assistant / deprecation API와 system feature migration 단계가 포함됩니다."}]},{"id":"es-snapshot","file":"install/guides/es-snapshot.json","group":"Elasticsearch","engineId":"elasticsearch","category":"backup","title":"Elasticsearch snapshot 및 restore (fs / S3 repository, SLM)","summary":"설치된 Elasticsearch 8.x / 9.x 클러스터에 snapshot(백업) 체계를 구성하고 복구 방법을 설명합니다. 공유 파일 시스템(NFS + path.repo) 또는 S3(keystore) repository, rolling restart를 통한 적용과 검증, 수동 snapshot(indices, include_global_state, feature_states), SLM을 이용","level":"중급","duration":"45–90분","topology":"Elasticsearch 8.x / 9.x 클러스터(1–N 노드) + 공유 디렉터리(NFS) 또는 S3 호환 오브젝트 스토리지","tags":["elasticsearch","snapshot","restore","백업","backup","복원","repository","path.repo","nfs","s3","keystore","slm","snapshot lifecycle","retention","rename_pattern","feature_states","readonly repository","migration","slm","snapshot lifecycle","quartz cron","utc","netrc"],"order":15,"steps":33,"scenarios":[]},{"id":"es-ops","file":"install/guides/es-ops.json","group":"Elasticsearch","engineId":"elasticsearch","category":"ops","title":"Elasticsearch 일상 운영 및 장애 시나리오 (8.x / 9.x)","summary":"설치 가이드가 아닙니다. 설치된 Elasticsearch 8.x / 9.x cluster를 일상적으로 관리하고 문제가 생겼을 때 복구하기 위한 시나리오별 runbook입니다. 상태 점검과 allocation explain, 할당되지 않은 shard 해결(디스크, filtering, retry_failed, replica, 최후 수단 stale/empty primary), disk waterm","level":"고급","duration":"5분(점검) – 수 시간(reindex, node 비우기)","topology":"Elasticsearch 8.x / 9.x cluster(node 1–N개), TLS 및 인증 활성화","tags":["elasticsearch","elastic","cluster health","red","yellow","unassigned","allocation explain","reroute","retry_failed","allocate_stale_primary","watermark","flood stage","read_only_allow_delete","exclude","voting_config_exclusions","enrollment token","reindex","alias","ILM","rollover","shrink","split","snapshot restore","운영","장애 처리"],"order":20,"steps":6,"scenarios":[{"k":"scenario","v":"health","l":"Cluster 상태: status, node, 할당되지 않은 shard, 디스크","m":"모니터링 및 진단 red yellow _cluster/health _cat/shards UNASSIGNED allocation explain _cluster/allocation/explain status red status yellow cluster 상태 pending tasks master_not_discovered_exception NoNodeAvailableException circuit_breaking_exception heap cluster 상태 점검 읽기 전용: green/yellow/red, node 목록, 할당되지 않은(UNASSIGNED) shard와 그 원인(allocation explain), 디스크 사용량과 watermark, 대기 중인 작업. 무엇을 할지 확실하지 않으면 여기서 시작하"},{"k":"scenario","v":"unassigned","l":"할당되지 않은(UNASSIGNED) shard 해결 (explain 기준)","m":"모니터링 및 진단 UNASSIGNED unassigned shards NODE_LEFT ALLOCATION_FAILED max_retry shard has exceeded the maximum number of retries retry_failed no_valid_shard_copy cannot allocate because a previous copy of the primary shard existed allocate_stale_primary allocate_empty_primary the node is above the high watermark same_shard awareness filter NO_VALID_SHARD_COPY CLUSTER_RECOVERED REPLICA_ADDED cannot al"},{"k":"scenario","v":"watermark","l":"Disk watermark 설정 확인 / 변경","m":"디스크 및 블록 watermark disk.watermark low disk watermark high disk watermark flood stage disk watermark 디스크 가득 참 disk full cluster.routing.allocation.disk cluster.routing.allocation.disk.watermark.high disk usage exceeded 디스크 임계값 디스크 사용량 때문에 shard가 이동하거나 할당되지 않습니다. watermark 임계값(low / high / flood_stage)을 일시적 또는 영구적으로 변경하거나 기본값으로 되돌립니다."},{"k":"scenario","v":"readonly","l":"index가 읽기 전용이 됨: read_only_allow_delete 블록 제거","m":"디스크 및 블록 read_only_allow_delete FORBIDDEN/12/index read-only / allow delete (api) TOO_MANY_REQUESTS/12/disk usage exceeded flood-stage watermark cluster_block_exception index.blocks.read_only_allow_delete flood stage 읽기 전용 read-only blocked by: [FORBIDDEN/12/index read-only / allow delete (api)] index read-only 쓰기 거부 쓰기가 'FORBIDDEN/12/index read-only / allow delete (api)' 또는 'TOO_MANY_REQUESTS/12/"},{"k":"scenario","v":"add_node","l":"cluster에 node 추가 (enrollment token)","m":"Node 관리 node 추가 add node enrollment token elasticsearch-create-enrollment-token elasticsearch-reconfigure-node scale out join cluster master not discovered master not discovered yet, this node has not previously joined a bootstrapped cluster 용량 증설 새 node 용량이 필요합니다. 새 서버에 Elasticsearch를 설치했고, enrollment token으로 cluster에 합류합니다. shard는 자동으로 재분배됩니다. 서비스 중단은 없습니다."},{"k":"scenario","v":"remove_node","l":"node 안전하게 제거 (exclude, 비우기, 종료)","m":"Node 관리 node 제거 remove node decommission cluster.routing.allocation.exclude._name exclude voting_config_exclusions relocating node 비우기 voting configuration node 퇴역 서버 교체 node를 퇴역시키거나 하드웨어를 교체합니다. allocation exclude로 shard를 다른 node로 옮기고, node가 비었는지 확인한 뒤, master-eligible이면 voting exclusion을 지정하고 종료합니다."},{"k":"scenario","v":"reindex","l":"Reindex: mapping/설정 변경을 위해 새 index와 alias로 무중단 전환","m":"Index 관리 _reindex reindex mapping 변경 mapper_parsing_exception illegal_argument_exception: mapper alias _aliases 무중단 전환 zero downtime task version_conflict_engine_exception cannot be changed from type mapper [ analyzer 변경 mapping 필드 타입, analyzer, shard 수를 바꿔야 합니다. 새 index를 만들고 _reindex로 데이터를 복사한 뒤 alias를 새 index로 원자적으로 전환합니다."},{"k":"scenario","v":"ilm","l":"Index lifecycle (ILM) 및 rollover 구성 / 문제 해결","m":"Index 관리 ILM index lifecycle rollover _rollover _ilm/explain is_write_index rollover_alias ilm policy index.lifecycle.name ERROR step illegal_argument_exception: index.lifecycle.rollover_alias rollover_alias does not point to index ILM ERROR index가 삭제되지 않음 로그 index 증가 로그/시계열 index가 계속 커지거나 삭제되지 않습니다. ILM 정책(rollover + delete), index template, 초기 index(write alias)를 구성하고, 기존 정책이 진행되지 않는 이유를 explain"},{"k":"scenario","v":"resize","l":"shard 수 조정: _shrink (줄이기) / _split (늘리기)","m":"Index 관리 _shrink _split shrink split shard 수 number_of_shards too many shards 작은 shard 과다 oversharding index.blocks.write must be read-only to resize index must have all shards allocated on the same node to shrink index number_of_routing_shards index의 shard 수를 잘못 정했습니다. 너무 작은 shard(oversharding)는 _shrink로 합치고, 너무 큰 shard는 _split으로 나눕니다. 전환은 alias로 합니다."},{"k":"scenario","v":"snaprestore","l":"snapshot에서 단일 index를 새 이름으로 복원","m":"Index 관리 snapshot restore _restore 단일 index 복원 rename_pattern rename_replacement index 삭제됨 deleted index index_not_found_exception cannot restore index because an open index with same name already exists snapshot_missing_exception 실수로 삭제 index 복구 index가 삭제되었거나 손상되었습니다. 운영 중인 index는 건드리지 않고 snapshot repository에서 해당 index만 새 이름(rename)으로 복원하며, 검증 후 alias로 서비스에 투입합니다."},{"k":"un_fix","v":"retry","l":"재시도 한도 초과: 재시도 (_cluster/reroute?retry_failed)","m":"max_retry shard has exceeded the maximum number of retries ALLOCATION_FAILED explain: 'shard has exceeded the maximum number of retries'. 먼저 근본 오류(디스크, mapping, 메모리)를 해결하십시오."},{"k":"un_fix","v":"replicas","l":"replica 수가 node 수보다 많음: replica 줄이기","m":"same_shard a copy of this shard is already allocated to this node explain: same_shard(같은 node에 사본을 둘 수 없음). 단일 node cluster에서는 replica 0."},{"k":"un_fix","v":"filter","l":"Allocation filtering / exclude가 막고 있음: 필터 제거","m":"filter node does not match index setting [index.routing.allocation node matches cluster setting [cluster.routing.allocation.exclude explain: filter decider('node does not match index setting [index.routing.allocation…]' 또는 cluster exclude)."},{"k":"un_fix","v":"disk","l":"Disk watermark가 막고 있음","m":"disk_threshold the node is above the low watermark the node is above the high watermark explain: disk_threshold decider. watermark 시나리오로 안내합니다."},{"k":"un_fix","v":"stale","l":"최후 수단: 이전(stale) 사본에서 primary 할당","m":"no_valid_shard_copy all the copies of its data in the cluster are stale or corrupt allocate_stale_primary primary의 최신 사본이 유실되고 이전 사본만 남아 있습니다. 그 사본 이후의 최근 쓰기는 유실됩니다."},{"k":"un_fix","v":"empty","l":"가장 마지막 수단: 빈 primary 할당 (shard의 모든 데이터 유실)","m":"no_valid_shard_copy there are no copies of its data in the cluster allocate_empty_primary 사본이 전혀 없고 index를 snapshot에서 복원할 수도 없는 경우."},{"k":"wm_op","v":"show","l":"보기만 (디스크 사용량과 현재 임계값)","m":"읽기 전용: node별 디스크와 현재 임계값."},{"k":"wm_op","v":"set","l":"임계값 변경","m":"임계값을 일시적으로 올립니다(예: 큰 디스크에서 백분율 대신 남은 공간 `50gb`). 문제가 해결되면 기본값으로 되돌리십시오."},{"k":"wm_op","v":"reset","l":"기본값으로 되돌리기 (85% / 90% / 95%)","m":"이전에 변경한 임계값을 삭제하고 기본값(85% / 90% / 95%)으로 돌아갑니다."},{"k":"ilm_op","v":"setup","l":"새 정책 + template + 초기 index (write alias)","m":"정책(rollover + 삭제), index template, 초기 index(`-000001`, write alias)를 만듭니다. 새 로그 스트림용입니다."},{"k":"ilm_op","v":"debug","l":"기존 index가 왜 진행되지 않습니까? (explain, retry)","m":"index가 rollover되지 않거나 삭제되지 않습니다. `_ilm/explain`이 실패한 단계를 보여 주며, 원인을 해결하면 retry합니다."},{"k":"ilm_op","v":"rollover","l":"수동 rollover","m":"조건을 기다리지 않고 새 write index를 만듭니다(mapping 변경 후 또는 비대해진 index용)."},{"k":"rs_kind","v":"shrink","l":"_shrink: shard 수 줄이기 (대상은 현재 수의 약수여야 함)","m":""},{"k":"rs_kind","v":"split","l":"_split: shard 수 늘리기 (대상은 현재 수의 배수여야 함)","m":""}]},{"id":"es-security","file":"install/guides/es-security.json","group":"Elasticsearch","engineId":"elasticsearch","category":"security","title":"Elasticsearch 보안 강화 (security, TLS, 롤, audit)","summary":"설치나 일상 운영 가이드가 아닙니다. 운영 중인 Elasticsearch cluster를 강화하기 위한 시나리오 기반 runbook입니다. 읽기 전용 보안 점검, 비활성화된 security를 transport TLS와 함께 활성화(전체 cluster 재시작), HTTP TLS로의 rolling 전환, DBA/애플리케이션 롤, 만료 기간이 있는 API key, 비상용 file realm 사용자","level":"고급","duration":"10분(점검) – 2시간(전체 강화)","topology":"1–12개 node의 Elasticsearch cluster (+ Kibana)","tags":["elasticsearch 보안","elasticsearch security","hardening","보안 강화","xpack.security","TLS","elasticsearch-certutil","elasticsearch-users","API key","role mapping","elastic superuser","anonymous","audit logging","IP filtering","kibana_system","service account token","CIS"],"order":20,"steps":4,"scenarios":[{"k":"op","v":"audit","l":"보안 상태 점검 (읽기 전용)","m":"점검 elasticsearch 보안 점검 elasticsearch security audit CIS Elasticsearch hardening 보안 강화 외부 노출 elasticsearch exposed elasticsearch xpack.security.enabled missing authentication credentials for REST request security_exception elasticsearch 보안 검사 elasticsearch pentest RISK 아무것도 변경하지 않습니다. Security 활성화 여부, 인증 없는 접근, HTTP/transport TLS, anonymous 접근, 사용자와 elastic 사용 현황, 만료 기간 없는 API key, audit logging, I"},{"k":"op","v":"all","l":"전체 강화 (모두 순서대로)","m":"점검 전체 보안 강화 full hardening elastic 보안 프로젝트 모의해킹 결과 점검 → security + transport/HTTP TLS(비활성화된 경우, 전체 cluster 재시작) → 사용자/롤/API key, anonymous 차단, elastic을 일상 업무에서 제외 → 네트워크 및 IP 필터링 → audit logging(라이선스가 허용하는 경우) → snapshot repository 접근 → Kibana 연결 → 최종 점검."},{"k":"op","v":"enable","l":"Security 비활성화: 보안과 transport TLS 활성화","m":"인증과 암호화 xpack.security.enabled: false security disabled 인증 없는 접근 Elasticsearch built-in security features are not enabled Transport SSL must be enabled if security is enabled invalid configuration for xpack.security.transport.ssl 7.x 업그레이드 보안 Security is disabled xpack.security.transport.ssl 인증 없음 `xpack.security.enabled: false`이거나 7.x에서 업그레이드되어 인증 없이 접근되는 cluster입니다. 인증서를 생성하고 transport(필요하면 HTTP"},{"k":"op","v":"httptls","l":"HTTP(REST) 트래픽 TLS 전환","m":"인증과 암호화 http.ssl xpack.security.http.ssl.enabled received plaintext http traffic on an https channel plaintext http https 9200 elasticsearch-certutil http SSLHandshakeException curl: (52) Empty reply from server http:// 9200 비밀번호 평문 노출 Security는 활성화되어 있지만 9200 포트가 평문 HTTP입니다. 비밀번호가 네트워크에서 노출됩니다. HTTP 인증서를 배포하고 node를 하나씩 재시작하며, 클라이언트는 `https://`로 전환합니다."},{"k":"op","v":"users","l":"사용자, 롤, API key; elastic 및 anonymous 접근","m":"인증과 암호화 elastic 사용자 elastic superuser superuser role mapping API key api_key elasticsearch-users anonymous xpack.security.authc.anonymous action [indices:data/write/bulk[s]] is unauthorized is unauthorized for user unable to authenticate user action [indices:data/read/search] is unauthorized for user api key 만료 만료 없는 api key break-glass 애플리케이션이 `elastic` superuser로 접속하거나, anonymous 접근이 열려 있거나, 만료 "},{"k":"op","v":"auditlog","l":"Audit logging 활성화","m":"감사 추적과 접근 audit log xpack.security.audit.enabled audit logging _audit.json 감사 기록 누가 삭제했나 개인정보 보호법 (PIPA) PCI DSS xpack.security.audit.logfile.events.include 접근 기록 누가 어떤 index에 접근했는지, 로그인 실패 시도, 권한 거부, 보안 설정 변경을 기록해야 합니다. `xpack.security.audit.enabled`(정적 설정, rolling 재시작)는 라이선스가 필요합니다(이 단계에서 라이선스를 확인합니다)."},{"k":"op","v":"net","l":"네트워크 접근과 IP 필터링","m":"감사 추적과 접근 network.host 0.0.0.0 0.0.0.0:9200 9200 포트 열림 xpack.security.transport.filter xpack.security.http.filter ip filtering shodan firewall 0.0.0.0:9300 network.host: 0.0.0.0 _site_ 공인 IP 9200/9300이 모든 인터페이스에서 리스닝하거나 누구에게나 열려 있습니다. `network.host`를 내부 IP로 제한하고, firewall과 Elasticsearch IP 필터링으로 node, Kibana, 애플리케이션 네트워크만 허용합니다."},{"k":"op","v":"snapshot","l":"Snapshot repository 접근 보호","m":"감사 추적과 접근 snapshot repo path.repo repository_exception s3.client access_key readonly repository snapshot 보안 snapshot_operator Object Lock 백업 보안 Snapshot 디렉터리를 누구나 읽을 수 있거나, repository 관리 권한이 넓거나, 클라우드 액세스 키가 elasticsearch.yml에 있습니다. 디렉터리 권한, snapshot 운영자 롤, keystore, 두 번째 cluster에서의 읽기 전용 repository 등록을 다룹니다."},{"k":"op","v":"kibana","l":"Kibana 연결 보호","m":"감사 추적과 접근 kibana_system elasticsearch.username: elastic kibana.yml 비밀번호 service account token elasticsearch.serviceAccountToken xpack.encryptedSavedObjects.encryptionKey Kibana server is not ready yet unable to retrieve version information from Elasticsearch nodes value of \"elastic\" is forbidden serviceAccountToken kibana.keystore Kibana가 `elastic` 사용자로 접속하거나 암호화 없이 연결되며, 비밀번호가 kibana.yml에 평문으로 있습"}]},{"id":"mon-prometheus-grafana","file":"install/guides/mon-prometheus-grafana.json","group":"모니터링","engineId":null,"category":"monitoring","title":"Prometheus + Grafana로 데이터베이스 모니터링","summary":"중앙 모니터링 서버(Prometheus, Alertmanager, Grafana)를 설치하고, 모니터링 대상 서버에 node_exporter와 선택한 데이터베이스 exporter를 추가합니다: PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Redis, Elasticsearch. 최소 권한 모니터링 사용자, systemd 서비스, 기본 alert 규칙, ","level":"중급","duration":"60–120분","topology":"모니터링 서버 1대 + 모니터링 대상 서버","tags":["prometheus","grafana","alertmanager","node_exporter","postgres_exporter","mysqld_exporter","oracledb_exporter","sql_exporter","mongodb_exporter","redis_exporter","elasticsearch_exporter","모니터링","monitoring","alert","dashboard"],"order":1,"steps":44,"scenarios":[]},{"id":"ora19-install","file":"install/guides/ora19-install.json","group":"Oracle","engineId":"oracle","category":"install","title":"Oracle Database 19c 단일 인스턴스 설치 (EE / SE2)","summary":"Oracle Linux / RHEL 7.9, 8 또는 9를 실행하는 단일 서버에 Oracle Database 19c를 처음부터 설치합니다: 운영체제 준비(preinstall RPM 또는 수동), OFA 디렉터리, Release Update를 포함한 silent 소프트웨어 설치, listener, CDB + PDB(AL32UTF8, RAM 기준 SGA/PGA, redo, FRA, ARCHIVE","level":"중급","duration":"90–150분","topology":"서버 1대","tags":["oracle","19c","설치","install","runInstaller","dbca","netca","silent","release update","applyRU","opatch","cdb","pdb","hugepages","rman","oracle linux","rhel","asm","ol7","multipath","oracle restart"],"order":1,"steps":38,"scenarios":[]},{"id":"ora23-install","file":"install/guides/ora23-install.json","group":"Oracle","engineId":"oracle","category":"install","title":"Oracle AI Database 26ai / 23ai 설치 (Linux)","summary":"단일 서버에 Oracle AI Database 26ai(23ai를 대체하는 릴리스, 버전 번호 23.26.x)를 설치합니다. 세 가지 공식 경로 중 하나를 선택합니다: 운영 환경용 gold image ZIP을 이용한 OFA 설치(runInstaller -setupDBHome + dbca, 파일 시스템 또는 미리 설치된 26ai Oracle Restart의 ASM), 빠른 Enterprise ","level":"중급","duration":"45–120분","topology":"서버 1대","tags":["oracle","26ai","23ai","ai database","설치","install","rpm","free","gold image","setupDBHome","dbca","cdb","pdb","oracle linux","rhel","asm","oracle restart"],"order":2,"steps":30,"scenarios":[]},{"id":"ora-storage-prep","file":"install/guides/ora-storage-prep.json","group":"Oracle","engineId":"oracle","category":"install","title":"Oracle용 스토리지 준비: multipath, 영구 디바이스 이름, ASM 디스크","summary":"Oracle Grid Infrastructure, Oracle Restart 또는 데이터베이스 설치 전에 한 번 적용합니다. SAN LUN을 WWID로 식별하고, 필요하면 DM-Multipath alias를 구성하며, ASM 디스크는 udev 규칙 또는 ASMLib 레이블로 영구 이름과 grid:asmadmin 소유권을 갖게 됩니다(AFD를 선택한 경우 설치 과정에서 레이블을 쓰도록 준비).","level":"중급","duration":"30–90분","topology":"서버 1대 또는 RAC 노드 2–8개 + SAN/iSCSI/VM 디스크","tags":["oracle","asm","multipath","device-mapper-multipath","dm-multipath","multipath.conf","wwid","udev","udev 규칙","asmlib","oracleasm","afd","asm filter driver","lvm","xfs","fstab","san","lun","스토리지 준비","storage","디스크","rac","oracle restart"],"order":3,"steps":10,"scenarios":[]},{"id":"ora-grid-asm","file":"install/guides/ora-grid-asm.json","group":"Oracle","engineId":"oracle","category":"install","title":"Oracle Restart: Grid Infrastructure 19c + ASM (단일 서버)","summary":"단일 서버에 Oracle Restart(Grid Infrastructure 19c standalone, HA_CONFIG)와 ASM을 설치합니다. grid 사용자와 ASM 그룹, 영구 디스크 이름(udev, ASMLib 또는 ASM Filter Driver, 단일 경로 또는 multipath 디스크), RU를 함께 적용하는 gridSetup.sh -silent, DATA 및 FRA 디스크 그","level":"고급","duration":"90–150분","topology":"서버 1대 + 비공유 ASM 디스크","tags":["oracle","19c","grid infrastructure","oracle restart","asm","gridSetup","HA_CONFIG","udev","asmlib","oracleasm","afd","asm filter driver","srvctl","asmcmd","설치","multipath","ol7"],"order":3,"steps":30,"scenarios":[]},{"id":"ora19-patch-ru","file":"install/guides/ora19-patch-ru.json","group":"Oracle","engineId":"oracle","category":"patch","title":"Oracle Database 19c Release Update (RU) 적용","summary":"단일 인스턴스 또는 Oracle Restart(Grid Infrastructure + ASM) 19c 데이터베이스에 분기별 Release Update를 적용합니다: OPatch 업데이트, 충돌 및 공간 사전 점검, ORACLE_HOME 백업과 guaranteed restore point, opatch apply(Oracle Restart는 opatchauto), 선택 사항인 OJVM RU, ","level":"중급","duration":"60–120분 (다운타임: in-place 30–60분, out-of-place 10–20분)","topology":"서버 1대 (단일 인스턴스 또는 Oracle Restart), RAC는 rolling patch 가이드","tags":["oracle","19c","patch","release update","ru","opatch","opatchauto","datapatch","ojvm","out-of-place","rollback","restore point","oracle restart","rac","rolling","standby-first","data guard","standby","1265700.1"],"order":10,"steps":25,"scenarios":[]},{"id":"ora19-rac","file":"install/guides/ora19-rac.json","group":"Oracle","engineId":"oracle","category":"ha","title":"Oracle RAC 19c 설치 (Grid Infrastructure + ASM + RAC 데이터베이스)","summary":"두 개 이상의 노드에 Oracle Grid Infrastructure 19c(Flex ASM, SCAN)와 Oracle RAC 19c 데이터베이스를 처음부터 설치합니다: 네트워크 및 DNS 계획, 공유 디스크 준비(udev 또는 ASMLib, multipath 또는 단일 경로), 사용자와 SSH, cluvfy, 설치 중 RU를 적용하는 silent gridSetup/runInstaller, ","level":"고급","duration":"4–6시간","topology":"노드 2–8대 + 공유 디스크 (SAN/iSCSI) + DNS의 SCAN","tags":["oracle","rac","19c","grid infrastructure","clusterware","asm","scan","gridsetup","dbca","srvctl","crsctl","클러스터","ha","udev","asmlib","multipath","ol7","scan","ocr","voting disk"],"order":10,"steps":68,"scenarios":[]},{"id":"ora-rac-patch","file":"install/guides/ora-rac-patch.json","group":"Oracle","engineId":"oracle","category":"patch","title":"Oracle RAC 19c rolling Release Update (opatchauto)","summary":"운영 중인 2–8 노드 RAC 19c 클러스터에 분기별 GI Release Update를 무중단(rolling)으로 적용합니다: OPatch 업데이트, opatchauto analyze를 통한 충돌 검사, OCR 백업과 복귀 지점, 서비스를 재배치하면서 노드별 적용, 한 노드에서 datapatch 완료, 검증, 필요 시 rollback. 단일 인스턴스와 Oracle Restart에는 \"Ora","level":"고급","duration":"노드당 45–90분","topology":"기존 2–8 노드 RAC 19c","tags":["oracle","rac","19c","패치","release update","ru","opatchauto","opatch","datapatch","rolling","grid infrastructure","rollback","scan","서비스","standby-first","srvctl"],"order":11,"steps":38,"scenarios":[]},{"id":"ora-rac-ops","file":"install/guides/ora-rac-ops.json","group":"Oracle","engineId":"oracle","category":"ops","title":"Oracle RAC 일상 운영과 장애 시나리오","summary":"설치되어 운영 중인 Oracle RAC(Grid Infrastructure 19c) 클러스터의 일상 운영과 장애 대응: 상태 점검, node eviction 진단, 노드 유지보수 전환, 클러스터 전체 계획 종료와 기동, 노드 추가와 제거, 서비스 관리(preferred/available, AC/TAC), SCAN/VIP/포트/interconnect 변경, OCR/OLR 백업과 복원, OCR과","level":"고급","duration":"15분 – 3시간(시나리오에 따라)","topology":"기존 2–8 노드 RAC(Grid Infrastructure 19c, 23ai/26ai 차이는 참고 사항에)","tags":["oracle","rac","grid infrastructure","clusterware","crsctl","srvctl","olsnodes","cluvfy","addnode","노드 추가","노드 제거","delete node","ocr","olr","voting disk","votedisk","ocrconfig","ocrcheck","scan","vip","oifcfg","interconnect","eviction","ocssd","misscount","service","relocate","application continuity","taf","fan","일상 운영","장애"],"order":12,"steps":10,"scenarios":[{"k":"scenario","v":"health","l":"클러스터 상태 점검(일상 / 유지보수 전)","m":"점검과 진단 crsctl check cluster crsctl stat res -t cluvfy OFFLINE 리소스 CRS-4535 CRS-4530 CRS-4639 health check 상태 점검 INTERMEDIATE CRS-4537 CRS-2765 CRS-2878 CRS-4700 CRS-4701 crsctl check crs 클러스터 상태 cluster status 리소스 중지됨 resource offline orachk tfactl 유지보수 전 점검 아무것도 변경하지 않습니다. Clusterware 스택, 리소스, OCR/voting, ASM, interconnect, 시간, 최근 경고를 점검합니다. 일상 점검, 모든 유지보수와 패치 전, 그리고 문제가 보고되었을 때 가장 먼저 선택하십시오."},{"k":"scenario","v":"eviction","l":"노드가 클러스터에서 축출되었거나 스스로 재시작됨(eviction 진단)","m":"점검과 진단 CRS-1609 CRS-1610 CRS-1611 CRS-1612 CRS-1615 CRS-1625 CRS-1632 CRS-1656 CRS-8011 node eviction evicted node reboot split brain ocssd.trc misscount disktimeout 노드 축출 CRS-1601 CRS-1606 CRS-1607 CRS-1608 CRS-1613 CRS-1614 CRS-8013 ORA-29740 ORA-29770 IPC Send timeout network heartbeat disk heartbeat 노드 재시작됨 서버가 스스로 재부팅됨 rebootless restart instance eviction 읽기 전용: 노드가 예기치 않게 재시작되었거나 클러스터에서 축출되었"},{"k":"scenario","v":"stopnode","l":"유지보수를 위해 노드 중지 후 재기동","m":"시작 / 중지 crsctl stop crs crsctl start crs 노드 유지보수 OS patch reboot CRS-4133 CRS-4123 drain_timeout srvctl stop instance CRS-2673 CRS-2677 CRS-2675 crsctl disable crs crsctl enable crs 유지보수 모드 maintenance kernel update 커널 업데이트 firmware 한 노드에서 운영체제 패치, 커널 또는 하드웨어 유지보수를 할 때 선택하십시오. 다른 노드는 계속 동작합니다. 해당 노드 인스턴스의 서비스는 이전되고, 세션은 drain 시간이 끝나면 종료됩니다."},{"k":"scenario","v":"stopall","l":"클러스터 전체 계획 종료와 기동","m":"시작 / 중지 crsctl stop cluster -all crsctl start cluster -all 전체 종료 cluster shutdown 계획 다운타임 srvctl stop database CRS-2677 CRS-4133 CRS-4123 crsctl stop crs crsctl start crs SAN 유지보수 데이터센터 유지보수 datacenter maintenance power off 모든 서버 종료 SAN 또는 데이터센터 유지보수처럼 모든 노드를 동시에 종료해야 할 때 선택하십시오. 전체 다운타임: 모든 노드에서 모든 데이터베이스, ASM, Clusterware가 중지됩니다."},{"k":"scenario","v":"addnode","l":"클러스터에 새 노드 추가","m":"노드 추가 / 제거 addnode.sh addNode gridSetup.sh -addNode 노드 추가 add node cluvfy stage -pre nodeadd dbca -addInstance INS-06006 PRVG- PRVF- INS-13013 INS-40912 CLSRSC-184 PRVG-11050 PRVF-4664 PRVF-7617 cvuqdisk new node 새 서버 추가 클러스터 확장 새 서버를 클러스터에 합류시킬 때 선택하십시오(용량 증설, 하드웨어 교체). 무중단: 사전 점검(cluvfy), Grid home(addnode.sh), DB home과 새 인스턴스(dbca -addInstance)."},{"k":"scenario","v":"delnode","l":"클러스터에서 노드 제거","m":"노드 추가 / 제거 crsctl delete node deinstall -local dbca -deleteInstance 노드 제거 delete node remove node cluvfy stage -post nodedel crsctl unpin css CRS-4658 CRS-4661 runInstaller -updateNodeList srvctl remove instance decommission 서버를 클러스터에서 제거 기존 서버 철거 서버를 클러스터에서 영구적으로 제거할 때 선택하십시오(반납, 하드웨어 장애). 인스턴스를 삭제하고, 해당 노드에서 DB home과 Grid home을 제거한 뒤, 노드를 클러스터에서 삭제합니다. 다른 노드는 계속 열려 있습니다."},{"k":"scenario","v":"service","l":"서비스 추가, 변경, 이전 또는 삭제","m":"서비스와 네트워크 srvctl add service srvctl modify service srvctl relocate service srvctl remove service ORA-12514 PRCD-1026 preferred available TAF Application Continuity TAC FAN 서비스 이전 PRKO-2017 PRCR-1090 ORA-12514: TNS:listener does not currently know of service srvctl start service srvctl status service failback 서비스가 원위치로 돌아오지 않음 drain_timeout 서비스 추가 create service 애플리케이션이 접속하는 서비스를 정의하거나, 배치(preferre"},{"k":"scenario","v":"net","l":"SCAN, VIP, 리스너 포트 또는 interconnect 변경","m":"서비스와 네트워크 srvctl modify scan srvctl modify scan_listener srvctl modify nodeapps srvctl modify listener oifcfg setif oifcfg delif oifcfg getif IP 변경 SCAN IP VIP 변경 interconnect private network ORA-12545 listener port PRCS-1034 PRKO-2310 PRCR-1079 oifcfg HAIP 169.254 포트 변경 SCAN 변경 VIP change remote_listener ASMNET1LSNR_ASM SCAN 이름/IP, 노드 VIP, 리스너 포트 또는 private 네트워크(interconnect)를 변경할 때 선택하십시오. inter"},{"k":"scenario","v":"ocr_backup","l":"OCR과 OLR 백업 및 백업 확인","m":"OCR, OLR, voting ocrconfig -manualbackup ocrconfig -showbackup ocrconfig -local -manualbackup OLR 백업 OCR 백업 OCR backup ocrcheck ocrdump PROT- ocrconfig -copy ocrconfig -showbackuploc OLR backup OCR 백업 위치 수동 OCR 백업 무중단: 수동 OCR 백업, 각 노드의 OLR 백업, 백업을 클러스터 디스크 밖으로 복사. 패치, 노드 추가/제거, 네트워크 변경 같은 위험한 작업 전에 항상 선택하십시오."},{"k":"scenario","v":"ocr_restore","l":"OCR 손상 또는 CRS 디스크 그룹 손실: 백업에서 복원","m":"OCR, OLR, voting PROT-602 PROC-26 CRS-4535 CRS-4000 CRS-1606 ocrconfig -restore crsctl start crs -excl -nocrs OCR corruption OCR 손상 CRS 디스크 그룹 손실 voting disk 손실 ORA-15017 ORA-15063 PROT-35 PROT-19 ORA-15018 ORA-15033 ocrdump OCR restore CRSD 기동 안 됨 Logical corruption check failed `ocrcheck`가 손상을 보고하거나, CRSD가 기동되지 않거나(PROT-/PROC- 오류), CRS 디스크 그룹이 완전히 손실되었을 때 선택하십시오. 클러스터 전체가 중지됩니다. OCR을 exclusive 모"},{"k":"scenario","v":"olr_restore","l":"한 노드에서 Clusterware가 기동되지 않음: OLR 손상","m":"OCR, OLR, voting ocrcheck -local ocrconfig -local -restore OLR 손상 OLR corrupt ohasd 기동 안 됨 CRS-4124 CRS-4639 PROCL- CRS-4123 ohasd.trc olr.loc ocrconfig -local -showbackup Local registry integrity check failed OHASD 기동 안 됨 한 노드만 기동 안 됨 다른 노드는 정상인데 한 노드에서만 `crsctl start crs`가 OHASD를 기동하지 못하고 `ocrcheck -local`이 OLR 오류를 보고할 때 선택하십시오. 해당 노드만 영향을 받으며, OLR을 백업에서 복원합니다."},{"k":"scenario","v":"ocrvote_move","l":"OCR과 voting 파일을 다른 디스크 그룹으로 이전","m":"OCR, OLR, voting crsctl replace votedisk crsctl query css votedisk ocrconfig -add ocrconfig -delete voting disk 이전 votedisk OCR 이전 storage migration ORA-15274 ocrconfig -backuploc asmcmd spmove asmcmd pwmove CRS 디스크 그룹 이전 voting 파일 이전 OCR move 스토리지 교체 무중단: ocrconfig -add/-delete와 crsctl replace votedisk. 스토리지(SAN) 교체, CRS 디스크 그룹의 이중화 수준 상향, 또는 OCR을 두 번째 디스크 그룹에도 기록하려 할 때 선택하십시오."}]},{"id":"ora-asm-ops","file":"install/guides/ora-asm-ops.json","group":"Oracle","engineId":"oracle","category":"ops","title":"Oracle ASM 일상 운영 및 장애 시나리오","summary":"설치된 Oracle Restart 또는 RAC 환경에서 ASM 디스크 그룹의 일상 운영과 장애 대응을 다룹니다: 용량 점검(usable_file_mb), rebalance 모니터링, 디스크 추가/교체/제거, offline 디스크, 새 디스크 그룹, mount/dismount, ASM 구성 백업, 검색 문자열. ASM 설치는 다루지 않습니다(설치: Oracle Restart 또는 RAC 가이드","level":"고급","duration":"10분 – 몇 시간(rebalance 소요 시간에 따라)","topology":"Oracle Restart(단일 서버) 또는 2–8 node RAC, Grid Infrastructure 19c(23ai/26ai 차이는 참고 사항에)","tags":["oracle","asm","disk group","diskgroup","add disk","drop disk","replace disk","rebalance","v$asm_operation","usable_file_mb","disk_repair_time","online disk","offline disk","failgroup","compatible.asm","asmcmd","spbackup","md_backup","asm_diskstring","dsset","udev","asmlib","afd","multipath","rac","oracle restart","일상 운영","장애"],"order":13,"steps":4,"scenarios":[{"k":"op","v":"capacity","l":"용량 및 디스크 그룹 상태 점검","m":"점검 및 모니터링 usable_file_mb required_mirror_free_mb asmcmd lsdg 디스크 그룹 가득 참 ASM 사용률 free_mb 용량 ORA-15041 ORA-01653 ORA-17502 ORA-15041: diskgroup space exhausted diskgroup space exhausted disk group full ASM disk group full usable_file_mb 음수 negative usable_file_mb ASM 공간 부족 asmcmd du 아무것도 변경하지 않습니다: 디스크 그룹이 가득 찼는지, 남은 공간이 얼마인지(이중화 방식별 usable_file_mb의 올바른 해석), offline 디스크, 디스크 불균형, 호환성 속성. 공간 문제, 경보 "},{"k":"op","v":"rebal","l":"Rebalance 모니터링, 가속 또는 감속","m":"점검 및 모니터링 v$asm_operation gv$asm_operation rebalance REBALANCE POWER asm_power_limit asmcmd lsop est_minutes resync compact rebalance 느림 rebalance slow rebalance 멈춤 rebalance hung ERRS ARB0 rebalance 언제 끝나나 진행 중인 rebalance/resync의 진행률과 남은 시간(v$asm_operation, est_minutes), POWER 변경. 데이터베이스는 열린 상태로 유지되며 I/O 부하만 달라집니다."},{"k":"op","v":"add","l":"디스크 그룹에 새 디스크 추가","m":"디스크 작업 ADD DISK ALTER DISKGROUP ADD DISK 디스크 추가 공간 확보 용량 증설 ORA-15041 ORA-15031 ORA-15033 ORA-15014 ORA-15410 ORA-15032 ORA-15075 ORA-17502 add disk 새 LUN new LUN 디스크 그룹 확장 extend diskgroup 디스크 그룹의 공간이 부족하거나 확장이 필요한 경우: 새 LUN을 모든 node에서 준비하고(multipath/udev/ASMLib/AFD), ADD DISK로 추가한 뒤 rebalance를 모니터링합니다. 데이터베이스는 열린 상태로 유지됩니다."},{"k":"op","v":"replace","l":"디스크를 새 디스크로 교체(REPLACE DISK)","m":"디스크 작업 REPLACE DISK 디스크 교체 replace disk ORA-15145 storage migration 디스크 갱신 ORA-15221 ORA-15033 replace failed disk 장애 디스크 교체 disk replacement 스토리지 이전 storage migration ASM 장애(offline) 디스크나 교체 대상 디스크를 한 번의 작업으로 새 디스크로 바꿉니다. 해당 디스크의 데이터만 복사됩니다(12.1+, NORMAL/HIGH). EXTERNAL에서는 추가 + 제거를 한 명령으로 수행합니다. 데이터베이스는 열린 상태로 유지됩니다."},{"k":"op","v":"drop","l":"디스크 그룹에서 디스크 제거","m":"디스크 작업 DROP DISK 디스크 제거 UNDROP DISKS FORMER ORA-15041 ORA-15067 LUN 반납 drop disk DROPPING 디스크 분리 remove disk from diskgroup LUN 반환 LUN을 반납하거나 스토리지에서 제거하기 위한 작업입니다. DROP DISK로 데이터를 나머지 디스크로 옮기며, rebalance가 끝나고 디스크가 FORMER가 되기 전에는 디스크를 분리하지 않습니다. 데이터베이스는 열린 상태로 유지됩니다."},{"k":"op","v":"failed","l":"디스크 offline 또는 장애(NORMAL/HIGH redundancy)","m":"디스크 작업 ONLINE DISK OFFLINE DISK disk_repair_time repair_timer mode_status OFFLINE ORA-15042 ORA-15040 ORA-15130 ORA-15063 missing disk disk offline fast mirror resync ORA-15080 ORA-15081 ORA-27061 ORA-27072 ORA-15066 WARNING: Write Failed WARNING: Read Failed remaining active paths: 0 디스크 offline 됨 SYNCING 디스크 읽기 불가 I/O error ASM NORMAL/HIGH 디스크 그룹에서 디스크가 OFFLINE으로 보이거나 alert log에 쓰기/읽기 오류가 있는 경우:"},{"k":"op","v":"create","l":"새 디스크 그룹 생성","m":"디스크 그룹 관리 CREATE DISKGROUP 디스크 그룹 생성 au_size compatible.asm compatible.rdbms failgroup ORA-15018 ORA-15221 ORA-15072 ORA-15031 ORA-15033 create diskgroup new disk group 새 디스크 그룹 새 데이터, FRA 또는 애플리케이션 디스크 그룹을 만들기 위한 작업: redundancy, AU 크기, compatible.asm/rdbms 속성, RAC에서는 모든 node의 mount와 Clusterware 리소스. 기존 디스크 그룹은 영향을 받지 않습니다."},{"k":"op","v":"mount","l":"디스크 그룹 mount 또는 dismount","m":"디스크 그룹 관리 srvctl start diskgroup srvctl stop diskgroup ALTER DISKGROUP MOUNT DISMOUNT ORA-15027 ORA-15017 디스크 그룹 mount 안 됨 v$asm_client ORA-15063 ORA-15040 ORA-15130 ORA-15032 ORA-15042 PRCR-1065 CRS-2674 MOUNT FORCE diskgroup dismounted 디스크 그룹 열리지 않음 insufficient number of disks 디스크 그룹을 계획적으로 내리거나(dismount) 내려가 있거나 열리지 않는 디스크 그룹을 올리기(mount) 위한 작업으로, srvctl start/stop diskgroup으로 모든 node에서 수행합니다. "},{"k":"op","v":"spbackup","l":"ASM spfile, 비밀번호 파일 및 메타데이터 백업","m":"구성 asmcmd spbackup asmcmd spget asmcmd pwget asmcmd pwcopy md_backup md_restore ASM spfile 백업 orapwASM asmcmd spcopy GPnP profile profile.xml ASM password file backup ASM metadata backup ASM 백업 중단 없음: ASM spfile, 비밀번호 파일, 디스크 그룹 구조(md_backup), RAC에서는 GPnP 프로파일을 ASM 외부로 백업하여 디스크 그룹을 잃어도 ASM 구성을 다시 만들 수 있게 합니다. 설치 후와 구조를 변경할 때마다 실행합니다."},{"k":"op","v":"dstring","l":"ASM 디스크 검색 문자열(asm_diskstring) 변경","m":"구성 asm_diskstring asmcmd dsset asmcmd dsget discovery string 검색 문자열 ORA-15014 kfod ORA-15020 discovered duplicate ASM disk afd_dsset asm_diskstring change 디스크를 찾을 수 없음 ASM이 디스크를 찾는 경로 패턴을 바꾸기 위한 작업입니다(예: multipath로 전환, 새 udev 디렉터리, ASMLib/AFD). 먼저 kfod로 시험한 뒤 asmcmd dsset을 실행합니다. 기존 모든 디스크(OCR/voting 포함)가 새 문자열로도 검색되어야 합니다."}]},{"id":"ora-autoupgrade","file":"install/guides/ora-autoupgrade.json","group":"Oracle","engineId":"oracle","category":"upgrade","title":"19c → Oracle AI Database 26ai (23ai) 업그레이드: AutoUpgrade","summary":"메이저 릴리스 업그레이드(19c → 26ai)에 사용합니다. 같은 릴리스 안의 분기별 RU에는 \"Oracle Database 19c Release Update (RU) 적용\" 가이드를 사용합니다. Oracle이 권장하는 AutoUpgrade 도구로 진행합니다: 최신 autoupgrade.jar, config 파일, analyze → fixups → deploy, guaranteed resto","level":"고급","duration":"2–4시간(다운타임: 데이터베이스 크기와 구성 요소에 따라 30–90분)","topology":"서버 1대, Oracle Restart 또는 RAC(26ai home은 모든 노드에)","tags":["oracle","업그레이드","autoupgrade","19c","26ai","23ai","noncdb","pdb","target_cdb","restore point","restoration","timezone","compatible","롤백","rac","oracle restart","cluster_database","srvctl"],"order":20,"steps":25,"scenarios":[]},{"id":"ora-dataguard","file":"install/guides/ora-dataguard.json","group":"Oracle","engineId":"oracle","category":"ha","title":"Oracle Data Guard 19c physical standby (Broker 사용; 단일 인스턴스, Oracle Restart, RAC)","summary":"운영 중인 19c primary 데이터베이스에 대해 RMAN DUPLICATE … FROM ACTIVE DATABASE로 physical standby를 구축하고 Data Guard Broker에 연결합니다. 결과적으로 redo를 실시간으로 수신·적용하는 standby, SUCCESS 상태의 broker 구성, 검증된 switchover(선택 사항으로 failover/reinstate 테스트","level":"고급","duration":"2–4시간 (데이터 크기에 따라)","topology":"Primary (단일 인스턴스 / Oracle Restart / RAC 2–8 노드) + physical standby (단일 인스턴스 / Oracle Restart / RAC) + 선택 사항 observer","tags":["oracle","data guard","dataguard","standby","physical standby","dgmgrl","broker","rman duplicate","switchover","failover","fast-start failover","observer","19c","재해 복구","rac","oracle restart","srvctl","standby redo log","apply 인스턴스"],"order":20,"steps":43,"scenarios":[]},{"id":"ora-datapump","file":"install/guides/ora-datapump.json","group":"Oracle","engineId":"oracle","category":"backup","title":"Oracle Data Pump: export / import, 이관, 스키마 리프레시 및 transportable","summary":"expdp/impdp를 사용한 테이블, 스키마, 테이블스페이스 또는 전체(full) 범위의 export와 import, 두 데이터베이스 간 이관(덤프 파일 + scp + checksum 또는 NETWORK_LINK), 덤프에서 DDL 추출(SQLFILE). 디렉터리, 사용자 및 parfile 준비, 일관성, 병렬 처리, remap, 모니터링, 중단된 job 정리를 포함합니다. RAC에서는 S","level":"중급","duration":"30–90분(데이터 크기에 따라 다름)","topology":"데이터베이스 1개(단일 인스턴스, Oracle Restart 또는 RAC), 이관 시 소스 + 타깃","tags":["oracle","data pump","datapump","expdp","impdp","export","import","dump","parfile","remap_schema","network_link","sqlfile","table_exists_action","flashback_time","데이터 이관","migration","logical backup","논리 백업","rac","scan","cluster","acfs","cron","wallet","스케줄링","refresh","스키마 리프레시","prod to test","drop user cascade","transportable tablespace","tts","xtts","full transportable","transportable=always","dbms_tts","transport_set_check","endian","rman convert","cross-platform","11g to 19c","non-cdb to pdb","encryption","encryption_password","compression","attach","kill_job","start_job","continue_client","orphan","dba_datapump_jobs","lob","securefile","data_options"],"order":20,"steps":16,"scenarios":[{"k":"op","v":"export","l":"Export: 데이터베이스에서 덤프 파일 생성","m":"Export / import expdp export 덤프 받기 논리 백업 logical backup ORA-39001 ORA-39000 ORA-39070 ORA-31633 ORA-01555 ORA-39095 ORA-39094 ORA-00439 LRM-00123 UDE-00002 ORA-39127 테이블, 스키마, 테이블스페이스 또는 데이터베이스 전체를 덤프 파일에 기록합니다(expdp). 데이터베이스는 오픈 상태를 유지하고 애플리케이션도 계속 동작합니다. 논리적 복사본이므로 RMAN 백업을 대체하지 않습니다(시점 복구와 완전 복구에는 RMAN이 필요합니다)."},{"k":"op","v":"import","l":"Import: 덤프 파일을 데이터베이스에 적재","m":"Export / import impdp import 덤프 적재 REMAP_SCHEMA REMAP_TABLESPACE TABLE_EXISTS_ACTION ORA-39002 ORA-39087 ORA-39142 ORA-39143 ORA-39083 ORA-12899 ORA-39171 ORA-39082 ORA-31684 ORA-31640 ORA-39059 ORA-01917 ORA-39151 ORA-39111 ORA-01653 ORA-30036 이전에 받은 덤프 파일을 impdp로 적재합니다. 스키마/테이블스페이스/테이블 이름을 바꿀 수 있습니다. 타깃은 오픈 상태를 유지하며, 적재되는 객체를 사용하는 애플리케이션은 중지해야 합니다."},{"k":"op","v":"move","l":"이관: 소스 데이터베이스에서 타깃으로 이동","m":"이관 및 리프레시 데이터 이관 migration NETWORK_LINK database link scp checksum ORA-31679 ORA-02085 ORA-39183 Export → 파일 복사 → import, 또는 NETWORK_LINK로 덤프 파일 없이 직접 이관합니다. 타깃에 스키마가 아직 없으면(초기 적재) 이 카드를 선택하고, 타깃에 같은 스키마가 이미 있어 운영 데이터로 교체해야 하면 \"스키마 리프레시\" 카드를 선택합니다. 소스는 오픈 상태를 유지합니다."},{"k":"op","v":"sqlfile","l":"덤프 파일에서 DDL만 추출(SQLFILE)","m":"Export / import SQLFILE DDL 추출 덤프 내용 덤프에 무엇이 있는지 metadata DBMS_METADATA 아무것도 적재하지 않으며, 객체의 CREATE 문을 .sql 파일에 기록합니다. 데이터베이스는 변경되지 않습니다."},{"k":"op","v":"refresh","l":"운영에서 테스트로 스키마 리프레시(refresh)","m":"이관 및 리프레시 refresh 스키마 리프레시 테스트 리프레시 prod to test 운영에서 테스트로 UAT 리프레시 DROP USER CASCADE ORA-01940 ORA-02380 ORA-01917 ORA-31684 ORA-00031 TABLE_EXISTS_ACTION=REPLACE 테스트/UAT에 이미 있는 스키마를 운영의 최신 상태로 교체합니다: 타깃 스키마 백업, 세션 종료, DROP USER … CASCADE 또는 TABLE_EXISTS_ACTION=REPLACE, 패스워드/프로파일 및 권한 보존. 소스는 오픈 상태를 유지하며, 타깃 스키마는 작업 동안 사용할 수 없습니다."},{"k":"op","v":"tts","l":"데이터 파일 복사로 테이블스페이스 이동(transportable tablespace)","m":"Transportable(데이터 파일 복사) transportable tablespace TTS XTTS TRANSPORT_TABLESPACES TRANSPORT_DATAFILES DBMS_TTS TRANSPORT_SET_CHECK endian cross-platform RMAN CONVERT ORA-29335 ORA-29341 ORA-29342 ORA-29345 ORA-29349 ORA-19722 ORA-01647 대용량 테이블스페이스(수백 GB 이상)는 행 단위가 아니라 데이터 파일을 복사하여 이동합니다. 동일하거나 다른 플랫폼/endian(RMAN CONVERT) 모두 가능합니다. 문자 집합은 양쪽이 같아야 합니다. 작업 동안 소스의 테이블스페이스는 READ ONLY가 됩니다(쓰기 중단)."},{"k":"op","v":"fulltts","l":"데이터베이스 전체 이동: full transportable(11g/12c → 19c, non-CDB → PDB)","m":"Transportable(데이터 파일 복사) full transportable TRANSPORTABLE=ALWAYS FULL=Y VERSION=12 11g to 19c 11g에서 19c로 non-CDB to PDB PDB로 이관 migration upgrade by migration ORA-39123 ORA-01647 ORA-29349 ORA-39127 FULL=Y TRANSPORTABLE=ALWAYS로 사용자 테이블스페이스는 데이터 파일로, 나머지 전부(사용자, PL/SQL, 권한)는 메타데이터로 새 PDB에 이동합니다. 11.2.0.3+ 소스에서는 VERSION=12를 지정합니다. 작업 동안 소스의 모든 사용자 테이블스페이스는 READ ONLY가 됩니다."},{"k":"op","v":"jobfix","l":"중단되었거나 대기 중인 Data Pump job: 모니터링, 재개, 종료","m":"모니터링 및 장애 ATTACH START_JOB CONTINUE_CLIENT KILL_JOB STOP_JOB dba_datapump_jobs orphan master table NOT RUNNING IDLING 대기 상태 resumable ORA-39171 ORA-31626 ORA-31633 ORA-39002 ORA-31634 ORA-39097 ORA-39126 expdp/impdp job이 대기 상태(resumable wait)이거나 중지되었거나 중간에 끊겼습니다. ATTACH로 접속해 재개하거나, KILL_JOB으로 종료하거나, orphan 마스터 테이블을 정리하려는 경우입니다. 데이터베이스는 오픈 상태를 유지합니다."}]},{"id":"ora-dataguard-ops","file":"install/guides/ora-dataguard-ops.json","group":"Oracle","engineId":"oracle","category":"ops","title":"Oracle Data Guard 일상 운영 및 장애 시나리오 (19c / 23ai)","summary":"이미 구성된 Data Guard(physical standby) 환경의 일상 운영 및 장애 복구 runbook입니다. 최초 구성은 Data Guard 설치 가이드를 사용합니다. 무엇을 해야 할지 확실하지 않으면 '상태 및 지연 점검'부터 시작합니다. 읽기 전용 진단이 알맞은 시나리오(계획된 switchover, failover, 이전 primary reinstate, 아카이브 갭, RECOV","level":"고급","duration":"5분(점검) – 4시간(재구축)","topology":"Primary(단일 인스턴스 / Oracle Restart / RAC) + physical standby(단일 인스턴스 / Oracle Restart / RAC) + 선택 사항 observer","tags":["oracle","data guard","dataguard","standby","physical standby","dgmgrl","broker","switchover","failover","reinstate","archive gap","fal","recover from service","roll forward","snapshot standby","active data guard","redo apply","mrp","apply lag","transport lag","protection mode","fast-start failover","fsfo","observer","standby_file_management","standby redo log","unnamed","rman","runbook","dr","rac","19c","23ai"],"order":21,"steps":5,"scenarios":[{"k":"scenario","v":"health","l":"상태 및 지연 점검 (모두 정상인가?)","m":"모니터링 및 점검 ORA-16809 ORA-16810 ORA-16853 ORA-16855 ORA-16857 ORA-16766 ORA-16825 apply lag transport lag 지연 lag SHOW CONFIGURATION WARNING Data Guard 상태 ORA-16854 ORA-16856 ORA-16191 ORA-16198 ORA-03135 ORA-16714 ORA-16792 ORA-16047 standby 지연 여부 transport lag 높음 Data Guard 상태 점검 health check 읽기 전용: 구성, transport/apply lag, 아카이브 갭, 프로세스, alert log를 확인하며 아무것도 변경하지 않습니다. 무엇을 해야 할지 확실하지 않으면 여기서 시작합니다."},{"k":"scenario","v":"switchover","l":"계획된 switchover (역할 교체)","m":"역할 전환 switchover 역할 변경 role transition DR 훈련 ORA-16475 ORA-16470 ORA-16467 Ready for Switchover ORA-16541 ORA-16516 switchover_status TO STANDBY SESSIONS ACTIVE switchback 역할 원복 계획된 유지보수 사이트 전환 Primary와 standby가 정상이고 역할을 계획적으로 교체하려는 경우입니다(유지보수, DR 훈련, 역할 원복). 데이터 손실은 없으며 애플리케이션은 몇 분간 중단됩니다. Primary에 접속할 수 있는 한 역할 전환에는 항상 이 시나리오를 선택합니다."},{"k":"scenario","v":"failover","l":"Primary 상실: standby로 failover","m":"장애 및 복구 failover primary 다운 primary down 사이트 상실 disaster ORA-16472 ORA-01034 ORA-12541 FAILOVER TO ACTIVATE PHYSICAL STANDBY ORA-12514 ORA-12170 ORA-03113 ORA-16625 ORA-16778 primary 접속 불가 데이터센터 상실 FAILOVER TO IMMEDIATE 긴급 상황 Primary에 접속할 수 없고 복구되지 않는 경우입니다(서버, 스토리지, 사이트 상실). standby가 primary가 됩니다. 되돌릴 수 없습니다. MaxPerformance에서는 마지막 몇 초간의 데이터가 손실될 수 있고, 이전 primary는 이후 reinstate 또는 재구축이 필요합니다. Prima"},{"k":"scenario","v":"reinstate","l":"Failover 후 이전 primary를 standby로 복귀 (reinstate)","m":"장애 및 복구 reinstate ORA-16661 ORA-16795 ORA-38729 ORA-16653 needs to be reinstated 이전 primary flashback ORA-16649 split-brain 이전 primary가 열리지 않음 FastStartFailoverAutoReinstate REINSTATE DATABASE standby_became_primary_scn Failover가 수행되었고 이전 primary 서버가 다시 살아난 경우입니다. Flashback Database가 활성화되어 있으면 재구축 없이 새 primary의 standby로 만듭니다. 새 primary는 계속 열려 있습니다. Flashback이 비활성화되어 있거나 flashback 기간이 부족하면 진단 단계가 재"},{"k":"scenario","v":"gap","l":"아카이브 갭(gap): standby가 누락된 아카이브 로그를 기다림","m":"장애 및 복구 archive gap 아카이브 갭 v$archive_gap FAL Fetching gap ORA-00308 ORA-16401 ORA-27037 ORA-16055 gap sequence waiting for log WAIT_FOR_GAP FAL[client] Media Recovery Waiting for thread 누락된 아카이브 로그 missing archive log ORA-16089 `v$archive_gap`이 행을 반환하거나 alert log에 FAL/gap 메시지가 있고, apply가 특정 sequence에서 멈춰 있습니다. 데이터베이스는 열린 상태를 유지합니다. 누락된 아카이브 로그가 어디에도 없으면 폼에서 선택하여 롤 포워드 경로로 전환합니다."},{"k":"scenario","v":"rollfwd","l":"Standby가 크게 뒤처졌거나 아카이브 로그가 없음: 롤 포워드 (RECOVER FROM SERVICE)","m":"장애 및 복구 roll forward standby 롤 포워드 RECOVER STANDBY DATABASE FROM SERVICE nologging ORA-01578 ORA-26040 standby 지연 resync 재동기화 ORA-10567 BACKUP INCREMENTAL FROM SCN standby 며칠 지연 NONLOGGED BLOCK incremental roll forward 갭에 해당하는 아카이브 로그가 어디에도 없거나, NOLOGGING 작업으로 standby가 손상되었거나, standby가 며칠씩 뒤처진 경우입니다. standby를 재구축하지 않고 primary에서 증분 방식으로 동기화합니다(18c+)."},{"k":"scenario","v":"rebuild","l":"Standby를 처음부터 재구축","m":"장애 및 복구 rebuild 재구축 re-create standby ORA-16795 DUPLICATE standby 상실 컨트롤 파일 상실 ORA-00205 ORA-01157 standby 파일 삭제됨 DROP DATABASE standby 디스크 상실 Reinstate(flashback 없음)와 롤 포워드가 모두 불가능하거나, standby의 파일/컨트롤 파일이 상실된 경우입니다. standby를 삭제하고 DUPLICATE로 다시 생성합니다. Primary는 열린 상태를 유지하지만 재구축이 끝날 때까지 보호되지 않습니다(다른 standby가 없는 경우)."},{"k":"scenario","v":"snapshot","l":"Snapshot standby로 전환 / 원복 (테스트용 쓰기 가능 복사본)","m":"테스트 및 조회 snapshot standby CONVERT TO SNAPSHOT STANDBY 테스트 환경 read write standby ORA-16000 ORA-38784 ORA-16433 CONVERT TO PHYSICAL STANDBY SNAPSHOT STANDBY guaranteed restore point 테스트용 standby 오픈 Standby를 일시적으로 읽기-쓰기로 열어 테스트한 뒤 변경 사항을 버리고 physical standby로 돌아가려는 경우입니다. Primary는 영향을 받지 않고 redo도 계속 수신되지만, snapshot standby 상태인 동안 이 standby는 failover 준비 상태가 아닙니다."},{"k":"scenario","v":"apply","l":"Redo Apply 중지 / 시작, apply 인스턴스 변경, read-only 오픈","m":"일상 운영 MRP redo apply APPLY-OFF APPLY-ON apply instance read only Active Data Guard ORA-16766 ORA-16136 ORA-01153 real-time query WAIT_FOR_LOG APPLYING_LOG READ ONLY WITH APPLY OPEN READ ONLY PreferredApplyInstance intended state apply 중지됨 MRP 미실행 유지보수를 위해 MRP를 중지하거나 재시작하거나, RAC standby에서 apply를 다른 인스턴스로 옮기거나, 리포트용으로 standby를 열려는 경우입니다(Active Data Guard 라이선스 주의)."},{"k":"scenario","v":"prot","l":"보호 모드, Fast-Start Failover, observer","m":"일상 운영 protection mode MaxAvailability MaxPerformance MaxProtection SYNC ASYNC FSFO fast-start failover observer ORA-16627 ORA-16651 ORA-16820 ORA-16819 ORA-16072 ORA-16647 NetTimeout LogXptMode FASTSYNC protection_level 보호 모드 MaxPerformance / MaxAvailability / MaxProtection 간 전환, FSFO 활성화/비활성화 또는 observer 관리를 하려는 경우입니다."},{"k":"scenario","v":"files","l":"Datafile, redo log, standby redo log 추가 / UNNAMED 파일","m":"일상 운영 standby_file_management UNNAMED ORA-01111 ORA-01110 ORA-01157 ORA-01274 datafile 추가 add datafile redo log 추가 standby redo log SRL ORA-16789 db_file_name_convert ADD STANDBY LOGFILE ORA-01136 datafile 추가 standby Primary에 datafile 또는 redo log를 추가해야 하거나, standby에 UNNAMED 파일이 생겼거나(ORA-01111/ORA-01274), standby redo log가 누락된 경우입니다."},{"k":"scenario","v":"backup","l":"Standby에서 RMAN 백업 수행","m":"일상 운영 rman standby backup standby에서 백업 archivelog deletion policy RMAN-08137 recovery catalog offload backup RMAN-06820 BACKED UP 1 TIMES CONFIGURE DB_UNIQUE_NAME standby 백업 Primary의 백업 부하를 덜기 위해 RMAN 백업을 standby에서 실행하고, 이에 맞춰 아카이브 로그 삭제 정책을 설정하려는 경우입니다."}]},{"id":"ora-backup-strategy","file":"install/guides/ora-backup-strategy.json","group":"Oracle","engineId":"oracle","category":"backup","title":"Oracle 백업 전략 및 자동화 (RMAN + Data Pump)","summary":"설치된 데이터베이스에 상시 자동 백업 체계를 구성합니다(장애 시 복구는 별도 가이드 \"RMAN restore 및 recovery\"에서 다룹니다): 주간 level 0 + 일간 level 1(또는 매일 full), 잦은 아카이브 로그 백업, FRA 또는 별도 디렉터리, retention, 압축, 암호화, block change tracking, 병렬 처리. 추가로 wallet을 사용하는 전체 ","level":"중급","duration":"60–120분(첫 level 0 소요 시간 제외)","topology":"서버 1대(단일 인스턴스 또는 Oracle Restart) 또는 RAC(2–8 node)","tags":["oracle","rman","백업","backup","전략","level 0","level 1","incremental","archivelog","fra","retention","block change tracking","data pump","expdp","crontab","cron","systemd timer","dbms_scheduler","자동화","wallet","mkstore","rac","oracle restart","snapshot controlfile","asm","/etc/cron.d","ol7","recovery catalog","create catalog","register database","복구 훈련","restore drill","restore validate","preview"],"order":21,"steps":33,"scenarios":[]},{"id":"ora-rman-restore","file":"install/guides/ora-rman-restore.json","group":"Oracle","engineId":"oracle","category":"backup","title":"RMAN을 이용한 restore 및 recovery","summary":"장애 발생 시 올바른 RMAN 복구를 단계별로 진행합니다. 19개 시나리오: datafile, SYSTEM/UNDO, tempfile, 손상 블록, online redo log, controlfile, spfile 손실; 전체 restore, 다른 디렉터리/ASM 및 다른 서버로 restore; 시점 복구(CDB 또는 PDB PITR), Flashback Database, tablespace","level":"고급","duration":"30분 – 수 시간(데이터베이스 크기에 따라 다름)","topology":"서버 1대(단일 인스턴스 또는 Oracle Restart) 또는 RAC. 새 호스트 시나리오에서는 소스 + 대상","tags":["oracle","rman","restore","recovery","데이터베이스 복구","pitr","point in time","resetlogs","recover table","controlfile","spfile","autobackup","datafile","preview","validate","dbid","새 서버","disaster recovery","rac","oracle restart","cluster_database","srvctl","asm","block corruption","손상 블록","ORA-01578","redo log","ORA-00313","clear logfile","tempfile","tspitr","flashback database","restore point","dropped pdb","unplug","from service","data guard","restore archivelog","catalog start with","set newname","undo","system tablespace"],"order":22,"steps":10,"scenarios":[{"k":"scenario","v":"preview","l":"모의 실행: RESTORE … PREVIEW / VALIDATE (아무것도 변경하지 않음)","m":"모의 실행 및 백업 기록 RESTORE PREVIEW RESTORE VALIDATE restore 모의 실행 restore 테스트 백업이 정상인지 backup test restore drill 복구 훈련 RMAN VALIDATE 백업 무결성 확인 손상된 것은 없으며, 데이터를 쓰지 않고 백업이 실제 복구에 충분한지 확인하려는 경우입니다(데이터베이스는 열린 상태 유지). 월간 restore 훈련이나 어떤 시나리오를 골라야 할지 확신이 없을 때의 첫 단계이기도 합니다."},{"k":"scenario","v":"catalog","l":"백업이 다른 디렉터리로 이동됨 / RMAN이 백업을 찾지 못함 (CATALOG)","m":"모의 실행 및 백업 기록 CATALOG START WITH RMAN-06023 RMAN-06025 RMAN-06026 ORA-19505 ORA-27037 EXPIRED 백업 이동됨 backup moved no backup found CROSSCHECK NFS 변경됨 백업을 찾을 수 없음 백업 piece는 있지만 RMAN이 알지 못합니다(이동됨, NFS 변경, 다른 서버에서 복사됨). RMAN-06023/06025가 발생합니다. 데이터베이스는 열린 상태를 유지합니다."},{"k":"scenario","v":"arch","l":"아카이브 로그만 restore (RESTORE ARCHIVELOG)","m":"모의 실행 및 백업 기록 RESTORE ARCHIVELOG ORA-00308 아카이브 로그 restore archive log restore archive gap LogMiner SET ARCHIVELOG DESTINATION LogMiner, standby gap 또는 수동 recovery를 위해 특정 sequence 범위의 아카이브 로그가 디스크에 필요합니다. 데이터베이스는 열린 상태를 유지하며 어떤 파일도 덮어쓰지 않습니다."},{"k":"scenario","v":"datafile","l":"단일 datafile 또는 tablespace 손실/손상 (데이터베이스는 열린 상태 유지)","m":"손실 / 손상 ORA-01157 ORA-01110 ORA-01116 ORA-27041 ORA-00376 ORA-01113 datafile 손실 datafile 삭제됨 missing datafile lost datafile tablespace 손실 ORA-27037 ORA-01135 ORA-01115 ORA-01114 datafile 손상 데이터베이스는 열려 있고 datafile 또는 tablespace 하나만 읽을 수 없습니다(ORA-01157/01110/01116). 나머지는 모두 정상입니다. 파일이 SYSTEM/UNDO에 속하면 \"SYSTEM 또는 UNDO\" 카드를, tempfile이면 \"Tempfile\" 카드를, 파일은 열리지만 일부 블록이 손상되었으면(ORA-01578) \"손상 블록\" 카드를 선택하십"},{"k":"scenario","v":"block","l":"손상 블록 (ORA-01578, block media recovery)","m":"손실 / 손상 ORA-01578 ORA-26040 ORA-01110 손상 블록 block corruption corrupt block v$database_block_corruption RECOVER CORRUPTION LIST block media recovery nologging ORA-19566 Marked Corrupt Corrupt block relative dba DBVERIFY 쿼리에서 ORA-01578/ORA-26040이 발생하거나, 백업이 ORA-19566으로 중단되거나, VALIDATE가 손상 블록을 발견했습니다. 파일의 나머지 부분은 정상입니다. 데이터베이스는 열린 상태를 유지하고 손상된 블록만 복구합니다(Enterprise Edition. SE2에서는 파일을 restore합니다). 파일 "},{"k":"scenario","v":"sysdf","l":"SYSTEM 또는 UNDO datafile 손실 (데이터베이스가 열리지 않음)","m":"손실 / 손상 ORA-01157 ORA-01110 ORA-01116 ORA-01147 ORA-00376 ORA-01541 SYSTEM datafile UNDO datafile undo tablespace 손실 system tablespace lost 데이터베이스가 열리지 않음 ORA-01113 system01.dbf undotbs 손실되거나 손상된 파일이 SYSTEM 또는 UNDO tablespace에 속합니다. offline으로 전환할 수 없으므로 데이터베이스(PDB 파일이면 해당 PDB만)를 종료하고 MOUNT 상태에서 restore합니다. 데이터 손실은 없습니다. 다른 tablespace의 파일이면 \"단일 datafile\" 카드로 데이터베이스를 종료하지 않고 해결합니다."},{"k":"scenario","v":"temp","l":"Tempfile 손실 또는 손상 (TEMP tablespace)","m":"손실 / 손상 ORA-01157 ORA-01187 ORA-25153 ORA-01116 ORA-01122 ORA-01110 tempfile temp tablespace 임시 tablespace TEMP 손실 temp01.dbf dba_temp_files 정렬/해시 작업에서 ORA-01157/ORA-01187/ORA-25153이 발생하고 해당 파일이 tempfile입니다(오류 메시지의 번호가 `db_files` 값보다 큼, 예: `data file 201`). restore는 필요 없으며 새 tempfile을 추가합니다. 데이터베이스는 열린 상태를 유지합니다."},{"k":"scenario","v":"redo","l":"Online redo log 손실 또는 손상","m":"손실 / 손상 ORA-00313 ORA-00312 ORA-00327 ORA-00366 ORA-00367 ORA-00353 ORA-00354 ORA-00316 ORA-01624 redo log 손실 online redo lost redo log CLEAR LOGFILE ORA-00314 ORA-00320 ORA-00321 ORA-00340 ORA-00350 ORA-00470 CLEAR UNARCHIVED Alert log에 ORA-00313/00312(열 수 없음) 또는 ORA-00327/00366/00367(손상)이 기록됩니다. 멤버 하나만 손실되었거나 그룹이 INACTIVE이면 데이터베이스는 열린 상태를 유지합니다. CURRENT 그룹이 손실되면 restore + RESETLOGS가 필요하며 마지막 트랜잭"},{"k":"scenario","v":"controlfile","l":"모든 controlfile 사본 손실","m":"손실 / 손상 ORA-00205 ORA-00202 ORA-00210 ORA-27037 controlfile 손실 control file lost RESTORE CONTROLFILE autobackup ORA-00206 ORA-00214 ORA-00221 ORA-00227 controlfile 손상 인스턴스가 MOUNT 단계에서 controlfile을 찾거나 읽지 못합니다(ORA-00205/00202/00210). datafile과 redo는 정상입니다. 사본 중 하나가 정상이면 restore가 필요 없습니다(진단 단계에서 확인). 모두 손실되었으면 autobackup에서 restore하고 RESETLOGS로 엽니다. online redo가 정상이므로 데이터 손실은 없습니다."},{"k":"scenario","v":"spfile","l":"spfile 손실","m":"손실 / 손상 ORA-01078 LRM-00109 ORA-01565 spfile 손실 spfile missing parameter file RESTORE SPFILE CRS-5017 PRCR-1079 spfile 삭제됨 spfile이 삭제되었거나 손상되었습니다. 인스턴스가 시작되지 않거나(ORA-01078/LRM-00109) 실행 중이지만 파일이 없습니다. datafile은 건드리지 않으며 데이터 손실이 없습니다. 인스턴스가 아직 실행 중이면 서비스 중단도 없습니다."},{"k":"scenario","v":"full","l":"전체 restore, 동일 서버 (datafile 손실, controlfile 및 redo 정상)","m":"손실 / 손상 모든 datafile 손실 all datafiles lost full restore RESTORE DATABASE 디스크 장애 storage failure 디스크 손실 LUN 손실 datafile 삭제됨 datafile 전부 또는 대부분이 사라졌고 controlfile과 online redo log는 정상입니다. 데이터베이스를 종료하며 데이터 손실은 없습니다. 파일이 있던 기존 디스크/디렉터리를 더 이상 쓸 수 없으면 \"다른 디렉터리\" 카드를, controlfile도 손실되었으면 먼저 \"controlfile\" 카드를, 서버 전체가 사라졌으면 \"다른 서버로 restore\" 카드를 선택하십시오."},{"k":"scenario","v":"relocate","l":"디스크 손실: 데이터베이스를 다른 디렉터리 / ASM으로 restore (SET NEWNAME)","m":"손실 / 손상 SET NEWNAME SWITCH DATAFILE ALL 다른 디렉터리로 restore ASM으로 restore new location 디스크 교체됨 mount point 손실 relocate ORA-15001 ORA-15040 disk group dismounted datafile이 있는 디스크 또는 디스크 그룹을 사용할 수 없습니다. controlfile과 redo는 정상입니다. 파일을 새 디렉터리나 ASM 디스크 그룹으로 restore하며, 데이터베이스는 종료되고 데이터 손실은 없습니다."},{"k":"scenario","v":"pitr","l":"시점 / SCN 복구 (PITR, RESETLOGS)","m":"시점 복구 및 되돌리기 PITR point in time recovery 시점 복구 SCN으로 복구 잘못된 DELETE 잘못된 UPDATE TRUNCATE 사용자 오류 logical error UNTIL TIME 데이터 삭제됨 DROP USER UNTIL SCN RESETLOGS 잘못된 DELETE/UPDATE/TRUNCATE 또는 결함 있는 애플리케이션 릴리스: 데이터베이스(또는 PDB만)를 과거 시점으로 되돌립니다. 그 시점 이후의 모든 변경 사항은 손실됩니다(RESETLOGS). 먼저 범위가 더 좁은 방법을 검토하십시오. 단일 테이블 → \"단일 테이블 복구\", 단일 tablespace → TSPITR, flashback이 활성화되어 있으면 → \"Flashback Database\"(restore 없음, "},{"k":"scenario","v":"flashback","l":"Flashback Database로 되감기 (restore point / SCN / 시간)","m":"시점 복구 및 되돌리기 FLASHBACK DATABASE FLASHBACK PLUGGABLE DATABASE restore point guaranteed restore point 데이터베이스 되감기 업그레이드 롤백 테스트 후 되돌리기 FLASHBACK_ON FLASHBACK TO RESTORE POINT Flashback이 활성화되어 있거나 guaranteed restore point가 있고 손실된 파일이 없습니다. restore 없이 몇 분 안에 되감습니다(Enterprise Edition). RESETLOGS가 필요합니다. 활성화 여부: `SELECT flashback_on FROM v$database;` → YES, 또는 `v$restore_point`에 GUARANTEE_FLASHBACK_DATABA"},{"k":"scenario","v":"tspitr","l":"tablespace를 과거 시점으로 되돌리기 (TSPITR)","m":"시점 복구 및 되돌리기 TSPITR tablespace point in time RECOVER TABLESPACE AUXILIARY DESTINATION DBMS_TTS tablespace 복구 ORA-29308 잘못된 작업이 tablespace 하나의 객체에만 영향을 주었고, 데이터베이스의 나머지는 현재 상태를 유지해야 합니다(Enterprise Edition). 작업 동안 tablespace는 offline이 되며 그 안의 모든 객체가 대상 시점으로 돌아갑니다. 테이블 한두 개만 영향을 받았다면 \"단일 테이블 복구\"가 되돌리는 범위가 더 작습니다."},{"k":"scenario","v":"table","l":"단일 테이블 복구 (RECOVER TABLE, 12c+ Enterprise Edition)","m":"시점 복구 및 되돌리기 RECOVER TABLE 테이블 삭제됨 DROP TABLE 테이블 복구 table recovery ORA-00942 REMAP TABLE ORA-38305 FLASHBACK TABLE recyclebin PURGE 테이블 하나가 실수로 삭제되었거나 손상되었고, 데이터베이스의 나머지는 그대로 유지되어야 합니다(12c+ Enterprise Edition). DROP되었고 PURGE하지 않았다면 먼저 recyclebin을 확인하십시오. `FLASHBACK TABLE … TO BEFORE DROP`은 몇 초면 됩니다(진단 단계에서 확인)."},{"k":"scenario","v":"pdbdrop","l":"삭제된 PDB 되살리기 (DROP PLUGGABLE DATABASE)","m":"시점 복구 및 되돌리기 DROP PLUGGABLE DATABASE PDB 삭제됨 dropped PDB RMAN-06813 ORA-65011 ORA-65179 KEEP DATAFILES PDB 복구 unplug DBMS_PDB.RECOVER PDB가 `DROP PLUGGABLE DATABASE`로 삭제되었습니다. 파일이 남아 있으면 XML로 다시 plug in하고, 삭제되었으면 백업에서 되살립니다(21c+는 단일 명령, 19c는 임시 CDB를 통해). CDB는 열린 상태를 유지합니다."},{"k":"scenario","v":"newhost","l":"다른 서버로 restore (새 호스트, controlfile autobackup에서)","m":"다른 서버 및 standby 새 서버 다른 서버 서버 손실 disaster recovery DR new host 데이터베이스 복제 clone 테스트 환경 서버 소실 server lost restore to another host 서버가 완전히 사라졌거나 사본/테스트 환경을 구축하려는 경우입니다. 백업을 다른 서버로 restore합니다(단일 인스턴스, 파일 시스템만 해당). 백업된 마지막 아카이브 로그까지 복구되며 그 이후는 손실됩니다. 사본은 RESETLOGS로 엽니다."},{"k":"scenario","v":"fromsvc","l":"Data Guard: standby/primary에서 네트워크로 파일 가져오기 (FROM SERVICE)","m":"다른 서버 및 standby RESTORE FROM SERVICE FROM SERVICE RECOVER STANDBY DATABASE standby에서 restore Data Guard standby 지연 archive gap standby controlfile ORA-01110 v$archive_gap apply lag Data Guard 환경입니다. primary의 손실된 datafile은 standby에서, 뒤처진 standby나 손실된 standby controlfile은 primary에서 백업 piece 없이 가져옵니다(12c+)."}]},{"id":"ora-goldengate","file":"install/guides/ora-goldengate.json","group":"Oracle","engineId":"oracle","category":"ha","title":"Oracle GoldenGate Microservices: Oracle → Oracle 단방향 복제","summary":"GoldenGate 21c 또는 23ai/26ai Microservices로 PDB 스키마 하나를 다른 데이터베이스로 지속적으로(수 초 지연으로) 복제합니다. 완료 후 소스에서는 Integrated Extract와 Distribution Path가, 타깃에서는 Parallel Replicat이 실행됩니다. 타깃 스키마는 일관된 SCN 기준의 초기 로드로 채워지고, heartbeat로 종단 간","level":"고급","duration":"3–5시간","topology":"소스 DB 서버(OGG 소스 deployment) + 타깃 DB 서버(OGG 타깃 deployment). RAC에서는 GoldenGate를 1번 노드에 설치(또는 XAG로 클러스터 리소스로 등록)","tags":["oracle","goldengate","ogg","microservices","복제","replication","cdc","integrated extract","parallel replicat","distribution path","adminclient","oggca","21c","23ai","26ai","rac","scan","xag","acfs","tranlogoptions"],"order":30,"steps":42,"scenarios":[]},{"id":"ora-goldengate-ops","file":"install/guides/ora-goldengate-ops.json","group":"Oracle","engineId":"oracle","category":"ops","title":"Oracle GoldenGate 일상 운영 및 장애 시나리오 (Microservices 21c / 23ai)","summary":"이미 구성된 GoldenGate Microservices 복제의 일상 운영 및 장애 복구 runbook입니다(설치는 GoldenGate 설치 가이드 참조). 무엇을 해야 할지 확실하지 않으면 '상태 및 지연 점검'부터 시작합니다. 읽기 전용 진단이 알맞은 시나리오로 안내합니다 — Extract와 Replicat ABEND(report, discard, OGG-xxxxx 오류), 데이터가 있는","level":"고급","duration":"5분 (점검) – 2시간 (업그레이드, 테이블 추가)","topology":"소스 서버 (소스 deployment, Integrated Extract, Distribution Path) + 타깃 서버 (타깃 deployment, Parallel Replicat)","tags":["oracle","goldengate","ogg","microservices","adminclient","runbook","abend","extract","replicat","lag","stats","info all","heartbeat","report","discard","reperror","handlecollisions","cdr","충돌","add trandata","schematrandata","aftercsn","atcsn","mapexclude","tableexclude","resync","purge trail","purgeoldextracts","trail","alter extract scn","etrollover","업그레이드","패치","opatch","oggHome","data guard","switchover","failover","handledlfailover","OGG-00664","OGG-01004","OGG-01296","OGG-02912","21c","23ai","26ai"],"order":31,"steps":8,"scenarios":[{"k":"scenario","v":"health","l":"상태 및 지연 점검 (모두 정상인가?)","m":"모니터링 및 점검 INFO ALL lag 지연 STATS heartbeat GG_LAG Service Manager ggserr.log OGG-01027 OGG-01738 Long Running Transaction STOPPED RUNNING Distribution Path DISTPATH Receiver INFO DISTPATH 복제 지연 replication lag 읽기 전용: Service Manager와 microservice, INFO ALL, lag, STATS, heartbeat 지연, 데이터베이스의 capture 상태, ggserr.log. 아무것도 변경하지 않습니다. 무엇을 해야 할지 확실하지 않으면 여기서 시작합니다."},{"k":"scenario","v":"ext_abend","l":"Extract ABENDED (오류로 중지됨)","m":"장애 (ABEND) Extract ABENDED ABEND OGG-00664 OGG-00446 OGG-02171 OGG-02912 OGG-02022 OGG-02091 OGG-01044 OGG-01668 ORA-01291 ORA-01017 ORA-28001 ORA-04031 ORA-01031 missing log archived log not found logmining OGG-00868 Extract STARTING Extract 중지 capture ABORTED 소스의 Extract가 ABENDED이거나 계속 재시작됩니다. report 파일의 OGG-xxxxx 오류로 원인(연결, 누락된 아카이브 로그, log mining, 권한, 디스크)을 찾아 해결한 뒤 Extract를 멈춘 지점부터 다시 시작합니다. "},{"k":"scenario","v":"rep_abend","l":"Replicat ABENDED (타깃에 적용 불가)","m":"장애 (ABEND) Replicat ABENDED OGG-01004 OGG-01296 OGG-01154 OGG-00869 OGG-01161 OGG-01169 OGG-01431 OGG-00199 OGG-00423 ORA-01403 ORA-00001 ORA-02291 ORA-00942 ORA-01653 no data found unique constraint discard REPERROR HANDLECOLLISIONS CDR 충돌 Replicat 중지 ORA-01400 ORA-12899 value too large 타깃의 Replicat이 트랜잭션을 적용하지 못했습니다. report와 discard 파일에서 테이블, 작업, 오류를 찾고, 데이터를 수정하거나 임시 REPERROR로 건너뛴 뒤 Replicat을 멈"},{"k":"scenario","v":"add_table","l":"데이터가 있는 테이블을 복제에 추가 (초기 적재 + AFTERCSN)","m":"테이블 관리 테이블 추가 add table ADD TRANDATA ADD SCHEMATRANDATA AFTERCSN ATCSN initial load 초기 적재 flashback_scn MAPEXCLUDE 새 테이블 이미 존재하고 데이터가 들어 있는 테이블을 실행 중인 복제에 중단 없이 추가합니다: ADD TRANDATA, 일관된 SCN으로 expdp/impdp, 그리고 이 테이블만 해당 SCN 이후부터 적용하는 별도 Replicat(AFTERCSN). 다른 테이블은 계속 복제됩니다."},{"k":"scenario","v":"remove_table","l":"복제에서 테이블 제외","m":"테이블 관리 테이블 제외 remove table TABLEEXCLUDE MAPEXCLUDE DELETE TRANDATA exclude 테이블을 더 이상 복제하지 않습니다: Extract에 TABLEEXCLUDE(또는 TABLE 줄 제거), Replicat에 MAPEXCLUDE, 필요하면 DELETE TRANDATA. 프로세스는 잠시 재시작됩니다."},{"k":"scenario","v":"resync","l":"단일 테이블 재동기화 (resync)","m":"테이블 관리 resync 재동기화 out of sync 데이터 불일치 어긋남 Veridata OGG-01004 ORA-01403 ORA-00001 row count 행 수 불일치 타깃의 테이블 데이터가 소스와 어긋났습니다(수동 변경, 건너뛴 오류, 불완전한 초기 적재). 테이블을 소스에서 일관된 SCN으로 다시 적재하고, 임시 Replicat이 그 SCN 이후의 변경을 적용합니다. 다른 테이블은 영향을 받지 않습니다."},{"k":"scenario","v":"purge","l":"Trail 파일이 디스크를 채움: 정리 (Purge Trail)","m":"유지보수 trail purge PURGEOLDEXTRACTS Purge Trail 디스크 가득 참 No space left on device trail 정리 var/lib/data trail 디렉터리 가득 참 ENOSPC Trail 디렉터리가 커지거나 디스크가 곧 가득 찹니다. 어떤 파일을 읽고 있는지 확인하고, Purge Trail 작업(Classic의 PURGEOLDEXTRACTS에 해당)을 USECHECKPOINTS로 정의합니다. 긴급 시에는 이미 읽은 파일을 수동으로 삭제합니다."},{"k":"scenario","v":"reposition","l":"Extract를 특정 SCN으로 재배치","m":"유지보수 reposition ALTER EXTRACT SCN BEGIN SCN ETROLLOVER SCN부터 시작 trail 손실 first_scn 되감기 Extract가 처음부터 다시 읽기를 원하는 SCN이 있습니다(trail 손실, 잘못된 시작점, 타깃 롤백). ALTER EXTRACT … SCN과 ETROLLOVER로 새 trail 파일부터 시작합니다. 같은 데이터가 타깃에 두 번 도착할 수 있습니다."},{"k":"scenario","v":"upgrade","l":"GoldenGate 버전 업그레이드 / 패치 (Microservices)","m":"유지보수 업그레이드 패치 업그레이드 OPatch out-of-place oggHome Service Manager upgrade bundle patch 21c 23ai 26ai release update GoldenGate patch Service Manager PATCH 새 GoldenGate 릴리스나 패치로 전환합니다: out-of-place(새 home을 설치하고 Service Manager와 deployment를 REST로 새 home으로 이동) 또는 in-place OPatch. Extract/Replicat이 잠시 중지되며, 롤백은 이전 home으로 돌아가는 것입니다."},{"k":"scenario","v":"dg_role","l":"소스 Data Guard switchover/failover 후 Extract 계속 운영","m":"역할 전환 Data Guard switchover failover role transition HANDLEDLFAILOVER ORA-01033 ORA-16000 ORA-01109 새 primary standby FSFO ORA-12514 역할 전환 Extract 새 primary FAILOVERTARGETDESTID 소스 데이터베이스에서 switchover 또는 failover가 일어났습니다. Extract는 새 primary에 접속해 checkpoint부터 계속해야 합니다. 연결(credential alias/서비스), capture 상태, HANDLEDLFAILOVER, 데이터 불일치 가능성을 점검합니다."}]},{"id":"ora-security","file":"install/guides/ora-security.json","group":"Oracle","engineId":"oracle","category":"security","title":"Oracle Database 보안 강화 (점검, 네트워크, TLS, 인증, 권한, Unified Auditing, TDE)","summary":"설치 가이드가 아닙니다: 운영 중인 Oracle 19c / 23ai (26ai) 데이터베이스(단일 인스턴스, Oracle Restart 또는 RAC, CDB/PDB)의 보안 취약점을 찾아 해소합니다. 먼저 읽기 전용 상태 점검이 취약점을 심각도 순으로 나열하고 각 발견 사항이 어느 카드로 이어지는지 알려줍니다. 그다음 영역별로 하나씩 또는 전체를 순서대로 해소합니다: listener와 TCP","level":"고급","duration":"5분(점검) – 4시간(전체 보안 강화)","topology":"단일 인스턴스 / Oracle Restart / RAC(2–8 노드), CDB + PDB, 애플리케이션 서버(클라이언트 검증)","tags":["oracle","보안","security","hardening","보안 강화","cis benchmark","stig","tcps","wallet","orapki","sqlnet.ora","native network encryption","TCP.VALIDNODE_CHECKING","ALLOWED_LOGON_VERSION_SERVER","profile","ORA12C_STRONG_VERIFY_FUNCTION","unified auditing","AUDSYS","DBMS_AUDIT_MGMT","TDE","keystore","ADMINISTER KEY MANAGEMENT","UTL_FILE","PUBLIC","기본 비밀번호"],"order":40,"steps":2,"scenarios":[{"k":"scenario","v":"scan","l":"상태 점검: 어떤 취약점이 있는가? (읽기 전용)","m":"시작 보안 점검 security audit hardening check CIS STIG DBSAT 취약점 보안 취약점 vulnerability 모의 해킹 결과 dba_users_with_defpwd 점검 쿼리 보안 스캔 security scan CRITICAL 가장 먼저 실행할 스캔: 기본 비밀번호를 가진 계정과 open 상태 계정, 구버전 비밀번호 verifier, 프로파일, DBA/ANY 권한, PUBLIC에 부여된 UTL_* PL/SQL 패키지, audit, TDE, 암호화되지 않은 세션, listener와 파일 권한. 아무것도 변경하지 않습니다."},{"k":"scenario","v":"all","l":"전체 보안 강화 (모두 순서대로)","m":"시작 hardening 보안 강화 전체 보안 full hardening CIS benchmark 적용 STIG 새로 설치했거나 한 번도 보안 강화를 하지 않은 데이터베이스: 모든 영역을 안전한 순서로 적용합니다. listener와 데이터베이스를 한두 번 재시작합니다(유지보수 시간대 필요)."},{"k":"scenario","v":"net","l":"네트워크와 접속: 누가 어디서 접속할 수 있는가? (listener, TCP.VALIDNODE_CHECKING, firewall)","m":"네트워크와 인증 TCP.VALIDNODE_CHECKING TCP.INVITED_NODES listener 1521 5500 ORA-12537 TNS-12537 ORA-12541 ADMIN_RESTRICTIONS firewall TNS-12546 ORA-12170 invited nodes listener.log 외부 접근 접근 제한 listener가 모든 주소에 열려 있고, invited nodes가 정의되지 않았으며, 1521/5500이 누구에게나 열려 있습니다. listener를 재시작합니다(열린 세션은 영향받지 않음)."},{"k":"scenario","v":"tls","l":"접속 암호화 (TCPS wallet 또는 native network encryption)","m":"네트워크와 인증 TCPS SSL TLS wallet orapki SQLNET.ENCRYPTION_SERVER ORA-28860 ORA-29024 ORA-28759 ORA-29106 ORA-12696 ORA-12660 SSL_SERVER_DN_MATCH 인증서 만료 ORA-29003 ORA-29002 TNS-12560 TLS 인증서 체인 certificate chain native network encryption 네트워크 트래픽이 암호화되지 않았습니다. TLS(TCPS, wallet, 인증서, 클라이언트의 SSL_SERVER_DN_MATCH) 또는 인증서 없는 native encryption(SQLNET.ENCRYPTION_SERVER=REQUIRED). 두 방법 모두 라이선스가 필요 없습니다. listen"},{"k":"scenario","v":"auth","l":"인증과 비밀번호 (12C verifier, 프로파일, 기본 계정)","m":"네트워크와 인증 ALLOWED_LOGON_VERSION_SERVER ORA-28040 ORA-01017 ORA-28000 ORA-28001 ORA-28003 ORA-28007 password_versions 10G PROFILE ORA12C_STRONG_VERIFY_FUNCTION 기본 비밀번호 default password SCOTT ORA-65066 ORA-00988 password_versions 10G 비밀번호 프로파일 password policy FAILED_LOGIN_ATTEMPTS 10G/11G 비밀번호 verifier, 비밀번호 프로파일 없음, 기본 비밀번호 계정이나 샘플 계정이 open 상태, 개인 DBA 계정. 다운타임 없음."},{"k":"scenario","v":"priv","l":"최소 권한: 롤, PUBLIC과 애플리케이션 계정","m":"권한과 감사 추적 PUBLIC UTL_FILE UTL_TCP UTL_HTTP UTL_SMTP DBA role RESOURCE ANY privilege least privilege 최소 권한 ORA-01031 ORA-00942 schema-only account PLS-00201 ORA-04021 GRANT ANY UNLIMITED TABLESPACE 권한 보고서 애플리케이션 계정의 DBA/RESOURCE/ANY 권한, PUBLIC에 부여된 UTL_FILE/UTL_TCP/UTL_HTTP. 스키마 소유자, 읽기/쓰기 롤과 권한 보고서. 다운타임 없음."},{"k":"scenario","v":"audit","l":"Audit: 누가 무엇을 했는가? (Unified Auditing, AUDSYS 정리)","m":"권한과 감사 추적 unified auditing AUDIT POLICY AUDSYS DBMS_AUDIT_MGMT UNIFIED_AUDIT_TRAIL ORA_LOGON_FAILURES ORA_SECURECONFIG UNIFIED_AUDIT_SYSTEMLOG SYSAUX 가득 참 감사 추적 SIEM ORA-46267 unified audit policy 누가 무엇을 했는가 who changed 감사인이 로그인, 계정 및 스키마 변경 기록을 요구합니다: unified audit 정책, 보존 기간에 따른 AUDSYS 정리, 레코드를 syslog/중앙 시스템으로 전송. Unified Auditing은 라이선스가 필요 없습니다."},{"k":"scenario","v":"tde","l":"디스크 데이터 암호화 (TDE)","m":"데이터와 백업 TDE transparent data encryption keystore wallet ADMINISTER KEY MANAGEMENT WALLET_ROOT ORA-28365 ORA-28374 ORA-46665 ENCRYPT_NEW_TABLESPACES encryption at rest ORA-28367 ORA-46658 ORA-28353 Advanced Security master key TDE_CONFIGURATION datafile, 백업 또는 디스크 사본의 유출에 대비한 TDE: WALLET_ROOT, keystore, master key, tablespace 암호화(online). Advanced Security 라이선스가 필요하며 WALLET_ROOT 때문에 재시작이 한 번 필요합니다."},{"k":"scenario","v":"backup","l":"백업 보안 (RMAN 암호화, 비밀번호 없는 자동화, 권한)","m":"데이터와 백업 RMAN encryption CONFIGURE ENCRYPTION ORA-19913 expdp ENCRYPTION secure external password store mkstore SEPS 백업 권한 ORA-19914 Advanced Security 라이선스 암호화되지 않은 백업 unencrypted backup ENCRYPTION_PWD_PROMPT RMAN/Data Pump 백업이 암호화되지 않았고, 스크립트에 평문 비밀번호가 있으며, 백업 디렉터리가 누구에게나 열려 있습니다. RMAN encryption, ENCRYPTION_PWD_PROMPT, wallet(SEPS)과 디렉터리 권한. RMAN과 Data Pump 암호화는 Advanced Security 라이선스가 필요합니다."},{"k":"scenario","v":"os","l":"운영체제 측면 (oracle 계정, 파일 권한, core dump, SELinux)","m":"서버와 버전 SELinux orapw oracle 계정 파일 권한 core dump ORACLE_HOME 권한 sudo avc: denied passwd -l oracle setuid 6751 limits.d oracle 계정에 비밀번호 로그인이 가능하고, 비밀번호 파일/network admin/wallet 권한이 느슨하며, SELinux가 꺼져 있습니다. core dump 제한은 새 세션 또는 재시작 시 적용됩니다."},{"k":"scenario","v":"patch","l":"패치와 버전: 최신 RU인가?","m":"서버와 버전 RU release update CPU Critical Patch Update opatch lspatches datapatch dba_registry_sqlpatch CVE 지원 종료 opatch Doc ID 742060.1 패치 일정 설치된 RU, datapatch 상태와 지원 일정. 적용은 RU 패치 가이드로 연결됩니다. 읽기 전용."}]},{"id":"ora-oem","file":"install/guides/ora-oem.json","group":"Oracle","engineId":"oracle","category":"monitoring","title":"Oracle Enterprise Manager Cloud Control 13c Release 5 (13.5) 설치","summary":"Oracle Enterprise Manager Cloud Control 13.5를 설치합니다. 기존 19c 데이터베이스를 Management Repository로 준비하고(문서에 명시된 파라미터, optimizer 설정 초기화), OMS 서버에 silent 설치를 수행하며(OL9에서는 소프트웨어만 설치 + RU + ConfigureGC), Release Update를 적용하고, 대상 서버에 ","level":"고급","duration":"3–5시간","topology":"OMS 서버 + repository 데이터베이스 서버 (+ 모니터링할 대상 서버)","tags":["oracle","enterprise manager","oem","em13c","13.5","cloud control","oms","agent","emctl","emcli","monitoring","모니터링","rac","클러스터","discover_db","data guard","exportconfig"],"order":40,"steps":32,"scenarios":[]}]}}