2011-02-08 32 views
5

Vui lòng giải thích độ dài của câu hỏi. Tôi đã bao gồm một kịch bản thử nghiệm để giới thiệu tình hình và nỗ lực tốt nhất của tôi tại một giải pháp.Tham gia [một từ mỗi hàng] thành các hàng cụm từ có [nhiều từ mỗi hàng]

Có hai bảng:

  1. test_WORDS = Words chiết xuất theo thứ tự từ nhiều nguồn khác nhau. Cột OBJ_FK là ID của nguồn. WORD_ID là số nhận dạng cho chính từ đó là duy nhất trong nguồn. Mỗi hàng chứa một từ.
  2. test_PHRASE = danh sách các cụm từ được tìm kiếm trong test_WORDS. Cột PHRASE_TEXT là cụm từ được phân tách bằng dấu cách như 'foo bar' (xem bên dưới) để mỗi hàng chứa nhiều từ.

Yêu cầu: Return từ đầu tiên từ test_WORDS đó là khởi đầu của một phù hợp với một cụm từ từ test_PHRASE.

Tôi muốn một cái gì đó được thiết lập dựa trên cách tiếp cận RBAR bên dưới. Ngoài ra giải pháp của tôi được giới hạn trong 5 cụm từ. Tôi cần hỗ trợ tối đa 20 cụm từ. Có thể đối sánh các từ từ một hàng trong test_PHRASE với các hàng liền kề trong test_WORD mà không có con trỏ không?

Sau khi ngắt cụm từ ra thành một bảng tạm thời, vấn đề sẽ giảm xuống để khớp các phần của hai bộ với nhau theo thứ tự hàng.

-- 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] 

giải pháp được sửa đổi:

Đây là một chỉnh sửa của giải pháp đúng quy định dưới đây để giải thích cho các ID từ không tiếp giáp. Hy vọng điều này sẽ giúp một ai đó nhiều như nó đã làm tôi.

;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 

Lưu ý: Truy vấn cuối cùng tôi sử dụng trong sản xuất sử dụng bảng tạm thời được lập chỉ mục thay vì CTE. Tôi cũng giới hạn chiều dài của cột PHRASE_TEXT theo nhu cầu của tôi. Với những cải tiến này, tôi đã có thể giảm thời gian truy vấn của mình từ hơn 3 phút xuống còn 3 giây!

+0

"Hãy giúp tôi tìm cách tốt hơn". - Tốt hơn theo chỉ số nào? –

+0

@Mitch: Câu hỏi đã được cập nhật trong khi bạn đang nhận xét. Xem "Vấn đề với giải pháp của tôi ..." – Laramie

+3

Điều này không thực sự giống như một cái gì đó bạn nên làm trong SQL –

Trả lời

3

Dưới đây là một giải pháp mà sử dụng một cách tiếp cận khác: thay vì tách các cụm từ diễn tả bằng lời nó kết hợp các từ thành cụm từ.

Đã chỉnh sửa: đã thay đổi biểu thức rowcnt để sử dụng COUNT(*) OVER …, như được đề xuất bởi @ErikE trong các nhận xét.

;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

Một nửa mã, cài đặt và 10X nhanh hơn. Khéo léo. Và họ (@ Joe) nói rằng nó coudn't/không nên được thực hiện. – Laramie

+0

@Laramie: Cảm ơn, bạn rất tốt bụng. Bạn biết đấy, khi Joe nói rằng nó không * trông * giống như một cái gì đó bạn nên làm trong SQL, có lẽ anh ta thực sự có nghĩa là anh ta không thích nó trông như thế nào? Nhưng điều đó không có gì lạ, bạn cũng vậy! :) –

+0

@Andiry: Điểm lấy về "có vẻ như nó không nên được thực hiện" bình luận, nhưng tôi phải thừa nhận tôi là một chút miffed tại upvotes nó nhận được khi nó đã được thêm một loại bỏ các câu hỏi của tôi hơn một cái gì đó xây dựng. Họ có nói với Ric Ocasek rằng anh ta không thể cưới Paulina Porizkova? Bạn đã cho thấy rằng nó có thể được thực hiện hiệu quả bằng cách sử dụng các công cụ như dự định. – Laramie

0

Sử dụng chức năng Split sẽ hoạt động.

Chia Chức năng

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 Statement

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

Đây là một cách tiếp cận vững chắc để tách các cụm từ, nhưng nó không đáp ứng yêu cầu. Điều này khớp với bất kỳ bản ghi nào từ test_PHRASE có tất cả các từ tồn tại trong test_Word, đó là mọi cụm từ trong ví dụ. Mục tiêu là đối sánh từ đầu tiên từ test_Word nếu đó là từ đầu tiên của một trong các cụm từ từ test_phrase. Sử dụng dữ liệu từ trường hợp thử nghiệm, 'xxx xxx xxx' và 'xxx www ppp' không được khớp với một từ trong test_word vì [xxx] [xxx] [xxx] và [xxx] [www] [ppp] không xuất hiện liên tục trong test_Word. Xin lỗi nếu tôi không rõ ràng. Thật khó để truyền đạt sự phức tạp của yêu cầu. – Laramie

+0

@Laramie, thx để theo dõi. Đọc lại câu hỏi của bạn, nó đã đủ rõ ràng. Lỗi ở bên cạnh tôi. –

0

Điều này thực hiện tốt hơn một chút so với các giải pháp khác được đưa ra. nếu bạn không cần WORD_ID, chỉ cần WORD_TEXT, bạn có thể xóa toàn bộ cột.Tôi biết điều này đã được hơn một năm trước, nhưng tôi tự hỏi nếu bạn có thể nhận được 3 giây xuống đến 30 ms? :)

Nếu truy vấn này có vẻ tốt, thì lời khuyên tốc độ lớn nhất của tôi là đặt toàn bộ cụm từ vào một bảng riêng (sử dụng dữ liệu ví dụ của bạn, nó sẽ chỉ có 2 hàng có cụm từ dài 8 từ và 4 từ).

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 + '%'; 

Đây là phiên bản khác vì mục đích tò mò. Nó không hoạt động khá tốt.

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; 
Các vấn đề liên quan