VMWare Player 3.0

4 Diciembre 2009

VMWare acaba de sacar VMWare Player 3.0. Y no es sólo otra versión del VMWare Player de toda la vida, que sólo podía correr máquinas virtuales, no crearlas (para eso se necesitaba la versión Workstation -de pago-, la versión Server, etc…)

Pues sí: la nueva versión Player 3.0 sigue siendo gratis y además permite crear máquinas virtuales. ¿Y entonces, qué pasa con la versión ‘Workstation’? Pues lo mismo que pasaba con relación al VMWare Server: Workstation te permite funcionalidades avanzadas que el nuevo Player no presenta: (Snapshots, Clones, Virtual Rights). No obstante, para un usuario medio lo que ofrece el nuevo Player parece más que suficiente.

Editado más tarde: Una de las cosas que no aparecen en esta versión es la herramienta de configuración de redes virtuales. Tras jugar un rato, he  echado de menos todas las posibilidades de configuración de las mismas de la versión  ‘Workstation’.

Con esto VMWare cierra el círculo de productos gratuítos comenzado con el VMWare Server (muy bueno el 1,5, no tanto el 2.0…) y el ESXi server. Este producto orientado a ‘desktops’ (o ‘workstations’, si se quiere) es una respuesta a la gran cantidad de productos de virtualización gratuítos que han aparecido recientemente, con VirtualBox a la cabeza.

Por otra parte, tambien merece la pena echarle un vistazo al VMWare vCenter Converter, heredero de los VMware p2v y VMware Converter, que son herramientas “p2v” (physical to virtual) que convierten máquinas físicas en máquinas virtuales.

Finalmente, VMWare tiene otro producto en el que ha puesto muchas esperanzas: el VMWare View. Este producto -no es gratis- quiere introducir la virtualización de ‘desktops’: los ‘desktops’ son virtualizados, almacenados y distribuidos en un servidor y se corren en las máquinas físicas como máquinas virtuales. Tu llegas a la oficina, te sientas en cualquier equipo, te conectas al servidor y te bajas tu ‘desktop’ con todas tus aplicaciones, configuraciones, etc… es como tener un portátil virtual, pero sin el soporte físico (el propio portátil).

En fin: nuevos juguetes para jugar estas navidades…

Saludos.

Carlos.


Only numbers

2 Diciembre 2009

Un compañero vino a preguntarme si en Teradata existe alguna función para saber si todos los caracteres de una cadena son números.

Aunque existe la conocida función (UDF) CHKNUM()/Is_numeric , ésta permite caracteres no numéricos (puntos, comas, signos +/-), y no es esto lo que buscamos exactamente.

Para ver que en una cadena sólo hay números se puede utilizar oTransalate() (una de mis funciones favoritas en Oracle y también en Teradata) en una construcción como esta:

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT CASE
WHEN CHARACTERS(otranslate(TRIM(COALESCE('123465','')),
                           'a1234567890',
                           'a')) = 0
   THEN 'is only numbers'
   ELSE 'is not only numbers'
END
;

 *** Query completed. One row found. One column returned.
 *** Total elapsed time was 1 second.

-------------------
is only numbers

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT CASE
WHEN CHARACTERS(otranslate(TRIM(COALESCE('123465-A','')),
                           'a1234567890',
                           'a')) = 0
   THEN 'is only numbers'
   ELSE 'is not only numbers'
END
;

 *** Query completed. One row found. One column returned.
 *** Total elapsed time was 1 second.

-------------------
is not only numbers

La idea es eliminar de la cadena todos los caracteres numéricos y verificar si la longitud (CHARACTERS() es análoga a LENGTH()) es cero o no. Si es cero, todos los caracteres (numéricos) fueron eliminados y, por tanto, la cadena era ‘only numbers’.
Si la longitud no es cero, hay caracteres no numéricos y la cadena era ‘not only numbers’.

Por supuesto, esta construcción se puede utilizar con otros patrones para verificar diferentes formatos y demás… (¡y en Oracle también funciona!)

Una última cosa: Se necesita la ‘a’ en la segunda y tercera cadenas para evitar que oTranslate() devuelva NULL .

Saludos.

Carlos.


Backup/Restore: P2V the hard way.

20 Noviembre 2009

El asunto parece sencillo:  coges un equipo, haces un ‘backup‘ del sistema completo y lo restauras en otra parte. En la web hay mil teorías de cómo hacerlo. Es muy sencillo -dicen-. Hasta que realmente TIENES que hacerlo.

La realidad: Tenemos un equipo (un viejo PIV con 512 MB) en el que hay instalado un Windows 2003 Server con varias aplicaciones imprescindibles (aplicaciones ‘web’ que corren en el IIS y varias transferencias de ficheros que dependen del ‘FTP Server‘) del que nadie se ha acordado -ni preocupado- de hacer un ‘backup’… hasta que un día la fuente de alimentación se muere repentinamente, a consecuencia de un pico de tensión.

Como quiera que estamos tabajando para la Administración, esto supone una llamada al CAU, que vengan dos técnicos a ver el equipo, que llamen al fabricante (pues resulta que el vejestorio todavía está en garantía), que el fabricante se digne a venir a ver el equipo, valore los daños y -por fin- cambien la fuente de alimentación. Resultado: más de una semana sin los imprescindibles servicios que nos prestaba el cacharrillo. Visto lo visto, mi jefe decide que hay que hacer algo por si la contingencia se vuelve a producir. Mi compañero S. y yo nos ponemos manos a la obra.

