1. 컬럼 타입은 text 가 아닌 json 컬럼 타입을 사용한다. 

    text 타입을 사용할 경우 json 문법을 안지켜서 나중에 문제가 되는 경우가 발생한다. 

 

2. json 내부의 속성중 업데이트가 자주 발생하는 경우 컴럼 전체 update를 하지 않고 

   json 함수를 사용하여 업데이트할 속성만 업데이트한다. 

   json 전체를 업데이트할 경우 binlog 증가 이슈가 발생한다. 

 

3. json 내부의 속성값을 조회조건으로 사용하는 경우 가상컬럼을 만들어서(인덱스 생성 필수) 사용한다. 

   json 을 그냥 검색할 경우 full scan 이슈가 발생한다. 

'MySQL' 카테고리의 다른 글

Index Hint  (0) 2022.09.22
Metadata Lock 조회  (0) 2022.09.22
MySQL JSON, Generated Columns  (0) 2022.09.15
MySQL Performance Best Practice  (0) 2021.12.26
xtrabackup 8.0 (2)  (0) 2021.07.06

 

MySQL 5.7.8 부터 JSON 을 지원한다. 

JSON 문서 내의 값에서 Generated Column을 생성한 다음 해당 컬럼을 인덱싱하면 실제로 JSON 필드를 인덱싱할 수 있다. 

 

JSON 테스트 데이터 

{
    "id": 1,  
    "name": "Sally",  
    "games_played":{    
       "Battlefield": {
          "weapon": "sniper rifle",
          "rank": "Sergeant V",
          "level": 20
        },                                                                                                                          
       "Crazy Tennis": {
          "won": 4,
          "lost": 1
        },  
       "Puzzler": {
          "time": 7
        }
     }
 }

테스트 테이블 생성

CREATE TABLE `players` (  
   `id` INT UNSIGNED NOT NULL,
   `player_and_games` JSON NOT NULL,
   `names_virtual` VARCHAR(20) GENERATED ALWAYS AS (`player_and_games` ->> '$.name') NOT NULL, 
   PRIMARY KEY (`id`)
);

위 DDL문에서 아래 부분이 JSON 타입 컬럼 player_and_game 에서 name 속성 값을 참조하는 Generated Column (names_virtual) 을 지정한 부분이다. 

`names_virtual` VARCHAR(20) GENERATED ALWAYS AS (`player_and_games` ->> '$.name')

Generated Column 에 지정할수 있는 제약조건은 아래와 같다. 

[VIRTUAL|STORED] [UNIQUE [KEY]] [[NOT] NULL] [[PRIMARY] KEY]

VIRTUAL, STORED는 컬럼의 값이 실제로 저장되는지 여부를 나타낸다.

- VIRTUAL : 기본값, 컬럼의 값 저장안함, 저장 공간없음, 행을 읽을 때마다 계산해서 처리한다.

                    인덱스를 생성하면 값이 인덱스 자체에 저장된다.

- STORED : 데이터가 테이블에 저장(입력,수정)될때 컬럼의 값도 저장된다. 

                    인덱스는 일반적인 인덱스와 동일하게 처리된다. 

 

아래에 테스트 소스 깃헙에 있는 mysql-json.sql 파일에 있는 insert 문을 사용한다. 

INSERT INTO `players` (`id`, `player_and_games`) VALUES (1, '{  
    "id": 1,  
    "name": "Sally",
    "games_played":{    
       "Battlefield": {
          "weapon": "sniper rifle",
          "rank": "Sergeant V",
          "level": 20
        },                                                                                                                          
       "Crazy Tennis": {
          "won": 4,
          "lost": 1
        },  
       "Puzzler": {
          "time": 7
        }
      }
   }'
);
....

입력된 데이터 확인, names_virtual 컬럼에서 JSON 문서의 name 속성값 확인됨 

mysql> select * from players;
+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
| id | player_and_games                                                                                                                                                                                           | names_virtual |
+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
|  1 | {"id": 1, "name": "Sally", "games_played": {"Puzzler": {"time": 7}, "Battlefield": {"rank": "Sergeant V", "level": 20, "weapon": "sniper rifle"}, "Crazy Tennis": {"won": 4, "lost": 1}}}                  | Sally         |
|  2 | {"id": 2, "name": "Thom", "games_played": {"Puzzler": {"time": 25}, "Battlefield": {"rank": "Major General VIII", "level": 127, "weapon": "carbine"}, "Crazy Tennis": {"won": 10, "lost": 30}}}            | Thom          |
|  3 | {"id": 3, "name": "Ali", "games_played": {"Puzzler": {"time": 12}, "Battlefield": {"rank": "First Sergeant II", "level": 37, "weapon": "machine gun"}, "Crazy Tennis": {"won": 30, "lost": 21}}}           | Ali           |
|  4 | {"id": 4, "name": "Alfred", "games_played": {"Puzzler": {"time": 10}, "Battlefield": {"rank": "Chief Warrant Officer Five III", "level": 73, "weapon": "pistol"}, "Crazy Tennis": {"won": 47, "lost": 2}}} | Alfred        |
|  5 | {"id": 5, "name": "Phil", "games_played": {"Puzzler": {"time": 7}, "Battlefield": {"rank": "Lt. Colonel III", "level": 98, "weapon": "assault rifle"}, "Crazy Tennis": {"won": 130, "lost": 75}}}          | Phil          |
|  6 | {"id": 6, "name": "Henry", "games_played": {"Puzzler": {"time": 17}, "Battlefield": {"rank": "Captain II", "level": 87, "weapon": "assault rifle"}, "Crazy Tennis": {"won": 68, "lost": 149}}}             | Henry         |
+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
6 rows in set (0.00 sec)

