Oracle SQL: наиболее эффективный способ (с точки зрения сетевых обходов) выполнять случайные небольшие и разбросанные записи в столбец LOB

У меня есть таблица со столбцом BLOB. Размер каждого BLOB-объекта может достигать 1 ГБ. Обновления существующих постоянных BLOB-объектов обычно состоят из тысяч (~ 10 тыс.) микрочанков записи со случайными смещениями около 100 байт каждый, поэтому вероятность того, что эти записи будут смежными, очень мала.

Нам нужно разработать способ группировки всех наших операций записи и отправки их в Oracle таким образом, чтобы это было эффективно как для сервера (ввод-вывод), так и для клиента (количество сетевых обращений). Я думаю, это можно сделать с помощью OCILobWrite2, но я не уверен, как:

  • Я не понимаю разницы и/или связи между режимом опроса и потоковой передачи.
  • Я не знаю связи между опросом/потоковой передачей и количеством сетевых обращений. Начнется ли сам «обход по сети», когда будет записан последний фрагмент, и все предыдущие записи будут каким-то образом накапливаться в памяти с помощью OCI?
  • Я не знаю, будет ли OCI разумно «группировать» смежные записи или мне придется делать это самому.
  • В документе утверждается, что лучше группировать записи, выровненные по CHUNKSIZE, но:
    • Если я использую режим опроса и/или потоковой передачи, нужно ли мне позаботиться и об этом?
    • SecureFiles использует фрагменты динамического размера (насколько я понимаю), поэтому CHUNKSIZE предусмотрен только для обратной совместимости. Является ли этот совет по выравниванию фрагментов «устаревшим советом» для SecureFile, но никто из Oracle не вспомнил, как исправить совет в документации?
    • В любом случае, если в куске размером 16 КБ на самом деле изменилось только 100 байт, нужно ли мне все равно беспокоиться о выполнении операций записи с выравниванием по частям?

ПРИМЕЧАНИЕ. Я знаю, что в OCI есть режим буферизации для больших объектов, но он устарел и не поддерживается.

Крошечные 100-байтовые изменения будут проблематичными по нескольким причинам, не только из-за сетевой болтовни, но и если фрагменты имеют минимальный размер ввода-вывода LOB, при условии, что у вас размер блока 8 КБ и, следовательно, размер фрагмента не менее 8 КБ ( Securefile по-прежнему не будет меньше размера вашего блока), вы будете каждый раз выполнять гораздо больше операций ввода-вывода, чем вам нужно. LOB не предназначались для такого частого обновления по несколько байтов за раз. Мне кажется, что нужен редизайн.

Paul W 02.05.2024 04:18

Можете ли вы вместо этого представить свои данные в виде дочерней таблицы (или даже вложенной таблицы) RAW(100), чтобы вы могли выполнять обычные обновления строк, которые будут кэшироваться, а грязные буферы будут записываться DBWR лениво/асинхронно, как обычные данные таблицы, а также требовать меньше сетевых подключений? для обновления? Если 100-байтовые разделы имеют независимое значение, и бизнесу необходимо обновлять данные с такой степенью детализации, возможно, ваш тип данных должен быть согласован с этой степенью детализации, а не быть составным (LOB), с которым вам придется работать хирургическим путем и неэффективно.

Paul W 02.05.2024 04:30
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
72
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

LOB-объекты Securefile имеют некоторые улучшения производительности по сравнению со старыми LOB-объектами Basicfile, главным образом кэш-память Write Gather Cache (WGC), которая буферизует записи и периодически очищает их, чтобы уменьшить количество операций ввода-вывода при записи на диск. Таким образом, это не так плохо, как запись одного фрагмента (кратного размера вашего блока) для каждой операции записи, хотя этот фрагмент остается базовой единицей ввода-вывода на диск для LOB-сегментов.

Я провел несколько экспериментов, сравнивая 10 000 случайных записей по 100 байт (всего 1 МБ) с BLOB-объектом размером 16 МБ и 10 000 случайных обновлений строк в столбце RAW(100), чтобы сравнить производительность между использованием BLOB и использованием дочерней таблицы RAW(100).

