MySQL 기준..

 

1. "SELECT ... FOR UPDATE"란?

하나 또는 특정 범위의 row에 대해 여러 세션에서 접근하여 발생할 수 있는 동시성 문제를 해결하기 위해 이용할 수 있다. "SELECT ... FOR UPDATE" 쿼리를 실행하면(auto commit 모드를 비활성화 시킨 후에 "SELECT ... FOR UPDATE" 쿼리 실행해야 한다), Intention Lock(table-level) 걸린 후, Exclusive Lock(row-level) 걸리게 된다.

 

2. "SELECT ... FOR UPDATE" 이슈

=> 존재하지 않는 데이터에 대해서는 모든 세션에서 "SELECT FOR UPDATE"/"UPDATE"/"DELETE" 문을 실행할 수 있는데,

데이터 수가 1개 이상인 테이블에서는 존재하지 않는 데이터에 대해 여러 세션에서 "SELECT FOR UPDATE" 문을 실행하더라도 id가 동일한 데이터에 동시에 접근하지 않도록만 하면  Dead Lock이 발생하지 않지만

만약, 데이터 수가 0개인 테이블에 대해 여러 세션에서 "SELECT FOR UPDATE"를 실행시키면, 모든 세션에서 INSERT문을 아예 실행할 수 없다.

 

3. 테스트

(1) 데이터 수가 0개인 테이블에서 각 A, B 세션에서 "SELECT ... FOR UPDATE" 쿼리로 조회한 데이터가 아무것도 없는 경우

=> 두 세션 중 하나라도 트랜잭션을 종료(commit/rollback) 시키기 전까지는 두 세션 모두 INSERT 쿼리 자체를 실행할 수 없음. 해당 데이터가 아예 없기 때문에 두 세션에서 UPDATE/DELETE 쿼리는 실행 가능.

 

(2) id가 2인 데이터만 있는 상황에서

A세션이  "SELECT ... FOR UPDATE" 쿼리로 id가 1인 데이터를 조회하고

B세션이 "SELECT ... FOR UPDATE" 쿼리로 id가 3인 데이터를 조회한 경우

=> A세션에서는 id가 1인 데이터에 대해서 INSERT/UPDATE/DELETE 실행 가능하다.

B세션에서는 id가 3인 데이터에 대해서 INSERT/UPDATE/DELETE 실행 가능하다.

id가 2이거나 각 세션에서 "SELECT ... FOR UPDATE" 쿼리로 취급하지 않은 데이터에 대해서는 상황에 따라 다른 결과가 나와서 추가적으로 테스트가 필요하다.

 

 

따라서,

여러 세션에서 이미 commit되어 입력된 데이터를 대상으로 "SELECT ... FOR UPDATE" 쿼리를 실행하도록 해야 Dead Lock 문제를 예방할 수 있다.

 


[테스트 코드]

<세션1>

*******

SET autocommit = 1; 
INSERT INTO sss VALUES(2); 

SET autocommit = 0; 
SELECT * FROM sss WHERE id = 1 FOR UPDATE; 
INSERT INTO sss VALUES(4); 
UPDATE sss SET id = 3 WHERE id = 2; 
DELETE FROM sss WHERE id = 2; 
COMMIT; 

SELECT * FROM sss WHERE id = 2;
*******

 

-- 세션2
*******

SET autocommit = 1;
INSERT INTO sss VALUES(2);

SET autocommit = 0;
SELECT * FROM sss WHERE id = 2 FOR UPDATE;
INSERT INTO sss VALUES(4);
UPDATE sss SET id = 3 WHERE id = 1;
DELETE FROM sss WHERE id = 1;
COMMIT;

SELECT * FROM sss WHERE id = 1;
*******

1) Shared Lock = Read Lock

SELECT 실행 => Shared Lock 설정 => Shared Lock이 걸린 데이터를 다른 트랜잭션들이 단순히 읽어 들여 Shared Lock을 계속 걸 수는 있지만 UPDATE/DELETE 실행(Exclusive Lock)은 불가능하다. 역시, 다른 트랜잭션에 의해 Exclusive Lock(INSERT/UPDATE/DELETE)이 걸린 데이터에는 Shared Lock(SELECT)을 걸 수 없어 읽을 수 없다.

2) Exclusive Lock = Write Lock

INSERT/UPDATE/DELETE 실행 => Exclusive Lock 설정 => 해당 트랜잭션이 완료될 때까지 유지되어 다른 트랜잭션은 해당 데이터에 접근(SELECT/UPDATE/DELETE)할 수 없다. 즉, 다른 트랜잭션에 의해 Shared/Exclusive Lock이 걸린 데이터에는 Shared Lock(SELECT), Exclusive Lock(UPDATE/DELETE)을 걸 수 없다.

