SQLServer 2005 Express

20 Febrero 2007

Como tengo que ponerme al día con el SQLServer 2005, me he bajado e instalado en casa la versión Express (sp1). Lejos quedan mis tiempos con el SQLServer 2000 y 7.0

Para instalar el ‘juguete’ hay que bajarse primero el ‘windows installer 3.1′, y la versión 2 del ‘framework’ de ‘.net’. Luego se instala el SQLServer Express y por fin el ‘SQLServer Management Studio’ (la nueva versión del ‘SQLServer enterprise manager’). Después he actualizado a la versión SP1. Hay que tener en cuenta que ahora Microsoft requiere autentificación del ’sofware’ instalado en la máquina hasta para bajar productos de libre distribución (es la ofensiva contra la piratería que ha iniciado últimamente).

Tras la instalación, la primera pregunta de un ‘oraclero’: ¿SQLServer no permite crear ‘primary keys’ de forma explícita y utilizar un índice ya creado (para evitar redundancias) como se hace en Oracle?

Por otra parte, conseguí generar un origen de datos (ODBC) y ver las tablas desde access (como ‘linked tables’).

También he conseguido registrar como ‘linked server’ el Oracle9iR2, pero no sé muy bien qué hacer con él…

El asunto es probar un entorno ‘heterogéneo’ e intentar que las bases de datos interactúen. Si esto se convierte en algo habitual, llegará el momento de añadir una categoría ‘SQLServer’ al ‘blog’.

Como siempre: probaremos y probaremos y probaremos y probaremos y probaremos…

Saludos.

Carlos.


Funciones en columnas indexadas en cláusulas ‘WHERE’

15 Febrero 2007

Yo, como muchos otros, asumí y seguí a pies juntillas la máxima ‘no-uses-funciones-en-columnas-indexadas-en-las-cláusulas-where’. Esto siempre es dicho por parte de alguien que ’sabe’ Oracle (y suele ir acompañado de un cierto gesto grave y a menudo displicente).

Pero es que la documentación de Oracle también lo dice:

“Do not use SQL functions in predicate clauses or WHERE clauses. Any expression
using a column, such as a function having the column as its argument, causes the
optimizer to ignore the possibility of using an index on that column, even a unique
index, unless there is a function-based index defined that can be used.”

“No uses funciones SQL en cláusulas where o predicados. Cualquier expresión que use una columna (…) hace que el optimizador ignore la posibilidad de utilizar un índice en esa columna”

Pero esto es un ejemplo de lo que puede ocurrir: usamos una vieja (aunque en producción: vivita y coleando) base de datos Oracle 8i (Siebel):

(El modo del optimizador -”optimizer_mode”- es “choose” en el init.ora)

system@SIEBELDB> select * from V$VERSION;

BANNER
----------------------------------------------------------------
Oracle8i Enterprise Edition Release 8.1.7.4.0 - 64bit Production
PL/SQL Release 8.1.7.4.0 - Production
CORE    8.1.7.0.0       Production
TNS for HPUX: Version 8.1.7.4.0 - Production
NLSRTL Version 3.4.1.0.0 - Production

system@SIEBELDB> set autotrace traceonly;
system@SIEBELDB> select max(DOM_DNS_FECHA_SOLICITUD)
2  from USERPROP.DOMINIO_DNS
3 where LOWER(DOM_DNS_DOMINIO) = LOWER(:nombreDominio);

Plan de Ejecución
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   SORT (AGGREGATE)
   2    1     TABLE ACCESS (FULL) OF 'DOMINIO_DNS'

Estadísticas
----------------------------------------------------------
          0  recursive calls
          4  db block gets
       4225  consistent gets
       4223  physical reads
          0  redo size
        211  bytes sent via SQL*Net to client
        250  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

Hasta aquí, todo según la máxima (los expertos siguen sonriendo displicentemente).

Pero, si cambiamos el modo del optimizador para que utilice costes:

system@SIEBELDB> alter session set optimizer_mode=first_rows;

Sesión modificada.