Настройка LOB:

 drop table lobtest;
 create table lobtest (myblob blob);

/*
** Initial population
*/
DECLARE
  var_myblob blob;
  var_mybuffer raw(100) := HEXTORAW('7785069247a730153313c7c8244db9228b0ce07a2aa8646cf1bc908bc7a275913c16a0081594f1b3979a515b7c37b61993e17caaccbe1401bd52ca34bf21d7a99b0c8220eae182a0f7ec71db249da8d96a585cf8fa81af6d09129d82afaafd7d60d77e4c');
  var_offset pls_integer;
  var_lob_size integer;
BEGIN
  INSERT INTO lobtest VALUES (EMPTY_BLOB()) RETURNING myblob INTO var_myblob;
  
  FOR i IN 1..100000
  LOOP
    var_offset := ((i-1)*100)+1;
    dbms_lob.write(var_myblob,100,var_offset,var_mybuffer);
  END LOOP;
END;
/
/*
** Show LOB segment size
*/
SELECT ROUND(s.bytes/(1024*1024)) blob_mb,l.*
  FROM user_lobs l,
       user_segments s
 WHERE l.column_name = 'MYBLOB'
   AND l.segment_name = s.segment_name;  
/     

Результат: 16 МБ

Тест обновления LOB

/*
** Capture session stats before
*/
drop table stat$before;
create table stat$before as 
SELECT sn.name,ss.value
  FROM v$mystat ss,
       v$statname sn
 WHERE ss.statistic# = sn.statistic#       
   AND (sn.name IN ('physical writes direct (lob)',
                   'physical write bytes',
                   'physical read bytes',
                   'physical write total bytes',
                   'redo size',
                   'undo change vector size')
        OR sn.name LIKE 'securefile%')                   
 ORDER BY ss.value DESC               
 /
/*
** Do 10,000 random writes to the BLOB created above in a single transaction
*/
DECLARE
  var_myblob blob;
  var_mybuffer raw(100) := HEXTORAW('7785069247a730153313c7c8244db9228b0ce07a2aa8646cf1bc908bc7a275913c16a0081594f1b3979a515b7c37b61993e17caaccbe1401bd52ca34bf21d7a99b0c8220eae182a0f7ec71db249da8d96a585cf8fa81af6d09129d82afaafd7d60d77e4c');
  var_offset pls_integer;
  var_lob_size integer;
  var_start_ts timestamp with time zone;
BEGIN
  var_start_ts := SYSTIMESTAMP;
  
  SELECT myblob INTO var_myblob FROM lobtest WHERE ROWNUM = 1 FOR UPDATE;
    
  FOR i IN 1..10000
  LOOP
    var_offset := LEAST(FLOOR(ABS(dbms_random.random())/10000)*100,16000000)+1;
    dbms_lob.write(var_myblob,100,var_offset,var_mybuffer);
  END LOOP;
  
  COMMIT;
  
  dbms_output.put_line(SYSTIMESTAMP - var_start_ts);
END;
/

/*
** Capture session stats after and calculate diffs
*/
SELECT sn.name,ss.value - NVL(b.value,0) diff
  FROM v$mystat ss,
       v$statname sn,
       stat$before b
 WHERE ss.statistic# = sn.statistic#       
   AND (sn.name IN ('physical writes direct (lob)',
                   'physical write bytes',
                   'physical read bytes',
                   'physical write total bytes',
                   'redo size',
                   'undo change vector size')
        OR sn.name LIKE 'securefile%')  
   AND sn.name = b.name(+)
 ORDER BY diff DESC
 /

Результат:

Это произошло после многократного запуска вышеуказанного теста обновления. Каждый раз, когда я запускал его, цифры падали, показывая эффект кэша записи и сбора данных. Эти цифры показывают, где они достигли дна и достигли максимальной эффективности. В ходе теста было обновлено 1 МБ (securefile bytes non-transformed = 1000000), используя 10 000 отдельных операций записи (securefile number of non-transformed flushes = 10000). Он прочитал 15 МБ (physical read bytes), сгенерировал 10 МБ повтора в журнал (redo size) и 4 МБ отмены (undo change vector size). Кэш Write Gather очищался 3565 раз (securefile number of flushes), или примерно 1 из каждых 3 операций записи. Счетчики для physical write bytes/physical write total bytes, похоже, не настроены должным образом — в моем случае 8192 — это ровно один блок/кусок, и он не может записать только 1 блок, когда выполнил 3565 сбросов. Похоже, что кэш Write Gather маскирует реальный объем записи на диск. Если предположить, что каждый сброс представляет собой фрагмент, то можно предположить, что общий объем операций ввода-вывода составляет 29 МБ (3565 * 8192). v$segstat дает цифру 894 физических операций записи (894*8192=7МБ). В любом случае, мы выполняем намного больше операций записи, чем 1 МБ, но кэширование не позволяет нам записывать 81 МБ, как мы (предположительно) делали бы это с базовым файлом (10000 * 8192). Общее время теста: 3,3 секунды.

Настройка RAW(100)

 drop table lobtest2;
 create table lobtest2 (offset integer, blob_data raw(100));
 
 
DECLARE
  var_mybuffer raw(100) := HEXTORAW('7785069247a730153313c7c8244db9228b0ce07a2aa8646cf1bc908bc7a275913c16a0081594f1b3979a515b7c37b61993e17caaccbe1401bd52ca34bf21d7a99b0c8220eae182a0f7ec71db249da8d96a585cf8fa81af6d09129d82afaafd7d60d77e4c');
  var_offset pls_integer;
BEGIN
  FOR i IN 1..100000
  LOOP
    var_offset := ((i-1)*100)+1;
    INSERT INTO lobtest2 VALUES (var_offset,var_mybuffer);
  END LOOP;
  COMMIT;
END;
/ 
create index i_lobtest2 on lobtest2(offset);
/

Тест обновления RAW(100)

drop table stat$before;
create table stat$before as 
SELECT sn.name,ss.value
  FROM v$mystat ss,
       v$statname sn
 WHERE ss.statistic# = sn.statistic#       
   AND (sn.name IN ('physical writes direct (lob)',
                   'physical write bytes',
                   'physical read bytes',
                   'physical write total bytes',
                   'redo size',
                   'undo change vector size'))                   
 ORDER BY ss.value DESC               
 /
  
DECLARE
  var_mybuffer raw(100) := HEXTORAW('7785069247a730153313c7c8244db9228b0ce07a2aa8646cf1bc908bc7a275913c16a0081594f1b3979a515b7c37b61993e17caaccbe1401bd52ca34bf21d7a99b0c8220eae182a0f7ec71db249da8d96a585cf8fa81af6d09129d82afaafd7d60d77e4c');
  var_offset pls_integer;
  var_start_ts timestamp with time zone;
BEGIN
  var_start_ts := SYSTIMESTAMP;
  
  FOR rec_offset IN (SELECT offset FROM lobtest2 ORDER BY REVERSE(TO_CHAR(offset)) FETCH FIRST 10000 ROWS ONLY)
  LOOP
    UPDATE lobtest2 SET blob_data = var_mybuffer WHERE offset = rec_offset.offset;
  END LOOP;
  
  COMMIT;
  
  dbms_output.put_line(SYSTIMESTAMP - var_start_ts);  
END;
/ 
SELECT sn.name,ss.value - NVL(b.value,0) diff
  FROM v$mystat ss,
       v$statname sn,
       stat$before b
 WHERE ss.statistic# = sn.statistic#       
   AND (sn.name IN ('physical writes direct (lob)',
                   'physical write bytes',
                   'physical read bytes',
                   'physical write total bytes',
                   'redo size',
                   'undo change vector size'))  
   AND sn.name = b.name(+)
 ORDER BY diff DESC
 /

Результат:

Мы по-прежнему выполнили 1 МБ обновлений, включающих 10 000 случайных 100-байтовых изменений, как и в предыдущем тесте. Метрики физического чтения/записи бессмысленны, поскольку это были буферизованные операции чтения/записи через кэш, как и обычные операции с таблицами. Процессу переднего плана не нужно выполнять какие-либо собственные записи (кроме журнала повторного выполнения), поскольку DBWR выполнит эту задачу позже, когда пожелает. Общий размер повтора составил 3 МБ, размер отмены — 1 МБ, общее время теста: 0,18 секунды.

Сравнивая два теста, мы видим, что запись в BLOB должна читаться в BLOB-объекте с диска (не кэшированном. Вы можете изменить это с помощью настройки CACHE, но BLOB-объект размером 1 ГБ слишком велик для кэширования), поэтому их много. физического прямого пути чтения, который не нужен в сценарии RAW(100), который вообще не выполняет чтения, когда все находится в буферном кеше. Метод BLOB сгенерировал в три раза больше повторов, в четыре раза больше отмен и выполнил неисчислимое количество операций записи (где-то в 7–20 раз больше фактического количества логических изменений). С точки зрения чистого времени метод RAW(100) работал в 17 раз быстрее, чем метод BLOB.

Таким образом, хотя LOB-объекты Securefile имеют некоторые хорошие улучшения производительности по сравнению с базовым файлом, они все равно не могут сравниться с использованием обычных типов данных в строке. Если вам нужна скорость, механизмы параллелизма и блокировки обычных операций со строками, а также все преимущества кэширования, вам лучше использовать подход, не связанный с LOB, например предложенный метод RAW(100). Вы можете написать функцию PL/SQL для повторной сборки строк в правильном порядке и возврата временного BLOB-объекта, если он нужен вашей программе-потребителю в этом формате.

Очевидно, что это не решает проблему задержки в сети, которую можно уменьшить только за счет буферизации на стороне клиента, чтобы в базу данных отправлялось меньше изменений. Но даже в этом случае требуется меньше сетевых обращений туда и обратно с UPDATE обычного типа данных в строке, чем с чем-либо, связанным с LOB, поскольку операции LOB требуют нескольких операций на строку (создание или выборка локатора, открытие объекта, запись, закрытие, и т. д..). Таким образом, отказ от BLOB также поможет вам справиться с задержкой в ​​сети.

Очень интересный анализ. Все еще думаю об этом. Во-первых, в случайно сгенерированном смещении (в случае LOB) вы имели в виду abs(dbms_random.random()) % 10000? Потому что вы получили частное вместо оставшегося, что означает, что вы, вероятно, выполняете все записи в конце LOB. Вероятно, поэтому физические байты записи показывают 8 КБ (последний фрагмент).

ABu 02.05.2024 17:33

@Абу, нет, я протестировал эту функцию и заставил ее работать так, как я хотел - какое-то случайное число с границей 100 байт, но меньше 16 МБ. Я уверен, что есть лучший способ, но это было довольно случайно.

Paul W 02.05.2024 17:57

Хорошо, ты меня вроде как убедил. Однако наличие таблицы размером 1 ГиБ/100 = 10 миллионов строк меня немного пугает, и мне также нужно проверить время загрузки 10 миллионов строк для нашего первоначального чтения большого двоичного объекта: мы загружаем весь большой двоичный объект в память в -memory, и мы никогда не отключаем его. Наш план состоит в том, чтобы операции микрозаписи записывали как в память, так и в большой двоичный объект (с буферизацией на стороне клиента или чем-то еще), поэтому мы никогда не читаем из большого двоичного объекта, кроме самого начала (когда наши системы включены). В настоящее время чтение BLOB-объекта занимает всего 3 секунды, и это достаточно быстро, поскольку мы платим эту цену только один раз. Однако 10 миллионов строк — это безумие.

ABu 02.05.2024 18:35

@ABu, это все относительно. Во многих средах 10 м — это ничто. Если вы тестируете вставку строк, обязательно используйте массовые привязки массива, чтобы вам не требовался отдельный исполнитель для этой вставки для каждого 100-байтового фрагмента.

Paul W 02.05.2024 19:03

