2025, Oct 30 20:19

Почему CLOB тормозит в python-oracledb и как это исправить

Почему обновления CLOB в python-oracledb медленны в облаке Oracle: LOB-локаторы и задержки сети. Решения: varchar2(4000), executemany, Thin-режим. Плюс советы.

Медленные обновления CLOB через python-oracledb могут сбивать с толку, когда всё остальное работает очень быстро. Ситуация типичная: операции с объектными типами, которые на локальной базе занимают мгновения, в облачном экземпляре Oracle начинают тянуться. Причина не в объёме данных, а в профиле сетевых задержек и в том, как LOB обрабатываются внутри драйвера.

Как воспроизвести окружение

В примере ниже создаётся объектный тип с атрибутом CLOB, после чего из Python наполняются 40 000 экземпляров. Логика отражает обычный цикл создания/обновления объектов; меняются лишь имена.

-- Объектный тип с атрибутом CLOB
create TYPE DOC_REC_T as OBJECT(
   DOC_ID NUMBER,
   DOC_BODY CLOB
);
import os
import time
import numpy as np
import oracledb
import pandas as pd
dsn = "localhost:1521/ORCLCDB"
db_user = "SYS"
db_pass = "mypassword1"
try:
    client_dir = os.path.join(os.environ.get("HOME"), "db", "instantclient_23_3")
    oracledb.init_oracle_client(lib_dir=client_dir)
    total_items = 40000
    payload = {
        'txt_col': [f"This is a very long string example for row {k}. It can contain various characters and be quite lengthy to simulate real-world data scenarios." * 5 for k in range(total_items)],
        'num_col': np.random.randint(1000, 100000, size=total_items)
    }
    frame = pd.DataFrame(payload)
    conn = oracledb.connect(user=db_user, password=db_pass, dsn=dsn, mode=oracledb.SYSDBA)
    print("Connected to Oracle Database (CDB Root) using Thin/Thick Client successfully!")
    cur = conn.cursor()
    objects_out = []
    obj_type = conn.gettype("DOC_REC_T")
    pool = [obj_type.newobject() for _ in range(total_items)]
    t0 = time.time()
    for idx, row in enumerate(frame.itertuples(index=False), start=0):
        rec = pool[idx]
        setattr(rec, 'DOC_ID', idx)
        setattr(rec, 'DOC_BODY', f"Name_{idx}" * 4000)
        objects_out.append(rec)
    t1 = time.time()
    print(f"Time to create objects and assign attributes: {t1 - t0:.4f} seconds")
    print(f"Total objects: {len(objects_out)}")
except oracledb.Error as exc:
    err, = exc.args
    print(f"Oracle error: {err.message}")
finally:
    if 'cur' in locals() and cur:
        cur.close()
    if 'conn' in locals() and conn:
        conn.close()

Что на самом деле тормозит

Перенос базы данных в облако меняет «физику» сети. Задержка между локальной средой или рабочей станцией и облаком легко достигает десятков миллисекунд из‑за географии, уровней межсетевых экранов, изолированных сегментов и инспекции трафика. Это на порядки больше, чем единицы миллисекунд в пределах локальной сети.

Ключевой момент — как LOB передаются по сети. Когда участвует LOB, клиент не отправляет и не получает полное значение как обычный скаляр за один вызов. Вместо этого передаётся локатор LOB, а содержание LOB читается или записывается через дополнительные вызовы. Даже если LOB небольшой, на каждую строку приходится как минимум один дополнительный круговой запрос. При тысячах строк суммарная задержка на строку начинает доминировать во времени выполнения. Поэтому локальные прогоны выглядят нормально, а в облаке всё кажется мучительно медленным.

Типы данных LOB требуют особой обработки на стороне драйвера. Драйвер выполняет дополнительные LOB‑вызовы на каждую строку, и высокая задержка оплачивается на каждом из них.

Помимо специфики LOB, использование пользовательских объектных типов вносит больше сетевых обменов по сравнению с чистыми скалярами, что усиливает эффект.

Как подойти к решению

Самый действенный способ сократить время — по возможности избежать обработки LOB через высоколатентные каналы. Если текст всегда меньше 4 КБ, сделайте его скаляром и полностью уберите семантику LOB, использовав varchar2(4000). Если изменить схему нельзя, применяйте приведение типов при чтении или записи.

