PostgreSQL запит дуже повільний, коли додається підзапит


10

У мене відносно простий запит на таблиці з 1,5М рядками:

SELECT mtid FROM publication
WHERE mtid IN (9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZE вихід:

Limit  (cost=8.84..12.86 rows=1 width=8) (actual time=0.985..0.986 rows=1 loops=1)
  ->  Bitmap Heap Scan on publication  (cost=8.84..12.86 rows=1 width=8) (actual time=0.984..0.985 rows=1 loops=1)
        Recheck Cond: ((mtid = 9762715) OR (last_modifier = 21321))
        ->  BitmapOr  (cost=8.84..8.84 rows=1 width=0) (actual time=0.971..0.971 rows=0 loops=1)
              ->  Bitmap Index Scan on publication_pkey  (cost=0.00..4.42 rows=1 width=0) (actual time=0.295..0.295 rows=1 loops=1)
                    Index Cond: (mtid = 9762715)
              ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..4.42 rows=1 width=0) (actual time=0.674..0.674 rows=0 loops=1)
                    Index Cond: (last_modifier = 21321)
Total runtime: 1.027 ms

Поки що добре, швидко і використовує доступні індекси.
Тепер, якщо я трохи змінити запит, результат буде таким:

SELECT mtid FROM publication
WHERE mtid IN (SELECT 9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZEвихід:

Limit  (cost=0.01..2347.74 rows=5000 width=8) (actual time=2735.891..2841.398 rows=1 loops=1)
  ->  Seq Scan on publication  (cost=0.01..349652.84 rows=744661 width=8) (actual time=2735.888..2841.393 rows=1 loops=1)
        Filter: ((hashed SubPlan 1) OR (last_modifier = 21321))
        SubPlan 1
          ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
Total runtime: 2841.442 ms

Не так швидко, і за допомогою Seq-сканування ...

Звичайно, оригінальний запит, який виконується додатком, є дещо складнішим і навіть повільнішим, і, звичайно, оригінальний сплячий оригінал - ні (SELECT 9762715), але повільність існує навіть для цього (SELECT 9762715)! Запит генерується в сплячому режимі, тому змінити їх є досить складним завданням, а деякі функції недоступні (наприклад UNION, недоступно, що було б швидко).

Питання

  1. Чому індекс не можна використовувати у другому випадку? Як їх можна було використовувати?
  2. Чи можу я покращити ефективність запитів іншим способом?

Додаткові думки

Здається, що ми могли використати перший випадок, зробивши SELECT вручну, а потім поставивши отриманий список у запит. Навіть з 5000 цифрами у списку IN () це в чотири рази швидше, ніж друге рішення. Однак це просто здається НЕПОЗНАЧНИМ (також, це може бути в 100 разів швидше :)). Зовсім незрозуміло, чому планувальник запитів використовує зовсім інший метод для цих двох запитів, тому я хотів би знайти приємніше рішення цієї проблеми.


Чи можете ви якось переписати свій код, щоб сплячий режим створив JOINзамість IN ()? Також publicationпроаналізовано нещодавно?
dezso

Так, я зробив як ВАКУУМНИЙ АНАЛІЗ, так і ВАКУУМ ПОВНИЙ. Зміни в роботі не відбулися. Щодо другого, AFAIR ми це спробували, і це не вплинуло на ефективність запитів.
P.Péter

1
Якщо Hibernate не спромоглася створити належний запит, чому б вам просто не використати необроблений SQL? Це як наполягати на Google translate, поки ви вже краще знаєте, як це висловити англійською. Що стосується вашого питання: він дійсно залежить від фактичного запиту, прихованого за ним (SELECT 9762715).
Ервін Брандстеттер

Як я вже згадував нижче, це повільно, навіть якщо внутрішній запит є (SELECT 9762715) . До сплячого питання: це можна зробити, але потрібно серйозно переписати код, оскільки у нас є визначені користувачем критерії сплячого режиму, які перекладаються на ходу. Тож по суті ми б модифікували сплячку, яка є величезною справою з великою кількістю можливих побічних ефектів.
P.Péter

Відповіді:


6

Тут стає очевидною суть проблеми:

Послідовність сканування публікації (вартість = 0,01..349652,84 рядків = 744661 ширина = 8) (фактичний час = 2735,888..2841,393 рядки = 1 петля = 1)

За оцінками, Postgres повертає 744661 рядків, а насправді це виявляється одним рядком. Якщо Postgres не знає краще, чого очікувати від запиту, він не може краще планувати. Нам потрібно було б побачити фактичний запит, прихований за ним, (SELECT 9762715)- і, ймовірно, також знати визначення таблиці, обмеження, кардинальності та розподіл даних. Очевидно, Postgres НЕ в змозі передбачити , як кілька рядків будуть повернуті їм. Можливо, є способи переписати запит, залежно від того, що це таке .

Якщо ви знаєте, що підзапит ніколи не може повернути більше, ніж nрядки, ви можете просто повідомити Postgres, використовуючи:

SELECT mtid
FROM   publication
WHERE  mtid IN (SELECT ... LIMIT n) --  OR last_modifier=21321
LIMIT  5000;

Якщо n досить малий, Postgres перейде до (растрового) сканування індексу. Однак це працює лише для простого випадку. Перестає працювати при додаванні ORумови: планувальник запитів наразі не може впоратися з цим.

