SQL NULL은 누락되거나 알려지지 않은 데이터를 나타냅니다. mssql-python 드라이버는 SQL NULL을 PythonNone에 매핑합니다. 이 구분이 중요한 이유는 NULL이 자신을 포함해 아무것도 의미하지 않기 때문입니다. SQL에서는 NULL = NULL가 NULL(알 수 없음)로 평가되며 참이 아니므로, 쿼리에서는 IS NULL를 사용하고 Python에서는 is None를 사용하세요.
NULL 값을 받으세요
가져오기 결과에서 NULL
드라이버는 SQL Server에서 Python None형식으로 NULL 값을 반환합니다:
import mssql_python
conn = mssql_python.connect(connection_string)
cursor = conn.cursor()
cursor.execute(
"SELECT TOP 1 FirstName, MiddleName, LastName "
"FROM Person.Person WHERE MiddleName IS NULL"
)
row = cursor.fetchone()
print(row.FirstName) # First name value
print(row.MiddleName) # None (NULL in database)
print(row.LastName) # Last name value
NULL 값 확인
결과를 반복 처리할 때 is 연산자를 사용하여 값이 None인지 확인하세요:
cursor.execute(
"SELECT FirstName, MiddleName, LastName FROM Person.Person WHERE BusinessEntityID <= 10"
)
for row in cursor:
if row.MiddleName is None:
print(f"{row.FirstName} {row.LastName}: No middle name")
else:
print(f"{row.FirstName} {row.MiddleName} {row.LastName}")
is None 대신 == None 사용
is None를 항상 NULL 검사에 사용하세요.
is 연산자는 동일성(즉, 값이 문자 그대로 None인지 여부)을 확인하는 반면, ==는 __eq__를 호출하며 사용자 정의 객체에서는 예상치 못한 결과를 낼 수 있습니다:
# Correct
if row.MiddleName is None:
full_name = f"{row.FirstName} {row.LastName}"
# Avoid (works but not idiomatic)
if row.MiddleName == None:
full_name = f"{row.FirstName} {row.LastName}"
NULL 값을 보내기
None으로 NULL 삽입
NULL 값을 삽입하려면 다음을 전달 None하세요:
cursor.execute(
"CREATE TABLE #NullInsertDemo "
"(Name NVARCHAR(50), Email NVARCHAR(100), Phone NVARCHAR(20))"
)
cursor.execute(
"INSERT INTO #NullInsertDemo (Name, Email, Phone) "
"VALUES (%(name)s, %(email)s, %(phone)s)",
{"name": "Alice", "email": None, "phone": "555-1234"}
)
conn.commit()
NULL로의 업데이트
열을 NULL로 설정하려면 다음 매개변수를 입력 None 합니다:
cursor.execute(
"CREATE TABLE #UpdateDemo (ID INT, Email NVARCHAR(100))"
)
cursor.execute("INSERT INTO #UpdateDemo VALUES (100, 'old@example.com')")
cursor.execute(
"UPDATE #UpdateDemo SET Email = %(email)s WHERE ID = %(id)s",
{"email": None, "id": 100}
)
conn.commit()
조건부 NULL 처리
선택적 매개변수를 처리하는 함수를 정의하면, 제공되지 않은 경우 다음과 None 같이 설정하세요:
def update_record(cursor, record_id: int, name: str, email: str | None = None):
"""Update record, setting email to NULL if not provided."""
cursor.execute(
"UPDATE #Records SET Name = %(name)s, Email = %(email)s "
"WHERE ID = %(id)s",
{"name": name, "email": email, "id": record_id}
)
WHERE절에서 NULL
쿼리에서 IS NULL입니다
NULL 비교를 위한 SQL의 활용 IS NULL :
# Find people without a middle name
cursor.execute("SELECT FirstName FROM Person.Person WHERE MiddleName IS NULL")
# Find people with a middle name
cursor.execute("SELECT FirstName FROM Person.Person WHERE MiddleName IS NOT NULL")
동적 NULL 처리
매개변수가 NULL일 수 있다면, 조건부 논리를 사용하여 적절한 쿼리를 구성합니다:
def find_people(cursor, middle_name: str | None = None):
"""Find people, optionally filtering by middle name."""
if middle_name is None:
# Find people with NULL middle name
cursor.execute("SELECT * FROM Person.Person WHERE MiddleName IS NULL")
else:
# Find people with specific middle name
cursor.execute(
"SELECT * FROM Person.Person WHERE MiddleName = %(middle_name)s",
{"middle_name": middle_name},
)
return cursor.fetchall()
NULL 치환을 위한 COALESCE
SQL 수준에서 NULL의 기본값을 대체하는 데 사용 COALESCE 하세요.
COALESCEPython에서 검사 None 하는 것보다 더 효율적입니다. 치환이 서버에서 이루어져 애플리케이션 내 조건부 논리의 양을 줄이기 때문입니다:
cursor.execute("""
SELECT
FirstName,
COALESCE(MiddleName, '(none)') AS MiddleName,
COALESCE(Suffix, 'N/A') AS Suffix
FROM Person.Person
WHERE BusinessEntityID <= 10
""")
for row in cursor:
# MiddleName and Suffix will never be None
print(f"{row.FirstName}: {row.MiddleName}, {row.Suffix}")
NULL-안전 연산
Python의 기본 값
cursor.execute("SELECT TOP 10 Name, Color FROM Production.Product")
for row in cursor:
# Use or to provide default
color = row.Color or "No color"
print(f"{row.Name}: {color}")
NULL 값 서식 지정
def format_address(row):
"""Format address handling NULL components."""
parts = [
row.AddressLine1,
row.AddressLine2,
row.City,
row.PostalCode,
]
# Filter out None values
return ", ".join(str(p) for p in parts if p is not None)
cursor.execute(
"SELECT TOP 10 AddressLine1, AddressLine2, City, PostalCode "
"FROM Person.Address"
)
for row in cursor:
print(format_address(row))
집계 시 NULL
SQL 집계 함수는 NULL 값을 예상하는 것과 다르게 처리합니다.
COUNT(column) NULL이 아닌 값만 세고, COUNT(*) 모든 행을 세는 값입니다.
AVG, , SUM, MIN그리고 MAX 모두 NULL 값을 무시합니다. 열의 모든 값이 NULL이라면, 이 함수들은 NULL을 반환합니다(0이 아닙니다).
# COUNT excludes NULL values
cursor.execute("SELECT COUNT(Color) FROM Production.Product") # Counts non-NULL colors
color_count = cursor.fetchval()
# COUNT(*) includes all rows
cursor.execute("SELECT COUNT(*) FROM Production.Product") # Counts all products
total_count = cursor.fetchval()
# AVG ignores NULL
cursor.execute("SELECT AVG(Weight) FROM Production.Product") # Average of non-NULL weights
average_weight = cursor.fetchval()
데이터 형식이 있는 NULL
NULL 숫자형 값
from decimal import Decimal
cursor.execute("SELECT ListPrice FROM Production.Product WHERE ProductID = 1")
row = cursor.fetchone()
# Check before arithmetic
if row.ListPrice is not None:
tax = row.ListPrice * Decimal("0.08")
total = row.ListPrice + tax
else:
total = Decimal("0")
NULL 날짜 값
비교나 계산에 사용하기 전에 날짜 열이 있는지 None 확인하세요:
from datetime import date
cursor.execute("SELECT Name, SellEndDate FROM Production.Product WHERE ProductID <= 10")
for row in cursor:
if row.SellEndDate is None:
print(f"{row.Name}: Currently selling")
else:
print(f"{row.Name}: Discontinued on {row.SellEndDate}")
NULL 문자열 값
문자열 열을 연결하기 전에 None인지 확인하여 NULL 문자열 열을 처리합니다:
cursor.execute(
"SELECT TOP 10 FirstName, MiddleName, LastName FROM Person.Person"
)
for row in cursor:
# Build full name, handling NULL middle name
if row.MiddleName:
full_name = f"{row.FirstName} {row.MiddleName} {row.LastName}"
else:
full_name = f"{row.FirstName} {row.LastName}"
print(full_name)
NULL을 사용한 대량 작업
NULL 값으로 executemany 실행
executemany()를 사용할 때는 NULL이어야 하는 열에 대해 사전에서 None를 전달하세요:
users = [
{"name": "Alice", "title": "Ms.", "suffix": "Jr."},
{"name": "Bob", "title": None, "suffix": "Sr."}, # NULL title
{"name": "Carol", "title": "Dr.", "suffix": None}, # NULL suffix
]
cursor.executemany(
"SELECT FirstName FROM Person.Person WHERE FirstName = %(name)s",
users
)
NULL이 포함된 대량 복사
대량 복사 작업은 데이터 구조에서 NULL 값을 보존합니다:
cursor = conn.cursor()
cursor.execute("CREATE TABLE ##NullDemo (Name NVARCHAR(50), Email NVARCHAR(100), Phone NVARCHAR(20))")
conn.commit()
data = [
("Alice", "alice@example.com", "555-0001"),
("Bob", None, "555-0002"), # NULL Email
("Carol", "carol@example.com", None), # NULL Phone
]
result = cursor.bulkcopy("##NullDemo", data)
conn.commit()
print(f"Copied {result['rows_copied']} rows")
일반적인 패턴
선택적 현장 처리
타입 힌트를 사용하여 행을 데이터클래스에 매핑할 때 어떤 필드가 NULL이 될 수 있는지 명확히 하세요:
from dataclasses import dataclass
from typing import Optional
@dataclass
class PersonRecord:
business_entity_id: int
first_name: str
middle_name: Optional[str] = None
suffix: Optional[str] = None
def fetch_person(cursor, person_id: int) -> Optional[PersonRecord]:
cursor.execute(
"SELECT BusinessEntityID, FirstName, MiddleName, Suffix "
"FROM Person.Person WHERE BusinessEntityID = %(id)s",
{"id": person_id},
)
row = cursor.fetchone()
if row is None:
return None
return PersonRecord(
business_entity_id=row.BusinessEntityID,
first_name=row.FirstName,
middle_name=row.MiddleName, # Will be None if NULL
suffix=row.Suffix, # Will be None if NULL
)
NULL을 이용한 JSON 직렬화
Python None 값은 json 모듈을 사용할 때 자동으로 JSON null으로 변환됩니다:
import json
cursor.execute(
"SELECT TOP 5 BusinessEntityID, FirstName, MiddleName FROM Person.Person"
)
rows = cursor.fetchall()
# Convert to JSON-serializable list
people = []
for row in rows:
people.append({
"id": row.BusinessEntityID,
"name": row.FirstName,
"middle_name": row.MiddleName, # None becomes null in JSON
})
json_output = json.dumps(people, indent=2)
print(json_output)
# [
# {"id": 1, "name": "Ken", "middle_name": "J"},
# {"id": 3, "name": "Roberto", "middle_name": null}
# ]
NULL 필터링이 적용된 사전입니다
행을 딕셔너리로 변환할 때 NULL 값 제외 선택:
def row_to_dict(row, cursor) -> dict:
"""Convert row to dict, optionally excluding NULL values."""
columns = [col[0] for col in cursor.description]
return {col: val for col, val in zip(columns, row) if val is not None}
cursor.execute("SELECT * FROM Person.Person WHERE BusinessEntityID = 1")
row = cursor.fetchone()
person_dict = row_to_dict(row, cursor)
# Only includes non-NULL columns
데이터프레임에서의 NULL
pandas나 Polars DataFrames를 다룰 때는 null 값에 특히 주의해야 하는데, 이 라이브러리들은 자체 센티넬 값을 사용하기 때문입니다.
판다 NaN과 NaT
pandas는 누락된 숫자형 및 문자열 값에 NaN(Not a Number)를 사용하고, 누락된 datetime 값에 NaT(Not a Time)를 사용합니다. 두 값 모두 PythonNone과 같지 않습니다:
import pandas as pd
import numpy as np
# When reading SQL results into pandas, NULL becomes NaN or NaT
cursor.execute("SELECT Name, Weight, SellEndDate FROM Production.Product")
table = cursor.arrow()
df = table.to_pandas()
# Check for missing values (covers NaN, NaT, and None)
print(df["Weight"].isna().sum()) # Count of NULL weights
print(df["SellEndDate"].isna().sum()) # Count of NULL dates
# Stage the data in a temp table to avoid mutating the source table
cursor.execute("CREATE TABLE #ProductWeights (Name NVARCHAR(100), Weight DECIMAL(8, 2) NULL)")
# Convert NaN back to None so NULL values round-trip correctly
for _, row in df.iterrows():
weight = None if pd.isna(row["Weight"]) else float(row["Weight"])
cursor.execute(
"INSERT INTO #ProductWeights (Name, Weight) VALUES (%(name)s, %(weight)s)",
{"name": row["Name"], "weight": weight}
)
Warning
비교 == np.nan== pd.NaT하지 마세요. 이 비교들은 항상 를 반환합니다 False. 대신 pd.isna() 또는 pd.notna()를 사용하십시오.
폴라스 널 핸들링
Polars는 자체 null 값(not NaN)을 사용하며, 이는 PythonNone에 직접 매핑됩니다:
import polars as pl
cursor.execute("SELECT Name, Weight, Color FROM Production.Product")
table = cursor.arrow()
df = pl.from_arrow(table)
# Filter rows with non-null values
has_weight = df.filter(pl.col("Weight").is_not_null())
# Replace null with a default
df = df.with_columns(pl.col("Color").fill_null("No color"))