개요

  • MySQL/MariaDB는 전통적으로 특정 테이블에 대한 ALTER 명령 실행시, 작업이 완료될 때까지 해당 테이블 전체에 락을 걸어 읽기, 쓰기 작업이 불가능했다.
  • MySQL 5.6(MariaDB 10.0)부터 InnoDB 테이블에 Online DDL 기능이 추가되어 테이블 락을 최소화한 테이블 수정이 가능해졌다. 사용자는 ALTER 명령 실행시 ALGORITHM, LOCK 2개 옵션을 추가로 명시해주면 테이블 락을 최소화하여 테이블 스키마를 수정하는 작업을 수행할 수 있게 되었다.
ALTER TABLE foobar ADD COLUMN is_foobar TINYINT(1) NULL DEFAULT 0 COMMENT 'foobar 여부', ALGORITHM=INSTANT;
ALTER TABLE foobar ADD COLUMN is_foobar TINYINT(1) NULL DEFAULT 0 COMMENT 'foobar 여부' AFTER id, ALGORITHM=INPLACE, LOCK=NONE;

ALGORITHM 모드의 종류

  • ALGORITHM=COPY는 대상 테이블의 원본은 놔두고 변경될 테이블을 새로 생성한 후 원본으로부터 데이터를 로우 단위로 복제하는 방식이다. 모든 복제 작업과 인덱스 생성 작업이 완료되면 원본을 변경된 테이블로 바꾼다. 복제본을 만들어야 하기 때문에 시스템 자원을 많이 소모한다.
  • ALGORITHM=INPLACE은 대상 테이블의 복제 과정 없이 메타 데이터 변경 만으로 빠르게 변경사항을 반영하는 방식이다. 속도가 가장 빠르며, 시스템 자원도 거의 소모하지 않는다. MySQL 8.0 하위 버전에서는 가장 최선의 방법이라고 할 수 있다. 다만, ALGORITHM=INPLACE에는 주의할만한 제약사항이 있다. 이런 상황을 가정해보자. 만약, MySQL 5.6 이전 버전을 사용하면서 테이블을 생성했고, 이후 MySQL 5.6으로 업그레이드했다면 ALGORITHM=INPLACE는 당장 사용이 불가능하다. ERROR 1846 (0A000) 오류가 발생한다. 해결책은 최초 1번은 ALGORITHM=COPY를 사용하여 테이블을 리빌드하는 작업이 필요하다. 리빌드가 완료된 후에는 ALGORITHM=INPLACE의 사용이 가능해진다.
  • ALGORITHM=INSTANT는 MySQL 8.0(MariaDB 10.3.7)부터 지원하는 최신 모드이다. ALGORITHM=COPY, ALGORITHM=INPLACE, LOCK=NONE과 다르게 테이블 락이 전혀 발생하지 않는다. (텐센트가 오픈 소스를 기여했다.)

LOCK=NONE 제약사항

  • AUTO_INCREMENT 속성의 컬럼을 추가할 때는 불가능하다.
  • 대상 테이블이 FULLTEXT 인덱스를 가지거나, FTS_DOC_ID 컬럼을 가질 경우 불가능하다.
  • 대상 테이블이 FOREIGN KEY와 함께 ON … CASCADE 또는 ON … SET NULL 제약조건을 가질 경우 불가능하다.
# 모든 테이블의 FK 제약 조건 조회
# UPDATE_RULE, DELETE_RULE 컬럼을 통해 CASCADE|NO_ACTION 지정 여부를 확인 가능
SELECT * FROM information_schema.REFERENTIAL_CONSTRAINTS;

# 이미 설정된 CASCADE를 변경하려면 FK를 삭제 후 재생성해야함
# FK 제거 후 재생성하지 않고, 인덱스만 유지하는 것도 방법
ALTER TABLE foo DROP FOREIGN KEY bar;

ALGORITHM=INSTANT 소개

  • ALGORITHM=INSTANT는 Online DDL 실행시 테이블 락이 전혀 발생하지 않아 현재 최상의 알고리즘으로 권장된다. ALGORITHM=INPLACE, LOCK=NONE와 달리 해당 테이블에 대한 모든 실행 중인 쿼리가 종료될 때까지 기다려야 하는 일시적인 메타데이터 잠금이 전혀 발생하지 않는 장점이 있다. 즉, 전혀 락이 발생하지 않는다. [관련 링크]
  • 사용법은 ALGORITHM=INPLACE와 달리 LOCK=NONE 키워드 없이 단독으로 사용하면 된다.
ALTER TABLE foo ADD COLUMN bar1 INT, ADD COLUMN bar2 INT, ALGORITHM=INSTANT;

ALGORITHM=INSTANT 쓰임새

  • 새로운 컬럼을 추가할 때 사용 가능하다.
  • 기존 컬럼의 기본값을 추가하거나 삭제할 때 사용 가능하다.
  • 인덱스 타입을 변경할 때 사용 가능하다.
  • 테이블 이름을 변경할 때 사용 가능하다.

ALGORITHM=INSTANT 제약사항

  • MySQL 8.0.12부터 가능하다. (Amazon Aurora는 2021-11-18 부로 3 엔진 버전을 통해 지원한다.)
  • ALGORITHM=INSTANT가 불가능한 구문이 포함되면 사용이 불가능하다. (컬럼 추가시 ALTER TABLE을 단독으로 사용하는 것이 좋다.)
  • 컬럼 추가시 위치를 명시적으로 지정하면 안된다. 즉, 추가되는 컬럼의 위치는 마지막 컬럼 뒤어야 한다. (컬럼 위치 조정이 불가피하게 필요하다면 ALGORITHM=INPLACE, LOCK=NONE을 사용해야 하며, 이 경우 테이블 변경 작업의 처음과 마지막 시점에 테이블 락이 발생한다.)
  • 대상 테이블이 ROW_FORMAT=COMPRESSED이면 안된다. (테이블 생성시 기본 옵션은 ROW_FORMAT=DYNAMIC이기 때문에 거의 해당되지 않는다.)
  • 대상 테이블이 FULLTEXT 인덱스를 가지면 안된다.
  • 대상 테이블이 TEMPORARY 테이블이면 안된다.

갈레라 클러스터에서의 ALTER TABLE 실행

  • 갈레라 클러스터는 멀티 마스터 구조이기 때문에 ALTER TABLE의 실행이 전체 노드에 영향을 끼친다. 즉, 일반적인 방법으로 스키마 변경을 실행하면 전체 노드에 한꺼번에 쓰기 락이 발생한다. 이 방법은 24시간 매출이 발생하는 운영 서비스에 치명적일 수 있다. 최선의 방법은 운영 서비스에 영향을 최소화하기 위해 노드 단위로 차례로 스키마를 변경하는 것이다. 이를 위해 RSU(Rolling Schema Update) 모드를 사용해야 한다.(이와 반대되는 기본 모드가 TOI이다.) 방법은 아래와 같다.
$ mysql -u root
> USE somedb;

-- RSU 모드 전환
-- 이제부터 현재 노드에서 실행된 DDL을 다른 노드에 전파하지 않음
> SET GLOBAL wsrep_OSU_method = 'RSU';

-- 테이블 스키마 변경 실행
-- 앞서 RSU 모드 전환 덕분에 다른 노드에는 영향을 미치지 않음
-- DDL 실행 중에는 클러스터에서 노드가 일시적으로 이탈
-- DDL 완료 후 클러스터에 노드가 복귀하여 버퍼에 저장된 데이터 변경점을 반영
> ALTER TABLE foobar ...;
Stage: 1 of 2 'copy to tmp table' 100.0% OF stage done
Stage: 2 of 2 'Enabling keys' 100.0% OF stage done
Query OK, 3698216 ROWS affected (2 MIN 51.01 sec)
Records: 3698216  Duplicates: 0  Warnings: 0

-- TOI 모드 복귀
> SET GLOBAL wsrep_OSU_method = 'TOI';

다른 대안, gh-ost 마이그레이션 도구 사용

  • 써드 파티 마이그레이션 도구를 이용한 테이블 스키마 변경도 가능하다. GitHub이 직접 개발하여 오픈 소스로 공개한 gh-ost가 대표적이다. [관련 링크]

참고 글

'MySQL > ADMIN' 카테고리의 다른 글

1205 : Lock wait timeout >> kill  (0) 2024.01.07
Maria DB show 명령어  (0) 2023.08.31

우리는 SQL에서 2개의 테이블을 합칠 수 있다는 사실을 압니다. 바로 JOIN 이라는 개념을 사용해서요. 그렇다면, 세 개 혹은 그것보다 많은 테이블을 LEFT JOIN 할 수 있을까요? 네, 할 수 있습니다. 이 글은 여러 개의 테이블을 LEFT JOIN 하는 법에 관해 살펴볼 것입니다. 또한, 그 과정에서 우리가 우연히 놓칠 수 있는 개념에 대해서도 함께 다뤄보도록 하겠습니다.

 

LEFT JOIN이란?

LEFT JOIN이 무엇이었는지 되짚어보는 것부터 시작해보죠. 혹시 SQL의 조인 중에서 INNER JOIN을 기억하시나요? INNER JOIN은 조인하는 두 테이블에 공통으로 존재하는 값만 반환합니다. 반면에 LEFT JOIN의 경우, 왼쪽 테이블에서는 모든 값을 오른쪽 테이블에서는 왼쪽 테이블과 일치되는 값만 반환해줍니다. 그렇다면 어떠한 값이 왼쪽 테이블에는 존재하지만 오른쪽 테이블에는 존재하지 않는다면 두 테이블을 LEFT JOIN 한 결과는 어떠할까요? LEFT JOIN을 하는 이상 왼쪽 테이블이 주인공이기 때문에 왼쪽 테이블에서 가져온 행의 값들은 빠짐없이 전부다 반환될 것이지만, 오른쪽 테이블에서 가져오는 행의 값들은 존재하지 않으므로 NULL 값으로 반환됩니다. 그렇다면, 오른쪽 테이블에 있는 값이 왼쪽 테이블에는 존재하지 않는다면 두 테이블을 LEFT JOIN 한 결과는 어떠할까요? 결과 테이블에는 해당 값이 등장조차 하지 않을 것입니다. 왜냐, 주인공은 왼쪽 테이블이니까요.

아래는 LEFT JOIN의 가장 기본적인 구문입니다.

SELECT column names
FROM table1 AS t1
LEFT JOIN table2 AS t2
    ON t1.common_column = t2.common_column;

LEFT JOIN은 SQL을 통한 분석을 할 때 상당히 자주 사용되는 문법 중 하나입니다. 왜냐하면 이는 두 테이블 간의 차이를 파악할 때 상당히 용이하기 때문입니다. 즉, 특정 테이블에는 값이 존재하지만 다른 테이블에는 존재하지 않는 값을 분별해 낼 수 있기 때문입니다. 만약, 왼쪽 테이블에는 값이 존재하지만 오른쪽 테이블에는 존재하지 않는 값들만 따로 조회하고 싶다면 어떤 식으로 쿼리문을 구성해보면 좋을까요? LEFT JOIN을 한 후에 WHERE 절을 추가하여 오른쪽 테이블 열의 값이 NULL인 행을 추가로 필터링해달라고 요청하면 됩니다. 

