Spark JOIN не сопоставляет NULL: когда использовать eqNullSafe

В фактах и справочнике есть строки с customer_id=NULL. После обычного JOIN они не совпадают, хотя на экране оба значения выглядят одинаково. Причина в SQL-семантике: NULL означает отсутствие известного значения, и сравнение NULL с NULL через равенство не даёт true.

Прежде чем менять условие, решите, должны ли такие строки вообще соединяться. Два неизвестных клиента не становятся одним человеком. Но NULL может означать согласованную общую категорию, например тариф без региона. Тогда null-safe сравнение соответствует договору данных.

Сравним два условия на одном входе

В PySpark у колонки есть eqNullSafe, эквивалент SQL-оператора <=>. Пример создаёт два факта и два уникальных элемента справочника. Левое соединение сохраняет оба факта в любом случае, меняется только найденное название категории.

from pyspark.sql import SparkSession, functions as F, types as T

spark = SparkSession.builder.master("local[1]").appName("null-join-demo").getOrCreate()
try:
    facts = spark.createDataFrame([(1, 10), (2, None)], "id long, region long").alias("f")
    names = spark.createDataFrame([(10, "North"), (None, "Default")],
                                  "region long, label string").alias("d")
    ordinary = facts.join(names, F.col("f.region") == F.col("d.region"), "left")
    safe = facts.join(names, F.col("f.region").eqNullSafe(F.col("d.region")), "left")
    ordinary_rows = ordinary.select("f.id", "d.label").orderBy("id").collect()
    safe_rows = safe.select("f.id", "d.label").orderBy("id").collect()
    assert [(r.id, r.label) for r in ordinary_rows] == [(1, "North"), (2, None)]
    assert [(r.id, r.label) for r in safe_rows] == [(1, "North"), (2, "Default")]
    safe.select("f.id", "d.label").orderBy("id").show()
finally:
    spark.stop()

Для второго факта обычное равенство оставит label пустым, null-safe выберет Default. Первый факт в обоих случаях получит North. Явные alias отделяют одинаково названные region и помогают избежать неоднозначности после соединения.

Если NULL в фактах означает неизвестного клиента, оставьте обычное условие и отдельно посчитайте число ненайденных. Если NULL в справочнике — одна осмысленная строка «по умолчанию», null-safe может быть верным. Решение определяется значением ключа, а не желанием убрать пустые значения из результата.

Проверьте уникальность пустого ключа

Две строки NULL в справочнике совпадут с каждым фактом NULL при eqNullSafe. Это размножит результат. Проверяйте groupBy по ключу и count, включая пустую группу. Для справочника, обещающего одно значение на категорию, каждая группа должна содержать не больше одной строки.

Замена NULL на ноль через coalesce не является нейтральным исправлением. В источнике может существовать настоящий идентификатор 0, и тогда неизвестные данные соединятся с реальным клиентом. Null-safe оператор хотя бы сохраняет исходное различие между пропуском и обычным значением.

Семантика важнее числа совпадений

Проверьте входы с известным ключом, неизвестным непустым ключом, NULL и дублирующим NULL в справочнике. Сравнивайте число фактов и назначенные категории. Увеличение доли совпадений само по себе не доказывает правильность соединения.

Производительность — следующий вопрос. После выбора корректного условия посмотрите план и распределение ключей. Большая пустая группа может создать перекос, но оптимизация shuffle не должна незаметно менять бизнес-правило сопоставления.

В Data Engineering Fast-Track вы отработаете Spark-преобразования, ключи и качество данных, затем включите расчёт в end-to-end pipeline. Откройте программу и научитесь проверять смысл соединения до настройки его скорости.

Продолжить на практике

  • Data Engineering Fast‑Track: Kafka → Airflow → Spark ↗

    Практический fast-track по data engineering: за 4–6 недель собираем рабочий конвейер Kafka → Spark → Lakehouse под управлением Airflow. Разберём batch и streaming, окна и watermark, схемы и контракты. Мини‑проекты, Docker Compose, шаблоны DAG и пайплайнов. Курс подойдёт инженерам, аналитикам и тем, кто хочет освоить современные пайплайны данных с уверенным Python.