Pitfalls & Anti-Patterns¶
Common mistakes when working with CUBRID + Python, and how to avoid them.
1. Creating a New Connection Per Request¶
Problem: A fresh engine and connection took ~5.9 ms per query versus ~0.8 ms from a pool (pycubrid 1.10.0, 2026-10-09). At scale this adds up fast and may exhaust server connection limits.
# ❌ Anti-pattern — new connection on every request
@app.get("/items")
def list_items():
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
cursor = conn.cursor()
cursor.execute("SELECT id, val FROM cookbook_items")
rows = cursor.fetchall()
cursor.close()
conn.close() # Connection destroyed, never reused
return rows
Fix: Use a connection pool via SQLAlchemy or a shared connection manager.
# ✅ Correct — connection pool reuses connections
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine(
"cubrid+pycubrid://dba@localhost:33000/testdb",
pool_size=5,
pool_pre_ping=True,
)
SessionLocal = sessionmaker(bind=engine)
@app.get("/items")
def list_items():
with SessionLocal() as session:
# Session returns connection to pool on exit
return session.execute(text("SELECT id, val FROM cookbook_items")).all()
See: performance/connection-pooling/
2. N+1 Query Problem¶
Problem: Loading related objects in a loop fires one query per parent row.
# ❌ Anti-pattern — 1 query for categories + N queries for items
categories = session.execute(select(CookbookCategory)).scalars().all()
for cat in categories:
print(cat.name, len(cat.items)) # Each access triggers a lazy-load query
Fix: Use joinedload or selectinload to prefetch relationships.
from sqlalchemy.orm import selectinload
# ✅ Correct — 2 queries total (1 for categories, 1 for all items)
stmt = select(CookbookCategory).options(selectinload(CookbookCategory.items))
categories = session.execute(stmt).scalars().all()
for cat in categories:
print(cat.name, len(cat.items)) # Already loaded, no extra query
| Strategy | Queries | Use When |
|---|---|---|
selectinload |
2 (parent + children via IN) | Default choice, works well for most cases |
joinedload |
1 (single JOIN) | Small result sets, single child relationship |
subqueryload |
2 (parent + subquery) | Complex filters on parent query |
3. Committing Per Row in Batch Operations¶
Problem: COMMIT is the most expensive operation (INSERT + COMMIT ~36 ms per row on pycubrid 1.10.0, 2026-10-09). Per-row commits made inserts about 45x slower than a COMMIT every 500 rows.
# ❌ Anti-pattern — COMMIT per row (10K rows ≈ 357 s, extrapolated from the 500-row run, pycubrid 1.10.0)
for i in range(10000):
cursor.execute("INSERT INTO cookbook_logs (msg) VALUES (?)", (f"log_{i}",))
conn.commit() # ~36 ms × 10,000 ≈ 357 seconds (extrapolated)
Fix: Batch your commits.
# ✅ Correct — COMMIT per batch (10K rows ≈ 12 seconds)
BATCH_SIZE = 1000
for batch_start in range(0, 10000, BATCH_SIZE):
for i in range(batch_start, min(batch_start + BATCH_SIZE, 10000)):
cursor.execute("INSERT INTO cookbook_logs (msg) VALUES (?)", (f"log_{i}",))
conn.commit() # only 10 COMMITs instead of 10,000
4. Using CUBRID Reserved Words as Column Names¶
Problem: CUBRID reserves many common words — for example key, value, count, data, date, day, level, size and user. Using one as an unquoted column name fails with a syntax error that often points at the next token rather than the reserved word itself.
# ❌ Fails — both "key" and "value" are CUBRID reserved words
cursor.execute("CREATE TABLE cookbook_settings (key VARCHAR(50), value VARCHAR(255))")
# pycubrid.ProgrammingError: ... Syntax error: unexpected 'VARCHAR' ...
Renaming only value is not enough: (key VARCHAR(50), val VARCHAR(255)) is rejected in the same way, because key is reserved too.
Fix: Rename every reserved column to a non-reserved name.
# ✅ Correct — no reserved words left in the DDL
cursor.execute("CREATE TABLE cookbook_settings (setting_key VARCHAR(50), val VARCHAR(255))")
| Reserved Word | Replacement |
|---|---|
key |
setting_key (or another descriptive *_key) |
value |
val |
count |
cnt |
data |
file_data |
date / day |
created_date / metric_day |
If a reserved name is unavoidable (for example, an existing schema), quote it with double quotes, square brackets or backticks — CUBRID accepts all three — and quote it in every statement that uses it:
cursor.execute('CREATE TABLE cookbook_settings ("key" VARCHAR(50), "value" VARCHAR(255))')
cursor.execute('INSERT INTO cookbook_settings ("key", "value") VALUES (?, ?)', ("theme", "dark"))
name is not a reserved word and can be used unquoted. Check candidate names against the official reserved-word list for your server version: CUBRID 11.2, CUBRID 11.4. SQLAlchemy's CUBRID dialect quotes reserved words in generated DDL automatically; see KNOWN_ISSUES.md.
Every statement in this section is checked against a live server by reserved-words/01_reserved_words.py (make verify, CUBRID 11.2 and 11.4 in CI).
5. SQL String Interpolation¶
Problem: Building queries with f-strings or % formatting opens the door to SQL injection and type errors.
# ❌ Anti-pattern — SQL injection vulnerability
user_input = "'; DROP TABLE cookbook_items; --"
cursor.execute(f"SELECT * FROM cookbook_items WHERE val = '{user_input}'")
Fix: Always use parameterized queries.
# ✅ Correct — parameterized query (pycubrid uses ? placeholders)
cursor.execute("SELECT * FROM cookbook_items WHERE val = ?", (user_input,))
# ✅ Correct — SQLAlchemy ORM (automatically parameterized)
stmt = select(CookbookItem).where(CookbookItem.val == user_input)
session.execute(stmt)
6. Blocking Sync Operations in Async Context¶
Problem: The classic DB-API entry points — pycubrid.connect() and its connection/cursor methods, and the legacy CUBRIDdb driver — are synchronous. Calling them directly in an async def handler blocks the event loop for every network round trip.
# ❌ Anti-pattern — sync DB call in async handler blocks the event loop
@app.get("/items")
async def list_items():
conn = pycubrid.connect(...) # Blocks event loop
cursor = conn.cursor()
cursor.execute("SELECT * FROM cookbook_items") # Blocks event loop
rows = cursor.fetchall()
cursor.close()
conn.close()
return rows
Fix: Use a native async driver in async code, or keep the synchronous driver out of the event loop.
# ✅ Option A — native asyncio driver: pycubrid.aio
import pycubrid.aio
@app.get("/items")
async def list_items():
conn = await pycubrid.aio.connect(host="localhost", port=33000, database="testdb", user="dba")
try:
cur = conn.cursor()
await cur.execute("SELECT id, val FROM cookbook_items")
rows = await cur.fetchall()
await cur.close()
return rows
finally:
await conn.close()
# ✅ Option B — SQLAlchemy async engine on top of pycubrid.aio
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine("cubrid+aiopycubrid://dba@localhost:33000/testdb")
# ✅ Option C — sync handler (FastAPI runs it in a thread pool)
@app.get("/items")
def list_items(db: Session = Depends(get_db)):
return db.execute(text("SELECT * FROM cookbook_items")).all()
# ✅ Option D — explicit thread offload for existing sync code in an async handler
import asyncio
@app.get("/items")
async def list_items():
return await asyncio.to_thread(sync_fetch_items)
Options A and B are demonstrated end to end in fundamentals/async/, whose requirements.txt pins the tested floors: pycubrid >= 1.6.1, sqlalchemy-cubrid >= 1.5 (the first release whose cubrid+aiopycubrid:// dialect works with SQLAlchemy 2.1) and sqlalchemy[asyncio].
Recommendation: In an asyncio application, use
pycubrid.aioor thecubrid+aiopycubrid://engine. With synchronous code, keep handlers synchronous (FastAPI threads them automatically) or offload the call to a thread — never call the sync API directly insideasync def.
7. Forgetting to Close Cursors and Connections¶
Problem: Unclosed cursors/connections leak resources and may exhaust the connection pool or server limits.
# ❌ Anti-pattern — exception before close() leaks resources
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
cursor = conn.cursor()
cursor.execute("SELECT * FROM cookbook_items")
rows = cursor.fetchall()
# If an exception occurs here, close() is never called
process(rows)
cursor.close()
conn.close()
Fix: Use context managers or try/finally.
# ✅ Correct — try/finally guarantees cleanup
conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba")
try:
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM cookbook_items")
rows = cursor.fetchall()
process(rows)
finally:
cursor.close()
finally:
conn.close()
# ✅ Better — SQLAlchemy Session handles this automatically
with SessionLocal() as session:
result = session.execute(text("SELECT * FROM cookbook_items"))
process(result.all())
# Session and connection automatically returned to pool
Quick Reference¶
| Pitfall | Impact | Difficulty to Fix |
|---|---|---|
| Connection per request | High (throughput) | Easy (add pool) |
| N+1 queries | High (latency) | Medium (eager loading) |
| Per-row COMMIT | High (throughput) | Easy (batch) |
| Reserved words | Medium (errors) | Easy (rename) |
| SQL interpolation | Critical (security) | Easy (parameterize) |
| Sync in async | High (throughput) | Easy (pycubrid.aio or sync handlers) |
| Unclosed resources | Medium (leaks) | Easy (context managers) |