count(*) nie taki zły jak sądzisz

Z pewnością wiesz, że count(*) to samo zło. Robi pełny skan tabeli, czyta z dysku wszystkie kolumny przez co bloki bazodanowe nie są zachowywane w pamięci podręcznej bazy danych. W efekcie jest bardzo powolną komendą.

Czy na pewno?

Stałe

Może obiło Ci się o uszy, że aby przyspieszyć wykonanie count zamiast gwiazdki należy użyć stałej? Na przykład:

select count(1) from tabela;

lub

select count('a') from tabela;

Może nawet pamiętasz, że jako parametr count, można podać nazwę kolumny.

select count(kolumna) from tabela;

Jakie narzędzie jest przydatne aby sprawdzić wpływ na wydajność każdego z podejść?

Masz rację, warto zerknąć na plan wykonania.

Tworzę tabelę testową o mówiącej nazwie t1.

create table t1 (
    id number primary key
    ,c1 char
    ,c2 date not null
);

Składa się z trzech kolumn z czego jedna – id – jest kluczem głównym. Jak zapewne wiesz na klucz główny składają się unikalne wartości bez NULL.

Uruchamiam explain plan dla select count(*) i zgodnie z oczekiwaniami widzę pełny skan tabeli.

explain plan for 
select count(*) from t1;
select * from table(dbms_xplan.display);
-------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Cost (%CPU)| Time     |
-------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |      |     1 |            |          |
|   2 |   TABLE ACCESS FULL| T1   |     1 |     2   (0)| 00:00:01 |
-------------------------------------------------------------------

Hmm, czyżby twórcy Oracle przez 40 lat nie wpadli na pomysł zoptymalizowania count(*)?

Może wpadli, ale ja nie przygotowałem danych o tabeli t1 dla optymalizatora kosztowego. Trzeba zebrać statystyki.

exec dbms_stats.gather_table_stats(ownname=>null,tabname=>'T1');

Uruchamiam explain plan dla select count(*) jeszcze raz i teraz widać, że dla policzenia rekordów użyty został, niejawnie utworzony dla wymuszenia unikalności klucza głównego, indeks.

-----------------------------------------------------------------------------
| Id  | Operation        | Name             | Rows  | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT |                  |     1 |     0   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE  |                  |     1 |            |          |
|   2 |   INDEX FULL SCAN| SYS_C00147319778 |     1 |     0   (0)| 00:00:01 |
-----------------------------------------------------------------------------

Brawo twórcy Oracle!

Indeks na jednej kolumnie jest mniejszy – zajmuje mniej bloków bazodanowych – niż cała tabela. Skan całego indeksu będzie szybszy niż skan całej tabeli.

Podobnie sprytnie zachowa się oracle gdy w wywołaniu count użyjesz stałej.

Zarówno w przypadku użycia, jako parametr wywołania funkcji count, stałej liczbowej jak i znakowej optymalizator zdecydował się na skan indeksu na kolumnie id zamiast na skan tabeli.

Kolumny

Co się stanie gdy jako parametru wywołania count użyjesz nazwy kolumny?

Gdy użyjesz, jako parametru wywołania funkcji count, kolumny c1 w efekcie dostaniesz skan tabeli.

explain plan for 
select count(c1) from t1;
select * from table(dbms_xplan.display);
---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |     3 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |      |     1 |     3 |            |          |
|   2 |   TABLE ACCESS FULL| T1   |     1 |     3 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------

Zwróć uwagę, że kolumna c1 dozwala wstawianie NULLi.

Natomiast użycie, nie dozwalającej NULLi, kolumny c2 skutkuje użyciem indeksu utworzonego dla klucza głównego.

Gdyby na kolumnie c2 był założony indeks to zostałby użyty zamiast indeksu dla klucza głównego.

create index ix1 on t1(c2);
-----------------------------------------------------------------
| Id  | Operation        | Name | Rows  | Cost (%CPU)| Time     |
-----------------------------------------------------------------
|   0 | SELECT STATEMENT |      |     1 |     0   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE  |      |     1 |            |          |
|   2 |   INDEX FULL SCAN| IX1  |     1 |     0   (0)| 00:00:01 |
-----------------------------------------------------------------

Jak sądzisz: dlaczego optymalizator używa indeksu gdy liczysz rekordy używając kolumny NOT NULL, a nie używa gdy liczysz używając kolumny dozwalającej NULL?

Podsumowanie

Podczas produkcji tego materiału korzystałem z Oracle 19c dostępnego za darmo na livesql.

Z tego materiału wiesz jak istotne jest sprawdzanie planu wykonania, odpowiednie indeksowanie oraz tworzenie kolumn NOT NULL. A także aktualne statystyki.

Najważniejsze do zapamiętania jest to, że optymalizator Oracle, do policzenia rekordów w tabeli, użyje indeksu na kolumnie NOT NULL.

O ile taki istnieje i o ile optymalizator o nim wie.

Zrównoleglenie

Ale jeśli upierasz się przy skanowaniu całej tabeli to możesz rozważyć zrównoleglenie procesu poprzez użycie hintu parallel.

select /*+ parallel */ count(c1) from t1;

Skomentuj jakie mechanizmy optymalizacji count dostarcza motor bazy danych na którym Ty pracujesz.

Marcin Badtke

Przyjaźnie o SQL, bazach danych i ludziach

Może Ci się spodobać...

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *