Funciones en PHP: parámetros, return, alcance, tipado y callbacks

Hasta ahora vimos variables, condicionales, bucles y arrays. A medida que un programa crece, aparece un problema muy común: la misma lógica empieza a repetirse en varios lugares.

Las funciones permiten agrupar instrucciones bajo un nombre, reutilizarlas y dividir un programa en partes más pequeñas y comprensibles.

¿Qué es una función?

Una función es un bloque de código que realiza una tarea concreta y que puede ejecutarse cuando sea necesario.

En vez de repetir varias veces:

$precio = 1000;
$iva = $precio * 0.21;
$total = $precio + $iva;

podemos encapsular esa lógica dentro de una función y reutilizarla.

Funciones incorporadas y funciones propias

PHP incluye muchas funciones listas para usar, como:

count()
strlen()
var_dump()
array_filter()
date()

Estas funciones ya vienen con PHP.

Pero también podemos crear nuestras propias funciones con la palabra clave function.

Crear una función

function saludar() {
    echo 'Hola desde una función';
}

Esto define la función, pero todavía no la ejecuta.

Llamar una función

saludar();

Cuando PHP encuentra esa llamada, ejecuta el código que está dentro de la función.

¿Por qué usar funciones?

Las funciones ayudan a:

  • evitar código repetido;
  • dividir problemas grandes en partes pequeñas;
  • mejorar la lectura del código;
  • facilitar pruebas y mantenimiento;
  • reutilizar lógica.

Parámetros

Una función puede recibir valores desde afuera mediante parámetros.

function saludar($nombre) {
    echo 'Hola ' . $nombre;
}

saludar('Ana');

En este caso $nombre es el parámetro de la función.

Parámetros y argumentos

Aunque suelen confundirse, no son exactamente lo mismo:

  • parámetro: variable declarada en la función;
  • argumento: valor que enviamos al llamar la función.

Ejemplo:

function saludar($nombre) {
    // $nombre es parámetro
}

saludar('Ana'); // 'Ana' es argumento

Más de un parámetro

function sumar($a, $b) {
    return $a + $b;
}

La función recibe dos valores y los utiliza dentro de su lógica.

return

return devuelve un valor al lugar desde donde se llamó la función.

function sumar($a, $b) {
    return $a + $b;
}

$resultado = sumar(10, 5);

echo $resultado;

Resultado:

15

Diferencia entre echo y return

Esta diferencia es fundamental.

echo muestra contenido:

function saludar($nombre) {
    echo 'Hola ' . $nombre;
}

return devuelve un valor para poder seguir utilizándolo:

function obtenerSaludo($nombre) {
    return 'Hola ' . $nombre;
}

$mensaje = obtenerSaludo('Ana');

En general, una función es más reutilizable cuando devuelve datos en lugar de imprimirlos directamente.

return termina la función

Cuando PHP ejecuta return, la función finaliza.

function ejemplo() {
    return 10;

    echo 'Esto nunca se ejecuta';
}

Parámetros con valores por defecto

Podemos hacer que un parámetro sea opcional.

function saludar($nombre = 'Invitado') {
    return 'Hola ' . $nombre;
}

echo saludar();
echo saludar('Ana');

Si no enviamos un argumento, se utiliza 'Invitado'.

Los parámetros obligatorios deben ir primero

Es recomendable escribir primero los parámetros obligatorios y luego los opcionales.

function crearUsuario($nombre, $activo = true) {
    // ...
}

Ejemplo: calcular IVA

function calcularIva($importe, $porcentaje = 21) {
    return $importe * $porcentaje / 100;
}

$iva = calcularIva(10000);

echo $iva;

La lógica del cálculo queda centralizada en un único lugar.

Ejemplo: calcular total con IVA

function calcularTotalConIva($importe, $porcentaje = 21) {
    $iva = $importe * $porcentaje / 100;

    return $importe + $iva;
}

$total = calcularTotalConIva(10000);

Alcance de variables

Una variable creada fuera de una función no está disponible automáticamente dentro de ella.

$nombre = 'Ana';

function mostrarNombre() {
    echo $nombre;
}

Ese código no funciona como muchos principiantes esperan, porque $nombre pertenece al ámbito exterior.

Variables locales

Las variables creadas dentro de una función existen solamente dentro de esa función.

function calcular() {
    $resultado = 10 + 5;

    return $resultado;
}

$resultado es una variable local.

La palabra global

PHP permite acceder a una variable externa mediante global.

$iva = 21;

function mostrarIva() {
    global $iva;

    echo $iva;
}

Aunque existe, conviene evitar depender de variables globales siempre que sea posible. Las funciones suelen ser más fáciles de entender y probar cuando reciben los datos mediante parámetros.

Una alternativa mejor a global

$iva = 21;

function calcularImpuesto($importe, $iva) {
    return $importe * $iva / 100;
}

$resultado = calcularImpuesto(10000, $iva);

La dependencia queda explícita.

Variables static dentro de funciones

Una variable local normalmente se crea de nuevo cada vez que llamamos a la función.

Con static podemos conservar su valor entre llamadas.

function contador() {
    static $numero = 0;

    $numero++;

    return $numero;
}

echo contador(); // 1
echo contador(); // 2
echo contador(); // 3

Tipado de parámetros

PHP permite indicar qué tipo esperamos recibir.

function sumar(int $a, int $b) {
    return $a + $b;
}

Esto expresa que ambos parámetros deben representar enteros.

Tipado de retorno

También podemos indicar qué tipo debe devolver la función.

function sumar(int $a, int $b): int {
    return $a + $b;
}

El : int indica el tipo de retorno.

Ejemplo con string

function nombreCompleto(string $nombre, string $apellido): string {
    return $nombre . ' ' . $apellido;
}

Ejemplo con bool

function esMayorDeEdad(int $edad): bool {
    return $edad >= 18;
}

Esta función no imprime un mensaje. Devuelve true o false, lo que permite reutilizarla dentro de otras condiciones.

Nullable types

En algunos casos queremos aceptar un tipo o null.

function mostrarTelefono(?string $telefono): string {
    return $telefono ?? 'Sin teléfono';
}

?string significa:

string o null

Union types

Desde PHP 8 podemos declarar más de un tipo posible utilizando |.

function mostrarId(int|string $id): string {
    return 'ID: ' . $id;
}

La función acepta un entero o un string.

Funciones que devuelven arrays

function crearUsuario(string $nombre, string $email): array {
    return [
        'nombre' => $nombre,
        'email' => $email,
        'activo' => true
    ];
}

Los arrays son un tipo de retorno muy habitual en PHP.

Paso por valor

Por defecto, los valores simples se pasan de forma que modificar el parámetro no altera necesariamente la variable original.

function duplicar($numero) {
    $numero *= 2;
}

$valor = 10;
duplicar($valor);

echo $valor;

El valor exterior sigue siendo 10.

Paso por referencia con &

Podemos indicar que queremos trabajar directamente sobre la variable original utilizando &.

function duplicar(&$numero) {
    $numero *= 2;
}

$valor = 10;
duplicar($valor);

echo $valor;

Ahora el resultado es:

20

El paso por referencia debe utilizarse con cuidado porque la función produce un efecto sobre una variable externa.

Funciones anónimas

Una función no siempre necesita tener un nombre.

$saludar = function ($nombre) {
    return 'Hola ' . $nombre;
};

echo $saludar('Ana');

Este tipo de función es muy utilizado en callbacks.

Callbacks

Un callback es una función que enviamos a otra función para que sea ejecutada.

Ya vimos un ejemplo con array_map():

$numeros = [1, 2, 3, 4];

$dobles = array_map(function ($numero) {
    return $numero * 2;
}, $numeros);

array_map() ejecuta nuestro callback para cada elemento.

Arrow functions

PHP permite escribir funciones anónimas simples de forma más compacta mediante fn.

$numeros = [1, 2, 3, 4];

$dobles = array_map(
    fn($numero) => $numero * 2,
    $numeros
);

Captura de variables en arrow functions

Las arrow functions pueden utilizar variables del ámbito exterior automáticamente.

$multiplicador = 3;

$resultado = array_map(
    fn($numero) => $numero * $multiplicador,
    [1, 2, 3]
);

Funciones anónimas y use

En una función anónima tradicional podemos importar variables externas mediante use.

$iva = 21;

$calcular = function ($importe) use ($iva) {
    return $importe * $iva / 100;
};

Ejemplo práctico: calcular descuento

function calcularDescuento(float $total, bool $premium): float {
    if ($premium && $total >= 100000) {
        return $total * 0.20;
    }

    if ($total >= 100000) {
        return $total * 0.10;
    }

    return 0;
}

La función recibe toda la información necesaria y devuelve únicamente el descuento calculado.

Ejemplo práctico: total de productos

function calcularTotal(array $productos): float {
    $total = 0;

    foreach ($productos as $producto) {
        $total += $producto['precio'] * $producto['cantidad'];
    }

    return $total;
}

Una función debería hacer una tarea clara

En general, una función es más fácil de mantener cuando tiene una responsabilidad bien definida.

Es mejor tener:

calcularSubtotal()
calcularIva()
calcularDescuento()

que una función gigantesca llamada:

procesarTodo()

Nombres descriptivos

El nombre debería explicar qué hace la función.

Buenos ejemplos:

calcularTotal()
esMayorDeEdad()
obtenerNombreCompleto()
filtrarProductosActivos()

Nombres poco claros:

hacer()
procesar()
x()

Errores comunes

  • confundir echo con return;
  • olvidar llamar la función después de crearla;
  • depender demasiado de variables globales;
  • crear funciones que hacen demasiadas cosas;
  • no usar nombres descriptivos;
  • usar paso por referencia sin necesidad;
  • declarar tipos incompatibles con lo que realmente devuelve la función;
  • duplicar lógica en vez de reutilizar una función existente.

Buenas prácticas

  • dar a cada función una responsabilidad clara;
  • preferir parámetros frente a variables globales;
  • devolver datos con return cuando sea posible;
  • usar tipos cuando ayuden a expresar la intención;
  • mantener funciones relativamente pequeñas;
  • elegir nombres que describan la acción.