Есть ли способ выполнить итерацию или просмотреть статистику по частям LOB? Мне любопытно узнать, каков фактический средний размер фрагмента моего LOB, чтобы понять, почему, если кэш записи и сбора данных имеет размер 4 МБ и сбрасывается при заполнении, почему происходит сброс всего за 3 чтения. 3 записи (= 3 блока по 16 КБ каждый, что является «иллюстративным» размером блока), уже приводит к принудительному сбросу. Теоретически сбросы должны происходить каждые 256 операций записи. Если это не так, это означает, что SecureFiles использует блоки размером в МБ.

ABu 02.05.2024 20:53

@ABu, эта статистика сеансов - это немного вуду, вы склонны ускользать от попыток точно предсказать и объяснить. Oracle не реализовала каждую вещь так, как можно было бы ожидать, поэтому иногда мы можем неправильно интерпретировать числа. Проверьте dbms_lob.getchunksize или вы можете прочитать все целиком и проверить статистику сеанса securefile allocation chunks. Вам также следует запустить собственные тесты, чтобы сравнить методы, используя предположения, более точно соответствующие вашему варианту использования; посмотрите, что обещает больше всего.

Paul W 02.05.2024 21:14

Я провел более строгий тест для случая RAW, более похожий на мой реальный случай. Отличия: я вставляю 1867776 разных строк (просто потому, что длинная история) и вместо этого использую в качестве смещения «индекс фрагмента», поэтому цикл идет от i = 1 до 1867776, а вставки равны INSERT INTO lobtest2 VALUES (i, var_mybuffer);. Я создаю индекс, а затем тест обновления переходит от i = 1 к 10000, а смещение выбирается как: var_offset := 1 + mod(ABS(dbms_random.random()), 1867776);. Время выполнения больше не составляет порядка миллисекунд, а занимает от 5 до 10 секунд.

ABu 03.05.2024 14:53

Другая статистика очень похожа на вашу, поэтому я думаю, что вы видите эффекты кэша, которых я не вижу, потому что мое смещение очень сильно различается. В вашем более коротком случае моя статистика похожа на вашу, в том числе с точки зрения времени, но, может быть, потому, что в кеше гораздо больше «вещей», поскольку ваши записи гораздо более локализованы (закрыты в пространстве)?

ABu 03.05.2024 14:55

@ABu, убедитесь, что эти обновления используют индекс, и вы не фиксируете каждую часть по отдельности. 10 секунд — это слишком долго для 10 000 простых обновлений.

Paul W 03.05.2024 15:12

Я позитивный. Если я изменю количество строк с 1 миллиона на 100 тысяч (в 10 раз короче), время упадет с 10 секунд до < 0.5s (в 10 раз быстрее), что означает, что время будет пропорционально размеру таблицы, а не количеству строк. обновления, которые исправлены (10к). Я составил план объяснения запроса (для случая 1 миллиона), и в плане говорится, что выполняется сканирование диапазона индекса, поэтому индекс правильный.

ABu 03.05.2024 18:53

@ABu, если время пропорционально размеру таблицы, то вы не используете индекс. Доступ к индексу для фиксированного количества строк будет осуществляться одинаково независимо от размера таблицы. План запроса, который вы получаете, когда вручную объясняете запрос, и то, что получает программа, могут не совпадать. Можете ли вы поделиться своим обновленным SQL, DDL таблицы/индекса и планом выполнения?

Paul W 03.05.2024 19:34

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

ABu 03.05.2024 19:47

Наконец, решение заключалось в том, что вместо замены большого двоичного объекта миллионами фрагментов мы решили сохранить большой двоичный объект в таблице, но, вдохновленные вашей идеей, мы заменяем все записи вставками в дочернюю таблицу, как если бы это была буфер записи. Задание Oracle, которое выполняется один или два раза в день, объединяет все фрагменты обратно в большой двоичный объект с помощью пакета dbms_lob (и очищает дочернюю таблицу). «Загрузка» файла теперь загружает большой двоичный объект И все измененные фрагменты. Благодарим за идею использования дочернего стола, она великолепна.

ABu 07.05.2024 20:34

Другие вопросы по теме