sql >> Database teknologi >  >> RDS >> Database

NULL-kompleksiteter – Del 3, Manglende standardfunktioner og T-SQL-alternativer

Denne artikel er den tredje del i en serie om NULL-kompleksiteter. I del 1 dækkede jeg betydningen af ​​NULL-markøren og hvordan den opfører sig i sammenligninger. I del 2 beskrev jeg NULL-behandlingens uoverensstemmelser i forskellige sprogelementer. I denne måned beskriver jeg kraftfulde standard NULL-håndteringsfunktioner, der endnu ikke er nået til T-SQL, og de løsninger, som folk i øjeblikket bruger.

Jeg fortsætter med at bruge prøvedatabasen TSQLV5 som sidste måned i nogle af mine eksempler. Du kan finde scriptet, der opretter og udfylder denne database her, og dets ER-diagram her.

DISTINCT prædikat

I del 1 i serien forklarede jeg, hvordan NULL'er opfører sig i sammenligninger og kompleksiteten omkring den tre-værdiprædikatlogik, som SQL og T-SQL anvender. Overvej følgende prædikat:

X =Y

Hvis et prædikant er NULL - inklusive når begge er NULL - er resultatet af dette prædikat den logiske værdi UKENDT. Med undtagelse af operatorerne IS NULL og IS NOT NULL, gælder det samme for alle andre operatorer, inklusive anderledes end (<>):

X <> Y

Ofte i praksis ønsker man, at NULL'er skal opføre sig ligesom ikke-NULL-værdier til sammenligningsformål. Det er især tilfældet, når du bruger dem til at repræsentere manglende, men uanvendelige værdier. Standarden har en løsning til dette behov i form af en funktion kaldet DISTINCT prædikatet, som bruger følgende form:

ER [IKKE] FORSKELLIG FRA

I stedet for at bruge ligheds- eller ulighedssemantik, bruger dette prædikat distinktitetsbaseret semantik, når man sammenligner prædikander. Som et alternativ til en lighedsoperator (=), vil du bruge følgende formular til at få en SAND, når de to prædikander er ens, inklusive når begge er NULL, og en FALSK, når de ikke er det, inklusive når den ene er NULL og andet er ikke:

X ER IKKE FORSKELLENDE FRA Y

Som et alternativ til en anden end operator (<>), vil du bruge følgende form til at få en TRUE, når de to prædikander er forskellige, inklusive når den ene er NULL, og den anden ikke er, og en FALSE, når de er ens, inklusive når begge er NULL:

X ER FORSKELLIG FRA Y

Lad os anvende DISTINCT-prædikatet på de eksempler, vi brugte i del 1 i serien. Husk, at du skulle skrive en forespørgsel, der givet en inputparameter @dt returnerer ordrer, der blev afsendt på inputdatoen, hvis den ikke er NULL, eller som slet ikke blev afsendt, hvis inputtet er NULL. I henhold til standarden vil du bruge følgende kode med DISTINCT-prædikatet til at håndtere dette behov:

SELECT orderid, shippeddate
FROM Sales.Orders
WHERE shippeddate IS NOT DISTINCT FROM @dt;

Indtil videre husker du fra del 1, at du kan bruge en kombination af EXISTS-prædikatet og INTERSECT-operatoren som en SARG-bar løsning i T-SQL, sådan:

SELECT orderid, shippeddate
FROM Sales.Orders
WHERE EXISTS(SELECT shippeddate INTERSECT SELECT @dt);

For at returnere ordrer, der blev afsendt på en anden dato end (adskilt fra) inputdatoen @dt, skal du bruge følgende forespørgsel:

SELECT orderid, shippeddate
FROM Sales.Orders
WHERE shippeddate IS DISTINCT FROM @dt;

Løsningen, der virker i T-SQL, bruger en kombination af EXISTS-prædikatet og EXCEPT-operatoren, som sådan:

SELECT orderid, shippeddate
FROM Sales.Orders
WHERE EXISTS(SELECT shippeddate EXCEPT SELECT @dt);

I del 1 diskuterede jeg også scenarier, hvor du skal forbinde tabeller og anvende distinktitetsbaseret semantik i joinprædikatet. I mine eksempler brugte jeg tabeller kaldet T1 og T2, med NULLable join-kolonner kaldet k1, k2 og k3 på begge sider. I henhold til standarden vil du bruge følgende kode til at håndtere en sådan joinforbindelse:

