JavaRush /Курси /Модуль 4: FastAPI /Як оптимізувати запити до бази даних через SQLAlchemy

Як оптимізувати запити до бази даних через SQLAlchemy

Модуль 4: FastAPI
Рівень 11 , Лекція 4
Відкрита

Коли ми розробляємо додаток, найчастіше він працює з базою даних за принципом "запит-відповідь". Проте, як показує практика, "в лоб" вирішувати всі завдання не завжди ефективно. Давай почнемо з основ: навіщо взагалі потрібно оптимізувати запити?

Уяви, що твоя база даних — це великий склад, а запит — це замовлення на пакування посилки. Якщо склад погано організований (наприклад, товари звалені в одну купу), то пошук однієї коробки може зайняти цілий день. SQLAlchemy допомагає нам структурувати склад (створювати таблиці і звʼязки), але без оптимізації запитів навіть ідеальна структура перетворюється на болото.

Ключові причини для оптимізації запитів:

  • Збільшення швидкості виконання запитів.
  • Зниження навантаження на сервер бази даних.
  • Економія ресурсів у системі.

Запити очима SQLAlchemy

SQLAlchemy надає два основні методи завантаження даних:

  1. Lazy loading — завантаження даних за потреби.
  2. Eager loading — завантаження всіх пов'язаних даних за один запит.

Ці методи впливають на продуктивність. Наприклад, Lazy loading може викликати серію "N+1 запитів", тоді як Eager loading дозволяє цього уникнути. Про це поговоримо трохи пізніше.


Використання індексів і ключів

Індекси — це рятувальний круг для даних, які часто шукають. Якщо таблиця — це книга, то індекс — це її зміст, який допомагає миттєво знаходити потрібні сторінки.

Як індекси впливають на продуктивність?

Вони прискорюють вибірку даних за рахунок оптимізації пошуку. Без індексу база даних має сканувати всю таблицю, як робот-прибиральник, поки не знайде підходящу запис.

Приклад: припустимо, у нас є таблиця користувачів з мільйоном записів, і ми хочемо знайти користувача з певним email. Без індексу запит на пошук буде сканувати кожний рядок, поки не знайде потрібний.

Наведемо приклад створення індексів з SQLAlchemy:


from sqlalchemy import Column, Integer, String, Index
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'user'

    id = Column(Integer, primary_key=True)
    username = Column(String, nullable=False)
    email = Column(String, nullable=False, unique=True)

    # Індекси
    __table_args__ = (
        Index('idx_email', 'email'),  # Створюємо індекс для поля email
    )

У наведеному коді ми додали індекс на стовпець email. Тепер пошук по цьому полю відбуватиметься значно швидше.

Зовнішні та первинні ключі автоматично створюють приховані індекси. Якщо у вас є відношення типу ForeignKey, то SQLAlchemy створює індекс для прискорення пошуку по пов'язаних записах.


Lazy loading і Eager loading

Lazy loading завантажує дані по мірі їх звернення. Це корисно, коли ти не знаєш, чи знадобляться пов'язані дані. Однак такий підхід може призвести до проблеми "N+1 запитів".

Приклад проблеми:


# Припустимо, ми завантажуємо користувачів з їхніми постами
users = session.query(User).all()
for user in users:
    print(user.posts)  # Тут для кожного користувача виконується окремий запит!

Якщо users містить 100 записів, то буде виконано 1 запит на користувачів і 100 додаткових запитів для завантаження постів. Це може бути катастрофічно повільно!

Eager loading дозволяє завантажувати всі пов'язані дані за один запит. Для цього використовується метод joinedload:


from sqlalchemy.orm import joinedload

users = session.query(User).options(joinedload(User.posts)).all()
for user in users:
    print(user.posts)  # Тепер дані постів завантажуються заздалегідь

Eager loading вирішує проблему "N+1 запитів" і робить роботу з даними значно швидшою, особливо коли ми знаємо, що пов'язані дані точно будуть потрібні.


Підходи до оптимізації

Не вибирай більше даних, ніж тобі потрібно. Вибір без обмежень — це як замовляти всі страви в меню, коли хочеш лише чашку кави. Використовуйте методи filter і limit:


# Приклад вибірки лише перших 10 користувачів
users = session.query(User).filter(User.is_active == True).limit(10).all()

Профілювання запитів

SQLAlchemy дозволяє легко перевірити, скільки запитів виконується і скільки вони займають часу. Для цього можна увімкнути логування SQL-запитів:


import logging
logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)

# Тепер усі запити до бази будуть виводитись у консоль

Використання підзапитів

Підзапити дозволяють виконувати одну операцію за іншою, підвищуючи ефективність.

Приклад:


from sqlalchemy.sql import select

# Підзапит для отримання активних користувачів
active_users_query = select(User.id).where(User.is_active == True).subquery()

# Використовуємо підзапит у основному запиті
posts = session.query(Post).filter(Post.user_id.in_(active_users_query)).all()

Приклади з реальних додатків

Сценарій: пости і коментарі

Уявімо, що ми пишемо блог. У нас є дві таблиці: Post і Comment. Ми хочемо отримати список постів з кількістю коментарів для кожного.

Приклад реалізації:


from sqlalchemy import func

# Використовуємо агрегацію для підрахунку коментарів
posts = session.query(
    Post.title,
    func.count(Comment.id).label('comment_count')
).join(Comment).group_by(Post.id).all()

for post in posts:
    print(f"Post: {post.title}, Comments: {post.comment_count}")

Цей запит підрахує кількість коментарів для кожного поста за один прохід. Без агрегації нам би довелося виконувати додаткові запити для кожного поста.

Сценарій: динамічна фільтрація

Складні фільтри часто використовуються в реальних додатках. Приклад:


filters = [User.is_active == True]
if username_filter:
    filters.append(User.username.ilike(f"%{username_filter}%"))

users = session.query(User).filter(*filters).all()

Цей код динамічно застосовує фільтри залежно від умов.


Інструменти для профілювання і аналізу

SQLAlchemy Profile: надає вбудовані механізми для аналізу запитів. За його допомогою можна зрозуміти, де знаходяться вузькі місця.

Приклад аналізу:


from sqlalchemy.ext.baked import BakedQuery

# Використовуємо BakedQuery для оптимізації повторюваних запитів
bq = BakedQuery(session.query(User))
bq += lambda q: q.filter(User.is_active == True)
users = bq.all()

Ми щойно пройшли потужні інструменти оптимізації запитів у SQLAlchemy. Використовуючи такі підходи, ти зможеш створювати швидкі й продуктивні додатки. І пам’ятай: "Немає нічого повільнішого за поганий SQL-запит, хіба що поганий SQL-запит з мільярдом записів!" 😊

3
Опитування
Основи транзакцій у SQL та їх використання в SQLAlchemy, рівень 11, лекція 4
Недоступний
Основи транзакцій у SQL та їх використання в SQLAlchemy
Основи транзакцій у SQL та їх використання в SQLAlchemy
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