Wie erhalte ich den letzten Nicht-Null-Wert in einer geordneten Spalte einer riesigen Tabelle?

13

Ich habe folgenden Input:

 id | value 
----+-------
  1 |   136
  2 |  NULL
  3 |   650
  4 |  NULL
  5 |  NULL
  6 |  NULL
  7 |   954
  8 |  NULL
  9 |   104
 10 |  NULL

Ich erwarte folgendes Ergebnis:

 id | value 
----+-------
  1 |   136
  2 |   136
  3 |   650
  4 |   650
  5 |   650
  6 |   650
  7 |   954
  8 |   954
  9 |   104
 10 |   104

Die einfache Lösung wäre, die Tabellen mit einer <Relation zu verknüpfen und dann den MAXWert in a auszuwählen GROUP BY:

WITH tmp AS (
  SELECT t2.id, MAX(t1.id) AS lastKnownId
  FROM t t1, t t2
  WHERE
    t1.value IS NOT NULL
    AND
    t2.id >= t1.id
  GROUP BY t2.id
)
SELECT
  tmp.id, t.value
FROM t, tmp
WHERE t.id = tmp.lastKnownId;

Die einfache Ausführung dieses Codes würde jedoch intern das Quadrat der Anzahl der Zeilen der Eingabetabelle erzeugen ( O (n ^ 2) ). Ich habe erwartet, dass t-sql es optimiert - auf Block- / Record-Ebene ist die Aufgabe sehr einfach und linear, im Wesentlichen eine for-Schleife ( O (n) ).

In meinen Experimenten kann die aktuelle Version von MS SQL 2016 diese Abfrage jedoch nicht ordnungsgemäß optimieren, sodass diese Abfrage für eine große Eingabetabelle nicht ausgeführt werden kann.

Darüber hinaus muss die Abfrage schnell ausgeführt werden, was eine ähnlich einfache (aber sehr unterschiedliche) Lösung auf Cursorbasis unmöglich macht.

Die Verwendung einer speichergestützten temporären Tabelle könnte ein guter Kompromiss sein, aber ich bin nicht sicher, ob sie wesentlich schneller ausgeführt werden kann, da meine Beispielabfrage mit Unterabfragen nicht funktioniert hat.

Ich denke auch darüber nach, eine Fensterfunktion aus den t-sql-Dokumenten herauszusuchen, die ausgetrickst werden könnte, um das zu tun, was ich will. Zum Beispiel funktioniert die kumulative Summe sehr ähnlich, aber ich konnte es nicht austricksen, um das neueste Nicht-Null-Element und nicht die Summe der vorherigen Elemente zu erhalten.

Die ideale Lösung wäre eine schnelle Abfrage ohne Prozedurcode oder temporäre Tabellen. Alternativ ist auch eine Lösung mit temporären Tabellen in Ordnung, die prozedurale Iteration der Tabelle jedoch nicht.

Peterh: Setzen Sie Monica wieder ein
quelle

Antworten:

12

Eine übliche Lösung für diese Art von Problem gibt Itzik Ben-Gan in seinem Artikel Das letzte nicht-NULL-Puzzle :

DROP TABLE IF EXISTS dbo.Example;

CREATE TABLE dbo.Example
(
    id integer PRIMARY KEY,
    val integer NULL
);

INSERT dbo.Example
    (id, val)
VALUES
    (1, 136),
    (2, NULL),
    (3, 650),
    (4, NULL),
    (5, NULL),
    (6, NULL),
    (7, 954),
    (8, NULL),
    (9, 104),
    (10, NULL);

SELECT
    E.id,
    E.val,
    lastval =
        CAST(
            SUBSTRING(
                MAX(CAST(E.id AS binary(4)) + CAST(E.val AS binary(4))) OVER (
                    ORDER BY E.id
                    ROWS UNBOUNDED PRECEDING),
            5, 4)
        AS integer)
FROM dbo.Example AS E
ORDER BY
    E.id;

Demo: db <> fiddle

Paul White 9
quelle
11

Ich habe erwartet, dass t-sql es optimiert - auf Block- / Record-Ebene ist die Aufgabe sehr einfach und linear, im Wesentlichen eine for-Schleife (O (n)).

