Learning Platform
Глоссарий Troubleshooting
Урок 17.03 · 20 мин
Начальный
dbt_expectationsTestsData qualityPackages

dbt_expectations: продвинутые тесты

В уроках 7-8 модуля мы рассмотрели generic-тесты (not_null, unique, accepted_values, relationships). Они покрывают самое базовое — структурные проверки. Но что насчёт более тонких:

  • «Значения колонки должны быть в диапазоне 0-100.»
  • «Sum revenue не должен резко отличаться от вчерашнего ± 20%.»
  • «Длина email — между 5 и 100 символов.»
  • «Дата transaction’а — не в будущем.»

Generic тесты этого не покрывают. Можно писать singular tests (один SQL-файл на проверку), но это много кода. dbt_expectations — пакет от Calogica, port популярной Python-библиотеки Great Expectations на dbt. Десятки готовых параметризированных тестов.

В этом уроке — обзор, ключевые тесты, как организовать.


Установка

# packages.yml
packages:
  - package: dbt-labs/dbt_utils
    version: 1.3.0
  - package: calogica/dbt_expectations
    version: 0.10.4
dbt deps

После этого тесты dbt_expectations доступны через namespace dbt_expectations.*.

NOTE

dbt_expectations зависит от dbt_utils. Если ставите dbt_expectations — dbt_utils тоже должен быть в packages.yml с совместимой версией. dbt deps вам это скажет если что-то не сходится.


Категории тестов

dbt_expectations организованы по категориям из Great Expectations:

Категории тестов dbt_expectations

Всего — 50+ тестов. Обозревать все нет смысла. Покажу 10 самых полезных для junior-проекта.


Топ-10 тестов dbt_expectations для junior

1. expect_column_values_to_be_between

models:
  - name: fct_orders
    columns:
      - name: amount
        data_tests:
          - dbt_expectations.expect_column_values_to_be_between:
              min_value: 0
              max_value: 100000
              strictly: false   # >= 0 AND <= 100000, не >

Тест, что все значения колонки в range. Падает, если есть хоть одна строка вне диапазона.

Дополнительные параметры:

- dbt_expectations.expect_column_values_to_be_between:
    min_value: 0
    row_condition: "status = 'paid'"  # только для paid строк
    strictly: false

row_condition — SQL-фильтр, к которым применяется проверка. Очень удобно для conditional rules.

2. expect_column_values_to_match_regex

models:
  - name: stg_jaffle__customers
    columns:
      - name: email
        data_tests:
          - dbt_expectations.expect_column_values_to_match_regex:
              regex: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$"

Email-валидация, телефонные номера, ZIP codes — стандартный кейс.

WARNING

Regex для email — это не валидация настоящего email. RFC 5322 регекс — десятки строк, и даже он не гарантирует, что email доставится. Юзайте как basic sanity check, не как полную валидацию.

3. expect_column_value_lengths_to_be_between

models:
  - name: stg_jaffle__customers
    columns:
      - name: phone_number
        data_tests:
          - dbt_expectations.expect_column_value_lengths_to_be_between:
              min_value: 10
              max_value: 15

Длина строки в диапазоне. Удобно для phone numbers, ZIP codes, hash values.

4. expect_column_distinct_count_to_be_between

models:
  - name: stg_jaffle__orders
    columns:
      - name: status
        data_tests:
          - dbt_expectations.expect_column_distinct_count_to_be_between:
              min_value: 3
              max_value: 7

Количество уникальных значений в колонке — в диапазоне. Если у вас 6 valid статусов заказа (placed, paid, shipped, delivered, cancelled, refunded), и тест говорит «не должно быть меньше 3 и больше 7» — это catches новые непредвиденные статусы.

5. expect_table_row_count_to_be_between

models:
  - name: fct_orders
    data_tests:
      - dbt_expectations.expect_table_row_count_to_be_between:
          min_value: 1000
          max_value: 1000000

