ORA-PT
22
Mar 12
Por Luís Marques, às 21:43 | comentar

As  Leighton L. Nelson point me out in G+, there must other way to corrupt blocks directly in ASM making this task easier for testing purposes. My scenario to make this happen is the following:

* OEL5.7 (Red Hat Enterprise Linux Server release 5.7)
* 2 RAC nodes 11g (11.2.0.1.0)
* VirtualBox (4.1.10)
* VirtualBox shared storage disk /dev/sdh (1GB)

 

We will need a separate DiskGroup with only one ASM disk to accommodate the tablespace and the table with random data to corrupt.


[root@rac1 oracle]# /etc/init.d/oracleasm createdisk TEST_DSK /dev/sdh1
Marking disk "TEST" as an ASM disk: [ OK ]


[oracle@rac1 bin]$ asmcmd lsdg
State Type Rebal Sector Block AU Total_MB Free_MB Req_mir_free_MB Usable_file_MB Offline_disks Voting_files Name
....
MOUNTED EXTERN N 512 4096 1048576 1019 924 0 924 0 N TEST_DG/


SQL> create tablespace ts_small datafile '+TEST_DG/RAC/DATAFILE/tdg.df' size 100k autoextend on;

Tablespace created.


SQL> alter user user_dbc default tablespace ts_small;

User altered.
 

Now using user_dbc create a small table that fits easily on 100k tablespace:


SQL> create table t_small_corrupt tablespace ts_small as select 'rownum:' || rownum R1 from dual connect by level <= 1200;

Table created.

First thing now is to discover datafile inside ASM, we can use X$KFFXP that contains the mapping between files, extents and AUs. With this table we can track the  position of datafile inside ASM (please note that external redundancy applies in this case):

SQL> column DSK_PATH format a25;
SQL> select distinct GROUP_KFFXP,DISK_KFFXP,NUMBER_KFFXP, (select path from v$asm_disk where group_number=GROUP_KFFXP and disk_number=DISK_KFFXP) DSK_PATH from x$kffxp where number_kffxp=(select file_number from v$asm_alias where name='tdg.df') and GROUP_KFFXP=(select group_number from v$asm_alias where name='tdg.df');

GROUP_KFFXP DISK_KFFXP NUMBER_KFFXP DSK_PATH
----------- ---------- ------------ -------------------------
3 0 256 /dev/oracleasm/disks/TEST_DSK

 

GROUP_KFFXP - ASM disk group number
DISK_KFFXP - Disk number where extent is allocated
NUMBER_KFFXP - ASM file number
DSK_PATH - Device path for Disk

Next, we need to get the extent map for the datafile tdg.df (group 3, file number 256):


SQL> select xnum_kffxp,lxn_kffxp,pxn_kffxp,au_kffxp from x$kffxp where group_kffxp=3 and number_kffxp=256;

XNUM_KFFXP LXN_KFFXP PXN_KFFXP AU_KFFXP
---------- ---------- ---------- ----------
0 0 0 102

XNUM_KFFP - File extent number
LXN_KFFXP - 0 means that is a primary extent and not a mirrored extent (1 or 2, depends of redundancy)
PXN_KFFXP - Progressive file extent number that in this case (no mirrored extents) is always the same as XNUM_KFFXP
AU_KFFXP - Is the relative position of AU from the beginning of the disk

Time to use dd to access directy to disk using information we got from extent map of datafile. Use bs=1024k as AU for ASM is 1MB and skip parameter = (some) AU_KFFXP.

[root@rac1 oracle]# dd if=/dev/oracleasm/disks/TEST_DSK bs=1024k count=1 skip=102|strings|more
}|{z
TS_SMALL
r d V H : ,
rownum:458,
rownum:457,
rownum:456,
rownum:455,
...
1+0 records in
1+0 records out
1048576 bytes (1.0 MB) copied, 0.081027 seconds, 12.9 MB/s

So for sure, we hit the data now. No chance to miss!! :)


[root@rac1 oracle]# dd if=/dev/zero of=/dev/oracleasm/disks/TEST_DSK bs=1024k count=1 skip=102
1+0 records in
1+0 records out
1048576 bytes (1.0 MB) copied, 0.011318 seconds, 92.6 MB/s

Ok, all done, let's see if Oracle is aware of some block corruption:

SQL>  select * from v$database_block_corruption;

no rows selected

 

To unsure we are not missing anything let's run Oracle Health Monitor manually:


Basic Run Information
Run Name : my_test_run
Run Id : 1521
Check Name : DB Structure Integrity Check
Mode : MANUAL
Status : COMPLETED
Start Time : 2012-03-22 17:55:19.578660 +00:00
End Time : 2012-03-22 17:55:19.593574 +00:00
Error Encountered : 0
Source Incident Id : 0
Number of Incidents Created : 0

Conclusion here is simple: no data corruption. My next ideia was to use strace to find anything that can give some hints:


mmap(NULL, 1060864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1537f06000
ioctl(0, MGSL_IOCSTXIDLE or MTIOCGET or SNDCTL_MIDI_MPUCMD, 0x7fffc53e6c60) = -1 ENOTTY (Inappropriate ioctl for device)
lseek(0, 106954752, SEEK_CUR) = 0
read(0, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1048576) = 1048576
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1048576) = 1048576
munmap(0x7f1537f06000, 1060864) = 0
close(0) = 0
close(1) = 0
...
write(2, "1+0 records in\n1+0 records out\n", 311+0 records in
1+0 records out
) = 31
write(2, "1048576 bytes (1.0 MB) copied", 291048576 bytes (1.0 MB) copied) = 29
write(2, ", 0.00314435 seconds, 333 MB/s\n", 31, 0.00314435 seconds, 333 MB/s
 

I'm no strace expert, but besides ENOTTY error (Inappropriate I/O control operation) it SEEMS that dd can seek and write 0's on the disk with success.

Conclusions so far:

0) I can backup datafile with RMAN with no errors
1) I can "full table scan" data (even after reset buffer cache and shared pool)
2) v$database_block_corruption returns no rows
3) Health Monitor reports nothing
4) Nothing on alert.log (on both instances)
5) Can read table data using dd (dd if=/dev/oracleasm/disks/TEST_DSK bs=1024k count=1 skip=102|strings|more)

6) ??

7) I was unable to corrupt anything!!

 

This is probably a VirtualBox issue or trick, i'm now short on time to make a few more tests (using VMWare for example), but i excepted that this method works on a non-virtualized environment. Next time i will blog about things that work :-)

 

PS: Thanks to Martin Berger for time spent discussing this issue.

tags: , ,

03
Mar 12
Por Luís Marques, às 21:11 | comentar

Block corruption rarely happen. This is a fact and a good fact. The only problem with this is that it will be difficult to get some experience with because is too rare (from my experience). The only option is to get some tools in our lab to ensure that we can do some corruption. This time i'll use dd. Please note that this article is mainly a note for me (and for you, if you want) not really an innovative article.

Scenario

We need to create a tablespace, a user and a table to handle the experiments:

SQL> create tablespace ts_dbc datafile '+DATA/RAC/DATAFILE/ts_dbc.df' size 20m autoextend on next 10m extent management local segment space management auto;

Tablespace created.


SQL> create user user_dbc identified by user_dbc default tablespace ts_dbc;

User created.


SQL> grant connect, resource to user_dbc;

Grant succeeded.


[oracle@rac1 ~]$ sqlplus user_dbc/user_dbc@RAC_VMs


SQL> create table table_dbc as select dbms_random.value(0,100) N1 from dual connect by level <= 1000;

Table created.


SQL> column segment_name format a10;
SQL> select segment_name, tablespace_name from user_segments where segment_name='TABLE_DBC';

SEGMENT_NA TABLESPACE_NAME
---------- ------------------------------
TABLE_DBC TS_DBC


Using dd

It will be just easy to generate some trash in the block to make corruption happen, but with ASM (AFAIK) dd can't access directly to the ASM raw filesystem and we need to perform additional steps to get datafile out of ASM and in again after generate some noise in the block. To make task easier i'll use RMAN:

Take datafile offline:


SQL> alter database datafile '+DATA/RAC/DATAFILE/ts_dbc.df' offline;

Database altered.


[oracle@rac1 ~]$ export ORACLE_SID=RAC1
[oracle@rac1 ~]$ rman target=/

RMAN> BACKUP AS COPY DATAFILE '+DATA/RAC/DATAFILE/ts_dbc.df' FORMAT '/tmp/ts_dbc_%f';
...
channel ORA_DISK_1: starting datafile copy
input datafile file number=00007 name=+DATA/rac/datafile/ts_dbc.df
output file name=/tmp/ts_dbc_7 tag=TAG20120303T111330 RECID=2 STAMP=776949222
channel ORA_DISK_1: datafile copy complete, elapsed time: 00:00:16
..


After getting datafile out of ASM, we need to check the header_block:

SQL> select header_file, header_block from dba_segments where segment_name='TABLE_DBC';

HEADER_FILE HEADER_BLOCK
----------- ------------
7 130

 

So the header of segment (table) is block 130, so if the block 131,132,133,... get some noise it will eventually end up as a datafile with corrupted blocks :-)


[oracle@rac1 tmp]$ dd of=ts_dbc_7 bs=8192 conv=notrunc seek=131 << EOF
> #allglorytocorruption#
> EOF
0+1 records in
0+1 records out
23 bytes (23 B) copied, 0.0801337 seconds, 0.3 kB/s
[oracle@rac1 tmp]$ dd of=ts_dbc_7 bs=8192 conv=notrunc seek=132 << EOF
> #allglorytocorruption#
> EOF
0+1 records in
0+1 records out
23 bytes (23 B) copied, 0.0411593 seconds, 0.6 kB/s

Let's make sure using dbv (DBVERIFY) that we have generated some corruption:


[oracle@rac1 bin]$ dbv file=/tmp/ts_dbc_7 blocksize=8192

DBVERIFY - Verification starting : FILE = /tmp/ts_dbc_7
Page 131 is marked corrupt
Corrupt block relative dba: 0x01c00083 (file 7, block 131)

Page 132 is marked corrupt
Corrupt block relative dba: 0x01c00084 (file 7, block 132)

Page 133 is marked corrupt
Corrupt block relative dba: 0x01c00085 (file 7, block 133)

Another way to verify corruption on block is dump the block and read the output generated. Trace will show that datafile header is corrupt, meaning that checksum present in header is not consistent:


SQL> alter system dump datafile 7 block 131

