Как оптимизировать выбор из нескольких таблиц с миллионами строк - PullRequest
7 голосов
/ 17 ноября 2010

Имеются следующие таблицы (Oracle 10g):

catalog (
  id NUMBER PRIMARY KEY,
  name VARCHAR2(255),
  owner NUMBER,
  root NUMBER REFERENCES catalog(id)
  ...
)
university (
  id NUMBER PRIMARY KEY,
  ...
)
securitygroup (
  id NUMBER PRIMARY KEY
  ...
)
catalog_securitygroup (
  catalog REFERENCES catalog(id),
  securitygroup REFERENCES securitygroup(id)
)
catalog_university (
  catalog REFERENCES catalog(id),
  university REFERENCES university(id)
)

Каталог: 500 000 строк, каталог-университет: 500 000, каталог-группа безопасности: 1 500 000.

Мне нужно выбрать любые 50 строк из каталога с указанным корнем, упорядоченным по имени для текущего университета и текущей группы безопасности. Есть запрос:

SELECT ccc.* FROM (
  SELECT cc.*, ROWNUM AS n FROM (
      SELECT c.id, c.name, c.owner
        FROM catalog c, catalog_securitygroup cs, catalog_university cu
        WHERE c.root = 100
          AND cs.catalog = c.id
          AND cs.securitygroup = 200
          AND cu.catalog = c.id
          AND cu.university = 300
        ORDER BY name
    ) cc 
) ccc WHERE ccc.n > 0 AND ccc.n <= 50;

Где 100 - какой-то каталог, 200 - какая-то группа безопасности, 300 - какой-то университет. Этот запрос возвращает 50 строк из ~ 170 000 за 3 минуты.

Но следующий запрос вернет эти строки через 2 секунды:

SELECT ccc.* FROM (
  SELECT cc.*, ROWNUM AS n FROM (
      SELECT c.id, c.name, c.owner
        FROM catalog c
        WHERE c.root = 100
        ORDER BY name
    ) cc 
) ccc WHERE ccc.n > 0 AND ccc.n <= 50;

Я строю следующие индексы: (catalog.id, catalog.name, catalog.owner), (catalog_securitygroup.catalog, catalog_securitygroup.index), (catalog_university.catalog, catalog_university.university).

Планирование первого запроса (с использованием PLSQL Developer):

http://habreffect.ru/66c/f25faa5f8/plan2.jpg

План для второго запроса:

http://habreffect.ru/f91/86e780cc7/plan1.jpg

Как оптимизировать мой запрос?

Ответы [ 5 ]

3 голосов
/ 17 ноября 2010

Индексы, которые могут быть полезны и должны рассматриваться как имеющие отношение к

WHERE c.root = 100
      AND cs.catalog = c.id
      AND cs.securitygroup = 200
      AND cu.catalog = c.id
      AND cu.university = 300

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

c: id, root   
cs: catalog, securitygroup   
cu: catalog, university

Итак, попробуйте создать

(catalog_securitygroup.catalog, catalog_securitygroup.securitygroup)

и

(catalog_university.catalog, catalog_university.university)

РЕДАКТИРОВАТЬ: Я пропустил ORDER BY - эти поля также следует учитывать, поэтому

(catalog.name, catalog.id)

может быть полезным (или некоторый другой составной индекс, который можно использовать для сортировки и условий - возможно (catalog.root, catalog.name, catalog.id))

EDIT2 Хотядругой вопрос принят, я дам еще немного пищи для размышлений.Я создал несколько тестовых данных и провел несколько тестов.

Тестовые случаи минимальны с точки зрения ширины записи (в catalog_securitygroup и catalog_university первичными ключами являются (каталог, группа безопасности) и (каталог, университет)).Вот количество записей в таблице:

test=# SELECT (SELECT COUNT(*) FROM catalog), (SELECT COUNT(*) FROM catalog_securitygroup), (SELECT COUNT(*) FROM catalog_university);
 ?column? | ?column? | ?column? 
----------+----------+----------
   500000 |  1497501 |   500000
(1 row)

