2011-02-08 12 views
5

Disculpe la longitud de la pregunta. Incluí un script de prueba para probar la situación y mi mejor intento de encontrar una solución.Unir [una palabra por fila] a filas de frases con [varias palabras por fila]

Hay dos tablas:

  1. test_WORDS = Palabras extraídos con el fin de varias fuentes. La columna OBJ_FK es la ID de la fuente. WORD_ID es un identificador de la palabra que es único dentro de la fuente. Cada fila contiene una palabra.
  2. test_PHRASE = una lista de frases para buscar en test_WORDS. La columna PHRASE_TEXT es una frase separada por espacios como 'foo bar' (ver más abajo), de modo que cada fila contiene varias palabras.

Requisito: Volver la primera palabra de test_WORDS que es el comienzo de un juego una frase de test_PHRASE.

Preferiría algo configurado para evitar el enfoque RBAR a continuación. También mi solución está limitada a frases de 5 palabras. Necesito apoyar frases de hasta 20 palabras. ¿Es posible hacer coincidir las palabras de una fila en test_PHRASE con las filas contiguas en test_WORD sin cursores?

Después de separar las palabras de la frase en una tabla temporal, el problema se reduce a hacer coincidir partes de dos conjuntos en orden de fila.

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

solución Editado:

Esta es una edición de la solución correcta proporcionada a continuación para dar cuenta de IDs de palabras no contiguas. Espero que esto ayude a alguien tanto como a mí.

;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 

Nota: La consulta final que utilicé en producción usa tablas temporales indexadas en lugar de CTE. También limité la longitud de la columna PHRASE_TEXT a mis necesidades. Con estas mejoras, pude reducir mi tiempo de consulta de más de 3 minutos a 3 segundos.

+0

"Por favor, ayúdame a encontrar una manera mejor que esto." - ¿Mejor por qué métrica? –

+0

@Mitch: la pregunta se estaba actualizando mientras comentabas. Consulte "El problema con mi solución ..." – Laramie

+3

Esto realmente no parece algo que debería estar haciendo en SQL –

Respuesta

3

he aquí una solución que utiliza un enfoque diferente: en lugar de dividir las frases con palabras que combina las palabras en frases.

Editado: cambió la expresión rowcnt a usar COUNT(*) OVER …, según lo sugerido por @ErikE en los comentarios.

;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

La mitad del código, basado en conjunto y 10 veces más rápido. Ingenioso. Y ellos (@Joe) dijeron que no podía/no debería hacerse. – Laramie

+0

@Laramie: Gracias, eres muy amable. Sabes, cuando Joe dijo que no se veía * como algo que deberías estar haciendo en SQL, ¿tal vez realmente quería decir que simplemente no le gustaba cómo se veía? Pero no es de extrañar, ¡tú tampoco! :) –

+0

@Andiry: Punto tomado sobre el comentario de "parece que no se debe hacer", pero tengo que admitir que me sentí un poco molesto por las votaciones al alza que recibió cuando fue más una desestimación de mi pregunta que algo constructivo. ¿Le dijeron a Ric Ocasek que no podía casarse con Paulina Porizkova? Has demostrado que se puede hacer de manera efectiva utilizando las herramientas como se esperaba. – Laramie

0

El uso de una función Split debería funcionar.

Function 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 Declaración

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

Este es un enfoque sólido para dividir las frases, pero no cumple con el requisito. Esto coincide con cualquier registro de test_PHRASE cuyas palabras todas existan en test_Word, es decir, cada frase en el ejemplo. El objetivo es hacer coincidir la primera palabra de test_Word si es la primera palabra de una de las frases de test_phrase. Con los datos del caso de prueba, 'xxx xxx xxx' y 'xxx www ppp' no deberían coincidir con una palabra en test_word, ya que [xxx] [xxx] [xxx] y [xxx] [www] [ppp] no aparecen contiguamente en test_Word. Lo siento si no estaba claro. Es difícil comunicar la complejidad del requisito. – Laramie

+0

@Laramie, thx para el seguimiento. Releyendo tu pregunta, fue lo suficientemente claro. El error estuvo de mi lado. –

0

Esto funciona un poco mejor que otras soluciones dadas. si no necesita WORD_ID, solo WORD_TEXT, puede eliminar una columna completa.Sé que esto fue hace más de un año, pero me pregunto si puede obtener 3 segundos hasta 30 ms? :)

Si esta consulta parece buena, entonces mi consejo más rápido es poner las frases completas en una tabla separada (usando sus datos de ejemplo, solo tendría 2 filas con frases de 8 palabras de longitud y 4 palabras).

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

Aquí hay otra versión por curiosidad. No funciona tan bien.

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; 
Cuestiones relacionadas