Получить последний дочерний элемент на одного родителя из большой таблицы - запрос слишком медленный - PullRequest
2 голосов
/ 11 ноября 2011

У меня есть запрос, сгенерированный ORM Джанго, который занимает несколько часов.

Таблица report_rank (50 миллионов строк) находится в отношении один ко многим с report_profile (100k строк). Я пытаюсь получить последние report_rank для каждого report_profile.

Я использую Postgres 9.1 на очень большом сервере Amazon EC2 с большим количеством доступной оперативной памяти (используется 2 ГБ / 15 ГБ). Дисковый ввод-вывод, конечно, очень плохой.

У меня есть индексы для report_rank.created, а также для всех полей внешнего ключа.

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

EXPLAIN 
SELECT "report_rank"."id", "report_rank"."keyword_id", "report_rank"."site_id"
     , "report_rank"."rank", "report_rank"."url", "report_rank"."competition"
     , "report_rank"."source", "report_rank"."country", "report_rank"."created"
     , MAX(T7."created") AS "max" 
FROM "report_rank" 
LEFT OUTER JOIN "report_site" 
  ON ("report_rank"."site_id" = "report_site"."id") 
INNER JOIN "report_profile" 
  ON ("report_site"."id" = "report_profile"."site_id") 
INNER JOIN "crm_client" 
  ON ("report_profile"."client_id" = "crm_client"."id") 
INNER JOIN "auth_user" 
  ON ("crm_client"."user_id" = "auth_user"."id") 
LEFT OUTER JOIN "report_rank" T7 
  ON ("report_site"."id" = T7."site_id") 
WHERE ("auth_user"."is_active" = True  AND "crm_client"."is_deleted" = False ) 
GROUP BY "report_rank"."id", "report_rank"."keyword_id", "report_rank"."site_id"
     , "report_rank"."rank", "report_rank"."url", "report_rank"."competition"
     , "report_rank"."source", "report_rank"."country", "report_rank"."created" 
HAVING MAX(T7."created") =  "report_rank"."created";

Выход EXPLAIN:

GroupAggregate  (cost=1136244292.46..1276589375.47 rows=48133327 width=72)
  Filter: (max(t7.created) = report_rank.created)
  ->  Sort  (cost=1136244292.46..1147889577.16 rows=4658113881 width=72)
        Sort Key: report_rank.id, report_rank.keyword_id, report_rank.site_id, report_rank.rank, report_rank.url, report_rank.competition, report_rank.source, report_rank.country, report_rank.created
        ->  Hash Join  (cost=1323766.36..6107863.59 rows=4658113881 width=72)
              Hash Cond: (report_rank.site_id = report_site.id)
              ->  Seq Scan on report_rank  (cost=0.00..1076119.27 rows=48133327 width=64)
              ->  Hash  (cost=1312601.51..1312601.51 rows=893188 width=16)
                    ->  Hash Right Join  (cost=47050.38..1312601.51 rows=893188 width=16)
                          Hash Cond: (t7.site_id = report_site.id)
                          ->  Seq Scan on report_rank t7  (cost=0.00..1076119.27 rows=48133327 width=12)
                          ->  Hash  (cost=46692.28..46692.28 rows=28648 width=8)
                                ->  Nested Loop  (cost=2201.98..46692.28 rows=28648 width=8)
                                      ->  Hash Join  (cost=2201.98..5733.23 rows=28648 width=4)
                                            Hash Cond: (crm_client.user_id = auth_user.id)
                                            ->  Hash Join  (cost=2040.73..5006.71 rows=44606 width=8)
                                                  Hash Cond: (report_profile.client_id = crm_client.id)
                                                  ->  Seq Scan on report_profile  (cost=0.00..1706.09 rows=93009 width=8)
                                                  ->  Hash  (cost=1761.98..1761.98 rows=22300 width=8)
                                                        ->  Seq Scan on crm_client  (cost=0.00..1761.98 rows=22300 width=8)
                                                              Filter: (NOT is_deleted)
                                            ->  Hash  (cost=126.85..126.85 rows=2752 width=4)
                                                  ->  Seq Scan on auth_user  (cost=0.00..126.85 rows=2752 width=4)
                                                        Filter: is_active
                                      ->  Index Scan using report_site_pkey on report_site  (cost=0.00..1.42 rows=1 width=4)
                                            Index Cond: (id = report_profile.site_id)

Ответы [ 3 ]

7 голосов
/ 11 ноября 2011

Основной момент, скорее всего, что вы JOIN и GROUP за все, чтобы получить max(created).Получите это значение отдельно.

Вы упомянули все необходимые здесь индексы: на report_rank.created и на внешних ключах.Там у тебя все хорошо.(Если вас интересует лучше, чем "хорошо", продолжайте читать !)

LEFT JOIN report_site будет принудительно переведен в простой JOIN по предложению WHERE.Я подставил равнину JOIN.Я также значительно упростил ваш синтаксис.

Обновлен в июле 2015 года с более простыми, быстрыми запросами и более умными функциями.

Решение для нескольких строк