SELECT T1.k1, T1.K2, T1.K3, T1.val1, T2.val2
FROM dbo.T1
INNER JOIN dbo.T2
  ON T1.k1 IS NOT DISTINCT FROM T2.k1
 AND T1.k2 IS NOT DISTINCT FROM T2.k2
 AND T1.k3 IS NOT DISTINCT FROM T2.k3;

Lige nu kan du, i lighed med de tidligere filtreringsopgaver, bruge en kombination af EXISTS-prædikatet og INTERSECT-operatoren i joinets ON-klausul til at efterligne det distinkte prædikat i T-SQL, som sådan:

SELECT T1.k1, T1.K2, T1.K3, T1.val1, T2.val2
FROM dbo.T1
INNER JOIN dbo.T2
  ON EXISTS(SELECT T1.k1, T1.k2, T1.k3 INTERSECT SELECT T2.k1, T2.k2, T2.k3);

Når den bruges i et filter, kan denne formular SARG, og når den bruges i joins, kan denne formular muligvis stole på indeksrækkefølge.

Hvis du gerne vil se DISTINCT-prædikatet tilføjet til T-SQL, kan du stemme på det her.

Hvis du efter at have læst dette afsnit stadig føler dig en smule utryg ved DISTINCT-prædikatet, er du ikke alene. Måske er dette prædikat meget bedre end nogen eksisterende løsning, vi i øjeblikket har i T-SQL, men det er en smule udførligt og lidt forvirrende. Den bruger en negativ form til at anvende, hvad der i vores sind er en positiv sammenligning, og omvendt. Nå, ingen sagde, at alle standardforslagene er perfekte. Som Charlie bemærkede i en af ​​sine kommentarer til del 1, ville følgende forenklede formular fungere bedre:

ER [ IKKE ]

Det er kortfattet og meget mere intuitivt. I stedet for X ER IKKE FORSKELLIG FRA Y, ville du bruge:

X ER Y

Og i stedet for X ER FORSKELLIG FRA Y, ville du bruge:

X ER IKKE Y

Denne foreslåede operator er faktisk tilpasset de allerede eksisterende IS NULL og IS NOT NULL operatorer.

Anvendt på vores forespørgselsopgave, for at returnere ordrer, der blev afsendt på inputdatoen (eller som ikke blev afsendt, hvis input er NULL), ville du bruge følgende kode:

SELECT orderid, shippeddate
FROM Sales.Orders
WHERE shippeddate IS @dt;

For at returnere ordrer, der blev afsendt på en anden dato end inputdatoen, skal du bruge følgende kode:

SELECT orderid, shippeddate
FROM Sales.Orders
WHERE shippeddate IS NOT @dt;

Hvis Microsoft nogensinde beslutter sig for at tilføje det distinkte prædikat, ville det være godt, hvis de understøttede både standard verbose form og denne ikke-standard, endnu mere kortfattede og mere intuitive form. Mærkeligt nok understøtter SQL Servers forespørgselsprocessor allerede en intern sammenligningsoperator IS, som bruger samme semantik som den ønskede IS-operator, jeg beskrev her. Du kan finde detaljer om denne operatør i Paul Whites artikel Undocumented Query Plans:Equality Comparisons (opslag "IS i stedet for EQ"). Det, der mangler, er at eksponere det eksternt som en del af T-SQL.

NULL-behandlingsklausul (IGNOR NULLS | RESPECT NULLS)

Når du bruger offset-vinduets funktioner LAG, LEAD, FIRST_VALUE og LAST_VALUE, skal du nogle gange kontrollere NULL-behandlingsadfærden. Som standard returnerer disse funktioner resultatet af det anmodede udtryk i den anmodede position, uanset om resultatet af udtrykket er en faktisk værdi eller en NULL. Nogle gange vil du dog fortsætte med at bevæge dig i den relevante retning (tilbage for LAG og LAST_VALUE, fremad for LEAD og FIRST_VALUE), og returnere den første ikke-NULL-værdi, hvis den er til stede, og NULL ellers. Standarden giver dig kontrol over denne adfærd ved hjælp af en NULL-behandlingsklausul med følgende syntaks:

offset_function() IGNORE_NULLS | RESPECT NULLS OVER()

Standarden i tilfælde af, at NULL-behandlingsklausulen ikke er angivet, er RESPECT NULLS-indstillingen, hvilket betyder at returnere alt, hvad der er til stede i den anmodede position, selvom NULL. Desværre er denne klausul endnu ikke tilgængelig i T-SQL. Jeg vil give eksempler på standardsyntaksen ved hjælp af LAG- og FIRST_VALUE-funktionerne, samt løsninger, der virker i T-SQL. Du kan bruge lignende teknikker, hvis du har brug for en sådan funktionalitet med LEAD og LAST_VALUE.

