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

Моя таблица lead имеет индекс:

\d lead
...
Indexes:
    "lead_pkey" PRIMARY KEY, btree (id)
    "lead_account__c" btree (account__c)
    ...
    "lead_email" btree (email)
    "lead_id_prefix" btree (id text_pattern_ops)

Почему PG (9.1) не использует индекс для этого простого выбора равенства? Электронные письма почти все уникальны ....

db=> explain select * from lead where email = 'blah';
                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on lead  (cost=0.00..319599.38 rows=1 width=5108)
   Filter: (email = 'blah'::text)
(2 rows)

Другие запросы, попадающие в индекс, кажутся нормальными (хотя я не знаю, почему этот не просто использует индекс pkey):

db=> explain select * from lead where id = '';
                                  QUERY PLAN
------------------------------------------------------------------------------
 Index Scan using lead_id_prefix on lead  (cost=0.00..8.57 rows=1 width=5108)
   Index Cond: (id = ''::text)
(2 rows)

db=> explain select * from lead where account__c = '';
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Index Scan using lead_account__c on lead  (cost=0.00..201.05 rows=49 width=5108)
   Index Cond: (account__c = ''::text)
(2 rows)

Сначала я подумал, что это может быть из-за недостаточного количества различных значений email. Например, если в статистике указано, что email равно blah для большей части таблицы, то последовательное сканирование будет быстрее. Но это не так:

db=> select count(*), count(distinct email) from lead;
 count  | count
--------+--------
 749148 | 733416
(1 row)

Даже если я принудительно отключу сканирование seq, планировщик будет вести себя так, как будто у него нет другого выбора:

db=> set enable_seqscan = off;
SET
db=> show enable_seqscan;
 enable_seqscan
----------------
 off
(1 row)

db=> explain select * from lead where email = '[email protected]';
                            QUERY PLAN
---------------------------------------------------------------------------
 Seq Scan on lead  (cost=10000000000.00..10000319599.38 rows=1 width=5108)
   Filter: (email = '[email protected]'::text)
(2 rows)

Также пробовал EXPLAIN ANALYZE:

db=> explain analyze select * from lead where email = '[email protected]';
                                                         QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------
 Seq Scan on lead  (cost=10000000000.00..10000319732.76 rows=1 width=5102) (actual time=77845.244..77845.244 rows=0 loops=1)
   Filter: (email = '[email protected]'::text)
 Total runtime: 77857.215 ms
(3 rows)

