2020년 12월 30일 수요일

Hash Join 에 대하여

MySQL 8.0.18 부터 추가된 Hash Join에 대하여 알아보자.

Hash Join 이란
조인을 수행할 테이블의 조인 컬럼을 기준으로 해쉬함수를 수행한 후, 서로 동일한 해쉬 값을 갖는 것 중에서 실제 값이 같은지 비교하면서 조인을 수행하는 메커니즘이다. 
  1. 선행 테이블에서 주어진 조건을 만족하는 로우 검색
  2. 선행 테이블의 조인 키를 기준으로 해쉬 함수 적용 후, 해쉬 테이블 생성. 조인 컬럼과 검색하는 컬럼도 함께 저장
  3. 선행 테이블의 조건을 만족하는 모든 로우를 1~2 과정 수행 후, 후행 테이블에서 주어진 조건을 만족하는 로우 검색
  4. 후행 테이블의 조인 키를 기준으로 해쉬 함수를 적용하여 선행 테이블과 비교
  5. 조인 성공 시, 추출버퍼에 격납
  6. 후행 테이블의 조건을 만족하는 모든 로우를 3~5 과정 수행
해쉬 테이블의 크기가 메모리에 적재할 수 있는 크기보다 클 경우, 임시 영역에 해쉬 테이블을 저장하므로
사이즈가 작은 테이블을 선행으로 사용하는 것이 좋다.

Hash Join 채용 조건
해쉬 조인은 등가조건의 조인이고, 조인하는 컬럼에 Index가 없는 경우에 사용된다
해쉬 조인의 사용유무는 EXPLAIN ANALYZE 또는 EXPLAIN FORMAT=TREE: 로 확인 가능하다.

# MySQL 8.0.20 에서 Hash Join이 강화되어 채용 조건이 확대되었다
  • 등가 조인 이외의 INNER JOIN
  • 세미조인
  • 안티조인
  • LEFT OUTER JOIN / RIGHT OUTER JOIN
상세한 내용은 공식 사이트 참고
https://dev.mysql.com/doc/refman/8.0/en/hash-joins.html

Hash Join 검증
t1, t2 테이블 작성 후 테스트용 데이터(랜덤으로 생성한 20문자) 16384건 등록하자.
mysql> CREATE TABLE t1 (a VARCHAR(20));
Query OK, 0 rows affected (0.10 sec)

mysql> CREATE TABLE t2 (a VARCHAR(20));
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO t1 VALUES ();
mysql> INSERT INTO t1 SELECT * FROM t1;
mysql> INSERT INTO t1 SELECT * FROM t1;
mysql> INSERT INTO t1 SELECT * FROM t1;
 
(생략)
 
mysql> INSERT INTO t1 SELECT * FROM t1;
mysql> INSERT INTO t1 SELECT * FROM t1;
mysql> INSERT INTO t1 SELECT * FROM t1;
Query OK, 8192 rows affected (0.11 sec)
Records: 8192  Duplicates: 0  Warnings: 0
 
mysql> UPDATE t1 SET a = SUBSTRING(MD5(RAND()), 1, 20);
Query OK, 16384 rows affected (0.81 sec)
Rows matched: 16384  Changed: 16384  Warnings: 0

mysql> INSERT INTO t2 SELECT * FROM t1;
Query OK, 16384 rows affected (0.58 sec)
Records: 16384  Duplicates: 0  Warnings: 0
 
mysql> UPDATE t2 SET a = SUBSTRING(MD5(RAND()), 1, 20);
Query OK, 16384 rows affected (0.30 sec)
Rows matched: 16384  Changed: 16384  Warnings: 0

작성한 테이블 t1, t2에 대하여 아래의 쿼리를 실행해보자
mysql> EXPLAIN ANALYZE SELECT * FROM t1 INNER JOIN t2 ON t1.a = t2.a;
+-------------------------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                                           |
+-------------------------------------------------------------------------------------------------------------------+
| -> Inner hash join (t1.a = t2.a)  (cost=26368857.25 rows=26367169) (actual time=42.485..52.619 rows=5 loops=1)
    -> Table scan on t1  (cost=0.01 rows=16269) (actual time=0.016..10.830 rows=16384 loops=1)
    -> Hash
        -> Table scan on t2  (cost=1644.95 rows=16207) (actual time=0.051..10.163 rows=16384 loops=1)
 |