База данных postgres 8.4, установка Ubuntu по умолчанию, аппаратное обеспечение i5, 4GRAM

Сначала я переписал запрос на

SELECT c.id, c.name, c.owner
FROM catalog c, catalog_securitygroup cs, catalog_university cu
WHERE c.root < 50 
  AND cs.catalog = c.id 
  AND cu.catalog = c.id
  AND cs.securitygroup < 200
  AND cu.university < 200
ORDER BY c.name
LIMIT 50 OFFSET 100

примечание: условия преобразуются в меньшее, чем для поддержания сопоставимого количества промежуточных строк (приведенный выше запрос вернул бы 198 801 строк без предложения LIMIT)

При выполнении, как указано выше, без каких-либо дополнительных индексов (за исключением PKи внешние ключи) он запускается в 556 мс в холодной базе данных (это фактически указывает на то, что я как-то упростил выборочные данные - я был бы счастлив, если бы у меня здесь было 2-4 с, не прибегая к меньшему количеству операторов)

Это подводит меня к моей точке зрения - любой прямой запрос, который только объединяет и фильтрует (определенное количество таблиц) и возвращает только определенное количество записей, должен выполняться под единицей в любой приличной базе данных без необходимости использовать курсоры илиденормализовать данные (на днях мне придется написать сообщение об этом).

Кроме того, если запросвозвращает только 50 строк и выполняет простые объединения на равные и ограничительные условия равенства, он должен работать даже намного быстрее.

Теперь давайте посмотрим, добавлю ли я некоторые индексы, наибольший потенциал в таких запросах, как правило, это порядок сортировки, поэтомуя попробую это:

CREATE INDEX test1 ON catalog (name, id);

Это делает время выполнения запроса - 22 мс в холодной базе данных.

И это точка -если вы пытаетесь получить только страницу данных, вы должны получить только страницу данных и время выполнения запросов, например, для нормализованных данных с правильными индексами должно занять меньше100 мс на приличном оборудовании.

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

Итак, вывод

  • на вашем месте я бы не прекратил подстройку индексов (и SQL), пока не получуВыполнение запроса должно быть меньше 200 мс, как правило.
  • , только если я найду объективное объяснение, почему он не может опуститься ниже такого значения, я прибегну к денормализации и / или курсорам и т. д..
2 голосов
/ 17 ноября 2010

Сначала я предполагаю, что ваши таблицы University и SecurityGroup довольно малы.Вы указали размер больших таблиц, но на самом деле это проблема других размеров

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

Если у вас всегда есть current_univ и current_secgrp иroot в качестве входных данных вы хотите использовать их для фильтрации как можно скорее.Единственный способ сделать это - немного изменить свою схему.На самом деле, если нужно, вы можете оставить существующие таблицы на месте, но с этим предложением вы добавите пространство.

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

Univ_id, SecGrp_ID, Root, catalog_id.Сделайте его организованной в индекс таблицей первых трех столбцов как pk.

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

0 голосов
/ 17 ноября 2010

Использование rownum является неправильным и приводит к обработке всех строк. Он обработает все строки, назначит им все номера строк, а затем найдет их в диапазоне от 0 до 50. Если вы хотите найти в плане объяснения значение COUNT STOPKEY, а не просто считать

Запрос ниже должен быть улучшен, так как он получит только первые 50 строк ... но все еще есть проблема с объединениями:

SELECT ccc.* FROM (
  SELECT cc.*, ROWNUM AS n FROM (
      SELECT c.id, c.name, c.owner
        FROM catalog c
        WHERE c.root = 100
        ORDER BY name
    ) cc 
    where rownum <= 50
) ccc WHERE ccc.n > 0 AND ccc.n <= 50;

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

0 голосов
/ 17 ноября 2010

Оптимизатор на основе затрат Oracle использует всю информацию, необходимую для определения наилучших путей доступа к данным и наименее затратных методов получения этих данных. Ниже приведены некоторые случайные моменты, связанные с вашим вопросом.

