Postgres использует неправильный индекс

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

EXPLAIN ANALYZE
SELECT CAST(DATE(associationtime) AS text) AS date ,
       cast(SUM(extract(epoch
                        FROM disassociationtime) - extract(epoch
                                                           FROM associationtime)) AS bigint) AS sessionduration,
       cast(SUM(tx) AS bigint)AS tx,
       cast(SUM(rx) AS bigint) AS rx,
       cast(SUM(dataRetries) AS bigint) AS DATA,
       cast(SUM(rtsRetries) AS bigint) AS rts,
       count(*)
FROM SESSION
WHERE ssid_id=42
  AND ap_id=1731
  AND DATE(associationtime)>=DATE('Tue Nov 04 00:00:00 MSK 2014')
  AND DATE(associationtime)<=DATE('Thu Nov 20 00:00:00 MSK 2014')
GROUP BY(DATE(associationtime))
ORDER BY DATE(associationtime);

Результат:

 GroupAggregate  (cost=0.44..17710.66 rows=1 width=32) (actual time=4.501..78.880 rows=17 loops=1)
   ->  Index Scan using session_lim_values_idx on session  (cost=0.44..17538.94 rows=6868 width=32) (actual time=0.074..73.266 rows=7869 loops=1)
         Index Cond: ((date(associationtime) >= '2014-11-04'::date) AND (date(associationtime) <= '2014-11-20'::date))
         Filter: ((ssid_id = 42) AND (ap_id = 1731))
         Rows Removed by Filter: 297425
 Total runtime: 78.932 ms

Посмотрите на эту строку:

Index Scan using session_lim_values_idx

Как видите, запрос использует для сканирования три поля: ssid_id, ap_id и время ассоциации. У меня есть индекс для этого:

ssid_pkey                  | btree | {id}
ap_pkey                    | btree | {id}
testingshit_pkey           | btree | {one,two,three}
session_date_ssid_idx      | btree | {ssid_id,date(associationtime),"date_trunc('hour'::text, associationtime)"}
session_pkey               | btree | {associationtime,disassociationtime,sessionduration,clientip,clientmac,devicename,tx,rx,protocol,snr,rssi,dataretries,rtsretries }
session_main_idx           | btree | {ssid_id,ap_id,associationtime,disassociationtime,sessionduration,clientip,clientmac,devicename,tx,rx,protocol,snr,rssi,dataretres,rtsretries}
session_date_idx           | btree | {date(associationtime),"date_trunc('hour'::text, associationtime)"}
session_date_apid_idx      | btree | {ap_id,date(associationtime),"date_trunc('hour'::text, associationtime)"}
session_date_ssid_apid_idx | btree | {ssid_id,ap_id,date(associationtime),"date_trunc('hour'::text, associationtime)"}
ap_apname_idx              | btree | {apname}
users_pkey                 | btree | {username}
user_roles_pkey            | btree | {user_role_id}
session_lim_values_idx     | btree | {date(associationtime)}

Он называется session_date_ssid_apid_idx. Но почему запрос использует неправильный индекс?

session_date_ssid_apid_idx:

------------+-----------------------------+-------------------------------------------
 ssid_id    | integer                     | ssid_id
 ap_id      | integer                     | ap_id
 date       | date                        | date(associationtime)
 date_trunc | timestamp without time zone | date_trunc('hour'::text, associationtime)

session_lim_values_idx:

date    | date | date(associationtime)

Какой индекс вы бы создали?

UPD: \d session

 --------------------+-----------------------------+------------------------------------------------------
 id                 | integer                     | NOT NULL DEFAULT nextval('session_id_seq'::regclass)
 ssid_id            | integer                     | NOT NULL
 ap_id              | integer                     | NOT NULL
 associationtime    | timestamp without time zone | NOT NULL
 disassociationtime | timestamp without time zone | NOT NULL
 sessionduration    | character varying(100)      | NOT NULL
 clientip           | character varying(100)      | NOT NULL
 clientmac          | character varying(100)      | NOT NULL
 devicename         | character varying(100)      | NOT NULL
 tx                 | integer                     | NOT NULL
 rx                 | integer                     | NOT NULL
 protocol           | character varying(100)      | NOT NULL
 snr                | integer                     | NOT NULL
 rssi               | integer                     | NOT NULL
 dataretries        | integer                     | NOT NULL
 rtsretries         | integer                     | NOT NULL
╚эфхъё√:
    "session_pkey" PRIMARY KEY, btree (associationtime, disassociationtime, sessionduration, clientip, clientmac, devicename, tx, rx, protocol, snr, rssi, dataretries, rtsretries)
    "session_date_ap_ssid_idx" btree (ssid_id, ap_id, associationtime)
    "session_date_apid_idx" btree (ap_id, date(associationtime), date_trunc('hour'::text, associationtime))
    "session_date_idx" btree (date(associationtime), date_trunc('hour'::text, associationtime))
    "session_date_ssid_apid_idx" btree (ssid_id, ap_id, associationtime)
    "session_date_ssid_idx" btree (ssid_id, date(associationtime), date_trunc('hour'::text, associationtime))
    "session_lim_values_idx" btree (date(associationtime))
    "session_main_idx" btree (ssid_id, ap_id, associationtime, disassociationtime, sessionduration, clientip, clientmac, devicename, tx, rx, protocol, snr, rssi, dataretries, rtsretries)