실제 비즈니스 케이스를 통해 이야기해보도록 하겠습니다. 우리가 온라인 서점을 운영한다고 가정해 보겠습니다. 우리는 지난 6개월 간의 주문 내역을 통해 해당 기간 동안 구매 활동이 뜸해진 고객, 즉 비활성 고객이 누구인지 알고 싶습니다. 하지만 비활성 고객의 목록만 보는 것이 아니라, 최근에 구매 활동이 있었던 활성 고객의 목록도 같이 보았으면 합니다. 즉, 모든 고객의 정보를 나열하고, 그 고객이 구매를 했는지 안 했는지를 추가로 알 수 있게 결과 테이블이 구성되었으면 합니다. 지금 설명한 예시가 바로 LEFT JOIN를 사용하기에 아주 좋은 사례입니다.

우리 상점 내의 데이터베이스 테이블들을 살펴 보도록 합시다.

1. 고객 테이블 (customers)

id first_name last_name gender age customer_since
1 Daniel Black M 34 2014-10-13
2 Erik Brown M 25 2015-06-10
3 Diana Trump F 39 2015-10-25
4 Anna Yao F 19 2017-02-20
5 Christian Sanders M 42 2018-01-31

2. 매출 테이블 (sales)

id date book_id customer_id quantity amount
1 2019-09-02 2 3 1 14.99
2 2019-10-01 1 2 1 12.99
3 2019-10-01 3 4 1 15.75

최근 6개월간의 비활성 고객과 활성 고객을 구하는 쿼리문을 작성하면 다음과 같습니다.

SELECT
 c.id,
 c.first_name,
 c.last_name,
 c.gender,
 c.age,
 c.customer_since,
 s.date AS sales_date,
 SUM(s.amount) AS total_spent
FROM customers AS c
LEFT JOIN sales AS s
    ON c.id = s.customer_id
GROUP BY c.id;

해당 쿼리는 customers 테이블에서 고객의 정보를 전부 추출할 뿐만 아니라 그들의 구매 날짜와 구매액을 계산합니다. 구매 날짜와 구매액은 sales 테이블에서 가져와야 할 열입니다. 해당 쿼리문을 실행해보면 아래와 같은 결과를 얻을 수 있는데요. 이중 볼드 처리된 2개의 행은 LEFT JOIN으로만 얻어낼 수 있는 결과입니다. 만약 위 쿼리문에서 LEFT JOIN 대신 INNER JOIN을 사용한다면 테이블 양쪽에 공통으로 존재하는 값만 반환할 것이므로 구매액 정보가 없는 아이디 1과 5는 조회되지 않을 것입니다.

id first_name last_name gender age customer_since sales_date total_spent
1 Daniel Black M 34 2014-10-13 [NULL] [NULL]
2 Erik Brown M 25 2015-06-10 2019-10-01 12.99
3 Diana Trump F 39 2015-10-25 2019-09-02 14.99
4 Anna Yao F 19 2017-02-20 2019-10-01 15.75
5 Christian Sanders M 42 2018-01-31 [NULL] [NULL]

보시다시피, 해당 기간에 아무런 구매 활동이 없었던 고객의 경우 total_spent 열의 값이 존재하지 않습니다. 하지만, 그들의 고객 정보가 주인공 테이블인 왼쪽 테이블 customers 에 존재했기에 그들의 아이디, 이름, 성별, 나이 등의 정보가 조회될 수 있었습니다. 하지만, 오른쪽 테이블에서 가져오고자 했던 그들의 구매 날짜와 구매액에 관한 데이터는 존재하지 않으므로 전부 NULL값이 나왔습니다. 이게 LEFT JOIN의 모습입니다. 왼쪽 테이블과 공통되는 값이 없다고 해서 왼쪽 테이블 내의 행을 날려버리지 않습니다. 

이젠 좀더 복잡한 경우를 살펴보도록 하겠습니다.

 

하나의 쿼리 속에 여러 번의 LEFT JOIN

분석을 하다보면 2개의 테이블만으로는 분석이 힘들고 3개 이상의 테이블을 조인해야 하는 상황이 발생합니다. 그래서 이번에는 3개 이상의 테이블을 LEFT JOIN 하는 모습을 보여드리도록 하겠습니다. 아래 테이블을 보시죠. 

3. 프로모션 테이블 (promotions)

id campaign customer_id date
1 SMS_discount10 2 2019-09-01
2 SMS_discount10 3 2019-09-01
3 SMS_discount10 5 2019-09-01

우리는 해당 테이블을 활용하여 최근에 진행한 프로모션이 고객의 구매에 영향을 끼쳤는지 알고 싶습니다. 이를 구하기 위해서는 고객(customers), 매출(sales), 프로모션(promotion) 총 3개의 테이블을 조인하면 됩니다. 우리가 원하는 정보가 세 개의 테이블에 흩어져 있으니까요.

SELECT
 c.id,
 c.first_name,
 c.last_name,
 c.gender,
 c.age,
 c.customer_since,
 s.date AS sale,
 p.date AS promotion
FROM customers AS c
LEFT JOIN sales AS s
   ON c.id = s.customer_id
LEFT JOIN promotions AS p
    ON c.id = p.customer_id;

위 쿼리를 실행하면 아래와 같은 결과를 얻습니다.

id first_name last_name gender age customer_since sale promotion
1 Daniel Black M 34 2014-10-13 [NULL] [NULL]
2 Erik Brown M 25 2015-06-10 2019-10-01 2019-09-01
3 Diana Trump F 39 2015-10-25 2019-09-02 2019-09-01
4 Anna Yao F 19 2017-02-20 2019-10-01 [NULL]
5 Christian Sanders M 42 2018-01-31 [NULL] 2019-09-01

LEFT JOIN을 사용했기 때문에 고객의 구매 여부나 프로모션 참여 여부와는 상관없이 모든 고객의 목록을 조회할 수 있었습니다. 만약 3개의 테이블을 INNER JOIN으로 합쳤다면, 결과 테이블은 구매도 하고 프로모션에도 참여한 고객만 조회되었을 것입니다. LEFT JOIN은 한 테이블에는 값이 있지만 다른 테이블에는 그 값이 없어도 여전히 기존 행을 조회할 수 있습니다. 고객 1을 보시죠. 고객 1은 구매도 하지 않았고 프로모션 메시지를 받은 적도 없습니다. 그럼에도 불구하고 우리는 고객 1에 관한 기본적인 정보를 조회받지 않았습니까? 이게 바로 LEFT JOIN의 힘입니다. 고객 4도 살펴보시죠. 이 고객의 경우 구매 이력은 있지만 프로모션 메시지를 받은 적은 없습니다. 고객 5의 경우는 구매 이력은 없지만 프로모션 메시지는 받았습니다. 마지막으로, 구매 이력도 있고 프로모션 메시지도 받은 고객 2와 3도 결과 테이블에서 찾아볼 수 있습니다. 이 2명의 고객의 결과는 INNER JOIN을 사용했을 때도 얻을 수 있는 결과일 것입니다.

이 테이블 결과를 얻기 위해 작성한 쿼리문으로 돌아가보면, 우리는 두 번째 테이블(sales)와 세 번째 테이블(promotions)을 조인하기 위해 첫 번째 테이블(customers)과 겹치는 열을 사용하였습니다. 하지만, 항상 첫 번째 행과 겹치는 열을 찾아 조인해야 하는 것은 아닙니다. 세 번째의 테이블을 조인하고자 두 번째와의 공통된 열을 사용해도 괜찮습니다. 아래 예시에서 해당 케이스를 다뤄보도록 하겠습니다.

이번에는 고객에게 반응이 좋았던 책의 장르들을 알아보고 싶습니다. 이런 분석은 고객에게 책 추천과 같은 개인화된 경험을 제공할 수 있기 때문에 상당히 유용한 정보입니다. 해당 분석을 위해 총 3개의 테이블에서 각기 다른 데이터를 가져올 예정입니다. 테이블은 customers 테이블, sales 테이블, books 테이블이고요. 이미 앞에서 customers 와 sales 테이블을 조인한 적이 있으니, 3번째 테이블인 books 만 추가로 조인하는 과정을 보여드리도록 하겠습니다.

4. 책 테이블 (books)

id name author genre quantity price
1 The Lord of the Rings J. R. R. Tolkien fantasy 7 12.99
2 Lolita Vladimir Nabokov novel 4 14.99
4 The Hobbit J. R. R. Tolkien fantasy 10 10.75
5 Death on the Nile Agatha Christie detective 8 9.75

쿼리문은 아래와 같습니다.

SELECT
 c.id,
 c.first_name,
 c.last_name,
 s.date AS sale,
 b.name AS book,
 b.genre
FROM customers AS c
LEFT JOIN sales AS s
   ON c.id = s.customer_id
LEFT JOIN books AS b
    ON s.book_id = b.id;

아래는 customers, sales, books 테이블을 LEFT JOIN 한 결과입니다. 테이블의 구성을 보니, 최근 특정 고객이 주문한 책의 이름과 장르라는 정보가 고객의 정보에 합쳐진 것을 확인할 수 있습니다.

id first_name last_name sale book genre
1 Daniel Black [NULL] [NULL] [NULL]
2 Erik Brown 2019-10-01 The Lord of the Rings fantasy
3 Diana Trump 2019-09-02 Lolita novel
4 Anna Yao 2019-10-01 [NULL] [NULL]
5 Christian Sanders [NULL] [NULL] [NULL]

결과 테이블을 보니, 최근에 구매 이력이 없기에 sales 테이블에서 가져온 sale 열의 값이 NULL인 고객이 2명 있습니다(고객 1과 5). 하지만, 우리가 INNER JOIN이 아닌 LEFT JOIN을 사용했기 때문에 이 둘의 결과를 조회받을 수 있었습니다. 이번에는 고객 4를 보시죠. 최근에 책을 구매하긴 했지만, 우리의 책 테이블 내에는 정보가 등록되어 있지 않은 책을 구매했나 봅니다. 그래서 sales 테이블에서 가져온 sale 열 값은 존재하지만 book 테이블에서 가져온 book 열과 genre 열의 값은 NULL 인 것을 확인할 수 있습니다. 계속해서 언급하는 부분이지만, LEFT JOIN을 사용했기 때문에 이렇게 한 쪽에는 값이 존재하지 않더라도 여전히 왼쪽 테이블의 결과가 조회되는 이런 구성의 결과를 얻을 수 있는 것입니다.

