Оптимізація запитів на діапазоні часових позначок (два стовпці)


96

Я використовую PostgreSQL 9.1 на Ubuntu 12.04.

Мені потрібно вибрати записи протягом певного проміжку часу: у моїй таблиці time_limitsє два timestampполя та одне integerвластивість. У моїй дійсній таблиці є додаткові стовпці, які не беруть участь у цьому запиті.

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);

Ця таблиця містить приблизно 2М записів.

Такі запити забирали величезну кількість часу:

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';

Тому я спробував додати ще один індекс - зворотний ПК:

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

У мене склалося враження, що продуктивність покращується: час доступу до записів посередині таблиці здається більш розумним: десь між 40 і 90 секундами.

Але це ще кілька десятків секунд для значень у середині часового діапазону. І ще вдвічі, коли орієнтуєтесь на кінець таблиці (хронологічно кажучи).

Я explain analyzeвперше спробував отримати цей план запитів:

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms

Результати дивіться на depesz.com.

Що я можу зробити, щоб оптимізувати пошук? Ви можете бачити, що весь час витрачається на сканування двох стовпців часових позначок, коли один раз id_phiвстановлено 0. І я не розумію великого сканування (60К рядків!) На часових позначках. Чи не вони були індексовані первинним ключем, і idx_inversedя додав?

Чи слід змінити типи часових позначок на щось інше?

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


1
ну це 45-ті. Я не знаю, чому це говорить 45 мс. Я б навіть не почав скаржитися, якби це було так швидко, як 45 мс ... :-) Можливо, помилка у висновку аналізу пояснити. А може, час аналізу зробити. Данно. Але 40/50 секунд - це те, що я вимірюю.
Стефан Ролланд

2
Час, який повідомляється у explain analyzeвисновку, - це час запиту, необхідний на сервері . Якщо ваш запит займає 45 секунд, то додатковий час витрачається на передачу даних з бази даних до програми, яка виконує запит Зрештою, це 62682 рядки, і якщо кожен рядок великий (наприклад, має довгі varcharабо textстовпці), це може вплинути на час передачі кардинально.
a_horse_with_no_name

@a_horse_with_no_name: оцінкаrows=62682 rows планувальника . Запит повертає 0 рядків. (actual time=44.446..44.446 rows=0 loops=1)
Ервін Брандстеттер

@ErwinBrandstetter: ах, правильно. Я це не помітив. Але досі я ніколи не бачив висновку брехні роз'яснення аналізу про час виконання.
a_horse_with_no_name

Відповіді:


162

Для Postgres 9.1 або новішої версії:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

У більшості випадків порядок сортування індексу навряд чи є актуальним. Postgres може сканувати назад практично так само швидко. Але для запитів про діапазон у кількох стовпцях це може змінити велику кількість . Тісно пов'язані:

Розглянемо ваш запит:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';

Порядок сортування першого стовпця id_phiв індексі не має значення. Оскільки це перевірено на рівність ( =), він повинен стати першим. Ви маєте це право. Детальніше в цій відповіді:

Postgres може перейти до id_phi = 0наступного часу і розглядати наступні два стовпці відповідного індексу. Вони запитуються з умовами діапазону перевернутого порядку сортування ( <=, >=). У моєму індексі перші класифікують рядки. Потрібно зробити найшвидший спосіб із індексом B-Tree 1 :

  • Ви хочете start_date_time <= something: індекс має найперші часові позначки.
    • Якщо він кваліфікується, також встановіть прапорець у стовпці 3.
      Повторіть, поки перший рядок не зможе виконати кваліфікацію (супершвидкий).
  • Ви хочете end_date_time >= something: спочатку індекс має останню позначку часу.
    • Якщо він кваліфікується, продовжуйте отримувати рядки, поки перший не стане (надшвидкий).
      Продовжуйте наступне значення для стовпця 2.

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

Скільки рядків збігаються на перших двох стовпцях?
Лише небагато з start_date_timeблизьким до початку часового діапазону таблиці. Але майже всі рядки з id_phi = 0хронологічним кінцем таблиці! Тому продуктивність погіршується з пізнішими часом запуску.

Планові оцінки

Планові оцінки rows=62682для вашого прикладу запиту. З них жоден не кваліфікується ( rows=0). Ви можете отримати кращі оцінки, якщо збільшите цільову статистику для таблиці. За 2 000 000 рядків ...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;

... може заплатити. Або навіть вище. Детальніше у цій відповіді:

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

CLUSTER / pg_repack

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

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

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

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

Індекс GiST у Postgres 9.2+

