В современных data-процессах ключевую роль играет обеспечение качества данных. Рассмотрим четыре популярных подхода: DBT, SQL, Python (Pandas/SQLAlchemy) и Great Expectations, оценив их эффективность для различных сценариев проверки данных. 

Эта статья будет интересна и полезна Data-инженерам, аналитикам данных и специалистам Data Quality для выбора оптимального метода валидации данных в зависимости от стека технологий и сложности бизнес-логики. Материал ориентирован на начинающий уровень подготовки: тем, кто еще не сталкивался системно с инструментами управления качеством данных.

Привет, Хабр! Меня зовут Мария, я Data-инженер в SimbirSoft, и предлагаю для начала немного познакомиться с каждым из вышеперечисленных инструментов.

Содержание статьи

Общая информация

Контекст исследования

1. Сравнение методов проверки минимальной и максимальной даты

1.1 Реализация в DBT

1.2. Реализация в Python (Pandas)

1.3. Реализация в Python (SQLAlchemy)

1.4. Реализация в чистом SQL

1.5. Реализация в Great Expectations

2. Сравнение методов подсчета общего количества записей в таблице

2.1. Реализация в DBT

2.2. Реализация в Python (Pandas)

2.3 Реализация в Python (SQLAlchemy)

2.4. Реализация в чистом SQL

2.5. Реализация в Great Expectations

3. Сравнение методов проверки пустых значений в таблице

3.1. Реализация в DBT

3.2. Реализация в Python (Pandas)

3.3. Реализация в Python (SQLAlchemy)

3.4. Реализация в чистом SQL

3.5. Реализация в Great Expectations

4. Сравнение методов проверки дубликатов по бизнес-ключу

4.1. Реализация в DBT

4.2. Реализация в Python (Pandas)

4.3. Реализация в Python (SQLAlchemy)

4.4. Реализация в чистом SQL

4.5. Реализация в Great Expectations

5. Сравнение методов проверки качества данных в таблице raw_flight_data

5.1. Реализация в DBT

5.2. Реализация в Python (Pandas)

5.3. Реализация в Python (SQLAlchemy)

5.4. Реализация в чистом SQL (PL/pgSQL)

5.5. Реализация в Great Expectations

Анализ результатов тестирования производительности

Заключение

1. DBT 

DBT (Data Build Tool) — это инструмент для трансформации данных, который позволяет не только строить аналитические модели, но и встроенными средствами проверять их качество. Тестирование в DBT — ключевой механизм обеспечения надежности данных, помогающий выявлять ошибки, аномалии и нарушения бизнес-правил еще на этапе формирования данных.

Плюсы:

  • Встроенные тесты (unique, not_null).

  • Кастомные проверки на SQL/Python.

  • Инкрементальная валидация.

Минусы: Материализация результатов может затянуться.

DBT улучшает Data Quality с помощью встроенных тестов (Generic tests), кастомных тестов (Custom tests, с помощью SQL или Python), схемных тестов (Schema tests), инкрементальной валидации, интеграции с мониторингом.

2. SQL

SQL — это самый быстрый и прямой способ проверки качества данных с помощью прямых запросов и хранимых процедур. Он работает на уровне СУБД без накладных расходов на передачу данных в другие системы. 

Плюсы:

  • Молниеносные запросы.

  • Не требует сложных setup-ов.

  • Идеальна для ad-hoc-проверок.

Минусы: Нет встроенного мониторинга.

3. Python (Pandas / SQLAlchemy)

Pandas — библиотека Python для анализа и обработки табличных данных (как «Excel на стероидах»). Используется для чтения, фильтрации и преобразования данных; визуализации и статистического анализа.

SQLAlchemy — библиотека Python для работы с SQL-базами через Python-код. Используется для выполнения SQL-запросов без ручного соединения с БД.

Плюсы:

  •  Pandas — удобен для исследований.

  •  SQLAlchemy — баланс между скоростью SQL и гибкостью Python.

Минусы:

  •  Pandas «задыхается» на 8 млн. строк.

  •  Чанкирование спасает.

4. Great Expectations 

Great Expectations — фреймворк для автоматизированной валидации данных (как «пилот для вашего Data Quality»). Он необходим для проверки данных на соответствие правилам, документирования требований к данным, автоматических отчетов при ошибках, интеграции в ETL-пайплайны.