...
Read of datafile '+DATA/rac/datafile/ts_dbc.df' (fno 7) header failed with ORA-01210
Hex dump of (file 7, block 1)
...

After inject some noise in datafile using dd, we need to restore the corrupted datafile back to ASM filesystem using RMAN:

RMAN> RUN 
2> {
3> RESTORE DATAFILE '+DATA/RAC/DATAFILE/ts_dbc.df' FROM TAG 'TAG20120303T111330';
4> RECOVER DATAFILE '+DATA/RAC/DATAFILE/ts_dbc.df';
5> }


RMAN script will eventually fails with ORA-19566 [exceeded limit of 0 corrupt blocks for file /tmp/ts_dbc_7] because RMAN will not allow to restore any datafile with corrupted blocks. The workaround for this is to set parameter MAXCORRUPT

Note: You will need to remove the file directly in asmcmd to RMAN performs the restore and recovery otherwise you will end up with and ORA 01135: file n accessed for DML/query is offline.

RMAN> RUN
2> {
3> SET MAXCORRUPT FOR DATAFILE '+DATA/rac/datafile/ts_dbc.df' TO 3;
4> RESTORE DATAFILE '+DATA/RAC/DATAFILE/ts_dbc.df' FROM TAG 'TAG20120303T111330';
5> RECOVER DATAFILE '+DATA/RAC/DATAFILE/ts_dbc.df';
6> }
 

After restore and recover datafile must be online:


SQL> alter database datafile '+DATA/RAC/DATAFILE/ts_dbc.df' ONLINE;

Database altered.

 

Finally we query v$database_block_corruption to unsure that Oracle is fully aware of corrupted blocks:

SQL> select * from v$database_block_corruption;

FILE# BLOCK# BLOCKS CORRUPTION_CHANGE# CORRUPTIO
---------- ---------- ---------- ------------------ ---------
7 131 3 0 CORRUPT

tags: ,

26
Jan 12
Por Luís Marques, às 15:49 | comentar

Ontem em jeito de discussão com um colega de profissão quando falávamos sobre FLASHBACK em Oracle, mais propriamente sobre Flasback Table que permite como todos sabemos para restaurar um estado antigo de uma tabela devido a erro aplicacional por exemplo. Depende este FLASHBACK da quantidade de UNDO no sistema (a sua retenção e o seu tamanho).

Nessa conversa surgiu uma dúvida que é basicamente se num FLASHBACK TABLE preserva ou não os ROWID aquando do restauro. Decidi montar o estaminé e testar. Antes disso queria referir que para restaurar uma tabela para um SCN antigo o row movement deve estar activo para as tabelas afectadas.O row_movement_clause serve para permitir à base de dado mover um registo, ou seja, parte do principio que para utilizar o FLASHBACK TABLE é necessário movimentar os registos e a consequência disso é _obviamente_ numa heap table modificar o rowid.


SQL> alter database flashback on;
Database altered.

SQL> create table t1_rowmove as select dbms_random.value(0,100) N1 from dual connect by level <=100000;

Table created.

SQL> !date
Thu Jan 26 15:21:39 WET 2012


Para podermos comparar os rowids aplicados criaremos uma tabela auxiliar que guarda todos os rowids da tabela t1_rowmove:


SQL> create table t1_rowids as select ROWID row_id from t1_rowmove;

Table created.

SQL> select * from t1_rowids where rownum < 5;

ROW_ID
------------------
AAAS8zAAEAAAACrAAA
AAAS8zAAEAAAACrAAB
AAAS8zAAEAAAACrAAC
AAAS8zAAEAAAACrAAD


Vamos agora apagar todos os registos da tabela t1_rowmove com o critério N1 < 50 e seguidamente usaremos o FLASHBACK TABLE para restaurar a tabela antes do DELETE:


SQL> delete from t1_rowmove where n1 < 50;

49898 rows deleted.

SQL> commit;

Commit complete.

SQL> alter table t1_rowmove enable row movement;

Table altered.

SQL> flashback table t1_rowmove o timestamp TO_TIMESTAMP('2012-01-26 15:21:39', 'YYYY-MM-DD hh24:mi:ss');

Flashback complete.


Pois bem, temos a tabela tal e qual a criámos, com o mesmo número de registos e vamos agora averiguar a questão dos ROWIDs. Para tal criaremos uma outra tabela auxiliar que guarda os ROWIDs da tabela "recem restaurada":


SQL> create table t2_rowids
2 as select ROWID row_id from t1_rowmove;

Table created.


Comparando os ROWIDs presentes nas tabelas t1_rowids vs t2_rowids:


SQL> select count(1) from t1_rowids where row_id not in (select row_id from t2_rowids);

COUNT(1)
----------
50102

SQL> select count(1) from t2_rowids where row_id not in (select row_id from t1_rowids);

COUNT(1)
----------
50102

 

Conclusão é simples, existe row movement e o resultado disso são rowids que mudam apesar dos dados permanecerem intactos com o FLASHBACK. Portanto os ROWIDs não são preservados. Faz também sentido que isto apenas ocorrerá em heap tables sendo que nas IOT os rowids se mantenham intactos.


07
Jan 12
Por Luís Marques, às 11:47 | comentar

As promised, i will share the code of Parke (new shiny name), the 10053 trace files Parser for Oracle. It is on github here: https://github.com/lcmarques/Parke  

 

