Python Data Visualization, SQL Connectivity & EDA
Back to CONNECTING PYTHON TO RELATIONAL DATABASES & SQL

Enterprise Database Connectivity: PostgreSQL, MySQL & SQLAlchemy

Comprehensive guide on SQLAlchemy connection strings, psycopg2, pooling, chunking, and parameterized query security.

9 minutesReading
๐ŸŽฏ Free Guest Mode: You are learning for free. Sign in to save your completion progress and quiz answers.

From CSV Files to Live Databases: Connecting Python to Production Data

You have spent the last two lessons loading data with pd.read_csv("sales.csv"). That works in a classroom. On your first real job you will hear this instead: "The orders are in the Postgres warehouse โ€” pull the last 90 days into Python and send me the RFM." No CSV was attached. No file to download. The table lives on a server and you must query it into a DataFrame, analyze it, and write your results back. This article makes that jump concrete โ€” with the same e-commerce data you will use in the notebook and Project 35.

1. Why Your Notebook Needs a Database Connection

Think of a database as a shared Excel file that 200 people and 15 services write to at the same time โ€” but it enforces types, prevents corruption, and answers questions in milliseconds. Your Python notebook is the client that asks questions (SQL) and gets answers (rows) back as a DataFrame.

You want toโ€ฆOld CSV wayProduction way (this article)
Load orderspd.read_csv("orders.csv")pd.read_sql_query("SELECT * FROM orders", engine)
Filter completed onlyLoad everything, then df[df.status=="completed"]Filter inside the DB: WHERE status='completed' โ€” less RAM, faster
Save your RFM resultdf.to_csv("rfm.csv") on your laptopdf.to_sql("rfm_scores", engine, schema="marketing_analytics") โ€” visible to the whole team

2. The Two Layers Between Python and the Database

Every Python DB connection has two parts. You already used Layer 1 with SQLite โ€” now we add Layer 2 for everything else:

LayerWhat it doesExamplesAnalogy
1 โ€” DB-API DriverSpeaks the database's native network protocol (TCP, auth, wire format)sqlite3 (built-in), psycopg2 (Postgres), pymysql (MySQL)The phone that dials the warehouse
2 โ€” SQLAlchemy EngineManages connection pooling, translates dialects, plugs directly into pd.read_sql / to_sqlcreate_engine("postgresql+psycopg2://...")The switchboard that pools calls and translates languages

For SQLite the driver is built into Python (import sqlite3). For Postgres/MySQL you install the driver once with pip install psycopg2-binary or pymysql and SQLAlchemy handles the rest.

3. Your Starting Point: The Same Orders Table โ€” Now Inside Postgres

In the notebook you saw this table in SQLite :memory:. In production it lives at prod-db.company.com in the table orders. Same columns, same 280 rows โ€” just a different address:

transaction_idcustomer_idtransaction_datestatusquantityunit_price
1001C0012024-06-22completed1899.00
1002C0022024-02-12completed580.00
1003C0032024-06-25completed145.00
1004C0012024-06-28completed345.00

The code below connects to both โ€” one line changes (the URI), everything else stays identical.

from sqlalchemy import create_engine
import pandas as pd

# Postgres warehouse (production โ€” needs psycopg2 driver)
engine_pg = create_engine("postgresql+psycopg2://read_only_user:SecretPass123@prod-db.company.com:5432/analytics_db")

# SQLite local file (what the videos used โ€” same API, different URI)
engine_sqlite = create_engine("sqlite:///ecommerce_warehouse.db")

# SQLite in-memory (what the notebook uses: no file at all)
import sqlite3
conn = sqlite3.connect(":memory:")

URI pattern to memorize: dialect+driver://user:password@host:port/database โ€” only the prefix changes (postgresql+psycopg2:// vs mysql+pymysql:// vs sqlite:///).

4. Reading: Three Pandas Functions โ€” When to Use Which

All three return a DataFrame. Pick based on how much SQL you want to write:

FunctionWhat you writeBest forExample
pd.read_sql_table("orders", engine)Just a table nameDump a whole table, no filterdf = pd.read_sql_table("orders", engine_pg)
pd.read_sql_query("SELECT ...", engine)Full SQL with WHERE/JOIN/GROUP BYFiltered, aggregated extracts โ€” use this 90% of the timepd.read_sql_query("SELECT customer_id, SUM(quantity*unit_price) AS total FROM orders WHERE status='completed' GROUP BY customer_id", engine_pg)
pd.read_sql("SELECT ...", engine)Either of the above (auto-detects)Quick explorations; prefers explicit choice in productionpd.read_sql(query, engine_pg)

