Módulo 31: Consultas básicas
Objetivos del módulo
- Situar la sentencia
SELECTdentro del DML y relacionarla con el álgebra relacional - Conocer la sintaxis general de
SELECTy, 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
DISTINCTy usar la tablaDUAL(y elSELECTsinFROMde Oracle 23ai) - Filtrar filas con
WHERE: comparaciones,BETWEEN,IN,LIKE,IS NULLy operadores lógicos - Entender la lógica trivaluada y los problemas que causan los valores nulos, incluido el temido
NOT INconNULL - Ordenar resultados con
ORDER BY(alias, posición, expresiones,NULLS FIRST/LAST) - Limitar y paginar resultados con
FETCH FIRST,WITH TIES,PERCENT,OFFSET, y conocerROWNUMyROWID - 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) yDELETE(borrar), másMERGE(insertar o modificar). Algunos autores llaman aSELECTpor 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
SELECTcomo 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
SELECTes un multiconjunto (bag) y sí puede tener filas repetidas. Para eliminarlas hay que pedirlo explícitamente conDISTINCT(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 enLINESIZEla 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 comandoCOLUMN nombre FORMAT A27de 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_TERRITORYreiniciaNLS_DATE_FORMATal valor por defecto del territorio (DD/MM/RRen 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ódigoORA-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 erroresORAse 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 identifieres 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 EMPLEADOSyselect nombre from empleadosson 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.03solo existe en el resultado de la consulta. Para cambiar datos se usaUPDATE(módulo 34).
3.4. NULL en las expresiones
📘 Concepto:
NULLrepresenta un valor desconocido o inexistente. No es cero ni una cadena vacía. Cualquier operación aritmética conNULLdaNULL: 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 errorORA-00923: FROM keyword not found where expected. Además, un alias entre comillas dobles es sensible a mayúsculas: si enORDER BYescribes"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 deNLS_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
CONCATadmite exactamente dos argumentos (con tres daORA-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:
DISTINCTsolo puede ir una vez, justo detrás deSELECT.SELECT dept_id, DISTINCT oficioes 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; usaFROM dual, que funciona en todas las versiones (también en 23ai/26ai).
💡 Consejo: La fecha mostrada por
SYSDATEes 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ásUPPERyLOWERpara 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 otroNLS_DATE_FORMAT. UsaDATE 'AAAA-MM-DD'oTO_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
BETWEENy las fechas con hora. UnDATEde 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 esfecha >= 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:
INes más legible que muchosORy 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:
LIKEdistingue 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”),
LIKEse queda corto. En el módulo 28 verásREGEXP_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
ANDyOR, 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 INcontiene algúnNULL, el resultado es siempre vacío. Cuando usesNOT INcon una subconsulta, asegúrate de que la columna no puede ser nula (o filtra conIS NOT NULL), o usaNOT 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 BYes 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 delSELECT, 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_SORTafecta aORDER BY, pero las comparaciones delWHEREsiguen siendo binarias mientrasNLS_COMP = BINARY(el valor por defecto):'Á' > 'Z'es cierto en unWHERE. 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
pcontfilas por página:OFFSET (p - 1) * t ROWS FETCH NEXT t ROWS ONLY. ElORDER BYdebe 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. ConoceROWNUMporque 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:
ROWIDes ú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 antesSET 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
ANDyOR. - 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.
Ejercicio 33: Paginación del catálogo
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.
- Muestra nombre, localidad y presupuesto de los departamentos con presupuesto superior a 200 000 € (3 filas).
- Muestra los productos de las categorías 5 y 6 ordenados por precio descendente (5 filas).
- Lista los empleados cuyo nombre empieza por
S,MoR(6 filas). - Muestra los clientes dados de alta en 2024 o en 2025 (4 filas).
- Muestra los pedidos del cliente 1002 ordenados del más reciente al más antiguo (3 filas).
- Muestra los empleados cuyo email contiene
.r(por ejemplo,carmen.ruiz@...) (4 filas). - Muestra las líneas de pedido con un descuento de al menos el 10 % (8 filas).
- Lista los proyectos que siguen abiertos (sin fecha de fin) (3 filas).
- Muestra los empleados cuyo jefe es el 101 o el 120 y que ganan menos de 2500 € (6 filas).
- Muestra nombre y precio con IVA (21 %) de los productos de menos de 100 €, con el alias
"Precio con IVA"(7 filas). - Muestra los clientes que no son de Valencia ni de Madrid, incluyendo los que no tuvieran ciudad (10 filas).
- Obtén los pedidos 6.º a 10.º más antiguos (paginación con
OFFSET) (5 filas). - 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). - Muestra los productos cuyo nombre tiene exactamente 9 caracteres (pista: 9 guiones bajos) (2 filas).
- Muestra los empleados que no son directores (ni
DIRECTORniDIRECTOR 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 |