← Назад к вопросам

Когда нужно использовать сырой SQL?

2.0 Middle🔥 271 комментариев
#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Когда использовать сырой SQL

Raw SQL - мощный инструмент, но его нужно использовать осмотрительно. Рассмотрю когда это оправдано.

Почему нужен Raw SQL

ORM (SQLAlchemy, Django ORM) отлично для 80% случаев:

# ORM - простой CRUD
user = User.objects.get(id=123)
user.email = 'new@example.com'
user.save()

posts = Post.objects.filter(user=user).order_by('-created_at')[:10]

Но ORM может быть медленнее:

# ❌ Проблема N+1
users = User.objects.all()[:1000]
for user in users:  # +1000 дополнительных запросов!
    print(user.posts.count())

# ✓ Raw SQL решает это
result = db.execute("""
    SELECT u.*, COUNT(p.id) as post_count
    FROM users u
    LEFT JOIN posts p ON u.id = p.user_id
    GROUP BY u.id
    LIMIT 1000
""")

Случаи когда нужен Raw SQL

1. Сложные аналитические запросы

# Сложный report с множественными JOIN
query = """
    SELECT 
        DATE_TRUNC('month', p.created_at) as month,
        u.country,
        COUNT(DISTINCT u.id) as unique_users,
        COUNT(p.id) as total_posts,
        AVG(p.views) as avg_views,
        SUM(CASE WHEN p.status = 'published' THEN 1 ELSE 0 END) as published_count
    FROM users u
    INNER JOIN posts p ON u.id = p.user_id
    LEFT JOIN comments c ON p.id = c.post_id
    WHERE u.created_at > NOW() - INTERVAL '1 year'
        AND p.status IN ('published', 'draft')
    GROUP BY DATE_TRUNC('month', p.created_at), u.country
    HAVING COUNT(DISTINCT u.id) > 10
    ORDER BY month DESC, unique_users DESC
"""

results = db.execute(query).fetchall()

Почему ORM не справляется:

  • Множественные JOIN и GROUP BY
  • Window functions (ROW_NUMBER, RANK)
  • CASE statements
  • Вложенные подзапросы
  • Специальные функции СУБД

2. Performance-critical операции

# Массовый импорт 1M записей
# ❌ ORM - очень медленно
for row in csv_data:
    User.objects.create(
        name=row['name'],
        email=row['email']
    )
# Это 1M отдельных INSERT запросов

# ✓ Raw SQL - в 100x быстрее
from sqlalchemy import insert

values = [
    {'name': row['name'], 'email': row['email']}
    for row in csv_data
]
db.execute(insert(User), values)

# Или прямой COPY (PostgreSQL)
with open('data.csv', 'r') as f:
    cursor.copy_expert(
        "COPY users (name, email) FROM STDIN WITH CSV",
        f
    )

3. Специфичные для СУБД функции

# PostgreSQL fulltext search
# ❌ Трудно делать через ORM
# ✓ Просто в SQL
query = """
    SELECT *, 
           ts_rank(textsearch, query) as rank
    FROM documents,
         to_tsquery('russian', :search_query) query,
         to_tsvector('russian', content) textsearch
    WHERE textsearch @@ query
    ORDER BY rank DESC
"""

results = db.execute(query, {'search_query': 'python django'}).fetchall()

# JSON операции (PostgreSQL)
query = """
    SELECT id, data->'user'->>'name' as user_name,
           data->'metadata'->>'created_date' as created
    FROM events
    WHERE data @> '{"type": "login"}'
"""

# Window functions
query = """
    SELECT *,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) as row_num,
           LAG(amount) OVER (PARTITION BY user_id ORDER BY created_at) as prev_amount
    FROM transactions
"""

4. Лучше контролировать JOIN

# ORM может создать неправильный JOIN
# ❌ ORM (может быть неоптимально)
users_with_posts = (
    User.objects
    .filter(posts__status='published')
    .distinct()  # Нужно для избежания дублей
)
# Может создать Cartesian product

# ✓ Raw SQL (точный контроль)
query = """
    SELECT DISTINCT u.* FROM users u
    INNER JOIN posts p ON u.id = p.user_id
    WHERE p.status = 'published'
"""

users = db.execute(query).fetchall()

5. Транзакции и блокировки

# Сложная бизнес-логика с гарантиями
from sqlalchemy import text

