image

Релиз PostgreSQL 11 состоится еще не скоро, только в октябре. Но фичфриз уже наступил, а значит мы знаем, какие фичи попали в этот релиз, и можем их потестировать, собрав PostgreSQL из ветки master. Особого внимания заслуживает фича под названием INCLUDE-индексы. Патч изначально написан Анастасией Лубенниковой, а потом допилен Александром Коротковым и Федором Сигаевым. Протолкнуть его в PostgreSQL заняло «всего лишь» что-то около трех лет.

Давайте попробуем разобраться, что же это за индексы за такие. Для начала создадим табличку для опытов:

create table test (k serial primary key, v text, ts timestamp);
insert into test (v, ts) select 'key_' || s , now() from generate_series(1, 10000) as s;

… и построим по ней обычный btree-индекс:

create index on test (v);

Взглянем на план выполнения следующего запроса:

=# explain select v, ts from test where v > 'key_1337' and v < 'key_2337';
                                 QUERY PLAN                                  
-----------------------------------------------------------------------------
 Bitmap Heap Scan on test  (cost=31.57..112.09 rows=1101 width=16)
   Recheck Cond: ((v > 'key_1337'::text) AND (v < 'key_2337'::text))
   ->  Bitmap Index Scan on test_v_idx  (cost=0.00..31.29 rows=1101 width=0)
         Index Cond: ((v > 'key_1337'::text) AND (v < 'key_2337'::text))
(4 rows)

Смотрите, что происходит. Поскольку индекс построен по колонке v, а в запросе мы выбираем v и ts, PostgreSQL вынужден выполнять запрос в два шага. Сначала он идет по индексу и находит строки, удовлетворяющие условию. Затем ему приходится сходить в таблицу для получения ts.

Идея INCLUDE-индексов заключается в том, чтобы включить все необходимые для выполнения запроса данные прямо в индекс (но не индексировать их). Таким образом, запрос становится возможно выполнить за один index scan.

Давайте проверим:

drop index test_v_idx;
create index on test (v) include (ts);
explain select v, ts from test where v > 'key_1337' and v < 'key_2337';

Результат:

 Index Only Scan using test_v_ts_idx on test  (cost=0.29..46.30 rows=1101 width=16)
   Index Cond: ((v > 'key_1337'::text) AND (v < 'key_2337'::text))
(2 rows)

За счет того, что теперь мы не ходим в таблицу, запрос должен работать быстрее. Стоит однако отметить, что на практике все зависит от ваших данных. Каждый случай уникален, поэтому я сознательно не привожу здесь каких-то синтетических бенчмарков. Может оказаться, что на ваших объемах данных index only scan с include-индексами работает так же быстро, как и в случае с обычными индексами. А то и вовсе накопленная статистика говорит PostgreSQL, что запрос быстрее сделать heap scan'ом. Такое может произойти, например, если селективность вашего запроса низка.

Так или иначе, знать про эту возможность полезно, и я искренне рад, что она появится в PostgreSQL 11.