For now, it has only 2 "less than useful" features: hints and explain plans. I will probably improve it next months to make it very complex matching Oracle software.


27
Dez 11
Por Luís Marques, às 17:05 | comentar

Last time i wrote about SYS_DL_CURSOR hint to find out if i can make use of it, but i realized that i rely many times on trace files, mainly 10053 tracefile, so i decided to write a simple parser (in Python) to help me. It's very simple and for now i will not share source code with you until i have a "good" and readeable version of the code :-)

 

10053Parser has 2 features for now:

1 - Hints

2 - Explain plans 

 

Feature #1 - Hints

 

10053Parser will (eventually) parse contents to find out how many DML/DDL statements were executed and which hints were used on statements showing the output in a good/fashion way:

 
[oracle@localhost trace]$ ./trace_10053.py --hints testSID_ora_14131_MESSI.trc
Report Hints for [testSID_ora_14131_MESSI.trc] ...
Hint | Used | Error | Level | SQL FULL TEXT
----------------------------------------------------------------------------------------------

N/A | N/A | N/A | N/A | create table t1_abc as select 1 as N1 from dual
SYS_DL_CURSOR () | 0 | 0 | 1 | select /*+ full(a) SYS_DL_CURSOR */ N1 from t1_abc a
FULL ("A") | 1 | 0 | 3 | select /*+ full(a) SYS_DL_CURSOR */ N1 from t1_abc a
INDEX ("A") | 1 | 0 | 3 | select /*+ index(a) */ N1 from t1_abc a

 

 

As you can see, output is easy to read and you have all information regarding hints, even if the statements uses multiple hints.

 

 

Feature #2 - Explain Plans

 

Instead of trying to remember explain plans for every statement, 10053Parser allows you to output the list of explain plans and corresponding statement:

 

[oracle@localhost trace]$ ./trace_10053.py --explain testSID_ora_14131_MESSI.trc
Report Explain for [testSID_ora_14131_MESSI.trc] ...
SQL: create table t1_abc as select 1 as N1 from dual

------------------------------------------+-----------------------------------+
| Id | Operation | Name | Rows | Bytes | Cost | Time |
------------------------------------------+-----------------------------------+
| 0 | CREATE TABLE STATEMENT | | | | 3 | |
| 1 | LOAD AS SELECT | | | | | |
| 2 | FAST DUAL | | 1 | | 2 | 00:00:01 |
------------------------------------------+-----------------------------------+

SQL: select /*+ full(a) SYS_DL_CURSOR */ N1 from t1_abc a

-------------------------------------+-----------------------------------+
| Id | Operation | Name | Rows | Bytes | Cost | Time |
-------------------------------------+-----------------------------------+
| 0 | SELECT STATEMENT | | | | 2 | |
| 1 | TABLE ACCESS FULL | T1_ABC | 1 | 13 | 2 | 00:00:01 |
-------------------------------------+-----------------------------------+

SQL: select /*+ index(a) */ N1 from t1_abc a

-------------------------------------+-----------------------------------+
| Id | Operation | Name | Rows | Bytes | Cost | Time |
-------------------------------------+-----------------------------------+
| 0 | SELECT STATEMENT | | | | 2 | |
| 1 | TABLE ACCESS FULL | T1_ABC | 1 | 13 | 2 | 00:00:01 |
-------------------------------------+-----------------------------------+

 

As soon i have the courage i will release source code :)


17
Dez 11
Por Luís Marques, às 14:20 | comentar

[English]

Some days ago i came across with a pl/sql code where they used (for some reason) an Oracle hint called SYS_DL_CURSOR. I got it when i was looking into V$SQL and i was not familiar with this, maybe the only thing i ("think") know about it is that in some conditions it performs a direct path insert, just like append hint. Hint itself seems to be not official documented by Oracle and i figured out that some aplications (Hi Informatica PowerCenter) uses it for some direct path insert ETL.
This is not by any means a "standard" way to do direct path inserts but if it works good, we need to get into it. So i decided to take a more deep look inside this hint to make sure that i understand what it really does.

A good way to verify if your direct path insert got right is try to query the table segment before your COMMIT or ROLLBACK operation. Let's create a table and try a simple direct path insert:


SQL> create table t1_dpi(n1 number, n2 number, CONSTRAINT pk_n UNIQUE(n1));


SQL> insert /*+ append_values */ into t1_dpi VALUES(1,1);
SQL> insert /*+ append_values */ into t1_dpi VALUES(2,2);


SQL> select * from t1_dpi where rownum < 1;
select * from t1_dpi where rownum < 1
*
ERROR at line 1:
ORA-12838: cannot read/modify an object after modifying it in parallel

 

As you can easily see, we got an ORA-12838 after querying segment when doing a direct path. This happens because the transaction made an attempt to read (or modify) statements on a table and this is not allowed in direct loads. It will prevent data inconsistency [see this].
In this way you ensure that you will use an direct path insert using the common hint /*+ append_values */ (or /*+ append */) however you can use some 10046 trace:


SQL> alter session set events='10046 trace name context forever, level 12';
Session altered.

insert /*+ append_values */ into lcmarques.t1_dpi VALUES(2,2)
END OF STMT
PARSE #3:c=1000,e=926,p=0,cr=0,cu=0,mis=1,r=0,dep=0,og=1,plh=3581094869,tim=1324125386379660
WAIT #3: nam='direct path write' ela= 37 file number=4 first dba=10908 block cnt=1 obj#=-1 tim=1324125386380206

 