지금까지 다룬 내용을 보아, LEFT JOIN을 통해 3개 이상의 테이블을 조인 하는데는 큰 문제가 없습니다. 하지만, 내가 원하는 결과를 제대로 얻기 위해서는 아래 2개의 주의사항들만 기억해주세요.

 

LEFT JOIN을 여러번 할 때 주의할 점

SQL에서 2개가 아닌 3개 이상의 테이블을 조인하는 것은 헷갈리고 꽤나 까다로운 작업입니다. 그래서 3개 이상의 테이블을 LEFT JOIN을 할 경우 아래의 2가지를 기억해주세요.

첫째, INNER JOIN과는 달리 LEFT JOIN은 조인하는 테이블의 순서가 상당히 중요합니다. 여러분이 어떤 순서로 테이블을 조인하는지에 따라 결과 테이블에 조회되는 행의 개수며 구성 등이 달라질 수 있습니다. 따라서 JOIN 문을 작성하실 때, 만약 여러분이 LEFT JOIN을 하실 거라면 가장 첫 번째의 테이블로 SELECT문에 가장 많은 열을 가져와야 할 테이블을 우선으로 적어주세요. 

둘째, 조인을 여러 번 해야하는데 시작을 LEFT JOIN으로 했다면 보통의 경우는 나머지 조인도 LEFT JOIN을 합니다. 즉, LEFT JOIN을 쓰다가 갑자기 INNER JOIN 이나 다른 조인을 사용하지 않는다는 이야기입니다. 우리가 LEFT JOIN을 사용하는 이유가 무엇입니까? 어떤 값이 비록 조인하는 여러 개 테이블 내에는 공통적으로 존재하진 않더라도, 여전히 그 값을 결과 테이블에서 보고 싶기 때문입니다. 애초에 우리가 다양한 JOIN 종류 중에서 LEFT JOIN을 사용한 이유를 잊지 않으면서, 추가적인 LEFT JOIN을 구성해 나가야 합니다. 부디 갑자기 INNER JOIN을 사용함으로써 원하는 값이 조회되지 않는 불상사를 겪지 않으시길 바랍니다.

'ORACLE > SQL' 카테고리의 다른 글

SQL 문장의 실행 원리  (0) 2022.11.30
피벗(1) - LISTAGG, WM_CONCAT  (0) 2020.12.29
피벗(2) - PIVOT, UNPIVOT, (행을 열로, 열을 행으로)  (0) 2020.12.29
FETCH .. BULK COLLECT INTO  (0) 2020.07.22
SQL Profile - plan 변경(응급조치)  (0) 2019.10.18

MariaDB / Mysql 실행계획을 간단히 정리해보자면 아래와 같습니다.

 

-. 효율적인 플랜

  1. const
  2. ref
  3. eq_ref
  4. range

-. 비효율적인 플랜

  1. derived : from 절에 사용된 서브쿼리로부터 생성된 인시 테이블 => 가능하면 JOIN으로 풀도록 수정
  2. uncacheable subquery : MariaDB 옵티마이저는 서브쿼리를 최대한 캐싱하여 재사용되도록 유도하지만
    사용자 변수나 함수가 사용되면 이러한 캐시기능을 사용할  없음
  3. dependent subquery : 외부 쿼리에서 값을 전달 받아 실행되는 서브쿼리의 경우가 해당됨.
    이런 경우 서브쿼리가 먼저 실행되지 못하고 서브쿼리 외부의 결과값에 의존적이기 때문에 성능이 저하됨
  4. ALL / index : ALL 풀테이블 스캔 / index  인덱스  스캔으로 OLTP 환경에서는 적합하지 않은 경우가 많음

 

 

이번 글에서 살펴볼 실행계획은 "Using where" 입니다.

 

 

 

MariaDB / Mysql 은 크게 MariaDB 엔진과 스토리지 엔진, 두 개의 레이어로 나뉨

스토리지 엔진에서는 디스크나 메모리 상에서 필요한 레코드를 읽어오고 MariaDB 엔진은 스토리지 엔진으로 부터 받은 레코드를 연산하는 작업을 수행함

 

* 스토리지 엔진 레벨에서 처리 될 때 실행계획에서 Using index(커버링 인덱스) 로 풀리고  MariaDB 엔진으로 올라와 필터링 될 때 Using where 로 풀림

Using where 은 흔한 플랜이지만 처리한 rows 수와 실제 결과값의 차이가 많이 난다면 스토리지 엔진에서 불필요하게 너무 많은 데이터에 access 한다는 의미이므로

인덱스 설계가 제대로 되지않았을 가능성이 높음

 

-. Using where

 

MariaDB [employees]> explain extended select * from employees where emp_no  between 10001 and 10100 and gender='F';
+------+-------------+-----------+-------+---------------+---------+---------+------+------+----------+-------------+
| id   | select_type | table     | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+------+-------------+-----------+-------+---------------+---------+---------+------+------+----------+-------------+
|    1 | SIMPLE      | employees | range | PRIMARY       | PRIMARY | 4       | NULL |  100 |   100.00 | Using where |
+------+-------------+-----------+-------+---------------+---------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.000 sec)

MariaDB [employees]> select count(*) from employees where emp_no  between 10001 and 10100 and gender='F';
+----------+
| count(*) |
+----------+
|       37 |
+----------+
1 row in set (0.000 sec)

=> 실제 결과값은 37 건이지만 스토리지 엔진에서는 100 rows 를 읽어감. MariaDB 엔진에서 63건을 필터링 했다는 의미로 스토리지 엔진에서 불필요한 I/O 가 발생한 상황

 

 

MariaDB [employees]> show status like '%handler%read%';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| Handler_read_first       | 0     |
| Handler_read_key         | 1     |
| Handler_read_last        | 0     |
| Handler_read_next        | 100   |
| Handler_read_prev        | 0     |
| Handler_read_retry       | 0     |
| Handler_read_rnd         | 0     |
| Handler_read_rnd_deleted | 0     |
| Handler_read_rnd_next    | 0     |
+--------------------------+-------+
9 rows in set (0.001 sec)

=> handler 는 쿼리가 storage 엔진의 데이터에 접근하는 패턴 / 방식으로 

Handler_read_next 는 인덱스 컬럼을 range 방식으로 읽을 때 발생하는 것으로 100번 요청됨

 

 

-. Using index

 

MariaDB [employees]> show index from employees;
+-----------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table     | Non_unique | Key_name     | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-----------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| employees |          0 | PRIMARY      |            1 | emp_no      | A         |      299556 |     NULL | NULL   |      | BTREE      |         |               |
| employees |          1 | ix_firstname |            1 | first_name  | A         |        2582 |     NULL | NULL   |      | BTREE      |         |               
| employees |          1 | ix_hiredate  |            1 | hire_date   | A         |       10698 |     NULL | NULL   |      | BTREE      |         |               |
| employees |          1 | test         |            1 | emp_no      | A         |      299556 |     NULL | NULL   |      | BTREE      |         |               |
| employees |          1 | test         |            2 | gender      | A         |      299556 |     NULL | NULL   |      | BTREE      |         |               |
+-----------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
5 rows in set (0.001 sec)

=> emp_no + gender 인덱스 추가 생성

 

 

 

MariaDB [employees]> explain extended select * from employees use index(test) where emp_no  between 10001 and 10100 and gender='F';
+------+-------------+-----------+-------+---------------+------+---------+------+------+----------+-----------------------+
| id   | select_type | table     | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra                 |
+------+-------------+-----------+-------+---------------+------+---------+------+------+----------+-----------------------+
|    1 | SIMPLE      | employees | range | test          | test | 5       | NULL |   99 |   100.00 | Using index condition |
+------+-------------+-----------+-------+---------------+------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.006 sec)

=> 통계정보 + range 방식으로 접근 rows 는 99건, 별차이 없지만 Using index 로 풀린 것 확인

 

 

MariaDB [employees]> show status like '%handler%read%';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| Handler_read_first       | 0     |
| Handler_read_key         | 1     |
| Handler_read_last        | 0     |
| Handler_read_next        | 37    |
| Handler_read_prev        | 0     |
| Handler_read_retry       | 0     |
| Handler_read_rnd         | 0     |
| Handler_read_rnd_deleted | 0     |
| Handler_read_rnd_next    | 0     |
+--------------------------+-------+
9 rows in set (0.001 sec)

=> Handler_read_next 37건 요청된 것 확인.

-1. SHOW 명령어란

MySQL 로그인후 쉘상에서 실행되는 SHOW 명령어로 
Oracle의 show parameter, dbms_metadata 명령어와 동일한 역할 수행
 

-2. SHOW 명령어 리스트

 
1) SHOW DATABASE;
=> 현재 계정이 사용가능한 DATABASE 목록
   
2) SHOW CREATE TABLE db명.TABLE_NAME;
=> 해당 테이블을 만들기 위한 SQL 쿼리문을 출력함
 
3) show tables from db명;
=> 선택 DB의 table 리스트를 출력 
 
4) show index from db명.table;
=> 해당 table의 인덱스 보기
 
5) show columns from db명.테이블명;
=> desc db명.테이블명 과 같은 명령어로 해당 table의 테이블 구조를 볼 수 있다
 
6) show table status from db명 like 'table_name';
=> 해당 table의 정보를 상세하게 출력
※show table status의 항목 설명 
MariaDB [(none)]> show table status from testdb like 'test'\G;
*************************** 1. row ***************************
           Name: test
         Engine: InnoDB
        Version: 10
     Row_format: Compact
           Rows: 13010
 Avg_row_length: 122
    Data_length: 1589248
Max_data_length: 0
   Index_length: 0
      Data_free: 4194304
 Auto_increment: NULL
    Create_time: 2017-10-31 04:58:39
    Update_time: NULL
     Check_time: NULL
      Collation: utf8_bin
       Checksum: NULL
 Create_options:
        Comment:
1 row in set (0.00 sec)
 
 
 -Name : 테이블 이름 
 -Engine : 해당 테이블의 엔진
 -Row_format : 열저장 형태 (fixed,Dynamic,Compressed) 
 -Rows : 열의 수 
 -Avg_row_length : 열의 평균 길이 
 -Data_length : 데이타파일의 길이 = 위의 Rows * Avg_row_length 값과 근사치
 -Max_data_length : 해당 테이블이 사용할 수 있는 최대 데이터 파일의 크기 => myIsam 엔진 사용 테이블에 해당하는 설정으로 max_rows를 설정하여 제한 가능.
 -Index_length : 인덱스 파일의 길이 
 -Data_free : Max_data_length - Data_length 로 가용공간을 의미 
 -Auto_increment : 다음 자동증가 변수 
 -Create_time : 테이블이 생성된 시간 
 -Update_time : 데이타파일의 마지막 UPDATE 시간 
 -Check_time : 테이블의 마지막 체크시간 
 -Create_options : 테이블 생성시의 기타옵션 
 -Comment : 테이블 생성시의 명령어 
 
 