Плюсы:

  • Готовые «правила знакомств» (expectations) для данных — никаких неожиданностей!

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

Минусы: медленнее некоторых конкурентов.

Контекст исследования

В ходе исследования я применила dbt к реальному набору данных об авиаперелетах (Kaggle Dataset), чтобы оценить его возможности в области Data Quality.

1. Характеристики тестируемых таблиц:

Параметр

light_data (исходные данные)

raw_flight_data (DBT-модель)

Источник данных

Kaggle Dataset

Преобразованная модель на основе flight_data

Количество строк

8,249,965

87,657 (данные за 1 день)

Количество полей

27

28

Период данных

Полный период

Данные за 1 день (по searchDate)

2. Инфраструктура:

  •  СУБД: GreenPlum

  •  ОС: Ubuntu

3. Тестируемые сценарии:

Сценарий тестирования

Проверяемые параметры

Актуальные для Data Quality

1

Минимальная и максимальная бизнес-дата

Корректность временного периода данных

Полнота данных

2

Количество записей

Соответствие ожидаемому объему данных

Полнота данных

3

Количество записей 

с пустыми полями

Заполненность критически важных полей

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

4

Количество дубликатов по бизнес-ключу

Уникальность записей

Целостность данных

5

Проверка полей в DBT-модели (raw_flight_data)

Соответствие преобразованных данных требованиям

Качество трансформации

 

4. Методы тестирования.

Для каждого сценария использовались 5 подходов:

Метод

Версия

Описание

Преимущества

Ограничения

1

DBT

DBT-core=1.5.10

DBT-greenplum=1.5.0

DBT-labs/

DBT_utils= 0.8.0

Встроенные тесты и материализация таблиц

Встроенная валидация, документация

Требует знания YAML-синтаксиса

2

Pandas

pandas=2.1.4

 

Полная выгрузка в DataFrame (с чанкированием для сценария 3)

Гибкость анализа

Высокое потребление памяти

 

3

SQLAlchemy

SQLAlchemy=2.0.41

Выполнение SQL-запросов через Python-библиотеку

Баланс между гибкостью и производительностью

Требуется знание ORM

4

Чистый SQL (GP)

PostgreSQL=9.4.24 (Greenplum Database 6.12.0 build dev)

Напрямую в СУБД

Максимальная производительность

Меньше гибкости для сложных проверок

5

Great Expectations

great-expectations=1.5.2

 

Фреймворк для валидации данных

Богатые возможности валидации

Дополнительная настройка

1. Сравнение методов проверки минимальной и максимальной даты

1.1 Реализация в DBT 

{{
  config(
    materialized='table',
    compresstype='zlib'
  )
}}
 
SELECT 
  MIN(searchdate) AS min_search_date,
  MAX(searchdate) AS max_search_date
FROM {{ source('test', 'flight_data') }}
  • Используется материализация в таблицу с компрессией (zlib для экономии места).

  • Запрос выполняется непосредственно в DBT, что удобно для документирования и интеграции в пайплайн.

  • Подходит для регулярного мониторинга временного диапазона данных.

1.2. Реализация в Python (Pandas)

df = pd.read_sql_table(
    table_name='flight_data',
    con=engine,
    schema='test',
    columns=['searchdate']
    )
min_search_date = df['searchdate'].min()
max_search_date = df['searchdate'].max()
  • Для снижения ресурсоемкости в dataframe выгружается только 1 колонка.

  • Простота реализации, но требует больше памяти.

  • Подходит для единоразовых проверок или анализа в Jupyter-ноутбуках.

1.3. Реализация в Python (SQLAlchemy)

result = conn.execute(text(f"SELECT  MIN(searchdate) AS min_search_date, MAX(searchdate) AS max_search_date FROM {DB_SCHEMA}.flight_data;"))
 
row = result.fetchone()
  • Запрос выполняется на стороне СУБД, что эффективнее полной выгрузки.

  • Используется text() для RAW SQL, что дает гибкость, но требует аккуратности с инъекциями.

  • Оптимален для скриптов, где нужны только агрегированные результаты.

1.4. Реализация в чистом SQL

SELECT 
  MIN(searchdate) AS min_search_date,
  MAX(searchdate) AS max_search_date
