Telegram Web
Завтра, в субботу 7 сентября, в 12:00 мск будет стрим-интервью с @digitalberd

Расскажу о своём пути, почему и как пришёл в менторство и в инженерию данных, затронем ещё пару тем.

https://www.youtube.com/live/Zh5WGo18-L8?si=OicFyIX2nr6VFHmE

Стрим начинается!
Александр вёл нешуточную борьбу с техническими неполадками.

Часть стрима провели в онлайне, но потом он всё же упал. Ссылка на то, что удалось спасти, в комментах.

Разошлись немного ожидания с Александром по формату, оказалось что это не совсем интервью.

Возможно проведу часть два так, как я сам это вижу. Stay tuned
#зачем_нужно

Кастомные материализации в dbt: мощный инструмент или поворот не туда? 1/2

🔸 dbt предоставляет стандартные материализации (table, view, incremental, ephemeral), которые покрывают большинство сценариев. Но иногда возникает потребность в чем-то особенном. Как и в airflow, в dbt можно переопределить всё что угодно и написать свою имплементацию Х, подложив код в папку с проектом.

🔸 Когда стоит задуматься о кастомных материализациях?
* Специфические требования к производительности
* Уникальные особенности вашего DWH
* Сложная бизнес-логика, выходящая за рамки стандартных подходов
* Необходимость в повторяющихся паттернах, которые не покрываются существующими материализациями. Например, когда при full-refresh нужно добавить промежуточные шаги для backfill'a или хочется добавить возможность скастить поле в table без пересчёта.

🔸 Важно помнить: кастомные материализации гарантированно усложнят проект, а полезными не факт, что будут. Перед их внедрением задай себе несколько вопросов:
* Действительно ли стандартные материализации не справляются с задачей? Может, я "не умею их готовить"?
* Можно ли решить проблему с помощью post-hooks или pre-hooks?
* Готова ли команда поддерживать кастомный код в долгосрочной перспективе?

🔸 Когда лучше воздержаться от кастомных материализаций:
* Команда не имеет глубокого понимания dbt и SQL или её воротит от одной мысли поддержки pascal-like скриптов
* Проект часто мигрирует между разными типами баз данных
* Нет четкого плана по поддержке и обновлению кастомного кода
* Кастомизация требуется только для одной или двух моделей
Кастомные материализации в dbt: мощный инструмент или поворот не туда? 2/2

Пример базовой кастомной материализации my_project/macros/my_materialization.sql:
{% materialization custom_table, default %}
{%- set target_relation = this.incorporate(type='table') %}

{{ run_hooks(pre_hooks) }}

-- в main нужно обязательно сформировать код, который отправится в `target/compile` и `target/run` директории для этой модельки
{% call statement('main') -%}
{{ create_table_as(target_relation, sql) }}
{%- endcall %}

{{ run_hooks(post_hooks) }}

{{ return({'relations': [target_relation]}) }}
{% endmaterialization %}


Тогда модель my_project/models/my_model.sql будет выглядеть так:
{{ config(materialized='custom_table') }}

select *
from {{ source("default", "my_raw_model") }}


В реальных сценариях нужно добавить специфичную для адаптера логику (например, через макросы и adapter-dispatch) или дополнительные шаги обработки. Изучи реализации, поставленные "из коробки", прежде чем писать своё :)

А тебе приходилось сталкиваться с ситуациями, когда кастомные материализации реально спасли проект? Или, может быть, наоборот, усложнили его до невозможности? Поделись опытом в комментариях!
Пока не вернул настрой на регулярные посты, мидлам и сеньорам рекомендую канал Василия Трещева по техническим нюансам работы greenplum.

Он крутой инженер "старой закалки", который в любой непонятной ситуации читает исходный код.

https://www.tgoop.com/GreenplumSQLClub
#термин_дня

Кратко о видах Executor в Airflow

Экзекьютор это компонент Airflow, который определяет, где и как будет запущен таск дага.

На момент написания поста (airflow 2.10) есть 4 основных варианта:
🔸 SequentialExecutor — включён по умолчанию, очень медленный, простой как палка. Плох даже для локальных дев-стендов и пет-проектов.

