Volver al blog

Cómo obtener la última factura de cada cliente en MySQL con ROW_NUMBER()

Portada de Cómo obtener la última factura de cada cliente en MySQL con ROW_NUMBER()

En cualquier sistema administrativo, ERP, CRM o plataforma de facturación electrónica en México, hay una pregunta recurrente que el área de ventas, contabilidad o cobranza hace a cada momento: “¿Cuál fue la última factura emitida a cada cliente, en qué fecha y por qué importe?”.

A nivel de base de datos relacional (MySQL o MariaDB), parece una consulta sencilla: basta con agrupar por cliente y pedir la fecha máxima. Sin embargo, este es uno de los tropiezos más comunes y engañosos del desarrollo de software: muchos queries que parecen correctos terminan entregando importes de facturas equivocadas sin mandar ningún aviso de advertencia.

En este artículo te mostramos por qué la solución tradicional con GROUP BY falla silenciosamente, cómo MySQL 8 protege la integridad de tus datos y cómo resolverlo de forma limpia, exacta y profesional usando la función de ventana ROW_NUMBER().


El problema: la trampa del GROUP BY tradicional

Imagina una tabla clásica donde guardas los comprobantes fiscales digitales emitidos (facturas), identificados por el RFC del receptor, la fecha de emisión y el importe total:

CREATE TABLE facturas (
  id INT AUTO_INCREMENT PRIMARY KEY,
  rfc_receptor VARCHAR(13) NOT NULL,
  fecha DATE NOT NULL,
  total DECIMAL(12,2) NOT NULL
);

INSERT INTO facturas (rfc_receptor, fecha, total) VALUES
 ('XAXX010101000', '2026-09-01', 1160.00),
 ('XAXX010101000', '2026-09-20',  580.00),
 ('EKU9003173C9',  '2026-08-15', 2320.00),
 ('EKU9003173C9',  '2026-09-28',  348.00),
 ('EKU9003173C9',  '2026-09-10',  116.00);

❌ Lo que muchos intentan hacer

La primera intuición suele ser escribir una consulta como esta:

SELECT rfc_receptor, MAX(fecha) AS fecha, total
FROM facturas
GROUP BY rfc_receptor;

A primera vista parece lógica: agrupamos por RFC del cliente, calculamos la fecha más reciente con MAX(fecha) y seleccionamos el total. Pero aquí está la trampa:

  1. En servidores antiguos o sin ONLY_FULL_GROUP_BY: MySQL calcula correctamente la fecha máxima, pero para la columna total (que no forma parte del GROUP BY ni está envuelta en una función de agregación) el motor toma un valor arbitrario del grupo.

    El resultado real de esa consulta es una mezcla desastrosa:

    +---------------+------------+---------+
    | rfc_receptor  | fecha      | total   |
    +---------------+------------+---------+
    | EKU9003173C9  | 2026-09-28 | 2320.00 |  <-- ¡La factura del 28/09 era de $348.00!
    | XAXX010101000 | 2026-09-20 | 1160.00 |  <-- ¡La factura del 20/09 era de $580.00!
    +---------------+------------+---------+

    El reporte arroja la fecha del comprobante más nuevo con el importe de una factura anterior. En un reporte de cobranza o estados de cuenta de clientes, esto provoca discrepancias graves.

  2. En MySQL 8.0+ y MariaDB modernas (con ONLY_FULL_GROUP_BY): El motor detecta de inmediato la ambigüedad y detiene la consulta con el error 1055:

    ERROR 1055 (42000): Expression #3 of SELECT list is not in GROUP BY clause 
    and contains nonaggregated column 'facturas.total' which is not functionally 
    dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

¡Cuidado con la tentación de desactivar ONLY_FULL_GROUP_BY!
Algunos tutoriales sugieren “solucionar” el error 1055 eliminando esta directiva de la configuración (my.cnf o sql_mode). Eso no soluciona el problema de fondo: únicamente silencia la alerta y permite que tu base de datos devuelva reportes con datos falsos.


La solución correcta: Funciones de ventana con ROW_NUMBER()