Комментарии (25)


  1. miolini
    09.04.2018 18:25

    Огонь! :-)


  1. AxisPod
    09.04.2018 19:01

    Назовите по человечески «Покрывающие индексы».


    1. x-wao
      09.04.2018 22:46

      Мы говорим так. Но это спорное название, т.к. понятие «покрывающий индекс» определено по отношению к конкретному запросу. По сути, это индекс, которого достаточно для index-only scan в этом запросе. Не обязательно с INCLUDE. См use-the-index-luke.com/sql/clustering/index-only-scan-covering-index


  1. mr_bag
    09.04.2018 20:53

    Осталось немного:
    — table in memory
    — columnstore index
    :)


  1. sebres
    09.04.2018 21:46
    +1

    Нужно наверное добавить, что де факто — эта штука "слизана" с INCLUDE-INDEX for SQL Server от мелко-мягких… (которая по моему появилась в MSSQL аж в 2005-й версии).


    Реально полезная штука…
    И тут по моему не совсем понятно описаны преимущества сего действа — т.е. грубо говоря для чего оно вообще имеет место.
    В общем, вот это вот:


    CREATE UNIQUE INDEX newidx ON sometab (c1, c2) INCLUDE (c3, c4);

    заменяет следующие два индекса (в старом виде):


    CREATE UNIQUE INDEX oldunqidx ON sometab (c1, c2);
    CREATE        INDEX oldcvridx ON sometab (c1, c2, c3, c4);

    без хранения редундантной "дубликации" для одинаковых значений.


    Т.е. прирост скорости тут скорее вторичен (например через меньшее вымывание кэша на соответственно меньшем индексе и за счет меньших потерь времени на перестроение его на CUD-операциях).


    Например, для этого примера (если все c1-c4 int):


      Name    | RowCnt  | Reserved | Data     | IndexSize | Unused
      ------- | ------- | -------- | -------- | --------- | ------
    - testold | 638976  | 49112 KB | 15832 KB | 33192 KB  | 88 KB
    + testnew | 638976  | 35472 KB | 15832 KB | 19560 KB  | 80 KB

    Про скорость оно всё сложно на самом деле, но если тупо в лоб при прочих равных (под паразитной нагрузкой) — как-то так:


    # 1M rows
    - testold:  365.057 µs/# 2740 # 2739.3 #/sec
    + testnew:  357.219 µs/# 2800 # 2799.4 #/sec
    # 10M rows
    - testold:  413.326 µs/# 2420 # 2419.4 #/sec
    + testnew:  367.916 µs/# 2719 # 2718.0 #/sec
    # 100M rows
    - testold:  696.943 µs/# 1435 # 1434.8 #/sec
    + testnew:  455.329 µs/# 2197 # 2196.2 #/sec

    Ну и при таком "сахаре" бонусом сверху возможность использовать короткий UNIQUE индекс по значениям c1, c2 при scan по короткому кортежу (как в этом примере и др. тому подобные вещи).


    1. DenisTrunin
      10.04.2018 07:10

      заменяет не 2 индекса, а 3, порядок полей в Include неважен, в этом вся и суть
      (c1, c2); (c1, c2, c3, c4); (c1, c2, c4, c3);


      1. bolk
        10.04.2018 09:40

        Что-то я не пойму никак зачем тут три индекса, если бы хватило одного, который накрывает все 4 поля?

        => create index on test(a,b,c,d);
        CREATE INDEX
        => explain select c,d from test where a=100 and b=1;
                                            QUERY PLAN
        -----------------------------------------------------------------------
         Index Only Scan using test_a_b_c_d_idx on test  (cost=0.42..8.45 rows=1 width=8)
           Index Cond: ((a = 100) AND (b = 1))
        (2 rows)


        1. Alozar
          10.04.2018 10:25
          -1

          Не надо забывать, что индекс по нескольким полям не во всех случаях поможет, а СУБД нагружает всегда.
          В вашем примере всё работает хорошо, т.к. отбор идёт по первым двум полям. Если же потребуется отбор по полям a и c, индекс уже не поможет.


          1. bolk
            10.04.2018 10:42

            разве по «c» в INCLUDE можно будет искать? там видимо filter будет?


        1. sebres
          10.04.2018 10:41

          Заменяет два, потому что oldunqidx — UNIQUE и покрывает ключевые поля (и соответственно быстрее), кроме того играет дополнительно роль CONSTRAINT по кортежу (c1,c2);


          Собрав же только один индекс oldcvridx (даже если и UNIQUE, но уже по 4-м полям), вы "теряете" обе эти возможности.


          1. bolk
            10.04.2018 10:42

            UNIQUE просмотрел, точно.


          1. bolk
            10.04.2018 10:51

            Так нет, непонятно всё же. По include индексированный поиск возможен разве? сортировка?


            1. sebres
              10.04.2018 11:04

              1. Да
              2. Да, но смысл сомнителен при выборке по юник-ключам (c1, c2)


      1. sebres
        10.04.2018 10:32

        порядок полей в Include неважен

        Ну уточните тогда, неважен для какого действия?
        Где например ниже будет важен порядок, т.е. по вашему выходит следующие два запроса разные?


        select c3, c4 from sometab where c1 = ?v1 and c2 = ?v2
        select c4, c3 from sometab where c1 = ?v1 and c2 = ?v2


        1. Alozar
          10.04.2018 10:35
          -1

          Порядок полей неважен, когда по include-полям требуется только получение данных, а отбор не производится.


    1. gsl23
      10.04.2018 15:04

      Не совсем заменяет.
      Подумайте как запрос вида

       select .. from where c1 = var1 and c2 = var2 and c3 = var3 and c4 = var4

      Будет вести себя с
      CREATE        INDEX oldcvridx ON sometab (c1, c2, c3, c4);

      и с
      CREATE UNIQUE INDEX newidx ON sometab (c1, c2) INCLUDE (c3, c4);


      1. sebres
        10.04.2018 15:15

        Иии… что там должно чем отличатся?
        Вы правда про запрос по одной row спрашиваете (я напомню кортеж (с1, с2) уникален).


        Весь смысл в том что весь кортеж (с1, с2, c3, c4) находится в индексе, т.е. ему не нужно лезть в таблицу для проверки данных.


        А вообще-то, даже следующее будет с INCLUDE-индексом если не быстрее, то за то-же самое время:


        select ... where c1 = ?v1 and c2 >= ?v2fr and c2 <= ?v2to and c3 = ?v3 and c4 = ?v4

        Собственно по той же причине.
        По крайней мере для MSSQL проверялось мною не раз — чистый index-seek в обоих случаях.


        1. gsl23
          10.04.2018 16:07

          Иии… что там должно чем отличатся?
          Вы правда про запрос по одной row спрашиваете (я напомню кортеж (с1, с2) уникален).

          Мм, в случае уникальности (с1, с2) пожалуй, скорей всего, вы правы. Тогда это не совсем понял ваш пример. Почему индекс по (с1, с2, c3, c4) в вашем примере не уникальный, если кортеж c1, c2 уникален?


          1. sebres
            10.04.2018 16:33

            Потому что только ключ (или ключи) включаются в индекс напрямую, а покрытие (nokeys) соответственно в INCLUDE.
            Грубо причина описана выше.


  1. coctic
    10.04.2018 11:57
    +1

    То есть если включить в индекс все поля исходной таблицы, то получится Index Organized Table, за тем исключением, что данные будут дублированы и в индексе, и в таблице?


    1. x-wao
      10.04.2018 14:43
      +1

      Индекс не очень-то приспособлен для таких вещей. Вот например, размер записи в btree ограничен тем, что на странице индекса их должно минимум три умещаться. Большие поля не пройдут, а toast в индексе нет.


      1. coctic
        10.04.2018 14:44
        +1

        Ну то есть в очень ограниченном виде, но иногда можно и так использовать.


  1. vovik0134
    10.04.2018 23:38

    Правильно ли я понимаю, что такой подход сводит на нет оптимизацию HOT-update в случае, если изменяется «включенное» поле? Так же не очень понятно на счёт работы MVCC. PostgreSQL в худшем случае все равно придётся сходить в таблицу, чтобы узнать xmin/xmax?


  1. LeshaRB
    11.04.2018 07:36

    Случайно наткнулся на данную статью, и стало интересно.


    В чем отличие multicolumn index от include?


    1. vovik0134
      11.04.2018 10:51

      В том что, во многоколоночном индексе индексируются все поля, а в данном случае включённые поля не индексируются. Просто хранятся в индексе