Ejercicios

  1. crear una función que salude a una persona;
  2. crear una función que sume dos números;
  3. calcular IVA utilizando un parámetro opcional;
  4. crear una función que determine si una persona es mayor de edad;
  5. crear una función que devuelva nombre y apellido unidos;
  6. crear una función que reciba un array y devuelva su suma;
  7. declarar tipos de parámetros y retorno;
  8. crear una función que acepte string|null;
  9. crear una función que acepte un id int|string;
  10. probar paso por valor y paso por referencia;
  11. duplicar un array de números usando array_map() y una función anónima;
  12. reescribir el ejercicio anterior mediante una arrow function;
  13. crear una función para calcular descuentos;
  14. crear una función que calcule el total de un array de productos.

Qué aprendimos

  • qué es una función;
  • funciones incorporadas y propias;
  • parámetros y argumentos;
  • return;
  • diferencia entre echo y return;
  • valores por defecto;
  • alcance de variables;
  • global y static;
  • tipado de parámetros y retorno;
  • nullable y union types;
  • paso por valor y por referencia;
  • funciones anónimas;
  • callbacks;
  • arrow functions.

¿Qué sigue?

En la próxima guía comenzaremos a trabajar con formularios HTML y PHP: métodos GET y POST, las variables $_GET y $_POST, validación y procesamiento de datos enviados por el usuario.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Arrays en PHP: indexados, asociativos y multidimensionales con ejemplos prácticos

Hasta ahora trabajamos con variables que guardaban un solo valor. Pero en muchas situaciones necesitamos almacenar varios datos relacionados dentro de una misma estructura.

Para eso PHP utiliza los arrays. Son fundamentales porque aparecen constantemente en formularios, resultados de bases de datos, APIs, configuraciones y colecciones de objetos.

¿Qué es un array?

Un array es una estructura que permite guardar varios valores dentro de una misma variable.

En lugar de hacer esto:

$color1 = 'rojo';
$color2 = 'verde';
$color3 = 'azul';

podemos agruparlos:

$colores = ['rojo', 'verde', 'azul'];

Ahora $colores contiene tres elementos.

Array indexado

Un array indexado utiliza posiciones numéricas para identificar cada elemento.

$frutas = ['Manzana', 'Pera', 'Naranja'];

PHP comienza los índices numéricos en 0.

0 → Manzana
1 → Pera
2 → Naranja

Acceder a un elemento

$frutas = ['Manzana', 'Pera', 'Naranja'];

echo $frutas[0];

Resultado:

Manzana

Modificar un elemento

$frutas[1] = 'Banana';

Ahora el array contiene:

Manzana
Banana
Naranja

Agregar elementos

La forma más simple es utilizar corchetes vacíos:

$frutas[] = 'Kiwi';

PHP agrega el nuevo valor al final del array.

Array asociativo

Un array asociativo utiliza claves con significado en lugar de índices numéricos.

$persona = [
    'nombre' => 'Ana',
    'edad' => 25,
    'ciudad' => 'Santa Rosa'
];

En este caso las claves son:

nombre
edad
ciudad

Acceder por clave

echo $persona['nombre'];

Resultado:

Ana

Modificar un valor asociativo

$persona['edad'] = 26;

Agregar una nueva clave

$persona['email'] = 'ana@email.com';

Clave y valor

En un array asociativo cada elemento se compone de dos partes:

clave => valor

Por ejemplo:

'nombre' => 'Ana'

La clave identifica el dato y el valor contiene la información.

Arrays multidimensionales

Un array puede contener otros arrays.

$usuarios = [
    [
        'nombre' => 'Ana',
        'edad' => 25
    ],
    [
        'nombre' => 'Juan',
        'edad' => 31
    ]
];

Esto permite representar estructuras más complejas.

Acceder a un array multidimensional

echo $usuarios[0]['nombre'];

Resultado:

Ana

Primero seleccionamos el elemento 0 del array principal y luego la clave nombre.

Ejemplo real: productos

$productos = [
    [
        'nombre' => 'Teclado',
        'precio' => 25000,
        'stock' => 10
    ],
    [
        'nombre' => 'Mouse',
        'precio' => 15000,
        'stock' => 25
    ]
];

Este formato es muy parecido a la información que luego podemos obtener desde una base de datos.

Recorrer arrays con foreach

foreach es la estructura más cómoda para recorrer arrays.

$frutas = ['Manzana', 'Pera', 'Naranja'];

foreach ($frutas as $fruta) {
    echo $fruta . '
'
; }

Recorrer clave y valor

foreach ($persona as $clave => $valor) {
    echo $clave . ': ' . $valor . '
'
; }

Recorrer arrays multidimensionales

foreach ($productos as $producto) {
    echo $producto['nombre'] . ' - $' . $producto['precio'] . '
'
; }

count()

count() devuelve la cantidad de elementos de un array.

$frutas = ['Manzana', 'Pera', 'Naranja'];

echo count($frutas);

Resultado:

3

in_array()

in_array() permite saber si un valor existe dentro de un array.

$roles = ['admin', 'editor', 'usuario'];

if (in_array('admin', $roles, true)) {
    echo 'Existe el rol admin';
}

El tercer parámetro true solicita comparación estricta.

array_key_exists()

En arrays asociativos podemos comprobar si existe una clave.

if (array_key_exists('email', $persona)) {
    echo $persona['email'];
}

isset() y una diferencia importante

isset() también suele utilizarse para comprobar datos:

if (isset($persona['email']))) {
    echo 'Existe y no es NULL';
}

Pero isset() devuelve falso si la clave existe y su valor es null. array_key_exists(), en cambio, verifica la existencia de la clave independientemente de su valor.

Eliminar elementos con unset()

$frutas = ['Manzana', 'Pera', 'Naranja'];

unset($frutas[1]);

El elemento se elimina, pero PHP no necesariamente reordena automáticamente los índices existentes.

Agregar con array_push()

array_push($frutas, 'Kiwi');

Aunque funciona, para agregar un único elemento suele ser más simple:

$frutas[] = 'Kiwi';

array_pop()

Elimina y devuelve el último elemento.

$ultimo = array_pop($frutas);

array_shift()

Elimina y devuelve el primer elemento.

$primero = array_shift($frutas);

array_unshift()

Agrega uno o más elementos al comienzo.

array_unshift($frutas, 'Frutilla');

Ordenar arrays

sort()

Ordena un array indexado de menor a mayor.

$numeros = [5, 2, 9, 1];

sort($numeros);

rsort()

Ordena en sentido descendente.

rsort($numeros);

Ordenar arrays asociativos

asort() ordena por valor manteniendo la relación con las claves.

$precios = [
    'teclado' => 25000,
    'mouse' => 15000,
    'monitor' => 180000
];

asort($precios);

ksort() ordena por clave.

ksort($precios);

array_merge()

Permite combinar arrays.

$grupo1 = ['Ana', 'Juan'];
$grupo2 = ['Lucía', 'Pedro'];

$todos = array_merge($grupo1, $grupo2);

implode()

implode() convierte un array en un string utilizando un separador.

$nombres = ['Ana', 'Juan', 'Lucía'];

$texto = implode(', ', $nombres);

echo $texto;

Resultado:

Ana, Juan, Lucía

explode()

explode() realiza la operación inversa: separa un string y devuelve un array.

$texto = 'Ana,Juan,Lucía';

$nombres = explode(',', $texto);

array_map()

array_map() permite transformar cada elemento de un array.

$numeros = [1, 2, 3, 4];

$dobles = array_map(function ($numero) {
    return $numero * 2;
}, $numeros);

El resultado será:

[2, 4, 6, 8]

¿Qué es un callback?

En el ejemplo anterior, array_map() recibe una función que PHP ejecutará para cada elemento.

A una función que se entrega a otra función para que sea ejecutada posteriormente se la suele llamar callback.

Más adelante veremos funciones con mayor profundidad.

array_filter()

array_filter() permite conservar únicamente los elementos que cumplen una condición.

$numeros = [1, 2, 3, 4, 5, 6];

$pares = array_filter($numeros, function ($numero) {
    return $numero % 2 === 0;
});

Ejemplo práctico: filtrar productos con stock

$productos = [
    ['nombre' => 'Teclado', 'stock' => 10],
    ['nombre' => 'Mouse', 'stock' => 0],
    ['nombre' => 'Monitor', 'stock' => 5]
];

$disponibles = array_filter($productos, function ($producto) {
    return $producto['stock'] > 0;
});

Ahora $disponibles contiene solamente productos con stock.

Acceso seguro con ??

Cuando una clave puede no existir, podemos usar el operador ??.

$telefono = $persona['telefono'] ?? 'Sin teléfono';

Si la clave no existe o contiene null, se utiliza el valor alternativo.

Arrays y datos de formularios

Cuando trabajemos con formularios veremos variables como:

$_GET
$_POST
$_FILES
$_SESSION

Varias de estas estructuras son arrays asociativos proporcionados por PHP.

Por eso dominar arrays es fundamental antes de avanzar hacia formularios y aplicaciones reales.

Arrays y bases de datos

Cuando consultamos una base de datos desde PHP, es habitual recibir resultados con una estructura similar a:

[
    ['id' => 1, 'nombre' => 'Ana'],
    ['id' => 2, 'nombre' => 'Juan']
]

Luego podemos recorrer esos registros con foreach.

Errores comunes

  • olvidar que los arrays indexados comienzan en 0;
  • intentar acceder a una clave que no existe;
  • confundir índice con valor;
  • usar sort() sobre un array asociativo cuando queremos conservar sus claves;
  • no diferenciar isset() de array_key_exists();
  • crear estructuras multidimensionales innecesariamente complejas;
  • modificar un array sin comprender cómo afecta sus índices.

Buenas prácticas

  • usar claves descriptivas;
  • mantener estructuras consistentes;
  • usar foreach para recorrer colecciones;
  • comprobar claves opcionales antes de acceder;
  • evitar demasiados niveles de anidación;
  • utilizar funciones de arrays cuando expresen mejor la intención del código.

Ejercicios

  1. crear un array con cinco nombres y recorrerlo;
  2. mostrar el tercer elemento de un array indexado;
  3. crear un array asociativo con nombre, edad y email;
  4. agregar una nueva clave al array;
  5. crear un array de productos con nombre, precio y stock;
  6. recorrer productos con foreach;
  7. contar productos utilizando count();
  8. comprobar si existe un valor con in_array();
  9. comprobar una clave con array_key_exists();
  10. ordenar números ascendente y descendentemente;
  11. convertir un array de nombres a texto con implode();
  12. convertir un texto separado por comas en array con explode();
  13. duplicar números utilizando array_map();
  14. filtrar números pares con array_filter();
  15. filtrar productos con stock mayor a cero.

