Módulo 31: Consultas básicas

Objetivos del módulo

  • Situar la sentencia SELECT dentro del DML y relacionarla con el álgebra relacional
  • Conocer la sintaxis general de SELECT y, sobre todo, su orden lógico de evaluación
  • Seleccionar columnas, calcular expresiones aritméticas y usar alias, literales y concatenación
  • Eliminar filas repetidas con DISTINCT y usar la tabla DUAL (y el SELECT sin FROM de Oracle 23ai)
  • Filtrar filas con WHERE: comparaciones, BETWEEN, IN, LIKE, IS NULL y operadores lógicos
  • Entender la lógica trivaluada y los problemas que causan los valores nulos, incluido el temido NOT IN con NULL
  • Ordenar resultados con ORDER BY (alias, posición, expresiones, NULLS FIRST/LAST)
  • Limitar y paginar resultados con FETCH FIRST, WITH TIES, PERCENT, OFFSET, y conocer ROWNUM y ROWID
  • Usar variables de sustitución de SQL*Plus para hacer consultas reutilizables
  • Resolver más de 40 consultas sobre el esquema EMPRESA

1. Del DDL al DML: consultar es preguntar

En el bloque anterior construiste las tablas del esquema EMPRESA con el lenguaje de definición de datos (DDL). Ahora empieza la parte que vas a usar el 90 % de tu vida profesional: el lenguaje de manipulación de datos (Data Manipulation Language, DML), que sirve para consultar y modificar el contenido de esas tablas.

📘 Concepto: El DML de SQL tiene cuatro sentencias principales: SELECT (consultar), INSERT (insertar), UPDATE (modificar) y DELETE (borrar), más MERGE (insertar o modificar). Algunos autores llaman a SELECT por separado DQL (Data Query Language), porque es la única que no cambia los datos.

Una consulta es una pregunta que le haces a la base de datos: “¿qué empleados trabajan en Valencia?”, “¿qué productos no tienen stock?”, “¿cuáles son los cinco pedidos más grandes?”. El SGBD responde siempre con lo mismo: una tabla (un conjunto de filas y columnas), aunque tenga una sola fila, una sola columna o ninguna fila. Esa propiedad de cierre es la que permite, más adelante, usar el resultado de una consulta dentro de otra.

💡 Consejo: Piensa en SELECT como en un filtro de hoja de cálculo muy potente: eliges qué columnas ver (proyección), qué filas te interesan (selección) y en qué orden mostrarlas. La gran diferencia es que SQL es declarativo: dices qué quieres, no cómo obtenerlo. El optimizador de Oracle decide el camino.

1.1. SELECT y el álgebra relacional

En el módulo 18 estudiaste el álgebra relacional. Las dos operaciones básicas tienen traducción directa a SQL:

Álgebra relacional SQL Qué hace
π[nombre, salario](EMPLEADOS) (proyección) SELECT nombre, salario FROM empleados Elige columnas
σ[dept_id = 30](EMPLEADOS) (selección) SELECT * FROM empleados WHERE dept_id = 30 Elige filas
π[nombre](σ[dept_id = 30](EMPLEADOS)) SELECT nombre FROM empleados WHERE dept_id = 30 Las dos cosas

⚠️ Importante: Hay una diferencia teórica fundamental: en el álgebra relacional una relación es un conjunto y no tiene filas repetidas; en SQL el resultado de un SELECT es un multiconjunto (bag) y sí puede tener filas repetidas. Para eliminarlas hay que pedirlo explícitamente con DISTINCT (apartado 4).

1.2. Preparar la sesión de trabajo

Todas las salidas de este módulo se han obtenido en Oracle AI Database 26ai Free con SQL*Plus, conectado con un usuario que tiene cargado el esquema EMPRESA (empresa_crear.sql y empresa_datos.sql). Antes de empezar se configura la sesión así:

-- Formato de pantalla de SQL*Plus (en SQL Developer no hace falta)
SET LINESIZE 200 PAGESIZE 100 FEEDBACK ON
-- Mostrar los valores nulos como (nulo) en lugar de un hueco en blanco
SET NULL '(nulo)'
-- Idioma y convenciones regionales de España
ALTER SESSION SET NLS_LANGUAGE = 'SPANISH';
ALTER SESSION SET NLS_TERRITORY = 'SPAIN';
-- Formato de fecha (SIEMPRE después de NLS_TERRITORY)
ALTER SESSION SET NLS_DATE_FORMAT = 'DD/MM/YYYY';
Sesión modificada.

Sesión modificada.

Sesión modificada.

SET LINESIZE, SET PAGESIZE, SET FEEDBACK y SET NULL son comandos *de SQLPlus** (no de SQL): solo cambian la presentación en pantalla. Gracias a SET NULL '(nulo)' verás claramente qué celdas son nulas en todas las salidas del curso; sin él, SQL*Plus deja la celda en blanco y SQL Developer muestra (null).

💡 Consejo: SQLPlus reserva para cada columna de texto el ancho máximo que podría tener (60 caracteres para clientes.nombre, hasta 4000 para el resultado de algunas funciones), y si la fila no cabe en LINESIZE la parte en varias líneas. Para que las salidas del curso se lean bien, las columnas de texto se muestran con el ancho de su contenido, que es lo que conseguirías con el comando COLUMN nombre FORMAT A27 de SQLPlus (módulo 28) o lo que ves directamente en la rejilla de SQL Developer. Los valores son exactamente los que devuelve Oracle.

Los parámetros NLS (National Language Support) controlan cómo se muestran fechas y números:

Parámetro Valor Efecto en las salidas
NLS_LANGUAGE SPANISH Nombres de meses y días en español; además fija NLS_SORT = SPANISH (ordenación alfabética española)
NLS_TERRITORY SPAIN Coma decimal y punto de miles (2,5 en vez de 2.5), símbolo €, semana que empieza el lunes
NLS_DATE_FORMAT DD/MM/YYYY Las fechas se muestran como 15/01/2025

⚠️ Importante: Cambiar NLS_TERRITORY reinicia NLS_DATE_FORMAT al valor por defecto del territorio (DD/MM/RR en España). Por eso el formato de fecha se fija en último lugar. Y recuerda: estos parámetros solo afectan a cómo se muestran los datos, nunca a cómo se guardan.

💡 Consejo: La imagen Docker de Oracle Free solo incluye los mensajes en inglés, por eso los errores aparecen como ORA-00942: table or view ... does not exist. En una instalación en español verías el mismo código con el texto traducido. Lo importante es siempre el código ORA-nnnnn, que es igual en todos los idiomas y es lo que debes buscar en la documentación. En las salidas del curso, los mensajes propios de SQL*Plus se muestran en español (3 filas seleccionadas., Sesión modificada., ERROR en línea 1:), tal y como aparecen en una instalación en español, mientras que el texto de los errores ORA se deja exactamente como lo devuelve el servidor.


2. Sintaxis general y orden lógico de evaluación

2.1. Las cláusulas de SELECT

Esta es la estructura completa de una consulta sobre una tabla (las combinaciones de varias tablas llegarán en el módulo 30 y las agrupaciones en el 25):

SELECT   [DISTINCT] lista_de_columnas_o_expresiones   -- 5 y 6. qué columnas mostrar
FROM     tabla [alias]                                 -- 1. de dónde salen los datos
[WHERE   condición_sobre_filas]                        -- 2. qué filas se quedan
[GROUP BY expresiones_de_agrupación]                   -- 3. cómo se agrupan
[HAVING  condición_sobre_grupos]                       -- 4. qué grupos se quedan
[ORDER BY expresiones [ASC | DESC] [NULLS FIRST | NULLS LAST]]   -- 7. en qué orden
[OFFSET n ROWS] [FETCH FIRST m ROWS ONLY];             -- 8. cuántas filas devolver

Las únicas cláusulas obligatorias son SELECT y (hasta Oracle 21c) FROM. Las demás son opcionales, pero si aparecen, lo hacen en ese orden escrito.

2.2. El orden en que Oracle “piensa” la consulta

La consulta se escribe empezando por SELECT, pero se evalúa lógicamente en otro orden. Entender esto evita la mitad de los errores de principiante.

flowchart LR
    F["1. FROM<br/>tabla de origen"] --> W["2. WHERE<br/>filtra filas"]
    W --> G["3. GROUP BY<br/>forma grupos"]
    G --> H["4. HAVING<br/>filtra grupos"]
    H --> S["5. SELECT<br/>calcula columnas y alias"]
    S --> D["6. DISTINCT<br/>quita repetidos"]
    D --> O["7. ORDER BY<br/>ordena"]
    O --> L["8. OFFSET / FETCH<br/>limita filas"]

📘 Concepto: El orden lógico de evaluación describe el significado de la consulta, no necesariamente cómo la ejecuta físicamente el optimizador (que puede usar índices, reordenar operaciones, etc.). Pero el resultado siempre es como si se hubiera seguido este orden.

Consecuencias prácticas que verás a lo largo del módulo:

Consecuencia Por qué
Un alias de columna no se puede usar en WHERE WHERE se evalúa (paso 2) antes de que SELECT cree el alias (paso 5)
Un alias de columna sí se puede usar en ORDER BY ORDER BY (paso 7) va después de SELECT
Se puede ordenar por una columna que no se muestra En el paso 7 la fila todavía “conoce” todas sus columnas (salvo con DISTINCT)
FETCH FIRST actúa sobre el resultado ya ordenado Es el último paso
ROWNUM se asigna antes de ORDER BY ROWNUM se numera al leer las filas, en el paso 2 (la trampa del apartado 10.5)

Compruébalo: el alias anual existe en ORDER BY pero no en WHERE.

SELECT nombre, salario * 12 AS anual
FROM empleados
WHERE anual > 50000;
WHERE anual > 50000
      *
ERROR en línea 3:
ORA-00904: "ANUAL": invalid identifier
Help: https://docs.oracle.com/error-help/db/ora-00904/
SELECT nombre, salario * 12 AS anual
FROM empleados
WHERE salario * 12 > 50000      -- en WHERE hay que repetir la expresión
ORDER BY anual DESC;
NOMBRE      ANUAL
------ ----------
Ana         78000
Marta       60000
Luis        57600
Laura       51600
Pedro       50400

5 filas seleccionadas.

⚠️ Importante: ORA-00904: invalid identifier es el error más frecuente al empezar: aparece cuando escribes mal un nombre de columna o cuando usas un alias donde todavía no existe. Lee siempre el asterisco que SQL*Plus pone debajo de la palabra culpable.


3. Seleccionar columnas

3.1. Todas las columnas: SELECT *

El asterisco significa “todas las columnas de la tabla, en el orden en que se definieron”:

SELECT * FROM departamentos;
   DEPT_ID NOMBRE        LOCALIDAD PRESUPUESTO
---------- ------------- --------- -----------
        10 DIRECCIÓN     Madrid         500000
        20 VENTAS        Madrid         350000
        30 INFORMÁTICA   Valencia       420000
        40 CONTABILIDAD  Sevilla        150000
        50 MARKETING     Barcelona      200000
        60 LOGÍSTICA     Zaragoza       180000
        70 INVESTIGACIÓN Bilbao    (nulo)

7 filas seleccionadas.

Antes de consultar una tabla que no conoces, consulta su estructura con el comando DESCRIBE (abreviado DESC) de SQL*Plus, SQLcl y SQL Developer:

DESC empleados
 Name                                      Null?    Type
 ----------------------------------------- -------- ----------------------------
 EMP_ID                                    NOT NULL NUMBER(6)
 NOMBRE                                    NOT NULL VARCHAR2(30)
 APELLIDOS                                 NOT NULL VARCHAR2(50)
 EMAIL                                     NOT NULL VARCHAR2(60)
 TELEFONO                                           VARCHAR2(15)
 FECHA_ALTA                                NOT NULL DATE
 OFICIO                                    NOT NULL VARCHAR2(30)
 SALARIO                                   NOT NULL NUMBER(8,2)
 COMISION                                           NUMBER(8,2)
 JEFE_ID                                            NUMBER(6)
 DEPT_ID                                            NUMBER(4)

