Join table based on column value in SQL Server - sql

Item table
id key code description
------------------------------
1 1 misc miscellaneous
2 1 med medicine
Miscellaneous table:
id code description
------------------------
1 misc1 miscellaneous
2 misc1 miscellaneous
Medicine table:
id code description
---------------------------
1 medicine1 medicine
2 medicine1 medicine
I have this table structure; my main table is the Item table and I want to JOIN the main table with other table based on the column value in main table. The column that determines the table to be joined is code. If code is misc join with misc table if value is med join with medicine table.
I know the basic JOIN of table like
SELECT *
FROM item
INNER JOIN miscellaneous ON item.key = miscellaneous.id
But I don't know how to join when there is a condition that will point to which table to JOIN

You can use left join. Something like this:
select i.*,
coalesce(mi.code, me.code) as code_1,
coalesce(mi.description, me.description) as description_1
from item i left join
miscellaneous mi
on mi.code = i.key and i.code = 'misc' left join
medicine me
on me.code = i.key and i.code = 'med';

You can try a LEFT JOIN, that will be the easiest method to implement this.
But if you want both the table result to come under one column name, use UNION ALL
Using UNION ALL
SELECT *
FROM item i
INNER JOIN miscellaneous m on m.code=i.code
UNION ALL
SELECT *
FROM item i
INNER JOIN medicine me on me.code=i.code
Using LEFT JOIN
SELECT *
FROM item i
LEFT JOIN miscellaneous m on m.code=i.code
LEFT JOIN medicine me on me.code=i.code

Since Item is your 'base' / main table, you could match other tables with LEFT JOIN so that all rows from Item tables are present.
SELECT *
FROM Item AS i
LEFT JOIN Miscellaneous AS mi ON (mi.[id] = i.[key] AND i.code = 'misc')
LEFT JOIN Medicine AS me ON (me.[id] = i.[key] AND i.code = 'med');
You could also combine Miscellaneous & Medicine tables columns using ISNULL() function with some pre-condition
SELECT i.*
, ISNULL(mi.id, me.id) AS m_id
, ISNULL(mi.code, me.code) AS m_code
, ISNULL(mi.description, me.description) AS m_description
FROM Item AS i
LEFT JOIN Miscellaneous AS mi ON (mi.id = i.[key] AND i.code = 'misc')
LEFT JOIN Medicine AS me ON (me.id = i.[key] AND i.code = 'med');
SqlFiddle Demo

