查找多列的第一个非空值

时间:2010-01-11 22:03:19

标签: sql sql-server

我试图在一组很多列中获取第一个非null值。我知道我可以使用每列的子查询来完成此操作。在性能的名称中,在这种情况下确实很重要,我想一次性完成这项工作。

采用以下示例数据:

col1     col2     col3     sortCol
====================================
NULL     4        8        1
1        NULL     0        2
5        7        NULL     3

我的梦想查询会在每个数据列中找到第一个非空值,按sortCol排序。

例如,在选择前三列的魔法聚合时,按sortCol降序排序。

col1     col2     col3
========================
5        7         0

或者升序排序时:

col1     col2     col3
========================
1        4         8

有人知道解决方案吗?

5 个答案:

答案 0 :(得分:8)

在拒绝之前,您是否真的对此解决方案进行了性能测试?

SELECT
    (SELECT TOP(1) col1 FROM Table1 WHERE col1 IS NOT NULL ORDER BY SortCol) AS col1,
    (SELECT TOP(1) col2 FROM Table1 WHERE col2 IS NOT NULL ORDER BY SortCol) AS col2,
    (SELECT TOP(1) col3 FROM Table1 WHERE col3 IS NOT NULL ORDER BY SortCol) AS col3

如果这很慢,可能是因为你没有合适的索引。你有什么指数?

答案 1 :(得分:6)

将此实现为聚合的问题(例如,如果您实现了“First-Non-Null”SQL CLR聚合,您确实可以执行此操作)是在通常只读取每行时浪费的IO对前几行很感兴趣。聚合不会在第一个非null之后停止,即使它的实现会忽略其他值。聚合也是无序的,因此您的结果将取决于查询引擎选择的索引的顺序。

相比之下,子查询解决方案为每个查询读取最少的行(因为您只需要第一个匹配的行)并支持任何排序。它也适用于无法定义自定义聚合的数据库平台。

哪一个表现更好可能取决于表中的行数和列数以及数据的稀疏程度。其他行需要为聚合方法读取更多行。其他列需要其他子查询。稀疏数据需要在每个子查询中检查更多行。

以下是各种表格大小的一些结果:

Rows  Cols  Aggregation IO  CPU  Subquery IO  CPU
3     3                 2   0             6   0
1728  3                 8   63            6   0
1728  8                 12  266           16  0

此处测量的IO是逻辑读取的数量。请注意,子查询方法的逻辑读取数不会随表中的行数而变化。还要记住,每个附加子查询执行的逻辑读取可能适用于相同的数据页(包含前几行)。另一方面,聚合必须处理整个表,并且需要一些CPU时间来完成。

这是我用来测试的代码...... SortCol上的聚簇索引是必需的,因为(在这种情况下)它将确定聚合的顺序。

定义表并插入测试数据:

CREATE TABLE Table1 (Col1 int null, Col2 int null, Col3 int null, SortCol int);
CREATE CLUSTERED INDEX IX_Table1 ON Table1 (SortCol);

WITH R (i) AS
(
 SELECT null

 UNION ALL

 SELECT 0

 UNION ALL

 SELECT i + 1
 FROM R
 WHERE i < 10
)
INSERT INTO Table1
SELECT a.i, b.i, c.i, ROW_NUMBER() OVER (ORDER BY NEWID())
FROM R a, R b, R c;

查询表格:

SET STATISTICS IO ON;

--aggregation
SELECT TOP(0) * FROM Table1 --shortcut to convert columns back to their types
UNION ALL
SELECT
 dbo.FirstNonNull(Col1),
 dbo.FirstNonNull(Col2),
 dbo.FirstNonNull(Col3),
 null
FROM Table1;


--subquery
SELECT
    (SELECT TOP(1) Col1 FROM Table1 WHERE Col1 IS NOT NULL ORDER BY SortCol) AS Col1,
    (SELECT TOP(1) Col2 FROM Table1 WHERE Col2 IS NOT NULL ORDER BY SortCol) AS Col2,
    (SELECT TOP(1) Col3 FROM Table1 WHERE Col3 IS NOT NULL ORDER BY SortCol) AS Col3;