💡 Consejo: SELECT * es perfecto para explorar, pero evítalo en programas y vistas: si mañana alguien añade una columna a la tabla, el resultado cambia y la aplicación puede romperse; además traes datos que no necesitas. Enumera siempre las columnas que vas a usar.

3.2. Columnas concretas

Escribe los nombres separados por comas, en el orden en que quieres verlas (no tiene por qué coincidir con el de la tabla):

SELECT apellidos, nombre, oficio
FROM empleados
WHERE dept_id = 30;
APELLIDOS      NOMBRE OFICIO
-------------- ------ ----------------
Sánchez Gil    Marta  DIRECTOR
Moreno Herrera Pablo  ANALISTA
Molina Prieto  Raquel PROGRAMADOR
Castro Méndez  Iván   PROGRAMADOR
Ortiz Delgado  Sara   PROGRAMADOR
Rubio Marín    Hugo   TÉCNICO SISTEMAS

6 filas seleccionadas.

📘 Concepto: Los identificadores (nombres de tablas y columnas) no distinguen mayúsculas y minúsculas: SELECT NOMBRE FROM EMPLEADOS y select nombre from empleados son la misma consulta, porque Oracle los guarda en mayúsculas en el diccionario de datos. Los datos (el contenido de las cadenas) sí distinguen: 'Ana' no es igual a 'ANA'.

3.3. Expresiones aritméticas

En la lista del SELECT puedes escribir expresiones que se calculan para cada fila. Los operadores aritméticos son +, -, * y /, con la precedencia matemática habitual: primero * y /, después + y -, y de izquierda a derecha a igual precedencia. Los paréntesis cambian el orden.

SELECT 7 + 3 * 2, (7 + 3) * 2, 10 / 4, 20 / 4 / 2, -2 * -3
FROM dual;
     7+3*2    (7+3)*2       10/4     20/4/2      -2*-3
---------- ---------- ---------- ---------- ----------
        13         20        2,5        2,5          6

1 fila seleccionada.

Observa 10 / 4 = 2,5: en Oracle la división entre enteros no trunca (a diferencia de Java o C). Y la coma decimal aparece porque la sesión usa NLS_TERRITORY = 'SPAIN'.

Ejemplo real: subida del 3 % y salario anual en 14 pagas de los empleados de Contabilidad.

SELECT nombre,
       salario,
       salario * 1.03          AS salario_subido,
       salario * 14            AS anual_14_pagas,
       salario * 14 + 1000     AS anual_con_bonus,
       (salario + 100) * 14    AS anual_con_plus
FROM empleados
WHERE dept_id = 40;
NOMBRE    SALARIO SALARIO_SUBIDO ANUAL_14_PAGAS ANUAL_CON_BONUS ANUAL_CON_PLUS
------ ---------- -------------- -------------- --------------- --------------
Pedro        4200           4326          58800           59800          60200
Isabel       2600           2678          36400           37400          37800
Jorge        1650         1699,5          23100           24100          24500

3 filas seleccionadas.

⚠️ Importante: Una expresión calculada no modifica la tabla: salario * 1.03 solo existe en el resultado de la consulta. Para cambiar datos se usa UPDATE (módulo 34).

3.4. NULL en las expresiones

📘 Concepto: NULL representa un valor desconocido o inexistente. No es cero ni una cadena vacía. Cualquier operación aritmética con NULL da NULL: si no sabes cuánto vale la comisión, tampoco sabes cuánto vale el salario más la comisión.

SELECT emp_id, nombre, salario, comision, salario + comision AS total
FROM empleados
WHERE dept_id = 20;
    EMP_ID NOMBRE    SALARIO   COMISION      TOTAL
---------- ------ ---------- ---------- ----------
       101 Luis         4800 (nulo)     (nulo)
       110 Carmen       2200        500       2700
       111 David        2100        800       2900
       112 Elena        2300        300       2600
       113 Sergio       1900          0       1900
       114 Lucía        1700 (nulo)     (nulo)

6 filas seleccionadas.

Luis y Lucía no tienen comisión (NULL), así que su total también es NULL. Sergio tiene comisión 0, que sí es un valor, y su total es 1900. En el módulo 28 aprenderás a resolverlo con NVL(comision, 0) o COALESCE.

3.5. Alias de columna

Un alias es un nombre alternativo para una columna del resultado. Se escribe detrás de la expresión, opcionalmente precedido de AS:

Forma Ejemplo Resultado de la cabecera
Sin comillas salario * 14 AS anual o salario * 14 anual ANUAL (se pasa a mayúsculas)
Con comillas dobles salario * 14 AS "Salario anual" Salario anual (respeta mayúsculas, espacios y tildes)
SELECT nombre        AS "Nombre",
       apellidos     "Apellidos",
       salario * 14  AS "Salario anual (€)",
       oficio        puesto
FROM empleados
WHERE dept_id = 50;
Nombre Apellidos        Salario anual (€) PUESTO
------ ---------------- ----------------- ---------
Laura  Fernández Moreno             60200 DIRECTOR
Nuria  Gil Soto                     32200 DISEÑADOR
Álvaro Peña Cano                    37800 ANALISTA

3 filas seleccionadas.

⚠️ Importante: En Oracle las comillas dobles son para identificadores y alias; las comillas simples son para literales de texto. AS 'Salario' produce el error ORA-00923: FROM keyword not found where expected. Además, un alias entre comillas dobles es sensible a mayúsculas: si en ORDER BY escribes "nombre" en vez de "Nombre", Oracle no lo encontrará.

💡 Consejo: SQL*Plus recorta la cabecera de las columnas de texto al ancho máximo del dato (por eso a veces ves cabeceras cortadas, como CLIENTE_ en el apartado siguiente) y da a las numéricas un ancho de 10 caracteres. SQL Developer muestra una rejilla y no tiene ese problema. Es solo presentación: los datos son los mismos.

3.6. Literales

Un literal es un valor constante escrito en la consulta. Aparece igual en todas las filas:

Tipo Ejemplos Notas
Texto 'Madrid', 'O''Brien' Comillas simples; una comilla dentro se escribe duplicada
Texto con comillas alternativas q'[L'Hospitalet]', q'{It's}' El operador q permite usar comillas sin duplicarlas
Numérico 100, 3.14, -5, 1.5E3 El separador decimal en el código SQL es siempre el punto
Fecha DATE '2025-01-15' Formato ANSI AAAA-MM-DD, independiente de la sesión
Fecha y hora TIMESTAMP '2025-01-15 09:30:00' Formato ANSI
SELECT nombre, 'trabaja en el departamento' AS texto, dept_id,
       q'[L'Oréal]' AS cliente_vip, 1.5E3 AS numero, DATE '2025-01-15' AS fecha
FROM empleados
WHERE dept_id = 40;
NOMBRE TEXTO                         DEPT_ID CLIENTE_     NUMERO FECHA
------ -------------------------- ---------- -------- ---------- ----------
Pedro  trabaja en el departamento         40 L'Oréal        1500 15/01/2025
Isabel trabaja en el departamento         40 L'Oréal        1500 15/01/2025
Jorge  trabaja en el departamento         40 L'Oréal        1500 15/01/2025

3 filas seleccionadas.

⚠️ Importante: No confundas el separador decimal del código (siempre .: salario * 1.03) con el de la salida (depende de NLS_TERRITORY: en España, ,).

3.7. Concatenación: || y CONCAT

El operador || une cadenas. Los números y las fechas se convierten automáticamente a texto:

SELECT nombre || ' ' || apellidos AS empleado,
       'Gana ' || salario || ' € al mes desde el ' || fecha_alta AS frase
FROM empleados
WHERE dept_id = 60;
EMPLEADO               FRASE
---------------------- --------------------------------------
Javier López Navarro   Gana 4000 € al mes desde el 11/11/2014
Rosa Iglesias Cruz     Gana 1600 € al mes desde el 03/03/2017
Manuel Herrero Lozano  Gana 1600 € al mes desde el 17/10/2022
Cristina Medina Blanco Gana 1750 € al mes desde el 22/07/2019

4 filas seleccionadas.

La función CONCAT hace lo mismo con notación de función:

SELECT CONCAT(nombre, apellidos) AS pegado,
       CONCAT(CONCAT(nombre, ' '), apellidos) AS con_espacio
FROM empleados
WHERE dept_id = 40;
PEGADO               CON_ESPACIO
-------------------- ---------------------
PedroGómez Díaz      Pedro Gómez Díaz
IsabelDomínguez Vega Isabel Domínguez Vega
JorgeVázquez Ramos   Jorge Vázquez Ramos

3 filas seleccionadas.

⚠️ Importante: En Oracle 19c y 21c CONCAT admite exactamente dos argumentos (con tres da ORA-00909: invalid number of arguments), por eso es habitual anidarla como en el ejemplo. La versión 26ai de nuestro contenedor ya acepta más de dos, pero para escribir código portable usa ||, que funciona en todas las versiones y es más legible.

Una peculiaridad muy importante de Oracle: la cadena vacía '' es lo mismo que NULL, y al concatenar, NULL se trata como cadena vacía (en el estándar SQL y en otros SGBD, 'a' || NULL da NULL):

SELECT 'a' || NULL || 'b' AS concatenado,
       nombre || ' - tel: ' || telefono AS contacto
FROM empleados
WHERE dept_id = 40;
CO CONTACTO
-- -----------------------
ab Pedro - tel: 600100103
ab Isabel - tel: 600100130
ab Jorge - tel:

3 filas seleccionadas.

Jorge no tiene teléfono y la concatenación no se anula: simplemente no aparece nada detrás de tel:.


4. Eliminar filas repetidas: DISTINCT

Si consultas el oficio de todos los empleados, cada oficio aparece tantas veces como empleados lo tengan:

SELECT oficio FROM empleados WHERE dept_id IN (20, 30);
OFICIO
----------------
DIRECTOR
DIRECTOR
VENDEDOR
VENDEDOR
VENDEDOR
VENDEDOR
ADMINISTRATIVO
ANALISTA
PROGRAMADOR
PROGRAMADOR
PROGRAMADOR
TÉCNICO SISTEMAS

12 filas seleccionadas.

DISTINCT (o su sinónimo UNIQUE, propio de Oracle) elimina las filas repetidas del resultado:

SELECT DISTINCT oficio
FROM empleados
ORDER BY oficio;
OFICIO
----------------
ADMINISTRATIVO
ANALISTA
BECARIO
CONTABLE
DIRECTOR
DIRECTOR GENERAL
DISEÑADOR
OPERARIO
PROGRAMADOR
TÉCNICO SISTEMAS
VENDEDOR

11 filas seleccionadas.

4.1. DISTINCT con varias columnas

DISTINCT se aplica a la fila completa del resultado, no a la primera columna. Se elimina una fila si todas sus columnas coinciden con otra:

SELECT DISTINCT dept_id, oficio
FROM empleados
ORDER BY dept_id, oficio;
   DEPT_ID OFICIO
---------- ----------------
        10 DIRECTOR GENERAL
        20 ADMINISTRATIVO
        20 DIRECTOR
        20 VENDEDOR
        30 ANALISTA
        30 DIRECTOR
        30 PROGRAMADOR
        30 TÉCNICO SISTEMAS
        40 ADMINISTRATIVO
        40 CONTABLE
        40 DIRECTOR
        50 ANALISTA
        50 DIRECTOR
        50 DISEÑADOR
        60 ADMINISTRATIVO
        60 DIRECTOR
        60 OPERARIO
(nulo)     BECARIO

18 filas seleccionadas.

Hay un único VENDEDOR en el departamento 20 (aunque son cuatro empleados), pero ADMINISTRATIVO aparece tres veces porque está en los departamentos 20, 40 y 60: las parejas (20, ADMINISTRATIVO), (40, ADMINISTRATIVO) y (60, ADMINISTRATIVO) son distintas.

⚠️ Importante: DISTINCT solo puede ir una vez, justo detrás de SELECT. SELECT dept_id, DISTINCT oficio es un error de sintaxis (ORA-00936: missing expression).

4.2. DISTINCT y los nulos

Para DISTINCT todos los NULL se consideran iguales entre sí (aunque, como verás, NULL = NULL no es cierto en un WHERE):

SELECT DISTINCT comision
FROM empleados
ORDER BY comision;
  COMISION
----------
         0
       300
       500
       800