Qué aprendimos

  • qué es un array;
  • arrays indexados;
  • arrays asociativos;
  • arrays multidimensionales;
  • clave y valor;
  • recorrido con foreach;
  • count();
  • in_array();
  • array_key_exists();
  • isset();
  • ordenamiento;
  • array_merge();
  • implode() y explode();
  • array_map() y array_filter();
  • callbacks;
  • relación de los arrays con formularios y bases de datos.

¿Qué sigue?

En la próxima guía veremos funciones en PHP: cómo crear nuestras propias funciones, parámetros, valores de retorno, alcance de variables y tipado.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Bucles en PHP: for, while, do while y foreach con ejemplos prácticos

Muchas tareas de programación requieren repetir instrucciones: mostrar una lista, recorrer productos, sumar valores, buscar un dato o ejecutar una acción varias veces.

En PHP estas repeticiones se realizan mediante bucles. En esta guía vamos a aprender cómo funcionan for, while, do while y foreach, cuándo conviene usar cada uno y cómo controlar su ejecución.

¿Qué es un bucle?

Un bucle es una estructura que permite ejecutar un bloque de código varias veces.

Por ejemplo, si queremos mostrar los números del 1 al 5, podríamos escribir:

echo 1;
echo 2;
echo 3;
echo 4;
echo 5;

Pero eso es repetitivo. Un bucle permite expresar la misma idea de forma más compacta y flexible.

Dos formas comunes de repetir

Podemos pensar los bucles de dos maneras principales:

  • por cantidad: repetir un número conocido de veces;
  • por condición: repetir mientras algo siga siendo verdadero.

Por ejemplo:

  • mostrar del 1 al 10 → cantidad conocida;
  • seguir leyendo hasta encontrar un dato → depende de una condición.

El bucle for

for es ideal cuando sabemos cuántas veces queremos repetir algo.

for ($i = 1; $i <= 5; $i++) {
    echo $i;
}

Este bucle muestra:

12345

Las tres partes del for

La estructura general es:

for (inicialización; condición; actualización) {
    // instrucciones
}

En:

for ($i = 1; $i <= 5; $i++)

tenemos:

  • $i = 1: valor inicial;
  • $i <= 5: condición para seguir;
  • $i++: incremento después de cada vuelta.

¿Qué es una iteración?

Cada repetición completa del bucle se denomina iteración.

En un for del 1 al 5 hay cinco iteraciones.

Mostrar números del 1 al 10

for ($i = 1; $i <= 10; $i++) {
    echo $i . ' ';
}

Contar hacia atrás

for ($i = 10; $i >= 1; $i--) {
    echo $i . ' ';
}

En este caso utilizamos $i-- para disminuir el contador.

Tabla de multiplicar

$numero = 7;

for ($i = 1; $i <= 10; $i++) {
    $resultado = $numero * $i;
    echo "$numero x $i = $resultado" . '
'
; }

El contador cambia en cada iteración y permite calcular una fila diferente.

Contadores y acumuladores

Un contador normalmente aumenta o disminuye para controlar repeticiones.

$contador = 0;
$contador++;

Un acumulador guarda un resultado que se va construyendo.

$suma = 0;

for ($i = 1; $i <= 100; $i++) {
    $suma += $i;
}

echo $suma;

En cada vuelta sumamos el valor actual de $i al acumulador.

El bucle while

while repite un bloque mientras una condición sea verdadera.

$i = 1;

while ($i <= 5) {
    echo $i;
    $i++;
}

Antes de cada iteración, PHP evalúa la condición.

¿Cuándo conviene while?

while es útil cuando no sabemos exactamente cuántas repeticiones serán necesarias.

Por ejemplo:

  • esperar hasta encontrar un valor;
  • procesar datos mientras existan;
  • repetir una operación hasta alcanzar una condición.

El peligro de un bucle infinito

Si la condición nunca se vuelve falsa, el bucle puede ejecutarse indefinidamente.

$i = 1;

while ($i <= 5) {
    echo $i;
}

Este ejemplo es incorrecto porque $i nunca cambia.

La versión correcta sería:

$i = 1;

while ($i <= 5) {
    echo $i;
    $i++;
}

do while

do while es parecido a while, pero garantiza que el bloque se ejecute al menos una vez.

$i = 1;

do {
    echo $i;
    $i++;
} while ($i <= 5);

Diferencia entre while y do while

Con while la condición se evalúa antes de ejecutar el bloque.

Con do while la condición se evalúa después.

while → evalúa → ejecuta

do while → ejecuta → evalúa

Por eso do while siempre ejecuta al menos una iteración.

Ejemplo donde do while ejecuta una vez

$numero = 10;

do {
    echo 'Se ejecutó una vez';
} while ($numero < 5);

Aunque la condición sea falsa desde el principio, el bloque se ejecuta una vez.

foreach

foreach está diseñado especialmente para recorrer arrays y estructuras iterables.

$nombres = ['Ana', 'Juan', 'Lucía'];

foreach ($nombres as $nombre) {
    echo $nombre . '
'
; }

En cada iteración, $nombre toma el valor de un elemento del array.

¿Qué significa as?

La palabra as indica qué variable utilizar para representar el elemento actual.

foreach ($nombres as $nombre)

Puede leerse como:

“por cada elemento de $nombres, guardalo temporalmente en $nombre”.

foreach con clave y valor

También podemos obtener la clave de cada elemento.

$persona = [
    'nombre' => 'Ana',
    'edad' => 25,
    'ciudad' => 'Santa Rosa'
];

foreach ($persona as $clave => $valor) {
    echo $clave . ': ' . $valor . '
'
; }

En cada vuelta:

  • $clave recibe la clave;
  • $valor recibe el contenido.

Recorrer productos

$productos = [
    ['nombre' => 'Teclado', 'precio' => 25000],
    ['nombre' => 'Mouse', 'precio' => 15000],
    ['nombre' => 'Monitor', 'precio' => 180000]
];

foreach ($productos as $producto) {
    echo $producto['nombre'] . ' - $' . $producto['precio'] . '
'
; }

Este tipo de estructura es muy común al trabajar con resultados de bases de datos o APIs.

break

break permite finalizar un bucle antes de que termine normalmente.

for ($i = 1; $i <= 10; $i++) {
    if ($i === 5) {
        break;
    }

    echo $i . ' ';
}

Resultado:

1 2 3 4

Buscar un valor con break

$numeros = [3, 8, 12, 20, 25];
$buscado = 12;

foreach ($numeros as $numero) {
    if ($numero === $buscado) {
        echo 'Encontrado';
        break;
    }
}

Una vez encontrado el valor, no tiene sentido seguir recorriendo el array.

continue

continue salta la iteración actual y pasa a la siguiente.

for ($i = 1; $i <= 10; $i++) {
    if ($i === 5) {
        continue;
    }

    echo $i . ' ';
}

Resultado:

1 2 3 4 6 7 8 9 10

Ejemplo: mostrar solo números pares

for ($i = 1; $i <= 20; $i++) {
    if ($i % 2 !== 0) {
        continue;
    }

    echo $i . ' ';
}

Los números impares se saltan y solamente se muestran los pares.

Bucles anidados

Un bucle puede contener otro bucle.

for ($fila = 1; $fila <= 3; $fila++) {
    for ($columna = 1; $columna <= 3; $columna++) {
        echo "($fila,$columna) ";
    }

    echo '
'
; }

Por cada iteración del bucle exterior, el bucle interior se ejecuta completo.

¿Cuántas veces se ejecuta un bucle anidado?

Si el bucle exterior se ejecuta 3 veces y el interior 4 veces, el bloque interior se ejecuta:

3 × 4 = 12 veces

Esto es importante porque los bucles anidados pueden aumentar rápidamente el trabajo realizado.

for, while, do while o foreach

Una guía rápida:

  • for: cuando tenemos un contador o cantidad conocida;
  • while: cuando repetimos mientras una condición sea verdadera;
  • do while: cuando necesitamos ejecutar al menos una vez;
  • foreach: cuando recorremos arrays u objetos iterables.

Ejemplo completo: total de una compra

$productos = [
    ['nombre' => 'Teclado', 'precio' => 25000, 'cantidad' => 2],
    ['nombre' => 'Mouse', 'precio' => 15000, 'cantidad' => 1],
    ['nombre' => 'Monitor', 'precio' => 180000, 'cantidad' => 1]
];

$total = 0;

foreach ($productos as $producto) {
    $subtotal = $producto['precio'] * $producto['cantidad'];
    $total += $subtotal;

    echo $producto['nombre'] . ' - $' . $subtotal . '
'
; } echo 'Total: $' . $total;

En este ejemplo usamos:

  • un array de productos;
  • foreach para recorrerlo;
  • una variable $subtotal;
  • un acumulador $total.

Errores comunes

  • crear un bucle infinito;
  • olvidar incrementar el contador en un while;
  • usar for cuando foreach sería más claro;
  • confundir break con continue;
  • modificar accidentalmente variables de control;
  • crear demasiados niveles de bucles anidados;
  • usar nombres poco descriptivos para los elementos recorridos.

Buenas prácticas

  • usar nombres claros como $producto en lugar de $x;
  • mantener simple la condición del bucle;
  • evitar trabajo innecesario dentro de cada iteración;
  • usar break cuando ya no sea necesario continuar;
  • preferir foreach para recorrer arrays.

Ejercicios

  1. mostrar los números del 1 al 20 con for;
  2. mostrar los números del 20 al 1;
  3. sumar los números del 1 al 100;
  4. mostrar una tabla de multiplicar;
  5. resolver los ejercicios anteriores utilizando while;
  6. crear un ejemplo donde do while se ejecute una sola vez;
  7. recorrer un array de nombres con foreach;
  8. recorrer un array asociativo mostrando clave y valor;
  9. buscar un número y detener el recorrido con break;
  10. mostrar únicamente números pares utilizando continue;
  11. crear una matriz 3 × 3 mediante bucles anidados;
  12. calcular el total de un listado de productos.

Qué aprendimos

  • qué es un bucle;
  • qué es una iteración;
  • cómo funciona for;
  • contadores y acumuladores;
  • while;
  • do while;
  • foreach;
  • clave y valor;
  • break;
  • continue;
  • bucles anidados;
  • cuándo conviene cada estructura.

¿Qué sigue?