🔸 LocalExecutor — работает как часть Scheduler'a и не требует отдельных сервисов-воркеров. Поддерживает параллельный запуск, отлично подходит для локальной отладки и небольших дев-стендов.

🔸 CeleryExecutor использует Redis или RabbitMQ в качестве бэкенда для координации тасков между пред-настроенными воркерами. Отлично подходит для большого объёма коротких тасок. Также есть полезный UI flower для мониторинг, который можно запустить отдельным процессом. По моему опыту, Celery воркеры обычно запускаются на виртуальных машинах.

Ограничения: все воркеры должны быть строго одинаково настроены, включая набор и версии python пакетов и всяких CLI, синхронизацию DAGS_FOLDER (обычно через гит). Есть возможность обойти, но это сильно усложнит поддержку.

🔸 KubernetesExecutor — запускает отдельный под в K8s для каждой таски, можно выделить больше ресурсов для тяжёлых задач. Под уничтожается после окончания работы таски.

Этот режим добавляет накладные расходы (десятки секунд) на запуск и остановку подов, что лучше подходит для длинных и редко запускаемых задач. Серия мелких тасок будет тормозить. Зато можно высвобождать ресурсы и не платить 24/7 за простой воркеров.

🔸 И, как со всем в Airflow, можно написать свой Executor. Но должна быть уверенность, что задачу не получится решить через стандартные.

Для дочитавших бонус: с v2.10 можно сконфигурировать несколько экзекьюторов параллельно и назначать для каждой таски свой тип — или Celery, или K8s. Есть даже гибриды вида CeleryKubernetesExecutor, но они не рекомендуются командой Airflow.

Напиши в комментах, какие у тебя весёлые истории встречались с K8s экзекьютором, если используешь на проде :)
Эксперимент — серия постов будет выходить средними кусочками несколько дней подряд

#зачем_нужно

Проблемы и решения в очистке данных 1/?

При загрузке данных из исходных систем мы почти всегда сталкиваемся с "грязными" данными - опечатки, разные форматы, технические ошибки. Если не обработать такие случаи, таблицы перестанут джойниться или будут выдавать мусор на выходе (в BI, отчётах и пр.).

Изучение и очистка данных на первом этапе помогает избежать неприятных сюрпризов в будущем и сэкономить время на исправлении ошибок. Вот основные трансформации, с которыми ты можешь столкнуться. Синтаксис стараюсь брать из ANSI или распространённых надстроек:

🔶 Название поля не соответствует naming convention в DWH
column as new_column

Лучше хотя бы на raw слое оставить исходные названия колонок для lineage и traceability. И старайся не множить сущности, где возможно, приводи к единому стилю (naming convention) и называй одинаковые параметры одинаково, а разные — по-разному.

🔶 Формат даты

try_cast(date_column as date) /* для безопасного приведения */

to_date(date_string, 'YYYY-MM-DD') /* если известен формат */

case when date_column ~ '^\d{4}-\d{2}-\d{2}$' then cast(date_column as date) end /* с валидацией */


🔶 JSON, который нужно распарсить и разложить по колонкам

case when is_valid_json(json_column) then /* проверка валидности */
json_value(json_column, '$.field_name'),
json_query(json_column, '$.contacts[*].phone'), /* массив */
json_value(json_column, '$.address.city'), /* вложенный объект */
(select string_agg(value, ',')
from json_table(json_column, '$.tags[*]' columns (value varchar path '$'))
) as tags /* массив в строку */
end

Не забывай обрабатывать случаи с пустыми JSON'ами и массивами.

🔶 Вручную заполняемые поля "перечисляемого типа", которые нужно привести к одному виду

Использовать нечёткое сопоставление, например splink на python, или вручную заполненный маппинг значений, предварительно приведённых к trim(upper(replace(column, ' ', ''))) или другому подобному формату.

Написание запросов а-ля lower(col) like '%sub%string%' плохо масштабируется и зачастую приводит к неожиданным результатам (когда под шаблон начинают попадать "не те" категории).
#зачем_нужно

Проблемы и решения в очистке данных 2/?

🔶 Значение "по умолчанию" для отсутствия данных в виде пустой строки или "Nan", когда для обработки нужен NULL
case when trim(lower(column)) in (
'', 'null', 'none', 'n/a', 'na', '-',
'#n/a', '#н/д', '(empty)', 'undefined'
) or column ~ '^\s*$' /* только пробелы */
then null
else column
end as clean_column