(nulo)

5 filas seleccionadas.

Hay 20 empleados sin comisión, pero NULL aparece una sola vez (la última, porque al ordenar de forma ascendente Oracle coloca los nulos al final).

💡 Consejo: No uses DISTINCT “por si acaso”. Obliga a Oracle a ordenar o agrupar el resultado (más trabajo) y a menudo esconde un error de lógica, por ejemplo una combinación de tablas mal hecha que multiplica filas. Si aparecen duplicados inesperados, busca la causa.


5. La tabla DUAL y el SELECT sin FROM

A veces quieres calcular algo que no depende de ninguna tabla: una operación, la fecha actual, el usuario conectado… En Oracle, hasta la versión 21c, FROM es obligatorio, así que existe una tabla especial, DUAL, que tiene una columna (DUMMY) y exactamente una fila:

SELECT * FROM dual;
D
-
X

1 fila seleccionada.
SELECT 2 * 3 AS producto, SYSDATE AS hoy, 'Hola' || ' mundo' AS saludo
FROM dual;
  PRODUCTO HOY        SALUDO
---------- ---------- ----------
         6 03/10/2026 Hola mundo

1 fila seleccionada.

Desde Oracle 23ai puedes omitir el FROM cuando no lo necesitas, como en otros SGBD:

SELECT 2 * 3 AS producto, SYSDATE AS hoy;
  PRODUCTO HOY
---------- ----------
         6 03/10/2026

1 fila seleccionada.

⚠️ Importante: Disponible desde Oracle 23ai. En Oracle 19c y 21c la consulta anterior da ORA-00923: FROM keyword not found where expected; usa FROM dual, que funciona en todas las versiones (también en 23ai/26ai).

💡 Consejo: La fecha mostrada por SYSDATE es la del servidor en el momento de ejecutar la consulta; si repites los ejemplos, verás tu fecha actual.


6. Filtrar filas: la cláusula WHERE

WHERE contiene una condición que se evalúa para cada fila de la tabla. Solo pasan al resultado las filas para las que la condición es verdadera (TRUE). Las que dan falso (FALSE) o desconocido (UNKNOWN, cuando interviene un NULL) se descartan.

6.1. Operadores de comparación

Operador Significado Ejemplo
= Igual dept_id = 30
<>, !=, ^= Distinto (los tres equivalen; <> es el estándar) oficio <> 'VENDEDOR'
<, > Menor, mayor salario > 3000
<=, >= Menor o igual, mayor o igual fecha_alta >= DATE '2020-01-01'

Con números:

SELECT nombre, apellidos, salario
FROM empleados
WHERE salario >= 4300;
NOMBRE APELLIDOS           SALARIO
------ ---------------- ----------
Ana    García López           6500
Luis   Martínez Ruiz          4800
Marta  Sánchez Gil            5000
Laura  Fernández Moreno       4300

4 filas seleccionadas.

Con cadenas, el valor va entre comillas simples y se distingue entre mayúsculas y minúsculas:

SELECT nombre, apellidos FROM empleados WHERE oficio = 'ANALISTA';
NOMBRE APELLIDOS
------ --------------
Pablo  Moreno Herrera
Álvaro Peña Cano

2 filas seleccionadas.
SELECT nombre, apellidos FROM empleados WHERE oficio = 'analista';
ninguna fila seleccionada

💡 Consejo: “ninguna fila seleccionada” no es un error: la consulta es correcta, simplemente ninguna fila cumple la condición. Si esperabas datos, revisa mayúsculas, espacios y tildes del literal ('INFORMATICA' no es 'INFORMÁTICA'). En el módulo 28 verás UPPER y LOWER para comparar sin distinguir mayúsculas.

Con fechas, compara siempre contra un literal de fecha, nunca contra un texto que dependa del formato de la sesión:

SELECT nombre, apellidos, fecha_alta
FROM empleados
WHERE fecha_alta >= DATE '2022-01-01';
NOMBRE APELLIDOS      FECHA_ALTA
------ -------------- ----------
Sara   Ortiz Delgado  01/06/2022
Jorge  Vázquez Ramos  09/01/2023
Manuel Herrero Lozano 17/10/2022
Tomás  Reyes Fuentes  01/09/2025

4 filas seleccionadas.

Las fechas se comparan cronológicamente (una fecha posterior es “mayor”). Las cadenas se comparan carácter a carácter según el código del carácter ('B' > 'A', 'a' > 'Z', porque las minúsculas tienen códigos mayores).

⚠️ Importante: WHERE fecha_alta >= '01/01/2022' puede “funcionar” en tu ordenador por conversión implícita, pero falla o da resultados erróneos si la sesión tiene otro NLS_DATE_FORMAT. Usa DATE 'AAAA-MM-DD' o TO_DATE('01/01/2022', 'DD/MM/YYYY').

6.2. BETWEEN: rangos

expr BETWEEN a AND b equivale a expr >= a AND expr <= b. Los dos extremos están incluidos.

SELECT nombre, salario
FROM empleados
WHERE salario BETWEEN 2500 AND 2800
ORDER BY salario;
NOMBRE    SALARIO
------ ----------
Hugo         2500
Iván         2600
Isabel       2600
Álvaro       2700
Raquel       2800

5 filas seleccionadas.

Funciona igual con fechas (pedidos del primer trimestre de 2026):

SELECT pedido_id, cliente_id, fecha_pedido, estado
FROM pedidos
WHERE fecha_pedido BETWEEN DATE '2026-01-01' AND DATE '2026-03-31';
 PEDIDO_ID CLIENTE_ID FECHA_PEDI ESTADO
---------- ---------- ---------- ---------
      5016       1010 12/01/2026 ENTREGADO
      5017       1011 09/02/2026 ENTREGADO
      5018       1002 03/03/2026 ENTREGADO

3 filas seleccionadas.

El orden de los extremos importa: el menor va primero. Al revés, la condición nunca se cumple:

SELECT nombre, salario FROM empleados WHERE salario BETWEEN 2800 AND 2500;
ninguna fila seleccionada

NOT BETWEEN devuelve los que quedan fuera del rango:

SELECT nombre, precio
FROM productos
WHERE precio NOT BETWEEN 50 AND 1000
ORDER BY precio;
NOMBRE                   PRECIO
-------------------- ----------
Ratón inalámbrico          19,9
Pack teclado y ratón       34,5
Webcam HD                    45
Altavoz Inteligente          49
Portátil Gaming 16         1499
Torre Workstation          1899

6 filas seleccionadas.

⚠️ Importante: Cuidado con BETWEEN y las fechas con hora. Un DATE de Oracle siempre guarda también horas, minutos y segundos. BETWEEN DATE '2026-03-01' AND DATE '2026-03-31' no incluye un pedido del 31 de marzo a las 10:00, porque es mayor que el 31 de marzo a las 00:00. Para rangos de fechas la forma robusta es fecha >= DATE '2026-03-01' AND fecha < DATE '2026-04-01'. En EMPRESA las fechas no tienen hora, pero en las tablas reales casi siempre la tienen.

6.3. IN: lista de valores

expr IN (v1, v2, ...) es cierto si expr es igual a alguno de los valores. Equivale a una cadena de OR:

SELECT nombre, apellidos, oficio
FROM empleados
WHERE oficio IN ('ANALISTA', 'PROGRAMADOR', 'TÉCNICO SISTEMAS');
NOMBRE APELLIDOS      OFICIO
------ -------------- ----------------
Pablo  Moreno Herrera ANALISTA
Raquel Molina Prieto  PROGRAMADOR
Iván   Castro Méndez  PROGRAMADOR
Sara   Ortiz Delgado  PROGRAMADOR
Hugo   Rubio Marín    TÉCNICO SISTEMAS
Álvaro Peña Cano      ANALISTA

6 filas seleccionadas.
SELECT cliente_id, nombre, provincia
FROM clientes
WHERE provincia NOT IN ('Madrid', 'Valencia', 'Sevilla');
CLIENTE_ID NOMBRE                      PROVINCIA
---------- --------------------------- -------------
      1002 Construcciones Levante S.A. Alicante
      1007 Hotel Mar Azul S.L.         Illes Balears
      1008 Gestoría Delta              Zaragoza
      1009 Laura Benítez Sanz          Bizkaia
      1010 Instituto Tecnológico Sur   Granada
      1011 Panadería La Espiga         Valladolid
      1012 Diseños Creativos S.L.      Barcelona
      1014 Farmacia Central            Asturias

8 filas seleccionadas.

💡 Consejo: IN es más legible que muchos OR y Oracle lo optimiza igual. La lista puede tener hasta 1000 elementos; para más (o para listas que salen de otra tabla) se usa una subconsulta: WHERE dept_id IN (SELECT ...), que verás en el módulo 31.

6.4. LIKE: patrones de texto

LIKE compara una cadena con un patrón que puede contener dos comodines:

Comodín Significado Ejemplo Coincide con
% Cualquier secuencia de caracteres (cero o más) 'Port%' Portátil, Port, Portugal
_ Exactamente un carácter cualquiera '_ar%' Carmen, Marta, Sara
-- Productos cuyo nombre empieza por "Monitor"
SELECT producto_id, nombre FROM productos WHERE nombre LIKE 'Monitor%';
PRODUCTO_ID NOMBRE
----------- --------------
          9 Monitor 24 FHD
         10 Monitor 27 4K

2 filas seleccionadas.
-- Empleados cuyo nombre tiene una "a" en la segunda posición
SELECT nombre FROM empleados WHERE nombre LIKE '_a%';
NOMBRE
------
Marta
Laura
Javier
Carmen
David
Pablo
Raquel
Sara
Manuel

9 filas seleccionadas.
-- Clientes que son sociedades limitadas (contienen "S.L.") o tienen email de correo.es
SELECT nombre, email
FROM clientes
WHERE nombre LIKE '%S.L.%' OR email LIKE '%@correo.es';
NOMBRE                 EMAIL
---------------------- -------------------------
Talleres Martín S.L.   compras@talleresmartin.es
María José Herrera     mjherrera@correo.es
Hotel Mar Azul S.L.    reservas@marazul.es
Laura Benítez Sanz     laurabs@correo.es
Diseños Creativos S.L. hola@disenoscreativos.es
Carlos Ibáñez Rey      cibanez@correo.es

6 filas seleccionadas.
-- NIF de exactamente 9 caracteres que empiezan por B (9 posiciones: B + 8 guiones bajos)
SELECT nombre, nif FROM clientes WHERE nif LIKE 'B________' AND ciudad LIKE 'V%';
NOMBRE               NIF
-------------------- ---------
Talleres Martín S.L. B12345678
Panadería La Espiga  B78901234

2 filas seleccionadas.

⚠️ Importante: LIKE distingue mayúsculas y minúsculas y tildes: nombre LIKE 'portátil%' no encuentra nada y 'Portatil%' tampoco. Y sin comodines, LIKE 'Ana' equivale a = 'Ana', pero es más lento de leer: usa = cuando no haya patrón.

La cláusula ESCAPE

¿Y si quieres buscar un % o un _ de verdad? Hay que “escaparlo” eligiendo un carácter de escape con ESCAPE. Para probarlo creamos una tabla de demostración:

CREATE TABLE cupones_demo (
    codigo       VARCHAR2(20),
    descripcion  VARCHAR2(40)
);
INSERT INTO cupones_demo VALUES ('VERANO_10', 'Diez por ciento en verano');
INSERT INTO cupones_demo VALUES ('VERANO10',  'Cupón antiguo sin guion');
INSERT INTO cupones_demo VALUES ('DTO50%',    'Cincuenta por ciento');
INSERT INTO cupones_demo VALUES ('DTO50',     'Cincuenta euros');
INSERT INTO cupones_demo VALUES ('NAVIDAD_5', 'Cinco por ciento en Navidad');
Tabla creada.

1 fila creada.

1 fila creada.

1 fila creada.

1 fila creada.

1 fila creada.
-- Sin ESCAPE, "_" es un comodín: encuentra también VERANO10
SELECT codigo FROM cupones_demo WHERE codigo LIKE 'VERANO_%';
CODIGO
---------
VERANO_10
VERANO10

