autocommit=False (default) means all DML must be followed by conn.commit() or conn.rollback().
cursor.fast_executemany = True enables bulk-insert mode for large payloads.
pandas Integration
pd.read_sql(query, conn) returns a DataFrame directly from a connection or SQLAlchemy engine.
df.to_sql(table, engine, if_exists=...) writes a DataFrame to a database table.
Chunked reads via chunksize parameter avoid memory overload on large result sets.
SQLAlchemy engines are preferred over raw connections for pd.read_sql to avoid deprecation warnings.
SQLAlchemy ORM
create_engine(url) creates the central engine with a connection pool; create once and share.
DeclarativeBase + mapped_column is the SQLAlchemy 2.0 pattern for defining ORM models.
Session is the unit-of-work context: session.add() stages objects; session.commit() persists them.
session.execute(select(Model)) queries via ORM; text() wraps raw SQL for engine-level execution.
Relationships are declared with relationship() and navigated as Python attributes.
DuckDB — Embedded OLAP Engine
duckdb.connect() opens an in-process analytical database; no server required.
DuckDB queries Parquet, CSV, and JSON files directly via read_parquet(), read_csv(), read_json().
Results convert to pandas with .df() or to Polars with .pl(); COPY TO exports files.
Columnar execution and vectorised I/O make DuckDB faster than pandas for analytical aggregations.
Querying Files — DuckDB vs Polars vs Pandas
All three can read Parquet, CSV, and JSON; DuckDB uses SQL syntax, Polars uses method chaining, pandas uses read_* functions.
Polars lazy evaluation (scan_parquet) defers work until .collect(); avoids loading full files.
DuckDB is optimal for ad-hoc SQL over multiple files; Polars for typed pipeline transforms; pandas for compatibility with the broader ecosystem.
Glossary
sqlite3
Python’s built-in module for SQLite — a file-based (or in-memory) relational database that requires no server process.
Use for local development, unit tests, embedded caches, and prototyping; switch to SQL Server or PostgreSQL when concurrent writes or row-level locking are needed.
In-memory mode
Pass ":memory:" as the path to keep the database entirely in RAM — the DB is destroyed when the connection closes, making it ideal for isolated test fixtures.
pyodbc
Python ODBC bridge library that connects to SQL Server and any other ODBC-compliant database via a driver registered in the OS.
The primary SQL Server access method in this vault; always use ? placeholders in execute() — never f-strings.
SQL injection via string formatting
Building SQL with f"... WHERE id = {val}" allows arbitrary SQL to be injected. Always use cursor.execute(sql, (val,)) with ? placeholders.
Connection string
A string encoding the ODBC driver name, server address, database name, and authentication details needed to open a connection.
The most common setup failure is format mismatch; Trusted_Connection=yes (Windows auth) and explicit UID/PWD credentials are mutually exclusive.
URL-encoded connection strings for SQLAlchemy
When passing a pyodbc connection string to create_engine, wrap it with urllib.parse.quote_plus() inside the mssql+pyodbc:///?odbc_connect=... URL scheme so special characters are handled correctly.
Cursor
A database cursor object returned by conn.cursor() that sends SQL to the server and buffers the result set locally.
The main interface for executing queries and reading rows; do not reuse a cursor while iterating its previous result — call fetchall() first or open a new cursor.
Cursor reuse during iteration
Issuing a new execute() on a cursor that is mid-iteration silently discards the remaining rows. Either consume the full result with fetchall() before re-executing, or open a second cursor.
Parameterised query
A query using ? (pyodbc/sqlite3) or :name / %s placeholders instead of string interpolation to bind values to SQL statements.
Prevents SQL injection and avoids type-conversion bugs; the driver handles quoting, escaping, and type binding automatically.
Safe parameterisation pattern
cur.execute("SELECT * FROM trades WHERE ticker = ?", (ticker,))cur.executemany("INSERT INTO trades VALUES (?,?,?,?,?,?)", rows)
Transaction
A unit of work (BEGIN → DML → COMMIT or ROLLBACK) that is atomic: either all statements succeed or none take effect.
Required for data integrity when modifying multiple rows across one or more tables; conn.commit() must be called explicitly when autocommit=False.
Silent data loss without commit
If conn.commit() is never called, changes exist only in the session and are rolled back on disconnect. Always commit after successful DML, or use the with conn: context manager which auto-commits.
SQLAlchemy
Python’s most widely used database toolkit — provides a Core expression language for SQL construction and an ORM layer for object-oriented access on top of raw connections.
Abstracts over multiple database engines via dialect plugins; a single codebase can target SQLite, SQL Server, PostgreSQL, and BigQuery without changing application logic.
Core vs ORM
SQLAlchemy Core gives full SQL control via text() and select() constructs. The ORM adds class-to-table mapping, relationship navigation, and the Session unit-of-work. Use Core for bulk ETL; ORM for CRUD-heavy application logic.
Engine
A SQLAlchemy Engine object that owns the connection pool and the dialect for a specific database URL — the central, long-lived object in SQLAlchemy applications.
Create once at application startup and share across the codebase; creating a new Engine per query discards the connection pool and causes significant overhead.
Pool sizing guidance
Default pool_size=5. Increase for high-concurrency services; keep small for ETL jobs that run one thread at a time. Setting pool_size above the database server’s connection limit causes connection refusals.
ORM
Object-Relational Mapper — maps Python classes to database tables so application code queries and modifies data via Python objects rather than raw SQL strings.
Reduces boilerplate for CRUD operations and enforces schema as code; the trade-off is that complex analytical queries are often clearer as raw SQL via text() or Core.
Unstaged ORM writes
session.add(obj) stages the object in memory but does not write to the database. session.commit() must be called to persist. Forgetting this is the most common ORM mistake.
DeclarativeBase / mapped_column
SQLAlchemy 2.0 API for defining ORM models as Python classes with typed, annotated columns using Mapped[type] and mapped_column().
Replaces the legacy Base = declarative_base() + Column() pattern from SQLAlchemy 1.x; the two styles mix poorly in fully typed code.
A SQLAlchemy Session is a short-lived unit-of-work context for ORM operations: load objects, make changes, commit or roll back, then close.
All ORM writes go through a session; keep sessions short-lived — a session that spans a long request accumulates identity-map state and memory pressure.
Context-manager pattern
Use with Session(engine) as session: to ensure the session is closed even if an exception is raised. Never share a single session across threads.
pd.read_sql
Pandas function that executes a SQL query string against a connection or SQLAlchemy engine and returns the result as a DataFrame.
The simplest bridge between SQL and DataFrame workflows; pass a SQLAlchemy engine (not a raw pyodbc connection) to avoid RemovedIn20Warning deprecation errors in pandas 2.x.
Raw connection deprecation in pandas 2.x
Passing a raw pyodbc connection directly to pd.read_sql triggers UserWarning: pandas only supports SQLAlchemy connectable. Wrap the connection with create_engine or use sqlalchemy.text() for the query string.
DuckDB
An in-process analytical SQL database optimised for OLAP workloads — columnar execution, vectorised I/O, no server process required.
Reads Parquet, CSV, and JSON files directly without importing them first; integrates with pandas via .df() and Polars via .pl().
DuckDB vs SQL Server BCP
DuckDB’s COPY TO 'file.parquet' exports query results to a file in the current process. SQL Server’s BCP is a separate OS-level bulk copy utility — the two are not interchangeable.
Polars
A DataFrame library written in Rust with lazy evaluation, columnar in-memory format, and native Parquet/CSV/JSON scanning — significantly faster than pandas for analytical transforms on large files.
Used alongside DuckDB for in-memory analytics; scan_parquet() / scan_csv() build a lazy query plan; .collect() triggers execution.
Premature .to_pandas() defeats lazy evaluation
Calling .to_pandas() immediately after scan_* forces full materialization into a pandas DataFrame, discarding Polars’ streaming and predicate-pushdown optimisations. Collect only when the result is needed for output or export.
Connection pool
A SQLAlchemy-managed cache of open database connections reused across queries, eliminating the per-request cost of establishing a new TCP connection and authenticating.
Critical for performance in web services and ETL loops; default pool_size=5 with max_overflow=10; size it to match the worker thread count, not higher.
Pool exhaustion signal
If queries queue or time out under load, check engine.pool.status(). A checked out count equal to pool_size + max_overflow means the pool is exhausted — increase pool_size or reduce connection hold time.
Setup imports and notebook display helpers.
import osimport sqlite3import pyodbcimport pandas as pdimport urllib.parseimport duckdbimport polars as plfrom sqlalchemy import create_engine, textfrom sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_columnfrom datetime import date as Date, datetimeimport timeDATA = "C:/Users/aperi/DEV/LANG/data"# Polars: strip quotes from string values in HTML display_html_fmt = get_ipython().display_formatter.formatters["text/html"]_html_fmt.for_type(pl.DataFrame, lambda df: df.to_pandas().style.hide(axis="index").to_html())
SQLite — Built-in Embedded Database
Connection, Schema, and CRUD
SQLite — connect and CREATE TABLE
SQLite basics
sqlite3.connect(":memory:") — in-memory; or pass a file path
cursor.execute(sql, params) — ? placeholders for parameterized queries
Context manager (with conn:) — auto-commits on success, rolls back on exception
Built-in — no pip install, no server
Use for tests, prototyping, local caches; for concurrent access, use SQL Server or PostgreSQL
SQL injection
Never use f-strings in SQL — always use ? parameter placeholders.
Use parameterized queries
Example: Use parameterized queries.
# Safe: parameter placeholdercur.execute("SELECT * FROM trades WHERE ticker = ?", (ticker,))# Safe: multiple parameterscur.execute("INSERT INTO trades VALUES (?, ?, ?)", (id, ticker, price))
Example: SQLite — connect and CREATE TABLE.
conn = sqlite3.connect(":memory:")conn.row_factory = sqlite3.Row # dict-like row accesscur = conn.cursor()cur.execute(""" CREATE TABLE trades ( trade_id TEXT PRIMARY KEY, ticker TEXT NOT NULL, side TEXT NOT NULL CHECK(side IN ('BUY', 'SELL')), quantity INTEGER NOT NULL CHECK(quantity > 0), price REAL NOT NULL CHECK(price > 0), trade_date TEXT NOT NULL DEFAULT (Date('now')) )""")print(cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='trades'").fetchone()[0])
trades
SQLite — INSERT with ? parameterised queries
executemany() bulk-inserts a list of tuples in a single call. Use ? placeholders for every user-supplied value — never build SQL strings with f-strings. conn.commit() persists the inserts to disk.
Example: SQLite — INSERT with ? parameterised queries.
cursor.fetchall() loads the entire result set into memory
For large tables (millions of rows), fetchall() or pd.read_sql() without a WHERE/LIMIT clause can exhaust RAM and crash your process. Use fetchmany(batch_size) for streaming, or push filtering to SQL with WHERE/LIMIT. For analytics, prefer DuckDB which streams columnar data efficiently.
Stream large results with fetchmany or push filtering to SQL
Example: Stream large results with fetchmany or push filtering to SQL.
# Stream in batches instead of loading all rowscur.execute("SELECT * FROM trades")while batch := cur.fetchmany(1000): process(batch)# Or push filtering to SQLcur.execute("SELECT * FROM trades WHERE trade_date >= ?", ("2024-01-01",))
Example: SQLite — SELECT into pandas DataFrame.
# SELECT — display as pandas DataFramepd.read_sql("SELECT *, quantity * price AS notional FROM trades ORDER BY trade_date, trade_id", conn)
trade_id
ticker
side
quantity
price
trade_date
notional
0
TRD_001
ASML.AS
BUY
100
685.4
2026-03-15
68540.0
1
TRD_002
MC.PA
BUY
50
890.2
2026-03-15
44510.0
2
TRD_003
SAP.DE
SELL
75
245.8
2026-03-15
18435.0
3
TRD_004
ASML.AS
SELL
30
690.0
2026-03-16
20700.0
4
TRD_005
RMS.PA
BUY
20
2850.0
2026-03-16
57000.0
5
TRD_006
SIE.DE
BUY
200
198.5
2026-03-17
39700.0
SQLite — SELECT with WHERE parameter
Pass a parameter tuple to params= in pd.read_sql() to safely inject filter values into a WHERE clause. The ? placeholder is substituted by the underlying cursor, preventing SQL injection.
Example: SQLite — SELECT with WHERE parameter.
pd.read_sql("SELECT * FROM trades WHERE ticker = ?", conn, params=("ASML.AS",))
trade_id
ticker
side
quantity
price
trade_date
0
TRD_001
ASML.AS
BUY
100
685.4
2026-03-15
1
TRD_004
ASML.AS
SELL
30
690.0
2026-03-16
SQLite — aggregate with GROUP BY
GROUP BY with conditional aggregation computes net position per ticker in a single pass. SUM(CASE WHEN side='BUY' THEN quantity ELSE -quantity END) is more efficient than two separate queries.
Example: SQLite — aggregate with GROUP BY.
pd.read_sql(""" SELECT ticker, SUM(CASE WHEN side='BUY' THEN quantity ELSE -quantity END) AS net_shares, ROUND(SUM(CASE WHEN side='BUY' THEN quantity*price ELSE -quantity*price END), 2) AS net_notional, COUNT(*) AS trade_count FROM trades GROUP BY ticker ORDER BY net_notional DESC""", conn)
ticker
net_shares
net_notional
trade_count
0
RMS.PA
20
57000.0
1
1
ASML.AS
70
47840.0
2
2
MC.PA
50
44510.0
1
3
SIE.DE
200
39700.0
1
4
SAP.DE
-75
-18435.0
1
SQLite — UPDATE and DELETE
cursor.rowcount returns the number of rows affected after an UPDATE or DELETE. Changes are not persisted until conn.commit() is called (or the connection context manager exits successfully).
Example: SQLite — UPDATE and DELETE.
cur.execute("UPDATE trades SET price = ? WHERE trade_id = ?", (700.00, "TRD_004"))cur.rowcount # UPDATE rows affectedcur.execute("DELETE FROM trades WHERE trade_id = ?", ("TRD_006",))cur.rowcount # DELETE rows affectedconn.commit()
1 row1 row
Transactions
SQLite — transaction with context manager
Using with conn: as a context manager automatically commits on success and rolls back on any exception — without needing an explicit conn.commit() call. This is the safest way to group multiple DML statements atomically.
Example: SQLite — transaction with context manager.
try: with conn: conn.execute("INSERT INTO trades VALUES (?,?,?,?,?,?)", ("TRD_007", "TTE.PA", "BUY", 150, 58.30, Date.today().isoformat())) conn.execute("INSERT INTO trades VALUES (?,?,?,?,?,?)", ("TRD_008", "BNP.PA", "BUY", 80, 72.10, Date.today().isoformat())) print("Transaction committed (2 trades)")except Exception as e: print(f"Transaction rolled back: {e}")conn.execute('SELECT COUNT(*) FROM trades').fetchone()[0] # Total trades
Transaction committed (2 trades)7
PRAGMA and Indexes
SQLite — PRAGMA settings for performance
PRAGMA statements configure SQLite behavior per connection. WAL mode allows concurrent readers while a writer is active. synchronous=NORMAL reduces fsync calls for a significant write speed boost with minimal durability risk. Settings do not persist — they must be re-applied each time a connection is opened.
Example: SQLite — PRAGMA settings for performance.
for pragma, value in [ ("journal_mode", "WAL"), ("synchronous", "NORMAL"), ("cache_size", "-20000"), ("busy_timeout", "5000"), ("foreign_keys", "ON"),]: cur.execute(f"PRAGMA {pragma}={value}") result = cur.execute(f"PRAGMA {pragma}").fetchone()[0] print(f" {pragma:20s} = {result}")
EXPLAIN QUERY PLAN shows whether SQLite uses a full table scan or an index seek for a given query. Use it to verify that indexes are being picked up — the output changes from SCAN trades to SEARCH trades USING INDEX once the index exists.
Example: SQLite — CREATE INDEX and EXPLAIN QUERY PLAN.
cur.execute("CREATE INDEX IF NOT EXISTS idx_trades_ticker ON trades(ticker)")print(cur.execute("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_trades_ticker'").fetchone()[0])plan = cur.execute("EXPLAIN QUERY PLAN SELECT * FROM trades WHERE ticker = 'ASML.AS'").fetchall()for row in plan: print(f" {dict(row)}")conn.close()
idx_trades_ticker {'id': 3, 'parent': 0, 'notused': 0, 'detail': 'SEARCH trades USING INDEX idx_trades_ticker (ticker=?)'}
SQL Server — pyodbc (ODBC Driver 18)
The SQL patterns used below (parameterised queries, window functions, CTEs) follow the same T-SQL dialect covered in sql-fundamentals. For how connection pooling interacts with SQL Server lock behavior under concurrent writes, see blocking-and-locking.
Connection and CRUD
SQL Server — connect and list schemas/tables
Connection pool exhaustion
Connection pool exhausting — always close connections
pyodbc.connect() without with or explicit .close() leaks connections. SQL Server defaults to a max pool of 100 connections — once exhausted, new connections block or fail with timeout errors. Always use with conn: or wrap in try/finally. For SQLAlchemy, engine.dispose() reclaims all pooled connections.
Always close connections with a context manager or try/finally
Example: Always close connections with a context manager or try/finally.
# Preferred: context manager auto-closes on exitwith pyodbc.connect(conn_str) as conn: with conn.cursor() as cur: cur.execute("SELECT 1")# Alternative: explicit close in finallyconn = pyodbc.connect(conn_str)try: ...finally: conn.close()
SQL Server connection pattern
pyodbc.connect() — direct cursor for DML (fast, rowcount available)
Set SQLSERVER_STOXX_ODBC to the full ODBC connection string before running the SQL Server cells. Use Windows authentication or a secret-managed SQL login instead of embedding credentials in the note body.
Example: SQL Server — connect and list schemas/tables.
import urllib.parseconn_str = os.environ["SQLSERVER_STOXX_ODBC"]# pyodbc connection for DML (INSERT/UPDATE/DELETE)sql_conn = pyodbc.connect(conn_str)cur = sql_conn.cursor()# SQLAlchemy engine for pd.read_sql (no warnings)odbc_params = urllib.parse.quote_plus(conn_str)sql_engine = create_engine(f"mssql+pyodbc:///?odbc_connect={odbc_params}")print(f"{sql_conn.getinfo(pyodbc.SQL_DATABASE_NAME)} database")
stoxx database
SQL Server — SELECT with parameterised query
pd.read_sql() passes bound parameters through the underlying driver. Use ? placeholders with raw pyodbc cursors and :name bind parameters with SQLAlchemy text() — never use f-string interpolation.
Example: SQL Server — SELECT with parameterised query.
pd.read_sql(text(""" SELECT TOP 10 symbol, date, [open], high, low, [close], volume FROM silver.eurostoxx50_ohlcv WHERE symbol = :symbol ORDER BY date DESC"""), con=sql_engine, params={"symbol": "SAP.DE"})
symbol
date
open
high
low
close
volume
0
SAP.DE
2026-03-12
163.00
166.74
162.80
166.52
806722
1
SAP.DE
2026-03-11
167.10
168.96
163.02
165.44
2953782
2
SAP.DE
2026-03-10
171.60
172.88
166.46
169.60
3187246
3
SAP.DE
2026-03-09
173.72
173.86
168.52
171.88
1990823
4
SAP.DE
2026-03-06
173.66
175.10
170.24
172.74
3347221
5
SAP.DE
2026-03-05
167.50
172.80
166.48
170.98
2961032
6
SAP.DE
2026-03-04
169.22
169.22
165.94
167.38
2443582
7
SAP.DE
2026-03-03
165.60
166.16
161.28
165.48
3971985
8
SAP.DE
2026-03-02
166.62
169.10
164.86
167.10
2776438
9
SAP.DE
2026-02-27
172.00
173.34
168.28
170.96
2673448
SQL Server — aggregate with GROUP BY
SUM(CAST(volume AS BIGINT)) is required here because SQL Server’s volume column is stored as INT and summing 1300+ trading days across 50 stocks would overflow a 32-bit integer. ROUND(AVG(CAST([close] AS FLOAT)), 2) avoids integer division truncation.
Example: SQL Server — aggregate with GROUP BY.
pd.read_sql(text(""" SELECT TOP 10 symbol, COUNT(*) AS trading_days, ROUND(AVG(CAST([close] AS FLOAT)), 2) AS avg_close, SUM(CAST(volume AS BIGINT)) AS total_volume FROM silver.eurostoxx50_ohlcv GROUP BY symbol ORDER BY total_volume DESC"""), con=sql_engine)
symbol
trading_days
avg_close
total_volume
0
ISP.MI
1321
3.15
115704541969
1
SAN.MC
1329
4.43
55513641918
2
ENEL.MI
1321
6.82
32600561934
3
BBVA.MC
1329
8.65
22133773194
4
UCG.MI
1321
28.46
18366801099
5
ENI.MI
1321
13.40
17141570967
6
INGA.AS
1331
14.04
17041577555
7
IBE.MC
1329
12.26
15994295949
8
DTE.DE
1324
22.43
10029411390
9
NDA-FI.HE
1306
10.85
7020342991
SQL Server — INSERT, UPDATE, DELETE
pyodbc passes positional parameters directly after the SQL string (not as a tuple), unlike sqlite3. cur.rowcount returns the number of rows affected. Every DML batch must end with conn.commit() — changes are not visible to other connections until committed.
Example: SQL Server — INSERT, UPDATE, DELETE.
cur.execute(""" IF OBJECT_ID('dbo.trades_demo', 'U') IS NOT NULL DROP TABLE dbo.trades_demo; CREATE TABLE dbo.trades_demo ( trade_id NVARCHAR(20) PRIMARY KEY, ticker NVARCHAR(10), side NVARCHAR(4), quantity INT, price DECIMAL(10,2))""")cur.execute("INSERT INTO dbo.trades_demo VALUES (?,?,?,?,?)", "TRD_001", "ASML.AS", "BUY", 100, 685.40)print(cur.rowcount)# UPDATEcur.execute("UPDATE dbo.trades_demo SET price = ? WHERE trade_id = ?", 700.00, "TRD_001")print(cur.rowcount)# DELETEcur.execute("DELETE FROM dbo.trades_demo WHERE trade_id = ?", "TRD_001")print(cur.rowcount)cur.execute("DROP TABLE dbo.trades_demo")sql_conn.commit()
111
Silent rollback when commit() is missing
pyodbc operates with autocommit=False by default. Any INSERT, UPDATE, or DELETE that is not followed by conn.commit() will be silently rolled back when the connection closes — no error is raised, data simply disappears. This is the most common source of “my writes don’t persist” bugs with pyodbc.
Always commit after DML, or use autocommit for DDL
Example: Always commit after DML, or use autocommit for DDL.
# Preferred: explicit commit after DMLcur.execute("INSERT INTO dbo.trades VALUES (?,?,?,?,?)", row)conn.commit()# For DDL (CREATE TABLE, DROP TABLE) that cannot run in a transaction:conn = pyodbc.connect(conn_str, autocommit=True)cur.execute("CREATE TABLE ...")
Indexes and Query Performance
SQL Server — list indexes on a table
sys.indexes joined with sys.index_columns and sys.columns reveals all indexes on a table including their key columns. STRING_AGG() concatenates key column names in ordinal order. Use this to verify coverage before writing expensive queries.
Example: SQL Server — list indexes on a table.
pd.read_sql(text(""" SELECT i.name AS index_name, i.type_desc, i.is_unique, STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) AS columns FROM sys.indexes i JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE i.object_id = OBJECT_ID('silver.eurostoxx50_ohlcv') GROUP BY i.name, i.type_desc, i.is_unique ORDER BY i.name"""), con=sql_engine)
index_name
type_desc
is_unique
columns
0
IX_silver_eurostoxx50_ohlcv_symbol_date
NONCLUSTERED
True
symbol, date
1
PK__eurostox__3213E83FDF67D274
CLUSTERED
True
id
SQL Server — index fragmentation
sys.dm_db_index_physical_stats returns fragmentation percentages for each index. The 'LIMITED' scan mode is fast and suitable for production — it samples page headers rather than reading all pages. The rule of thumb: < 10% = OK, 10–30% = REORGANIZE, > 30% = REBUILD.
Example: SQL Server — index fragmentation.
pd.read_sql(text(""" SELECT TOP 10 OBJECT_NAME(ips.object_id) AS [table], i.name AS [index], ips.index_type_desc AS type, ROUND(ips.avg_fragmentation_in_percent, 1) AS frag_pct, ips.page_count AS pages, CASE WHEN ips.avg_fragmentation_in_percent < 10 THEN 'OK' WHEN ips.avg_fragmentation_in_percent < 30 THEN 'REORGANIZE' ELSE 'REBUILD' END AS action FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id WHERE ips.page_count > 10 ORDER BY ips.avg_fragmentation_in_percent DESC"""), con=sql_engine)
table
index
type
frag_pct
pages
action
0
index_dim
PK__index_di__3213E83FDB4E5BA9
CLUSTERED INDEX
13.6
88
REORGANIZE
1
index_performance
UX_gold_index_performance
NONCLUSTERED INDEX
5.3
19
OK
2
index_performance
PK__index_pe__3213E83FBBB2393E
CLUSTERED INDEX
4.9
81
OK
3
stoxxusa50_ohlcv
PK__stoxxusa__3213E83FC84E3F24
CLUSTERED INDEX
1.4
724
OK
4
index_dim
PK__index_di__3213E83F590AA69E
CLUSTERED INDEX
1.2
85
OK
5
stoxxusa50_ohlcv
IX_silver_stoxxusa50_ohlcv_symbol_date
NONCLUSTERED INDEX
0.6
163
OK
6
stoxxasia50_ohlcv
IX_silver_stoxxasia50_ohlcv_symbol_date
NONCLUSTERED INDEX
0.5
183
OK
7
stoxxasia50_ohlcv
PK__stoxxasi__3213E83F66A8DE5E
CLUSTERED INDEX
0.4
729
OK
8
eurostoxx50_ohlcv
PK__eurostox__3213E83FDF67D274
CLUSTERED INDEX
0.4
757
OK
9
oil20_ohlcv
PK__oil20_oh__3213E83F544EB286
CLUSTERED INDEX
0.4
275
OK
SQL Server — database and table sizes
sys.database_files returns total database file size. sys.allocation_units tracks actual page usage per table partition. Together they give a two-level view: total DB size and per-table footprint — useful for capacity planning and identifying bloated tables.
Example: SQL Server — database and table sizes.
display(pd.read_sql(text("SELECT DB_NAME() AS db, CAST(SUM(size)*8.0/1024 AS DECIMAL(10,2)) AS size_mb FROM sys.database_files"), con=sql_engine))pd.read_sql(text(""" SELECT TOP 10 s.name + '.' + t.name AS [table], FORMAT(SUM(p.rows), 'N0') AS rows, CAST(SUM(a.total_pages)*8.0/1024 AS DECIMAL(10,2)) AS size_mb FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id JOIN sys.indexes i ON t.object_id = i.object_id JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id JOIN sys.allocation_units a ON p.partition_id = a.container_id GROUP BY s.name, t.name ORDER BY SUM(a.total_pages) DESC"""), con=sql_engine)
db
size_mb
0
stoxx
272.0
table
rows
size_mb
0
silver.eurostoxx50_ohlcv
132,710
7.52
1
silver.stoxxasia50_ohlcv
128,090
7.27
2
silver.stoxxusa50_ohlcv
130,200
7.08
3
silver.oil20_ohlcv
49,476
2.77
4
bronze.trading_calendar
58,670
1.58
5
bronze.index_dim
676
1.02
6
gold.index_performance
10,562
1.02
7
silver.index_dim
676
0.77
8
bronze.eurostoxx50_ohlcv
100
0.33
9
gold.scores_daily
932
0.33
Server Configuration and Administration
SQL Server — server info and configuration
@@VERSION returns the full SQL Server build string — SUBSTRING extracts just the version name. sys.configurations shows instance-level settings like max server memory (MB), max degree of parallelism (MAXDOP), and cost threshold for parallelism — critical parameters for tuning query plan behavior.
Example: SQL Server — server info and configuration.
display(pd.read_sql(text(""" SELECT SUBSTRING(@@VERSION, 1, CHARINDEX(' (', @@VERSION)-1) AS version, CAST(SERVERPROPERTY('Edition') AS NVARCHAR(100)) AS edition, CAST(SERVERPROPERTY('Collation') AS NVARCHAR(100)) AS collation"""), con=sql_engine))pd.read_sql(text(""" SELECT name AS setting, CAST(value_in_use AS NVARCHAR(30)) AS value FROM sys.configurations WHERE name IN ('max server memory (MB)', 'max degree of parallelism', 'cost threshold for parallelism') ORDER BY name"""), con=sql_engine)
version
edition
collation
0
Microsoft SQL Server 2022
Developer Edition (64-bit)
SQL_Latin1_General_CP1_CI_AS
setting
value
0
cost threshold for parallelism
5
1
max degree of parallelism
0
2
max server memory (MB)
2147483647
pandas Integration — pd.read_sql and to_sql
pandas — read_sql into DataFrame with SQLAlchemy engine
pd.read_sql(text(sql), con=engine) executes SQL and returns a DataFrame in one line. SQLAlchemy engine handles connection pooling and dialect translation, and works with any database SQLAlchemy supports. For streaming large results row by row, use cursor.fetchmany() instead.
Anti-patterns
pd.read_sql with raw pyodbc — works but triggers Pylance/UserWarning
Reading entire large table — add WHERE/LIMIT clauses
Use a SQLAlchemy engine and add filters
Example: Use a SQLAlchemy engine and add filters.
# Pass a SQLAlchemy engine, not a raw pyodbc connectionengine = create_engine(f"mssql+pyodbc:///?odbc_connect={odbc_params}")df = pd.read_sql(text("SELECT * FROM trades WHERE trade_date >= :start_date"), con=engine, params={"start_date": "2024-01-01"})
Example: pandas — read_sql into DataFrame with SQLAlchemy engine.
odbc_params = urllib.parse.quote_plus(os.environ["SQLSERVER_STOXX_ODBC"])engine = create_engine(f"mssql+pyodbc:///?odbc_connect={odbc_params}")pd.read_sql(text("SELECT TOP 5 symbol, date, [close], volume FROM silver.eurostoxx50_ohlcv ORDER BY date DESC"), con=engine)
symbol
date
close
volume
0
ASML.AS
2026-03-12
1190.80
128223
1
MC.PA
2026-03-12
494.35
171997
2
RMS.PA
2026-03-12
1906.00
18681
3
OR.PA
2026-03-12
360.80
82621
4
SAP.DE
2026-03-12
166.52
806722
pandas — to_sql to write DataFrame to database
df.to_sql(table, engine, if_exists=) writes a DataFrame to a database table. if_exists='replace' drops and recreates the table; 'append' adds rows without altering the schema. Pass index=False unless you want the DataFrame index as a column.
Example: pandas — to_sql to write DataFrame to database.
Python’s equivalent of EF Core. Define model classes inheriting from DeclarativeBase with Mapped[type] for typed columns. Session manages transactions — session.add() + session.commit() generates INSERT SQL automatically. Use Alembic for migrations (like dotnet ef). For complex analytics SQL, use raw SQL or DuckDB instead.
Anti-patterns
N+1 queries — use joinedload() or selectinload()
Session per query — reuse sessions within a request
Eager-load relationships and reuse sessions
Example: Eager-load relationships and reuse sessions.
# Eager load to avoid N+1stmt = select(Portfolio).options(joinedload(Portfolio.positions))portfolios = session.scalars(stmt).unique().all()# Reuse session within a unit of workwith Session(engine) as session: session.add(obj1) session.add(obj2) session.commit()
SQLAlchemy — INSERT with session.add() and commit()
session.add() marks an ORM object as pending. session.commit() flushes all pending changes and generates the appropriate INSERT SQL. session.add_all() is the equivalent of executemany() — use it for bulk inserts rather than calling add() in a loop.
Example: SQLAlchemy — INSERT with session.add() and commit().
SQLAlchemy — SELECT with session.query() and filter()
session.query(Model).filter(condition).all() generates a SELECT with a WHERE clause. .first() fetches one row with LIMIT 1. The ORM translates Python attribute access (StockPrice.symbol) to column names automatically.
Example: SQLAlchemy — SELECT with session.query() and filter().
with Session(orm_engine) as session: results = session.query(StockPrice).filter( StockPrice.symbol == "ASML.AS" ).order_by(StockPrice.trade_date.desc()).all() for r in results: print(f" {r.symbol} | {r.trade_date} | {r.close} | {r.volume}")
ASML.AS | 2025-03-15 | 685.4 | 2500000
SQLAlchemy — UPDATE and DELETE
Mutate an object fetched within a session, then call session.commit() to generate the UPDATE. For DELETE, call session.delete(obj) then commit. SQLAlchemy tracks object state — you do not write UPDATE/DELETE SQL manually.
Example: SQLAlchemy — UPDATE and DELETE.
with Session(orm_engine) as session: stock = session.query(StockPrice).filter(StockPrice.symbol == "SAP.DE").first() stock.close = 999.99 session.commit() print(f"Updated SAP.DE close to {stock.close}")# DELETEwith Session(orm_engine) as session: stock = session.query(StockPrice).filter(StockPrice.symbol == "MC.PA").first() session.delete(stock) session.commit() print(f"Deleted MC.PA, remaining: {session.query(StockPrice).count()}")
Updated SAP.DE close to 999.99Deleted MC.PA, remaining: 2
SQLAlchemy — raw SQL with text()
text() wraps a raw SQL string for safe execution through SQLAlchemy. Named parameters use :name syntax (not ?), passed as a dict. This is the escape hatch for complex queries (window functions, CTEs) where the ORM is too verbose.
Example: SQLAlchemy — raw SQL with text().
with engine.connect() as c: result = c.execute(text(""" SELECT TOP 5 symbol, date, [close], volume FROM silver.eurostoxx50_ohlcv WHERE symbol = :symbol ORDER BY date DESC"""), {"symbol": "ASML.AS"}) df = pd.DataFrame(result.fetchall(), columns=result.keys())df
symbol
date
close
volume
0
ASML.AS
2026-03-12
1190.8
128223
1
ASML.AS
2026-03-11
1198.8
562904
2
ASML.AS
2026-03-10
1200.0
800815
3
ASML.AS
2026-03-09
1147.6
689086
4
ASML.AS
2026-03-06
1147.0
857271
DuckDB — Embedded Analytical SQL Database
DuckDB — connect and CREATE TABLE
Embedded columnar database — no server, in-process, 10-100x faster than row-stores for analytics. Full SQL:2003 with window functions, CTEs, QUALIFY, PIVOT. Queries files directly (SELECT * FROM 'data.parquet') and returns pandas DataFrames natively with .df(). Not suited for OLTP or concurrent writers — use SQL Server for those.
Example: DuckDB — connect and CREATE TABLE.
duck = duckdb.connect(":memory:")duck.execute(""" CREATE OR REPLACE TABLE ohlcv ( symbol VARCHAR, date DATE, open DOUBLE, high DOUBLE, low DOUBLE, close DOUBLE, volume BIGINT )""")print(duck.execute("SHOW TABLES").fetchone()[0])
ohlcv
DuckDB — load data from SQL Server
executemany() bulk-inserts rows fetched from pyodbc into DuckDB. Explicit casting (float(), int()) is necessary because pyodbc returns Decimal and datetime objects that DuckDB’s schema expects as native Python floats and ints.
fetchall() on large tables loads
fetchall() on large tables loads everything into Python memory
The 66K rows below are fine, but fetchall() on a million-row table can OOM your process. For large transfers, use fetchmany(batch_size) in a loop, or let DuckDB read files directly (SELECT * FROM 'data.parquet').
Stream with fetchmany or query files directly in DuckDB
Example: Stream with fetchmany or query files directly in DuckDB.
# Stream in batches from pyodbc into DuckDBmssql_cur.execute("SELECT * FROM large_table")while batch := mssql_cur.fetchmany(10_000): ddb.executemany("INSERT INTO target VALUES (?, ?)", batch)# Or let DuckDB read the file directly — no Python memory overheadddb.execute("CREATE TABLE target AS SELECT * FROM 'export.parquet'")
Example: DuckDB — load data from SQL Server.
# Load from SQL Server via pyodbc into DuckDBcur = sql_conn.cursor()cur.execute("SELECT symbol, date, [open], high, low, [close], volume FROM silver.eurostoxx50_ohlcv")rows = cur.fetchall()duck.executemany("INSERT INTO ohlcv VALUES (?,?,?,?,?,?,?)", [(r[0], r[1], float(r[2]), float(r[3]), float(r[4]), float(r[5]), int(r[6])) for r in rows])print(duck.execute('SELECT COUNT(*) FROM ohlcv').fetchone()[0])
66355
DuckDB — Appender for fastest bulk load
duckdb.Appender is the highest-throughput path for inserting rows into DuckDB — faster than executemany() because it bypasses SQL parsing and parameter binding overhead. Use it when loading millions of rows from Python (e.g., from a pyodbc cursor or a Parquet-to-DuckDB ETL). Call .flush() periodically to avoid accumulating too many rows in memory, and .close() to commit the final batch.
Example: DuckDB — Appender for fastest bulk load.
import duckdbduck2 = duckdb.connect()duck2.execute(""" CREATE OR REPLACE TABLE ohlcv ( symbol VARCHAR, date DATE, open DOUBLE, high DOUBLE, low DOUBLE, close DOUBLE, volume BIGINT )""")# Appender — fastest bulk insert path (bypasses SQL parsing)with duck2.cursor() as cur: cur.execute("SELECT symbol, date, open, high, low, close, volume FROM silver.eurostoxx50_ohlcv", [], sql_conn) rows = cur.fetchall()with duck2.appender("ohlcv") as app: for r in rows: app.append_row(r[0], r[1], float(r[2]), float(r[3]), float(r[4]), float(r[5]), int(r[6]))duck2.execute("SELECT COUNT(*) FROM ohlcv").fetchone()[0]
66355
Use Appender with fetchmany() for streaming bulk loads
For very large tables, combine fetchmany(10_000) with the Appender to avoid loading all rows into Python memory before inserting. Call .flush() every N batches if memory pressure is a concern:
Example: Use Appender with fetchmany() for streaming bulk loads.
with duck2.appender("ohlcv") as app: while batch := cur.fetchmany(10_000): for r in batch: app.append_row(*r)
DuckDB — SELECT with .df() for pandas DataFrame
.df() converts the DuckDB result set to a pandas DataFrame in one step — equivalent to .fetchdf(). For raw Python tuples use .fetchall(), or .fetchone() for a single row. Use LIMIT to avoid loading large results into memory.
Example: DuckDB — SELECT with .df() for pandas DataFrame.
duck.execute("SELECT symbol, date, close, volume FROM ohlcv WHERE symbol = 'SAP.DE' ORDER BY date DESC LIMIT 5").df()
symbol
date
close
volume
0
SAP.DE
2026-03-12
166.52
806722
1
SAP.DE
2026-03-11
165.44
2953782
2
SAP.DE
2026-03-10
169.60
3187246
3
SAP.DE
2026-03-09
171.88
1990823
4
SAP.DE
2026-03-06
172.74
3347221
DuckDB — aggregate with GROUP BY
Standard SQL GROUP BY with COUNT, AVG, and SUM. DuckDB executes this in-process using columnar vectorised execution — significantly faster than row-based databases for aggregations over millions of rows.
Example: DuckDB — aggregate with GROUP BY.
duck.execute(""" SELECT symbol, COUNT(*) AS days, ROUND(AVG(close), 2) AS avg_close, SUM(volume) AS total_volume FROM ohlcv GROUP BY symbol ORDER BY total_volume DESC LIMIT 10""").df()
symbol
days
avg_close
total_volume
0
ISP.MI
1321
3.15
1.157045e+11
1
SAN.MC
1329
4.43
5.551364e+10
2
ENEL.MI
1321
6.82
3.260056e+10
3
BBVA.MC
1329
8.65
2.213377e+10
4
UCG.MI
1321
28.46
1.836680e+10
5
ENI.MI
1321
13.40
1.714157e+10
6
INGA.AS
1331
14.04
1.704158e+10
7
IBE.MC
1329
12.26
1.599430e+10
8
DTE.DE
1324
22.43
1.002941e+10
9
NDA-FI.HE
1306
10.85
7.020343e+09
DuckDB — window function: LAG for daily returns
LAG(col) OVER (PARTITION BY symbol ORDER BY date) accesses the previous row’s value within each symbol group. DuckDB supports all standard window functions including LAG, LEAD, ROW_NUMBER, RANK, and NTILE. SQLite does not support window functions.
Example: DuckDB — window function: LAG for daily returns.
duck.execute(""" SELECT symbol, date, close, LAG(close) OVER (PARTITION BY symbol ORDER BY date) AS prev_close, ROUND((close - LAG(close) OVER (PARTITION BY symbol ORDER BY date)) / LAG(close) OVER (PARTITION BY symbol ORDER BY date) * 100, 2) AS daily_return_pct FROM ohlcv WHERE symbol = 'ASML.AS' ORDER BY date DESC LIMIT 10""").df()
symbol
date
close
prev_close
daily_return_pct
0
ASML.AS
2026-03-12
1190.8
1198.8
-0.67
1
ASML.AS
2026-03-11
1198.8
1200.0
-0.10
2
ASML.AS
2026-03-10
1200.0
1147.6
4.57
3
ASML.AS
2026-03-09
1147.6
1147.0
0.05
4
ASML.AS
2026-03-06
1147.0
1186.0
-3.29
5
ASML.AS
2026-03-05
1186.0
1199.8
-1.15
6
ASML.AS
2026-03-04
1199.8
1161.8
3.27
7
ASML.AS
2026-03-03
1161.8
1210.4
-4.02
8
ASML.AS
2026-03-02
1210.4
1233.4
-1.86
9
ASML.AS
2026-02-27
1233.4
1232.4
0.08
DuckDB — CTE for annualized volatility
A CTE (WITH ... AS (...)) names an intermediate result for reuse in the outer query. Here, daily_returns computes per-day returns, and the outer query aggregates over a trailing window. Annualized volatility = STDDEV(daily_return) * SQRT(252) — the standard market convention.
Example: DuckDB — CTE for annualized volatility.
duck.execute(""" WITH daily_returns AS ( SELECT symbol, date, close, (close - LAG(close) OVER (PARTITION BY symbol ORDER BY date)) / LAG(close) OVER (PARTITION BY symbol ORDER BY date) AS daily_return FROM ohlcv ) SELECT symbol, COUNT(*) AS days, ROUND(STDDEV(daily_return) * SQRT(252) * 100, 2) AS annualized_vol_pct FROM daily_returns WHERE daily_return IS NOT NULL GROUP BY symbol ORDER BY annualized_vol_pct DESC""").df().head()
symbol
days
annualized_vol_pct
0
ADYEN.AS
1330
50.30
1
ENR.DE
1323
50.05
2
RHM.DE
1323
40.85
3
PRX.AS
1330
39.72
4
ARGX.BR
1330
39.31
DuckDB — SUMMARIZE for data profiling
SUMMARIZE table returns per-column statistics including min, max, approximate distinct count, mean, std, quartiles, total count, and null percentage — equivalent to pandas df.describe() but faster and with null tracking. Useful as the first step in data quality checks.
Example: DuckDB — SUMMARIZE for data profiling.
duck.execute("SUMMARIZE ohlcv").df()
column_name
column_type
min
max
approx_unique
avg
std
q25
q50
q75
count
null_pct
0
symbol
VARCHAR
ABI.BR
WKL.AS
51
None
None
None
None
None
66355
0.0
1
date
DATE
2021-01-04
2026-03-12
1516
2023-08-05 00:56:42.354005
None
2022-04-18
2023-08-03
2024-11-18
66355
0.0
2
open
DOUBLE
1.601
2926.0
25981
197.04
363.15
29.93
70.86
186.66
66355
0.0
3
high
DOUBLE
1.6628
2957.0
30967
199.36
367.87
30.07
72.03
188.90
66355
0.0
4
low
DOUBLE
1.5842
2813.0
34449
194.59
358.01
29.54
70.57
184.59
66355
0.0
5
close
DOUBLE
1.6066
2839.0
31796
197.03
363.05
30.00
71.37
186.45
66355
0.0
6
volume
BIGINT
0
376391539
75668
5942123.69
16156185.53
510041
1415355
4094451
66355
0.0
DuckDB — COPY TO export to Parquet
COPY (query) TO 'file.parquet' (FORMAT PARQUET) exports a query result directly to a file without materialising a DataFrame. This is the most efficient export path — no Python memory allocation required.
Example: DuckDB — COPY TO export to Parquet.
duck.execute(f""" COPY (SELECT symbol, COUNT(*) AS days, ROUND(AVG(close), 2) AS avg_close FROM ohlcv GROUP BY symbol ORDER BY avg_close DESC) TO '{DATA}/duckdb_py_export.parquet' (FORMAT PARQUET)""")print("duckdb_py_export.parquet")
duckdb_py_export.parquet
DuckDB vs SQL Server — benchmark same queries
Compares wall-clock time for GROUP BY and LAG window function queries on the same 66K-row dataset — once via DuckDB (in-process columnar) and once via SQL Server (network round-trip + row-based executor). The columnar execution and lack of network overhead make DuckDB 5–10x faster for analytics.
Example: DuckDB vs SQL Server — benchmark same queries.
results = []for label, fn in [ ("DuckDB GROUP BY", lambda: duck.execute("SELECT symbol, AVG(close) FROM ohlcv GROUP BY symbol").fetchall()), ("SQL Server GROUP BY", lambda: pd.read_sql(text("SELECT symbol, AVG(CAST([close] AS FLOAT)) FROM silver.eurostoxx50_ohlcv GROUP BY symbol"), con=sql_engine)), ("DuckDB LAG()", lambda: duck.execute("SELECT symbol, date, close, LAG(close) OVER (PARTITION BY symbol ORDER BY date) FROM ohlcv").fetchall()), ("SQL Server LAG()", lambda: pd.read_sql(text("SELECT symbol, date, [close], LAG([close]) OVER (PARTITION BY symbol ORDER BY date) FROM silver.eurostoxx50_ohlcv"), con=sql_engine)),]: start = time.perf_counter() fn() elapsed = (time.perf_counter() - start) * 1000 results.append({"Engine": label.split()[0], "Query": " ".join(label.split()[1:]), "Time (ms)": round(elapsed, 1)})pd.DataFrame(results)
Engine
Query
Time (ms)
0
DuckDB
GROUP BY
3.2
1
SQL
Server GROUP BY
17.2
2
DuckDB
LAG()
23.9
3
SQL
Server LAG()
181.3
Querying Files — DuckDB vs Polars vs Pandas
Read Parquet
DuckDB — read Parquet file with SELECT
DuckDB reads Parquet directly from disk with a SQL SELECT — no intermediate load step. The file path is embedded in the FROM clause as a string literal. Columnar pushdown means only the requested columns are read from disk.
Example: DuckDB — read Parquet file with SELECT.
duck.execute("SELECT symbol, date, close, volume FROM 'C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet' LIMIT 5").df()
symbol
date
close
volume
0
ABI.BR
2021-01-04
57.21
1513937
1
ABI.BR
2021-01-05
57.18
1382722
2
ABI.BR
2021-01-06
58.77
1370204
3
ABI.BR
2021-01-07
58.40
1469911
4
ABI.BR
2021-01-08
57.86
1428681
Polars — read Parquet file with pl.read_parquet()
pl.read_parquet() loads a Parquet file into a Polars DataFrame. .select() pushes column projection to the file reader — only specified columns are deserialised. Polars reads Parquet with Arrow-native columnar decoding, which is faster than pandas’ default path.
Example: Polars — read Parquet file with pl.read_parquet().
pd.read_parquet() uses PyArrow (default) to load a Parquet file. The columns= parameter pushes column projection to the reader — unselected columns are not read from disk. Dates are returned as datetime64[ns] (vs Polars’ Date type or DuckDB’s DATE).
Example: Pandas — read Parquet file with pd.read_parquet().
DuckDB auto-detects CSV schema (delimiter, types, header) and reads the file directly in SQL. No pandas intermediary is needed. CSV is significantly slower than Parquet for large files — see the benchmark in ### Performance — format comparison.
Example: DuckDB — read CSV file with SELECT.
duck.execute("SELECT symbol, date, close, volume FROM 'C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.csv' LIMIT 5").df()
symbol
date
close
volume
0
ABI.BR
2021-01-04
57.21
1513937
1
ABI.BR
2021-01-05
57.18
1382722
2
ABI.BR
2021-01-06
58.77
1370204
3
ABI.BR
2021-01-07
58.40
1469911
4
ABI.BR
2021-01-08
57.86
1428681
Polars — read CSV file with pl.read_csv()
pl.read_csv() infers schema automatically. Column selection via .select() happens after loading (CSV doesn’t support columnar pushdown). For large CSVs, pl.scan_csv() with lazy evaluation avoids loading all columns into memory.
Example: Polars — read CSV file with pl.read_csv().
pd.read_csv() with usecols= loads only the specified columns into memory. Unlike Parquet, CSV is read row-by-row, so usecols saves memory but not I/O time. Parse date columns explicitly with parse_dates=["date"] if you need datetime64 types.
Example: Pandas — read CSV file with pd.read_csv().
DuckDB pushes the WHERE predicate into the Parquet reader — rows that don’t match the filter are never loaded into memory. This makes DuckDB significantly more efficient than loading the full file into pandas and filtering afterwards.
Example: DuckDB — filter with WHERE.
duck.execute("SELECT symbol, date, close, volume FROM 'C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet' WHERE symbol = 'SAP.DE' ORDER BY date DESC LIMIT 5").df()
symbol
date
close
volume
0
SAP.DE
2026-03-12
166.52
806722
1
SAP.DE
2026-03-11
165.44
2953782
2
SAP.DE
2026-03-10
169.60
3187246
3
SAP.DE
2026-03-09
171.88
1990823
4
SAP.DE
2026-03-06
172.74
3347221
Polars — filter with filter() and select()
.filter(pl.col("symbol") == "SAP.DE") applies a row predicate. Polars pushes this filter into the Parquet reader via scan_parquet() in lazy mode — read_parquet() loads first then filters. For large files, use pl.scan_parquet().filter(...).collect() instead.
Example: Polars — filter with filter() and select().
Boolean indexing df[df["col"] == value] creates a boolean Series and selects matching rows. The full file is loaded into memory first (no pushdown). The result retains original integer row indexes (not reset to 0).
GROUP BY with aggregate functions run directly on the Parquet file — DuckDB reads only the required columns and applies the aggregation without loading the full dataset into Python memory.
Example: DuckDB — aggregate with GROUP BY.
duck.execute(""" SELECT symbol, COUNT(*) AS days, ROUND(AVG(close), 2) AS avg_close, SUM(volume) AS total_volume FROM 'C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet' GROUP BY symbol ORDER BY total_volume DESC LIMIT 10""").df()
symbol
days
avg_close
total_volume
0
ISP.MI
1321
3.15
1.157045e+11
1
SAN.MC
1329
4.43
5.551364e+10
2
ENEL.MI
1321
6.82
3.260056e+10
3
BBVA.MC
1329
8.65
2.213377e+10
4
UCG.MI
1321
28.46
1.836680e+10
5
ENI.MI
1321
13.40
1.714157e+10
6
INGA.AS
1331
14.04
1.704158e+10
7
IBE.MC
1329
12.26
1.599430e+10
8
DTE.DE
1324
22.43
1.002941e+10
9
NDA-FI.HE
1306
10.85
7.020343e+09
Polars — aggregate with group_by() and agg()
.group_by("symbol").agg(...) groups by one or more columns and computes aggregations. Polars agg() takes named expressions — pl.col("close").count().alias("days") — rather than strings. Results are unordered by default; chain .sort() for a consistent output.
Example: Polars — aggregate with group_by() and agg().
df.groupby("symbol").agg(named_agg=...) uses pandas’ named aggregation syntax. The groupby key becomes the row index (shown as symbol in the output). Use .reset_index() to convert it back to a regular column.
Example: Pandas — aggregate with groupby() and agg().
Standard SQL column projection — only listed columns are read from the Parquet file. DuckDB’s Parquet reader skips unselected column chunks entirely, reducing I/O proportionally to how many columns are omitted.
Example: DuckDB — select columns with SELECT.
duck.execute("SELECT symbol, close FROM 'C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet' LIMIT 5").df()
symbol
close
0
ABI.BR
57.21
1
ABI.BR
57.18
2
ABI.BR
58.77
3
ABI.BR
58.40
4
ABI.BR
57.86
Polars — select columns with select()
.select("col1", "col2") returns a DataFrame with only the listed columns. When used in a lazy pipeline (scan_parquet().select()), Polars pushes column projection into the file reader; with read_parquet() (eager), all columns are loaded first.
pd.read_parquet(..., columns=[...]) passes the column list directly to the PyArrow Parquet reader — unselected columns are skipped at read time. This is the most memory-efficient way to load a subset of a wide Parquet file in pandas.
ORDER BY col DESC LIMIT n in DuckDB uses a tournament sort that only retains the top-N rows in memory — equivalent to a heap sort. This is far more efficient than sorting all rows and taking a slice.
Example: DuckDB — sort with ORDER BY and LIMIT.
# DuckDB — sort by close descending, top 5duck.execute("SELECT symbol, date, close FROM 'C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet' ORDER BY close DESC LIMIT 5").df()
symbol
date
close
0
RMS.PA
2025-02-14
2839.0
1
RMS.PA
2025-02-13
2816.0
2
RMS.PA
2025-02-17
2809.0
3
RMS.PA
2025-02-18
2806.0
4
ADYEN.AS
2021-08-24
2766.0
Polars — sort with sort() and head()
.sort("col", descending=True).head(n) sorts the full DataFrame and takes the top N rows. Unlike DuckDB’s tournament sort, Polars sorts all rows first — for very large datasets use .top_k(n, by="col") (Polars 0.19+) for efficient top-N without full sort.
Example: Polars — sort with sort() and head().
# Polars — sort by close descending, top 5pl.read_parquet("C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet").select("symbol", "date", "close").sort("close", descending=True).head(5)
symbol
date
close
0
RMS.PA
2025-02-14 00:00:00
2839.000000
1
RMS.PA
2025-02-13 00:00:00
2816.000000
2
RMS.PA
2025-02-17 00:00:00
2809.000000
3
RMS.PA
2025-02-18 00:00:00
2806.000000
4
ADYEN.AS
2021-08-24 00:00:00
2766.000000
Pandas — sort with sort_values() and head()
.sort_values("col", ascending=False).head(n) sorts a pandas DataFrame in place (returns a view) then slices. Like Polars, this sorts all rows — use df.nlargest(n, "col") for a more efficient top-N path.
Example: Pandas — sort with sort_values() and head().
# Pandas — sort by close descending, top 5pd.read_parquet("C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet", columns=["symbol", "date", "close"]).sort_values("close", ascending=False).head(5)
symbol
date
close
51473
RMS.PA
2025-02-14
2839.0
51472
RMS.PA
2025-02-13
2816.0
51474
RMS.PA
2025-02-17
2809.0
51475
RMS.PA
2025-02-18
2806.0
4150
ADYEN.AS
2021-08-24
2766.0
Multiple filters (AND / OR) directly from file
DuckDB — multi-condition filter with WHERE AND
Combine conditions with AND / OR directly in the WHERE clause. DuckDB evaluates compound predicates in the Parquet reader — only rows matching all conditions reach the query engine.
Example: DuckDB — multi-condition filter with WHERE AND.
# DuckDB — WHERE with AND + ORduck.execute(""" SELECT symbol, date, close, volume FROM 'C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet' WHERE symbol = 'ASML.AS' AND close > 700 ORDER BY date DESC LIMIT 5""").df()
symbol
date
close
volume
0
ASML.AS
2026-03-12
1190.8
128223
1
ASML.AS
2026-03-11
1198.8
562904
2
ASML.AS
2026-03-10
1200.0
800815
3
ASML.AS
2026-03-09
1147.6
689086
4
ASML.AS
2026-03-06
1147.0
857271
Polars — multi-condition filter with & and |
Combine conditions with & (AND) and | (OR). Each condition must be a pl.col() expression — use parentheses around each condition when combining, as Python operator precedence can cause unexpected grouping.
Example: Polars — multi-condition filter with & and |.
# Polars — AND filter with &pl.read_parquet("C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet").filter( (pl.col("symbol") == "ASML.AS") & (pl.col("close") > 700)).select("symbol", "date", "close", "volume").sort("date", descending=True).head(5)
symbol
date
close
volume
0
ASML.AS
2026-03-12 00:00:00
1190.800000
128223
1
ASML.AS
2026-03-11 00:00:00
1198.800000
562904
2
ASML.AS
2026-03-10 00:00:00
1200.000000
800815
3
ASML.AS
2026-03-09 00:00:00
1147.600000
689086
4
ASML.AS
2026-03-06 00:00:00
1147.000000
857271
Pandas — multi-condition filter with & and |
Same parentheses rule as Polars: wrap each condition in parentheses. & is bitwise AND on boolean Series; and is a Python keyword and will not work with pandas. For more readable filters, use df.query("symbol == 'ASML.AS' and close > 700").
Example: Pandas — multi-condition filter with & and |.
# Pandas — AND filter with &df = pd.read_parquet("C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet", columns=["symbol", "date", "close", "volume"])df[(df["symbol"] == "ASML.AS") & (df["close"] > 700)].sort_values("date", ascending=False).head(5)
symbol
date
close
volume
11964
ASML.AS
2026-03-12
1190.8
128223
11963
ASML.AS
2026-03-11
1198.8
562904
11962
ASML.AS
2026-03-10
1200.0
800815
11961
ASML.AS
2026-03-09
1147.6
689086
11960
ASML.AS
2026-03-06
1147.0
857271
Add computed column directly from file
DuckDB — computed column with SELECT expression
Computed columns in SQL are expressions in the SELECT list, aliased with AS. ROUND(high - low, 2) is evaluated per row — no intermediate storage. DuckDB can push the ORDER BY over computed columns without materialising them first.
Example: DuckDB — computed column with SELECT expression.
# DuckDB — add daily range columnduck.execute(""" SELECT symbol, date, high, low, ROUND(high - low, 2) AS daily_range FROM 'C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet' WHERE symbol = 'ASML.AS' ORDER BY daily_range DESC LIMIT 5""").df()
symbol
date
high
low
daily_range
0
ASML.AS
2024-10-15
804.6
665.0
139.6
1
ASML.AS
2026-01-28
1309.0
1185.4
123.6
2
ASML.AS
2026-02-26
1304.3
1210.8
93.5
3
ASML.AS
2024-08-05
749.9
657.0
92.9
4
ASML.AS
2025-04-07
596.2
508.4
87.8
Polars — computed column with with_columns()
.with_columns(expr.alias("name")) adds one or more computed columns without modifying existing ones. Expressions operate on full columns (vectorised), not row-by-row — no Python loop is needed.
Example: Polars — computed column with with_columns().
.assign(col=lambda d: expr) adds a computed column in a method-chain-friendly way. The lambda receives the current DataFrame (d), allowing reference to other columns. round() is a Python built-in here — use df["col"].round(n) for the pandas vectorised version.
DuckDB can apply window functions directly on a Parquet file without loading it into memory first — unique to DuckDB among the three tools. Polars and pandas must load the full dataset before computing shifted values.
Example: DuckDB — LAG() window function on Parquet file.
# DuckDB — daily returns with LAG() directly on Parquet (not possible in Polars/Pandas without loading)duck.execute(""" SELECT symbol, date, close, LAG(close) OVER (PARTITION BY symbol ORDER BY date) AS prev_close, ROUND((close - LAG(close) OVER (PARTITION BY symbol ORDER BY date)) / LAG(close) OVER (PARTITION BY symbol ORDER BY date) * 100, 2) AS daily_return_pct FROM 'C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet' WHERE symbol = 'ASML.AS' ORDER BY date DESC LIMIT 10""").df()
symbol
date
close
prev_close
daily_return_pct
0
ASML.AS
2026-03-12
1190.8
1198.8
-0.67
1
ASML.AS
2026-03-11
1198.8
1200.0
-0.10
2
ASML.AS
2026-03-10
1200.0
1147.6
4.57
3
ASML.AS
2026-03-09
1147.6
1147.0
0.05
4
ASML.AS
2026-03-06
1147.0
1186.0
-3.29
5
ASML.AS
2026-03-05
1186.0
1199.8
-1.15
6
ASML.AS
2026-03-04
1199.8
1161.8
3.27
7
ASML.AS
2026-03-03
1161.8
1210.4
-4.02
8
ASML.AS
2026-03-02
1210.4
1233.4
-1.86
9
ASML.AS
2026-02-27
1233.4
1232.4
0.08
Polars — equivalent with shift() (must load data first)
.shift(1).over("symbol") is Polars’ LAG equivalent — it shifts values by N positions within each group defined by .over(). The full dataset must be in memory first; there is no Parquet-level pushdown for window functions in Polars.
Example: Polars — equivalent with shift() (must load data first).
# Polars — LAG equivalent with shift().over() (data must be loaded, not lazy on file)pl.read_parquet("C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet").filter( pl.col("symbol") == "ASML.AS").sort("date").with_columns( pl.col("close").shift(1).over("symbol").alias("prev_close")).with_columns( ((pl.col("close") - pl.col("prev_close")) / pl.col("prev_close") * 100).round(2).alias("daily_return_pct")).select("symbol", "date", "close", "prev_close", "daily_return_pct").sort("date", descending=True).head(10)
symbol
date
close
prev_close
daily_return_pct
0
ASML.AS
2026-03-12 00:00:00
1190.800000
1198.800000
-0.670000
1
ASML.AS
2026-03-11 00:00:00
1198.800000
1200.000000
-0.100000
2
ASML.AS
2026-03-10 00:00:00
1200.000000
1147.600000
4.570000
3
ASML.AS
2026-03-09 00:00:00
1147.600000
1147.000000
0.050000
4
ASML.AS
2026-03-06 00:00:00
1147.000000
1186.000000
-3.290000
5
ASML.AS
2026-03-05 00:00:00
1186.000000
1199.800000
-1.150000
6
ASML.AS
2026-03-04 00:00:00
1199.800000
1161.800000
3.270000
7
ASML.AS
2026-03-03 00:00:00
1161.800000
1210.400000
-4.020000
8
ASML.AS
2026-03-02 00:00:00
1210.400000
1233.400000
-1.860000
9
ASML.AS
2026-02-27 00:00:00
1233.400000
1232.400000
0.080000
Performance — format comparison
DuckDB — benchmark same query on CSV vs Parquet vs JSON
Same GROUP BY query on three file formats — same data, different encodings. Parquet is columnar and compressed so only the symbol and close columns are read; CSV and JSON require full row parsing. The result quantifies the I/O advantage of columnar formats.
Example: DuckDB — benchmark same query on CSV vs Parquet vs JSON.
# Format comparison timingresults = []for fmt, path in [("CSV", "C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.csv"), ("Parquet", "C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.parquet"), ("JSON", "C:/Users/aperi/DEV/LANG/data/eurostoxx50_ohlcv.json")]: start = time.perf_counter() rows = duck.execute(f"SELECT symbol, COUNT(*), AVG(close) FROM '{path}' GROUP BY symbol").fetchall() elapsed = (time.perf_counter() - start) * 1000 results.append({"Format": fmt, "Rows": len(rows), "Time (ms)": round(elapsed, 1)})pd.DataFrame(results)
Format
Rows
Time (ms)
0
CSV
50
59.8
1
Parquet
50
3.1
2
JSON
50
76.7
DuckDB vs Polars vs Pandas — reference
Operation
DuckDB SQL
Polars
Pandas
Read Parquet
SELECT FROM 'file.parquet'
pl.read_parquet(path)
pd.read_parquet(path)
Read CSV
SELECT FROM 'file.csv'
pl.read_csv(path)
pd.read_csv(path)
Filter
WHERE col = 'val'
df.filter(pl.col("c")==v)
df[df["c"]==v]
Select cols
SELECT a, b
df.select("a","b")
df[["a","b"]]
Sort
ORDER BY col DESC
df.sort("c", descending=True)
df.sort_values("c", ascending=False)
Limit
LIMIT 10
df.head(10)
df.head(10)
Group + Agg
GROUP BY ... AVG(c)
df.group_by("c").agg(...)
df.groupby("c").agg(...)
Window
LAG() OVER (PARTITION BY ...)
pl.col("c").shift(1).over("g")
df.groupby("g")["c"].shift(1)
Export
COPY TO 'file.parquet'
df.write_parquet(path)
df.to_parquet(path)
Lazy eval
No
pl.scan_parquet(path)
No
Returns
.df() → pandas DataFrame
Polars DataFrame
pandas DataFrame
Operational Risks
Query safety and transaction durability
Interpolated SQL with f"...{value}..."
Direct interpolation pushes user-controlled values into the SQL parser. Use ? with sqlite3 or pyodbc, and use :name bind parameters inside SQLAlchemy text().
Example: Interpolated SQL with f"...{value}...".
name = "alice' OR 1=1 --"unsafe = f"SELECT * FROM users WHERE name = '{name}'"safe_sql = "SELECT * FROM users WHERE name = ?"safe_params = (name,)print(unsafe)print(safe_sql)print(safe_params)
SELECT * FROM users WHERE name = 'alice' OR 1=1 --'SELECT * FROM users WHERE name = ?("alice' OR 1=1 --",)
DML without conn.commit()
Manual-commit drivers leave inserts, updates, and deletes pending until conn.commit() runs. If the session rolls back or closes first, the write set disappears.
Example: DML without conn.commit().
import sqlite3conn = sqlite3.connect(":memory:")conn.execute("CREATE TABLE trades (ticker TEXT)")conn.execute("INSERT INTO trades VALUES ('ASML.AS')")print(conn.total_changes)conn.rollback()print(conn.execute("SELECT COUNT(*) FROM trades").fetchone()[0])conn.close()
10
SQLAlchemy lifecycle and API drift
Recreating create_engine() inside hot paths
Each create_engine() call creates a separate connection pool. Building one per request discards pooling benefits and increases connection churn.
Example: Recreating create_engine() inside hot paths.
from sqlalchemy import create_engineengines = [create_engine("sqlite:///:memory:") for _ in range(3)]print(len({id(engine.pool) for engine in engines}))
3
Long-lived Session objects
A long-lived Session keeps transaction state and identity-map data resident for longer than necessary. Keep a Session scoped to one unit of work, then close it.
Example: Long-lived Session objects.
from sqlalchemy import create_enginefrom sqlalchemy.orm import Sessionengine = create_engine("sqlite:///:memory:")with Session(engine) as short_lived: print(short_lived.is_active)long_lived = Session(engine)long_lived.info["pending_batch"] = ["ASML.AS", "SAP.DE", "MC.PA"]print(len(long_lived.info["pending_batch"]))long_lived.close()
True3
Raw strings in pd.read_sql() with SQLAlchemy 2.x
When the connection layer is SQLAlchemy, wrap the statement in text() before calling pd.read_sql(). That keeps the query aligned with SQLAlchemy 2.x expectations.
Example: Raw strings in pd.read_sql() with SQLAlchemy 2.x.
from sqlalchemy import textstatement = text("SELECT 1 AS value")print(type(statement).__name__)print("pd.read_sql(text('SELECT 1 AS value'), con=engine)")
TextClausepd.read_sql(text('SELECT 1 AS value'), con=engine)
Recommended Patterns
Secure connection and transaction setup
Use ? or :name placeholders with context managers
Pair bound parameters with a with sqlite3.connect(...) as conn: block or a short-lived Session. That gives safe input handling and a clear transaction boundary in the same pattern.
Example: Use ? or :name placeholders with context managers.
import sqlite3with sqlite3.connect(":memory:") as conn: conn.execute("CREATE TABLE trades (ticker TEXT)") conn.execute("INSERT INTO trades VALUES (?)", ("ASML.AS",)) print(conn.execute("SELECT COUNT(*) FROM trades WHERE ticker = ?", ("ASML.AS",)).fetchone()[0])
1
Load SQL Server connection strings from SQLSERVER_STOXX_ODBC
Keep SQLSERVER_STOXX_ODBC in the environment or a secret store so the note body never becomes the credential system of record. The same variable can hold either Trusted_Connection=yes or a SQL login supplied elsewhere.
Example: Load SQL Server connection strings from SQLSERVER_STOXX_ODBC.
import osos.environ["SQLSERVER_STOXX_ODBC"] = ( "Driver={ODBC Driver 18 for SQL Server};" "Server=localhost,1434;Database=stoxx;" "Trusted_Connection=yes;Encrypt=yes;TrustServerCertificate=yes;")print("Database=stoxx" in os.environ["SQLSERVER_STOXX_ODBC"])print("PWD=" in os.environ["SQLSERVER_STOXX_ODBC"])
TrueFalse
Group related DML in one explicit transaction
Batch related writes inside one with conn: block so the unit of work either commits together or rolls back together. This is the simplest way to keep INSERT and UPDATE steps atomic.
Example: Group related DML in one explicit transaction.
import sqlite3with sqlite3.connect(":memory:") as conn: conn.execute("CREATE TABLE trades (ticker TEXT, qty INTEGER)") conn.executemany("INSERT INTO trades VALUES (?, ?)", [("ASML.AS", 10), ("SAP.DE", 20)]) print(conn.execute("SELECT SUM(qty) FROM trades").fetchone()[0])
30
Efficient reads and ORM defaults
Reuse one engine and short-lived Session objects
Create one shared engine, then open and close a Session around each unit of work. That reuses the pool while keeping transaction scope tight.
Example: Reuse one engine and short-lived Session objects.
from sqlalchemy import create_enginefrom sqlalchemy.orm import Sessionengine = create_engine("sqlite:///:memory:")with Session(engine) as first: print(first.bind is engine)with Session(engine) as second: print(second.bind is engine)
TrueTrue
Stream large reads with chunksize
pd.read_sql(..., chunksize=...) returns an iterator of DataFrames instead of materialising the whole result set at once. Use it when the query is large but row order is still important.
Example: Stream large reads with chunksize.
import pandas as pdfrom sqlalchemy import create_engine, textengine = create_engine("sqlite:///:memory:")with engine.begin() as conn: conn.execute(text("CREATE TABLE ticks (id INTEGER PRIMARY KEY, symbol TEXT)")) conn.execute(text("INSERT INTO ticks (symbol) VALUES ('ASML.AS'), ('SAP.DE'), ('MC.PA')"))with engine.connect() as conn: for chunk in pd.read_sql(text("SELECT * FROM ticks ORDER BY id"), con=conn, chunksize=2): print(len(chunk))
21
Prefer mapped_column() with Mapped[T]
mapped_column() and Mapped[T] keep SQLAlchemy 2.x models typed and explicit. That pattern reads better than mixing legacy Column() declarations into otherwise typed models.
DuckDB is optimized for analytical scans and SQL over files. Reach for duckdb.connect() and file-backed SQL when the workload is ad hoc analytics rather than row-by-row OLTP.
Example: Use DuckDB for file-based analytics.
import duckdbduck = duckdb.connect(":memory:")print(duck.execute("SELECT SUM(i) FROM range(5) tbl(i)").fetchone()[0])
10
Python Database Troubleshooting
Minimal probes below are designed to confirm the failure boundary quickly before you revisit the larger examples earlier in the note.
SQLite state and filesystem issues
OperationalError: no such table
If OperationalError: no such table appears, confirm that the schema was created in the same database file or in the same :memory: connection before you query it.
Example: OperationalError: no such table.
import sqlite3with sqlite3.connect(":memory:") as conn: conn.execute("CREATE TABLE trades (id INTEGER)") print(conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchone()[0])
trades
Lost writes after script exit without commit()
If rows disappear after the script exits, inspect the transaction boundary first. A missing commit() or an explicit rollback() is enough to erase the pending DML.
Example: Lost writes after script exit without commit().
import sqlite3conn = sqlite3.connect(":memory:")conn.execute("CREATE TABLE trades (id INTEGER)")conn.execute("INSERT INTO trades VALUES (1)")conn.rollback()print(conn.execute("SELECT COUNT(*) FROM trades").fetchone()[0])conn.close()
0
sqlite3.ProgrammingError after conn.close()
A closed connection cannot be reused. If you see sqlite3.ProgrammingError, reopen the database and rebuild any cursor tied to the old conn.
Example: sqlite3.ProgrammingError after conn.close().
import sqlite3conn = sqlite3.connect(":memory:")conn.close()try: conn.execute("SELECT 1")except sqlite3.ProgrammingError as exc: print(exc)
Cannot operate on a closed database.
SQL Server connectivity and parameter binding
pyodbc.Error: [08001] on connect
A pyodbc.Error: [08001] usually points to a reachability or naming issue outside the query text. Verify Server=..., the firewall rule, and the SQL Server Browser service before changing the SQL itself.
Example: pyodbc.Error: [08001] on connect.
checks = [ "Server=localhost,1434", "Firewall allows TCP 1434", "SQL Server Browser running",]for item in checks: print(item)
Server=localhost,1434Firewall allows TCP 1434SQL Server Browser running
InterfaceError: parameter marker not supported
InterfaceError: parameter marker not supported almost always means the placeholder style does not match the driver layer. Check whether the call site expects ?, %s, or :name before you retry.
Example: InterfaceError: parameter marker not supported.
A DetachedInstanceError means the ORM object wants data from a Session that is already gone. Use joinedload(), expire_on_commit=False, or access the relationship inside the Session block.
Example: DetachedInstanceError after the session closes.
fixes = ["joinedload()", "expire_on_commit=False", "access related rows before session exit"]for item in fixes: print(item)
joinedload()expire_on_commit=Falseaccess related rows before session exit
Slow DataFrame.to_sql() writes
If DataFrame.to_sql() is slow, switch from row-by-row inserts to batched inserts with method="multi" and a practical chunksize.
Example: Slow DataFrame.to_sql() writes.
options = {"method": "multi", "chunksize": 1000}for key, value in options.items(): print(f"{key}={value}")
method=multichunksize=1000
ORM insert appears to do nothing without session.commit()
session.add() only stages the object. Query the table after the block if you need to prove whether session.commit() actually ran.
Example: ORM insert appears to do nothing without session.commit().
from sqlalchemy import create_engine, textfrom sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_columnclass ItemBase(DeclarativeBase): passclass Item(ItemBase): __tablename__ = "items" id: Mapped[int] = mapped_column(primary_key=True)engine = create_engine("sqlite:///:memory:")ItemBase.metadata.create_all(engine)with Session(engine) as session: session.add(Item(id=1))with engine.connect() as conn: print(conn.execute(text("SELECT COUNT(*) FROM items")).scalar())with Session(engine) as session: session.add(Item(id=1)) session.commit()with engine.connect() as conn: print(conn.execute(text("SELECT COUNT(*) FROM items")).scalar())
01
DuckDB schema diagnostics
BinderException: referenced column not found
When DuckDB raises BinderException: referenced column not found, inspect the schema first. DESCRIBE SELECT * FROM file is the fastest way to confirm the available column names before you rework the query.
Example: BinderException: referenced column not found.
import duckdbduck = duckdb.connect(":memory:")print(duck.execute("DESCRIBE SELECT * FROM range(1) tbl(id)").fetchone()[0])