A partir de MySQL 8.0 y MariaDB 10.2, el estándar SQL incorporó de forma nativa las funciones de ventana (Window Functions). A diferencia de GROUP BY, que colapsa todas las filas en un solo resumen, las funciones de ventana realizan cálculos sobre un conjunto de filas relacionadas sin perder el detalle individual de cada registro.

Para obtener la última factura, la función ideal es ROW_NUMBER():

SELECT rfc_receptor, fecha, total
FROM (
  SELECT 
    id,
    rfc_receptor,
    fecha,
    total,
    ROW_NUMBER() OVER (
      PARTITION BY rfc_receptor 
      ORDER BY fecha DESC, id DESC
    ) AS rn
  FROM facturas
) AS t
WHERE rn = 1;

O si prefieres una sintaxis moderna y legible con Common Table Expressions (CTE / WITH):

WITH facturas_numeradas AS (
  SELECT 
    id,
    rfc_receptor,
    fecha,
    total,
    ROW_NUMBER() OVER (
      PARTITION BY rfc_receptor 
      ORDER BY fecha DESC, id DESC
    ) AS rn
  FROM facturas
)
SELECT rfc_receptor, fecha, total
FROM facturas_numeradas
WHERE rn = 1;

¿Cómo funciona paso a paso?

  1. PARTITION BY rfc_receptor: Divide el conjunto de facturas en grupos o “cajones” independientes, uno por cada cliente o RFC.
  2. ORDER BY fecha DESC, id DESC: Dentro de cada cajón, ordena las facturas de la más reciente a la más antigua. Agregar id DESC asegura un criterio de desempate en caso de que existan dos facturas emitidas el mismo día.
  3. ROW_NUMBER(): Asigna una numeración secuencial (1, 2, 3…) a cada factura dentro de su cajón. La factura más reciente siempre recibe el número 1.
  4. WHERE rn = 1: En el query exterior, simplemente filtramos aquellas filas que tienen la posición número 1.

✅ Resultado exacto y consistente

+---------------+------------+--------+
| rfc_receptor  | fecha      | total  |
+---------------+------------+--------+
| EKU9003173C9  | 2026-09-28 | 348.00 |  <-- Fecha correcta y monto real
| XAXX010101000 | 2026-09-20 | 580.00 |  <-- Fecha correcta y monto real
+---------------+------------+--------+

Todos los datos pertenecen a la misma fila física, preservando la coherencia del registro.


¿Y si utilizas una versión anterior a MySQL 8.0?

Si administras un servidor heredado con MySQL 5.7 (que ya alcanzó el fin de su ciclo de soporte oficial) y no cuentas con funciones de ventana, la alternativa segura es realizar una subconsulta con INNER JOIN:

SELECT f.rfc_receptor, f.fecha, f.total
FROM facturas f
INNER JOIN (
  SELECT rfc_receptor, MAX(fecha) AS max_fecha
  FROM facturas
  GROUP BY rfc_receptor
) ultimas 
  ON f.rfc_receptor = ultimas.rfc_receptor 
 AND f.fecha = ultimas.max_fecha;

Advertencia de duplicados:
Si un cliente tiene dos facturas emitidas en la misma fecha máxima, este método devolverá ambas. Para evitar duplicados en versiones antiguas debías agrupar por MAX(id) o anidar subconsultas adicionales, lo que degrada el rendimiento en tablas con miles de transacciones. Por eso ROW_NUMBER() es la alternativa estándar en la actualidad.


Más allá de la última factura: casos prácticos en tu negocio

La ventaja de dominar ROW_NUMBER() es que este mismo patrón resuelve decenas de necesidades en sistemas administrativos y de facturación:

  • Obtener las últimas 3 compras de cada cliente: Basta con cambiar el filtro final a WHERE rn <= 3. En SQL tradicional con subconsultas, lograr esto era sumamente complejo y lento.
  • Último complemento de pago emitido: Para saber qué clientes tienen pagos recientes registrados en parcialidades o diferidos (PPD).
  • Último inicio de sesión de usuarios: Conocer la fecha y dirección IP del acceso más reciente en un panel administrativo.
  • Último movimiento de inventario: Identificar la fecha y precio del lote más reciente por código de producto.