7) show variables;
=> 서버의 variables(환경 변수 등) 출력
예) show variables like '%buffer%';
buffer 관련 설정들의 값을 확인할 수 있음
 
 
8) show processlist
=> 현재 mysql db server에 연결되어있는 connection ( thread ) 정보를 출력할때 사용.
root로 접속시 모든 사용자의 연결이 보이며 계정사용자는 자신의 연결정보만 보임.
 
MariaDB [(none)]> show processlist\G;
*************************** 10. row ***************************
      Id: 2911252
    User: test_app
    Host: 10.10.10.10:49730
      db: test
 Command: Execute
    Time: 0
   State: Sending data
    Info: SELECT test(test.test)
    INTO test
    FROM (
          SELECT DISTINCT test
            FROM (  .....
Progress: 0.000
 
※show processlist의 항목 설명 
 -Id : 해당 커넥션(thread) 의 session id 
 -User : 해당 커넥션의 db계정
 -Host : 해당 커넥션의 접속 IP
 -db : 해당 커넥션이 수행하는 쿼리의 대상 db
 -Command : 해당 커넥션의 현재 command 상태로 execute , sleep 등이 있음
 -Time : 프로세스가 현재 커멘드 상태에서 동작한 시간
 -State : 해당 커넥션 (thread) 의 현재 상태에 대한 정보
 -Info : 현재 실행하고 있는 SQL로 show processlist로는 최대 100자까지 표시됨. 
         전부 표시하려면 show full processlist 커맨드로 수행
 
* 갱신되는 processlist 현황 모니터링 방법
리눅스쉘상태에서만 실행 가능하며
=> mysqladmin -u 아이디 -p패스워드 -i1 processlist
해당 계정에서의 processlist를 지속으로 화면에 -i1 1초에 한번씩 새로 화면에 출력하는 커맨드
 
 

'MySQL > ADMIN' 카테고리의 다른 글

1205 : Lock wait timeout >> kill  (0) 2024.01.07
MySQL/MariaDB, 테이블 락 최소화하여 변경하기  (1) 2023.11.09

슬로우 쿼리 원인 분석 및 해결

  • 데이터베이스의 슬로우 쿼리를 유발하는 요소는 많다. 형사가 단서를 잡아 하나씩 수사망을 좁혀가듯이 원인을 분석해가야 한다. 가장 먼저 선행되어야할, 그리고 가장 많이 사용해야 하는 것은 EXPLAIN(실행 계획) 또는 EXPLAIN FORMAT=JSON(전자보다 제공하는 정보가 더 많음) 문을 사용하여 해당 슬로우 쿼리의 인덱스 사용 정보를 확인하는 것이다. 아래와 같이 모든 쿼리에 사용이 가능하다.
EXPLAIN FORMAT=JSON
SELECT *
FROM USER 

 

  • MYSQL은 인덱스 생성시 컬럼의 정렬 순서로 ASC만을 지원한다. 인덱스 생성 사양에는 컬럼의 정렬 순서(ASC, DESC)를 명시할 수 있게 되어 있지만 실제로 DESC는 무시되고 ASC로만 인덱스를 생성한다. 즉, ORDER BY에 명시된 멀티 컬럼에 ASC, DESC가 혼재되어 있다면 인덱스는 무시된다.

innodb_buffer_pool_size

  • 인덱스 설계가 잘 되어 있는데도 슬로우 쿼리가 해결되지 않는다면? innodb_buffer_pool_size 파라메터를 의심해봐야 한다. (이름이 의미하듯이 InnoDB 스토리지 엔진에만 해당한다.) 해당 파라메터의 크기가 클수록 쿼리 실행시 디스크보다 메모리를 사용하게 되어 빠른 결과를 얻을 수 있다.
# 현재 설정된 innodb_buffer_pool_size 값 확인, 바이트 단위로 출력
SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool_%';
innodb_buffer_pool_size = 268435456

# innodb_buffer_pool_size 값 설정, MySQL 5.7.5 이상 및 SUPER 권한 필요
SET GLOBAL innodb_buffer_pool_size = 8589934592;

# innodb_buffer_pool_size 값 설정, MySQL 5.7.5 미만, 서비스 재시작 필요
# innodb_buffer_pool_instances는 설정된 innodb_buffer_pool_size를 쪼개어 병렬로 제어할 쓰레드의 개수, 각 인스턴스의 크기가 1GB 이상일 경우에만 작동
$ nano /etc/my.cnf innodb_buffer_pool_size = 8589934592 innodb_buffer_pool_instances = 8 

 

  • 현재 설정된 innodb_buffer_pool_size이 충분한지는 아래와 같이 information_schema.TABLES 테이블에서 현재 사용량을 조회하여 확인할 수 있다.
SELECT engine,
count(*) AS table_count,
concat(round(sum(table_rows)/1000000,2),'M') AS table_rows,
concat(round(sum(data_length)/(1024*1024*1024),2),'G') AS total_data,
concat(round(sum(index_length)/(1024*1024*1024),2),'G') AS total_index,
concat(round(sum(data_length+index_length)/(1024*1024*1024),2),'G') AS total_size,
round(sum(index_length)/sum(data_length),2) AS index_frac
FROM information_schema.TABLES  GROUP BY engine
ORDER BY sum(data_length+index_length) DESC 

innodb_flush_log_at_trx_commit

  • INSERT, UPDATE, DELETE 쿼리로 변동되는 MySQL의 데이터베이스 상태는 일시적으로 메모리(innodb_buffer_pool)에 보관된다. 메모리에 보관된 데이터는 정전이나 운영체제 장애가 발생할 경우 유실될 우려가 있다. flush 행위가 발생해야만 비로소 디스크에 영구적으로 저장된다. MySQL은 기본 설정 상태에서 매 트랜잭션 커밋 시점마다 flush를 발생시켜 데이터베이스의 원자성을 보장한다. 반면 잦은 flush는 결국 I/O 블로킹으로 이어져 응답 속도를 늦추는 결과를 초래한다. 극단적인 퍼포먼스가 필요할 경우 이 값을 수정하여 응답 속도를 4배 이상 향상시킬 수 있다. 목적에 따라 innodb_flush_log_at_trx_commit 값을 아래와 같이 수정할 수 있다. 관련 링크
$ nano /etc/my.cnf [mysqld]
// 기본값으로 가장 안전하고 권장되는 설정, 매 트랜잭션 커밋 시점마다 flush 발생
innodb_flush_log_at_trx_commit = 1 sync_binlog = 1 

// 매 초마다 flush 발생, 응답 속도는 증가하나 운영체제 장애나 정전시 초 단위로 데이터 유실 발생
innodb_flush_log_at_trx_commit = 2 
  • 단일 노드가 아닌 갈레라 클러스터 구성의 경우 동시에 모든 노드의 하드웨어가 장애로 중단되는 경우는 극히 드물기 때문에 innodb_flush_log_at_trx_commit = 2 옵션을 안전하게 사용할 수 있다. 관련 링크

performance_schema

 

 

  • 비활성화된 기능을 활성화하려면 아래와 같이 my.cnf 파일을 수정하고 서비스를 재시작해야 한다.
# performance_schema 활성화 여부 확인
SHOW GLOBAL VARIABLES LIKE 'performance_schema'

# 기능 활성화 후 서비스 재시작
$ nano /etc/my.cnf performance_schema = 1

# 특정 사용자에게 권한 부여
GRANT ALL ON performance_schema.* TO '{user_id}'@'{ip_address}' IDENTIFIED BY '{user_password}'
  • performance_schema 기능이 활성화되면 동일한 이름의 데이터베이스를 조회할 수 있다. 다양한 테이블을 제공하는데 대표적으로 현재 연결된 클라이언트 상태라던가, 슬로우 쿼리를 포함한 전체 쿼리 패턴 정보 등을 확인할 수 있다. 특히 performance_schema.threads 테이블은 운영 환경에 전혀 영향을 주지 않는다. (비슷한 정보를 제공하는 SHOW PROCESSLIST(SELECT * FROM information_schema.PROCESSLIST의 축약 명령)의 경우 실행시 일시적인 뮤텍스 락이 발생하는 단점이 있다.)
# performance_schema 데이터베이스 사용
USE performance_schema 

# 현재 쓰레드(연결) 개수 확인
SELECT * FROM performance_schema.threads

# 현재까지 누적된 쿼리 패턴 통계 확인
SELECT * FROM performance_schema.events_statements_summary_by_digest

# 현재까지 누적된 쿼리 패턴 통계 초기화
TRUNCATE performance_schema.events_statements_summary_by_digest

# 현재 실행 중인 쿼리 확인
SELECT * FROM performance_schema.events_statements_current

# 최근 실행된 쿼리 이력 기능 활성화
UPDATE performance_schema.setup_consumers SET ENABLED = 'yes' WHERE NAME = 'events_statements_history'
UPDATE performance_schema.setup_consumers SET ENABLED = 'yes' WHERE NAME = 'events_statements_history_long'

# 최근 실행된 쿼리 이력 확인
SELECT * FROM performance_schema.events_statements_history

# 최근 실행된 10,000개 쿼리 이력 확인
SELECT * FROM performance_schema.events_statements_history_long 
  • 제공되는 정보를 적절히 가공하여 아래와 같이 모든 실행 쿼리 패턴 단위의 시간 통계 및 풀 테이블 스캔 여부를 확인할 수도 있다. 소요 시간과 관계된 컬럼들은 SEC_TO_TIME({COLUMN}/1000000000000)를 사용하면 한 눈에 확인이 가능하게 출력해준다.
SELECT digest_text                                 AS query,
       IF(sum_no_good_index_used > 0
           OR sum_no_index_used > 0, '*', '')      AS full_scan,
       count_star                                  AS exec_count,
       sum_errors                                  AS err_count,
       sum_warnings                                AS warn_count,
       Sec_to_time(sum_timer_wait / 1000000000000) AS exec_time_total,
       Sec_to_time(max_timer_wait / 1000000000000) AS exec_time_max,
       Sec_to_time(avg_timer_wait / 1000000000000) AS exec_time_avg_ms,
       sum_rows_sent                               AS rows_sent,
       Round(sum_rows_sent / count_star)           AS rows_sent_avg,
       sum_rows_examined                           AS rows_scanned,
       digest                                      AS digest
FROM   performance_schema.events_statements_summary_by_digest
ORDER  BY sum_timer_wait DESC;

SHOW GLOBAL STATUS

  • SHOW GLOBAL STATUS 명령을 사용하면 현재 데이터베이스의 상태를 알 수 있는 다양한 정보를 조회할 수 있다. 그 중에는 현재 상태를 의미하는 것도 있고 누적된 수치를 제공하는 것도 있다. 이 정보를 적절히 활용하면 원격지에서 데이터베이스의 상황을 관찰할 수 있다.
// 현재 상태 조회 > SHOW GLOBAL STATUS;  Memory_used = 577656392 // 현재 사용 중인 메모리 bytes Max_used_connections = 306 // flush 이후 최대 동시 접속 수  // 상태를 초기화, RELOAD 권한 필요 > FLUSH STATUS; 
  • 한편 SHOW GLOBAL STATUS LIKE 'wsrep_%'는 갈레라 클러스터의 현재 정보를 확인할 수 있는 정보만 추려서 확인할 수 있다. 관련 링크 제공되는 주요 정보는 아래와 같다.
# 갈레라 클러스터 정보를 조회
> SHOW GLOBAL STATUS LIKE 'wsrep_%';
wsrep_cluster_state_uuid = '2eebce42-dbf0-11e7-8263-b34f5d2d9960'
# 클러스터의 현재 상태
UUID wsrep_local_state_uuid = '2eebce42-dbf0-11e7-8263-b34f5d2d9960'
# 노드의 현재 상태 UUID, 모든 노드가 wsrep_cluster_state_uuid와 동일해야 정상
wsrep_cluster_size = 3
# 클러스터를 구성하는 노드 수
wsrep_ready = 'ON'
# 현재 노드의 클러스터 작동 여부 [ON, OFF]
wsrep_connected = 'ON'
# 현재 노드의 클러스터 동기화 여부
wsrep_local_state_comment = 'Synced'

현재 연결된 IP 주소 목록 확인

  • INFORMATION_SCHEMA.PROCESSLIST 테이블의 쿼리를 가공하면 현재 연결되 IP 주소 목록을 확인할 수 있다.
SELECT 
  tmp.ipAddress, 
  COUNT(*) AS numConnections, 
  FLOOR(
    AVG(tmp.time)
  ) AS timeAVG, 
  MAX(tmp.time) AS timeMAX 
FROM 
  (
    SELECT 
      pl.id, 
      pl.user, 
      pl.host, 
      pl.db, 
      pl.command, 
      pl.time, 
      pl.state, 
      pl.info, 
      LEFT(
        pl.host, 
        (
          LOCATE(':', pl.host) -1
        )
      ) AS ipAddress 
    FROM 
      INFORMATION_SCHEMA.PROCESSLIST pl
  ) AS tmp 
GROUP BY 
  tmp.ipAddress 
ORDER BY 
  numConnections DESC;

== OGG GLOBALS 파일 설정(Genaral) ======================================================================

#> ./ggsci

GGSCI> EDIT PARAM ./GLOBALS

해당 명령어를 이용하여 OGG 설치 디렉토리의 GLOBALS 파일을 편집하여

저장 할 수 있다

GGSCHEMA OGG -- DB내에 이용할 스키마명

CHECKPOINTTABLE OGG.GGSCHKPT -- DB내에 사용할 체크포인트 테이블명

SYSLOG NONE -- SYS로그 여부

-- SYSLOG 때문에 사용하던 기능이었는데 12cR3 부터는 필요 없어짐으로 있어도 되고 없어도 되는 설정임

== OGG Manager Process 설정 및 기동

#> ./ggsci -- ggsci 툴 이용하여 OGG 연결

GGSCI> EDIT PARAM MGR --

해당 명령어를 이용하여 OGG 설치 디렉토리내 dirprm 디렉토리 밑에 mgr.prm파일을

편집하여 저장 할 수 있다.

-- mgr.prm 파일내용(Source)

PORT 9010

-- mgr.prm 파일내용(Target)

PORT 9010

위와 같이 OGG에서 사용할 수 있는 TCP/IP Port 를 지정한뒤에 파일을 저장한다

GGSCI> START MGR -- Manger Process 기동

GGSCI> info ALL -- 체크

GGSCI> info MGR -- 체크

== 테이블 데이터 동기화를 위한 Supplemental Logging 설정(Source) ================================================================

#> ./ggsci

GGSCI> dblogin userid [OGG Schema]

ex) dblogin userid ogg

passwod : ogg

GGSCI> INFO TRANDATA [owner].[table_name] -- 테이블 Supplemental Logging 설정 여부 확인

ex) INFO TRANDATA APP.TCUSTMER

