이 글은 'Real MySQL - 백은빈,이성욱'을 공부하며 작성한 글로 생략된 내용은 책을 구매해서 확인하는 것을 권장합니다.
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=278488709
Real MySQL 8.0 1권
《Real MySQL》을 정제해서 꼭 필요한 내용으로 압축하고, MySQL 8.0의 GTID와 InnoDB 클러스터 기능들과 소프트웨어 업계 트렌드를 반영한 GIS 및 전문 검색 등의 확장 기능들을 추가로 수록했다.
www.aladin.co.kr
4.2 InnoDB 스토리지 엔진 아키텍처
InnoDB 스토리지 엔진은 MySQL의 스토리지 엔진 중 가장 많이 사용되는 엔진으로, 스토리지 엔진 중 거의 유일하게 레코드 기반의 잠금을 제공(= 높은 동시성 처리 가능)한다.
4.2.1 프라이머리 키에 의한 클러스터링
InnoDB의 모든 테이블은 기본적으로 프라이머리 키를 기준(= 프라이커리 키 값의 순서대로 디스크에 저장)으로 클러스터링되어 저장된다.
모든 세컨더리 인덱스는 레코드의 주소 대신 프라이머리 키의 값을 논리적인 주소로 사용한다.
-> 프라이머리 키가 클러스터링 인덱스이기 때문에 이를 이용한 range scan은 빠른 처리가 가능하다.
-> 결과적으로 쿼리의 실행 계획에서 프라이머리 키가 다른 보조 인덱스에 비해 비중이 높게 설정(선택될 확률 높음)된다.
4.2.2 외래 키 지원
InnoDB 스토리지 엔진 레벨에서 지원하는 기능 (= MyISAM이나 MEMORY 테이블에서는 사용 불가)
외래 키는 개발 환경에서의 데이터베이스에서는 좋은 가이드의 역할을 할 수 있지만, 변경 시 부모 및 자식 테이블을 모두 체크해보아야 하기 때문에, 잠금이 여러 테이블로 전파되면서 그로 인해 데드락이 발생할 때도 많다.
부모와 자식 테이블의 관계 파악을 덜한 채로 수동으로 데이터를 적재하거나 스티마 변경을 시도한다면 실패할 수 있다.
그렇다면 테이블의 관계를 명확하게 파악하고 순서대로 작업하기에는 긴급하게 뭔가 조치를 해야하는 상황이라면 어떻게 할까?
-> foreign_key_checks 시스템 변수를 OFF로 설정하여 외래 키 관계에 대한 체크 작업을 일시적으로 멈추는 방법이 있다.
이는 긴급한 상황에서 사용할 수 있는 방법이기에 작업 후 일관성을 다시 맞춘 뒤에 재활성화를 해주어야 한다.
비활성화하게 되면 외래키 관계의 부모 테이블에 대한 작업 (ON DELETE CASCADE와 ON UPDATE CASCADE 옵션)도 무시하게 된다.
4.2.3 MVCC(Multi Version Concurrency Control)
레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능
- 잠금을 사용하는지 않는 일관된 읽기를 제공하는 거이 목적이며, InnoDB는 Undo log를 이용해 구현하고 있다.
- 하나의 레코드에 대해 여러 버전이 유지된 채로 동시에 관리
예를 들어, 한 테이블에 한 건의 레코드를 INSERT하고 UPDATE한다고 하면
InnoDB 버퍼 풀에는 새로 UPDATE한 레코드가 반영
Undo log에는 이전 버전의 레코드가 기록
데이터 파일(디스크)에는 체크포인트나 InnoDB의 Write 스레드에 의해 업데이트되어 있을 수도 아닐 수도 있다.
위 상태에서 아직 COMMIT이나 ROLLBACK이 되지 않았다면 어디에서 데이터를 조회할 까?
-> MySQL 서버의 시스템 변수의 격리 수준에 따라 다르다!
-> 격리 수준이 READ_UNCOMMITTED인 경우에는 InnoDB 버퍼 풀에서 반환. (즉, 커밋 여부 상관없이 변경된 데이터를 반환)
-> 격리 수준이 READ_COMMITTED나 그 이상의 격리 수준인 경우에는 아직 커밋되지 않았다는 점을 고려, Undo log에 있는 데이터를 반환
실제 상황에서 InnoDB는 ACID를 보장하기 때문에 일반적으로는 InnoDB 버퍼 풀과 데이터 파일은 동일한 상태라고 봐도 된다.
undo 명령어를 실행시 백업된 데이터를 InnoDB 버퍼 풀로 다시 복구하고 언두 영역의 데이터를 삭제한다. 즉, 커밋이 된다고 언두 영역의 백업 데이터를 바로 삭제되지는 않고, 이 언두 영역을 필요로 하는 트랜잭션이 더는 없을 때 삭제된다.
4.2.4 잠금 없는 일관된 읽기 (Non-Locking Consistent Read)
InnoDB 스토리지 엔진은 MVVC 기술을 이용해 잠금을 걸지 않고 읽기를 수행한다. (격리 수준 SERIALIZABLE 제외)
즉, 다른 레코드에 대해 변경 작업이 이뤄지더라도 다른 트랜잭션의 SELECT 작업을 방해하지는 않는다.
SELECT를 실행하면 위에서도 한번 말했듯이 아래와 같이 반환한다.
-> 격리 수준이 READ_UNCOMMITTED인 경우에는 InnoDB 버퍼 풀에서 반환.
-> 격리 수준이 READ_COMMITTED나 REPEATABLE_READ인 경우에는 언두 로그에서 반환.
4.2.5 자동 데드락 감지
InnoDB 스토리지 엔진은 내부적으로 잠금이 교착 상태에 빠지지 않았는지 체크하기 위해 잠금 대기 목록을 그래프(Wait-for List) 형태로 관리한다.
데드락 감지 스레드가 주기적으로 잠금 대기 그래프를 검사해 교착 상태에 빠진 트랙잭션들을 찾아 그 중 하나를 강제 종료 한다.
종료할 트랜잭션을 선택하는 기준은?
-> 트랜잭션의 언두 로그 양이 가장 적은 트랜잭션을 선택한다.
-> 강제 롤백을 했을 때 언두 처리 할 양이 가장 적기 때문 (MySQL 서버의 부하가 덜 함)
InnoDB 스토리지 엔진은 상위 레이어인 MySQL 엔진에서 관리되는 테이블 잠금(LOCK TABLES 명령으로 잠긴 테이블)은 볼 수가 없기에 데드락 감지는 불확실할 수 있다.
innodb_table_locks 시스템 변수를 활성화하면 InnoDB 스토리지 엔진 내부의 레코드 잠금 뿐만 아니라 테이블 레벨의 잠금 감지가 가능하기에 특별한 이유가 없다면 innodb_table_locks 시스템 변수를 활성화하는 것을 추천한다.
동시 처리 스레드가 많은 경우 데드락 감지 스레드가 데드락 감지를 위해 잠금상태가 변경되지 않도록 잠금 목록이 저장된 리스트에 새로운 잠금을 걸고 데드락 스레드를 찾게 된다. 데드락 감지 스레드가 느려지면 서비스 쿼리를 처리 중인 스레드도 대기하게 된다.
-> 동시 처리 스레드가 많은 경우 데드락 감지 스레드는 많은 CPU 자원을 소모하게 된다.
위 문제를 해결하기 위한 방법은 없을까?
-> innodb_deadlock_detect 시스템 변수로 데드락 감지를 비활성화하고 innodb_lock_wait_timeout으로 잠금 아웃 시간을 작게 설정해줌으로써, 지정한 시간 안에 획득하지 못하면 에러를 반환하도록 한다.
4.2.6 자동화된 장애 복구
InnoDB 데이터 파일은 기본적으로 MySQL 서버가 시작될 때 항상 자동 복구를 시행하는 데, 이 단계에서 자동 복구될 수 없는 손상이라면 자동 복구를 멈추고 MySQL 서버는 종료되어 버린다.
이런 경우 MySQL 서버의 설정 파일에 innodb_force_recovery 시스템 변수를 아래 1~6 중에 설정해서 MySQL 서버를 다시 시작할 수 있다. 값이 커질수록 데이터 손실 가능성은 커지고 복구 가능성은 적어진다.
MySQL 서버가 재가동되고 InnoDB 테이블이 인식된다면 mysqldump를 이용해 데이터를 가능한 만큼 백업하고 그 데이터로 데이터베이스를 다시 구축하는 것이 좋다.
innodb_force_recovery 값 | 설명 |
1(SRV_FORCE_IGNORE_CORRUPT) | - InnoDB의 테이블 스페이스의 데이터나 인덱스 페이지에서 손상된 부분 무시하고 서버를 시작한다. |
2(SRV_FORCE_NO_BACKGROUND) | - InnoDB는 쿼리의 처리를 위해 여러 종류의 백그라운드를 동시에 사용하는데, 이 중 백그라운드 스레드 가운데 메인 스레드를 시작하지 않고 MySQL 서버를 시작한다. |
3(SRV_FORCE_NO_TRX_UNDO) | - 언두 영역의 데이터, 리두 로그 영역의 데이터를 사용하여 장애 시점의 데이터 상태를 만들어낸 후, 정상적인 경우 커밋되지 않은 트랜잭션은 롤백을 수행하지만 해당 설정 모드는 롤백하지 않고 재시작하게 된다. |
4(SRV_FORCE_NO_IBUF_MERGE) | - 인덱스 변경에 사용되는 인서트 버퍼의 내용을 무시하고 서버를 시작한다. - 인서트 버퍼는 실제 데이터가 아닌 인덱스에 관련된 부분이므로 덤프를 사용하면 데이터의 손실없이 복구할 수 있다. |
5(SRV_FORCE_NO_UNDO_LOG_SCAN) | - 언두 로그를 모두 무시하고 서버를 시작한다. - 커밋되지 않았던 작업도 커밋된 것처럼 처리되므로 잘못된 데이터가 데이터베이스에 남을 수 있다. |
6(SRV_FORCE_NO_LOG_REDO) | - 리두 로그를 모두 무시하고 서버를 시작한다. -> 마지막 체크 포인트 시점의 데이터만 남게 된다. |
4.2.7 InnoDB 버퍼 풀
디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해두는 공간을 말하며, 쓰기 작업을 지연시켜 일괄 작업으로 처리할 수 있게 하는 버퍼의 역할도 한다.
버퍼 풀의 크기 설정
InnoDB의 버퍼 풀은 무조건 전체 물리 메모리의 80% 정도를 부여하기 보다는 운영체제와 각 클라이언트 스레드가 사용할 메모리를 충분히 고려해서 설정해야 한다. 처음으로 MySQL 서버를 준비한다면 운영체제 공간이 8GB 미만이라면 전체 메모리의 50% 정도, 그 이상이라면 50%부터 시작해서 조금씩 올리면서 최적점을 찾아야 한다.
innodb_buffer_poll_size 시스템 변수로 버퍼 풀의 크기를 설정할 수 있으며, 동적으로 버퍼 풀의 크기를 확장할 수 있다. (이는 크리티컬한 변경이기에 서버가 한가한 시점에 진행할 것)
innodb_buffer_pool_instances 시스템 변수를 이용해 버퍼 풀의 개수를 변경할 수 있다. (기본값은 8개)
전체 버퍼 풀을 위한 메모리 크기가
- 1GB 미만이라면 1개만 생성,
- 40GB이하라면 기본 값 8개,
- 메모리가 크다면 버퍼 풀 인스턴스당 5GB 정도
버퍼 풀의 구조
버퍼 풀이라는 거대한 메모리 공간을 페이지 크기(innodb_page_size 시스템 변수에 설정된)의 조각으로 쪼개어 InnoDB 스토리지 엔진 데이터를 필요로 할 때, 해당 데이터 페이지를 읽어서 각 조각에 저장한다.
버퍼 풀의 페이지 크기를 관리하기 위해 InnoDB 스토리지 엔진은 크게 LRU(Least Recently Used), Flush, Free 리스트 자료 구조로 관리한다.
- LRU 리스트 : 따지자면 LRU와 MRU 리스트가 결합된 형태로 Old 서브리스트 영역은 LRU, New 서브리스트 영역은 MRU 정도로 보면 된다.
- 디스크로부터 한 번 읽어온 페이지를 최대한 오랫동안 InnoDB 버퍼 풀의 메모리에 유지해서 티스크 읽기를 최소화하기 위해 사용
- 거의 사용되지 않는 데이터 페이지는 새로 읽는 데이터 페이지들에 밀리다가 버퍼 풀에서 제거
- Flush 리스트 : 디스크로 동기화되지 않은 데이터를 가진 데이터 페이지(더티 페이지) 목록 관리
- Free 리스트 : InnoDB 버퍼 풀에서 실제 사용자 데이터로 채워지지 않은 비어 있는 페이지들의 목록
InnoDB 스토리지 엔진에서 데이터를 찾는 과정
- 어댑티브 해시 인덱스와 B-tree 인덱스를 사용해서 필요한 레코드가 저장된 데이터 페이지가 버퍼 풀에 있는지 검사
- 없다면, 디스크에서 필요한 데이터 페이지를 버퍼 풀에 적재하고, 적재된 페이지에 대한 포인터를 LRU 헤더 부분에 추가, 버퍼 풀의 LRU 헤더 부분에 적재된 데이터 페이지가 실제로 읽히면 MRU 헤더 부분으로 이동
- 버퍼 풀에 상주하는 데이터 페이지는 사용자 쿼리가 얼마나 최근에 접근했는지에 따라 Age가 부여되고, 상주하는 동안 쿼리에서 오랫동안 사용되지 않으면 Age 순으로 버퍼 풀에서 제거된다.
- 버퍼 풀의 데이터 페이지가 다시 사용되면 새롭게 나이 부여하고 MRU 헤더 부분으로 이동
- 필요한 데이터가 자주 접근됐다면 해당 페이지의 인덱스 키를 어댑티브 해시 인덱스에 추가
버퍼 풀의 리두 로그
InnoDB 버퍼 풀은 데이터베이스의 성능 향상을 위해 데이터 캐시와 쓰기 버퍼링이라는 2가지 용도를 가진다. 버퍼 풀의 메모리 공간만 늘리는 것은 데이터 캐시 기능만 향상시키는 것!
쓰기 버퍼링도 고려하기 위해 리두 로그 파일의 크기도 고려해야 한다.
InnoDB의 버퍼 풀은 클린 페이지 뿐만 아니라 더티 페이지도 가지고 있다.
- 클린 페이지 : 디스크에서 읽은 상태 그대로 전혀 변경되지 않은 페이지
- 더티 페이지 : INSERT, UPDATE, DELETE 명령으로 변경된 데이터를 가진 페이지
- 더티 페이지는 특정 리두 로그 엔트리와 관계를 맺고 체크 포인트 발생 시 체크 포인트 LSN보다 작은 리두 로그 엔트리와 관련된 더티 페이지는 모두 디스크로 동기화되어야 한다. (리두 로그 엔트리도 디스크로 동기화)
더티 페이지는 디스크와 메모리(버퍼 풀)의 상태가 다르기 때문에 언젠가는 디스크로 기록되어야 한다!
하지만 버퍼 풀에 무한정 머무를 수 있는 페이지는 아니기 때문에 InnoDB 스토리지 엔진은 주기적으로 체크포인트 이벤트를 발동 시켜 리두 로그와 버퍼 풀의 더티 페이지를 디스크에 동기화시킨다.
- 활성 리두 로그 공간 : 더티 페이지와 관계를 맺은 채 아직 디스크로 동기화되지 않은 공간 (재사용 불가능)
가장 최근 체크포인트의 LSN(Long Sequence Number)이 활성 리두 로그 공간의 시작점이 된다.
Checkpoint Age (활성 리두 로그 공간의 크기) = 가장 최근 체크 포인트의 LSN - 마지막 리두 로그 엔트리의 LSN
버퍼 풀 플러시 (Buffer Pool Flush)
InnoDB 스토리지 엔진은 버퍼 풀에서 아직 디스크로 기록되지 않은 더티 페이지들을 성능상의 악영향 없이 디스크에 동기화하기 위해 2개의 플러시 기능을 백그라운드에서 실행한다.
- 플러시 리스트 플러시 : 플러시 리스트에서 오래 전에 변경된 데이터 페이지를 순서대로 디스크에 동기화하는 작업
- 리두 로그 공간의 재활용을 위해 주기적으로 오래된 리두 로그 엔트리가 사용하는 공간을 비워야 하는데, 이깨 버퍼 풀의 더티 페이지가 반드시 먼저 디스크에 동기화되어야 해 사용
버퍼 풀에 더티 페이지가 많을수록 디스크 쓰기 폭발이 일어날 가능성이 높아지는 데, 이는 어떻게 방지할 수 있을까?
-> 어댑티브 플러시라는 기능을 제공한다. (트래픽을 봐가면서 innodb_io_capacity와 innodb_io_capacity_max를 설정하는 것은 번거롭기에 제공)
-> 이 기능은 리두 로그의 증가 속도를 분석하여 적절한 수준의 더티 페이지가 버퍼 풀에 유지될 수 있도록 디스크 쓰기를 실행한다.
-
- 버퍼 풀에 더티 페이지가 많을수록 디스크 쓰기 폭발이 일어날 가능성이 높아지는
- LRU 리스트 플러시 : LRU 리스트에서 사용 빈도가 낮은 데이터 페이지들을 제거하는 작업
버퍼 풀 상태 백업 및 복구
디스크의 데이터가 버퍼 풀에 적재돼 있는 상태를 워밍업이라 하며, 워밍업의 정도에 따라 성능 차이가 크게 있기에 MySQL 5.5 버전에서는 서버가 셧다운 후 재시작하는 경우 주요 테이블과 인덱스에 대해 풀스캔을 실행했다.
MySQL 5.6 버전 이후부터는
-> 서버를 셧다운 하기 전에 innodb_buffer_pool_dump_now 시스템 변수를 이용해 현재 버퍼 풀의 상태를 백업
-> 재시작 한 뒤, innodb_buffer_pool_load_now로 백업된 버퍼 풀의 상태로 다시 복구하는 방식
버퍼 풀의 적재 내용 확인
MySQL 5.6 버전에서 사용된 방법은 서비스 쿼리가 많이 느려지는 문제가 있어 실제 서비스에서 버퍼 풀의 상태를 확인하는 것은 거의 불가능
-> MySQL 8.0부터는 information_schema의 innodb_cached_indexes 테이블을 통해 인덱스별로 데이터 페이지가 얼마나 InnoDB 버퍼 풀에 적재되어 있는지 확인할 수 있다.
4.2.8 Double Write Buffer
InnoDB 스토리지 엔진의 리두 로그는 리두 로그의 공간 낭비를 막기 위해 페이지의 변경된 내용만 기록한다.
즉, 더티 페이지를 디스크 파일로 플러시할 때 일부만 기록되는 문제가 발생한다면 그 페이지의 내용은 복구할 수 없을 수도 있다.
위처럼 페이지의 일부만 기록되는 현상을 파셜 페이지(Partial-Page) 또는 톤 페이지(Torn-Page)라고 하며, 하드웨어의 오작동이나 시스템의 비정상 종료 등으로 발생한다.
이를 막기 위해 InnoDB 스토리지 엔진에서는 Double-Write 기법을 사용한다.
- Double-Write 기법 : 더티 페이지(변경된 내용)를 디스크 파일에 기록하기 전에 더티 페이지를 묶어 한 번의 쓰기로 시스템 테이블스페이스의 DoubleWrite 버퍼에 기록한 뒤, 각 더티 페이지를 파일의 적당한 위치에 하나씩 랜덤으로 쓰기를 실행한다.
- 이 기능은 innodb_doublewrite 시스템 변수로 제어 가능
4.2.9 언두 로그
InnoDB 스토리지 엔진은 트랜잭션과 격리 수준을 보장하기 위해 DML로 변경되기 이전 버전 데이터를 별도로 백업한다. 이렇게 백업된 데이터를 언두 로그라 한다.
언두 로그 기능 | 설명 |
트랜잭션 보장 | - 트랜잭션 롤백 시 언두 로그에 백업해둔 데이터를 사용해 데이터를 복구한다. |
격리 수준 보장 | - 특정 커넥션에서 데이터를 변경하는 도중에 다른 커넥션에서 데이터를 조회하면 트랜잭션 격리 수준에 맞게 변경 중인 레코드 또는 언두 로그에 백업해둔 데이터를 읽어 반환한다. |
언두 로그 모니터링
InnoDB 스토리지 엔진은 빈번하게 변경된 레코드를 조회하는 쿼리가 실행되면 언두 로그의 이력을 필요한 만큼 스캔해야 레코드를 찾음 -> 언두 로그가 누적되어 있다면 쿼리 성능이 떨어짐
그래서 MySQL 서버의 언두 로그 레코드를 모니터링하여 급증 여부를 확인하면서 불필요하게 활성화되어 있는 트랜잭션이 없는 지 확인이 필요하다.
언두 테이블스페이스 관리
언두 테이블스페이스 : 언두 로그가 저장되는 공간
- MySQL 5.6 이전 버전에서는 언두 로그를 모두 시스템 테이블스페이스에 저장 (MySQL 서버가 초기화될 떄 생성되기 때문에 확장에 한계가 있음)
- MySQL 8.0 버전 부터는 언두 로그는 항상 시스템 테이블스페이스 외부의 별도 로그 파일에 기록
- 1개 이상의 언두 슬롯을 가진 1~128 개의 롤백 세그먼트를 가진 구조
최대 동시 트랜잭션 수 = (InnoDB 페이지 크기) / 16 * (롤백 세그먼트 개수) * (언두 테이블스페이스 개수)
4.2.10 체인지 버퍼
변경해야 할 인덱스 페이지를 버퍼 풀이 아닌 디스크를 통해 읽어와야 할 경우 즉시 실행하지 않고 임시 공간에 저장하는데 이 공간을 체인지 버퍼라고 한다.
4.2.11 리두 로그 및 로그 버퍼
리두로그
- MySQL 서버가 비정상적으로 종료됐을 때 데이터 파일에 기록되지 못한 데이터를 잃지 않게 해주는 안전장치
- 비정상적으로 종료된 데이터 종류
- 커밋됐지만 기록되지 않은 데이터 -> 리두 로그를 사용해 복구
- 롤백됐지만 데이터 파일에 이미 기록된 데이터 -> 리두 로그와 언두 로그 모두 사용해 복구
리두 로그는 트랜잭션이 커밋되면 즉시 디스크로 기록되도록 하는 것을 권장한다. 매번 그렇게 하는 것은 많은 부하를 유발하기에 innodb_flush_log_at_trx_commit 시스템 변수를 활용해 일정 주기별로 디스크에 동기화 시키도록 하는 것이 좋다.
리두 로그 활성화 및 비활성화
MySQL 8.0 부터는 수동으로 리두 로그를 비활성화할 수 있게 되었다.
-> 데이터를 복구하거나 대용량 데이터를 한번에 적재하는 경우 데이터의 적재시간 단축 가능
4.2.12 어댑티브 해시 인덱스
사용자가 직접 생성한 인덱스가 아닌 InnoDB 에서 자주 요청하는 데이터에 대해 자동으로 생성하는 인덱스
- B-Tree 검색 시간을 줄이기 위해 도입된 기능
- 자주 읽히는 데이터 페이지의 키 값을 이용해 해시 인덱스를 만들고 필요할 때마다 어댑티브 해시 인덱스를 검색해서 레코드가 저장된 데이터 페이지를 즉시 찾아감 (-> 루프노드~리프노드까지 찾아갈 필요 없음)
어댑티브 해시 인덱스는 데이터 페이지를 메모리(버퍼 풀)에서 빠르게 접근하기 위한 기능이기에 데이터 페이지를 디스크에서 읽어오는 경우가 빈번한 서버에서는 도움이 되지 않는다.
4.2.13 InnoDB와 MyISAM, MEMORY 스토리지 엔진 비교
이전엔 MyISAM이 기본 스토리지 엔진으로 사용되는 경우가 많았지만, MySQL 5.5 버전부터 InnoDB가 기본 스토리지 엔진으로 채택되면서 점차 InnoDB 에서 모든 기능을 구현할 수 있도록 변경되었다.
- 전문 검색이나 공간 좌표 검색 기능 -> MyISAM 만 지원했다가 8.0 부터 InnoDB 에서도 지원
- 하나의 스레드에서만 데이터를 읽고 쓴다면 MEMORY 가 InnoDB 보다 빠를 수 있지만 그런 환경에서 사용하는 경우는 없다.
MySQL 8.0에서는 모든 기능이 InnoDB 스토리지 엔진 기반으로 재편되었고, MyISAM 만의 장점이 사라지게 되었다.
참고
'Real MySQL 8.0' 카테고리의 다른 글
[Real MySQL 8.0 (1권)] 4.1 - MySQL 엔진 아키텍처 (1) | 2024.12.09 |
---|---|
[Real MySQL 8.0 (1권)] 3장 - 사용자 및 권한 (0) | 2024.12.01 |
[Real MySQL 8.0 (1권)] 2장 - 설치와 설정 (1) | 2024.11.29 |
[Real MySQL 8.0 (1권)] 1장 - 소개 (1) | 2024.11.29 |
댓글