Translate

viernes, 5 de junio de 2026

¿Por qué Java Streams no pueden ser LINQ?


Imaginá esto:

repoPersona

    .filter(p -> p.getEdad() > 50)

    .toList();



Y que mágicamente se transforme en:

SELECT * FROM persona WHERE edad > 50;


Sería hermoso.

Pero… ¿no son lazy los streams?

Sí, los streams en Java son lazy:


users.stream()

     .filter(u -> u.getAge() > 50)

     .toList();


No se ejecuta hasta el final


Peeeero...

Lazy no es lo mismo que interpretable


En Java, esto:

u -> u.getAge() > 50


Se compila a bytecode

No queda como estructura accesible


Es decir:

  • No podés analizarla
  • No podés transformarla
  • No podés convertirla en SQL


 ¿Por qué en C# sí se puede?


En .NET LINQ pasa algo distinto:

repo.Where(x => x.Age > 50)


Eso no es una función normal

Es: Expression<Func<T, bool>>


El código es datos (un AST)


Por esta razón necesitamos de cosas como Specification de jpa para tipar nuestras querys :(


Java Streams no pueden ser LINQ porque:  Java no puede “leer” lambdas como datos


Y eso obliga a usar soluciones como Specifications.


Specifications de JPA: Queries dinámicas y tipadas en Java


Cuando trabajamos con bases de datos en Java usando Spring Data JPA, tarde o temprano aparece un problema:

¿Cómo construyo queries dinámicas sin escribir SQL a mano?

Ahí es donde entra el patrón Specification.

Una Specification<T> es básicamente un predicado tipado que se traduce a SQL.


Permite expresar condiciones de forma declarativa:

Specification<User> isAdult = (root, query, cb) ->

    cb.greaterThanOrEqualTo(root.get("age"), 18);


Esto termina siendo:

WHERE age >= 18


Composición: el verdadero poder. Lo más interesante es que las specifications se pueden combinar:


Specification<User> isAdult = (root, query, cb) ->

    cb.greaterThanOrEqualTo(root.get("age"), 18);


Specification<User> nameStartsWithA = (root, query, cb) ->

    cb.like(root.get("name"), "A%");


Specification<User> combined =

    isAdult.and(nameStartsWithA);


WHERE age >= 18 AND name LIKE 'A%'


Se puede usar en el dao o repositorio así:


List<User> result = userRepository.findAll(combined);


Simple, expresivo y reutilizable.


Ventajas:

  • Queries dinámicas
  • Composición (`and`, `or`)
  • Tipado fuerte
  • Evita strings SQL


Desventajas:

  • Verboso (Criteria API no es linda)
  • Poco intuitivo al principio
  • Debug complicado


¿Qué está pasando por debajo?


Todo esto se apoya en JPA Criteria API

  • No estás ejecutando código
  • Estás construyendo un árbol de expresión (AST)


Por eso puede traducirse a SQL.

Specification es una forma poderosa de:

construir queries dinámicas, reutilizables y tipadas en Java


Pero… deja una pregunta interesante. ¿No podríamos hacer esto mismo con Streams?

Lo veremos en próximos posts ...



miércoles, 3 de junio de 2026

Jakarta Persistence 4.0: El mayor salto de JPA en años



Durante mucho tiempo, cuando hablábamos de persistencia en Java, hablábamos de JPA. Con la migración de Java EE a Jakarta EE, JPA pasó a llamarse Jakarta Persistence, pero durante varias versiones los cambios fueron relativamente modestos.

Con Jakarta Persistence 4.0 eso cambia.

Esta nueva versión incorpora características largamente esperadas, mejora la seguridad de tipos y prepara el terreno para trabajar junto a Jakarta Data. Veamos qué trae de nuevo.


¿Qué es Jakarta Persistence?

Jakarta Persistence es la especificación estándar para el mapeo objeto-relacional (ORM) en Java.

Permite representar tablas como objetos Java y realizar operaciones CRUD sin escribir SQL para cada interacción.


@Entity

public class Cliente {


    @Id

    private Long id;

    private String nombre;

}


Implementaciones populares incluyen:

  • Hibernate ORM
  • EclipseLink


Novedades más importantes de Jakarta Persistence 4.0:

1. EntityAgent: trabajar sin Persistence Context

Una de las novedades más llamativas es la incorporación de EntityAgent.

Hasta ahora, casi todas las operaciones pasaban por un EntityManager y un Persistence Context.


entityManager.persist(cliente);


Con EntityAgent se pueden realizar operaciones sobre entidades desacopladas del contexto de persistencia, simplificando ciertos escenarios y reduciendo overhead. 


Conceptualmente:

entityAgent.insert(cliente);


Esto acerca la API a modelos más ligeros y modernos.


2. Carga masiva por ID

Un problema clásico:


for(Long id : ids) {

    entityManager.find(Cliente.class, id);

}


Esto puede generar el famoso problema N+1.

Jakarta Persistence 4.0 incorpora soporte para obtener múltiples entidades por identificador en una sola operación. 


La ventaja:

  • Menos viajes a la base de datos.
  • Mejor rendimiento.
  • Código más simple.


3. Consultas más seguras con Static Query

Una crítica frecuente a JPA era que las consultas JPQL eran simples Strings.


@Query("select c from Cliente c")


Si el nombre de una propiedad cambiaba, el error aparecía recién en tiempo de ejecución.

Persistence 4.0 incorpora una API de Static Queries que permite mayor verificación en compilación. 


Beneficios:

  • Más seguridad de tipos.
  • Menos errores en producción.
  • Mejor soporte para herramientas y refactorización.


4. Result Set Mapping programático

Hasta ahora era habitual definir mapeos complejos mediante anotaciones.


@SqlResultSetMapping(...)


La nueva versión agrega una API programática para definir estos mappings. 


Ventajas:

  • Más flexible.
  • Más reutilizable.
  • Menos anotaciones gigantes.


5. Nuevas capacidades para Entity Graphs

Los Entity Graphs fueron introducidos para controlar qué relaciones se cargan.


@EntityGraph(attributePaths = {

    "pedidos",

    "direccion"

})


Persistence 4.0 mejora significativamente esta funcionalidad:

Nuevas anotaciones.

Mejor integración con operaciones existentes.

Uso de Entity Graphs en refresh(). 


6. Soporte para entidades Read-Only

En muchos escenarios solo queremos leer datos.

Antes:


Cliente cliente =

    entityManager.find(Cliente.class, id);


La entidad quedaba administrada por el contexto.

Persistence 4.0 agrega soporte explícito para carga en modo solo lectura. 


Beneficios:

  • Menor consumo de memoria.
  • Menor costo de dirty checking.
  • Mejor rendimiento.


7. @PreMerge

Hasta ahora existían callbacks como:


@PrePersist

@PostPersist

@PreUpdate

@PostUpdate



Ahora aparece:

@PreMerge

public void beforeMerge() {

    System.out.println("Fusionando entidad");

}


Esto permite interceptar el proceso de merge antes de que ocurra. 


8. Excluir campos del Optimistic Locking

Hasta ahora cualquier modificación podía afectar el control de versiones.

Persistence 4.0 incorpora una anotación para excluir ciertos atributos del mecanismo de optimistic locking. 


Ideal para:

  • Campos calculados.
  • Contadores.
  • Metadatos auxiliares.


9. select new implícito

Actualmente:


select new ClienteDTO(

    c.id,

    c.nombre

)

from Cliente c


Persistence 4.0 simplifica este escenario permitiendo inferir automáticamente el DTO cuando se especifica el tipo de resultado. 


10. Preparándose para Jakarta Data

Quizás el cambio más importante no sea una característica puntual.

Muchas de las nuevas APIs fueron diseñadas para trabajar mejor con Jakarta Data

La idea es ofrecer una experiencia similar a:

  • Spring Data
  • Micronaut Data
  • Quarkus Panache


pero de forma estandarizada dentro del ecosistema Jakarta EE. 


¿Vale la pena actualizar?

  • Si estás usando:
  • Hibernate moderno
  • Jakarta EE 11 o futuro Jakarta EE 12
  • Nuevos proyectos


la respuesta es sí.


Las mejoras apuntan a problemas reales que los desarrolladores vienen sufriendo desde hace años:

  • Más type safety
  • Menos Strings mágicos
  • Mejor rendimiento
  • Mejor integración con Jakarta Data
  • APIs más modernas


Jakarta Persistence 4.0 representa probablemente la evolución más importante de JPA desde la llegada de los Entity Graphs y los Stored Procedures.

La incorporación de EntityAgent, Static Queries, Result Set Mapping programático, mejoras en Entity Graphs y nuevas capacidades de carga hacen que la especificación se vea mucho más moderna y preparada para competir con las soluciones de persistencia actuales. 

Para quienes pensaban que JPA había quedado estancado, Jakarta Persistence 4.0 demuestra exactamente lo contrario.


Dejo link: 

https://jakarta.ee/specifications/persistence/4.0/



¿Qué es Native AOT en .NET?


Durante muchos años, las aplicaciones .NET funcionaron utilizando un modelo basado en JIT (Just-In-Time Compilation).

Esto significa que el código C# no se ejecuta directamente como código máquina, sino que primero se compila a un lenguaje intermedio llamado IL (Intermediate Language), y luego el runtime de .NET lo convierte a código nativo en tiempo de ejecución.

Este modelo funciona muy bien… pero tiene costos:

  • startup más lento
  • mayor consumo de memoria
  • necesidad de instalar el runtime
  • contenedores más pesados


Con la llegada del cloud, Kubernetes y serverless, estos problemas comenzaron a ser cada vez más importantes.

Para resolverlos, Microsoft introdujo Native AOT.

Native AOT (Ahead-Of-Time Compilation) es una tecnología de .NET que permite compilar una aplicación directamente a código máquina nativo.


En lugar de:

C# -> IL -> JIT -> Código Máquina


tenemos:

C# -> IL -> Código Máquina Nativo


El resultado es un ejecutable standalone que no necesita el runtime de .NET instalado en la máquina destino.


Conceptualmente, es muy similar a:

  • GraalVM
  • Go
  • Rust
  • Zig


Crearemos una aplicación Native AOT

Paso 1: Crear el proyecto

dotnet new console -n HolaNativeAot

cd HolaNativeAot


Paso 2: Modificar el .csproj

Agregamos:

<PropertyGroup>

    <PublishAot>true</PublishAot>

</PropertyGroup>


El archivo queda algo así:

<Project Sdk="Microsoft.NET.Sdk">


  <PropertyGroup>

    <OutputType>Exe</OutputType>

    <TargetFramework>net9.0</TargetFramework>

    <PublishAot>true</PublishAot>

  </PropertyGroup>


</Project>


Paso 3: Compilar


en Windows

dotnet publish -c Release -r win-x64


en Linux

dotnet publish -c Release -r linux-x64


Se genera algo similar a:

bin/Release/net9.0/linux-x64/publish/


Y dentro encontraremos un ejecutable nativo:

HolaNativeAot


o en Windows:

HolaNativeAot.exe


¿Qué ventajas tiene?

Al no existir JIT:

  • a aplicación arranca casi instantáneamente
  • ideal para serverless
  • ideal para microservicios


Menor consumo de memoria, porque:

  • no hay compilación runtime
  • menos metadata
  • runtime reducido


No hace falta instalar .NET.

El ejecutable ya contiene todo lo necesario.

Native AOT funciona muy bien junto con Docker.

Podemos generar imágenes muy livianas y rápidas de iniciar.


No todo es perfecto.

Native AOT necesita saber en tiempo de compilación qué código será utilizado.

Pero reflection rompe esa idea.


Ejemplo:

var type = Type.GetType("MiClase");

Activator.CreateInstance(type);


El compilador no puede garantizar que ese tipo será usado.

Entonces puede eliminarlo durante el trimming.

Native AOT utiliza una técnica llamada: IL Trimming

El compilador elimina código que considera innecesario.

Muy parecido al concepto de: Closed World Assumption utilizado por GraalVM.


Las librerías más dinámicas suelen tener problemas.


Por ejemplo:

  • reflection pesada
  • generación dinámica de código
  • plugins
  • proxies dinámicos
  • serializers mágicos


Native AOT funciona especialmente bien con:

  • Minimal APIs
  • gRPC
  • Workers
  • herramientas CLI
  • microservicios simples


ASP.NET Core tiene soporte parcial para Native AOT.


Se complica si usamos :

  • MVC tradicional
  • Razor Pages dinámicas
  • plugins
  • reflection intensiva


Existe también: ReadyToRun (R2R)


Se activa con:

<PublishReadyToRun>true</PublishReadyToRun>


No elimina completamente el JIT, pero mejora considerablemente el startup.

Sería una especie de “semi-AOT”.


Native AOT aparece en un contexto muy concreto:

  • Kubernetes
  • containers efímeros
  • serverless
  • autoscaling
  • cold starts


En estos escenarios:

  • arrancar rápido importa
  • consumir menos RAM importa
  • tener imágenes pequeñas importa


El modelo JIT sigue siendo excelente para:

  • aplicaciones grandes
  • desktop
  • sistemas dinámicos
  • frameworks complejos
  • escenarios con mucha reflection


Native AOT está pensado principalmente para:

  • cloud-native
  • microservicios
  • serverless
  • APIs livianas
  • herramientas CLI


Native AOT representa uno de los cambios más importantes en la evolución moderna de .NET.


donde el resultado final es:

  • un único binario
  • startup instantáneo
  • menor consumo de memoria
  • deploy extremadamente simple


Sin embargo, también obliga a abandonar parte de la flexibilidad dinámica que históricamente caracterizó a plataformas como .NET y Java.


Y justamente ahí es donde comienza uno de los debates más interesantes del desarrollo moderno: ¿El futuro es AOT o JIT?




lunes, 1 de junio de 2026

Problema: Java consumiendo mucho CPU (sin GUI ni puertos)


Si no podés usar VisualVM ni JMX remoto, la estrategia correcta es:

CLI + correlación de datos


1. Encontrar el proceso

jps -l

o directamente:

ps aux | grep java

guardate el PID


2. Confirmar que el problema es CPU

top -p <pid>

o mejor:

top -H -p <pid>

Esto muestra threads individuales


Acá está la clave:

  • Vas a ver un thread con CPU alto (ej: 200%)
  • Anotá el TID (thread id)


3. Convertir TID a HEX (clave para Java)


Java muestra threads en hexadecimal, Linux en decimal.

printf "%x\n" <tid>


Ejemplo:

12345 → 3039


4.Sacar thread dump

jstack <pid> > dump.txt


5. Buscar el thread problemático


Dentro del dump:

grep -i 3039 dump.txt


Ese es el thread que está consumiendo CPU

¿Qué vas a encontrar? Generalmente algo así:

"id=123 nid=0x3039 runnable"


Y abajo el stack:

at com.miapp.CalculoPesado.procesar(CalculoPesado.java:42)

at com.miapp.Service.loop(Service.java:88)


¡encontraste el problema!


¿Dónde entra jstat en todo esto?

Acá es donde sumás contexto.


Ejecutá:

jstat -gcutil <pid> 1000


 Qué mirar para CPU alto


Caso 1: GC excesivo

  • YGC sube MUY rápido
  • GCT crece constantemente


CPU alto por Garbage Collector


Caso 2: Full GC frecuentes

  • FGC aumenta
  • FGCT alto


pausas pesadas → CPU + latencia


Caso 3: GC normal

  • GC estable
  • memoria OK


Entonces el problema es código, no GC

domingo, 31 de mayo de 2026

jstat en Java: cómo entender el Garbage Collector desde la consola


Cuando hablamos de monitoreo en Java, muchos van directo a VisualVM.

Pero en entornos reales (servidores, contenedores, CI), necesitás herramientas sin GUI. Ahí aparece jstat.

Una herramienta liviana, incluida en la JVM, que te permite ver qué está pasando con la memoria y el Garbage Collector en tiempo real.

jstat (Java Virtual Machine Statistics Monitoring Tool) es una utilidad de consola que permite consultar métricas internas de la JVM.

  • Uso de memoria
  • Actividad del GC
  • Cantidad de recolecciones
  • Tiempos acumulados


Primero necesitás el PID del proceso:


jps -l


Después ejecutás:


jstat -gc <pid> 1000


Esto imprime estadísticas cada 1 segundo.


 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU     YGC   YGCT   FGC    FGCT     GCT

1024.0 1024.0  0.0   512.0   8192.0   4096.0   16384.0   8000.0   5120.0 4800.0 1024.0  900.0     10    0.25    2     0.50     0.75


Ahora lo importante: entender esto 👇

Generación joven (Young Gen)


S0C / S1C → tamaño de Survivor 0 y 1

S0U / S1U → uso actual

EC → tamaño de Eden

EU → uso de Eden

Acá viven los objetos nuevos.


Generación vieja (Old Gen)

OC → capacidad

OU → uso


👉 Si esto crece mucho y no baja → posible problema de memoria.


Metaspace

MC / MU → capacidad y uso

CCSC / CCSU → Compressed Class Space


👉 Relacionado con clases cargadas.


YGC → cantidad de GC jóvenes

YGCT → tiempo total en GC joven

FGC → cantidad de Full GC

FGCT → tiempo total en Full GC

GCT → tiempo total de GC


Veamos un caso saludable

  • Eden sube y baja constantemente
  • YGC aumenta progresivamente
  • Old Gen estable


👉 Todo OK.


Posible memory leak

  • OU (Old Gen) sube y nunca baja
  • Full GC frecuentes


👉 Algo está reteniendo objetos.


🟠 Problema de GC

  • YGC muy alto en poco tiempo
  • GCT creciendo rápido


👉 La app está gastando demasiado tiempo en GC.


⚙️ Otros modos útiles

-gcutil (más simple y legible)


jstat -gcutil <pid> 1000


Salida en porcentajes:

 S0     S1     E      O      M     CCS    YGC   YGCT   FGC   FGCT     GCT

 0.00  50.00  60.00  48.00  93.75 87.89   10    0.25    2     0.50     0.75


👉 Ideal para monitoreo rápido.


Ejecutar N veces

jstat -gc <pid> 1000 10


👉 Ejecuta 10 veces y termina.


jstat es una herramienta simple pero extremadamente poderosa.


✔ Ideal para servidores sin GUI

✔ Perfecta para debugging en vivo

✔ Fundamental para entender el GC

jueves, 28 de mayo de 2026

Implementando reduce y construyendo map en Scala


Una de las ideas más interesantes de la programación funcional es que muchas operaciones sobre colecciones pueden construirse a partir de una sola función general: fold (o un reduce más flexible).


Por ejemplo, este reduce recursivo:


def reduce[A, B](list: List[A], initial: B)(f: (B, A) => B): B =

  if (list.isEmpty) initial

  else reduce(list.tail, f(initial, list.head))(f)


Recorre la lista acumulando un resultado.

¿Qué hace exactamente?

La función recibe:

  • una lista
  • un valor inicial
  • una función acumuladora


La función f recibe:

(B, A) => B


Es decir:

  • el acumulador actual (B)
  • el elemento actual (A)
  • y devuelve un nuevo acumulador (B)


Ejemplo: sumar números

val numbers = List(1, 2, 3, 4)

val result =  reduce(numbers, 0)((acc, n) => acc + n)

println(result)


Salida: 10


El proceso sería algo así:

(((0 + 1) + 2) + 3) + 4


Ejemplo: concatenar strings


val words = List("Hola", "Scala", "!")

val result =  reduce(words, "")((acc, word) => acc + " " + word)

println(result)


Salida: Hola Scala !


Acá viene la parte interesante.

map transforma cada elemento de una lista:


List(1, 2, 3).map(_ * 2)


Resultado: List(2, 4, 6)


Pero podemos implementarlo usando únicamente nuestro reduce.


def map[A, B](list: List[A])(f: A => B): List[B] =

  reduce(list, List.empty[B]) { (acc, elem) =>

    acc :+ f(elem)

  }


val numbers = List(1, 2, 3)

val doubled =  map(numbers)(_ * 2)


println(doubled)


Salida: List(2, 4, 6)


En cada iteración:

acc :+ f(elem)


1. Se transforma el elemento con f

2. Se agrega al acumulador


Por ejemplo:

List()

List(2)

List(2, 4)

List(2, 4, 6)


Lo interesante es que:

  • sum
  • filter
  • map
  • count
  • flatMap

y muchas otras operaciones funcionales pueden construirse a partir de un único patrón: recorrer una estructura acumulando un resultado. Ese patrón es justamente fold.



martes, 26 de mayo de 2026

Cómo detectar cuando MySQL elige un mal plan de ejecución?


El optimizador de MySQL es muy bueno… pero no es perfecto.


A veces elige un plan que:

❌ No usa índices

❌ Hace scans innecesarios

❌ Escala mal con más datos


El problema es que la query funciona… pero lento.


Primero: qué es un “mal plan”. Un mal plan no significa que falle.

Significa que:

  • Hace más trabajo del necesario
  • No usa la mejor estrategia disponible


Señales claras de un mal plan

❌ 1. Gran diferencia entre estimado y real

rows=1000 (estimated)

actual rows=10


Esto es una alarma 🚨

MySQL toma decisiones basadas en estimaciones.


Si están mal:

  • Puede elegir un índice incorrecto
  • O directamente no usar índice

❌ 2. Full Table Scan inesperado

Table scan on libros


Si tenés índice y aun así escanea todo:

🚨 Algo está mal


Posibles causas

  • Falta de índice
  • Índice no selectivo
  • Uso de funciones en columnas
  • LIKE '%texto%'


❌ 3. Índice ignorado

Tenés índice:

CREATE INDEX idx_nombre ON autores(nombre);


Pero el plan dice:

Table scan on autores


❌ No lo está usando


Causas típicas

  • Baja selectividad
  • Estadísticas desactualizadas
  • El optimizador cree que el scan es más barato


❌ 4. Nested loops con muchas filas

Nested loop (loops=10000)


Esto escala MUY mal


Problema

Por cada fila de A:

  • Busca en B


Complejidad tipo: O(n * m)


❌ 5. Filtros aplicados tarde


👉 Si aparece después de un scan:

❌ Primero lee todo

❌ Después filtra


❌ 6. Uso excesivo de “Using temporary” o “Sort”

Using temporary

Sort: ...


Señales de:

  • Falta de índices
  • Ordenamientos costosos


Ejemplo real:

SELECT *

FROM libros

WHERE titulo LIKE '%Java%';


Plan:

Table scan on libros


Aunque tengas índice en titulo:

No se usa por el `%` inicial


Caso interesante: estimaciones incorrectas

rows=1.25 actual rows=1

Esto es normal.


Pero:

rows=10000 actual rows=1

Problema serio


Consecuencia


MySQL puede pensar:

“Esto devuelve muchas filas → mejor full scan”

Pero en realidad devuelve pocas


Cómo confirmar que el plan es malo


1. Usar EXPLAIN ANALYZE

EXPLAIN ANALYZE SELECT ...


Comparar:

  • estimado vs real
  • tiempos


2. Medir tiempo real

A veces el plan “feo” igual es rápido.


3. Probar alternativas

  • Reescribir query
  • Forzar índice:

SELECT * FROM libros FORCE INDEX (idx_titulo)

WHERE titulo LIKE 'Java%';


Cómo corregir un mal plan

1. Actualizar estadísticas

ANALYZE TABLE libros;

Muchas veces esto solo ya arregla todo


2. Crear índices correctos

Pensar en:

  • WHERE
  • JOIN
  • ORDER BY


3. Cambiar la query


Ejemplo:

LIKE '%Java%' ❌

LIKE 'Java%'  ✔️


4. Reducir datos

Menos filas = mejores decisiones


5. Forzar plan (último recurso)

USE INDEX / FORCE INDEX

Solo si estás seguro


Error común

“Si EXPLAIN dice que usa índice, está todo bien”

❌ No necesariamente

👉 Puede usarlo… pero mal


Regla de oro

Siempre mirar:

  • rows vs actual rows
  • loops
  • tipo de acceso


El optimizador de MySQL:

✔️ Es bueno

❌ Pero depende de estadísticas


Y cuando se equivoca:

  • No falla
  • Solo se vuelve lento 

lunes, 25 de mayo de 2026

¿Cómo monitorear la JVM desde la consola?


Cuando trabajamos con Java, una de las herramientas más conocidas para monitoreo es VisualVM.

Nos permite analizar memoria, CPU, hilos, GC y mucho más… todo desde una interfaz gráfica muy cómoda.

Pero hay un problema: ¿Qué pasa cuando estás en un servidor sin entorno gráfico?

Ahí es donde VisualVM ya no alcanza.


Java ya trae todo lo necesario… solo que más low level.


1. jps – listar procesos Java


jps -l


Salida típica:

1234 com.miapp.Main

5678 org.apache.catalina.startup.Bootstrap


Es el primer paso: obtener el PID.


2. jstat – métricas del GC


jstat -gc 1234 1000


Muestra:

  • uso de memoria
  • comportamiento del Garbage Collector
  • generaciones (Eden, Survivor, Old)


Ideal para detectar problemas de memoria en vivo.


3. jstack – dump de hilos


jstack 1234


Te da:

  • todos los threads
  • estados (RUNNABLE, BLOCKED, etc.)
  • posibles deadlocks


Clave para debugging de concurrencia.


4. jmap – análisis de memoria


jmap -heap 1234


O histogramas:


jmap -histo 1234


Podés ver qué objetos están ocupando memoria.


5. jcmd – el reemplazo moderno

Esta es la más importante.


jcmd 1234 help


Ejemplos:

jcmd 1234 GC.heap_info

jcmd 1234 Thread.print

jcmd 1234 GC.run


Es como un “super comando” que reemplaza a varios anteriores.


Veamos un ejemplo: 

En entornos reales se suele hacer esto:


1. Servidor (Linux sin GUI)

  • Aplicación Java corriendo
  • JMX habilitado


2. Diagnóstico:

  • CLI (jcmd, jstat, etc.)
  • o conexión remota desde VisualVM


Este enfoque es estándar en sistemas productivos.


VisualVM no es “la herramienta”, es solo un cliente visual.

Las verdaderas herramientas son:

  • APIs internas de la JVM
  • JMX
  • comandos como jcmd


VisualVM simplemente las consume y las muestra bonito.


sábado, 23 de mayo de 2026

Guía de EXPLAIN ANALYZE en MySQL: cómo leer el plan de ejecución de verdad


Cuando trabajamos con performance en MySQL, entender el plan de ejecución es fundamental.


EXPLAIN nos da una idea pero EXPLAIN ANALYZE nos dice la verdad.


Si hacemos: 

EXPLAIN ANALYZE SELECT ...


Ejecuta la query (Ojo! si la query demora tanto que no termina la ejecución, ya esta no podemos usarlo) y devuelve:

  • El plan de ejecución
  • Métricas reales de ejecución
  • Comparación entre estimaciones y realidad


Ejemplo típico:


-> Nested loop inner join  (cost=0.787 rows=1.25)

   (actual time=0.0644..0.0705 rows=1 loops=1)


    -> Index lookup on tabla1 ...

       (cost=...) (actual time=... rows=... loops=...)


    -> Index lookup on tabla2 ...

       (cost=...) (actual time=... rows=... loops=...)


Esto es un árbol de ejecución:

  • De arriba hacia abajo
  • Cada nodo es una operación


Conceptos clave (los más importantes):

cost


(cost=0.787 rows=1.25)


Es una estimación del optimizador.

Incluye:

  • I/O esperado
  • CPU estimado
  • Lecturas de índice / tabla


⚠️ Importante:

❌ No es tiempo real

❌ No está en milisegundos

✔️ Solo sirve para comparar planes


rows (estimado)

rows=1.25

Cantidad estimada de filas.


Puede ser decimal porque:

  • Es un promedio estadístico
  • Ej: 1.25 filas por iteración


actual time


actual time=0.0644..0.0705


Tiempo real en milisegundos:

  • Primer valor → tiempo hasta la primera fila
  • Segundo valor → tiempo hasta la última fila


actual rows

rows=1

Filas reales procesadas.


loops

loops=1


Cuántas veces se ejecutó ese paso.


Relación clave

filas totales ≈ rows × loops


Tipos de operaciones (nodos del plan)


Nested Loop Join

MySQL usa principalmente este tipo de join.


Funcionamiento:

  1. Toma una fila de la tabla A
  2. Busca coincidencias en tabla B
  3. Repite


Index Lookup

Index lookup on tabla using index_name


Búsqueda usando índice:

✔️ Rápido

✔️ Ideal


Covering Index Lookup

El índice tiene todas las columnas necesarias.


✔️ No accede a la tabla

✔️ Más eficiente


Table Scan

Table scan on tabla


Escaneo completo.

❌ Costoso

❌ Señal de posible problema


Range Scan

Range scan on index


Usa índice, pero en un rango:

WHERE fecha BETWEEN ...


✔️ Bueno, pero no tan óptimo como lookup exacto


Filter

Filter: (condición)


Filtra filas después de leerlas.


⚠️ Si aparece mucho → puede faltar índice


Sort

Ordenamiento (`ORDER BY`)

❌ Puede usar memoria o disco


Temporary Table

Using temporary


MySQL crea tabla intermedia y puede impactar performance


Cómo leer un plan (estrategia real)


Paso 1: leer de abajo hacia arriba

  • Primero las hojas (tablas base)
  • Luego los joins


Paso 2: comparar estimado vs real


rows=1000 vs actual rows=10


Mala estimación → posible mal plan


Paso 3: mirar loops

Detecta:

  • nested loops costosos
  • operaciones repetidas


Paso 4: identificar scans


Buscar:

Table scan


Candidato a optimización


Problemas comunes detectables

1. Mala estimación

2. Full table scan innecesario

3. Nested loops costosos

4. Filtros tardíos


Veamos un ejemplo:


-> Nested loop inner join  (rows=1.25)

   (actual rows=1 loops=1)


    -> Covering index lookup on autores

       (rows=1 actual rows=1)


    -> Index lookup on libros

       (rows=1.25 actual rows=1)


Interpretación:

1. Encuentra 1 autor

2. Estima 1.25 libros por autor

3. Realmente hay 1


✔️ Plan correcto

✔️ Estimación aceptable


EXPLAIN ANALYZE es la herramienta más poderosa para entender performance en MySQL.


Pero requiere cambiar la mentalidad:

  • No mirar solo el resultado
  • Entender cómo MySQL llega a él



miércoles, 20 de mayo de 2026

std::optional en C++


En muchos lenguajes modernos existe alguna forma de representar “un valor que puede no existir”:

  • Optional en Java
  • Option en Scala
  • Maybe en Haskell
  • Option en Rust (Some/None)


En C++, la respuesta oficial es std::optional, introducido en C++17 y mejorado muchísimo en C++23 con operaciones monádicas.

¿Qué problema resuelve?

Antes de optional, era común devolver:

  • nullptr
  • valores mágicos (-1)
  • flags adicionales
  • excepciones


Ejemplo clásico:


int findUserId(const std::string& name) {

    if(name == "emanuel")

        return 10;

    return -1;

}


Problema:

  • -1 no expresa claramente ausencia
  • alguien podría olvidarse de validarlo
  • el contrato del método no es explícito


Con std::optional:


#include <optional>


std::optional<int> findUserId(const std::string& name) {

    if(name == "emanuel")

        return 10;


    return std::nullopt;

}


Ahora el tipo expresa claramente: “puede devolver un entero… o no”.


Crear un optional

std::optional<int> number = 10;


Vacío:

std::optional<int> empty = std::nullopt;


Verificar si tiene valor

if(number.has_value()) {

    std::cout << "Tiene valor";

}


o más idiomático:

if(number) {

    std::cout << "Tiene valor";

}


Obtener el valor

std::cout << number.value();


Ojo! Si no tiene valor, lanza excepción.


Más seguro:

if(number) {

    std::cout << *number;

}


Valor por defecto

std::optional<int> x;

std::cout << x.value_or(0);


Resultado: 0


Muy parecido a:

  • orElse() en Java
  • getOrElse() en Scala


¿Por qué es mejor que punteros?

Muchas APIs antiguas usan punteros nullable:

User* findUser();


Problemas:

  • ownership ambiguo
  • riesgo de dangling pointers
  • semántica poco clara


Con optional:

std::optional<User> findUser();


El contrato queda explícito y seguro.

En programación funcional, una mónada permite:

  • encadenar operaciones
  • evitar checks manuales
  • propagar automáticamente ausencia/error


Antes de C++23 esto era incómodo.


Había que hacer:

if(result) {

    ...

}


todo el tiempo.


C++23 agregó operaciones monádicas oficiales.

transform

Equivalente a map.

Transforma el valor si existe.


std::optional<int> number = 10;


auto result =

    number.transform([](int x) {

        return x * 2;

    });


std::cout << *result;


Resultado: 20

Si el optional está vacío, no ejecuta nada.


and_then

Equivalente a flatMap.

Permite encadenar funciones que devuelven optional.


std::optional<int> parse(const std::string& s) {

    if(s == "42")

        return 42;


    return std::nullopt;

}


auto result =

    parse("42")

        .and_then([](int x) -> std::optional<int> {

            if(x > 0)

                return x * 2;


            return std::nullopt;

        });


Diferencia entre transform y and_then

transform convierte:

optional<T> -> optional<U>

cuando la función devuelve un valor normal, and_then convierte:

optional<T> -> optional<U>


pero la función YA devuelve optional.

Evita nested optionals: optional<optional<int>>


or_else

Permite ejecutar lógica si está vacío.


std::optional<int> value;


value.or_else([] {

    std::cout << "No había valor";

    return std::optional<int>{0};

});


Ahora podemos escribir código mucho más declarativo:


auto result =

    parse("42")

        .transform([](int x) {

            return x * 2;

        })

        .and_then([](int x) -> std::optional<int> {

            if(x < 100)

                return x;


            return std::nullopt;

        })

        .or_else([] {

            return std::optional<int>{0};

        });


Esto ya se parece muchísimo a:

  • Scala
  • Haskell
  • Rust
  • Kotlin

¿Cuándo usar optional?

Ideal para:

  • búsquedas
  • parseos
  • resultados opcionales
  • operaciones que pueden fallar naturalmente


¿Cuándo NO usarlo?

  • errores complejos
  • información detallada de fallos


En esos casos es mejor:

  • std::expected (C++23)
  • excepciones


std::optional empezó en C++17 como una forma segura de representar ausencia de valor.

Pero en C++23 evolucionó muchísimo:

  • transform
  • and_then
  • or_else


lo convierten en una herramienta claramente influenciada por programación funcional y mónadas como Maybe.

C++ sigue siendo multiparadigma, pero cada vez incorpora más ideas del mundo funcional… sin dejar de ser C++.

Errores más comunes al usar particionado en MySQL (y cómo evitarlos)


El particionado en MySQL es una herramienta poderosa, pero también tiene varias reglas “ocultas” que suelen generar errores frustrantes.


1. La clave primaria no incluye la columna de partición


Este es el clásico:

PARTITION BY RANGE (anio_publicacion)

PRIMARY KEY (id_libro)


Error: A PRIMARY KEY must include all columns in the table's partitioning function


Solución

PRIMARY KEY (id_libro, anio_publicacion)


MySQL necesita garantizar unicidad global entre particiones. Por eso tiene que ser primary key la partición. 


2. Índices UNIQUE que no incluyen la columna de partición


No solo afecta a la PK:

UNIQUE (email)


También falla si email no incluye la columna de partición.


Solución: 

UNIQUE (email, anio_publicacion)


3. Pensar que el particionado reemplaza índices


Error conceptual muy común:

> “Particiono y ya no necesito índices”


❌ Incorrecto


Particionado → reduce datos a escanear

Índices → optimizan acceso dentro de la partición


Se complementan, no compiten.


4. No usar la columna de partición en las consultas


Ejemplo:

SELECT * FROM logs WHERE usuario = 'Juan';


MySQL tiene que escanear todas las particiones.

Mejor:


SELECT * FROM logs 

WHERE fecha >= '2026-01-01'

AND usuario = 'Juan';


Activa partition pruning


5. Definir mal los rangos (RANGE)


Ejemplo peligroso:

PARTITION p2023 VALUES LESS THAN (2023),

PARTITION p2024 VALUES LESS THAN (2024)


 ¿Dónde van los valores de 2023? (error lógico)


Correcto:

PARTITION p2023 VALUES LESS THAN (2024)


6. Olvidarse de MAXVALUE


PARTITION p2024 VALUES LESS THAN (2025)


¿Qué pasa con datos futuros?

Solución:

PARTITION pFuture VALUES LESS THAN MAXVALUE


Evita errores al insertar nuevos datos.

7. Usar particionado en tablas pequeñas

Overkill total.

Problema:

  • Más complejidad
  • Sin beneficios reales
  • Incluso puede empeorar performance


8. Pensar que particionado = sharding

Error conceptual importante.

Diferencia

Particionado → dentro de una misma tabla (mismo servidor)

Sharding → distribución en múltiples servidores


9. Usar funciones no determinísticas o inválidas

Ejemplo:

PARTITION BY RANGE (YEAR(NOW()))

❌ No permitido


10. Problemas con AUTO_INCREMENT

Cuando combinás:

  • AUTO_INCREMENT
  • PK compuesta
  • Particionado


Puede volverse confuso

Tip:

El AUTO_INCREMENT:

  • Debe estar en una clave indexada
  • Y respetar la estructura de la PK


12. Creer que DELETE masivo es la mejor opción

DELETE FROM logs WHERE fecha < '2023-01-01';

Lento, bloqueante


Mejor con particionado:

ALTER TABLE logs DROP PARTITION p2022;

Instantáneo



El particionado en MySQL:

✔️ Puede mejorar muchísimo la performance

❌ Pero introduce restricciones de diseño importantes


La clave es entender que:

  • No es transparente
  • Afecta claves, índices y consultas
  • Debe planificarse desde el diseño


domingo, 17 de mayo de 2026

Particionado de Tablas en MySQL: Qué es, tipos y cuándo usarlo




El particionado en MySQL es una técnica que permite dividir una tabla grande en partes más pequeñas (particiones), manteniendo la ilusión de que sigue siendo una única tabla.


Esto es clave cuando trabajamos con:

  • Grandes volúmenes de datos (millones o billones de filas)
  • Consultas que pueden beneficiarse de “leer menos datos”
  • Mantenimiento más eficiente (archivar, borrar, etc.)


Imaginemos una tabla de logs:

logs(id, fecha, usuario, accion)


Con millones de registros, consultas como:

SELECT * FROM logs WHERE fecha >= '2026-01-01';


terminan escaneando demasiados datos.


Con particionado, MySQL puede hacer partition pruning:

  • Solo accede a las particiones relevantes
  • Reduce I/O y mejora performance


Tipos de particionado en MySQL


 RANGE (por rango)

Ideal para fechas o valores ordenados.


CREATE TABLE logs (

  id INT,

  fecha DATE,

  mensaje VARCHAR(255)

)

PARTITION BY RANGE (YEAR(fecha)) (

  PARTITION p2023 VALUES LESS THAN (2024),

  PARTITION p2024 VALUES LESS THAN (2025),

  PARTITION pFuture VALUES LESS THAN MAXVALUE

);


Muy usado para:

  • Logs
  • Eventos
  • Datos históricos


LIST (por valores específicos)


CREATE TABLE usuarios (

  id INT,

  pais VARCHAR(50)

)

PARTITION BY LIST COLUMNS(pais) (

  PARTITION p_latam VALUES IN ('Argentina', 'Brasil'),

  PARTITION p_europa VALUES IN ('España', 'Francia')

);


Útil cuando tenés categorías bien definidas.


HASH (distribución uniforme)


CREATE TABLE pedidos (

  id INT,

  cliente_id INT

)

PARTITION BY HASH(cliente_id)

PARTITIONS 4;


MySQL distribuye automáticamente los datos.


KEY (similar a HASH pero interno)


CREATE TABLE sesiones (

  id INT,

  usuario_id INT

)

PARTITION BY KEY(usuario_id)

PARTITIONS 4;


Usa funciones hash internas de MySQL.


Limitaciones importantes

Antes de usar particionado, hay que tener en cuenta:

  • Todas las claves primarias deben incluir la columna de partición
  • No todas las engines soportan particionado (InnoDB sí)
  • No reemplaza índices (se complementan)
  • Puede complicar el diseño si no se usa bien


Cuándo usar particionado:

✔️ Tablas muy grandes (millones de filas)

✔️ Consultas filtradas por la columna de partición

✔️ Necesidad de borrar datos antiguos fácilmente


Cuándo NO usar particionado:

❌ Tablas pequeñas

❌ Consultas que no usan la columna de partición

❌ Como reemplazo de índices


miércoles, 13 de mayo de 2026

Kotlin 2.4.0


La versión 2.4.0 de Kotlin sigue consolidando muchas características que venían evolucionando desde releases anteriores.

Más que agregar “una gran feature”, esta versión termina de estabilizar varias piezas importantes del ecosistema.

El nuevo compilador K2 dejó de sentirse “experimental” y pasó a ser la base real del futuro de Kotlin.


¿Qué aporta?

  • Compilaciones más rápidas
  • Mejor análisis de tipos
  • Mensajes de error más claros
  • Infraestructura más simple para futuras features


K2 no es solamente una optimización, es prácticamente una reescritura del compilador.


Los Context Parameters siguen evolucionando y acercan a Kotlin a ideas similares a:

  • implicits de Scala
  • type classes funcionales
  • dependency injection implícita


Ejemplo:


context(Logger)

fun processOrder() {

    log("Processing order")

}


Esto permite escribir APIs mucho más declarativas.


Kotlin sigue empujando fuerte el desarrollo multiplataforma; en 2.4.0 hay mejoras importantes en:

  • compilación incremental
  • interoperabilidad con iOS
  • performance de Kotlin/Native
  • sharing de código entre plataformas


El garbage collector y el manejo de memoria continúan mejorando para Kotlin/Native. 


Esto impacta directamente en:

  • apps iOS
  • aplicaciones embebidas
  • performance general


Históricamente Kotlin/Native era uno de los puntos más débiles del ecosistema. Las últimas versiones muestran una mejora enorme.


También hay mejoras en:

  • IntelliJ IDEA
  • Gradle
  • debugging
  • análisis estático
  • tiempos de indexing


Muchas veces estas mejoras no aparecen en los titulares, pero son las que realmente cambian la experiencia diaria.


Lo más interesante de Kotlin 2.4.0 quizás no sea una feature puntual.

Es que muchas ideas que antes parecían experimentales ahora empiezan a sentirse “normales”:

  • K2
  • Multiplatform
  • Native
  • Context Parameters


Kotlin está entrando en una etapa mucho más madura del lenguaje.


domingo, 10 de mayo de 2026

Cómo usar EXPLAIN en MySQL para optimizar tus consultas


EXPLAIN te muestra el plan de ejecución de una consulta SQL.


En otras palabras, responde preguntas como:

  • ¿Qué tablas se recorren?
  • ¿En qué orden?
  • ¿Se están usando índices?
  • ¿Cuántas filas se estiman procesar?


EXPLAIN 

SELECT * 

FROM libro 

WHERE titulo = 'El Quijote';


Salida típica:


| id | select_type | table | type | possible_keys | key  | rows | Extra       |

| -- | ----------- | ----- | ---- | ------------- | ---- | ---- | ----------- |

| 1  | SIMPLE      | libro | ALL  | idx_titulo    | NULL | 1000 | Using where |


Qué podemos saber con esto, sin ser un erudito: 

type = ALL → Full Table Scan ❌

key = NULL → no está usando ningún índice


Esto significa que MySQL recorre toda la tabla.


Veamos agregar un indice: 

CREATE INDEX idx_titulo ON libro(titulo);


Volvemos a ejecutar:

EXPLAIN 

SELECT * 

FROM libro 

WHERE titulo = 'El Quijote';


Nueva salida:

| type | key        | rows |

| ---- | ---------- | ---- |

| ref  | idx_titulo | 1    |


✅ Ahora sí:

  • Usa índice
  • Escanea pocas filas
  • Mucho más eficiente


Veamos las columnas clave de EXPLAIN

type (MUY importante)

Indica cómo accede a la tabla:


| type   | Significado             |

| ------ | ----------------------- |

| ALL    | Full scan (malo) ❌      |

| index  | Scan de índice completo |

| ref    | Uso de índice 👍        |

| eq_ref | Uso óptimo 🚀           |

| const  | Mejor caso posible 💎   |


key

  • Índice que se está utilizando
  • Si es `NULL` → problema 😬


rows

  • Cantidad estimada de filas a procesar
  • Mientras menor, mejor


Extra

Algunos valores importantes:


| Extra           | Significado             |

| --------------- | ----------------------- |

| Using where     | Filtro aplicado         |

| Using index     | Index-only scan 🚀      |

| Using temporary | Tabla temporal ⚠️       |

| Using filesort  | Ordenamiento costoso ⚠️ |


Veamos un ejemplo con JOIN


EXPLAIN

SELECT l.titulo, a.nombre

FROM libro l

JOIN autor a ON l.autor_id = a.id

WHERE a.nombre = 'Cervantes';


👉 Cosas a observar:

  • Orden de las tablas
  • Índices en autor.nombre y libro.autor_id
  • Tipo de join (`ref`, `eq_ref`, etc.)


Problema típico: filesort

EXPLAIN

SELECT * 

FROM libro

ORDER BY fecha_publicacion;


Si ves: Using filesort


MySQL está ordenando en memoria (o peor, en disco)

Solución:

CREATE INDEX idx_fecha ON libro(fecha_publicacion);


EXPLAIN ANALYZE (MySQL 8+)


Mysql agrega el analyze que es propio de mysql, por lo menos no conozco en otras bases. (si conocen me dicen en los comentarios) Analyze corre la query y nos tira data más precisa. Tema que hay que esperar a que ejecute la query. 


EXPLAIN ANALYZE

SELECT * 

FROM libro 

WHERE titulo = 'El Quijote';


Que diferencia tiene con explain clave:


| EXPLAIN             | EXPLAIN ANALYZE |

| Estimaciones         | Ejecución real  |

| No ejecuta query   | Ejecuta query   |

| Rápido                   | Más preciso     |


Ejemplo:


-> Index lookup on libro using idx_titulo

(cost=0.35 rows=1)

(actual time=0.01..0.02 rows=1 loops=1)


Como conclusión, EXPLAIN no es opcional: es una herramienta esencial.


Te permite:

  • Entender cómo piensa MySQL
  • Detectar problemas de performance
  • Validar el uso de índices
  • Optimizar queries complejas


Un buen flujo de trabajo sería:


1. Escribir la query

2. Ejecutar EXPLAIN

3. Detectar problemas

4. Agregar índices / refactorizar

5. Validar con EXPLAIN ANALYZE


Nunca andar tirando índices a lo loco o trabajar a tientas.