players 테이블 컬럼 정보 확인 

mysql> show columns from players;
+------------------+--------------+------+-----+---------+-------------------+
| Field            | Type         | Null | Key | Default | Extra             |
+------------------+--------------+------+-----+---------+-------------------+
| id               | int unsigned | NO   | PRI | NULL    |                   |
| player_and_games | json         | NO   |     | NULL    |                   |
| names_virtual    | varchar(20)  | NO   |     | NULL    | VIRTUAL GENERATED |
+------------------+--------------+------+-----+---------+-------------------+

Extra 에 VIRTUAL GENERATED 으로 Generated Column 이  VIRTUAL 인것을 보여준다. 

STORED 로 지정된 컬럼의 경우 STORED GENERATED 로 표시된다. 

 

Battlefield 레벨, 승리한 테니스 게임, 패배한 테니스 게임 및 퍼즐 게임 시간 컬럼을 추가한다.

ALTER TABLE `players` ADD COLUMN `battlefield_level_virtual` INT GENERATED ALWAYS AS (`player_and_games` ->> '$.games_played.Battlefield.level') NOT NULL AFTER `names_virtual`;  
ALTER TABLE `players` ADD COLUMN `tennis_won_virtual` INT GENERATED ALWAYS AS (`player_and_games` ->> '$.games_played."Crazy Tennis".won') NOT NULL AFTER `battlefield_level_virtual`;  
ALTER TABLE `players` ADD COLUMN `tennis_lost_virtual` INT GENERATED ALWAYS AS (`player_and_games` ->> '$.games_played."Crazy Tennis".lost') NOT NULL AFTER `tennis_won_virtual`;  
ALTER TABLE `players` ADD COLUMN `times_virtual` INT GENERATED ALWAYS AS (`player_and_games` ->> '$.games_played.Puzzler.time') NOT NULL AFTER `tennis_lost_virtual`;

mysql> show columns from players;
+---------------------------+--------------+------+-----+---------+-------------------+
| Field                     | Type         | Null | Key | Default | Extra             |
+---------------------------+--------------+------+-----+---------+-------------------+
| id                        | int unsigned | NO   | PRI | NULL    |                   |
| player_and_games          | json         | NO   |     | NULL    |                   |
| names_virtual             | varchar(20)  | NO   |     | NULL    | VIRTUAL GENERATED |
| battlefield_level_virtual | int          | NO   |     | NULL    | VIRTUAL GENERATED |
| tennis_won_virtual        | int          | NO   |     | NULL    | VIRTUAL GENERATED |
| tennis_lost_virtual       | int          | NO   |     | NULL    | VIRTUAL GENERATED |
| times_virtual             | int          | NO   |     | NULL    | VIRTUAL GENERATED |
+---------------------------+--------------+------+-----+---------+-------------------+

 

Generated Column을 검색하는 쿼리 실행계획 확인 

mysql> EXPLAIN SELECT * FROM `players` WHERE `names_virtual` = "Sally"\G  
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: players
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 6
     filtered: 16.67
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

해당 컬럼에 인덱스 생성하고 실행계획 확인, 생성한 인덱스 인덱스 잘탄다. 

mysql> CREATE INDEX `names_idx` ON `players`(`names_virtual`);  
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> EXPLAIN SELECT * FROM `players` WHERE `names_virtual` = "Sally"\G  
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: players
   partitions: NULL
         type: ref
possible_keys: names_idx
          key: names_idx
      key_len: 82
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

다른 Generated Column 들도 인덱스를 생성하고, 생성된 인덱스를 확인한다. 

mysql> CREATE INDEX `times_idx` ON `players`(`times_virtual`);  
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> CREATE INDEX `won_idx` ON `players`(`tennis_won_virtual`);  
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> CREATE INDEX `lost_idx` ON `players`(`tennis_lost_virtual`);  
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> CREATE INDEX `level_idx` ON `players`(`battlefield_level_virtual`);  
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SHOW INDEX FROM `players`;
+---------+------------+-----------+--------------+---------------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table   | Non_unique | Key_name  | Seq_in_index | Column_name               | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+---------+------------+-----------+--------------+---------------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| players |          0 | PRIMARY   |            1 | id                        | A         |           6 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
| players |          1 | names_idx |            1 | names_virtual             | A         |           6 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
| players |          1 | times_idx |            1 | times_virtual             | A         |           5 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
| players |          1 | won_idx   |            1 | tennis_won_virtual        | A         |           6 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
| players |          1 | lost_idx  |            1 | tennis_lost_virtual       | A         |           6 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
| players |          1 | level_idx |            1 | battlefield_level_virtual | A         |           6 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
+---------+------------+-----------+--------------+---------------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
6 rows in set (0.03 sec)

 

