Потому что queryExecute просто отправляет строку SQL в базу "как есть" — он не делает автоматическую подстановку плейсхолдеров. Методы вроде forSql просто экранируют значение, но не заменяют ? / %s / :name в вашем SQL. Если вы хотите подставить значения, их нужно либо вручную вставить в строку (после экранирования), либо использовать подготовленные запросы / утилиты SQL‑хелпера.
Варианты решения:
1) Вставлять значения вручную, но безопасно (экранировать строки и приводить числа):
```php
$sqlHelper = $connection->getSqlHelper();
$sql = "UPDATE my_table SET TITLE = '".$sqlHelper->forSql($data['TITLE'])."' WHERE ID = ".(int)$id;
$connection->queryExecute($sql);
```
2) Использовать prepareUpdate/prepareInsert у SqlHelper (они формируют корректный фрагмент SQL с экранированными значениями):
```php
$sqlHelper = $connection->getSqlHelper();
$update = $sqlHelper->prepareUpdate('my_table', ['TITLE' => $data['TITLE']]);
$connection->queryExecute("UPDATE my_table SET ".$update." WHERE ID = ".(int)$id);
```
3) Использовать ORM/DataManager (рекомендуется в Bitrix):
```php
\My\Table\MyTable::update($id, ['TITLE' => $data['TITLE']]);
```
4) Если вам нужны настоящие подготовленные запросы с биндингом — работать через PDO (или API, которое возвращает PDO‑ресурс) и bindParam/bindValue.
Итого: queryExecute не подставляет плейсхолдеры автоматически — либо собирайте SQL корректно с помощью forSql/prepareUpdate, либо используйте подготовленные запросы/ORM для биндинга.