Som eksempeldata vil jeg bruge en tabel kaldet T4, som du opretter og udfylder ved hjælp af følgende kode:

DROP TABLE IF EXISTS dbo.T4;
GO
 
CREATE TABLE dbo.T4
(
  id INT NOT NULL CONSTRAINT PK_T4 PRIMARY KEY,
  col1 INT NULL
);
 
INSERT INTO dbo.T4(id, col1) VALUES
( 2, NULL),
( 3,   10),
( 5,   -1),
( 7, NULL),
(11, NULL),
(13,  -12),
(17, NULL),
(19, NULL),
(23, 1759);

Der er en almindelig opgave, der involverer returnering af det sidste relevante værdi. En NULL i col1 indikerer ingen ændring i værdien, hvorimod en ikke-NULL værdi indikerer en ny relevant værdi. Du skal returnere den sidste ikke-NULL col1-værdi baseret på id-bestilling. Ved at bruge standard NULL-behandlingsklausulen ville du håndtere opgaven sådan:

SELECT id, col1,
COALESCE(col1, LAG(col1) IGNORE NULLS OVER(ORDER BY id)) AS lastval
FROM dbo.T4;

Her er det forventede output fra denne forespørgsel:

id          col1        lastval
----------- ----------- -----------
2           NULL        NULL
3           10          10
5           -1          -1
7           NULL        -1
11          NULL        -1
13          -12         -12
17          NULL        -12
19          NULL        -12
23          1759        1759

Der er en løsning i T-SQL, men det involverer to lag af vinduesfunktioner og et tabeludtryk.

I det første trin bruger du MAX-vinduefunktionen til at beregne en kolonne kaldet grp, der har den maksimale id-værdi indtil videre, når col1 ikke er NULL, som sådan:

SELECT id, col1,
MAX(CASE WHEN col1 IS NOT NULL THEN id END)
  OVER(ORDER BY id
       ROWS UNBOUNDED PRECEDING) AS grp
FROM dbo.T4;

Denne kode genererer følgende output:

id          col1        grp
----------- ----------- -----------
2           NULL        NULL
3           10          3
5           -1          5
7           NULL        5
11          NULL        5
13          -12         13
17          NULL        13
19          NULL        13
23          1759        23

Som du kan se, oprettes der en unik grp-værdi, når der sker en ændring i col1-værdien.

I det andet trin definerer du en CTE baseret på forespørgslen fra det første trin. Derefter returnerer du i den ydre forespørgsel den maksimale col1-værdi indtil videre inden for hver partition defineret af grp. Det er den sidste ikke-NULL col1-værdi. Her er den komplette løsningskode:

WITH C AS
(
SELECT id, col1,
  MAX(CASE WHEN col1 IS NOT NULL THEN id END)
    OVER(ORDER BY id
         ROWS UNBOUNDED PRECEDING) AS grp
FROM dbo.T4
)
SELECT id, col1,
MAX(col1) OVER(PARTITION BY grp
               ORDER BY id
               ROWS UNBOUNDED PRECEDING) AS lastval
FROM C;

Det er klart, at det er meget mere kode og arbejde sammenlignet med bare at sige IGNORE_NULLS.

Et andet almindeligt behov er at returnere den første relevante værdi. I vores tilfælde, antag, at du skal returnere den første ikke-NULL col1-værdi indtil videre baseret på id-bestilling. Ved at bruge standard NULL-behandlingsklausulen ville du håndtere opgaven med FIRST_VALUE-funktionen og IGNORE NULLS-indstillingen, som sådan:

SELECT id, col1,
FIRST_VALUE(col1) IGNORE NULLS 
  OVER(ORDER BY id
       ROWS UNBOUNDED PRECEDING) AS firstval
FROM dbo.T4;

Her er det forventede output fra denne forespørgsel:

id          col1        firstval
----------- ----------- -----------
2           NULL        NULL
3           10          10
5           -1          10
7           NULL        10
11          NULL        10
13          -12         10
17          NULL        10
19          NULL        10
23          1759        10

Løsningen i T-SQL bruger en teknik, der ligner den, der blev brugt til den sidste ikke-NULL-værdi, kun i stedet for en dobbelt-MAX-tilgang, bruger du FIRST_VALUE-funktionen oven på en MIN-funktion.

I det første trin bruger du MIN-vinduefunktionen til at beregne en kolonne kaldet grp, der har den mindste id-værdi indtil videre, når col1 ikke er NULL, som sådan:

