Почему postgres выполняет сканирование таблицы вместо использования моего индекса?

Я работаю с набором данных HackerNews в Postgres. Около 17 миллионов строк, из них 14,5 миллионов комментариев и около 2,5 миллионов историй. Есть очень активный пользователь по имени «rbanffy», у которого 25 тысяч материалов, примерно поровну разделенных историй/комментариев. И «by», и «type» имеют отдельные индексы.

У меня есть запрос:

SELECT *
FROM "hn_items"
WHERE by = 'rbanffy'
and type = 'story'
ORDER BY id DESC
LIMIT 20 OFFSET 0

Это работает быстро (используется индекс «by»). Если я изменю тип на «комментарий», это будет очень медленно. Судя по объяснению, он не использует ни один индекс и выполняет сканирование.

Limit  (cost=0.56..56948.32 rows=20 width=1937)
  ->  Index Scan using hn_items_pkey on hn_items  (cost=0.56..45823012.32 rows=16093 width=1937)
        Filter: (((by)::text = 'rbanffy'::text) AND ((type)::text = 'comment'::text))

Если я изменю запрос на type||''='comment', тогда он будет использовать индекс 'by' и будет выполняться быстро.

Почему это происходит? Я понимаю из https://stackoverflow.com/a/309814/214545, что такой взлом подразумевает что-то неправильно. Но я не знаю что.

РЕДАКТИРОВАТЬ:
Это объяснение для type='story'

Limit  (cost=72553.07..72553.12 rows=20 width=1255)
  ->  Sort  (cost=72553.07..72561.25 rows=3271 width=1255)
        Sort Key: id DESC
        ->  Bitmap Heap Scan on hn_items  (cost=814.59..72466.03 rows=3271 width=1255)
              Recheck Cond: ((by)::text = 'rbanffy'::text)
              Filter: ((type)::text = 'story'::text)
              ->  Bitmap Index Scan on hn_items_by_index  (cost=0.00..813.77 rows=19361 width=0)
                    Index Cond: ((by)::text = 'rbanffy'::text)

РЕДАКТИРОВАТЬ: ОБЪЯСНИТЬ (АНАЛИЗИРОВАТЬ, БУФЕРЫ)

Limit  (cost=0.56..59510.10 rows=20 width=1255) (actual time=20.856..545.282 rows=20 loops=1)
  Buffers: shared hit=21597 read=2658 dirtied=32
  ->  Index Scan using hn_items_pkey on hn_items  (cost=0.56..47780210.70 rows=16058 width=1255) (actual time=20.855..545.271 rows=20 loops=1)
        Filter: (((by)::text = 'rbanffy'::text) AND ((type)::text = 'comment'::text))
        Rows Removed by Filter: 46798
        Buffers: shared hit=21597 read=2658 dirtied=32
Planning time: 0.173 ms
Execution time: 545.318 ms

РЕДАКТИРОВАТЬ: ОБЪЯСНИТЬ (АНАЛИЗИРОВАТЬ, БУФЕРЫ) type='story'

Limit  (cost=72553.07..72553.12 rows=20 width=1255) (actual time=44.121..44.127 rows=20 loops=1)
  Buffers: shared hit=20137
  ->  Sort  (cost=72553.07..72561.25 rows=3271 width=1255) (actual time=44.120..44.123 rows=20 loops=1)
        Sort Key: id DESC
        Sort Method: top-N heapsort  Memory: 42kB
        Buffers: shared hit=20137
        ->  Bitmap Heap Scan on hn_items  (cost=814.59..72466.03 rows=3271 width=1255) (actual time=6.778..37.774 rows=11630 loops=1)
              Recheck Cond: ((by)::text = 'rbanffy'::text)
              Filter: ((type)::text = 'story'::text)
              Rows Removed by Filter: 12587
              Heap Blocks: exact=19985
              Buffers: shared hit=20137
              ->  Bitmap Index Scan on hn_items_by_index  (cost=0.00..813.77 rows=19361 width=0) (actual time=3.812..3.812 rows=24387 loops=1)
                    Index Cond: ((by)::text = 'rbanffy'::text)
                    Buffers: shared hit=152
