SQL 查询 - 返回连接表的前两条记录的连接

Posted

技术标签:

【中文标题】SQL 查询 - 返回连接表的前两条记录的连接【英文标题】:SQL query - Join that returns the first two records of joining table 【发布时间】:2009-03-10 07:14:30 【问题描述】:

我有两张桌子:

患者

pkPatientId 名字 姓氏

患者状态

pkPatientStatusId fkPatientId 状态码 开始日期 结束日期

Patient -> PatientStatus 是一对多的关系。

我想知道是否可以在 SQL 中执行一个仅返回每个患者的前两个​​ PatientStatus 记录的连接。如果仅存在一条 PatientStatus 记录,则不应在结果中返回。

我的查询的正常连接是:

SELECT FROM Patient p INNER JOIN PatientStatus ps ON p.pkPatientId = ps.fkPatientId
ORDER BY ps.fkPatientId, ps.StartDate

【问题讨论】:

【参考方案1】:

如果您使用的是 SQL Server 2005 或更高版本,CTE 可能是您最好的选择,但如果您想要与其他平台更兼容的东西,这应该可行:

SELECT
     P.pkPatientID,
     P.FirstName,
     P.LastName,
     PS1.StatusCode AS FirstStatusCode,
     PS1.StartDate AS FirstStatusStartDate,
     PS1.EndDate AS FirstStatusEndDate,
     PS2.StatusCode AS SecondStatusCode,
     PS2.StartDate AS SecondStatusStartDate,
     PS2.EndDate AS SecondStatusEndDate
FROM
     Patient P
INNER JOIN PatientStatus PS1 ON
     PS1.fkPatientID = P.pkPatientID
INNER JOIN PatientStatus PS2 ON
     PS2.fkPatientID = P.pkPatientID AND
     PS2.StartDate > PS1.StartDate
LEFT OUTER JOIN PatientStatus PS3 ON
     PS3.fkPatientID = P.pkPatientID AND
     PS3.StartDate < PS1.StartDate
LEFT OUTER JOIN PatientStatus PS4 ON
     PS4.fkPatientID = P.pkPatientID AND
     PS4.StartDate > PS1.StartDate AND
     PS4.StartDate < PS2.StartDate
WHERE
     PS3.pkPatientStatusID IS NULL AND
     PS4.pkPatientStatusID IS NULL

您想要前两种状态而不是后两种状态对我来说确实有点奇怪,但我假设您知道自己想要什么。

如果您获得更好的性能,您也可以使用 WHERE NOT EXISTS 代替 PS3 和 PS4 连接。

【讨论】:

【参考方案2】:

这是我的尝试 - 由于使用了公用表表达式,它应该可以在 SQL Server 2005 和 SQL Server 2008(在 SQL Server 2008 上测试)上运行:

WITH CTE AS
(
    SELECT  fkPatientId
          , StatusCode
          -- add more columns here
          , ROW_NUMBER() OVER
    (
    PARTITION BY fkPatientId ORDER BY fkPatientId desc) AS [Row_Number] 
    from PatientStatus
    where fkPatientId in
    (
        select fkPatientId
        from PatientStatus
        group by fkPatientId
        having COUNT(*) >= 2
    )
)
SELECT p.pkPatientId,
    p.FirstName,
    CTE.StatusCode  
FROM [Patient] as p
    INNER JOIN CTE
        ON p.[pkPatientId] = CTE.fkPatientId
WHERE CTE.[Row_Number] = 1 
or CTE.[Row_Number] = 2

【讨论】:

SQL Server 是否足够了解以只运行一次按 fkPatientID 分组的子查询?否则,通过将 fkPatientID 约束放在其中,您可能会获得更好的性能。【参考方案3】:

编辑:以下两种解决方案都要求PatientStatus.StartDate 在每个患者中都是唯一的。

传统方式(兼容SQL Server 2000):

SELECT 
  p.pkPatientId,
  p.FirstName,
  p.Surname,
  ps.StatusCode,
  ps.StartDate,
  ps.EndDate
FROM 
  Patient p 
  INNER JOIN PatientStatus ps ON 
    p.pkPatientId = ps.fkPatientId
    AND ps.StartDate IN (
      SELECT TOP 2 StartDate 
      FROM     PatientStatus 
      WHERE    fkPatientId = ps.fkPatientId
      ORDER BY StartDate  /* DESC (to switch between first/last records) */
    )
WHERE 
  EXISTS (
    SELECT   1 
    FROM     PatientStatus
    WHERE    fkPatientId = p.pkPatientId
    GROUP BY fkPatientId
    HAVING   COUNT(*) >= 2
  )
ORDER BY 
  ps.fkPatientId, 
  ps.StartDate

一个更有趣的替代方案(您必须尝试比较它的性能):

SELECT 
  p.pkPatientId,
  p.FirstName,
  p.Surname,
  ps.StatusCode,
  ps.StartDate,
  ps.EndDate
