Як зробити DISTINCT ON швидшим у PostgreSQL?


13

У мене є таблиця station_logsв базі даних PostgreSQL 9.6:

    Column     |            Type             |    
---------------+-----------------------------+
 id            | bigint                      | bigserial
 station_id    | integer                     | not null
 submitted_at  | timestamp without time zone | 
 level_sensor  | double precision            | 
Indexes:
    "station_logs_pkey" PRIMARY KEY, btree (id)
    "uniq_sid_sat" UNIQUE CONSTRAINT, btree (station_id, submitted_at)

Я намагаюся отримати останнє level_sensorзначення на основі submitted_atдля кожного station_id. Налічується близько 400 унікальних station_idзначень і близько 20 тис station_id. Рядків на день .

Перед створенням індексу:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 Унікальний (вартість = 4347852.14..4450301.72 рядки = 89 ширина = 20) (фактичний час = 22202.080..27619.167 рядків = 98 петель = 1)
   -> Сортувати (вартість = 4347852.14..4399076.93 рядки = 20489916 ширина = 20) (фактичний час = 22202.077..26540.827 рядків = 20489812 петлі = 1)
         Ключ сортування: station_id, представлено_at DESC
         Спосіб сортування: зовнішній злиття Диск: 681040kB
         -> сканування послідовності на station_logs (вартість = 0,00..598895.16 рядків = 20489916 ширина = 20) (фактичний час = 0,023..3443,587 рядків = 20489812 циклів = $
 Час планування: 0,072 мс
 Час виконання: 27690.644 мс

Створення індексу:

CREATE INDEX station_id__submitted_at ON station_logs(station_id, submitted_at DESC);

Після створення індексу для того ж запиту:

 Унікальний (вартість = 0,56..2156367,51 рядків = 89 ширина = 20) (фактичний час = 0,184..16263,413 рядків = 98 петель = 1)
   -> Сканування покажчика за допомогою станції_id__submitted_at на station_logs (вартість = 0,56..2105142,98 рядків = 20489812 ширина = 20) (фактичний час = 0,181..1 $
 Час планування: 0,206 мс
 Час виконання: 16263.490 мс

Чи є спосіб зробити цей запит швидшим? Як, наприклад, 1 сек, 16 сек все ще занадто багато.


2
Скільки різних ідентифікаторів станції, тобто скільки рядків повертається запит? А яка версія Postgres?
ypercubeᵀᴹ

Postgre 9.6, близько 400 унікальних станцій_id та близько 20 тис. Записів на день на station_id
Kokizzu

Цей запит повертає в «останнє значення level_sensor засноване на submitted_at, для кожного station_id». DISTINCT ON включає випадковий вибір, за винятком випадків, коли він вам не потрібен.
philipxy

Відповіді:


18

Лише 400 станцій цей запит буде значно швидше:

SELECT s.station_id, l.submitted_at, l.level_sensor
FROM   station s
CROSS  JOIN LATERAL (
   SELECT submitted_at, level_sensor
   FROM   station_logs
   WHERE  station_id = s.station_id
   ORDER  BY submitted_at DESC NULLS LAST
   LIMIT  1
   ) l;

dbfiddle тут
(порівнюючи плани цього запиту, альтернативу Abelisto та оригінал)

Результат EXPLAIN ANALYZE, передбачений ОП:

 Вкладена петля (вартість = 0,56..356,65 рядків = 102 ширина = 20) (фактичний час = 0,034..0,979 рядків = 98 петель = 1)
   -> Сканування послідовності на станціях s (вартість = 0,00..3,02 рядків = 102 ширина = 4) (фактичний час = 0,009..0,016 рядків = 102 петлі = 1)
   -> Обмеження (вартість = 0,56..3,45 рядків = 1 ширина = 16) (фактичний час = 0,009..0,009 рядів = 1 петля = 102)
         -> Сканування покажчика за допомогою станції_id__submitted_at на station_logs (вартість = 0,56..664062,38 рядків = 230223 ширина = 16) (фактичний час = 0,009 $
               Індекс Cond: (station_id = s.id)
 Час планування: 0,542 мс
 Час виконання: 1.013 мс   - !!

Єдиний індекс вам потрібно , це один створений Вами station_id__submitted_at. UNIQUEОбмеження uniq_sid_satтакож робить роботу, в основному. Збереження обох здається витратою простору дискового простору та продуктивності запису.

Я додав NULLS LASTдо ORDER BYзапиту, оскільки submitted_atне визначений NOT NULL. В ідеалі, якщо це можливо !, додайте NOT NULLобмеження до стовпця submitted_at, відмініть додатковий індекс та видаліть NULLS LASTіз запиту.

Якщо submitted_atце можливо NULL, створіть цей UNIQUEіндекс, щоб замінити ваш поточний індекс та унікальне обмеження:

CREATE UNIQUE INDEX station_logs_uni ON station_logs(station_id, submitted_at DESC NULLS LAST);

Поміркуйте:

Це передбачається окрема таблицяstation з одним рядком на відповідний station_id(зазвичай ПК) - який у вас повинен бути будь-який спосіб. Якщо у вас його немає, створіть його. Знову ж таки, дуже швидко з цією технікою rCTE:

CREATE TABLE station AS
WITH RECURSIVE cte AS (
   (
   SELECT station_id
   FROM   station_logs
   ORDER  BY station_id
   LIMIT  1
   )
   UNION ALL
   SELECT l.station_id
   FROM   cte c
   ,      LATERAL (   
      SELECT station_id
      FROM   station_logs
      WHERE  station_id > c.station_id
      ORDER  BY station_id
      LIMIT  1
      ) l
   )
TABLE cte;

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

Детальні інструкції, пояснення та альтернативи:

Оптимізуйте індекс

Ваш запит зараз має бути дуже швидким. Тільки якщо вам все-таки потрібно оптимізувати продуктивність читання ...

Можливо, буде доцільно додати level_sensorдо індексу як останній стовпець, щоб дозволити сканування лише для індексу , як, наприклад, прокоментував joanolo .
Con: Він збільшує індекс - це додає невеликих витрат на всі запити, що використовують його.
Про: Якщо ви фактично отримуєте з нього лише скани з індексом, запит під рукою зовсім не повинен відвідувати купі сторінки, що робить його приблизно вдвічі швидшим. Але це може бути суттєвим виграшем для дуже швидкого запиту зараз.

Однак я не очікую, що це спрацює у вашій справі. Ви згадали:

... близько 20 тис. рядків на день station_id.

Як правило, це свідчить про невпинне завантаження запису (1 на station_idкожні 5 секунд). І вас цікавить останній ряд. Сканування, призначене лише для покажчиків, працює лише для купи сторінок, видимих ​​для всіх транзакцій (встановлено біт на карті видимості). Вам слід запустити надзвичайно агресивні VACUUMнастройки для таблиці, щоб не відставати від завантаження запису, і це все ще не працюватиме більшу частину часу. Якщо мої припущення є правильними, сканування, призначене лише для індексу, немає, не додайте level_sensorйого до індексу.

ОТОХ, якщо мої припущення виконуються, а ваша таблиця зростає дуже великою , індекс BRIN може допомогти. Пов'язані:

Або ще більш спеціалізований та ефективніший: частковий індекс лише для останніх доповнень, щоб відрізати основну частину невідповідних рядків:

CREATE INDEX station_id__submitted_at_recent_idx ON station_logs(station_id, submitted_at DESC NULLS LAST)
WHERE submitted_at > '2017-06-24 00:00';

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

...
WHERE  station_id = s.station_id
AND    submitted_at > '2017-06-24 00:00'
...

Вам доведеться час від часу адаптувати індекс і запит.
Відповідні відповіді з більш детальною інформацією:


Коли я знаю, що мені потрібно вкладений цикл (часто), використання LATERAL - це підвищення продуктивності для ряду ситуацій.
Пол Дрейпер

6

Спробуйте класичний спосіб:

create index idx_station_logs__station_id on station_logs(station_id);
create index idx_station_logs__submitted_at on station_logs(submitted_at);

analyse station_logs;

with t as (
  select station_id, max(submitted_at) submitted_at 
  from station_logs 
  group by station_id)
select * 
from t join station_logs l on (
  l.station_id = t.station_id and l.submitted_at = t.submitted_at);

dbfiddle

ПОЯСНІТЬ АНАЛІЗ ThreadStarter

 Nested Loop  (cost=701344.63..702110.58 rows=4 width=155) (actual time=6253.062..6253.544 rows=98 loops=1)
   CTE t
     ->  HashAggregate  (cost=701343.18..701344.07 rows=89 width=12) (actual time=6253.042..6253.069 rows=98 loops=1)
           Group Key: station_logs.station_id
           ->  Seq Scan on station_logs  (cost=0.00..598894.12 rows=20489812 width=12) (actual time=0.034..1841.848 rows=20489812 loop$
   ->  CTE Scan on t  (cost=0.00..1.78 rows=89 width=12) (actual time=6253.047..6253.085 rows=98 loops=1)
   ->  Index Scan using station_id__submitted_at on station_logs l  (cost=0.56..8.58 rows=1 width=143) (actual time=0.004..0.004 rows=$
         Index Cond: ((station_id = t.station_id) AND (submitted_at = t.submitted_at))
 Planning time: 0.542 ms
 Execution time: 6253.701 ms
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.