Execute Raw SQL
How to execute raw SQL queries safely with parameterized statements.
Overview
Even with ORMs, raw SQL is sometimes necessary for complex queries, migrations, or performance optimization. However, executing raw SQL without safeguards is a primary cause of SQL injection vulnerabilities. How to execute raw SQL safely using parameterized queries in Python, JavaScript, and Java.
When to Use
Use this resource when:
- Writing complex analytics queries that ORMs cannot express efficiently
- Executing database migrations or administrative commands
- Optimizing performance with database-specific SQL capabilities
Solution
Python
import psycopg2
conn = psycopg2.connect(host="localhost", database="mydb", user="user", password="pass")
cursor = conn.cursor()
# Safe parameterized query
cursor.execute("SELECT * FROM users WHERE email = %s AND active = %s", (email, True))
rows = cursor.fetchall()
# Safe insert with RETURNING
cursor.execute(
"INSERT INTO users (email, role) VALUES (%s, %s) RETURNING id",
(email, role)
)
user_id = cursor.fetchone()[0]
conn.commit()
cursor.close()
conn.close()
JavaScript
const { Pool } = require('pg');
const pool = new Pool({ /* config */ });
// Safe parameterized query
async function findUser(email) {
const result = await pool.query(
'SELECT * FROM users WHERE email = $1 AND active = $2',
[email, true]
);
return result.rows;
}
// Safe insert with RETURNING
async function createUser(email, role) {
const result = await pool.query(
'INSERT INTO users (email, role) VALUES ($1, $2) RETURNING id',
[email, role]
);
return result.rows[0].id;
}
Java
import java.sql.*;
public class RawSQL {
public void findUser(Connection conn, String email) throws SQLException {
String sql = "SELECT * FROM users WHERE email = ? AND active = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, email);
stmt.setBoolean(2, true);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("name"));
}
}
}
public int createUser(Connection conn, String email, String role) throws SQLException {
String sql = "INSERT INTO users (email, role) VALUES (?, ?) RETURNING id";
try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, email);
stmt.setString(2, role);
stmt.executeUpdate();
try (ResultSet keys = stmt.getGeneratedKeys()) {
keys.next();
return keys.getInt(1);
}
}
}
}
Explanation
Parameterized queries (prepared statements) separate SQL logic from data. The database compiles the SQL template once and binds values at execution time, making injection impossible. In Python, %s is a placeholder, not a format string. In JavaScript, $1, $2 are positional parameters. In Java, ? is the JDBC placeholder. None of these concatenate user input into the SQL string.
Variants
| Technology | Approach | Notes |
|---|---|---|
| Python | SQLAlchemy text() | Raw SQL within ORM with bound parameters |
| JavaScript | knex.raw() | Query builder with raw SQL and bindings |
| Java | Jdbi | Fluent API over JDBC with parameter binding |
What Works
- Never concatenate user input into SQL strings; always use parameterized queries
- Use
RETURNING(PostgreSQL) orgetGeneratedKeys()(JDBC) instead of separateSELECT MAX(id) - Wrap multiple statements in transactions with proper rollback on error
- Validate and whitelist table/column names when they must be live
- Log SQL execution times to detect slow queries and N+1 patterns
Common Mistakes
- Using Python f-strings, JS template literals, or Java
+concatenation for SQL - Assuming ORMs are always safe;
.query("..." + input)is still vulnerable - Sanitizing input with regex instead of using parameterized queries
- Forgetting to commit transactions, leaving data in an inconsistent state
- Using
Statementinstead ofPreparedStatementin Java
Additional Common Mistakes
-
Using
executemany()for bulk inserts without testing. Some drivers execute individual statements, making it no faster than a loop. UseCOPYin PostgreSQL or batch inserts withVALUESlists. -
Not closing cursors and connections. Use context managers (
withblocks) to ensure resources are released. -
Ignoring result set size. Fetching millions of rows into memory causes OOM. Use server-side cursors or pagination:
cursor.execute("SELECT * FROM large_table")
while True:
rows = cursor.fetchmany(1000)
if not rows:
break
process(rows)
-
Mixing parameterized and string-formatted SQL. Even one
f-stringinterpolation in a parameterized query introduces injection risk. -
Not handling
NULLin parameterized queries.WHERE col = %swithNonereturns no rows. UseIS DISTINCT FROMorIS NULLfor NULL-safe comparisons.
Performance Tips
-
Use
COPYfor bulk inserts in PostgreSQL. It is 10-100x faster than individualINSERTstatements. -
Use
execute_valuesfor batch inserts in Python. Reduces round-trips by sending multiple rows in one statement:
from psycopg2.extras import execute_values
execute_values(
cursor,
"INSERT INTO users (email, name) VALUES %s",
[("alice@example.com", "Alice"), ("bob@example.com", "Bob")]
)
- Use server-side cursors for large result sets. Avoid loading millions of rows into memory:
cursor = conn.cursor("server_side_cursor")
cursor.execute("SELECT * FROM large_table")
for row in cursor:
process(row)
cursor.close()
- Prefer
EXISTSoverCOUNT(*)for existence checks.EXISTSstops scanning as soon as a match is found:
SELECT EXISTS(SELECT 1 FROM users WHERE email = 'alice@example.com');
- Use
PREPAREfor frequently repeated queries. PostgreSQL caches the query plan, reducing parse overhead for repeated executions.
Frequently Asked Questions
Is cursor.execute(f"SELECT * FROM {table}") safe?
No. Table and column names cannot be parameterized in most drivers. If live table names are required, whitelist them against a known set of valid names.
Can I use parameterized queries for IN clauses?
Most drivers do not support IN (%s) with a list. Use driver-specific extensions: ANY($1) in PostgreSQL, generate placeholders dynamically in Python/Java, or use find_in_set in MySQL.
Should I avoid raw SQL entirely and only use ORMs?
Not necessarily. ORMs excel at CRUD but struggle with complex aggregations, window functions, and database-specific optimizations. Use raw SQL for these cases, but always parameterize inputs.
Python with SQLAlchemy text()
from sqlalchemy import create_engine, text
engine = create_engine("postgresql://user:pass@localhost/mydb")
with engine.connect() as conn:
# Parameterized raw SQL within SQLAlchemy
result = conn.execute(
text("SELECT * FROM users WHERE email = :email AND active = :active"),
{"email": "alice@example.com", "active": True}
)
for row in result:
print(row.name, row.email)
# Transaction with raw SQL
conn.execute(
text("INSERT INTO audit_log (action, user_id) VALUES (:action, :user_id)"),
{"action": "login", "user_id": 1}
)
conn.commit()
JavaScript with knex.raw()
const knex = require('knex')({
client: 'pg',
connection: 'postgresql://user:pass@localhost/mydb'
});
// Raw SQL with bindings
const users = await knex.raw(
'SELECT * FROM users WHERE email = ? AND active = ?',
['alice@example.com', true]
);
// Raw SQL in a query builder chain
const activeUsers = await knex('users')
.whereRaw('created_at > NOW() - INTERVAL ? DAYS', [30])
.select('id', 'email');
Handling IN clauses safely
# Python: generate placeholders dynamically
emails = ['alice@example.com', 'bob@example.com']
placeholders = ','.join(['%s'] * len(emails))
cursor.execute(
f"SELECT * FROM users WHERE email IN ({placeholders})",
emails
)
# PostgreSQL: use ANY() with an array
cursor.execute(
"SELECT * FROM users WHERE email = ANY(%s)",
(emails,)
)
// JavaScript: use ANY() in PostgreSQL
const result = await pool.query(
'SELECT * FROM users WHERE email = ANY($1::text[])',
[emails]
);
// Java: build PreparedStatement with dynamic placeholders
List<String> emails = List.of("alice@example.com", "bob@example.com");
String placeholders = String.join(",", Collections.nCopies(emails.size(), "?"));
String sql = "SELECT * FROM users WHERE email IN (" + placeholders + ")";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
for (int i = 0; i < emails.size(); i++) {
stmt.setString(i + 1, emails.get(i));
}
ResultSet rs = stmt.executeQuery();
}
Whitelisting table/column names
ALLOWED_TABLES = {"users", "orders", "products"}
ALLOWED_COLUMNS = {"id", "name", "email", "amount", "status"}
def safe_query(table_name, column_name, value):
if table_name not in ALLOWED_TABLES:
raise ValueError(f"Invalid table: {table_name}")
if column_name not in ALLOWED_COLUMNS:
raise ValueError(f"Invalid column: {column_name}")
cursor.execute(
f"SELECT * FROM {table_name} WHERE {column_name} = %s",
(value,)
)
return cursor.fetchall()
Related Resources
Connect to MySQL
How to connect to MySQL databases in Python, JavaScript, and Java.
RecipeConnect to PostgreSQL
How to connect to PostgreSQL databases in Python, JavaScript, and Java.
RecipeConnect to Redis
How to connect to Redis and perform basic operations in Python, JavaScript, and Java.
RecipeEscape HTML Entities
How to escape HTML entities to prevent XSS attacks in Python, Java, and JavaScript.
RecipeSanitize User Input
How to sanitize and validate user input in Python, Java, and JavaScript to prevent injection attacks.
RecipeUse ORM for CRUD
How to perform CRUD operations using ORMs in Python, JavaScript, and Java.