STORED 로 지정된 Generated Column 을 사용하는 경우 

1. PRIMARY KEY 를 잡아야될경우 
2. 일반 BTREE 대신 FULLTEXT 또는 RTREE 인덱스가 필요한 경우 
3. 인덱스를 사용하지 않는 스캔이 많이 발생하는 컬럼일 경우 

 

테스트용 테이블 생성 

CREATE TABLE `players_two` (  
    `id` INT GENERATED ALWAYS AS (`player_and_games` ->> '$.id') STORED NOT NULL,
    `player_and_games` JSON NOT NULL,
    `names_virtual` VARCHAR(20) GENERATED ALWAYS AS (`player_and_games` ->> '$.name') NOT NULL,
    `times_virtual` INT GENERATED ALWAYS AS (`player_and_games` ->> '$.games_played.Puzzler.time') NOT NULL,
    `tennis_won_virtual` INT GENERATED ALWAYS AS (`player_and_games` ->> '$.games_played."Crazy Tennis".won') NOT NULL,
    `tennis_lost_virtual` INT GENERATED ALWAYS AS (`player_and_games` ->> '$.games_played."Crazy Tennis".lost') NOT NULL,
    `battlefield_level_virtual` INT GENERATED ALWAYS AS (`player_and_games` ->> '$.games_played.Battlefield.level') NOT NULL, 
    PRIMARY KEY (`id`),
    INDEX `times_index` (`times_virtual`),
    INDEX `names_index` (`names_virtual`),
    INDEX `won_index` (`tennis_won_virtual`),
    INDEX `lost_index` (`tennis_lost_virtual`),
    INDEX `level_index` (`battlefield_level_virtual`)
);

PK 컬럼을 STORED 로 지정하였다. 

`id` INT GENERATED ALWAYS AS (`player_and_games` ->> '$.id') STORED NOT NULL,

SHOW COLUMNS 로 컬럼 확인 

mysql> SHOW COLUMNS FROM `players_two`;
+---------------------------+-------------+------+-----+---------+-------------------+
| Field                     | Type        | Null | Key | Default | Extra             |
+---------------------------+-------------+------+-----+---------+-------------------+
| id                        | int         | NO   | PRI | NULL    | STORED GENERATED  |
| player_and_games          | json        | NO   |     | NULL    |                   |
| names_virtual             | varchar(20) | NO   | MUL | NULL    | VIRTUAL GENERATED |
| times_virtual             | int         | NO   | MUL | NULL    | VIRTUAL GENERATED |
| tennis_won_virtual        | int         | NO   | MUL | NULL    | VIRTUAL GENERATED |
| tennis_lost_virtual       | int         | NO   | MUL | NULL    | VIRTUAL GENERATED |
| battlefield_level_virtual | int         | NO   | MUL | NULL    | VIRTUAL GENERATED |
+---------------------------+-------------+------+-----+---------+-------------------+

 

출처 :

https://www.compose.com/articles/mysql-for-json-generated-columns-and-indexing/

https://dev.mysql.com/blog-archive/indexing-json-documents-via-virtual-columns/

테스트 소스 :

https://github.com/compose-ex/mysql-json-indexing-generated-columns/blob/master/mysql-json.sql

 

'MySQL' 카테고리의 다른 글

Metadata Lock 조회  (0) 2022.09.22
MySQL JSON 사용시 주의사항  (0) 2022.09.16
MySQL Performance Best Practice  (0) 2021.12.26
xtrabackup 8.0 (2)  (0) 2021.07.06
xtrabackup 8.0 (1)  (0) 2021.06.30

 

열 암호화시에 char, nchar 데이터 타입 컬럼의 공백  검색 이슈가 있다.

 

테스트 테이블 생성

이름 컬럼 암호화 

 

이름 검색시 오류가 발생한다. 

피연산자 유형 충돌: varchar은(는) varchar(8000) encrypted with (encryption_type = 'DETERMINISTIC', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = 'ColEncKey', column_encryption_key_database_name = 'Test') collation_name = 'Korean_Wansung_CI_AS'과(와) 호환되지 않습니다.

 

 

아래와 같은 형식으로 검색해야 에러없이 검색된다. 

그런데 '일호'가 검색되지 않는다.. 

왜?

이름 컬럼은 nchar(20) 이다. char, nchar 는 입력한 값이 데이터 타입의 크기보다 작으면 그만큼 공백이 입력된다. 

'일호                  ' 로 저장되어 있다. 

열 암호화가 적용된 컬럼에 테스트용 데이터 입력 

열 암호화가 적용된 컬럼은 insert 시에도 변수를 선언해서 사용해야 한다.

특이사항은 insert 문 하나인데 (1개 행 적용됨)이 3개가 뜬다..

궁금하면 이것은 프로파일러를 떠보기 바란다. 

 

문제가 되는 부분

열 암호화 이후에 insert 된 데이터는 공백 없이 조회가 된다. 

 

빈 테이블을 열 암호화해서 사용할 경우 문제가 없겠지만 데이터가 있는 상태에서 열 암호화 해서 사용할 경우

