Привет, Хабр! Достаточно часто используются иерархические фильтры или отчеты с иерархией, и представление иерархии может быть актуально как для UI (например, иерархических фильтров), так и для отчетов или дашбордов. Если рассматривать только структуру запроса с иерархией, без расчета промежуточных итогов и т.д., то сохранение структуры иерархического UI элемента при большом уровне вложенности, а также передача этой иерархии с UI на бэкенд и дальше, например, в виде SQL запроса в СУБД может быть относительно нетривиальной задачей. При относительно большом уровне вложенности (например, иерархия в 10 уровней), при решении «в лоб» и сохранении всех 10 выбранных значений на последнем уровне иерархии, станет неудобно хранить и передавать в качестве параметров с UI на бэкенд (для 1000 строк и 10 уровней вложенности может быть уже условно 10000 параметров), также растет и количество параметров в SQL, и проблемы усугубляются в случае микросервисной архитектуры, когда запрос SQL не сразу отправляется, например, в ClickHouse, а ещё эти 10000 параметров «путешествуют» из UI в один или несколько микросервисов, пока не попадут в ClickHouse. В связи с этим хочу рассмотреть одно из возможных решений проблемы с помощью хеширования на примере C# и ClickHouse, но это «не идеи, проверенные на продакшене», больше тема к обсуждению. Тем, кому интересно решение проблем иерархических запросов на примере C# и ClickHouse — добро пожаловать под кат 🙂
Рассмотрим пример иерархического контрола (например, иерархического фильтра), причем у нас есть 2 продукта, 3 клиента, 2 склада, всего 12 записей, из них 3 записи выбраны.

Для наглядности создадим схему данных ClickHouse и 12 строк тестовых данных:
CREATE OR REPLACE TABLE sales ( order_number Int64, amount Float64, order_date Date, product_id Int64, customer_id Int64, store_id Int64 ) ENGINE = Log; INSERT INTO sales SELECT toString(1000000000 + number) AS order_number, number % 100 AS amount, dateAdd(number % 700, toDate('2023-01-01')), number / 6 % 2 + 1 AS product_id, number / 2 % 3 + 1 AS customer_id, number % 2 + 1 AS store_id FROM system.numbers LIMIT 12;
ClickHouse поддерживает murmurHash2_32, и здесь используется MurmurHash2 для иллюстрации, т.к. он быстрый, и более сложный для примера и не нужен, и коллизии достаточно просто разрешить проверкой реальных значений из запроса. По поводу объединения хешей — предлагается использовать ещё более простой подход, чем в ClickHouse, считаем, что для иллюстрации этого достаточно, т.е. обычный XOR для объединения двух хешей. Конечно, для использования в продакшене нужно учесть разные типы данных, выбрать подходящую инкрементальную хеш функцию и т.д.
Предлагается рассчитывать хеши вида:
SELECT murmurHash2_32(product_id) AS product_id_hash, murmurHash2_32(bitXor(customer_id, product_id_hash)) AS customer_id_hash, murmurHash2_32(bitXor(store_id, customer_id_hash)) AS store_id_hash, order_number, amount, order_date, product_id, customer_id, store_id FROM sales;
Это дает результат запроса такого вида (в выделенных строках — то, что соответствует иерархическому фильтру из UI, пока без фильтрации):

В качестве иллюстрации подсчитаем MurmurHash2 на C# для первых двух выбранных строк, т.е. для Product #1 и Customer #1 (что соответствует двум складам, Store #1 и Store #2).
using System; public class MurmurHash2 { public static uint Hash(byte[] data, uint seed = 0) { const uint m = 0x5bd1e995; const int r = 24; int length = data.Length; uint h = seed ^ (uint)length; int i = 0; while (length >= 4) { uint k = BitConverter.ToUInt32(data, i); k *= m; k ^= k >> r; k *= m; h *= m; h ^= k; i += 4; length -= 4; } switch (length) { case 3: h ^= (uint)(data[i + 2] << 16); goto case 2; case 2: h ^= (uint)(data[i + 1] << 8); goto case 1; case 1: h ^= data[i]; h *= m; break; } h ^= h >> 13; h *= m; h ^= h >> 15; return h; } public static void Main(string[] args) { const UInt64 product1 = 1; const UInt64 customer1 = 1; var bytesProduct1 = BitConverter.GetBytes(product1); UInt64 hashProduct1 = Hash(bytesProduct1); var combinedProduct1Customer1 = (hashProduct1 ^ customer1); var bytesCombinedProduct1Customer1 = BitConverter.GetBytes(combinedProduct1Customer1); UInt64 hashCombinedProduct1Customer1 = Hash(bytesCombinedProduct1Customer1); Console.WriteLine($"Hash value: {hashProduct1}, {hashCombinedProduct1Customer1}"); } }
Этот C# код возвращает следующие хеши для уровня Product и для уровня Customer: Hash value: 3340017859, 1304444856. Для Store значение не вычисляется в C#, т.к. нас устраивают оба склада Store #1 и Store #2.
SQL решения в лоб (конечно, можно упростить первые два условия в WHERE, здесь простейшее решение, для наглядности):
SELECT order_number, amount, order_date, product_id, customer_id, store_id FROM sales WHERE product_id = 1 AND customer_id = 1 AND store_id = 1 OR product_id = 1 AND customer_id = 1 AND store_id = 2 OR product_id = 1 AND customer_id = 3 AND store_id = 2;

Видно, что даже для 12 строк и трех уровней иерархии уже 9 параметров.
SQL решения через хеши для трех выбранных строк, соответствующих выбранным элементам из UI фильтра, причем фильтрация по хешу на уровне Customer дает две записи (со Store #1 и Store #2), и ещё одна фильтрация на уровне Store дает одну запись:
SELECT murmurHash2_32(product_id) AS product_id_hash, murmurHash2_32(bitXor(customer_id, product_id_hash)) AS customer_id_hash, murmurHash2_32(bitXor(store_id, customer_id_hash)) AS store_id_hash, order_number, amount, order_date, product_id, customer_id, store_id FROM sales WHERE customer_id_hash = 1304444856 OR store_id_hash = 3547175208;

Видно, что в WHERE с хешами всего два параметра, а в предыдущем решении 9.
Таким образом, видны преимущества работы с хешами, особенно если сохранить иерархические результаты во временную таблицу. Что касается коллизий — их легко разрешить, т.к. в результатах запроса есть все данные для этого. Также легко хранить структуру запроса (всего 2 хеша вместо 9 параметров WHERE), и наблюдаем снижение количества параметров — это удобнее для микросервисной архитектуры. Использование хешей может быть плюсом и при логировании параметров с точки зрения их шифрования. Конечно, для использования в продакшене нужно учесть разные типы данных, выбрать подходящую инкрементальную хеш функцию и т.д.
Надеюсь, такое решение может быть интересно в текущем виде, или вдохновит на лучшее 🙂
ссылка на оригинал статьи https://habr.com/ru/articles/893628/
Добавить комментарий