Почему CTE намного хуже, чем встроенные подзапросы

11

Я пытаюсь лучше понять, как работает планировщик запросов в postgresql.

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

select id from users 
    where id <> 2
    and gender = (select gender from users where id = 2)
    order by latest_location::geometry <-> (select latest_location from users where id = 2) ASC
    limit 50

Он работает менее чем за 10 мс в моей базе данных с около 500 000 записей в таблице пользователей.

Затем я подумал, что во избежание дублирования вложенных элементов я мог бы переписать запрос как CTE, например так:

with me as (
    select * from users where id = 2
)
select u.id, u.popularity from users u, me 
    where u.gender = me.gender
    order by  u.latest_location::geometry <-> me.latest_location::geometry ASC
    limit 50;

Однако этот переписанный запрос выполняется примерно за 1 секунду! Почему это происходит? Я вижу в объяснениях, что он не использует индекс геометрии, но можно ли что-нибудь сделать для этого? Спасибо!

Другой способ написать запрос:

select u.id, u.popularity from users u, (select gender, latest_location from users where id = 2) as me 
    where u.gender = me.gender
    order by  u.latest_location::geometry <-> me.latest_location::geometry ASC
    limit 50;

Тем не менее, это также будет так же медленно, как CTE.

С другой стороны, если я извлекаю параметры me и вставляю их статически, запрос снова выполняется быстро:

select u.id, u.popularity from users u
    where u.gender = 'male'
    order by  u.latest_location::geometry <-> '0101000000A49DE61DA71C5A403D0AD7A370F54340'::geometry ASC
    limit 50;

Объяснить первый (быстрый) запрос

 Limit  (cost=5.69..20.11 rows=50 width=36) (actual time=0.512..8.114 rows=50 loops=1)
   InitPlan 1 (returns $0)
     ->  Index Scan using users_pkey on users users_1  (cost=0.42..2.64 rows=1 width=32) (actual time=0.032..0.033 rows=1 loops=1)
           Index Cond: (id = 2)
   InitPlan 2 (returns $1)
     ->  Index Scan using users_pkey on users users_2  (cost=0.42..2.64 rows=1 width=4) (actual time=0.009..0.010 rows=1 loops=1)
           Index Cond: (id = 2)
   ->  Index Scan using users_latest_location_gix on users  (cost=0.41..70796.51 rows=245470 width=36) (actual time=0.509..8.100 rows=50 loops=1)
         Order By: (latest_location <-> $0)
         Filter: (gender = $1)
         Rows Removed by Filter: 20
 Total runtime: 8.211 ms
(12 rows)

Объясните второй (медленный) запрос

Limit  (cost=62419.82..62419.95 rows=50 width=76) (actual time=1024.963..1024.970 rows=50 loops=1)
   CTE me
     ->  Index Scan using users_pkey on users  (cost=0.42..2.64 rows=1 width=221) (actual time=0.037..0.038 rows=1 loops=1)
           Index Cond: (id = 2)
   ->  Sort  (cost=62417.18..63030.86 rows=245470 width=76) (actual time=1024.959..1024.963 rows=50 loops=1)
         Sort Key: ((u.latest_location <-> me.latest_location))
         Sort Method: top-N heapsort  Memory: 28kB
         ->  Hash Join  (cost=0.03..54262.85 rows=245470 width=76) (actual time=0.122..938.131 rows=288646 loops=1)
               Hash Cond: (u.gender = me.gender)
               ->  Seq Scan on users u  (cost=0.00..49353.41 rows=490941 width=48) (actual time=0.021..465.025 rows=490994 loops=1)
               ->  Hash  (cost=0.02..0.02 rows=1 width=36) (actual time=0.054..0.054 rows=1 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 1kB
                     ->  CTE Scan on me  (cost=0.00..0.02 rows=1 width=36) (actual time=0.047..0.049 rows=1 loops=1)
 Total runtime: 1025.096 ms
viblo
источник
3
Я писал об этом недавно; см. blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences . Хотя в настоящее время существуют некоторые проблемы DNS, которые могут ограничить доступность этого сайта. Попробуйте использовать подзапрос FROMвместо термина CTE для достижения наилучших результатов.
Крейг Рингер
Что (select id, latest_location from users where id = 2)делать, если вы используете в качестве cte? Может быть, это *, что вызывает эту проблему
cha
Я бы подумал , что вы бы искать ближайшие пользователь противоположного пола :)
тя
@cha Не делает различий в скорости, просто выбирая пол и местоположение в cte. (В моем случае я хочу взять среднее число похожих пользователей, просто я упростил запрос к вопросу)
viblo
@CraigRinger Я не думаю, что это забор оптимизации. Я также попробовал ваше предложение, и оно также было медленным. С другой стороны, если я извлекаю параметры вручную, это быстро (и в моем случае это реальная опция, конечный результат в любом случае является функцией).
Вибло

Ответы:

11

Попробуй это:

with me as (
    select * from users where id = 2
)
select u.id, u.popularity from users u, me 
    where u.gender = (select gender from me)
    order by  u.latest_location::geometry <-> (select latest_location from me)::geometry ASC
    limit 50;

Когда я смотрю на быстрый план, вот что выскакивает из меня (выделено жирным шрифтом):

 Предел (стоимость = 5.69..20.11 строк = 50 ширина = 36) (фактическое время = 0.512..8.114 строк = 50 циклов = 1)
   InitPlan 1 ( возвращает $ 0 )
     -> Сканирование индекса с использованием users_pkey для пользователей users_1 (стоимость = 0.42..2.64 строк = 1 ширина = 32) (фактическое время = 0.032..0.033 строк = 1 цикл = 1)
           Индекс Cond: (id = 2)
   InitPlan 2 ( возвращает $ 1 )
     -> Сканирование индекса с использованием users_pkey для пользователей users_2 (стоимость = 0.42..2.64 строк = 1 ширина = 4) (фактическое время = 0.009..0.010 строк = 1 цикл = 1)
           Индекс Cond: (id = 2)
   -> Сканирование индекса с использованием users_latest_location_gix для пользователей (стоимость = 0,41,70796,51 строк = 245470 ширины = 36) (фактическое время = 0,509,8,100 строк = 50 циклов = 1)
         Сортировать по: (latest_location   $ 0 )
         Фильтр: (пол = $ 1 )
         Строк, удаленных фильтром: 20
 Общее время выполнения: 8,211 мс
(12 рядов)

В медленной версии планировщик запросов оценивает оператор равенства genderи оператор геометрии latest_locationв контексте объединения , где значение meможет варьироваться в зависимости от каждой строки (даже если оно правильно оценило только 1 строку). В быстрой версии значения genderи latest_locationобрабатываются как скаляры, поскольку они отправляются встроенными подзапросами, что говорит планировщику запросов, что он имеет только одно значение для каждого. Это та же самая причина, по которой вы получаете быстрый план при вставке литеральных значений.

Ной Йеттер
источник
Я думаю, что вы можете удалить meиз fromпункта сейчас.
Яриус Хебзо