Tuesday, August 7, 2012

Happy three friends: VARCHAR2, UNICODE and CYRILLIC.


Many of DBA's know that database migration from single-byte character set (like CL8MSWIN1251, WE8MSWIN1252 etc) to multi-byte character set (AL32UTF8) is a comprehensive task.
In this article I'll talk about 2 very important quations, that I think must be resolved during migration procedure:
 1. Considerations about cyrillic VARCHAR2 data migrations (or any other non latin symbols) from single-byte database to multibyte database.
 2. How works parameter NLS_LENGTH_SEMANTICS in both cases.
The best way to demonstrate how Oracle implicates this - test cases. 
An example assumes that database operates in 2 languages - english and russian.
We want to migrate from 10g CL8MSWIN1251(single-byte) to 11g AL32UTF8(multibyte).
/* examples done in PL\SQL, so I've got an PL/SQL errors like ORA-01461,ORA-06512. In SQLPlus, Datapump, SQL*Loader U will see another errors like "ORA-12899: value too large for column " and so on. */



10g test with CL8MSWIN1251:
SQL> select * from nls_database_parameters where parameter in ('NLS_RDBMS_VERSION','NLS_CHARACTERSET','NLS_NCHAR_CHARACTERSET','NLS_LANGUAGE','NLS_TERRITORY','NLS_LENGTH_SEMANTICS');

PARAMETER                      VALUE
------------------------------ ----------------------------------------
NLS_LANGUAGE                   AMERICAN
NLS_TERRITORY                  AMERICA
NLS_CHARACTERSET               CL8MSWIN1251
NLS_LENGTH_SEMANTICS           BYTE
NLS_NCHAR_CHARACTERSET         AL16UTF16
NLS_RDBMS_VERSION              10.2.0.4.0

 6 rows selected


SQL> create table TEST (b varchar2(4000));

Table created

SQL> declare
  2    a varchar(4000) := 'я'; /* я - russian unique letter */
  3  begin
  4    dbms_output.enable(4000);
  5    while length(a) < 4000 loop
  6      a := a || 'я';
  7    end loop;
  8    insert into test (b) values (a);
  9    commit;
 10    dbms_output.put_line(length(a) ||'..inserted');
 11  end;
 12  /

PL/SQL procedure successfully completed

So, default NLS_LENGTH_SEMANTICS is BYTE. Lets change it.

SQL> alter session set NLS_LENGTH_SEMANTICS='CHAR';

Session altered

SQL> declare
  2    a varchar(4000) := 'я';
  3  begin
  4    dbms_output.enable(4000);
  5    while length(a) < 4000 loop
  6      a := a || 'я';
  7    end loop;
  8    insert into test (b) values (a);
  9    commit;
 10    dbms_output.put_line(length(a) ||'..inserted');
 11  end;
 12  /

PL/SQL procedure successfully completed


SQL> select length(b) from test;

 LENGTH(B)
----------
      4000
      4000

As we see, since CL8MSWIN1251 encodes any cyrillic symbol in 1 byte - we have no limits to insert 4000 chars( = bytes) into single-byte database, despite of NLS_LENGTH_SEMANTICS = CHAR or BYTE.



11g test with AL32UTF8:

SQL> select * from nls_database_parameters where parameter in ('NLS_RDBMS_VERSION','NLS_CHARACTERSET','NLS_NCHAR_CHARACTERSET','NLS_LANGUAGE','NLS_TERRITORY','NLS_LENGTH_SEMANTICS');

PARAMETER                      VALUE
------------------------------ --------------------------------------------------------------------------------
NLS_LANGUAGE                   AMERICAN
NLS_TERRITORY                  AMERICA
NLS_CHARACTERSET               AL32UTF8
NLS_LENGTH_SEMANTICS           BYTE
NLS_NCHAR_CHARACTERSET         AL16UTF16
NLS_RDBMS_VERSION              11.2.0.3.0

6 rows selected

To see how  NLS_LENGTH_SEMANTICS works:


SQL> create table TEST (b varchar2(4000));

Table created


SQL> declare
  2    a varchar(4000) := 'я';
  3  begin
  4    dbms_output.enable(4000);
  5    while length(a) < 4000 loop
  6      a := a || 'я';
  7    end loop;
  8    insert into test (b) values (a);
  9    commit;
 10    dbms_output.put_line(length(a) || '..inserted');
 11  end;
 12  /


ORA-06502: PL/SQL: numeric or value error: character string buffer too small
ORA-06512: at line 7

Variable 'a' - exceeds 4000 byte limit. Fortunately for VARCHAR2 in PL\SQL (in contrast with SQL) - the hard limit is 32767 byte. Also lets change commit frequency to see how many insert are done.



SQL> declare
  2    a varchar(8000) := 'я';
  3  begin
  4    dbms_output.enable(4000);
  5    while length(a) < 4000 loop
  6      insert into test (b) values (a);
  7      commit;
  8      a := a || 'я';
  9       end loop;
 10    dbms_output.put_line(length(a) || '..inserted');
 11  end;
 12  /


ORA-01461: can bind a LONG value only for insert into a LONG column
ORA-06512: at line 9


SQL> select max(LENGTH(b)) from test;


MAX(LENGTH(B))

--------------

          2000

Now we see that only 2000 symbols inserted. In UNICODE, cyrillic letters encoded by 2 bytes,  that is 2000*2 = 4000byte limit. Now what if we change table column to VARCHAR2 CHAR..? (or change NLS_LENGTH_SEMANTICS to CHAR) ?


SQL> drop table test;
Table dropped

SQL> create table test(b varchar2(4000 CHAR));
Table created