[Read Uncommitted]
트랜잭션에서 처리(SELECT/INSERT/UPDATE/DELETE)중에 있는 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 것을 허용한다

 

[Read Committed]
트랜잭션들이 Commit된 데이터만 읽게 함으로써 Dirty Read를 막는다. Commit된 데이터만 읽더라도 읽는 시점에 따라 결과가 다를 수 있어서 Non-Repeatable Read와 Phantom Read 현상이 발생할 수 있다. 한 트랜잭션 내에서 쿼리를 두 번 수행할 경우, 두 쿼리 사이에서 다른 트랜잭션에 의해 해당 데이터가 UPDATE/DELETE되거나 새로운 데이터가 INSERT되는 일이 발생할 수 있다. 

 

[Repeatable Read]
트랜잭션 내에서 쿼리를 두 번 이상 수행할 경우, 첫 번째 쿼리 관련 데이터가 UPDATE/DELETE되어 그 다음번째 쿼리에서부터 변경된 값을 읽게 되는 현상을 막는다. 그렇지만 Phantom Read 현상이 발생할 수 있다. 한 트랜잭션 내에서 쿼리를 두 번 수행할 경우, 두 쿼리 사이에서 다른 트랜잭션에 의해 새로운 데이터가 INSERT되어 두 쿼리의 결과가 서로 달라질 수 있다.

 

[Serializable Read]
트랜잭션 내에서 쿼리를 두 번 이상 수행할 경우, 두 쿼리 사이에서 첫 번째 쿼리 관련 데이터가 UPDATE/DELETE 되거나 새로운 데이터가 INSERT 되는 현상을 막는다.

참고: 

- https://stackoverflow.com/questions/6630887/using-is-null-or-is-not-null-on-join-conditions-theory-question

- https://community.teradata.com/t5/General/Is-Null-conditions-in-ON-Join-Vs-Is-Null-in-Where/td-p/50758




1. JOIN ON 절에 IS NULL 체크 관련 쿼리문 예시

select *

from TB_PARENT P with(nolock)

left outer join TB_CHILD C with(nolock)

on P.pid = C.pid 

and C.pid is null

결과: JOIN ON 조건문은 테이블들이 서로 JOIN되는 과정에서 적용된다. TB_CHILD 테이블의 pid 컬럼값이 NULL인 것은 없을 것이며 left outer join이 되고 나서야 NULL값이 생성되기 때문에, 위의 JOIN ON 조건은 항상 false가 되어 LEFT OUTER JOIN문은 왼쪽 테이블(TB_PARENT)의 모든 ROW를 반환할 때, 오른쪽 테이블(TB_CHILD) 관련 컬럼값을 NULL로 채워 반환한다. 따라서, 자식이 없는 부모 데이터를 찾을 수 없다.




2. WHERE 절에  IS NULL 체크 관련 쿼리문 예시

select *

from TB_PARENT P with(nolock)

left outer join TB_CHILD C with(nolock)

on P.pid = C.pid

where C.pid is null  

결과: WHERE 조건문은 테이블들이 서로 JOIN된 후에 적용된다. 왼쪽 테이블(TB_PARENT)과 오른쪽 테이블(TB_CHILD)이 LEFT OUTER JOIN되고 나서 where 조건문이 적용되기 때문에 자식이 없는 부모 데이터를 찾을 수 있다.



>참고: http://www.dbguide.net/dbqna.db?cmd=view&boardUid=143789&boardConfigUid=31&boardStep=0&categoryUid=206


>참고: https://www.codeproject.com/Tips/81400/Useful-DateTime-Functions




-- 특정 월의 N번째 특정 요일에 해당하는 날짜 찾기

DATEADD(wk, DATEDIFF(wk,0, dateadd(dd,[특정 요일을 구하는 경우의 N번째주에 존재할 수 있는 최대일수]-datepart(day,[MONTH]),[MONTH])), [DAY])

ㄴ [특정 요일을 구하는 경우의 N번째주에 존재할 수 있는 최대일수] 

ex1) 일요일 => 첫번째주:7, 두번째주:14, 세번째주:21, 네번째주:28 

ex2) 월요일 => 첫번째주:6, 두번째주:13, 세번째주:20, 네번째주:27 

ex3) 화요일 => 첫번째주:5, 두번째주:12, 세번째주:19, 네번째주:26 

ex4) 수요일 => 첫번째주:4, 두번째주:11, 세번째주:18, 네번째주:25

ex5) 목요일 => 첫번째주:3, 두번째주:10, 세번째주:17, 네번째주:24