person Tony    schedule 20.11.2014    source источник
comment
Это действительно полный вывод плана выполнения? Я бы ожидал, по крайней мере, еще одного шага, который выполняет поиск других столбцов. Кстати: вы можете удалить один из индексов: ssid_pkey или ap_pkey они идентичны. Также лучше показать список индексов из вывода команды psql \d, а не (как-то запутанно) содержимое системного каталога (или, по крайней мере, использовать представление pg_indexes)   -  person a_horse_with_no_name    schedule 20.11.2014
comment
Из того, что я вижу до сих пор, индекс session_date_ssid_apid_idx должен привыкнуть. Либо в вашем вопросе чего-то не хватает, либо с вашей БД что-то не так. Я бы удалил этот индекс (или все из них), запустил VACUUM FULL ANALYZE session, воссоздал индекс (или все из них) и попробовал еще раз. Или используйте pg_repack, если вы не можете позволить себе заблокировать таблицу. Или большинство ваших столбцов имеют ssid_id=42 AND ap_id=1731, так что эти предикаты несущественны для выбора индекса и дешевле использовать меньший индекс и фильтровать остальные.   -  person Erwin Brandstetter    schedule 20.11.2014
comment
@ErwinBrandstetter, кажется, ты прав насчет ssid_id=42 AND ap_id=1731. Если я изменю эти значения на менее популярные, будет выбран новый индекс (правый индекс).   -  person Tony    schedule 20.11.2014
comment
Что вы получаете за SELECT count(*) AS a, count(ssid_id=42 AND ap_id=1731 OR NULL) AS b FROM session?   -  person Erwin Brandstetter    schedule 20.11.2014
comment
А для SELECT count(associationtime BETWEEN '2014-11-04 0:0' AND '2014-11-20 0:0' OR NULL) AS a, count(associationtime BETWEEN '2014-11-04 0:0' AND '2014-11-20 0:0' AND ssid_id=42 AND ap_id=1731 OR NULL) AS b FROM session?   -  person Erwin Brandstetter    schedule 20.11.2014
comment
Еще два: для SHOW default_statistics_target; и SELECT attname, attstattarget FROM pg_attribute WHERE attrelid = 'session'::regclass AND attnum > 0; Лучше всего указать данные в своем вопросе.   -  person Erwin Brandstetter    schedule 20.11.2014
comment
Первый запрос: 100. Второй в теме   -  person Tony    schedule 20.11.2014
comment
Хорошо, я работаю над ответом. Можете ли вы удалить результаты из запроса статистики (это ожидаемые значения по умолчанию) и вместо этого добавить определение таблицы (что вы получите с \d session в psql)?   -  person Erwin Brandstetter    schedule 20.11.2014


Ответы (1)


Очень распространенные значения в предикатах для ssid_id и ap_id могут удешевить для Postgres выбор меньшего индекса session_lim_values_idx (только 1 столбец date) вместо, казалось бы, более подходящего, но большего индекса session_date_ssid_apid_idx (4 столбца) и отфильтровать остальные.

В вашем случае около 4% строк имеют ssid_id=42 AND ap_id=1731. Обычно это не должно гарантировать переход на меньший индекс. Но есть несколько других факторов, которые могут изменить чашу весов, в основном настройки стоимости и статистика. Подробности:

Что делать?

  • Настройте параметры стоимости, если вы еще этого не сделали, как указано в ссылка на ответ выше.

  • Увеличьте цель статистики для задействованных столбцов ssid_id, ap_id и запустите ANALYZE:

    Здесь есть один особый фактор: Postgres собирает отдельную статистику для выражений в индексах. Проверить с:

    SELECT * FROM pg_statistic
    WHERE starelid = 'session_date_ssid_apid_idx'::regclass;
    

    Вы найдете специальную строку для выражения date(associationtime). Подробнее:

  • Сделайте индекс session_date_ssid_apid_idx более привлекательным (меньше), удалив 4-й столбец "date_trunc('hour'::text, associationtime). Глядя на добавленное позже определение таблицы, вы уже сделали это.

  • Я бы предпочел использовать стандартный синтаксис для приведений: cast(associationtime AS date) вместо синтаксиса функции date(associationtime). Не говорю, что это вообще имеет значение, я просто знаю стандартный способ правильной работы. Вы можете использовать сокращенный синтаксис associationtime::date в своих запросах, который совместим с индексом выражения, но использовать подробную форму в определении индекса.

Кроме того, проверьте с помощью EXPLAIN ANALYZE, какой план запроса на самом деле быстрее, удалив или заново создав только тот индекс, который вы хотите протестировать. Тогда вы увидите, действительно ли Postgres выбрал лучший план.

У вас довольно много индексов, я бы проверил, все ли они действительно используются, и избавился от остальных. Индексы требуют затрат на обслуживание, и, как правило, лучше сосредоточиться на меньшем количестве индексов, если это возможно (более легко помещается в кеш и может уже кешироваться при необходимости). Взвесьте затраты и выгоды.

В стороне

я бы использовал:

SUM(extract(epoch FROM disassociationtime
                     - associationtime)::int) AS sessionduration
person Erwin Brandstetter    schedule 20.11.2014
comment
Разочаровывает то, что база данных в 2020 году выбирает совершенно неправильные индексы и плохо планирует... После всего этого мы еще раз подумали о MySQL, но нуждаемся в геопространственной поддержке. Я установил случайную стоимость страницы на 1,2, но он выбирает неправильный индекс для простого запроса, в зависимости от того, как долго xyz_id в списке полей. Чем более длинные значения заставляют postgres выбирать правильный индекс, которого не должно быть. - person Kevin Parker; 13.03.2020
comment
Нам пришлось отключить систему сортировки в целом, чтобы она могла выбрать правильный индекс, в противном случае она выбирает индекс одного столбца и решает сортировать его, что требует огромных затрат времени и средств. Сколько бы мы ни вакуумировали анализ полный и даже не ставили default_statistics_target=10000 (якобы самая точная настройка) не работает. PG11. - person Kevin Parker; 13.03.2020