report_rank.created это не уникально и вы хотите все самые последние строки.
Использование оконной функции rank() в подзапросе.

SELECT r.id, r.keyword_id, r.site_id
     , r.rank, r.url, r.competition
     , r.source, r.country, r.created  -- same as "max"
FROM  (
   SELECT *, rank() OVER (ORDER BY created DESC NULLS LAST) AS rnk
   FROM   report_rank r
   WHERE  EXISTS (
      SELECT *
      FROM   report_site    s
      JOIN   report_profile p ON p.site_id = s.id
      JOIN   crm_client     c ON c.id      = p.client_id
      JOIN   auth_user      u ON u.id      = c.user_id
      WHERE  s.id = r.site_id
      AND    u.is_active
      AND    c.is_deleted = FALSE
      )
   ) sub
WHERE  rnk = 1;

Почему DESC NULLS LAST?

Решение для одной строки

Если report_rank.created является уникальным или вас устраивает любой 1 ряд с max(created):

SELECT id, keyword_id, site_id
     , rank, url, competition
     , source, country, created  -- same as "max"
FROM   report_rank r
WHERE  EXISTS (
    SELECT 1
    FROM   report_site    s
    JOIN   report_profile p ON p.site_id = s.id
    JOIN   crm_client     c ON c.id      = p.client_id
    JOIN   auth_user      u ON u.id      = c.user_id
    WHERE  s.id = r.site_id
    AND    u.is_active
    AND    c.is_deleted = FALSE
   )
-- AND  r.created > f_report_rank_cap()
ORDER  BY r.created DESC NULLS LAST
LIMIT  1;

Должно быть быстрее,еще.Дополнительные параметры:

Максимальная скорость с динамически настраиваемым частичным индексом

Возможно, вы заметили прокомментированную часть в последнем запросе:

AND  r.created > f_report_rank_cap()

Вы упомянули 50 млн.строк, это много.Вот способ ускорить процесс:

  • Создайте простую IMMUTABLE функцию, возвращающую метку времени, которая гарантированно будет старше строк, представляющих интерес, и при этом будет максимально молодой.
  • Создать частичный индекс только для младших строк - на основе этой функции.
  • Использовать условие WHERE в запросах, которое соответствует условию индекса.
  • Создайте еще одну функцию, которая обновляет эти объекты до последней строки с динамическим DDL.(Минус безопасное поле в случае, если самые новые строки будут удалены / деактивированы - если это может произойти)
  • Вызовите эту вторичную функцию в нерабочее время с минимумом одновременной активностиCronjob или по требованию.Как часто, как вы хотите, не можете причинить вреда, ему просто нужна короткая эксклюзивная блокировка на столе.

Вот полная рабочая демонстрация .
@erikcwвам нужно будет активировать закомментированную часть, как указано ниже.

CREATE TABLE report_rank(created timestamp);
INSERT INTO report_rank VALUES ('2011-11-11 11:11'),(now());

-- initial function
CREATE OR REPLACE FUNCTION f_report_rank_cap()
  RETURNS timestamp LANGUAGE sql COST 1 IMMUTABLE AS
$y$SELECT timestamp '-infinity'$y$;  -- or as high as you can safely bet.

-- initial index; 1st run indexes whole tbl if starting with '-infinity'
CREATE INDEX report_rank_recent_idx ON report_rank (created DESC NULLS LAST)
WHERE  created > f_report_rank_cap();

-- function to update function & reindex
CREATE OR REPLACE FUNCTION f_report_rank_set_cap()
  RETURNS void AS
$func$
DECLARE
   _secure_margin CONSTANT interval := interval '1 day';  -- adjust to your case
   _cap timestamp;  -- exclude older rows than this from partial index
