EXISTS birden çok sütunda etkin bir şekilde nasıl kontrol edilir?


26

Bu periyodik olarak karşılaştığım ve henüz iyi bir çözüm bulamadığım bir konudur.

Aşağıdaki tablo yapısını varsayalım

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

ve gereklilik, ya null değerine sahip sütunlardan birinin ya Bda Cgerçekten herhangi bir NULLdeğer içerip içermediğinin belirlenmesidir (ve eğer öyleyse hangisi (ler)).

Ayrıca, tablonun milyonlarca satır içerdiğini varsayalım (ve bu sorgu sınıfı için daha genel bir çözümle ilgilendiğim için bakılabilecek sütun istatistiklerinin bulunmadığını varsayalım).

Buna yaklaşmanın birkaç yolunu düşünebilirim ama hepsinin zayıf yönleri var.

İki ayrı EXISTSifade. Bu, sorguların en kısa sürede NULLbulunursa taramayı durdurmalarına izin verme avantajına sahip olacaktır . Ancak aslında her iki sütun da NULLs içermiyorsa, iki tam tarama sonuçlanır.

Tekli Toplam Sorgu

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

Bu, her iki sütunu aynı anda işleyebilir, bu nedenle bir tam taramanın en kötü durumudur. Dezavantajı, karşılaşsa bile birNULL sorguda çok erken iki sütunda , tablonun geri kalanının tamamını taramaya devam etmesidir.

Kullanıcı değişkenleri

Ben yapabilirsiniz bunu yapmanın üçüncü yol düşünemiyorum

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)

ancak toplu bir birleştirme sorgusu için doğru davranış tanımsız olduğundan bu, üretim kodu için uygun değildir . ve bir hata atarak taramayı sonlandırmak yine de çok korkunç bir çözüm.

Yukarıdaki yaklaşımların güçlü yönlerini birleştiren başka bir seçenek var mı?

Düzenle

Sadece bunu şimdiye kadar gönderilen cevaplarla ilgili okuduğum sonuçlarla güncellemek için (@ ypercube test verilerini kullanarak)

+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          | 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)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+

Thomas'ın cevap @ ben değiştirdi için TOP 3için TOP 2potansiyel izin daha önce çıkmak için. Bu cevap için varsayılan olarak paralel bir plan aldım, bu nedenle MAXDOP 1okuma sayısını diğer planlarla daha karşılaştırılabilir hale getirmek için bir ipucu ile de denedim . Daha önceki testimde bu sorguyu kısa devreyi tüm tabloyu okumadan görmüş olduğum için elde ettiğim sonuçlara biraz şaşırdım.

Test verilerim için kısa devre planı

Shortcircuits

Ypercube'un verileri için plan:

Kısa Devre Değil

Böylece plana engelleyici bir sıralama işleci ekler. Ben de HASH GROUPipucu ile denedim ama yine de tüm satırları okuma biter

Kısa Devre Değil

Bu yüzden, anahtar, bir hash match (flow distinct)operatörün bu planın kısa devre yapmasına izin vermesi gibi görünüyor , çünkü diğer alternatifler yine de tüm satırları engeller ve tüketir. Bunu özellikle zorlamak için bir ipucu olduğunu sanmıyorum ama görünüşe göre "genel olarak, iyimser, girdi kümesinde farklı değerlerden daha az çıktı satırının gerekli olduğunu belirlediği bir Akış Ayrıntısı seçer". .

@ ypercube'ın verileri her sütunda yalnızca NULLdeğerleri olan 1 satır vardır (tablo kardinalitesi = 30300) ve operatörün içine giren ve çıkan tahmini satırlar her ikisi dedir 1. Öngörüyü, optimizer için biraz daha opak hale getirerek, Flow Distinct operatörüyle bir plan oluşturdu.

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 

Düzenle 2

Aklıma gelen son bir çentik, yukarıdaki sorgunun hala NULLher iki sütunda Bve NULL değerine sahip olduğu ilk satırın karşılaştığı durumlarda gerekenden daha fazla satır işlemesiyle sonuçlanabileceğidir C. Hemen çıkmak yerine taramaya devam edecektir. Bundan kaçınmanın bir yolu, tarandıkları sırada satırları açmaktır. Öyleyse Thomas Kejer'in cevabındaki son değişikim aşağıda

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

Tahminin olması muhtemelen daha iyi olurdu, WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULLancak önceki test verisine karşı, birinin bana bir Akış Ayrıntısı olan bir plan vermemesi, bir tanesinin yapması daha iyi olurdu NullExists IS NOT NULL(aşağıdaki plan).

özetlenmemiş

Yanıtlar:


20

Peki ya:

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

Bu yaklaşımı seviyorum. Yine de sorumla ilgili düzenlemelerde değindiğim birkaç olası sorun var. Yazılı olarak TOP 3sadece olabilir TOP 2o aşağıdakilerden her biri birini bulana kadar tarar anda olarak (NOT_NULL,NULL), (NULL,NOT_NULL), (NULL,NULL). Bu 3 kişiden 2'si yeterli olacaktır - ve (NULL,NULL)ilk bulursa ikinciye de ihtiyaç duyulmaz. Ayrıca, kısa devre yapmak için, planın farklılığı hash match (flow distinct)operatörün yerine hash match (aggregate)veya yerine bir operatör vasıtasıyla uygulaması gerekecektirdistinct sort
Martin Smith,

