#title 이전 Row 접근하기
[[TableOfContents]]

==== 개요 ====
SQL을 작성하다 보면 이전 Row 또는 바로 다음 Row를 접근 할 필요가 있다. 예를들어, 기간 업무를 수행하는 어플리케이션에서 로그인, 로그아웃에 대한 기록을 남긴다고 가정해 보자. 테이블은 다음과 같은 구조일 것이다. 

{{{
empid      log_dt                  log_type
---------- ----------------------- --------
yasicom    2007-05-30 22:11:33.570 1
mkex       2007-05-30 22:11:34.560 1
yasicom    2007-05-30 22:11:37.577 0
endless    2007-05-30 22:11:41.570 1
soma       2007-05-30 22:11:43.563 1
mkex       2007-05-30 22:11:48.560 0
yasicom    2007-05-30 22:11:59.557 1
soma       2007-05-30 22:12:03.563 0
endless    2007-05-30 22:12:04.553 0
endless    2007-05-30 22:23:17.363 1
}}} 

만약 empid별 시스템의 사용 시간을 알고 싶다면 어떻게 SQL을 작성 할 것인가? 이런 경우 필자가 아는 몇 가지 솔루션을 소개하도록 하겠다. (참고: log_type = 1 이면 로그인, 0 이면 로그아웃)

==== 테스트 데이터 만들기 ====
{{{
if object_id('tempdb..#temp') is not null
        drop table #temp
 
create table #temp(
        empid varchar(10)
,       log_dt datetime
,       log_type bit
)

insert #temp values('yasicom', getdate(), 1)
waitfor delay '00:00:01'

insert #temp values('mkex', getdate(), 1)
waitfor delay '00:00:03'

insert #temp values('yasicom', getdate(), 0)
waitfor delay '00:00:04'

insert #temp values('endless', getdate(), 1)
waitfor delay '00:00:02'

insert #temp values('soma', getdate(), 1)
waitfor delay '00:00:05'

insert #temp values('mkex', getdate(), 0)
waitfor delay '00:00:11'

insert #temp values('yasicom', getdate(), 1)
waitfor delay '00:00:04'

insert #temp values('soma', getdate(), 0)
waitfor delay '00:00:01'

insert #temp values('endless', getdate(), 0)
waitfor delay '00:00:05'

insert #temp values('endless', getdate(), 1)
go
}}}

==== SQL1: self join을 이용한 방법 ====
{{{
--1. 먼저 임시 테이블에 넣어서 일련번호를 생성한다. 정렬 순서가 맞아야 한다. 
--   2005 버전의 경우는 row_number()함수를 이용한다. 임시테이블에 넣기 귀찮으므로
--   row_number()를이용하겠다. 

set statistics io on
with tmp(rownum, empid, log_dt, log_type)
as
(
	select
		row_number() over(order by empid, log_dt, log_type desc) rownum
	,	empid
	,	log_dt
	,	log_type
	from #temp
)
select 
	a.empid
,	sum(datediff(ss, a.log_dt, b.log_dt)) usedtime
from tmp a inner join tmp b
on a.rownum = b.rownum - 1 -- 여기가 핵심이다.
and a.empid = b.empid
where cast(a.log_type as varchar) + cast(b.log_type as varchar) = '10'
group by a.empid
go
}}}

==== SQL2: cross join과 group by를 이용한 방법 ====
{{{
with tmp(rownum, empid, log_dt, log_type)
as
(
	select
		row_number() over(order by empid, log_dt, log_type desc) rownum
	,	empid
	,	log_dt
	,	log_type
	from #temp
)
select
	empid
,	sum(datediff(ss, login_dt, logout_dt)) usedtime
from (
	select 
		a.empid
	,	min(case when b.id = 1 and log_type = 1 then log_dt end) login_dt
	,	min(case when b.id = 2 and log_type = 0 then log_dt end) logout_dt
	from tmp a cross join (select 1 id union all select 2) b
	group by 
		a.empid
	,	case when b.id = 1 then rownum else rownum - 1 end --여기가 핵심이다.
) t
group by empid
go
}}}

