PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Оптимизирайте заявката GROUP BY, за да извлечете последния ред на потребител

За най-добра производителност при четене имате нужда от многоколонен индекс:

CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);

За да направите само индексно сканиране възможно, добавете иначе ненужната колона payload в покриващ индекс с INCLUDE клауза (Postgres 11 или по-нова версия):

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);

Вижте:

  • Покриването на индекси в PostgreSQL помага ли на JOIN колони?

Резервно за по-стари версии:

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);

Защо DESC NULLS LAST ?

  • Неизползван индекс в заявката за диапазон от дати

Заняколко редове за user_id или малки таблици DISTINCT ON обикновено е най-бързият и прост:

  • Изберете ли първия ред във всяка група GROUP BY?

Замного редове за user_id сканиране с пропускане на индекс (или разхлабено сканиране на индекс ) е (много) по-ефективен. Това не е внедрено до Postgres 12 – работата е в ход за Postgres 14. Но има начини да се емулира ефективно.

Общите таблични изрази изискват Postgres 8.4+ .
LATERAL изисква Postgres 9.3+ .
Следните решения надхвърлят това, което е покрито в Postgres Wiki .

1. Няма отделна таблица с уникални потребители

С отделни users таблица, решения в2. по-долу обикновено са по-прости и по-бързи. Прескочете напред.

1a. Рекурсивен CTE с LATERAL присъединете се

