Python Tutorial
Python MySQL - Join
Combine rows from two or more tables using a related column.
Why Join?
Relational databases split data across tables to avoid duplication. A join stitches related rows back together using a shared key — typically a foreign key that points to another table's primary key.
# users(id, name) orders(id, user_id, product)
# orders.user_id -> users.id is the relationship we join onINNER JOIN
An inner join returns only rows that have a match in both tables.
import mysql.connector
conn = mysql.connector.connect(
host="localhost", user="root", password="secret", database="shop")
cursor = conn.cursor()
cursor.execute(
"SELECT users.name, orders.product "
"FROM users "
"INNER JOIN orders ON users.id = orders.user_id"
)
for name, product in cursor.fetchall():
print(name, "ordered", product)LEFT JOIN and RIGHT JOIN
A LEFT JOIN keeps every row from the left table, filling NULL where the right table has no match — perfect for "users, even those with no orders". A RIGHT JOIN does the mirror image.
cursor.execute(
"SELECT users.name, orders.product "
"FROM users "
"LEFT JOIN orders ON users.id = orders.user_id")
# users with no orders appear with product = None| Join | Returns |
|---|---|
| INNER | Only matching rows in both tables |
| LEFT | All left rows + matches (NULL if none) |
| RIGHT | All right rows + matches (NULL if none) |
Table Aliases
Short aliases make multi-table queries readable and are required when a table joins to itself.
cursor.execute(
"SELECT u.name, o.product, o.amount "
"FROM users AS u "
"JOIN orders AS o ON u.id = o.user_id "
"WHERE o.amount > %s "
"ORDER BY o.amount DESC",
(100,))Joining Three Tables
cursor.execute(
"SELECT u.name, p.title, o.quantity "
"FROM orders o "
"JOIN users u ON o.user_id = u.id "
"JOIN products p ON o.product_id = p.id")Index the columns you join on (foreign keys). Joining on unindexed columns forces full table scans and is a common cause of slow queries.
Aggregating Across a Join
Combine joins with GROUP BY to summarize related data — for example, each user's total spend.
cursor.execute(
"SELECT u.name, COUNT(o.id) AS orders, COALESCE(SUM(o.amount), 0) AS total "
"FROM users u "
"LEFT JOIN orders o ON u.id = o.user_id "
"GROUP BY u.id, u.name "
"ORDER BY total DESC")
for row in cursor.fetchall():
print(row)Best Practices
- Choose the join type deliberately: INNER for matches only, LEFT to keep all left rows.
- Index foreign-key/join columns for performance.
- Qualify column names (
table.column) to avoid ambiguity. - Use
COALESCEto turn NULLs from outer joins into sensible defaults.
Try It Yourself
Exercise 1: You want all users, including those with no orders. Which join?
Show solution
A LEFT JOIN from users — it keeps every user, filling NULL where there is no matching order.
Exercise 2: Write an INNER JOIN listing each order's product with the buyer's name.
Show solution
cursor.execute(
"SELECT u.name, o.product FROM orders o "
"JOIN users u ON o.user_id = u.id")📘 Real-World Deep Dive
Knowing <strong>MySQL Join (MySQL)</strong> well is what turns MySQL from a curiosity into a daily tool — you'll reach for it in nearly every real project.
Real-Life Scenario
An end-to-end usage of MySQL Join that you'd actually see in a data pipeline or analytics notebook.
Real-Life Example
SELECT u.name, o.total
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'paid'
ORDER BY o.total DESC
LIMIT 10;Expected Output
(no output)Common mistakes
- Reading with the default cursor returns tuples — switch to
DictCursorfor named access. - Strings without parameter binding produce SQL-injection holes — never use f-strings in queries.
- Connecting through a long-lived daemon pool: make sure
pool_reset_connectionis on to avoid session state leakage. - Treating MySQL Join as a black box without reading the docs — the API has subtle defaults that bite when you scale.
🚀 Performance & Best Practices
- Use prepared statements (
cursor.executemany) for bulk inserts. - Fetch with
cursor.fetchmany(1000)in streams instead offetchall(). - Use
pymysql's server-side cursors for very large result sets. - When working with MySQL, prefer vectorised / batched operations over Python loops.
🧪 Try It Yourself
- Reproduce the snippet on a representative slice of your own data.
- Profile the snippet with
cProfileortimeitand find the single biggest improvement. - Generalise the snippet into a small, reusable function you can drop into future projects.