В каких случаях использование нативного SQL (сырых запросов) предпочтительнее ORM?

«В каких случаях использование нативного SQL (сырых запросов) предпочтительнее ORM?» — вопрос из категории Базы данных, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Использование нативного SQL оправдано, когда ORM (Hibernate, JPA) генерирует неэффективные запросы или не поддерживает специфичные функции СУБД.

Ключевые сценарии:

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

    • Оконные функции (ROW_NUMBER(), LAG(), LEAD(), агрегаты с OVER).
    • Рекурсивные запросы (WITH RECURSIVE).
    • Группировки и агрегация с CUBE, ROLLUP, GROUPING SETS.
      -- Пример: ранжирование с помощью оконной функции
      SELECT
      user_id,
      order_date,
      amount,
      SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS running_total
      FROM orders
      WHERE order_date >= '2024-01-01';
  2. Массовые операции (Bulk Updates/Deletes):

    • ORM может загружать сущности в память, что неэффективно. Нативный SQL выполнит одну операцию на стороне БД.
      -- Эффективное обновление
      UPDATE orders SET status = 'ARCHIVED' WHERE created_at < '2023-01-01';
  3. Использование специфичных функций СУБД:

    • Геопространственные функции (PostGIS в PostgreSQL).
    • Полнотекстовый поиск (tsvector/tsquery в PostgreSQL, MATCH ... AGAINST в MySQL).
    • JSON/XML функции для нереляционных данных внутри БД.
  4. Оптимизация производительности критичных запросов:

    • Когда DBA или разработчик должен явно контролировать план выполнения, использовать хинты или определенные индексы.
  5. Создание и модификация схемы БД (DDL):

    • Создание сложных индексов (частичных, составных), триггеров, материализованных представлений.

Как использовать в Java (Spring JPA):

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {

    // Использование @Query с nativeQuery = true
    @Query(value = 
            "SELECT * FROM orders o " +
            "WHERE o.region_id = :regionId " +
            "AND earth_distance(ll_to_earth(o.lat, o.lng), ll_to_earth(:lat, :lng)) < :radius",
            nativeQuery = true)
    List<Order> findOrdersInRadius(@Param("regionId") Long regionId,
                                   @Param("lat") double latitude,
                                   @Param("lng") double longitude,
                                   @Param("radius") double radiusMeters);

    // Массовое обновление через @Modifying
    @Modifying
    @Query(value = "UPDATE orders SET priority = :newPriority WHERE status = 'PENDING'",
           nativeQuery = true)
    @Transactional
    int bulkUpdatePriority(@Param("newPriority") int priority);
}

Риски: Нативный SQL теряет переносимость между СУБД и может быть подвержен SQL-инъекциям, если не использовать параметризованные запросы.