2016-08-08 1 views
1

Я использую PostgreSQL 9.5. Ниже вы можете найти структуру моей таблицы, мой запрос и результат запроса. Я хотел бы увеличить производительность моего запроса. Запрос подсчитывает записи на определенный промежуток времени, например: 250 milliseconds, 1 second, 22 minutes, 2 days and 30 minutes и т.д.Как повысить производительность запроса, который подсчитывает записи на определенный промежуток времени?

запрос быстро для больших интервалов, как 60 minutes, но и для небольших интервалов, как 4 seconds это очень медленно.

Самые важные вещи:

  • Я работаю с большой базой данных (20 миллионов строк и больше, но в запросе я использую часть этой базы данных, используя WHERE пункт, например: 1 миллион или больше).
  • В статье WHERE всегда есть столбцы id_user_table и sip. В некоторых случаях в столбце WHERE включены все столбцы таблицы, это зависит от выбора пользователя.
  • В настоящее время я создал индекс B-дерева на starttime колонки:

    CREATE INDEX starttime_interval ON data_store (starttime); 
    

Вы знаете, некоторые способы повышения производительности моего запроса?

Например, с помощью:

  • создания некоторых индексов по столбцам (?, Какие индексы и как создавать их),
  • улучшение моего запроса
  • изменения некоторых настроек в PostgreSQL,
  • или что-то еще.

Вот структура моего стола:

column_name | udt_name | length | is_nullable | key 
---------------+-------------+--------+-------------+-------- 
id    | int8  |  |  NO  | PK 
id_user_table | int4  |  |  NO  | FK 
starttime  | timestamptz |  |  NO  | 
time   | float8 |  |  NO  | 
sip   | varchar | 100 |  NO  | 
dip   | varchar | 100 |  NO  | 
sport   | int4  |  |  YES  | 
dport   | int4  |  |  YES  | 
proto   | varchar | 50 |  NO  | 
totbytes  | int8  |  |  YES  | 
info   | text  |  |  YES  | 
label   | varchar | 10 |  NO  | 

Простые SELECT * FROM data_Store WHERE id_user_table=1 and sip='147.32.84.138' ORDER BY starttime возвращает это:

id | id_user_table |   starttime   |  sip  | other columns... 
-----+---------------+----------------------------+---------------+-------------------- 
185 |  1  | 2011-09-12 15:24:03.248+02 | 147.32.84.138 |  ... 
189 |  1  | 2011-09-12 15:24:03.256+02 | 147.32.84.138 |  ... 
312 |  1  | 2011-09-12 15:24:06.112+02 | 147.32.84.138 |  ... 
313 |  1  | 2011-09-12 15:24:06.119+02 | 147.32.84.138 |  ... 
450 |  1  | 2011-09-12 15:24:09.196+02 | 147.32.84.138 |  ... 
451 |  1  | 2011-09-12 15:24:09.203+02 | 147.32.84.138 |  ... 
452 |  1  | 2011-09-12 15:24:09.21+02 | 147.32.84.138 |  ... 

Вот мой запрос в течение интервалов времени 4 секунды:

WITH generate_period AS(

    SELECT generate_series(date_trunc('second',min(starttime)), 
          date_trunc('second',max(starttime)), 
          interval '4 second') as tp 
    FROM data_store 
    WHERE id_user_table=1 and sip='147.32.84.138' --other restrictions 

), data_series AS(

    SELECT date_trunc('second', starttime) AS starttime, count(*) AS ct 
    FROM data_store 
    WHERE id_user_table=1 and sip='147.32.84.138' --other restrictions 
    GROUP BY 1 

) 

SELECT gp.tp AS starttime-from, 
     gp.tp + interval '4 second' AS starttime-to, 
     COALESCE(sum(ds.ct),0) AS ct 
FROM generate_period gp 
LEFT JOIN data_series ds ON date_trunc('second',ds.starttime) >= gp.tp 
         and date_trunc('second',ds.starttime) < gp.tp + interval '4 second' 
GROUP BY 1 
ORDER BY 1; 

Вот результат запроса:

 starttime-from |  starttime-to  | ct 
