MySQL Join
Объединяйте таблицы MySQL в Python с помощью INNER JOIN, LEFT JOIN, RIGHT JOIN и FULL OUTER JOIN. Примеры с обработкой ошибок.
SQL JOIN позволяет объединять строки из двух или более таблиц на основе связанного столбца. На этой странице объясняется каждый тип соединения, поддерживаемый при работе с MySQL из Python, приводятся полные готовые к выполнению примеры для каждого из них, а также рассматриваются лучшие практики: параметризованные запросы и корректное освобождение ресурсов.
Перед чтением этой главы убедитесь, что вы умеете подключаться к MySQL, создавать таблицы и выбирать строки.
Предварительные требования
Установите коннектор, если вы ещё этого не сделали:
pip install mysql-connector-pythonПримерные таблицы, используемые в этой главе
Все приведённые ниже примеры предполагают, что в базе данных mydatabase существуют две таблицы — customers и orders. Выполните этот SQL один раз, чтобы создать и заполнить их:
CREATE TABLE IF NOT EXISTS customers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
address VARCHAR(200)
);
CREATE TABLE IF NOT EXISTS orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT,
order_date DATE,
order_total DECIMAL(10, 2),
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
INSERT INTO customers (name, address) VALUES
('Alice', '123 Maple St'),
('Bob', '456 Oak Ave'),
('Charlie', '789 Pine Rd');
INSERT INTO orders (customer_id, order_date, order_total) VALUES
(1, '2024-01-10', 99.99),
(1, '2024-02-14', 45.00),
(2, '2024-03-05', 210.50);
-- Charlie has no orders, so he will appear only in LEFT/FULL joins.Обратите внимание, что у Charlie нет соответствующих заказов. Это различие делает разницу между типами соединений наглядно видимой в результатах.
Типы соединений таблиц
| Тип соединения | Что возвращает |
|---|---|
INNER JOIN | Только строки, совпадающие в обеих таблицах |
LEFT JOIN | Все строки из левой таблицы; NULL там, где нет совпадения справа |
RIGHT JOIN | Все строки из правой таблицы; NULL там, где нет совпадения слева |
FULL OUTER JOIN (через UNION) | Все строки из обеих таблиц; NULL на той стороне, где нет совпадения |
В MySQL отсутствует ключевое слово FULL OUTER JOIN. Для получения того же результата используйте UNION из LEFT JOIN и RIGHT JOIN.
INNER JOIN
INNER JOIN возвращает только те строки, для которых условие соединения выполняется в обеих таблицах. Используйте его, когда вас интересуют только клиенты, у которых действительно есть заказы.
import mysql.connector
from mysql.connector import Error
try:
mydb = mysql.connector.connect(
host="localhost",
user="yourusername",
password="yourpassword",
database="mydatabase"
)
mycursor = mydb.cursor()
sql = """
SELECT customers.name, customers.address,
orders.order_date, orders.order_total
FROM customers
INNER JOIN orders ON customers.id = orders.customer_id
"""
mycursor.execute(sql)
results = mycursor.fetchall()
for row in results:
print(row)
except Error as e:
print(f"Error: {e}")
finally:
if mycursor:
mycursor.close()
if mydb.is_connected():
mydb.close()Ожидаемый вывод (с использованием примерных данных выше):
('Alice', '123 Maple St', datetime.date(2024, 1, 10), Decimal('99.99'))
('Alice', '123 Maple St', datetime.date(2024, 2, 14), Decimal('45.00'))
('Bob', '456 Oak Ave', datetime.date(2024, 3, 5), Decimal('210.50'))Charlie отсутствует, потому что у него нет соответствующих строк заказов.
LEFT JOIN
LEFT JOIN возвращает каждую строку из левой таблицы (customers) и соответствующие строки из правой таблицы (orders). Когда совпадение отсутствует, столбцы из правой таблицы принимают значение None в Python.
Используйте LEFT JOIN, если хотите видеть всех клиентов, в том числе тех, кто ещё не сделал ни одного заказа.
import mysql.connector
from mysql.connector import Error
try:
mydb = mysql.connector.connect(
host="localhost",
user="yourusername",
password="yourpassword",
database="mydatabase"
)
mycursor = mydb.cursor()
sql = """
SELECT customers.name, customers.address,
orders.order_date, orders.order_total
FROM customers
LEFT JOIN orders ON customers.id = orders.customer_id
"""
mycursor.execute(sql)
results = mycursor.fetchall()
for row in results:
print(row)
except Error as e:
print(f"Error: {e}")
finally:
if mycursor:
mycursor.close()
if mydb.is_connected():
mydb.close()Ожидаемый вывод:
('Alice', '123 Maple St', datetime.date(2024, 1, 10), Decimal('99.99'))
('Alice', '123 Maple St', datetime.date(2024, 2, 14), Decimal('45.00'))
('Bob', '456 Oak Ave', datetime.date(2024, 3, 5), Decimal('210.50'))
('Charlie', '789 Pine Rd', None, None)Charlie появляется со значением None в столбцах заказов, поскольку у него нет заказов.
RIGHT JOIN
RIGHT JOIN — зеркальное отражение LEFT JOIN. Он возвращает каждую строку из правой таблицы (orders) и соответствующие строки из левой таблицы (customers). Строки в orders, для которых нет совпадающего клиента, показывают None в столбцах клиента.
На практике RIGHT JOIN используется реже, чем LEFT JOIN, поскольку его всегда можно переписать как LEFT JOIN, поменяв таблицы местами.
import mysql.connector
from mysql.connector import Error
try:
mydb = mysql.connector.connect(
host="localhost",
user="yourusername",
password="yourpassword",
database="mydatabase"
)
mycursor = mydb.cursor()
sql = """
SELECT customers.name, customers.address,
orders.order_date, orders.order_total
FROM customers
RIGHT JOIN orders ON customers.id = orders.customer_id
"""
mycursor.execute(sql)
results = mycursor.fetchall()
for row in results:
print(row)
except Error as e:
print(f"Error: {e}")
finally:
if mycursor:
mycursor.close()
if mydb.is_connected():
mydb.close()Ожидаемый вывод (с примерными данными все заказы имеют совпадающего клиента, поэтому результат выглядит так же, как при INNER JOIN):
('Alice', '123 Maple St', datetime.date(2024, 1, 10), Decimal('99.99'))
('Alice', '123 Maple St', datetime.date(2024, 2, 14), Decimal('45.00'))
('Bob', '456 Oak Ave', datetime.date(2024, 3, 5), Decimal('210.50'))FULL OUTER JOIN (через UNION)
В MySQL нет ключевого слова FULL OUTER JOIN, но тот же результат можно получить, объединив LEFT JOIN и RIGHT JOIN с помощью UNION. UNION автоматически удаляет дублирующиеся строки.
import mysql.connector
from mysql.connector import Error
try:
mydb = mysql.connector.connect(
host="localhost",
user="yourusername",
password="yourpassword",
database="mydatabase"
)
mycursor = mydb.cursor()
sql = """
SELECT customers.name, customers.address,
orders.order_date, orders.order_total
FROM customers
LEFT JOIN orders ON customers.id = orders.customer_id
UNION
SELECT customers.name, customers.address,
orders.order_date, orders.order_total
FROM customers
RIGHT JOIN orders ON customers.id = orders.customer_id
"""
mycursor.execute(sql)
results = mycursor.fetchall()
for row in results:
print(row)
except Error as e:
print(f"Error: {e}")
finally:
if mycursor:
mycursor.close()
if mydb.is_connected():
mydb.close()Ожидаемый вывод:
('Alice', '123 Maple St', datetime.date(2024, 1, 10), Decimal('99.99'))
('Alice', '123 Maple St', datetime.date(2024, 2, 14), Decimal('45.00'))
('Bob', '456 Oak Ave', datetime.date(2024, 3, 5), Decimal('210.50'))
('Charlie', '789 Pine Rd', None, None)Все клиенты появляются в результате (включая Charlie без заказов), и все заказы тоже присутствуют (в том числе те, у которых может не быть соответствующего клиента).
Фильтрация результатов JOIN с помощью WHERE
К любому соединению можно добавить предложение WHERE, чтобы сузить набор результатов. Всегда используйте параметризованные запросы (заполнитель %s) вместо форматирования строк, чтобы избежать SQL-инъекций.
Следующий пример извлекает только заказы конкретного клиента по имени:
import mysql.connector
from mysql.connector import Error
try:
mydb = mysql.connector.connect(
host="localhost",
user="yourusername",
password="yourpassword",
database="mydatabase"
)
mycursor = mydb.cursor()
sql = """
SELECT customers.name, orders.order_date, orders.order_total
FROM customers
INNER JOIN orders ON customers.id = orders.customer_id
WHERE customers.name = %s
"""
val = ("Alice",)
mycursor.execute(sql, val)
results = mycursor.fetchall()
for row in results:
print(row)
except Error as e:
print(f"Error: {e}")
finally:
if mycursor:
mycursor.close()
if mydb.is_connected():
mydb.close()Ожидаемый вывод:
('Alice', datetime.date(2024, 1, 10), Decimal('99.99'))
('Alice', datetime.date(2024, 2, 14), Decimal('45.00'))Сортировка результатов JOIN с помощью ORDER BY
Комбинируйте соединение с ORDER BY для управления порядком вывода. В этом примере все заказы клиентов выводятся отсортированными по дате от самого нового к самому старому:
import mysql.connector
from mysql.connector import Error
try:
mydb = mysql.connector.connect(
host="localhost",
user="yourusername",
password="yourpassword",
database="mydatabase"
)
mycursor = mydb.cursor()
sql = """
SELECT customers.name, orders.order_date, orders.order_total
FROM customers
INNER JOIN orders ON customers.id = orders.customer_id
ORDER BY orders.order_date DESC
"""
mycursor.execute(sql)
results = mycursor.fetchall()
for row in results:
print(row)
except Error as e:
print(f"Error: {e}")
finally:
if mycursor:
mycursor.close()
if mydb.is_connected():
mydb.close()Ожидаемый вывод:
('Bob', datetime.date(2024, 3, 5), Decimal('210.50'))
('Alice', datetime.date(2024, 2, 14), Decimal('45.00'))
('Alice', datetime.date(2024, 1, 10), Decimal('99.99'))Ограничение результатов JOIN с помощью LIMIT
Сочетайте соединение с предложением LIMIT для эффективной постраничной навигации по большим наборам результатов:
import mysql.connector
from mysql.connector import Error
try:
mydb = mysql.connector.connect(
host="localhost",
user="yourusername",
password="yourpassword",
database="mydatabase"
)
mycursor = mydb.cursor()
sql = """
SELECT customers.name, orders.order_date, orders.order_total
FROM customers
INNER JOIN orders ON customers.id = orders.customer_id
ORDER BY orders.order_date DESC
LIMIT 2
"""
mycursor.execute(sql)
results = mycursor.fetchall()
for row in results:
print(row)
except Error as e:
print(f"Error: {e}")
finally:
if mycursor:
mycursor.close()
if mydb.is_connected():
mydb.close()Ожидаемый вывод (только два самых последних заказа):
('Bob', datetime.date(2024, 3, 5), Decimal('210.50'))
('Alice', datetime.date(2024, 2, 14), Decimal('45.00'))Лучшие практики
- Используйте параметризованные запросы. Передавайте введённые пользователем значения вторым аргументом в
cursor.execute()с заполнителями%s. Никогда не используйте форматирование строк Python или f-строки для построения SQL — это открывает приложение для SQL-инъекций. - Оборачивайте код базы данных в
try...except...finally. Это гарантирует, что соединения и курсоры всегда будут закрыты, даже при возникновении ошибки. - Выбирайте только нужные столбцы. Использование
SELECT *при объединении таблиц может включать множество избыточных столбцов и снижать производительность на больших таблицах. - Добавляйте индексы на столбцы соединения. Если
orders.customer_idне проиндексирован, MySQL будет сканировать всю таблицу при каждом соединении. Ограничение внешнего ключа (как показано в скрипте настройки выше) создаёт индекс автоматически. - Предпочитайте
LEFT JOINвместоRIGHT JOINдля удобочитаемости.RIGHT JOINвсегда можно переписать какLEFT JOIN, поменяв местами таблицы, что большинству разработчиков легче воспринимать.
Краткий справочник
| Сценарий | Используемое соединение |
|---|---|
| Только записи с совпадениями в обеих таблицах | INNER JOIN |
| Все записи из основной (левой) таблицы, независимо от совпадений | LEFT JOIN |
| Все записи из вторичной (правой) таблицы, независимо от совпадений | RIGHT JOIN |
| Все записи из обеих таблиц, независимо от совпадений | LEFT JOIN ... UNION ... RIGHT JOIN |
| Сужение объединённых строк | Добавьте предложение WHERE с параметризованными значениями |
| Управление порядком вывода | Добавьте ORDER BY column ASC|DESC |
| Постраничный вывод результатов | Добавьте LIMIT n (см. MySQL Limit) |