You can do this in a dynamic way
preparation of the data set for testing
IF (OBJECT_ID('item') IS NULL)
BEGIN
CREATE TABLE item (id int, [key] nvarchar(128), code nvarchar(128),
description nvarchar(512))
INSERT INTO item (id, [key] , code, description)
SELECT 1, 1, 'misc', 'miscellaneous' UNION ALL
SELECT 2, 1, 'med', 'medicine'UNION ALL
SELECT 3, 2, 'test not existing table', 'not_existing_table';
END
IF (OBJECT_ID('miscellaneous') IS NULL)
BEGIN
CREATE TABLEe miscellaneous (id int, code nvarchar(128), description
nvarchar(512))
INSERT INTO miscellaneous (id, code, description)
SELECT 1, 'misc1', 'miscellaneous' UNION ALL
SELECT 2, 'misc2', 'miscellaneous_xxx';
END
IF (OBJECT_ID('medicine') IS NULL)
BEGIN
CREATE TABLE medicine (id int, code nvarchar(128), description
nvarchar(512))
INSERT INTO medicine (id, code, description)
SELECT 1, 'medicine1', 'medicine' UNION ALL
SELECT 2, 'medicine2', 'medicine_xxx';
END
SQL Script
DECLARE #joins nvarchar(max) ='',#sql NVARCHAR(MAX) = ' SELECT * FROM item';
SELECT #joins = STUFF((SELECT ( CHAR(13)+CHAR(10) + ' LEFT JOIN ' +
description + ' ' + description + ' ON ' + description +'.id = item.[key] AND item.description = ''' + description + '''' )
FROM item i WHERE ISNULL(description,'') != '' AND
(select OBJECT_ID(description) ) IS NOT NULL
GROUP BY description
ORDER BY description
FOR XML PATH(''), TYPE ).value('.', 'NVARCHAR(MAX)') ,1,1,'');
IF(ISNULL(#joins,'') != '' )
BEGIN
SET #sql = #sql + #joins;
EXECUTE sp_executesql #sql;
PRINT #sql
END;

Related

Update data based on parameter with operator from another table

I have 2 tables with sample data below:
tblSale
PartCode PartGroup SaleQty
a FM 600
b MM 202
c SM 10
d NM 0
tblCondition
PartGroup Condition
FM >500
MM >=200
SM >=1
NM 0
in SQL Server stored procedure i want to update PartGroup in tblSale by PartGroup in tblCondition based on sum(SaleQty) and compare with Condition.
Any help please.
UPDATE:
Example:
PartCode 'A' is has PartGroup='FM' and SaleQty=500.
if SaleQty=400 then update PartGroup='MM' based on Condition in tblCondition.
UPDATE tblSale
SET tblSale.PartGroup=tblCondition.PartGroup
WHERE SUM(tblSale.Sale) ??? tblCondition.Condition
I don't think you will be able to do this without dynamic code.
For my solution you will need make some changes/notes:
change PartGroup NM condition 0 to =0
make sure that tblCondition table conditions are inserted from biggest (500) to lowest (0)
First what I do is create CASE for every row from tblCondition table.
Then I SUM data to temp table by PartCode (I split PartCode 'c' to 2 rows for testing)
And for the last, create dynamic code, which will update data
/*
CREATE TABLE #tblSale ( PartCode VARCHAR(10), PartGroup VARCHAR(10), SaleQty INT)
INSERT INTO #tblSale SELECT 'a', 'FM', 600
INSERT INTO #tblSale SELECT 'b', 'MM', 202
INSERT INTO #tblSale SELECT 'c', 'SM', 5
INSERT INTO #tblSale SELECT 'd', 'NM', 0
INSERT INTO #tblSale SELECT 'c', 'SM', 5
CREATE TABLE #tblCondition ( PartGroup VARCHAR(10), Condition VARCHAR(10))
INSERT INTO #tblCondition SELECT 'FM', '>500'
INSERT INTO #tblCondition SELECT 'MM', '>=200'
INSERT INTO #tblCondition SELECT 'SM', '>=1'
INSERT INTO #tblCondition SELECT 'NM', '=0'
*/
--CREATE CASES
DECLARE #CaseStr NVARCHAR(1000) = 'CASE '
SELECT #CaseStr = #CaseStr + '
WHEN SaleSUM ' + Condition + ' THEN '''+ PartGroup + ''' '
FROM #tblCondition
SET #CaseStr = #CaseStr + ' END'
-- SUM data by PartCode
SELECT PartCode, SUM(SaleQty) AS SaleSUM
INTO #tblSaleSUM
FROM #tblSale
GROUP BY PartCode
-- Create dynamic code for update
DECLARE #query NVARCHAR(MAX)
SET #query = N'
UPDATE S
SET S.PartGroup = SS.PartGroup
FROM #tblSale AS S
INNER JOIN
(
SELECT PartCode, ' + #CaseStr + ' AS PartGroup
FROM #tblSaleSUM
) AS SS
ON SS.PartCode = S.PartCode
'
EXEC sp_executesql #query
Use this
update ts set partGroup = something
from tblSale ts
inner join tblCondition tc
on tc.PartGroup=ts.PartGroup
inner join (Select PartGroup, sum(SaleQty) as SumSaleQty
from tblSale
group by PartGroup) as sums
on sums.PartGroup = tc.PartGroup
and sums.SumSaleQty >= tc.Condition

SQL Server 2008 - For XML Path is very slow

I need help to increase the performance of my query, the example is below. I have a SELECT query list multiple fields of CUSTOMER table, this is really fast, about 15 ms. However, when I include the statement below using FOR XML PATH to grab the Customer POs (multiple) and combine them into one column, it is very slow, but it works.
Any suggestion how to increase the performance, but still getting the same results (combine the Customer POs into one column)? A sample code would be appreciated.
Select
Col1, Col2,
(SELECT
STUFF((SELECT ', ' + CustomerPO
FROM dbo.Tbl_CustomerPO
WHERE CustomerID = cus.CustomerID
FOR XML PATH('')), 1, 1, '')
) AS CustomerPOs
FROM Tbl_Customer cus
Thank you,
Try something like this ....
With a TINY recordset the "string concatenating" function method appears better from an execution plan cost, IO, and Time bases.
IF OBJECT_ID(N'fnConcatenateCustPOs', N'fN') IS NOT NULL
BEGIN
DROP FUNCTION dbo.fnConcatenateCustPOs
END
GO
CREATE FUNCTION dbo.fnConcatenateCustPOs
(
#CustomerID INT
)
RETURNS nvarchar(max)
--WITH ENCRYPTION
AS
BEGIN
DECLARE #StrFP nvarchar(3750)
--DECLARE #Custpo TABLE(CustomerPOId INT, CustomerID INT)
SET #StrFP = ''
SET #StrFP = ''
SELECT #StrFP = + #StrFP + ',' + CAST(CustomerPOId AS nvarchar(50))
FROM Custpo co
WHERE co.CustomerID = #CustomerID
RETURN SUBSTRING(#StrFP, 2, LEN(#StrFP))
END
GO
IF OBJECT_ID(N'Cust', N'U') IS NOT NULL
BEGIN
DROP TABLE Cust
END
IF OBJECT_ID(N'Custpo', N'U') IS NOT NULL
BEGIN
DROP TABLE CustPO
END
CREATE TABLE Cust (CustomerID INT)
CREATE TABLE CustPO (CustomerPOId INT, CustomerId INT)
INSERT Cust
SELECT 1
UNION
SELECT 2
INSERT CustPO
SELECT 10, 1
UNION
SELECT 20, 1
UNION
SELECT 30, 2
UNION
SELECT 31, 2
SET STATISTICS IO ON
SET STATISTICS TIME ON
SELECT CustomerId, dbo.fnConcatenateCustPOs(CustomerID)
FROM Cust cus
Select
CustomerID,
(SELECT
STUFF((SELECT ', ' + CAST(CustomerPOId AS nvarchar(50))
FROM dbo.CustPO
WHERE CustomerID = cus.CustomerID
FOR XML PATH('')), 1, 1, '')
) AS CustomerPOs
FROM Cust cus
SET STATISTICS IO OFF
SET STATISTICS TIME OFFTry:
Select
Col1, Col2,
(
STUFF((', ' + CustomerPO
FOR XML PATH('')), 1, 1, '')
) AS CustomerPOs
FROM Tbl_Customer cus
INNER JOIN dbo.Tbl_CustomerPO cpo ON cus.CustomerID = cpo.CustomerID
You're introducing a JOIN to your query, which will inherently affect performance.
If you index the joining field CustomerID you can speed up this query. Not much else to do here.
Note: Since you're prefixing your CustomerPo list with a comma and a space, you should use:
FOR XML PATH('')), 1, 2, '')
If you don't want your resulting strings to all start with a space.

Concatenate Rows into String with Many to Many Joins

I have a query that is working for almost all my data scenarios...except one. I have a master table, a detail table (with one to many line items), and many description tables that join to the detail table (Code Descriptions, etc). I need to concatenate all detail records into one string value so that I have one record per master record. So if there's 3 details for the master record, all values need to be concatenated into one record to represent the master.
My issue comes when the Master record has multiple detail records but the detail records don't have a match to the description tables 1 to 1. For instance, in my scenario the master record has 3 detail records which only one of the detail records has a description record. So 1 to 3 to 1, Master to Detail to Description. This is causing an issue. When trying to concatenate the records the code is not working because of the NULL value created from the Detail to Description join. The only way I can seem to get this to work is to do a Distinct sub-query and then do my concatenation logic on the outside of that. I feel like there has to be a better way or that I am simply missing something. I provided example code below to show my issue. There are 3 selects that get run. The first is the flat result with all records from the joins. The second is my original logic showing the flaw. The third is a working version that I am hoping someone knows how to do better. I greatly appreciate any help with this issue.
DECLARE #Notification table(
SystemID int NOT NULL,
NotificationID int
);
DECLARE #NotificationItems table(
SystemID int NOT NULL,
NotificationID VARCHAR(100),
LineItem VARCHAR(100)
);
DECLARE #NotificationCauses table(
SystemID int NOT NULL,
NotificationID VARCHAR(100),
LineItem VARCHAR(100),
TestValue VARCHAR(100)
);
INSERT INTO #Notification
SELECT 40,1 UNION
SELECT 40,2 UNION
SELECT 40,3 UNION
SELECT 40,4
INSERT INTO #NotificationItems
SELECT 40,1,1 UNION
SELECT 40,1,2 UNION
SELECT 40,1,3 UNION
SELECT 40,2,1 UNION
SELECT 40,2,2 UNION
SELECT 40,3,1
INSERT INTO #NotificationCauses
SELECT 40,1,1,'Code_A' UNION
SELECT 40,2,1,'Code_B' UNION
SELECT 40,2,2,'Code_C' UNION
SELECT 40,3,1,'Code_D'
--SELECT *
--FROM #Notification
--SELECT *
--FROM #NotificationItems
SELECT *
FROM #Notification AS n
LEFT OUTER JOIN #NotificationItems AS ni
ON n.NotificationID = ni.NotificationID
AND n.SystemID = ni.SystemID
LEFT OUTER JOIN #NotificationCauses AS nc
ON ni.NotificationID = nc.NotificationID
AND ni.SystemID = nc.SystemID
AND ni.LineItem = nc.LineItem
SELECT DISTINCT n.SystemID, n.NotificationID
,SUBSTRING(
(
SELECT DISTINCT
CASE WHEN LTRIM(RTRIM(ni1.LineItem)) <> ISNULL('','') THEN ', '+ni1.LineItem ELSE '' END AS [text()]
FROM #NotificationItems AS ni1
WHERE ni1.SystemID = ni.SystemID AND ni1.NotificationID = ni.NotificationID --AND a1.LineItem = a.LineItem
ORDER BY 1
FOR XML PATH ('')
), 2, 1000) AS [LineItem]
,SUBSTRING(
(
SELECT DISTINCT
CASE WHEN LTRIM(RTRIM(nc1.TestValue)) <> ISNULL('','') THEN ', '+nc1.TestValue ELSE '' END AS [text()]
FROM #NotificationCauses AS nc1
WHERE nc1.SystemID = nc.SystemID AND nc1.NotificationID = nc.NotificationID --AND nc1.LineItem = nc.LineItem
ORDER BY 1
FOR XML PATH ('')
), 2, 1000) AS [TestValues]
FROM #Notification AS n
LEFT OUTER JOIN #NotificationItems AS ni
ON n.SystemID = ni.SystemID
AND n.NotificationID = ni.NotificationID
LEFT OUTER JOIN #NotificationCauses AS nc
ON ni.SystemID = nc.SystemID
AND ni.NotificationID = nc.NotificationID
AND ni.LineItem = nc.LineItem
SELECT DISTINCT SystemID, NotificationID
,SUBSTRING(
(
SELECT DISTINCT
CASE WHEN LTRIM(RTRIM(a1.LineItem)) <> ISNULL('','') THEN ', '+a1.LineItem ELSE '' END AS [text()]
FROM #NotificationItems AS a1
WHERE a1.SystemID = a.SystemID AND a1.NotificationID = a.NotificationID --AND a1.LineItem = a.LineItem
ORDER BY 1
FOR XML PATH ('')
), 2, 1000) AS [LineItem]
,SUBSTRING(
(
SELECT DISTINCT
CASE WHEN LTRIM(RTRIM(a1.TestValue)) <> ISNULL('','') THEN ', '+a1.TestValue ELSE '' END AS [text()]
FROM #NotificationCauses AS a1
WHERE a1.SystemID = a.SystemID AND a1.NotificationID = a.NotificationID --AND a1.LineItem = a.LineItem
ORDER BY 1
FOR XML PATH ('')
), 2, 1000) AS [TestValues]
FROM
(
SELECT DISTINCT n.NotificationID, n.SystemID, ni.LineItem, nc.TestValue
FROM #Notification AS n
LEFT OUTER JOIN #NotificationItems AS ni
ON n.SystemID = ni.SystemID
AND n.NotificationID = ni.NotificationID
LEFT OUTER JOIN #NotificationCauses AS nc
ON ni.SystemID = nc.SystemID
AND ni.NotificationID = nc.NotificationID
AND ni.LineItem = nc.LineItem
) AS a
I have cleared up the query and that's the result
SELECT n.SystemID, n.NotificationID
, SUBSTRING((SELECT COALESCE(', ' + ni.LineItem, '') [text()]
FROM NotificationItems AS ni
WHERE ni.SystemID = n.SystemID
AND ni.NotificationID = n.NotificationID
ORDER BY 1
FOR XML PATH ('')
), 2, 1000) AS [LineItem]
, SUBSTRING((SELECT COALESCE(', ' + nc.TestValue, '') [text()]
FROM NotificationItems AS ni
INNER JOIN NotificationCauses nc
ON ni.SystemID = nc.SystemID
AND ni.NotificationID = nc.NotificationID
AND ni.LineItem = nc.LineItem
WHERE ni.SystemID = n.SystemID
AND ni.NotificationID = n.NotificationID
ORDER BY 1
FOR XML PATH ('')
), 2, 1000) AS [TestValues]
FROM Notification n
Those are the "smell" founded:
the ISNULL('', ''), it really does nothing, replaced with blank string '' (more to come)
the FROM of the main select, there are three table when only one is really used, the unused two are removed, that needed an update of the FROM and WHERE condition subqueries
the DISTINCT of the subqueries, again it does nothing, stripped away
the CASE in the subqueries add a comma before a value, if the trimmed value is not null, that is exactly what COALESCE(', ' + value, '') does (if the trim is needed re-add it)
Everything else is only formatting the query my way, 'cause it's easier to read if it's formatted your way (something like review by refactoring)
Here is a SQLFiddle demo of the cleared query with the data provided

Comma Separated SQL Server Result Set 'JOINED' with other Columns

I have a table say ProjectMaster:
Id ProjectName
1 A
2 B
3 C
another table ProjectMeter
Id ProjectId MeterNumber
1 1 #0001
2 1 #0002
3 1 #0003
4 2 #0004
5 2 #0005
6 3 #0006
I wish to have following output
ProjectName MeterNumbers
A #0001, #0002, #0003
B #0004, #0005
C #0006
I tried this and this, but unable to solve my problem.
I cannot use a table variable.
I have a already written Stored Procedure and it brings data from many joined tables. ProjectMaster also happens to be joined in one of these tables. Now am required to fetch data from ProjectMeter, such that, each row has concatenated ProjectMeter.MeterNumber corresponding to the ProjectId in that column.
right now, I get concatenated list of all meternumbers in all the rows.
I cannot use CURSOR, TABLE variable , Temp TABLE
( I hope still something can be done to my cause)
please help.....
Try this:
SELECT projectname, STUFF((SELECT distinct ', ' + meternumber
from projectmeter m
where p.id = m.projectid
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'') MeterNumbers
from projectmaster p
See SQL Fiddle with Demo
DECLARE #ProjectMaster AS TABLE
(
ID INT IDENTITY(1, 1) ,
ProjectName VARCHAR(2)
)
DECLARE #ProjectMeter AS TABLE
(
ID INT IDENTITY(1, 1) ,
ProjectID INT ,
MeterNumber VARCHAR(50)
)
INSERT INTO #ProjectMaster
( ProjectName )
VALUES ( 'A' )
INSERT INTO #ProjectMeter
( ProjectID, MeterNumber )
VALUES ( 1, '#0001' )
INSERT INTO #ProjectMeter
( ProjectID, MeterNumber )
VALUES ( 1, '#0002' )
SELECT pMaster.ID, STUFF(( SELECT ',' + MeterNumber
FROM #ProjectMeter
FOR
XML PATH('')
), 1, 1, '') AS 'Concat Result'
FROM #ProjectMeter pMeter
INNER JOIN #ProjectMaster pMaster ON pMaster.ID = pMeter.ProjectID
GROUP BY pMaster.ID
I have used table variables here but surely you just need to drop the #'s as I have used the same table names as you have specified? Not sure if this is okay? :)
Also in MS SQL you can do it using recursive query with CTE.
Here is a SQLFiddle demo
;with t1 as (
select t.*,
cast(meternumber as varchar(max)) as m2,
0 as level
from ProjectMeter t
where not exists
(select id
from ProjectMeter l
where l.id<t.id and l.ProjectId=t.ProjectID
)
union all
select b.*,
cast(c.m2+','+b.MeterNumber as varchar(max)) as m2,
c.level+1 as level
from ProjectMeter b
inner join t1 c
on (c.id < b.id) and (b.ProjectID=c.ProjectId)
)
select pm.ProjectName as ProjectName,
t1.m2 as MeterNumbers
from t1
inner join
(select ProjectId,max(level) ml
from t1
group by ProjectId
) t2
on (t1.ProjectId=t2.ProjectID) and (t1.level=t2.ml)
left join ProjectMaster pm
on (t1.ProjectId=pm.Id)
order by t1.ProjectID

Join [one word per row] to rows of phrases with [multiple words per row]

Please excuse the length of the question. I included a test script to demo the situation and my best attempt at a solution.
There are two tables:
test_WORDS = Words extracted in order from several sources. The OBJ_FK column is the ID of the source. WORD_ID is an identifier for the word itself that is unique within the source. Each row contains one word.
test_PHRASE = a list of phrases to be searched for in test_WORDS. The PHRASE_TEXT column is a space separated phrase like 'foo bar' (see below) so that each row contains multiple words.
Requirement:
Return the first word from test_WORDS that is the start of a matching a phrase from test_PHRASE.
I would prefer something set based to avoid RBAR approach below. Also my solution is limited to 5 word phrases. I need to support up to 20 word phrases. Is it possible to match the words from a row in test_PHRASE to contiguous rows in the test_WORD without cursors?
After breaking the phrase words out into a temporary table, the problem boils down to matching portions of two sets together in row order.
-- 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]
Edited solution:
This is an edit of the correct solution provided below to account for non-contiguous word IDs. Hope this helps someone as much as it did me.
;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
Note: The final query I used in production uses indexed temp tables instead of CTEs. I also limited the length of the PHRASE_TEXT column to my needs. With these improvements, I was able to reduce my query time from over 3 minutes to 3 seconds!
Here's a solution that uses a different approach: instead of splitting the phrases into words it combines the words into phrases.
Edited: changed the rowcnt expression to using COUNT(*) OVER …, as suggested by #ErikE in the comments.
;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
Using a Split function should work.
Split Function
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
This performs a little better than other solutions given. if you don't need WORD_ID, just WORD_TEXT, you can remove a whole column. I know this was over a year ago, but I wonder if you can get 3 seconds down to 30 ms? :)
If this query seems good, then my biggest speed advice is to put the entire phrases into a separate table (using your example data, it would have only 2 rows with phrases of length 8 words and 4 words).
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 + '%';
Here's another version for curiosity's sake. It doesn't perform quite as well.
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;