🔶 Разный тип данных при union/union all или колонок из условия on в join
case when numeric_col ~ '^\d+(\.\d+)?$' 
then cast(numeric_col as decimal(18,2))
else null
end /* для последующих join по числовым полям */


cast(num_id as varchar(20)) as num_id /* для join, 
где с одной стороны поле varchar(20), а в другой -- числа */


union example:
select cast(id as varchar) as id, name from table1
union all
select id, name from table2 /* приводим к более широкому типу */


case when try_cast(date_field as date) is not null 
then try_cast(date_field as date)
else try_cast(date_field as timestamp)::date
end /* для разных форматов дат */


🔶 Вставка в таргет-таблицу NOT NULL поля из источника, где значение может отсутствовать
coalesce(column, 'default') as column 


🔶 У одного из объектов, объединяемых через union/union all, не достаёт колонки
cast(null as [data_type]) as column /* null может быть разных типов */


🔶 Разъединить одну колонку на несколько
split_part(full_name, ' ', 1) as surname,
split_part(full_name, ' ', 2) as name /* наивный подход,
для каждого отдельного случая может быть сильно сложнее, вплоть до регулярок */


🔶 Объединить несколько колонок в одну
concat_ws(' ', nullif(address_line_1, ''), nullif(address_line_2, '')) as address


А в этом посте уже было что-то новое? Делись в комментах
Альтернативная обложка для поста про NULL'ы
#зачем_нужно

Проблемы и решения в очистке данных 3/?

🔶 Даты из далёкого будущего или прошлого
 
case
when date_column between '1900-01-01' and '2100-12-31'
then date_column
else cast(null as date)
end as valid_date /* уточняй, как должно быть
по бизнес-требованиям. иногда даты из средне-далёкого
будущего это ок, например "плановая дата закрытия ипотеки" */


🔶 Объединить значения из нескольких строк в один массив

select key,
string_agg(values, ',')
from ...
group by key


🔶 Полные дубли (совпадают все поля)
 
select distinct col1, col2, col3
from table

/* или */

select col1, col2, col3
from table
group by col1, col2, col3

Вопрос со звёздочкой*: какая разница между distinct и group by в этом примере?

🔶 Неполные дубли (различаются технические поля)
with prep_cte as (
select col1, col2, business_key, updated_at,
row_number() over (
partition by business_key
order by coalesce(updated_at, '1900-01-01') desc
) as rn
from table
)
select * from prep_cte
where rn = 1
/* оставляем последнюю версию строки по каждому бизнес-ключу */


А здесь было что-то, что пригодится уже завтра на работе?
#зачем_нужно

Проблемы и решения в очистке данных 4/4


🔶 Нормализация unicode символов
 
translate(column, 'áéíóúàèìòùãẽĩõũâêîôûäëïöüçñ', 'aeiouaeiouaeiouaeiouaeioucn')

regexp_replace(column, '[^\x00-\x7F]+', '') /* удаление не ASCII символов */

Рассказать в следующем посте про colation настройки БД (или почему иногда сортировка одних и тех же данных может выдавать разный результат) ? :)

🔶 Конвертация часовых поясов
 
select timestampz_column at time zone 'Europe/Moscow'

coalesce(
try_cast(timestamp_column as timestamp with time zone),
timestamp_column::timestamp at time zone 'UTC'
) /* если не всегда указана временная зона */


🔶 Обработка спецсимволов в текстовых полях
 
regexp_replace(column, '[^a-zA-Z0-9\s]', '') /* только буквы, цифры и пробелы */

translate(column, '[]{}()*#$%@!?+=/\|', '') /* удаление конкретных символов */


🔶 Приведение булевых значений к стандартному виду
 
case
when lower(column) in ('1', 'yes', 'true', 'y', 't') then true
when lower(column) in ('0', 'no', 'false', 'n', 'f') then false
else null
end as bool_column


Ну что, как тебе формат? Голосуй реактами и комментами, если хочешь больше такого
2024/11/26 12:13:16
Back to Top
HTML Embed Code: