MySQL - 아키텍처 정리 (2) - Real MySQL 8.0

Share

Last Updated on 4월 6, 2022 by Jade(정현호)

안녕하세요 
이번 포스팅에서는 MySQL 전반적인 아키텍처에 대해서 정리 하였으며 그중에서 이번에는 InnoDB 스토리지 엔진 아키텍처를 정리하였습니다. 
해당 내용은 Real MySQL 8.0 책에 대해서 정리한 내용 이며 아래 포스팅에서 이어지는 글 입니다.

        

InnoDB 스토리지 엔진 아키텍처

InnoDB 는 MySQL 에서 사용할 수 있는 스토리지 엔진 중에서 거의 유일하게 레코드 기반 의 잠금을 제공 하며, 그렇기 때문에 높은 동시성 처리가 가능하고 안전성과 성능이 뛰어 납니다.

[InnoDB Architecture]

위의 그림이 InnoDB 아키텍처를 아주 간단하게 보여주며 각 부분의 자세한 내용은 다음 장에서 설명 드리도록 하겠습니다.
      

InnoDB 주요 특징

InnoDB 의 주요 특징에 대해서 먼저 확인 해보도록 하겠습니다.
      

Primary Key에 의한 클러스터링

InnoDB의 모든 테이블은 기본적으로 프라이머리 키를 기준으로 클러스터링되어 저장 됩니다.

즉, Primary Key(프라이머리 키) 값의 순서대로 디스크에 저장된다는 뜻 이며, 모든 세컨더리 인덱스는 레코드 주소 대신 프라이머리 키의 값을 논리적인 주소로 사용합니다.

프라이머리 키가 클러스터링 인덱스 이기 때문에 프라이머리 키를 이용한 레인지 스캔은 상당히 빠르게 처리가 되며 결과적으로 쿼리의 실행 계획에서 프라이머리 키는 기본적으로 다른 보조 인덱스에 비해 비중이 높게 설정(쿼리의 실행계획에서 프라이머리 키가 선태될 확율이 높음) 이 됩니다.

오라클 DBMS 의 IOT 와 동일한 구조가 InnoDB에서는 일반적인 테이블의 구조가 되는 것 입니다.
       

외래 키 지원(FK)

외래 키(FK) 에 대한 지원은 InnoDB 스토리지 엔진 레벨에서 지원하는 기능으로 MyISAM 이나 MEMORY 테이블에서는 사용할 수 0없습니다. 외래 키는 데이터베이스 서버 운영의 불편함 때문에 서비스용 데이터베이스에서는 생성하지 않는 경우도 자주 있습니다.

그렇더라도 개발 환경에서는 좋은 개발 가이드가 될수도 있으며 InnoDB에서 외래 키는 부모 테이블과 자식 테이블 모두 해당 컬럼에 인덱스 생성이 필요하고 변경 시에는 반드시 부모 테이블이나 자식 테이블에 데이터가 있는지를 체크하는 작업이 필요하므로 잠금(Lock)이 여러 테이블로 전파가 되고 그로 인하여 데드락이 발생될 때가 많아 질수도 있습니다

수동으로 데이터를 적재하거나 스키마 변경 등을 할 경우에도 작업이 실패가 할 수 있을 수도 있으며, 부모-자식 관계를 명확히 파악해서 순서대로 작업을 해야하며, 서비스에 문제가 생겨서 긴급하게 무엇인가 조치를 해야할 경우에도 이런 FK 가 설정되어 있을 경우 조금 더 조급 해질 수 도 있는 상황이 만들어 질 수 도 있습니다

이런 경우 foreign_key_checks 시스템 변수를 OFF 로 하면 외래 키 관계에 대한 체크 작업을 잠시 멈출수 는 있습니다.

mysql> set session foreign_key_checks=OFF;

        

MVCC

MVCC 는 Multi Version Concurrency Control 의 약자로 일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능 이며, MVCC의 가장 큰 목적은 잠금(Lock)을 사용하지 않는 일관된 읽기를 제공하는 데 있습니다.

InnoDB 는 언두 로그(Undo Log) 통해서 이를 구현하며 이것은 Oracle DBMS 도 동일 합니다.

멀티 버전이라는 것은 하나의 레코드에 대해서 여러개의 버전이 동시에 관리 된다는 의미이며 이를 위해 격리 수준(Isolation level) 도 관련이 있습니다.