2 filas seleccionadas.
-- Con ESCAPE '\', "\_" significa un guion bajo literal
SELECT codigo FROM cupones_demo WHERE codigo LIKE '%\_%' ESCAPE '\';
CODIGO
---------
VERANO_10
NAVIDAD_5

2 filas seleccionadas.
-- Códigos que terminan en un % literal
SELECT codigo, descripcion FROM cupones_demo WHERE codigo LIKE '%!%' ESCAPE '!';
CODIGO DESCRIPCION
------ --------------------
DTO50% Cincuenta por ciento

1 fila seleccionada.
DROP TABLE cupones_demo PURGE;
Tabla borrada.

💡 Consejo: Para patrones más complejos (por ejemplo, “un NIF con 8 dígitos y una letra”), LIKE se queda corto. En el módulo 28 verás REGEXP_LIKE, que usa expresiones regulares.

6.5. IS NULL e IS NOT NULL

Para saber si una columna es nula no se puede usar =: hay que usar IS NULL o IS NOT NULL.

SELECT emp_id, nombre, apellidos, telefono
FROM empleados
WHERE telefono IS NULL;
    EMP_ID NOMBRE APELLIDOS     TELEFONO
---------- ------ ------------- --------
       123 Sara   Ortiz Delgado (nulo)
       131 Jorge  Vázquez Ramos (nulo)
       160 Tomás  Reyes Fuentes (nulo)

3 filas seleccionadas.
-- ¡Mal! Nunca devuelve filas, aunque haya nulos
SELECT emp_id, nombre FROM empleados WHERE telefono = NULL;
ninguna fila seleccionada
SELECT pedido_id, fecha_pedido, estado
FROM pedidos
WHERE fecha_envio IS NULL;
 PEDIDO_ID FECHA_PEDI ESTADO
---------- ---------- ---------
      5008 06/05/2025 CANCELADO
      5022 01/07/2026 PENDIENTE
      5023 08/07/2026 PENDIENTE
      5024 10/07/2026 PENDIENTE

4 filas seleccionadas.

La explicación de por qué = NULL nunca funciona está en el apartado 8. Antes necesitamos los operadores lógicos.


7. Operadores lógicos: AND, OR, NOT

Las condiciones simples se combinan con AND (y), OR (o) y NOT (no):

-- Vendedores que ganan más de 2000
SELECT nombre, oficio, salario
FROM empleados
WHERE oficio = 'VENDEDOR' AND salario > 2000;
NOMBRE OFICIO      SALARIO
------ -------- ----------
Carmen VENDEDOR       2200
David  VENDEDOR       2100
Elena  VENDEDOR       2300

3 filas seleccionadas.
-- Empleados del departamento 40 o que ganan más de 4500
SELECT nombre, dept_id, salario
FROM empleados
WHERE dept_id = 40 OR salario > 4500;
NOMBRE    DEPT_ID    SALARIO
------ ---------- ----------
Ana            10       6500
Luis           20       4800
Marta          30       5000
Pedro          40       4200
Isabel         40       2600
Jorge          40       1650

6 filas seleccionadas.
-- Productos que NO son de la categoría 2 y cuestan menos de 50
SELECT nombre, categoria_id, precio
FROM productos
WHERE NOT categoria_id = 2 AND precio < 50;
NOMBRE               CATEGORIA_ID     PRECIO
-------------------- ------------ ----------
Ratón inalámbrico               5       19,9
Pack teclado y ratón            5       34,5
Altavoz Inteligente             9         49
Webcam HD                       4         45

4 filas seleccionadas.

7.1. Precedencia: NOT, después AND, después OR

Prioridad Operadores
1 (máxima) Aritméticos: *, /, después +, -, \|\|
2 Comparación: =, <>, <, >, <=, >=, IS NULL, LIKE, BETWEEN, IN
3 NOT
4 AND
5 (mínima) OR

Como AND tiene más prioridad que OR, estas dos consultas no son iguales. Queremos “vendedores o administrativos que ganen más de 2000 €”:

-- Sin paréntesis se lee: VENDEDOR OR (ADMINISTRATIVO AND salario > 2000)
SELECT nombre, oficio, salario
FROM empleados
WHERE oficio = 'VENDEDOR' OR oficio = 'ADMINISTRATIVO' AND salario > 2000;
NOMBRE OFICIO      SALARIO
------ -------- ----------
Carmen VENDEDOR       2200
David  VENDEDOR       2100
Elena  VENDEDOR       2300
Sergio VENDEDOR       1900

4 filas seleccionadas.
-- Con paréntesis: (VENDEDOR OR ADMINISTRATIVO) AND salario > 2000
SELECT nombre, oficio, salario
FROM empleados
WHERE (oficio = 'VENDEDOR' OR oficio = 'ADMINISTRATIVO') AND salario > 2000;
NOMBRE OFICIO      SALARIO
------ -------- ----------
Carmen VENDEDOR       2200
David  VENDEDOR       2100
Elena  VENDEDOR       2300

3 filas seleccionadas.

En la primera versión la condición de salario solo afecta a los administrativos, así que se cuela Sergio, un vendedor que gana 1900 €. La segunda es la correcta. Fíjate en que el error no produce ningún mensaje: la consulta “funciona” y devuelve datos plausibles, y por eso es tan peligroso.

💡 Consejo: Pon siempre paréntesis cuando mezcles AND y OR, aunque la precedencia ya dé el resultado que quieres. El código queda autoexplicado y evitas errores cuando alguien añada otra condición.


8. El problema de NULL: lógica trivaluada

8.1. Tres valores de verdad

Como NULL significa “desconocido”, comparar algo con NULL no puede dar ni verdadero ni falso: da desconocido (UNKNOWN). SQL usa una lógica trivaluada (TRUE, FALSE, UNKNOWN) con estas tablas de verdad (V = verdadero, F = falso, D = desconocido):

AND V F D
V V F D
F F F F
D D F D
OR V F D
V V V V
F V F D
D V D D
NOT  
V F
F V
D D

Reglas para recordarlas: FALSE AND lo_que_sea es FALSE; TRUE OR lo_que_sea es TRUE; en el resto de casos, si interviene un desconocido, el resultado es desconocido. Y la regla de oro: WHERE solo deja pasar las filas con resultado TRUE.

Expresión (con comision nula) Resultado
comision = 500 UNKNOWN
comision <> 500 UNKNOWN
comision = NULL UNKNOWN (¡incluso si es nula!)
NULL = NULL UNKNOWN
comision IS NULL TRUE
NOT (comision = 500) UNKNOWN

8.2. Las filas que “desaparecen”

Observa este experimento con las comisiones (24 empleados: 4 con comisión y 20 con NULL):

SELECT emp_id, nombre, comision FROM empleados WHERE comision > 400;
    EMP_ID NOMBRE   COMISION
---------- ------ ----------
       110 Carmen        500
       111 David         800

2 filas seleccionadas.
SELECT emp_id, nombre, comision FROM empleados WHERE NOT (comision > 400);
    EMP_ID NOMBRE   COMISION
---------- ------ ----------
       112 Elena         300
       113 Sergio          0

2 filas seleccionadas.

Intuitivamente, una condición y su negación deberían repartirse todas las filas: 2 + 2 = 4, pero hay 24 empleados. Los 20 con comisión nula no están en ninguno de los dos resultados, porque para ellos tanto comision > 400 como NOT (comision > 400) valen UNKNOWN.

Lo mismo pasa con el departamento. El becario Tomás (160) no tiene departamento. ¿Quién no trabaja en Ventas (20), Informática (30) ni Contabilidad (40)?

SELECT emp_id, nombre, dept_id
FROM empleados
WHERE dept_id <> 20 AND dept_id <> 30 AND dept_id <> 40;
    EMP_ID NOMBRE      DEPT_ID
---------- -------- ----------
       100 Ana              10
       104 Laura            50
       105 Javier           60
       140 Nuria            50
       141 Álvaro           50
       150 Rosa             60
       151 Manuel           60
       152 Cristina         60

8 filas seleccionadas.

Tomás tampoco trabaja en ninguno de esos tres departamentos y, sin embargo, no aparece: para él las tres comparaciones son UNKNOWN. Si quieres incluirlo hay que pedirlo explícitamente:

SELECT emp_id, nombre, dept_id
FROM empleados
WHERE (dept_id NOT IN (20, 30, 40)) OR dept_id IS NULL;
    EMP_ID NOMBRE      DEPT_ID
---------- -------- ----------
       100 Ana              10
       104 Laura            50
       105 Javier           60
       140 Nuria            50
       141 Álvaro           50
       150 Rosa             60
       151 Manuel           60
       152 Cristina         60
       160 Tomás    (nulo)

9 filas seleccionadas.

8.3. La trampa de NOT IN con NULL

dept_id IN (40, NULL) se convierte en dept_id = 40 OR dept_id = NULL. El segundo término es UNKNOWN, pero TRUE OR UNKNOWN = TRUE, así que los empleados del 40 salen sin problemas:

SELECT nombre, dept_id FROM empleados WHERE dept_id IN (40, NULL);
NOMBRE    DEPT_ID
------ ----------
Pedro          40
Isabel         40
Jorge          40

3 filas seleccionadas.

En cambio, dept_id NOT IN (20, 30, NULL) se convierte en dept_id <> 20 AND dept_id <> 30 AND dept_id <> NULL. El último término es siempre UNKNOWN, y TRUE AND UNKNOWN = UNKNOWN: ninguna fila puede cumplir la condición.

SELECT nombre, dept_id FROM empleados WHERE dept_id NOT IN (20, 30, NULL);
ninguna fila seleccionada

Escrito así parece un error tonto, pero en la vida real la lista sale de una subconsulta (módulo 31) y el NULL está escondido en los datos. Pregunta: “¿qué empleados no son jefes de nadie?”. La idea natural es buscar los que no aparecen en la columna jefe_id… pero la directora general (100) tiene jefe_id nulo:

SELECT emp_id, nombre
FROM empleados
WHERE emp_id NOT IN (SELECT jefe_id FROM empleados);
ninguna fila seleccionada

¡Ninguno! Y hay 17 empleados que no son jefes. La solución es excluir los nulos de la lista:

SELECT emp_id, nombre
FROM empleados
WHERE emp_id NOT IN (SELECT jefe_id FROM empleados WHERE jefe_id IS NOT NULL)
ORDER BY emp_id;
    EMP_ID NOMBRE
---------- --------
       110 Carmen
       111 David
       112 Elena
       113 Sergio
       114 Lucía
       121 Raquel
       122 Iván
       123 Sara
...

17 filas seleccionadas.

⚠️ Importante: Si una lista de NOT IN contiene algún NULL, el resultado es siempre vacío. Cuando uses NOT IN con una subconsulta, asegúrate de que la columna no puede ser nula (o filtra con IS NOT NULL), o usa NOT EXISTS, que no tiene este problema (módulo 31).

8.4. Resumen del comportamiento de NULL

Situación Comportamiento
Aritmética (salario + comision) Si un operando es NULL, el resultado es NULL
Concatenación ('a' \|\| NULL) En Oracle, NULL se trata como cadena vacía
Comparaciones (=, <>, >, LIKE, BETWEEN, IN) Con un NULL dan UNKNOWN y la fila se descarta en WHERE
IS NULL / IS NOT NULL Única forma de preguntar por nulos: dan TRUE o FALSE
NOT IN con algún NULL en la lista Nunca devuelve filas
DISTINCT Todos los NULL cuentan como un único valor
ORDER BY ascendente Los NULL van al final (se consideran mayores que cualquier valor)
Cadena vacía '' En Oracle es NULL (WHERE email = '' nunca encuentra nada)

9. Ordenar el resultado: ORDER BY

📘 Concepto: Una tabla relacional no tiene orden. Si no escribes ORDER BY, Oracle devuelve las filas en el orden que le resulte más cómodo, que puede cambiar de un día para otro (tras un borrado, un índice nuevo o una actualización de versión). Si el orden importa, ORDER BY es obligatorio.

9.1. ASC y DESC

ASC (ascendente, por defecto) y DESC (descendente):

SELECT nombre, salario
FROM empleados
WHERE dept_id = 30
ORDER BY salario DESC;
NOMBRE    SALARIO
------ ----------
Marta        5000
Pablo        3500
Raquel       2800
Iván         2600
Hugo         2500
Sara         2400

