Оптимизировать большое условие IN для запроса Redshift

У меня есть полностью очищенная таблица Redshift ~ 2 ТБ с distkey phash (высокая кардинальность, сотни миллионов значений) и составными ключами сортировки (phash, last_seen).

Когда я делаю запрос вроде:

SELECT
    DISTINCT ret_field
FROM
    table
WHERE
    phash IN (
        '5c8615fa967576019f846b55f11b6e41',
        '8719c8caa9740bec10f914fc2434ccfd',
        '9b657c9f6bf7c5bbd04b5baf94e61dae'
    )
AND
    last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

Он возвращается очень быстро. Однако, когда я увеличиваю количество хэшей больше 10, Redshift преобразует условие IN из набора OR в массив согласно http://docs.aws.amazon.com/redshift/latest/dg/r_in_condition.html#r_in_condition-optimization-for-large-in-lists< /а>

Проблема в том, что когда у меня есть пара десятков значений phash, время ответа "оптимизированного" запроса сокращается с менее чем секунды до более чем получаса. Другими словами, он перестает использовать ключ сортировки и выполняет полное сканирование таблицы.

Любая идея, как я могу предотвратить такое поведение и сохранить использование ключей сортировки, чтобы запрос оставался быстрым?

Вот разница в EXPLAIN между ‹10 хэшей и >10 хэшей:

Менее 10 (0,4 секунды):

XN Unique  (cost=0.00..157253450.20 rows=43 width=27)
    ->  XN Seq Scan on table  (cost=0.00..157253393.92 rows=22510 width=27)
                Filter: ((((phash)::text = '394e9a527f93377912cbdcf6789787f1'::text) OR ((phash)::text = '4534f9f8f68cc937f66b50760790c795'::text) OR ((phash)::text = '5c8615fa967576019f846b55f11b6e61'::text) OR ((phash)::text = '5d5743a86b5ff3d60b133c6475e7dce0'::text) OR ((phash)::text = '8719c8caa9740bec10f914fc2434cced'::text) OR ((phash)::text = '9b657c9f6bf7c5bbd04b5baf94e61d9e'::text) OR ((phash)::text = 'd7337d324be519abf6dbfd3612aad0c0'::text) OR ((phash)::text = 'ea43b04ac2f84710dd1f775efcd5ab40'::text)) AND (last_seen >= '2015-10-01 00:00:00'::timestamp without time zone) AND (last_seen <= '2015-10-31 23:59:59'::timestamp without time zone))

Более 10 (45-60 минут):

XN Unique  (cost=0.00..181985241.25 rows=1717530 width=27)
    ->  XN Seq Scan on table  (cost=0.00..179718164.48 rows=906830708 width=27)
                Filter: ((last_seen >= '2015-10-01 00:00:00'::timestamp without time zone) AND (last_seen <= '2015-10-31 23:59:59'::timestamp without time zone) AND ((phash)::text = ANY ('{33b84c5775b6862df965a0e00478840e,394e9a527f93377912cbdcf6789787f1,3d27b96948b6905ffae503d48d75f3d1,4534f9f8f68cc937f66b50760790c795,5a63cd6686f7c7ed07a614e245da60c2,5c8615fa967576019f846b55f11b6e61,5d5743a86b5ff3d60b133c6475e7dce0,8719c8caa9740bec10f914fc2434cced,9b657c9f6bf7c5bbd04b5baf94e61d9e,d7337d324be519abf6dbfd3612aad0c0,dbf4c743832c72e9c8c3cc3b17bfae5f,ea43b04ac2f84710dd1f775efcd5ab40,fb4b83121cad6d23e6da6c7b14d2724c}'::text[])))

comment
Я не понимаю, когда вы говорите, что он перестает использовать ключ сортировки и выполняет полное сканирование таблицы. Redshift всегда выполняет полное сканирование таблицы, но может использовать ключ сортировки для пропуска блоков. Можете ли вы предоставить точное объяснение запроса?   -  person Mark Hildreth    schedule 17.11.2015
comment
Нет проблем @MarkHildreth - я только что отредактировал основной пост, включив в него EXPLAIN запросов.   -  person Harry    schedule 17.11.2015
comment
Обратите внимание, что это не очень справедливо по отношению к читателям и пользователям SO (но вы можете опубликовать решение здесь): существует специальный список рассылки для вопросов о производительности postgresql.   -  person Str.    schedule 20.11.2015
comment
Покажите нам структуру таблиц   -  person Muhammad Muazzam    schedule 02.12.2015
comment
Фактическое определение таблицы, показывающее типы данных и ограничения, важно для такого вопроса производительности. Желательно полный оператор CREATE TABLE и все соответствующие определения индекса.   -  person Erwin Brandstetter    schedule 03.12.2015


