잠금 에스컬레이션과 스냅샷 격리, 교착 상태 분석으로 동시성 내부를 깊이 파헤칩니다.
동시성 제어와 격리 수준 심화
트랜잭션과 잠금의 기초는 이미 입문편에서 다뤘습니다. 이 레슨에서는 한 단계 더 깊이 들어가, SQL Server가 동시성을 내부적으로 어떻게 제어하는지 살펴봅니다. 격리 수준마다 어떤 이상 현상을 허용하는지, 잠금이 어떻게 에스컬레이션되는지, 그리고 운영 환경에서 교착 상태와 블로킹 체인을 어떻게 진단하고 해소하는지에 집중합니다.
실무에서 성능 문제의 상당 부분은 "잠금이 너무 오래 유지된다"거나 "격리 수준을 잘못 선택했다"는 원인에서 비롯됩니다. 내부 메커니즘을 정확히 이해하면, 단순히 트랜잭션을 짧게 유지하라는 경험적 조언을 넘어서 상황에 맞는 설계 결정을 내릴 수 있습니다.
학습 목표
- SQL Server의 다섯 가지 격리 수준과 각 수준에서 허용되는 읽기 이상 현상의 차이를 설명할 수 있다.
- S/X/U/IS/IX 잠금의 호환성과 잠금 에스컬레이션(행 → 페이지 → 테이블) 메커니즘을 이해한다.
- RCSI와 SNAPSHOT 격리의 버전 저장소(tempdb) 동작 원리를 설명하고 설정할 수 있다.
- 교착 상태 그래프를 분석하고 회피 패턴을 적용할 수 있다.
sys.dm_exec_requests,sys.dm_tran_locks를 이용해 블로킹 체인을 실시간 진단할 수 있다.
격리 수준과 읽기 이상 현상
SQL Server는 ANSI/ISO 표준의 네 가지 격리 수준에 더해 자체적으로 두 가지 스냅샷 기반 수준을 제공합니다. 격리 수준이 높아질수록 동시성은 낮아지고 데이터 일관성은 높아집니다.
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read | 잠금 방식 |
|---|---|---|---|---|
| READ UNCOMMITTED | 허용 | 허용 | 허용 | 읽기 잠금 없음 |
| READ COMMITTED (기본값) | 차단 | 허용 | 허용 | 단문 수준 S 잠금 |
| REPEATABLE READ | 차단 | 차단 | 허용 | 트랜잭션 종료까지 S 잠금 유지 |
| SERIALIZABLE | 차단 | 차단 | 차단 | 범위 잠금(키 범위 잠금) 사용 |
| READ COMMITTED SNAPSHOT (RCSI) | 차단 | 허용 | 허용 | 읽기 잠금 없음, 버전 저장소 사용 |
| SNAPSHOT | 차단 | 차단 | 차단 | 읽기 잠금 없음, 트랜잭션 시작 시점 스냅샷 |
Dirty Read는 아직 커밋되지 않은 데이터를 읽는 현상, Non-Repeatable Read는 같은 트랜잭션 내에서 같은 행을 두 번 읽었을 때 값이 달라지는 현상, Phantom Read는 범위 조회 결과에 새로운 행이 끼어드는 현상입니다.
세션 수준에서 격리 수준을 변경하는 방법은 다음과 같습니다.
-- 세션의 격리 수준을 SERIALIZABLE로 변경
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
SELECT ProductID, Stock
FROM Products
WHERE CategoryID = 3;
-- 이 범위에 다른 세션이 INSERT를 시도하면 차단됨
WAITFOR DELAY '00:00:05'; -- 의도적으로 지연
SELECT ProductID, Stock
FROM Products
WHERE CategoryID = 3; -- 첫 번째와 동일한 결과 보장
COMMIT;
-- 세션 격리 수준 초기화
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
⚠️ 주의 SERIALIZABLE은 팬텀 읽기를 막기 위해 키 범위 잠금(Key-Range Lock)을 사용합니다. 범위가 넓을수록 잠금 경합이 심해져 처리량이 급감할 수 있습니다. OLTP 환경에서는 RCSI 또는 SNAPSHOT 격리가 더 나은 대안입니다.
잠금 종류와 호환성, 에스컬레이션
잠금 모드
SQL Server의 주요 잠금 모드는 다음과 같습니다.
| 잠금 모드 | 약어 | 용도 |
|---|---|---|
| Shared | S | SELECT (읽기) |
| Exclusive | X | INSERT / UPDATE / DELETE (쓰기) |
| Update | U | UPDATE 후보 행 탐색 단계 |
| Intent Shared | IS | 상위 리소스(페이지/테이블)에 S 잠금 예고 |
| Intent Exclusive | IX | 상위 리소스에 X 잠금 예고 |
| Shared with Intent Exclusive | SIX | 테이블 전체 읽기 + 일부 행 쓰기 |
**의도 잠금(Intent Lock)**은 계층 구조 최적화를 위한 것입니다. 행 수준 X 잠금을 설정할 때, SQL Server는 해당 페이지와 테이블에 IX 잠금을 먼저 설정합니다. 이렇게 하면 다른 세션이 테이블 수준 S 잠금을 요청할 때 행 레벨까지 내려가지 않고도 충돌 여부를 즉시 판단할 수 있습니다.
**U 잠금(Update Lock)**은 교착 상태를 줄이기 위한 중간 상태입니다. UPDATE 문이 대상 행을 탐색할 때 S 잠금 대신 U 잠금을 사용합니다. U와 U는 서로 호환되지 않으므로, 두 세션이 동시에 같은 행을 UPDATE하려 할 때 한 세션은 대기하게 됩니다. 만약 S 잠금을 사용했다면 S-S 호환으로 둘 다 통과한 뒤 X로 올리려 할 때 교착이 발생할 수 있습니다.
-- 잠금 호환성 확인: 현재 세션에 걸린 잠금 조회
SELECT
tl.resource_type,
tl.resource_description,
tl.request_mode,
tl.request_status,
tl.request_session_id
FROM sys.dm_tran_locks AS tl
WHERE tl.request_session_id = @@SPID;
잠금 에스컬레이션
SQL Server는 단일 문이 한 테이블/인덱스에서 약 5,000개 잠금을 획득하거나, 인스턴스의 잠금 메모리 임계치를 초과하면 세밀한 잠금을 하나의 테이블 수준 잠금으로 통합합니다. 이것이 잠금 에스컬레이션입니다.
에스컬레이션은 메모리를 아끼지만 동시성을 크게 저해합니다. 한 세션이 테이블 X 잠금을 보유하면 다른 모든 읽기/쓰기가 차단됩니다.
-- ✅ 파티셔닝된 테이블에서 에스컬레이션 단위를 파티션으로 제한
ALTER TABLE Orders
SET (LOCK_ESCALATION = AUTO); -- 파티션 단위로 에스컬레이션
-- ✅ 에스컬레이션 완전 비활성화 (메모리 주의)
ALTER TABLE SmallLookup
SET (LOCK_ESCALATION = DISABLE);
-- ❌ 대용량 테이블에 DISABLE 적용 시 잠금 메모리 폭증 위험
ALTER TABLE HugeOrderHistory
SET (LOCK_ESCALATION = DISABLE); -- 위험
에스컬레이션 발생 여부는 확장 이벤트(Extended Events)의 lock_escalation 이벤트로 추적할 수 있습니다.
-- 잠금 에스컬레이션 추적 세션 생성
CREATE EVENT SESSION [TrackLockEscalation] ON SERVER
ADD EVENT sqlserver.lock_escalation(
ACTION(sqlserver.sql_text, sqlserver.session_id)
)
ADD TARGET package0.ring_buffer
WITH (MAX_DISPATCH_LATENCY = 5 SECONDS);
ALTER EVENT SESSION [TrackLockEscalation] ON SERVER STATE = START;
💡 TIP 배치 UPDATE/DELETE 작업에서 에스컬레이션을 피하려면 한 번에 처리하는 행 수를 제한하는 청크 처리 패턴을 사용하세요. 5,000행 미만으로 나눠 처리하면 에스컬레이션 임계치를 넘지 않습니다.
RCSI와 SNAPSHOT 격리의 버전 저장소 동작 원리
낙관적 동시성의 핵심은 **행 버전 저장소(Row Version Store)**입니다. 읽기 작업이 잠금을 획득하는 대신 이전 버전의 데이터를 읽어 쓰기와 충돌을 피합니다.
RCSI (Read Committed Snapshot Isolation)
RCSI는 데이터베이스 수준 옵션입니다. 활성화하면 READ COMMITTED 격리 수준의 읽기가 잠금 대신 버전 저장소를 사용합니다.
-- RCSI 활성화 (단독 접속 필요)
ALTER DATABASE AdventureWorks
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK AFTER 10 SECONDS; -- 10초 후 기존 연결 강제 종료
-- 활성화 여부 확인
SELECT name, is_read_committed_snapshot_on
FROM sys.databases
WHERE name = 'AdventureWorks';
RCSI가 켜지면 각 행이 수정될 때 이전 버전이 tempdb의 버전 저장소에 기록됩니다. 읽기 세션은 쿼리 시작 시점에 커밋된 가장 최신 버전을 조회합니다. 쓰기 세션은 여전히 X 잠금을 사용하므로 쓰기-쓰기 충돌은 차단됩니다.
SNAPSHOT 격리
SNAPSHOT은 세션 수준 옵션입니다. 트랜잭션 시작 시점의 스냅샷을 사용하므로 트랜잭션 내내 일관된 뷰를 제공합니다(REPEATABLE READ + No Phantom 효과).
-- SNAPSHOT 격리 활성화 (데이터베이스 수준 허용)
ALTER DATABASE AdventureWorks
SET ALLOW_SNAPSHOT_ISOLATION ON;
-- 세션에서 SNAPSHOT 격리 사용
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
-- 이 시점의 스냅샷 사용
SELECT OrderID, TotalAmount
FROM Orders
WHERE CustomerID = 1001;
WAITFOR DELAY '00:00:10'; -- 다른 세션이 데이터를 변경해도
SELECT OrderID, TotalAmount
FROM Orders
WHERE CustomerID = 1001; -- 트랜잭션 시작 시점 값을 반환
COMMIT;
버전 저장소 모니터링
-- tempdb 버전 저장소 사용량 확인
SELECT
SUM(version_store_reserved_page_count) * 8 / 1024 AS version_store_mb,
SUM(user_object_reserved_page_count) * 8 / 1024 AS user_objects_mb,
SUM(internal_object_reserved_page_count) * 8 / 1024 AS internal_objects_mb
FROM sys.dm_db_file_space_usage
WHERE database_id = 2; -- tempdb
-- 버전 저장소 정리 속도 vs 생성 속도
SELECT
version_store_reserved_page_count,
version_cleanup_rate_kb_per_s,
version_generation_rate_kb_per_s
FROM sys.dm_tran_version_store_space_usage
WHERE database_id = DB_ID('AdventureWorks');
⚠️ 주의 오래 실행되는 SNAPSHOT 트랜잭션은 버전 저장소를 비울 수 없게 만들어 tempdb를 급격히 팽창시킵니다. 장기 트랜잭션을 피하고, tempdb 자동 증가를 사전에 충분히 설정해 두세요.
교착 상태 그래프 분석과 회피 패턴
**교착 상태(Deadlock)**는 두 세션이 서로 상대방이 보유한 잠금을 기다릴 때 발생합니다. SQL Server는 교착 상태 탐지 스레드가 주기적으로 순환 대기를 감지하고, 롤백 비용이 낮은 세션을 희생자(victim)로 선택해 오류 1205를 반환합니다.
교착 상태 추적
-- 시스템 헬스 세션에서 교착 상태 XML 추출 (SQL Server 2008 이상 기본 수집)
SELECT
xdr.value('@timestamp', 'DATETIME2') AS deadlock_time,
xdr.query('.') AS deadlock_graph
FROM (
SELECT CAST(target_data AS XML) AS target_data
FROM sys.dm_xe_session_targets AS t
JOIN sys.dm_xe_sessions AS s
ON t.event_session_address = s.address
WHERE s.name = 'system_health'
AND t.target_name = 'ring_buffer'
) AS data
CROSS APPLY target_data.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS xdt(xdr)
ORDER BY deadlock_time DESC;
추출한 XML을 SSMS의 "Deadlock" 탭에 붙여넣으면 시각적 그래프를 볼 수 있습니다. 그래프에서 확인해야 할 핵심 정보는 다음과 같습니다.
- 각 노드: 관련 세션과 실행 중인 쿼리
- 화살표 방향: "이 세션이 보유한 잠금을 저 세션이 기다린다"
- 희생자 표시: 취소된 세션 (빨간 X)
전형적인 교착 패턴과 회피
-- ❌ 교착 상태를 유발하는 전형적인 패턴
-- 세션 A
BEGIN TRANSACTION;
UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 1; -- X on row 1
-- (잠시 대기)
UPDATE Accounts SET Balance = Balance + 100 WHERE AccountID = 2; -- X on row 2 대기
-- 세션 B (동시에 실행)
BEGIN TRANSACTION;
UPDATE Accounts SET Balance = Balance - 50 WHERE AccountID = 2; -- X on row 2
-- (잠시 대기)
UPDATE Accounts SET Balance = Balance + 50 WHERE AccountID = 1; -- X on row 1 대기 → 교착!
-- ✅ 잠금 순서 통일로 교착 상태 회피
-- 항상 AccountID 오름차순으로 처리
CREATE PROCEDURE usp_TransferFunds
@FromID INT, @ToID INT, @Amount DECIMAL(18,2)
AS
BEGIN
-- 낮은 ID를 먼저 잠그도록 순서 보장
DECLARE @FirstID INT = CASE WHEN @FromID < @ToID THEN @FromID ELSE @ToID END;
DECLARE @SecondID INT = CASE WHEN @FromID < @ToID THEN @ToID ELSE @FromID END;
BEGIN TRANSACTION;
BEGIN TRY
UPDATE Accounts SET Balance = Balance + CASE WHEN AccountID = @FromID THEN -@Amount ELSE @Amount END
WHERE AccountID = @FirstID;
UPDATE Accounts SET Balance = Balance + CASE WHEN AccountID = @FromID THEN -@Amount ELSE @Amount END
WHERE AccountID = @SecondID;
COMMIT;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
END;
💡 TIP 교착 상태를 완전히 없애기 어렵다면, 애플리케이션 레이어에서 1205 오류를 감지해 짧은 대기 후 재시도하는 로직을 구현하세요. 재시도는 보통 3회, 대기는 50~200ms 랜덤 지터를 권장합니다.
또 다른 회피 패턴은 UPDLOCK 힌트를 사용해 읽기 단계부터 U 잠금을 선점하는 것입니다.
-- ✅ UPDLOCK으로 SELECT 후 UPDATE 패턴의 교착 방지
BEGIN TRANSACTION;
SELECT AccountID, Balance
FROM Accounts WITH (UPDLOCK, ROWLOCK) -- 즉시 U 잠금 획득
WHERE AccountID = 1;
-- 비즈니스 로직 처리 후
UPDATE Accounts SET Balance = Balance - 100
WHERE AccountID = 1;
COMMIT;
블로킹 체인 진단
블로킹은 교착 상태와 달리 자동으로 해소되지 않습니다. 한 세션이 잠금을 보유한 채 오래 대기하면 연쇄적으로 다른 세션들이 줄지어 대기하는 블로킹 체인이 형성됩니다.
블로킹 체인 실시간 조회
-- 현재 블로킹 체인 전체 조회
WITH BlockingChain AS (
-- 블로킹 루트(head): 스스로는 아무도 기다리지 않지만 다른 세션을 막는 세션
SELECT
r.session_id,
r.blocking_session_id,
r.wait_type,
r.wait_time / 1000.0 AS wait_sec,
r.status,
SUBSTRING(t.text, (r.statement_start_offset / 2) + 1,
((CASE r.statement_end_offset
WHEN -1 THEN DATALENGTH(t.text)
ELSE r.statement_end_offset
END - r.statement_start_offset) / 2)) AS current_sql,
0 AS depth
FROM sys.dm_exec_requests AS r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE r.blocking_session_id = 0
AND EXISTS (
SELECT 1 FROM sys.dm_exec_requests r2
WHERE r2.blocking_session_id = r.session_id
)
UNION ALL
-- 블로킹 당하는 세션 재귀 조회
SELECT
r.session_id,
r.blocking_session_id,
r.wait_type,
r.wait_time / 1000.0,
r.status,
SUBSTRING(t.text, (r.statement_start_offset / 2) + 1,
((CASE r.statement_end_offset
WHEN -1 THEN DATALENGTH(t.text)
ELSE r.statement_end_offset
END - r.statement_start_offset) / 2)),
bc.depth + 1
FROM sys.dm_exec_requests AS r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
JOIN BlockingChain AS bc ON r.blocking_session_id = bc.session_id
)
SELECT
REPLICATE(' ', depth) + CAST(session_id AS VARCHAR) AS session_tree,
blocking_session_id,
wait_type,
wait_sec,
status,
current_sql
FROM BlockingChain
ORDER BY depth, session_id;
잠금 상세 조회
-- 특정 테이블에 걸린 잠금 현황
SELECT
tl.request_session_id AS session_id,
tl.resource_type,
tl.resource_description,
tl.request_mode AS lock_mode,
tl.request_status,
OBJECT_NAME(p.object_id) AS table_name,
s.login_name,
s.program_name,
s.host_name
FROM sys.dm_tran_locks AS tl
LEFT JOIN sys.partitions AS p
ON tl.resource_associated_entity_id = p.hobt_id
AND tl.resource_type IN ('PAGE', 'RID', 'KEY')
LEFT JOIN sys.dm_exec_sessions AS s
ON tl.request_session_id = s.session_id
WHERE OBJECT_NAME(p.object_id) = 'Orders' -- 조사할 테이블명
ORDER BY tl.request_session_id;
블로킹 체인 분석 후 루트 세션을 파악했다면, 해당 세션의 실행 중인 트랜잭션을 확인하고 필요시 종료할 수 있습니다.
-- 특정 세션의 활성 트랜잭션 정보
SELECT
s.session_id,
s.login_name,
s.program_name,
at.transaction_begin_time,
DATEDIFF(SECOND, at.transaction_begin_time, GETDATE()) AS open_sec,
at.transaction_state -- 0:초기화, 1:시작, 2:활성, 3:종료, 4:커밋중, 5:준비완료
FROM sys.dm_exec_sessions AS s
JOIN sys.dm_tran_session_transactions AS st ON s.session_id = st.session_id
JOIN sys.dm_tran_active_transactions AS at ON st.transaction_id = at.transaction_id
WHERE s.session_id <> @@SPID
ORDER BY at.transaction_begin_time;
-- 루트 블로커 세션 강제 종료 (DBA 권한 필요, 신중하게 사용)
-- KILL <session_id>;
낙관적 vs 비관적 동시성 전략과 애플리케이션 설계
**비관적 동시성(Pessimistic Concurrency)**은 충돌이 자주 일어날 것으로 가정하고, 데이터를 읽는 순간부터 잠금을 획득해 다른 세션의 수정을 차단합니다. SQL Server의 기본 잠금 메커니즘이 이 방식입니다.
**낙관적 동시성(Optimistic Concurrency)**은 충돌이 드물 것으로 가정하고, 잠금 없이 읽은 뒤 수정 시점에 충돌 여부를 검사합니다. RCSI, SNAPSHOT 격리 또는 애플리케이션 레이어의 타임스탬프 비교가 여기에 해당합니다.
애플리케이션 레이어 낙관적 동시성 (rowversion 사용)
-- ✅ rowversion 컬럼으로 낙관적 동시성 구현
ALTER TABLE Products ADD RowVer ROWVERSION NOT NULL;
-- 읽기: 현재 rowversion 값을 함께 가져옴
SELECT ProductID, ProductName, Price, RowVer
FROM Products
WHERE ProductID = 42;
-- 수정: 읽었을 때의 rowversion과 현재 값을 비교
UPDATE Products
SET Price = 29900, ProductName = '프리미엄 키보드'
WHERE ProductID = 42
AND RowVer = 0x00000000000007D2; -- 애플리케이션에서 전달한 값
-- 영향 받은 행이 0이면 다른 세션이 먼저 수정한 것
IF @@ROWCOUNT = 0
BEGIN
THROW 50001, '다른 사용자가 해당 데이터를 수정했습니다. 다시 조회 후 수정하세요.', 1;
END
전략 선택 가이드
| 상황 | 권장 전략 |
|---|---|
| 읽기 중심, 충돌 드묾 (예: 상품 조회) | RCSI + 낙관적 동시성 |
| 쓰기 충돌 잦음 (예: 재고 차감) | 비관적 동시성 (UPDLOCK) |
| 장기 보고서 쿼리 + OLTP 혼용 | SNAPSHOT 격리 (보고서 세션에만) |
| 회계·금융 정합성 최우선 | SERIALIZABLE 또는 비관적 동시성 |
| 마이크로서비스, 낮은 결합도 | 낙관적 동시성 + 재시도 패턴 |
💡 TIP RCSI는 기존 READ COMMITTED 코드를 변경하지 않고도 활성화할 수 있어 가장 도입이 쉽습니다. 단, tempdb 부하가 증가하므로 tempdb를 별도 디스크에 분리하고 여러 파일로 구성해 두는 것이 전제 조건입니다.
요약
- SQL Server의 다섯 가지 격리 수준은 더티 읽기 / 비반복 읽기 / 팬텀 읽기의 허용 여부로 구분되며, 높은 격리일수록 잠금 경합이 커집니다.
- S/X/U/IS/IX 잠금은 계층 구조로 동작하며, 잠금 수가 임계치를 초과하면 행 → 페이지 → 테이블로 에스컬레이션됩니다.
LOCK_ESCALATION = AUTO로 파티션 단위 에스컬레이션을 유도할 수 있습니다. - RCSI는 쿼리 시작 시점, SNAPSHOT은 트랜잭션 시작 시점의 버전을 읽어 읽기-쓰기 경합을 없애지만, 오랜 트랜잭션이 버전 저장소를 팽창시키므로 tempdb 용량 관리가 필수입니다.
- 교착 상태는 잠금 획득 순서 통일, UPDLOCK 힌트 사용, RCSI/SNAPSHOT 도입으로 빈도를 크게 줄일 수 있으며, 불가피하다면 애플리케이션 재시도 로직으로 보완합니다.
- 블로킹 체인은
sys.dm_exec_requests와sys.dm_tran_locks를 재귀 CTE로 조합해 루트 블로커와 대기 세션을 한눈에 파악할 수 있습니다. - 낙관적 동시성은 읽기 중심 / 충돌 희소 환경에서, 비관적 동시성은 쓰기 충돌이 잦은 환경에서 더 효율적이며,
ROWVERSION컬럼으로 애플리케이션 레이어 낙관적 동시성을 구현할 수 있습니다.
연습문제
-
아래 두 세션이 동시에 실행될 때 교착 상태가 발생하는 이유를 설명하고, 교착 상태를 방지하도록 두 세션의 UPDATE 순서를 수정하세요.
-- 세션 A BEGIN TRANSACTION; UPDATE Inventory SET Qty = Qty - 10 WHERE ItemID = 'A001'; UPDATE Inventory SET Qty = Qty + 10 WHERE ItemID = 'B002'; COMMIT; -- 세션 B BEGIN TRANSACTION; UPDATE Inventory SET Qty = Qty - 5 WHERE ItemID = 'B002'; UPDATE Inventory SET Qty = Qty + 5 WHERE ItemID = 'A001'; COMMIT;힌트 두 세션이 서로 다른 순서로 잠금을 획득하는 구조를 주목하세요.
-
현재 데이터베이스에서 30초 이상 열려 있는 활성 트랜잭션의
session_id, 시작 시간, 경과 초, 세션 로그인 이름을 조회하는 쿼리를 작성하세요.힌트
sys.dm_tran_active_transactions,sys.dm_tran_session_transactions,sys.dm_exec_sessions를 조인하세요. -
SalesOrder테이블에RowVer ROWVERSION컬럼이 있다고 가정합니다. 애플리케이션이OrderID = 500인 행을 읽은 뒤Status를 'Shipped'로 업데이트하려 합니다. 낙관적 동시성 패턴으로 다른 세션이 먼저 수정했을 경우 오류 메시지(50010, '다른 세션이 데이터를 수정했습니다.')를 발생시키는 T-SQL을 작성하세요.힌트
@@ROWCOUNT를 확인하고 0이면THROW로 오류를 발생시키세요. -
Products테이블의 잠금 에스컬레이션 정책을 파티션 단위(AUTO)로 변경하고, 변경 전후의 정책 값을sys.tables카탈로그 뷰에서 조회하는 스크립트를 작성하세요.힌트
sys.tables의lock_escalation_desc컬럼을 활용하세요.
💡 연습문제 풀이
불러오는 중…
댓글 0
“MSSQL 심화” 강좌에 대한 댓글입니다.