FROM test.flight_data;
  • Самый быстрый метод, так как выполняется напрямую в СУБД.

  • Нет накладных расходов на ORM или выгрузку в Python.

  • Лучший выбор для ad-hoc-проверок в SQL-клиенте.

1.5. Реализация в Great Expectations

    expectation_date_range = gxe.ExpectColumnValuesToBeBetween(
        column="searchdate",
        min_value=datetime(2022, 4, 16),
        max_value=datetime(2022, 5, 4)
        )
    res = batch.validate(expectation_date_range)
  • Проверяет, что все даты попадают в ожидаемый диапазон (валидация против эталона).

  • Генерация детализированного отчета о валидации.

  • Полезно для регулярного мониторинга качества данных.

2. Сравнение методов подсчета общего количества записей в таблице

2.1. Реализация в DBT

  config(
    materialized='table',
    compresstype='zlib'
  )
}}

 
SELECT 
  count(1)
FROM {{ source('test', 'flight_data') }}

2.2. Реализация в Python (Pandas)

df = pd.read_sql_table(
table_name='flight_data',
con=engine,
schema='test',
columns=['legid']
)
 
count = df['legid'].count()

2.3 Реализация в Python (SQLAlchemy)

result = conn.execute(text(f"SELECT count(1) FROM {DB_SCHEMA}.flight_data;"))
 
row = result.fetchone()

2.4. Реализация в чистом SQL

SELECT count(1) FROM test.flight_data;

2.5. Реализация в Great Expectations

expectation_count = gxe.ExpectTableRowCountToBeBetween(min_value=2, max_value=9000000)
    res = batch.validate(expectation_count)
  • Проверка соответствия количества строк ожидаемому диапазону.

  • Позволяет установить границы допустимых значений.

3. Сравнение методов проверки пустых значений в таблице

Особенности реализации:

  • Все SQL-методы используют одинаковую логику подсчета.

  • Great Expectations дает наиболее детализированную информацию.

  • Pandas требует чанкирования для больших таблиц.

3.1. Реализация в DBT

{{
  config(
    materialized='table',
    compresstype='zlib'
  )
}}
 
select
    sum(case 
            when legid is null 
            or searchdate is null
            /* ... остальные 25 полей ... */
            or segmentscabincode is null
        then 1 else 0 
        end) as is_null
from {{ source('test', 'flight_data') }}
  • Полноценный SQL-запрос с проверкой всех 27 полей.

3.2. Реализация в Python (Pandas)

null_counts = pd.Series()
chunk_size = 50000
 
for chunk in pd.read_sql_table(
        'flight_data',
        engine,
        schema='test',
        chunksize=chunk_size
    ):
       
        chunk_null_counts = chunk.isnull().sum()
       null_counts = null_counts.add(chunk_null_counts, fill_value=0)
  • Построчная обработка данных чанками по 50,000 записей.

  • Постепенное суммирование null-значений по всем столбцам.

  • Требует значительных ресурсов памяти и времени.

3.3. Реализация в Python (SQLAlchemy)

result = conn.execute(text(f"""
select
    sum(case
            when legid is null
            or searchdate is null
            /* ... остальные 25 полей ... */
            or segmentscabincode is null
        then 1 else 0
        end) as is_null
from {DB_SCHEMA}.flight_data;"""))
 
row = result.fetchone()

3.4. Реализация в чистом SQL

select
    sum(case 
            when legid is null 
            or searchdate is null
           /* ... остальные 25 полей ... */
            or segmentscabincode is null
        then 1 else 0 
        end) as is_null
from test.flight_data;

3.5. Реализация в Great Expectations

columns = ["legid", "searchdate", ...] # все 27 полей
    n = 0
 
    for col in columns:
        print(f'{col}:')
        expectation_null = gxe.ExpectColumnValuesToNotBeNull(column=col)
        res = batch.validate(expectation_null)
        if res.success == False:
            print(f"{col} is null: {res.result["unexpected_count"]}")
            n += res.result["unexpected_count"]
  • Поочередная проверка каждого столбца.

  • Детализированный отчет по каждому полю.

  • Возможность установки различных правил валидации.

4. Сравнение методов проверки дубликатов по бизнес-ключу