So as everybody expected append_values hint works as advertised :). Now let's try SYS_DL_CURSOR

 


SQL> insert /*+ SYS_DL_CURSOR */ into t1_dpi values (8,8);

1 row created.

SQL> select * from t1_dpi where rownum < 1;

no rows selected

 

It seems that we can query the table (no ORA-12838), so probably the HINT is not make direct path insert and Oracle is ignoring it. Let's check explain plan and 10046 trace to make sure:


--------------------------------------------+-----------------------------------+
| Id | Operation | Name | Rows | Bytes | Cost | Time |
--------------------------------------------+-----------------------------------+
| 0 | INSERT STATEMENT | | | | 1 | |
| 1 | LOAD TABLE CONVENTIONAL | | | | | |
--------------------------------------------+-----------------------------------+


insert /*+ SYS_DL_CURSOR */ into t1_dpi values (8,8)
END OF STMT
PARSE #3:c=1000,e=931,p=0,cr=0,cu=0,mis=1,r=0,dep=0,og=1,plh=0,tim=1324126029360833
EXEC #3:c=0,e=169,p=0,cr=1,cu=7,mis=0,r=1,dep=0,og=1,plh=0,tim=1324126029361070
STAT #3 id=1 cnt=0 pid=0 pos=1 obj=0 op='LOAD TABLE CONVENTIONAL (cr=1 pr=0 pw=0 time=0 us)'

 

No luck here! Next option is now use a 10053 to unsure that CBO is not ignoring silently SYS_DL_CURSOR hint. Dumping Hints section shows even invalid/malformed hints here and for some invalid hints it shows err=1 or used=0. On our case used=0 shows that for some reason this hint is not beeing used:

SQL>  alter session set events='10053 trace name context forever, level 1';

Dumping Hints
=============
atom_hint=(@=0x8cd666f0 err=0 resol=0 used=0 token=914 org=1 lvl=1 txt=SYS_DL_CURSOR ())
====================== END SQL Statement Dump ======================


Another ideia might be trying to use sqlldr (i googled some cases) to generate SYS_DL_CURSOR hint for direct path loading, but no luck here too:

[oracle@localhost scripts]$ sqlldr lcmarques/lcmarques@testSID control=loader.ctl log=logfile.log direct=true;
Load completed - logical record count 9.


SELECT /* OPT_DYN_SAMP */ /*+ ALL_ROWS IGNORE_WHERE_CLAUSE NO_PARALLEL(SAMPLESUB
) opt_param('parallel_execution_enabled', 'false') NO_PARALLEL_INDEX(SAMPLESUB)
NO_SQL_TUNE */ NVL(SUM(C1),:"SYS_B_0"), NVL(SUM(C2),:"SYS_B_1") FROM (SELECT /*+
NO_PARALLEL("T1_DPI") FULL("T1_DPI") NO_PARALLEL_INDEX("T1_DPI") */ :"SYS_B_2"
AS C1, :"SYS_B_3" AS C2 FROM "T1_DPI" "T1_DPI") SAMPLESUB

 

Last thing was to check in v$sql_hint for this hint:


SQL> select class from v$sql_hint where class like 'SYS_DL%';

CLASS
----------------------------------------------------------------
SYS_DL_CURSOR

 

Conclusion is simple, i can't make it. For some reason Oracle CBO is ignoring SYS_DL_CURSOR. Maybe this is valid under certain circunstances that i really don't know or is already depreceated for direct path inserts or 11g doesn't really like it.


08
Dez 11
Por Luís Marques, às 19:44 | comentar

Hoje a matéria é muito simples, é mais uma clarificação "Back to Basics" do que um post elaborado com investigação.
Serve assim de material de base para quando surgirem mais dúvidas deste tipo no meu local de trabalho ;)

A ideia é simples: Aplicar um select _durante_ o update e garantir que existe total consistência de leitura
e que não existe qualquer "bloqueio" em nenhuma sessão dada a arquitectura. Depois do exemplo prático, explicarei como funciona.

SQL> create table t2_locks as select rownum N1, dbms_random.string('A', 10) S1 from dual connect by level <= 1000000;

Table created.


SQL> select sid from v$session where audsid=userenv('SESSIONID');

SID
----------
42


SQL> select sid from v$session where audsid=userenv('SESSIONID');

SID
----------
32


Validações na SID 42:

 

SQL> select MIN(N1) from t2_locks;

MIN(N1)
----------
1


SQL> select MAX(N1) from t2_locks;

MAX(N1)
----------
1000000


SQL> select avg(N1) from t2_locks;

AVG(N1)
----------
500000.5


Estas três validações irão permitir verificar a consistência dos dados durante o update e após o update (sem COMMIT).

Avançaremos então com o update demorado à SID 42, enquanto na SID 32 faremos as validações enquando decorre o update.
Para facilitar a compreensão, farei um pequeno video (desta vez sem som ;)):

O update será o seguinte:

 

 

update t2_locks set N1 = (select MIN(dbms_random.value(0,40000)) from dual connect by level <= 2000000);






