2010-01-11 17 views
14

Sto tentando di ottenere il primo valore non nullo in un insieme di molte colonne. Sono consapevole che potrei realizzare questo usando una sotto-query per colonna. Nel nome della performance, che conta davvero in questo scenario, mi piacerebbe farlo in un solo passaggio.Trova i primi valori non nulli per più colonne

Prendere i seguenti dati esempio:

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

La mia domanda sogno sarebbe trovare il primo valore non nullo in ciascuna delle colonne di dati, ordinato sulla sortCol.

Ad esempio, quando si seleziona l'aggregato magico delle prime tre colonne, ordinato in base allo sortCol discendente.

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

O quando l'ordinamento ascendente:

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

Qualcuno sa una soluzione?

+0

Hai bisogno della prima colonna non nulla o della prima riga non nulla? – feihtthief

+1

Hai sempre solo bisogno della prima riga o potresti aver bisogno dell'intero set. SortCol è unico? – feihtthief

+0

@feihtthief: il primo valore non nullo in ogni colonna. Penso che l'output di esempio dovrebbe mostrare bene l'effetto desiderato. @Mark Byers: Dal momento che non ho una soluzione che funzionerà in un singolo passaggio, posso solo intuire le sue prestazioni, ma l'approccio della sub-query lascia molto a desiderare. Nel mio tavolo attuale, ho circa 20 file che devo arrotolare in questo modo. Con l'approccio sub-query, gli indici non sono particolarmente utili. Credo che un approccio di scansione singola abbia il potenziale di essere molto più veloce con molte colonne. – EvilRyry

risposta

7

Avete effettivamente testato questa soluzione prima di rifiutarla?

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 

Se questo è lento è probabilmente perché non si dispone di un indice appropriato. Quali indici hai?

+0

In questo momento esiste un indice su SortCol che viene utilizzato in modo abbastanza efficace. Dopo un po 'di indagini, ho ricostruito l'indice che ha aiutato sostanzialmente. Questa soluzione ora è di circa 10ms, che può essere abbastanza buona. Penso che potrei raccogliere ancora un po 'di tempo includendo alcune delle altre colonne per eliminare le ricerche RID che attualmente consumano 2/3 del tempo totale. – EvilRyry

1

Non proprio elegante, ma può farlo in una singola query. Sebbene questo probabilmente renderà gli indici piuttosto inutili, così come accennato, è probabile che il metodo di sub-query sia più veloce.


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 
6

Il problema di attuare questo come aggregazione (che effettivamente potrebbe fare se, ad esempio, è implementato un "primo non nullo" SQL CLR aggregato) è la sprecata IO leggere ogni riga quando si in genere sono interessato solo alle prime righe. L'aggregazione non si fermerà solo dopo il primo non null, anche se la sua implementazione ignorerebbe ulteriori valori. Anche le aggregazioni non sono ordinate, quindi il risultato dipende dall'ordinamento dell'indice selezionato dal motore di query.

La soluzione di subquery, al contrario, legge le righe minime per ogni query (poiché è necessaria solo la prima riga corrispondente) e supporta qualsiasi ordine. Funzionerà anche su piattaforme di database in cui non è possibile definire aggregati personalizzati.

Il rendimento migliore dipenderà probabilmente dal numero di righe e colonne nella tabella e da quanto scarsi siano i dati. Le righe aggiuntive richiedono la lettura di più righe per l'approccio aggregato. Le colonne aggiuntive richiedono subquery aggiuntive. I dati di Sparser richiedono il controllo di più righe all'interno di ciascuna subquery.

Ecco alcuni risultati di varie dimensioni da tavolo:

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 

L'IO misurata qui è il numero di letture logiche. Si noti che il numero di letture logiche per l'approccio di subquery non cambia con il numero di righe nella tabella. Inoltre, tieni presente che le letture logiche eseguite da ogni subquery aggiuntiva saranno probabilmente per le stesse pagine di dati (contenenti le prime poche righe). L'aggregazione, d'altra parte, deve elaborare l'intera tabella e richiede del tempo di CPU per farlo.

Questo è il codice che ho usato per testare ...l'indice cluster su SortCol è richiesto poiché (in questo caso) determinerà l'ordine dell'aggregazione.

Definizione del tavolo e l'inserimento dei dati di test:

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; 

Interrogazione tabella:

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 "primo non nullo" aggregato di prova:

[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 
} 
1

Ecco un altro modo per farlo. Ciò sarà di grande utilità se il tuo database non consente l'uso di top (N) nelle subquery (come mine, Teradata).

Per confronto, ecco la soluzione delle altre persone menzionati, utilizzando top(1):

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

In un mondo ideale, che mi sembra il modo migliore per farlo - pulita, intuitiva, efficiente (a quanto pare) .

In alternativa si può fare questo:

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

Entrambe le soluzioni recuperare la 'prima' record di lungo una colonna ordinata. Top(1) lo fa in modo decisamente più elegante e probabilmente più efficiente. Il secondo metodo fa la stessa cosa concettualmente, solo con più implementazione manuale/esplicita da una prospettiva di codice.

Il motivo per il max() nella selezione radice è che è possibile ottenere più risultati se il valore min(SortCol) compare in più di una riga in Table1. Non sono sicuro del modo in cui lo scenario Top(1) gestisce questo scenario.

Problemi correlati