OK. Lo primero es buscar una herramienta de ‘backup‘. Miramos herramientas gratuitas y probamos Paragon Backup & Recovery 10 Free Edition. Después de instalarlo, nos dice que no funciona con Windows 2003 Server. La siguiente opción es EASEUS Todo Backup 1.0 que promete hacer ‘backups‘ fiables y generar CD’s de recuperación (boot-CD) para el caso de que el vejestorio sufra un percance mayor que su fuente de alimentación inoperativa.

Bajamos e instalamos el software y realizamos una copia de seguridad del sistema en un disco USB externo. El EASEUS Todo Backup genera dicho backup en ficheros de algo más de 4GB (creemos que para que quepan en DVD’s) sin mayores problemas. Hacemos una verificación del backup, por si las moscas. Todo OK, siempre según el EASEUS Todo Backup.

También generamos un CD de recuperación, que el software se encarga de crear y grabar en un CD.

Todo Backup 10

Todo Backup 10

Hasta aquí la cosa va como la seda. Coser y cantar. Pero ahora hay que probar que podemos recuperar el backup obtenido. El asunto no tendría mayor importancia… a no ser que no fuera la Administración: no hay equipos donde recuperar (restore) el ‘backup‘ realizado. Y ni de coña lo vamos a intentar en el propio equipo del que hicimos el ‘backup‘: si algo falla, nos quedamos en bragas.

Así que sopesamos las opciones. Obviamente, iremos a alguna opción de virtualización: el ‘restore‘ en una máquina virtual nos aseguraría un ‘restore‘ en una máquina real, e incluso nos permitiría tener el Windows 2003 Server corriendo virtualizado hasta disponer de un equipo de repuesto para nuestro matusalén. Comenzamos con VirtualBox, que es gratis.

Corremos VirtualBox en un equipo con Windows XP. Generamos una máquina virtual con un disco de espacio suficiente para recuperar el backup. Introducimos el CD de recuperación generado con EASEUS Todo Backup y arrancamos. Vemos que se trata de algún Linux que intenta arrancar… hasta que un pantallazo hace que la máquina virtual se reinicie. Así una y otra vez. Estamos casi seguros de que es algo relacionado con la resolución de la pantalla de la máquina virtual, pero aunque probamos diferentes configuraciones con “VBoxManage” siempre llegamos al mismo resultado. Así que VirtualBox fuera.

La siguiente: la siempre fiable VMWare Workstation. Como sólo queremos ver que el ‘backup’ es recuperable, nos valdrá con una versión de evaluación. Mismo procedimiento: Generamos una máquina virtual con un disco de espacio suficiente para recuperar el backup. Introducimos el CD de recuperación generado con EASEUS Todo Backup y arrancamos. Aquí las cosas empiezan a mejorar: aunque el pantallazo es el mismo que en el VirtualBox, VMWare se da cuenta y nos ofrece cambiar la resolución de la pantalla. Elegimos 800×600, volvemos a arrancar y… voìlá. Tenemos el Linux arrancado con el EASEUS Todo Backup en orden de revista.

Comenzamos con el proceso de recupeación del ‘backup‘. Aquí pasan cosas un poco raras: las pantallas para elegir de dónde seleccionar los ficheros de ‘backup‘ para la recuperación y el disco de destino a recuperar, a veces vemos dos unidades (el DVD y el disco USB) y a veces tres (el DVD, el disco USB y el disco duro virtual). Creemos que es porque el proceso necesita un tiempo para reconocer todas las unidades, y nosotros estamos ya un poco impacientes.

Finalmente elegimos ficheros origen, disco de destino y ejecutamos la recuperación, que lógicamente se toma su tiempo. Al terminar tenemos -en teoría- un sistema virtual idéntico al sistema real del que hicimos el backup.

“Lo tenemos casi”, pensamos. Sólo hay que arrancar el sistema y ver el Windows 2003 Server ‘alive and kicking’.

Así que arrancamos la máquina virtual. Aparece la pantalla de Windows 2003 Server con el ‘ascensor’ horizontal. “Ya está, ya está” pero:

Blue Screen of Death

Blue Screen of Death

El “Blue Screen of Death“. Con el STOP: 0X0000007B. Las maldiciones se deben de oir hasta en la China.

El msdn dice que esos errores son de problemas para encontrar un medio de arranque. Así que la respuesta está en el disco duro virtual. Probamos todas las opciones que nos permite VMWare, pero siempre con el mismo resultado: BSD.

Ya no sabemos qué hacer. Como la respuesta no aparece del lado de Microsoft, buscamos en el lado de VMWare.

En la base de conocimientos damos con algo que nos pone las orejas tiesas:

For all versions of GSX Server, Workstation and VMware Server, this problem can also be caused by the use of a third party IDE controller driver on your
native Windows 2000, Windows XP or Windows Server 2003 partition.

“A ver si va a ser esto…” Nos vamos corriendo al equipo con el Windows 2003 Server. Miramos el Administrador de dispositivos y ¡ahí está!: hay controladores SATA ‘intel’ instalados (suponemos que con algún CD de instalación del equipo). Esto hace que, al no ser controladores ‘genéricos Windows’, cuando el sistema intenta arrancar en la máquina virtual con estos ‘drivers‘ no reconoce el disco duro virtual, ya que espera uno específico para el ‘driver‘.

¿Cómo solventarlo? Con un nuevo perfil ‘hardware‘. Creamos un nuevo perfil ‘hardware‘ y le sustituimos todos los ‘drivers‘ de ‘third party‘ por controladores genéricos IDE de Windows.
Arrancamos el sistema con este nuevo perfil y vuelta a empezar: nueva copia de seguridad del sistema.