WITH RECURSIVE cte AS (
   (                                -- parentheses required
   SELECT user_id, log_date, payload
   FROM   log
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT l.*
   FROM   cte c
   CROSS  JOIN LATERAL (
      SELECT l.user_id, l.log_date, l.payload
      FROM   log l
      WHERE  l.user_id > c.user_id  -- lateral reference
      AND    log_date <= :mydate    -- repeat condition
      ORDER  BY l.user_id, l.log_date DESC NULLS LAST
      LIMIT  1
      ) l
   )
TABLE  cte
ORDER  BY user_id;

Това е лесно за извличане на произволни колони и вероятно най-добре в текущия Postgres. Повече обяснения в глава 2a. по-долу.

1b. Рекурсивна CTE с корелирана подзаявка

WITH RECURSIVE cte AS (
   (                                           -- parentheses required
   SELECT l AS my_row                          -- whole row
   FROM   log l
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT (SELECT l                            -- whole row
           FROM   log l
           WHERE  l.user_id > (c.my_row).user_id
           AND    l.log_date <= :mydate        -- repeat condition
           ORDER  BY l.user_id, l.log_date DESC NULLS LAST
           LIMIT  1)
   FROM   cte c
   WHERE  (c.my_row).user_id IS NOT NULL       -- note parentheses
   )
SELECT (my_row).*                              -- decompose row
FROM   cte
WHERE  (my_row).user_id IS NOT NULL
ORDER  BY (my_row).user_id;

Удобно е за извличане на единична колона или целия ред . Примерът използва целия тип ред на таблицата. Възможни са и други варианти.

За да потвърдите, че в предишната итерация е намерен ред, тествайте една колона NOT NULL (като първичния ключ).

Повече обяснение за тази заявка в глава 2b. по-долу.

Свързано:

  • Запитване за последните N свързани реда на ред
  • ГРУПИРАНЕ ПО една колона, докато сортиране по друга в PostgreSQL

2. С отделни users маса

Оформлението на таблицата едва ли има значение, стига точно един ред на релевантен user_id е гарантирано. Пример:

CREATE TABLE users (
   user_id  serial PRIMARY KEY
 , username text NOT NULL
);

В идеалния случай таблицата е физически сортирана в синхрон с log маса. Вижте:

  • Оптимизиране на обхвата на заявки за времеви печати в Postgres

Или е достатъчно малък (ниска мощност), че едва ли има значение. В противен случай сортирането на редове в заявката може да помогне за допълнително оптимизиране на производителността. Вижте допълнението на Gang Liang. Ако физическият ред на сортиране на users таблицата съвпада с индекса в log , това може да е без значение.

2a. LATERAL присъединете се

SELECT u.user_id, l.log_date, l.payload
FROM   users u
CROSS  JOIN LATERAL (
   SELECT l.log_date, l.payload
   FROM   log l
   WHERE  l.user_id = u.user_id         -- lateral reference
   AND    l.log_date <= :mydate
   ORDER  BY l.log_date DESC NULLS LAST
   LIMIT  1
   ) l;

JOIN LATERAL позволява препратка към предходния FROM елементи на същото ниво на заявка. Вижте:

  • Каква е разликата между LATERAL JOIN и подзаявка в PostgreSQL?

Резултатът е едно търсене в индекс (само) на потребител.

Не връща ред за потребители, липсващи в users маса. Обикновено външен ключ ограничението, налагащо референтната цялост, би изключило това.

Също така няма ред за потребители без съответстващ запис в log - в съответствие с първоначалния въпрос. За да запазите тези потребители в резултата, използвайте LEFT JOIN LATERAL ... ON true вместо CROSS JOIN LATERAL :

  • Извикване на функция за връщане на набор с аргумент на масив няколко пъти

Използвайте LIMIT n вместо LIMIT 1 за извличане на повече от един ред (но не всички) на потребител.

На практика всички те правят едно и също:

JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...

Последният обаче има по-нисък приоритет. Изрично JOIN се свързва преди запетая. Тази фина разлика може да има значение за повече таблици за присъединяване. Вижте:

  • „невалидна препратка към запис на клауза FROM за таблица“ в заявката на Postgres

2b. Корелирана подзаявка

Добър избор за извличане на единична колона от единедин ред . Пример за код:

  • Оптимизиране на груповата максимална заявка

Същото е възможно замножество колони , но имате нужда от повече интелигентност:

CREATE TEMP TABLE combo (log_date date, payload int);

SELECT user_id, (combo1).*              -- note parentheses
FROM (
   SELECT u.user_id
        , (SELECT (l.log_date, l.payload)::combo
           FROM   log l
           WHERE  l.user_id = u.user_id
           AND    l.log_date <= :mydate
           ORDER  BY l.log_date DESC NULLS LAST
           LIMIT  1) AS combo1
   FROM   users u
   ) sub;

Като LEFT JOIN LATERAL по-горе, този вариант включва всички потребители, дори без записи в log . Получавате NULL за combo1 , който можете лесно да филтрирате с WHERE клауза във външната заявка, ако е необходимо.
Нитка:във външната заявка не можете да различите дали подзаявката не е намерила ред или всички стойности на колоните са NULL - същият резултат. Нуждаете се от NOT NULL колона в подзаявката, за да избегнете тази неяснота.

Свързана подзаявка може да върне само единична стойност . Можете да увиете няколко колони в композитен тип. Но за да го разложи по-късно, Postgres изисква добре познат композитен тип. Анонимните записи могат да бъдат разложени само при предоставяне на списък с дефиниции на колони.
Използвайте регистриран тип като типа на ред на съществуваща таблица. Или регистрирайте съставен тип изрично (и за постоянно) с CREATE TYPE . Или създайте временна таблица (отпадаща автоматично в края на сесията), за да регистрирате временно нейния тип ред. Синтаксис за предаване:(log_date, payload)::combo

И накрая, ние не искаме да разлагаме combo1 на същото ниво на заявка. Поради слабост в планировчика на заявки това ще оцени подзаявката веднъж за всяка колона (все още е вярно в Postgres 12). Вместо това я направете като подзаявка и я разложете във външната заявка.

Свързано:

  • Вземете стойности от първия и последния ред на група

Демонстриране на всички 4 заявки със 100 000 записи в регистрационния файл и 1 000 потребители:
db<>fiddle тук - стр. 11
Стар sqlfiddle



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да създам индекс на полето JSON в Postgres?

  2. Django моделира един външен ключ към много таблици

  3. Как мога да задам параметър String[] на собствена заявка?

  4. Има ли начин да се зададе време на изтичане, след което запис на данни се изтрива автоматично в PostgreSQL?

  5. Получаване на име на текущата функция вътре във функцията с plpgsql