FROM 
  Patient p 
  INNER JOIN PatientStatus ps ON p.pkPatientId = ps.fkPatientId
WHERE
  /* the "2" is the maximum number of rows returned */
  2 > (
    SELECT 
      COUNT(*)
    FROM 
      Patient p_i 
      INNER JOIN PatientStatus ps_i ON p_i.pkPatientId = ps_i.fkPatientId
    WHERE
      ps_i.fkPatientId = ps.fkPatientId
      AND ps_i.StartDate < ps.StartDate
      /* switch between "<" and ">" to get the first/last rows */
  )
  AND EXISTS (
    SELECT   1 
    FROM     PatientStatus
    WHERE    fkPatientId = p.pkPatientId
    GROUP BY fkPatientId
    HAVING   COUNT(*) >= 2
  )
ORDER BY 
  ps.fkPatientId, 
  ps.StartDate

旁注:对于 mysql,后一个查询可能是唯一的选择 - 直到子查询支持 LIMIT。

编辑:我添加了一个条件,它排除了只有一个 PatientStatus 记录的患者。 (感谢您的提示,Ryan!)

【讨论】:

【参考方案4】:

我没有尝试,但这可以工作;

SELECT /*(your select columns here)*/, row_number() over(ORDER BY ps.fkPatientId, ps.StartDate) as rownumber FROM Patient p INNER JOIN PatientStatus ps ON p.pkPatientId = ps.fkPatientId
where rownumber between 1 and 2

如果这不起作用,请参阅this 链接。

【讨论】:

【参考方案5】:

将这个 WHERE 子句添加到 Tomalak 的第一个解决方案的外部查询将防止返回少于 2 条状态记录的患者。您也可以在第二个查询的 WHERE 子句中“和”它以获得相同的结果。

WHERE pkPatientId IN (
    SELECT pkPatientID 
    FROM Patient JOIN PatientStatus ON pkPatientId = fkPatientId
    GROUP BY pkPatientID HAVING Count(*) >= 2
)

【讨论】:

感谢您的提示,我忽略了这个问题的特殊要求。不过,我提出的条件与您的条件不同。 Np。我认为这只是一个疏忽。干杯。【参考方案6】:

检查您的服务器是否支持窗口函数:

SELECT * 
FROM Patient p
LEFT JOIN PatientStatus ps ON p.pkPatientId = ps.fkPatientId
QUALIFY ROW_NUMBER() OVER (PARTITION BY ps.fkPatientId ORDER BY ps.StartDate) < 3

另一种可能性,应该适用于 SQL Server 2005:

SELECT * FROM Patient p
LEFT JOIN ( 
    SELECT *, ROW_NUMBER(PARTITION BY fsPatientId ORDER by StartDate) rn
    FROM PatientStatus) ps
ON p.pkPatientId = ps.fkPatientID 
and ps.rn < 3

【讨论】:

【参考方案7】:

这是我的处理方法:

-- Patients with at least 2 status records
with PatientsWithEnoughRecords as (
    select fkPatientId
        from PatientStatus as ps
        group by 
            fkPatientId
        having
            count(*) >= 2
)
select top 2 *
    from PatientsWithEnoughRecords as er 
        left join PatientStatus as ps on
            er.fkPatientId = ps.fkPatientId
    order by StartDate asc

我不确定是什么决定了您案例中“前”两个状态记录,所以我假设您想要最早的两个 StartDate**s。修改最后的**order by子句,获取你感兴趣的记录。

编辑:SQL Server 2000 不支持 CTE,所以这个解决方案确实只能直接在 2005 年及以后的版本上运行。

【讨论】:

【参考方案8】:

丑,但是这个不依赖 StartDate 的唯一性,适用于 SQL 2000

select * 
from Patient p 
join PatientStatus ps on p.pkPatientId=ps.fkPatientId
where pkPatientStatusId in (
 select top 2 pkPatientStatusId 
 from PatientStatus 
 where fkPatientId=ps.fkPatientId 
 order by StartDate
) and pkPatientId in (
 select fkPatientId
 from PatientStatus
 group by fkPatientId
 having count(*)>=2
)

【讨论】:

这是 2000 年和 2005 年的最佳解决方案;但是您想将两个子查询放在 2000 年的临时表和 2005 年以上的 CTE 中 (如果你不这样做,那么你会得到 O(N1*N2*N2) 作为 O(N1+N2+N2) 的操作,其中 N1 是患者人数,N2 是患者人数状态记录。

以上是关于SQL 查询 - 返回连接表的前两条记录的连接的主要内容,如果未能解决你的问题,请参考以下文章

SQL查询每月每天的前两条数据....

Oracle_SQL 连接和子查询

连接两个表的复杂 SQL 查询

您如何检索每个分组中的前两条记录

如何查询一个表中,各个分类的前面2条数据,用一条sql语句

SQL语句中两个表的连接