Tras otro buen rato esperando el ‘backup‘ al USB y un nuevo ‘restore‘ en la máquina virtual arrancanda con el CD de recuperación de EASEUS Todo Backup, tenemos todo a punto de caramelo…

Cruzamos los dedos. Arrancamos la máquina virtual con el disco restaurado. Aparece la pantalla de Windows 2003 Server con el ‘ascensor’ horizontal… ¡y arranca!.

Tenemos un sistema Windows 2003 Server virtualizado que es una copia exacta del que hay en el cacharro renqueante. Y puede funcionar (si configuramos la red virtual como ‘bridged‘) en sustitución del sistema físico original en el caso de una avería permanente o irrecuperable.

Todo esto sólo nos ha llevado tres días…

Saludos.

Carlos.


UltraEdit 32

20 Agosto 2009

Allá por el año 1999 -o quizá el 2000- un compañero me habló de un magnífico editor de texto para programadores llamado UltraEdit. Yo llevaba mucho, pero que mucho tiempo programando con el Norton Editor y no había oído hablar del editor en cuestión, con lo que al principio lo miraba con escepticismo. Siempre he sido muy crítico con las herramientas ‘GUI’ que tienen multitud de botoncitos, ventanitas, colores, opciones… (soy acérrimo del sql*plus en Oracle y del bteq en Teradata) pero tras un primer vistazo, me di cuenta de que el diseño e interfaz era lo bastante sobrio para que pasase mis ‘filtros anti GUI’.

Así pues me bajé una versión ‘trial‘ (creo que de 30 días) y poco a poco, y casi sin darme cuenta, me fui haciendo adicto a él. Total, que el ‘trial‘ expiró y me compré la licencia (la versión del momento era la 7.20). Desde entonces nos hemos hecho inseparables y en todos los trabajos y en todos los ordenadores con los que he trabajado (si Windows) he instalado este editor con el que me siento como en casa (prácticamente conozco de memoria todos los ‘shortcuts‘ de teclado).

Su simplicidad, su efectividad, su modo de edición en columna, la edición hexadecimal…

Tiene ciertas carencias: mi versión no soporta conexión SSH/SFTP (aunque se puede hacer a través de FileZilla) y a veces tiene problemas con el UNICODE y los BOM, pero no lo cambio por nada.

A través de los años he probado otros editores, y me han ofrecido versiones más modernas (y todo hay que decirlo, casi todas piratas) del UltraEdit, con nuevas ‘features‘ y más ‘buzzs and bells‘, pero yo sigo fiel a mi UltraEdit versión 7.20, compañera en mil batallas, con la que he escrito miles y miles de líneas de código, ya sea SQL, shell scripts, HTML, java, C/C++, PL/SQL, Visual Basic scripts… y con la que espero escribir aun muchas líneas más.

Porque no necesito otra cosa.

(Dedicado a La Parte Contraria).

UltraEdit 7.20

Saludos.

Carlos.


Importando fechas julianas a Teradata.

14 Julio 2009

Actualmente estoy trabajando en un proceso de incorporación de datos de una Base de datos OLTP Oracle a un Data Warehouse Teradata. La cosa no parecía muy complicada hasta que nos hemos dado cuenta que dicha base de datos Oracle es a su vez resultado de una migración -y no muy bien hecha- de un DB2. Entre otras particularidades nos hemos encontrado con que hay tablas sin ‘primary key‘ y que las tablas y columnas tienen nombres restringidos a 8 dígitos (herencia DB2, suponemos). También que las tablas presentan cláusulas de almacenamiento (‘STORAGE’) con ‘INITIAL’ 1K, 2K, 3K,… (¡Hey! ¡Estamos en 2009! ¿alguien ha oído hablar de los LMT’s?) lo que para un ‘datablock’ de 8K (suponemos) resulta ser un tanto ’surrealista’.

Pero lo mejor es que todas las fechas aparecen como NUMBER(7). Al preguntar, nos dicen que son fechas ‘en juliano’ (‘julian date‘).

El calendario juliano es un calendario en el que se cuentan los días desde el 1 de enero del 4713 antes de Cristo a mediodía. Así el 14/07/2009 será el 2455027.

Oracle maneja las fechas julianas sin problemas mediante el formato ‘J’ de las funciones TO_CHAR y TO_DATE:

CARLOS@XE.localhost> SELECT TO_CHAR(SYSDATE, 'J') FROM DUAL;

TO_CHAR
-------
2455027

CARLOS@XE.localhost> SELECT TO_DATE('2455027', 'J') FROM DUAL;