SELECT id, col1,
MIN(CASE WHEN col1 IS NOT NULL THEN id END)
  OVER(ORDER BY id
       ROWS UNBOUNDED PRECEDING) AS grp
FROM dbo.T4;

Denne kode genererer følgende output:

id          col1        grp
----------- ----------- -----------
2           NULL        NULL
3           10          3
5           -1          3
7           NULL        3
11          NULL        3
13          -12         3
17          NULL        3
19          NULL        3
23          1759        3

Hvis der er NULL'er til stede før den første relevante værdi, ender du med to grupper - den første med NULL som grp-værdi og den anden med det første ikke-NULL-id som grp-værdi.

I det andet trin placerer du det første trins kode i et tabeludtryk. Så i den ydre forespørgsel bruger du FIRST_VALUE-funktionen, partitioneret af grp, til at indsamle den første relevante (ikke-NULL) værdi, hvis den er til stede, og NULL ellers, som sådan:

WITH C AS
(
SELECT id, col1,
  MIN(CASE WHEN col1 IS NOT NULL THEN id END)
    OVER(ORDER BY id
         ROWS UNBOUNDED PRECEDING) AS grp
FROM dbo.T4
)
SELECT id, col1,
FIRST_VALUE(col1) 
  OVER(PARTITION BY grp
       ORDER BY id
       ROWS UNBOUNDED PRECEDING) AS firstval
FROM C;

Igen, det er meget kode og arbejde sammenlignet med blot at bruge IGNORE_NULLS-indstillingen.

Hvis du føler, at denne funktion kan være nyttig for dig, kan du stemme for dens optagelse i T-SQL her.

ORDER AF NULLER FØRST | NULLER VARER