+-------------------------------------------------------------------------------------------------------------------+
1 row in set (0.05 sec)

실행계획에 "Inner hash join" 이 출력되어 있고, Hash Join이 실행된 것을 알 수 있다.

# Refer
https://mysqlserverteam.com/hash-join-in-mysql-8/

2020년 12월 27일 일요일

Clone Plugin 에 대하여

MySQL 8.0.17 부터 추가된 Clone 플러그인에 대하여 알아보자.

Clone plugin 이란
온라인으로 습득할 수 있는 풀 백업으로, 동일한 데이터의 MySQL 서버를 작성할 수 있다.

Clone plugin 도입
MySQL 서버 패키지에 들어 있고, 사용하기 위해서는 INSTALL PLUGIN 으로 설치해야 한다.
INSTALL PLUGIN 실행 후에 SHOW PLUGIN 으로 설치되어 있는 것을 확인할 수 있다.
mysql> INSTALL PLUGIN clone SONAME 'mysql_clone.so';
Query OK, 0 rows affected (0.49 sec)

mysql> SHOW PLGUINS;

...

| mysqlx                          | ACTIVE   | DAEMON             | NULL           | GPL     |
| clone                           | ACTIVE   | CLONE              | mysql_clone.so | GPL     |
+---------------------------------+----------+--------------------+----------------+---------+
45 rows in set (0.06 sec)

Clone으로 데이터 복사
Clone을 사용하여 데이터를 습득 하려면, CLONE LOCAL DATA DIRECTORY = <습득 하려고 하는 디렉토리 패스>를 지정해야 한다. 
실행하기 위해서는 모든 스키마, 테이블(*.*)에 대하여 CLONE_ADMIN 권한이 필요하다(Local 환경에서 실행하는 경우는 BACKUP_ADMIN 권한으로 가능)

또한, Clone을 실행할 때 지정한 디렉토리가 존재하는 경우, 에러가 발생하여 데이터 습득에 실패하게 된다.
ERROR 1007 (HY000): Can't create database '/tmp/testdb'; database exists

문제 없이 실행된 경우는, 아래와 같이 Query OK가 표시 된다.
mysql> CLONE LOCAL DATA DIRECTORY = '/tmp/clone_testdb';
Query OK, 0 rows affected (2.30 sec)

지정한 디렉토리에 복사된 데이터를 확인해보자.
$ ll /tmp/clone_testdb
total 682064
drwxr-x--- 2 mysql mysql      4096 Dec 22 03:12 #clone
drwxr-x--- 2 mysql mysql      4096 Dec 22 03:12 d1
drwxr-x--- 2 mysql mysql      4096 Dec 22 03:12 d2
-rw-r----- 1 mysql mysql     24800 Dec 22 03:12 ib_buffer_pool
-rw-r----- 1 mysql mysql  12582912 Dec 22 03:12 ibdata1
-rw-r----- 1 mysql mysql 268435456 Dec 22 03:12 ib_logfile0
-rw-r----- 1 mysql mysql 268435456 Dec 22 03:12 ib_logfile1
drwxr-x--- 2 mysql mysql      4096 Dec 22 03:12 mysql
-rw-r----- 1 mysql mysql  79691776 Dec 22 03:12 mysql.ibd
drwxr-x--- 2 mysql mysql      4096 Dec 22 03:12 sys
drwxr-x--- 2 mysql mysql      4096 Dec 22 03:12 test
drwxr-x--- 2 mysql mysql      4096 Dec 22 03:12 trxtest
-rw-r----- 1 mysql mysql  14680064 Dec 22 03:12 undo_001
-rw-r----- 1 mysql mysql  26214400 Dec 22 03:12 undo_002
-rw-r----- 1 mysql mysql  28311552 Dec 22 03:12 undo_table_space.ibu
drwxr-x--- 2 mysql mysql      4096 Dec 22 03:12 world