Если значения обычно короткие, но иногда превышают 4 КБ, разделите работу на два прохода. Сначала обрабатывайте строки, где можно привести к varchar2, а «длинный хвост» оставьте для отдельного этапа, где LOB действительно необходим.

Если нужна только начальная часть содержимого, усеките её на стороне сервера при доступе. Это позволит передавать данные как скаляры.

-- Оставляйте всё скалярным, когда значения <= 4000 байт
SELECT CAST(mylob AS varchar2(4000)) AS mylob, col2, col3 FROM your_table;
-- Если нужен только префикс, усекайте при доступе
SELECT CAST(SUBSTR(mylob, 1, 4000) AS varchar2(4000)) AS mylob, col2, col3 FROM your_table;

При обновлении, а не только при чтении, для LOB действуют те же издержки на задержку. Практичный обходной путь тот же: используйте varchar2(4000), когда данные помещаются, или минимизируйте число строк, для которых требуется обработка LOB.

Более быстрый способ загрузки данных

Объекты и локаторы LOB добавляют сетевые обмены. Прямое связывание скаляров и пакетная отправка операторов обычно дают большую пропускную способность. Вместо конструирования 40 000 объектных экземпляров вставляйте строки пакетно как пару NUMBER и CLOB. Так вы избегаете накладных расходов на объекты и задействуете конвейеризацию пакетной обработки в драйвере.

-- Таблица со скалярными столбцами
CREATE TABLE DOCS_TBL (
  DOC_ID   NUMBER,
  DOC_BODY CLOB
);
import oracledb
# предполагается, что conn уже открыт
cur = conn.cursor()
rows = [(i, f"Name_{i}" * 4000) for i in range(40000)]
cur.executemany(
    "insert into DOCS_TBL (DOC_ID, DOC_BODY) values (:1, :2)",
    rows
)
conn.commit()

Такой подход соответствует рекомендациям по настройке python-oracledb: зачастую быстрее избегать объектов, а использование локаторов LOB медленнее, чем связывание строк/буферов. Кроме того, драйвер и база данных смогут оптимизировать пакетирование в меньшем числе, но более крупных сетевых операций.

О режиме драйвера

Если вы используете python-oracledb в Thick‑режиме, попробуйте протестировать Thin‑режим. В подобных сценариях можно получить небольшой прирост, а для выборки объектов Thin‑режим особенно выгоден. Хорошую перспективу и цифры по производительности объектов в Thin‑режиме даёт статья «Python-oracledb Thin mode Object performance». Однако если в вашем коде неизбежны объекты и LOB, базовые характеристики задержек при обработке LOB всё равно никуда не денутся.

Когда LOB не избежать

Если большинство значений действительно превышают 4 КБ и требуется полная точность, важнее всего сократить число сетевых обменов. Перестройте процесс так, чтобы избегать полноценных выборок там, где достаточно инкрементальных извлечений, или переносите операции ближе к данным, уменьшая «болтовню» между клиентом и сервером. Если содержимое фактически структурировано, нормализуйте его в скалярные столбцы и дочерние таблицы вместо хранения в CLOB — это превращает LOB в стандартные типы и часто полностью убирает LOB‑путь.

Почему это важно

Разница между секундными прогонами и многочасовыми часто сводится к стоимости круговых вызовов на строку, умноженной на десятки тысяч строк. LOB превращают то, что выглядит как одно обновление, в серию сетевых вызовов, а высокая латентность делает каждый из них дорогим. Понимание того, когда ваша архитектура неявно задействует обработку LOB, позволяет выбирать альтернативы, сохраняющие данные в скалярном виде или резко сокращающие число обменов.

Заключительные рекомендации

Начните с проверки профиля задержек между клиентом и облачной базой: наблюдаемое поведение соответствует высоколатентным каналам. Для короткого текста предпочитайте varchar2(4000), чтобы оставаться на скалярном пути. Когда без LOB не обойтись, сузьте набор строк, которым действительно требуется LOB‑семантика, — с помощью приведения, усечения или разделения нагрузки. Избегайте пользовательских объектов на «горячих» путях и используйте пакетную вставку executemany. Если позволяет среда, протестируйте Thin‑режим для дополнительного выигрыша. Эти шаги бьют по реальной причине — множественным круговым вызовам при обработке LOB в условиях высокой латентности — и возвращают время выполнения к результатам, близким к локальным.

Статья основана на вопросе на StackOverflow от omkar kothiwale и ответе Paul W.