En la próxima guía profundizaremos en arrays en PHP: arrays indexados, asociativos, multidimensionales y las funciones más importantes para trabajar con ellos.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Condicionales en PHP: if, elseif, else, switch, match y operador ternario

Los programas no siempre ejecutan exactamente las mismas instrucciones. Muchas veces necesitan tomar decisiones según los datos disponibles: permitir o negar acceso, aplicar un descuento, mostrar un mensaje distinto o ejecutar una acción determinada.

En PHP estas decisiones se construyen mediante condicionales. En esta guía vamos a aprender cómo funcionan if, elseif, else, switch, match y el operador ternario.

¿Qué es una condición?

Una condición es una expresión que PHP puede evaluar como verdadera o falsa.

Por ejemplo:

$edad = 20;

$edad >= 18

La expresión $edad >= 18 produce un valor booleano:

true

Ese resultado puede utilizarse para decidir qué bloque de código ejecutar.

Recordatorio: operadores de comparación

Las condiciones suelen construirse con operadores como:

  • ==: igualdad flexible;
  • ===: igualdad estricta;
  • !=: distinto;
  • !==: distinto en valor o tipo;
  • >: mayor;
  • <: menor;
  • >=: mayor o igual;
  • <=: menor o igual.

La estructura if

if ejecuta un bloque de código solamente cuando una condición es verdadera.

<?php

$edad = 20;

if ($edad >= 18) {
    echo 'Es mayor de edad';
}

PHP evalúa primero:

$edad >= 18

Como el resultado es true, ejecuta el contenido entre llaves.

¿Qué representan las llaves?

Las llaves { } delimitan el bloque de instrucciones que pertenece al condicional.

if ($condicion) {
    echo 'Primera instrucción';
    echo 'Segunda instrucción';
}

Si la condición es falsa, ninguna de esas instrucciones se ejecuta.

if y else

else permite definir qué debe ocurrir cuando la condición del if es falsa.

$edad = 16;

if ($edad >= 18) {
    echo 'Es mayor de edad';
} else {
    echo 'Es menor de edad';
}

Solo uno de los dos bloques será ejecutado.

if, elseif y else

Cuando existen más de dos posibilidades podemos utilizar elseif.

$nota = 8;

if ($nota >= 9) {
    echo 'Excelente';
} elseif ($nota >= 6) {
    echo 'Aprobado';
} else {
    echo 'Desaprobado';
}

PHP evalúa las condiciones de arriba hacia abajo y ejecuta el primer bloque cuya condición sea verdadera.

El orden de las condiciones importa

Este ejemplo está mal planteado:

if ($nota >= 6) {
    echo 'Aprobado';
} elseif ($nota >= 9) {
    echo 'Excelente';
}

Si la nota es 10, la primera condición ya es verdadera y PHP nunca llega al segundo bloque.

Por eso conviene ordenar primero las condiciones más específicas.

Condiciones compuestas

Podemos combinar varias condiciones mediante operadores lógicos.

AND con &&

$edad = 20;
$tieneDocumento = true;

if ($edad >= 18 && $tieneDocumento === true) {
    echo 'Puede ingresar';
}

Ambas condiciones deben ser verdaderas.

OR con ||

$esAdmin = false;
$esSupervisor = true;

if ($esAdmin || $esSupervisor) {
    echo 'Tiene permisos';
}

Alcanza con que una de las dos condiciones sea verdadera.

NOT con !

$bloqueado = false;

if (!$bloqueado) {
    echo 'Usuario habilitado';
}

El operador ! invierte un valor booleano.

Usar paréntesis para expresar la lógica

Cuando combinamos varias condiciones, los paréntesis ayudan a dejar claro el orden de evaluación.

if (($edad >= 18 && $tieneDocumento) || $esAdmin) {
    echo 'Acceso permitido';
}

La intención resulta mucho más clara que en una expresión larga sin agrupación visual.

Comparación estricta dentro de condicionales

Cuando queremos comparar no solo el valor sino también el tipo, utilizamos ===.

$estado = '1';

if ($estado === 1) {
    echo 'Activo';
}

Este bloque no se ejecuta porque '1' es un string y 1 es un entero.

Valores truthy y falsy

PHP puede interpretar ciertos valores como verdaderos o falsos incluso si no son booleanos explícitos.

Por ejemplo, estos valores suelen evaluarse como falsos:

  • false;
  • 0;
  • 0.0;
  • '';
  • '0';
  • null;
  • un array vacío.

Ejemplo:

$nombre = '';

if ($nombre) {
    echo 'Hay un nombre';
} else {
    echo 'El nombre está vacío';
}

Esto puede ser útil, pero también puede generar confusión. Cuando el tipo importa, conviene escribir condiciones explícitas.

switch

switch resulta útil cuando queremos comparar una misma variable contra varios valores posibles.

$estado = 'pendiente';

switch ($estado) {
    case 'pendiente':
        echo 'Pedido pendiente';
        break;

    case 'pagado':
        echo 'Pedido pagado';
        break;

    case 'cancelado':
        echo 'Pedido cancelado';
        break;

    default:
        echo 'Estado desconocido';
}

¿Para qué sirve break?

break detiene la ejecución del switch una vez encontrada una coincidencia.

Si lo omitimos, PHP puede continuar ejecutando los siguientes casos. Este comportamiento se denomina fall-through.

default

default funciona como un caso alternativo cuando ningún case coincide.

Es comparable al else de una estructura if.

¿Cuándo conviene switch?

Puede ser más legible que muchos elseif cuando comparamos una misma variable contra valores concretos.

Por ejemplo, estados como:

pendiente
pagado
cancelado
enviado

son un buen caso de uso.

match en PHP moderno

Desde PHP 8 existe match, una expresión más moderna para resolver ciertos casos similares a switch.

$estado = 'pagado';

$mensaje = match ($estado) {
    'pendiente' => 'Pedido pendiente',
    'pagado' => 'Pedido pagado',
    'cancelado' => 'Pedido cancelado',
    default => 'Estado desconocido',
};

echo $mensaje;

match devuelve un valor, por eso podemos asignar directamente su resultado a una variable.

Diferencias importantes entre switch y match

match tiene algunas ventajas:

  • utiliza comparación estricta;
  • no necesita break;
  • devuelve un valor;
  • suele ser más compacto.

Para código PHP moderno suele ser una alternativa muy clara cuando necesitamos mapear un valor a otro.

Operador ternario

El operador ternario permite expresar una decisión simple en una sola expresión.

$edad = 20;

$mensaje = $edad >= 18 ? 'Mayor de edad' : 'Menor de edad';

echo $mensaje;

Su estructura es:

condición ? valor_si_true : valor_si_false

¿Cuándo usar el ternario?

Conviene utilizarlo para decisiones breves y fáciles de leer.

Si la condición se vuelve compleja, un if suele ser más claro.

Ternario corto

PHP también permite:

$nombreMostrado = $nombre ?: 'Sin nombre';

Si $nombre es evaluado como verdadero, se utiliza su valor. De lo contrario, se utiliza 'Sin nombre'.

Ternario corto vs operador ??

No hacen exactamente lo mismo.

$valor = $nombre ?: 'Sin nombre';
$valor = $nombre ?? 'Sin nombre';

?: utiliza el valor alternativo cuando el primero es falsy.

?? utiliza el alternativo solamente cuando la variable no existe o es null.

Ejemplo: sistema de descuentos

$total = 150000;
$esClientePremium = true;

if ($esClientePremium && $total >= 100000) {
    $descuento = 20;
} elseif ($total >= 100000) {
    $descuento = 10;
} else {
    $descuento = 0;
}

echo "Descuento: $descuento%";

Acá combinamos una condición numérica con una condición booleana.

Ejemplo: permisos de usuario

$rol = 'admin';
$activo = true;

if ($activo && $rol === 'admin') {
    echo 'Acceso completo';
} elseif ($activo && $rol === 'editor') {
    echo 'Acceso de edición';
} else {
    echo 'Acceso limitado';
}

Errores comunes

  • confundir = con == o ===;
  • ordenar mal los elseif;
  • olvidar break dentro de un switch;
  • escribir condiciones demasiado largas;
  • abusar del operador ternario;
  • no tener en cuenta los valores truthy y falsy;
  • usar comparaciones flexibles cuando el tipo importa.

Ejercicios

  1. determinar si una persona es mayor de edad;
  2. indicar si un número es positivo, negativo o cero;
  3. clasificar una nota como desaprobada, aprobada o excelente;
  4. determinar si un número es par utilizando %;
  5. crear un sistema de descuento según importe;
  6. mostrar un mensaje diferente según el rol del usuario;
  7. resolver un estado de pedido con switch;
  8. reescribir ese mismo ejemplo utilizando match;
  9. mostrar “Activo” o “Inactivo” mediante un ternario;
  10. comparar el comportamiento de ?: y ??.

Qué aprendimos

  • qué es una condición;
  • cómo funciona if;
  • else y elseif;
  • condiciones compuestas;
  • operadores lógicos;
  • truthy y falsy;
  • switch y break;
  • match;
  • operador ternario;
  • diferencias entre ?: y ??.

¿Qué sigue?

En la próxima guía veremos bucles en PHP: for, while, do while y foreach, para repetir instrucciones de forma controlada.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Variables, tipos de datos y operadores en PHP: guía completa desde cero

En PHP, casi cualquier programa necesita guardar valores, operar con ellos y tomar decisiones a partir de esos datos. Para eso utilizamos variables, tipos de datos y operadores.

Esta guía parte desde cero y explica no solo la sintaxis, sino también qué representa cada concepto y por qué PHP se comporta de determinada manera.

¿Qué es una variable?

Una variable es un nombre que utilizamos para referirnos a un valor durante la ejecución de un programa.

Por ejemplo, en vez de escribir el nombre de una persona muchas veces podemos guardarlo una sola vez:

<?php

$nombre = 'Ana';

echo $nombre;

En este ejemplo:

  • $nombre es la variable;
  • = es el operador de asignación;
  • 'Ana' es el valor almacenado.

¿Por qué las variables comienzan con $?

En PHP todas las variables comienzan con el símbolo $.

$nombre
$edad
$precio
$activo

El símbolo forma parte de la sintaxis del lenguaje y permite identificar rápidamente una variable.

Asignar un valor

El operador = asigna el valor de la derecha a la variable de la izquierda.

$edad = 25;

No significa “es igual a” en el sentido matemático. Significa:

guardá el valor 25 dentro de la variable $edad.

Reglas para nombrar variables

Un nombre de variable:

  • debe comenzar con $;
  • después del símbolo debe comenzar con una letra o guion bajo;
  • puede contener números después del primer carácter;
  • no puede contener espacios;
  • distingue mayúsculas y minúsculas.

Ejemplos válidos:

$nombre
$nombreCompleto
$_usuario
$precio2

Ejemplos incorrectos:

$2precio
$nombre completo

PHP distingue mayúsculas y minúsculas en variables

Estas dos variables son diferentes:

$nombre = 'Ana';
$Nombre = 'Juan';

Por claridad, suele utilizarse camelCase:

$nombreCompleto
$precioFinal
$cantidadProductos

¿Qué es un tipo de dato?

El tipo indica qué clase de información representa un valor.

No es lo mismo guardar:

25
'25'
true
25.50

Aunque algunos valores parezcan similares, PHP puede tratarlos de manera diferente.

Tipos de datos básicos en PHP

String

Un string representa texto.

$nombre = 'Ana';
$ciudad = "Santa Rosa";

Integer

Un int representa números enteros.

$edad = 25;
$cantidad = 10;

Float

Un float representa números con decimales.

$precio = 1250.50;
$altura = 1.78;

Boolean

Un bool solo puede representar verdadero o falso.

$activo = true;
$bloqueado = false;

Los booleanos son fundamentales para condiciones que veremos en la próxima guía.

NULL

null representa ausencia de valor.

$telefono = null;

No significa cero ni una cadena vacía. Significa que la variable no posee actualmente un valor útil.

Array

Un array permite almacenar varios valores dentro de una misma variable.

$colores = ['rojo', 'verde', 'azul'];

Más adelante dedicaremos una guía completa a arrays.

PHP tiene tipado dinámico

En PHP no necesitamos declarar de antemano el tipo de una variable en los casos simples.

$dato = 10;
$dato = 'Ahora soy texto';

La misma variable primero contiene un entero y luego un string.

Esto se conoce como tipado dinámico: el tipo está asociado al valor en tiempo de ejecución.

¿Qué significa que PHP tenga coerción de tipos?

PHP puede convertir automáticamente ciertos valores cuando una operación lo requiere.

$numero = '10';
$resultado = $numero + 5;

echo $resultado;

En este caso PHP puede interpretar el string '10' como número y producir:

15

Esta flexibilidad puede ser útil, pero también producir errores difíciles de detectar. Por eso conviene entender siempre con qué tipos estamos trabajando.

Funciones incorporadas de PHP

PHP incluye una gran cantidad de funciones listas para usar. Estas funciones forman parte del propio lenguaje o de sus extensiones estándar y permiten realizar tareas frecuentes sin tener que programarlas desde cero.

Algunos ejemplos son:

  • var_dump(): permite inspeccionar una variable y ver su tipo y valor;
  • gettype(): devuelve el tipo de dato de una variable;
  • strlen(): obtiene la longitud de un texto;
  • count(): cuenta los elementos de un array;
  • date(): permite trabajar con fechas y formatos de fecha.

Estas son funciones propias de PHP y podemos utilizarlas directamente. Más adelante también veremos cómo crear nuestras propias funciones mediante la palabra clave function.

Podés consultar muchas más funciones y su documentación en el Manual oficial de PHP: referencia de funciones.

var_dump()

var_dump() muestra el tipo y el valor de una variable.

$edad = 25;

var_dump($edad);

El resultado será similar a:

int(25)

Con un string:

$nombre = 'Ana';
var_dump($nombre);

podríamos obtener:

string(3) "Ana"

gettype()

gettype() devuelve el nombre del tipo.

$precio = 1500.50;

echo gettype($precio);

Resultado:

double

En PHP, los valores que normalmente llamamos float pueden aparecer como double en ciertas funciones históricas.

Conversión explícita de tipos

También podemos indicar manualmente el tipo que queremos.

$texto = '25';
$edad = (int) $texto;

Otros casts habituales son:

(int)
(float)
(string)
(bool)
(array)

Strings: comillas simples y dobles

PHP permite escribir strings con comillas simples o dobles.

$nombre = 'Ana';

echo 'Hola $nombre';

Con comillas simples, PHP muestra literalmente:

Hola $nombre

En cambio:

echo "Hola $nombre";

produce:

Hola Ana

Interpolación de variables

La posibilidad de incluir una variable dentro de un string con comillas dobles se llama interpolación.

$nombre = 'Ana';
$edad = 25;

echo "$nombre tiene $edad años";

Concatenación con .

PHP utiliza el punto . para unir strings.

$nombre = 'Ana';

echo 'Hola ' . $nombre;

Resultado:

Hola Ana

Operadores aritméticos

Permiten realizar cálculos.

  • + suma;
  • - resta;
  • * multiplicación;
  • / división;
  • % resto;
  • ** potencia.
$a = 10;
$b = 3;

echo $a + $b;
echo $a - $b;
echo $a * $b;
echo $a / $b;
echo $a % $b;
echo $a ** $b;

El operador módulo %

El operador módulo devuelve el resto de una división.

10 % 3

devuelve:

1

Es muy útil, por ejemplo, para comprobar si un número es par:

$numero = 8;

$resto = $numero % 2;

Si el resto es cero, el número es divisible por 2.

Operadores de asignación

Podemos combinar una operación con la asignación.

$numero = 10;

$numero += 5;

Es equivalente a:

$numero = $numero + 5;

También existen:

+=
-=
*=
/=
%=

Incremento y decremento

$contador = 1;

$contador++;
$contador--;

++ aumenta en uno y -- disminuye en uno.

Operadores de comparación

Los operadores de comparación producen un valor booleano: true o false.

== igualdad flexible

var_dump(10 == '10');

Puede devolver true porque PHP convierte tipos para comparar.

=== igualdad estricta

var_dump(10 === '10');

Devuelve false porque uno es entero y el otro string.

Por qué suele preferirse ===

La comparación estricta exige que coincidan valor y tipo.

Esto evita resultados inesperados producidos por conversiones automáticas.

Otros operadores de comparación

  • !=: distinto;
  • !==: distinto en valor o tipo;
  • >: mayor;
  • <: menor;
  • >=: mayor o igual;
  • <=: menor o igual.

Operador spaceship <=>

El operador <=> compara dos valores y devuelve:

  • -1 si el primero es menor;
  • 0 si son equivalentes;
  • 1 si el primero es mayor.
$resultado = 10  20;

echo $resultado;

Resultado:

-1

Operadores lógicos

Permiten combinar condiciones booleanas.

  • &&: AND;
  • ||: OR;
  • !: NOT.
$mayorDeEdad = true;
$tienePermiso = false;

$puedeEntrar = $mayorDeEdad && $tienePermiso;

El resultado será false porque ambas condiciones deberían ser verdaderas.

Operador ??

El operador de coalescencia nula ?? permite usar un valor alternativo cuando una variable o clave no existe o contiene NULL.

$nombre = null;

echo $nombre ?? 'Sin nombre';

Resultado:

Sin nombre

Más adelante resulta especialmente útil al trabajar con formularios.

Precedencia de operadores

PHP no evalúa todas las operaciones simplemente de izquierda a derecha. Algunos operadores tienen mayor prioridad.

$resultado = 2 + 3 * 4;

El resultado es:

14

porque primero se realiza la multiplicación.

Si queremos cambiar el orden usamos paréntesis:

$resultado = (2 + 3) * 4;

Ahora el resultado es:

20

Ejemplo completo

<?php

$nombre = 'Ana';
$edad = 25;
$precio = 1250.50;
$cantidad = 3;
$activo = true;

$total = $precio * $cantidad;

echo "Cliente: $nombre" . '\n';
echo "Edad: $edad" . '\n';
echo "Total: $total";

En este ejemplo:

  1. creamos variables de distintos tipos;
  2. multiplicamos precio por cantidad;
  3. guardamos el resultado en otra variable;
  4. mostramos los valores mediante echo.

Buenas prácticas iniciales

  • usar nombres descriptivos;
  • evitar variables como $x salvo casos muy simples;
  • preferir comparaciones estrictas cuando corresponda;
  • usar paréntesis cuando una expresión pueda resultar ambigua;
  • comprobar tipos con var_dump() durante el aprendizaje y depuración.

Ejercicios

  1. crear variables para nombre, edad y ciudad y mostrarlas;
  2. calcular el área de un rectángulo;
  3. calcular el total de una compra usando precio y cantidad;
  4. convertir un string numérico a entero;
  5. comparar 10 == '10' y 10 === '10';
  6. comprobar el resto de dividir un número por 2;
  7. usar ?? para mostrar un valor alternativo;
  8. crear una expresión que combine operadores aritméticos y paréntesis.

Qué aprendimos

  • qué es una variable;
  • cómo funciona $;
  • qué son los tipos de datos;
  • strings, enteros, floats, booleanos, NULL y arrays;
  • tipado dinámico y coerción;
  • var_dump() y gettype();
  • concatenación e interpolación;
  • operadores aritméticos;
  • operadores de comparación;
  • operadores lógicos;
  • ??;
  • precedencia de operadores.

¿Qué sigue?

En la próxima guía veremos condicionales en PHP: if, elseif, else, switch y operadores ternarios.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

20 ejercicios intermedios y avanzados de SQL resueltos

Estos ejercicios suben el nivel porque obligan a decidir cómo relacionar tablas, qué agrupar y cuándo usar subconsultas o HAVING.

La dificultad ya no está tanto en recordar una sentencia, sino en descomponer un problema en etapas.

Antes de empezar: estrategia para consultas complejas

Una consulta avanzada suele construirse mejor en pasos:

  1. obtener primero las filas base;
  2. agregar los JOIN necesarios;
  3. verificar que no se multipliquen filas inesperadamente;
  4. agrupar;
  5. agregar funciones como SUM o COUNT;
  6. filtrar grupos con HAVING;
  7. recién después incorporar subconsultas o cálculos adicionales.

Si intentamos escribir todo de una vez, es mucho más difícil detectar dónde está el error.

1. Ventas en un rango de fechas

SELECT * FROM ventas
WHERE fecha BETWEEN '2026-01-01' AND '2026-12-31 23:59:59';

Explicación: BETWEEN permite definir un intervalo de fechas inclusivo. En campos DATETIME conviene pensar cuidadosamente el límite superior para no excluir registros del último día.