要测试的CLR“first-non-null”聚合:

 [Serializable]
 [SqlUserDefinedAggregate(
  Format.UserDefined,
  IsNullIfEmpty = true,
  IsInvariantToNulls = true,
  IsInvariantToDuplicates = true,
  IsInvariantToOrder = false, 
#if(SQL90)
  MaxByteSize = 8000
#else
  MaxByteSize = -1
#endif
 )]
 public sealed class FirstNonNull : IBinarySerialize
 {
  private SqlBinary Value;

  public void Init()
  {
   Value = SqlBinary.Null;
  }

  public void Accumulate(SqlBinary next)
  {
   if (Value.IsNull && !next.IsNull)
   {
    Value = next;
   }
  }

  public void Merge(FirstNonNull other)
  {
   Accumulate(other.Value);
  }

  public SqlBinary Terminate()
  {
   return Value;
  }

  #region IBinarySerialize Members

  public void Read(BinaryReader r)
  {
   int Length = r.ReadInt32();

   if (Length < 0)
   {
    Value = SqlBinary.Null;
   }
   else
   {
    byte[] Buffer = new byte[Length];
    r.Read(Buffer, 0, Length);

    Value = new SqlBinary(Buffer);
   }
  }

  public void Write(BinaryWriter w)
  {
   if (Value.IsNull)
   {
    w.Write(-1);
   }
   else
   {
    w.Write(Value.Length);
    w.Write(Value.Value);
   }
  }

  #endregion
 }

答案 2 :(得分:1)

不完全优雅,但它可以在一个查询中完成。虽然这可能会使任何索引变得无用,但如上所述,多子查询方法可能会更快。


create table Foo (data1 tinyint, data2 tinyint, data3 tinyint, seq int not null)
go

insert into Foo (data1, data2, data3, seq)
values (NULL, 4, 8, 1), (1, NULL, 0, 2), (5, 7, NULL, 3)
go

with unpivoted as (
    select seq, value, col
    from (select seq, data1, data2, data3 from Foo) a
    unpivot (value FOR col IN (data1, data2, data3)) b
), firstSeq as (
    select min(seq) as seq, col
    from unpivoted
    group by col
), data as (
    select b.col, b.value
    from firstSeq a
    inner join unpivoted b on a.seq = b.seq and a.col = b.col
)
select * from data pivot (min(value) for col in (data1, data2, data3)) d
go

drop table Foo
go

答案 3 :(得分:1)

这是另一种方法。如果您的数据库不允许子查询中的top(N)(例如我的,Teradata),这将是最有用的。

为了比较,这是其他人提到的解决方案,使用top(1)

select top(1) Col1 
from Table1 
where Col1 is not null 
order by SortCol asc

在一个理想的世界里,在我看来,这似乎是最好的方式 - 干净,直观,高效(显然)。

或者你可以这样做:

select max(Col1) -- max() guarantees a unique result
from Table1 
where SortCol in (
    select min(SortCol) 
    from Table1 
    where Col1 is not null
)

两个解决方案都沿着有序列检索“第一个”记录。 Top(1)确实更优雅,也可能更有效率。第二种方法在概念上做同样的事情,从代码的角度来看,只需要更多的手动/显式实现。

根选择中max()的原因是,如果值min(SortCol)出现在Table1中的多个行中,则可以获得多个结果。顺便说一下,我不确定Top(1)如何处理这种情况。

答案 4 :(得分:0)

使用first_value()

first_value(col)可以与and OVER (ORDER BY CASE WHEN col IS NOT NULL THEN sortcol ELSE maxvalue END)一起使用。 ELSE maxvalue是必需的,因为SQL Server首先对null进行排序)

CREATE TABLE foo(a int, b int, c int, sortCol int);
INSERT INTO foo VALUES
    (null, 4, 8, 1),
    (1, null, 0, 2),
    (5, 7, null, 3);

现在,您可以看到我们需要执行什么操作来强制将空值排在sortcol之后。要执行desc,必须确保它们的值为负数。

SELECT TOP(1)
     first_value(a) OVER (ORDER BY CASE WHEN a IS NOT NULL THEN sortcol ELSE 2^31-1 END) AS a,
     first_value(b) OVER (ORDER BY CASE WHEN b IS NOT NULL THEN sortcol ELSE 2^31-1 END) AS b,
     first_value(c) OVER (ORDER BY CASE WHEN c IS NOT NULL THEN sortcol ELSE 2^31-1 END) AS c
FROM foo;

PostgreSQL

PostgreSQL稍微简单一点,

CREATE TABLE foo(a,b,c,sortCol)
AS VALUES
  (null, 4, 8, 1),
  (1, null, 0, 2),
  (5, 7, null, 3);

SELECT
     first_value(a) OVER (ORDER BY CASE WHEN a IS NOT NULL THEN sortcol END) AS a,
     first_value(b) OVER (ORDER BY CASE WHEN b IS NOT NULL THEN sortcol END) AS b,
     first_value(c) OVER (ORDER BY CASE WHEN c IS NOT NULL THEN sortcol END) AS c
FROM foo
FETCH FIRST ROW ONLY;

我相信,当RDBMS开始采用IGNORE NULLS时,所有这些都将消失。然后就是first_value(a IGNORE NULLS)