==== 이런 비슷한 것도.. ====
{{{
--WITH Game
--AS
--(
--SELECT 'A' CharName, 1 No1, 1 Val01, 0 Val02, 1 Val03 UNION ALL
--SELECT 'B' CharName, 2 No1, 0 Val01, 0 Val02, 0 Val03 UNION ALL
--SELECT 'C' CharName, 3 No1, 0 Val01, 2 Val02, 1 Val03 UNION ALL
--SELECT 'D' CharName, 4 No1, 1 Val01, 1 Val02, 0 Val03 UNION ALL
--SELECT 'E' CharName, 5 No1, 2 Val01, 2 Val02, 1 Val03 UNION ALL
--SELECT 'F' CharName, 6 No1, 1 Val01, 0 Val02, 2 Val03 UNION ALL
--SELECT 'G' CharName, 7 No1, 0 Val01, 2 Val02, 0 Val03 UNION ALL
--SELECT 'H' CharName, 8 No1, 1 Val01, 0 Val02, 2 Val03 UNION ALL
--SELECT 'I' CharName, 9 No1, 0 Val01, 2 Val02, 2 Val03 UNION ALL
--SELECT 'J' CharName, 10 No1, 1 Val01, 0 Val02, 1 Val03 UNION ALL
--SELECT 'K' CharName, 11 No1, 0 Val01, 2 Val02, 2 Val03 UNION ALL
--SELECT 'L' CharName, 12 No1, 1 Val01, 2 Val02, 0 Val03 UNION ALL
--SELECT 'M' CharName, 13 No1, 0 Val01, 1 Val02, 1 Val03 UNION ALL
--SELECT 'N' CharName, 14 No1, 2 Val01, 0 Val02, 0 Val03 UNION ALL
--SELECT 'O' CharName, 15 No1, 1 Val01, 2 Val02, 1 Val03 UNION ALL
--SELECT 'P' CharName, 16 No1, 2 Val01, 1 Val02, 2 Val03
--)
--SELECT * INTO #Temp 
--FROM Game

SELECT
	GroupNo
,	MIN(CASE WHEN No1%2=1 THEN No1 END) NoA
,	MIN(CASE WHEN No1%2=1 THEN CharName END) A
,	MIN(CASE WHEN No1%2=1 THEN Val01 END) V1
,	MIN(CASE WHEN No1%2=1 THEN Val02 END) V2
,	MIN(CASE WHEN No1%2=1 THEN Val03 END) V3
,	MIN(CASE WHEN No1%2=0 THEN No1 END) NoB
,	MIN(CASE WHEN No1%2=0 THEN CharName END) B
,	MIN(CASE WHEN No1%2=0 THEN Val01 END) V4
,	MIN(CASE WHEN No1%2=0 THEN Val02 END) V5
,	MIN(CASE WHEN No1%2=0 THEN Val03 END) V6
FROM (
	SELECT 
		No1
	,	CharName
	,	Val01
	,	Val02
	,	Val03
	,	(Row_NUmBER() OVER(ORDER BY No1)+1)/2 GroupNo
	FROM #Temp
) T
GROUP BY
	GroupNo

}}}

==== 결론 ====
두 쿼리의 비용은 비슷하다. 현재로써는 그래도 2번째 쿼리가 비용은 더 적게 든다. 비용이 비슷하고, 성능이 비슷하다면 어떤 것을 쓰겠는가? 시스템의 리소스에 여유가 있다면 첫 번째 쿼리를 권하고 싶다. 왜냐하면 좀 더 이해하기 쉽고 명확하기 때문이다. 그러므로 유지보수 측면에서 보면 유리하다. 만약 그래도 테이블을 2번 읽는 것이 꿉꿉하다면 2번째 방법을 사용하기 바란다.