2. Total vendido por mes

SELECT YEAR(fecha) AS anio, MONTH(fecha) AS mes, SUM(total) AS total_vendido
FROM ventas
GROUP BY YEAR(fecha), MONTH(fecha)
ORDER BY anio, mes;

Explicación: Extraemos año y mes, agrupamos por ambos y sumamos el total. Agrupar solo por mes mezclaría, por ejemplo, enero de distintos años.

3. Cliente con más compras

SELECT c.id, c.nombre, c.apellido, COUNT(v.id) AS cantidad_compras
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellido
ORDER BY cantidad_compras DESC
LIMIT 1;

Explicación: COUNT mide cantidad de operaciones, no dinero gastado.

4. Clientes que gastaron más que el promedio

SELECT c.id,c.nombre,c.apellido,SUM(v.total) AS total_gastado
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id,c.nombre,c.apellido
HAVING SUM(v.total) > (
    SELECT AVG(total_cliente)
    FROM (SELECT SUM(total) AS total_cliente FROM ventas GROUP BY cliente_id) AS totales
);

Explicación: Primero calculamos el total por cliente y luego el promedio de esos totales. Hay dos niveles de agregación: uno dentro de la subconsulta y otro en la consulta exterior.

5. Producto más vendido

SELECT p.id,p.nombre,SUM(vd.cantidad) AS unidades
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id,p.nombre
ORDER BY unidades DESC
LIMIT 1;

Explicación: La métrica es cantidad de unidades, no facturación.

6. Producto con mayor facturación

SELECT p.nombre,SUM(vd.subtotal) AS facturacion
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id,p.nombre
ORDER BY facturacion DESC
LIMIT 1;

Explicación: Aquí la métrica cambia a dinero generado.

7. Categorías sin productos

SELECT c.id,c.nombre
FROM categorias AS c
LEFT JOIN productos AS p ON p.categoria_id = c.id
WHERE p.id IS NULL;

Explicación: LEFT JOIN permite detectar ausencia de relación.

8. Clientes que nunca compraron

SELECT c.id,c.nombre,c.apellido
FROM clientes AS c
WHERE NOT EXISTS (
    SELECT 1 FROM ventas AS v WHERE v.cliente_id = c.id
);

Explicación: NOT EXISTS expresa directamente que no debe existir ninguna venta relacionada. La subconsulta está correlacionada porque utiliza c.id de la fila externa.

9. Ticket promedio por cliente

SELECT c.id,c.nombre,c.apellido,AVG(v.total) AS ticket_promedio
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id,c.nombre,c.apellido;

Explicación: AVG calcula cuánto gasta en promedio el cliente por operación.

10. Categorías con más de 5 productos

SELECT c.id,c.nombre,COUNT(p.id) AS cantidad_productos
FROM categorias AS c
INNER JOIN productos AS p ON p.categoria_id = c.id
GROUP BY c.id,c.nombre
HAVING COUNT(p.id) > 5;

Explicación: HAVING es necesario porque filtramos sobre un COUNT agregado.

11. Total vendido por ciudad

SELECT c.ciudad,SUM(v.total) AS total_vendido
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.ciudad
ORDER BY total_vendido DESC;

Explicación: La ciudad proviene del cliente y el importe de ventas.

12. Última compra de cada cliente

SELECT c.id,c.nombre,c.apellido,MAX(v.fecha) AS ultima_compra
FROM clientes AS c
LEFT JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id,c.nombre,c.apellido;

Explicación: MAX(fecha) obtiene la fecha más reciente de cada grupo.

13. Ventas con más de 3 productos distintos

SELECT venta_id,COUNT(DISTINCT producto_id) AS productos_distintos
FROM ventas_detalles
GROUP BY venta_id
HAVING COUNT(DISTINCT producto_id) > 3;

Explicación: DISTINCT evita contar dos veces el mismo producto dentro de la misma venta.

14. Clientes que compraron en más de una categoría

SELECT c.id,c.nombre,c.apellido,COUNT(DISTINCT p.categoria_id) AS categorias_compradas
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
INNER JOIN ventas_detalles AS vd ON vd.venta_id = v.id
INNER JOIN productos AS p ON p.id = vd.producto_id
GROUP BY c.id,c.nombre,c.apellido
HAVING COUNT(DISTINCT p.categoria_id) > 1;

Explicación: Hay que recorrer cliente → venta → detalle → producto para conocer categorías. Este ejercicio muestra por qué es importante entender el camino entre entidades antes de escribir JOIN.

15. Porcentaje de facturación por categoría

SELECT c.nombre,SUM(vd.subtotal) AS total_categoria,
ROUND(SUM(vd.subtotal)*100/(SELECT SUM(subtotal) FROM ventas_detalles),2) AS porcentaje
FROM categorias AS c
INNER JOIN productos AS p ON p.categoria_id = c.id
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY c.id,c.nombre;

Explicación: Dividimos la facturación de cada categoría por la facturación total. La subconsulta calcula una sola vez el total conceptual contra el que se compara cada grupo.

16. Clientes con compras en distintos meses

SELECT c.id,c.nombre,c.apellido,
COUNT(DISTINCT DATE_FORMAT(v.fecha,'%Y-%m')) AS meses_con_compras
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id,c.nombre,c.apellido
HAVING meses_con_compras > 1;

Explicación: Convertimos cada fecha al período año-mes y contamos períodos distintos.

17. Stock menor que unidades vendidas

SELECT p.id,p.nombre,p.stock,SUM(vd.cantidad) AS unidades_vendidas
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id,p.nombre,p.stock
HAVING p.stock < SUM(vd.cantidad);

Explicación: Comparamos una columna del grupo con un valor agregado.

18. Resumen general por cliente

SELECT c.id,c.nombre,c.apellido,
COUNT(v.id) AS cantidad_ventas,
COALESCE(SUM(v.total),0) AS total_gastado,
COALESCE(AVG(v.total),0) AS ticket_promedio,
MAX(v.fecha) AS ultima_compra
FROM clientes AS c
LEFT JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id,c.nombre,c.apellido
ORDER BY total_gastado DESC;

Explicación: COALESCE reemplaza NULL por cero para clientes sin ventas. Como usamos LEFT JOIN, los clientes sin operaciones siguen apareciendo en el reporte.

19. Desafío: producto más vendido por categoría

-- Intentá resolverlo combinando GROUP BY,
-- SUM(cantidad) y una subconsulta o función de ventana.

Explicación: El desafío consiste en obtener el máximo dentro de cada categoría, no el máximo global.

20. Desafío final

-- Reporte por categoría:
-- cantidad de productos
-- unidades vendidas
-- facturación
-- precio promedio
-- producto más vendido

Explicación: Este ejercicio combina relaciones, agregaciones y ranking por grupo.

Cómo depurar una consulta avanzada

Cuando una consulta devuelve números incorrectos, no siempre hay un error de sintaxis. Un JOIN puede multiplicar filas y alterar SUM o COUNT.

Una técnica útil es ir agregando columnas de identificación temporalmente para observar qué filas está produciendo cada unión.

Consejo para resolver consultas avanzadas

  1. escribí primero la consulta más simple posible;
  2. probá cada JOIN por separado;
  3. agregá agrupaciones después;
  4. verificá la subconsulta de forma independiente;
  5. recién al final optimizá.

Primero buscá una consulta correcta y comprensible. Después utilizá índices y EXPLAIN para estudiar su rendimiento.

¿Qué sigue?

Con esta base ya podés comenzar a conectar MySQL con PHP u otro lenguaje.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← 20 ejercicios de SQL resueltos

20 ejercicios de SQL resueltos: SELECT, WHERE, JOIN, GROUP BY y subconsultas

Estos 20 ejercicios permiten practicar SQL de forma progresiva. Antes de mirar cada solución, intentá resolver el enunciado por tu cuenta.

Base de datos utilizada

Trabajamos con clientes, categorías, productos, ventas y ventas_detalles.

Las relaciones principales son:

  • productos.categoria_id → categorias.id;
  • ventas.cliente_id → clientes.id;
  • ventas_detalles.venta_id → ventas.id;
  • ventas_detalles.producto_id → productos.id.

Conviene tener este mapa mental antes de resolver los ejercicios con JOIN.

Cómo leer cada ejercicio

Antes de mirar la solución, intentá responder:

  1. ¿qué resultado final necesito?
  2. ¿qué tabla tiene esa información?
  3. ¿necesito filtrar filas?
  4. ¿necesito relacionar tablas?
  5. ¿necesito agrupar o calcular un resumen?

Resolver SQL es, en gran parte, aprender a traducir una pregunta del negocio a esas decisiones.

1. Mostrar todos los clientes

SELECT * FROM clientes;

Explicación: Usamos SELECT * porque queremos todas las columnas de todas las filas.

2. Mostrar nombre y apellido

SELECT nombre, apellido FROM clientes;

Explicación: Seleccionar solo las columnas necesarias hace la consulta más clara.

3. Clientes de Santa Rosa

SELECT * FROM clientes WHERE ciudad = 'Santa Rosa';

Explicación: WHERE filtra las filas por ciudad. La consulta primero parte de todos los clientes y después conserva solamente los que cumplen la condición.

4. Productos con precio mayor a 100000

SELECT nombre, precio FROM productos WHERE precio > 100000;

Explicación: El operador > compara valores numéricos.

5. Productos con stock bajo

SELECT nombre, stock FROM productos WHERE stock < 10;

Explicación: Este tipo de consulta puede utilizarse para alertas de reposición.

6. Ordenar productos por precio

SELECT nombre, precio FROM productos ORDER BY precio DESC;

Explicación: DESC ordena del valor más alto al más bajo.

7. Tres productos más caros

SELECT nombre, precio FROM productos ORDER BY precio DESC LIMIT 3;

Explicación: Primero ordenamos y luego LIMIT conserva solo las tres primeras filas.

8. Contar productos

SELECT COUNT(*) AS total_productos FROM productos;

Explicación: COUNT(*) devuelve cuántas filas tiene la tabla.

9. Precio promedio

SELECT AVG(precio) AS precio_promedio FROM productos;

Explicación: AVG calcula la media aritmética de la columna.

10. Productos por categoría

SELECT categoria_id, COUNT(*) AS cantidad
FROM productos
GROUP BY categoria_id;