Realistic filtered extract you will actually run (same idea as Project 35):

query = """
SELECT customer_id, DATE(transaction_date) AS order_date,
       SUM(quantity * unit_price) AS total_spend
FROM orders
WHERE status = 'completed' AND transaction_date >= '2024-01-01'
GROUP BY customer_id, DATE(transaction_date)
"""
df_orders = pd.read_sql_query(query, engine_pg)
print(df_orders.head(3))
#   customer_id  order_date  total_spend
# 0        C001  2024-06-22       899.00
# 1        C001  2024-06-28       135.00
# 2        C002  2024-02-12       400.00

5. Writing Back: Share Your Results With the Team

After engineering RFM you need to publish rfm_scores so Tableau and the marketing team can use it โ€” not leave it on your laptop. That is to_sql:

rfm = df_orders.groupby('customer_id')['total_spend'].sum().reset_index()

rfm.to_sql(
    name='customer_lifetime_spend',     # new table name
    con=engine_pg,
    schema='marketing_analytics',       # Postgres schema (like a folder for tables)
    if_exists='replace',                # 'fail' = error if exists | 'replace' = drop+recreate | 'append' = keep old + add rows
    index=False,                        # don't write the DataFrame's 0,1,2 index as a column
    chunksize=1000                      # write 1000 rows at a time (see next section)
)
if_existsWhat happens if the table already exists?When to use
'fail'Raises an error โ€” refuses to overwrite (default, safe)First-time creation; guard against accidents
'replace'Drops the old table and creates a fresh oneRefreshing a report table daily
'append'Keeps existing rows, inserts new onesAppending a new day's increment

Why index=False matters: Without it, Pandas adds an index column (0,1,2โ€ฆ) to your table. Every downstream query then has a mystery extra column โ€” easy to forget, annoying to clean.

6. When the Table Is Too Big for RAM: chunksize

Your warehouse orders table has 40 million rows โ€” SELECT * into one DataFrame will crash the notebook. Both reading and writing have the same fix: stream in batches.

# Reading: process 5000 rows at a time, never holding all 40M in memory
total = 0
for chunk in pd.read_sql_query("SELECT * FROM orders", engine_pg, chunksize=5000):
    total += chunk['quantity'].sum()   # or filter/aggregate/write each chunk
print(f"Total units sold: {total}")

# Writing: flush your 2M-row DataFrame 1000 rows at a time
rfm.to_sql('customer_lifetime_spend', engine_pg, if_exists='replace', index=False, chunksize=1000)

Quiz Q1-08 tests exactly this. Whenever the prompt says "millions of rows / won't fit in RAM" think chunksize immediately.

7. Production Hygiene: Don't Hard-Code Secrets, Don't Leak Connections

Two mistakes every beginner makes once (and then never again): committing a password to Git, and leaving a connection open until the DB refuses new queries.

# .env file โ€” lives on your machine, never committed (add .env to .gitignore):
# DB_URI=postgresql+psycopg2://read_only_user:SecretPass123@prod-db.company.com:5432/analytics_db

from dotenv import load_dotenv
import os
load_dotenv()  # reads .env into environment variables

engine = create_engine(
    os.getenv("DB_URI"),
    pool_size=5, max_overflow=10,    # keep 5 connections warm, burst to 15 under load
    pool_pre_ping=True               # test that a pooled connection is still alive before using it
)

# Use the engine, then close it when the notebook is done:
# engine.dispose()

# For user-supplied filters, never use f-strings โ€” use bound parameters:
from sqlalchemy import text
with engine.connect() as conn:   # auto-closes even if the query fails
    result = conn.execute(text("SELECT * FROM orders WHERE status = :s"), {"s": "completed"})
    df = pd.DataFrame(result.fetchall(), columns=result.keys())
print(df.head(2))
MistakeWhat happensFix you just saw
Hard-code "postgresql://user:pass@..." in the notebookPassword leaks to GitHub; key rotation breaks the notebookos.getenv("DB_URI") + .env + .gitignore
Leave engine open foreverPool exhausts; next read_sql hangswith engine.connect() as conn: or engine.dispose()
f"SELECT ... WHERE status='{user_input}'"SQL injection โ€” user can type '; DROP TABLE orders; --text("... WHERE status=:s") with {"s": user_input}
Loading progress...