ex6) 금요일 => 첫번째주:2, 두번째주:9, 세번째주:16, 네번째주:23

ex7) 토요일 => 첫번째주:1, 두번째주:8, 세번째주:15, 네번째주:22

ㄴ [MONTH]: 찾고자 하는 월의 날짜 (데이터타입은 datetime 또는 'yyyyMMdd')

ㄴ [DAY]: 찾고자 하는 요일의 값 (-1:일요일, 0:월요일, 1:화요일, 2:수요일, 3:목요일, 4:금요일, 5:토요일)



예) 

-- 이번달의 1~4번째 일요일에 해당하는 날짜 찾기

select DATEADD(wk, DATEDIFF(wk,0, dateadd(dd,7-datepart(day,getdate()),getdate())), -1) -- 첫번째 일요일 날짜

select DATEADD(wk, DATEDIFF(wk,0, dateadd(dd,14-datepart(day,getdate()),getdate())), -1) -- 두번째 일요일 날짜

select DATEADD(wk, DATEDIFF(wk,0, dateadd(dd,21-datepart(day,getdate()),getdate())), -1) -- 세번재 일요일 날짜

select DATEADD(wk, DATEDIFF(wk,0, dateadd(dd,28-datepart(day,getdate()),getdate())), -1) -- 네번째 일요일 날짜


-- 이번달의 1~4번째 화요일에 해당하는 날짜 찾기

select DATEADD(wk, DATEDIFF(wk,0, dateadd(dd,5-datepart(day,getdate()),getdate())), -1) -- 첫번째 화요일 날짜

select DATEADD(wk, DATEDIFF(wk,0, dateadd(dd,12-datepart(day,getdate()),getdate())), -1) -- 두번째 화요일 날짜

select DATEADD(wk, DATEDIFF(wk,0, dateadd(dd,19-datepart(day,getdate()),getdate())), -1) -- 세번재 화요일 날짜

select DATEADD(wk, DATEDIFF(wk,0, dateadd(dd,26-datepart(day,getdate()),getdate())), -1) -- 네번째 화요일 날짜




ACID (트랜잭션의 특성 관련..)

 

  • 원자성(Atomicity)은 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 능력이다. 예를 들어, 자금 이체는 성공할 수도 실패할 수도 있지만 보내는 쪽에서 돈을 빼 오는 작업만 성공하고 받는 쪽에 돈을 넣는 작업을 실패해서는 안된다. 원자성은 이와 같이 중간 단계까지 실행되고 실패하는 일이 없도록 하는 것이다.
  • 일관성(Consistency)은 트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지하는 것을 의미한다. 무결성 제약이 모든 계좌는 잔고가 있어야 한다면 이를 위반하는 트랜잭션은 중단된다.
  • 고립성(Isolation)은 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미한다. 이것은 트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없음을 의미한다. 은행 관리자는 이체 작업을 하는 도중에 쿼리를 실행하더라도 특정 계좌간 이체하는 양 쪽을 볼 수 없다. 공식적으로 고립성은 트랜잭션 실행내역은 연속적이어야 함을 의미한다. 성능관련 이유로 인해 이 특성은 가장 유연성 있는 제약 조건이다. 자세한 내용은 관련 문서를 참조해야 한다.
  • 지속성(Durability)은 성공적으로 수행된 트랜잭션은 영원히 반영되어야 함을 의미한다. 시스템 문제, DB 일관성 체크 등을 하더라도 유지되어야 함을 의미한다. 전형적으로 모든 트랜잭션은 로그로 남고 시스템 장애 발생 전 상태로 되돌릴 수 있다. 트랜잭션은 로그에 모든 것이 저장된 후에만 commit 상태로 간주될 수 있다.



출처 :  http://blog.naver.com/PostView.nhn?blogId=waws01&logNo=60181424769&redirect=Dlog&widgetTypeCall=true



★ Best 실행계획은 Clustered Index Seek (Clustered) 이다!!!



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

CREATE TABLE tmp_myfan

(

     fan_id   INT          NOT NULL                                           -- 고유값

   , fan_name NVARCHAR(20) NOT NULL                                           -- 날조아하는여학생명

   , fan_feel FLOAT        NOT NULL DEFAULT 0                                 -- 호감지수

   , fan_gift NVARCHAR(30)     NULL                                           -- 그녀들이내게한선물

   , fan_date VARCHAR(8)   NOT NULL DEFAULT CONVERT(CHAR(8), GETDATE(), 112)  -- 선물받은날짜

   , cr_date  DATETIME              DEFAULT GETDATE()                         -- 입력날짜

);

CREATE UNIQUE CLUSTERED INDEX ix_tmp_myfan ON tmp_myfan(fan_id, cr_date);  

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