Первые три таблицы, которые вы перечислили, имеют первичные ключи. Есть ли в других таблицах (catalog_university и catalog_securitygroup) первичные ключи? Первичный ключ определяет столбец или набор столбцов, которые являются ненулевыми и уникальными и очень важны в реляционной базе данных.

Oracle обычно применяет первичный ключ, генерируя уникальный индекс для указанных столбцов. Оптимизатор Oracle с большей вероятностью использует уникальный индекс, если он доступен, поскольку он более избирателен.

Если возможно, индекс, содержащий уникальные значения, должен быть определен как уникальный (CREATE UNIQUE INDEX...), и это даст оптимизатору больше информации.

Предоставленные вами дополнительные индексы не более избирательны, чем существующие индексы. Например, индекс для (catalog.id, catalog.name, catalog.owner) уникален, но менее полезен, чем существующий индекс первичного ключа для (catalog.id). Если запрос написан для выбора в столбце catalog.name, это можно сделать и индексировать, пропустить сканирование, но это становится дорогостоящим (и в этом случае большинство даже невозможно).

Поскольку вы пытаетесь выбрать на основе столбца catalog.root, возможно, стоит добавить индекс для этого столбца. Это будет означать, что он может быстро найти соответствующие строки из таблицы каталога. Время для второго запроса может быть немного вводящим в заблуждение. Чтобы найти 50 подходящих строк из каталога, может потребоваться 2 секунды, но это могут быть первые 50 строк в таблице каталога ... поиск 50, соответствующих всем вашим условиям, может занять больше времени, и не только потому, что вам нужно присоединиться к другим столам, чтобы получить их. Я бы всегда использовал create table as select, не ограничивая rownum, когда пытался настроить производительность. Со сложным запросом я бы обычно заботился о том, сколько времени потребуется, чтобы вернуть все строки обратно ... и простой выбор с rownum может ввести в заблуждение

Все, что касается настройки производительности Oracle, заключается в предоставлении оптимизатору достаточного количества информации и правильных инструментов (индексов, ограничений и т. Д.) Для правильной работы. По этой причине важно получать статистику оптимизатора, используя что-то вроде DBMS_STATS.GATHER_TABLE_STATS(). Индексы должны автоматически собирать статистику в Oracle 10g или новее.

Каким-то образом это вылилось в довольно длинный ответ об оптимизаторе Oracle. Надеюсь, что-то из этого ответит на ваш вопрос. Вот краткое изложение того, что сказано выше:

  • Предоставьте оптимизатору как можно больше информации, например, если индекс уникален, тогда объявите его как таковой.
  • Добавление индексов на пути доступа
  • Найти правильное время для запросов без ограничения rowwnum. Первые 50 M & Ms всегда будут быстрее найти в банке, чем первые 50 красных M & Ms
  • Сбор статистики оптимизатора
  • Добавить уникальные / первичные ключи во все таблицы, где они существуют.
0 голосов
/ 17 ноября 2010

попробуйте объявить курсор.Я не знаю, оракул, но в SqlServer будет выглядеть так:

declare @result 
table ( 
    id numeric,
    name varchar(255)
); 

declare __dyn_select_cursor cursor LOCAL SCROLL DYNAMIC for 

--Select
select distinct 
    c.id, c.name
From [catalog] c
    inner join university u
    on     u.catalog = c.id
       and u.university = 300
    inner join catalog_securitygroup s
    on     s.catalog = c.id
       and s.securitygroup = 200
Where
    c.root = 100
Order by name   

--Cursor
declare @id numeric;
declare @name varchar(255);

open __dyn_select_cursor; 

fetch relative 1 from __dyn_select_cursor into @id,@name declare @maxrowscount int 

set @maxrowscount = 50

while (@@fetch_status = 0 and @maxrowscount <> 0) 
begin 
     insert into @result values (@id, @name);
     set @maxrowscount = @maxrowscount - 1;
     fetch next from __dyn_select_cursor into  @id, @name; 
end 
close __dyn_select_cursor; 
deallocate __dyn_select_cursor; 


--Select temp, final result
select 
 id, 
 name
from @result; 
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...