Проверка, что таблица в разумных размерах. Если внезапно 0 строк — значит pipeline сломался. Если 10М — что-то не так с дедупом.

6. expect_table_row_count_to_equal_other_table

models:
  - name: stg_jaffle__orders
    data_tests:
      - dbt_expectations.expect_table_row_count_to_equal_other_table:
          compare_model: source('jaffle', 'orders')

После staging-чистки rows должны совпадать с source. Если меньше — кто-то фильтрует случайно.

7. expect_column_values_to_be_in_type_list

models:
  - name: fct_orders
    columns:
      - name: order_date
        data_tests:
          - dbt_expectations.expect_column_values_to_be_of_type:
              column_type: date

Проверка типа колонки. Если кто-то случайно заменил DATE на VARCHAR — тест падает.

8. expect_row_values_to_have_data_for_every_n_datepart

models:
  - name: revenue_daily
    data_tests:
      - dbt_expectations.expect_row_values_to_have_data_for_every_n_datepart:
          date_col: date
          date_part: day
          interval: 1

Проверка, что нет gaps в датах. Должна быть строка на каждый день (или week, month и т.д.). Идеально для daily/weekly mart-таблиц.

Если за 2026-03-15 нет данных — тест падает с указанием, какие даты пропущены.

9. expect_column_values_to_be_increasing

models:
  - name: fct_orders
    columns:
      - name: order_id
        data_tests:
          - dbt_expectations.expect_column_values_to_be_increasing

Проверка монотонности. Полезно для sequence ID, timestamps, version numbers.

10. expect_column_pair_values_to_be_in_set

models:
  - name: fct_orders
    data_tests:
      - dbt_expectations.expect_column_pair_values_to_be_in_set:
          column_A: status
          column_B: is_paid
          value_pairs_set:
            - ['placed', false]
            - ['paid', true]
            - ['shipped', true]
            - ['delivered', true]
            - ['cancelled', false]
            - ['refunded', true]

Проверка cross-column rules. Например: «если status=‘placed’, is_paid должен быть false». Это бизнес-правила на уровне строки.


Полный пример: качество staging-модели

# models/staging/jaffle/_jaffle__models.yml
version: 2

models:
  - name: stg_jaffle__customers
    description: "Чищенные customers"
    data_tests:
      # Размер таблицы — sanity check
      - dbt_expectations.expect_table_row_count_to_be_between:
          min_value: 100
          max_value: 1000000

      # Strictly == size of source (без потерь после чистки)
      - dbt_expectations.expect_table_row_count_to_equal_other_table:
          compare_model: source('jaffle', 'customers')

    columns:
      - name: customer_id
        description: "Primary key"
        data_tests:
          - unique
          - not_null
          # Распределение значений
          - dbt_expectations.expect_column_distinct_count_to_equal_other_table:
              compare_model: source('jaffle', 'customers')
              compare_column: id

      - name: email
        description: "Email клиента"
        data_tests:
          - not_null
          # Email format
          - dbt_expectations.expect_column_values_to_match_regex:
              regex: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
          # Length sanity
          - dbt_expectations.expect_column_value_lengths_to_be_between:
              min_value: 5
              max_value: 100

      - name: created_date
        description: "Дата регистрации"
        data_tests:
          - not_null
          # Не в будущем
          - dbt_expectations.expect_column_values_to_be_between:
              max_value: "current_date"
              min_value: "cast('2010-01-01' as date)"

Это 8 тестов на одной модели, покрывающие: размер, ключи, формат email, длина email, диапазон дат.

Запустите dbt test --select stg_jaffle__customers. Каждый тест — отдельная query, результат — pass / fail с details.


severity и store_failures

Тесты могут быть error (default — падает CI) или warn (warning без crash). Используется для soft checks:

- dbt_expectations.expect_table_row_count_to_be_between:
    min_value: 1000
    max_value: 1000000
    config:
      severity: warn  # не падает CI, только warning

store_failures: true — сохранять failed rows в audit-таблицу для investigation:

