Bagaimana cara memeriksa EXIS pada beberapa kolom secara efisien?

26

Ini adalah masalah yang saya hadapi secara berkala dan belum menemukan solusi yang baik untuk itu.

Misalkan struktur tabel berikut

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

dan persyaratannya adalah untuk menentukan apakah salah satu kolom yang dapat dibatalkan Batau Cbenar - benar berisi NULLnilai apa pun (dan jika demikian yang mana).

Juga asumsikan tabel berisi jutaan baris (dan bahwa tidak ada statistik kolom yang tersedia yang dapat diintip karena saya tertarik pada solusi yang lebih umum untuk kelas kueri ini).

Saya bisa memikirkan beberapa cara untuk mendekati ini tetapi semua memiliki kelemahan.

Dua EXISTSpernyataan terpisah . Ini akan memiliki keuntungan membiarkan kueri berhenti memindai lebih awal segera setelah NULLditemukan. Tetapi jika kedua kolom sebenarnya mengandung no NULLs maka dua pemindaian penuh akan menghasilkan.

Permintaan Agregat Tunggal

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

Ini dapat memproses kedua kolom secara bersamaan sehingga memiliki kasus terburuk dari satu pemindaian penuh. Kerugiannya adalah bahwa bahkan jika itu bertemu NULLdi kedua kolom sangat awal pada permintaan masih akan berakhir memindai seluruh sisa tabel.

Variabel pengguna

Saya bisa memikirkan cara ketiga untuk melakukan ini

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)

tetapi ini tidak cocok untuk kode produksi karena perilaku yang benar untuk kueri gabungan agregat tidak ditentukan. dan mengakhiri pemindaian dengan membuat kesalahan adalah solusi yang cukup mengerikan.

Apakah ada opsi lain yang menggabungkan kekuatan pendekatan di atas?

Edit

Hanya untuk memperbarui ini dengan hasil yang saya dapatkan dari bacaan untuk jawaban yang diajukan sejauh ini (menggunakan data uji @ 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)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+

Untuk @ jawaban Thomas aku berubah TOP 3untuk TOP 2untuk berpotensi memungkinkan untuk keluar sebelumnya. Saya mendapat paket paralel secara default untuk jawaban itu, jadi saya juga mencobanya dengan MAXDOP 1petunjuk agar jumlah bacaan lebih sebanding dengan paket lainnya. Saya agak terkejut dengan hasilnya seperti pada tes saya sebelumnya saya telah melihat bahwa hubung singkat tanpa membaca seluruh tabel.

Rencana untuk data pengujian saya bahwa hubung singkat ada di bawah

Sirkuit pendek

Paket data ypercube adalah

Bukan Shortcircuit

Jadi itu menambahkan operator semacam pemblokiran ke rencana. Saya juga mencoba dengan HASH GROUPpetunjuk tetapi itu masih berakhir dengan membaca semua baris

Bukan Shortcircuit

Jadi kuncinya adalah untuk mendapatkan hash match (flow distinct)operator untuk membiarkan rencana ini mengalami hubungan pendek karena alternatif lain akan memblokir dan mengkonsumsi semua baris pula. Saya tidak berpikir ada petunjuk untuk memaksakan ini secara khusus tetapi tampaknya "secara umum, pengoptimal memilih Perbedaan Arus di mana ia menentukan bahwa lebih sedikit baris output yang diperlukan daripada ada nilai yang berbeda dalam set input." .

@ data ypercube hanya memiliki 1 baris di setiap kolom dengan NULLnilai (tabel kardinalitas = 30300) dan baris yang diperkirakan masuk dan keluar dari operator keduanya 1. Dengan membuat predikat sedikit lebih buram bagi pengoptimal itu menghasilkan rencana dengan operator Flow Distinct.

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 

Edit 2

Tweak terakhir yang muncul pada saya adalah bahwa kueri di atas masih dapat memproses lebih banyak baris daripada yang diperlukan jika baris pertama yang ditemuinya NULLmemiliki NULL di kolom Bdan C. Ini akan melanjutkan pemindaian alih-alih segera keluar. Salah satu cara untuk menghindarinya adalah dengan tidak memproteksi baris saat mereka dipindai. Jadi jawaban terakhir saya untuk jawaban Thomas Kejser ada di bawah ini

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

Mungkin akan lebih baik untuk predikat itu WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULLtetapi terhadap data uji sebelumnya bahwa seseorang tidak memberi saya rencana dengan Flow Distinct, sedangkan yang NullExists IS NOT NULLmelakukannya (rencana di bawah).

Tidak dipatenkan

Martin Smith
sumber

Jawaban:

20

Bagaimana tentang:

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
Thomas Kejser
sumber
Saya suka pendekatan ini. Ada beberapa kemungkinan masalah yang saya bahas dalam editan untuk pertanyaan saya. Seperti ditulis TOP 3hanya bisa menjadi TOP 2seperti saat ini akan memindai sampai menemukan satu dari masing-masing berikut ini (NOT_NULL,NULL), (NULL,NOT_NULL), (NULL,NULL). Setiap 2 dari 3 itu akan cukup - dan jika ditemukan (NULL,NULL)pertama maka yang kedua tidak akan diperlukan juga. Juga untuk hubungan pendek rencana perlu menerapkan perbedaan melalui hash match (flow distinct)operator daripada hash match (aggregate)ataudistinct sort
Martin Smith
6