4.1. Реализация в DBT

{{
  config(
    materialized='table',
    compresstype='zlib'
  )
}}
 
select coalesce(sum(cnt), count(1)) from 
   (select
      count(1) as cnt
    from {{ source('test', 'flight_data') }}
    group by legid, searchdate
    having count(1) > 1
    ) t
  • Использование COALESCE для корректной обработки случая без дубликатов.

  • Группировка по бизнес-ключу (legid + searchdate).

4.2. Реализация в Python (Pandas)

df = pd.read_sql_table(
    table_name='flight_data',
    con=engine,
    schema='test',
    columns=['legid', 'searchdate']
    )
 
grouped = df.groupby([
    "legid",
    "searchdate"
])
 
duplicate_keys = grouped.filter(lambda x: len(x) > 1)
 
duplicate_count = duplicate_keys.groupby([
    "legid",
    "searchdate"
]).ngroups

4.3. Реализация в Python (SQLAlchemy)

result = conn.execute(text(f"""
select coalesce(sum(cnt), count(1)) from
   ( select
        count(1) as cnt
    from {DB_SCHEMA}.flight_data
    group by legid, searchdate
    having count(1) > 1 ) t;"""))
 
row = result.fetchone()

4.4. Реализация в чистом SQL

select coalesce(sum(cnt), count(1)) from 
   (select
      count(1) as cnt
    from test.flight_data
    group by legid, searchdate
    having count(1) > 1
    ) t;

4.5. Реализация в Great Expectations

expectation_dbl = gxe.ExpectCompoundColumnsToBeUnique(
        column_list=["legid", "searchdate"]
    )
 
    res = batch.validate(expectation_dbl)

5. Сравнение методов проверки качества данных в таблице raw_flight_data

5.1. Реализация в DBT

models:
  - name: raw_flight_data
    columns:
      - name: legid
        tests:
          - not_null
         
      - name: flightdate
        tests:
          - not_null
          - DBT_utils.expression_is_true:
              expression: " >= searchdate"
         
      - name: startingairport
        tests:
          - not_null
          - DBT_utils.expression_is_true:
              expression: " != destinationairport" 
         
      - name: totalfare
        tests:
          - not_null
          - DBT_utils.expression_is_true:
              expression: " >= basefare"
  • Декларативный подход через YAML-конфигурацию.

  • Использование встроенных тестов (not_null) и кастомных проверок через dbt_utils.

  • Интеграция в DBT-пайплайн с автоматическим выполнением.

  • Легко читаемая и поддерживаемая конфигурация.

5.2. Реализация в Python (Pandas)

df = pd.read_sql_table(
        table_name='raw_flight_data',
        con=engine,
        schema=DB_SCHEMA,
        columns=['legid', 'flightdate', 'searchdate', 'startingairport', 'destinationairport', 'basefare', 'totalfare']
    )
   
    # Тест 1: legid не должен содержать NULL
    assert df['legid'].isnull().sum() == 0
   
    # Тест 2: flightdate >= searchdate
    assert (df['flightdate'] >= df['searchdate']).all()
   
    # Тест 3: startingairport != destinationairport
    assert (df['startingairport'] != df['destinationairport']).all()
   
    # Тест 4: totalfare >= basefare
    assert (df['totalfare'] >= df['basefare']).all()
  • Полная выгрузка данных в DataFrame.

  • Простые проверки через assert.

  • Позволяет гибко обрабатывать результаты.

  • Ресурсоемкий подход для больших таблиц.

5.3. Реализация в Python (SQLAlchemy)

   # Тест 1: Проверка NULL в legid
    null_legid = conn.execute(text(f"""
        SELECT COUNT(*) FROM {DB_SCHEMA}.raw_flight_data WHERE legid IS NULL
    """)).scalar()
    assert null_legid == 0
   
    # Тест 2: flightdate < searchdate
    invalid_dates = conn.execute(text(f"""
        SELECT COUNT(*) FROM {DB_SCHEMA}.raw_flight_data
            WHERE flightdate < searchdate
    """)).scalar()
    assert invalid_dates == 0
   
    # Тест 3: startingairport = destinationairport
    same_airports = conn.execute(text(f"""
        SELECT COUNT(*) FROM {DB_SCHEMA}.raw_flight_data
        WHERE startingairport = destinationairport
    """)).scalar()
    assert same_airports == 0
   
    # Тест 4: totalfare < basefare
    invalid_fares = conn.execute(text(f"""
        SELECT COUNT(*) FROM {DB_SCHEMA}.raw_flight_data
        WHERE totalfare < basefare
    """)).scalar()
    assert invalid_fares == 0
  • Выполнение SQL-запросов через SQLAlchemy.

  • Проверки происходят на стороне СУБД.

  • Более эффективен чем Pandas для больших таблиц.