6 filas seleccionadas.

9.2. Por varias columnas, por alias, por posición y por expresión

Con varias columnas, la segunda solo decide cuando hay empate en la primera, y cada una tiene su propio ASC/DESC:

SELECT dept_id, nombre, salario
FROM empleados
WHERE dept_id IN (20, 60)
ORDER BY dept_id ASC, salario DESC, nombre;
   DEPT_ID NOMBRE      SALARIO
---------- -------- ----------
        20 Luis           4800
        20 Elena          2300
        20 Carmen         2200
        20 David          2100
        20 Sergio         1900
        20 Lucía          1700
        60 Javier         4000
        60 Cristina       1750
        60 Manuel         1600
        60 Rosa           1600

10 filas seleccionadas.

Rosa y Manuel empatan en departamento y salario, así que decide el tercer criterio (nombre).

Se puede ordenar por alias, por posición en la lista del SELECT (1 = primera columna) o por cualquier expresión, aunque no se muestre:

SELECT nombre, precio, stock, precio * stock AS valor_stock
FROM productos
WHERE categoria_id IN (5, 6)
ORDER BY valor_stock DESC;
NOMBRE                   PRECIO      STOCK VALOR_STOCK
-------------------- ---------- ---------- -----------
Monitor 24 FHD              139         35        4865
Teclado mecánico           79,9         60        4794
Ratón inalámbrico          19,9        200        3980
Monitor 27 4K               329         12        3948
Pack teclado y ratón       34,5          0           0

5 filas seleccionadas.
-- Mismo orden indicando la posición (4.ª columna)
SELECT nombre, precio, stock, precio * stock AS valor_stock
FROM productos
WHERE categoria_id IN (5, 6)
ORDER BY 4 DESC;
NOMBRE                   PRECIO      STOCK VALOR_STOCK
-------------------- ---------- ---------- -----------
Monitor 24 FHD              139         35        4865
Teclado mecánico           79,9         60        4794
Ratón inalámbrico          19,9        200        3980
Monitor 27 4K               329         12        3948
Pack teclado y ratón       34,5          0           0

5 filas seleccionadas.
-- Ordenar por una columna que no se muestra
SELECT nombre, apellidos
FROM empleados
WHERE dept_id = 30
ORDER BY fecha_alta;
NOMBRE APELLIDOS
------ --------------
Marta  Sánchez Gil
Pablo  Moreno Herrera
Raquel Molina Prieto
Hugo   Rubio Marín
Iván   Castro Méndez
Sara   Ortiz Delgado

6 filas seleccionadas.

💡 Consejo: Ordenar por posición (ORDER BY 4) es cómodo al probar, pero frágil: si alguien añade una columna al principio del SELECT, el orden cambia sin dar error. En código definitivo usa nombres o alias.

9.3. NULLS FIRST y NULLS LAST

Oracle considera NULL mayor que cualquier valor: en orden ascendente los nulos salen al final y en descendente al principio. Puedes cambiarlo:

SELECT nombre, comision
FROM empleados
WHERE dept_id = 20
ORDER BY comision DESC;
NOMBRE   COMISION
------ ----------
Luis   (nulo)
Lucía  (nulo)
David         800
Carmen        500
Elena         300
Sergio          0

6 filas seleccionadas.
SELECT nombre, comision
FROM empleados
WHERE dept_id = 20
ORDER BY comision DESC NULLS LAST;
NOMBRE   COMISION
------ ----------
David         800
Carmen        500
Elena         300
Sergio          0
Lucía  (nulo)
Luis   (nulo)

6 filas seleccionadas.

9.4. Orden alfabético y NLS_SORT

Con NLS_LANGUAGE = 'SPANISH' la sesión ordena las cadenas con criterio lingüístico español (NLS_SORT = SPANISH): Álvaro va junto a las demás “A”. Con orden binario (por código de carácter), las letras con tilde van detrás de la z, así que en orden descendente Álvaro sale el primero:

SELECT nombre FROM empleados ORDER BY nombre FETCH FIRST 4 ROWS ONLY;
NOMBRE
--------
Álvaro
Ana
Carmen
Cristina

4 filas seleccionadas.
ALTER SESSION SET NLS_SORT = BINARY;
SELECT nombre FROM empleados ORDER BY nombre DESC FETCH FIRST 3 ROWS ONLY;
ALTER SESSION SET NLS_SORT = SPANISH;
Sesión modificada.

NOMBRE
------
Álvaro
Tomás
Sergio

3 filas seleccionadas.

Sesión modificada.

⚠️ Importante: NLS_SORT afecta a ORDER BY, pero las comparaciones del WHERE siguen siendo binarias mientras NLS_COMP = BINARY (el valor por defecto): 'Á' > 'Z' es cierto en un WHERE. Ten en cuenta que el orden de los textos puede variar entre instalaciones con distinta configuración regional.


10. Limitar el número de filas

10.1. FETCH FIRST n ROWS ONLY (consultas Top-N)

Desde Oracle 12c existe la cláusula de limitación de filas del estándar SQL:2008. Va al final, después de ORDER BY:

-- Los 5 productos más caros
SELECT nombre, precio
FROM productos
ORDER BY precio DESC
FETCH FIRST 5 ROWS ONLY;
NOMBRE                    PRECIO
--------------------- ----------
Torre Workstation           1899
Portátil Gaming 16          1499
Smartphone Gama Alta         999
Portátil UltraBook 14        899
Torre Oficina i5             649

5 filas seleccionadas.

Variantes de sintaxis equivalentes: FETCH FIRST 5 ROWS ONLY, FETCH NEXT 5 ROWS ONLY, FETCH FIRST 5 ROW ONLY. FIRST/NEXT y ROW/ROWS son sinónimos para que la frase suene natural en inglés.

10.2. WITH TIES: incluir los empates

¿Cuáles son los dos salarios más bajos? Si el segundo puesto está empatado, ONLY corta arbitrariamente y WITH TIES incluye a todos los empatados con la última fila:

SELECT emp_id, nombre, salario
FROM empleados
ORDER BY salario
FETCH FIRST 2 ROWS ONLY;
    EMP_ID NOMBRE    SALARIO
---------- ------ ----------
       160 Tomás         900
       150 Rosa         1600

2 filas seleccionadas.
SELECT emp_id, nombre, salario
FROM empleados
ORDER BY salario
FETCH FIRST 2 ROWS WITH TIES;
    EMP_ID NOMBRE    SALARIO
---------- ------ ----------
       160 Tomás         900
       150 Rosa         1600
       151 Manuel       1600

3 filas seleccionadas.

Con ONLY sale uno de los dos operarios que ganan 1600 (¿por qué Rosa y no Manuel? por nada: es arbitrario y podría cambiar). Con WITH TIES salen los dos. WITH TIES necesita ORDER BY para saber qué es un empate.

10.3. PERCENT

-- El 10 % de los empleados con mayor salario (24 * 0,10 = 2,4 -> se redondea hacia arriba: 3)
SELECT nombre, salario
FROM empleados
ORDER BY salario DESC
FETCH FIRST 10 PERCENT ROWS ONLY;
NOMBRE    SALARIO
------ ----------
Ana          6500
Marta        5000
Luis         4800

3 filas seleccionadas.

10.4. OFFSET: paginación

OFFSET n ROWS se salta las primeras n filas. Combinado con FETCH NEXT permite paginar, como hace una tienda online que muestra los productos de 5 en 5:

-- Página 2 (productos del 6 al 10 por orden alfabético)
SELECT producto_id, nombre, precio
FROM productos
ORDER BY nombre
OFFSET 5 ROWS FETCH NEXT 5 ROWS ONLY;
PRODUCTO_ID NOMBRE                   PRECIO
----------- -------------------- ----------
          9 Monitor 24 FHD              139
         10 Monitor 27 4K               329
          8 Pack teclado y ratón       34,5
          3 Portátil Básico 15          499
          2 Portátil Gaming 16         1499

5 filas seleccionadas.

📘 Concepto: Para la página p con t filas por página: OFFSET (p - 1) * t ROWS FETCH NEXT t ROWS ONLY. El ORDER BY debe ser determinista (que no haya empates, por ejemplo añadiendo la clave primaria al final), o una misma fila podría aparecer en dos páginas o en ninguna.

10.5. La pseudocolumna ROWNUM y su trampa

Antes de 12c, la única forma de limitar filas en Oracle era ROWNUM, una pseudocolumna (una columna que no existe en la tabla pero que Oracle calcula) que numera las filas a medida que se leen y pasan el WHERE: 1, 2, 3…

SELECT ROWNUM, emp_id, nombre FROM empleados WHERE dept_id = 30;
    ROWNUM     EMP_ID NOMBRE
---------- ---------- ------
         1        102 Marta
         2        120 Pablo
         3        121 Raquel
         4        122 Iván
         5        123 Sara
         6        124 Hugo

6 filas seleccionadas.

La trampa: ROWNUM se asigna antes de ordenar (paso 2 del orden lógico). Queremos los 3 sueldos más bajos:

-- ¡MAL! Toma 3 filas cualesquiera y DESPUÉS las ordena
SELECT emp_id, nombre, salario
FROM empleados
WHERE ROWNUM <= 3
ORDER BY salario;
    EMP_ID NOMBRE    SALARIO
---------- ------ ----------
       101 Luis         4800
       102 Marta        5000
       100 Ana          6500

3 filas seleccionadas.

Ha cogido las tres primeras filas que ha leído (los directivos 100, 101 y 102) y las ha ordenado. Los salarios más bajos son 900 y 1600. La solución clásica es ordenar primero en una subconsulta en el FROM (vista en línea, módulo 31) y numerar después:

SELECT emp_id, nombre, salario
FROM (SELECT emp_id, nombre, salario
      FROM empleados
      ORDER BY salario)
WHERE ROWNUM <= 3;
    EMP_ID NOMBRE    SALARIO
---------- ------ ----------
       160 Tomás         900
       150 Rosa         1600
       151 Manuel       1600

3 filas seleccionadas.

Otra sorpresa: ROWNUM = 2 o ROWNUM > 1 nunca devuelven filas. La primera fila leída recibe el número 1, no cumple la condición y se descarta; la siguiente vuelve a recibir el 1… y así con todas.

SELECT emp_id, nombre FROM empleados WHERE ROWNUM = 2;
ninguna fila seleccionada

💡 Consejo: En código nuevo usa FETCH FIRST / OFFSET: es estándar, se lee mejor y no tiene trampas. Conoce ROWNUM porque lo encontrarás en muchísimo código heredado (de Oracle 11g y anteriores).

10.6. La pseudocolumna ROWID

ROWID es la dirección física de una fila: identifica el objeto, el fichero de datos, el bloque y la posición dentro del bloque. Es la forma más rápida de llegar a una fila concreta.

SELECT ROWID, emp_id, nombre FROM empleados WHERE dept_id = 40;
ROWID                  EMP_ID NOMBRE
------------------ ---------- ------
AAAR4xAAAAAAAVLAAD        103 Pedro
AAAR4xAAAAAAAVLAAQ        130 Isabel
AAAR4xAAAAAAAVLAAR        131 Jorge

3 filas seleccionadas.

⚠️ Importante: ROWID es útil para tareas puntuales (por ejemplo, localizar y borrar filas duplicadas), pero no lo uses como clave: puede cambiar si la tabla se reorganiza, se exporta e importa o la fila se mueve de partición. Los valores que veas en tu base de datos serán distintos de los de este ejemplo.


11. Variables de sustitución de SQL*Plus

Las herramientas de Oracle (SQL*Plus, SQLcl, SQL Developer) permiten escribir variables de sustitución con &. Antes de enviar la sentencia al servidor, la herramienta pregunta el valor y sustituye el texto. No son una característica de SQL, sino del cliente.

SQL> SELECT nombre, salario FROM empleados WHERE dept_id = &departamento;
Introduzca un valor para departamento: 40
Forma Efecto
&variable Pregunta el valor cada vez que aparece
&&variable Pregunta la primera vez y lo recuerda para el resto de la sesión
DEFINE variable = valor Asigna el valor sin preguntar
UNDEFINE variable Olvida el valor
SET VERIFY ON/OFF Muestra u oculta las líneas old/new con la sustitución
SET DEFINE OFF Desactiva la sustitución (útil si tus datos contienen &)