ROW_NUMBER() vs. RANK() vs. DENSE_RANK()

MySQL ofrece tres funciones similares para ordenar posiciones. Esta es la diferencia práctica cuando hay empates en fecha:

FunciónComportamiento ante empatesEjemplo de salidaIdeal para
ROW_NUMBER()Asigna números únicos consecutivos (1, 2, 3…) sin empates.1, 2, 3, 4Quedarse con exactamente una fila por grupo.
RANK()Asigna la misma posición a valores iguales y salta números.1, 1, 3, 4Cuadros de honor o rankings deportivos.
DENSE_RANK()Asigna la misma posición a valores iguales sin saltar números.1, 1, 2, 3Clasificaciones densas por puntaje.

Para obtener el último registro de una entidad, ROW_NUMBER() es siempre la opción indicada.


Tabla comparativa de enfoques en MySQL

CriterioGROUP BY con columnas libresINNER JOIN con MAX()ROW_NUMBER() (Ventana)
Integridad de datos❌ Falso (mezcla columnas)✅ Preciso✅ 100% Preciso y consistente
Manejo de empates❌ Incontrolable⚠️ Requiere agrupar por MAX(id)✅ Desempate limpio con ORDER BY
Flexibilidad (últimas N filas)❌ Imposible❌ Muy complejo✅ Inmediato (WHERE rn <= N)
CompatibilidadMySQL < 8.0 sin strict modeTodas las versionesMySQL 8.0+ / MariaDB 10.2+
RendimientoEngañosoRegular (doble lectura)Óptimo con índices adecuados

Checklist para optimizar consultas de facturación

Antes de enviar esta consulta a producción en tu sistema ERP o reporte web, revisa estos puntos clave:

  • Mantén activo ONLY_FULL_GROUP_BY: Asegúrate de que el modo SQL de tu servidor prevenga consultas ambiguas por defecto.
  • Crea un índice compuesto: Si la tabla facturas tiene decenas o cientos de miles de registros, crea un índice que cubra la partición y el ordenamiento:
    CREATE INDEX idx_facturas_cliente_fecha ON facturas (rfc_receptor, fecha DESC, id DESC);
  • Desempata siempre con la clave primaria: Incluye el campo id autoincremental al final de ORDER BY para que el orden sea 100% determinista.
  • Verifica el tipo de dato de fecha: Si tu campo almacena fecha y hora (DATETIME o TIMESTAMP), el ordenamiento por fecha reflejará con mayor precisión el segundo exacto de la emisión del CFDI.
  • Usa DECIMAL para montos contables: Nunca uses FLOAT ni DOUBLE para almacenar subtotales o totales en facturación.

Simplifica la facturación de tu empresa con IOSoluciones

Si desarrollas sistemas a medida, administras bases de datos de clientes o gestionas la facturación de una o varias empresas, contar con una arquitectura sólida te ahorra miles de horas y evita discrepancias contables.

  • ¿Buscas un sistema de facturación electrónica confiable y multi-RFC?
    Conoce Orbis Facturación Electrónica, nuestra plataforma en la nube diseñada para PyMEs y profesionistas en México. Emite facturas CFDI 4.0, complementos de pago y notas de crédito en segundos, sin complicaciones técnicas.
  • ¿Necesitas optimización de bases de datos o desarrollo de sistemas?
    En IOSoluciones brindamos servicios de Soporte Técnico y Mantenimiento y desarrollo de software para mantener tus aplicaciones seguras, rápidas y escalables.

¿Tienes dudas sobre cómo estructurar tus reportes o implementar facturación en tu plataforma? Contáctanos y con gusto platicamos sobre tu proyecto.


Fuentes oficiales

Consultadas en octubre de 2026:

¿Quieres optimizar tu presencia digital o tus sistemas?

En IOSoluciones desarrollamos sitios web rápidos, plataformas a la medida y ofrecemos soporte técnico especializado para empresas en México.

Solicitar Cotización Gratis Ver Planes de Desarrollo
¿Tienes un proyecto? Hablemos