이를 이용하여, 다른 포트로 MySQL을 기동해보자. 필자의 환경에서는 /data 밑에 datadir를 배치하고 있으므로, 동일하게 /data 밑으로 이동시킨 후, my.cnf를 준비하여 기동시켜보자
$ mv /tmp/clone_testdb /data/clone_testdb
$ cp /data/testdb/my.cnf /data/clone_testdb/my.cnf
$ vi /data/clone_testdb/my.cnf <-port, server-id, datadir  변경
$ /usr/bin/mysqld_safe --defaults-file=/data/clone_testdb/my.cnf &
$ /usr/bin/mysql --defaults-file=/data/clone_testdb/my.cnf -uroot -p
mysql> SELECT @@port;
+--------+
| @@port |
+--------+
|   3310 |
+--------+
1 row in set (0.00 sec)

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| d1                 |
| d2                 |
| information_schema |
| mysql              |
| nyumon             |
| performance_schema |
| sys                |
| test               |
| trxtest            |
| world              |
+--------------------+
10 rows in set (0.18 sec)

GTID를 이용하고 있을 때는 GTID 관련 정보도 복사되기 때문에, MASTER_AUTO_POSITION=1 을 지정하여 CHANGE MASTER statement를 실행하면 슬레이브로 추가할 수 있다.

GTID를 이용하고 있지 않은 경우에도, performance_schema.clone_status 테이블에서 CLONE 실시 정보를 습득할 수 있으므로, 이 정보를 토대로 CHANGE MASTER statement를 실행하여 슬레이브를 추가할 수 있다.
mysql> SELECT * FROM performance_schema.clone_status\G
*************************** 1. row ***************************
             ID: 1
            PID: 12
          STATE: Completed
     BEGIN_TIME: 2020-12-22 03:12:13.358
       END_TIME: 2020-12-22 03:12:14.556
         SOURCE: LOCAL INSTANCE
    DESTINATION: /tmp/clone_testdb/
       ERROR_NO: 0
  ERROR_MESSAGE:
    BINLOG_FILE:
BINLOG_POSITION: 0
  GTID_EXECUTED:
1 row in set (0.15 sec)

리모트 서버에 대한 Clone은 아래와 같이 가능하다.
CLONE INSTANCE FROM 'clone 실행 유저'@'Donar IP':'Donar port' IDENTIFIED BY '비밀번호' (DATA DIRECTORY = '습득처')

DATA DIRECTIRY는 옵션이므로 괄호로 감싸져 있다. 만약 지정하지 않은 경우는 Recipient의 데이터가 Donar과 같은 데이터로 변경되고 서버가 재기동된다. DATA DIRECTIRY를 기술하지 않으면, 데이터가 옮겨져 Recipient에 존재하던 데이터가 사라지기 때문에 주의가 필요하다.
mysql> CLONE INSTANCE FROM 'root'@'127.0.0.1':3306 IDENTIFIED BY 'xxxxxx' DATA DIRECTORY = '/tmp/remote_testdb';
Query OK, 0 rows affected (2.17 sec)

또한, 리모트 서버로 실행하는 경우, Donar 인스턴스 정보가 Recipient의 clone_valid_donor_list에 설정되어 있어야 한다. 
mysql> SET GLOBAL clone_valid_donor_list = '127.0.0.1:3306';
Query OK, 0 rows affected (0.06 sec)

mysql> SHOW VARIABLES LIKE 'clone_valid_donor_list';
+------------------------+----------------+
| Variable_name          | Value          |
+------------------------+----------------+
| clone_valid_donor_list | 127.0.0.1:3306 |
+------------------------+----------------+
1 row in set (0.07 sec)

주의점
Clone 플러그인을 이용할 때는 몇가지 주의점이 있다.
  • Donar측에 병렬 DDL은 허용하지 않는다. 만약, 빈번히 DDL이 실행되는 환경이라면, 참조 없고 지연되도 상관없는 슬레이브 서버 등에서 실행하는 걸 추천
  • Donar 과 Recipient 버전이 다른 경우, 클론 불가능
  • Clone 대상이 되는 데이터는 InnoDB 스토리지 엔진이다. 실행 전에 InnoDB 이외의 테이블이 없는 지 확인 필요

# Refer
https://dev.mysql.com/doc/refman/8.0/en/clone-plugin.html

2020년 12월 15일 화요일

CHECK 제약에 대해서

MySQL 8.0.16 부터 새롭게 추가된 CHECK 제약애 대해서 알아보자.

CHECK 제약
테이블에 데이터를 등록 또는 갱신할 때에 조건을 만족하는 지 검증하는 기능이다.

