วิธีตรวจสอบ EXISTS อย่างมีประสิทธิภาพในหลาย ๆ คอลัมน์?


26

นี่เป็นปัญหาที่ฉันคิดขึ้นมาเป็นระยะและยังไม่พบทางออกที่ดีสำหรับ

เผื่อว่าโครงสร้างตารางต่อไปนี้

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

และข้อกำหนดคือการพิจารณาว่าคอลัมน์ใดคอลัมน์หนึ่งที่มีค่าว่างBหรือCมีNULLค่าใด ๆ(และถ้าเป็นเช่นนั้น)

นอกจากนี้สมมติว่าตารางมีแถวนับล้านแถว (และไม่มีสถิติคอลัมน์ที่สามารถมองได้เนื่องจากฉันสนใจวิธีแก้ปัญหาทั่วไปสำหรับคิวรีประเภทนี้)

ฉันสามารถคิดถึงวิธีการไม่กี่ทางที่จะเข้าใกล้สิ่งนี้ แต่ทุกคนก็มีจุดอ่อน

EXISTSงบสองแยก นี่จะมีข้อดีของการอนุญาตให้แบบสอบถามหยุดสแกนเร็วที่สุดเท่าที่NULLจะพบ แต่ถ้าในความเป็นจริงไม่มีทั้งคอลัมน์ทั้งNULLสองการสแกนแบบเต็มจะส่งผลให้

แบบสอบถามรวมครั้งเดียว

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

นี่สามารถประมวลผลทั้งสองคอลัมน์ในเวลาเดียวกันดังนั้นจึงมีกรณีที่เลวร้ายที่สุดของการสแกนแบบเต็ม ข้อเสียคือแม้ว่าจะพบ a NULLในคอลัมน์ทั้งสอง แต่เนิ่นๆบนเคียวรีจะยังคงสแกนส่วนที่เหลือทั้งหมดของตาราง

ตัวแปรผู้ใช้

ฉันสามารถคิดถึงวิธีที่สามในการทำสิ่งนี้

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)

แต่สิ่งนี้ไม่เหมาะสำหรับรหัสการผลิตเนื่องจากพฤติกรรมที่ถูกต้องสำหรับแบบสอบถามการต่อข้อมูลรวมไม่ได้ถูกกำหนดไว้ และการยกเลิกการสแกนโดยการโยนข้อผิดพลาดเป็นวิธีที่น่ากลัวอยู่ดี

มีตัวเลือกอื่นที่รวมจุดแข็งของวิธีการข้างต้นหรือไม่

แก้ไข

เพียงอัปเดตสิ่งนี้กับผลลัพธ์ที่ฉันได้รับในแง่ของการอ่านสำหรับคำตอบที่ส่งมาจนถึงตอนนี้ (โดยใช้ข้อมูลการทดสอบของ @ 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)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+

สำหรับ @ คำตอบของโทมัสผมเปลี่ยนTOP 3ไปTOP 2ที่อาจอนุญาตให้ออกจากก่อนหน้านี้ ฉันได้รับแผนแบบขนานโดยค่าเริ่มต้นสำหรับคำตอบนั้นลองด้วยMAXDOP 1คำใบ้เพื่อให้จำนวนการอ่านเปรียบเทียบกับแผนอื่นมากขึ้น ฉันค่อนข้างประหลาดใจกับผลลัพธ์ที่ได้จากการทดสอบก่อนหน้านี้ฉันเห็นว่ามีการลัดวงจรแบบสอบถามโดยไม่อ่านตารางทั้งหมด

แผนสำหรับข้อมูลการทดสอบของฉันที่มีวงจรสั้นอยู่ด้านล่าง

Shortcircuits

แผนสำหรับข้อมูลของ ypercube คือ

ไม่ใช่ Shortcircuit

ดังนั้นจึงเพิ่มตัวดำเนินการเรียงลำดับการบล็อกในแผน ฉันลองด้วยHASH GROUPคำใบ้ แต่ก็ยังจบลงด้วยการอ่านทุกแถว

ไม่ใช่ Shortcircuit

ดังนั้นดูเหมือนว่ากุญแจสำคัญคือการให้hash match (flow distinct)โอเปอเรเตอร์อนุญาตให้แผนนี้ลัดวงจรเนื่องจากทางเลือกอื่นจะปิดกั้นและใช้แถวทั้งหมด ฉันไม่คิดว่าจะมีคำใบ้ที่จะบังคับสิ่งนี้โดยเฉพาะ แต่เห็นได้ชัดว่า"โดยทั่วไปเครื่องมือเพิ่มประสิทธิภาพเลือก Flow Distinct ที่กำหนดว่าต้องใช้แถวเอาท์พุทน้อยลงกว่าค่าที่แตกต่างกันในชุดอินพุต" .

