Величезна невідповідність між розміром повідомленого індексу та кількістю буферів у плані виконання


10

Питання

У нас є такий запит

SELECT COUNT(1) 
  FROM article
  JOIN reservation ON a_id = r_article_id 
 WHERE r_last_modified < now() - '8 weeks'::interval 
   AND r_group_id = 1 
   AND r_status = 'OPEN';

Оскільки у неї виникає час очікування (через 10 хвилин), я вирішив розглянути проблему.

EXPLAIN (ANALYZE, BUFFERS)Результат виглядає наступним чином :

 Aggregate  (cost=264775.48..264775.49 rows=1 width=0) (actual time=238960.290..238960.291 rows=1 loops=1)
   Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
   I/O Timings: read=169806.955 write=0.154
   ->  Hash Join  (cost=52413.67..264647.65 rows=51130 width=0) (actual time=1845.483..238957.588 rows=21644 loops=1)
         Hash Cond: (reservation.r_article_id = article.a_id)
         Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
         I/O Timings: read=169806.955 write=0.154
         ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..205458.72 rows=51130 width=4) (actual time=34.035..237000.197 rows=21644 loops=1)
               Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
               Rows Removed by Filter: 151549
               Buffers: shared hit=200193 read=48853 dirtied=450 written=8
               I/O Timings: read=168614.105 write=0.154
         ->  Hash  (cost=29662.22..29662.22 rows=1386722 width=4) (actual time=1749.392..1749.392 rows=1386814 loops=1)
               Buckets: 32768  Batches: 8  Memory Usage: 6109kB
               Buffers: shared hit=287 read=15508 dirtied=216, temp written=3551
               I/O Timings: read=1192.850
               ->  Seq Scan on article  (cost=0.00..29662.22 rows=1386722 width=4) (actual time=23.822..1439.310 rows=1386814 loops=1)
                     Buffers: shared hit=287 read=15508 dirtied=216
                     I/O Timings: read=1192.850
 Total runtime: 238961.812 ms

Вузький вузький вузол, очевидно, є індексним скануванням. Отже, давайте подивимося на визначення індексу:

CREATE INDEX reservation_r_article_id_idx1 
    ON reservation USING btree (r_article_id)
 WHERE (r_status <> ALL (ARRAY['FULFILLED', 'CLOSED', 'CANCELED']));

Розміри та номери рядків

Її розмір (повідомляється \di+або відвідується фізичний файл) становить 36 Мб. Оскільки застереження зазвичай витрачають лише відносно короткий час у всіх перерахованих вище статусах, відбувається багато оновлень, тому індекс досить роздутий (тут витрачається близько 24 Мб) - все-таки розмір порівняно невеликий.

reservationТаблиця становить близько 3,8 ГБ, що містить близько 40 мільйонів рядків. Кількість бронювання, які ще не закриті, становить близько 170 000 (точна кількість повідомляється у вузлі сканування індексу вище).

Тепер сюрприз: індексне сканування повідомляє про отримання величезної кількості буферів (тобто 8 кб сторінок):

Buffers: shared hit=200193 read=48853 dirtied=450 written=8

Числа, прочитані з кешу та диска (або кешу ОС), становлять до 1,9 ГБ!

Найгірший випадок

З іншого боку, найгірший сценарій, коли кожен кортеж сидить на іншій сторінці таблиці, враховуватиме відвідування (21644 + 151549) + 4608 сторінок (загальна кількість рядків, отриманих із таблиці плюс номер індексної сторінки від фізичної розмір). Це все ще лише під 180 000 - набагато нижче спостережуваних майже 250 000.

Цікавим (і, можливо, важливим) є те, що швидкість читання диска становить близько 2,2 МБ / с, що цілком нормально, я думаю.

І що?

Хтось має уявлення про те, звідки може виникнути ця невідповідність?

Примітка. Щоб зрозуміти, у нас є ідеї, що тут поліпшити / змінити, але мені дуже хотілося б зрозуміти отримані цифри - ось про що йдеться.

Оновлення: перевірка ефекту кешування чи мікровакуумування

