T-SQL - How to write a complex conditional join including a many-to-many join
I have tried solving this problem by posting other related questions here that focused on parts of the query. However I might as well post the entire thing and see if anyone can help. I have the following tables with the following fields:
tblPerson - PersonID, PersonName
tblGroup - GroupID, Name tblGroupMembership - PersonID, GroupID tblPersonCities - CityID, PersonID, CityIt's a pretty simple setup. We have Person and Group and the GroupMembership is a many to many join between the two. And then People can have multiple Cities.
What I am trying to do is write a stored procedure for searching this data based on a few different parameters. The stored procedure will only return Person information. The stored procedure should take 3 parameters:
@PersonName - a person's name or part of a person's name
@GroupIDList - a comma delimited list of GroupIDs @City - a city name or part of a city nameI'd like for the stored procedure to be able to not require values for any of the parameters. So if all of the parameters were NULL then it should return all of the Person records. If a list of GroupIDs is passed in then it should only return Person records that match ALL of the Groups in the passed in list. I hope I have explained this correctly. I know this is a long question but I can't explain it any other way. I have some code that ALMOST works. The only problem is that it doesn't seem to work if all of the parameters are NULL. (And I haven't figured out how to bring in the City) Here is my code sample. (NOTE: fnSplit is a custom function that takes a comma delimited string and returns a table with the different values)
declare @name varchar(50)
declare @city varchar(50)
declare @grouplist varchar(50)
set @name = null
set @city = null
set @grouplist = null
select distinct
p.PersonID,
p.PersonName,
c.City
from
tblPerson p left join tblCities c on p.PersonID = c.PersonID
join
(
开发者_如何学Python select m.PersonID
from tblGroupMembership m
where (m.GroupID in (select item from fnSplit(@grouplist, ',')))
group by m.PersonID
having (count(*) = (select count(*) from fnSplit(@grouplist, ',')))
) as filter
on (@grouplist is not null) and (p.PersonID = filter.PersonID)
where
((@name is null) or (p.PersonName like '%' + @name + '%')) and
((@city is null) or (c.City like '%' + @city + '%'))
I lean towards using dynamic sql in these situations, because using OR
s to accommodate this logic are horrible for performance and sargability. The following example is intended for SQL Server 2005+:
DECLARE @SQL NVARCHAR(4000)
SET @SQL = 'SELECT DISTINCT
p.personid,
p.personname,
c.city
FROM TBLPERSON p
LEFT JOIN TBLCITIES c ON c.personid = p.personid '
SET @SQL = @SQL + CASE
WHEN @grouplist IS NOT NULL THEN
' JOIN (SELECT m.PersonID
FROM TBLGROUPMEMBERSHIP m
WHERE m.GroupID IN (SELECT item FROM fnSplit(@grouplist, ',')))
GROUP BY m.PersonID
HAVING COUNT(*) = (SELECT COUNT(*) FROM fnSplit(@grouplist, ',')))) g ON g.personid = p.personid '
ELSE
' '
END
SET @SQL = @SQL + ' WHERE 1 = 1 ' --trick to make contatentating WHERE clause easier
IF @name IS NOT NULL
SET @SQL = @SQL + ' AND p.personname LIKE '%' + @name + '% '
IF @city IS NOT NULL
SET @SQL = @SQL + ' AND c.city LIKE '%' + @city + '% '
BEGIN
EXEC sp_executesql @SQL N'@grouplist varchar(50), @grouplist varchar(50), @name varchar(50), @city varchar(50)',
@grouplist, @grouplist, @name, @city
END
Mind that sp_executesql will cache the query plan - per The curse and blessings of dynamic SQL.
try this:
First is a function to turn the comma delimited list of GroupIds into a table variable...
CREATE FUNCTION [dbo].[ParseString] (@S Text, @delim VarChar(5))
Returns @tOut Table
(ValNum Integer Identity Primary Key,
sVal VarChar(8000))
As
Begin
Declare @dLLen TinyInt -- Length of delimiter
Declare @sWin VarChar(8000)-- Will Contain Window into text string
Declare @wLen Integer -- Length of Window
Declare @wLast TinyInt -- Boolean to indicate processing Last Window
Declare @wPos Integer -- Start Position of Window within Text String
Declare @sVal VarChar(8000)-- String Data to insert into output Table
Declare @BtchSiz Integer -- Maximum Size of Window
Set @BtchSiz = 7900 -- (Reset to smaller values to test routine)
Declare @dPos Integer -- Position within Window of next Delimiter
Declare @Strt Integer -- Start Position of each data value within Window
-- -------------------------------------------------------------------------
-- ---------------------------
If @delim is Null Set @delim = '|'
If DataLength(@S) = 0 Or
Substring(@S, 1, @BtchSiz) = @delim Return
-- ---------------------------
Select @dLLen = Len(@delim),
@Strt = 1, @wPos = 1,
@sWin = Substring(@S, 1, @BtchSiz)
Select @wLen = Len(@sWin),
@wLast = Case When Len(@sWin) = @BtchSiz
Then 0 Else 1 End,
@dPos = CharIndex(@delim, @sWin, @Strt)
-- ----------------------------
While @Strt <= @wLen
Begin
If @dPos = 0 Begin -- No More delimiters in window
If @wLast = 1 Set @dPos = @wLen + 1
Else Begin
Set @wPos = @wPos + @Strt - 1
Set @sWin = Substring(@S, @wPos, @BtchSiz)
-- ----------------------------------------
Select @wLen = Len(@sWin), @Strt = 1,
@wLast = Case When Len(@sWin) = @BtchSiz
Then 0 Else 1 End,
@dPos = CharIndex(@delim, @sWin, 1)
If @dPos = 0 Set @dPos = @wLen + 1
End
End
-- -------------------------------
Set @sVal = LTrim(Substring(@sWin, @Strt, @dPos - @Strt))
Insert @tOut (sVal) Values (@sVal)
-- -------------------------------
-- Move @Strt to char after last delimiter
Set @Strt = @dPos + @dLLen
Set @dPos = CharIndex(@delim, @sWin, @Strt)
End
Return
End
Then here's the stored procedure
Create Procedure GetPersons
@PersonName varChar(50) = nULL,
@City varChar(50) = Null,
@GroupIDList varChar(5000)
As
Set NoCOunt On
Declare @Groups Table (GId Integer Primary Key Not Null)
If Len(@GroupIDList) = 0
Insert @Groups(GId)
Select GroupId From tblGroup
Else
Insert @Groups(GId)
Select Cast(sVal as Integer)
From dbo.ParseString(@GroupIDList, ',')
Select PersonId, PersonName
From tblPerson p
Where Exists (Select * From tblGroupMembership gm
Join @Groups g On g.GId = gm.GroupId
Where PersonId = p.PersonId)
And Exists (Select * From tblPersonCities
Where PersonId = p.PersonId
And City = IsNull(@City, City))
When you say "any parameter" is null, does this include @grouplist as well?
If so, it may be because you are doing an inner join to the filter table. If @grouplist is null, then there will be no rows to meet that join as I'm assuming that fnSplit will return no rows and so the in statement will never be true. I'm just eyeballing it at this point...
精彩评论