Seperti yang saya pahami pertanyaannya, Anda ingin tahu apakah nol ada di salah satu nilai kolom sebagai lawan untuk benar-benar mengembalikan baris di mana B atau C adalah nol. Jika itu masalahnya, mengapa tidak:

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

Pada rig pengujian saya dengan SQL 2008 R2 dan satu juta baris, saya mendapat hasil berikut dalam ms dari tab Statistik Klien:

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

Jika Anda menambahkan petunjuk nolock, hasilnya bahkan lebih cepat:

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

Untuk referensi saya menggunakan Generator SQL Red-gate untuk menghasilkan data. Dari satu juta baris saya, 9.886 baris memiliki nilai B nol dan 10.019 memiliki nilai C nol.

Dalam rangkaian pengujian ini, setiap baris di kolom B memiliki nilai:

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

Sebelum setiap tes (kedua set) saya berlari CHECKPOINTdan DBCC DROPCLEANBUFFERS.

Ini adalah hasil ketika tidak ada null dalam tabel. Perhatikan bahwa 2 solusi yang disediakan oleh ypercube hampir identik dengan saya dalam hal waktu baca dan eksekusi. Saya (kami) percaya ini karena kelebihan dari edisi Perusahaan / Pengembang yang menggunakan Pemindaian Lanjutan . Jika Anda hanya menggunakan edisi Standar atau lebih rendah, solusi Kejser mungkin merupakan solusi tercepat.

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
Thomas
sumber
4

Apakah IFpernyataan diizinkan?

Ini harus memungkinkan Anda untuk mengkonfirmasi keberadaan B atau C pada satu melewati tabel:

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      
8kb
sumber
4

Diuji dalam SQL-Fiddle dalam versi: 2008 r2 dan 2012 dengan 30K baris.

  • The EXISTSpermintaan menunjukkan manfaat yang sangat besar dalam efisiensi ketika menemukan Nulls awal - yang diharapkan.
  • Saya mendapatkan kinerja yang lebih baik dengan EXISTSkueri - dalam semua kasus di 2012, yang tidak dapat saya jelaskan.
  • Di 2008R2, ketika tidak ada Nulls, itu lebih lambat dari 2 kueri lainnya. Semakin dini ia menemukan Nulls, semakin cepat ia mendapat dan ketika kedua kolom memiliki nol lebih awal, itu jauh lebih cepat daripada 2 kueri lainnya.
  • Permintaan Thomas Kejser tampaknya berkinerja sedikit tetapi terus-menerus lebih baik di 2012 dan lebih buruk di 2008R2, dibandingkan dengan CASEpermintaan Martin .
  • Versi 2012 tampaknya memiliki kinerja yang jauh lebih baik. Mungkin ada hubungannya dengan pengaturan server SQL-Fiddle dan tidak hanya dengan perbaikan pada pengoptimal.

Kueri dan timing. Pengaturan waktu dimana dilakukan:

  • 1 tanpa Nulls sama sekali
  • 2 dengan kolom Bmemiliki satu NULLper satu id.
  • 3 dan kedua kolom memiliki NULLmasing-masing di id kecil.

Ini dia (ada masalah dengan rencana, saya akan coba lagi nanti. Ikuti tautan untuk saat ini):


Query dengan 2 subqueries EXISTS

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

Permintaan Agregat Tunggal Martin Smith

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

Permintaan Thomas Kejser

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

Saran saya (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

Dibutuhkan beberapa pemolesan pada output tetapi efisiensinya mirip dengan EXISTSquery. Saya pikir akan lebih baik ketika tidak ada null tetapi pengujian menunjukkan tidak.


Saran (2)

Mencoba menyederhanakan logika:

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 ;

Tampaknya berkinerja lebih baik di 2008R2 daripada saran sebelumnya tetapi lebih buruk di 2012 (mungkin ke-2 INSERTdapat ditulis ulang menggunakan IF, seperti jawaban @ 8kb):

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

Ketika Anda menggunakan EXIS, SQL Server tahu Anda sedang melakukan pemeriksaan keberadaan. Ketika menemukan nilai yang cocok pertama, itu mengembalikan TRUE dan berhenti mencari.

ketika Anda menggabungkan 2 kolom dan jika ada yang nol hasilnya akan menjadi nol

misalnya

null + 'a' = null

jadi periksa kode ini

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
AmmarR
sumber
-3

Bagaimana tentang:

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

Jika ini berhasil (saya belum mengujinya), itu akan menghasilkan tabel satu baris dengan 2 kolom, masing-masing baik BENAR atau SALAH. Saya tidak menguji efisiensinya.

David Horowitz
sumber
2
Bahkan jika ini berlaku di DBMS lain, saya ragu itu memiliki semantik yang benar. Dengan asumsi bahwa T.B is nulldiperlakukan sebagai hasil boolean kemudian EXISTS(SELECT true)dan EXISTS(SELECT false)akan baik kembali benar. Contoh MySQL ini menunjukkan bahwa kedua kolom berisi NULL ketika ternyata tidak ada
Martin Smith