Explicación: GROUP BY crea un grupo por categoría y COUNT cuenta sus productos. El resultado tendrá una fila por cada categoria_id, no una fila por cada producto.

11. Productos con categoría

SELECT p.nombre AS producto, c.nombre AS categoria
FROM productos AS p
INNER JOIN categorias AS c ON p.categoria_id = c.id;

Explicación: JOIN combina cada producto con la categoría relacionada. La condición p.categoria_id = c.id indica exactamente qué filas deben emparejarse.

12. Ventas con nombre del cliente

SELECT v.id, v.fecha, c.nombre, c.apellido, v.total
FROM ventas AS v
INNER JOIN clientes AS c ON v.cliente_id = c.id;

Explicación: Reemplazamos el identificador del cliente por información legible.

13. Total comprado por cliente

SELECT c.id, c.nombre, c.apellido, SUM(v.total) AS total_comprado
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellido;

Explicación: SUM acumula las ventas dentro de cada grupo de cliente. Primero JOIN conecta ventas con clientes; después GROUP BY reúne las ventas de la misma persona y finalmente SUM calcula el total.

14. Clientes que gastaron más de 100000

SELECT c.nombre, c.apellido, SUM(v.total) AS total_comprado
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellido
HAVING SUM(v.total) > 100000;

Explicación: HAVING filtra después de calcular la suma por cliente. No usamos WHERE para SUM(v.total) porque ese valor todavía no existe antes de formar los grupos.

15. Clientes sin ventas

SELECT c.id, c.nombre, c.apellido
FROM clientes AS c
LEFT JOIN ventas AS v ON v.cliente_id = c.id
WHERE v.id IS NULL;

Explicación: LEFT JOIN conserva clientes sin coincidencia; NULL identifica los que nunca compraron. Es un patrón muy usado para buscar “registros sin relación”.

16. Productos más vendidos

SELECT p.nombre, SUM(vd.cantidad) AS unidades_vendidas
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id, p.nombre
ORDER BY unidades_vendidas DESC;

Explicación: Sumamos cantidades por producto y ordenamos el ranking.

17. Producto con mayor facturación

SELECT p.nombre, SUM(vd.subtotal) AS facturacion
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id, p.nombre
ORDER BY facturacion DESC
LIMIT 1;

Explicación: El primer registro del ranking es el producto con mayor facturación.

18. Ventas por encima del promedio

SELECT id, cliente_id, total
FROM ventas
WHERE total > (SELECT AVG(total) FROM ventas);

Explicación: La subconsulta calcula el promedio y la consulta externa compara cada venta. Conviene ejecutar primero la subconsulta sola para comprobar qué valor produce.

19. Productos nunca vendidos

SELECT p.id, p.nombre
FROM productos AS p
LEFT JOIN ventas_detalles AS vd ON vd.producto_id = p.id
WHERE vd.id IS NULL;

Explicación: Buscamos productos que no tienen ninguna fila relacionada en detalles.

20. Cliente que más gastó

SELECT c.id, c.nombre, c.apellido, SUM(v.total) AS total_gastado
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellido
ORDER BY total_gastado DESC
LIMIT 1;

Explicación: Agrupamos, sumamos, ordenamos y conservamos el primer cliente.

Qué conceptos aparecen en estos 20 ejercicios

  • selección de columnas;
  • filtros con WHERE;
  • orden y límites;
  • funciones de agregación;
  • GROUP BY y HAVING;
  • INNER JOIN y LEFT JOIN;
  • subconsultas;
  • rankings simples.

Cómo practicar

  1. identificá qué tabla contiene el dato;
  2. decidí si necesitás WHERE;
  3. si hay varias tablas, buscá la relación;
  4. si necesitás resumir, pensá en GROUP BY;
  5. recién después escribí SQL.

¿Qué sigue?

20 ejercicios intermedios y avanzados.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Proyecto práctico SQL/MySQL

20 ejercicios intermedios y avanzados →

Proyecto práctico SQL/MySQL: sistema de ventas con tablas, relaciones, JOIN, reportes e índices

En este proyecto vamos a aplicar los principales conceptos de SQL y MySQL construyendo un pequeño sistema de ventas. La idea no es solamente copiar consultas, sino entender por qué existe cada tabla y cómo se relacionan.

El objetivo del proyecto

Queremos que al finalizar puedas mirar un requerimiento simple y convertirlo en tablas relacionadas, además de construir consultas de reporte sobre ellas.

Este proyecto integra lo visto anteriormente en lugar de presentar conceptos aislados.

¿Qué vamos a modelar?

El sistema manejará:

  • clientes;
  • categorías;
  • productos;
  • ventas;
  • detalle de cada venta.

¿Por qué varias tablas?

Separar entidades evita duplicación. Un cliente puede tener muchas ventas, una categoría muchos productos y una venta muchos detalles.

Las relaciones principales son:

  • cliente 1 → N ventas;
  • categoría 1 → N productos;
  • venta 1 → N detalles;
  • producto 1 → N detalles.

El detalle actúa como puente entre la venta y los productos comprados.

Crear la base

CREATE DATABASE tienda;
USE tienda;

Tabla clientes

Guarda la información propia del cliente. email es UNIQUE para evitar duplicados.

CREATE TABLE clientes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    apellido VARCHAR(100) NOT NULL,
    email VARCHAR(150) UNIQUE,
    ciudad VARCHAR(100),
    activo BOOLEAN DEFAULT TRUE,
    fecha_alta DATETIME DEFAULT CURRENT_TIMESTAMP
);

Tabla categorias

Las categorías se separan para no repetir el nombre en cada producto.

CREATE TABLE categorias (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL UNIQUE
);

Tabla productos

categoria_id conecta el producto con su categoría.

CREATE TABLE productos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(150) NOT NULL,
    precio DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    categoria_id INT NOT NULL,
    activo BOOLEAN DEFAULT TRUE,
    FOREIGN KEY (categoria_id) REFERENCES categorias(id)
);

Tabla ventas

Representa la cabecera de una operación. Guarda cliente, fecha y total.

CREATE TABLE ventas (
    id INT AUTO_INCREMENT PRIMARY KEY,
    cliente_id INT NOT NULL,
    fecha DATETIME DEFAULT CURRENT_TIMESTAMP,
    total DECIMAL(12,2) NOT NULL DEFAULT 0,
    FOREIGN KEY (cliente_id) REFERENCES clientes(id)
);

Cabecera y detalle

Este patrón aparece en facturas, pedidos, presupuestos y remitos. La cabecera representa la operación general; el detalle representa sus ítems.

Una venta puede tener diez productos sin repetir diez veces la fecha, el cliente y otros datos generales.

Tabla ventas_detalles

Cada fila representa un producto dentro de una venta. Guardamos el precio unitario de ese momento para conservar el valor histórico.

CREATE TABLE ventas_detalles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    venta_id INT NOT NULL,
    producto_id INT NOT NULL,
    cantidad INT NOT NULL,
    precio_unitario DECIMAL(10,2) NOT NULL,
    subtotal DECIMAL(12,2) NOT NULL,
    FOREIGN KEY (venta_id) REFERENCES ventas(id) ON DELETE CASCADE,
    FOREIGN KEY (producto_id) REFERENCES productos(id)
);

Registrar una venta con transacción

Venta, detalle y stock deben mantenerse consistentes. Por eso usamos una transacción.

Si se crea la venta pero falla el descuento de stock, no queremos dejar una operación parcialmente registrada.

START TRANSACTION;

INSERT INTO ventas (cliente_id, total)
VALUES (1, 0);

SET @venta_id = LAST_INSERT_ID();

INSERT INTO ventas_detalles (venta_id, producto_id, cantidad, precio_unitario, subtotal)
VALUES
(@venta_id, 3, 2, 95000, 190000),
(@venta_id, 4, 1, 45000, 45000);

UPDATE productos SET stock = stock - 2 WHERE id = 3;
UPDATE productos SET stock = stock - 1 WHERE id = 4;
UPDATE ventas SET total = 235000 WHERE id = @venta_id;

COMMIT;

¿Qué hace LAST_INSERT_ID()?

Devuelve el último identificador AUTO_INCREMENT generado en esa conexión. Lo usamos para saber qué venta acabamos de crear.

Es importante porque los detalles necesitan conocer el id de la cabecera recién insertada.

De guardar datos a obtener información

El diseño de tablas es solo la primera parte. El verdadero valor aparece cuando combinamos esas tablas para responder preguntas del negocio.

Consultar ventas con clientes

SELECT v.id, v.fecha, c.nombre, c.apellido, v.total
FROM ventas AS v
INNER JOIN clientes AS c
    ON v.cliente_id = c.id
ORDER BY v.fecha DESC;

JOIN permite reemplazar el cliente_id por información legible del cliente.

Detalle completo de una venta

SELECT v.id AS venta,
       c.nombre AS cliente,
       p.nombre AS producto,
       vd.cantidad,
       vd.precio_unitario,
       vd.subtotal
FROM ventas AS v
INNER JOIN clientes AS c ON v.cliente_id = c.id
INNER JOIN ventas_detalles AS vd ON vd.venta_id = v.id
INNER JOIN productos AS p ON vd.producto_id = p.id;

Total comprado por cliente

Agrupamos por cliente y sumamos sus ventas.

SELECT c.id, c.nombre, c.apellido,
       SUM(v.total) AS total_comprado
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellido
ORDER BY total_comprado DESC;

Productos más vendidos

SELECT p.nombre,
       SUM(vd.cantidad) AS unidades_vendidas
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id, p.nombre
ORDER BY unidades_vendidas DESC;

Clientes sin ventas

LEFT JOIN conserva todos los clientes y luego filtramos los que no tienen coincidencia.

SELECT c.id, c.nombre, c.apellido
FROM clientes AS c
LEFT JOIN ventas AS v ON v.cliente_id = c.id
WHERE v.id IS NULL;

Ventas superiores al promedio

SELECT id, cliente_id, total
FROM ventas
WHERE total > (
    SELECT AVG(total)
    FROM ventas
);

Validar stock antes de vender

El ejemplo descuenta stock directamente para concentrarse en SQL. En una aplicación real deberíamos verificar que exista cantidad suficiente y manejar concurrencia para evitar vender más unidades de las disponibles.

Índices

Estas columnas participan frecuentemente en JOIN y filtros.

No agregamos índices “porque sí”: los elegimos en función de las consultas reales del sistema.