서비스에서 데이터 조회하는 부분에서 이슈가 된다. (데이터가 입력된 시점에 따라 어떤건 조회되고 어떤건 안되고..)

 

해결방안

- trim() 해서 업데이트 하면되지 않아? 라고 생각할 수 있다.

  여러가지 방법으로 trim() 해보려고 했지만 쿼리나 프로시져로는 안된다. 

  정리하기 힘들정도로 여러가지 형태의 쿼리, 프로시져를 짜서 삽질을 해봤다.  안된다. 

- set ansi_padding off 를 적용해서 임시 테이블을 만들어서 데이터를 입력하고 열 암호화를 적용하면 되지 않을까?

  char 는 set ansi_padding off 가 먹히지만 nchar 는 안먹힌다. 

  문제되는 대부분의 컬럼이 nchar 다.. 

  컬럼 타입을 char 나 nvarchar 로 바꿀까? 안된데.. 못 바꾼데.. 

- 데이터를 다 읽어서 다시 update 하는 어플리케이션을 만들어서 돌려야된다.

  이거 말고는 답을 못 찾았다. ㅡ_ㅡ);

 

'SQL Server' 카테고리의 다른 글

Full Text Catalog 채우기가 동작하지 않을 경우  (0) 2022.10.25
Always Encrypted  (0) 2022.09.02
set ansi_padding off  (0) 2022.09.02

 

SQL Server 2016 에 추가된 기능으로 특정 컬럼을 암호화할 수 있는 기능이다.

이때만 해도 내가 이걸 쓰게될줄은 몰랐다. 

 

시작하기전에 아래의 몇가지 사항을 이해하고 넘어가면 좋다. 

- 열 마스터 키 (CMK) : 열 암호화 키를 보호하는 암호화 키

- 열 암호화 키 (CEK) : 암호화된 열을 보호하는 암호화 키

- 열 마스터 키의 저장소 

   - Azure Key Vault : Microsoft Azure 내에서 중요한 데이터의 암호화 및 암호 해독에 사용되는 암호화 키 및 비밀을 보호하고 관리하는 데 사용되는 키 자격 증명 모음 (주의사항 이걸 사용하다 그만 써야지 하면 뒷감당이 힘들다.)

  - Windows 인증서 저장소 : 인증서를 저장하고 관리하는 Windows에 내장된 인증서 컨테이너

  - HSM : 민감한 데이터를 안전하게 저장하도록 특별히 설계된 하드웨어 장치 

- 열 수준 암호화 유형
  - 결정적 : 항상 동일한 암호문으로 암호화하므로 특정 작업(포인트 조회, 구별, 그룹화 기준)에 사용할 수 있고 인덱싱가능
  - 무작위 : 더 안전하지만 어떤 작업에서도 평가하거나 사용할 수 없으며(쓰기/표시 전용) 인덱싱불가

 

1. 열 마스터 키 만들기 (column master key)

- 개체 탐색기에서 데이터베이스를 선택
- 보안 -> Always Encrypted 키 -> 열 마스터 키 
  새 열 마스터 키 생성 

- 키 저장소 Windows 인증서 저장소 - 로컬 컴퓨터를 선택
  인증서 생성 

- 인증서 생성 버튼 클릭시 인증서가 생성되며 생성된 정보가 노출된다. 

- Windows 인증서 저장소에 인증서가 생성되어 있는지 확인 


2. 열 암호화 키 생성 (column encryption key)

- 개체 탐색기에서 데이터베이스를 선택
- 보안 -> Always Encrypted 키 -> 열 암호화 키 
  새 열 암호화 키 생성 

- 새 열 암호화키 생성 
  생성한 열 마스터 키를 선택하고 열 암호화 키 이름을 입력한다. 


3. 테스트 데이터 생성

CREATE TABLE [dbo].[tb_test]
(
 [Seq] [int] IDENTITY(1,1), 
 [Name] [nvarchar](20) not null, 
 [Phone] [varchar](20) not null, 
 [Email] [varchar](50) not null,
 PRIMARY KEY CLUSTERED ([Seq] ASC)
)
GO

insert into [dbo].[tb_test] values ('홍길동','010-1234-5678','hhhhh@email.com');
insert into [dbo].[tb_test] values ('김길동','010-1234-1234','kim@email.com');
insert into [dbo].[tb_test] values ('최길동','010-5678-5678','choi@email.com');
insert into [dbo].[tb_test] values ('강길동','010-5678-1234','kang@email.com');
insert into [dbo].[tb_test] values ('한길동','010-5678-5678','han@email.com');
GO

select * from [dbo].[tb_test];


4. 열 암호화 

- 개체 탐색기에서 데이터베이스를 선택
   태스크 -> 열 암호화 

- 암호화할 열 선택 

 

5. 데이터 확인 (데이터베이스 서버에서)

- 암호화한 컬럼은 데이터가 암호화되어 확인이 안된다. 

- 새 세션으로 접속시 Always Encrypted -> Always Encrypted(열 암호화) 사용 선택후 접속 

- 데이터 조회시 오류가 발생한다. 


6. Windows 인증서 저장소에 저장된 열 마스터 키 만들때 사용한 인증서 내보내기 

- 열 마스터 키를 만들때 사용한 인증서를 내보낸다. 