Planning time: 0.156 ms
Execution time: 44.422 ms

РЕДАКТИРОВАТЬ: последние результаты теста. Я играл с запросом type='comment' и заметил, что если изменить предел на более высокое число, например 100, он использует индекс by. Я играл со значениями, пока не нашел критическое число «47». Если у меня было ограничение 47, то использовался индекс by, если у меня было ограничение 46, то это было полное сканирование. Я предполагаю, что это число не волшебное, просто это порог для моего набора данных или какой-то другой переменной, которую я не знаю. Я не знаю, поможет ли это.


person Yehosef    schedule 29.05.2018    source источник
comment
@a_horse_with_no_name — я не знаю, как показать больше плана выполнения — я запустил EXPLAIN ‹the_query›, и вот результат. что еще мне делать? Запуск анализа ничего не изменил.   -  person Yehosef    schedule 29.05.2018
comment
Это может быть проблема с физическим порядком данных. Можем ли мы получить EXPLAIN (ANALYZE, BUFFERS) вывод?   -  person Laurenz Albe    schedule 29.05.2018
comment
@LaurenzAlbe - добавлено. Обратите внимание, что идентификатор ПК имеет индекс ASC, и я заказываю DESC. Но я попытался изменить свой порядок на ASC, и это не меняет объяснения. (Я удалил Backward после сканирования индекса, чтобы избежать путаницы, поскольку это не имело значения.)   -  person Yehosef    schedule 29.05.2018
comment
Для справки: если я удалю порядок, он использует правильный индекс без взлома. Если я изменяю порядок на by или type вместо id, он использует индекс (но это не то, что я хочу). Когда я выбираю порядок по id (asc||desc) - он его не использует. Это все при использовании type=comment — type=story работает всегда.   -  person Yehosef    schedule 29.05.2018
comment
Кажется, что оптимизатор недооценивает количество строк, возвращаемых запросом, и считает, что накладные расходы на сортировку строк больше, чем накладные расходы при использовании неправильного индекса. Пожалуйста, отредактируйте вопрос и добавьте определение индекса hn_items_by_index. Можете ли вы показать результат explain (analyze, buffers) для запроса fast?   -  person a_horse_with_no_name    schedule 29.05.2018
comment
@a_horse_with_no_name — добавлено.   -  person Yehosef    schedule 29.05.2018
comment
возможно, нужно запустить/перезапустить вакуумный анализ (чтобы статистика таблиц обновлялась и оптимизатор мог принимать более правильные решения) еще одна вещь, которую стоит попробовать: set enable_seqscan=false;   -  person weinerk    schedule 29.05.2018
comment
@weinerk - спасибо. Пробовал оба, улучшений нет.   -  person Yehosef    schedule 29.05.2018


Ответы (1)


Поскольку rbanffy съело много comment, PostgreSQL предполагает, что будет достаточно быстро, если он будет искать таблицу в порядке, подразумеваемом предложением ORDER BY (которое может использовать индекс первичного ключа), пока не найдет 20 строк, соответствующих условию поиска.

К сожалению, случилось так, что в последнее время парень стал ленивым, PostgreSQL должен сканировать 46798 самых высоких id, пока не найдет свои 20 совпадений. (Вы действительно не должны были удалять Backwards, это меня смутило.)

Лучший способ обойти это — запутать PostgreSQL, чтобы он не выбирал индекс первичного ключа, например, так:

SELECT *
FROM (SELECT * FROM hn_items
      WHERE by = 'rbanffy'
        AND type = 'comment'
      OFFSET 0) q