UPDATE 문장이 실행되면 커밋 실행 여부와 상관없이 InnoDB 버퍼풀은 새로운 값으로 업데이트를 하게 되며, 다른 세션에서 작업 중인 레코드를 조회하게 되면 어떻게 될까요?

이 부분에서 MySQL 서버에서 설정된 격리 수준(Isolation level) 따라서 약간은 달라지며 격리 수준이 READ_UNCOMMITTED 인 경우 COMMIT 이 되지 않은 데이터를 읽을 수 있게 됩니다. 

READ_COMMITTED 나 그 이상의 격리 수준(REPEATABLE_READ,SERIALIZABLE) 인 경우에는 아직 커밋이 되지 않았기 때문에 다른 세션에서 조회를 요청할 경우 이전 내용을 보관하는 언두 영역에서 변경 전 데이터를 반환을 하게 됩니다.

이러한 과정을 MVCCC 라고 표현을 합니다. 즉 하나의 레코드에 대해서 2개의 버전이 유지되고 설정에 따라서 어느 데이터가 보여지는지가 달라지는 구조 입니다.

커밋을 하게 되면 변경된 데이터를 영구적으로 저장을 하게 됩니다. 

롤백을 실행하면 Undo 영역에 있는 백업 데이터 데이터를 InnoDB 버퍼풀로 다시 복구하고 언두 영역을 삭제 하게 됩니다. Undo 영역은 커밋이 되었다고 바로 삭제되는 것은 아니며 필요로 하는 트랜잭션이 더는 없을 때 삭제 됩니다.
     

잠금 없는 일관된 읽기

InnoDB 스토리지 엔진은 MVCC 기술을 이용해 잠금을 사용하지 않고 읽기 작업을 수행하게 됩니다.

잠금을 걸지 않기 때문에 InnoDB에서 읽기 작업은 다른 트랜잭션이 가지고 있는 잠금(Lock) 기다리지 않고 읽기 작업을 수행할 수 있게 됩니다.

격리 수준이 SERIALIZABLE 이 아닌 수준인 경우에는 Insert 와 연결되지 않은 Select 작업은 다른 트랜잭션의 변경작업과 관계없이 항상 잠금을 대기하지 않고 바로 실행 되게 됩니다.

레코드가 변경 후 아직 커밋을 하지 않았을 경우 에도 데이터를 읽을 때 조회 작업이 방해 되지 않으며 Undo 영역에서 
이를 '잠금 없는 일관된 읽기' 라고 표혈 할 수 있습니다.
      

자동 데드락 감지

InnoDB 스토리지 엔진은 잠금이 교착 상태에 빠지지 않았는지 내부적으로 체크하기 위해 잠금 대기 목록을 그래프 형태로 관리를 하게 됩니다.

InnoDB 스토리지 엔진은 데드락 감지 스레드를 가지고 있어서 데드락 감지 스레드가 주기적으로 잠금 대기 그래프를 검사하여 교착 상태에 빠진 트랜잭션들을 찾아서 그 중 하나를 강제 종료 하게 됩니다.

어때 어느 트랜잭션을 먼저 강제 종료 할 것인지를 판단하는 기준은 트랜잭션의 언두 로그 양이며, 언두 로그 레코드를 더 적게 가진 트랜잭션이 일반적으로 롤백의 대상이 되게 됩니다.

상위 레이어인 MySQL 엔진에서 관리하는 테이블 잠금(LOCK TABLES 명령어) 은 볼수가 없어서 데드락 감지가 불확실할 수도 있는데, innodb_table_locks 시스템 변수를 활성화 하면 InnoDB 스토리지 엔진 내부의 레코드 잠금 뿐만 아니라 테이블 레벨의 잠금 까지 감지 할 수 있게 됨으로 특별한 이유가 없다면 innodb_table_locks 시스템변수 활성화를 고려 해보는 것도 좋을 것도 같습니다.

일반 적인 서비스나 상황에서는 데드락 감지 스레드가 트랜잭션의 잠금 목록을 검사하는 과정이 크게 부담 되지는 않습니다.
하지만 동시 처리가 스레드가 많거나 각 트랜잭션이 가진 잠금의 개수가 많아지게 되면 데드락 스레드가 느려질수도 있습니다.

데드락 감지 감지 스레드는 잠금 목록을 검사해야 하며 이 과정에서 스레드가 느려지게 되면 서비스 쿼리를 처리 중인 스레드도 같이 느려지게 되는 상황이 될수도 있습니다.

이런 문제점에 대해서는 innodb_deadlock_detect 시스템 변수를 제공하고 있으며 OFF 로 설정시에는 데드락 스레드가 작동하지 않게 됩니다.

데드락 발생시 중재하는 역할이 없기 때문에 대기하게 되며 innodb_lock_wait_timeout 설정 만큼 대기 하다가 요청이 실패가 되게 됩니다.
       

자동회된 장애 복구

InnoDB에는 손실이나 장애로부터 데이터를 보호하기 위한 여러가지 매커니즘이 탑재되어 있습니다.

그러한 매커지즘을 이용해 MySQL 서버가 시작 될 대 완료 되지 못한 트랜잭션이나 디스크에 일부만 기록된(Partial write) 데이터 페이지 등에 대한 복구 작업이 자동으로 진행 되게 됩니다.

InnoDB 데이터 파일은 기본적으로 MySQL 서버가 시작될 때 항상 자동 복구를 수행한다. 이 단계에서 자동으로 복구 될수 없는 경우(복구 될수 없는 손상이 발생한 경우) 자동 복구를 멈추고 MySQL 서버는 기동이 되지 않고 종료가 되게 됩니다.

이럴 때는 MySQL 서버의 설정파일에 innodb_force_recovery 시스템 변수를 설정해서 MySQL 서버 시작을 시도 해봐야 합니다.
해당 시스템 변수에 설정하는 모드에 따라서 처리되는 부분은 조금 씩 다르며 1부터 6까지 변경해 가면서 복구를 시도를 합니다.

• 각 모드별 내용은 문서 참조


위와 같이 진행하였지만 MySQL 서버가 시작 되지 않는다면 백업을 이용해서 다시 구성하는 방법 밖에 없을 것으로 생각 됩니다.

백업이 있다면 백업으로 복구후에 백업 또는 가지고 있는 바이너리 로그를 통해서 최대한 최근의 시점까지 복구를 시도 할 수 있습니다.
     

InnoDB 버퍼 풀

InnoDB 스토리지 엔진에서 가장 핵심 적인 부분으로 디스크의 데이터파일이나 인덱스 정보를 메모리에 캐시해 두는 공간으로 쓰기 작업을 지연시켜 일괄 작업으로 처리 할 수 있는 버퍼 역할도 같이 하게 됩니다.

Insert,Update,Delete 처럼 데이터를 변경하는 쿼리는 여러곳에 위치한 데이터 파일을 변경하는 과정에서 랜덤한 디스크 작업을 하게 되는데 버퍼풀은 이러한 변경 작업을 모아서 처리하면서 랜덤한 디스크 쓰기 작업 횟수를 줄여줄 수도 있습니다.
      

버퍼 풀의 크기

InnoDB 버퍼풀 의 크기는 단순하게 % 로 설정한다로는 명확하게 정의하기는 힘든 부분이 있습니다.

클라이언트 스레드가 사용하는 메모리 와 OS 에서 사용되는 메모리 등도 고려가 필요한 부분이 있습니다.
레코드 버퍼가 상당한 메모리를 사용할 수 도 있으며 레코드 버퍼는 각 클라이언트 세션에서 테이블의 레코드를 읽고 쓸 때 버퍼로 사용되는 공간을 말하며 커넥션이 많고 사용하는 테이블도 많다면 메모리 사용이 많을 수도 있습니다.

MySQL 5.7 부터는 InnoDB 버퍼풀 크기를 동적으로 조절할 수 있게 개선이 되었으며 그래서 InnoDB 버퍼풀의 크기를 적절하게 작은 값으로 설정해서 사용하면서 조금씩 늘려가는 것도 하나의 방법이 될 수 있습니다.

InnoDB 버퍼 풀은 내부적으로 128MB 크기의 청크 단위로 관리되며 이는 버퍼풀 크기를 변경하는 단위의 크기가 됩니다.
즉 버퍼 풀 크기를 변경 시에는 128MB 단위로 처리되게 됩니다.