Como puderam ver no vídeo e apesar de não ter mostrado os locks feitos na v$lock durante o processo todo, não existe qualquer interferência das sessões entre elas, pois o Oracle possui um mecanismo de consistência de leitura que faz com que o resultado da query venha apenas de um ponto no tempo (na altura que a query começa) para uma ou mais transacções. Quando acontece o update (ou qualquer DML), o Oracle usa os rollback segments para providenciar dados consistentes, ou seja, os rollback segments contem os valores antigos dos dados que vão sendo sujeitos ao update (ou qualquer DML). O select poderá ler parte dos dados do table segment (da tabela em si) e outra parte dos rollback segment enquanto decorre o update ou este não está "commited". É basicamente um snapshot dos data blocks antes da sua modificação. Assim que é feito o commit e executado um select, os data blocks presentes no rollback segments são descartados e os dados do select virão exclusivamente do table segment (ou index segment, se for o caso).

É este rollback segment que permite que aquando de um rollback exista o processo de reversão dos dados sujeitos ao DML.
Explica também porque é que um rollback em média demora o mesmo tempo que a própria operação de DML que o originou, ou seja,
o Oracle tem que ir ao rollback segment lê-lo e repor tudo como estava, ou seja, a operação inversa aquando do DML a nível de
segments.

 

PS: Acabei por fazer metade em texto, metade em vídeo :-)


04
Dez 11
Por Luís Marques, às 20:21 | comentar

Desde o inicio que qualquer Oracle implementa mecanismos de lock para lidar com problemas de concorrência e de consistência na Base de Dados. Aceder a dados simultâneamente enquanto é possível dar a outras sessões dados  consistentes, permitindo coerência na leitura e escrita dos dados permanece a função mais importante de uma Base de Dados. Sem estar a querer entrar pelo mundo teórico do modelo ACID, vou apenas demonstrar alguns pontos pertinentes dos mecanismos de Lock no Oracle.

Vou usar 2 sessões e analisar os respectivos locks à medida das operações que vou fazendo. Para facilitar a compreensão vou também explicando:

SQL> create table t1_locks as select rownum as N1, dbms_random.string('A',2) S1 from dual connect by level <= 5;

Table created.

SQL> select sid from v$session where audsid=userenv('SESSIONID');

SID
----------
40

SQL> select sid from v$session where audsid=userenv('SESSIONID');

SID
----------

29

SQL> column s1 format a3;
SQL> select * from t1_locks;

N1 S1
---------- ---
1 PN
2 fP
3 Yo
4 Ur
5 KX

 

Na SID 40 será feito um insert:

SQL> insert into t1_locks (N1,S1) values(6,'SL');

1 row created.


Sem o commit executado veremos o que vê a sessão com o ID 29:

 


SQL> select * from t1_locks;

N1 S1
---------- ---
1 PN
2 fP
3 Yo
4 Ur
5 KX

 


Naturalmente a SID 29 não verá as alterações feitas pela SID 40, pois não existe commit. Veremos o que se passou
no Oracle ao nível do mecanismo de locking:


SQL> select sid,type,id1,lmode,request from v$lock where sid in (40,29);

SID TY ID1 LMODE REQUEST
---------- -- ---------- ---------- ----------
40 AE 100 4 0
29 AE 100 4 0
40 TM 74204 3 0
40 TX 458771 6 0

 

Os "AE" são para desprezar (significa basicamente Application Edition lock, 11g apenas), mas os outros não. "TX" representa Transaction Lock, ou Transaction Enqueue e "TM" significa DML ou Table lock (DML Enqueue), ou seja, o TX  representa a transação, é usado principalmente para prevenir que uma outra transacção modifique o mesmo registo, assim cada vez que um transacção necessita de modificar um registo adquire um TX. O "TM" é usado principalmente para gerir mecanismos de concorrência de operações DDL, como por exemplo, tentar fazer "drop" a uma tabela durante uma operação de insert (ou outro qualquer DML)
A coluna LMODE representa o modo de lock.
Na "TM" o modo de lock será row-exclusive (row-X, mode 3), ou seja, apenas os registos inseridos, enquanto do ponto de vista da "TX" o modo de lock será exclusive, já que o "TX" se refere à propria transacção.

Podemos de uma forma simples verificar se o lock "TM" está aplicado na tabela:


SQL> select name from sys.obj$ where obj#=74204;

NAME
------------------------------
T1_LOCKS


Confirma-se que existe um lock no objecto T1_LOCKS.
Já vimos que a SID 29 não consegue ver as alterações feitas pela SID 40, pois não houve lugar a commit.
O próximo passo é inserir um registo com a SID 29 e fazer update a um já existente (N1=1). Vejamos:

 


SQL> insert into t1_locks (N1,S1) values(7,'XX');

1 row created.


SQL> update t1_locks set S1='BB' where N1=1;

1 row updated.


Não será feito commit. Analisaremos os locks agora referentes às duas sessões:

 

SQL>  select sid,type,id1,lmode,request from v$lock where sid in (40,29);

SID TY ID1 LMODE REQUEST
---------- -- ---------- ---------- ----------

40 TM 74204 3 0
29 TM 74204 3 0
29 TX 393239 6 0
40 TX 458771 6 0

 

Tudo faz sentido, 1 lock de cada tipo para cada SID diferente. As SID 29 e 40 estão a fazer "Table Lock" com row-S e ambas estão a trazer "Transaction Lock" para os registos inseridos e modificados. Até agora cada sessão consegue apenas visualizar aquilo que inseriu e/ou modificou. Vejamos:

SID 40:


SQL> select * from t1_locks; [Inclui INSERT do N1=6]

N1 S1
---------- ---
1 PN
2 fP
3 Yo
4 Ur
5 KX
6 SL

 

SID 29:


SQL> select * from t1_locks; [Inclui INSERT do N1 = 7 e UPDATE do N1=1]

N1 S1
---------- ---
1 BB
2 fP
3 Yo
4 Ur
5 KX
7 XX

 

Vamos piorar as coisas. Vamos na SID 40 tentar fazer update na tabela para N1=1, tal e qual fizemos na SID 29:


SQL> update t1_locks set S1='CC' where N1=1;
 

A sessão está bloqueada, não existiu lugar a update, até que a SID 29 faça commit/rollback das alterações. Podemos ver isto em "directo":

SQL>  select event, seconds_in_wait, sid from v$session_wait where sid in (40,29);

EVENT SECONDS_IN_WAIT SID
-------------------- --------------- ----------
SQL*Net message from 246 29
client

enq: TX - row lock c 105 40
ontention


A coluna SECONDS_IN_WAIT permite-nos saber À quanto tempo o lock espera, ou seja, o evento "row lock contention" não é mais que uma sessão que espera por um row lock feito por outra sessão. No nosso caso o modo 6 (coluna LMODE) indica mesmo que para resolver este problema deverá ser feito um rollback ou commit dos dados. Na SID 29 faremos o commit:


SQL> commit;

Commit complete.

 

Entretanto na SID 40:


SQL> update t1_locks set S1='CC' where N1=1;

1 row updated.

 

Faremos agora um select * a ambas as tabelas:

SID 40:

SQL> select * from t1_locks;

N1 S1
---------- ---
1 CC
2 fP
3 Yo
4 Ur
5 KX
7 XX
6 SL

7 rows selected.


SID 29:


SQL> select * from t1_locks;

N1 S1
---------- ---
1 BB
2 fP
3 Yo
4 Ur
5 KX
7 XX


O resultado é simples de interpretar. Enquanto que a SID 40, vê as alterações efectuadas pela SID 29, o contrário não acontece pois ainda não foi feito qualquer commit na SID 40. De notar que o valor para N1=1 é diferente em ambas as sessões já que na SID 40 foi feito o update para CC mas sem commit, sendo isto apenas visto pela SID 40 e não pela SID 29. De notar ainda que a SID 40 contêm um registo [N1=7] que foi inserido pela SID 29. Para terminar este exemplo bastante simples vamos ver de novo o estado dos locks:


SQL> select sid,type,id1,lmode,request from v$lock where sid in (40,29);

SID TY ID1 LMODE REQUEST
---------- -- ---------- ---------- ----------
40 TM 74204 3 0
40 TX 458771 6 0


Portanto apenas a SID 40 ainda detêm locks pois não foi feito qualquer rollback ou commit.


tags:

19
Nov 11
Por Luís Marques, às 17:25 | comentar

Tinha mostrado anteriormente que apenas reorganizando a tabela seria possível alterar o valor do CF e que  uma reorganização do índice nada poderia fazer, pois o CF depende directamente da desordem dos blocos na tabela comparada com a organização no índice. No entanto existe uma forma relativamente simples de alterar o valor do CF que é usando reverse key index (não sei o termo em português).

Existem alguns casos onde este tipo de índices é útil, que foram desenhados essencialmente para resolver o problema de contenção nos blocos (index block contention). Basicamente ocorrem em cenários de muita concorrência (DML insert, update ou delete) onde as várias sessões concorrentes precisam de aceder ao mesmo bloco (chamado "hot block") gerando assim contenção ao nível do bloco do índice causando inumeros wait events do tipo "buffer busy waits", por exemplo.

Mas o post não é sobre os Reverse Key Index, mas sim sobre o CF e como este valor pode ser alterado usando um índice reverted. A alteração da ordem no índice leva a que a desordem na tabela seja diferente. Como exemplo simples, se um ID para inserir na tabela for gerado como 112233 será inserido como 332211 no índice. Este tipo de "reverse" permite que os inserts sejam espalhados por toda a estrutura do índice, evitando a contenção em apenas um só leaf bloco (o mais há direita). Com isto fazemos com que as entradas no índice deixem de estar ordenadas da forma natural e como conhecemos, ou seja, o 112234 a seguir ao 112233.
Apesar de parecer resolver alguns problemas nomeadamente em ambientes RAC muito concorridos, cria uma outra panóplia de problemas que não discutiremos neste post.

O código seguinte mostrará uma tabela, um índice normal que depois será convertido para "reverse" e os respectivos
valores do CF após cada etapa:

 


SQL> create table t_cf2 as select ceil(dbms_random.value(0,100000)) N1 from dual connect by level <= 100000;

Table created.


SQL> create index i_cf2 on t_cf2(N1);

Index created.


SQL> exec dbms_stats.gather_table_stats(null,'T_CF2', cascade=>TRUE);

PL/SQL procedure successfully completed.


SQL> select blevel, leaf_blocks, clustering_factor from user_indexes where index_name = 'I_CF2';

BLEVEL LEAF_BLOCKS CLUSTERING_FACTOR
---------- ----------- -----------------
1 222 99270

SQL> alter index i_cf2 rebuild reverse;