GGSCI> ADD TRANDATA [owner].[table_name] -- 해당 테이블 Supplemental Logging 설정

ex) ADD TRANDATA APP.TCUSTMER

반드시 PK가 있어야 함 없을시 Warning 에러 발생

GGSCI> INFO TRANDATA [owner].[table_name] -- 테이블 Supplemental Logging 설정 여부 확인

== Checkpoint 테이블 생성(Target) Source에는 필요없음.

#> ./ggsci

GGSCI> dblogin userid [OGG Schema]

ex) dblogin userid ogg

passwod : imsi00

GGSCI> ADD CHECKPOINTTABLE

== Extract Process 생성 및 기동(Source) ====================================================================

OGG에서 가장 중요한 Extrac 즉 Oline Redo/Archive LOG를 읽어서 변경 데이터를

추출하는 Process

#> ./ggsci

GGSCI> EDIT PARAM [EXTRACT_NAME]

ex) EDIT PARAM ext01

해당 명령어를 이용하여 OGG 설치 디렉토리내 dirprm 디렉토리 밑에 ext01.prm파일을

편집하여 저장 할 수 있다.

############################################################################################

-- ext01.prm 파일 내용 작성하여 설정

--# Extract Name

extract ext02

--# DB Information

UserID ogg, Password ogg

TRANLOGOPTIONS INTEGRATEDPARAMS(max_sga_size 2048, parallelism 2, _LOGMINER_READ_BUFFERS 256)

TRANLOGOPTIONS BUFSIZE 10000000

TRANLOGOPTIONS _READAHEADCOUNT 64

_TRAILOUTBUFFERSIZE 1048576

LOGALLSUPCOLS

UPDATERECORDFORMAT COMPACT

--# Management Discard File

DiscardFile ./dirout/ext02.dec, append, megabytes 50

DisCardRollover at 00:01

--# Report File

ReportCount Every 1 Records

ReportRollover at 00:01

--# Trail (Ext)

ExtTrail ./dirdat/et

--# Table List

TABLE GRN.*;

TABLE PRD.*;

############################################################################################

-- Extract Process 등록

위에서 등록한 내용을 토대로 OGG에 Extract 등록

GGSCI> INFO ALL -- 기존 정보 확인

GGSCI> dblogin userid ogg -- ogg 스키마로 DB접속

Password : ogg

GGSCI> ADD EXTRACT [extract name], TRANLOG, BEGIN NOW -- EXT01 이름으로 EXTRACT Process 등록

ex)ADD EXTRACT EXT01, TRANLOG, BEGIN NOW -- EXT01 이름으로 EXTRACT Process 등록

GGSCI> ADD EXTTRAIL ./[extrail 경로], EXTRACT [extract name], MEGEBYTES [file size] -- Trail 파일 경로, EXTRACE 파일명, 파일 사이즈 지정

ex)ADD EXTTRAIL ./dirdat/ex, EXTRACT ext01, MEGEBYTES 50

GGSCI> INFO ALL -- 추가된 EXTRACT Process 확인

GGSCI> INFO [extract name] -- EXTRACT PROCESS 상세 확인

ex) INFO EXT01

-- integrated 모드

GGSCI> ADD EXTRACT TEST_EXT INTEGRATED TRANLOG, BEGIN now

-- Extract Process 삭제

잘못된 Extract Process 등록시 삭제

#> ./ggsci

GGSCI> DELETE [extract name]

ex) DELETE EXT01

-- Extract Process 시작

GGSCI> Start [extract name] -- EXTRACT Process 기동

ex) START EXT01

GGSCI> VEIW REPORT [extract name] -- 기동 상태 및 에러 확인

ex) VEIW REPORT EXT01

GGSCI> INFO [extract name] -- EXTRACT PROCESS 상세 확인

ex) INFO EXT01

-- Extract 디비에 등록

REGISTER EXTRACT TOT_EXT DATABASE

-- intgrated 모드

ADD EXTRACT TOT_EXT INTEGRATED TRANLOG, BEGIN now

== PUMP Process 생성 및 기동(Source) ========================================================================

OGG에서 변경데이터를 전송하는 역할을 하는 PUMP Procss

#> ./ggsci

GGSCI> EDIT PARAM [PUMP_NAME]

ex) EDIT PARAM pmp01

해당 명령어를 이용하여 OGG 설치 디렉토리내 dirprm 디렉토리 밑에 pmp01.prm파일을

편집하여 저장 할 수 있다.

############################################################################################

-- pmp01.prm 파일 내용 작성하여 설정

--# Pump Name

extract pmp05

--# Mode

PassThru

EOFDELAYCSECS 30

FLUSHCSECS 30

--# Target Information

RmtHost 192.168.24.11, MGRPORT 9020

--# Trail (Rmt)

RmtTrail ./dirdat/ek

--# Report File

ReportCount Every 1 Records

--ReportCount EVERY 1 HOURS, RATE

ReportRollover at 00:01

DISCARDFILE ./dirout/psdb01.dsc, APPEND, MEGABYTES 2000

--# Table List

TABLE APP.*;

############################################################################################

-- Pump Process 등록

위에서 등록한 내용을 토대로 OGG에 PUMP 등록

GGSCI> ADD EXTRACT [pump name], exttrailsource [extrail 경로] -- EXT01 이름으로 EXTRACT Process 등록

ex)ADD EXTRACT pmp01, exttrailsource ./dirdat/ex -- EXT01 이름으로 EXTRACT Process 등록

GGSCI> ADD RMTTRAIL ./[extrail 경로], EXTRACT [pump name], MEGEBYTES [file size] -- Trail 파일 경로, EXTRACE 파일명, 파일 사이즈 지정

ex)ADD RMTTRAIL ./dirdat/ex, EXTRACT pmp01, MEGABYTES 50

GGSCI> INFO ALL -- 추가된 EXTRACT Process 확인

GGSCI> INFO [pump name] -- EXTRACT PROCESS 상세 확인

ex) INFO pump01

-- Pump Process 삭제

잘못된 Pump Process 등록시 삭제

#> ./ggsci

GGSCI> DELETE [pump name]

ex) DELETE pump01

-- Pump Process 시작

GGSCI> Start [Pump name] -- EXTRACT Process 기동

ex) START pmp01

GGSCI> VEIW REPORT [Pump name] -- 기동 상태 및 에러 확인

ex) VEIW REPORT pmp01

GGSCI> INFO [Pump name] -- EXTRACT PROCESS 상세 확인

ex) INFO pmp01

== REPLICAT Process 생성 및 기동(TARGET) ===============================================================================================================================================================

Source 시스템의 변경데이터를 Extract/Pump를 받은 변경데이터를 Target DB에 반영하는

REPLICAT Process

#> ./ggsci

GGSCI> EDIT PARAM [REPLICAT_NAME]

ex) EDIT PARAM rep01

해당 명령어를 이용하여 OGG 설치 디렉토리내 dirprm 디렉토리 밑에 rep01.prm파일을

편집하여 저장 할 수 있다.

############################################################################################