Виходячи з відповіді jjanes , я перевірив, що відбувається, коли я знову запускаю абсолютно той самий запит. Кількість уражених буферів насправді не змінюється. (Для цього я спростив запит до його мінімуму, який все ще показує проблему.) Це те, що я бачу з першого запуску:

 Aggregate  (cost=240541.52..240541.53 rows=1 width=0) (actual time=97703.589..97703.590 rows=1 loops=1)
   Buffers: shared hit=413981 read=46977 dirtied=56
   I/O Timings: read=96807.444
   ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..240380.54 rows=64392 width=0) (actual time=13.757..97698.461 rows=19236 loops=1)
         Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
         Rows Removed by Filter: 232481
         Buffers: shared hit=413981 read=46977 dirtied=56
         I/O Timings: read=96807.444
 Total runtime: 97703.694 ms

а після другого:

 Aggregate  (cost=240543.26..240543.27 rows=1 width=0) (actual time=388.123..388.124 rows=1 loops=1)
   Buffers: shared hit=460990
   ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..240382.28 rows=64392 width=0) (actual time=0.032..385.900 rows=19236 loops=1)
         Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
         Rows Removed by Filter: 232584
         Buffers: shared hit=460990
 Total runtime: 388.187 ms

1
Можливо, це не має значення, але чи потрібно вам приєднання article? Схоже, що всі стовпці, що беруть участь у них, знаходяться з reservationтаблиці, і (якщо припустити) є FK, результат повинен бути однаковим.
ypercubeᵀᴹ

Це дуже гарне запитання. І ви маєте рацію, це не потрібно - це запит, який використовується для моніторингу іншою командою. І все-таки, принаймні дивлячись на план запитів, все інше є лише прикрасою для цієї неприємної перевірки покажчика :)
dezso

1
Дозвольте додати, що видалення з'єднання не має великої різниці - перекритий індекс-сканування залишається там.
dezso

Тост доступ до столу? Хоча я сумніваюся, що будь-яка з показаних вами колонок була б обсмаженою. Якщо у вас є тестуючий клон бази даних для цілей тестування, ви можете запустити pg_stat_reset()на ній, а потім запустити запит, а потім заглянути, pg_statio_user_tablesщоб побачити, куди він приписує блоки.
jjanes

Відповіді:


4

Я думаю, що ключовим тут є велика кількість оновлень та роздут на індексі.

Індекс містить покажчики на рядки таблиці, які вже не є "живими". Це старі версії оновлених рядків. Старі версії рядків зберігаються деякий час, щоб задовольнити запити зі старим знімком, а потім зберігаються ще деякий час, оскільки ніхто не хоче виконувати роботу над їх видаленням частіше, ніж потрібно.

Під час сканування індексу потрібно відвідувати ці рядки, а потім помічає, що їх більше не видно, тому їх ігнорує. explain (analyze,buffers)Заява не повідомляє про цю діяльність в явному вигляді, крім як через підрахунок буферів читання / попадання в процесі перевірки цих рядків.

Існує деякий код "мікровакууму" для btrees, такий, що коли сканування знову повертається до індексу, він пам'ятає, що вказівник, який він переслідував, вже не живе, і позначає його як мертве в індексі. Таким чином, наступний подібний запит, який запускається, не потребує повторного переслідування. Тож якщо ви знову запустите такий самий запит, ви, ймовірно, побачите доступ до буфера ближче до того, що ви передбачили.

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


Будь ласка, дивіться мою редакцію - для мене це схоже на кешування, а не на мікровакуум.
dezso

Ваші нові номери значно відрізняються від старих (приблизно вдвічі), тому важко інтерпретувати, що вони означають, не бачачи також нові числа за фактичними рядками та рядками, відфільтрованими для сканування індексу.
jjanes

Додано повні плани, як вони виглядають сьогодні. Кількість уражених буферів значно зросла з п’ятниці, як і кількість рядків.
дезсо

У вас довгі транзакції? Якщо так, можливо, сканування індексу все ще відслідковує не видимі для нього рядки (що спричиняє додаткові звернення буфера), але воно ще не може мікровакуумувати їх, оскільки вони можуть бути видимими іншим зі старшим знімок.
jjanes

У мене нічого немає - типова транзакція займає менше секунди. Інколи кілька секунд, але не довше.
dezso
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.