我试图在一组很多列中获取第一个非空值.我知道我可以使用每列的子查询来完成此操作.在性能的名称中,在这种情况下确实很重要,我想一次性完成这项工作.
以下示例数据为例:
col1 col2 col3 sortCol
====================================
NULL 4 8 1
1 NULL 0 2
5 7 NULL 3
Run Code Online (Sandbox Code Playgroud)
我的梦想查询会在每个数据列中找到第一个非空值,按照排序sortCol.
例如,在选择前三列的魔法聚合时,按sortCol降序排序.
col1 col2 col3
========================
5 7 0
Run Code Online (Sandbox Code Playgroud)
或者在升序排序时:
col1 col2 col3
========================
1 4 8
Run Code Online (Sandbox Code Playgroud)
有谁知道解决方案?
在拒绝之前,您是否对此解决方案进行了性能测试?
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
Run Code Online (Sandbox Code Playgroud)
如果这很慢,可能是因为你没有合适的索引.你有什么指数?
与实施本为聚合(你如果,例如,您实施了"第一非空" SQL CLR聚合确实可以做)的问题是浪费的IO时,你通常只在兴趣看的每一行前几行.聚合不会在第一个非null之后停止,即使它的实现会忽略其他值.聚合也是无序的,因此您的结果将取决于查询引擎选择的索引的顺序.
相反,子查询解决方案为每个查询读取最少的行(因为您只需要第一个匹配的行)并支持任何排序.它也适用于无法定义自定义聚合的数据库平台.
哪一个表现更好可能取决于表中的行数和列数以及数据的稀疏程度.其他行需要为聚合方法读取更多行.其他列需要其他子查询.Sparser数据需要检查每个子查询中的更多行.
以下是各种表格大小的一些结果:
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
Run Code Online (Sandbox Code Playgroud)
这里测量的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;
Run Code Online (Sandbox Code Playgroud)
查询表格:
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;
Run Code Online (Sandbox Code Playgroud)
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
}
Run Code Online (Sandbox Code Playgroud)
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);
Run Code Online (Sandbox Code Playgroud)
现在您可以看到我们必须做什么才能强制空值在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;
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
我相信当 RDBMS 开始采用时,这一切都会消失IGNORE NULLS。那么就这样了first_value(a IGNORE NULLS)。