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

Как оператор GROUP BY обрабатывает поля с NULL?

1.6 Junior🔥 181 комментариев
#SQL и базы данных

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

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

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

GROUP BY и NULL значения в SQL

GROUP BY обрабатывает NULL как одно значение. Все строки с NULL в группируемом столбце объединяются в одну группу. Это может быть источником ошибок, если вы не ожидаете это поведение.

Базовое поведение

-- Пример таблицы
CREATE TABLE sales (
    id INT,
    product_id INT,
    category VARCHAR(50),
    amount DECIMAL(10, 2)
);

INSERT INTO sales VALUES
(1, 101, 'Electronics', 1000),
(2, 102, NULL, 1500),
(3, 103, 'Electronics', 2000),
(4, 104, NULL, 1200),
(5, 105, 'Clothing', 800),
(6, 106, NULL, 900);

-- Что происходит с NULL при GROUP BY?
SELECT category, COUNT(*) as count
FROM sales
GROUP BY category;

-- Результат:
-- category    | count
-- -----------|-------
-- Electronics | 2
-- Clothing    | 1
-- NULL        | 3      <-- ВСЕ NULL объединены в одну группу!

Как это работает

import pandas as pd
import numpy as np

print("ПОВЕДЕНИЕ GROUP BY с NULL:")
print("="*60)

print("\nПринцип: SQL обрабатывает NULL как одно ЗНАЧЕНИЕ")
print("Все NULL-значения в GROUP BY считаются ОДИНАКОВЫМИ")
print("\nПример в Python (pandas):")

df = pd.DataFrame({
    'category': ['Electronics', None, 'Electronics', None, 'Clothing', None],
    'amount': [1000, 1500, 2000, 1200, 800, 900]
})

print("\nДанные:")
print(df)

print("\nГруппировка по category:")
result = df.groupby('category', dropna=False).agg({'amount': 'count'})
print(result)

print("\nВывод: NULL образует отдельную группу как обычное значение")

Различие между NULL и пустой строкой

-- Важный момент: NULL ≠ пустая строка ('')

CREATE TABLE test_null (
    id INT,
    description VARCHAR(100)
);

INSERT INTO test_null VALUES
(1, NULL),
(2, ''),
(3, 'Text'),
(4, NULL),
(5, '');

-- GROUP BY
SELECT description, COUNT(*) as count
FROM test_null
GROUP BY description;

-- Результат:
-- description | count
-- -----------|-------
-- (NULL)     | 2     <-- NULL значения
-- ''         | 2     <-- пустые строки (отдельно!)
-- 'Text'     | 1

-- Проверка:
SELECT 
    CASE WHEN description IS NULL THEN 'NULL' 
         WHEN description = '' THEN 'EMPTY' 
         ELSE 'VALUE' END as type,
    COUNT(*) as count
FROM test_null
GROUP BY description;

Проблемы и ошибки

Проблема 1: Неожиданное объединение NULL-значений

-- Ошибка: забыли про NULL
SELECT 
    department,
    AVG(salary) as avg_salary
FROM employees
GROUP BY department;

-- Если есть NULL-ы в department, они создадут отдельную группу
-- И вы получите непонятную строку с NULL в результатах

-- Правильно: явно обработайте NULL
SELECT 
    COALESCE(department, 'Unknown') as department,
    AVG(salary) as avg_salary
FROM employees
GROUP BY COALESCE(department, 'Unknown');

-- Или фильтруйте NULL
SELECT 
    department,
    AVG(salary) as avg_salary
FROM employees
WHERE department IS NOT NULL
GROUP BY department;

Проблема 2: Считание GROUP BY с NULL

-- Как правильно считать группы?

SELECT COUNT(DISTINCT category) as distinct_categories
FROM sales;

-- Результат: 3 (Electronics, Clothing, NULL)
-- Но NULL может быть не "категорией" в смысле бизнеса!

-- Правильно:
SELECT COUNT(DISTINCT category) as distinct_categories
FROM sales
WHERE category IS NOT NULL;

-- Результат: 2 (только real категории)

Проблема 3: HAVING с NULL

-- HAVING также обрабатывает NULL
SELECT 
    category,
    COUNT(*) as count
FROM sales
GROUP BY category
HAVING COUNT(*) > 1;

-- Результат:
-- category | count
-- ---------|------
-- Electronics | 2
-- NULL     | 3    <-- Группа NULL попадает в результат!

-- Если нужно исключить NULL:
SELECT 
    category,
    COUNT(*) as count
FROM sales
WHERE category IS NOT NULL  -- <-- Фильтруем ДО GROUP BY
GROUP BY category
HAVING COUNT(*) > 1;

COALESCE и GROUP BY

-- Безопасный способ работы с NULL

SELECT 
    COALESCE(category, 'Uncategorized') as category,
    COUNT(*) as count,
    SUM(amount) as total_amount
FROM sales
GROUP BY COALESCE(category, 'Uncategorized')
ORDER BY count DESC;

-- Результат:
-- category       | count | total_amount
-- --------------|-------|-------------
-- Uncategorized | 3     | 3600
-- Electronics   | 2     | 3000
-- Clothing      | 1     | 800

-- Это более читаемо и ясно показывает неклассифицированные товары

GROUP BY с CASE и NULL

-- Сложный случай: условная группировка с NULL

SELECT 
    CASE 
        WHEN category IS NULL THEN 'Unknown'
        WHEN category IN ('Electronics', 'Clothing') THEN 'Retail'
        ELSE 'Other'
    END as category_group,
    COUNT(*) as count,
    AVG(amount) as avg_amount
FROM sales
GROUP BY 
    CASE 
        WHEN category IS NULL THEN 'Unknown'
        WHEN category IN ('Electronics', 'Clothing') THEN 'Retail'
        ELSE 'Other'
    END
ORDER BY count DESC;

-- Результат:
-- category_group | count | avg_amount
-- --------------|-------|----------
-- Unknown       | 3     | 1200
-- Retail        | 3     | 1533.33

Множественные столбцы с NULL

-- Что если группируем по нескольким столбцам с NULL?

CREATE TABLE orders (
    id INT,
    customer_id INT,
    product_category VARCHAR(50),
    region VARCHAR(50)
);

INSERT INTO orders VALUES
(1, 1, 'Electronics', 'North'),
(2, 1, NULL, 'North'),
(3, 2, 'Electronics', NULL),
(4, 2, NULL, NULL),
(5, 3, 'Clothing', 'South');

-- GROUP BY с несколькими NULL столбцами
SELECT 
    customer_id,
    product_category,
    region,
    COUNT(*) as count
FROM orders
GROUP BY customer_id, product_category, region;

-- Результат: КАЖДАЯ комбинация NULL значений = отдельная группа
-- customer_id | product_category | region | count
-- -----------|-----------------|--------|------
-- 1           | Electronics     | North  | 1
-- 1           | NULL            | North  | 1  <-- (1, NULL, North)
-- 2           | Electronics     | NULL   | 1  <-- (2, Electronics, NULL)
-- 2           | NULL            | NULL   | 1  <-- (2, NULL, NULL)
-- 3           | Clothing        | South  | 1

Избегание проблем

import pandas as pd

print("СТРАТЕГИИ РАБОТЫ С NULL В GROUP BY:")
print("="*60)

print("\n1. ФИЛЬТРАЦИЯ ДО GROUP BY (когда логично):")
print("   SELECT category, COUNT(*)")
print("   FROM sales")
print("   WHERE category IS NOT NULL  -- Убираем NULL ДО группировки")
print("   GROUP BY category;")
print("   Когда использовать: категория должна быть непустой")

print("\n2. ЗАМЕНА NULL НА ЗНАЧЕНИЕ:")
print("   SELECT COALESCE(category, 'Unknown') as category, COUNT(*)")
print("   FROM sales")
print("   GROUP BY COALESCE(category, 'Unknown');")
print("   Когда использовать: хотим видеть NULL группу явно")

print("\n3. УСЛОВНАЯ ГРУППИРОВКА:")
print("   SELECT")
print("       CASE")
print("           WHEN category IS NULL THEN 'Uncategorized'")
print("           ELSE category")
print("       END,")
print("       COUNT(*)")
print("   FROM sales")
print("   GROUP BY CASE ... END;")
print("   Когда использовать: сложная логика обработки NULL")

print("\n4. ОТДЕЛЬНАЯ СТАТИСТИКА:")
print("   SELECT")
print("       category,")
print("       COUNT(*) as total,")
print("       COUNT(CASE WHEN col IS NOT NULL THEN 1 END) as non_null")
print("   FROM sales")
print("   GROUP BY category;")
print("   Когда использовать: анализ качества данных")

Практический пример

-- Реальная задача: Анализ продаж по категориям

-- НЕПРАВИЛЬНО (забыли про NULL):
SELECT 
    category,
    COUNT(*) as products,
    AVG(price) as avg_price
FROM products
GROUP BY category;

-- Результат содержит скрытую группу NULL, которая путает анализ

-- ПРАВИЛЬНО:
SELECT 
    COALESCE(category, 'Uncategorized') as category,
    COUNT(*) as products,
    AVG(price) as avg_price,
    CASE 
        WHEN category IS NULL THEN 'WARNING: Missing category'
        ELSE 'OK'
    END as data_quality
FROM products
GROUP BY COALESCE(category, 'Uncategorized')
ORDER BY products DESC;

В Pandas (Python)

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'category': ['Electronics', None, 'Electronics', None, 'Clothing'],
    'amount': [1000, 1500, 2000, 1200, 800]
})

print("Pandas GROUP BY с NULL:")
print(
    df.groupby('category', dropna=False).agg({
        'amount': ['count', 'sum', 'mean']
    })
)

# dropna=False: включать NaN как отдельную группу
# dropna=True (по умолчанию): исключать NaN группу

print("\nВывод: Используй dropna=False если хочешь видеть NULL группу")

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

print("\nКЛЮЧЕВЫЕ МОМЕНТЫ:")
print("="*60)

print("\n1. NULL в GROUP BY объединяются в ОДНУ группу")
print("   - Все NULL = одинаковые")
print("   - Образуют отдельную группу")

print("\n2. NULL ≠ пустая строка ('')")
print("   - NULL это отсутствие значения")
print("   - '' это строка без символов")
print("   - GROUP BY различает их")

print("\n3. Всегда обрабатывайте NULL явно")
print("   - Используйте COALESCE, CASE или WHERE")
print("   - Не полагайтесь на неявное поведение")

print("\n4. Лучшие практики:")
print("   - Фильтруйте NULL если они не нужны (WHERE)")
print("   - Заменяйте на значение если нужна группа (COALESCE)")
print("   - Используйте CASE для сложной логики")
print("   - Всегда документируйте это в коде")

ЗАВОДУ: GROUP BY обрабатывает NULL как обычное значение, создавая отдельную группу. Это может привести к ошибкам в анализе, если вы это забудете. Всегда явно обрабатывайте NULL в GROUP BY запросах!