Para el ejemplo usamos DEFINE (así no hace falta teclear nada) y SET VERIFY ON para ver la sustitución:

SET DEFINE ON
SET VERIFY ON
DEFINE dep = 40
SELECT nombre, salario
FROM empleados
WHERE dept_id = &dep;
SELECT nombre FROM empleados WHERE oficio = '&&puesto' AND dept_id = &dep;
UNDEFINE dep
old   3: WHERE dept_id = &dep
new   3: WHERE dept_id = 40

NOMBRE    SALARIO
------ ----------
Pedro        4200
Isabel       2600
Jorge        1650

3 filas seleccionadas.

old   1: SELECT nombre FROM empleados WHERE oficio = '&&puesto' AND dept_id = &dep
new   1: SELECT nombre FROM empleados WHERE oficio = 'CONTABLE' AND dept_id = 40

NOMBRE
------
Isabel

1 fila seleccionada.

En la segunda consulta, '&&puesto' va entre comillas porque se sustituye texto: si el usuario escribe CONTABLE, la sentencia resultante es oficio = 'CONTABLE'. Las líneas old/new (en inglés en nuestra instalación) muestran la sentencia antes y después de la sustitución.

⚠️ Importante: Las variables de sustitución solo existen en las herramientas cliente de Oracle; una aplicación Java, PHP o Python usa variables de enlace (:dep, ?) que se ven en los cursos de programación. Y si un literal de tus datos contiene & (por ejemplo 'Martín & Hijos'), SQL*Plus te pedirá un valor: ejecuta antes SET DEFINE OFF.


12. Errores frecuentes y buenas prácticas

Error o síntoma Causa Solución
ORA-00904: invalid identifier Columna mal escrita o alias usado en WHERE Revisa con DESC; repite la expresión en WHERE
ORA-00923: FROM keyword not found where expected Falta una coma, alias con comillas simples o SELECT sin FROM en 19c Revisa comas y comillas; añade FROM dual
ORA-00936: missing expression Coma de más (SELECT a, FROM ...) o DISTINCT en mitad de la lista Revisa la lista del SELECT
ORA-00933: SQL command not properly ended Cláusulas en orden incorrecto (p. ej. WHERE después de ORDER BY) Respeta el orden SELECT-FROM-WHERE-ORDER BY-FETCH
ORA-01722: invalid number Comparas una cadena no numérica con un número Compara cada columna con un literal de su tipo
No sale ninguna fila Mayúsculas, tildes o espacios en un literal; = NULL; NOT IN con nulos Revisa el literal; usa IS NULL; filtra nulos
Faltan filas con nulos <>, NOT y comparaciones descartan los UNKNOWN Añade OR columna IS NULL si quieres incluirlas
Top-N incorrecto ROWNUM con ORDER BY en la misma consulta Usa FETCH FIRST
El orden “cambia solo” No hay ORDER BY Ordena siempre que el orden importe

Buenas prácticas de estilo (las que sigue todo el curso):

  • Palabras reservadas en MAYÚSCULAS e identificadores en minúsculas.
  • Una cláusula por línea en consultas no triviales y sangría para las listas largas.
  • Enumera las columnas en vez de SELECT * y pon alias legibles a las expresiones.
  • Usa literales de fecha DATE 'AAAA-MM-DD'.
  • Paréntesis siempre que mezcles AND y OR.
  • Piensa en los nulos de cada columna que filtras: ¿qué debe pasar con las filas que no tienen valor?

13. Ejercicios

A continuación tienes 42 ejercicios resueltos de dificultad creciente sobre el esquema EMPRESA y 15 ejercicios propuestos. Intenta resolver cada uno antes de desplegar la solución, y compara tu resultado con la salida real.

Ejercicio 1: Todos los proyectos

Muestra todas las columnas y filas de la tabla PROYECTOS.

Ver solución
SELECT * FROM proyectos;
PROYECTO_ID NOMBRE                     DEPT_ID FECHA_INIC FECHA_FIN  PRESUPUESTO
----------- ----------------------- ---------- ---------- ---------- -----------
          1 Migración a la nube             30 07/01/2025 30/09/2025      120000
          2 Nueva tienda online             30 01/06/2025 (nulo)           90000
          3 Campaña Navidad 2025            50 01/10/2025 31/12/2025       40000
          4 Optimización de almacén         60 01/02/2026 (nulo)           35000
          5 Plan estratégico 2027           10 01/09/2026 (nulo)     (nulo)

5 filas seleccionadas.

Los proyectos 2, 4 y 5 no tienen fecha de fin (siguen abiertos) y el 5 no tiene presupuesto asignado.

Ejercicio 2: Listado de clientes

Muestra el nombre, la ciudad y el límite de crédito de todos los clientes.

Ver solución
SELECT nombre, ciudad, limite_credito
FROM clientes;
NOMBRE                      CIUDAD     LIMITE_CREDITO
--------------------------- ---------- --------------
Talleres Martín S.L.        Valencia            10000
Construcciones Levante S.A. Alicante            25000
María José Herrera          Madrid               2000
Academia Nova               Madrid               8000
Clínica Dental Sonrisa      Sevilla              6000
Antonio Ruiz Pérez          Sevilla              1500
...

14 filas seleccionadas.

Una proyección pura: π[nombre, ciudad, limite_credito](CLIENTES). Sin WHERE salen las 14 filas.

Ejercicio 3: Catálogo con alias

Muestra el nombre y el precio de los productos con las cabeceras Producto y Precio (€).

Ver solución
SELECT nombre AS "Producto", precio AS "Precio (€)"
FROM productos;
Producto              Precio (€)
--------------------- ----------
Portátil UltraBook 14        899
Portátil Gaming 16          1499
Portátil Básico 15           499
Torre Oficina i5             649
Torre Workstation           1899
...

20 filas seleccionadas.

Los alias con mayúsculas, espacios o símbolos necesitan comillas dobles.

Ejercicio 4: Oficios de la empresa

Muestra los oficios distintos que existen, sin repeticiones y en orden alfabético.

Ver solución
SELECT DISTINCT oficio
FROM empleados
ORDER BY oficio;
OFICIO
----------------
ADMINISTRATIVO
ANALISTA
BECARIO
CONTABLE
DIRECTOR
DIRECTOR GENERAL
DISEÑADOR
OPERARIO
PROGRAMADOR
TÉCNICO SISTEMAS
VENDEDOR

11 filas seleccionadas.

Sin DISTINCT saldrían 24 filas, una por empleado.

Ejercicio 5: Provincias con clientes

Muestra las provincias distintas en las que hay clientes, ordenadas alfabéticamente.

Ver solución
SELECT DISTINCT provincia
FROM clientes
ORDER BY provincia;
PROVINCIA
-------------
Alicante
Asturias
Barcelona
Bizkaia
Granada
Illes Balears
Madrid
Sevilla
Valencia
Valladolid
Zaragoza

11 filas seleccionadas.

Ejercicio 6: Salario anual

Muestra nombre, apellidos, salario mensual y salario anual en 14 pagas (alias salario_anual) de los empleados del departamento 30.

Ver solución
SELECT nombre, apellidos, salario, salario * 14 AS salario_anual
FROM empleados
WHERE dept_id = 30;
NOMBRE APELLIDOS         SALARIO SALARIO_ANUAL
------ -------------- ---------- -------------
Marta  Sánchez Gil          5000         70000
Pablo  Moreno Herrera       3500         49000
Raquel Molina Prieto        2800         39200
Iván   Castro Méndez        2600         36400
Sara   Ortiz Delgado        2400         33600
Hugo   Rubio Marín          2500         35000

6 filas seleccionadas.

Ejercicio 7: Frases de presentación

Genera, para los empleados del departamento 50, una única columna llamada Presentación con frases como Nuria Gil Soto trabaja como DISEÑADOR.

Ver solución
SELECT nombre || ' ' || apellidos || ' trabaja como ' || oficio AS "Presentación"
FROM empleados
WHERE dept_id = 50;
Presentación
--------------------------------------------
Laura Fernández Moreno trabaja como DIRECTOR
Nuria Gil Soto trabaja como DISEÑADOR
Álvaro Peña Cano trabaja como ANALISTA

3 filas seleccionadas.

Ejercicio 8: Productos caros

Muestra el nombre y el precio de los productos que cuestan más de 500 €.

Ver solución
SELECT nombre, precio
FROM productos
WHERE precio > 500;
NOMBRE                    PRECIO
--------------------- ----------
Portátil UltraBook 14        899
Portátil Gaming 16          1499
Torre Oficina i5             649
Torre Workstation           1899
Smartphone Gama Alta         999

5 filas seleccionadas.

Ejercicio 9: Clientes de Madrid

Muestra el identificador, el nombre y el teléfono de los clientes de la provincia de Madrid.

Ver solución
SELECT cliente_id, nombre, telefono
FROM clientes
WHERE provincia = 'Madrid';
CLIENTE_ID NOMBRE             TELEFONO
---------- ------------------ ---------
      1003 María José Herrera 612345678
      1004 Academia Nova      915551234

2 filas seleccionadas.

El literal debe escribirse exactamente como está guardado: 'MADRID' no devolvería nada.

Ejercicio 10: Pedidos cancelados

Muestra todos los datos de los pedidos cuyo estado es CANCELADO.

Ver solución
SELECT *
FROM pedidos
WHERE estado = 'CANCELADO';
 PEDIDO_ID CLIENTE_ID EMPLEADO_ID FECHA_PEDI FECHA_ENVI ESTADO
---------- ---------- ----------- ---------- ---------- ---------
      5008       1008         113 06/05/2025 (nulo)     CANCELADO

1 fila seleccionada.

Ejercicio 11: Rango salarial

Muestra nombre, oficio y salario de los empleados que ganan entre 2000 y 3000 € (ambos incluidos), ordenados por salario de mayor a menor.

Ver solución
SELECT nombre, oficio, salario
FROM empleados
WHERE salario BETWEEN 2000 AND 3000
ORDER BY salario DESC;
NOMBRE OFICIO              SALARIO
------ ---------------- ----------
Raquel PROGRAMADOR            2800
Álvaro ANALISTA               2700
Iván   PROGRAMADOR            2600
Isabel CONTABLE               2600
Hugo   TÉCNICO SISTEMAS       2500
Sara   PROGRAMADOR            2400
Nuria  DISEÑADOR              2300
Elena  VENDEDOR               2300
Carmen VENDEDOR               2200
David  VENDEDOR               2100

10 filas seleccionadas.

Ejercicio 12: Personal técnico

Muestra nombre, apellidos y oficio de los empleados cuyo oficio es ANALISTA, PROGRAMADOR o DISEÑADOR, ordenados por oficio y apellidos.

Ver solución
SELECT nombre, apellidos, oficio
FROM empleados
WHERE oficio IN ('ANALISTA', 'PROGRAMADOR', 'DISEÑADOR')
ORDER BY oficio, apellidos;
NOMBRE APELLIDOS      OFICIO
------ -------------- -----------
Pablo  Moreno Herrera ANALISTA
Álvaro Peña Cano      ANALISTA
Nuria  Gil Soto       DISEÑADOR
Iván   Castro Méndez  PROGRAMADOR
Raquel Molina Prieto  PROGRAMADOR
Sara   Ortiz Delgado  PROGRAMADOR

6 filas seleccionadas.

Ejercicio 13: Portátiles

Muestra los productos cuyo nombre empieza por Portátil, con su precio y stock.

Ver solución
SELECT nombre, precio, stock
FROM productos
WHERE nombre LIKE 'Portátil%';
NOMBRE                    PRECIO      STOCK
--------------------- ---------- ----------
Portátil UltraBook 14        899         25
Portátil Gaming 16          1499         10
Portátil Básico 15           499         40

3 filas seleccionadas.

Ejercicio 14: Apellidos acabados en -ez

Muestra los empleados cuyo primer apellido termina en ez.