버퍼 풀에 관리는 세마포어가 하게 되며 내부적인 잠금 경합이 많이 유발되었으나 이런 경합을 줄이기 위해서 버퍼 풀을 여러개로 쪼개어 관리할 수 있게 개선되었습니다.
버퍼 풀이 여러개의 작은 버퍼풀로 쪼개지면서 개별 버퍼 풀 전체를 관리하는 세마포어 자체도 경합이 분산되는 효과를 내게 되는 것 입니다. 
innodb_buffer_pool_instance 시스템 변수를 사용해 버퍼 풀을 여러개로 분리해서 사용할 수 있습니다.

Note

MySQL 에서 버퍼풀을 여러개를 사용하는 것 과 유사하게 Oracle 에서는 SharedPool 에 대해서 Subpool 로 나눠서 관리할 수 있으며 그에 따라서 latch contention 을 줄이는 데 사용하고 있습니다.

       

버퍼 풀의 구조

버퍼풀 은 페이지 크기(innodb_page_size)에 따라서 조각으로 쪼개어 InnoDB 스토리지 엔진이 데이터를 필요로 할 때 해당 데이터 페이지를 읽어서 각 조각을 저장하게 됩니다.

이러한 페이지 조각을 관리하기 위해서 LRU(Least Recently Used) 리스트와 플러시(Flush) 리스트, 프리(Free) 리스트 3개의 자료 구조를 관리하게 됩니다.

엄밀하게 LRU 와 MRU(Most Recently Used) 나눠서 볼 수 있습니다.

[Buffer Pool List]

위의 그림에서 Old Sublist 영역은 LRU 영역에 해당 되며, New Sublist 는 MRU 영역에 해당 합니다.

LRU 리스트를 관리하는 목적은 디스크로 부터 읽어온 페이지를 최대한 오랫동안 InnoDB 버퍼풀의 메모리에 유지하기 위해서 입니다.

처음에 한번 읽힌 데이터 페이지가 이후 자주 사용된다면 그 데이터 페이지는 InnoDB 버퍼 풀의 MRU 영역에 있을 확율이 높거나 MRU 영역내에 있게 되어 계속 사용되며, 반대로 사용이 거의 되지 않는다면 새롭게 디스크에서 읽히는 데이터 페이지들에 의해서 밀려서 LRU 의 끝으로 이동하게 되고 결국 InnoDB 버퍼풀에서 제거 되게 됩니다.

플러시 리스트는 디스코 동기화 되지 않는 데이터를 가진 데이터 페이지(더티 페이지)의 변경 시점 기준의 페이지 목록을 관리 합니다.

데이터가 변경되면 리두 로그에 기록하고 버퍼 풀의 데이터 페이지에도 변경 내용을 반영  합니다. 그래서 리두 로그의 각 엔트리는 특정 데이터 페이지와 연결되게 됩니다.

통상 리두 로그가 디스크로 기록이 먼저 되고 체크 포인트가 발생할 때 데이터 파일에 내려 쓰게 됩니다.
관련해서는 Double Write Buffer 내용도 있습니다.
        

버퍼 풀 과 리두 로그

InnoDB 의 버퍼 풀과 리두 로그는 매우 밀접한 관계를 맺고 있습니다.

InnoDB 버퍼풀은 설정한 사이즈에 따라서 쿼리 성능은 더 빨아질 수 있습니다. 물론 모든 데이터 파일이 버퍼 풀에 적재 될 정도로 버퍼 풀 공간이 있다면 성능은 좋을 것 입니다.

InnoDB 버퍼풀은 데이터 캐시 와 쓰기 버퍼링 두가지 용도가 있으며 버퍼풀의 크기를 늘리는 것은 데이터 캐시 기능에 대한 향상 입니다. 쓰기 버퍼링 기능까지 향상 시키려면 버퍼풀과 리두와의 관계를 먼저 알아야 합니다.

InnoDB 버퍼 풀은 읽은 상태로 변경되지 않은 클린 페이지(Clean Page) 와 DML 에 의해서 변경된 데이터를 가진 더티 페이지(Dirty Page) 를 가지고 있습니다. 더티 페이지는 디스크와 메모리(버퍼 풀) 의 데이터 상태가 다르기 대문에 언젠가는 디스크에 기록되어야 합니다.

데이터 변경이 발생하면 InnoDB 스토리지 엔진에서는 리두 로그 파일에 기록 하게 되고 어느 순간 새로운 리두 로그 엔트리에 의해서 덮어 쓰이게 됩니다. 
전체 리두 로그 파일에서 재사용 가능 공간과 불가능한 공간이 구분되며, 재사용 불가능한 공간을 활성 리두 로그(Active Redo Log) 라고 합니다.