------------------------+------------------------+--------- 
2011-09-12 15:24:03+02 | 2011-09-12 15:24:07+02 | 4 
2011-09-12 15:24:07+02 | 2011-09-12 15:24:11+02 | 3 
2011-09-12 15:24:11+02 | 2011-09-12 15:24:15+02 | 0 
      ...   |   ...   | ... 

Вот результат EXPLAIN ANALYZE, который я получил в pgAdmin в течение интервалов времени 4 секунды:

Sort (cost=7477837.88..7477838.38 rows=200 width=16) (actual time=1537280.238..1537289.519 rows=60141 loops=1) 
    Sort Key: gp.tp 
    Sort Method: external merge Disk: 1792kB 
    CTE generate_period 
    -> Aggregate (cost=166919.73..166924.74 rows=1000 width=8) (actual time=752.301..823.022 rows=60141 loops=1) 
      -> Seq Scan on data_store (cost=0.00..163427.57 rows=698431 width=8) (actual time=0.034..703.845 rows=679951 loops=1) 
       Filter: ((id_user_table = 1) AND ((sip)::text = '147.32.84.138'::text)) 
       Rows Removed by Filter: 4030687 
    CTE data_series 
    -> GroupAggregate (cost=242521.00..250085.18 rows=186076 width=8) (actual time=1233.414..1341.701 rows=57555 loops=1) 
      Group Key: (date_trunc('second'::text, data_store_1.starttime)) 
      -> Sort (cost=242521.00..244267.08 rows=698431 width=8) (actual time=1233.407..1284.110 rows=679951 loops=1) 
       Sort Key: (date_trunc('second'::text, data_store_1.starttime)) 
       Sort Method: external sort Disk: 11960kB 
       -> Seq Scan on data_store data_store_1 (cost=0.00..165173.65 rows=698431 width=8) (actual time=0.043..886.224 rows=679951 loops=1) 
         Filter: ((id_user_table = 1) AND ((sip)::text = '147.32.84.138'::text)) 
         Rows Removed by Filter: 4030687 
    -> HashAggregate (cost=7060817.31..7060820.31 rows=200 width=16) (actual time=1537215.586..1537240.698 rows=60141 loops=1) 
     Group Key: gp.tp 
     -> Nested Loop Left Join (cost=0.00..6957441.76 rows=20675111 width=16) (actual time=1985.731..1536921.862 rows=74443 loops=1) 
       Join Filter: ((date_trunc('second'::text, ds.starttime) >= gp.tp) AND (date_trunc('second'::text, ds.starttime) < (gp.tp + '00:00:04'::interval))) 
       Rows Removed by Join Filter: 3461357700 
       -> CTE Scan on generate_period gp (cost=0.00..20.00 rows=1000 width=8) (actual time=752.303..910.810 rows=60141 loops=1) 
       -> CTE Scan on data_series ds (cost=0.00..3721.52 rows=186076 width=16) (actual time=0.021..3.716 rows=57555 loops=60141) 
Planning time: 0.258 ms 
Execution time: 1537389.102 ms 

Update

Вот другой запрос, но без WITH cte и date_trunc() экспрессии таким образом может быть этот запрос будет легче оптимизировать:

SELECT gp.tp AS starttime_from, 
     gp.tp + interval '4 second' AS starttime_to, 
     count(ds.id) 
FROM (SELECT generate_series(min(starttime), max(starttime), interval '4 second') as tp 
     FROM data_store 
     WHERE id_user_table=1 and sip='147.32.84.138' --other restrictions 
    ) gp 
    LEFT JOIN data_store ds 
    ON ds.starttime >= gp.tp and ds.starttime < gp.tp + interval '4 second' 
     and id_user_table=1 and sip='147.32.84.138' --other restrictions 
group by gp.tp 
order by gp.tp; 

Вышеупомянутый запрос намного быстрее, чем первый запрос. В настоящий момент индекс B-Tree на столбце starttime работает, но этого все еще недостаточно. Если я установил временные интервалы 100 milliseconds, я все равно буду ждать слишком долго. Диапазон 100 milliseconds - это минимальный интервал времени, который пользователь может установить. Я только что добавил индекс B-Tree на столбец sip, но это не поможет.

