Статьи

Добавленный вами индекс бесполезен. Зачем?

Недавно в офисе:

Боб: Я посмотрел на этот медленный вопрос, о котором ты мне говорил вчера, Алиса. Я добавил индексы, которые вы хотели. Теперь все должно быть хорошо

Алиса: Спасибо, Боб. Я быстро проверю… Нет, Боб, все еще медленно, похоже, не сработало

Боб: Ты прав, Алиса! Похоже, что Oracle не получает индекс для вашего запроса, даже если я добавлю /*+INDEX(...)*/подсказку. Я не знаю, что пошло не так !?

И так, история продолжается. Алиса расстроена, потому что ее функция не работает вовремя, Боб расстроен, потому что он думает, что Oracle не работает правильно.

Правдивая история!

Боб забыл про Oracle и NULL

Бедный Боб забыл (или не знал), что Oracle не помещает значения NULL в «обычные» индексы . Подумайте об этом таким образом:

CREATE TABLE person (
  id            NUMBER(38)   NOT NULL PRIMARY KEY,
  first_name    VARCHAR2(50) NOT NULL,
  last_name     VARCHAR2(50) NOT NULL,
  date_of_birth DATE             NULL
);
 
CREATE INDEX i_person_dob ON person(date_of_birth);

Теперь Боб считает, что его индекс решает все проблемы, потому что он проверил, работает ли индекс, используя следующий запрос:

SELECT *
FROM   person
WHERE  date_of_birth > DATE '1980-01-01';

(конечно, вы вообще не должны SELECT *)

И план выполнения выглядел хорошо:

----------------------------------------------------
| Id  | Operation                   | Name         |
----------------------------------------------------
|   0 | SELECT STATEMENT            |              |
|   1 |  TABLE ACCESS BY INDEX ROWID| PERSON       |
|*  2 |   INDEX RANGE SCAN          | I_PERSON_DOB |
----------------------------------------------------

Это потому, что предикат Боба не полагается на то, NULLчтобы быть частью I_PERSON_DOBиндекса. К сожалению, запрос Алисы выглядел больше так (упрощенная версия):

SELECT 1
FROM   dual
WHERE  DATE '1980-01-01' NOT IN (
  SELECT date_of_birth FROM person
);

Таким образом, по сути, запрос Алисы проверил, была ли у кого-либо дата рождения на данную дату. Ее план выполнения выглядел так:

-------------------------------------
| Id  | Operation          | Name   |
-------------------------------------
|   0 | SELECT STATEMENT   |        |
|*  1 |  FILTER            |        |
|   2 |   FAST DUAL        |        |
|*  3 |   TABLE ACCESS FULL| PERSON |
-------------------------------------

Как видите, ее запрос сделал TABLE ACCESS FULLоперацию, минуя индекс. Зачем? Это просто:

Даже если наше DATE '1980-01-01'значение находится в индексе или нет, нам все равно придется проверить всю таблицу, чтобы увидеть, NULLсодержится ли в date_of_birthстолбце одно значение . Потому что, если былоNULL значение, предикат в запросе Алисы никогда не дают или , но .NOT INTRUEFALSENULL

Алиса может решить эту проблему с NOT EXISTS

Алиса может решить легко себя, заменив NOT INчерез NOT EXISTS, предикат , который не страдает от своеобразной трехзначной булевой логики в SQL .

SELECT 1
FROM   dual
WHERE  NOT EXISTS (
  SELECT 1
  FROM   person
  WHERE  date_of_birth = DATE '1980-01-01'
);

Этот новый запрос теперь снова дает оптимальный план:

>------------------------------------------
| Id  | Operation         | Name         |
------------------------------------------
|   0 | SELECT STATEMENT  |              |
|*  1 |  FILTER           |              |
|   2 |   FAST DUAL       |              |
|*  3 |   INDEX RANGE SCAN| I_PERSON_DOB |
------------------------------------------

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

Боб должен просто установить для столбца NOT NULL

Лучшее решение, однако, это просто установить столбец в NOT NULL:

ALTER TABLE person
MODIFY date_of_birth DATE NOT NULL;

С этим ограничением NOT INзапрос в точности эквивалентен NOT EXISTSзапросу, и Боб и Алиса снова могут быть друзьями.

Вывод: как найти «плохие» столбцы?

Это просто. В следующем полезном запросе перечислены все индексы, в которых есть хотя бы один обнуляемый столбец.

SELECT
  i.table_name,
  i.index_name,
  LISTAGG(
    LPAD(i.column_position,  2) || ': ' ||
    RPAD(i.column_name    , 30) || ' '  ||
    DECODE(t.nullable, 'Y', '(NULL)', '(NOT NULL)'),
    ', '
  ) WITHIN GROUP (ORDER BY i.column_position)
    AS "NULLABLE columns in indexes"
FROM user_ind_columns i
JOIN user_tab_cols t
ON (t.table_name, t.column_name) =
  ((i.table_name, i.column_name))
WHERE EXISTS (
  SELECT 1
  FROM user_tab_cols t
  WHERE (t.table_name, t.column_name, t.nullable) =
       ((i.table_name, i.column_name, 'Y'       ))
)
GROUP BY i.table_name, i.index_name
ORDER BY i.index_name ASC;

При запуске против схемы Боба и Алисы приведенный выше запрос дает:

TABLE_NAME | INDEX_NAME   | NULLABLE columns in indexes
-----------+--------------+----------------------------
PERSON     | I_PERSON_DOB | 1: DATE_OF_BIRTH (NULL)

Используйте этот запрос в своей собственной схеме сейчас и просмотрите результаты, тщательно оценивая, действительно ли вам нужно сохранять этот столбец обнуляемым. В 50% случаев вы этого не делаете. Добавив NOT NULLограничение, вы можете значительно ускорить ваше приложение!