예를 들어, UNSIGNED TINYINT로 정의된 컬럼에는 0~255 까지의 숫자를 등록할 수 있지만 CHECK 제약을 이용하여 0~10 까지의 숫자로 한정하는 것이 가능하다.
mysql> CREATE TABLE t1(num TINYINT UNSIGNED CHECK(num < 11));
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO t1(num) VALUES (10);
Query OK, 1 row affected (0.01 sec)

mysql> INSERT INTO t1(num) VALUES (11);
ERROR 3819 (HY000): Check constraint 't1_chk_1' is violated.

mysql> SELECT * FROM t1;
+------+
| num  |
+------+
|   10 |
+------+
1 row in set (0.00 sec)

mysql> UPDATE t1 SET num = 11 WHERE num = 10;
ERROR 3819 (HY000): Check constraint 't1_chk_1' is violated.

CHECK 제약 작성
CHECK 제약은 테이블 또는 컬럼에 지정하는 게 가능하다.

컬럼 제약은 해당 컬럼만을 참조하여 정의하는 제약이고, 테이블 제약은 테이블의 컬럼들을 참조하여 정의하는 게 가능하다.
mysql> CREATE TABLE t1(
    ->   num1 INT CHECK (num1 > 1),
    ->   num2 INT CONSTRAINT num2_chk CHECK (num2 > 0),
    -> CHECK (num1 *2 < num2), # 제약 명칭을 지정하지 않으면 <table_name>_chk_<number>로 자동생성
    -> CONSTRAINT t1_chk CHECK (num1 <> 0)
    -> );
Query OK, 0 rows affected (0.01 sec)

작성한 CHECK 제약 확인
SHOW CREATE TABLE구문으로 확인하는 것이 가능하다.
또한 INFORMATION_SCHEMA.CHECK_CONSTRAINT 테이블에서도 CHECK 제약 리스트를 습득할 수 있다.
mysql> SELECT * FROM information_schema.check_constraints;
+--------------------+-------------------+-----------------+-------------------------+
| CONSTRAINT_CATALOG | CONSTRAINT_SCHEMA | CONSTRAINT_NAME | CHECK_CLAUSE            |
+--------------------+-------------------+-----------------+-------------------------+
| def                | check_database    | t1_chk_1        | (`num1` > 1)            |
| def                | check_database    | num2_chk        | (`num2` > 0)            |
| def                | check_database    | t1_chk_2        | ((`num1` * 2) < `num2`) |
| def                | check_database    | t1_chk          | (`num1` <> 0)           |
+--------------------+-------------------+-----------------+-------------------------+
4 rows in set (0.01 sec)

CHECK 제약위반이 발생한 경우는 performance_schema.events_errors_summary_by_* 계열 테이블의 ER_CHECK_CONSTRAINT_VIOLATED 에러 카운트가 증가한다.

CHECK 제약에 의해 에러가 발생하고 있는 지 확인해야 하는 경우는 해당 계측을 참조하면 좋을 거 같다.

CHECK 제약의 제한
이용하는 데 있어서 몇 가지의 제한이 있다. 아래와 같은 경우는 CHECK 제약을 이용할 수 없다.
  • AUTO_INCREMENT를 지정한 컬럼
  • 다른 테이블의 컬럼을 참조하여 CHECK 제약 정의
  • 사용자 함수 또는 프로시저 사용
  • 변수 사용
  • 서브 쿼리 사용
  • NOW(), CURRENT_USER() 와 같은 비결정성 함수 사용
보다 상세한 조건은 공식 문서 참조. 
https://dev.mysql.com/doc/refman/8.0/en/create-table-check-constraints.html

CHECK 제약 변경은 ALTER TABLE 구문을 이용해야 한다.
단, 기존 데이터가 CHECK 제약을 위반하고 있는 경우는 에러가 발생하므로 사전에 확인하는 게 좋다.
mysql> insert into t1 values(0),(1),(2);
Query OK, 3 rows affected (0.01 sec)
mysql> ALTER TABLE t1 ADD CONSTRAINT CHECK(num > 0);
ERROR 3819 (HY000): Check constraint 't1_chk_1' is violated.

2020년 12월 5일 토요일

Partial Revokes 시스템 변수에 대하여

MySQL 8.0.16에서 Partial Revokes 시스템 변수가 추가되었다.

