#title Split 패턴
[[TableOfContents]]
==== 일반적인 사항 ====
가끔씩 Split이란 단어로 여러 솔루션을 접하고는 한다. 오라클은 InStr함수가 존재하여 매우 쉽게 해결할 수 있다. 하지만 MS-SQL Server는 좀 처럼 쉽지가 않다. 대부분의 경우 UI에서 여러 데이터 항목을 체크하여 저장 프로시저로 '1;3;5;7;10'과 같은 형태의 매개변수로 사용된다. 필자의 홈페이지를 포함하여 커뮤니티에는 '1;3;5;7;10'로 합치고, 이것을 5개의 행으로 다시 만드는 솔루션으로 charindex를 이용하여 루프를 돌리는 솔루션이 많이 있다. 특히 2000 버전에서는 루프 자체가 쥐약이기 때문에 더욱 성능을 떨어뜨렸다. 하지만 SQL Server 2005에서는 XML에 대한 여러 지원으로 이를 쉽게 풀 수 있다. 2005에서는 함수의 수행성능도 눈에 띄게 향상되었기 때문에 2000에 비해서는 함수내의 루프를 이용한 솔루션도 괜찮다. XML과 함수 중에서는 함수가 더 낫다. 그러나 XML을 이용하는 것은 소스 코드의 이해가 쉽다. 또한 숫자테이블을 이용한 Join 솔루션도 있다.
==== 솔루션1 ====
{{{
--테이블 준비
--drop table #temp
create table #temp(t char(1), val int)
insert #temp values('a', 1)
insert #temp values('a', 2)
insert #temp values('a', 3)
insert #temp values('b', 4)
insert #temp values('b', 5)
insert #temp values('b', 6)
insert #temp values('c', 7)
go
select
t
, stuff((
select ';' + cast(val as varchar(100)) as 'text()'
from #temp where a.t = t for xml path('')),1,1,'') rs
from (select distinct t from #temp) a
/* 결과
t rs
---- ----------------
a 1;2;3
b 4;5;6
c 7
*/
}}}
==== 솔루션2 ====
{{{
--테이블준비
select
t
, stuff((
select ';' + cast(val as varchar(100)) as 'text()'
from #temp where a.t = t for xml path('')),1,1,'') rs
into #xml
from (select distinct t from #temp) a
--쪼개기
select
x.t
, convert(varchar(10), y.item.query('text()')) val
from (
select
t
, convert(xml, '' + replace(rs, ';', '') + '') xitem
from #xml) x cross apply x.xitem.nodes('/r') as y(item)
/* 결과
t val
---- ----------
a 1
a 2
a 3
b 4
b 5
b 6
c 7
*/
/*
--다중값 매개변수 처리
DECLARE @GameSeqs varchar(5000);
SET @GameSeqs = '34;36'
SET @GameSeqs = '' + replace(@GameSeqs, ';', '') + '';
SELECT
CONVERT(varchar(10), y.item.query('text()')) val
FROM (SELECT CONVERT(xml, @GameSeqs) xitem) x
CROSS APPLY x.xitem.nodes('/R') as y(item)
*/
}}}
==== 쉽게 .. ====
{{{
select
convert(xml, '' + replace(row, ',', '') + '').value('/r[1]', 'varchar(50)') row1
, convert(xml, '' + replace(row, ',', '') + '').value('/r[2]', 'varchar(50)') row1
, convert(xml, '' + replace(row, ',', '') + '').value('/r[3]', 'varchar(50)') row1
from (select '1,2,3,4,5' row) t
}}}
{{{
select
convert(xml, row).value('/r[1]', 'varchar(50)') row1
, convert(xml, row).value('/r[2]', 'varchar(50)') row2
, convert(xml, row).value('/r[3]', 'varchar(50)') row3
, convert(xml, row).value('/r[3]', 'varchar(50)') row4
from (select '' + replace('기타,스타크래프트,10,0', ',', '') + '' row) t
}}}
==== 솔루션3 ====
{{{
----CSV형태의 컬럼을 대상으로 쿼리하기
IF OBJECT_ID('dbo.instr') IS NOT NULL
DROP FUNCTION dbo.instr
GO
--아쉽게도 오라클의 instr 기능을 완벽히 수행하는 함수가 MSSQL은 없다.
--사용자정의함수를 만들자.
CREATE FUNCTION dbo.instr(@str VARCHAR(8000), @delimiter VARCHAR(100), @pos smallint)
RETURNS VARCHAR(8000)
AS
BEGIN
DECLARE
@i smallint
, @p1 smallint
, @p2 smallint
SET @i = 1
SET @p2 = 1
WHILE(@i <= @pos)
BEGIN
SET @p1 = @p2
SET @p2 = CHARINDEX(@delimiter, @str, CASE WHEN @p2 > 1 THEN @p2 + 1 ELSE @p2 END)
SET @i = @i + 1
END
SET @p1 = CASE WHEN @i > 2 THEN @p1 + 1 ELSE @p1 END
SET @p2 = CASE WHEN @p2 > 0 THEN @p2 ELSE 8000 END
RETURN SUBSTRING(@str, @p1, @p2 - @p1)
END
GO
CREATE TABLE #temp(m_str VARCHAR(100))
INSERT #temp VALUES('aaa;bbb;ccc;ddd')
INSERT #temp VALUES('111;222;333;444')
INSERT #temp VALUES('1;2')
CREATE TABLE #dumy(seq INT)
INSERT #dumy VALUES(1)
INSERT #dumy VALUES(2)
INSERT #dumy VALUES(3)
INSERT #dumy VALUES(4)
INSERT #dumy VALUES(5)
INSERT #dumy VALUES(6)
INSERT #dumy VALUES(7)
INSERT #dumy VALUES(8)
INSERT #dumy VALUES(9)
INSERT #dumy VALUES(10)
--CSV형태의 컬럼의 구분기호 개수로 전체 문자열 구분개수를 알아낸다.
--2000버전에서 테스트: 약6분, 2005버전에서 약 25초(데이터는 'aaa;bbb;ccc;ddd;aaa;bbb;ccc;ddd' 10만건
--len(m_str) - len(replace(m_str, ';', '')) + 1
--아..글고..문자열이 길어지면 허당
--글고..clr을 이용하면 매우 좋아..이런 경우는..
SELECT
dbo.instr(m_str, ';', seq) m_str
FROM (
SELECT
LEN(m_str) - LEN(REPLACE(m_str, ';', '')) + 1 str_cnt
, m_str
FROM #temp) a INNER JOIN #dumy b
ON a.str_cnt >= b.seq
GO
}}}
==== 또 다른 형태: 구분자로 구분된 문자열 숫자의 합계 구하기 ====
{{{
drop table #temp
create table #temp
(
idx int
, vals varchar(4000)
)
go
insert into #temp values (1, '1/2')
insert into #temp values (2, '1/2/3/4/5/6/7/8/9/10')
insert into #temp values (3, '1')
insert into #temp values (4, '40/30/20/10')
insert into #temp values (5, '4230/3/210/11110')
go
with dumy
as
(
select 1 seq
union all
select seq + 1 from dumy
where seq + 1 <= 2000
), temp
as
(
select
a.idx
, b.seq
, a.vals
, b.pos1
, case when b.pos2 = 0 then len(a.vals)+1 else b.pos2-1 end pos2
, b.delimiter
from #temp a
cross apply(
select
seq
, charindex('/', '/' + a.vals, seq) pos1
, charindex('/', '/' + a.vals, seq+1) pos2
, substring('/' + a.vals, seq, 1) delimiter
from dumy
where seq <= len(a.vals) -- len(replace(a.vals, '/', ''))+1
) b
where 1=1
and b.delimiter = '/' -- 이 조건을 빼면 문자열 세로 -> 가로로
--and a.idx = 5
)
select
idx
, sum(convert(int, substring(vals, pos1, pos2 - pos1)))
from temp
group by
idx
order by idx
option (maxrecursion 0);
}}}
==== 문자열 split ====
{{{
/*
--drop table dummy
;with dummy
as
(
select 1 seq
union all
select seq + 1 from dummy
where seq + 1 <= 100000
)
select *
into dummy
from dummy
option (maxrecursion 0)
go
create unique clustered index cix
on dummy(seq)
go
*/
;with temp
as
(
select 1 id, '1,200,3,4,5' string union all
select 2 id, '2,3,5,100,20'
)
select
a.id
, a.string
--, b.seq
--, charindex(',', a.string, b.seq+1)
, replace(substring(a.string, b.seq, case when charindex(',', a.string, b.seq+1) = 0 then 100 else charindex(',', a.string, b.seq+1) end - b.seq), ',', '') split
, b.idx
from temp a
cross apply (
select
seq
, row_number() over(order by seq) idx
from dummy
where len(a.string) >= seq
and substring(',' + a.string, seq, 1) = ','
) b
where 1=1
}}}
==== 더 좋은 성능 ====
위에서 제시한 솔루션보다 더 좋은 성능을 발휘하는 솔루션은 CLR을 이용하는 것이다. 성능은 필자가 아는 솔루션 중에는 단연 최고다. 하지만 CLR 버전에 따른 차이의 극복 및 프로그램 유지보수의 문제가 있다. 자세한 사항은 아래의 첨부된 파일을 참고하기 바란다.
attachment:UFN_CLR_Split.zip
attachment:Split패턴/CLR_UAgg__RowsToCol.zip
사용방법은 다음과 같다.
{{{
------------------
--Agg_RowsToColumn
------------------
--관련함수: UFN_CLR_Split
CREATE TABLE BookAuthors
(
BookID int NOT NULL,
AuthorName nvarchar(200) NOT NULL
);
INSERT BookAuthors VALUES(1, 'Johnson');
INSERT BookAuthors VALUES(2, 'Taylor');
INSERT BookAuthors VALUES(3, 'Steven');
INSERT BookAuthors VALUES(2, 'Mayler');
INSERT BookAuthors VALUES(3, 'Roberts');
INSERT BookAuthors VALUES(3, 'Michaels');
SELECT BookID, dbo.Agg_RowsToColumn(AuthorName)
FROM BookAuthors
GROUP BY BookID;
/*결과
BookID
----------- -------------------------
1 Johnson
2 Taylor,Mayler
3 Roberts,Michaels,Steven
(3 row(s) affected)
*/
---------------
--UFN_CLR_Split
---------------
--관련함수: Agg_RowsToColumn
select * from dbo.UFN_CLR_Split('a;b;c;d;e;f;g', ';');
/* 결과
SplitCol
-----------------------
a
b
c
d
e
f
g
(7 row(s) affected)
*/
--Agg_RowsToColumn함수와 UFN_CLR_Split 함수를 연합하여 응용
WITH exTable(BookID, SplitCol) AS
(
SELECT
BookID
, dbo.Agg_RowsToColumn(AuthorName) SplitCol
FROM BookAuthors
GROUP BY BookID
)
SELECT
A.BookID
, CONVERT(NVARCHAR(30), A.SplitCol) SplitCol_A
, CONVERT(NVARCHAR(30), B.SplitCol) SplitCol_B
FROM exTable A CROSS APPLY dbo.UFN_CLR_Split(A.SplitCol, ',') B
/* 결과
BookID SplitCol_A SplitCol_B
----------- ------------------------------ ------------------------------
1 Johnson Johnson
2 Taylor,Mayler Taylor
2 Taylor,Mayler Mayler
3 Roberts,Michaels,Steven Roberts
3 Roberts,Michaels,Steven Michaels
3 Roberts,Michaels,Steven Steven
(6 row(s) affected)
*/
}}}
==== 또 다른 예제 ====
다음과 같이 row가 구성되어 있다. 구분자고 '/'를 쓰는데, 가장 큰 값을 구하고 싶다.
{{{
1086505750/563631750
100
1114881625/583485375
139066000/1100000000/993888382
293533042/367173400
110000000/110000000/111233343/234234277
}}}
다음가 같이 하면 된다.
{{{
;with temp
as
(
select '1086505750/563631750' x union all
select '100' union all
select '1114881625/583485375' union all
select '139066000/1100000000/993888382' union all
select '293533042/367173400' union all
select '110000000/110000000/111233343/234234277'
)
select
a.x
, b.max_x
from temp a
cross apply (
select
max(y.item.value('.[1]', 'int')) max_x
from (select convert(xml, '' + replace(a.x, '/', '') + '') xitem) x
cross apply x.xitem.nodes('/r') as y(item)
) b
}}}
==== 적용 범위 ====
* Microsoft SQL Server 2005
==== 참고 자료 ====
* [http://www.sommarskog.se/arrays-in-sql-2005.html Arrays and Lists in SQL Server 2005]