-- rep01.prm 파일 내용 작성하여 설정

--# Replicat Name

Replicat rep05

--# DB Information

USERID ogg, Password "ogg!"

--# Management Discard File

DiscardFile ./dirout/rep05.dec, append, megabytes 200

DisCardRollover at 00:01

--# Report File

ReportCount Every 1 Records

--ReportCount EVERY 1 HOURS, RATE

ReportRollover at 00:01

--# Auto Mapping Column

AssumeTargetDefs

--# defgen file

--#SOURCEDEFS /apps/oggdev/ogg123/dirdef/record.def

--# Table List

map APP.*, target ETT.* ;

MAP GRN.TESTFROM, TARGET ODS.TESTTO&

COLMAP

(

USEDEFAULTS,

ODS_LUPD_DT=@DATENOW(),

RGCS_PHYC_DEL_YN=@CASE(@GETENV('GGHEADER', 'OPTYPE'),'DELETE','Y','N')

);

--ALLOWNOOPUPDATES

Updatedeletes

OVERRIDEDUPS

############################################################################################

-- REPILICAT Process 등록

위에서 등록한 내용을 토대로 OGG에 REPLICAT 등록

GGSCI> ADD REPLICAT [REPLICAT_NAME], exttrail [extrail 경로] -- REP01 이름으로 REPLICAT Process 등록

ex)ADD REPLICAT rep01, exttrail ./dirdat/ex -- REP01 이름으로 REPLICAT Process 등록

GGSCI> INFO ALL -- 추가된 REPLICAT Process 확인

GGSCI> INFO [REPLICAT name] -- REPLICAT PROCESS 상세 확인

ex) INFO REP01

-- REPLICAT Process 삭제

잘못된 REPLICAT Process 등록시 삭제

#> ./ggsci

GGSCI> DELETE [REPLICAT name]

ex) DELETE REP01

-- REPLICAT Process 시작

GGSCI> Start [REPLICAT name] -- EXTRACT Process 기동

ex) START pmp01

GGSCI> VEIW REPORT [REPLICAT name] -- 기동 상태 및 에러 확인

ex) VEIW REPORT pmp01

GGSCI> INFO [REPLICAT name] -- EXTRACT PROCESS 상세 확인

ex) INFO pmp01

-- REPLICAT 반영 건수 확인

GGSCI> STATS [REPLICAT_NAME]

===========================================================================

Colmap 사용하기

-- TARGET EXT05 내용

--# Table List

TABLE APP.TCUSTMER;

TABLE APP.TCUSTORD,

token(tk_rowid = @getenv('ORARECORD','ROWID'),

tk_commit_ts=@getenv('GGHEADER','COMMITTIMESTAMP'));

-- REPLICAT REP05 내용

--# Table List

MAP APP.TCUSTMER, TARGET ETT.TCUSTMER;

MAP APP.TCUSTORD, TARGET ETT.TCUSTORD &

COLMAP

(USEDEFAULTS, row_id=@token('tk_rowid'), commit_ts=@token('tk_commit_ts')),

keycols(row_id);

impdp test/test schemas=ODS directory=EXPDP1 logfile=ODS_TABLE_IMPDP.log job_name=ODS_TABLE_IMPDP content=metadata_only parallel=8 NETWORK_LINK=testdb

평소에는 [SCHMA].[*]로 전부 선택해서 사용 할 수있으나

만약 콜맵을 이요하여 변경내용만 추가해서 저장하거나 하는경우 반드시 명시해줘야함.

테스트 문서에는 " 더블쿼테이션으로 되어있으나 실제 테스트에는 '' 싱글 쿼테이션을 사용해야함

-- trail 파일 자동삭제 설정

mgr.prm파일에 작성할것

PURGEOLDEXTRACTS ./dirdat/*, usecheckpoints, minkeepdays 7

*/

-- Replicat 프로세스 초기화

ALTER REPLICAT [프로세스명] BEGIN NOW

-- 스키마 단위로 supplemental log 설정

GGSCI> ADD SCHEMATRANDATA [스키마명]

-- replicat 초기화

ALTER recplicat TEST_R01, BEGIN NOW

-- SCN 이용한 REPLICAT 프로세스 시작

start recplicat TEST_R01, aftercsn

start replicat [PROCESS NAME] aftercsn [SCN number] -- 반드시 aftercsn 옵션을 써서 수행해야 함.

-- datenow TIMESTAMP 추가

ODS_LUPD_TS=@DATE('YYYY-MM-DD HH:MI:SS.FFFFFF', 'JTS', @GETENV('JULIANTIMESTAMP')), &

ODS_LUPD_TS=@DATENOW(), &

-- 현재 SCN 확인 및 aftercsn 적용 방법

-- 현재 SCN 확인(SOURCE)

SELECT DBMS_FLASHBACK.GET_SYSTEM_CHANGE_NUMBER CURRENT_SCN

FROM DUAL;

CURRENT_SCN

------------

6041742631

-- timestamp 로 scn 확인

select timestamp_to_scn(TO_TIMESTAMP('20180411 14:00:29.988800','YYYYMMDD HH24:MI:SS.FF6'))

from dual

-- scn 으로 timestamp 확인

select scn_to_timestamp(6041841798)

from dual

-- 위에서 확인한 SCN 시점 데이터만 export (source)

expdp test/test dumpfile=test.dmp LOGFILE test.LOG schemas=test_user flashback_scn=6041742631

-- 확인한 SCN 시점 데이터만 import (target)

impdp test/test dumpfile=test.dmp LOGFILE test_imp.LOG schemas=test_user

-- 해당 시점이후 시점에서 동기화포인트 찾으면서 프로세스 스타트 (target)

START replicat test_rep, aftercsn 6041742631

[출처] OGG 프로세스 설정|작성자 경진

'ORACLE > 이중화(HA)' 카테고리의 다른 글

[OGG] COLMATCH  (0) 2024.01.20
[OGG] FILTER 데이터 선택  (0) 2024.01.20
OGG 12c 설치 구성  (0) 2019.10.18
integrated extract 구성  (0) 2019.10.18
parallel replicat  (0) 2019.10.18

SQL 문장의 실행 원리

1) 사용자 문장 실행 시 User Process에서 Server Process로 실행한 SQL문 전달
2) User Process로부터 문장을 받은 Server Process가 해당 문장의 세부적 체크 진행
· Syntax Check : SQL문이 적절한 문법을 사용했는지 검사
· Semantic Check : SQL문에 포함된 오브젝트들이 실제로 존재하는지 검사
3) Parse과정 후 Shared Pool의 Library Cache에서 공유되어있는 실행계획이 있는지 체크
4) 실행계획이 있을 경우 Execution 진행 (Soft Parsing)
5) 실행계획이 없을 경우 Optimizer를 통해 Data dictionary 등을 참조하여 실행계획을 새로 생성 후 Library Cache에 저장 (Hard Parsing)

 

 

SELECT 문장의 실행 원리


 Parse(구문분석) -> Bind(바인드) -> Execute(실행) -> Fetch(인출)


1) Parse(구문분석)
- User Process로부터 전달받은 SQL문장을 Server프로세스가 SQL문을 수행하기 위해 Parse Tree생성
- Parse Tree를 만드는 과정에서 Syntax Check, Semantic Check 진행
- Data Dictionary 조회를 통해 문법이 맞는지, 해당 테이블이 있는지 확인
- 자주 사용되는 Data Dictionary는 Shared Pool의 Dictionary Cache에 캐싱해 두어 성능을 높힘
- 오류가 없을 경우 SQL문장을 Hash 함수로 Hash Value로 변경한 후 Shared Pool의 Library cache에서
   Hash Value와 비교하여 동일한 값이 있는지 확인 ( 커서 공유 혹은 Soft Parsing이라고 함 )
· Cursor : 메모리에 데이터를 저장하기 위해 만든 임시 저장 공간 ( 공유 커서, 세션 커서, 어플리케이션 커서 )
· Library Cache 안에 있는 커서는 공유 커서를 의미
· 공유 커서
- 한번 수행된 SQL 문장의 실행계획과 관련 정보를 보관
- 재활용을 통해 Hard Parse의 부담을 줄여 SQL 문장의 수행속도를 빠르게 함.

SELECT /* cursor_test */ empno, ename
FROM emp
WHERE empno = 7521 ;

-- sql의 실행횟수 및 커서공유 현황
SELECT sql_id
, sql_text
, parse_calls -- parse 시도 횟수
, loads -- hard parsing을 거쳐 loading된 횟수
, executions -- 실행 횟수
FROM v$sql
WHERE sql_text LIKE '%cursor_test%'
AND sql_text NOT LIKE '%V$SQL%';

 --> parse 시도를 2번 했으며,
       메모리에 이미 등록된 SQL이므로 hard parsing은 1번 했고,
       실행 횟수는 2번 했음   

옵티마이저(Optimizer) : SQL의 실행 계획을 생성해주는 네비게이션 역할
옵티마이저 모드 : RBO, CBO
1) RBO : Rule Based Optimizer(11g부터 사용 불가)
2) CBO : Cost Based Optimizer
- 데이터 딕셔너리 정보를 이용하여 판단
- 옵티마이저가 참조하는 데이터 딕셔너리 중 대부분은 Static Dictionary, 즉, 항상 최신 정보를 가지고 있음
- 데이터 딕셔너리를 관리해야 함 ( 통계정보 생성 및 관리 ) 
 

2) BIND(바인드)
- SQL문이 정확히 일치해야 Soft Parsing이 가능해짐
- 변수 값이 달라지는 동일 쿼리에 대해 모두 다른 SQL로 인식 -> Hard Parsing 유발
- 변수 값이 달라지는 부분에 바인드 변수 처리
ex) select ............
from emp
where empno = :emp_num:
- 바인드 처리하면 모든 SQL이 동일 실행계획을 가짐

   

3) Execute(실행)
- Parse와 Bind 단계 후 해당 데이터를 가져오기 위해 데이터가 저장되어있는 데이터 블록을 찾아 DB Buffer Cache에       
복사하는 과정
- 사용자가 찾는 모든 데이터는 SGA의 DB Buffer Cache에 있어야 함
- 사용자가 찾거나 변경하는 모든 작업은 DB Buffer Cache에서 작업이 수행됨
- 서버프로세스는 해당블록을 찾기 위해 DB Buffer Cache를 확인 후 없는 경우 데이터 파일로부터 복사해옴
- 데이터 파일과 DB Buffer Cache의 데이터 이동은 BLOCK 단위
- DB_BLOCK_SIZE 크기만큼 데이터를 이동(default : 8K)

  

4) Fetch(인출)
- Execute 단계까지 수행하면, 원하는 데이터가 들어있는 블록이 DB Buffer Cache에 올라오게 됨
- 데이터의 I/O 최소 단위가 Block이므로, DB Buffer Cache에 원하는 데이터만 있는 것이 아닌 다른 데이터도 존재
- 사용자가 요청한 데이터만 골라내는 과정을 Fetch라고 함
- 정렬이 필요하거나 추가 작업을 요구하는 경우 Fetch과정에서 Sort를 하여 데이터를 보내주며, 정렬은 PGA영역(Program Global Area)에서 수행
 
 
  

