Спасибо за достаточно развернутый ответ. Хотелось бы немного дополнить вопросы примерами,
дабы действительно понять не на абстрактных вещах, а на конкретных примерах и попутно
задать еще парочку вопросов по индексам, оптимизаторе и тд.
1. Структуры данных. Пример:
-- таблица
CREATE TABLE MetricsStruct
(
EventDate Date,
CounterId UInt64,
Users UInt32,
Views UInt32,
Utm Nested (
Level UInt64, -- допустимо от 0-4
Name String
)
) ENGINE = MergeTree(EventDate, (CounterId, EventDate), 8192);
-- данные
INSERT INTO MetricsStruct VALUES (toDate(now()), 12345, 1, 1, [0, 1, 2, 3, 4], ['direct', 'cpc', 'phone', 'discount', 'buy_phone']);
INSERT INTO MetricsStruct VALUES (yesterday(), 12345, 1, 1, [0, 1, 2], ['direct', 'cpc', 'phone']);
INSERT INTO MetricsStruct VALUES (yesterday(), 54321, 1, 1, [0, 1, 2], ['direct', 'cpc', 'phone']);
-- получаем суммарные значения utm_source для CounterId по диапазону дат
SELECT
Utm.Name,
sum(Users),
sum(Views)
FROM MetricsStruct
ARRAY JOIN Utm AS Utm
WHERE CounterId = 12345 AND EventDate BETWEEN yesterday() AND toDate(now()) AND Utm.Level = 0
GROUP BY Utm.Name;
Вопросы:
1.1. Как отработает WHERE? EventDate находится в индексе, тогда как Utm.Level - нет. И мало того, оно находится во вложенной структуре.
Может лучше преобразовать запрос так и это будет более оптимально:
PREWHERE CounterId = 12345 AND EventDate BETWEEN yesterday() AND toDate(now())
WHERE Utm.Level = 0
1.1.1 В результате оптимизатор сначала отфильтрует данные с индекса, и после произведет фильтрацию по вложенной структуре,
что положительно скажется на производительности, тогда как в обычном случае не используя PREWHERE,
придется пройтись по всем данным проверяя и дату и значение структуры. Или все таки оптимизатор понимает, что сначала нужно выполнить фильтрацию по индексным полям и сам решит?
1.2. Возвращаясь к Вашему ответу: Если в таблице MetricsStruct будет более 3 млрд записей, а во вложенной структуре Utm каждой записи будет 5 значений (utm_source/utm_medium/utm_campaign/utm_content/utm_term) для 2-х колонок (Name/Level) - то это не оптимально, или же Вы имели ввиду кол-во колонок в самой структуре?
2. Возможно стоит переделать таблицу и это будет более оптимально в результате:
-- таблица
CREATE TABLE MetricsPath
(
EventDate Date,
CounterId UInt64,
Users UInt32,
Views UInt32,
Path UInt64, -- sipHash64(/utm_source/utm_medium/utm_campaign/utm_content/utm_term)
Name String
) ENGINE = MergeTree(EventDate, (CounterId, EventDate, Path), 8192);
-- данные
INSERT INTO MetricsPath VALUES (toDate(now()), 12345, 1, 1, sipHash64('/'), 'direct');
INSERT INTO MetricsPath VALUES (toDate(now()), 12345, 1, 1, sipHash64('/direct'), 'cpc');
INSERT INTO MetricsPath VALUES (toDate(now()), 12345, 1, 1, sipHash64('/direct/cpc'), 'phone');
INSERT INTO MetricsPath VALUES (toDate(now()), 12345, 1, 1, sipHash64('/direct/cpc/phone'), 'discount');
INSERT INTO MetricsPath VALUES (toDate(now()), 12345, 1, 1, sipHash64('/direct/cpc/phone/discount'), 'buy_phone');
INSERT INTO MetricsPath VALUES (toDate(now()), 12345, 1, 1, sipHash64('/'), 'direct');
INSERT INTO MetricsPath VALUES (toDate(now()), 12345, 1, 1, sipHash64('/direct'), 'cpc');
INSERT INTO MetricsPath VALUES (toDate(now()), 12345, 1, 1, sipHash64('/direct/cpc'), 'phone');
-- запрос
SELECT
Path,
sum(Users),
sum(Views)
FROM MetricsPath
WHERE CounterId = 12345 AND EventDate BETWEEN yesterday() AND toDate(now()) AND Path = sipHash64('/')
GROUP BY Path;
2.1 при таком подходе кол-во строк по сравнению с таблицей с вложенной структурой возрастет в 4-5 раз, а значит и потребление дискового пространства.
2.2 мы не используем JOIN, а значит меньше данных выгружаем в ОЗУ, меньшее кол-во открытых файлов дескрипторов, более эффективное планирование запроса по сравнению с вложенными структурами и более быстрый результат?
2.3 Действительно ли UInt64 Path будет в индексе? И он столь же эффективен как CounterId/EventDate при использовании в секции WHERE?
2.4 Path в этом случае UInt64, а значит он будет занимать меньше места и группировка по нему будет более эффективной, чем в случае String?
3. По коллизиям sipHash64.
3.1 Если у нас есть CounterId и в среднем данных для каждого CounterId - 300к, а не миллиарды, то возникновение коллизии будет крайне мала? (Нужно конечно же проверять этот момент)
3.2 Почему именно sipHash64? Можно выбрать к примеру MD5/SHA-1, но если их хранить как String - то памяти они при группировке, хранении и выборке будут жрать куда больше чем UInt64, и менее эффективными при выборках, когда посчитанное заранее значение лежит в таблице.
4. В конечном итоге смысл заключается в том, что хочется делать очень быстрые выборки с наименьшими накладными расходами на вычисление конечного результата.
Если бы я точно знал как обрабатываются запросы в 1-м и во 2-м случаях, возможно я бы даже объединил их, пожертвовав к примеру файловыми дескрипторами и занимаемом местом на HDD. Потому и уточняю, как работает БД и как правильно работать с ней. Был бы очень признательным если бы Вы прояснили все моменты. И даже подсказали более правильный путь на текущих примерах.
5. Является ли хранение дерева UTM в Яндекс.Метрике в качестве String и группировка по ним столь оптимальными, исходя из вопроса по Int/String?