tag:blogger.com,1999:blog-62309598403894816772008-05-07T14:27:26.341-07:00Oracle Notepadlferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comBlogger43125tag:blogger.com,1999:blog-6230959840389481677.post-66651998472519929962008-04-20T04:51:00.000-07:002008-04-21T18:48:03.234-07:00Incorporando Google Maps en Application Express<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp1.blogger.com/_fRmrOAuwnQ4/SAvD8wuVfEI/AAAAAAAAAEY/L0K6F5KcOD4/s1600-h/copa2.JPG"><img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer;" src="http://bp1.blogger.com/_fRmrOAuwnQ4/SAvD8wuVfEI/AAAAAAAAAEY/L0K6F5KcOD4/s320/copa2.JPG" alt="" id="BLOGGER_PHOTO_ID_5191458444113443906" border="0" /></a>Diversos artículos pueden encontrarse en Internet acerca de cómo implementar Google Maps en Oracle Application Express (APEX). Desafortunadamente, la mayoría son algo básicos y se limitan a cargar un mapa dentro de una región, no teniendo interacción con la base de datos.<br /><br />En esta oportunidad vamos a ver cómo lograr desplegar puntos dinámicamente en un mapa a partir de datos almacenados en una tabla. Únicamente se requieren conocimientos básicos de Application Express, PL/SQL y Javascript.<br /><br />Este artículo no pretende mostrar todas las funcionalidades de la API de Google Maps, sino resolver el problema puntual de programación en Oracle, luego dependerá de las habilidades Javascript del programador y de conocer a fondo las funcionalidades que brinda esta maravillosa API.<br /><br />Aclarado esto, comencemos, paso a paso:<br /><br /><span style="font-weight: bold; color: rgb(0, 0, 102);">Paso 1: Tener APEX instalado.</span><br /><br />Lo primero es tener APEX instalado, ya que necesitaremos la URL o IP para el paso siguiente.<br />Si tienen duda si APEX está instalado en su base de datos, ver mi <a href="http://oraclenotepad.blogspot.com/2008/04/como-saber-si-aplication-express-apex.html" target="_blank">artículo anterior</a>.<br /><br /><span style="font-weight: bold; color: rgb(0, 0, 102);">Paso 2: Solicitar una clave en Google Maps.</span><br /><br />Ir a la siguiente direccion <a href="http://code.google.com/apis/maps/signup.html" target="_blank">http://code.google.com/apis/maps/signup.html</a> para registrarnos y solicitar un código que necesitamos para utilizar el servicio. En el registro, se debe ingresar la URL donde será utilizado Google Maps.<br />Por ejemplo, si utilizamos el espacio gratuito <a href="http://apex.oracle.com/" target="_blank">Oracle Apex</a>, podemos ingresar la URL http://apex.oracle.com/pls/otn/. Si se está desarrollando localmente (en la propia máquina) puede registrarse la IP 127.0.0.1, pero cuando finalice el desarrollo habrá que solicitar una nueva clave con la URL de producción, ya que Google requiere que el sitio registrado sea de acceso público.<br />El código generado será referenciado desde de nuestro script.<br /><br /><span style="font-weight: bold; color: rgb(0, 0, 102);">Paso 3: Manos a la obra con APEX!</span><br /><br />En nuestra nueva aplicación APEX, como primer paso debemos crear una página.<br /><br />Editar los atributos de la página y en el campo <span style="font-style: italic;" class="htmldbLabelOptional">Enfoque de Cursor</span> elegir el valor <span style="font-style: italic;">No enfocar Cursor.</span><br /><br />En la sección Cabecera HTML, incluir el siguiente script reemplazando nuestra clave:<br /><pre style="color: rgb(0, 153, 0);"><script src="http://maps.google.com/maps?file=api&v=2&key=<span style="color: rgb(51, 51, 255);">la_clave_de_google_maps</span>&hl=es"<br />type="text/javascript"></script><br /><script type="text/javascript"><br /><br />//<![CDATA[<br /><br />function load() {<br />if (GBrowserIsCompatible()) {<br />miMapa();<br />}<br />}<br /><br />//]]><br /></script></pre>En este javascript acabamos de definir la función load, la cual invoca a otra función miMapa que crearemos más adelante.<br /><br />En el campo <span style="font-style: italic;">Atributo de Cuerpo HTML de Página</span>, pegar lo siguiente:<pre style="color: rgb(0, 153, 0);">onload="load();" onunload="GUnload();"</pre><ul><li>Se puede observar cómo en el evento onload de la página, se invoca a la función load incluída en el cabezal HTML. </li></ul>Hecho esto, guardar los cambios efectuados en la página<br /><br />Ahora queda la parte interesante, ya que deberemos crear la función miMapa la cual ejecutará la carga del mapa y los puntos de referencia extraídos de la base de datos. Esta función será generada por un bloque anónimo PL/SQL el cual será ejecutado cada vez que se carga la página. <br />Para este ejemplo, se creó una sencilla tabla <span style="font-style: italic;">geopuntos</span> la cual contiene información sobre los puntos de referencia que queremos desplegar en nuestro mapa:<pre style="color: rgb(0, 153, 0);">id_punto NUMBER(10) NOT NULL<br />latitud NUMBER(18,15) NOT NULL<br />longitud NUMBER(18,15) NOT NULL<br />descripcion VARCHAR2(100)</pre><ul><li>La precisión de <span style="font-style: italic;">latitud</span> y <span style="font-style: italic;">longitud</span> es adecuada para almacenar coordenadas geográficas de Google Maps.<br /></li><li>Cada punto puede enriquecerse con valiosa información como pueden ser imagenes, archivos, links, íconos, etc.<br /></li><li>Se recomienda consultar la extensa documentación y tutoriales que brinda Google Maps en su sitio oficial.</li></ul>En el área <span style="font-style: italic;">Presentación de Página</span>, en la sección <span style="font-style: italic;">Procesos</span>, vamos a implementar nuestro bloque PL/SQL.<br>Crear un nuevo proceso de tipo <span style="font-style: italic;">PL/SQL</span>. En <span style="font-style: italic;">Atributos de Proceso</span>, ingresar un nombre cualquiera y elegir el valor <span style="font-style: italic;">En Carga, Antes de Cabecera</span>. Clickear siguiente, y en <span style="font-style: italic;">Proceso</span>, incluir el siguiente bloque:<pre style="color: rgb(0, 153, 0);">DECLARE<br /> CURSOR cr_puntos IS select * from puntosmapa;<br />BEGIN<br /> htp.p('<script type="text/javascript">');<br /> htp.p('//<![CDATA['); <br /> <br /> htp.p('function crearMarca(lat, long, descr) {');<br /> htp.p(' var point = new GLatLng(lat,long);');<br /> htp.p(' var opts = {title: descr};');<br /> htp.p(' var mark = new GMarker(point,opts);');<br /> htp.p(' GEvent.addListener(mark, "click", function() {<br /> mark.openInfoWindowHtml(''<div><B>'' + descr + ''</B><BR>Lat: '' + lat +<br /> ''<BR>Long: '' + long + ''</div>'');');<br /> htp.p(' });');<br /> htp.p('return mark;');<br /> htp.p('}'); <br /> <br /> htp.p('function miMapa() {'); <br /> htp.p('var map = new GMap2(document.getElementById("map"));');<br /> htp.p('map.addControl(new GLargeMapControl());');<br /> htp.p('map.addControl(new GMapTypeControl());');<br /> htp.p('map.addControl(new GOverviewMapControl());');<br /> htp.p('map.setCenter(new GLatLng(-22.915739,-43.22912), 11);');<br /> <br /> -- Recorro cursor e inserto los puntos<br /> FOR punto IN cr_puntos<br /> LOOP<br /> htp.p('var marker = crearMarca('||to_char(punto.latitud,'999.9999999999999999')||','<br /> ||to_char(punto.longitud,'999.9999999999999999')||',"'||punto.descripcion||'");'); <br /> htp.p('map.addOverlay(marker);');<br /> END LOOP;<br /> htp.p('}');<br /><br /> htp.p('//]]>');<br /> htp.p('</script>');<br />END;</pre>Este es el punto neurálgico de la aplicación: en este bloque PL/SQL resolvemos la consulta a la base de datos, la creación del mapa y la publicación de los puntos de referencia extraídos. Notar el uso de la función PL/SQL <span style="font-weight: bold;">htp.p()</span>. Así como DBMS_OUTPUT.PUT_LINE envia caracteres a la salida en pantalla, la función htp.p envía cadenas de caracteres al browser, y esto precisamente lo que necesitamos ya que queremos construir el Javascript a ser ejecutado en la carga de la página.<br /><br />Puede apreciarse la sencillez de esta implementación, en pocas líneas crea un mapa, lo centra y carga los controles básicos de zoom. La particularidad en este caso es el FOR LOOP, el cual recorre un cursor definido sobre la tabla, y en cada iteración define una marca "publicando" Javascript en tiempo de ejecución. Cada referencia tiene un toolTip cuando se pasa el mouse por encima y un globo informativo que se abre al clickear sobre la marca.<br /><br />Y eso es todo, tras guardar el proceso (dejar el resto de los valores por defecto), hemos concluído nuestra aplicación y ya está lista para ser explorada.<br /><br />Las posibilidades son muchísimas, y cuanto más se profundice sobre la documentación, mejores funcionalidades podremos lograr en APEX.<br><br /><span style="font-weight: bold; color: rgb(0, 0, 102);">Un ejemplo implementado</span><pre style="color: rgb(0, 153, 0);"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp3.blogger.com/_fRmrOAuwnQ4/SAu79QuVfBI/AAAAAAAAAEA/m61b7IqCx24/s1600-h/gmapsdemo.JPG"><img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer;" src="http://bp3.blogger.com/_fRmrOAuwnQ4/SAu79QuVfBI/AAAAAAAAAEA/m61b7IqCx24/s320/gmapsdemo.JPG" alt="" id="BLOGGER_PHOTO_ID_5191449656610356242" border="0" /></a></pre>Hice una página de demostración para que puedan ver que realmente funciona. En mi tabla de datos tengo almacenados puntos turísticos de Río de Janeiro, ciudad donde vivo actualmente :)<br /><br />Para saber las coordenadas geográficas de un punto en la tierra, podemos utilizar por ejemplo la página <a href="http://itouchmap.com/latlong.html" target="_blank">Geocoder</a>.<br /><br />Hay mucha documentación y ejemplos disponibles sobre Google Maps y realmente es fácil comenzar a obtener resultados sorprendentes.<br /><br />Finalmente le agradezco a Emilio Le Mener por su ayuda en Javascript.<br /><br />Espero que este tutorial haya sido útil y puedan comenzar a implementar sin problemas con APEX y Google Maps.<br /><div style="text-align: center;"><br /><img id="BLOGGER_PHOTO_ID_5175489526410503874" style="cursor: pointer;" alt="" src="http://bp1.blogger.com/_fRmrOAuwnQ4/R9MITahz5sI/AAAAAAAAADA/L9NY7wTm94I/s320/execute.jpg" border="0" /> <a href="http://apex.oracle.com/pls/otn/f?p=45998:1:4251456126624924:::::" target="_blank">Probar la Demo</a><br /></div><div style="text-align: center;"></div><span style="font-weight: bold;">Ver también:</span><br /><a href="http://code.google.com/apis/maps/index.html" target="_blank">Google Maps API</a><br /><a href="http://doug.ricket.com/gdd2007/" target="_blank">Presentación Google Developer Day 2007</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-11989312056827472802008-04-18T00:05:00.000-07:002008-04-17T20:04:30.123-07:00Cómo saber si Application Express (APEX) está instalado en OracleEjecutar la siguiente consulta en SQL*Plus con un usuario con rol DBA:<br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >SELECT username </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >FROM dba_users </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >WHERE username IN ('FLOWS_010500','FLOWS_010600','FLOWS_020000','FLOWS_020200','FLOWS_030000', 'FLOWS_030100');</span><br /><br />Si no se obtiene ninguna fila como resultado, entonces APEX no está instalado en la base de datos.lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-75564794639045416402008-04-17T08:34:00.000-07:002008-04-17T20:05:19.623-07:00Oracle libera parches de seguridad para todos sus productos<img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer;" src="http://bp0.blogger.com/_fRmrOAuwnQ4/SAd1mt_1q1I/AAAAAAAAAD4/puXJg32_8n0/s320/alerta.JPG" alt="" id="BLOGGER_PHOTO_ID_5190246403610618706" border="0" /><br />Este 15 de abril fueron publicados parches para todos los productos Oracle, incluída la base de datos, la E-Business Suite, PeopleSoft, Siebel y otros productos.<br /><br />En cuanto a la base de datos, los parches abarcan todas las versiones desde la 9.0.1.5, y algunos de los bugs que se corrigen son vulnerabilidades graves que permiten explotar la base de datos aún sin autenticación. Este es un asunto de alerta máxima y no debe demorarse la decisión de actualizar el software. La instalación es sencilla y no toma mucho tiempo.<br /><br />El Application Server 9<i>i</i> Release 1, version 1.0.2.2 también presenta vulnerabilidades en el JInitiator 1.3.1.14, Enterprise Manager 1.0.2.2 y el Enterprise Portal 9.0.4.3. Estos agujeros de seguridad pueden ser explotados sin necesidad de usuario y password.<br /><br />Oracle recomienda fuertemente descargar estos patches (gratuitamente desde otn.oracle.com) y aplicarlos en todos los casos para evitar riesgos ya detectados.<br /><br /><span style="font-weight: bold;">Ver también:</span><br /><a href="http://www.oracle.com/technology/deploy/security/critical-patch-updates/cpuapr2008.html" target="_blank">Oracle Critical Patch Update Advisory - April 2008</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-86430979081499213952008-04-14T05:27:00.000-07:002008-04-16T09:18:29.751-07:00Sobre comentarios y cómo actualizar secuenciasCuando se realizan cargas de datos en un esquema existente y no se utilizan las secuencias para asignar los valores de llave primaria, se produce un desfasaje entre identificadores y secuencias. Este desfasaje provoca errores de clave primaria ya existente, ya que los valores retornados por <span style="font-style: italic;">secuencia.NEXTVAL</span>, pueden ya existir entre los datos recientemente cargados.<br /><br />Para sincronizar las secuencias con los identificadores de llave primaria, puede crearse un script en PL/SQL el cual recree cada secuencia con su valor inicial según el máximo identificador de la tabla correspondiente. Para poder realizarlo, necesitamos primero tener una correspondencia entre tabla y secuencia, y en Oracle esta dependencia no existe.<br /><br />Es una buena oportunidad para comenzar a adoptar la buena práctica de usar comentarios sobre los objetos. Muchas veces, en el proceso de creación de una base de datos, este paso es omitido y considerado innecesario. Supuestamente, los objetos "no necesitan describirse" fundamentandose que el diseño es lo suficientemente explicativo y que los creadores van a permanecer durante todo el proceso de desarrollo, por lo tanto no vale la pena perder el tiempo describiendo los objetos. ¿No suenan estas excusas a holgazanería?<br /><br />No se deben ignorar estas máximas: 1) el diseñador puede abandonar el proyecto y 2) las personas externas que miren nuestro modelo pueden no entenderlo.<br /><br />En este sentido, el responsable de base de datos debe velar porque el modelo quede totalmente documentado y que no dependa de las personas que lo crearon, en definitiva que sea lo más auto-explicativo posible.<br /><br />Alguien puede considerar suficiente que la base de datos quede documentada en un diagrama o en un documento de word, pero ningún esfuerzo es suficiente si podemos hacer un poquito más. Los comentarios sobre objetos son almacenados en el diccionario de datos, y van a ser exportados junto a los objetos cuando estos sean migrados. Sin necesidad de consultar diagramas ni documentos, un programador en apuros puede hacer un DESCRIBE sobre una tabla en SQLPLUS, y saber al instante donde encontrar el dato que busca. Los comentarios ayudan a disipar las dudas más rapido, sobre todo a los que no están empapados en el modelo.<br /><br />Ahora si, volviendo al tema inicial, ¿cómo relacionamos una tabla con su secuencia? Precisamente, colocando en el comentario de la columna, el nombre de la secuencia que se utiliza:<pre style="color: rgb(0, 153, 0);">SQL> COMMENT ON cuentas.id_cuenta IS 'CUENTA_SEQ'</pre>Acabamos de documentar que para la columna <span style="font-style: italic;">id_cuenta</span> de la tabla <span style="font-style: italic;">cuentas</span>, se debe utilizar la secuencia <span style="font-style: italic;">cuenta_seq</span>. Observar que el efecto de esta sentencia es únicamente documental, todavía puede utilizarse la secuencia de la forma que se desee. La ventaja es que ahora podemos consultar la tabla de diccionario dba_col_comments en nuestros scripts.<br /><br />A modo de nota, mencionamos que tambien se pueden comentar tablas y vistas, con esta sintaxis:<pre style="color: rgb(0, 153, 0);">SQL> COMMENT ON cuentas 'Tabla para el registro de cuentas corrientes personales'</pre>Finalmente, el bloque que actualiza las secuencias utilizando los comentarios sobre columnas.<pre><span style="color: rgb(0, 153, 0);">DECLARE</span><br /><span style="color: rgb(0, 153, 0);"> v_max NUMBER(10) := 0;</span><br /><span style="color: rgb(0, 153, 0);"> v_desc varchar2(50);</span><br /><span style="color: rgb(0, 153, 0);">BEGIN</span><br /><br /><span style="color: rgb(0, 153, 0);"> FOR t IN (SELECT u.table_name,</span><br /><span style="color: rgb(0, 153, 0);"> u.column_name,</span><br /><span style="color: rgb(0, 153, 0);"> uc.comments secuencia</span><br /><span style="color: rgb(0, 153, 0);"> FROM user_tab_columns u,</span><br /><span style="color: rgb(0, 153, 0);"> user_col_comments uc</span><br /><span style="color: rgb(0, 153, 0);"> WHERE u.table_name = uc.table_name</span><br /><span style="color: rgb(0, 153, 0);"> AND u.column_name = uc.column_name</span><span style="color: rgb(0, 153, 0);"></span><br /><span style="color: rgb(0, 153, 0);"> AND u.column_name LIKE 'ID_%')</span><br /><span style="color: rgb(0, 153, 0);"> LOOP</span><br /><span style="color: rgb(0, 153, 0);"> EXECUTE IMMEDIATE 'SELECT MAX(' || t.column_name || ') FROM ' ||</span><br /><span style="color: rgb(0, 153, 0);">t.table_name INTO v_max;</span><br /><br /><span style="color: rgb(0, 153, 0);"> dbms_output.put_line('Eliminando secuencia '|| t.secuencia||'...');</span><br /><span style="color: rgb(0, 153, 0);"> v_desc := 'DROP SEQUENCE '|| t.secuencia;</span><br /><span style="color: rgb(0, 153, 0);"> EXECUTE IMMEDIATE 'DROP SEQUENCE '|| t.secuencia;</span><br /><br /><span style="color: rgb(0, 153, 0);"> EXECUTE IMMEDIATE 'CREATE SEQUENCE '|| t.secuencia ||' INCREMENT BY</span><br /><span style="color: rgb(0, 153, 0);">1 START WITH '|| (NVL(v_max,0) + 1);</span><br /><span style="color: rgb(0, 153, 0);"> dbms_output.put_line('Secuencia '|| t.secuencia||' recreada.');</span><br /><br /><span style="color: rgb(0, 153, 0);"> END LOOP;</span><br /><span style="color: rgb(0, 153, 0);">END;</span><br /></pre>Se requiere el permiso CREATE SEQUENCE por parte del ejecutor de este script.lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-29080386266799535202008-03-28T13:31:00.000-07:002008-04-01T14:29:28.602-07:00Más control sobre los LOOPS en 11gEn PL/SQL podemos salir de un LOOP con la cláusula EXIT. Típicamente la usamos para la condición de salida de un loop:<pre><span style="color: rgb(0, 153, 0);"> BEGIN</span><br /><span style="color: rgb(0, 153, 0);"> LOOP</span><br /><span style="color: rgb(0, 153, 0);"> ... </span><br /><span style="color: rgb(0, 153, 0);"> <span style="color: rgb(51, 51, 255);">EXIT</span> WHEN </span><span style="font-style: italic; color: rgb(0, 153, 0);">condición</span><span style="color: rgb(0, 153, 0);"> ;</span><br /><span style="color: rgb(0, 153, 0);"> END LOOP;</span><br /><span style="color: rgb(0, 153, 0);"> -- el </span><span style="font-style: italic; color: rgb(0, 153, 0);">control sigue aquí</span><br /><span style="color: rgb(0, 153, 0);"> ...</span><br /><span style="color: rgb(0, 153, 0);"> END;</span></pre>Ahora la versión 11g agrega un nivel intermedio: CONTINUE. Con esta sentencia, podemos cancelar la iteración del loop actual y pasar a la siguiente, sin abortar el loop. Puede usarse simplemente <span style="color: rgb(51, 51, 255);">CONTINUE</span> o <span style="color: rgb(51, 51, 255);">CONTINUE WHEN</span>. En este último caso podremos evitar el uso de la sentencia IF para controlar la condición de salida de la iteración.<br /><pre><span style="color: rgb(0, 153, 0);"> BEGIN</span><br /><span style="color: rgb(0, 153, 0);"> LOOP</span><br /><span style="color: rgb(0, 153, 0);"></span><span style="color: rgb(0, 153, 0);"> ...<br /> <span style="color: rgb(51, 51, 255);">CONTINUE WHEN</span> condicion; --vuelve al comienzo del loop<br /><br /> -- si condicion=true, esta parte no se ejecuta<br /> ...<br /></span><span style="color: rgb(0, 153, 0);"> END LOOP;</span><br /><span style="color: rgb(0, 153, 0);"></span><span style="font-style: italic; color: rgb(0, 153, 0);"></span><span style="color: rgb(0, 153, 0);"> END;</span></pre>Los programadores conservadores se preguntarán para que necesitamos esta sentencia, despues de todo siempre nos hemos arreglado con la cláusula IF-THEN-ELSE de modo de avanzar a la siguiente iteración. Es cierto, no lo necesitamos para hacer cosas que antes no podíamos, pero sí para simplificar el código y facilitar la lectura.<br /><br /><span style="font-weight: bold;">Un ejemplo con CONTINUE</span><br /><br />Hay un caso típico donde CONTINUE puede beneficiarnos en la claridad de código para el seguimiento de la lógica: cuando tenemos que hacer múltiples validaciones antes de procesar un registro.<br /><br />Imaginemos nuestro loop actual (muy simplificado), en donde para cada registro hacemos una serie de validaciones antes de procesarlo. Si una validación falla, debemos pasar al siguiente registro. Para resolver esta lógica, debemos crear una estructura anidada de IFs, ya que debo evitar que se ejecute el resto del loop si una de las validaciones falla.<pre style="color: rgb(0, 153, 0);">LOOP<br /> EXIT WHEN <span style="font-style: italic;">condicion</span>;<br /> i:=i+1;<br /><br /> a := valida_1(T(i));<br /> IF (a) THEN<br /> b := valida_2(a);<br /> IF (b) THEN<br /> ....<br /> IF (c) THEN<br /><br /> .....<br /> procesoRegistro(T(i));<br /> .....<br /> END IF;<br /> ELSE<br /> ....<br /> END IF;<br /> END IF;<br /><br />END LOOP;</pre>Notemos como los múltiples IF anidados complican el seguimiento del flujo. Muchas veces nos hemos visto complicados al tener que agregar una nueva validación, ya que hay que agregar un nuevo IF al loop, y debo mantener consistentes los correspondientes END IF que deshacen el nivel de anidación.<br /><br />Ahora veamos como puede verse mejorado con CONTINUE-WHEN:<pre><span style="color: rgb(0, 153, 0);">LOOP</span><br /><span style="color: rgb(0, 153, 0);"> EXIT WHEN <span style="font-style: italic;">condicion</span>;</span><br /><span style="color: rgb(0, 153, 0);"> i:=i+1;</span><br /><span style="color: rgb(0, 153, 0);"> </span><br /><span style="color: rgb(0, 153, 0);"> a := valida_1(T(i));</span><br /><span style="color: rgb(0, 153, 0);"> CONTINUE WHEN NOT a;</span><br /><br /><span style="color: rgb(0, 153, 0);"> b := valida_2(a);</span><br /><span style="color: rgb(0, 153, 0);"> CONTINUE WHEN NOT b;</span><br /><br /><span style="color: rgb(0, 153, 0);"> ....</span><br /><br /><span style="color: rgb(0, 153, 0);"> procesoRegistro(T(i));</span><br /><span style="color: rgb(0, 153, 0);"> </span><br /><span style="color: rgb(0, 153, 0);">END LOOP;</span></pre>La mejoría es evidente, conseguimos disminuir la anidación (a 1) y evitar el uso repetitivo de IF/END-IF, reduciendo la posibilidad de cometer errores de lógica al momento de modificar.<br /><br />CONTINUE es un recurso que evidentemente solo puede utilizarse en Oracle 11g, sin embargo, siendo conscientes que nuestro código no va a compilar en versiones anteriores, es una práctica que contribuye a la legibilidad del código, con todos los beneficios que eso trae aparejado.<br /><br /><span style="font-weight: bold;">Ver también sobre 11g</span><br /><a href="http://oraclenotepad.blogspot.com/2008/03/11g-y-sus-ndices-invisibles.html">11g y sus índices invisibles</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-5654127756760127702008-03-26T11:36:00.000-07:002008-03-31T15:41:18.759-07:00NATURAL JOIN antinaturalEl NATURAL JOIN en Oracle tiene un bug. Las versiones 9i y 10g retornan extraños productos cartesianos cuando ejecutamos joins naturales con varias tablas a la vez.<br /><br /><strong>El join natural</strong><br />Un natural join toma las columnas de igual nombre entre dos tablas y las utiliza para realizar un join. ¿Cuál es el beneficio? No hay que nombrar las columnas en el join.<br />Si bien puede sonar fantástico el ahorrarnos de escribir las columnas en el JOIN, lo cierto es que el natural join introduce algunos riesgos que veremos más adelante.<br /><br /><strong>Un caso de prueba</strong><br />Tengo tablas que describen clientes, órdenes, y libros. Deseo realizar una consulta joineandolas a todas.<br /><br /><pre><span style="color:#009900;"><p>SQL> desc customers<br />Name Type Nullable Default Comments<br />--------- ------------ -------- ------- --------<br /><span style="color:#ff0000;">CUSTOMER#</span> NUMBER(4) <br />LASTNAME VARCHAR2(10) Y <br />FIRSTNAME VARCHAR2(10) Y <br />ADDRESS VARCHAR2(20) Y</p><br /><br /><p>SQL> desc orders<br />Name Type Nullable Default Comments<br />--------- --------- -------- ------- --------<br /><span style="color:#3333ff;">ORDER#</span> NUMBER(4)<br /><span style="color:#ff0000;">CUSTOMER#</span> NUMBER(4) Y<br />ORDERDATE DATE Y</p><br /><br /><p>SQL> desc orderitems<br />Name Type Nullable Default Comments<br />-------- ------------ -------- ------- --------<br /><span style="color:#3333ff;">ORDER#</span> NUMBER(4)<br /><span style="color:#ff9900;">ISBN</span> VARCHAR2(10)<br />QUANTITY NUMBER(3) Y</p><br /><br /><p>SQL> desc books<br />Name Type Nullable Default Comments<br />------- ------------ -------- ------- --------<br /><span style="color:#ff9900;">ISBN</span> VARCHAR2(10)<br />TITLE VARCHAR2(30) Y<br />PUBDATE DATE Y<br />COST NUMBER(5,2) Y</p></span> </pre><span style="font-family:Courier New;"></span><p>A considerar:</p><ul><li>Todas las tablas tienen primary key, foreign keys, índices y estadísticas.</li><li>El JOIN que deseo hacer puede verse mirando las cuatro tablas desde arriba hacia abajo.</li><li>Las columnas con igual color determinan las llaves del JOIN.</li><li>La consulta que quiero realizar es --> Cuáles son los libros que ordenó JAKE LUCAS?</li></ul><strong>Resultado con INNER JOIN</strong><br /><br /><pre><span style="color:#009900;"><p>SQL> SELECT isbn, title<br /> 2 FROM ((customers INNER JOIN orders USING (customer#))<br /> 3 INNER JOIN orderitems USING (order#))<br /> 4 INNER JOIN books USING (isbn)<br /> 5 WHERE firstname='JAKE' AND lastname='LUCAS';</p><br /><br /><p>ISBN TITLE<br />---------- ------------------------------<br />2491748320 PAINLESS CHILD-REARING<br />9247381001 HOW TO MANAGE THE MANAGER<br />2491748320 PAINLESS CHILD-REARING</p><br /><br /><p>3 rows selected.</p></span></pre><p>Es el resultado esperado. El cliente JAKE LUCAS ordenó los 3 libros que aparecen. ¿Qué sucede si ejecutamos la misma consulta pero con NATURAL JOIN?</p><strong>Resultado con NATURAL JOIN</strong><br /><pre><span style="color:#009900;"><p>SQL> SELECT isbn, title<br /> 2 FROM customers NATURAL JOIN orders<br /> 3 NATURAL JOIN orderitems<br /> 4 NATURAL JOIN books<br /> 5 WHERE firstname='JAKE' AND lastname='LUCAS';</p><br /><br /><p>ISBN TITLE<br />---------- ------------------------------<br />1059831198 BODYBUILD IN 10 MINUTES A DAY<br />0401140733 REVENGE OF MICKEY<br />0401140733 REVENGE OF MICKEY<br />0401140733 REVENGE OF MICKEY<br />0401140733 REVENGE OF MICKEY<br />8843172113 DATABASE IMPLEMENTATION<br />8843172113 DATABASE IMPLEMENTATION<br />8843172113 DATABASE IMPLEMENTATION<br />3437212490 COOKING WITH MUSHROOMS<br />3437212490 COOKING WITH MUSHROOMS<br />3437212490 COOKING WITH MUSHROOMS<br />3957136468 HOLY GRAIL OF ORACLE<br />1915762492 HANDCRANKED COMPUTERS<br />...</p><br /><br /><p>64 rows selected.</p></span></pre>Retorna 64 filas, algo inquietante. Se estará realizando un producto cartesiano? Veamos el plan de ejecución:<br /><pre><span style="color:#009900;"><p>SQL> set autotrace traceonly explain</p><br /><p>SQL> SELECT isbn, title<br /> 2 FROM customers NATURAL JOIN orders<br /> 3 NATURAL JOIN orderitems<br /> 4 NATURAL JOIN books<br /> 5 WHERE firstname='JAKE' AND lastname='LUCAS';</p><br /><br />Execution Plan<br />----------------------------------------------------------<br /> 0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=9 Card=3 Bytes=204)<br /> 1 0 NESTED LOOPS (Cost=9 Card=3 Bytes=204)<br /> 2 1 MERGE JOIN (CARTESIAN) (Cost=9 Card=1 Bytes=57)<br /> 3 2 MERGE JOIN (Cost=6 Card=1 Bytes=22)<br /> 4 3 TABLE ACCESS (BY INDEX ROWID) OF 'CUSTOMERS' (TABLE) (Cost=2 Card=1 Bytes=18)<br /> 5 4 INDEX (FULL SCAN) OF 'SYS_C00138991' (INDEX (UNIQUE)) (Cost=1 Card=20)<br /> 6 3 SORT (JOIN) (Cost=4 Card=21 Bytes=84)<br /> 7 6 TABLE ACCESS (FULL) OF 'ORDERS' (TABLE) (Cost=3 Card=21 Bytes=84)<br /> 8 2 BUFFER (SORT) (Cost=6 Card=14 Bytes=490)<br /> 9 8 TABLE ACCESS (FULL) OF 'BOOKS' (TABLE) (Cost=3 Card=14 Bytes=490)<br /> 10 1 INDEX (RANGE SCAN) OF 'IX_BOOKS' (INDEX) (Cost=0 Card=2 Bytes=22)</span></pre>Efectivamente, en la línea 2 del plan aparece un MERGE JOIN (CARTESIAN) lo cual nos indica que un producto cartesiano se llevó a cabo. Esto no debería suceder, ya que las columnas para vincular las tablas con NATURAL JOIN existen y fueron verificadas en la primera consulta, usando la cláusula USING del INNER JOIN.<br /><br /><strong>Resultado con NATURAL JOIN variando las columnas del SELECT </strong><br />Si bien lo anterior no era normal, es aún más desconcertante lo que obtenemos si variamos las columnas del select: ¡La cantidad de registros también varía!<br /><pre><span style="color:#009900;"><p>SQL> SELECT #order<br /> 2 FROM customers NATURAL JOIN orders<br /> 3 NATURAL JOIN orderitems<br /> 4 NATURAL JOIN books<br /> 5 WHERE firstname='JAKE' AND lastname='LUCAS';</p><br /><br /><p>42 rows selected.</p><br /><br /><p>SQL> SELECT isbn<br /> 2 FROM customers NATURAL JOIN orders<br /> 3 NATURAL JOIN orderitems<br /> 4 NATURAL JOIN books<br /> 5 WHERE firstname='JAKE' AND lastname='LUCAS';</p><br /><br /><p>64 rows selected.</p><br /><br /><p>SQL> SELECT #customer<br /> 2 FROM customers NATURAL JOIN orders<br /> 3 NATURAL JOIN orderitems<br /> 4 NATURAL JOIN books<br /> 5 WHERE firstname='JAKE' AND lastname='LUCAS';</p><br /><br /><p>896 rows selected.</p><br /><br /><p>SQL> SELECT *<br /> 2 FROM customers NATURAL JOIN orders<br /> 3 NATURAL JOIN orderitems<br /> 4 NATURAL JOIN books<br /> 5 WHERE firstname='JAKE' AND lastname='LUCAS';</p><br /><br /><p>3 rows selected.</p></span></pre>Unicamente cuando seleccionamos todas las columnas (*) obtenemos el resultado correcto. ¡Esto debería desalentar a cualquiera a usar NATURAL JOIN!<br /><br /><strong>Bug reconocido<br /></strong>La verdad es, que este bug fue reconocido por Oracle en Metalink, el soporte oficial. Lo que se informa es, que este bug se cree que será eliminado a partir de la versión 11.2. Habrá que esperar que salga el próximo release de 11g? Yo no me molestaría.<br /><br /><strong>Nunca usar NATURAL JOIN</strong><br />El NATURAL JOIN es una sentencia que está en Oracle desde la versión 9i en un esfuerzo por cumplir con el estándar ANSI SQL. Dentro del mismo SQL, el NATURAL JOIN es uno de esos accesorios inútiles, de los cuales podemos prescindir totalmente (de hecho deberíamos).<br /><br />Más allá de que no funcione correctamente en este RDBMS, imaginemos que si lo hace y que en Oracle 11g R2 tenemos este bug solucionado: el NATURAL JOIN funciona de maravillas.<br />Ahora, que sucede con mi procedimiento si el día de mañana agregan una columna de nombre <em>quantity</em> a mi tabla <em>books</em>?<br />Como <em>orderitems</em> ya tiene una columna <em>quantity</em>, se van a retornar resultados inesperados, ya que la tablas se van a combinar por <em>isbn</em> y <em>quantity </em>(lo cual no tiene sentido). Un cambio leve en las estructuras modifica el comportamiento de mi JOIN, y en este caso no se trata de un bug sino que sería bastante lógico que lo hiciera.<br /><br />Usar NATURAL JOIN también nos quita claridad en nuestro código: cada vez que lo encontramos en una consulta compleja perdemos tiempo buscando en las estructuras cuáles son las columnas que coinciden.<br /><br />Lo mejor en todos los casos es usar INNER JOIN o la notación original de Oracle por medio de comparadores en el where. Con ellas el programador explicita cuáles son las columnas que deben entrar en juego, sin dejar nada librado al azar.<br /><br />En conclusión, usar NATURAL JOIN es totalmente desaconsejado en SQL, ya que nuestras consultas dependen del nombrado de columnas en las tablas relacionadas. Un cambio insignificante como agregar una columna a una tabla, podría hacer dejar de funcionar mi aplicación.lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-74853809322279863782008-03-24T14:51:00.000-07:002008-03-27T11:00:34.591-07:00Compilación condicional en 9iLa compilación condicional permite indicarle al compilador los fragmentos de código que debe compilar en nuestro código PL/SQL, según las condiciones que el programador indique utilizando directivas especiales.<br />Ahora, si bien la compilación condicional fue presentada con Oracle 10g R2, es también posible contar con esta característica en 9i y 10g R1, gracias a la aplicación de patches sobre la base de datos.<br /><br /><strong>En Oracle 10g y superiores</strong><br />En Oracle 10g ya viene incorporada a partir de la versión 10.1.0.4 y activada por defecto, por lo tanto no debemos hacer nada para comenzar a utilizarla.<br /><br /><strong>En Oracle 9i</strong><br />Para contar con ella en 9i debemos primero tener instalado el patch 9.2.0.6 o superiores. Luego debemos setear el parámetro indocumentado <span style="font-family:courier new;color:#3333ff;">_plsql_conditional_compilation=TRUE</span>. Este parámetro es del sistema y no es dinámico, por lo que el DBA deberá incluirlo en el archivo de parámetros y reiniciar la instancia.<br /><br /><strong>En Oracle 8i e inferiores</strong><br />No es soportada.<br /><br />Una aplicación de la compilación condicional bastante práctica y recomendable es la de hacer que nuestro código PL/SQL compile en cualquier versión, utilizando los features propios de cada release cuando es posible, todo en el mismo código. Esta buena práctica tiene dos objetivos: el primero es hacer que nuestro paquete sea reutilizable (por ejemplo un paquete de funciones de uso general), y el segundo que nuestro código no genere un impacto al momento de migrar de versión.<br /><br /><strong>Un ejemplo de compilación condicional</strong><br />En este ejemplo, vamos a realizar un procedimiento para hacer un spool de determinadas cuentas que cumplen con un patrón en su código.<br />La compilación condicional va a entrar en juego con la siguiente lógica:<br />-Si la base es 9i, ejecuta SQL con invocaciones a SUBSTR para verificar el patrón<br />-Si la base es 10g, ejecuta SQL con expresiones regulares ya que es más performante<br /><br />Primero compilamos la especificación de nuestro paquete, que no tiene nada de especial:<br /><br /><pre><span style="color:#009900;">CREATE OR REPLACE PACKAGE pack_cuentas<br />IS<br /> PROCEDURE list_cuentas (p_max IN VARCHAR2);<br />END pack_cuentas;<br />/</span></pre>Luego compilamos el body, donde está la diferencia:<pre><span style="color:#009900;">CREATE OR REPLACE PACKAGE BODY pack_cuentas<br />IS<br /> PROCEDURE spool_cuentas (p_max IN VARCHAR2)<br /> IS<br /> BEGIN<br /> <span style="color:#3333ff;">$IF DBMS_DB_VERSION.VER_LE_9<br /> $THEN</span><br /> DECLARE<br /> v_pattern VARCHAR2(20) := '00[0-'||p_max||']-[0-9]+A';<br /> BEGIN<br /> FOR cue IN (SELECT cod_cuenta FROM cuentas<br /> WHERE activo='Y' AND REGEXP_LIKE (cod_cuenta,v_pattern))<br /> LOOP<br /> dbms_output.put_line(cue.cod_cuenta);<br /> END LOOP;<br /> END;<br /> <span style="color:#3333ff;">$ELSE</span><br /> FOR cue IN (SELECT cod_cuenta FROM cuentas<br /> WHERE activo='Y' AND SUBSTR(cod_cuenta,1,2)='00'<br /> AND SUBSTR(cod_cuenta,3,1) IN ('0','1','2','3')<br /> AND SUBSTR(cod_cuenta,LENGTH(cod_cuenta),-1)='A')<br /> LOOP<br /> dbms_output.put_line(cue.cod_cuenta);<br /> END LOOP;<br /> <span style="color:#3333ff;">$END</span><br /> END spool_cuentas;<br />END pack_cuentas;<br />/</span><br /></pre>Las directivas especiales $IF $THEN $END hacen el truco. Observar que este mismo código se compila en Oracle 9i, donde la función REGEXP_LIKE retornaría un error de compilación, sin embargo el fragmento de código que no corresponde a la versión ni siquiera es analizado por el compilador.<br />Para poder diferenciar entre las versiones 9i, 9iR1, 9iR2, 10g, 10gR1 y 10gR2 contamos con el paquete DBMS_DB_VERSION, el cual incluye constantes para cada versión, como la que usé en el ejemplo. En el ejemplo usamos la constante <em>ver_le_9</em> que implica 'menor o igual a 9i', pero también tenemos para indicar menor o igual a un release como por ejemplo <em>ver_le_9_2</em>.<pre><span style="color:#009900;">CREATE OR REPLACE package dbms_db_version is<br /> version constant pls_integer := 10; -- RDBMS version number<br /> release constant pls_integer := 2; -- RDBMS release number<br /> ver_le_9_1 constant boolean := FALSE;<br /> ver_le_9_2 constant boolean := FALSE;<br /> ver_le_9 constant boolean := FALSE;<br /> ver_le_10_1 constant boolean := FALSE;<br /> ver_le_10_2 constant boolean := TRUE;<br /> ver_le_10 constant boolean := TRUE;<br />end dbms_db_version;<br />/</span></pre>Ahora podremos arreglar nuestro viejo código para que sea más eficiente en las versiones siguientes y que no tengamos que hacer cambios al momento de migrar, en definitiva adelantarnos y minimizar el retrabajo en el futuro.lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-4641197480108986352008-03-19T13:47:00.000-07:002008-03-20T07:08:40.866-07:00Cómo verificar archivos y directorios en ShellEstos son mis recursos favoritos cuando necesito comandos en Unix para verificar la existencia de carpetas, saber si existen archivos o si están vacíos. Acudo a ellos comúnmente en scripts de respaldo de bases de datos, antes de invocar a RMAN o EXP bajo Linux y después de que se realizan los mismos para verificar los resultados.<br />También recurro al tamaño en bytes de archivos o de todos los archivos de un directorio, especialmente para generar reportes o logs en nuestros scripts.<br /><br /><strong>Verificar si una carpeta existe<br /></strong><pre><span style="font-family:courier new;color:#009900;"> if [ -d <em><span style="color:#3333ff;">archivo</span></em> <carpeta>]<br /> then<br /> # Codigo si existe<br /> echo existe<br /> else<br /> # codigo si no existe<br /> echo no existe<br /> fi</span></pre><strong>Verificar si un archivo de salida no esta vacío<br /></strong><pre><span style="font-family:courier new;color:#009900;"> if [ -s <span style="color:#3333ff;"><em>archivo</em></span> <archivo>]<br /> then<br /> # Codigo si existe y no es vacio<br /> ...<br /> else<br /> # codigo si no existe o es vacio<br /> ...<br /> fi</span></pre><strong>Verificar si un archivo es escribible<br /></strong><pre><span style="font-family:courier new;color:#009900;"> if [ -w <span style="color:#3333ff;"><em>archivo</em></span> <archivo>]<br /> then<br /> # Codigo si existe y es escribible<br /> ...<br /> else<br /> # codigo si no existe o no es escribible<br /> ...<br /> fi</span></pre><strong>Verificar la integridad de un archivo comparando checksum<br /></strong><pre><span style="font-family:courier new;color:#009900;"> check1=`md5sum <span style="color:#3333ff;"><em>archivo_origen</em></span> | cut -d' ' -f1`<br /><br /> check2=`md5sum <span style="color:#3333ff;"><em>archivo_destino</em></span> | cut -d' ' -f1`<br /><br /> if [ $check1 -eq $check2 ]<br /> then<br /> # Integridad del archivo correcta<br /> ...<br /> else<br /> # Error de checksum<br /> ...<br /> fi</span></pre><strong>Obtener el tamaño de un archivo en bytes<br /></strong><pre><span style="font-family:courier new;color:#009900;"> x=`du -m <span style="color:#3333ff;"><em>archivo</em></span> | cut -f1`</span></pre><strong>Sumar la cantidad total de bytes en un directorio<br /></strong><pre><span style="font-family:courier new;color:#009900;"> suma=0<br /> for arch in *<br /> do<br /> if [ ! -d $arch ]<br /> then<br /> tamano=`du -b $arch | cut -f1`<br /> let suma=$suma+$tamano<br /> fi<br /> done<br /> echo $suma </span></pre>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-30978201175787310292008-03-17T08:49:00.000-07:002008-03-31T15:23:25.253-07:00Error ORA-942 while gathering statisticsEste error puede suceder cuando un usuario no DBA quiere setear AUTOTRACE en su sesión.<br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >SQL> set autotrace traceonly statistics;</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >Error ORA-942 while gathering statistics</span> <div style="COLOR: rgb(0,153,0); FONT-FAMILY: courier new">SP2-0611: Error enabling STATISTICS report</div><br /><br />Si es un usuario con permisos limitados, entonces es de esperar que no tenga permisos de acceso sobre las vistas que se necesitan para leer las estadísticas generadas por AUTOTRACE. Se requieren permisos explícitos de lectura sobre las vistas <span style="font-family:courier new;">v$_session</span>, <span style="font-family:courier new;">v_$sesstat</span> y <span style="font-family:courier new;">v_$statname</span>.<br /><br />Para facilitar la implementación de estos permisos, el administrador cuenta con un script para crear un rol especial y así poder extenderlo a los usuarios.<br /><br />Script:<br /><span style="FONT-STYLE: italic">$ORACLE_HOME/sqlplus/admin/PLUSTRCE.SQL</span><br /><br /><br /><span style="FONT-WEIGHT: bold">Cómo hacer para que un usuario pueda usar AUTOTRACE</span><br /><br />1. En el sistema operativo, posicionarse sobre el directorio con el script.<br /><pre> <span style="COLOR: rgb(0,153,0)">lfer@linux> cd $ORACLE_HOME/sqlplus/admin</span><span style="FONT-STYLE: italic"><br /></span></pre>2. Loguearse a SQL*plus con usuario 'AS SYSDBA'<br /><br />3. Ejecutar en SQL*plus:<br /><pre> <span style="COLOR: rgb(0,153,0)">SQL> @PLUSTRCE.SQL</span><br /></pre>4. Otorgar el rol PLUSTRACE a todos los usuarios que deseen usar AUTOTRACE:<br /><pre> <span style="COLOR: rgb(0,153,0)">grant PLUSTRACE to </span><span style="COLOR: rgb(51,51,255)">USUARIO</span>;</pre><strong>Ver también:</strong><br /><a href="http://download.oracle.com/docs/cd/B19306_01/server.102/b14357/ch8.htm#i1037226" target="_blank">SQLPlus Users Guide and Reference 10g sobre AUTOTRACE</a><br /><a href="http://oraclenotepad.blogspot.com/2008/03/error-ora-942-while-gathering.html">Error ORA-942 while gathering statistics</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-75478137636064101102008-03-12T10:52:00.000-07:002008-04-17T08:03:34.254-07:00Cómo detectar caracteres extraños o no imprimiblesMuchas veces pasa que en las cargas de datos aparecen caracteres de control como ^M o símbolos ilegibles, y no es deseable que estos queden almacenados entre los datos.<br />¿Cómo detectar si existen?<br />Es sencillo si contamos con expresiones regulares en SQL como en 10g, pero en 9i igualmente podemos crear una función que recorra cada uno de los caracteres y verifique si pertenece al juego de caracteres 'raros'.<br />Mostraré una sencilla implementación compatible con todas las versiones de Oracle, pero puede considerarse utilizar compilación condicional para utilizar expresiones regulares cuando sea posible.<br />El código:<pre><span style="color: rgb(0, 153, 0);font-family:courier new;" >CREATE OR REPLACE FUNCTION buscar_no_imprimible(v_cadena VARCHAR2) </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >RETURN BOOLEAN IS<br /></span><span style="color: rgb(0, 153, 0);font-family:courier new;" > v_ret BOOLEAN := FALSE; </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > v_iter NUMBER := 1; </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > v_ascii_min NUMBER := 33; </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > v_ascii_max NUMBER := 126;</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >BEGIN</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > WHILE (v_iter <= LENGTH(v_cadena))</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > LOOP</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > IF ASCII(SUBSTR(v_cadena,v_iter,1))</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > NOT BETWEEN v_ascii_min AND v_ascii_max THEN</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > v_ret := TRUE;<br /> EXIT; </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > END IF; </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > v_iter := v_iter + 1; </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > END LOOP; </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > RETURN(v_ret);</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >END buscar_no_imprimible;</span><br /><span style="color: rgb(0, 153, 0);font-family:Courier New;" ></span></pre>Esta función booleana retorna TRUE si la cadena contiene un caracter no imprimible, FALSE en caso contrario. Una rápida mirada en una tabla <a href="http://www.techonthenet.com/ascii/chart.php" target="_blank">ASCII</a> nos dice que el rango de caracteres válidos se encuentra entre las representaciones decimales 33 y 126.<br /><br />Si se desean considerar caracteres del <a href="http://www.cdrummond.qc.ca/cegep/informat/Professeurs/Alain/files/ascii.htm" target="_blank">ASCII extendido</a> (por ejemplo vocales con acentos), bastará ajustar el procedimiento para incluir el conjunto de caracteres deseado.<br /><br /><strong>Ver también:</strong><br /><a href="http://oraclenotepad.blogspot.com/2007/10/acentos-del-idioma-espaol.html" target="_blank">Como eliminar acentos del español</a><br /><a href="http://oraclenotepad.blogspot.com/2007/09/limpiando-cdigo-ddl.html">Limpiando código DDL</a><br /><a href="http://oraclenotepad.blogspot.com/2008/03/compilacin-condicional-tambin-en-9i.html">Compilación condicional en 9i</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-13720735843812817632008-03-10T13:40:00.000-07:002008-03-24T10:02:46.406-07:0011g y sus Indices InvisiblesLos índices invisibles, disponibles a partir de Oracle Database 11g, según la documentación oficial, son una interesante alternativa al borrado de índices o seteo como "unusable". Pero, son realmente una alternativa?<br /><br />Según la información disponible, un índice invisible será ignorado por el optimizador al momento de evaluar los planes de ejecución, excepto que alteremos el valor de sesión (o sistema) <span style="font-family:courier new;">OPTIMIZER_USE_INVISIBLE_INDEXES</span>, pero en todo momento el estado del índice será válido. Esto nos brinda múltiples beneficios.<br /><br />Un índice <em>unusable</em> es una forma sutil de dejar un índice "fuera de combate" temporalmente en TODAS las sesiones. Un índice <em>invisible</em> en cambio es selectivo: podemos verlo (y usarlo) en una sesión mientras en el resto hacer como si no existiera.<br /><br />Un índice invisible puede volverse visible sin costo alguno, con una simple alteración del mismo: <span style="COLOR: rgb(0,153,0);font-family:courier new;" >ALTER INDEX mi_indice VISIBLE</span>. Un índice unusable en cambio deberá ser reconstruído, ya que las actualizaciones de datos sobre la tabla no fueron reflejadas mientras el índice estuvo en ese estado. Los índices invisibles SIEMPRE son mantenidos por Oracle con cada sentencia DML.<br /><br />Con estas afirmaciones, podemos concluir que los índices invisibles llegan para sustituir el estado unusable?<br />-No! El propósito es completamente diferente. Los índices invisibles evitarán, por ejemplo, que tengamos que 'inutilizar' índices para poder realizar performance tunning, pero en ciertos casos particulares seguiremos necesitando del estado unusable.<br /><br />Cuando tengamos que realizar una carga masiva de datos, tendremos que alterar el índice como 'unusable' para realizar la carga eficientemente, ya que la invisibilidad de índices de 11g tiene efecto sobre las consultas SELECT y no sobre las demás sentencias DML. El índice invisible continúa aportando overhead en cada modificación de datos.<br /><br />La clave de este novedoso estado está en el testeo de consultas. Por ejemplo, podremos probar cómo se comporta un SELECT con determinado índice, sin que el ambiente en el cual lo estoy probando se entere. Creo un índice invisible, seteo el valor de <span style="font-family:courier new;">OPTIMIZER_USE_INVISIBLE_INDEXES</span> como true, y testeo mi aplicación sin efectos colaterales.<br /><br />Otro beneficio, razonando en forma opuesta: ¿Cómo funcionaría mi aplicación si determinado índice no existiera?<br />Alteramos el índice como 'invisible', seteamos en nuestra sesión <span style="font-family:courier new;">OPTIMIZER_USE_INVISIBLE_INDEXES = false</span>, y probamos. Los demás usuarios mientras tanto, continuarán utilizándolo normalmente.<br /><br />Los índices invisibles son un valor agregado a nuestra forma de trabajo, ya que nos proveen transparencia sobre el ambiente. Nos permiten realizar nuestras pruebas tranquilos de que nadie está siendo afectado.<br /><br /><strong>Ver también:</strong><br /><a href="http://download.oracle.com/docs/cd/B28359_01/server.111/b28310/indexes003.htm#BABDHCJD" target="_blank">Oracle Database Administrator's Guide 11g sobre índices invisibles</a><br /><a href="http://oraclenotepad.blogspot.com/2007/11/indices-condicionales.html">Indices condicionales</a><br /><a href="http://oraclenotepad.blogspot.com/2007/05/indices-cmo-y-cundo.html">Mis índices no funcionan!</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-33032110772344463762008-03-08T13:19:00.000-08:002008-04-05T03:51:29.648-07:00SOUNDEX en españolSOUNDEX, función que ha estado presente en Oracle desde tiempos remotos, es un clásico algoritmo fonético utilizado para indexar nombres propios, asociando palabras que suenen igual a pesar de ocasionales diferencias en su escritura.<br /><br />El algoritmo SOUNDEX fue concebido teniendo en cuenta la fonética del idioma inglés, por lo que hace inadecuado su uso en el castellano. Realizando adaptaciones propias del idioma español, implementé una versión en PL/SQL llamada SOUNDESP, la cual respeta la esencia del algoritmo original.<br /><br /><strong>Los pasos básicos son:</strong><br /><br /><ol><li>Retener la primera letra de la cadena. Tener en cuenta las letras dobles como CH y LL.<br /></li><li>Remover todas las ocurrencias de las letras siguientes a partir de la segunda posición: a, e, i, o, u, h, w, y (cuando suena como vocal i )</li><li>Asignar números a las siguientes letras (luego de la primera): <ul><li>b, f, p, v = 1</li><li>c, g, j, k, q, s, x, z = 2</li><li>d, t = 3</li><li>l = 4</li><li>m, n = 5</li><li>r = 6</li><li>ll, y, ch = 7<br /></li></ul></li><li>Si hay números consecutivos, dejar solamente uno en la serie.</li><li>Retornar los cuatro primeros caracteres, si son menos de cuatro completar con ceros.</li></ol><p>SOUNDESP es un proyecto abierto y es bienvenido cualquier comentario para mejorar su implementación.<br /><br /><br /></p><div style="TEXT-ALIGN: center"><img id="BLOGGER_PHOTO_ID_5175490114821023442" style="CURSOR: pointer" alt="" src="http://bp2.blogger.com/_fRmrOAuwnQ4/R9MI1qhz5tI/AAAAAAAAADI/-IEgBrozxZA/s320/code.jpg" /> <a href="http://apex.oracle.com/pls/otn/wwv_flow_file_mgr.get_file?p_security_group_id=7043734611203540596&p_fname=pkg_snd.pck" target="_blank">Descargar Código</a> <img id="BLOGGER_PHOTO_ID_5175489526410503874" style="CURSOR: pointer" alt="" src="http://bp1.blogger.com/_fRmrOAuwnQ4/R9MITahz5sI/AAAAAAAAADA/L9NY7wTm94I/s320/execute.jpg" border="0" /> <a href="http://apex.oracle.com/pls/otn/f?p=18527:1:2128667202552406:::::" target="_blank">Probar SOUNDESP</a><br /></div><div style="TEXT-ALIGN: center"><span style="font-size:78%;">NOTA: Para descargar el código correctamente, haga click derecho y elija Guardar destino<br /></span></div><br /><span style="FONT-WEIGHT: bold">Ver también:</span><br /><a href="http://en.wikipedia.org/wiki/Soundex" target="_blank">Wikipedia: Soundex</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-85274755546390669432008-02-27T11:36:00.000-08:002008-03-24T09:57:56.908-07:00Insertar o actualizar un único registro con MERGELa versión 9i de Oracle Database traía entre sus SQL features, la incorporación de una novedosa sentencia: MERGE (ahora parte del estándar ANSI SQL). Hasta ese momento, cuando se quería insertar/actualizar datos en una tabla no vacía, había que revisar si el registro ya existía para aplicar INSERT o UPDATE, manejándolo proceduralmente con algún lenguaje, por ejemplo PL/SQL. Con MERGE, podemos tener esta lógica en una única sentencia SQL, simplificando el código y haciendo la tarea más performante.<br /><br />La sintáxis de MERGE está pensada para que la fuente de datos que se va a insertar sea una tabla o una consulta, de esta manera:<br /><br /><span style="COLOR: rgb(0,153,0)"><span style="font-family:courier new;">MERGE INTO <em><span style="color:#3333ff;">[tabla_destino]</span><span style="COLOR: rgb(51,51,255)"><tabla_destino></tabla_destino></span></em></span></span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >USING (<span style="COLOR: rgb(51,51,255)"><em>[tabla o vista o consulta]</em></span>)</span><br /><span style="COLOR: rgb(0,153,0)"><span style="font-family:courier new;">ON <em>(<span style="COLOR: rgb(51,51,255)">[condición de existencia de registro]</span><span style="COLOR: rgb(51,51,255)"><condicion></condicion></span></em>)<br />WHEN MATCHED THEN <span style="COLOR: rgb(51,51,255)"><span style="FONT-STYLE: italic">[sentencia de actualización]</span><em><span style="COLOR: rgb(51,51,255)"><sentencia></sentencia></span></em></span></span></span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >WHEN NOT MATCHED THEN <span style="COLOR: rgb(51,51,255)"><span style="FONT-STYLE: italic">[sentencia de inserción]</span><em><span style="COLOR: rgb(51,51,255)"><sentencia></sentencia></span></em></span>;</span><br /><span style="COLOR: rgb(0,153,0)"></span><br /><span style="COLOR: rgb(0,0,0)">Hay veces en que los datos a incorporar no provienen de ninguna tabla, sino que se trata de un único registro enviado por una aplicación (por ejemplo a través de parámetros). </span>Una forma de poder aprovechar la sentencia MERGE en estos casos, es construir una consulta sobre la tabla DUAL que convenientemente contiene una única fila.<br /><br />Partiendo de una tabla <em>tabla_destino</em>, donde queremos hacer la actualización de datos:<br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >CREATE TABLE tabla_destino (a int, b varchar2(10));</span><br /><br />Si:<br /><ul><li>Nuestro registro a insertar es {<em>param_a</em>, <em>param_b</em>}</li><li>La condición de que el registro exista es la columna a </li><li>Lo que queremos actualizar en caso que el registro exista es la columna b</li></ul>...entonces usamos MERGE así:<br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" ><span style="COLOR: rgb(0,0,0)">1 </span>MERGE INTO tabla_destino td</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" ><span style="COLOR: rgb(0,0,0)">2 </span>USING (SELECT <span style="COLOR: rgb(51,51,255)">param_a</span> a FROM dual) d</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" ><span style="COLOR: rgb(0,0,0)">3 </span>ON (td.a = d.a)</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" ><span style="COLOR: rgb(0,0,0)">4 </span>WHEN MATCHED THEN UPDATE SET td.b = <span style="COLOR: rgb(51,51,255)">param_b</span></span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" ><span style="COLOR: rgb(0,0,0)">5 </span>WHEN NOT MATCHED THEN INSERT (td.a, td.b) VALUES (<span style="COLOR: rgb(51,51,255)">param_a</span>, <span style="COLOR: rgb(51,51,255)">param_b</span>);</span><br /><br />En la línea 2 (USING) seleccionamos el registro que participa en la condición que el registro exista.<br />En la línea 3 especificamos cual es la condición que se considera que el registro ya existe<br />Las líneas 4 y 5 actualizan o insertan el registro.<br /><br />Una forma simplificada y equivalente es la siguiente:<br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >MERGE INTO tabla_destino td</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >USING dual d</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >ON (td.a = <span style="COLOR: rgb(51,51,255)">param_a</span>)</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >WHEN MATCHED THEN UPDATE SET td.b = <span style="COLOR: rgb(51,51,255)">param_b</span></span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >WHEN NOT MATCHED THEN INSERT (td.a, td.b) VALUES (<span style="COLOR: rgb(51,51,255)">param_a</span>, <span style="COLOR: rgb(51,51,255)">param_b</span>);</span><br /><br />Se recomienda el uso de MERGE ya que es una única sentencia SQL y naturalmente está optimizada por el motor. Hay excepciones como es el caso de cargas masivas de datos con millones de registros donde la performance de MERGE se degrada. Allí existen otras alternativas más eficientes.<br /><br /><strong>Ver también:</strong><br /><a href="http://download.oracle.com/docs/cd/B10501_01/server.920/a96540/statements_915a.htm#SQLRF01606" target="_blank">Oracle 9i SQL Reference para MERGE</a><br /><a href="http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/statements_9016.htm#sthref9516" target="_blank">Oracle 10g SQL Reference para MERGE</a><br /><a href="http://oraclenotepad.blogspot.com/2007/10/update-condicional.html" target="_blank">Update condicional</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-2483886623303527832008-01-29T04:39:00.000-08:002008-04-17T08:46:44.970-07:00Patches críticos en enero para todos los productos OracleAcaban de ser liberados patches críticos que afectan a los principales productos de Oracle: Oracle Database (9i, 10g, 11g), Application Server (9, 10g R2 y R3), Collaboration Suite 10g, E-Bussiness Suite (11i y 12) y People Soft PeopleTools.<br /><br />Se recomienda fuertemente que estos patches sean aplicados, ya que reducen potencialmente las posibilidades de ataques recientemente descubiertos.<br /><br />En la Base de Datos se descubrieron 8 puntos vulnerables en los siguientes módulos: XML DB, Advanced Queuing, Oracle Spatial, Ultra-Search y en el propio CORE de 11g.<br />En el Application Server los fixes afectan al J-Initiator, BPEL, Forms y JDeveloper 10g, Internet Directory.<br />Como parte de la Collaboration Suite, Oracle Ultra-Search 10.1.2 es el único componente afectado.<br /><br /><span style="font-weight: bold;">Vea también:</span><br /><a href="http://www.oracle.com/technology/deploy/security/critical-patch-updates/cpujan2008.html" target="_blank">Oracle Critical Patch Update Advisory - January 2008</a><br /><a href="http://oraclenotepad.blogspot.com/2008/04/oracle-libera-parches-de-seguridad-para.html">Oracle Critical Patch Update Advisory - April 2008</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-54081048489813399902008-01-28T12:33:00.000-08:002008-03-10T16:58:37.118-07:00Sesiones de BúsquedaYa lo habiamos comentado hace algún tiempo, la consola de administración de Oracle 10g en español tiene algunos errores de traducción. Este es de los más feos que he visto.<br /><br />Si en algún momento buscan el listado de sesiones de usuario en la instancia, no esperen hallarla con un nombre coherente.<br /><br />Les doy una pista, en la version en inglés se llega a a través del link <span style="font-weight: bold;">Session Search</span>...<br /><br />Espera encontrar Búsqueda de Sesiones? Mal!, no realice la traducción correctamente. Hallará en su lugar <span style="font-weight: bold;"><span style="color: rgb(51, 51, 255);">Sesiones de Búsqueda</span>.</span> Una preciosura. Ahora ejecute una búqueda en blanco para obtener el listado.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp2.blogger.com/_fRmrOAuwnQ4/R55BcirY7pI/AAAAAAAAACQ/DzTTQECs1Us/s1600-h/sesionesBusqueda.JPG"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="http://bp2.blogger.com/_fRmrOAuwnQ4/R55BcirY7pI/AAAAAAAAACQ/DzTTQECs1Us/s320/sesionesBusqueda.JPG" alt="" id="BLOGGER_PHOTO_ID_5160634181614104210" border="0" /></a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-80077352369608075312007-12-26T06:06:00.000-08:002007-12-26T08:47:04.646-08:00Cadena de conexión con jdbc thinError al testear la conexión JDBC en JDeveloper 10g -><br /><br /><span style="color: rgb(255, 0, 0);">Test Failed: La dirección URL de Oracle especificada no es válida</span><br /><br />Un método que puede solucionar este frustrante mensaje es usar la cadena de conexión que incluye la descripción del servicio (tal como la colocaríamos en el archivo tnsnames.ora)<br /><br />jdbc:oracle:thin:@<span style="color: rgb(0, 0, 153);">(DESCRIPTION =</span><br /><span style="color: rgb(0, 0, 153);"> (ADDRESS = (PROTOCOL = TCP)(HOST = host)(PORT = puerto))</span><br /><span style="color: rgb(0, 0, 153);"> (CONNECT_DATA =</span><br /><span style="color: rgb(0, 0, 153);"> (SERVER = DEDICATED)</span><br /><span style="color: rgb(0, 0, 153);"> (SERVICE_NAME = base_de_datos)</span><br /><span style="color: rgb(0, 0, 153);"> )</span><br /><span style="color: rgb(0, 0, 153);"> )<br /><br /><span style="color: rgb(0, 0, 0);">Si todavía no funciona es recomendable probar la conexión a través de un cliente Oracle, configurando el archivo tnsnames.ora. Al momento que esta conexión logre conectarnos, solo resta reemplazar en la cadena de conexión la misma descripción del servicio del tnsnames (arriba en azul). </span><br /><br /><br /></span>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-26253685782690192502007-11-11T17:18:00.000-08:002008-03-24T10:12:37.528-07:00Indices condicionalesHay tablas para las cuales desearíamos hacer valer condiciones de unicidad pero dadas ciertas condiciones que dependan de alguna otra columna de control.<br /><br />Un ejemplo típico se da cuando mantenemos registros en una tabla con borrado lógico, y queremos que exista un único registro activo (pudiendo aparecer el registro inactivo más de una vez). Digamos que tenemos la tabla t, con dos columnas fundamentales: un id y un indicador de registro activo Y/N. Queremos que el Id sea único solamente cuando Activo es 'Y'.<br /><br />Muchas veces, mucha gente se enfrenta a este problema y lo resuelve creando una tabla auxiliar para hacer valer la condición de unicidad, almacenando en ella las tuplas que no desea repetir para lograr dicho control.<br />Todo el problema que implica crear (y mantener) estructuras adicionales y la complejidad de manipulación, puede evitarse utilizando lo que siempre estuvo al alcance: el índice de función y la sentencia CASE.<br /><br /><span style="font-size:100%;"><span style="COLOR: rgb(0,153,0);font-family:courier new;" >SQL> CREATE TABLE t (sec int primary key, id VARCHAR2(100), activo VARCHAR2(1));</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >Table created.</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >SQL> CREATE UNIQUE INDEX ix_t ON t</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >2 (CASE WHEN activo='Y' THEN id ELSE NULL END);</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >Index created.</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >SQL> insert into t values (1, 'COD1', 'Y');</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >1 row created.</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >SQL> insert into t values (2, 'COD1', 'N');</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >1 row created.</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >SQL> insert into t values (3, 'COD1', 'N');</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >1 row created.</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >SQL> insert into t values (4, 'COD1', 'Y');</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >insert into t values (4, 'COD1', 'Y')</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >*</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >ERROR at line 1:</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >ORA-00001: unique constraint (LFER.IX_T) violated</span><br /></span><br />Los índices condicionales no son un feature especial de Oracle, sino que se construyen utilizando los mismos índices de función que usualmente definimos para todas las filas de una tabla. En este caso, la función CASE permite que el índice único se construya sobre un subconjunto de registros, aquellos en el que el campo activo sea igual a 'Y'.<br /><br /><strong>Ver también:</strong><br /><a href="http://oraclenotepad.blogspot.com/2008/03/11g-y-sus-ndices-invisibles.html">11g y sus índices invisibles</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-20844266889606697732007-11-07T16:18:00.000-08:002008-03-28T15:11:21.378-07:00Variables en SQL*Plus (parte 3)Anteriormente repasando variables en sqlplus, vimos las variables de sustitución y las de usuario. Finalmente llegamos al tercer tipo de variables, las variables bind (o de ligadura).<br /><br /><span style="font-weight: bold; color: rgb(0, 0, 153);">VARIABLES BIND</span><br /><br />Estas variables tienen la particularidad de poder ser definidas en sqlplus, usadas y/o asignadas dentro de nuestros programas PL/SQL y luego leídas una vez terminado el programa. Son convenientes cuando queremos tener comunicación entre scripts de sqlplus y bloques de PL/SQL.<br /><br />Las variables bind se definen así:<br /><pre class="CE"><span style="font-size:100%;"><span style="color: rgb(0, 153, 0);">SQL> VARIABLE bvar NUMBER;</span></span><br /></pre><span style="font-size:85%;">Pueden utilizarse también los tipos: CHAR, VARCHAR2, NCHAR, NVARCHAR2, CLOB, BINARY_FLOAT y BINARY_DOUBLE</span><br /><br />Si queremos asignar estas variables antes de llamar a nuestros bloques de PL/SQL, debemos hacerlo desde otro bloque BEGIN END o simplemente usando la sentencia EXEC:<br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >SQL> exec :bvar := 1;</span><br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >PL/SQL procedure successfully completed.</span><br /><br />Las variables dentro de PL/SQL se referencian con dos puntos seguidos del nombre de la misma.<br />Ahora ya podemos utilizar las variables dentro de nuestro programa e imprimir el resultado:<pre><span style="color: rgb(0, 153, 0);">SQL> BEGIN</span><br /><span style="color: rgb(0, 153, 0);"> 2 IF :bvar = 1 THEN</span><br /><span style="color: rgb(0, 153, 0);"> 3 :bvar := 0;</span><br /><span style="color: rgb(0, 153, 0);"> 4 ELSE</span><br /><span style="color: rgb(0, 153, 0);"> 5 :bvar := 1;</span><br /><span style="color: rgb(0, 153, 0);"> 6 END IF;</span><br /><span style="color: rgb(0, 153, 0);"> 7 END;</span><br /><span style="color: rgb(0, 153, 0);"> 8 /</span><br /></pre><span style="color: rgb(0, 153, 0);font-family:courier new;" >PL/SQL procedure successfully completed.</span><br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >SQL> print bvar</span><br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > BVAR</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >----------</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > 0</span><br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >SQL> /</span><br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >PL/SQL procedure successfully completed.</span><br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >SQL> print bvar</span><br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > BVAR</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >----------</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > 1</span><br /><br />Y con esto culminamos el repaso de variables en sql*plus. Finalmente como complemento, veremos brevemente los cursores referenciales los cuales son también útiles para recuperar un conjunto de tuplas de nuestros SELECTs, desde los programas PL/SQL hacia fuera, y así tener más flexibilidad.<br /><br /><span style="font-weight: bold; color: rgb(0, 0, 153);">REFCURSORS<br /></span><br />Se declaran como una variable bind, pero colocamos REFCURSOR en el tipo:<br /><br /><pre class="CE"><span style="font-size:100%;"><span style="color: rgb(0, 153, 0);">SQL> VARIABLE rcur REFCURSOR;</span></span><br /></pre><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >SQL> begin</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > 2 OPEN :rcur FOR select segment_name, segment_type from user_segments orde</span><span style="color: rgb(0, 153, 0);font-family:courier new;" >r by segment_name;</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > 3 end;</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > 4 /</span><br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >PL/SQL procedure successfully completed.</span><br /><br />A diferencia de los cursores tradicionales que se abren y requieren del uso de fetch, al abrir estos cursores bind ya son ejecutados automáticamente y sus resultados almacenados en memoria. Solo resta imprimir las filas de la consulta.<br /><br /><span style="color: rgb(0, 153, 0);">SQL> print rcur</span><pre style="color: rgb(0, 153, 0);">SEGMENT_NAME SEGMENT_TYPE<br />-------------------- ------------------<br />ACUMULADO_CONSUMO TABLE<br />BIG_TABLE TABLE<br />BIG_TABLE_PK INDEX<br />CLIENTE TABLE<br />COUNTRY_C_ID_PK INDEX<br />CUENTA TABLE<br />INVENTARIO TABLE<br />IX_T INDEX<br />MUNICIPIO TABLE<br />NDX_ACUMCON INDEX<br />NDX_ACUMRENT INDEX<br />NDX_ACUMTERC INDEX<br />NDX_ACUMVAR INDEX<br /><br />13 rows selected.<br /></pre><span style="color: rgb(0, 153, 0);"><br /><br /><span style="color: rgb(0, 0, 0); font-weight: bold;">Ver también:</span><br /></span><a href="http://oraclenotepad.blogspot.com/2007/06/variables-en-sqlplus-parte-1.html">Variables en SQLPlus (parte 1)</a> - Sobre variables de usuario<br /><a href="http://oraclenotepad.blogspot.com/2007/07/sqlplus-variables-parte-2.html">Variables en SQLPlus (parte 2)</a> - Sobre variables de sustitución<a href="http://oraclenotepad.blogspot.com/2007/06/variables-en-sqlplus-parte-1.html"></a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-35795135938649740642007-10-25T20:20:00.000-07:002008-03-12T11:33:11.187-07:00Acentos del idioma español¿Cómo comparamos palabras que pueden contener acentos?<br /><br />Un problema frecuente cuando manipulamos datos es la aparición de acentos propios del idioma español correctamente escrito. Como la mayoría de los juegos de caracteres están basados en Unicode, práctimente podemos encontrar símbolos de casi cualquier lengua.<br /><br />Con nuestro idioma español seteado (parámetro NLS_LANGUAGE), podemos comprobar que 'Á' es mayor que 'á':<br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >SQL> select greatest('Á','á') from dual;</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >GR</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >--</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >Á</span><br /><br />A su vez 'á' es mayor que 'a':<br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >SQL> select greatest('á','a') from dual;</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >GR</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >--</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >á</span><br /><br />Luego 'a' es mayor que 'A'<br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >SQL> select greatest('A','a') from dual;</span><br /><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >G</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >-</span><br /><span style="COLOR: rgb(0,153,0);font-family:courier new;" >a</span><br /><br />Pasando en limpio tenemos que: Á > á > a > A<br /><br />Para evitar estos problemas de orden de los caracteres con acentos (incluímos la ñ y Ñ) dentro de la plantilla de caracteres, nos conviene traducir estos caracteres a un solo formato en mayúscula y desasentuado (despues de todo para ordenar no nos importan estas diferencias).<br /><br />La función TRANSLATE viene como anillo al dedo para esta necesaria conversión de caracteres. La sintáxis básica es: TRANSLATE(<span style="FONT-STYLE: italic">cadena</span><cadena><cadena><span style="FONT-STYLE: italic">,caracteres_a_buscar</span><caracteres><caracteres traducir="" a=""><span style="FONT-STYLE: italic">,caracteres_</span><caracteres><caracteres a="" sustituir=""><span style="FONT-STYLE: italic">a_convertir</span>)<br /><br />Lo aplicaremos así:<br />UPPER(TRANSLATE(<cadena><span style="FONT-STYLE: italic">cadena</span>,'ÁÉÍÓÚáéíóú','AEIOUAEIOU'))<br /><br /><span style="COLOR: rgb(0,153,0)">SQL> SELECT UPPER(TRANSLATE('HABÍAn relámpagos','ÁÉÍÓÚáéíóú','AEIOUAEIOU'</span><br /><span style="COLOR: rgb(0,153,0)">)) TEXTO FROM DUAL;</span><br /><br /><span style="COLOR: rgb(0,153,0)">TEXTO</span><br /><span style="COLOR: rgb(0,153,0)">-----------------</span><br /><span style="COLOR: rgb(0,153,0)">HABIAN RELAMPAGOS</span><br /><br />Es aconsejable tener un paquete con una función que reciba una cadena, aplique estas funciones de conversión y retorne la cadena desacentuada. Evitará reescribir código y además tendremos la función cacheada en memoria para las siguientes invocaciones.<br /><br />UPPER/TRANSLATE podrá usarse en SQL para comparar cadenas, con el cuidado necesario para evitar anular índices por el uso de funciones. Es necesario evaluar el alterar los datos almacenados de forma de eliminar la acentuación, o crear índices de función que estén definidos con la funcion de conversión. Dependerá de cada caso particular que es lo más conveniente.<br /></cadena></caracteres></caracteres></caracteres></caracteres></cadena></cadena><br /><strong>Ver también:</strong><br /><a href="http://oraclenotepad.blogspot.com/2008/03/cmo-detectar-caracteres-extraos-o-no.html">Como detectar caracteres extraños o no imprimibles</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-72107598510638598992007-10-23T20:25:00.000-07:002008-03-24T09:53:30.703-07:00Update condicional¿Cómo se hace un UPDATE condicional?<br />Digamos que tengo una tabla t con 3 campos a, b y c. Quiero que si a=1 entonces actualizo b con algún valor, y si a=2 actualizo c.<br /><br />Alguien de mi equipo se encontró con este dilema y lo resolvió de forma muy práctica: dos updates, uno para actualizar aquellos valores con a=1 y otro para actualizar los a=2:<br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">1</span> BEGIN</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">2</span> UPDATE t</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">3</span> SET b=valor</span><valor><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">4</span> WHERE a=1;<br /><span style="color: rgb(0, 0, 0);">5</span></span><span style="color: rgb(0, 153, 0);"></span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">6</span> UPDATE t</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">7</span> SET c=valor</span><valor><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">8</span> WHERE a=2;</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">9</span> END;</span><br /><br />Otro programador del equipo refutó: -¿Para qué ejecutar dos sentencias update si en su lugar puedes hacer una?<br /><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">1</span> BEGIN</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">2</span> UPDATE t</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">3</span> SET B=DECODE(A,1,valor</span><valor><span style="color: rgb(0, 153, 0);font-family:courier new;" >,2,B),</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">4</span> C=DECODE(A,1,C,2,</span><valor><span style="color: rgb(0, 153, 0);font-family:courier new;" >valor); </span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" ><span style="color: rgb(0, 0, 0);">5</span> END;</span><br /><br /><br />¿Quién de los dos tiene razón?<br /><br />...depende.<br /><br />Dado que la performance parece ser lo que está en juego, la primera pregunta que cabe es:<br /><br /><span style="font-weight: bold;">¿Tiene la tabla t índice por A?</span><br /><br />Si la tabla no tiene índice, entonces debemos recorrerla por entero. En el primer caso recorremos la tabla dos veces, mientras que en el segundo caso lo hacemos solamente una! Por lo tanto la primera solución en este escenario es el doble más lenta que la segunda. Al notar que la tabla no tiene índices, el Programador 2 asiente la cabeza con orgullo.<br /><br />Inmediatamente el Programador 1 reacciona: -Pongámosle un índice a t entonces!<br />(el ávido Programador 1 sabe que la segunda consulta no puede sacar provecho de un índice por lo que su victoria parece asegurada)<br /><br />Y aquí es donde cabe la segunda pregunta:<br /><br /><span style="font-weight: bold;">¿Cuál es la cardinalidad de la columna A?</span><br /><br />Sería de gran ayuda saber si 1 y 2 son la mayoría de los valores que hay en la columna A, porque en ese caso, el optimizador va a continuar prefiriendo recorrer toda la tabla! Si la cardinalidad de los valores 1 y 2 son un porcentaje muy bajo del conjunto de valores distintos, entonces recién ahí los índices van a ser utilizados.<br />Cuando el índice es utilizado por la escasa presencia de valores 1 y 2, los tiempos de la primer solución es abismalmente inferior.<br /><br /><span style="font-weight: bold;">Conclusión:<br /><br /></span>La solución 1 es mejor si existen índice sobre la columna A y su cardinalidad es alta (hay pocos 1 y 2 en A).<br />La solución 2 es mejor si no existen índices o existe y la cardinalidad de A es baja para valores 1 y 2 (significa que 1 y 2 ocurren mucho en A).<br /><br /><span style="color: rgb(0, 0, 0); font-weight: bold;">Ver también:</span><br /><a href="http://oraclenotepad.blogspot.com/2007/05/indices-cmo-y-cundo.html">Mis índices no funcionan!</a><br /><a href="http://oraclenotepad.blogspot.com/2007/11/indices-condicionales.html<br />">Indices condicionales</a><br /></valor></valor></valor></valor>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-33562341652728480022007-09-26T20:56:00.000-07:002007-09-29T14:41:15.250-07:00tic... toc¿Cómo medir la performance de un código PL/SQL?<br /><br />Muchas veces nos encontramos con procedimientos lentos de muchos pasos, con llamadas a funciones, DML, loops con cursores, etc, que queremos revisar. Como SYSDATE nos da la cantidad de segundos como mínima fracción de tiempo, no nos es de mucha ayuda consultarlo entre líneas.<br /><br />Afortunadamente, contamos con la función <span style="color: rgb(0, 0, 153);">get_time</span> del paquete dbms_utility para obtener la cantidad de centésimas de segundo que transcurren entre dos instantes de tiempo.<br />A sabiendas de eso, debemos incluir los statements que invoquen a dichas funciones y desplieguen los tiempos por la salida, en un archivo o en una tabla de logs.<br /><br />Con el fin de evitar duplicar código y además aprovechar las bondades del cacheado de paquetes en el área de memoria compartida, creé un pequeño paquete exclusivo para ese fin. El mismo tiene dos procedimientos: tic y toc. Cuando quiero comenzar a medir tiempo, invoco a tic (con un mensaje para registro). Cuando quiero medir el tiempo transcurrido, uso toc. Así de simple.<br /><br /><span style="font-size:85%;"><span style="color: rgb(0, 153, 0);font-family:courier new;" >CREATE OR REPLACE PACKAGE crono AS</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > v_tic PLS_INTEGER;</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > evento VARCHAR2(100);</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > PROCEDURE tic(p_evento IN VARCHAR2);</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" > PROCEDURE toc;</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >END crono;</span><br /><span style="color: rgb(0, 153, 0);font-family:courier new;" >/</span></span><br /><br /><span style="color: rgb(0, 153, 0);font-size:85%;" ><span style="font-family:courier new;">CREATE OR REPLACE PACKAGE BODY crono AS</span><br /><span style="font-family:courier new;"> PROCEDURE tic(p_evento IN VARCHAR2) IS</span><br /><span style="font-family:courier new;"> BEGIN</span><br /><span style="font-family:courier new;"> v_tic := DBMS_UTILITY.GET_TIME;</span><br /><span style="font-family:courier new;"> evento := p_evento;</span><br /><span style="font-family:courier new;"> DBMS_OUTPUT.PUT_LINE('TIC: '||p_evento);</span><br /><span style="font-family:courier new;"> END tic;</span><br /><span style="font-family:courier new;"> </span><br /><span style="font-family:courier new;"> PROCEDURE toc IS</span><br /><span style="font-family:courier new;"> df pls_integer := CAST(DBMS_UTILITY.GET_TIME AS PLS_INTEGER)-v_tic;</span><br /><span style="font-family:courier new;"> BEGIN</span><br /><span style="font-family:courier new;"> DBMS_OUTPUT.PUT_LINE('TOC: '||evento||', T(1/100s): '||df);</span><br /><span style="font-family:courier new;"> END toc;</span><br /><span style="font-family:courier new;">END crono;</span><br /><span style="font-family:courier new;">/</span></span><br /><br />El uso del paquete en un procedimiento cualquiera puede ser:<br /><br /><span style="color: rgb(0, 153, 0);font-size:85%;" ><span style="font-family:courier new;">SQL> begin</span><br /><span style="font-family:courier new;"> 2 crono.tic('creo tabla');</span><br /><span style="font-family:courier new;"> 3 execute immediate 'CREATE TABLE testtable as select * from user_tables';</span><br /><span style="font-family:courier new;"> 4 crono.toc;</span><br /><span style="font-family:courier new;"> 5 crono.tic('elimino registros');</span><br /><span style="font-family:courier new;"> 6 execute immediate 'delete from testtable';</span><br /><span style="font-family:courier new;"> 7 crono.toc;</span><br /><span style="font-family:courier new;"> 8 crono.tic('hago commit');</span><br /><span style="font-family:courier new;"> 9 commit;</span><br /><span style="font-family:courier new;"> 10 crono.toc;</span><br /><span style="font-family:courier new;"> 11 crono.tic('borro tabla');</span><br /><span style="font-family:courier new;"> 12 execute immediate 'drop table testtable';</span><br /><span style="font-family:courier new;"> 13 crono.toc;</span><br /><span style="font-family:courier new;"> 14 end;</span><br /><span style="font-family:courier new;"> 15 /</span><br /><span style="font-family:courier new;">TIC: creo tabla</span><br /><span style="font-family:courier new;">TOC: creo tabla, T(1/100s): 34</span><br /><span style="font-family:courier new;">TIC: elimino registros</span><br /><span style="font-family:courier new;">TOC: elimino registros, T(1/100s): 5</span><br /><span style="font-family:courier new;">TIC: hago commit</span><br /><span style="font-family:courier new;">TOC: hago commit, T(1/100s): 0</span><br /><span style="font-family:courier new;">TIC: borro tabla</span><br /><span style="font-family:courier new;">TOC: borro tabla, T(1/100s): 50</span><br /><br /><span style="font-family:courier new;">Procedimiento PL/SQL terminado correctamente.</span><br /></span><br />Aunque esta experiencia es meramente demostrativa, notar que no pasó un segundo en toda la operación, y sin embargo estamos sacando medidas ilustrativas en cada paso.<br /><br />Es una solución práctica y disponible para todos los usuarios de la base de datos, solo tengo que dar permisos de ejecución a los desarrolladores o a public. Un sinónimo puede simplificar aún más las llamadas a tic y a toc.<br /><br />Pese al mínimo costo de salida por pantalla, una desventaja es que el procedimiento tiene que terminar exitosamente para obtener las medidas (dbms_output despliega su salida al final del proceso). Una alternativa para lograr la medición "<span style="font-style: italic;">on the run</span>", es implementar la salida a archivo (utilizando el paquete utl_file), o insertando en una tabla (con una transacción autónoma para no hacer commit sobre la actual).<br />Estos cambios pueden ser transparentes al usuario, ya que solo hará falta recompilar el body del paquete. Las ventajas están a la vista.<br /><br /><span style="font-weight: bold;">Ver también:</span><br /><a href="http://oraclenotepad.blogspot.com/2007/09/un-extractor-de-ddl-en-archivos.html">Uso del paquete utl_file</a>lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-73989212755095218472007-09-22T08:39:00.000-07:002007-09-23T21:11:12.765-07:00El usuario Scott no existe<span style="color: rgb(0, 153, 0);font-family:courier new;font-size:85%;" >SQL> conn scott/tiger<br />ERROR:<br />ORA-01017: invalid username/password; logon denied</span><br /><br />Dependiendo de las opciones de instalación o de los schemas que vengan instalados en algunas versiones del RDBMS (como Express por ejemplo), el usuario más famoso de Oracle, Scott, puede no estar presente. Citando el caso de Oracle XE, el único schema de ejemplo que viene instalado por defecto es HR.<br /><br />Pero no hay problema, lo único que tenemos que hacer para instalar el schema Scott con sus clásicas tablas EMP, DEPT, BONUS y SALGRADE, es correr (en SQL*Plus) el script <span style="font-weight: bold;">utlsampl.sql</span> que está en el directorio del servidor $ORACLE_HOME/RDBMS/ADMIN.<br /><br />Como se requiere de ciertos permisos especiales que un usuario corriente no tiene, deberá hacerlo el DBA.lferhttp://www.blogger.com/profile/00618392085702183279noreply@blogger.comtag:blogger.com,1999:blog-6230959840389481677.post-76675512159162314982007-09-10T16:35:00.000-07:002008-03-31T15:25:07.568-07:00Limpiando código DDLA propósito de <a href="http://oraclenotepad.blogspot.com/2007/09/un-extractor-de-ddl-en-archivos.html">Un extractor de DDL en archivos separados</a>, me preguntaron como evitaba exportar las cláusulas de storage en Oracle 9i, únicamente dejando las que especifican los tablespaces.<br /><br /><span style="COLOR: rgb(0,153,0);font-size:85%;" ><span style="font-family:courier new;">SQL> create table t (a int);</span><br /><br /><span style="font-family:courier new;">Tabla creada.</span><br /><br /><span style="font-family:courier new;"></span><span style="font-family:courier new;"></span><span style="font-family:courier new;">SQL> select dbms_metadata.get_ddl('TABLE','T') from dual;</span><br /><br /><span style="font-family:courier new;">DBMS_METADATA.GET_DDL('TABLE','T')</span><br /><span style="font-family:courier new;">--------------------------------------------------------------------------------</span><br /><br /><span style="font-family:courier new;">CREATE TABLE "LFERNANDEZ"."T"</span><br /><span style="font-family:courier new;">( "A" NUMBER(*,0)</span><br /><span style="font-family:courier new;">) PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING</span><br /><span style="font-family:courier new;">STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645</span><br /><span style="font-family:courier new;">PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)</span><br /><span style="font-family:courier new;">TABLESPACE "USERS"</span></span><br /><br />En la