6

Soruyu anladığım gibi, aslında B veya C'nin boş olduğu satırları döndürmenin tersine, sütun değerlerinden herhangi birinde bir boş değer olup olmadığını bilmek istersiniz. Bu durumda, neden olmasın ki:

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

SQL 2008 R2 ve bir milyon satır içeren test platformumda, Müşteri İstatistikleri sekmesinden ms cinsinden aşağıdaki sonuçları aldım:

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

Nolock ipucunu eklerseniz, sonuçlar daha da hızlıdır:

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

Başvuru için veri üretmek için Red-gate'in SQL Generator'ü kullandım. Bir milyon satırımdan 9.886 satır boş B değerine ve 10.019 boş C değerine sahipti.

Bu test serisinde, B sütunundaki her satırın bir değeri vardır:

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

Her testten önce (her iki sette) koştum CHECKPOINTve DBCC DROPCLEANBUFFERS.

Tabloda boş değer olmadığı zaman sonuçlar. Ypercube tarafından sağlanan çözümün mevcut okuma ve uygulama süresi bakımından benimki ile neredeyse aynı olduğunu unutmayın. Ben (biz) bunun Gelişmiş Tarama kullanımına sahip olan Enterprise / Developer sürümünün avantajlarından kaynaklandığına inanıyorum . Yalnızca Standart sürümü veya daha düşük bir sürümünü kullanıyorsanız, Kejser'in çözümü en hızlı çözüm olabilir.

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

4

Are IFifadeleri izin?

Bu, tablodaki bir geçişte B veya C'nin varlığını doğrulamanıza izin vermelidir:

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

SQL-Fiddle'da sürümlerinde test edilmiştir: 2008 R2 ve 2012'de 30.000 satır.

  • EXISTSSorgu gösterileri erken Boşlara bulduğu verimliliğinde büyük bir fayda - bekleniyor.
  • EXISTSSorgu ile daha iyi performans elde ediyorum - her durumda 2012'de açıklayamam.
  • 2008R2'de, Null yokken, diğer 2 sorgudan daha yavaştır. Null'ları ne kadar erken bulursa, o kadar çabuk alır ve her iki sütun da erken boşsa, diğer 2 sorgudan çok daha hızlı olur.
  • Thomas Kejser'in sorgusu, 2012'de hafif ama sürekli olarak daha iyi bir performans sergiliyor ve Martin'in CASEsorgusuyla karşılaştırıldığında 2008R2'de daha kötü görünüyor .
  • 2012 sürümü çok daha iyi performansa sahip görünüyor. Ancak, SQL-Fiddle sunucularının ayarları ile ilgili olmalı ve sadece optimizasyondaki gelişmelerle değil.

Sorgular ve zamanlamalar. Yapılan zamanlamalar:

  • 1 numarasız
  • 2. sütunda Bbiri NULLküçük olacak şekilde id.
  • NULLHer ikisi de birer tane küçük kimlikte olacak şekilde 3n.

İşte başlıyoruz (planlarla ilgili bir sorun var, daha sonra tekrar deneyeceğim. Şimdilik bağlantıları izleyin):


2 EXISTS alt sorgusuyla sorgu

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'in Tek Toplam Sorgusu

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'in sorgusu

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

Benim önerim (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

Çıktıda biraz cilalama gerektiriyor, ancak verimlilik EXISTSsorguya benzer . Boş olmadığında daha iyi olacağını düşünmüştüm, ancak test olmadığını gösteriyor.


Öneri (2)

Mantığı basitleştirmeye çalışıyorum:

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 ;

2008R2’de önceki öneriye göre daha iyi bir performans gösteriyor ancak 2012’de daha da kötüsü görünüyor (belki 2. baskın @ 8kb’in cevabı gibi INSERTyazılarak yeniden yazılabilir IF):

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

0

EXISTS kullandığınızda, SQL Server bir varlık kontrolü yaptığınızı bilir. İlk eşleşen değeri bulduğunda, TRUE değerini döndürür ve aramayı durdurur.

2 sütunu birleştirdiğinizde ve varsa null olursa sonuç null olur.

Örneğin

null + 'a' = null

öyleyse bu kodu kontrol et

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

-3

Peki ya:

select 
    exists(T.B is null) as 'B is null',
    exists(T.C is null) as 'C is null'
from T;

Bu işe yararsa (test etmediysem), her biri DOĞRU veya YANLIŞ olmak üzere 2 sütunlu bir satırlık bir tablo verirdi. Verimliliği test etmedim.


2
Bu, diğer herhangi bir DBMS'de geçerli olsa bile, doğru semantikaya sahip olduğundan şüpheliyim. Bunun T.B is nullbir boolean sonuç olarak değerlendirildiğini EXISTS(SELECT true)ve EXISTS(SELECT false)her ikisinin de gerçek olacağı varsayılır . Bu MySQL örneği, iki sütunun da aslında hiçbirinde yapmadıklarında NULL içerdiğini gösterir
Martin Smith,
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.