1 З pg 9.2+ є інший, можливо, більш швидкий варіант: індекс GiST для стовпця діапазону.

  • Існують вбудовані типи діапазонів для timestampі timestamp with time zone: tsrange,tstzrange . Індекс btree, як правило, швидший для додаткового integerстовпця, наприклад id_phi. Менший і дешевший в обслуговуванні, теж. Але запит, ймовірно, все-таки буде швидшим в цілому за допомогою комбінованого індексу.

  • Змініть визначення таблиці або використовуйте індекс вираження .

  • Для підключення багатоколінного індексу GiST вам також потрібен додатковий модуль, btree_gistвстановлений (один раз на базу даних), який забезпечує класи операторів для включення integer.

Трифекта! Многоколончатий функціональний індекс GiST :

CREATE EXTENSION IF NOT EXISTS btree_gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));

Скористайтеся оператором "містить діапазон"@> у своєму запиті зараз:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')

Індекс SP-GiST у Postgres 9.3+

SP-GiST індекс може бути навіть швидше , для такого роду запитів - за винятком того, що, цитую інструкцію :

Наразі лише індекси B-дерева, GiST, GIN та BRIN підтримують багатоколінні індекси.

Все ще вірно в Postgres 12.
Вам доведеться поєднувати spgistіндекс лише (tsrange(...))з другим btreeіндексом на (id_phi). З доданими накладними витратами, я не впевнений, що це може конкурувати.
Відповідна відповідь із орієнтиром лише для tsrangeстовпця:


78
Я хотів би сказати про це хоча б один раз, що кожна ваша відповідь на SO та DBA має дійсно високу додану вартість / експрес-експертизу , і більшість часу є найбільш повною. Просто сказати це один раз: Повага !.
Стефан Ролланд

1
Merci bien! :) Так ви швидше отримали результати?
Ервін Брандстеттер

Я повинен дозволити закінчити велику об'ємну копію, створену в результаті інтенсивно моєго запиту, тому зробити процес справді повільним, він обертався годинами, перш ніж я задав питання. Але я порахував, і я вирішив дозволити йому повернутися до ранку, коли він буде готовий, і новий стіл буде готовий до заповнення. Я намагався створити ваш індекс одночасно під час роботи, але через занадто великий доступ (я думаю) створення індексу має бути заблоковано. Я повторю цей самий тестовий час ще раз, щоб розібратися з вашим рішенням. Я також подивився, як оновлення до 9.2 ;-) для debian / ubuntu.
Стефан Ролланд

2
@StephaneRolland: все одно було б цікаво, чому вихідний аналіз аналізу показує 45 мілісекунд, поки ви бачите, що запит займає більше 40 секунд.
a_horse_with_no_name

1
@John: Postgres може просунути індекс вперед або назад, але він не може змінити напрямок у тому самому скануванні. В ідеалі, у вас є перші (або останні) всі рядки, що відповідають вимогам, але це має бути однакове вирівнювання (відповідні запити запитів) для всіх стовпців, щоб отримати найкращі результати.
Erwin Brandstetter

5

Відповідь Ервіна вже вичерпна:

Типи діапазону для часових позначок доступні в PostgreSQL 9.1 з розширенням Temporal від Джеффа Девіса: https://github.com/jeff-davis/PostgreSQL-Temporal

Примітка: має обмежені можливості (використовує Timestamptz, і ви можете мати лише стиль "[)", що перекриває afaik). Крім того, існує багато інших чудових причин для оновлення до PostgreSQL 9.2.


3

Ви можете спробувати створити індекс багатоколінок у іншому порядку:

primary key(id_phi, start_date_time,end_date_time);

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

Редагувати : моя помилка. Тепер я бачу, що у вас вже визначений цей індекс.


У мене вже є обидва індексу. За винятком первинного ключа є інший, але індекс, який ви пропонуєте, вже існує, і той, який використовується, якщо ви подивитесь на пояснення:Bitmap Index Scan on idx_time_limits_phi_start_end
Стефан Ролланд

1

Мені вдалося швидко збільшити (від 1 сек до 70 мс)

У мене є таблиця з сукупністю багатьох вимірювань і на багатьох рівнях ( lстовпець) (30s, 1m, 1h тощо) є два стовпчики, пов'язані діапазоном: $sдля початку та $eдля кінця.

Я створив два багатоколінні індекси: один для початку та один для кінця.

Я скоригував запит вибору: виберіть діапазони, де їх початкова межа знаходиться в заданому діапазоні. додатково виберіть діапазони, де їх кінцева межа знаходиться в заданому діапазоні.

Поясніть, показує два потоки рядків, що ефективно використовують наші індекси.

Індекси:

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

Виберіть запит:

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

Поясніть:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

Хитрість полягає в тому, що вузли плану містять лише потрібні рядки. Раніше ми отримали тисячі рядків у вузлі плану, оскільки він вибрав all points from some point in time to the very end, а потім наступний вузол видалив непотрібні рядки.

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