Для генерации уникальных значений ключа в Oracle используется объект — последовательность (sequence). Однако, на использование последовательности наложен ряд ограничений, например — запрет на использование в подзапросах. В данной заметке мы рассмотрим как обойти это ограничение и как произвести его оптимизацию.
Иногда, когда совсем этого не ждешь, появляется ошибка ORA-02287 — использование sequence там, где этого делать нельзя. Если посмотреть что скаано по этому повода на OraDoc, видим:
Restrictions on Sequence Values You cannot use
CURRVAL
andNEXTVAL
in the following constructs:
-
A subquery in a
DELETE
,SELECT
, orUPDATE
statement -
A query of a view or of a materialized view
-
A
SELECT
statement with theDISTINCT
operator -
A
SELECT
statement with aGROUP
BY
clause orORDER
BY
clause -
A
SELECT
statement that is combined with anotherSELECT
statement with theUNION
,INTERSECT
, orMINUS
set operator -
The
WHERE
clause of aSELECT
statement -
The
DEFAULT
value of a column in aCREATE
TABLE
orALTER
TABLE
statement -
The condition of a
CHECK
constraint
Причем, самое неприятное из перечисленного — невозможность использования последовательности в подзапросах.
Например:
INSERT ALL WHEN
MOD(id, 2) = 0 THEN INTO t1(id, VALUE)
VALUES
(id, VALUE)
WHEN MOD (id, 2) = 1 THEN INTO t2(id, VALUE)
VALUES
(id, VALUE)
SELECT sq$t_pk.NEXTVAL id, VALUE FROM TABLE(tbl_values);
где tbl_values — переменная типа коллекции, содержащей поле value. Т.е. в данном случае, пример иллюстрирует ситуацию, когда необходимо генерируемое уникальное значение ключа вставлять в одну таблицу и в другую. Условие разделение записи между двумя таблицами — в данном случае четное значение в одну таблицу, нечетное в другую.
Решить данную проблему можно использовав функцию вместо последовательности, которая уже в свою очередь обращается к последовательности. Ora Doc это не запрещает.
function get_sq_value return number
is
Result number;
begin
select sq$t_pk.nextval into Result from dual;
return Result;
end;
INSERT ALL WHEN
MOD(id, 2) = 0 THEN INTO t1(id, VALUE)
VALUES
(id, VALUE)
WHEN MOD (id, 2) = 1 THEN INTO t2(id, VALUE)
VALUES
(id, VALUE)
SELECT get_sq_value id, VALUE FROM TABLE(tbl_values);
Данный вариант хорош, когда записей в таблице источнике (будь то коллекция tbl_values или другая таблица) немного. Однако если их много, скажем больше 1000, то в данном случае внутренний запрос:
select sq$t_pk.nextval into Result from dual;
будет вызывать переключение контекста, что приведет к повышенной нагрузке на сервер и дополнительными, совершенно не нужными затратами на выполнение.
Можно размышлять следующим образом — что такое переключение контекста? Это когда PL/SQL движок останавливает свое выполнение и отдает команду SQL движку. Получается нужно избавиться от команды SELECT в самой функции. Совсем избавиться от нее, понятно, не получиться (надо же как то достать nextval), но можно свести к минимуму. Как? Выполним select…nextval один раз.
Объекты для сравнительного тестирования.
~Untitled.html
— необходимые объекты
create table t1(id number primary key,value varchar2(1000));
create table t2(id number primary key,value varchar2(1000));
create sequence SQ$T_PK minvalue 1 maxvalue 999999999999999999999999999 start with 1 increment by 1 cache 100 order;
— спека пакета
create or replace package pkg_test_insert is
function get_sq_value return number;
function get_sq_value(i_count in number) return number;
procedure insert_batch(tbl_values in tbl_t_type);
procedure insert_batch_cache_sq(tbl_values in tbl_t_type);
end pkg_test_insert;
/
— тело пакета
create or replace package body pkg_test_insert is
— тип для хранения идентификаторов.
type sq_values is table of number index by binary_integer;
— коллекция заданного типа.
g_sq_values sq_values;
g_pointer number:=1;
function get_sq_value(i_count in number) return number
is
begin
if g_pointer = 1 then
select sq$t_pk.nextval
bulk collect into g_sq_values
from (select rownum as l_date
from (select ‘x’ as con from dual) q
connect by q.con = q.con
) ql
where rownum <= i_count;
g_pointer:=g_pointer+1;
return g_sq_values(g_pointer—1)
else
g_pointer:=g_pointer+1;
return g_sq_values(g_pointer—1);
end if;
end;
— получение идентификатора (контекст)
function get_sq_value return number
is
Result number;
begin
select sq$t_pk.nextval into Result from dual;
return Result;
end;
— вставка записей (переключение контекста)
procedure insert_batch(tbl_values in tbl_t_type)
is
begin
INSERT ALL WHEN
MOD(id, 2) = 0 THEN INTO t1(id, VALUE)
VALUES
(id, VALUE)
WHEN MOD (id, 2) = 1 THEN INTO t2(id, VALUE)
VALUES
(id, VALUE)
SELECT get_sq_value id, VALUE FROM TABLE(tbl_values);
end;
— вставка записей (переключение контекста — один раз)
procedure insert_batch_cache_sq(tbl_values in tbl_t_type)
is
l_number number;
begin
g_pointer:=1;
l_number:=tbl_values.count;
INSERT ALL WHEN
MOD(id, 2) = 0 THEN INTO t1(id, VALUE)
VALUES
(id, VALUE)
WHEN MOD (id, 2) = 1 THEN INTO t2(id, VALUE)
VALUES
(id, VALUE)
SELECT get_sq_value(l_number) id, VALUE FROM TABLE(tbl_values);
end;
end pkg_test_insert;
/
А теперь протестируем два варианта (сравним два варианта с помощь runstats):
- Вставка записей с использованием метода с большим переключением контекста (insert_batch). На каждую запись будет вызывать внутрений select для получения nextval.
- Вставка записей с использованием метода с одним переключением контекста (insert_batch_cache_sq).
declare
tbl_values tbl_t_type:=tbl_t_type();
— количество строк в коллекции (равно количеству обрабатываемых строк)
l_cnt_values number:=50000;
begiN
tbl_values.extend(l_cnt_values);
— заполняем коллекцию.
for xxx in 1..l_cnt_values loop
tbl_values(xxx):=t_type(to_char(xxx));
end loop;
runStats_pkg.rs_start;
pkg_test_insert.insert_batch(tbl_values => tbl_values);
runStats_pkg.rs_middle;
pkg_test_insert.insert_batch_cache_sq(tbl_values => tbl_values);
runStats_pkg.rs_stop(p_difference_threshold => 0,p_output => ‘WORKLOAD’);
commit;
end;
Для 1000 записей в tbl_values:
Run1 ran in 11 hsecs
Run2 ran in 4 hsecs
run 1 ran in 275% of the time
Name Run1 Run2 Diff
STAT…Elapsed Time 16 10 -6
STAT…CPU used by this session 18 9 -9
STAT…recursive cpu usage 18 9 -9
STAT…session logical reads 138 88 -50
STAT…redo size 16,172 9,652 -6,520
STAT…session pga memory 0 65,536 65,536
Run1 latches total versus runs — difference and pct
Run1 Run2 Diff Pct
19,565 9,805 -9,760 199.54%
Для 50000 записей в tbl_values:
Run1 ran in 580 hsecs
Run2 ran in 187 hsecs
run 1 ran in 310.16% of the time
Name Run1 Run2 Diff
STAT…CPU used by this session 584 194 -390
STAT…recursive cpu usage 584 194 -390
STAT…Elapsed Time 586 193 -393
STAT…session logical reads 5,063 2,550 -2,513
STAT…redo size 654,652 328,812 -325,840
STAT…session uga memory 0 3,597,440 3,597,440
STAT…session pga memory 0 3,670,016 3,670,016
Run1 latches total versus runs — difference and pct
Run1 Run2 Diff Pct
1,266,940 482,741 -784,199 262.45%
Из результатов видно, что второй вариант легче (используется меньше процессорных ресурсов, выполняется быстрее в 2-4 раза), да и защелок аж в 2-3 раза меньше первого варианта.
Однако метрики использования памяти во втором варианте больше, причем они повышаются с увеличением числа входных строк. Что вполне объяснимо, с учетом того, что используем коллекцию, которая храниться в uga memory, которая в свою очередь (dedicated server) храниться в pga memory.
Сделать так, чтобы рост числа памяти был закономерен и всегда контролировался — это уже дело техники. Можно например в функции генерации идентификаторов:
function get_sq_value(i_count in number) return number
генерировать идентификаторы фиксированной пачкой, тогда рост использования памяти uga будет всегда фиксирован и равен числу идентификаторов, которые генерируются за один раз.
В примере, в качестве источника строк используется коллекция (например, внешнее приложение передает в БД Oracle набор строк, через коллекцию). Когда же источником является другая таблица, то выгоднее использовать аналитику для подсчета количества обрабатываемых строк, чем выполнять count(1) по таблице — будет один проход по таблице. Создадим другую функцию, где с помощю аналитики выполним подсчет строк (при этом, будем так же выполнять select из коллекции — для нас не важно из чего на данном этапе выполнять выборку):
~Untitled.html
procedure insert_batch_cache_sq_over(tbl_values in tbl_t_type)
is
begin
INSERT ALL WHEN
MOD(id, 2) = 0 THEN INTO t1(id, VALUE)
VALUES
(id, VALUE)
WHEN MOD (id, 2) = 1 THEN INTO t2(id, VALUE)
VALUES
(id, VALUE)
SELECT get_sq_value(cnt) id, VALUE
FROM
(select count(1) over() cnt, value from TABLE(tbl_values) );
end;
А теперь, произведем сравнение работы новой функции и функции insert_batch_cache_sq примерно таким образом:
~Untitled.html
declare
tbl_values tbl_t_type:=tbl_t_type();
l_cnt_values number:=50000;
begin
tbl_values.extend(l_cnt_values);
for xxx in 1..l_cnt_values loop
tbl_values(xxx):=t_type(to_char(xxx));
end loop;
runStats_pkg.rs_start;
pkg_test_insert.insert_batch_cache_sq(tbl_values
=> tbl_values);
runStats_pkg.rs_middle;
pkg_test_insert.insert_batch_cache_sq_over(tbl_values
=> tbl_values);
runStats_pkg.rs_stop(p_difference_threshold
=> 0,p_output => ‘WORKLOAD’);commit;
end;
Вот результаты:
Run1 ran in 189 hsecs
Run2 ran in 206 hsecs
run 1 ran in 91.75% of the time
Name Run1 Run2 Diff
STAT…CPU used by this session 195 212 17
STAT…Elapsed Time 195 212 17
STAT…recursive cpu usage 195 212 17
STAT…session logical reads 2,582 37 -2,545
STAT…sorts (rows) 7,279 57,278 49,999
STAT…redo size 329,048 2,996 -326,052
STAT…session uga memory 4,840,192 -65,408 -4,905,600
STAT…session pga memory 4,915,200 0 -4,915,200
Run1 latches total versus runs — difference and pct
Run1 Run2 Diff Pct
482,702 437 -482,265##########%
Видно, что время выполнения увеличилось (что понятно, т.к. нужна дополнительная сортировка), однако, количество защелок в варианте с over — значительно уменьшилось.
И получается, как говорил Том Кайт «Если второй метод работает немного дольше, но количество защелок используется меньше, я лучше выберу второй метод, т.к. он обеспечит большую маштабируемость».
Кстати, скорее всего, второй метод (который с over) использует меньше защелок за счет того, что данные из коллекции извлекаются «разом», т.к. в плане выполнения, из за наличия аналитической функции, происходит предварительное получение строк. Получается, Oracle как бы кэширует данные из коллекции, одновременно вычисляет аналитическую функцию и затем уже для закешированных результатов выполняет вызов функции, генерирующей идентификаторы. Но это только предположение, здесь необходимо исследование. К тому же, для текущего эксперимента нам это не особенно то и нужно.
- Ограничение ORA-02287 достаточно легко обходиться вполне стандартными средствами.
- Избавиться от переключения контекста в вызываемой функции генерации уникальных значений можно предварительно закешировав их в памяти.
Troubleshooting
Problem
You have a channel from MQ queue manager A (QM_A) to queue manager B (QM_B). You are sending messages from QM_A to QM_B when a network error occurs and causes the channel to disconnect. When the channel starts back up you get the following message on the receiver side (QM_B):
.
AMQ9526 Message sequence number error for channel QM_A-to-QM_B.
Explanation: The local and remote queue managers do not agree on the next message sequence number. A message with sequence number 15 has been sent when sequence number 13 was expected.
User Response: Determine the cause of the inconsistency. It could be that the synchronization information has become damaged, or has been backed out to a previous version.
If the situation cannot be resolved, the sequence number can be manually reset at the sending end of the channel using the RESET CHANNEL command.
.
You want to know when channel sequence numbers are updated and how the sender can be higher than the receiver.
Cause
Working as designed.
.
It is important to notice that in the vast majority of cases for similar situations caused by network interruptions, the sequence number mismatches will be resolved automatically, and no manual intervention would be required.
Resolving The Problem
When the sender channel sends the messages, it increments its sequence numbers (this is stored in the synchronization information for the channel).
After the messages arrive on the receiver side, the messages are put under syncpoint. Updating the synchronization receiver’s record happens only after making sure that the messages received are recoverable, and all the updates are committed. Once this has happened then the receiver will send back an ‘accept confirmation message’ to the sender.
During the time while a sender is waiting on the ‘accept confirmation message’ the channel will be in-doubt. If there is a network failure after the sender has sent the messages but before it has received the ‘accept confirmation message’ the sender and receiver may have different sequence numbers.
When the channel is restarted after the connection failure, if the channel is in-doubt because the ‘accept confirmation message’ was not received at the sender side, the sender will back out the sequence number changes and back out the messages. These messages will be on the transmit queue and will be resent.
Please keep in mind that in the vast majority of cases, the sequence number mismatches will be resolved automatically and no manual intervention would be required.
The only times a channel should remain in doubt and need manual intervention are if:
a) The MQ administrator actually connected it to a different queue manager.
b) The sync data at one end or the other got corrupted.
c) The MQ administrator had taken some non-routine administration action (restoring from backup, switching to a non-synchronous DR instance).
In the above cases, if a channel is not able to automatically resolve a in-doubt situation it may require manual intervention to determine whether the messages were actually received on the receiver queue manager or not. Based on that decision, you would need to RESOLVE the channel with ACTION(COMMIT) or ACTION(ROLLBACK).
Based on the actions taken it may leave the channel sequence numbers out of sync between the sender and receiver channels and this may require a RESET on the channel to correct this situation.
.
There have been cases where the problem persists, the stop channel mode force command should be used before issuing the reset and resolve commands.
STOP CHL(CHLNAME) MODE(FORCE)
RESOLVE CHANNEL (XXXXXXX) ACTION(BACKOUT)
RESET CHANNEL(XXXXXXXX) SEQNUM(1)
Both the RESOLVE and RESET commands are documented in the online documentation for MQ.
IBM MQ 9.1.x / IBM MQ / Reference / Administration reference / MQSC commands /
RESET CHANNEL
[{«Business Unit»:{«code»:»BU053″,»label»:»Cloud & Data Platform»},»Product»:{«code»:»SSFKSJ»,»label»:»WebSphere MQ»},»Component»:»Channels LU62 / TCP»,»Platform»:[{«code»:»PF002″,»label»:»AIX»},{«code»:»PF010″,»label»:»HP-UX»},{«code»:»PF016″,»label»:»Linux»},{«code»:»PF021″,»label»:»OpenVMS»},{«code»:»PF027″,»label»:»Solaris»},{«code»:»PF033″,»label»:»Windows»}],»Version»:»9.1;9.0;8.0;7.5;7.1;7.0″,»Edition»:»»,»Line of Business»:{«code»:»LOB45″,»label»:»Automation»}}]
So i looked a bit further into this and toyed around with various solutions and basically figured out the issue is when using kerberos/sasl communication such that the rpc calls are also encrypted. along with the data. I managed to get namenode stat calls (ls and the like) working by storing the sessionKey
retrieved from the kerberos ticket during doKerberosHandshake
and then modifying the namenode’s readResponse
method to look like so:
func (c *NamenodeConnection) readResponse(method string, resp proto.Message) error { rrh := &hadoop.RpcResponseHeaderProto{} var err error _, ok := resp.(*hadoop.RpcSaslProto) if c.kerberosClient != nil && !ok { // if we're using kerberos, and was opening a sasl message, use sasl to decrypt the payload sasl := &hadoop.RpcSaslProto{} err = readRPCPacket(c.conn, rrh, sasl) if err != nil { return err } else if int32(rrh.GetCallId()) != -33 { return errors.New("Kerberos Didn't send Sasl?") } var reqToken gssapi.WrapToken err = reqToken.Unmarshal(sasl.GetToken(), true) if err != nil { return err } unwrapped, err := crypto.DecryptMessage(reqToken.Payload, /* the stored sessionKey from the kerberos ticket */, c.currKey, keyusage.GSSAPI_ACCEPTOR_SEAL) if err != nil { return err } // read the decrypted actual message and then continue normally err = readRPCPacket(bytes.NewReader(unwrapped), rrh, resp) } else { err = readRPCPacket(c.conn, rrh, resp) } if err != nil { return err } else if int32(rrh.GetCallId()) != c.currentRequestID { return errors.New("unexpected sequence number") } else if rrh.GetStatus() != hadoop.RpcResponseHeaderProto_SUCCESS { return &NamenodeError{ method: method, message: rrh.GetErrorMsg(), code: int(rrh.GetErrorDetail()), exception: rrh.GetExceptionClassName(), } } return nil }
Granted, i’m sure there’s a more elegant way to do this, but this worked for my namenode interactions. Unfortunately, I ran into issues getting files to transfer and I’m guessing that it’s the same issues mentioned in #145
Based on looking at the code for other hdfs clients, I think the ideal solution for both this, and #145 would be to create a sasl requester so that the logic to transparently decrypt messages with callid -33 can be centralized and used by both the namenode and datanode interactions, or something along those lines.
i know this question was asked before, but i am at a point where i am really stuck and do not know how to continue with this problem and i would really like to know, what causes this error to come up.
we had a replication which worked without any problem. our setup includes one database master and four slaves, all running percona mysql, same version (5.5.x). the other day our database master died because of a hardware issue. so we went on and made a slave the new master. should not be much of a problem and was indeed no problem before. but now after getting things up again, i keep getting lot’s of log entries in my syslog like:
Mar 15 13:17:25 dbslave3 mysqld: 120315 13:17:25 InnoDB: Error: page 263337 log sequence number 34159788445
Mar 15 13:17:25 dbslave3 mysqld: InnoDB: is in the future! Current system log sequence number 1096541424.
Ok, so i used xtrabackup — which i used for this things before — to create a dump from the new master and used the dump for applying it on the slaves. without success. still the same problem with this error message. next i tried the following:
i took my site offline and made sure, that all data was replicated to all slaves — indeed: replication is working! no data get’s lost, every slave has all the data it should have.
next i did a reset slave
on the slaves and on the master i did a reset master
. in my opinion this should reset any binlogs and start over again with fresh sequence numbers. next i did a show master status
and used the new sequence file and number for the change master to ...
command on the slaves. i activated replication again and took my site online again.
without success! still the same problem. so my biggest question:
WTF is going on!? how can this error occur on:
- a fresh set-up slave using xtrabackup
- after a binlog reset on master and slave
how to solve it? i mean: i read a lot about inserting gigabytes of data in the database, that «the sequence pointer can catch up» … but this sounds a lot like a dirty hack and can’t be a real solution to this problem, no!? and my biggest concern: is the problem indeed solved using this dirty hack?
any other ideas what i could do? dumping the data using mysqldump and importing it again is no option btw., because the database is so big, that it would take to long to import it again.
UPDATE
what i also did is: dumping one of the slaves, including: deinstalling mysql completly, removing all mysql related stuff including binlogs, databases, etc. and setting it up again — without success. that’s the fresh install i am asking about above.