CREATE INDEX idx_productos_categoria ON productos(categoria_id);
CREATE INDEX idx_ventas_cliente_fecha ON ventas(cliente_id, fecha);
CREATE INDEX idx_detalles_producto ON ventas_detalles(producto_id);

Qué conceptos aplicamos

  • normalización;
  • claves primarias y foráneas;
  • INSERT y UPDATE;
  • transacciones;
  • JOIN;
  • GROUP BY;
  • subconsultas;
  • índices.

Ejercicios de ampliación

  1. agregar formas de pago;
  2. crear reporte mensual;
  3. obtener cliente que más gastó;
  4. detectar productos nunca vendidos;
  5. registrar devoluciones.

¿Qué sigue?

20 ejercicios de SQL resueltos.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Normalización de bases de datos

20 ejercicios de SQL resueltos →

Normalización de bases de datos: 1FN, 2FN y 3FN con ejemplos

La normalización es un proceso de diseño que busca organizar los datos para reducir redundancias e inconsistencias.

Normalización no es solo teoría

Las formas normales intentan evitar problemas reales de mantenimiento. Cuando una tabla mezcla demasiados conceptos, los errores aparecen al insertar, actualizar o eliminar.

La normalización obliga a preguntarnos de qué depende cada dato.

¿Qué problema resuelve?

Cuando la misma información se repite en muchas filas, actualizarla puede volverse peligroso. También puede ser difícil insertar o eliminar datos sin efectos no deseados.

Anomalía de actualización

El mismo dato aparece varias veces y debe modificarse en todas.

Ejemplo: si el teléfono de un profesor está repetido en 50 filas de alumnos y cambia, debemos actualizar las 50. Si una queda distinta, la base ya es inconsistente.

Anomalía de inserción

No podemos guardar una información sin crear otra que no debería ser necesaria.

Por ejemplo, no deberíamos necesitar inventar un alumno para poder registrar un nuevo curso.

Anomalía de eliminación

Al eliminar una fila perdemos información adicional.

Si el único registro del curso “SQL” estuviera mezclado con un alumno y eliminamos ese alumno, podríamos perder también la información del curso.

Primera Forma Normal: 1FN

Una tabla cumple 1FN cuando cada celda contiene un valor atómico y no listas de valores.

“Atómico” significa que, para el modelo que estamos diseñando, el valor se trata como una unidad. Guardar varios teléfonos separados por comas rompe esa idea porque luego es difícil buscar, validar o relacionar cada teléfono.

id | nombre | telefonos
1  | Ana    | 123456, 654321

Ese campo mezcla dos teléfonos. Es mejor crear otra tabla.

Separación correcta

alumnos
-------
id
nombre

telefonos_alumnos
-----------------
id
alumno_id
telefono

Dependencia funcional

Decimos que un dato depende de otro cuando conocer la clave permite determinar ese valor.

Por ejemplo, si alumno_id = 10 identifica a Ana, entonces el nombre depende de alumno_id.

Segunda Forma Normal: 2FN

Además de cumplir 1FN, todos los atributos no clave deben depender de toda la clave primaria.

El problema aparece especialmente con claves compuestas.

Si la clave está formada por dos columnas, un atributo no clave no debería depender solo de una mitad de esa clave.

alumno_id | curso_id | alumno_nombre | curso_nombre | nota

Si la clave es (alumno_id, curso_id), alumno_nombre depende solo de alumno_id y curso_nombre solo de curso_id.

Por qué separar en tres tablas

alumnos guarda datos propios del alumno, cursos datos propios del curso e inscripciones datos de la relación, como nota o fecha de inscripción.

Cada dato queda entonces en el lugar del que realmente depende.

Solución 2FN

Separar alumnos, cursos e inscripciones.

Tercera Forma Normal: 3FN

Además de cumplir 2FN, un atributo no clave no debería depender de otro atributo no clave.

alumno_id | nombre | ciudad_id | ciudad_nombre | provincia

El nombre de ciudad y provincia dependen de ciudad_id, no directamente del alumno.

Dependencia transitiva

Ocurre cuando A determina B y B determina C. Entonces C depende indirectamente de A.

En términos prácticos: si un dato describe a otra entidad intermedia y no a la fila principal, probablemente merece su propia tabla.

Muchos a muchos

Se resuelve habitualmente con una tabla intermedia.

CREATE TABLE alumnos_cursos (
    alumno_id INT NOT NULL,
    curso_id INT NOT NULL,
    fecha_inscripcion DATE,
    PRIMARY KEY (alumno_id, curso_id),
    FOREIGN KEY (alumno_id) REFERENCES alumnos(id),
    FOREIGN KEY (curso_id) REFERENCES cursos(id)
);

¿Hasta dónde normalizar?

En aplicaciones transaccionales, llegar a 3FN suele ser una base razonable de diseño. Existen formas normales más avanzadas, pero no siempre hacen falta para sistemas comunes.

Normalizar no significa dividir todo

El objetivo es mejorar consistencia y diseño, no crear tablas sin necesidad.

Desnormalización

A veces se duplica información intencionalmente para rendimiento o reportes. Debe ser una decisión consciente y medida.

Ejemplo histórico

En una venta conviene guardar precio_unitario aunque el producto ya tenga un precio actual, porque necesitamos conservar el valor histórico cobrado.

Resumen

  • 1FN: valores atómicos.
  • 2FN: sin dependencias parciales.
  • 3FN: sin dependencias transitivas.

Errores comunes

  • pensar que normalizar es solo crear más tablas;
  • guardar listas separadas por comas;
  • duplicar datos sin motivo;
  • desnormalizar antes de medir rendimiento.

Ejercicios

  1. normalizar teléfonos múltiples;
  2. separar alumnos y cursos;
  3. identificar dependencia parcial;
  4. identificar dependencia transitiva;
  5. diseñar clientes, pedidos y productos.

¿Qué sigue?

Proyecto práctico completo.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Triggers en MySQL

Proyecto práctico SQL/MySQL →

Triggers en MySQL: BEFORE, AFTER, INSERT, UPDATE y DELETE con ejemplos

Un trigger es una acción automática que MySQL ejecuta cuando ocurre un evento sobre una tabla.

¿Qué problema resuelve?

Permite ejecutar lógica automáticamente ante inserciones, modificaciones o eliminaciones, incluso si la operación proviene de distintas aplicaciones.

¿Cuándo conviene un trigger?

Un trigger es útil cuando una regla debe ejecutarse automáticamente sin importar qué aplicación haya realizado el cambio.

Ejemplos típicos:

  • registrar auditorías;
  • guardar históricos;
  • validar ciertas condiciones;
  • mantener datos derivados muy controlados.

La contrapartida es que la lógica queda “oculta” detrás de la operación principal, por eso conviene usarlos con criterio.

Eventos disponibles

  • INSERT
  • UPDATE
  • DELETE

BEFORE y AFTER

  • BEFORE: antes del cambio.
  • AFTER: después de que el cambio fue realizado.

Qué valores existen en cada evento

No siempre están disponibles OLD y NEW:

  • INSERT: existe NEW;
  • UPDATE: existen OLD y NEW;
  • DELETE: existe OLD.

NEW

NEW representa los valores nuevos de la fila.

En un BEFORE INSERT o BEFORE UPDATE incluso podemos modificar ciertos valores de NEW antes de que se guarden.

OLD

OLD representa los valores anteriores.

Es especialmente útil para auditorías porque permite comparar qué tenía una columna antes y qué tendrá después.

BEFORE INSERT

DELIMITER //
CREATE TRIGGER trg_productos_stock_insert
BEFORE INSERT ON productos
FOR EACH ROW
BEGIN
    IF NEW.stock < 0 THEN
        SET NEW.stock = 0;
    END IF;
END //
DELIMITER ;

BEFORE INSERT: corregir o rechazar

En el ejemplo anterior elegimos corregir un stock negativo y transformarlo en cero. En otros casos puede ser mejor rechazar la operación con SIGNAL.

Corregir silenciosamente y bloquear con error son decisiones distintas de negocio.

BEFORE UPDATE

Sirve para validar o transformar los nuevos valores antes de guardarlos.

Comparar OLD y NEW

IF OLD.precio <> NEW.precio THEN
    INSERT INTO precios_auditoria(producto_id, precio_anterior, precio_nuevo)
    VALUES(NEW.id, OLD.precio, NEW.precio);
END IF;

Auditoría con OLD y NEW

Si queremos registrar cambios de precio, podemos guardar ambos valores en una tabla de auditoría. Así queda trazabilidad de qué cambió.

AFTER INSERT

Es útil para auditorías o acciones posteriores al alta.

En AFTER el cambio principal ya ocurrió, por lo que se usa normalmente para registrar consecuencias, no para modificar el valor que se está insertando.

BEFORE DELETE

Podemos guardar un registro histórico antes de eliminar la fila.

SIGNAL

Permite generar un error y bloquear una operación inválida.

IF NEW.stock < 0 THEN
    SIGNAL SQLSTATE '45000'
    SET MESSAGE_TEXT = 'El stock no puede ser negativo';
END IF;

FOR EACH ROW

Un trigger se ejecuta una vez por cada fila afectada. Si un UPDATE modifica 10.000 filas, el trigger puede ejecutarse 10.000 veces.

Ver triggers

SHOW TRIGGERS;

Eliminar trigger

DROP TRIGGER IF EXISTS trg_productos_stock_insert;

Triggers dentro de transacciones

Las operaciones que ejecuta un trigger forman parte del mismo contexto transaccional de la sentencia que lo disparó. Si la operación principal se revierte, sus efectos asociados también pueden revertirse según el motor y las tablas involucradas.

Trigger o restricción

Si una regla puede resolverse con NOT NULL, UNIQUE, CHECK o FOREIGN KEY, normalmente esas opciones son más simples y explícitas.

El problema de la lógica oculta

Una sentencia UPDATE puede aparentar modificar una tabla, pero un trigger podría generar efectos adicionales. Por eso deben documentarse bien.

Errores comunes

  • usar triggers para todo;
  • crear efectos secundarios difíciles de detectar;
  • ignorar el costo por fila;
  • duplicar reglas que ya existen como restricciones.

Ejercicios

  1. impedir precios negativos;
  2. registrar cambios de precio;
  3. guardar filas antes de DELETE;
  4. crear historial de stock.

¿Qué sigue?

Normalización de bases de datos.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Funciones almacenadas

Normalización de bases de datos →