단순 조인에 사용되지 않은 기본 키 인덱스


16

다음과 같은 테이블 및 인덱스 정의가 있습니다.

CREATE TABLE munkalap (
    munkalap_id serial PRIMARY KEY,
    ...
);

CREATE TABLE munkalap_lepes (
    munkalap_lepes_id serial PRIMARY KEY,
    munkalap_id integer REFERENCES munkalap (munkalap_id),
    ...
);

CREATE INDEX idx_munkalap_lepes_munkalap_id ON munkalap_lepes (munkalap_id);

다음 쿼리에서 munkalap_id의 인덱스가 사용되지 않는 이유는 무엇입니까?

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id);

QUERY PLAN
Hash Join  (cost=119.17..2050.88 rows=38046 width=214) (actual time=0.824..18.011 rows=38046 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.005..4.574 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=3252 width=4) (actual time=0.810..0.810 rows=3253 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 115kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=3252 width=4) (actual time=0.003..0.398 rows=3253 loops=1)
Total runtime: 19.786 ms

필터를 추가해도 동일합니다.

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id) WHERE NOT lezarva;

QUERY PLAN
Hash Join  (cost=79.60..1545.79 rows=1006 width=214) (actual time=0.616..10.824 rows=964 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.007..5.061 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=86 width=4) (actual time=0.587..0.587 rows=87 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 4kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=86 width=4) (actual time=0.014..0.560 rows=87 loops=1)
              Filter: (NOT lezarva)
Total runtime: 10.911 ms

답변:


22

많은 사람들이 "순차 스캔이 나쁘다"는 지침을 듣고 계획에서이를 제거하려고하지만 그렇게 간단하지는 않습니다. 쿼리가 테이블의 모든 행을 다루는 경우 순차적 스캔이 해당 행을 얻는 가장 빠른 방법입니다. 이것이 두 테이블의 모든 행이 필요했기 때문에 원래 결합 쿼리가 seq 스캔을 사용한 이유입니다.

쿼리를 계획 할 때 Postgres의 플래너는 가능한 다른 방식으로 다양한 작업 (계산, 순차 및 임의 IO)의 비용을 추정하고 가장 낮은 비용으로 추정 한 계획을 선택합니다. 회전 스토리지 (디스크)에서 IO를 수행 할 때 임의 IO는 일반적으로 순차 IO보다 상당히 느립니다. random_page_cost 및 seq_page_cost 의 기본 pg 구성 은 비용의 4 : 1 차이를 추정합니다.

이러한 고려 사항은 인덱스를 사용하는 조인 또는 필터 방법과 테이블을 순차적으로 스캔하는 방법을 고려할 때 적용됩니다. 인덱스를 사용할 때 계획은 인덱스를 통해 빠르게 행을 찾은 다음 행 데이터를 해결하기 위해 임의 블록 읽기를 고려해야합니다. 필터링 조건자를 추가 한 두 번째 쿼리의 경우 WHERE NOT lezarvaEXPLAIN ANALYZE 결과에서 계획 예측에 어떤 영향을 주 었는지 확인할 수 있습니다. 플래너는 조인으로 인해 1006 개의 행을 추정합니다 (실제 결과 세트 964와 거의 일치 함). 더 큰 테이블 munkalap_lepes에 약 38K 개의 행이 포함되어 있으면 플래너는 조인이 테이블의 약 1006/38046 또는 1/38 개의 행에 액세스해야 함을 알 수 있습니다. 또한 평균 행 너비가 214 바이트이고 블록이 8K임을 알고 있으므로 약 38 행 / 블록이 있습니다.

이러한 통계를 통해 플래너는 조인이 테이블의 데이터 블록 전체 또는 대부분을 읽어야 할 것으로 간주합니다. 인덱스 조회도 무료가 아니며 필터 조건을 평가하는 블록을 스캔하는 계산은 IO에 비해 매우 저렴하므로 플래너는 순차적으로 테이블을 스캔하고 seq 스캔을 계산할 때 인덱스 오버 헤드 및 임의 읽기를 피하도록 선택했습니다. 더 빠를 것입니다.

실제로는 데이터가 종종 OS 페이지 캐시를 통해 메모리에서 사용 가능하므로 모든 블록 읽기에 IO가 필요한 것은 아닙니다. 주어진 쿼리에 대해 캐시가 얼마나 효과적인지 예측하기는 쉽지 않지만 Pg 플래너는 간단한 휴리스틱을 사용합니다. effective_cache_size 구성 값 은 플래너에게 실제 IO 비용이 발생할 가능성을 추정합니다. 값이 클수록 임의의 IO에 대한 저렴한 비용을 추정하게되므로 순차적 스캔보다 인덱스 기반 방법으로 바이어스 할 수 있습니다.


고마워, 이것은 지금까지 내가 읽은 최고의 (그리고 가장 간결한) 설명입니다. 몇 가지 핵심 사항이 명확 해졌습니다.
dezso

1
훌륭한 설명. 그러나 행 / 데이터 페이지 계산은 약간 벗어났습니다. MAXALIGN에 따라 페이지 헤더 (24 바이트) + 각 행당 항목 포인터에 대해 4 바이트 + 행 헤더 HeapTupleHeader(행당 23 바이트) + NULL 비트 마스크 + 정렬을 고려해야합니다. 마지막으로, 열의 데이터 유형 및 순서에 따라 데이터 정렬로 인해 알 수없는 양의 패딩이 발생합니다. 이 경우 8KB 페이지에는 33 행을 넘지 않습니다. (TOAST는 고려하지 않았습니다.)
Erwin Brandstetter

1
@ErwinBrandstetter보다 정확한 행 크기 계산을 작성해 주셔서 감사합니다. 나는 Explain에 의한 행 너비 추정 출력이 항상 헤더 및 NULL 비트 마스크와 같은 행 당 고려 사항을 포함하지만 페이지 레벨 오버 헤드는 고려하지 않는다고 가정했습니다.
dbenhur

1
@ dbenhur : EXPLAIN ANALYZE SELECT foo from bar기본 더미 테이블로 빠르게 실행 하여 확인할 수 있습니다. 또한 실제 온 디스크 공간은 데이터 정렬에 따라 달라 지므로 일부 행만 검색 할 때는 고려하기 어렵습니다. in의 행 너비 EXPLAIN는 검색된 열 세트에 대한 기본 공간 요구 사항을 나타냅니다.
Erwin Brandstetter

5

두 테이블에서 모든 행을 검색하므로 인덱스 스캔을 사용하면 실질적인 이점이 없습니다. 인덱스 스캔은 테이블에서 몇 개의 행만 선택하는 경우에만 의미가 있습니다 (일반적으로 10 % -15 % 미만).


그렇습니다, 당신은 옳습니다 :) 더 구체적인 경우로 상황을 명확히하려고 노력했습니다. 마지막 쿼리를 참조하십시오.
dezso

@dezso : 같은 것입니다. 인덱스가 (lezarva, munkalap_id)있고 충분히 선택적인 경우 사용할 수 있습니다. 그렇게 NOT하면 가능성이 줄어 듭니다.
ypercubeᵀᴹ

귀하의 제안에 따라 부분 색인을 추가했으며 사용하여 문제의 절반을 해결했습니다. 그러나 외래 키에 대한 인덱스는 원래
3252에

1
@dezso 행의 평균 폭은 214 바이트이므로 8K 데이터 블록 당 40 행 미만이됩니다. 지수의 선택도는 약 1/40 (1006/38046)입니다. 따라서 Pg는 인덱스를 사용할 때 모든 블록을 순차적으로 읽는 것이 거의 동일한 수의 블록을 무작위로 읽는 것보다 저렴하다는 것을 알았습니다. 이러한 예상 tradoff는 유효 _ 캐시 _ 크기 및 random_page_cost 구성 값의 영향을받을 수 있습니다.
dbenhur

@ dbenhur : 귀하의 의견을 올바른 답변으로 만들 수 있습니까?
dezso
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.