2011-02-08 6 views
5

질문의 길이를 용서하십시오. 상황을 시연하기위한 테스트 스크립트와 솔루션에 대한 최선의 시도를 포함 시켰습니다. 여러 소스에서 순서대로 추출[행당 한 단어]를 [한 행에 여러 단어가있는] 행의 행에 결합하십시오.

  1. test_WORDS = 단어 :

    은 두 개의 테이블이 있습니다. OBJ_FK 열은 소스의 ID입니다. WORD_ID은 소스 내에서 고유 한 단어 자체의 식별자입니다. 각 행에는 한 단어가 들어 있습니다.

  2. 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 초로 줄일 수있었습니다!

+0

"이보다 더 좋은 방법을 찾도록 도와주세요." - 어떤 측정 기준으로 더 좋습니까? –

+0

@Mitch : 댓글을 올리는 중에 질문이 업데이트되었습니다. "내 솔루션의 문제점 ..."을 참조하십시오. – Laramie

+3

이것은 실제로 SQL에서 수행해야하는 것과 같지 않습니다. –

답변

3

여기에 다른 접근 방식을 사용하는 솔루션입니다 : 그것은 문구에 단어를 결합하는 대신 단어로 문장을 분할의.

편집 : rowcnt 표현식을 COUNT(*) OVER …으로 변경 (의견에서 @ErikE으로 제안).

;WITH 
numberedwords AS (
    SELECT 
    OBJ_FK, 
    WORD_ID, 
    WORD_TEXT, 
    rowcnt = COUNT(*) 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.WORD_ID BETWEEN nw1.WORD_ID AND nw1.rowcnt 
     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 
) 
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 
+0

코드 절반, 집합 기반 및 10 배 빨라집니다. 영리한. 그리고 그들 (@Joe)은 끝내지 않아야한다고 말했다. – Laramie

+0

@Laramie : 감사합니다. 당신은 매우 친절합니다. 조 (Joe)는 SQL로해야 할 일을 보지 않았다고 말했을 때 실제로 어떻게 보일지를 마음에 들지 않았을 것입니다. 그러나 그것은 당연한 것입니다. :) –

+0

@Andiry : "끝내지 않아야하는 것처럼 보입니다"라고 말한 점에 대해 언급했지만, 건설적인 것보다 내 질문을 더 많이 해고했을 때받은 상행위에 대해 조금은 미안하다고 인정해야합니다. Ric Ocasek에게 Paulina Porizkova와 결혼 할 수 없다고 말 했나요? 의도 한대로 도구를 사용하여 효과적으로 수행 할 수 있음을 알았습니다. – Laramie

0

Split 기능을 사용해야 작동합니다.

분할 기능

CREATE FUNCTION dbo.Split 
(
    @RowData nvarchar(2000), 
    @SplitOn nvarchar(5) 
) 
RETURNS @RtnValue table 
(
    Id int identity(1,1), 
    Data nvarchar(100) 
) 
AS 
BEGIN 
    Declare @Cnt int 
    Set @Cnt = 1 

    While (Charindex(@SplitOn,@RowData)>0) 
    Begin 
     Insert Into @RtnValue (data) 
     Select 
      Data = ltrim(rtrim(Substring(@RowData,1,Charindex(@SplitOn,@RowData)-1))) 

     Set @RowData = Substring(@RowData,Charindex(@SplitOn,@RowData)+1,len(@RowData)) 
     Set @Cnt = @Cnt + 1 
    End 

    Insert Into @RtnValue (data) 
    Select Data = ltrim(rtrim(@RowData)) 

    Return 
END 

SQL 문

SELECT DISTINCT p.* 
FROM dbo.test_PHRASE p 
     LEFT OUTER JOIN (
      SELECT p.ID 
      FROM dbo.test_PHRASE p 
        CROSS APPLY dbo.Split(p.PHRASE_TEXT, ' ') sp 
        LEFT OUTER JOIN dbo.test_WORDS w ON w.WORD_TEXT = sp.Data 
      WHERE w.OBJ_FK IS NULL 
     ) ignore ON ignore.ID = p.ID 
WHERE ignore.ID IS NULL   
+1