5.4. Реализация в чистом SQL (PL/pgSQL)

CREATE OR REPLACE FUNCTION validate_flight_data()
RETURNS TABLE (
    test_name TEXT,
    error_count INTEGER,
    error_details TEXT
) AS $$
BEGIN
    -- 1. Проверка NULL в legid
    RETURN QUERY
    SELECT 
        'legid_not_null'::text AS test_name,
        COUNT(1)::int AS error_count,
        'Найдены записи с NULL в legid'::text AS error_details
    FROM raw.raw_flight_data
    WHERE legid IS NULL;

 
    -- 2. Проверка flightdate >= searchdate
    RETURN QUERY
    SELECT 
        'flightdate_after_searchdate'::text AS test_name,
        COUNT(*)::int AS error_count,
        'Найдены записи где flightdate раньше searchdate'::text AS error_details
    FROM raw.raw_flight_data
    WHERE flightdate < searchdate;

 
    -- 3. Проверка startingairport != destinationairport
    RETURN QUERY
    SELECT 
        'airports_different'::text AS test_name,
        COUNT(*)::int AS error_count,
        'Найдены записи с одинаковыми startingairport и destinationairport'::text AS error_details
    FROM raw.raw_flight_data
    WHERE startingairport = destinationairport;

 
    -- 4. Проверка totalfare >= basefare
    RETURN QUERY
    SELECT 
        'totalfare_ge_basefare'::text AS test_name,
        COUNT(*)::int AS error_count,
        'Найдены записи где totalfare меньше basefare'::text AS error_details
    FROM raw.raw_flight_data
    WHERE totalfare < basefare;

 
    RETURN;
END;
$$ LANGUAGE plpgsql;
  • Хранимая процедура в PostgreSQL.

  • Возвращает детализированный отчет об ошибках.

  • Максимальная производительность.

  • Сложность в поддержке и версионировании.

5.5. Реализация в Great Expectations

 # Тест 1: legid не должен содержать NULL
    expectation = gxe.ExpectColumnValuesToNotBeNull(column='legid')
    assert batch.validate(expectation).success
 
    # Тест 2: flightdate >= searchdate
    expectation = gxe.ExpectColumnPairValuesAToBeGreaterThanB(column_A='flightdate', column_B='searchdate')
    assert batch.validate(expectation).success
   
    # Тест 3: startingairport != destinationairport
    expectation = gxe.ExpectColumnPairValuesToBeEqual(column_A='startingairport', column_B='destinationairport')
    assert not batch.validate(expectation).success
   
    # Тест 4: totalfare >= basefare
    expectation = gxe.ExpectColumnPairValuesAToBeGreaterThanB(column_A='totalfare', column_B='basefare')
    assert batch.validate(expectation).success
  • Использование готовых проверок (Expectations).

  • Детализированные отчеты о валидации.

  • Возможность интеграции в пайплайны.

  • Требует настройки конфигурации.

Анализ результатов тестирования производительности:

 

 

DBT

Python/pandas

Python/sqlalchemy

Greate Expentations

SQL

Таблица

Время, сек

Время считывания в df, сек.

Время выполнения проверки, сек.

Всего, сек.

Время подключения к БД, сек.

Время выполнения проверки, сек.

Всего, сек.

Время получения батча, сек.

Время выполнения проверки, сек.

Всего, сек.

Время, сек

flight_data

1

7,29

30,13

0,03

30,16

0,19

1,58

1,77

0,1

3,64

3,74

1,14

flight_data

2

7,28

30,13

0,24

30,37

0,19

3,53

3,72

0,1

1,35

1,45

1,26

flight_data

3

7,21

327,7

24,74

352,44

0,19

2,65

2,84

0,1

130,44

130,54