Ver solución
SELECT nombre, apellidos
FROM empleados
WHERE apellidos LIKE '%ez %'
ORDER BY apellidos;
NOMBRE APELLIDOS
------ ----------------
Isabel Domínguez Vega
Laura  Fernández Moreno
Pedro  Gómez Díaz
Sergio Jiménez Vidal
Javier López Navarro
Luis   Martínez Ruiz
Marta  Sánchez Gil
Jorge  Vázquez Ramos

8 filas seleccionadas.

El truco es el espacio del patrón: '%ez %' exige que detrás de ez venga un espacio, es decir, que ez sea el final de la primera palabra. Con '%ez' solo saldrían los que tienen el segundo apellido en -ez (Iván Castro Méndez), y con '%ez%' saldrían los dos grupos.

Ejercicio 15: Empleados sin teléfono

Muestra los empleados que no tienen teléfono registrado.

Ver solución
SELECT emp_id, nombre, apellidos
FROM empleados
WHERE telefono IS NULL;
    EMP_ID NOMBRE APELLIDOS
---------- ------ -------------
       123 Sara   Ortiz Delgado
       131 Jorge  Vázquez Ramos
       160 Tomás  Reyes Fuentes

3 filas seleccionadas.

Ejercicio 16: Clientes localizables

Muestra el nombre, email y teléfono de los clientes que tienen ambos datos de contacto informados.

Ver solución
SELECT nombre, email, telefono
FROM clientes
WHERE email IS NOT NULL
  AND telefono IS NOT NULL;
NOMBRE                      EMAIL                      TELEFONO
--------------------------- -------------------------- ---------
Talleres Martín S.L.        compras@talleresmartin.es  961234567
Construcciones Levante S.A. info@clevante.es           963456789
María José Herrera          mjherrera@correo.es        612345678
Academia Nova               secretaria@academianova.es 915551234
Clínica Dental Sonrisa      admin@sonrisa.es           954112233
Hotel Mar Azul S.L.         reservas@marazul.es        971223344
...

11 filas seleccionadas.

De los 14 clientes, faltan el 1006 y el 1011 (sin email) y el 1009 (sin teléfono).

Ejercicio 17: Pedidos de 2025

Muestra identificador, cliente y fecha de los pedidos realizados durante el año 2025, ordenados por fecha.

Ver solución
SELECT pedido_id, cliente_id, fecha_pedido
FROM pedidos
WHERE fecha_pedido >= DATE '2025-01-01'
  AND fecha_pedido <  DATE '2026-01-01'
ORDER BY fecha_pedido;
 PEDIDO_ID CLIENTE_ID FECHA_PEDI
---------- ---------- ----------
      5001       1001 15/01/2025
      5002       1002 28/01/2025
      5003       1003 03/02/2025
      5004       1004 20/02/2025
      5005       1005 05/03/2025
      5006       1001 22/03/2025
...

15 filas seleccionadas.

Se usa el patrón robusto >= inicio AND < inicio_siguiente, que funciona aunque las fechas tengan hora. BETWEEN DATE '2025-01-01' AND DATE '2025-12-31' daría el mismo resultado aquí porque las fechas de EMPRESA no tienen hora.

Ejercicio 18: Productos agotados o descatalogados

Muestra los productos sin stock o que están descatalogados.

Ver solución
SELECT producto_id, nombre, stock, descatalogado
FROM productos
WHERE stock = 0 OR descatalogado = 'S';
PRODUCTO_ID NOMBRE                    STOCK D
----------- -------------------- ---------- -
          8 Pack teclado y ratón          0 N
         14 Altavoz Inteligente           0 S

2 filas seleccionadas.

El 14 cumple las dos condiciones pero aparece una sola vez: OR evalúa cada fila una vez.

Ejercicio 19: Altas recientes

Muestra nombre, apellidos y fecha de alta de los empleados contratados a partir del 1 de enero de 2020, del más reciente al más antiguo.

Ver solución
SELECT nombre, apellidos, fecha_alta
FROM empleados
WHERE fecha_alta >= DATE '2020-01-01'
ORDER BY fecha_alta DESC;
NOMBRE APELLIDOS      FECHA_ALTA
------ -------------- ----------
Tomás  Reyes Fuentes  01/09/2025
Jorge  Vázquez Ramos  09/01/2023
Manuel Herrero Lozano 17/10/2022
Sara   Ortiz Delgado  01/06/2022
Sergio Jiménez Vidal  01/09/2021
Álvaro Peña Cano      12/04/2021
Iván   Castro Méndez  16/03/2020
Lucía  Romero Serrano 10/02/2020

8 filas seleccionadas.

Ejercicio 20: Clientes sin comercial

Muestra los clientes que no tienen comercial asignado.

Ver solución
SELECT cliente_id, nombre, ciudad
FROM clientes
WHERE comercial_id IS NULL;
CLIENTE_ID NOMBRE             CIUDAD
---------- ------------------ --------
      1009 Laura Benítez Sanz Bilbao
      1013 Carlos Ibáñez Rey  Valencia

2 filas seleccionadas.

Ejercicio 21: Comisiones reales

Muestra los empleados que cobran una comisión mayor que cero.

Ver solución
SELECT nombre, salario, comision
FROM empleados
WHERE comision > 0;
NOMBRE    SALARIO   COMISION
------ ---------- ----------
Carmen       2200        500
David        2100        800
Elena        2300        300

3 filas seleccionadas.

No hace falta añadir comision IS NOT NULL: para los nulos, comision > 0 es UNKNOWN y ya se descartan.

Ejercicio 22: Sin comisión

Muestra los empleados del departamento 20 que no cobran comisión, entendiendo como tal tanto la comisión nula como la comisión cero.

Ver solución
SELECT nombre, oficio, comision
FROM empleados
WHERE dept_id = 20
  AND (comision IS NULL OR comision = 0);
NOMBRE OFICIO           COMISION
------ -------------- ----------
Luis   DIRECTOR       (nulo)
Sergio VENDEDOR                0
Lucía  ADMINISTRATIVO (nulo)

3 filas seleccionadas.

Los paréntesis son imprescindibles: sin ellos, dept_id = 20 AND comision IS NULL OR comision = 0 se leería como (dept_id = 20 AND comision IS NULL) OR comision = 0, que incluiría a cualquier empleado de cualquier departamento con comisión cero.

Ejercicio 23: Bien pagados de Ventas o Informática

Muestra los empleados de los departamentos 20 o 30 que ganan más de 2500 €.

Ver solución
SELECT nombre, dept_id, salario
FROM empleados
WHERE dept_id IN (20, 30)
  AND salario > 2500
ORDER BY dept_id, salario DESC;
NOMBRE    DEPT_ID    SALARIO
------ ---------- ----------
Luis           20       4800
Marta          30       5000
Pablo          30       3500
Raquel         30       2800
Iván           30       2600

5 filas seleccionadas.

IN evita los paréntesis que harían falta con (dept_id = 20 OR dept_id = 30) AND salario > 2500.

Ejercicio 24: Fuera de Ventas

Muestra todos los empleados que no pertenecen al departamento de Ventas (20), incluidos los que no tienen departamento.

Ver solución
SELECT emp_id, nombre, dept_id
FROM empleados
WHERE dept_id <> 20 OR dept_id IS NULL
ORDER BY emp_id;
    EMP_ID NOMBRE      DEPT_ID
---------- -------- ----------
       100 Ana              10
       102 Marta            30
       103 Pedro            40
       104 Laura            50
       105 Javier           60
       120 Pablo            30
...

18 filas seleccionadas.

Sin OR dept_id IS NULL saldrían 17 filas: el becario Tomás (160) desaparecería porque NULL <> 20 es UNKNOWN.

Ejercicio 25: Clientes grandes de Valencia y Madrid

Muestra los clientes de las provincias de Valencia o Madrid con un límite de crédito de al menos 5000 € y que tengan comercial asignado.

Ver solución
SELECT nombre, provincia, limite_credito, comercial_id
FROM clientes
WHERE provincia IN ('Valencia', 'Madrid')
  AND limite_credito >= 5000
  AND comercial_id IS NOT NULL;
NOMBRE               PROVINCIA LIMITE_CREDITO COMERCIAL_ID
-------------------- --------- -------------- ------------
Talleres Martín S.L. Valencia           10000          110
Academia Nova        Madrid              8000          111

2 filas seleccionadas.

Ejercicio 26: Gama media fuera de monitores y mobiliario

Muestra los productos con precio entre 100 y 400 € que no sean de las categorías 6 (Monitores) ni 11 (Mobiliario).

Ver solución
SELECT nombre, categoria_id, precio
FROM productos
WHERE precio BETWEEN 100 AND 400
  AND categoria_id NOT IN (6, 11)
ORDER BY precio;
NOMBRE                CATEGORIA_ID     PRECIO
--------------------- ------------ ----------
Impresora Láser                  4        189
Tablet 10                        7        279
Smartphone Gama Media            8        299

3 filas seleccionadas.

Aquí NOT IN es seguro porque la lista es fija y categoria_id es NOT NULL.

Ejercicio 27: Pedidos online

Muestra los pedidos que no fueron gestionados por ningún empleado.

Ver solución
SELECT pedido_id, cliente_id, fecha_pedido, estado
FROM pedidos
WHERE empleado_id IS NULL;
 PEDIDO_ID CLIENTE_ID FECHA_PEDI ESTADO
---------- ---------- ---------- ---------
      5014       1009 03/11/2025 ENTREGADO
      5023       1009 08/07/2026 PENDIENTE

2 filas seleccionadas.

Ejercicio 28: Ingresos superiores a 2600

Muestra nombre, salario, comisión y la suma de ambos (alias ingresos) de los empleados cuyos ingresos totales superan 2600 €. ¿Por qué no aparece ningún empleado sin comisión aunque gane más de 2600?

Ver solución
SELECT nombre, salario, comision, salario + comision AS ingresos
FROM empleados
WHERE salario + comision > 2600;
NOMBRE    SALARIO   COMISION   INGRESOS
------ ---------- ---------- ----------
Carmen       2200        500       2700
David        2100        800       2900

2 filas seleccionadas.

Para los 20 empleados sin comisión, salario + comision es NULL y la comparación da UNKNOWN, así que la directora general, con 6500 €, no aparece. Es casi seguro un resultado erróneo para el negocio; en el módulo 28 lo corregirás con salario + NVL(comision, 0).

Ejercicio 29: Plantilla por departamento

Muestra departamento, nombre y salario de todos los empleados ordenados por departamento (ascendente) y, dentro de cada uno, por salario (descendente). Los empleados sin departamento deben salir al principio.

Ver solución
SELECT dept_id, nombre, salario
FROM empleados
ORDER BY dept_id ASC NULLS FIRST, salario DESC;
   DEPT_ID NOMBRE      SALARIO
---------- -------- ----------
(nulo)     Tomás           900
        10 Ana            6500
        20 Luis           4800
        20 Elena          2300
        20 Carmen         2200
        20 David          2100
        20 Sergio         1900
        20 Lucía          1700
        30 Marta          5000
        30 Pablo          3500
...

24 filas seleccionadas.

Ejercicio 30: Clientes por comercial

Muestra los clientes ordenados por comercial asignado, con los que no tienen comercial al principio, y por nombre dentro de cada comercial.

Ver solución
SELECT comercial_id, nombre
FROM clientes
ORDER BY comercial_id NULLS FIRST, nombre;
COMERCIAL_ID NOMBRE
------------ ---------------------------
(nulo)       Carlos Ibáñez Rey
(nulo)       Laura Benítez Sanz
         110 Construcciones Levante S.A.
         110 Diseños Creativos S.L.
         110 Talleres Martín S.L.
         111 Academia Nova
         111 Hotel Mar Azul S.L.
         111 María José Herrera
...

14 filas seleccionadas.

Ejercicio 31: Top 3 de productos con más stock

Muestra los tres productos con más unidades en stock.

Ver solución
SELECT nombre, stock
FROM productos
ORDER BY stock DESC
FETCH FIRST 3 ROWS ONLY;
NOMBRE                     STOCK
--------------------- ----------
Ratón inalámbrico            200
Auriculares Bluetooth         80
Disco SSD 1TB                 70

3 filas seleccionadas.

Ejercicio 32: Los salarios más bajos con empates

Muestra los empleados con los 4 salarios más bajos, incluyendo a todos los que empaten con el cuarto.