7. 인증서 파일을 클라이언트로 복사하여 등록

- 인증 파일을 더블클릭하면 인증서 가져오기 마법사가 시작된다. 
  인증서 저장소 위치를 로컬 컴퓨터로 지정한다. 

- 인증서 내보내기에서 지정한 암호를 입력한다.


8. 인증서를 가져온 후 클라이언트에서 데이터 조회



 

 

테이블 생성시 데이터 타입 char(n), nchar(n) 으로 생성한 컬럼의 경우 입력한 데이터의 길이거 n보다 짧은 경우 뒤에 공백이 들어간다.

예) char(10) 에 'A' 를 넣으면 'A          ' 가 들어간다.

 

문제는.. char(10) 에 'A' 를 넣어야되는 상황이 발생했다.

 

set ansi_padding off 를 설정하면 char(n) 데이터 타입에도 공백이 들어가지 않는다. 

https://docs.microsoft.com/ko-kr/sql/t-sql/statements/set-ansi-padding-transact-sql 

 

-- 테스트 테이블 생성 
create table tb_test (
   colA char(10),
   colB varchar(10),
   colC nchar(10),
   colD nvarchar(10)
);

-- 테스트 데이터 입력 
insert into tb_test (colA, colB, colC, colD) values ('A','B','C','D');

-- 데이터 확인 
select 
	colA + '*',
	colB + '*',
	colC + '*',
	colD + '*'
from tb_test;

char(10), nchar(10) 은 공백이 들어간다. 

 

set ansi_padding off 테스트 

-- 테스트 테이블 삭제 
drop table tb_test;

set ansi_padding off;
-- 테스트 테이블 생성 
create table tb_test (
   colA char(10),
   colB varchar(10),
   colC nchar(10),
   colD nvarchar(10)
);
-- 테스트 데이터 입력
insert into tb_test (colA, colB, colC, colD) values ('A','B','C','D');
-- 데이터 확인 
select 
	colA + '*',
	colB + '*',
	colC + '*',
	colD + '*'
from tb_test;

char(10) 은 해결되었으나 nchar(10) 은 공백이 들어간다. 

 

BOL 을 보면.. nchar 는 안되는구나 ㅠㅠ

 

 

MongoDB 에서 insert 는 insertOne, insertMany 를 사용하는데 insertMany 테스트 

insertMany는 트랜잭션을 보장하지 않는다. insert 하다 에러가 발생하면 중단되고 그전에 insert 된것은 입력된 상태.

updateMany, deleteMany 도 동일할듯 

# 테스트용 test 컬렉션 생성 
test> db.test.insertOne({"name":"user1", "age":20})
{
  acknowledged: true,
  insertedId: ObjectId("61f52e50260badd9f57f3a1f")
}
# name 컬럼에 유니크 인덱스 생성 
test> db.test.createIndex(
...     {"name":1},
...     {unique: true}
... );
name_1

# insertMany 로 name: user2, user1 을 넣을 경우
test> to_insert = [
...      {"name":"user2", "age":19},
...      {"name":"user1", "age":20}
... ]
[ { name: 'user2', age: 19 }, { name: 'user1', age: 20 } ]

# 오류 발생 
test> db.test.insertMany(to_insert)
Uncaught:
MongoBulkWriteError: E11000 duplicate key error collection: test.test index: name_1 dup key: { name: "user1" }
Result: BulkWriteResult {
  result: {
    ok: 1,
    writeErrors: [
      WriteError {
        err: {
          index: 1,
          code: 11000,
          errmsg: 'E11000 duplicate key error collection: test.test index: name_1 dup key: { name: "user1" }',
          errInfo: undefined,
          op: {
            name: 'user1',
            age: 20,
            _id: ObjectId("61f52f4b260badd9f57f3a23")
          }
        }
      }
    ],
    writeConcernErrors: [],
    insertedIds: [
      { index: 0, _id: ObjectId("61f52f4b260badd9f57f3a22") },
      { index: 1, _id: ObjectId("61f52f4b260badd9f57f3a23") }
    ],
    nInserted: 1,
    nUpserted: 0,
    nMatched: 0,
    nModified: 0,
    nRemoved: 0,
    upserted: []
  }
}

# 먼저 입력된 name: user2 는 입력되고 name: user1 은 유니크 인덱스 오류로 입력 안됨 
test> db.test.find()
[
  { _id: ObjectId("61f52e50260badd9f57f3a1f"), name: 'user1', age: 20 },
  { _id: ObjectId("61f52f4b260badd9f57f3a22"), name: 'user2', age: 19 }
]

# name: user1, user2 순서로 입력하면 어떻게 될지 궁금해서 name:user2 를 삭제 
test> db.test.deleteMany({"name":"user2"})
{ acknowledged: true, deletedCount: 1 }
test> db.test.find()
[
  { _id: ObjectId("61f52e50260badd9f57f3a1f"), name: 'user1', age: 20 }
]

# name: user1, user2 순서로 입력
test> to_insert = [
...     {"name":"user1", "age":20}, 
...     {"name":"user2", "age":19}
... ]
[ { name: 'user1', age: 20 }, { name: 'user2', age: 19 } ]