Index altered.


SQL> select blevel, leaf_blocks, clustering_factor from user_indexes where index_name = 'I_CF2';

BLEVEL LEAF_BLOCKS CLUSTERING_FACTOR
---------- ----------- -----------------
1 222 99265


Temos uma valor diferente no CF, sendo que a diferença é pouca o que significa que o meu exemplo aqui não foi o melhor, no entanto, como nota final não devem de forma alguma usar os RKI sem cuidado, pois a ordem das chaves deixa de ser a natural e os "range scans" deixam de ser possíveis (predicados como BETWEEN, LIKE, > <) e o CBO vai por completo ignorar este tipo de índices. Estes dois posts sobre o CF foram apenas para fazer entender como o CF varia em função das várias ordens seja na tabela ou no índice.


tags: ,

05
Nov 11
Por Luís Marques, às 20:47 | comentar

Como já tinha explicado antes, o CF (vou me referir a partir de agora como CF, para me facilitar) é basicamente uma métrica que compara a ordem no índice com o grau de desordem na tabela, ou de outra forma se quiserem, é forma como os dados estão alinhados na tabela em relação à ordem no índice e o I/O necessário para ler a tabela inteira via full index scan.

Dado um índice é organizado e ordenado, um "rebuild" ao índice nunca (ou em circunstâncias especiais)  alterará o valor do CF, pois a ordem das entradas no índice mantêm-se igual após o rebuild tal e qual a ordem dos registos na tabela. Assim é facil entender que para alterar o valor do CF temos que reorganizar a tabela associada e assim alterar o valor do CF.

É bom relembrar ainda que por norma um bom CF é um valor igual (ou abaixo, dado que podem existir blocos vazios abaixo do HWM) ao número de registos da tabela a que se refere o índice.

Vamos então ao exemplo:


SQL> create table t_cf as select dbms_random.value(0,500) as N1, 
dbms_random.string('A',45) as A1,
dbms_random.string('l',45) as L1 from dual connect by level <= 400000;

Table created.


SQL> create index i1_cf on t_cf(A1);

Index created.


SQL> exec dbms_stats.gather_table_stats('LCMARQUES','T_CF', cascade=>TRUE);

PL/SQL procedure successfully completed.



SQL> select blevel, leaf_blocks, clustering_factor from user_indexes where index_name = 'I1_CF';

BLEVEL LEAF_BLOCKS CLUSTERING_FACTOR
---------- ----------- -----------------
2 3200 399942



SQL> select num_rows, blocks from user_tables where table_name = 'T_CF';

NUM_ROWS BLOCKS
---------- ----------
400000 6779

Temos um índice recém criado (I1_CF) que tem um CF de 3999942, um valor bastante distante do número de blocos da tabela (6779), fazendo dele um índice com um mau CF (relembrar apenas que o CF é apenas um dos critérios escolhidos pelo CBO). Vamos então tentar reorganizar os mesmo dados e obter um CF diferente, um pouco melhor:


SQL> create table t2_cf as select * from T_CF;

Table created.

SQL> truncate table T_CF;

Table truncated.

SQL> insert into t_cf select * from t2_cf order by A1;

400000 rows created.

SQL> exec dbms_stats.gather_table_stats('LCMARQUES','T_CF', cascade=>TRUE);


SQL> select blevel, leaf_blocks, clustering_factor from user_indexes where index_name = 'I1_CF';

BLEVEL LEAF_BLOCKS CLUSTERING_FACTOR
---------- ----------- -----------------
2 2858 6558



A estratégia foi simples, existem várias formas de reorganizar a tabela e uma delas é usando os CTAS, ou seja criou-se uma tabela auxiliar com os dados e inseriu-se posteriormente na tabela original (T_CF) ordenamente pela coluna que o índice contem. Assim os registos inseridos estão agora organizados exactamente da mesma forma que o índice, levando como é obvio a um valor bastante bom de CF e a um decréscimo do I/O na próxima visita à tabela por índice full scan.

No entanto, existe uma dúvida e é legitima, dado que criou-se uma tabela com 400k registos, depois um índice, levantou-se as estatísticas e o valor do CF saiu péssimo. Isto acontece pois na criação do índice inicialmente os registos na tabela estão desorganizados e na natureza de qualquer índice estes encontram-se devidamente ordenados segundo as colunas presentes, dai que a opção viável para alterar o CF será reordenar a tabela.

Vimos aqui como um índice recém-criado pode não corresponder às espectativas em termos de custo, levando a um excessido consumo de I/O dado que será necessário re-visitar o mesmo bloco "n" vezes dada a aleatoriedade dos blocos (e registos) na tabela, para tal a análise do CF deve ser cuidada, dado que não é o único factor que influencia a decisão do CBO, mas é um bastante importante. Importante também que se a tabela conter mais índices, decidir o critério de organização nem sempre é facil.

 

Existem outras formas de alterar o valor do CF, entre elas mexendo apenas no índice, mas deixarei isso para a parte 2 deste ponto.

tags: ,

Author
OCP 11g
Me
Março 2012
Dom
Seg
Ter
Qua
Qui
Sex
Sab

1
2
3

4
5
6
7
8
9
10

11
12
13
14
15
16
17

18
19
20
21
23
24

25
26
27
28
29
30
31


subscrever feeds
pesquisar blog
 
TWITTER