Вот результат \d (извините, нужно скрыть имена столбцов и обрезать, чтобы соответствовать ограничениям SO; см. Необрезанную версию на http://pastebin.com/ve3gzJpY):

                                 Table "lead"
                   Column                   |            Type             | Modifiers 
--------------------------------------------+-----------------------------+-----------
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | real                        | 
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | text                        | 
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | boolean                     | 
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | text                        | 
 ...
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | text                        | 
 email                                      | text                        | 
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | boolean                     | 
 ...
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | text                        | 
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | text                        | 
 account__c                                 | text                        | 
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | text                        | 
 ...
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | text                        | 
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | text                        | 
 id                                         | text                        | not null
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | real                        | 
 ...
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | timestamp without time zone | 
 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | real                        | 
Indexes:
    "lead_pkey" PRIMARY KEY, btree (id)
    "lead_account__c" btree (account__c)
    "lead_XXXXXXXXXXXXXXXXXXXXXX" btree (XXXXXXXXXXXXXXXXXXXXXX)
    "lead_XXXXXXXXXXXXXXXXXXXXXX" btree (XXXXXXXXXXXXXXXXXXXXXX)
    "lead_XXXXXXXXXXXXXXXXXXXXXX" btree (XXXXXXXXXXXXXXXXXXXXXX)
    "lead_email" btree (email)
    "lead_id_prefix" btree (id text_pattern_ops)

Вот pg_dump --schema-only -t lead (снова см. Несрезанные на http://pastebin.com/ve3gzJpY, с уникальными именами столбцов, а также в случае это помогает воспроизводимости):

--
-- PostgreSQL database dump
--

SET statement_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;

SET default_tablespace = '';

SET default_with_oids = false;

--
-- Name: lead; Type: TABLE; Schema: public; Owner: pod; Tablespace: 
--

CREATE TABLE lead (
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX real,
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX text,
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX boolean,
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX text,
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX text,
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX date,
    ...
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX text,
    account__c text,
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX text,
    ...
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX text,
    id text NOT NULL,
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX real,
    ...
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX timestamp without time zone,
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX real
);


ALTER TABLE lead OWNER TO pod;

--
-- Name: lead_pkey; Type: CONSTRAINT; Schema: public; Owner: pod; Tablespace: 
--

ALTER TABLE ONLY lead
    ADD CONSTRAINT lead_pkey PRIMARY KEY (id);


--
-- Name: lead_account__c; Type: INDEX; Schema: public; Owner: pod; Tablespace: 
--

CREATE INDEX lead_account__c ON lead USING btree (account__c);


--
-- Name: lead_XXXXXXXXXXXXXXXXXXXX; Type: INDEX; Schema: public; Owner: pod; Tablespace: 
--

CREATE INDEX lead_XXXXXXXXXXXXXXXXXXXX ON lead USING btree (XXXXXXXXXXXXXXXXXXXX);


--
-- Name: lead_XXXXXXXXXXXXXXXXXXXX; Type: INDEX; Schema: public; Owner: pod; Tablespace: 
--

CREATE INDEX lead_XXXXXXXXXXXXXXXXXXXX ON lead USING btree (XXXXXXXXXXXXXXXXXXXX);


--
-- Name: lead_XXXXXXXXXXXXXXXXXXXX; Type: INDEX; Schema: public; Owner: pod; Tablespace: 
--

CREATE INDEX lead_XXXXXXXXXXXXXXXXXXXX ON lead USING btree (XXXXXXXXXXXXXXXXXXXX);


--
-- Name: lead_email; Type: INDEX; Schema: public; Owner: pod; Tablespace: 
--

CREATE INDEX lead_email ON lead USING btree (email);


--
-- Name: lead_id_prefix; Type: INDEX; Schema: public; Owner: pod; Tablespace: 
--

CREATE INDEX lead_id_prefix ON lead USING btree (id text_pattern_ops);


--
-- PostgreSQL database dump complete
--

Некоторые заклинания каталога PG:

db=> select * from pg_index where indexrelid = 'lead_email'::regclass;
 indexrelid | indrelid  | indnatts | indisunique | indisprimary | indisexclusion | indimmediate | indisclustered | indisvalid | indcheckxmin | indisready | indkey | indcollation | indclass | indoption | indexprs | indpred
------------+-----------+----------+-------------+--------------+----------------+--------------+----------------+------------+--------------+------------+--------+--------------+----------+-----------+----------+---------
  215251995 | 101034456 |        1 | f           | f            | f              | t            | f              | t          | t            | t          | 101    | 100          | 10043    | 0         | ¤        | ¤
(1 row)

Некоторая информация о локали:

db=> show lc_collate;
 lc_collate  
-------------
 en_US.UTF-8
(1 row)

db=> show lc_ctype;
  lc_ctype   
-------------
 en_US.UTF-8
(1 row)

Я просмотрел большое количество прошлых вопросов SO, но ни один из них не касался простого запроса на равенство, подобного этому.


person Yang    schedule 12.04.2013    source источник
comment
Странно ... простому равенству не нужен индекс text_pattern_ops, поэтому это трудно объяснить. Можете ли вы воспроизвести это на небольшом образце? Если да, отправьте сообщение на sqlfiddle.com и сделайте ссылку здесь.   -  person Craig Ringer    schedule 12.04.2013
comment
Пожалуйста, покажите полное определение таблицы (желательно через pg_dump).   -  person Peter Eisentraut    schedule 12.04.2013
comment
@PeterEisentraut Обновил вопрос схемами \d и pg_dump.   -  person Yang    schedule 12.04.2013
comment
@CraigRinger Я изо всех сил постараюсь воспроизвести, но это может занять некоторое время - в этой таблице много данных, содержащих конфиденциальную информацию о клиентах.   -  person Yang    schedule 12.04.2013
comment
@a_horse_with_no_name Фактически отображается выбор количества - там около 750 тыс. строк   -  person Yang    schedule 12.04.2013
comment
изменение имени столбца было выполнено не очень хорошо - в итоге все столбцы были названы одинаково. Это затрудняет воспроизведение вашего сценария, потому что операторы CREATE INDEX относятся к неоднозначным именам (не говоря уже о том, что CREATE TABLE не работает из-за повторяющихся имен столбцов). Было бы намного лучше использовать разные имена для каждого столбца. Кроме того, каковы настройки lc_collate и lc_ctype? Это может быть важно для репродуктора (индекс имеет indcollate = 100, что означает сортировку по умолчанию). В любом случае, сортировка не по умолчанию будет отображаться как модификатор.   -  person alvherre    schedule 12.04.2013
comment
@alvherre А, я просто предположил (возможно, неправильно), что проблема зависит не только от схемы, поэтому я не стремился сделать что-то с возможностью копирования и вставки, а просто закрашивал имена. Я сам пытался воспроизвести это, но это было сложно, так как наш сервер БД ломается. В любом случае, добавление данных lc_collate / etc и обновление имен столбцов в pastebin.   -  person Yang    schedule 13.04.2013


Ответы (2)


Для устранения этих неполадок обязательно запускайте VACUUM ANALYZE для таблицы между действиями по устранению неполадок, чтобы увидеть, что работает. В противном случае вы можете не знать, что именно и где изменилось. Так что попробуйте это и запустите сначала снова и посмотрите, решит ли это проблему.

Следующие шаги, которые нужно выполнить (запустить анализ вакуума и контрольный пример между каждым из них):

ALTER TABLE lead ALTER COLUMN email SET STATISTICS 1000;

Может быть, это исправит. Может быть нет.

Если это не поможет, внимательно посмотрите на представление pg_stat:

SELECT * FROM pg_stat WHERE table_name = 'lead';

Пожалуйста, прочтите внимательно следующее и посмотрите, что вы видите неправильного в pg_stat;

http://www.postgresql.org/docs/9.0/static/planner-stats.html

РЕДАКТИРОВАТЬ: Чтобы быть предельно ясным, vacuum analyse - это еще не все устранение неполадок. Однако он ДОЛЖЕН запускаться между этапами устранения неполадок, потому что в противном случае вы не можете быть уверены, что планировщик принимает во внимание правильные данные.

person Chris Travers    schedule 15.05.2013
comment
Проблема исчезла так же загадочно, как и возникла, но стоит отметить, что я уже много раз пробовал запускать VACUUM ANALYZE. - person Yang; 16.05.2013
comment
Дело в том, чтобы запускать его между этапами устранения неполадок, чтобы убедиться, что анализатор использует текущие настройки и т. Д. - person Chris Travers; 16.05.2013

СОЗДАТЬ ИНДЕКС lead_id_prefix НА Лиде ИСПОЛЬЗУЯ btree (id text_pattern_ops);

Использование text_pattern_ops здесь кажется странным. Если ваш ID представляет собой какое-то целое число, я бы попробовал сбросить этот индекс в качестве теста. (Я без колебаний отказался бы от этого индекса на сервере разработки.) Поскольку у вас есть еще один индекс btree на "lead.id", я бы ожидал, что удаление этого индекса заставит оптимизатор использовать другой индекс на "lead.id".

Если это подтвердится, я постараюсь глубже изучить причины.

person Mike Sherrill 'Cat Recall'    schedule 12.04.2013
comment
Ха! Я прочитал сразу, где email = 'blah', где id = '', и id застрял у меня в голове! - person Mike Sherrill 'Cat Recall'; 13.04.2013