← Назад к вопросам

Реализован ли у вас детальный слой в вашей системе

2.0 Middle🔥 141 комментариев
#Опыт работы и проекты#Хранилища данных и ETL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Детальный слой данных: архитектура хранилища

Что такое детальный слой

Детальный слой (Detail Layer) — это слой хранилища данных, содержащий исходные, минимально обработанные данные. Он находится между сырыми данными (Raw Layer) и слоем витрин (Mart Layer).

Архитектура обычно выглядит так:

Source Systems (CRM, Analytics, API)
    ↓
Raw Layer (сырые данные как есть)
    ↓
Detail Layer (очищено, дедублировано, готово к анализу)
    ↓
Mart Layer (витрины для BI и аналитики)
    ↓
Reports & Dashboards

Да, в моих проектах это было реализовано

Пример архитектуры в одной из компаний:

Мы работали с e-commerce платформой с 5+ млн транзакций в месяц. Структура была такой:

Raw Layer

-- Данные как есть с источников
raw_schema.transactions_raw (copy-paste из Event Stream)
raw_schema.users_raw (с CRM API)
raw_schema.products_raw (с товарного каталога)

Detail Layer

-- Очищенные, проверенные данные
detail_schema.transactions (
  transaction_id UUID PRIMARY KEY,
  user_id UUID,
  product_id UUID,
  order_date TIMESTAMP WITH TIMEZONE,
  amount DECIMAL(10, 2),
  currency VARCHAR(3),
  status VARCHAR(20),
  created_at TIMESTAMP WITH TIMEZONE DEFAULT now(),
  updated_at TIMESTAMP WITH TIMEZONE DEFAULT now()
)

detail_schema.users (
  user_id UUID PRIMARY KEY,
  email VARCHAR(255) UNIQUE NOT NULL,
  phone VARCHAR(20),
  country_code VARCHAR(2),
  registration_date DATE,
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMP WITH TIMEZONE
)

Mart Layer

-- Витрины для аналитики
mart_schema.user_metrics (
  user_id UUID,
  first_purchase_date DATE,
  last_purchase_date DATE,
  total_purchases INT,
  total_revenue DECIMAL(10, 2),
  avg_order_value DECIMAL(10, 2),
  days_since_last_purchase INT
)

mart_schema.daily_sales (
  date DATE,
  category VARCHAR(100),
  revenue DECIMAL(15, 2),
  orders INT,
  avg_order_value DECIMAL(10, 2)
)

Как мы наполняли детальный слой

Использовали dbt (Data Build Tool) для трансформации данных:

-- models/detail/transactions.sql

{{ config(
  materialized='table',
  tags=['daily']
) }}

WITH raw_data AS (
  SELECT
    transaction_id,
    user_id,
    product_id,
    order_date,
    amount,
    currency,
    status,
    created_at
  FROM {{ source('raw', 'transactions_raw') }}
  WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
),

validated_data AS (
  SELECT
    transaction_id,
    user_id,
    product_id,
    order_date,
    amount,
    CASE 
      WHEN amount < 0 THEN NULL 
      WHEN amount > 1000000 THEN NULL  -- outliers
      ELSE amount 
    END AS amount_validated,
    currency,
    status,
    CASE 
      WHEN status NOT IN ('completed', 'pending', 'cancelled')
      THEN 'unknown'
      ELSE status
    END AS status_clean,
    created_at,
    CURRENT_TIMESTAMP AS processed_at
  FROM raw_data
  WHERE user_id IS NOT NULL
    AND product_id IS NOT NULL
    AND order_date IS NOT NULL
)

SELECT * FROM validated_data

Процесс обработки данных

Этапы трансформации в Detail Layer:

  1. Дедупликация
WITH deduped AS (
  SELECT DISTINCT ON (transaction_id)
    *
  FROM raw_schema.transactions_raw
  ORDER BY transaction_id, created_at DESC
)
SELECT * FROM deduped
  1. Валидация
WHERE amount > 0
  AND amount < 10000000  -- разумный лимит
  AND user_id IS NOT NULL
  AND order_date IS NOT NULL
  AND order_date <= CURRENT_TIMESTAMP
  1. Стандартизация
SELECT
  transaction_id,
  UPPER(TRIM(currency)) as currency,  -- стандартные коды
  DATE_TRUNC('day', order_date) as order_date_normalized,
  COALESCE(status, 'unknown') as status