1) SELECT절과 WHERE절에 CLUSTERED INDEX 컬럼인 경우

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

SELECT fan_id, cr_date FROM tmp_myfan WHERE fan_id = 110 ;

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

=> 실행계획 : Clustered Index Seek (Clustered)


2) SELECT절에는 CLUSTERED INDEX 컬럼, WHERE절에는 index가 아닌 컬럼인 경우

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

SELECT fan_id FROM tmp_myfan WHERE fan_feel > 95;

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

=> 실행계획 : Clustered Index Scan (Clustered)     <-- 사실상 Table Scan과 다를 바가 없다고 함.


3) SELECT절에는 CLUSTERED INDEX 컬럼, WHERE절에는 index 컬럼인 경우

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

CREATE INDEX ix_non_tmp_myfan ON tmp_myfan(fan_feel); -- fan_feel 컬럼에 인덱스 생성

SELECT fan_id FROM tmp_myfan WHERE fan_feel > 95;

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

=> 실행계획 : Index Seek (NonClustered)


4) fan_feel에 인덱스 생성하고 4-1, 4-2의 경우

   4-1) 

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

   CREATE INDEX ix_non_tmp_myfan ON tmp_myfan(fan_feel); -- fan_feel 컬럼에 인덱스 생성


   SET SHOWPLAN_ALL ON

   SELECT fan_feel FROM tmp_myfan WHERE fan_feel > 95 AND fan_id = 110;

   SET SHOWPLAN_ALL OFF


   |--Clustered Index Seek(OBJECT:([Test].[dbo].[tmp_myfan].[ix_tmp_myfan]), 

   SEEK:([Test].[dbo].[tmp_myfan].[fan_id]=(110)),  

   WHERE:([Test].[dbo].[tmp_myfan].[fan_feel]>(9.400000000000000e+001)) ORDERED FORWARD)

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

   => 해석 : ix_tmp_myfan 를 WHERE 순서를 무시하고 Clustered Index Seek 로 수행하고, 그때 행하고 ix_non_tmp_myfan 의 fan_feel는 필터링. 사실상 Seek시 범위를 줄여주지 못하는 상황.


   4-2) (4-1에 이어서..)

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

   SET SHOWPLAN_ALL ON



   |--Index Seek(OBJECT:([Test].[dbo].[tmp_myfan].[ix_non_tmp_myfan]), 

   SEEK:([Test].[dbo].[tmp_myfan].[fan_feel] > (9.800000000000000e+001)),  

   WHERE:([Test].[dbo].[tmp_myfan].[fan_id]>(110)) ORDERED FORWARD)

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

   => 해석 : ix_non_tmp_myfan 를 Index Seek 로 수행하고, 그때 행하고 ix_tmp_myfan 의 fan_id는 필터링. 역시나 데이터량이 많은 경우라면 특단의 조치가 필요한 상황.


5) fan_feel에 인덱스 생성하고 다음의 경우

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

CREATE INDEX ix_non_tmp_myfan ON tmp_myfan(fan_feel); -- fan_feel 컬럼에 인덱스 생성


SET SHOWPLAN_ALL ON

SELECT fan_feel FROM tmp_myfan WHERE fan_feel = 99 AND fan_id > 110;

SET SHOWPLAN_ALL OFF



|--Index Seek(OBJECT:([Test].[dbo].[tmp_myfan].[ix_non_tmp_myfan]), 

SEEK:([Test].[dbo].[tmp_myfan].[fan_feel]=CONVERT_IMPLICIT(float(53),[@1],0) 

AND [Test].[dbo].[tmp_myfan].[fan_id] > CONVERT_IMPLICIT(int,[@2],0)) ORDERED FORWARD)

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

=> 해석 : 아주 Best한 결과!! 

ix_non_tmp_myfan 를 Index Seek 로 수행할 때 ix_tmp_myfan 의 fan_id 를 NON LEAF LEVEL에 포함시켜 버린다. 

NONCLUSTERED INDEX 가 NON UNIQUE하고 포함된 COLUMN이 WHERE 절에서 등호(=)로 조회할때 

위와 같이 CLUSTERED INDEX CLUSTERING KEY가 NONCLUSTERED INDEX 의 NON LEAF LEVEL에 포함되게 된다.



6) SELECT 절에 index가 아닌 fan_name을 추가하여 조회한 경우

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

SELECT fan_id, fan_name FROM tmp_myfan WHERE fan_feel > 95;

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

=> 실행계획 : Clustered Index Scan (Clustered)

=> 해석 : fan_name과 같이 필요없는 컬럼을 함부로 조회하게 되면, 성능이 급격하게 저하된다.




+ Recent posts