system@SIEBELDB>  select max(DOM_DNS_FECHA_SOLICITUD)
2  from USERPROP.DOMINIO_DNS
3 where LOWER(DOM_DNS_DOMINIO) = LOWER(:nombreDominio);

Plan de Ejecución
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=FIRST_ROWS (Cost=3 Card=1 Bytes=138)
   1    0   SORT (AGGREGATE)
   2    1     INDEX (FAST FULL SCAN) OF 'IND_DOMINIO_DNS' (UNIQUE) (Cost=3 Card=832 Bytes=114816)

Estadísticas
----------------------------------------------------------
          0  recursive calls
          4  db block gets
       1731  consistent gets
       1695  physical reads
       2416  redo size
        226  bytes sent via SQL*Net to client
        250  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
          1  rows processed

¡Toma! Así que Oracle no utiliza un índice si se utilizan funciones en columnas indexadas ¿no? Pues ESTÁ UTILIZANDO EL ÍNDICE (en FAST FULL SCAN).

Seguimos:

system@SIEBELDB> select DOM_DNS_FECHA_SOLICITUD
2  from USERPROP.DOMINIO_DNS
3 where LOWER(DOM_DNS_DOMINIO) = LOWER(:nombreDominio);

ninguna fila seleccionada

Plan de Ejecución
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=FIRST_ROWS (Cost=26 Card=832 Bytes=114816)
   1    0   INDEX (FULL SCAN) OF 'IND_DOMINIO_DNS' (UNIQUE) (Cost=26 Card=832 Bytes=114816)

Estadísticas
----------------------------------------------------------
          0  recursive calls
          0  db block gets
       1662  consistent gets
       1659  physical reads
          0  redo size
        153  bytes sent via SQL*Net to client
        210  bytes received via SQL*Net from client
          1  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
          0  rows processed

system@SIEBELDB>

¡Otra vez! Ahora LO ESTÁ UTILIZANDO (en FULL SCAN).

Los expertos han cambiado su sonrisa displicente por una expresión un poco distinta. Uno de ellos tiene una idea. Se le ilumina el rostro: “El índice es un ‘function based index’”. Ya está. Eso lo explica todo. Las sonrisas displicentes vuelven a aparecer.

Pero:

system@SIEBELDB> select OWNER,
  2  INDEX_NAME,
  3  INDEX_TYPE,
  4  TABLE_OWNER,
  5  TABLE_NAME,
  6  TABLE_TYPE,
  7  UNIQUENESS,
  8  nvl(FUNCIDX_STATUS,'NO FBI')
  9    from DBA_INDEXES WHERE INDEX_NAME = 'IND_DOMINIO_DNS'
 10  /