Ver solución
SELECT nombre, oficio, salario
FROM empleados
ORDER BY salario
FETCH FIRST 4 ROWS WITH TIES;
NOMBRE OFICIO            SALARIO
------ -------------- ----------
Tomás  BECARIO               900
Rosa   OPERARIO             1600
Manuel OPERARIO             1600
Jorge  ADMINISTRATIVO       1650

4 filas seleccionadas.

El cuarto salario es 1650 (Jorge) y nadie más gana exactamente eso, así que salen 4 filas. Prueba con FETCH FIRST 2 ROWS WITH TIES para ver el empate de los dos operarios.

El catálogo web muestra 4 productos por página ordenados por precio ascendente (y, en caso de empate, por producto_id). Obtén la página 3.

Ver solución
SELECT producto_id, nombre, precio
FROM productos
ORDER BY precio, producto_id
OFFSET 8 ROWS FETCH NEXT 4 ROWS ONLY;
PRODUCTO_ID NOMBRE                    PRECIO
----------- --------------------- ----------
         18 Impresora Láser              189
         15 Silla Ergonómica             249
         20 Tablet 10                    279
         11 Smartphone Gama Media        299

4 filas seleccionadas.

Página 3 con 4 filas por página: OFFSET (3 - 1) * 4 = 8. Añadir producto_id al ORDER BY hace la ordenación determinista.

Ejercicio 34: El 25 % de los mejores clientes

Muestra el 25 % de los clientes con mayor límite de crédito.

Ver solución
SELECT nombre, limite_credito
FROM clientes
ORDER BY limite_credito DESC
FETCH FIRST 25 PERCENT ROWS ONLY;
NOMBRE                      LIMITE_CREDITO
--------------------------- --------------
Construcciones Levante S.A.          25000
Instituto Tecnológico Sur            20000
Hotel Mar Azul S.L.                  15000
Talleres Martín S.L.                 10000

4 filas seleccionadas.

14 clientes × 0,25 = 3,5, que se redondea hacia arriba a 4 filas.

Ejercicio 35: Últimos pedidos

Muestra los tres pedidos más recientes con su estado.

Ver solución
SELECT pedido_id, fecha_pedido, estado
FROM pedidos
ORDER BY fecha_pedido DESC
FETCH FIRST 3 ROWS ONLY;
 PEDIDO_ID FECHA_PEDI ESTADO
---------- ---------- ---------
      5024 10/07/2026 PENDIENTE
      5023 08/07/2026 PENDIENTE
      5022 01/07/2026 PENDIENTE

3 filas seleccionadas.

Ejercicio 36: Valor del inventario

Calcula el valor del stock (precio × stock) de cada producto no descatalogado y muestra los cinco de mayor valor.

Ver solución
SELECT nombre, precio, stock, precio * stock AS valor
FROM productos
WHERE descatalogado = 'N'
ORDER BY valor DESC
FETCH FIRST 5 ROWS ONLY;
NOMBRE                    PRECIO      STOCK      VALOR
--------------------- ---------- ---------- ----------
Portátil UltraBook 14        899         25      22475
Portátil Básico 15           499         40      19960
Portátil Gaming 16          1499         10      14990
Smartphone Gama Media        299         50      14950
Torre Oficina i5             649         15       9735

5 filas seleccionadas.

Ejercicio 37: Importe de las líneas de un pedido

Para el pedido 5010, muestra cada línea con la cantidad, el precio unitario, el descuento y el importe neto cantidad × precio_unitario × (1 − descuento / 100), ordenado por importe descendente.

Ver solución
SELECT num_linea, producto_id, cantidad, precio_unitario, descuento,
       cantidad * precio_unitario * (1 - descuento / 100) AS importe
FROM lineas_pedido
WHERE pedido_id = 5010
ORDER BY importe DESC;
 NUM_LINEA PRODUCTO_ID   CANTIDAD PRECIO_UNITARIO  DESCUENTO    IMPORTE
---------- ----------- ---------- --------------- ---------- ----------
         1           3         20             499         15       8483
         2           9         20             139         15       2363
         3           8         20            34,5         15      586,5
         4          17          5              45          0        225

4 filas seleccionadas.

Los paréntesis de (1 - descuento / 100) son necesarios: sin ellos se calcularía cantidad * precio_unitario * 1 - descuento / 100, restando una cantidad insignificante.

Ejercicio 38: Veteranos con margen de subida

Muestra los empleados contratados antes de 2015 que ganan menos de 4500 €, junto con el salario que tendrían con una subida del 5 %.

Ver solución
SELECT nombre, apellidos, fecha_alta, salario, salario * 1.05 AS salario_nuevo
FROM empleados
WHERE fecha_alta < DATE '2015-01-01'
  AND salario < 4500
ORDER BY fecha_alta;
NOMBRE APELLIDOS      FECHA_ALTA    SALARIO SALARIO_NUEVO
------ -------------- ---------- ---------- -------------
Pedro  Gómez Díaz     10/09/2013       4200          4410
Pablo  Moreno Herrera 05/05/2014       3500          3675
Javier López Navarro  11/11/2014       4000          4200

3 filas seleccionadas.

Ejercicio 39: Grandes dedicaciones

Muestra las asignaciones a proyectos de más de 300 horas, ordenadas por horas descendente, indicando empleado, proyecto, horas y función.

Ver solución
SELECT emp_id, proyecto_id, horas, funcion
FROM asignaciones
WHERE horas > 300
ORDER BY horas DESC;
    EMP_ID PROYECTO_ID      HORAS FUNCION
---------- ----------- ---------- -------------------------
       121           1        600 Desarrollador
       122           2        520 Desarrollador
       120           1        450 Jefe de proyecto
       123           2        410 Desarrollador
       124           1        380 Administrador de sistemas

5 filas seleccionadas.

Ejercicio 40: Top-N a la antigua

Obtén los tres productos más baratos usando ROWNUM (como se haría en Oracle 11g) y comprueba que coincide con la versión moderna.

Ver solución
SELECT nombre, precio
FROM (SELECT nombre, precio
      FROM productos
      ORDER BY precio)
WHERE ROWNUM <= 3;

SELECT nombre, precio
FROM productos
ORDER BY precio
FETCH FIRST 3 ROWS ONLY;
NOMBRE                   PRECIO
-------------------- ----------
Ratón inalámbrico          19,9
Pack teclado y ratón       34,5
Webcam HD                    45

3 filas seleccionadas.

NOMBRE                   PRECIO
-------------------- ----------
Ratón inalámbrico          19,9
Pack teclado y ratón       34,5
Webcam HD                    45

3 filas seleccionadas.

Con ROWNUM es obligatorio ordenar dentro de la subconsulta y filtrar fuera. Escribir WHERE ROWNUM <= 3 ORDER BY precio en una sola consulta tomaría tres productos cualesquiera.

Ejercicio 41: Clientes empresa sin email

Muestra los clientes cuyo NIF empieza por B (sociedades limitadas) y que no tienen email.

Ver solución
SELECT cliente_id, nombre, nif, telefono
FROM clientes
WHERE nif LIKE 'B%'
  AND email IS NULL;
CLIENTE_ID NOMBRE              NIF       TELEFONO
---------- ------------------- --------- ---------
      1011 Panadería La Espiga B78901234 983445566

1 fila seleccionada.

Ejercicio 42: Pedidos entregados con envío lento

Muestra los pedidos entregados en los que pasaron más de 2 días entre el pedido y el envío, con el número de días (alias dias). En Oracle, restar dos fechas da el número de días entre ellas.

Ver solución
SELECT pedido_id, fecha_pedido, fecha_envio, fecha_envio - fecha_pedido AS dias
FROM pedidos
WHERE estado = 'ENTREGADO'
  AND fecha_envio - fecha_pedido > 2
ORDER BY dias DESC, pedido_id;
 PEDIDO_ID FECHA_PEDI FECHA_ENVI       DIAS
---------- ---------- ---------- ----------
      5004 20/02/2025 24/02/2025          4
      5010 02/06/2025 06/06/2025          4
      5006 22/03/2025 25/03/2025          3
      5016 12/01/2026 15/01/2026          3
      5018 03/03/2026 06/03/2026          3

5 filas seleccionadas.

La aritmética de fechas se estudia a fondo en el módulo 28.

Ejercicios propuestos

Resuélvelos por tu cuenta; entre paréntesis tienes el número de filas que debe devolver tu consulta para que puedas comprobarla.

  1. Muestra nombre, localidad y presupuesto de los departamentos con presupuesto superior a 200 000 € (3 filas).
  2. Muestra los productos de las categorías 5 y 6 ordenados por precio descendente (5 filas).
  3. Lista los empleados cuyo nombre empieza por S, M o R (6 filas).
  4. Muestra los clientes dados de alta en 2024 o en 2025 (4 filas).
  5. Muestra los pedidos del cliente 1002 ordenados del más reciente al más antiguo (3 filas).
  6. Muestra los empleados cuyo email contiene .r (por ejemplo, carmen.ruiz@...) (4 filas).
  7. Muestra las líneas de pedido con un descuento de al menos el 10 % (8 filas).
  8. Lista los proyectos que siguen abiertos (sin fecha de fin) (3 filas).
  9. Muestra los empleados cuyo jefe es el 101 o el 120 y que ganan menos de 2500 € (6 filas).
  10. Muestra nombre y precio con IVA (21 %) de los productos de menos de 100 €, con el alias "Precio con IVA" (7 filas).
  11. Muestra los clientes que no son de Valencia ni de Madrid, incluyendo los que no tuvieran ciudad (10 filas).
  12. Obtén los pedidos 6.º a 10.º más antiguos (paginación con OFFSET) (5 filas).
  13. Muestra los 2 departamentos con mayor presupuesto con empates, y explica qué ocurre con el departamento sin presupuesto si ordenas de forma descendente sin NULLS LAST (2 filas).
  14. Muestra los productos cuyo nombre tiene exactamente 9 caracteres (pista: 9 guiones bajos) (2 filas).
  15. Muestra los empleados que no son directores (ni DIRECTOR ni DIRECTOR GENERAL) y que se dieron de alta entre 2018 y 2020, ambos años incluidos, ordenados por fecha de alta (7 filas).

Resumen

Concepto Sintaxis / ejemplo Recuerda
Orden lógico FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → FETCH El alias no existe en WHERE
Todas las columnas SELECT * FROM t Solo para explorar
Expresiones salario * 14, (salario + 100) * 14 * y / antes que + y -; con NULL da NULL
Alias expr AS alias, expr "Con espacios" Comillas dobles, nunca simples
Concatenación nombre \|\| ' ' \|\| apellidos, CONCAT(a, b) En Oracle NULL se concatena como ''
Literales 'texto', 'O''Brien', q'[It's]', DATE '2025-01-15' El decimal del código es .
Sin repetidos SELECT DISTINCT a, b Se aplica a la fila completa
DUAL SELECT SYSDATE FROM dual 23ai permite SELECT SYSDATE;
Comparación =, <>, !=, <, >, <=, >= Texto sensible a mayúsculas
Rango BETWEEN a AND b Extremos incluidos; menor primero
Lista IN (v1, v2), NOT IN (...) NOT IN con un NULL → ninguna fila
Patrones LIKE 'A%', LIKE '_a%', LIKE '%\_%' ESCAPE '\' % = 0 o más, _ = 1 carácter
Nulos IS NULL, IS NOT NULL = NULL nunca es cierto
Lógicos NOT > AND > OR Paréntesis siempre al mezclar
Ordenar ORDER BY a DESC, b NULLS FIRST, ORDER BY alias, ORDER BY 2 Sin ORDER BY no hay orden garantizado
Top-N FETCH FIRST n ROWS ONLY \| WITH TIES, FETCH FIRST p PERCENT ROWS ONLY Desde 12c
Paginación OFFSET n ROWS FETCH NEXT m ROWS ONLY Orden determinista
ROWNUM WHERE ROWNUM <= n Se asigna antes de ORDER BY
ROWID SELECT ROWID, ... Dirección física; no es una clave
Sustitución &var, &&var, DEFINE, SET VERIFY Solo en SQL*Plus, SQLcl y SQL Developer