it-swarm-eu.dev

Jak efektivně zkontrolovat EXISTY na více sloupcích?

Toto je problém, se kterým se pravidelně setkávám a dosud jsem nenašel dobré řešení.

Předpokládejme následující strukturu tabulky

CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)

a požadavkem je zjistit, zda některý z nulovatelných sloupců B nebo C ve skutečnosti obsahuje nějaké hodnoty NULL (a pokud ano, které z nich).

Předpokládejme také, že tabulka obsahuje miliony řádků (a že nejsou k dispozici žádné statistiky sloupců, které by bylo možné nahlédnout, protože mám zájem o obecnější řešení pro tuto třídu dotazů).

Dokážu si představit několik způsobů, jak k tomu přistupovat, ale všechny mají slabiny.

Dva samostatné příkazy EXISTS To by mělo výhodu v tom, že by dotazům bylo možné zastavit skenování, jakmile NULL je nalezeno. Pokud však oba sloupce ve skutečnosti neobsahují žádné NULLs, výsledkem budou dvě úplné kontroly.

Jeden agregovaný dotaz

SELECT 
    MAX(CASE WHEN B IS NULL THEN 1 ELSE 0 END) AS B,
    MAX(CASE WHEN C IS NULL THEN 1 ELSE 0 END) AS C
FROM T

To by mohlo zpracovat oba sloupce najednou, takže byste měli nejhorší případ jednoho úplného skenování. Nevýhodou je, že i když narazí na NULL v obou sloupcích velmi brzy na dotaz, stále skončí skenování celého zbytku tabulky.

Uživatelské proměnné

I can vymyslet třetí způsob, jak toho dosáhnout

BEGIN TRY
DECLARE @B INT, @C INT, @D INT

SELECT 
    @B = CASE WHEN B IS NULL THEN 1 ELSE @B END,
    @C = CASE WHEN C IS NULL THEN 1 ELSE @C END,
    /*Divide by zero error if both @B and @C are 1.
    Might happen next row as no guarantee of order of
    assignments*/
    @D = 1 / (2 - (@B + @C))
FROM T  
OPTION (MAXDOP 1)       
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 /*Divide by zero*/
    BEGIN
    SELECT 'B,C both contain NULLs'
    RETURN;
    END
ELSE
    RETURN;
END CATCH

SELECT ISNULL(@B,0),
       ISNULL(@C,0)

ale to není vhodné pro výrobní kód jako správné chování dotazu agregované zřetězení není definováno. a ukončení skenování vyvoláním chyby je stejně tak hrozné řešení.

Existuje jiná možnost, která kombinuje silné stránky výše uvedených přístupů?

Upravit

Jen abych to aktualizoval o výsledky, které dostanu, pokud jde o přečtení odpovědí dodaných doposud (pomocí testovacích dat @ ypercube)

+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          | 2 * EXISTS | CASE | Kejser  |  Kejser  |        Kejser        | ypercube |       8kb        |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          |            |      |         | MAXDOP 1 | HASH GROUP, MAXDOP 1 |          |                  |
| No Nulls |      15208 | 7604 |    8343 | 7604     | 7604                 |    15208 | 8346 (8343+3)    |
| One Null |       7613 | 7604 |    8343 | 7604     | 7604                 |     7620 | 7630 (25+7602+3) |
| Two Null |         23 | 7604 |    8343 | 7604     | 7604                 |       30 | 30 (18+12)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+

Pro odpověď @ Thomase jsem změnil TOP 3 Na TOP 2, Abych ji mohl ukončit dříve. Ve výchozím nastavení mám pro tuto odpověď paralelní plán, takže jsem to také zkusil s nápovědou MAXDOP 1, Aby byl počet přečtení srovnatelnější s ostatními plány. Výsledky jsem byl poněkud překvapen výsledky, jako v mém předchozím testu, který jsem viděl, že dotaz zkrat byl bez přečtení celé tabulky.

Plán mých testovacích dat, že zkraty jsou níže