@ ข้อมูล ypercube ของมีเพียง 1 แถวในแต่ละคอลัมน์มีNULLค่า (ตาราง cardinality = 30300) 1และแถวที่คาดจะเข้าและออกของผู้ประกอบการทั้งสอง ด้วยการทำให้เพรดิเคตเพิ่มความทึบแสงให้กับเครื่องมือเพิ่มประสิทธิภาพอีกเล็กน้อยจึงสร้างแผนพร้อมกับโอเปอเรเตอร์ 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 

แก้ไข 2

หนึ่งบิดสุดท้ายที่เกิดขึ้นกับผมคือว่าแบบสอบถามดังกล่าวข้างต้นยังคงสามารถจบลงด้วยการประมวลผลแถวเกินกว่าที่จำเป็นในกรณีที่แถวแรกจะพบกับNULLมี NULLs ในคอลัมน์ทั้งสองและB Cมันจะสแกนต่อไปแทนที่จะออกทันที วิธีหนึ่งในการหลีกเลี่ยงปัญหานี้คือการยกเลิกการหมุนแถวตามที่สแกน ดังนั้นการแก้ไขครั้งสุดท้ายของฉันสำหรับคำตอบของ Thomas Kejserอยู่ด้านล่าง

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

มันอาจจะดีกว่าสำหรับภาคที่จะเป็นWHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULLแต่กับข้อมูลการทดสอบก่อนหน้านี้ที่ไม่มีแผนให้ฉันด้วยความแตกต่างของการไหลในขณะที่NullExists IS NOT NULLคนที่ทำ (แผนด้านล่าง)

Unpivoted

คำตอบ:


20

เกี่ยวกับ:

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

ฉันชอบวิธีนี้ มีปัญหาที่เป็นไปได้เล็กน้อยที่ฉันได้กล่าวถึงในการแก้ไขคำถามของฉัน ตามที่เขียนไว้TOP 3ก็อาจจะTOP 2เป็นในขณะนี้จะสแกนจนกว่าจะพบหนึ่งของแต่ละต่อไป(NOT_NULL,NULL), ,(NULL,NOT_NULL) (NULL,NULL)2 จาก 3 เหล่านั้นจะเพียงพอ - และหากพบ(NULL,NULL)ก่อนก็ไม่จำเป็นต้องใช้สอง นอกจากนี้เพื่อให้การลัดวงจรแผนจะต้องดำเนินการที่แตกต่างกันผ่านทางhash match (flow distinct)ผู้ประกอบการมากกว่าhash match (aggregate)หรือdistinct sort
มาร์ตินสมิ ธ

6

เมื่อฉันเข้าใจคำถามคุณต้องการทราบว่ามีค่า Null อยู่ในค่าคอลัมน์ใด ๆ หรือไม่เมื่อเทียบกับคืนแถวที่ B หรือ C เป็นจริง ถ้าเป็นเช่นนั้นทำไมไม่ทำ:

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 และหนึ่งล้านแถวฉันได้ผลลัพธ์ต่อไปนี้เป็นมิลลิวินาทีจากแท็บสถิติลูกค้า:

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 ผลลัพธ์จะเร็วยิ่งขึ้น:

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

สำหรับการอ้างอิงฉันใช้ตัวสร้าง SQL ของ Red-gate เพื่อสร้างข้อมูล จากหนึ่งล้านแถวของฉัน 9,886 แถวมีค่า null B และ 10,019 มีค่า null C

ในชุดการทดสอบนี้ทุกแถวในคอลัมน์ B มีค่าดังนี้

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

ก่อนที่แต่ละการทดสอบ (ทั้งชุด) ฉันวิ่งและCHECKPOINTDBCC DROPCLEANBUFFERS

นี่คือผลลัพธ์เมื่อไม่มีค่าว่างในตาราง โปรดทราบว่าโซลูชันที่มีอยู่ 2 ของ ypercube นั้นเกือบจะเหมือนกับของฉันในแง่ของการอ่านและเวลาดำเนินการ ฉัน (เรา) เชื่อนี้เป็นเพราะข้อดีขององค์กร / รุ่นสำหรับนักพัฒนาที่มีการใช้งานขั้นสูงในการสแกน หากคุณใช้รุ่นมาตรฐานหรือต่ำกว่าโซลูชันของ Kejser อาจเป็นโซลูชันที่เร็วที่สุด

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

