여러 열에서 EXISTS를 효율적으로 확인하는 방법은 무엇입니까?


26

이것은 정기적으로 제기되는 문제이며 아직 좋은 해결책을 찾지 못했습니다.

다음 테이블 구조를 가정

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

그리고 요구 사항은 널 입력 가능 열 중 하나 B또는 C실제로 임의의 NULL값을 포함 하는지 여부를 판별하는 것입니다 (있는 경우).

또한 테이블에 수백만 개의 행이 포함되어 있다고 가정합니다 (이 클래스의 쿼리에 대한보다 일반적인 솔루션에 관심이 있으므로 열 통계를 사용할 수 없음).

나는 이것에 접근하는 몇 가지 방법을 생각할 수 있지만 모두 약점이 있습니다.

두 개의 별도 EXISTS진술. 이는 쿼리 NULL가 발견 되 자마자 검색을 일찍 중지 할 수 있다는 이점이 있습니다 . 그러나 실제로 두 열에 모두 NULLs 가 없으면 두 번의 전체 스캔이 수행됩니다.

단일 집계 쿼리

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

이렇게하면 두 열을 동시에 처리 할 수 ​​있으므로 전체 스캔이 최악 인 경우가 있습니다. 단점은 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)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+

@Thomas의 답변 TOP 3TOP 2위해 잠재적으로 더 일찍 종료되도록 변경 했습니다 . 나는 그 대답에 대해 기본적으로 병렬 계획을 얻었으므로 MAXDOP 1다른 계획과 비교하여 읽기 수를 더하기 위해 힌트로 시도했습니다 . 이전 테스트에서 전체 테이블을 읽지 않고 쿼리 단락을 보았으므로 결과에 다소 놀랐습니다.

단락이 다음과 같은 테스트 데이터 계획

단락

ypercube의 데이터 계획은

단락되지 않음

따라서 계획에 블로킹 정렬 연산자를 추가합니다. 나는 또한 HASH GROUP힌트로 시도 했지만 여전히 모든 행을 읽습니다.

단락되지 않음

따라서 hash match (flow distinct)다른 대안은 어쨌든 모든 행을 차단하고 소비하므로 운영자 가이 계획을 단락시킬 수 있도록 하는 것이 핵심 입니다. 나는 이것을 구체적으로 강요 할 힌트가 없다고 생각하지만 분명히 "일반적으로 옵티마이 저는 입력 세트에 고유 한 값보다 적은 수의 출력 행이 필요하다고 결정하는 Flow Distinct를 선택합니다." .

@ypercube의 데이터는 각 열에 하나의 행 NULL(값은 카디널리티 = 30300)을 가지며 연산자로 들어오고 나가는 예상 행은 모두 1입니다. 옵티 마이저에 술어를 좀 더 불투명하게하여 플로우 구별 연산자를 사용하여 계획을 생성했습니다.

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열 모두 NULL을 가지고 BC. 즉시 종료하지 않고 계속 스캔합니다. 이것을 피하는 한 가지 방법은 행을 스캔 할 때 피벗을 해제하는 것입니다. 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(아래 계획).

피봇 해제

답변:


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). 이 3 개 중 2 개는 충분할 것입니다. (NULL,NULL)첫 번째를 찾으면 두 번째도 필요하지 않습니다. 또한 단락을 위해 계획은 통해 구분을 구현해야 hash match (flow distinct)보다는 운영자 hash match (aggregate)또는distinct sort
마틴 스미스

6

질문을 이해함에 따라 B 또는 C가 null 인 행을 실제로 반환하는 것과 달리 열 값에 null이 있는지 알고 싶습니다. 그렇다면, 왜 그렇지 않습니까?

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 및 백만 개의 행이있는 테스트 장비의 클라이언트 통계 탭에서 다음과 같은 결과를 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

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

참고로 Red-gate의 SQL 생성기를 사용하여 데이터를 생성했습니다. 백만 개의 행 중 9,886 개의 행에는 널 B 값이 있고 10,019에는 널 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

각 테스트 (두 세트) 전에 내가 실행 CHECKPOINT하고 DBCC DROPCLEANBUFFERS.

다음은 테이블에 널이없는 결과입니다. ypercube에서 제공하는 2 가지 솔루션은 읽기 및 실행 시간 측면에서 내 솔루션과 거의 동일합니다. 본인은 이것이 고급 스캔 을 사용하는 Enterprise / Developer 에디션의 장점 때문이라고 생각합니다 . Standard 에디션 이하 만 사용하는 경우 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

2008 - r22012 버전의 SQL-Fiddle에서 30K 행으로 테스트되었습니다 .

  • EXISTS쿼리는 Null을 조기에 발견 할 때 효율성면에서 큰 이점을 보여줍니다.
  • EXISTS2012 년 모든 경우에 쿼리 성능이 향상되어 설명 할 수 없습니다.
  • 2008R2에서 Null이 없으면 다른 두 쿼리보다 느립니다. Null을 빨리 찾을수록 더 빨리 얻을 수 있으며 두 열에 모두 null이 있으면 다른 두 쿼리보다 훨씬 빠릅니다.
  • Thomas Kejser의 쿼리는 Martin의 CASE쿼리 와 비교하여 2012 년에 약간 그러나 지속적으로 개선되었으며 2008R2에서는 더 나빠졌습니다 .
  • 2012 버전은 훨씬 더 나은 성능을 가진 것으로 보입니다. 최적화 프로그램의 개선뿐만 아니라 SQL-Fiddle 서버의 설정과 관련이있을 수도 있습니다.

쿼리 및 타이밍. 완료된 타이밍 :

  • 널이 전혀없는 1 위
  • 작은 기둥 B이있는 2 열NULLid 있습니다.
  • 두 번째 열은 NULL각각 작은 ID로 하나씩 있습니다.

여기에 우리가 간다 (계획에 문제가있다, 나중에 다시 시도 할 것이다. 지금 링크를 따르십시오) :


EXISTS 하위 쿼리가 2 개인 쿼리

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

마틴 스미스의 단일 집계 쿼리

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

토마스 케이 저의 질문

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쿼리 와 유사 합니다. null이 없으면 테스트가 더 좋을 것이라고 생각했지만 테스트에서 그렇지 않은 것으로 나타났습니다.


제안 (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 년에는 더 좋지 않습니다 (아마도 2kb는 @ 8kb의 답변과 같이를 INSERT사용하여 다시 작성할 수 있음 IF).

------------------------------------------
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이 있으면 결과는 null입니다.

예 :

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;

이것이 작동하면 (테스트하지 않은 경우), 각각 TRUE 또는 FALSE 인 2 개의 열이있는 1 행 테이블이 생성됩니다. 나는 효율성을 테스트하지 않았다.


2
이것이 다른 DBMS에서 유효하더라도 올바른 의미가 있는지 의심합니다. 가정하면 T.B is null다음 부울 결과로 취급 EXISTS(SELECT true)하고 EXISTS(SELECT false)모두 반환 사실 것입니다. 이 MySQL의 예는 사실도 할 때 두 열이 NULL이 포함되어 있음을 나타냅니다
마틴 스미스에게
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.