Shortcircuits

Plán pro data ypercube je

Not Shortcircuit

Do plánu tak přidává operátor řazení blokování. Také jsem se pokusil s nápovědou HASH GROUP, Ale to stále končí čtením všech řádků

Not Shortcircuit

Zdá se tedy, že klíčem je získat operátora hash match (flow distinct), který umožní zkratu tohoto plánu, protože ostatní alternativy stejně zablokují a spotřebují všechny řádky. Nemyslím si, že existuje náznak, jak to nutit konkrétně, ale zjevně "obecně optimalizátor vybere proudový distinkt, kde určí, že je vyžadováno méně řádků výstupu, než jsou ve vstupní sadě odlišné hodnoty." =.

Data @ ypercube mají v každém sloupci pouze 1 řádek s hodnotami NULL (cardinality tabulky = 30300) a odhadované řádky vstupující do a od operátora jsou oba 1. Díky tomu, že byl predikát o něco více neprůhledný pro optimalizátor, vytvořil plán s operátorem Distinct Flow.

SELECT TOP 2 *
FROM (SELECT DISTINCT 
        CASE WHEN b IS NULL THEN NULL ELSE 'foo' END AS b
      , CASE WHEN c IS NULL THEN NULL ELSE 'bar' END AS c
  FROM test T 
  WHERE LEFT(b,1) + LEFT(c,1) IS NULL
) AS DT 

Upravit 2

Jeden poslední Tweak, který mi došlo, je, že výše uvedený dotaz mohl stále skončit zpracováním více řádků, než je nutné v případě, že první řádek, se kterým se setká s NULL, má NULL v obou sloupcích B a C. Bude pokračovat ve skenování a nebude okamžitě ukončen. Jedním ze způsobů, jak tomu zabránit, by bylo rozepnout řádky při jejich skenování. Takže moje poslední změna na odpověď Thomase Kejsera je níže

SELECT DISTINCT TOP 2 NullExists
FROM test T 
CROSS APPLY (VALUES(CASE WHEN b IS NULL THEN 'b' END),
                   (CASE WHEN c IS NULL THEN 'c' END)) V(NullExists)
WHERE NullExists IS NOT NULL

Pravděpodobně by bylo lepší, kdyby byl predikát WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULL, ale oproti předchozím testovacím datům mi nikdo nedá plán s Flow Distinct, zatímco NullExists IS NOT NULL Jeden dělá (plán níže) ).

Unpivoted

26
Martin Smith

Co takhle:

SELECT TOP 3 *
FROM (SELECT DISTINCT 
        CASE WHEN B IS NULL THEN NULL ELSE 'foo' END AS B
        , CASE WHEN C IS NULL THEN NULL ELSE 'bar' END AS C
  FROM T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT
20
Thomas Kejser

Když rozumím otázce, chcete vědět, zda v některé z hodnot sloupců existuje null, na rozdíl od skutečného vrácení řádků, ve kterých je B nebo C null. Pokud je tomu tak, proč ne:

Select Top 1 'B as nulls' As Col
From T
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T
Where T.C Is Null

Na mé testovací soupravě s SQL 2008 R2 a jedním milionem řádků jsem na kartě Statistiky klientů obdržel následující výsledky v ms:

Kejser                          2907,2875,2829,3576,3103
ypercube                        2454,1738,1743,1765,2305
OP single aggregate solution    (stopped after 120,000 ms) Wouldn't even finish
My solution                     1619,1564,1665,1675,1674

Pokud přidáte tip nolock, výsledky budou ještě rychlejší:

Select Top 1 'B as nulls' As Col
From T With(Nolock)
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T With(Nolock)
Where T.C Is Null

My solution (with nolock)       42,70,94,138,120

Pro informaci jsem použil generátor SQL Red-gate pro generování dat. Z mého jednoho milionu řádků mělo 9 886 řádků nulovou hodnotu B a 10 019 mělo nulovou hodnotu C.

V této sérii testů má každý řádek ve sloupci B hodnotu:

Kejser                          245200  Scan count 1, logical reads 367259, physical reads 858, read-ahead reads 367278
                                250540  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367280

ypercube(1)                     249137  Scan count 2, logical reads 367276, physical reads 850, read-ahead reads 367278
                                248276  Scan count 2, logical reads 367276, physical reads 869, read-ahead reads 368765

My solution                     250348  Scan count 2, logical reads 367276, physical reads 858, read-ahead reads 367278
                                250327  Scan count 2, logical reads 367276, physical reads 854, read-ahead reads 367278

Před každým testem (obě sady) jsem běžel CHECKPOINT a DBCC DROPCLEANBUFFERS.

Zde jsou výsledky, když v tabulce nejsou nulové hodnoty. Všimněte si, že řešení 2 poskytnuté společností ypercube je téměř identické s mými, pokud jde o čtení a dobu provedení. Věřím, že je to kvůli výhodám vydání Enterprise/Developer, které používá Advanced Scanning . Pokud jste používali pouze vydání Standard nebo nižší, Kejserovo řešení může být nejrychlejším řešením.

Kejser                          248875  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367290

ypercube(1)                     243349  Scan count 2, logical reads 367265, physical reads 851, read-ahead reads 367278
                                242729  Scan count 2, logical reads 367265, physical reads 858, read-ahead reads 367276
                                242531  Scan count 2, logical reads 367265, physical reads 855, read-ahead reads 367278

My solution                     243094  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
                                243444  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
6
Thomas

Testováno v SQL-Fiddle ve verzích: 2008 r2 a 2012 s řádky 30 kB.

  • Dotaz EXISTS ukazuje obrovskou výhodu v účinnosti, když zjistí, že Null je brzy - což se očekává.
  • Lepší výkon získám pomocí dotazu EXISTS - ve všech případech v roce 2012, což nedokážu vysvětlit.
  • V roce 2008R2, když neexistují žádné hodnoty Null, je to pomalejší než u ostatních 2 dotazů. Čím rychleji najde Null, tím rychleji se dostane a když oba sloupce mají null, je to mnohem rychlejší než ostatní 2 dotazy.
  • Zdá se, že dotaz Thomase Kejsera působí mírně, ale stále lépe v roce 2012 a horší v roce 2008R2, ve srovnání s Martinovým dotazem CASE.
  • Zdá se, že verze 2012 má mnohem lepší výkon. Může to však souviset s nastavením serverů SQL-Fiddle a nejen s vylepšeními optimalizátoru.

Dotazy a načasování. Časové rozvržení:

  • 1. bez nulových hodnot
  • 2. se sloupcem B s jedním NULL na malém id.
  • 3. s tím, že oba sloupce mají vždy jeden NULL v malých ID.

Tady to máme (je tu problém s plány, zkusím to znovu později. Postupujte podle odkazů):


Dotaz s 2 EXISTS subqueries

SELECT 
      CASE WHEN EXISTS (SELECT * FROM test WHERE b IS NULL)
             THEN 1 ELSE 0 
      END AS B,
      CASE WHEN EXISTS (SELECT * FROM test WHERE c IS NULL)
             THEN 1 ELSE 0 
      END AS C ;

-------------------------------------
Times in ms (2008R2): 1344 - 596 -  1  
Times in ms   (2012):   26 -  14 -  2

Martin Smithův jediný agregovaný dotaz

SELECT 
    MAX(CASE WHEN b IS NULL THEN 1 ELSE 0 END) AS B,
    MAX(CASE WHEN c IS NULL THEN 1 ELSE 0 END) AS C
FROM test ;

--------------------------------------
Times in ms (2008R2):  558 - 553 - 516  
Times in ms   (2012):   37 -  35 -  36

Thomas Kejserův dotaz

SELECT TOP 3 *
FROM (SELECT DISTINCT 
        CASE WHEN B IS NULL THEN NULL ELSE 'foo' END AS b
      , CASE WHEN C IS NULL THEN NULL ELSE 'bar' END AS c
  FROM test T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT ;

--------------------------------------
Times in ms (2008R2):  859 - 705 - 668  
Times in ms   (2012):   24 -  19 -  18

Můj návrh (1)

WITH tmp1 AS
  ( SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id 
  ) 

  SELECT 
      tmp1.*, 
      NULL AS id2, NULL AS b2, NULL AS c2
  FROM tmp1
UNION ALL
  SELECT *
  FROM
    ( SELECT TOP (1)
          tmp1.id, tmp1.b, tmp1.c,
          test.id AS id2, test.b AS b2, test.c AS c2 
      FROM test
        CROSS JOIN tmp1
      WHERE test.id >= tmp1.id
        AND ( test.b IS NULL AND tmp1.c IS NULL
           OR tmp1.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id
    ) AS x ;

--------------------------------------
Times in ms (2008R2): 1089 - 572 -  16   
Times in ms   (2012):   28 -  15 -   1

Na výstupu potřebuje určité leštění, ale účinnost je podobná dotazu EXISTS. Myslel jsem, že by bylo lepší, když neexistují nulové hodnoty, ale testování ukazuje, že tomu tak není.


Návrh (2)

Snažíme se zjednodušit logiku:

CREATE TABLE tmp
( id INT
, b CHAR(1000)
, c CHAR(1000)
) ;

DELETE  FROM tmp ;

INSERT INTO tmp 
    SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id  ; 

INSERT INTO tmp 
    SELECT TOP (1)
        test.id, test.b, test.c 
      FROM test
        JOIN tmp 
          ON test.id >= tmp.id
      WHERE ( test.b IS NULL AND tmp.c IS NULL
           OR tmp.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id ;

SELECT *
FROM tmp ;

Zdá se, že v roce 2008R2 má lepší výkon než předchozí návrh, ale v roce 2012 je horší (možná 2. INSERT lze přepsat pomocí IF, jako je odpověď @ 8kb):

------------------------------------------
Times in ms (2008R2): 416+6 - 1+127 -  1+1   
Times in ms   (2012):  14+1 - 0+27  -  0+29
4
ypercubeᵀᴹ

Jsou povoleny příkazy IF?

To vám umožní potvrdit existenci B nebo C při jednom průchodu tabulkou:

DECLARE 
  @A INT, 
  @B CHAR(10), 
  @C CHAR(10)

SET @B = 'X'
SET @C = 'X'

SELECT TOP 1 
  @A = A, 
  @B = B, 
  @C = C
FROM T 
WHERE B IS NULL OR C IS NULL 

IF @@ROWCOUNT = 0 
BEGIN 
  SELECT 'No nulls'
  RETURN
END

IF @B IS NULL AND @C IS NULL
BEGIN
  SELECT 'Both null'
  RETURN
END 

IF @B IS NULL 
BEGIN
  SELECT TOP 1 
    @C = C
  FROM T
  WHERE A > @A
  AND C IS NULL

  IF @B IS NULL AND @C IS NULL 
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'B is null'
    RETURN
  END
END

IF @C IS NULL 
BEGIN
  SELECT TOP 1 
    @B = B
  FROM T 
  WHERE A > @A
  AND B IS NULL

  IF @C IS NULL AND @B IS NULL
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'C is null'
    RETURN
  END
END      
4
8kb

Při použití EXISTY SQL Server ví, že provádíte kontrolu existence. Když najde první odpovídající hodnotu, vrátí PRAVDA a přestane hledat.

když zadáte 2 sloupce a pokud je null, výsledek bude null

např

null + 'a' = null

zkontrolujte tento kód

IF EXISTS (SELECT 1 FROM T WHERE B+C is null)
SELECT Top 1 ISNULL(B,'B ') + ISNULL(C,'C') as [Nullcolumn] FROM T WHERE B+C is null
0
AmmarR