# 유니크 인덱스 중복으로 오류 발생 
test> db.test.insertMany(to_insert)
Uncaught:
MongoBulkWriteError: E11000 duplicate key error collection: test.test index: name_1 dup key: { name: "user1" }
Result: BulkWriteResult {
  result: {
    ok: 1,
    writeErrors: [
      WriteError {
        err: {
          index: 0,
          code: 11000,
          errmsg: 'E11000 duplicate key error collection: test.test index: name_1 dup key: { name: "user1" }',
          errInfo: undefined,
          op: {
            name: 'user1',
            age: 20,
            _id: ObjectId("61f52f02260badd9f57f3a20")
          }
        }
      }
    ],
    writeConcernErrors: [],
    insertedIds: [
      { index: 0, _id: ObjectId("61f52f02260badd9f57f3a20") },
      { index: 1, _id: ObjectId("61f52f02260badd9f57f3a21") }
    ],
    nInserted: 0,
    nUpserted: 0,
    nMatched: 0,
    nModified: 0,
    nRemoved: 0,
    upserted: []
  }
}

# 입력 처리가 되지 않았다. 
test> db.test.find()
[
  { _id: ObjectId("61f52e50260badd9f57f3a1f"), name: 'user1', age: 20 }
]

 

 

 

 

출처 : https://www.youtube.com/watch?v=dF8qmCOSE7g 

 

MySQL 8.0 
- Redo log 디자인을 변경해서 성능향상
- system mutex 를 최대한 분리해서 IO Capacity 를 충분히 이용하도록 변경 
- Information Schema, Performance Schmea 를 조회하는 성능 향상

Memory 
- 작업 데이터셋이 innodb buffer pool 에 적합한지 확인
  사용 가능한 메모리의 70~80%를 innodb buffer pool 에 할당 할 수 있음 
- MySQL 은 Connection 당 메모리 소모, Connection 당 할당되는 메모리는 크게 할당하는것을 권장하지 않는다. 
  max_allowed_packet, thread_stack, net_buffer_length, max_digest_length
- 디스크 접속을 완하하기 위한 Caching 및 Buffer 
  Read log/Join/Sorting Buffer 와 Table Cache
  Replication 관련 max_binlog_cache_size 
- 기타
  - OS, FS cache
  - temporary tables tmp_table_size 
  - Performance Schema
- 모니터링 방법: top 명령어로 확인, 전체 메모리의 80% 이하 

Monitoring MySQL Memory Usage 
- 대부분 메모리 지표는 비활성화 됨 
  select * from performance_schema_setup_instruments where name like '%memory%';
- performance_schema 
  memory_summary_global_by_event_name 
- sys 스키마 
  memory_global_by_current_bytes 
- 상태값 확인
  show global status like 'table_open%';
  show global status like 'binlog%';
  show global status like 'created_tmp%';
  
  select 
    substring_index(event_name, '/', 2) as core_area, 
    format_bytes(sum(current_alloc)) as current_alloc
  from sys.x$memory_global_by_current_bytes
  group by substring_index(event_name, '/', 2)
  order by sum(current_alloc) desc;

Disk 
- SSD, NVMe 권장 
 innodb datadir, tmp 파일, undo 로그 모두 랜덤 IO, 빠른 SSD 권장
 inndb_page_size = 4K (디폴트는 16K, 디스크 page 사이즈와 매칭)
 innodb_flush_neighbors = 0 
- Spinning 디스크는 log (순서 IO)에 여전히 사용할 수 있음 
- RAID 디스크를 사용한다면 성능을 위해 RAID 1+0 권장 
- FBWC/BBWC (Flash/Battery-Backed Write Cache) 권장 
  write 성능 향상 및 crash-safe
- 디스크 유형(rpm 지표) 및 부하에 따라 innodb_io_capactiy 설정 
  디폴트값은 200, 느린 ssd 혹은 일반 하드 디스크에 적합 
  디스크 성능이 좋다면 200보다 더 큰값을 설정

OS 
- 성능만 고려하면 Linux 권장 
- MySQL 8.0 인 경우 최신 커널 권장하지
- ext4, xfs 파일 시스템 추천
- ulimit을 사용하여 file/process limits 확장 
- MySQL 전용 서버인 경우, innodb_numa-interleave = 1 권장 
- Swappiness, 권장 옵션은 (1-6): sysctl - vn.swappinness=1
- InnoDB 데이터 파일은 FS 캐시를 거치지 않음, innodb_flush_method=O_DIRECT 

MySQL Configuration 
- 8.0 설정 파일 : my.cnf, mysqld-auto.cnf (SET PERSIST 변수로 지정한 값을 mysqld 재기동 후에도 유지)
- 설정값 확인 테이블 추가 
  - performance_schema.global_variables
  - performance_schema.session_variables
  - performance_schema.variables_by_thread
  
- 클라이언트 커넥션 관련 설정 
  - max_connections 를 필요 이상으로 크게 설정하지 말아야 할 것 
   - OS limit on file descriptors 
     - Linux/Solaris : UP to 10,000
     - Windows: Up to 16,384
   - 응답 시간의 기대치 및 워크로드 감안
   - 많은 연결 수가 예상되면 버퍼에 따른 메모리 용량을 확복 
   - wait_timeout으로 연결을 닫음 
  - 상태값 확인
    - Connections
    - Max used connections
    - Connection errors max connections 
  - 커넥션별로 할당되는 버퍼 값은 작게 시작 
    - sort_buffer_size
    - binlog_cache_size
    - net_buffer_length ~ max_allowed_packet 
    - read_buffer_size 
    - read_rnd_buffer_size 
    
