T-SQL - 如何编写包含多对多连接的复杂条件连接

时间:2009-12-05 01:28:41

标签: sql sql-server tsql join

我尝试通过在此处发布其他相关问题来解决此问题,这些问题主要集中在查询的某些部分。但是,我不妨发布整个事情,看看是否有人可以提供帮助。我有以下表格,其中包含以下字段:

tblPerson - PersonID,PersonName
tblGroup - GroupID,名称
tblGroupMembership - PersonID,GroupID
tblPersonCities - CityID,PersonID,City

这是一个非常简单的设置。我们有Person和Group,GroupMembership是两者之间的多对多联盟。然后人们可以拥有多个城市。

我要做的是编写一个存储过程,用于根据几个不同的参数搜索这些数据。存储过程仅返回Person信息。存储过程应该采用3个参数:

@PersonName - 一个人的姓名或一个人姓名的一部分
@GroupIDList - 以逗号分隔的GroupID列表
@City - 城市名称或城市名称的一部分

我希望存储过程能够不需要任何参数的值。因此,如果所有参数都为NULL,那么它应该返回所有Person记录。如果传入GroupID列表,则它应仅返回与传入列表中的所有组匹配的Person记录。我希望我已经正确解释了这一点。我知道这是一个很长的问题,但我无法用其他方式解释。我有一些ALMOST工作的代码。唯一的问题是,如果所有参数都为NULL,它似乎不起作用。 (我还没弄明白如何引入城市)这是我的代码示例。 (注意:fnSplit是一个自定义函数,它使用逗号分隔的字符串并返回具有不同值的表)

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
    (
       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 + '%'))

3 个答案:

答案 0 :(得分:3)

我倾向于在这些情况下使用动态sql,因为使用OR来容纳这种逻辑对于性能和可攻击性来说是可怕的。以下示例适用于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

请注意,sp_executesql 缓存查询计划 - 每The curse and blessings of dynamic SQL

答案 1 :(得分:1)

试试这个:

首先是将逗号分隔的GroupIds列表转换为表变量的函数...

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

然后是存储过程

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

答案 2 :(得分:0)

如果您说“any parameter”为null,那还包括 @grouplist 吗?

如果是这样,可能是因为您正在对过滤器表进行内部联接。如果 @grouplist 为null,则不会有任何行来满足该连接,因为我假设fnSplit将不返回任何行,因此 in 语句将永远不会为true 。我现在只是在眼睛看它...