DML 문장의 실행 원리


- 모든 의의 수행원리는 동일하다.
- DML의 수행단계는 Fetch과정만 없고 나머지는 동일

Parse(구문분석) -> Bind(바인드) -> Execute(실행)

1) 서버 프로세스는 Parse과정 수행
   - Library Cache에 실행계획이 있는지 확인
   - 있으면 Soft Parse, 없으면 Hard Parse 수행
2) 실행계획을 받은 서버프로세스는 Library Cache에 Plan정보 등록한 후 Execute 수행
   - DB Buffer Cache에 update 하려는 데이터가 있는지 확인
   - 없을 경우 데이터파일의 해당 블록을 DB Buffer Cache에 복사
3) Execute 단계에서 원하는 데이터가 들어있는 DB Buffer Cache를 가져온 후 Server 프로세스는
   데이터 변경 내역을 Redo Log Buffer에 먼저 기록
4) 기록 후 Undo Segment에 원본 이미지를 기록한 후 DB Buffer Cache의 내용을 변경

 

□ [정리] 데이터 변경이 일어날 경우 순서


Redo Log Buffer에 기록 -> Undo Segment에 기록 -> DB Buffer Cache의 실제 데이터 변경
1. Parsing
2. Data Buffuer Cache 조회
3. datafile에서 DBC로 불러옴
4. 바꾼 데이터를 Redo Buffer에 올려둠
5. undo segment에 기록
6. Data Buffer Cache 데이터 변경
 

* Undo segment에 저장 하는 이유
실제 데이터 파일이므로 무거울 수 있지만
1. 서버가 갑작스럽게 내려갔을 때(memory flash) Before 이미지를 남기기 위해
2. 작업 외 인원이 조회하기 위해 ( 읽기의 일관성 )
  

Oracle Background Process

Oracle Process 종류
1) User Process : 사용자가 작성한 SQL 문장을 Server 프로세스로 전달하고 결과를 가져오는 프로세스
2) Server Process : User Process가 전해 준 SQL 문장을 실제 실행하는 프로세스
3) Background Process : Oracle Server가 시작되면 자동으로 시작되어 운영과 유지를 담당하는 프로세스
· User Process와 Server Process는 사용자가 접속하면 생성되고 접속을 종료하면 해제
· Background Process는 Oracle Server가 시작되면 함께 시작되고 종료되면 함께 종료
  
Database Writer (DBWR) [ Database Buffer Cache --> Data File ]
Database Buffer Cache에서 변경완료 후 저장 되어야 하는 블록(Dirty Block)을 데이터 파일로 저장하는 역할
□ DBWR 동작 시점
1. Checkpoint 신호가 발생 했을 때
2. Dirty Buffer 가 임계 값을 지났을 때
3. Time out 이 발생했을 때s
4. RAC Ping 이 발생했을 때 --> 서로 다른 instance에서 ins1에서 데이터 변경 했을 때 ins2가 해당 데이터 조회 했을 시
5. Tablespace 가 Read only 상태로 변경될 때
6. Tablespace 가 offline 될 때
7. Tablespace 가 begin backup 상태가 될 때
8. Drop table이나 Truncate table 될 때

1. redo #1 이 가득차면 archive에 저장
2. log change를 이용해 #2로 넘어감

 

Database Writer (DBWR) 개수 수정하기
select * from v$parameter
where name ='db_writer_processes';

DBW0~DBW9 (최대 10개)

 

Log Writer (LGWR)
- Server Process가 변경내역을 Redo Log Buffer에 기록하게 된다
- LGWR은 Redo Log Buffer에 있는 내용을 디스크의 Redo Log File로 저장한다.
- 만약 Commit 요청이 들어왔는데 Redo Log File이 없는 경우, Alert Log 파일에 해당 내용을 기록해두고
  LGWR은 다음 Commit 요청을 수행하지 않고 대기 ==> DB 전체 중단
□ LGWR 동작 시점
1. Commit 이 발생 했을 때
2. 1/3이 찼을 때
3. 변경량이 1M가 되었을 때
4. 3초 마다
5. DBWR이 내려 쓰기 전에 -- 무조건 LogWriter가 먼저 실행

PMON (Process Monitor)
- Server Process 생성 및 관리
- Server Process fail일 경우
□ commit 된 데이터 저장
□ commit 안된 데이터 rollback
□ Lock Release
     

SMON (System Monitor) - Instance Recovery
- 인스턴스가 비정상 종료 되었을 경우, 인스턴스를 시작할 때 Clean Up하는 역할 (Instance Recovery)
- Instance Recovery 과정에서 누락된 Transaction을 Recovery하는 역할
- 비정상 종료된 Transaction Temporary segment를 Clean up하는 역할
1. 사용자 A가 홍길동을 일지매로 변경
2. 사용자 A가 commit 수행해서 변경내용이 Redo log file에 기록 됨
3. 사용자 B가 이순신을 강감찬으로 변경
4. 사용자 B는 commit을 안 했지만 Redo log Buffer의 변경내용이 1M가 되어 Redo log file에 기록이 됨.
5. Shutdown abort;
---> commit 된 데이터만 DataFile로 이동 됨. ( 다 읽은 후 롤백 처리까지 )
Roll Forward --> DB Open --> Roll backward
  

CKPT(Checkpoint Process)
- CKPT Process는 DBWR에게 Checkpoint 신호를 전달해 주며, Control file과 Data file Header에 해당 Check Point 정보     를 기록하는 역할 수행.
- Checkpoint 정보에는 Checkpoint 위치와 SCN(데이터 변경 시점), 해당 내용을 담고 있는 Redo log 내용의 위치 값         을 담고 있다. 

 


 Oracle Startup & Shutdown

Shutdown --(Pfile/Spfile)--> No mount --(Control file)-->mount --(Data file/Redo log file)--> OPEN
SQL > startup; -- shutdown 상태부터 open 상태까지 진행
SQL > startup nomount; -- shutdown 상태부터 nomount 단계 까지 진행 이후 진행은 alter명령어로 순서
SQL > startup mount; -- shutdown 상태부터 mount 단계까지 진행
SQL > alter database mount; -- nomount 상태부터 mount단계까지 진행
SQL > shutdown;
SQL > shutdown immediate;
SQL > shutdown abort;

1) NOMOUNT
- 서버 프로세스가 Parameter File을 찾아 읽음
- 여러 인스턴스가 존재하는 경우 해당 SID를 읽어 들여 Parameter File을 찾음
- Parameter File에는 정적 파라미터 파일인 Pfile과 동적 Parameter 파일인 Spfile이 있음
- 정적 파라미터 파일은 관리자가 수동으로 변경
- 동적 파라미터 파일은 서버 프로세스가 자동으로 변경
- Parameter파일을 읽고, 그 안에 저장되어 있는 파라미터 값을 참고하여 인스턴스 구성
- 인스턴스는 SGA와 Background Process들로 구성되어 있으므로 MOUNT단계에서 RAM에 인스턴스가 생성되어
  작업할 수 있는 메모리 공간이 확보됨
- Alert Log 파일을 열어서 로깅을 시작
- Alert Log는 인스턴스가 시작되어 운영되고 종료될 때까지 중요한 내용을 모두 저장하는 파일
*profile에 등록하면 편하게 alert log를 확인할 수 있다.
alias alert='tail -f /oracle11/oracle/diag/rdbms/testdb/testdb/trace/alert_testdb.log'
 
2) MOUNT
- NOMOUNT 단계를 마치면, Control file을 읽고 MOUNT 단계 진행
- Control File의 위치정보는 Parameter File에 기록되어 있음
- MOUNT단계에서는 Database의 이상유무 확인 후 open 진행
- Instance Crash로 판단되면 OPEN 단계로 가기 전에 SMON이 Instance Recovery를 수행
- SMON이 Recovery를 수행할 경우 관련 내용을 Redo Log File에서 찾게 되는데, 복구하려는 내용이
   Redo Log File에 없을 경우에는 SMON에 의한 Recovery 실행 -> Media Recovery 수행(Archive 필요)
- instance crash는 control file와 data file파일의 scn(checkpoint) 정보가 서로 다를 경우 crash를 판단하게 되고,
  서로 다른 정보를 맞추는 과정이 instance recovery이다.

Oracle Parameter File

pfile - $ORACLE_HOME/dbs/initSID.ora
- Text file
- OS 편집기로 내용수정 가능
- 내용 변경 시 재 시작 해야 적용됨
- Dynamic으로 파라미터 적용 불가능
spfile - $ORACLE_HOME/dbs/spfileSID.ora
- Binary file
- OS 편집기로 내용수정 절대 불가능
- 내용 변경 시 재 시작 하지 않아도 적용됨(일부 제외)
- Dynamic으로 파라미터 적용 가능 (alter system set)
*** 둘 다 존재 시 SPFILE만 사용함 ***

*** 둘 다 존재 시 SPFILE만 사용함 ***

1) sqlplus / as sysdba 접속 후
create pfile from spfile;
 생성 후 spfile 지우고 다시 재부팅
2) pfile로 켜진지 확인 후 동적 변경이 가능한지 확인
· select * from v$parameter
where name like '%large_pool_size%'; -- 명령으론 수정 불가능 직접 init파일을 수정해야함!
· alter system set large_pool_size=16m; -- 오류는 안나나 실제적으론 수정이 안 되어있음 

spfile 환경일 경우 파라미터 변경 옵션
- SQL > ALTER SYSTEM SET DB_CACHE_SIZE=30m Scope=Memory;
- Scope 옵션
· MEMORY : Spfile 내용은 변경하지 말고 현재 작동 중인 인스턴스에만 적용
· SPFILE : 현재 운영중인 인스턴스에 적용하지 말고, SPFILE 내용만 변경 (DB 재기동시 변경되도록 함)
· BOTH : 두 가지 모두에 적용 (Scope 옵션을 사용하지 않을 경우 기본 모드)  

* SGA 남은 사이즈
SELECT CURRENT_SIZE/1024/1024 AS "FREE_MEMORY(MB)"
FROM V$SGA_DYNAMIC_FREE_MEMORY; 

select * from v$parameter
where name like '%sga_max_size%';
alter system set sga_max_size=400m scope=spfile;

