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

Что такое batch update?

2.2 Middle🔥 131 комментариев
#Базы данных и SQL

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

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

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

Batch Update

Batch Update — это техника оптимизации производительности баз данных, при которой несколько операций модификации (INSERT, UPDATE, DELETE) группируются и отправляются на сервер БД одной партией, вместо отправки каждой операции отдельно. Это значительно снижает сетевые задержки и повышает пропускную способность.

Проблема без batch обновлений

Неоптимизированный подход - множество круговых поездок (round-trips):

// ПЛОХО: 1000 операций = 1000 сетевых запросов!
public void updateEmployeesSalary(List<Employee> employees) throws SQLException {
    String updateQuery = "UPDATE employees SET salary = ? WHERE id = ?";
    
    for (Employee emp : employees) {
        try (PreparedStatement stmt = connection.prepareStatement(updateQuery)) {
            stmt.setDouble(1, emp.getSalary());
            stmt.setLong(2, emp.getId());
            stmt.executeUpdate(); // Отправляем на сервер
        }
    }
    // Итого: 1000 TCP соединений = очень медленно!
}

Проблемы такого подхода:

  • Задержка за счёт сетевого взаимодействия (RTT - Round Trip Time)
  • Нагрузка на сервер БД из-за множества отдельных транзакций
  • Низкая пропускная способность
  • Истощение ресурсов соединения

Batch Update с PreparedStatement

Оптимизированный подход - одна операция для всех данных:

// ХОРОШО: 1000 операций в одном batch!
public void updateEmployeesSalaryBatch(List<Employee> employees) throws SQLException {
    String updateQuery = "UPDATE employees SET salary = ? WHERE id = ?";
    
    try (PreparedStatement stmt = connection.prepareStatement(updateQuery)) {
        int batchSize = 1000;
        int count = 0;
        
        for (Employee emp : employees) {
            stmt.setDouble(1, emp.getSalary());
            stmt.setLong(2, emp.getId());
            stmt.addBatch(); // Добавляем в батч
            
            if (++count % batchSize == 0) {
                int[] results = stmt.executeBatch(); // Отправляем партию
                stmt.clearBatch();
                System.out.println("Обработано " + results.length + " записей");
            }
        }
        
        // Обработка оставшихся записей
        if (count % batchSize != 0) {
            stmt.executeBatch();
        }
    }
}

Batch INSERT

Массовая вставка данных:

public void insertProductsBatch(List<Product> products) throws SQLException {
    String insertQuery = "INSERT INTO products (name, price, category) VALUES (?, ?, ?)";
    
    try (PreparedStatement stmt = connection.prepareStatement(insertQuery)) {
        int batchSize = 500;
        int count = 0;
        
        for (Product product : products) {
            stmt.setString(1, product.getName());
            stmt.setDouble(2, product.getPrice());
            stmt.setString(3, product.getCategory());
            stmt.addBatch();
            
            if (++count % batchSize == 0) {
                int[] results = stmt.executeBatch();
                stmt.clearBatch();
            }
        }
        
        if (count % batchSize != 0) {
            stmt.executeBatch();
        }
    }
}

Batch DELETE

Массовое удаление:

public void deleteInactiveUsersBatch(List<Long> userIds) throws SQLException {
    String deleteQuery = "DELETE FROM users WHERE id = ?";
    
    try (PreparedStatement stmt = connection.prepareStatement(deleteQuery)) {
        for (Long userId : userIds) {
            stmt.setLong(1, userId);
            stmt.addBatch();
            
            if (userIds.indexOf(userId) % 100 == 0) {
                stmt.executeBatch();
                stmt.clearBatch();
            }
        }
        stmt.executeBatch();
    }
}

Batch обновления с Hibernate/JPA

Использование батчей в ORM:

@Service
public class EmployeeService {
    @Autowired
    private EmployeeRepository repository;
    
    @Autowired
    private EntityManager entityManager;
    
    // Способ 1: Использование flush и clear для управления памятью
    public void updateEmployeesSalaryBatch(List<Employee> employees) {
        int batchSize = 500;
        
        for (int i = 0; i < employees.size(); i++) {
            Employee emp = employees.get(i);
            emp.setSalary(emp.getSalary() * 1.1);
            entityManager.merge(emp);
            
            // Периодически сбрасываем батч
            if (i % batchSize == 0 && i > 0) {
                entityManager.flush();
                entityManager.clear(); // Освобождаем память
            }
        }
        entityManager.flush();
    }
}

Конфигурация Hibernate для батчей:

# application.properties
spring.jpa.properties.hibernate.jdbc.batch_size=20
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true

В коде:

@Service
public class ProductService {
    @Autowired
    private ProductRepository repository;
    
    public void insertProductsBatch(List<Product> products) {
        // Hibernate автоматически батчит благодаря конфигурации
        repository.saveAll(products);
    }
}