OWNER     INDEX_NAME       INDEX_TYPE TABLE_OWNER TABLE_NAME   TABLE_TYPE  UNIQUENES NVL(FUNC
--------- ---------------- ---------- ----------- ------------ ----------- --------- --------
USERPROP  IND_DOMINIO_DNS  NORMAL     USERPROP    DOMINIO_DNS  TABLE       UNIQUE    NO FBI

Así que, de eso, nada. Un índice corriente y moliente.

En la desesperación, otra conjetura: “Hay una ‘check constraint’ que usa el ‘lower()’ sobre la columna y el optimizador, que es muy listo, la está utilizando en el ‘query plan’”.

Brillante, pero tampoco:

system@SIEBELDB> SELECT * FROM DBA_CONSTRAINTS WHERE OWNER='USERPROP' AND TABLE_NAME='DOMINIO_DNS';

OWNER    CONSTRAINT_NAME C TABLE_NAME  SEARCH_CONDITION
-------- --------------- - ----------- -------------------------------------
USERPROP PK_DOM_DNS_ID   P DOMINIO_DNS
USERPROP SYS_C0019839    C DOMINIO_DNS "DOM_DNS_ID" IS NOT NULL
USERPROP SYS_C0019840    C DOMINIO_DNS "DOM_DNS_DOMINIO" IS NOT NULL
USERPROP SYS_C0019841    C DOMINIO_DNS "DOM_DNS_OPERACION" IS NOT NULL
USERPROP SYS_C0019842    C DOMINIO_DNS "DOM_DNS_FECHA_SOLICITUD" IS NOT NULL
USERPROP SYS_C0019843    C DOMINIO_DNS "DOM_DNS_ESTADO" IS NOT NULL
USERPROP SYS_C0019844    C DOMINIO_DNS "DOM_DNS_FECHA_ESTADO" IS NOT NULL   

7 filas seleccionadas.

Las sonrisas se han mudado en miradas atónitas.

El quid de la cuestión está en la documentación (en mi opinión errónea) de Oracle. Una función sobre una columna indexada puede deshabilitar el uso de un índice como ‘ACCESS PREDICATE’ (RANGE SCANS), pero el optimizador SÍ PUEDE utilizar el índice como ‘FILTER PREDICATE’.

Los expertos han aprendido una cosa más de la que presumir.

Nota: “No hay que dar nada por sentado”.

(Con agradecimiento a Anurag Varma y ‘gamyers’)

Saludos.

Carlos.


El Jabato, 37 años después.

13 Febrero 2007

Eran las navidades de 1970. Mis padres me regalaron ‘por Reyes’ dos o tres ‘cuentos’ de El Jabato. De aquellos amarillos con la imagen de la portada pintada ¿al óleo?.

El Jabato era un íbero que iba de aventura en aventura con su amigo Taurus y luchaba contra los romanos, cartagineses, africanos etc, etc…

Leí y releí aquellos cuentos una y mil veces. Tanto que durante toda mi vida he recordado el nombre del guerrero que luchaba con él en un duelo en el circo de una ciudad hitita: “Runabí”.

En la última viñeta del último de los cuentos El Jabato cabalgaba por el desierto, con el sol poniéndose al fondo, a la busca de su amigo Taurus, que había sido hecho prisionero por los cartagineses. Aquella imagen se ha quedado impresa en mi cerebro, hasta tal punto que muchas veces me he preguntado qué habría ocurrido en el siguiente tomo, cómo habría acabado aquella aventura…

Ayer, por casualidad, encontré el segundo volumen de El Jabato que ha reeditado ediciones B y que comienza justo donde terminó el último de los cuentos que me regalaron mis padres, en el momento que mi memoria congeló hace 37 años…

Hoy, 37 años después, he sabido por fin cómo acabó aquella aventura inconclusa.

Saludos.

Carlos.


GRANTs, ORA-01720 y VIEWs

12 Febrero 2007

La concesión de privilegios (‘GRANTs’) sobre vistas tiene algunas particularidades que pueden dar ciertos problemas inesperados.

Veamos un ejemplo. Empezamos con un usuario y una tabla simple:

usuario1@TEST10G> SELECT * FROM PRUEBA01;

        ID TXT
---------- -------------------------
         1 UNO
         2 DOS
         3 TRES
         4 CUATRO
         5 CINCO
         6 SEIS

6 filas seleccionadas.

usuario1@TEST10G> GRANT SELECT ON PRUEBA01 TO usuario2;

Concesión terminada correctamente.

usuario1@TEST10G> GRANT SELECT ON PRUEBA01 TO usuario3;

Concesión terminada correctamente.

Comprobamos que los usuarios 2 y 3 pueden acceder a la tabla.

El usuario2:

usuario2@TEST10G> SELECT * FROM usuario1.PRUEBA01;

        ID TXT
---------- -------------------------
         1 UNO
         2 DOS
         3 TRES
         4 CUATRO
         5 CINCO
         6 SEIS

6 filas seleccionadas.

…y el usuario3:

usuario3@TEST10G> SELECT * FROM usuario1.PRUEBA01;

        ID TXT
---------- -------------------------
         1 UNO
         2 DOS
         3 TRES
         4 CUATRO
         5 CINCO
         6 SEIS

6 filas seleccionadas.

Hasta aquí, todo correcto. El asunto se complica cuando el usuario2 crea una vista e intenta darle privilegios de lectura al usuario3:

usuario2@TEST10G> CREATE VIEW PRUEBAVW AS SELECT * FROM PRUEBA01;

Vista creada.

usuario2@TEST10G> GRANT SELECT ON PRUEBAVW TO usuario3;
GRANT SELECT ON PRUEBAVW TO usuario3
                *
ERROR en línea 1:
ORA-01720: la opción Otorgar no existe para 'USUARIO1.PRUEBA01'

Por alguna razón, el usuario2 no puede otorgar los privilegios sobre la vista al usuario3. (Recordemos que usuario3 tiene privilegios sobre la tabla base de la vista usuario1.PRUEBA01)

Ni siquiera SYSTEM puede:

system@TEST10G> GRANT SELECT ON usuario2.PRUEBAVW TO usuario3;
GRANT SELECT ON USUARIO2.PRUEBAVW TO USUARIO3
                       *
ERROR en línea 1:
ORA-01720: la opción Otorgar no existe para 'USUARIO1.PRUEBA01'

El problema está en que Oracle no verifica los ‘GRANTS’ sobre los objetos base de las vistas, sino que los debe conceder explícitamente para la ejecución de la vista. Y esto sólo se puede hacer otorgando los privilegios sobre los objetos base de la vista al propietario de la misma con la opción ‘WITH GRANT OPTION’.

Lo vemos a continuación:

usuario1@TEST10G> GRANT SELECT ON PRUEBA01 TO usuario2 WITH GRANT OPTION;

Concesión terminada correctamente.

Y ahora todo va bien:

usuario2@TEST10G>  GRANT SELECT ON PRUEBAVW TO usuario3;

Concesión terminada correctamente.

usuario2@TEST10G>

Y así:

usuario3@TEST10G> select * from usuario2.pruebavw;

        ID TXT
---------- -------------------------
         1 UNO
         2 DOS
         3 TRES
         4 CUATRO
         5 CINCO
         6 SEIS

6 filas seleccionadas.

Pero la cosa se vuelve más peliaguda si hacemos:

usuario1@TEST10G>  revoke SELECT ON PRUEBA01 from usuario2;

Revocación terminada correctamente.

usuario1@TEST10G> GRANT SELECT ON PRUEBA01 TO usuario2;

Concesión terminada correctamente.

Porque ahora el pobre usuario3, sin saber porqué, verá:

usuario3@TEST10G> select * from usuario2.pruebavw;
select * from usuario2.pruebavw
*
ERROR en línea 1:
ORA-01031: privilegios insuficientes

usuario3@TEST10G>

Así que hay que tener mucho cuidado cuando se otorgan privilegios sobre vistas si éstas hacen referencia a objetos base que pertenecen a otros esquemas. Y también si se revocan y vuelven a conceder los privilegios sobre los objetos base sin hacer caso al ‘WITH GRANT OPTION’.

Saludos.

Carlos.


Tablas mutantes… que no mutan!

8 Febrero 2007

Aunque el nombre que Oracle da a estas tablas (‘Mutating Tables’, Tablas Mutantes) parece más propio de una película de George A. Romero, el concepto es más sencillo (y menos terrorífico) que todo eso.

“A mutating table is a table that is currently being modified by an update, delete, or insert statement. When a trigger tries to reference a table that is in state of flux (being changed), it is considered
“mutating” and raises an error since Oracle should not return data that has not yet reached its final state”

Una tabla mutante es una tabla que está siendo modificada. Si un ‘trigger’ hace referencia a la propia tabla sobre la que se dispara se producirá una excepción con error ORA-04091.

Según esto, cualquier ‘trigger’ que haga una simple consulta sobre la tabla que lo dispara generará dicho ORA-04091.

Vamos a verlo.

SQL*Plus: Release 10.2.0.1.0 - Production on Jue Feb 8 10:08:18 2007

Copyright (c) 1982, 2005, Oracle.  All rights reserved.

Introduzca el nombre de usuario: carlos@test10g
Introduzca la contraseña:

Conectado a:
Oracle Database 10g Enterprise Edition Release 10.2.0.2.0 - Production
With the Partitioning, OLAP and Data Mining options

carlos@TEST10G> select * from prueba01;

        ID TXT
---------- -------------------------
         1 UNO
         2 DOS
         3 TRES
         4 CUATRO

carlos@TEST10G> create trigger BID_PRUEBA01_TRG
  2  BEFORE INSERT OR DELETE
  3  ON PRUEBA01
  4  FOR EACH ROW
  5  DECLARE
  6       i_dummy NUMBER;
  7  BEGIN
  8     SELECT MAX(ID)
  9       INTO i_dummy
 10       FROM PRUEBA01;
 11  END BI_PRUEBA01_TRG;
 12  /

Disparador creado.

Verificamos el comportamiento esperado borrando una fila:

carlos@TEST10G> delete from prueba01 where id=5;
delete from prueba01 where id=5
            *
ERROR en línea 1:
ORA-04091: la tabla CARLOS.PRUEBA01 está mutando, puede que el disparador/la función no puedan verla
ORA-06512: en "CARLOS.BID_PRUEBA01_TRG", línea 4
ORA-04088: error durante la ejecución del disparador 'CARLOS.BID_PRUEBA01_TRG'

carlos@TEST10G>

Hasta aquí todo ocurre como se espera.

Pero, veamos que ocurre con un INSERT:

carlos@TEST10G> INSERT INTO PRUEBA01 VALUES (5, 'CINCO');

1 fila creada.

¿Cómo? ¿Por qué? ¡La ejecución del trigger debería dar el mismo error que en el caso del DELETE!

Pero la inserción se ha efectuado:


carlos@TEST10G> SELECT * FROM PRUEBA01;

        ID TXT
---------- -------------------------
         1 UNO
         2 DOS
         3 TRES
         4 CUATRO
         5 CINCO

carlos@TEST10G>

Veamos que ocurre si insertamos una fila, pero de otra manera:

carlos@TEST10G> INSERT INTO PRUEBA01 SELECT 6,'SEIS' FROM DUAL;
INSERT INTO PRUEBA01 SELECT 6,'SEIS' FROM DUAL
            *
ERROR en línea 1:
ORA-04091: la tabla CARLOS.PRUEBA01 está mutando, puede que el disparador/la función no puedan verla
ORA-06512: en "CARLOS.BI_PRUEBA01_TRG", línea 4
ORA-04088: error durante la ejecución del disparador 'CARLOS.BI_PRUEBA01_TRG'

carlos@TEST10G>

Así pues, sólo en el caso de INSERT’s de una sola fila con VALUES, el ORA-04091 no se produce.

Pero, vayamos un paso más allá: ¿Qué pasará con operaciones ‘BULK INSERT’ que también tienen ‘VALUES’?

carlos@TEST10G> DECLARE
  2     TYPE T_T_PRUEBA01 IS TABLE OF PRUEBA01%ROWTYPE INDEX BY BINARY_INTEGER;
  3     T_PRUEBA01 T_T_PRUEBA01;
  4  BEGIN
  5     T_PRUEBA01(1).ID:= 6;
  6     T_PRUEBA01(1).TXT:= 'SEIS';
  7     T_PRUEBA01(2).ID:= 7;
  8     T_PRUEBA01(2).TXT:= 'SIETE';
  9
 10     FORALL i IN T_PRUEBA01.FIRST..T_PRUEBA01.LAST
 11        INSERT INTO PRUEBA01 VALUES T_PRUEBA01(i); --'VALUES' sin paréntesis!!
 12  END;
 13  /
DECLARE
*
ERROR en línea 1:
ORA-04091: la tabla CARLOS.PRUEBA01 está mutando, puede que el disparador/la función no puedan verla
ORA-06512: en "CARLOS.BID_PRUEBA01_TRG", línea 4
ORA-04088: error durante la ejecución del disparador 'CARLOS.BID_PRUEBA01_TRG'
ORA-06512: en línea 10

Como vemos, aquí se vuelve a dar el error (como era de esperar).

Pero, una vez más ¿y si la tabla sólo tiene una fila?

carlos@TEST10G> DECLARE
  2     TYPE T_T_PRUEBA01 IS TABLE OF PRUEBA01%ROWTYPE INDEX BY BINARY_INTEGER;
  3     T_PRUEBA01 T_T_PRUEBA01;
  4  BEGIN
  5     T_PRUEBA01(1).ID:= 6;
  6     T_PRUEBA01(1).TXT:= 'SEIS';
  7
  8     FORALL i IN T_PRUEBA01.FIRST..T_PRUEBA01.LAST
  9        INSERT INTO PRUEBA01 VALUES T_PRUEBA01(i); --'VALUES' sin paréntesis!!
 10  END;
 11  /

Procedimiento PL/SQL terminado correctamente.

system@TEST10G>

Aunque sea una operación BULK INSERT, la inserción de una sola fila es análoga al caso de INSERT INTO …VALUES simple.

Saludos.

Carlos.


SQL*Plus y variables de entorno (en Linux y Windows)

5 Febrero 2007

SQL*Plus es capaz de trabajar de manera natural con las variables de entorno del sistema operativo, de forma que dichas variables se pueden utilizar en ’scripts’. Esto es algo bastante común en entornos Linux:

REL4/oracle/10.2.0> DUMMY=${ORACLE_HOME}/basura/
REL4/oracle/10.2.0> export DUMMY
REL4/oracle/10.2.0> sqlplus xxxxxxx@yyyyyy.zzzzzz

SQL*Plus: Release 10.2.0.2.0 - Production on Lun Feb 5 09:12:10 2007

Copyright (c) 1982, 2005, Oracle.  All Rights Reserved.

Introduzca la contraseña:

Conectado a:
Oracle Database 10g Enterprise Edition Release 10.2.0.2.0 - Production
With the Partitioning, OLAP and Data Mining options

sql> spool ${DUMMY}prueba
sql> select sysdate from dual;

SYSDATE
-------------------
05/02/2007 09:13:54

sql> spool off
sql> quit
Desconectado de Oracle Database 10g Enterprise Edition Release 10.2.0.2.0 - Production
With the Partitioning, OLAP and Data Mining options

REL4/oracle/10.2.0> cd basura
REL4/oracle/10.2.0/basura> ls -ltr
total 12
-rw-r--r--    1 oracle   dba            96 Feb  5 09:13 prueba.lst

REL4/oracle/10.2.0/basura>

Lo que quizá es menos conocido es que también funciona en Windows (con su propia sintaxis para las variables de entorno, claro):

D:>SET DUMMY=D:BASURA

D:>SQLPLUS xxxxxxx@yyyyyy.zzzzzz

SQL*Plus: Release 9.2.0.8.0 - Production on Vie Feb 2 14:33:31 2007

Copyright (c) 1982, 2002, Oracle Corporation.  All rights reserved.

Introduzca la contraseña:

Conectado a:
Oracle Database 10g Enterprise Edition Release 10.2.0.2.0 - Production
With the Partitioning, OLAP and Data Mining options

SQL> spool %DUMMY%prueba.txt
SQL> select sysdate from dual;

SYSDATE
--------
02/02/07

SQL> spool off;
SQL> quit
Desconectado de Oracle Database 10g Enterprise Edition Release
10.2.0.2.0 - Production
With the Partitioning, OLAP and Data Mining options

D:>cd basura

D:basura>dir
 El volumen de la unidad D es DATOS
 El número de serie del volumen es: 7C84-6B91

 Directorio de D:basura

02/02/2007  14:32              .
02/02/2007  14:32              ..
02/02/2007  14:33               299 prueba.txt

D:basura>

Saludos.

Carlos.


CentOS-4.4… al fin.

3 Febrero 2007

Aprovechando el ancho de banda de que dispongo en mi nueva empresa (¡nunca había visto tasas de descarga de 1,6 MB/sg!) , he podido bajar los anhelados cuatro ISO’s de CentOS 4.4.

En cuanto he tenido un ratito no me he podido resistir… He aquí el resultado:

CentOS 4.4

Tengo la idea de montar un Oracle 10g en RAC sobre este sistema operativo. Veremos qué tal se da…

Un saludo.

Carlos.