BEGIN
   SELECT max(created) - _secure_margin
   FROM   report_rank
   WHERE  created > f_report_rank_cap() + _secure_margin
   /*  not needed for the demo; @erikcw needs to activate this
   AND    EXISTS (
     SELECT *
     FROM   report_site    s
     JOIN   report_profile p ON p.site_id = s.id
     JOIN   crm_client     c ON c.id      = p.client_id
     JOIN   auth_user      u ON u.id      = c.user_id
     WHERE  s.id = r.site_id
     AND    u.is_active
     AND    c.is_deleted = FALSE)
   */
   INTO   _cap;

   IF FOUND THEN
     -- recreate function
     EXECUTE format('
     CREATE OR REPLACE FUNCTION f_report_rank_cap()
       RETURNS timestamp LANGUAGE sql IMMUTABLE AS
     $y$SELECT %L::timestamp$y$', _cap);

     -- reindex
     REINDEX INDEX report_rank_recent_idx;
   END IF;
END
$func$  LANGUAGE plpgsql;

COMMENT ON FUNCTION f_report_rank_set_cap()
IS 'Dynamically recreate function f_report_rank_cap()
    and reindex partial index on report_rank.';

Звоните:

SELECT f_report_rank_set_cap();

См .:

SELECT f_report_rank_cap();

Раскомментируйте пункт AND r.created > f_report_rank_cap() в запросе выше и обратите внимание на разницу.Убедитесь, что индекс используется с EXPLAIN ANALYZE.

Руководством по параллелизму и REINDEX:

Для создания индекса без вмешательства в производство выследует удалить индекс и еще раз ввести команду CREATE INDEX CONCURRENTLY.

1 голос
/ 11 ноября 2011
-- modelled after Erwin's version
-- does the x query really return only one row?

SELECT r.id, r.keyword_id, r.site_id
    , r.rank, r.url, r.competition, r.source
    , r.country, r.created, x.max_created
-- UPDATE3: I forgot one, too
FROM report_rank r
LEFT   JOIN report_site s  ON (r.site_id = s.id) 
JOIN   report_profile   p  ON (s.id = p.site_id) 
JOIN   crm_client       c  ON (p.client_id = c.id) 
JOIN   auth_user        u  ON (c.user_id = u.id)
-- UPDATE2: t7 has left the building
WHERE  u.is_active
AND    c.is_deleted = FALSE
AND NOT EXISTS (SELECT * FROM report_rank x
       -- WHERE 1=1 -- uncorrelated subquery ??
       -- UPDATE1: no it's not. Erwin seems to have forgotten the t7 join
       WHERE r.id = x.site_id
       AND x.created > r.created
       ) 
;
0 голосов
/ 13 ноября 2011

Альтернативная интерпретация

Я был занят оптимизацией представленного вами запроса и пропустил фрагмент того, что вы написали:

Я пытаюсь получить последний report_rank для каждого report_profile.

Что полностью отличается от того, что пытается сделать ваш запрос.

Сначала , позвольте мне продемонстрировать, как я искалзапрос из того, что вы опубликовали.
Я удалил "" и шумовые слова, использовал псевдонимы и урезал формат, придя к следующему:

SELECT r.id, r.keyword_id, r.site_id, r.rank, r.url, r.competition
      ,r.source, r.country, r.created
      ,MAX(t7.created) AS max 
FROM   report_rank      r
LEFT   JOIN report_site s  ON (s.id      = r.site_id) 
JOIN   report_profile   p  ON (p.site_id = s.id) 
JOIN   crm_client       c  ON (c.id      = p.client_id) 
JOIN   auth_user        u  ON (u.id      = c.user_id) 
LEFT   JOIN report_rank t7 ON (t.site_id = s.id) 
WHERE  u.is_active
AND    c.is_deleted = False
GROUP  BY
       r.id
      ,r.keyword_id
      ,r.site_id
      ,r.rank
      ,r.url, r.competition
      ,r.source
      ,r.country
      ,r.created 
HAVING MAX(t7.created) =  r.created;
  • Что вы пытаетесь сделать с T7 и HAVING не могут работать на принципале, я обрезал это.
  • LEFT JOIN будет принудительно переведено на JOIN в обоих случаях.Я заменил соответственно.
  • Из вашего запроса я пришел к выводу, что report_site находится в отношении 1: n с report_rank и report_profile, и именно так эти два связаны.Таким образом, report_profile, принадлежащие одному и тому же report_site, имеют тот же самый последний report_rank.Вы также можете сгруппировать по report_site.Но я придерживался задаваемого вопроса.
  • Я исключил report_site из запроса.Это не имеет значения, пока оно существует , что я утверждаю.
  • Начиная с PostgreSQL 9.1 достаточно GROUP BY первичного ключа для таблицы.Я соответственно упростил.
  • Для упрощения я выбрал все столбцы report_rank

При всем этом я пришел к этому основному запросу :

SELECT r.*
FROM   report_rank    r
JOIN   report_profile p USING (site_id) 
JOIN   crm_client     c ON (c.id = p.client_id) 
JOIN   auth_user      u ON (u.id = c.user_id) 
WHERE  u.is_active
AND    c.is_deleted = FALSE
GROUP  BY r.id;

Основываясь на этом, я создал решение с ...

Последним report_rank для каждого report_profile

WITH p AS (
    SELECT p.id AS profile_id
          ,p.site_id
    FROM   report_profile p
    WHERE  EXISTS (
        SELECT *
        FROM   crm_client c
        JOIN   auth_user  u ON u.id = c.user_id
        WHERE  c.id = p.client_id
        AND    c.is_deleted = FALSE
        AND    u.is_active
        )
    ) x AS (
    SELECT p.profile_id
          ,r.*
    FROM   p
    JOIN   report_rank r USING (site_id)
    )
SELECT *
FROM   x
WHERE  NOT EXISTS (
    SELECT *
    FROM   x r
    WHERE  r.profile_id = x.profile_id
    AND    r.created > x.created
    );
  • Я предполагаю, что естьreport_profile.id хотя вы не упомянули об этом.
  • В 1-м CTE я получаю уникальный набор допустимых профилей.
  • Во 2-м CTE я соединяюсь с report_rank, чтобы получить результирующие строки
  • В последнем запросе я исключаю все, кроме самой последней report_rank за report_profile
  • Может быть одна или несколько строк, если created не уникален.
  • Решение с частичным индексом в моем другом ответе не применимо к этому варианту.

Наконец, совет по оптимизации производительности из вики PostgreSQL:

...