根据WHERE子句选择具有最小值(某些列)的行

时间:2014-12-19 10:33:41

标签: sql sql-server sybase-ase

想象一下:

我有一个表Customers,每个客户可以有0个或更多PhoneNumbers。电话号码已在Customers'CusId及其自己的ListIx(因此(CusId, ListIx))上编入索引。

我想选择一个客户列表及其第一个min(ListIx))电话号码。

问题,最优雅的方法是什么?

例如:

select
    c.FirstName, c.LastName, pn.PhoneNo
  from Customers c
    left join PhoneNumbers pn
      on c.CusId = pn.CusId
        and pn.ListIx = (select min(ListIx) from PhoneNumbers where CusId = c.CusId)

这样可行,但在此示例中,WHERE子句和JOIN足够简单。

但想象一个更复杂的例子:

while 1=1 begin
  select
      -- Combine all phone numbers along with their type into one string
      @PhoneComp = @PhoneComp + ' ' + PhoneNo
        + case when PhTp is not null then '/' + PhTp end,
      @ListIx = p.ListIx
    from PhoneNumbers
    where
      CusId = @CusId and ListIx > @ListIx and
      ListIx = (select min(ListIx) from PhoneNumbers where CusId = @CusId and ListIx > @ListIx)
end

即使这是简化的,但我担心子查询可能过于复杂。

您可以在Sybase ASE中看到以下内容:

while 1=1 begin
  select
      -- Combine all phone numbers along with their type into one string
      @PhoneComp = @PhoneComp + ' ' + PhoneNo
        + case when PhTp is not null then '/' + PhTp end,
      @ListIx = p.ListIx
    from PhoneNumbers
    where
      CusId = @CusId and ListIx > @ListIx
    having
      CusId = @CusId and ListIx > @ListIx and ListIx = min(ListIx)
end

即使你真的想到它,它也没有意义,但你知道它意味着什么(幸运的是Sybase)。但该代码让MS SQL Server抱怨:

Column 'PhoneNumbers.CusId' is invalid in the HAVING clause because it is not contained in either an aggregate function or the GROUP BY clause.
Column 'PhoneNumbers.ListIx' is invalid in the HAVING clause because it is not contained in either an aggregate function or the GROUP BY clause.

你可能会问,为什么WHERE条款基本上重复HAVING条款?这是因为当存在WHERE子句时,Sybase ASE会忽略HAVING子句。

例如在Sybase ASE中:

select
    ListIx, PhoneNo
  from PhoneNumbers
  where
    CusId = @CusId
  having
    ListIx=min(ListIx)

无论客户(@CusId)如何,都会获得ListIx最低的所有电话号码。编辑:我应该提到,通过添加group by CusId结果是预期的,但Sybase ASE甚至允许这样做的事实是值得怀疑的。

我在这里提到了Sybase ASE,因为我正在努力转换许多旧的Sybase ASE SQL代码以使用MS SQL Server。虽然子查询确实有效,但我想知道我是否缺少一些明显的解决方案。

3 个答案:

答案 0 :(得分:1)

回答直接问题

  

最优雅的方式是什么?

我会在SQL Server上使用OUTER APPLY

select
    c.FirstName
    ,c.LastName
    ,pn.PhoneNo
from
    Customers c
    OUTER APPLY
    (
        SELECT TOP(1) PhoneNumbers.PhoneNo
        FROM PhoneNumbers
        WHERE PhoneNumbers.CusId = c.CusId
        ORDER BY PhoneNumbers.ListIx
    ) AS ph

如果您需要一个适用于SQL Server和Sybase的代码,那就是另一个故事。

在我看来,OUTER APPLY是最优雅,最有效的方式。它还非常清楚地显示了您所追求的内容:对于每个客户,我们正在寻找一个电话号码,这是列表中的第一个,当按ListIx订购时。

此处我们应该使用OUTER APPLY,而不是CROSS APPLY,因为客户可能没有任何电话号码。使用OUTER APPLY,客户将被包含在结果中,并且电话号码为NULL。使用CROSS APPLY此类客户不会包含在结果中。

实际上,这取决于你真正需要的是什么,所以CROSS APPLY可能是正确的选择。

答案 1 :(得分:1)

select 
   FirstName,
   LastName,
   PhoneNo
from (
    select
        ROW_NUMBER() OVER (
             PARTITION BY
                c.CusId 
             ORDER BY
                pn.ListIx DESC
        ) r,
        c.FirstName, 
        c.LastName, 
        pn.PhoneNo
    from Customers c
    left join PhoneNumbers pn on 
        c.CusId = pn.CusId
) T
where
    r = 1

答案 2 :(得分:0)

以下内容适用于SQL Server:

select c.FirstName, c.LastName,
       (select top 1 pn.PhoneNo
        from PhoneNumbers pn
        where c.CusId = pn.CusId
        order by ListIx
       ) as phoneNo
from Customers c;

对于这两个数据库,我认为这有效:

select c.FirstName, c.LastName,
       (select PhoneNo
        from (select pn.PhoneNo, row_number() over (partition by CusId order by LIstIx) as seqnum
              from PhoneNumbers pn
              where c.CusId = pn.CusId
             ) t
        where seqnum = 1
       ) as phoneNo
from Customers c;

当然,并非所有版本的Sybase都支持窗口函数。