Я рідко використовую IN (SELECT ...)для початку. Зазвичай є кращий спосіб здійснити те саме, часто з EXISTSнапівз'єднанням. Іноді за допомогою ( LEFT) JOIN( LATERAL) ...

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


2
За запитом не ховається запит (SELECT 9762715) ! Якщо я запускаю точний запит, який ви бачите вище. Звичайно, оригінальний запит у сплячому режимі трохи складніший, але я (думаю, що мені) вдалося точно визначити, куди зникає планувальник запитів, тому я представив цю частину запиту. Однак, вищезгадане пояснення та запити є дослівними ctrl-cv.
P.Péter

Що стосується другої частини, внутрішня межа не працює: EXPLAIN ANALYZE SELECT mtid FROM publication WHERE mtid IN (SELECT 9762715 LIMIT 1) OR last_modifier=21321 LIMIT 5000;також робиться послідовне сканування, а також працює близько 3 секунд ...
P.Péter

@ P.Péter: Це працює для мене в моєму локальному тесті з фактичним підзапитом на Postgres 9.4. Якщо те, що ви показуєте, ваш справжній запит, то у вас вже є рішення: Використовуйте перший запит у своєму запитанні з константою замість підзапиту.
Ервін Брандстеттер

Ну, я теж намагався підзапит на новому тестовому столі: CREATE TABLE test (mtid bigint NOT NULL, last_modifier bigint, CONSTRAINT test_property_pkey PRIMARY KEY (mtid)); CREATE INDEX test_last_modifier_btree ON test USING btree (last_modifier); INSERT INTO test (mtid, last_modifier) SELECT mtid, last_modifier FROM publication;. І ефект все ще був для тих самих запитів на test: будь-який підзапит призводив до сканування послідовності ... Я спробував і 9.1, і 9.4. Ефект той самий.
P.Péter

1
@ P.Péter: Я знову запустив тест і зрозумів, що протестував без ORумови. Трюк з LIMITлише працює для більш простого випадку.
Erwin Brandstetter

6

Мій колега знайшов спосіб змінити запит, щоб він потребував простого перезапису і робив те, що йому потрібно робити, тобто виконувати підбір в один крок, а потім робити подальші операції з результатом:

SELECT mtid FROM publication 
WHERE 
  mtid = ANY( (SELECT ARRAY(SELECT 9762715))::bigint[] )
  OR last_modifier=21321
LIMIT 5000;

Аналіз пояснення зараз:

 Limit  (cost=92.58..9442.38 rows=2478 width=8) (actual time=0.071..0.074 rows=1 loops=1)
   InitPlan 2 (returns $1)
     ->  Result  (cost=0.01..0.02 rows=1 width=0) (actual time=0.010..0.011 rows=1 loops=1)
           InitPlan 1 (returns $0)
             ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.002 rows=1 loops=1)
   ->  Bitmap Heap Scan on publication  (cost=92.56..9442.36 rows=2478 width=8) (actual time=0.069..0.070 rows=1 loops=1)
         Recheck Cond: ((mtid = ANY (($1)::bigint[])) OR (last_modifier = 21321))
         Heap Blocks: exact=1
         ->  BitmapOr  (cost=92.56..92.56 rows=2478 width=0) (actual time=0.060..0.060 rows=0 loops=1)
               ->  Bitmap Index Scan on publication_pkey  (cost=0.00..44.38 rows=10 width=0) (actual time=0.046..0.046 rows=1 loops=1)
                     Index Cond: (mtid = ANY (($1)::bigint[]))
               ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..46.94 rows=2468 width=0) (actual time=0.011..0.011 rows=0 loops=1)
                     Index Cond: (last_modifier = 21321)
 Planning time: 0.704 ms
 Execution time: 0.153 ms

Здається, ми можемо створити простий аналізатор, який таким чином знаходить і переписує всі підселекти, і додає його до сплячого гака, щоб маніпулювати нативним запитом.


Звучить весело. Чи не простіше просто видалити всі SELECTзаписи, як це було у першому запиті у запитанні?
dezso

Звичайно, я можу зробити двоступеневий підхід: зробити SELECTокремо, а потім зробити зовнішній вибір зі статичним списком після IN. Однак це значно повільніше (у 5-10 разів, якщо підзапит має більше ніж декілька результатів), оскільки у вас є додаткові мережеві тури, плюс у вас є постгрес, форматування безлічі результатів, а потім Java аналіз цих результатів (а потім робити те саме знову назад). Вищевказане рішення робить те саме семантично, залишаючи процес всередині постгресів. Загалом, наразі це здається найшвидшим способом з найменшою модифікацією в нашому випадку.
П.Петер

А, бачу. Що я не знав, це те, що ви можете отримати багато ідентифікаторів одночасно.
dezso

1

Відповідь на друге запитання: Так, ви можете додати ORDER BY до свого підзапиту, що матиме позитивний вплив. Але це схоже на рішення "EXISTS (підзапит)" у виконанні. Існує значна різниця навіть у підзапиті, що призводить до двох рядів.

SELECT mtid FROM publication
WHERE mtid IN (SELECT #column# ORDER BY #column#) OR last_modifier=21321
LIMIT 5000;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.