리두 로그 파일은 순환되어 기록되며, 매번 기록 될때 마다 로그 포지션은 계속 증가 하게 되며, 이를 LSN(Log Sequence Number) 라고 합니다

InnoDB 스토리지 엔진은 주기적으로 체크 포인트를 발생시켜서 리두 로그와 버퍼 풀의 더티 페이지를 디스크로 동기화하는데, 이렇게 발생한 체크포인트 이벤트 중 가장 최근 체크포인트 지점의 LSN 이 활성 리두 공간의 시작점이 되게 됩니다

최근의 체크포인트의 LSN 과 마지막 리두 로그 엔트리의 LSN의 차이를 체크 포인트 에이지(Checkpoint Age) 라고 합니다.
즉 할성 리두 로그 공간의 크기를 의미 하게 됩니다.

리두 로그 파일이 너무 작게 되면 잦은 로그 스위치가 발생됨에 따라서 체크포인트도 자주 일어 날 것임으로 적절한 크기를 선택이 필요 합니다.
그렇다고 예를 들어 버퍼풀이 100GB 이기 때문에 리두 로그를 80GB 이렇게 과도 하게 설정 하지는 않아도 될 것 같습니다.
적절한 예시로 Redo Log 와 Log Switch 관련된 Oracle DB 에서도 이렇게 큰 용량의 Redo Log 는 사용하지 않기 때문입니다.

그래서 초반에는 적절한 용량을 선택 후에 필요 할때 마다 조금 씩 늘려가면서 최적의 값을 선택하는 방법이 좋을 것으로 생각 됩니다.
          

버퍼 풀 플러시

MySQL 5.7 을 거쳐서 8.0 버전으로 업그레이드 되면서 대부분의 서비스에서는 더티 페이지를 디스크에 동기화하는 부분(더티 페이지 플러시)에서 예전과 같은 디스크 쓰기 폭증 현상이 발생은 하지 않게 되었습니다.

InnoDB 스토리지 엔진은 버퍼 풀에서 아직 디스크로 기록되지 않은 더티 페이지들을 성능상의 악영향 없이 디스크에 동기화 하기 위해 다음과 같은 2개의 플러시 기능을 백그라운드로 실행하게 됩니다.

• 플러시 리스트(Flush_list) 플러시
• LRU 리스트(LRU_list) 플러스
      

플러시 리스트 플러시

InnoDB 스토리지 엔진은 리두 로그 공간의 재활용을 위해 주기적으로 오래된 리두 로그 엔트리가 사용하는 공간을 비워야 합니다.

그런데 이때 오래된 리두 로그 공간이 지워지려면 반드시 InnoDB 버퍼풀의 더티 페이지가 먼저 디스크에 동기화가 되어야 합니다.
이를 위해서 InnoDB 스토리지 엔진은 주기적으로 플러시 리스트(Flush_list) 플러시 함수를 호출해서 플러시 리스트에서 오래전에 변경된 데이터 페이지 순서대로 디스크에 동기화 하는 작업을 수행하게 됩니다.

디스크로 동기화(내려 쓰는) 과 관련하여 성능과 연관된 시스템 변수는 아래와 같습니다.

• innodb_page_cleaners
• innodb_max_dirty_pages_pct_lwm
• innodb_max_dirty_pages_pct
• innodb_io_capacity
• innodb_io_capacity_max
• innodb_flush_neighbors
• innodb_adaptive_flushing
• innodb_adaptive_flushing_lwm

더티 페이지를 디스크로 동기화 하는 스레드를 클리너 스레드(Cleaner Thread) 라고 하며, 버퍼 풀 마다 클리너 스레드가 동작하며 그래서 하나의 클리너 스레드는 하나의 버퍼 풀 인스턴스를 처리 하도록 자동으로 맞춰주게 됩니다.

하지만 버퍼 풀 수보다 innodb_page_cleaners 이 작다면 하나의 클리너 스레드가 여러개의 버퍼 풀을 인스턴스를 처리하게 됩니다.

innodb_max_dirty_pages_pct 시스템 변수는 버퍼 풀에서의 더티 페이지 비율을 조정하는 것 입니다.
버퍼 풀에 더티 페이지가 많다면 디스크 쓰기 버퍼링을 함으로써 쓰기 횟수를 줄일 수 있겠으나, 반대로 더티 페이지가 많을 경우 디스크 쓰기에 대한 부하(Disk IO Burst) 현상이 발생할 수도 있게 됩니다