Reusing Threads
- thread_cache_size : 서버가 스레드를 재사용할 수 있도록 저장한 스레드 개수 
  - 디폴트 값은 자동 설정 : 8 + (max_connections / 100)
  - Thread cache hit rate % = 100 - ((Threads_created / Connections) * 100)
- 연관된 상태 값 확인 
  - Connections
  - Threads_connected
  - Connections / Uptime 
  - Threads_created / Uptime 
  - Uptime_since_flush_status 
  - Threads_cached 
  - Threads_running 
- thread_pool_size 스레드 풀 성능 튜닝 (엔터프라이즈만 사용가능)
  - https://dev.mysql.com/doc/refman/8.0/en/thread-pool-tuning.html

InnoDB Buffer pool 
- innodb_buffer_pool_size 
  - 모든 데이터를 innodb_buffer_pool 에 넣어서 사용하면 이상적이나(이상일 뿐)
  - 테이블 및 인덱스 데이터를 캐시한 InnoDB의 메모리 영역, LRU 알고리즘에 의해 관리 
  - 전용 데이터베이스 서버에서 시스템의 실제 메모리 크기의 70%~80%정도로 설정 할 수 있음 
  - innodb_buffer_pool_size  = N * innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances 
- 확인 방법 : SHOW ENGINE INNODB STATUS;
  - Total memory allocated
  - Buffer pool size 
  - Free buffers 
  - Database pages, Old database pages 
  - Modified db pages 
  - Pending reads, Pending writes 
  - Buffer pool hit rate 

Innodb redo log (트랜잭션 로그)
- innodb data file에 발생하는 랜덤 io 를 red log file을 사용해서 순차 io 로 변경해서 처리 해줘서 write 성능 향상 
- innodb_log_file_size 
  - write 성능에 유리하지만 복구 시간에 영향 (크래시 리커버는 redo log 기반)
  - 총 사이즈(~512G) : innodb_log_file_size * innodb_log_files_in_group (기본값 2)
  - 운영 환경에서 최소 512M으로 권장하고, 가능한 충분히 크게 설정할 것 권장 
- redo 사용율 
  - SHOW ENGINE INNODB STATUS 에서 Log sequence number, Last checkpoint at 값을 확인하여 계산 
  - SELECT NAME, COUNT FROM information_schema.INNODB_METRICS WHERE NAME IN ('log_lsn_current', 'log_lsn_last_checkpoint')
  - Used % = (Used log / Total log) * 100 
    = (log_lsn_current - log_lsn_last_checkpoint / (innodb_log_file_size * innodb_log_files_in_group)) * 100
    = (47605855 / 100663296) * 100 = 47.29% 
  - Used % 가 50% 정도는 괜찮지만 80%정도일 경우 위험함 
    redo log 가 부족하게 되면 innodb buffer pool flushing 이 발생하는데 이때 대량의 io가 발생하여 성능 저하가 발생한다. 
- innodb_log_buffer_size = 16MB
  - Redo 로그 I/O를 완화하기 위한 버퍼 영역 
  - 일반적으로 16MB정도면 충분하지만 트랜잭션 사이즈가 크면 commit 전에 disk IO 가 발생되기에, 따라서 크게 조절할 필요가 있음 
 - innodb_flush_log_at_trx_commit = 1
  - InnoDB가 commit한 트랜젝션에 대해서 언제 flush 할 것인지 결정
  - 디폴트값인 1로 권장하지만 다음 상황에서 제외:
    - InnoDB가 commit한 트랜젝션에 대해서 언제 flush 할 것인지 결정
    - Bulk 데이타 로딩할 경우 2로 설정하거나 8.0 버전에서 redo로그를 비활성화
    - 예측하지 못한 peak 부하로 인해 디스크 I/O부하가 심할 경우
    - 데이터 로스가 발생하여도 괜찮을 경우

바이너리 로그 성능만- 
-  binlog_cache_size에 비해 트랜젝션이 너무 크면, 임시 파일에 저장하기에 성능 영향
- 관련된 상태 변수 확인:
  - Binlog_cache_use
  - Binlog_cache_disk_use
  - 캐시 효율성% = (100 - ( Cache Disk Use/Cache Use) * 100)
- 트랜젝션 사이즈를 제어함으로 바이너리 로그 사이즈 제어, 복제 효율성 향상
  - DELETE 대신에 DROP TABLE/PARTITION
  - DELETE FROM … WHERE … LIMIT 1000;
  - binlog_row_image = minimal
- 복제 지연 방지 설정
  - binlog_transaction_dependency_tracking = WRITESET
  - replica_parallel_type = LOGICAL_CLOCK
  - replica-parallel-workers = 16
  - binlog_order_commits=OFF
  - replica_preserve_commit_order = 1(optional)