Вот результат EXPLAIN ANALYZE, которую я получил в pgAdmin для интервалов времени 100 мс:

Sort (cost=14672356.96..14672357.46 rows=200 width=16) (actual time=9380.768..9951.074 rows=2405621 loops=1) 
    Sort Key: (generate_series(date_trunc('second'::text, $0), date_trunc('second'::text, $1), '00:00:00.1'::interval)) 
    Sort Method: external merge Disk: 79880kB 
    -> HashAggregate (cost=14672346.81..14672349.31 rows=200 width=16) (actual time=6199.538..7232.962 rows=2405621 loops=1) 
     Group Key: (generate_series(date_trunc('second'::text, $0), date_trunc('second'::text, $1), '00:00:00.1'::interval)) 
     -> Nested Loop Left Join (cost=2.02..14284329.59 rows=77603444 width=16) (actual time=0.321..4764.648 rows=3006226 loops=1) 
       -> Result (cost=1.58..6.59 rows=1000 width=0) (actual time=0.295..159.147 rows=2405621 loops=1) 
        InitPlan 1 (returns $0) 
         -> Limit (cost=0.43..0.79 rows=1 width=8) (actual time=0.208..0.208 rows=1 loops=1) 
          -> Index Scan using starttime_interval on data_store (cost=0.43..250437.98 rows=698431 width=8) (actual time=0.204..0.204 rows=1 loops=1) 
            Index Cond: (starttime IS NOT NULL) 
            Filter: ((id_user_table = 1) AND ((sip)::text = '147.32.84.138'::text)) 
            Rows Removed by Filter: 144 
        InitPlan 2 (returns $1) 
         -> Limit (cost=0.43..0.79 rows=1 width=8) (actual time=0.050..0.050 rows=1 loops=1) 
          -> Index Scan Backward using starttime_interval on data_store data_store_1 (cost=0.43..250437.98 rows=698431 width=8) (actual time=0.049..0.049 rows=1 loops=1) 
            Index Cond: (starttime IS NOT NULL) 
            Filter: ((id_user_table = 1) AND ((sip)::text = '147.32.84.138'::text)) 
            Rows Removed by Filter: 23 
       -> Index Scan using starttime_interval on data_store ds (cost=0.44..13508.28 rows=77603 width=16) (actual time=0.002..0.002 rows=0 loops=2405621) 
        Index Cond: ((starttime >= (generate_series(date_trunc('second'::text, $0), date_trunc('second'::text, $1), '00:00:00.1'::interval))) AND (starttime < ((generate_series(date_trunc('second'::text, $0), date_trunc('second'::text, $1), '00 (...) 
        Filter: ((id_user_table = 1) AND ((sip)::text = '147.32.84.138'::text)) 
        Rows Removed by Filter: 2 
Planning time: 1.299 ms 
Execution time: 11641.154 ms 
+0

Вы забыли опубликовать свой анализ объяснений – e4c5

+2

1-я нота: CTE действуют как барьеры оптимизации в PostgreSQL: 'data_series' будет работать как временная таблица, при этом запрос на него не будет использовать индексы' data_store'; Второй. примечание: если вы используете выражение типа date_trunc ('second', starttime) 'в' WHERE' или условие соединения, оно не будет использовать простой индекс 'starttime' ни (и вы не можете индексировать' date_trunc (' second ', starttime) ', если я хорошо помню: он не является неизменным - по крайней мере, недокументированный, который использует аргументы timestamptz' – pozs

+0

@pozs, спасибо за ваши советы, я только что обновил свой пост. Я добавил еще один запрос без выражения 'WITH cte' и' date_trunc', и теперь работает индекс B-Tree в столбце 'starttime'. Новый запрос выполняется намного быстрее, но мне нужно что-то быстрее, если оно возможно. – Robert

ответ

0

На основе @pozs и @ комментарии RadekPostołowicz, окончательный запрос следующим образом (в течение интервалов времени 4 секунды):

SELECT gp.tp AS starttime_from, gp.tp + interval '4 second' AS starttime_to, count(ds.id) 
FROM (SELECT generate_series(min(starttime),max(starttime), interval '4 second') as tp 
     FROM data_store 
     WHERE id_user_table=1 and sip='147.32.84.138' 
     ORDER BY 1 
    ) gp 
    LEFT JOIN data_store ds 
    ON ds.id_user_table=1 and ds.sip='147.32.84.138' 
     and ds.starttime >= gp.tp and ds.starttime < gp.tp + interval '4 second' 
GROUP BY starttime_from 

Как @pozs заметил, для очень малых интервалов времени, результат запроса включает в себя множество строк с нулевым подсчетом. Эти ряды едят пространство. В этом случае запрос должен содержать ограничение HAVING count(ds.id) > 0, но тогда вы должны обработать эти 0 на стороне клиента.Вот вторая версия запроса, который включает в себя HAVING ограничение:

SELECT gp.tp AS starttime_from, gp.tp + interval '4 second' AS starttime_to, count(ds.id) 
FROM (SELECT generate_series(min(starttime),max(starttime), interval '4 second') as tp 
     FROM data_store 
     WHERE id_user_table=1 and sip='147.32.84.138' 
     ORDER BY 1 
    ) gp 
    LEFT JOIN data_store ds 
    ON ds.id_user_table=1 and ds.sip='147.32.84.138' 
     and ds.starttime >= gp.tp and ds.starttime < gp.tp + interval '4 second' 
GROUP BY starttime_from 
HAVING count(ds.id) > 0 

Самое главное, создание многоколоночном индекс, который @ RadekPostołowicz создал в своем замечании/ответ:

CREATE INDEX my_index ON data_store (id_user_table, sip, starttime); 

Почему эти колонны ? Потому что в каждом запросе я всегда использую id_user_table, sip и столбцы starttime в предложении WHERE.

0

Как я уже писал в комментариях вы можете, используя многоколоночном индекс:

CREATE INDEX my_index ON data_store (id_user_table, sip, starttime); 

Этот должен удалить Filter: ((id_user_table = 1) AND ((sip)::text = '147.32.84.138'::text)) из вашего плана выполнения (и поскольку каждый такой фильтр выполняется в петле, экономия может быть довольно высокой).

Я также подготовил альтернативный запрос:

select 
    min + (max - min) * (least - 1) as starttime_from, 
    min + (max - min) * least as starttime_to, 
    count 
from (
    select 
     min, 
     max, 
     count(1), 
     least(
      width_bucket(
       extract(epoch from starttime)::double precision, 
       extract(epoch from min)::double precision, 
       extract(epoch from max)::double precision, 
       ceil(extract(epoch from (max - min))/extract(epoch from query_interval))::integer 
      ), 
      ceil(extract(epoch from (max - min))/extract(epoch from query_interval))::integer 
     ) 
    from (
     select 
      *, 
      max(starttime) over(), 
      min(starttime) over(), 
      '4 second'::interval as query_interval 
     from data_store 
    ) as subquery2 
    group by least, min, max 
) as subquery1; 

Следует избегать вложенных циклов, и я предполагаю, что это может быть гораздо быстрее. Однако может потребоваться некоторая корректировка, чтобы соответствовать вашему желаемому результату (некоторое усечение даты?).

+0

Спасибо за ваш ответ. Я попытался запустить ваш запрос, но получил странный результат. Существует не 4-секундный интервал времени, есть ровно 20 минут и 35 секунд интервала времени. Я не знаю, почему. На данный момент у меня недостаточно времени для анализа вашего запроса глубже, но я сделаю это позже. Прежде чем я это сделаю, я останусь с запросом, который я поставил в своем ответе. – Robert

+0

Интересно, если вы делитесь некоторыми тестовыми данными, которые вызывают интервал времени 20 минут и 35 секунд (на sqlfiddle), я проверю его. В любом случае, я не чувствую себя вынужденным, я понимаю, что ваша проблема решена, и вы можете не захотеть тратить на нее больше времени;). –