FROM raw_data
  1. Типизация
SELECT
  transaction_id::UUID,
  user_id::UUID,
  amount::DECIMAL(10, 2),
  order_date::TIMESTAMP WITH TIME ZONE,
  created_at::TIMESTAMP WITH TIME ZONE
FROM raw_data
  1. Добавление служебных полей
SELECT
  *,
  CURRENT_TIMESTAMP as loaded_at,
  CURRENT_DATE as loaded_date,
  MD5(CAST((transaction_id, user_id, amount) AS TEXT)) as source_hash
FROM validated_data

Управление инкрементальной загрузкой

-- Загружаем только новые/измененные записи
WHERE created_at > (
  SELECT MAX(created_at) FROM detail_schema.transactions
)
OR updated_at > (
  SELECT MAX(updated_at) FROM detail_schema.transactions
)

Качество данных в Detail Layer

Этап проверки качества (Data Validation):

# Python скрипт для dbt tests
import pandas as pd
from sqlalchemy import create_engine

engine = create_engine('postgresql://...')

# Тест 1: Нет NULL в критических полях
result = pd.read_sql("""
  SELECT COUNT(*) as null_count
  FROM detail_schema.transactions
  WHERE user_id IS NULL OR amount IS NULL
""", engine)

assert result['null_count'][0] == 0, "Found NULL values in critical fields"

# Тест 2: Отсутствие дубликатов
result = pd.read_sql("""
  SELECT transaction_id, COUNT(*)
  FROM detail_schema.transactions
  GROUP BY transaction_id
  HAVING COUNT(*) > 1
""", engine)

assert len(result) == 0, "Found duplicates"

# Тест 3: Данные свежие
result = pd.read_sql("""
  SELECT MAX(created_at) as max_date
  FROM detail_schema.transactions
""", engine)

max_date = result['max_date'][0]
assert (pd.Timestamp.now() - max_date).days < 1, "Data is stale"

Примеры витрин из этого слоя

-- Витрина для RFM анализа
CREATE OR REPLACE VIEW mart_schema.rfm_segment AS
WITH rfm_calc AS (
  SELECT
    user_id,
    MAX(order_date) as recency_date,
    COUNT(*) as frequency,
    SUM(amount) as monetary
  FROM detail_schema.transactions
  WHERE status = 'completed'
  GROUP BY user_id
),

ntile_calc AS (
  SELECT
    user_id,
    NTILE(5) OVER (ORDER BY recency_date DESC) as r_score,
    NTILE(5) OVER (ORDER BY frequency) as f_score,
    NTILE(5) OVER (ORDER BY monetary) as m_score
  FROM rfm_calc
)

SELECT
  user_id,
  CONCAT(r_score, f_score, m_score) as rfm_segment,
  CASE
    WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4 THEN 'Champions'
    WHEN r_score >= 3 AND f_score >= 3 AND m_score >= 3 THEN 'Loyal'
    WHEN r_score >= 2 THEN 'At Risk'
    ELSE 'Inactive'
  END as segment_name
FROM ntile_calc;

Преимущества детального слоя

  1. Разделение ответственности — Raw Layer содержит сырье, Detail Layer — чистые данные
  2. Переиспользование — витрины строятся из одного источника истины
  3. Отладка — легко найти проблему (на каком этапе она появилась)
  4. Performance — витрины работают быстро, потому что Detail Layer уже готов
  5. Версионирование — можешь менять витрины, не трогая Detail

SLA и мониторинг

Мы отслеживали:

-- Количество записей по дням
SELECT
  DATE(created_at) as load_date,
  COUNT(*) as record_count
FROM detail_schema.transactions
GROUP BY DATE(created_at)
ORDER BY load_date DESC
LIMIT 30;

-- Задержка загрузки
SELECT
  MAX(created_at) as last_source_timestamp,
  MAX(loaded_at) as last_load_timestamp,
  EXTRACT(HOUR FROM (MAX(loaded_at) - MAX(created_at))) as lag_hours
FROM detail_schema.transactions;

Заключение

Детальный слой — это не просто хорошая практика, это necessary foundation для любого серьезного хранилища данных. Без него ты либо работаешь с грязными данными, либо дублируешь логику очистки в каждой витрине.

Реализован ли у вас детальный слой в вашей системе | PrepBro