learn

Python에서 MySQL 연결하기

Python에서 MySQL 연결하기

드라이버 선택

드라이버는 크게 두 가지다.

mysql-connector-python — Oracle 공식 드라이버

pip install mysql-connector-python

PyMySQL — 순수 Python 구현, 더 가볍고 실제로 많이 쓰임

pip install pymysql

둘 다 DB-API 2.0 표준을 따르므로 사용법이 거의 동일하다.


기본 연결 (동기)

import pymysql

conn = pymysql.connect(
    host="localhost",
    port=3306,
    user="root",
    password="...",
    database="mydb",
    charset="utf8mb4"
)

try:
    with conn.cursor() as cursor:
        cursor.execute("SELECT * FROM users WHERE id = %s", (1,))
        row = cursor.fetchone()
        print(row)
    conn.commit()
finally:
    conn.close()

핵심 주의사항:

  • 파라미터는 반드시 %s placeholder 사용 — f-string으로 직접 삽입하면 SQL injection 위험
  • conn.commit()은 INSERT / UPDATE / DELETE 후 필수. SELECT만 할 때는 불필요
  • cursor는 with 블록으로 열면 자동 close

서버 환경 — 커넥션 풀 (비동기)

FastAPI처럼 비동기 서버 환경에서는 매 요청마다 connect/close를 반복하면 오버헤드가 크다. aiomysql + 커넥션 풀을 쓰는 것이 맞다.

import aiomysql

# lifespan에서 풀 생성
pool = await aiomysql.create_pool(
    host="localhost", port=3306,
    user="root", password="...",
    db="mydb", minsize=1, maxsize=10
)

# 라우터에서 사용
async with pool.acquire() as conn:
    async with conn.cursor() as cur:
        await cur.execute("SELECT ...")
        result = await cur.fetchall()

풀 생성은 FastAPI의 lifespan-context-manager|lifespan 패턴으로 앱 시작 시 한 번만 하는 것이 권장된다.


ORM vs Raw SQL

SQLAlchemy 같은 ORM을 쓰면 테이블을 Python 클래스로 매핑할 수 있다. 하지만:

  • LLM tool call이나 단순 쿼리 → raw SQL이 낫다. 범용성이 높고 직관적
  • 복잡한 앱 레이어 (관계 매핑, 마이그레이션 관리 등) → ORM이 개발 생산성을 높여줌

ORM은 쿼리 실행 성능을 약간 희생하는 대신 개발자 생산성을 얻는 트레이드오프다.


결과를 dict으로 받기 — DictCursor

기본 cursor는 결과를 tuple로 반환한다. DictCursor를 쓰면 컬럼명을 key로 갖는 dict로 받을 수 있다.

with conn.cursor(pymysql.cursors.DictCursor) as cursor:
    cursor.execute("SELECT id, name, age FROM users")
    rows = cursor.fetchall()

# rows = [
#   {"id": 1, "name": "Alice", "age": 25},
#   {"id": 2, "name": "Bob",   "age": 30},
# ]

pandas DataFrame으로 변환

dict 리스트를 그대로 pd.DataFrame에 넘기면 된다. 컬럼 순서는 SELECT 순서를 따른다.

import pandas as pd

df = pd.DataFrame(rows)
#    id   name  age
# 0   1  Alice   25
# 1   2    Bob   30

분석 스크립트라면 — pd.read_sql

pandas에는 DB 연결을 직접 받는 read_sql이 있다. DictCursor 과정을 내부에서 처리해줘서 코드가 더 짧다.

import pandas as pd
import pymysql

conn = pymysql.connect(...)
df = pd.read_sql("SELECT id, name, age FROM users WHERE age > %s", conn, params=(20,))
conn.close()

상황별 선택:

  • 서버 코드 (FastAPI 등) → DictCursor로 dict 리스트 반환
  • 분석 스크립트 → pd.read_sql이 가장 간결

MySQL DECIMAL 타입 → float 변환

MySQL의 DECIMAL 컬럼은 Python으로 올 때 decimal.Decimal 객체로 들어온다. pandas/numpy는 이를 float으로 자동 변환하지 못해서 연산 시 TypeError가 발생한다.

TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float'

가장 간단한 해결 — 컬럼 단위 변환:

df["daily_qty"] = df["daily_qty"].astype(float)

Decimal 컬럼 전체를 한 번에:

import decimal

for col in df.columns:
    if df[col].apply(lambda x: isinstance(x, decimal.Decimal)).any():
        df[col] = df[col].astype(float)

근본적으로 막으려면 — 쿼리 단에서 CAST:

SELECT CAST(daily_qty AS FLOAT) AS daily_qty, month FROM ...

DB에서 아예 float으로 내려보내면 Python 쪽에서 신경 쓸 필요가 없다. 재사용이 잦은 쿼리라면 이 방법이 가장 깔끔하다.