กำลังIFงบที่ได้รับอนุญาต?

สิ่งนี้จะช่วยให้คุณสามารถยืนยันการมีอยู่ของ B หรือ C ในการส่งผ่านหนึ่งครั้งผ่านตาราง:

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 ในเวอร์ชัน: 2008 r2และ2012ด้วยแถว 30K

  • EXISTSแบบสอบถามแสดงให้เห็นว่าเป็นประโยชน์มากในประสิทธิภาพเมื่อพบ Nulls ต้น - ซึ่งคาดว่า
  • ฉันได้รับประสิทธิภาพที่ดีขึ้นด้วยEXISTSข้อความค้นหา - ในทุกกรณีในปี 2012 ซึ่งฉันไม่สามารถอธิบายได้
  • ในปี 2008R2 เมื่อไม่มี Nulls จะช้ากว่าแบบสอบถาม 2 รายการอื่น ๆ ยิ่งเร็วเท่าไหร่ก็จะพบ Null ได้เร็วขึ้นและเมื่อทั้งสองคอลัมน์มีค่า null เร็วขึ้นก็จะเร็วกว่าอีก 2 ข้อความค้นหา
  • ข้อความค้นหาของ Thomas Kejser ดูเหมือนว่าจะทำงานได้เล็กน้อย แต่ดีขึ้นอย่างต่อเนื่องในปี 2012 และแย่ลงในปี 2008R2 เมื่อเปรียบเทียบกับCASEข้อความค้นหาของ Martin
  • ดูเหมือนว่าเวอร์ชั่น 2012 จะมีประสิทธิภาพที่ดีกว่ามาก อาจต้องดำเนินการกับการตั้งค่าเซิร์ฟเวอร์ SQL-Fiddle แต่ไม่เพียง แต่ปรับปรุงด้วยเครื่องมือเพิ่มประสิทธิภาพเท่านั้น

การค้นหาและการกำหนดเวลา กำหนดเวลาที่ทำ:

  • อันดับที่ 1 ที่ไม่มีค่า Null เลย
  • 2 คอลัมน์Bมีหนึ่งที่มีขนาดเล็กNULLid
  • 3 และทั้งสองคอลัมน์จะมีNULLแต่ละอันที่รหัสขนาดเล็ก

ที่นี่เราไป (มีปัญหากับแผนฉันจะลองอีกครั้งในภายหลังทำตามลิงค์สำหรับตอนนี้):


การค้นหาที่มี 2 แบบสอบถามย่อย 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

แบบสอบถามรวมครั้งเดียวของ 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

ข้อความค้นหาของ 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

คำแนะนำของฉัน (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

มันต้องการการขัดเอาท์พุท แต่ประสิทธิภาพคล้ายกับEXISTSเคียวรี ฉันคิดว่ามันจะดีกว่าเมื่อไม่มีค่าว่าง แต่การทดสอบแสดงว่าไม่เป็นเช่นนั้น


คำแนะนำ (2)

พยายามลดความซับซ้อนของตรรกะ:

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 กว่าคำแนะนำก่อนหน้านี้ แต่แย่กว่าในปี 2012 (อาจเป็นครั้งที่ 2 ที่INSERTสามารถเขียนใหม่ได้โดยใช้IFเช่นคำตอบของ @ 8kb):

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

0

เมื่อคุณใช้ EXISTS, SQL Server รู้ว่าคุณกำลังทำการตรวจสอบการมีอยู่ เมื่อพบการจับคู่ค่าแรกมันจะคืนค่า TRUE และหยุดการค้นหา

เมื่อคุณรวม 2 คอลัมน์และหากมีค่าใดผลลัพธ์จะเป็นโมฆะ

เช่น

null + 'a' = null

ดังนั้นตรวจสอบรหัสนี้

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

เกี่ยวกับ:

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

หากใช้งานได้ (ฉันยังไม่ได้ทดสอบ) มันจะให้ตารางหนึ่งแถวที่มี 2 คอลัมน์แต่ละอันเป็น TRUE หรือ FALSE ฉันไม่ได้ทดสอบประสิทธิภาพ


2
แม้ว่าสิ่งนี้จะถูกต้องใน DBMS อื่น ๆ ฉันสงสัยว่ามันมีความหมายที่ถูกต้อง สมมติว่าT.B is nullเป็นผลบูลีนแล้วEXISTS(SELECT true)และEXISTS(SELECT false)จะกลับมาจริง ตัวอย่าง MySQL นี้ระบุว่าทั้งสองคอลัมน์มีค่า NULL เมื่อไม่ได้ทำ
Martin Smith
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.