Sql 为多个列查找第一个非空值

Sql 为多个列查找第一个非空值,sql,sql-server,Sql,Sql Server,我正在尝试获取一组多个列中的第一个非空值。我知道我可以使用每列的子查询来实现这一点。以性能的名义,这在这个场景中确实很重要,我想在一次过程中完成这项工作 以以下数据为例: col1 col2 col3 sortCol ==================================== NULL 4 8 1 1 NULL 0 2 5 7 NULL 3 我的梦想

我正在尝试获取一组多个列中的第一个非空值。我知道我可以使用每列的子查询来实现这一点。以性能的名义,这在这个场景中确实很重要,我想在一次过程中完成这项工作

以以下数据为例:

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

有人知道解决方案吗?

在拒绝此解决方案之前,您是否确实对其进行了性能测试

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

如果这是缓慢的,可能是因为你没有一个适当的索引。你有什么索引?

不是很优雅,但它可以在一个查询中完成。尽管这可能会使任何索引变得毫无用处,所以正如前面提到的,多个子查询方法可能会更快


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

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

相反,子查询解决方案为每个查询读取最少的行(因为您只需要第一个匹配行),并支持任何排序。它还可以在无法定义自定义聚合的数据库平台上工作

哪一个性能更好可能取决于表中的行数和列数以及数据的稀疏程度。对于聚合方法,额外的行需要读取更多的行。额外的列需要额外的子查询。稀疏数据要求在每个子查询中检查更多行

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

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;
要测试的CLR“第一个非空”聚合:

 [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
 }
[可序列化]
[SqlUserDefinedAggregate(
Format.UserDefined,
IsNullIfEmpty=true,
IsInvariantToNulls=真,
IsInvariantToDuplicates=真,
IsInvariantToOrder=false,
#如果(SQL90)
MaxByteSize=8000
#否则
MaxByteSize=-1
#恩迪夫
)]
公共密封类FirstNonNull:IBinarySerialize
{
私有二进制值;
公共void Init()
{
Value=SqlBinary.Null;
}
公共无效累积(SqlBinary下一步)
{
if(Value.IsNull&&!next.IsNull)
{
值=下一个;
}
}
公共无效合并(第一个非空其他)
{
累积(其他价值);
}
公共SqlBinary终止()
{
返回值;
}
#区域IBinarySerialize成员
公共无效读取(二进制读取器r)
{
int Length=r.ReadInt32();
如果(长度<0)
{
Value=SqlBinary.Null;
}
其他的
{
字节[]缓冲区=新字节[长度];
r、 读取(缓冲区,0,长度);
值=新的SqlBinary(缓冲区);
}
}
公共无效写入(二进制写入程序w)
{
if(Value.IsNull)
{
w、 写入(-1);
}
其他的
{
w、 写入(值、长度);
w、 写(Value.Value);
}
}
#端区
}

这里有另一种方法。如果您的数据库不允许子查询(如mine、Teradata)中的top(N),这将非常有用

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

在一个理想的世界里,在我看来这是最好的方法——干净、直观、高效(显然)

或者,您可以执行以下操作:

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)
显示在
表1
的多行中,则可以得到多个结果。顺便说一下,我不确定
Top(1)
如何处理这种情况。

使用
first\u value()
第一个\u值(col)
可以与
一起使用,并且可以超过(col不为NULL时按情况排序,然后按sortcol ELSE maxvalue END)
ELSE maxvalue
是必需的,因为SQL Server首先对null进行排序)

现在您可以看到我们必须做些什么来强制null在
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开始采用
ignorenulls
时,所有这些都会消失。然后就是第一个值(忽略空值)

您需要第一个非空列还是第一个非空行?您只需要第一行还是需要整个集合。sortCol是唯一的吗?@feiththive:每列中的第一个非空值。我认为示例输出应该很好地显示所需的效果@马克·拜尔斯:因为我没有一个解决方案可以在一次过程中工作,所以我只能猜测它的性能,但是子查询方法还有很多需要改进的地方。在我的实际表格中,我有
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);
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;
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;