English version
This note covers two practical SQL Server topics that often show up in production tuning: lock hints and using temp tables to stabilize UPDATE statements.
The important point is that hints and temp tables are not magic performance switches. They are tools that can influence the optimizer and locking behavior, but they should be validated with execution plans, STATISTICS IO, and STATISTICS TIME.
ROWLOCK and NOLOCK
WITH(ROWLOCK) does not guarantee that SQL Server will always take only row-level locks. It is a hint that asks SQL Server to prefer row locks when possible. SQL Server can still choose page or table locks depending on the situation, and lock escalation can still happen.
That means ROWLOCK should not be understood as a strict command. It is a suggestion to the lock manager and optimizer.
WITH(NOLOCK) is also common, but it has an important risk. It allows dirty reads. A query can read data that has not been committed yet, and that data might later be rolled back. Because of that, NOLOCK should not be used casually when correctness matters.
Why route UPDATE through a temp table
A direct UPDATE usually looks like this:
UPDATE table_name
SET column1 = value1,
column2 = value2
WHERE condition;
In real workloads, the execution plan for an UPDATE can change depending on parameters, row counts, and statistics. If the plan is unstable, one useful pattern is to first collect the target keys into a temp table, then update the base table by joining to that temp table.
The flow is:
- Check the indexes on the source table.
- Select only the target keys into
#temp. - Add hints only when there is a clear reason.
- Run the UPDATE by joining the base table to the temp table.
Example:
IF OBJECT_ID('tempdb.dbo.#TEMP01') IS NOT NULL
DROP TABLE #TEMP01;
SELECT CustomerID,
ContactName,
Address
INTO #TEMP01
FROM CUSTOMERS WITH (NOLOCK)
WHERE CustomerID = @CustomerID
AND ContactName = @ContactName
AND Address = @Address;
UPDATE A
SET A.CustomerName = @CustomerName,
A.City = @City,
A.PostalCode = @PostalCode
FROM #TEMP01 AS TP1
INNER LOOP JOIN CUSTOMERS AS A
ON A.CustomerID = TP1.CustomerID
AND A.ContactName = TP1.ContactName
AND A.Address = TP1.Address;
This can help when the target row set is small, the temp table locks down the exact rows to update, and the base table can use a good index. It also makes the UPDATE phase simpler because the expensive filtering work has already happened.
But this pattern is not always better. Creating and reading the temp table has its own cost. NOLOCK may be unsafe. Join hints can lock the optimizer into a bad plan later. Always compare the direct UPDATE and the temp table pattern using actual execution plans and IO/time statistics.
Korean notes
회사 쿼리를 보다가 WITH(NOLOCK)은 자주 봤지만 WITH(ROWLOCK)은 처음 봤다. 이번 글은 그 메모에서 시작해서, SQL Server에서 lock hint를 어떻게 이해해야 하는지와 UPDATE 튜닝 때 temp table을 거치는 패턴을 정리한 것이다.
실무 쿼리 튜닝에서는 문법 자체보다 "SQL Server가 어떤 실행 계획을 선택하게 만들 것인가"가 더 중요하다. ROWLOCK, NOLOCK, join hint, temp table은 모두 그 선택에 영향을 줄 수 있지만, 동시에 잘못 쓰면 나중에 더 큰 문제가 될 수 있다.
Lock hint: WITH(NOLOCK)과 WITH(ROWLOCK)
회사 쿼리를 보다가 WITH(NOLOCK)은 자주 봤지만 WITH(ROWLOCK)은 처음 봤다.
WITH(ROWLOCK)을 적는다고 해서 SQL Server가 무조건 row 단위 lock만 잡는 것은 아니다. Lock hint는 말 그대로 옵티마이저와 lock manager에게 "가능하면 이 방향으로 잡아달라"고 알려주는 힌트에 가깝다. 상황에 따라 page lock이나 table lock으로 커질 수 있고, lock escalation도 발생할 수 있다.
그래서 ROWLOCK은 "항상 row lock 보장"으로 이해하면 안 되고, 특정 상황에서 lock 범위를 줄이고 싶을 때 쓰는 권고성 힌트로 봐야 한다.
UPDATE 튜닝: 바로 업데이트하지 않고 temp table을 거치는 방식
일반적인 UPDATE 문은 보통 이렇게 쓴다.
UPDATE table_name
SET column1 = value1,
column2 = value2
WHERE condition;
그런데 실무에서는 파라미터 값에 따라 실행 계획이 달라지고, 그 결과로 UPDATE 속도 차이가 크게 날 수 있다. 이런 경우 바로 UPDATE를 치는 대신, 먼저 대상 key를 temp table에 담아두고 그 temp table을 기준으로 UPDATE하는 방식도 쓴다.
핵심 흐름은 이렇다.
- 원본 테이블의 인덱스를 먼저 확인한다.
- 조건에 맞는 대상 row의 key 컬럼만
#temp에 담는다. - 필요하면 SELECT 단계에서 타야 하는 인덱스 힌트를 준다.
- UPDATE는
#temp와 원본 테이블을 JOIN해서 수행한다.
예를 들어 CUSTOMERS 테이블이 있다고 하자.
컬럼:
CustomerIDCustomerNameContactNameAddressCityPostalCodeCountry
인덱스 후보:
CustomerIDContactNameAddress
먼저 업데이트 대상이 되는 key를 temp table에 담는다.
IF OBJECT_ID('tempdb.dbo.#TEMP01') IS NOT NULL
DROP TABLE #TEMP01;
SELECT CustomerID,
ContactName,
Address
INTO #TEMP01
FROM CUSTOMERS WITH (NOLOCK)
WHERE CustomerID = @CustomerID
AND ContactName = @ContactName
AND Address = @Address;
그 다음 temp table과 원본 테이블을 JOIN해서 UPDATE한다.
UPDATE A
SET A.CustomerName = @CustomerName,
A.City = @City,
A.PostalCode = @PostalCode
FROM #TEMP01 AS TP1
INNER LOOP JOIN CUSTOMERS AS A
ON A.CustomerID = TP1.CustomerID
AND A.ContactName = TP1.ContactName
AND A.Address = TP1.Address;
여기서 INNER LOOP JOIN은 Nested Loops Join을 유도하는 join hint다. temp table에 담긴 대상 row 수가 작고, 원본 테이블 쪽에서 적절한 인덱스를 탈 수 있다면 유리할 수 있다.
다만 이 방식도 무조건 정답은 아니다. temp table을 만드는 비용이 있고, WITH(NOLOCK)은 dirty read 가능성이 있으므로 업데이트 대상 선정에 써도 되는지 업무적으로 확인해야 한다. 또한 join hint는 실행 계획을 강제하므로, 데이터 분포가 바뀌었을 때 오히려 느려질 수 있다.
정리하면, temp table을 거치는 UPDATE는 다음 상황에서 검토할 만하다.
- 파라미터에 따라 UPDATE 실행 시간이 크게 흔들릴 때
- 업데이트 대상 row를 먼저 안정적으로 고정하고 싶을 때
- 원본 테이블에서 어떤 인덱스를 타야 하는지 명확할 때
- 복잡한 조건을 한 번 계산한 뒤 UPDATE 단계에서는 단순 JOIN만 하고 싶을 때
반대로 대상 row가 매우 적거나 조건이 단순하고 실행 계획이 안정적이면, 바로 UPDATE하는 편이 더 간단할 수 있다.
정리
WITH(ROWLOCK)은 무조건 row lock을 보장하는 명령이 아니라 lock 방향을 제안하는 hint에 가깝다.WITH(NOLOCK)은 dirty read 가능성이 있으므로, 업데이트 대상 선정에 써도 되는지 업무적으로 확인해야 한다.- UPDATE 성능이 파라미터에 따라 크게 흔들리면, 대상 key를 temp table에 먼저 담고 UPDATE하는 방식을 검토할 수 있다.
- temp table 방식은 대상 row를 안정적으로 고정하고 실행 계획을 단순화하는 데 도움이 될 수 있다.
- 하지만 hint와 temp table은 모두 비용이 있으므로, 실행 계획과
STATISTICS IO/TIME으로 검증해야 한다.