Ответы (5)


Стоит попробовать установить sortkeys (last_seen, phash), поставив last_seen первым.

Причина медлительности может заключаться в том, что ведущей колонкой для ключа сортировки является phash, который выглядит как случайный символ. Как говорится в документации AWS redshift dev, столбцы меток времени должны быть ведущими столбцами для ключа сортировки, если они используются для условий where.

Если последние данные запрашиваются чаще всего, укажите столбец отметки времени в качестве ведущего столбца для ключа сортировки. – Выберите лучший ключ сортировки – Amazon Redshift< /а>

При таком порядке ключа сортировки все столбцы будут отсортированы по last_seen, затем по phash. (Что значит иметь несколько столбцов sortkey?)

Одно замечание: вам нужно воссоздать таблицу, чтобы изменить ключ сортировки. Это поможет вам в этом.

person Masashi M    schedule 02.12.2015
comment
Простое решение, но это решило его! Все еще не молниеносно, но очевидно, что ключи сортировки ужасно неэффективны для случайных строк. - person Harry; 09.12.2015

Вы можете попробовать создать временную таблицу/подзапрос:

SELECT DISTINCT t.ret_field
FROM table t
JOIN (
   SELECT '5c8615fa967576019f846b55f11b6e41' AS phash
   UNION ALL 
   SELECT '8719c8caa9740bec10f914fc2434ccfd' AS phash
   UNION ALL
   SELECT '9b657c9f6bf7c5bbd04b5baf94e61dae' AS phash
   -- UNION ALL
) AS sub
   ON t.phash = sub.phash
WHERE t.last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59';

В качестве альтернативы выполните поиск по частям (если оптимизатор запросов объединит их в одну, используйте вспомогательную таблицу для хранения промежуточных результатов):

SELECT ret_field
FROM table
WHERE phash IN (
        '5c8615fa967576019f846b55f11b6e41',
        '8719c8caa9740bec10f914fc2434ccfd',
        '9b657c9f6bf7c5bbd04b5baf94e61dae')
  AND last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'
UNION
SELECT ret_field
FROM table
WHERE phash IN ( ) -- more hashes)
  AND last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'
UNION 
-- ...

Если оптимизатор запросов объединит его в один, вы можете попробовать использовать временную таблицу для промежуточных результатов.

ИЗМЕНИТЬ:

SELECT DISTINCT t.ret_field
FROM table t
JOIN (SELECT ... AS phash
      FROM ...
) AS sub
   ON t.phash = sub.phash