- dbt_expectations.expect_column_values_to_be_between:
    min_value: 0
    max_value: 1000
    config:
      store_failures: true

После dbt test — в схеме появляется dbt_test__audit.{test_name} с failed rows. Можно поселектить и понять, какие конкретные строки failed.


Когда нужны expectations vs generic tests

ПроверкаГде
customer_id IS NOT NULLgeneric not_null
customer_id UNIQUEgeneric unique
(order_id, line_id) UNIQUEdbt_utils.unique_combination_of_columns
customer_id REFERENCES customersgeneric relationships
status IN ('placed', 'paid', ...)generic accepted_values
amount BETWEEN 0 AND 100000dbt_expectations.expect_column_values_to_be_between
email matches regexdbt_expectations.expect_column_values_to_match_regex
row count between 1000 and 1Mdbt_expectations.expect_table_row_count_to_be_between
no gaps in datesdbt_expectations.expect_row_values_to_have_data_for_every_n_datepart
business rule on multiple columnsdbt_expectations.expect_column_pair_values_to_be_in_set

Generic — структура. dbt_utils — composite uniqueness, custom SQL. dbt_expectations — values, ranges, distributions, cross-column.


Performance

Каждый тест — это отдельный SELECT в warehouse. На большой таблице это:

  1. Затратно: каждый тест запускает full scan.
  2. Параллелизуется через threads: в profiles.yml.

Best practices:

  • Не тестируйте всё подряд. Тесты на staging, marts — да. Тесты на 50 intermediate-моделей — overkill.
  • Limit scope. row_condition: "status = 'paid'" ограничивает scan.
  • store_failures — только для дебага. В обычном CI выключайте.
  • В larger projects тесты разделены: dbt test --select tag:critical в каждом merge, dbt test (полный) — ежедневно.

Антипаттерны

Антипаттерны dbt_expectations

Попробуй сам

В вашем dbt-проекте:

  1. Установите dbt_expectations:
packages:
  - package: dbt-labs/dbt_utils
    version: 1.3.0
  - package: calogica/dbt_expectations
    version: 0.10.4
dbt deps
  1. Добавьте 3 теста в _jaffle__models.yml:
models:
  - name: stg_jaffle__orders
    columns:
      - name: amount
        data_tests:
          - not_null
          - dbt_expectations.expect_column_values_to_be_between:
              min_value: 0
              max_value: 10000

      - name: order_date
        data_tests:
          - not_null
          - dbt_expectations.expect_column_values_to_be_between:
              max_value: "current_date"
              min_value: "cast('2020-01-01' as date)"
    data_tests:
      - dbt_expectations.expect_table_row_count_to_be_between:
          min_value: 10
          max_value: 100000
dbt test --select stg_jaffle__orders
  1. Specifically test что date не в будущем. Если у вас тестовые данные — добавьте строку с order_date = '2030-01-01' в seed (если есть). Прогоните тест — он fail.

Бонус: добавьте expect_column_values_to_match_regex для email validation. Попробуйте найти tests for timeseries-data: expect_row_values_to_have_data_for_every_n_datepart для revenue_daily.


Ключевые выводы

  1. dbt_expectations — пакет от Calogica, port Great Expectations на dbt. Намного больше тестов чем generic.
  2. Категории: table shape, column values, column aggregates, distribution, string matching, dates.
  3. Топ тесты для junior: expect_column_values_to_be_between, expect_column_values_to_match_regex, expect_column_value_lengths_to_be_between, expect_table_row_count_to_be_between, expect_row_values_to_have_data_for_every_n_datepart.
  4. row_condition — фильтр для conditional tests.
  5. severity: warn — soft check. store_failures: true — audit-таблица с failed rows (для дебага).
  6. Когда expectations vs generic: generic для структуры (not_null, unique), expectations для values / ranges / distributions / cross-column rules.
  7. Performance: каждый тест — отдельный SELECT. Не тестируйте всё подряд; таргетьте critical paths.
  8. Антипаттерны: 100 тестов на модель, magic numbers без объяснения, store_failures всегда.