ORDER BY id DESC
LIMIT 20;
person Laurenz Albe    schedule 29.05.2018
comment
Интересно, поможет ли оптимизатору создание расширенной статистики по обоим столбцам? - person a_horse_with_no_name; 29.05.2018
comment
Сомневаюсь, потому что проблема здесь не в неправильной оценке, а в неудачном распределении значений. - person Laurenz Albe; 29.05.2018
comment
Спасибо за ответ - переварил. Что касается Backwards, я мог бы задать тот же запрос, используя order by id ASC, он не печатает Backwards и не делает его лучше. Так что я не уверен, почему это должно иметь значение - person Yehosef; 29.05.2018
comment
Вероятно, это не имеет значения, но любая модификация вывода сбивает с толку. Без обратного это означало бы, что индекс первичного ключа упорядочен по убыванию, чего не может быть. - person Laurenz Albe; 29.05.2018
comment
Я не уверен, что понимаю - зачем ему сканировать таблицу вместо первой фильтрации по полю by. Возвращаясь к 50 тысячам записей, это не имеет большого значения для таблицы в 17 миллионов. И, FWIW, если я добавлю еще одно предложение where and descendants>250' using type='story'`, оно по-прежнему будет работать быстро и будет использовать индекс по индексу, даже если для получения 20 результатов ему придется сканировать более 10 миллионов строк. - person Yehosef; 29.05.2018
comment
Проблема в том, что я не хочу использовать хак - мой хак тоже работал нормально. Я ожидаю, что postgres будет достаточно умен в простом запросе, подобном этому, чтобы дать правильные результаты. В настоящее время я предполагаю, что где-то ошибся, но мне трудно доказать, что это правда. И я думаю, что Backwards это отвлекающий маневр. Если вы хотите, чтобы я переиндексировал базу данных, используя идентификатор DESC, и повторил запрос, я бы сделал это. Как я уже сказал, выполнение запроса с order by id asc не печатало в обратном порядке и не было лучше. - person Yehosef; 29.05.2018
comment
Вы неправильно меня понимаете. Забудем об обратном, оно неважно. Проблема в том, что PostgreSQL недостаточно умен, чтобы понять, что в последней части таблицы так мало совпадающих строк, поэтому выбранный план оказывается плохим. Вам понадобится хак, или вы должны сделать PostgreSQL умнее. Но это будет нелегко. Вы должны знать, по крайней мере, для наиболее часто встречающихся значений, как они физически распределены в таблице, и я не думаю, что есть разумный способ хранить эту лавину данных в статистике таблицы. Извините, но это должен быть взлом. Я попытался объяснить, почему. - person Laurenz Albe; 29.05.2018
comment
@LaurenzAlbe - я только что добавил дополнительную информацию к вопросу о том, что если я увеличу лимит, он будет использовать правильный индекс. Соответствует ли это вашему пониманию проблемы? Как я могу проверить вашу гипотезу? Знаете ли вы какие-либо источники, в которых говорится о таких проблемах, связанных с распространением данных? Просто кажется сломанным. - person Yehosef; 30.05.2018
comment
Да, это совпадает с моей гипотезой. Если предел слишком высок, PostgreSQL будет знать, что ему нужно слишком долго сканировать индекс, чтобы быть эффективным. Вы можете проверить мою гипотезу, обновив 20 строк с самыми высокими ids, чтобы они соответствовали условию, тогда это будет быстро. Разве Rows Removed by Filter: 46798 не убеждает вас? Конечно, это несколько нарушено, потому что оценки плохие. Если вы не знаете, как ее улучшить, и не хотите ее обходить, попробуйте другую базу данных. - person Laurenz Albe; 30.05.2018
comment
еще немного информации о том, что ограничение влияет на планирование postgresql.org/docs/9.6/ static/using-explain.html dba.stackexchange.com/questions/73614/ - person weinerk; 31.05.2018
comment
кстати, я думаю, что смысл хака с конкатенацией просто предотвращает конкретный индекс - добавляя динамическое выражение (в вашем случае - предотвращение типа индекса) postgresql.org/docs/9.6/static/indexes-expressional.html` - person weinerk; 31.05.2018