innodb_io_capacity 시스템 변수 설정 값을 기준으로 더티 페이지 쓰기를 실행하며, 해당 파라미터 값에도 디스크 쓰기가 폭증하는 것을 방지 하기 위해서 innodb_max_dirty_pages_pct_lwm 시스템 변수를 통해 일정 수준 이상의 더티 페이지가 발생하면 조금 씩 더티 페이지를 디스크로 기록하게 하고 있습니다.

io_capacity 관련된 값에 대해서 IO 량이나 트래픽을 일일이 모니터링 하면서 값을 설정하는 것은 사실 어려운 일 입니다.
그래서 InnoDB 스토리지 엔진은 어댑티브 플러시 기능을 제공하며 기본 값을 기능 활성화 입니다.

어댑티브 플러시 기능은 리두 로그의 증가 속도를 분석해서 적절한 수준의 더티 페이지가 버퍼 풀에서 유지  될 수 있도록 디스크에 내려 쓰기를 조절/설정 하게 됩니다.
       

LRU 리스트 플러시

InnoDB 버퍼풀에서 사용 빈도가 낮은 데이터 페이지들을 제거해서 새로운 페이지가 읽어올 공간을 만들어야 하며 이를 위해 LRU 리스트 플러시 함수가 사용 되게 됩니다. 

LRU 리스트 끝에서 부터 시작해서 최대 innodb_lru_scan_depth 만큼 페이지을 스캔하여, 클린 페이지는 프리(Free) 페이지 리스트로 옮기게 됩니다.
        

버퍼 풀 상태 백업 및 복구

MySQL 5.6 버전 부터 버퍼 풀에 대해서 덤프 및 적재(Load) 기능이 도입 되었으며, 그에 따라서 MySQL 서버 재시작 시 성능을 위해서 워밍업(Warming Up)을 별도로 하는 대신에 파라미터 설정이나 명령어로 버퍼 풀의 상태를 백업(dump) 과 적재(load) 할 수 있게 되었습니다.

덤프를 수행 하면 물리적인 파일이 생성되며, 파일 용량은 버퍼풀 용량에 비해 매우 작으며, 이유는 LRU 리스트에 적재된 데이터 페이지의 메타 정보만 가져와서 저장하기 때문 입니다.

그래서 저장(dump)이 빠르게 수행될 수 있습니다 다만 적재의 경우 다시 버퍼 풀로 복구 하는 과정이 버퍼 풀의 크기에 따라서 달라지며 크기가 클수록 소요 되는 시간은 길어 질 수 있습니다.

적재 과정에서의 진행상태는 아래와 같이 확인 할 수 있습니다.

mysql> show status like 'Innodb_buffer_pool_dump_status'\G


적재 과정이 너무 오래 걸려서 중간에 멈추고자 한다면 아래와 같이 시스템 변수를 설정하면 됩니다

mysql> set global innodb_buffer_pool_load_abort=0;

      

버퍼 풀의 적재 내용 확인

MySQL 5.6  버전 부터 제공되는 information_schema 에서 innodb_buffer_page 테이블을 이용해서 버퍼풀의 메모리에 어떤 테이블의 페이지가 적재 되어있는지 확인 할 수 있게 되었습니다 다만 버퍼 풀 사이즈가 큰 경우 조회에 상당히 큰 부하를 일으켜서 문제가 될 수 있었습니다.

MySQL 8.0 에서는 이러한 부분을 개선한 information_schema 데이터베이스에 innodb_cached_indexes 테이블이 새로 추가 되었습니다.

그래서 테이블 전체(인덱스 포함) 페이지 중에서 대략 어느정도 비율이 InnoDB 버퍼풀에 적재 되어 있는지를 확인 할 수 있게 되었습니다.

해당 포스팅은 Real MySQL 8.0 책의 많은 내용 중에서 일부분의 내용만 함축적으로 정리한 것으로 모든 내용 확인 및 이해를 위해서 직접 책을 통해 모든 내용을 확인하시는 것을 권해 드립니다


이어지는 글

               

Reference

Reference Book
 • Real MySQL 8.0


관련된 다른 글

 

 

 

      

0
글에 대한 당신의 생각을 기다립니다. 댓글 의견 주세요!x