질문의 길이를 용서하십시오. 상황을 시연하기위한 테스트 스크립트와 솔루션에 대한 최선의 시도를 포함 시켰습니다. 여러 소스에서 순서대로 추출[행당 한 단어]를 [한 행에 여러 단어가있는] 행의 행에 결합하십시오.
test_WORDS
= 단어 :은 두 개의 테이블이 있습니다.
OBJ_FK
열은 소스의 ID입니다.WORD_ID
은 소스 내에서 고유 한 단어 자체의 식별자입니다. 각 행에는 한 단어가 들어 있습니다.test_PHRASE
= 검색 할 구의 목록은test_WORDS
입니다.PHRASE_TEXT
열은 'foo bar'(아래 참조)와 같이 공백으로 구분 된 구문으로 각 행에 여러 단어가 포함되어 있습니다.
요구 사항 : 돌려이 test_PHRASE
에서 일치 구문의 시작 test_WORDS
에서 첫 번째 단어.
아래의 RBAR 접근 방식을 피하기 위해 뭔가를 기반으로 설정하는 것이 좋습니다. 또한 내 솔루션은 5 단어로 제한됩니다. 나는 20 단어까지 지원해야한다. test_PHRASE
행의 단어를 커서가없는 test_WORD
의 연속 행과 일치시킬 수 있습니까?
임시 단어를 임시 테이블로 분리 한 후 문제는 행 순서대로 두 세트의 일치하는 부분으로 함께 나타납니다.
-- Create test data
CREATE TABLE [dbo].[test_WORDS](
[OBJ_FK] [bigint] NOT NULL, --FK to the source object
[WORD_ID] [int] NOT NULL, --The word order in the source object
[WORD_TEXT] [nvarchar](50) NOT NULL,
CONSTRAINT [PK_test_WORDS] PRIMARY KEY CLUSTERED
(
[OBJ_FK] ASC,
[WORD_ID] ASC
)
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[test_PHRASE](
[ID] [int], --PHRASE ID
[PHRASE_TEXT] [nvarchar](150) NOT NULL --Space-separated phrase
CONSTRAINT [PK_test_PHRASE] PRIMARY KEY CLUSTERED
(
[ID] ASC
)
)
GO
INSERT INTO dbo.test_WORDS
SELECT 1,1,'aaa' UNION ALL
SELECT 1,2,'bbb' UNION ALL
SELECT 1,3,'ccc' UNION ALL
SELECT 1,4,'ddd' UNION ALL
SELECT 1,5,'eee' UNION ALL
SELECT 1,6,'fff' UNION ALL
SELECT 1,7,'ggg' UNION ALL
SELECT 1,8,'hhh' UNION ALL
SELECT 2,1,'zzz' UNION ALL
SELECT 2,2,'yyy' UNION ALL
SELECT 2,3,'xxx' UNION ALL
SELECT 2,4,'www'
INSERT INTO dbo.test_PHRASE
SELECT 1, 'bbb ccc ddd' UNION ALL --should match
SELECT 2, 'ddd eee fff' UNION ALL --should match
SELECT 3, 'xxx xxx xxx' UNION ALL --should NOT match
SELECT 4, 'zzz yyy xxx' UNION ALL --should match
SELECT 5, 'xxx www ppp' UNION ALL --should NOT match
SELECT 6, 'zzz yyy xxx www' --should match
-- Create variables
DECLARE @maxRow AS INTEGER
DECLARE @currentRow AS INTEGER
DECLARE @phraseSubsetTable AS TABLE(
[ROW] int IDENTITY(1,1) NOT NULL,
[ID] int NOT NULL, --PHRASE ID
[PHRASE_TEXT] nvarchar(150) NOT NULL
)
--used to split the phrase into words
--note: No permissions to sys.dm_fts_parser
DECLARE @WordList table
(
ID int,
WORD nvarchar(50)
)
--Records to be returned to caller
DECLARE @returnTable AS TABLE(
OBJECT_FK INT NOT NULL,
WORD_ID INT NOT NULL,
PHRASE_ID INT NOT NULL
)
DECLARE @phrase AS NVARCHAR(150)
DECLARE @phraseID AS INTEGER
-- Get subset of phrases to simulate a join that would occur in production
INSERT INTO @phraseSubsetTable
SELECT ID, PHRASE_TEXT
FROM dbo.test_PHRASE
--represent subset of phrases caused by join in production
WHERE ID IN (2,3,4)
-- Loop each phrase in the subset, split into rows of words and return matches to the test_WORDS table
SET @maxRow = @@ROWCOUNT
SET @currentRow = 1
WHILE @currentRow <= @maxRow
BEGIN
SELECT @phrase=PHRASE_TEXT, @phraseID=ID FROM @phraseSubsetTable WHERE row = @currentRow
--clear previous phrase that was split into rows
DELETE FROM @WordList
--Recursive Function with CTE to create recordset of words, one per row
;WITH Pieces(pn, start, stop) AS (
SELECT 1, 1, CHARINDEX(' ', @phrase)
UNION ALL
SELECT pn + 1, stop + 1, CHARINDEX(' ', @phrase, stop + 1)
FROM Pieces
WHERE stop > 0)
--Create the List of words with the CTE above
insert into @WordList
SELECT pn,
SUBSTRING(@phrase, start, CASE WHEN stop > 0 THEN stop-start ELSE 1056 END) AS WORD
FROM Pieces
DECLARE @wordCt as int
select @wordCt=count(ID) from @WordList;
-- Do the actual query using a CTE with a rownumber that repeats for every SOURCE OBJECT
;WITH WordOrder_CTE AS (
SELECT OBJ_FK, WORD_ID, WORD_TEXT,
ROW_NUMBER() OVER (Partition BY OBJ_FK ORDER BY WORD_ID) AS rownum
FROM test_WORDS)
--CREATE a flattened record of the first word in the phrase and join it to the rest of the words.
INSERT INTO @returnTable
SELECT r1.OBJ_FK, r1.WORD_ID, @phraseID AS PHRASE_ID
FROM WordOrder_CTE r1
INNER JOIN @WordList w1 ON r1.WORD_TEXT = w1.WORD and w1.ID=1
LEFT JOIN WordOrder_CTE r2
ON r1.rownum = r2.rownum - 1 and r1.OBJ_FK = r2.OBJ_FK
LEFT JOIN @WordList w2 ON r2.WORD_TEXT = w2.WORD and w2.ID=2
LEFT JOIN WordOrder_CTE r3
ON r1.rownum = r3.rownum - 2 and r1.OBJ_FK = r3.OBJ_FK
LEFT JOIN @WordList w3 ON r3.WORD_TEXT = w3.WORD and w3.ID=3
LEFT JOIN WordOrder_CTE r4
ON r1.rownum = r4.rownum - 3 and r1.OBJ_FK = r4.OBJ_FK
LEFT JOIN @WordList w4 ON r4.WORD_TEXT = w4.WORD and w4.ID=4
LEFT JOIN WordOrder_CTE r5
ON r1.rownum = r5.rownum - 4 and r1.OBJ_FK = r5.OBJ_FK
LEFT JOIN @WordList w5 ON r5.WORD_TEXT = w5.WORD and w5.ID=5
WHERE (@wordCt < 2 OR w2.ID is not null) and
(@wordCt < 3 OR w3.ID is not null) and
(@wordCt < 4 OR w4.ID is not null) and
(@wordCt < 5 OR w5.ID is not null)
--loop
SET @currentRow = @currentRow+1
END
--Return the first words of each matching phrase
SELECT OBJECT_FK, WORD_ID, PHRASE_ID FROM @returnTable
GO
--Clean up
DROP TABLE [dbo].[test_WORDS]
DROP TABLE [dbo].[test_PHRASE]
편집 솔루션 :
이 연속되지 않은 단어 ID를 설명하기 위해 아래에 제공된 올바른 솔루션의 편집이다. 이것이 내가 한 것처럼 누군가를 도울 수 있기를 바랍니다.
;WITH
numberedwords AS (
SELECT
OBJ_FK,
WORD_ID,
WORD_TEXT,
rowcnt = ROW_NUMBER() OVER
(PARTITION BY OBJ_FK ORDER BY WORD_ID DESC),
totalInSrc = COUNT(WORD_ID) OVER (PARTITION BY OBJ_FK)
FROM dbo.test_WORDS
),
phrasedwords AS (
SELECT
nw1.OBJ_FK,
nw1.WORD_ID,
nw1.WORD_TEXT,
PHRASE_TEXT = RTRIM((
SELECT [text()] = nw2.WORD_TEXT + ' '
FROM numberedwords nw2
WHERE nw1.OBJ_FK = nw2.OBJ_FK
AND nw2.rowcnt BETWEEN nw1.rowcnt AND nw1.totalInSrc
ORDER BY nw2.OBJ_FK, nw2.WORD_ID
FOR XML PATH ('')
))
FROM numberedwords nw1
GROUP BY nw1.OBJ_FK, nw1.WORD_ID, nw1.WORD_TEXT, nw1.rowcnt, nw1.totalInSrc
)
SELECT *
FROM phrasedwords pw
INNER JOIN test_PHRASE tp
ON LEFT(pw.PHRASE_TEXT, LEN(tp.PHRASE_TEXT)) = tp.PHRASE_TEXT
ORDER BY pw.OBJ_FK, pw.WORD_ID
참고 : 프로덕션 환경에서 사용한 최종 쿼리는 CTE 대신 인덱싱 된 임시 테이블을 사용합니다. PHRASE_TEXT 열의 길이를 필요에 따라 제한했습니다. 이러한 개선을 통해 쿼리 시간을 3 분에서 3 초로 줄일 수있었습니다!
"이보다 더 좋은 방법을 찾도록 도와주세요." - 어떤 측정 기준으로 더 좋습니까? –
@Mitch : 댓글을 올리는 중에 질문이 업데이트되었습니다. "내 솔루션의 문제점 ..."을 참조하십시오. – Laramie
이것은 실제로 SQL에서 수행해야하는 것과 같지 않습니다. –