Das ist nicht die Abfrage, die Sie geschrieben haben. Dies entspricht möglicherweise nicht der von Ihnen geschriebenen Abfrage, abhängig von einigen ansonsten geringfügigen Details des Tabellenschemas. Sie erwarten zu viel vom Abfrageoptimierer.

Mit der richtigen Indizierung können Sie den gesuchten Algorithmus über das folgende T-SQL erhalten:

SELECT t1.id, ca.[VALUE] 
FROM dbo.[BIG_TABLE(FOR_U)] t1
CROSS APPLY (
    SELECT TOP (1) [VALUE]
    FROM dbo.[BIG_TABLE(FOR_U)] t2
    WHERE t2.ID <= t1.ID AND t2.[VALUE] IS NOT NULL
    ORDER BY t2.ID DESC
) ca; --ORDER BY t1.ID ASC

Für jede Zeile durchläuft der Abfrageprozessor den Index rückwärts und stoppt, wenn er eine Zeile mit einem Wert ungleich Null für findet [VALUE]. Auf meinem Computer ist dies in etwa 90 Sekunden für 100 Millionen Zeilen in der Quelltabelle abgeschlossen. Die Abfrage wird länger als erforderlich ausgeführt, da einige Zeit auf dem Client verschwendet wird, der alle diese Zeilen verwirft.

Mir ist nicht klar, ob Sie geordnete Ergebnisse benötigen oder was Sie mit einer so großen Ergebnismenge vorhaben. Die Abfrage kann an das aktuelle Szenario angepasst werden. Der größte Vorteil dieses Ansatzes besteht darin, dass keine Sortierung im Abfrageplan erforderlich ist. Dies kann bei größeren Ergebnismengen hilfreich sein. Ein Nachteil ist, dass die Leistung nicht optimal ist, wenn die Tabelle viele NULL-Werte enthält, da viele Zeilen aus dem Index gelesen und verworfen werden. Sie sollten in der Lage sein, die Leistung mit einem gefilterten Index zu verbessern, der für diesen Fall NULL ausschließt.

Beispieldaten für den Test:

DROP TABLE IF EXISTS #t;

CREATE TABLE #t (
ID BIGINT NOT NULL
);

INSERT INTO #t WITH (TABLOCK)
SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

DROP TABLE IF EXISTS dbo.[BIG_TABLE(FOR_U)];

CREATE TABLE dbo.[BIG_TABLE(FOR_U)] (
ID BIGINT NOT NULL,
[VALUE] BIGINT NULL
);

INSERT INTO dbo.[BIG_TABLE(FOR_U)] WITH (TABLOCK)
SELECT 10000 * t1.ID + t2.ID, CASE WHEN (t1.ID + t2.ID) % 3 = 1 THEN t2.ID ELSE NULL END
FROM #t t1
CROSS JOIN #t t2;

CREATE UNIQUE CLUSTERED INDEX ADD_ORDERING ON dbo.[BIG_TABLE(FOR_U)] (ID);
Joe Obbish
quelle
7

Eine Methode, mit OVER()und MAX()und COUNT()basierend auf dieser Quelle könnte sein:

SELECT ID, MAX(value) OVER (PARTITION BY Value2) as value
FROM
(
    SELECT ID, value
        ,COUNT(value) OVER (ORDER BY ID) AS Value2
    FROM dbo.HugeTable
) a
ORDER BY ID;

Ergebnis

Id  UpdatedValue
1   136
2   136
3   650
4   650
5   650
6   650
7   954
8   954
9   104
10  104

Eine andere Methode, die auf dieser Quelle basiert und eng mit dem ersten Beispiel verwandt ist

;WITH CTE As 
( 
SELECT  value,
        Id, 
        COUNT(value) 
        OVER(ORDER BY Id) As  Value2 
FROM dbo.HugeTable
),

CTE2 AS ( 
SELECT Id,
       value,
       First_Value(value)  
       OVER( PARTITION BY Value2
             ORDER BY Id) As UpdatedValue 
FROM CTE 
            ) 
SELECT Id,UpdatedValue 
FROM CTE2;
Randi Vertongen
quelle
3
Erwägen Sie das Hinzufügen von Details zur Leistung dieser Ansätze bei einer "großen Tabelle".
Joe Obbish