통상적으로 모든 권한을 부여하지만, 일부 권한만 박탈할 수 기능으로 부분적인 제한 설정이 가능해졌다.
이 변수를 굳이 사용하지 않아도 동일한 권한을 부여할 수 있지만, 부여하는 스키마의 대상이 많은 경우는 해당 변수를 사용하는 게 편리할 지도 모른다.

partial_revokes 설정
기본 값이 OFF로 설정되어 있으므로, 기능을 사용하기 위해서는 ON으로 해주어야 한다. 
mysql> SET GLOBAL partial_revokes = 1;
Query OK, 0 rows affected (0.04 sec)

모든 데이터베이스의 SELECT권한을 갖는 유저를 작성한 후 mysql 스키마에 대한 권한을 박탈해보자.
mysql> CREATE USER test_user@localhost IDENTIFIED BY '*****';
Query OK, 0 rows affected (0.05 sec)


mysql>GRANT SELECT ON *.* TO test_user@localhost;
Query OK, 0 rows affected, 1 warning (0.00 sec) mysql>REVOKE SELECT ON mysql.* FROM test_user@localhost;
Query OK, 0 rows affected, 1 warning (0.00 sec) mysql>SHOW GRANTS FOR test_user@localhost;
+-----------------------------------------------------------+ | Grants for test_user@localhost |
+-----------------------------------------------------------+ | GRANT SELECT ON *.* TO `test_user `@`localhost` |
| REVOKE SELECT ON `mysql`.* FROM `test_user `@`localhost` |
+-----------------------------------------------------------+ 2 rows in set (0.00 sec)

실제로 작성한 유저로 mysql 스키마에 접근해보면 에러가 발생하는 것을 확인할 수 있다.
mysql> SELECT current_user();
+-----------------------+
| current_user()        |
+-----------------------+
| test_user@localhost |
+-----------------------+ 1 row in set (0.00 sec) mysql> SELECT * FROM mysql.user; ERROR 1142 (42000): SELECT command denied to user 'test_user'@'localhost' for table 'user'

partial_revokes는 "일부를 제외한 모든 권한을 부여한다" 라고 이해하면 될 거 같다.
실제로 partial_revokes를 이용한 revoke 정보는 mysql.user.user_attributes 컬럼에서 확인할 수 있다.
mysql> SELECT concat(user,'@', host), user_attributes FROM mysql.user WHERE user = 'test_user' and host = 'localhost';
+------------------------+---------------------------------------------------------------------+
| concat(user,'@', host) | user_attributes                                                     |
+------------------------+---------------------------------------------------------------------+
| test_user@localhost  | {"Restrictions": [{"Database": "mysql", "Privileges": ["SELECT"]}]} |
+------------------------+---------------------------------------------------------------------+ 1 row in set (0.00 sec)

partial_revokes 주의점
partial_revokes를 이용하고 있는 유저가 있는 경우, 설정을 OFF로 변경하는 건 불가능하다.
mysql> SET GLOBAL partial_revokes = 0;
ERROR 3896 (HY000): At least one partial revoke exists on a database. The system variable '@@partial_revokes' must be set to ON.

OFF로 변경하고 싶은 경우는, partial_revokes로 박탈된 권한 삭제 또는 유저 삭제로 partial_revokes를 이용하지 않는 상태로 되어야 한다.
partial_revokes가 이용되고 있는 유저의 유무 확인은 mysql.user.user_attributes 컬럼의 Restrictions 속성으로 가능하다.

partial_revokes는 스키마를 지정해야 하고 일부 테이블이나 컬럼, % 등의 와일드 카드를 이용해서 스키마를 제외하는 건 불가능하다. 
또한 CREATE TABLE이나 FILE과 같은 글로벌 레벨의 권한에 대해서도 partial_revokes를 이용할 수 없다.

replication구성에서 master가 partial_revokes ON, slave가 partial_revokes OFF인 경우 에러가 발생하기 때문에 각 인스턴스의 설정을 맞춰주어야 한다.
partial_revokes: master ON, slave OFF 상황에서의 에러

mysql> SHOW SLAVE STATUS\G
skip
Last_SQL_Errno: 1141
Last_SQL_Error: Error 'There is no such grant defined for user 'test_user' on host 'localhost'' on query. Default database: '
'. Query: 'REVOKE SELECT ON mysql.* FROM test_user@localhost'
  Replicate_Ignore_Server_Ids:

권한을 부여하는 유저가 partial_revokes에 의해 REVOKE되어 있는 스키마가 있는 경우, 권한을 부여할 때 revoke된 스키마도 인계되기 때문에 주의가 필요하다.
아래의 블로그에서 위와 같은 여러 패턴을 소개하고 있다
https://mysqlserverteam.com/partial-revokes-from-database-objects/

2020년 12월 4일 금요일

Nested subquery가 Correlated subquery처럼 동작하는 경우에 대하여

개발팀의 쿼리 검수요청을 처리하던 중 Nested subquery가 Correlated subquery처럼 동작하는 케이스를 발견하여 알아보도록 하자.
해당 DB는 MySQL 5.6에서 실험하였다

우선 간단하게 서브쿼리에 대하여 짚고 넘어가자.
Nested subquery(중첩 서브쿼리)
# 이너 쿼리를 먼저 실행
# 이너 쿼리의 결과를 통해 아우터 쿼리 조건 검색

SELECT SUM (Sales) FROM Store_Information
WHERE Store_Name IN
  ( SELECT Store_Name FROM Geography
    WHERE Region_Name = 'West');

Correlated subquery(상관 서브쿼리)
# 아우터 쿼리를 먼저 실행
# 접근한 로우의 컬럼 값을 이용해 이너 쿼리 실행 
# 아우터 쿼리의 로우가 끝날 때 까지 반복 

SELECT SUM (a1.Sales) FROM Store_Information a1
WHERE a1.Store_Name IN
  ( SELECT Store_Name FROM Geography a2
    WHERE a2.Store_Name = a1.Store_Name);

이제부터 검수 요청 받은 쿼리와 테이블에 대하여 알아보고 분석을 해보자
검수 쿼리
# Table
CREATE TABLE `table_aaa` (
  `transactionSeq` bigint(20) NOT NULL,
  `userId` varchar(100) NOT NULL,
  .
  .
  .
  `regDt` datetime NOT NULL,
  PRIMARY KEY (`transactionSeq`,`regDt`),
  KEY `TABLE_AAA_IDX1` (`regDt`,`userId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 

CREATE TABLE `table_bbb` (
  `userId` varchar(16) NOT NULL,
  .
  .
  .
  PRIMARY KEY (`userId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8


# Query
SELECT
    ...
FROM
    table_aaa
WHERE
    regDt >= '2020-01-01'
AND
    regDt < '2020-02-01'
AND
    userId IN (
        SELECT
            userId
        FROM
            table_bbb
        WHERE
            regDt >= '2020-01-01'
        AND
            regDt < '2020-02-01'
    )
GROUP BY
    ...;

쿼리를 보면 중첩 서브쿼리라고 생각이 들겠지만, 실제로 실행계획을 살펴보니 그게 아니었다.
# EXPLAIN
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_aaa
         type: range
possible_keys: TABLE_AAA_IDX1
          key: TABLE_AAA_IDX1
      key_len: 5
          ref: NULL
         rows: 39124
        Extra: Using index condition; Using temporary; Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_bbb
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 50
          ref: test_db.test_aaa.userId
         rows: 1
        Extra: Using where
2 rows in set (0.00 sec)

ERROR:
No query specified

분석
  1. TABLE_AAA_IDX1을 이용하여 table_aaa.regDt 검색
  2. TABLE_AAA_IDX1 인덱스가 (regDt, userId) 복합키로 설정되어 있으므로, 인덱스 페이지에서 조건에 맞는 로우의 userId를 이용하여 table_bbb의 PK인 userId를 검색
  3. PK를 통해 table_bbb의 로우에 접근 후 추가적으로 regDt 조건 필터
이너쿼리 조건절에 table_bbb.userId = table_aaa.userId가 기재되어 있는 것 같은 동작을 하고 있으므로,
옵티마이저가 중첩 서브쿼리를 채용한 것으로 보인다

결혼이민비자 신청방법(F-6-1 국민의 배우자)

 제가 일본인 여자친구와 결혼 후, 한국에 귀국하기 위해 신청한 결혼이민비자에 대하여 작성해보도록 하겠습니다. 필자는 일본에서 근무하고 있었으며, 한국에서의 소득은 없었습니다. 결혼이민비자를 신청한 날짜는 2021-04-21 이며, 사증이 발급된 날짜...