2,27

flight_data

4

8,67

30,13

193,05

223,18

0,19

4,59

4,78

0,1

21,97

22,07

4,97

raw_flight_data

5

0,57

-

-

0,85

0,19

0,1

0,29

0,1

0,51

0,61

0,15

Общее время:

 

31,02

418,09

218,06

637

0,19

12,45

12,64

0,1

157,91

158,01

9,8

Наиболее эффективные методы:

  • Чистый SQL (9.8 сек. суммарно) — абсолютный лидер по скорости.

  • SQLAlchemy (12.64 сек.) — минимальная разница с чистым SQL благодаря эффективному использованию соединения.

  • DBT (31.02 сек.) — дополнительные затраты на материализацию данных.

Наименее эффективные методы:

  • Pandas (637 сек.) — 95% времени тратится на загрузку данных в DataFrame.

  • Great Expectations (158 сек.) — высокие накладные расходы на валидацию.

Сравнение DBT и чистого SQL:

  • Разница в 3,2 раза объясняется материализацией результатов.

  • Без материализации время выполнения было бы сопоставимо с чистым SQL.

Особые случаи:

  • Проверка пустых значений в Pandas заняла 352 сек. (в 155 раз медленнее SQL).

  • Поиск дубликатов в Great Expectations — 22 сек. против 5 сек. в SQL.

В конечном итоге DBT показал себя эффективным для проверки дубликатов и NULL-значений (встроенные тесты unique и not_null), инкрементального тестирования (например, для raw_flight_data). Pandas требовал больше ресурсов для обработки больших таблиц (особенно при чанкировании), поэтому его стоит использовать только при необходимости последующей обработки данных в Python и последующей визуализации. SQLAlchemy и чистый SQL оказались быстрее для простых агрегаций (например, min/max даты). Great Expectations добавил детализированные отчеты, но увеличил время выполнения. 

Рекомендации по выбору инструмента:

Метод

Преимущества

Недостатки

Подходящие сценарии

Производительность

Поддержка сложных проверок

DBT

Интеграция в пайплайн, документация

Требует инфраструктуры DBT, ограниченный набор тестов

Регулярные проверки в ETL

Высокая

Средняя (через макросы)

Pandas

Простота в использовании и отладки, гибкость

Ресурсоемкость

Исследовательский анализ в Jupyter

Низкая

Высокая

SQLAlchemy

Баланс скорости и гибкости

Нужно экранировать запросы

Разовые проверки, высоконагруженные запросы

Высокая

Высокая

Чистый SQL

Максимальная скорость

Ручное выполнение, сложность поддержки

Быстрых ad-hoc-проверок, высоконагруженные запросы

Очень высокая

Высокая

Great Expectations

Валидация против эталонов, готовые проверки, детальные отчеты

Сложность настройки, дополнительные зависимости

Комплексные проверки качества с детализированными отчетами

Средняя

Очень высокая

Заключение

Мы с вами рассмотрели четыре популярных подхода к обеспечению качества данных: классический SQL, DBT, библиотеку Pandas и фреймворк Great Expectations. 

Для production-решений с большими объемами данных оптимальны SQL-подходы (чистый SQL/SQLAlchemy). Они позволяют выполнять проверки на стороне базы данных, минимизируя передачу данных и снижая нагрузки на вычислительные ресурсы. DBT сохраняет баланс между удобством и производительностью для регулярных задач. Благодаря встроенным возможностям для тестирования и управляемой структуре моделей, DBT отлично подходит для регулярных задач проверки данных в рамках современных ELT-процессов. Pandas и Great Expectations следует использовать осознанно, учитывая их накладные расходы.

Выбор подхода всегда зависит от контекста: объема данных, требований к скорости, зрелости инфраструктуры и уровня команды. Надеемся, что этот обзор поможет вам выбрать подходящий инструмент и уверенно начать строить надежные и качественные data-процессы.

Спасибо за внимание!

Больше авторских материалов для backend-разработчиков от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.

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


  1. savostin
    22.12.2025 22:05

    Имхо у вас SQL = SQLAlchemy


  1. savostin
    22.12.2025 22:05

    Замените PostgreSQL на Clickhouse и будет счет вестить на миллисекунды ибо 8 млн. записей для него - мелочь.