dbt_expectations: полный разбор Data quality: стратегии валидации
Проверка знанийKnowledge check
Команда хочет проверять, что в `revenue_daily` нет gaps в датах (каждый день должна быть строка). Как реализовать?
ОтветAnswer
Используйте `dbt_expectations.expect_row_values_to_have_data_for_every_n_datepart`:\n\n```yaml\nmodels:\n - name: revenue_daily\n description: "Ежедневная выручка"\n data_tests:\n - dbt_expectations.expect_row_values_to_have_data_for_every_n_datepart:\n date_col: date\n date_part: day\n interval: 1\n```\n\nЧто проверяет:\n\n1. Находит min(date) и max(date) в таблице.\n2. Генерирует ожидаемую последовательность дат с шагом 1 день.\n3. Сравнивает с actual датами в колонке.\n4. Если есть **пропущенный день** — тест падает с указанием, какие даты пропущены.\n\nДополнительные параметры:\n\n```yaml\n- dbt_expectations.expect_row_values_to_have_data_for_every_n_datepart:\n date_col: date\n date_part: day\n interval: 1\n test_start_date: "cast('2025-01-01' as date)" # начать с этой даты\n test_end_date: "current_date" # до сегодняшнего дня\n row_condition: "is_weekday = true" # только weekdays\n exclusion_condition: "date = '2025-12-25'" # исключения (праздники)\n```\n\nЭто **критичный тест** для time-series март: если pipeline вчера пропустил день, dashboard будет показывать gap. Тест падает на следующий run — alert.\n\nAlternative — singular test с recursive CTE для генерации дат и LEFT JOIN. Но это 30 строк SQL vs одна YAML-запись. Использовать expectations.
Проверка знанийKnowledge check
Команда добавила 50 dbt_expectations тестов. Теперь `dbt test` занимает 25 минут в CI, и иногда тайм-аутит. Как оптимизировать?
ОтветAnswer
Проблема: каждый тест — отдельный SELECT в warehouse. На 50 тестах * 5 минут scan = 25 минут на single-threaded. Решения:\n\n**1. Параллелизация — увеличить threads в profiles.yml**:\n\n```yaml\nprod:\n outputs:\n prod:\n type: duckdb\n threads: 16 # было 4, увеличили\n```\n\nЭто параллельно запустит до 16 тестов. DuckDB single-writer, но multi-reader — works for tests.\n\n**2. Tier по criticality:**\n\n```yaml\n- dbt_expectations.expect_column_values_to_be_between:\n min_value: 0\n config:\n severity: warn # не критичные — soft\n tags: ['quality_check']\n\n- not_null # критичные — hard fail\n config:\n tags: ['critical']\n```\n\nВ CI:\n```bash\ndbt test --select tag:critical # 5 минут, на каждый merge\ndbt test --select tag:quality_check # 25 минут, ежедневно ночью\n```\n\n**3. Limit scope тестов**:\n\n```yaml\n- dbt_expectations.expect_column_values_to_be_between:\n min_value: 0\n row_condition: "order_date не меньше current_date - 7" # только последние 7 дней\n```\n\nВместо full table scan — только recent rows.\n\n**4. Уберите дубли**:\n\nЕсли есть generic `not_null`, не добавляйте дополнительно `expect_column_values_to_not_be_null` — это same check. Аудитуйте тесты, ищите пересекающиеся.\n\n**5. Включайте store_failures только при дебаге**, не в каждом run.\n\n**6. Не тестируйте intermediate**: они эфемерные, тестов на них минимум. Фокус на staging + marts.\n\nОбычно после tier-ования + параллелизации — CI тестов уменьшается с 25 до 5-7 минут.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Команда хочет проверить: в `fct_orders.amount` все значения должны быть между 0 и 100 000. Generic `accepted_values` не подходит (это для категорий). Какой тест использовать?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4