TO_DATE(
--------
14/07/09

Con lo que la incorporación de enteros (NUMBER(7)) como fechas julianas a Oracle no presentaría mayor problema que el uso de un TO_DATE().

Pero Teradata es otra cuestión. Teradata no presenta algo análogo al formato ‘J’ de Oracle y hay que pensar cómo convertir esos NUMBER(7) a fechas.

El primer intento es obvio, pero fallido:

SELECT CAST('-4713/01/01' AS DATE FORMAT 'YYYY/MM/DD') + 2455027;
 *** Failure 2665 Invalid date.
                Statement# 1, Info =0
 *** Total elapsed time was 1 second.

De hecho, Teradata no soporta fechas anteriores al 1 de enero de 01 (al menos directamente):

Internally, Teradata Database stores each DATE value as a four-byte signed integer using the following formula:

(YEAR – 1900) * 10000 + (MONTH * 100) + DAY

where the YEAR, MONTH, and DAY components, defined appropriately for the Gregorian calendar, have the following range of values:

Range of values
Component Minimum Maximum
YEAR 1 9999
MONTH 1 12
DAY 1 28, 29, 30, or 31 (depending on the month and year)

En efecto:

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT CAST((1 - 1900) * 10000 + (1 * 100) + 1 AS DATE);

SELECT CAST((1 - 1900) * 10000 + (1 * 100) + 1 AS DATE);

 *** Query completed. One row found. One column returned.
 *** Total elapsed time was 1 second.

((((1-1900)*10000)+(1*100))+1)
------------------------------
                      01/01/01

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT CAST((1 - 1900) * 10000 + (1 * 100) + 1 AS DATE) - 1;

SELECT CAST((1 - 1900) * 10000 + (1 * 100) + 1 AS DATE) - 1;
 *** Failure 2665 Invalid date.
                Statement# 1, Info =0
 *** Total elapsed time was 1 second.

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT CAST('0001/01/01' AS DATE FORMAT 'YYYY/MM/DD');

SELECT CAST('0001/01/01' AS DATE FORMAT 'YYYY/MM/DD');

 *** Query completed. One row found. One column returned.
 *** Total elapsed time was 1 second.

'0001/01/01'
------------
  0001/01/01

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT CAST('0001/01/01' AS DATE FORMAT 'YYYY/MM/DD') - 1;

SELECT CAST('0001/01/01' AS DATE FORMAT 'YYYY/MM/DD') - 1;
 *** Failure 2665 Invalid date.
                Statement# 1, Info =0
 *** Total elapsed time was 1 second.

Entonces ¿cómo nos las ingeniamos? La respuesta está en algo llamado traslación. En vez de irnos al 1 de enero de 4713 antes de Cristo como origen de la aritmética de fechas, podemos hacer una traslación a -digamos- el 1 de enero de 1900: tomamos la fecha juliana de ese día, que resulta ser el número 2415021, y sacamos la diferencia con la fecha juliana recibida sabiendo la fecha base tomada. (También es bueno recordar que Teradata guarda este 1 de enero de 1900 como 101).

Así para nuestro 14/07/2009 (2455027 en juliano) tendremos que:

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT CAST(101 AS DATE) + (CAST(2455027 AS INTEGER) - 2415021) (FORMAT 'DD/MM/YYYY');

SELECT CAST(101 AS DATE) + (CAST(2455027 AS INTEGER) - 2415021) (FORMAT 'DD/MM/YYYY');

 *** Query completed. One row found. One column returned.
 *** Total elapsed time was 1 second.

(101+(2455027-2415021))
-----------------------
             14/07/2009

Y con esta técnica podemos incorporar las enrevesadas fechas julianas NUMBER(7) de origen a simples fechas DATE que todo el mundo entiende.

Saludos.

Carlos.


ANSI OUTER JOINS: parecido no es igual.

3 Junio 2009

¿Qué es un ‘outer join‘? Si estáis leyendo esto, seguramente ya lo sabeis. Un ‘outer join’ es un ‘join’ en el que las filas de la tabla principal son mantenidas en el resultado aunque no existan filas que les correspondan en la tabla con la que se combina.

La sintaxis ANSI es:

SELECT …
FROM Tabla1
(LEFT) OUTER JOIN Tabla2 ON Tabla1.Col1 = Tabla2.Col1
[AND Tabla1.Col2 = Tabla2.Col2 ...]

La teoría es fácil y la sintaxis también, pero a veces sentencias ‘muy parecidas’ que en un primer vistazo asumimos como idénticas pueden no serlo tanto y meternos en problemas al devolver resultados inesperados.

Vamos a verlo en Teradata, pero primero las tablas de rigor:

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
CREATE SET TABLE MY_DB.PRUEBA01 ,
     NO FALLBACK ,
     NO BEFORE JOURNAL,
     NO AFTER JOURNAL,
     CHECKSUM = DEFAULT
     (
      ID    INTEGER     NOT NULL,
      CTXT  VARCHAR(10) NOT NULL )
PRIMARY INDEX ( ID );

CREATE SET TABLE MY_DB.PRUEBA01 ,
     NO FALLBACK ,
     NO BEFORE JOURNAL,
     NO AFTER JOURNAL,
     CHECKSUM = DEFAULT
     (
      ID    INTEGER     NOT NULL,
      CTXT  VARCHAR(10) NOT NULL )
PRIMARY INDEX ( ID );

 *** Table has been created.
 *** Total elapsed time was 1 second.

 BTEQ -- Enter your DBC/SQL request or BTEQ command:

CREATE SET TABLE MY_DB.PRUEBA02 ,
     NO FALLBACK ,
     NO BEFORE JOURNAL,
     NO AFTER JOURNAL,
     CHECKSUM = DEFAULT
     (
      ID    INTEGER     NOT NULL,
      CTXT  VARCHAR(10) NOT NULL )
PRIMARY INDEX ( ID );

CREATE SET TABLE MY_DB.PRUEBA02 ,
     NO FALLBACK ,
     NO BEFORE JOURNAL,
     NO AFTER JOURNAL,
     CHECKSUM = DEFAULT
     (
      ID    INTEGER     NOT NULL,
      CTXT  VARCHAR(10) NOT NULL )
PRIMARY INDEX ( ID );

 *** Table has been created.
 *** Total elapsed time was 1 second.

 BTEQ -- Enter your DBC/SQL request or BTEQ command:

INSERT INTO MY_DB.PRUEBA01 (ID, CTXT) VALUES (1, 'UNO');

INSERT INTO MY_DB.PRUEBA01 (ID, CTXT) VALUES (1, 'UNO');

 *** Insert completed. One row added.
 *** Total elapsed time was 1 second.

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
INSERT INTO MY_DB.PRUEBA01 (ID, CTXT) VALUES (2, 'DOS');

INSERT INTO MY_DB.PRUEBA01 (ID, CTXT) VALUES (2, 'DOS');

 *** Insert completed. One row added.
 *** Total elapsed time was 1 second.

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
INSERT INTO MY_DB.PRUEBA01 (ID, CTXT) VALUES (3, 'TRES');

INSERT INTO MY_DB.PRUEBA01 (ID, CTXT) VALUES (3, 'TRES');

 *** Insert completed. One row added.
 *** Total elapsed time was 1 second.

 BTEQ -- Enter your DBC/SQL request or BTEQ command:

INSERT INTO MY_DB.PRUEBA02 (ID, CTXT) VALUES (1, 'ONE');

INSERT INTO MY_DB.PRUEBA02 (ID, CTXT) VALUES (1, 'ONE');

 *** Insert completed. One row added.
 *** Total elapsed time was 1 second.

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
INSERT INTO MY_DB.PRUEBA02 (ID, CTXT) VALUES (2, 'TWO');

INSERT INTO MY_DB.PRUEBA02 (ID, CTXT) VALUES (2, 'TWO');

 *** Insert completed. One row added.
 *** Total elapsed time was 1 second.

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
INSERT INTO MY_DB.PRUEBA02 (ID, CTXT) VALUES (3, 'THREE');

INSERT INTO MY_DB.PRUEBA02 (ID, CTXT) VALUES (3, 'THREE');

 *** Insert completed. One row added.
 *** Total elapsed time was 1 second.

 BTEQ -- Enter your DBC/SQL request or BTEQ command:

Las tablas quedan tal que así:

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT *
  FROM MY_DB.PRUEBA01;

SELECT *
  FROM MY_DB.PRUEBA01;

 *** Query completed. 3 rows found. 2 columns returned.
 *** Total elapsed time was 1 second.

         ID  CTXT
-----------  ----------
          2  DOS
          3  TRES
          1  UNO

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT *
  FROM MY_DB.PRUEBA02;

SELECT *
  FROM MY_DB.PRUEBA02;

 *** Query completed. 3 rows found. 2 columns returned.
 *** Total elapsed time was 1 second.

         ID  CTXT
-----------  ----------
          2  TWO
          3  THREE
          1  ONE

 BTEQ -- Enter your DBC/SQL request or BTEQ command:

Ahora vamos con la query: un ‘OUTER JOIN’ ANSI con una condición ‘WHERE’:

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT a.ID,
       a.CTXT,
       b.CTXT
  FROM MY_DB.PRUEBA01 a
  LEFT OUTER JOIN MY_DB.PRUEBA02 b
             ON a.ID = b.ID
 WHERE a.ID > 1;

SELECT a.ID,
       a.CTXT,
       b.CTXT
  FROM MY_DB.PRUEBA01 a
  LEFT OUTER JOIN MY_DB.PRUEBA02 b
             ON a.ID = b.ID
 WHERE a.ID > 1;

 *** Query completed. 2 rows found. 3 columns returned.
 *** Total elapsed time was 1 second.

         ID  CTXT        CTXT
-----------  ----------  ----------
          2  DOS         TWO
          3  TRES        THREE

OK. Ahora vamos a escribir la misma ‘query’ de otra manera para obtener los mismos resultados:

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT a.ID,
       a.CTXT,
       b.CTXT
  FROM MY_DB.PRUEBA01 a
  LEFT OUTER JOIN MY_DB.PRUEBA02 b
             ON a.ID = b.ID
   AND a.ID > 1;

SELECT a.ID,
       a.CTXT,
       b.CTXT
  FROM MY_DB.PRUEBA01 a
  LEFT OUTER JOIN MY_DB.PRUEBA02 b
             ON a.ID = b.ID
   AND a.ID > 1;

 *** Query completed. 3 rows found. 3 columns returned.
 *** Total elapsed time was 1 second.

         ID  CTXT        CTXT
-----------  ----------  ----------
          2  DOS         TWO
          3  TRES        THREE
          1  UNO         (null)

 BTEQ -- Enter your DBC/SQL request or BTEQ command:

¿Cómo? ¡Aparece una última línea que no debería aparecer! ¿Qué es esto? ¿Acaso un ‘bug’ de Teradata? No. No hay nada errónero en el resultado, lo erróneo está en nuestra cabeza y lo que esperamos ver. Quizá apereca más claro si lo escribimos así:

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT a.ID,
       a.CTXT,
       b.CTXT
  FROM MY_DB.PRUEBA01 a
  LEFT OUTER JOIN MY_DB.PRUEBA02 b
             ON ( a.ID = b.ID
                  AND
                  a.ID > 1 )
;

SELECT a.ID,
       a.CTXT,
       b.CTXT
  FROM MY_DB.PRUEBA01 a
  LEFT OUTER JOIN MY_DB.PRUEBA02 b
             ON ( a.ID = b.ID
                  AND
                  a.ID > 1 )
;

 *** Query completed. 3 rows found. 3 columns returned.
 *** Total elapsed time was 1 second.

         ID  CTXT        CTXT
-----------  ----------  ----------
          2  DOS         TWO
          3  TRES        THREE
          1  UNO         (null)

Ahora se ilumina una lucecilla y empezamos a ver qué es lo que está pasando: Teradata tiene razón y está simplemente siguiendo las reglas y mecanismos de los ‘outer joins’. En el primer ejemplo efectúa un ‘outer join’ con la condición ‘ON a.ID = b.ID’ y a este resultado le aplica el filtro del ‘where’: ‘WHERE a.ID > 1′, donde se elimina la fila correspondiente a a.ID = 1.

En el segundo ejemplo combina todas las filas de la tabla MY_DB.PRUEBA01 a con las de la tabla MY_DB.PRUEBA02 b y las que no cumplen la condición (aquí es donde está la trampa) ‘AND a.ID > 1′ les asigna filas ‘nulas’ de MY_DB.PRUEBA02 b. De ahí que aparezca la fila “1, ‘UNO’, (null)”: No hay filtro que elimine luego esa fila.

Así que las partículas ‘WHERE a.ID > 1′ y ‘AND a.ID > 1′ aunque parezcan iguales, no lo son.

Esto no es algo propio de Teradata. Oracle funciona de la misma manera:

SQL*Plus: Release 10.2.0.1.0 - Production on Mié Jun 3 11:33:20 2009

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

Introduzca el nombre de usuario: carlos@XE.localhost
Introduzca la contraseña:

Conectado a:
Oracle Database 10g Express Edition Release 10.2.0.1.0 - Production

CARLOS@XE.localhost> CREATE TABLE PRUEBA01(IDN INTEGER NOT NULL,
  2                        CTXT  VARCHAR2(10) NOT NULL,
  3                        CONSTRAINT PRUEBA01_PK PRIMARY KEY(IDN)
  4                        )
  5  ;

Tabla creada.

CARLOS@XE.localhost> CREATE TABLE PRUEBA02(IDN INTEGER NOT NULL,
  2                        CTXT  VARCHAR2(10) NOT NULL,
  3                        CONSTRAINT PRUEBA02_PK PRIMARY KEY(IDN)
  4                        )
  5  ;

Tabla creada.

CARLOS@XE.localhost> INSERT INTO PRUEBA01(IDN, CTXT) VALUES (1,'UNO');

1 fila creada.

CARLOS@XE.localhost> INSERT INTO PRUEBA01(IDN, CTXT) VALUES (2,'DOS');

1 fila creada.

CARLOS@XE.localhost> INSERT INTO PRUEBA01(IDN, CTXT) VALUES (3,'TRES');

1 fila creada.

CARLOS@XE.localhost>
CARLOS@XE.localhost> INSERT INTO PRUEBA02(IDN, CTXT) VALUES (1,'ONE');

1 fila creada.

CARLOS@XE.localhost> INSERT INTO PRUEBA02(IDN, CTXT) VALUES (2,'TWO');

1 fila creada.

CARLOS@XE.localhost> INSERT INTO PRUEBA02(IDN, CTXT) VALUES (3,'THREE');

1 fila creada.

CARLOS@XE.localhost> commit;

Confirmación terminada.

CARLOS@XE.localhost> SELECT a.IDN,
  2         a.CTXT,
  3         b.CTXT
  4    FROM PRUEBA01 a
  5    LEFT OUTER JOIN PRUEBA02 b
  6               ON a.IDN = b.IDN
  7   WHERE a.IDN > 1;

       IDN CTXT       CTXT
---------- ---------- ----------
         2 DOS        TWO
         3 TRES       THREE

CARLOS@XE.localhost> SELECT a.IDN,
  2         a.CTXT,
  3         b.CTXT
  4    FROM PRUEBA01 a
  5    LEFT OUTER JOIN PRUEBA02 b
  6               ON a.IDN = b.IDN
  7     AND a.IDN > 1;

       IDN CTXT       CTXT
---------- ---------- ----------
         1 UNO
         2 DOS        TWO
         3 TRES       THREE

Por último, los ‘oracleros’ pueden ver a cual de las dos sintaxis ANSI corresponde su sintaxis nativa:

CARLOS@XE.localhost> SELECT a.IDN,
  2         a.CTXT,
  3         b.CTXT
  4    FROM PRUEBA01 a,
  5         PRUEBA02 b
  6   WHERE a.IDN = b.IDN(+)
  7     AND a.IDN > 1;

       IDN CTXT       CTXT
---------- ---------- ----------
         2 DOS        TWO
         3 TRES       THREE

CARLOS@XE.localhost>

En efecto: con la ‘buena’: aquí el ‘AND’ se considera en el ‘WHERE’.

Saludos.

Carlos.


Oracle compra Sun (II)

21 Abril 2009

La nota oficial aquí.

Me temo que corren malos tiempos para MySQL…

Saludos.

Carlos.


¡¡Oracle compra Sun!!

20 Abril 2009

¡Oracle compra Sun microsystems por 7.400 Millones de dólares!

Más información: aquí, aquí, aquí y aquí.

¿Qué será lo próximo? ¿unbreakable solaris?

Saludos.

Carlos.


Insertar ficheros de imagen en Teradata como BLOBs usando Windows Scripting Host y OLEDB/ADO.

23 Noviembre 2008

Las Bases de Datos actuales -y más las Data Warehouses- almacenan ya no sólo datos de los tipos básicos (caracteres, números, fechas) sino que cada vez más son requeridas para almacenar datos heterogéneos: documentos (.pdf, .doc …), imágenes, sonidos, etc…

Teradata -como casi todos los SGBDRs- maneja datos binarios grandes (BLOBs -Binary Large OBjects-) y es capaz de almacenarlos de forma eficiente. La documentación invita a utilizar estas capacidades, pero aparece la eterna pregunta: ¿Cómo cargo esos datos en la Base de Datos?

Las utilidades Teradata ofrecen pocas alternativas: las únicas que son capaces de llevar a cabo la tarea son ‘SQL Assistant’ y ‘bteq’, y ambas con funcionalidad limitada.

Así que si se necesita una solución rápida y fácil para incorporar los datos binarios, enseguida se piensa en Windows Scripting Host y Visual Basic Script por su simplicidad y facilidad de programación. (He de decir que, aunque no soy ningún fan de Microsoft, encuentro que el WSH es una muy buena herramienta)

No nos podemos olvidar del acceso a la base de datos: personalmente me gusta OLEDB, con lo que una instalación del Teradata OLEDB provider es necesaria. Utilizaremos ADO como capa de acceso a la base de Datos.

Con todos estos ingredientes podemos comenzar la tarea: cargar imágenes (pero podría ser cualquier otro tipo de datos binario) a partir de ficheros .jpg en Teradata.

Lo primero, vamos a crear una tabla que contenga, por ejemplo, el nombre del fichero y la imagen como columna BLOB:

 BTEQ -- Enter your DBC/SQL request or BTEQ command:

CREATE SET TABLE MY_DATABASE.PRUEBA_BLOB,
           NO FALLBACK,
           NO JOURNAL
           (
              NOM_FICHERO VARCHAR(64) NOT NULL  CHARACTER SET LATIN,
              BLOB_IMAGE  BLOB(2M)        NULL
           )
           UNIQUE PRIMARY INDEX (NOM_FICHERO)
;

 *** Table has been created.
 *** Total elapsed time was 1 second.

 BTEQ -- Enter your DBC/SQL request or BTEQ command:

La columna BLOB se puede dejar sin tamaño, pero esto hará que tome el valor por defecto: 2 Gb. ¿Un poco excesivo, no? Nos conformamos con unas muy generosas 2 megas por imagen.

Con la tabla creada nos sumergimos en el mundo del Visual Basic Script:

Const CONNECTIONSTRING = "Provider=TDOLEDB;Data Source=MY_DW;
User Id=myuser;password=mypass;" 'OLEDB
Dim vNombreFichero

'Conexión
Dim objConnection

Main

Sub Main

   If WScript.Arguments.Length  1 Then
      vNombreFichero = InputBox("Introduzca el nombre del fichero a tratar:", "PruebaBLOB.vbs")
      If vNombreFichero = "" Or IsNull(vNombreFichero) Then
         WScript.Echo "ERROR: Falta fichero a tratar." & Chr(13) & Chr(10) & Chr(13) & Chr(10) & "(Uso: PruebaBLOB.vbs )"
         WScript.Quit
      End If
   Else
      vNombreFichero = WScript.Arguments(0)
   End If

   Wscript.Echo "Let's roll..." 

   AbrirConexion

   CerrarConexion

   Wscript.Echo "Bye."
End Sub

Sub AbrirConexion

   Set objConnection = WScript.CreateObject("ADODB.Connection")

   objConnection.ConnectionString = CONNECTIONSTRING
   objConnection.Open()

   Wscript.Echo "Conectando a Teradata..."

   If Err.Number  0 Then
      Wscript.Echo "Err# " & Err.Number & " - " & Err.Description
   Else
      Wscript.Echo "Connected to database as " & objConnection.Properties("User Name") & " en " & objConnection.Properties("Server Name") & "."
   End If
End Sub

Sub CerrarConexion
   objConnection.Close()
   Set objConnection = Nothing
End Sub

Esto no es más que probar la conexión -abrir y cerrar- y una comprobación básica de que debe llegar el nombre del fichero como argumento:

C:\Procesos\CargarBLOBs>cscript PruebaBLOB.vbs  foto1.jpg
Microsoft (R) Windows Script Host versión 5.6
Copyright (C) Microsoft Corporation 1996-2001. Reservados todos los derechos.

Let's roll...
Conectando a Teradata...
Connected to database as myuser en MY_DW.
Bye.

C:\Procesos\CargarBLOBs>

Así pues, todo parece funcionar (‘so far, so good…‘)

Lo bueno empieza ahora. La primera idea que nos viene a la cabeza es utilizar los FileSystemObjects del WSH para abrir el fichero, leerlo e insertarlo en la base de datos. Pero la cosa no va por ahí: el manejo de ficheros que se proporciona en el WSH y sus ’streams’ están pensados para trabajar únicamente en modo texto, por lo que no nos sirven de mucho.

Los tiros van por otro lado: Las últimas versiones de ADO incorporan un objeto que permite trabajar con datos binarios: ‘ADODB.Stream’.

En efecto, con objetos ADODB.Stream podemos manipular ficheros binarios:

Sub InsertarImagen

   'Cargar BLOB con fichero
   Const adTypeBinary = 1

   'Create Stream object
   Dim inStream
   Dim vBuffer
   Set inStream = CreateObject("ADODB.Stream")
   inStream.Open
   inStream.type=adTypeBinary

   Wscript.Echo "Cargando Fichero Imagen..."
   inStream.LoadFromFile vNombreFichero

   Wscript.Echo "Tamaño de la imagen: " & inStream.Size
   ...

Para ver que la cosa funciona bastará hacer una prueba y verificar que inStream.Size da el tamaño del fichero de imagen:

C:\Procesos\CargarBLOBs>cscript PruebaBLOB.vbs  foto1.jpg
Microsoft (R) Windows Script Host versión 5.6
Copyright (C) Microsoft Corporation 1996-2001. Reservados todos los derechos.

Let's roll...
Conectando a Teradata...
Connected to database as myuser en MY_DW.
Cargando Fichero Imagen...
Tamaño de la imagen: 710312
Bye.

C:\Procesos\CargarBLOBs>

Así pues, vemos que el objeto ADO.Stream funciona según lo esperado.

Siguiente asunto: ¿qué tipo de dato ADO utilizamos para ‘mapear’ el dato BLOB? Esta es fácil: la documentación de ADO lo indica claramente: LongVarBinary.

Aquí hay dos cosas a tener en cuenta: Primero, en ADO todos los parámetros de longitud variable necesitan un argumento de tamaño en la creación del objeto:

Si hacemos:

   Set objParm2 = objCommand.CreateParameter("blob_image", adLongVarBinary,
adParamInput)
   objCommand.Parameters.Append objParm2

Tendremos que:

C:\Procesos\CargarBLOBs\PruebaBLOB.vbs(157, 4) ADODB.Parameters:
Objeto Parameter mal definido. Se proporcionó información incompleta o incoherente.

Por lo que hay que definir el parámetro con tamaño:

   Set objParm2 = objCommand.CreateParameter("blob_image", adLongVarBinary,
adParamInput, 2097152)
   objCommand.Parameters.Append objParm2

La segunda cuestión a recordar es que debemos asignar el valor del parámetro no con ‘Value’, sino con AppendChunk.

 objParm2.AppendChunk inStream.Read

Teniendo en cuenta estas dos cosas podemos ya ejecutar un ‘ADO.Command’ con el ‘INSERT’:

   ...
   Dim objCommand
   Dim sCommandText
   Dim objParm1,objParm2

   sCommandText = "INSERT INTO MY_DATABASE.PRUEBA_BLOB(NOM_FICHERO, BLOB_IMAGE) " & _
                  "VALUES ( ?, ? )"

   Set objCommand = Wscript.CreateObject("ADODB.Command")

   objCommand.CommandType = adCmdText

   objCommand.CommandText = sCommandText 

   'Command Prepared
   objCommand.Prepared = True

   objCommand.ActiveConnection = objConnection

   'Parámetros:
   Set objParm1 = objCommand.CreateParameter("nom_fichero", adVarChar,
adParamInput, 64 )
   objCommand.Parameters.Append objParm1

   objParm1.Value = vNombreFichero

   Set objParm2 = objCommand.CreateParameter("blob_image", adLongVarBinary,
adParamInput, 2097152) 'inStream.Size
   objCommand.Parameters.Append objParm2

   objParm2.AppendChunk inStream.Read

   objCommand.Execute

   'Close the stream
   inStream.Close()
   Set inStream = Nothing

   Set objCommand = Nothing

End Sub

Ejecutamos:

C:\Procesos\CargarBLOBs>cscript PruebaBLOB.vbs  foto1.jpg
Microsoft (R) Windows Script Host versión 5.6
Copyright (C) Microsoft Corporation 1996-2001. Reservados todos los derechos.

Let's roll...
Conectando a Teradata...
Connected to database as myuser en MY_DW.
Cargando Fichero Imagen...
Tamaño de la imagen: 710312

C:\Procesos\CargarBLOBs\PruebaBLOB.vbs(161, 4)
OLE DB Provider for Teradata: [NCR] Invalid precision: cbColDef value out of range

¡ERROR! ¿Por qué? Hemos hecho todo según la documentación. ¿Se nos ha pasado algo?

Sí. La respuesta está en la documentación de Teradata: ‘OLE DB Provider for Teradata’. En un lugar recóndito se nos dice que hay que habilitar el tratamiento de LOBs mediante las ‘extended properties’ del OLEDB provider:

Const CONNECTIONSTRING = "Provider=TDOLEDB;Data Source=MY_DW;
User Id=myuser;password=mypass;
Extended Properties=""EnableLOBSupport=Yes;"";" 'OLEDB EnableLOBSupport

Y ahora todo va a ir OK:

C:\Procesos\CargarBLOBs>cscript PruebaBLOB.vbs  foto1.jpg
Microsoft (R) Windows Script Host versión 5.6
Copyright (C) Microsoft Corporation 1996-2001. Reservados todos los derechos.

Let's roll...
Conectando a Teradata...
Connected to database as myuser en MY_DW.
Cargando Fichero Imagen...
Tamaño de la imagen: 710312
Bye.

C:\Procesos\CargarBLOBs>

Y vemos que la inserción se realizó:

 BTEQ -- Enter your DBC/SQL request or BTEQ command:
SELECT * FROM  MY_DATABASE.PRUEBA_BLOB;

SELECT * FROM  MY_DATABASE.PRUEBA_BLOB;

 *** Query completed. One row found. 2 columns returned.
 *** Total elapsed time was 1 second.

NOM_FICHERO  BLOB_IMAGE
------------ -------------------------------------------------------

foto1.jpg    FFD8FFE000104A46494600010101012C012C0000FFFE0029433A5C5

Evidentemente, BTEQ sólo nos muestra el contenido hexadecimal de la imagen. Si queremos ver la foto deberemos utilizar el ‘SQL Assisstant’ o algún otro ’software’, pero la imagen está ahí. Y eso era lo que queríamos.

Una última cosa: el ejemplo mostrado aquí es fácilmente portable a Visual Basic. Sólo hay que tener en cuenta las diferencias que existen entre VB y VBScript.

Saludos.

Carlos.


Sun Data Warehouse Appliance.

14 Octubre 2008

Sun ha sacado su contraparte a la HP Oracle Database Machine: Sun Data Warehouse Appliance, se trata de un “join venture” con Greenplum, un (no demasiado conocido, al menos por mí) software de base de datos de “Data Warehousing” que bebe, como Teradata, en los principios del “share nothing”.

Saludos.

Carlos.