【问题标题】:T-SQL - How to write a complex conditional join including a many-to-many joinT-SQL - 如何编写一个复杂的条件连接,包括多对多连接
【发布时间】:2009-12-05 01:28:41
【问题描述】:

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

tblPerson - PersonID、PersonName
tblGroup - 组 ID、名称
tblGroupMembership - PersonID、GroupID
tblPersonCities - CityID、PersonID、城市

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

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

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

我希望存储过程能够不需要任何参数的值。因此,如果所有参数都为 NULL,那么它应该返回所有 Person 记录。如果传入 GroupID 列表,则它应该只返回与传入列表中的所有组匹配的人员记录。我希望我已经正确解释了这一点。我知道这是一个很长的问题,但我无法以任何其他方式解释它。我有一些几乎可以工作的代码。唯一的问题是,如果所有参数都为 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 + '%'))

【问题讨论】:

    标签: sql sql-server tsql join


    【解决方案1】:

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

    【讨论】:

    • 我一直认为动态sql不好,因为它不会被服务器缓存并且会慢得多。我会试试看。
    【解决方案2】:

    试试这个:

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

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

    【讨论】:

      【解决方案3】:

      当您说“任何参数”为空时,这是否也包括 @grouplist

      如果是这样,可能是因为您正在对 filter 表进行内部连接。如果 @grouplist 为空,那么将没有行来满足该联接,因为我假设 fnSplit 将不返回任何行,因此 in 语句永远不会为真.我只是在这一点上盯着它......

      【讨论】:

      • 是的,即使@grouplist 也可以为NULL。你是对的。这就是为什么我没有得到任何结果。我想知道如果@grouplist 为空,是否有某种方法可以完全忽略整个连接。我只是对 SQL 了解不够,不知道这是否可能。如果@grouplist 为空,则不要加入;如果 is 有值,则进行连接。
      猜你喜欢
      • 2010-12-23
      • 2013-12-29
      • 2011-03-23
      • 1970-01-01
      • 2011-12-11
      • 1970-01-01
      • 1970-01-01
      • 2014-12-31
      • 1970-01-01
      相关资源
      最近更新 更多