Når du bestiller data, hvad enten det er til præsentationsformål, vinduesvisning, TOP/OFFSET-FETCH-filtrering eller ethvert andet formål, er der spørgsmålet om, hvordan NULL'er skal opføre sig i denne sammenhæng? SQL-standarden siger, at NULL'er skal sortere sammen enten før eller efter ikke-NULL'er, og de overlader det til implementeringen at bestemme den ene eller den anden måde. Uanset hvad leverandøren vælger, skal det dog være konsekvent. I T-SQL ordnes NULL'er først (før ikke-NULL'er), når du bruger stigende rækkefølge. Overvej følgende forespørgsel som et eksempel:

SELECT orderid, shippeddate
FROM Sales.Orders
ORDER BY shippeddate, orderid;

Denne forespørgsel genererer følgende output:

orderid     shippeddate
----------- -----------
11008       NULL
11019       NULL
11039       NULL
...
10249       2017-07-10
10252       2017-07-11
10250       2017-07-12
...
11063       2019-05-06
11067       2019-05-06
11069       2019-05-06

Outputtet viser, at ikke-afsendte ordrer, som har en NULL afsendelsesdato, bestiller før afsendte ordrer, som har en eksisterende gældende afsendelsesdato.

Men hvad nu hvis du har brug for NULLs for at bestille sidst, når du bruger stigende rækkefølge? ISO/IEC SQL-standarden understøtter en klausul, som du anvender på et bestillingsudtryk, der kontrollerer, om NULLs rækkefølge først eller sidst. Syntaksen for denne klausul er:

NULLER FØRST | NULLER VARER

For at håndtere vores behov, returnering af ordrer sorteret efter deres afsendelsesdatoer, stigende, men med ikke-afsendte ordrer returneret sidst, og derefter efter deres ordre-id'er som en tiebreaker, ville du bruge følgende kode:

SELECT orderid, shippeddate
FROM Sales.Orders
ORDER BY shippeddate NULLS LAST, orderid;

Desværre er denne NULLS-bestillingsklausul ikke tilgængelig i T-SQL.

En almindelig løsning, folk bruger i T-SQL, er at gå foran rækkefølgeudtrykket med et CASE-udtryk, der returnerer en konstant med en lavere rækkefølgeværdi for ikke-NULL-værdier end for NULL-værdier, som sådan (vi kalder denne løsning forespørgsel 1):

SELECT orderid, shippeddate
FROM Sales.Orders
ORDER BY CASE WHEN shippeddate IS NOT NULL THEN 0 ELSE 1 END, shippeddate, orderid;

Denne forespørgsel genererer det ønskede output med NULL'er, der vises sidst:

orderid     shippeddate
----------- -----------
10249       2017-07-10
10252       2017-07-11
10250       2017-07-12
...
11063       2019-05-06
11067       2019-05-06
11069       2019-05-06
11008       NULL
11019       NULL
11039       NULL
...

Der er et dækkende indeks defineret i Sales.Orders-tabellen med kolonnen shippeddate som nøglen. På samme måde som en manipuleret filtreringskolonne forhindrer filterets SARG-evne og muligheden for at anvende et søge et indeks, forhindrer en manipuleret bestillingskolonne muligheden for at stole på indeksbestilling til at understøtte forespørgslens ORDER BY-klausul. Derfor genererer SQL Server en plan for forespørgsel 1 med en eksplicit sorteringsoperator, som vist i figur 1.

Figur 1:Plan for forespørgsel 1

Nogle gange er størrelsen af ​​dataene ikke så stor, at den eksplicitte sortering er et problem. Men nogle gange er det. Med eksplicit sortering bliver forespørgslens skalerbarhed ekstra-lineær (du betaler mere pr. række, jo flere rækker du har), og responstiden (tiden det tager den første række at blive returneret) er forsinket.

Der er et trick, som du kan bruge til at undgå eksplicit sortering i et sådant tilfælde med en løsning, der bliver optimeret ved hjælp af en ordrebevarende Merge Join Concatenation-operator. Du kan finde en detaljeret dækning af denne teknik, der anvendes i forskellige scenarier i SQL Server:Undgå en sortering med Merge Join-sammenkædning. Det første trin i løsningen forener resultaterne af to forespørgsler:en forespørgsel, der returnerer rækkerne, hvor rækkefølgekolonnen ikke er NULL med en resultatkolonne (vi kalder det sortcol) baseret på en konstant med en eller anden rækkefølgeværdi, f.eks. 0, og en anden forespørgsel, der returnerer rækkerne med NULL'erne, med sortcol sat til en konstant med en højere ordensværdi end i den første forespørgsel, f.eks. 1. I det andet trin definerer du så et tabeludtryk baseret på koden fra første trin, og derefter i den ydre forespørgsel bestiller du rækkerne fra tabeludtrykket først efter sortcol, og derefter efter de resterende bestillingselementer. Her er den komplette løsnings kode, der implementerer denne teknik (vi kalder denne løsning forespørgsel 2):

WITH C AS
(
SELECT orderid, shippeddate, 0 AS sortcol
FROM Sales.Orders
WHERE shippeddate IS NOT NULL
 
UNION ALL
 
SELECT orderid, shippeddate, 1 AS sortcol
FROM Sales.Orders
WHERE shippeddate IS NULL
)
SELECT orderid, shippeddate
FROM C
ORDER BY sortcol, shippeddate, orderid;

Planen for denne forespørgsel er vist i figur 2.

Figur 2:Plan for forespørgsel 2

Læg mærke til to søgninger og bestilte rækkeviddescanninger i det dækkende indeks idx_nc_shippeddate – en trækker rækkerne, hvor shippeddateis ikke er NULL, og en anden trækker rækker, hvor shippeddate er NULL. Derefter, på samme måde som Merge Join-algoritmen fungerer i en joinforbindelse, forener Merge Join (Concatenation)-algoritmen rækkerne fra de to ordnede sider på en lynlås-lignende måde og bevarer den indlæste rækkefølge for at understøtte forespørgslens behov for præsentationsbestilling. Jeg siger ikke, at denne teknik altid er hurtigere end den mere typiske løsning med CASE-udtrykket, som anvender eksplicit sortering. Førstnævnte har dog lineær skalering, og sidstnævnte har n log n skalering. Så førstnævnte vil have en tendens til at klare sig bedre med et stort antal rækker og sidstnævnte med små tal.

Det er klart, at det er godt at have en løsning til dette almindelige behov, men det vil være meget bedre, hvis T-SQL tilføjede understøttelse af standard NULL-bestillingsklausulen i fremtiden.

Konklusion

ISO/IEC SQL-standarden har en hel del NULL-håndteringsfunktioner, som endnu ikke er nået til T-SQL. I denne artikel dækkede jeg nogle af dem:DISTINCT-prædikatet, NULL-behandlingsklausulen og kontrol af, om NULLs rækker først eller sidst. Jeg gav også løsninger til disse funktioner, der understøttes i T-SQL, men de er naturligvis besværlige. Næste måned fortsætter jeg diskussionen ved at dække den unikke standardbegrænsning, hvordan den adskiller sig fra T-SQL-implementeringen og de løsninger, der kan implementeres i T-SQL.


  1. ORA-00904:ugyldig identifikator

  2. postgresql generere sekvens uden mellemrum

  3. Top almindelige MySQL-forespørgsler

  4. MySQL (eller PHP?) grupperer resultater efter feltdata