Как оптимизировать соединение с умеренно большим столом типа II на снежинке? - PullRequest
0 голосов
/ 01 июня 2018

Фон

Предположим, у меня есть следующие таблицы:

-- 33M rows
CREATE TABLE lkp.session (
    session_id BIGINT,
    visitor_id BIGINT,
    session_datetime TIMESTAMP
);

-- 17M rows
CREATE TABLE lkp.visitor_customer_hist (
    visitor_id BIGINT,
    customer_id BIGINT,
    from_datetime TIMESTAMP,
    to_datetime TIMESTAMP
);

Visitor_customer_hist дает идентификатор customer_id, действующий для каждого посетителя в каждый момент времени.

Цель состоит в том, чтобы найти идентификатор клиента, который действовал для каждого сеанса, используя visitor_id и session_datetime.

CREATE TABLE lkp.session_effective_customer AS
    SELECT
        s.session_id,
        vch.customer_id AS effective_customer_id
    FROM lkp.session s
    JOIN lkp.visitor_customer_hist vch ON vch.visitor_id = s.visitor_id
        AND s.session_datetime >= vch.from_datetime
        AND s.session_datetime < vch.to_datetime;

Задача

Даже при большом масштабе хранилища этот запрос будет чрезвычайно медленным.Это заняло 1 час 15 минут, и это был единственный запрос, выполняемый на складе.

Я убедился, что в visitor_customer_hist нет перекрывающихся значений, наличие которых может вызвать дублирующее соединение.

Isснежинка просто очень плохо в таком виде присоединяется?Я ищу предложения о том, как я мог бы оптимизировать таблицы для этого вида запроса, повторной кластеризации, или любой техники оптимизации или доработки запроса, например, может быть коррелированный подзапрос или что-то.

Дополнительная информация

Профиль: Profile

Ответы [ 3 ]

0 голосов
/ 06 июня 2018

После ответа Стюарта , мы можем отфильтровать его немного, посмотрев на минимальные и максимальные значения посетителя.Примерно так:

CREATE TEMPORARY TABLE _vch AS
    SELECT
        l.visitor_id,
        l.customer_id,
        l.from_datetime,
        l.to_datetime
    FROM (
             SELECT
                 l.visitor_id,
                 min(l.session_datetime) AS mindt,
                 max(l.session_datetime) AS maxdt
             FROM lkp.session l
             GROUP BY l.visitor_id
         ) a
    JOIN lkp.visitor_customer_hist l ON a.visitor_id = l.visitor_id
        AND l.from_datetime >= a.mindt
        AND l.to_datetime <= a.maxdt;

Тогда с нашей более легкой таблицей истории, возможно, нам повезет больше:

CREATE TABLE lkp.session_effective_customer AS
    SELECT
        s.session_id,
        vch.customer_id AS effective_customer_id
    FROM lkp.session s
    JOIN _vch vch ON vch.visitor_id = s.visitor_id
        AND s.session_local_datetime >= vch.from_datetime
        AND s.session_local_datetime < vch.to_datetime;

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

Однако в других обстоятельствах это может сработать.

0 голосов
/ 06 июня 2018

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

В конечном итоге я решил эту проблему: очистить таблицу visitor_customer_hist и написать пользовательскую оконную функцию / udtf.

Изначально я создал lkp.visitor_customer_hist таблицу, потому что она моглабыть созданы с использованием существующих оконных функций, и в базе данных SQL не-MPP могут быть созданы соответствующие индексы, которые сделают поиск достаточно производительным.Он был создан так:

CREATE TABLE lkp.visitor_customer_hist AS
    SELECT
        a.visitor_id AS visitor_id,
        a.customer_id AS customer_id,
        nvl(lag(a.session_datetime) OVER ( PARTITION BY a.visitor_id
            ORDER BY a.session_datetime ), '1900-01-01') AS from_datetime,
        CASE WHEN lead(a.session_datetime) OVER ( PARTITION BY a.visitor_id
            ORDER BY a.session_datetime ) IS NULL THEN '9999-12-31'
        ELSE a.session_datetime END AS to_datetime
    FROM (
             SELECT
                 s.session_id,
                 vs.visitor_id,
                 customer_id,
                 row_number() OVER ( PARTITION BY vs.visitor_id, s.session_datetime
                     ORDER BY s.session_id ) AS rn,
                 lead(s.customer_id) OVER ( PARTITION BY vs.visitor_id
                     ORDER BY s.session_datetime ) AS next_cust_id,
                 session_datetime
             FROM "session" s
             JOIN "visitor_session" vs ON vs.session_id = s.session_id
             WHERE s.customer_id <> -2
         ) a
    WHERE (a.next_cust_id <> a.customer_id
        OR a.next_cust_id IS NULL) AND a.rn = 1;

Итак, отбрасывая этот подход , я написал следующую вставку UDTF:

CREATE OR REPLACE FUNCTION udtf_eff_customer(customer_id FLOAT)
    RETURNS TABLE(effective_customer_id FLOAT)
LANGUAGE JAVASCRIPT
IMMUTABLE
AS '
{
    initialize: function() {
        this.customer_id = -1;
    },

    processRow: function (row, rowWriter, context) {
        if (row.CUSTOMER_ID != -1) {
            this.customer_id = row.CUSTOMER_ID;
        }
        rowWriter.writeRow({EFFECTIVE_CUSTOMER_ID:  this.customer_id});
    },

    finalize: function (rowWriter, context) {/*...*/},
}
';

И его можно применить так:

SELECT
    iff(a.customer_id <> -1, a.customer_id, ec.effective_customer_id) AS customer_id,
    a.session_id
FROM "session" a
JOIN table(udtf_eff_customer(nvl2(a.visitor_id, a.customer_id, NULL) :: DOUBLE) OVER ( PARTITION BY a.visitor_id
    ORDER BY a.session_datetime DESC )) ec

Таким образом, это приводит к желаемому результату: для каждого сеанса, если customer_id не является «неизвестным», тогда мы идем дальше и используем это;в противном случае мы используем следующий customer_id (если он существует), который можно связать с этим посетителем (упорядочение по времени сеанса).

Это гораздо лучшее решение, чем создание таблицы поиска;по сути, он занимает всего один проход для данных, требует гораздо меньше кода / сложности и идет очень быстро.

0 голосов
/ 01 июня 2018

Если таблица lkp.session содержит узкий временной диапазон, а таблица lkp.visitor_customer_hist содержит широкий временной диапазонвы можете выиграть от переписывания запроса, добавив избыточное условие, ограничивающее диапазон строк, рассматриваемых в объединении:

CREATE TABLE lkp.session_effective_customer AS
SELECT
    s.session_id,
    vch.customer_id AS effective_customer_id
FROM lkp.session s
JOIN lkp.visitor_customer_hist vch ON vch.visitor_id = s.visitor_id
    AND s.session_datetime >= vch.from_datetime
    AND s.session_datetime < vch.to_datetime
WHERE vch.to_datetime >= (select min(session_datetime) from lkp.session)
    AND  vch.from_datetime <= (select max(session_datetime) from lkp.session);

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

...