Native batch updates с JPA

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    
    @Modifying
    @Transactional
    @Query("UPDATE Employee e SET e.salary = e.salary * 1.1 WHERE e.departmentId = :deptId")
    int increaseSalaryByDepartment(@Param("deptId") Long deptId);
}

@Service
public class EmployeeService {
    @Autowired
    private EmployeeRepository repository;
    
    @Transactional
    public void updateEmployeesByDepartment(List<Long> departmentIds) {
        for (Long deptId : departmentIds) {
            int updated = repository.increaseSalaryByDepartment(deptId);
            System.out.println("Обновлено " + updated + " сотрудников");
        }
    }
}

Управление размером батча

Оптимальный размер батча зависит от:

public class BatchConfiguration {
    // Общие рекомендации:
    // - Маленькие записи (< 100 bytes): 500-1000
    // - Средние записи (100-1000 bytes): 100-500
    // - Большие записи (> 1000 bytes): 10-100
    
    public static final int SMALL_BATCH_SIZE = 1000;    // VARCHAR, INTEGER
    public static final int MEDIUM_BATCH_SIZE = 500;    // JSON, CLOB
    public static final int LARGE_BATCH_SIZE = 50;      // BLOB, большие данные
    
    public void processBatch(List<Record> records, int batchSize) {
        for (int i = 0; i < records.size(); i += batchSize) {
            int end = Math.min(i + batchSize, records.size());
            List<Record> batch = records.subList(i, end);
            processSingleBatch(batch);
        }
    }
}

Сравнение производительности

Тест: Вставка 10,000 записей

@SpringBootTest
public class BatchPerformanceTest {
    
    @Test
    public void testNonBatchInsertion() {
        long startTime = System.currentTimeMillis();
        
        for (int i = 0; i < 10000; i++) {
            Product p = new Product("Product " + i, 100.0);
            productRepository.save(p); // Каждая вставка = отдельная транзакция
        }
        
        long elapsed = System.currentTimeMillis() - startTime;
        System.out.println("Без батча: " + elapsed + "ms"); // ~2500-3000ms
    }
    
    @Test
    public void testBatchInsertion() {
        long startTime = System.currentTimeMillis();
        
        List<Product> products = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            products.add(new Product("Product " + i, 100.0));
        }
        productRepository.saveAll(products); // Один батч
        
        long elapsed = System.currentTimeMillis() - startTime;
        System.out.println("С батчем: " + elapsed + "ms"); // ~200-300ms
    }
}

Обработка ошибок в батчах

public void updateBatchWithErrorHandling(List<Employee> employees) throws SQLException {
    String updateQuery = "UPDATE employees SET salary = ? WHERE id = ?";
    
    try (PreparedStatement stmt = connection.prepareStatement(updateQuery)) {
        List<Employee> failedUpdates = new ArrayList<>();
        int batchSize = 100;
        int count = 0;
        
        for (Employee emp : employees) {
            try {
                stmt.setDouble(1, emp.getSalary());
                stmt.setLong(2, emp.getId());
                stmt.addBatch();
                
                if (++count % batchSize == 0) {
                    int[] results = stmt.executeBatch();
                    stmt.clearBatch();
                    
                    // Проверяем результаты
                    for (int result : results) {
                        if (result == Statement.EXECUTE_FAILED) {
                            failedUpdates.add(emp);
                        }
                    }
                }
            } catch (SQLException e) {
                failedUpdates.add(emp);
                logger.error("Ошибка обновления сотрудника " + emp.getId(), e);
            }
        }
        
        if (!failedUpdates.isEmpty()) {
            logger.warn("Не удалось обновить " + failedUpdates.size() + " записей");
        }
    }
}

Best Practices

1. Всегда используй PreparedStatement для батчей:

// ✅ Правильно
try (PreparedStatement stmt = connection.prepareStatement(query)) {
    stmt.addBatch();
    stmt.executeBatch();
}

// ❌ Неправильно
Statement stmt = connection.createStatement();
stmt.addBatch(query); // Небезопасно

2. Управляй транзакциями:

public void batchUpdateWithTransaction(List<Employee> employees) throws SQLException {
    connection.setAutoCommit(false);
    try {
        // batch update логика
        connection.commit();
    } catch (SQLException e) {
        connection.rollback();
        throw e;
    } finally {
        connection.setAutoCommit(true);
    }
}

3. Учитывай ограничения памяти:

// Для больших наборов данных используй потоковую обработку
public void streamProcessBatch(Stream<Employee> employees) {
    employees
        .collect(Collectors.toList())
        .stream()
        .collect(Collectors.groupingBy(
            e -> employees.indexOf(e) / 100 // батчи по 100
        ))
        .forEach(this::updateBatch);
}

Заключение

Batch обновления — это критическая техника оптимизации для работы с большими объёмами данных. При правильном использовании они могут улучшить производительность в 5-10 раз, снизить нагрузку на БД и повысить пропускную способность приложения.

Что такое batch update? | PrepBro