https://dev.mysql.com/blog-archive/improving-the-parallel-applier-with-writeset-based-dependency-tracking/



'MySQL' 카테고리의 다른 글

MySQL JSON 사용시 주의사항  (0) 2022.09.16
MySQL JSON, Generated Columns  (0) 2022.09.15
xtrabackup 8.0 (2)  (0) 2021.07.06
xtrabackup 8.0 (1)  (0) 2021.06.30
Hash join in MySQL 8  (0) 2021.06.25

 

OPS Manager를 쓸수 있으면 좋겠지만.. 라이센스 이슈로 사용하지 못할때 사용할만한 모니터링 솔루션!

Percona Monitoring and Management (PMM)

구성 계획

- OS : CentOS 8

- PMM Server : 192.168.137.100

- PMM Client : 192.168.137.21, 호스트명 mongodb 

 

PMM Server 설치 

참고 : https://www.percona.com/doc/percona-monitoring-and-management/2.x/index.html

# yum-config-manager 사용하기 위해 yum-utils설치 
sudo yum -y install yum-utils

# 레포지토리 설치
sudo --add-repo https://download.docker.com/linux/centos/docker-ce.repo
 
# Docker 설치 및 구동 
sudo yum -y install docker-ce
sudo usermod -aG docker $USER
sudo systemctl enable docker
sudo systemctl start docker

# 이미지 받아오기
docker pull percona/pmm-server:2

# 영구 데이터 컨테이너 생성 
docker create --volume /srv \
--name pmm-data \
percona/pmm-server:2 /bin/true

# PMM 서버 시작
docker run --detach --restart always \
--publish 443:443 \
--volumes-from pmm-data \
--name pmm-server \
percona/pmm-server:2

- 브라우저로  admin/admin 으로 로그인 
https://192.168.137.100/graph/login

 

PMM Client 설치 

참고 : https://www.percona.com/doc/percona-monitoring-and-management/2.x/setting-up/client/index.html

# PMM Client 설치 
sudo yum -y install https://repo.percona.com/yum/percona-release-latest.noarch.rpm
sudo yum -y install pmm2-client

# PMM Server 에 Client 등록 
sudo pmm-admin config --server-insecure-tls --server-url=https://admin:admin@192.168.137.100:443 192.168.137.21 generic mongodb

# MongoDB에 DB 모니터링 계정 생성 
db.getSiblingDB("admin").createRole({
    role: "explainRole",
    privileges: [{
        resource: {
            db: "",
            collection: ""
            },
        actions: [
            "listIndexes",
            "listCollections",
            "dbStats",
            "dbHash",
            "collStats",
            "find"
            ]
        }],
    roles:[]
})

db.getSiblingDB("admin").createUser({
   user: "pmm_mongodb",
   pwd: "password",
   roles: [
      { role: "explainRole", db: "admin" },
      { role: "clusterMonitor", db: "admin" },
      { role: "read", db: "local" }
   ]
})

# PMM Server 에 MongoDB 등록 
sudo pmm-admin add mongodb --username=pmm_mongodb --password=password --service-name=mongodb --host=127.0.0.1 --port=27017

 

EMail 알람 설정

참고 : https://www.percona.com/blog/2020/07/07/enable-email-sending-in-percona-monitoring-and-management-2/

# pmm-server 컨테이너 접속 
sudo docker exec -it pmm-server bash
# pmm-server 컨테이너의 /etc/grafana/grafana.ini 파일에 email 서버 설정 
vi /etc/grafana/grafana.ini
[smtp]
enabled = true
host = smtp.gmail.com:587
user = <gmail username>
password = <gmail password>
;cert_file =
;key_file =
skip_verify = false
from_address = <gmail email address>
;from_name = Grafana
;EHLO identity in SMTP dialog (defaults to instance_name)
;ehlo_identity = dashboard.example.com
[emails]
;welcome_email_on_sign_up = false
# pmm-server 컨테이너에서 빠져나온다.
exit

# pmm-server 컨테이너 재기동
sudo docker restart pmm-server

# 컨테이너 생성시에 email 서버를 지정하는 경우 
sudo docker run -d -p 80:80 -p 443:443 --volumes-from pmm-data --name pmm-server \
--restart always \
-e GF_SMTP_ENABLED=true \
-e GF_SMTP_HOST=smtp.gmail.com:587 \
-e GF_SMTP_USER=<gmail username> \
-e GF_SMTP_PASSWORD=<gmail password> \
-e GF_SMTP_SKIP_VERIFY=false \
-e GF_SMTP_FROM_ADDRESS=<gmail email address> \
-e GF_SMTP_FROM_NAME=Grafana \
percona/pmm-server:2

 

- google 의 smtp 를 사용하려면 Google 계정 관리 -> 보안 -> 보안 수준이 낮은 앱의 액세스를 허용으로 변경해야한다. 

 

- grafana 의 Alterting -> Notification channels 설정 후 "Test" 버튼을 클릭하면 테스트 메일이 발송된다.

'MongoDB' 카테고리의 다른 글

insertMany  (0) 2022.01.29
네이버의 MongoDB 활용 사례  (0) 2021.05.20
작은 컬렉션에서 슬로우 쿼리가 발생할때  (0) 2021.05.14

+ Recent posts