spfile환경에서) sga_max_size는 동적으로 변경 불가능하므로 both, memory 설정 불가능

   
Oracle shutdown
1) NORMAL ( 사용할 일 절대 없음 )
- shutdown normal 또는 shutdown 명령어로 종료
- 명령 전에 접속되어 있던 사용자가 있을 경우, 사용자가 있을 경우, 사용자들이 모두 스스로 접속을 종료할 때까지 기다렸다가 종료
- 사용자가 접속을 종료하지 않으면, Instance는 종료되지 않고 무한 대기
2) TRANSACTIONAL
- shutdown transactional 명령어로 종료
- 사용자가 스스로 접속을 종료할 때 가지 기다리지 않고 강제로 접속을 중단시킨 후 Instance를 종료
- 접속을 강제로 중단 시키는 시점은 사용자가 수행중인 Transaction이 끝나는 시점
- DML 작업을 수행 중일 경우에는 해당 트랜잭션을 종료하는 명령어(DDL,DCL,TCL) 수행 시 인스턴스를 종료
- 사용자가 트랜잭션을 종료하지 않게 되면 Instance를 종료할 수 없음(사용자가 COMMIT,ROLLBACK 명령어를              수행해야 함)
3) IMMEDIATE
- shutdown immediate 명령어로 종료
- 사용자의 행동에 상관없이 즉시 접속을 강제로 종료
- 접속이 종료되는 시점까지 해당 사용자가 수행한 작업중에 Commit이 완료된 데이터를 DB Buffer cache에서 찾아
   데이터 파일로 저장해주고, 완료되지 않은 작업은 Rollback 시킨 후 Instance 종료
4) ABORT
- shutdown abort 명령어로 종료
- 사용자의 행동에 상관없이 즉시 접속을 강제로 종료
- immediate와 차이점은, 사용자가 수행한 작업을 저장하지도 Rollback하지도 않고 즉시 Instance 종료
- 다시 Startup될 때 SMON이 Instance Recovery를 수행해서 복구함
---> IMMEDIATE는 CheckPoint를 발생시키고 ABORT는 CheckPoint를 발생시키지 않음

 

방법 : 오라클 19c 하드파싱 쿼리 플랜 변경 의심시 조치방법

운영중 특정 쿼리로 인해 db의 부하가 심해지고 cpu 사용률이 올라갈때

아래 쿼리로 쿼리의 실행계획이 변경되었는지 확인가능함

1. 차일드 커서 체크(실행계획 변경여부 확인 child_number 0이상이면 실행계획이 변경되었을 수 있음)

 
SQL>
select sql_id,
  PARSING_SCHEMA_NAME "USER",
  PLAN_HASH_VALUE,
  OPTIMIZER_MODE,
  child_number,
  parse_calls,
  USERS_OPENING,
  USERS_EXECUTING,
  loads,
  executions,
  invalidations,
  decode(sign(invalidations), 1, (loads-invalidations), 0) reloads,
  FIRST_LOAD_TIME,
  LAST_LOAD_TIME
from v$sql
where sql_id='sql_id'
and  sql_text not like '%v$sql%';

 

2. 동일 sql_id의 다른 plan_hash_value 확인

 
SQL>
select distinct sql_id, sql_plan_hash_value from dba_hist_active_sess_history
where sql_id = 'sql_id';

 

3. sql_id 실행계획 plan_hash_value 별 성능 확인

 
SQL> 
select a.instance_number inst_id, a.snap_id,a.plan_hash_value, to_char(begin_interval_time,'dd-mon-yy hh24:mi') btime, 
abs(extract(minute from (end_interval_time-begin_interval_time)) + extract(hour from (end_interval_time-begin_interval_time))*60 + extract(day from (end_interval_time-begin_interval_time))*24*60) minutes,
executions_delta executions, round(ELAPSED_TIME_delta/1000000/greatest(executions_delta,1),4"avg duration (sec)" 
from dba_hist_sqlstat a, dba_hist_snapshot b
where sql_id='sql_id' and a.snap_id=b.snap_id
and a.instance_number=b.instance_number
order by snap_id desc, a.instance_number;
cs

이전에 수행된적이 있거나 바인드 변수를 사용한 쿼리의 경우 위 쿼리에  sql_id를 넣어조회할 경우

플랜이 변경되었는지 여부를 알수 있고 spm을 이용해서 변경전 good 플랜으로 다시 고정시켜줄 수 있음하지만

이전에 수행된적이 없거나 바인드 변수를 사용하지 않은 쿼리의 경우 하드 파싱이 일어나고위 쿼리를 이용해 조회해도

이전 good 플랜을 찾아볼 수 없음

이 경우 v$sql에서 sql_text 로 like 조건으로 비슷한 쿼리가 수행된 적이 있는지 확인후

해당 플랜으로 수행되도록 힌트를 넣어 튜닝해줄 수 있음

비슷한 쿼리가 수행되었는지 확인

 
SQL> 
select sql_id, plan_hash_value, last_load_time, sql_text
from v$sql
where sql_text like '%select * from ttest1 where col1>=3%';
cs

비슷한 쿼리를 찾았다면 쿼리의 full_text를 복사 한뒤

해당 쿼리에 gather_plan_statistics 힌트를 넣어 실행, 또는 예상실행계획 확인(일반 xplan 또는 autotrace)

 
SQL> select /*+ gather_plan_statistics test1 */ 
*
from ttest1
where col1>=3
;

v$sql 에서 해당 sql의 sql_id 확인

 
SQL> 
select sql_id, plan_hash_value, last_load_time, sql_text
from v$sql
where sql_text like '%select /*+ gather_plan_statistics test1 */%';

xplan display_cursor로 커서에 올라간 실제 플랜 조회

 
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('62qrgv4fh00xt'NULL'ADVANCED ALLSTATS LAST'));
 
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID    62qrgv4fh00xt, child number 0
-------------------------------------
select /*+ gather_plan_statistics test1 */ * from ttest1 where col1>=3
 
Plan hash value: 4041766012
 
-------------------------------------------------------------------------------------------------------------------------
| Id  | Operation     | Name       | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time    | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT |          |      1 |    |    |     2 (100)|        |      1 |00:00:00.01 |       2 |
|*  1 |  INDEX RANGE SCAN| TTEST1_IX1 |      1 |      1 |    13 |     2   (0)| 00:00:01 |      1 |00:00:00.01 |       2 |
-------------------------------------------------------------------------------------------------------------------------
 
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
 
   1 - SEL$1 / TTEST1@SEL$1
 
Outline Data
-------------
 
  /*+
      BEGIN_OUTLINE_DATA
      IGNORE_OPTIM_EMBEDDED_HINTS
      OPTIMIZER_FEATURES_ENABLE('19.1.0')
      DB_VERSION('19.1.0')
      ALL_ROWS
      OUTLINE_LEAF(@"SEL$1")
      INDEX(@"SEL$1" "TTEST1"@"SEL$1" ("TTEST1"."COL1"))
      END_OUTLINE_DATA
  */
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - access("COL1">=3)
 
Column Projection Information (identified by operation id):
-----------------------------------------------------------
 
   1 - "COL1"[NUMBER,22]
 
Hint Report (identified by operation id / Query Block Name / Object Alias):
Total hints for statement: 1 (E - Syntax error (1))
---------------------------------------------------------------------------
 
   1 -    SEL$1
     E -  test1
 
Note
-----
   - dynamic statistics used: dynamic sampling (level=2)
 
Query Block Registry:
---------------------
 
  <q o="2" f="y"><n><![CDATA[SEL$1]]></n><f><h><t><![CDATA[TTEST1]]></t><s><![CDATA[SEL$1]]></s></h></f></q>
 
 
 
59 rows selected.

성능이 좋았던 쿼리가 ttest1_ix1 index를 사용하고 있음을 확인

해당 쿼리 실행계획 확인 후 느려진 쿼리에 힌트 적용

 
SQL> select /*+ index(ttest1 ttest1_ix1) */
 * 
from ttest1 
where col1>=3;

 

출처 : https://positivemh.tistory.com/849

 

오라클 19c 하드파싱 쿼리 플랜 변경 의심시 조치방법

OS환경 : Oracle Linux 7.6 (64bit) DB 환경 : Oracle Database 19.10.0.0 방법 : 오라클 19c 하드파싱 쿼리 플랜 변경 의심시 조치방법 운영중 특정 쿼리로 인해 db의 부하가 심해지고 cpu 사용률이 올라갈때 아래

positivemh.tistory.com

 

명령행 "vmstat 3 5" 은 3초 간격으로 모니터링 정보를 5개 출력 하라는 것을 의미한다.
vmstat명령어 결과 중 첫 번째 라인은 부팅한 후부터 각 통계치에 대한 평균값을 보여주므로 무시하고 두 번째 줄부터 통계를 보면 된다.
각 필드가 나타내는 의미는 다음과 같다.

proc 항목
     r: 현재 실행중인 프로세스의 수
     b: 인터럽트가 불가능한 sleep 상태에 있는 프로세스의 수 (I/O 처리를 하는 동안 블럭 처리된 프로세스)
     w: 강제로 스왑아웃된 프로세스

memory 항목
     swpd: 사용하고 있는 swap 메모리 양
     free: 사용가능한 메모리 양
     buff: 버퍼로 사용되고 있는 메모리 양
     cache: 캐시로 사용되고 있는 메모리 양

swap 항목
     si : swap in
     so: swap out

io 항목
     bi: 초당 블럭 디바이스로 보내는 블럭 수
     bo: 초당 블럭 디바이스로부터 받은 블럭 수

system 항목
     in: 초당 인터럽트 되는 양
     cs: 초당 context switch되는 양

cpu 항목
     us: 사용자의 CPU 사용 시간 비율
     sy: 시스템의 CPU 사용 시간 비율
     id: idle

스왑아웃이 지속적으로 발생한다면 메모리가 부족한 것이다. w필드의 값이 증가하면 메모리가 부족하다는 의미이므로 메모리를 늘려야한다. so필드(swap out)는 0에 가까워야 한다. 평소에 swpd필드의 값이 높다고 해도 free 메모리에 여유가 있다면 메모리가 부족한 것이 아니다.

sy필드의 값이 지나치게 높으면 디스크 I/O에 문제가 있을 가능성이 크다.
그리고 시스템 전체의 부하가 높은데 id필드의 값이 일반적으로 10%를 넘는다면 I/O나 메모리에 문제가 있을 가능성이 있다. I/O에 문제점이 있다는 것을 발견하면 iostat 등의 명령어를 추가로 사용하여 세부사항을 분석할 수 있다.

id필드의 값이 항상 0이라면 CPU를 100% 사용하고 있다는 것을 의미한다. 그러나 항상 100%로 사용하고 있다면 어떤 작업이 계속 축적되고 있으며 CPU가 과부하를 가진다는 것을 의미한다. 이 때는 top, ps, sar등의 명령어를 사용하여 CPU를 계속 사용하고 있는 프로세스를 찾아 적절하게 대응해야 한다.

출처:[리눅스] vmstat 명령어

'몰랑몰랑 IT' 카테고리의 다른 글

마이크로서비스 아키텍처(MSA)  (0) 2024.01.11
심심할때 ChatGPT 랑 놀아요  (0) 2024.01.07
DevOps? SRE?  (1) 2024.01.03

+ Recent posts