WHERE t.last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59';
person Lukasz Szozda    schedule 17.11.2015
comment
Это действительно работает, чтобы продолжить использование индекса (спасибо!), но мне нужно вернуть список значений phash из другого подзапроса... это не вручную/не закодировано. Есть ли способ использовать/злоупотреблять UNION ALL из результата другого подзапроса? :( - person Harry; 17.11.2015
comment
@Harry Вы можете изменить UNION ALL на все, что возвращает phash - person Lukasz Szozda; 17.11.2015
comment
Я пробовал это EDIT раньше, и он имеет тот же эффект сканирования таблицы. И я не могу разбить его на куски, потому что все хэши возвращаются из Redshift одним большим пакетом. - person Harry; 17.11.2015
comment
@ lad2025, ваш второй вариант, который разбивает хэши на небольшие фрагменты с использованием SELECT DISTINCT ... UNION ALL SELECT DISTINCT ... UNION ALL ..., не эквивалентен исходному запросу в вопросе. Исходный запрос имеет DISTINCT по всем значениям ret_field. Ваш вариант может возвращать дубликаты. Кажется, вам нужно использовать UNION, а не UNION ALL. А с UNION нет необходимости в DISTINCTс. - person Vladimir Baranov; 21.11.2015
comment
@ lad2025 Это интересная идея, но опять же я не могу контролировать количество или ручные итерации. Мне нужно построить запрос, который может обрабатывать от 2-3 строк до десятков тысяч строк. Любые идеи? - person Harry; 21.11.2015

Вам действительно нужно DISTINCT ? Этот оператор может быть дорогим.

Я бы попробовал использовать LATERAL JOIN. В приведенном ниже запросе в таблице Hashes есть столбец phash — это ваша большая партия хэшей. Это может быть временная таблица, (под)запрос, что угодно.

SELECT DISTINCT T.ret_field
FROM
    Hashes
    INNER JOIN LATERAL
    (
        SELECT table.ret_field
        FROM table
        WHERE
            table.phash = Hashes.phash
            AND table.last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'
    ) AS T ON true

Вполне вероятно, что оптимизатор реализует LATERAL JOIN как вложенный цикл. Он будет перебирать все строки в Hashes и для каждой строки запускать SELECT FROM table. Внутренний SELECT должен использовать индекс, который у вас есть на (phash, last_seen). На всякий случай включите ret_field в индекс, чтобы сделать его покрывающим индексом: (phash, last_seen, ret_field).


В ответе @Diego есть очень важный момент: вместо того, чтобы помещать в запрос постоянные значения phash, поместите их во временную или постоянную таблицу.

Я хотел бы расширить ответ @Diego и добавить, что важно, чтобы эта таблица с хэшами имела индекс, уникальный индекс.

Итак, создайте таблицу Hashes с одним столбцом phash точно такого же типа, как в вашей основной таблице table.phash. Важно, чтобы типы совпадали. Сделайте этот столбец первичным ключом с уникальным кластеризованным индексом. Сбросьте десятки значений phash в таблицу Hashes.

Тогда запрос становится простым INNER JOIN, а не боковым:

SELECT DISTINCT T.ret_field
FROM
    Hashes
    INNER JOIN table ON table.phash = Hashes.phash
WHERE
    table.last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

По-прежнему важно, чтобы table имел индекс на (phash, last_seen, ret_field).

Оптимизатор должен иметь возможность воспользоваться тем фактом, что обе соединенные таблицы отсортированы по столбцу phash и что он уникален в таблице Hashes.

person Vladimir Baranov    schedule 21.11.2015
comment
Я перепробовал все возможные варианты боковых соединений и постоянно получаю синтаксическую ошибку. Вы уверены, что они поддерживаются в Redshift? - person Harry; 21.11.2015
comment
@ Гарри, нет, я не уверен, что у Redshift есть LATERAL JOIN. Я видел тег Postgres и не обратил внимания на тег Redshift. Невезение. Есть ли в Redshift хранимые процедуры и курсоры? Обычно курсоры работают медленнее, чем декларативный SQL, когда делают то же самое. Но в этом случае декларативный SQL не выполняет поиск по индексу для каждого phash, поэтому явный цикл для каждого phash с добавлением результатов во временную таблицу в целом может быть быстрее. - person Vladimir Baranov; 22.11.2015

вы можете избавиться от «ИЛИ», вставив нужные данные во временную таблицу и объединив их с вашей фактической таблицей.

Вот пример (я использую CTE, потому что с помощью инструмента, который я использую, трудно зафиксировать план, когда у вас есть более одного оператора SQL, но, если можете, используйте временную таблицу)

select * 
from <my_table>
where checksum in 
(
'd7360f1b600ae9e895e8b38262cee47936fb6ced',
'd1606f795152c73558513909cd59a8bc3ad865a8',
'bb3f6bb3d1a98d35a0f952a53d738ddec5c72c84',
'b2cad5a92575ed3868ac6e405647c2213eea74a5'
)

ПРОТИВ

with foo as
(
    select 'd7360f1b600ae9e895e8b38262cee47936fb6ced' as my_key union
    select 'd1606f795152c73558513909cd59a8bc3ad865a8' union
    select 'bb3f6bb3d1a98d35a0f952a53d738ddec5c72c84' union
    select 'b2cad5a92575ed3868ac6e405647c2213eea74a5'
)
select  * 
from <my_table> r 
     join foo f on r.checksum = F.my_key

и вот план, как вы можете видеть, он выглядит более сложным, но это из-за CTE, он не будет выглядеть так на временной таблице:

введите здесь описание изображения

person Diego    schedule 07.12.2015

Вы пытались использовать объединение для всех значений phash?

Просто так:

SELECT ret_field 
FROM   table 
WHERE  phash = '5c8615fa967576019f846b55f11b6e41' -- 1st phash value
and    last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

UNION 

SELECT ret_field 
FROM   table 
WHERE  phash = '8719c8caa9740bec10f914fc2434ccfd' -- 2nd phash value
and    last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

UNION 

SELECT ret_field 
FROM   table 
WHERE  phash = '9b657c9f6bf7c5bbd04b5baf94e61dae' -- 3rd phash value
and    last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

-- and so on...

UNION 

SELECT ret_field 
FROM   table 
WHERE  phash = 'nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn' -- Nth phash value
and    last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'
person Christian    schedule 07.12.2015