이것은 문구를 분리하는 견고한 접근 방법이지만 요구 사항을 충족시키지 못합니다. 이것은 단어가 모두 test_Word에 존재하는 test_PHRASE의 모든 레코드, 즉 예제의 모든 구문과 일치합니다. test_Word의 첫 번째 단어가 test_phrase의 첫 단어 인 경우 목표와 일치시키는 것이 목표입니다. [xxx] [xxx] [xxx]와 [xxx] [www] [ppp]가 나타나지 않기 때문에 'xxx xxx xxx'와 'xxx www ppp'는 test_word의 단어와 일치하지 않아야합니다. test_Word에서 연속적으로. 내가 분명하지 않으면 미안해. 요구 사항의 복잡성을 전달하는 것은 어려운 일입니다. – Laramie

+0

@Laramie, 후속 조치를 위해 thx. 귀하의 질문을 다시 읽으십시오. 그 오류는 내 편이 었어. –

0

이것은 다른 해결책보다 약간 성능이 좋습니다. WORD_ID가 필요하지 않은 경우 WORD_TEXT 만 있으면 전체 열을 제거 할 수 있습니다.1 년 전 이었지만 3 초를 30ms까지 줄 수 있을지 궁금합니다. :)

이 쿼리가 좋은 것처럼 보인다면, 가장 큰 속삭임은 전체 구문을 별도의 테이블에 넣는 것입니다 (예제 데이터를 사용하면 길이가 8 단어 및 4 단어의 구가있는 행이 2 개 밖에 안됩니다).

SELECT 
    W.OBJ_FK, 
    X.Phrase, 
    P.*, 
    Left(P.PHRASE_TEXT, 
     IsNull(NullIf(CharIndex(' ', P.PHRASE_TEXT), 0) - 1, 2147483647) 
    ) WORD_TEXT, 
    Len(Left(X.Phrase, PatIndex('%' + P.PHRASE_TEXT + '%', ' ' + X.Phrase) - 1)) 
     - Len(Replace(
     Left(X.Phrase, PatIndex('%' + P.PHRASE_TEXT + '%', X.Phrase) - 1), ' ', '') 
    ) 
     WORD_ID 
FROM 
    (SELECT DISTINCT OBJ_FK FROM dbo.test_WORDS) W 
    CROSS APPLY (
     SELECT RTrim((SELECT WORD_TEXT + ' ' 
     FROM dbo.test_WORDS W2 
     WHERE W.OBJ_FK = W2.OBJ_FK 
     ORDER BY W2.WORD_ID 
     FOR XML PATH (''))) Phrase 
    ) X 
    INNER JOIN dbo.test_PHRASE P 
     ON X.Phrase LIKE '%' + P.PHRASE_TEXT + '%'; 

호기심을위한 또 다른 버전입니다. 그것은 꽤 잘 수행하지 않습니다.

WITH Calc AS (
    SELECT 
     P.ID, 
     P.PHRASE_TEXT, 
     W.OBJ_FK, 
     W.WORD_ID StartID, 
     W.WORD_TEXT StartText, 
     W.WORD_ID, 
     Len(W.WORD_TEXT) + 2 NextPos, 
     Convert(varchar(150), W.WORD_TEXT) MatchingPhrase 
    FROM 
     dbo.test_PHRASE P 
     INNER JOIN dbo.test_WORDS W 
     ON P.PHRASE_TEXT + ' ' LIKE W.WORD_TEXT + ' %' 
    UNION ALL 
    SELECT 
     C.ID, 
     C.PHRASE_TEXT, 
     C.OBJ_FK, 
     C.StartID, 
     C.StartText, 
     W.WORD_ID, 
     C.NextPos + Len(W.WORD_TEXT) + 1, 
     Convert(varchar(150), C.MatchingPhrase + Coalesce(' ' + W.WORD_TEXT, '')) 
    FROM 
     Calc C 
     INNER JOIN dbo.test_WORDS W 
     ON C.OBJ_FK = W.OBJ_FK 
     AND C.WORD_ID + 1 = W.WORD_ID 
     AND Substring(C.PHRASE_TEXT, C.NextPos, 2147483647) + ' ' LIKE W.WORD_TEXT + ' %' 
) 
SELECT C.OBJ_FK, C.PHRASE_TEXT, C.StartID, C.StartText, C.ID 
FROM Calc C 
WHERE C.PHRASE_TEXT = C.MatchingPhrase; 
관련 문제