It is not necessary but still:

SQL> alter session set NLS_LENGTH_SEMANTICS='CHAR';
Session altered


SQL> declare
  2    a varchar(8000) := 'я';
  3  begin
  4    dbms_output.enable(4000);
  5    while length(a) < 4000 loop
  6      a := a || 'я';
  7    end loop;
  8    insert into test (b) values (a);
  9    commit;
 10    dbms_output.put_line(length(a) || '..inserted');
 11  end;
 12  /

ORA-01461: can bind a LONG value only for insert into a LONG column
ORA-06512: at line 9


Wow, we still can't insert 4000 symbols? wtf? Lets look how many inserts:


SQL> declare
  2    a varchar(8000) := 'я';
  3  begin
  4    dbms_output.enable(4000);
  5    while length(a) < 4000 loop
  6      insert into test (b) values (a);
  7      commit;
  8      a := a || 'я';
  9       end loop;
 10    dbms_output.put_line(length(a) || '..inserted');
 11  end;
 12  /


SQL> select max(LENGTH(b)) from test;
MAX(LENGTH(B))
--------------
          2000

Still 2000 only. What about non cyrillic symbols?


SQL> truncate table test;
Table truncated

SQL> declare
  2    a varchar(8000) := 'z';
  3  begin
  4    dbms_output.enable(4000);
  5    while length(a) < 4000 loop
  6      a := a || 'z';
  7    end loop;
  8    insert into test (b) values (a);
  9    commit;
 10    dbms_output.put_line(length(a) || '..inserted');
 11  end;
 12  /
PL/SQL procedure successfully completed

SQL> select max(LENGTH(b)) from test;
MAX(LENGTH(B))
--------------
          4000


Obviously, latin symbols encodes in 1 byte, and cyrillic in 2 
bytes. I remind, that session and column semantic is still CHAR...so wtf? The answer is in the: 


Examples and limits of BYTE and CHAR semantics usage (NLS_LENGTH_SEMANTICS) [ID 144808.1]

where we read:

You may have defined the column as 4000 CHAR, but the "limit behind the scene" is 4000 BYTES 

Nice. I'm happy that behind this scene we didn't saw some wall


3 comments:

  1. Здравствуйте, Jamal
    Спасибо за статью. Столкнулся с проблемой конвертиции БД с CL8MSWIN1251 на AL32UTF8. Решил делать максимально "вручную". На примере одной схемы в БД с однобайтовой кодировкой. Сделал экспорт схемы через expdp. И в новую БД с юникодом с помощью SQLFILE через создание скриптов на отдельные объекты схемы (таблицы, представления, процедуры, пакеты итд) пытаюсь сделать перенос схемы с данными. В скрипте с таблицами меняю VARCHAR2(N) на VARCHAR2( N CHAR), создаю таблицы, затем вставляю данные в эти таблицы, всё ок. Но вот когда начинаю создавать PL\SQL объекты (представления, процедуры, пакеты итд), сыпятся ошибки о которых Вы сказали (ORA-01461; ORA-06512; ORA-06502). Если честно, то я так и не понял, что мне нужно предпринять для решения данных ошибок (доступа на MOS у меня в данный момент, к сожалению, нет), не могли бы Вы разъяснить мне последовательность действий? Спасибо.

    ReplyDelete
  2. Привет, Sergey Atrokhov
    Ошибки вида ORA-01461 говорят о том, что что-то куда-то не влезает. В целом, миграция больших, сложных по логике БД в юникод - это очень комплексная задача. Cкорее всего - в нем есть какие - то типы данных (%rowtype к примеру) которые становятся не валидными после трансформации таблиц\вьюх.
    Есть большая нота Doc ID 260192.1, где концептуально описана миграция в юникод. В твоем случае, алгоритм примерно такой:
    1. проверить БД-источник оракловой утилитой Csscan. Тулза находит так называемые lossy данные. Бывают 2х типов (Convertible - правиться сменой семантики(скрипты как это сделать для всей БД есть в 313175.1), и not Convertible - к примеру поле содержит 2001 кириллический символ - это не исправишь какак, кроме удаления 1 символа). Тулза проверяет какие таблицы содержат данные которые не поместятся в UTF + проверяет код который ссылается на таблицы, который станет не валидным. (отдельная история с encrypted-данными, bin-like данными(лобы, файлы итп) - это все отдельно переносить) Думаю, тут всплывет весь код который у тебя валится с ошибками. Отдельная история с Lossy данными в словаре-это целая кухня(надеюсь БД не от 1c, где кириллица не возбраняется в именах таблиц, полей итп) . Также есть gui версия - dmu (db migrate assistant for unicode) - я им не пользовался, потыкал ради любопытства, но скорее всего работает примерно также.
    2. Начать чинить все что было найдено в п1., пока csscan не покажет что все ок.
    3. Запустить новую utf БД с параметром BLANK_TRIMMING=TRUE
    3. Сделать expdp\impdp только метаданных(CONTENT=METADATA_ONLY). Пофиксить ошибки в структуре и коде, если будут. Повторить
    4. Сделать expdp\impdp только данных (CONTENT=DATA_ONLY). Пофиксить ошибки в данных, если будут.Повторить
    Мы делали миграцию огромной oltp БД через Golden Gate, с минимальным downtime - там алгоритм немного отличается после п2. Но основная идея неизменна - поправить все ограничения ДО самой миграции(exp\imp)

    ReplyDelete
  3. Спасибо за советы, Jamal Sale. Буду читать доку (доступ к металинку у меня появится только весной-летом)..

    ReplyDelete