with db.begin():
    # SELECT FOR UPDATE - заблокировать строки
    user = db.execute(
        text("SELECT * FROM users WHERE id = :id FOR UPDATE"),
        {'id': user_id}
    ).first()
    
    if user['balance'] >= amount:
        db.execute(
            text("UPDATE users SET balance = balance - :amount WHERE id = :id"),
            {'amount': amount, 'id': user_id}
        )
        db.execute(
            text("INSERT INTO transactions (user_id, amount) VALUES (:user_id, :amount)"),
            {'user_id': user_id, 'amount': amount}
        )
    else:
        raise InsufficientBalance()
    # Автоматический COMMIT если успех, ROLLBACK если ошибка

6. Миграции и schema changes

# Alembic (для Django - South)
from alembic import op
import sqlalchemy as sa

def upgrade():
    # Raw SQL для специальных операций
    op.execute("""
        CREATE INDEX CONCURRENTLY idx_posts_user_id 
        ON posts(user_id) 
        WHERE status = 'published'
    """)
    
    # Заполнение данных
    op.execute("""
        UPDATE posts 
        SET slug = lower(replace(title, ' ', '-'))
        WHERE slug IS NULL
    """)

Когда НЕ использовать Raw SQL

# ❌ НЕ используй для простого CRUD
# ❌ ORM всегда проще и безопаснее

# Плохо
query = "SELECT * FROM users WHERE id = ?" 
user = db.execute(query, (user_id,)).fetchone()

# Хорошо
user = session.query(User).filter(User.id == user_id).first()

# ❌ НЕ используй если не знаешь SQL хорошо
# Риск SQL injection, неправильные JOIN

# ❌ НЕ используй для кода, который может менять СУБД
# PostgreSQL != MySQL != SQLite синтаксис

Правило и best practices

class RawSQLDecisions:
    
    def rule_1_use_orm(self):
        """Начни с ORM, потом оптимизируй"""
        # ✓ Хорошо
        users = session.query(User).all()
        
        # Если медленно - профилируй
        # EXPLAIN ANALYZE показывает реальный план запроса
        # Потом переписывай в raw SQL если нужно
    
    def rule_2_parametrize(self):
        """ВСЕГДА используй параметризованные запросы"""
        # ❌ SQL injection!
        query = f"SELECT * FROM users WHERE name = '{name}'"
        
        # ✓ Безопасно
        query = "SELECT * FROM users WHERE name = :name"
        db.execute(query, {'name': name})
    
    def rule_3_explain(self):
        """Проверяй план запроса"""
        # EXPLAIN ANALYZE показывает реальное время выполнения
        EXPLAIN ANALYZE SELECT ...
        # Ищи Sequential Scans вместо Index Scans
    
    def rule_4_test(self):
        """Тестируй сложные запросы"""
        # Юнит тесты для raw SQL
        def test_user_report_query(self):
            result = db.execute(REPORT_QUERY).fetchall()
            self.assertEqual(len(result), 2)
            self.assertEqual(result[0]['user_count'], 100)
    
    def rule_5_comment(self):
        """Комментируй сложные запросы"""
        # Что делает этот запрос?
        # Почему именно этот синтаксис?
        query = """
            -- Get unique users with published posts
            -- Uses DISTINCT ON for efficiency over GROUP BY
            SELECT DISTINCT ON (u.id) u.* FROM users u
            INNER JOIN posts p ON u.id = p.user_id
            WHERE p.status = 'published'
        """

print(RawSQLDecisions.__doc__)

Сравнение ORM vs Raw SQL

АспектORMRaw SQL
Простота✓✓-
Безопасность✓✓Зависит
Производительность✓✓
Гибкость✓✓
Кроссплатформность✓✓-
Для сложных запросов-✓✓
Для CRUD✓✓-

Практический алгоритм

def should_use_raw_sql(task):
    # Шаг 1: Попробуй ORM
    try:
        result = ORM_approach()
        if is_fast_enough(result):
            return False  # Используй ORM
    except:
        pass
    
    # Шаг 2: Профилируй
    import time
    start = time.time()
    orm_result = orm_approach()
    orm_time = time.time() - start
    
    if orm_time > THRESHOLD:  # > 100ms
        # Шаг 3: Оптимизируй с Raw SQL
        start = time.time()
        sql_result = raw_sql_approach()
        sql_time = time.time() - start
        
        if sql_time < orm_time * 0.5:  # At least 2x faster
            return True  # Используй raw SQL
    
    return False  # Используй ORM

Вывод

Raw SQL нужен когда:

  1. ORM создает N+1 problem или неправильный JOIN
  2. Нужна максимальная производительность
  3. Используются специфичные для СУБД функции
  4. Сложные аналитические запросы
  5. Массовые операции (import, bulk updates)

Начинай с ORM и переходи на Raw SQL только если измеренные результаты показывают необходимость.