Cada empresa tiene el mismo problema: los datos llegan sucios, dispersos en varios archivos y con formatos inconsistentes. El analista pasa horas limpiando manualmente antes de poder armar un solo reporte.
El lenguaje M de Power Query cambia eso. Una vez que escribís la transformación, se ejecuta sola en cada actualización — sin intervención, sin errores manuales, siempre igual.
Esta guía está organizada por casos reales de empresa, no por teoría abstracta. Cada sección parte de un problema concreto que probablemente ya enfrentaste.
¿Qué es el lenguaje M y por qué importa en la empresa?
M (Power Query Formula Language) es el lenguaje que corre detrás de cada paso del editor de Power Query. Cuando filtrás, renombrás una columna o cambiás un tipo desde los menús, Power Query escribe código M por vos. Ese código queda guardado y se puede leer — y modificar — en el Editor avanzado.
Cinco reglas que definen cómo funciona:
- Es sensible a mayúsculas.
Text.Trimfunciona;text.trimfalla. - Estructura
let … in. Cada consulta tiene pasos nombrados enlety devuelve el resultado conin. - Evaluación perezosa. Solo evalúa lo que necesita para el resultado final — no re-procesa pasos innecesarios.
- Inmutable. Cada transformación crea una copia; no modifica el dato original.
- Ecosistema amplio. El mismo M funciona en Power BI, Excel, Dataflows de Fabric, Power Automate y Azure Data Factory.
Aprendelo una vez y lo aplicás en todas las herramientas del ecosistema Microsoft.
La estructura base: let … in
Todo código M vive dentro de un bloque let … in. Los pasos del let se ejecutan en secuencia; el in dice cuál es el resultado final.
Caso: el área de Finanzas recibe un CSV con ventas diarias y necesita cargarlo, limpiar los nombres de vendedor y filtrar solo las ventas aprobadas.
let
// Paso 1: conectar al archivo
Origen = Csv.Document(File.Contents("C:\Reportes\Ventas.csv"),
[Delimiter=",", Encoding=65001]),
// Paso 2: promover la primera fila como encabezados
Encabezados = Table.PromoteHeaders(Origen, [PromoteAllScalars=true]),
// Paso 3: asignar tipos de dato correctos
Tipos = Table.TransformColumnTypes(Encabezados, {
{"Fecha", type date}, {"Vendedor", type text},
{"Monto", Currency.Type}, {"Estado", type text}}),
// Paso 4: solo ventas aprobadas
Aprobadas = Table.SelectRows(Tipos, each [Estado] = "Aprobada")
in
Aprobadas
Cada paso referencia al anterior. Renombrá los pasos con nombres descriptivos — Aprobadas explica más que "Filas filtradas".
Tipos de datos: definirlos bien evita el 80 % de los errores
El error más frecuente en Power Query es trabajar con columnas sin tipo definido. Una columna de fecha que Power Query lee como text no puede filtrarse por mes. Un monto leído como text no puede sumarse.
| Tipo M | Cuándo usarlo | Ejemplo |
|---|---|---|
type text |
Nombres, códigos, descripciones | "Quito", "C-0042" |
type number |
Cantidades, porcentajes | 42, 0.15 |
Currency.Type |
Montos monetarios (evita errores de redondeo) | 1250.50 |
type date |
Fechas sin hora | #date(2026, 6, 9) |
type datetime |
Fecha con hora (registros de sistema) | #datetime(2026,6,9,14,0,0) |
type logical |
Banderas sí/no, activo/inactivo | true, false |
null |
Celda vacía o dato faltante | null |
Regla práctica: definí los tipos en el segundo o tercer paso de toda consulta, apenas después de cargar los encabezados. Hacerlo tarde multiplica los errores aguas abajo.
Table.TransformColumnTypes(Encabezados, {
{"Fecha_Factura", type date},
{"Codigo_Cliente", type text},
{"Importe", Currency.Type},
{"Pagado", type logical}
})
Caso 1: Limpiar la base de clientes
Problema real: el CRM exporta nombres de cliente con espacios extra, mezcla de mayúsculas y caracteres de control invisibles. El equipo de ventas no puede cruzar datos porque «EMPRESA S.A. » (con espacio al final) y «Empresa S.A.» se tratan como dos clientes distintos.
Solución en M:
let
Origen = Excel.Workbook(File.Contents("C:\CRM\Clientes_Export.xlsx"), null, true)
{[Name="Hoja1"]}[Data],
Tipos = Table.TransformColumnTypes(Origen, {
{"ID_Cliente", type text}, {"Nombre", type text},
{"Ciudad", type text}, {"Segmento", type text}}),
// Limpiar nombre: quitar espacios, caracteres invisibles y estandarizar mayúsculas
Limpio = Table.TransformColumns(Tipos, {
{"Nombre", each Text.Proper(Text.Trim(Text.Clean(_))), type text},
{"Ciudad", each Text.Proper(Text.Trim(_)), type text},
{"Segmento",each Text.Upper(Text.Trim(_)), type text}
}),
// Eliminar duplicados por ID (si el CRM exportó el mismo cliente dos veces)
SinDuplicados = Table.Distinct(Limpio, {"ID_Cliente"})
in
SinDuplicados
Funciones de texto más usadas:
Text.Trim(" Empresa S.A. ") // → "Empresa S.A." (quita extremos)
Text.Clean("Empresa\n S.A.") // → "Empresa S.A." (quita caracteres de control)
Text.Proper("EMPRESA s.a.") // → "Empresa S.A." (capitaliza)
Text.Upper("empresa") // → "EMPRESA"
Text.Lower("EMPRESA") // → "empresa"
Text.Replace("S.A.","SA") // → reemplaza
Text.Contains([Nombre], "S.A.") // → true / false
Text.Length([Nombre]) // → cantidad de caracteres
Truco:
Text.Trimno elimina los espacios dobles internos (entre palabras). Si tu fuente los tiene, usá este patrón para colapsarlos:m Text.Combine(List.Select(Text.Split([Nombre], " "), each _ <> ""), " ")
Caso 2: Calcular aging de cartera (días de mora)
Problema real: el área de Cobranzas necesita clasificar cada factura pendiente en tramos de antigüedad (0-30, 31-60, 61-90, más de 90 días) para el reporte mensual de riesgo.
let
Origen = Csv.Document(File.Contents("C:\Finanzas\Facturas_Pendientes.csv"),
[Delimiter=";", Encoding=65001]),
Tipos = Table.TransformColumnTypes(
Table.PromoteHeaders(Origen, [PromoteAllScalars=true]), {
{"Nro_Factura", type text},
{"Fecha_Vencimiento", type date},
{"Saldo", Currency.Type}}),
// Calcular días de mora respecto a hoy
ConDias = Table.AddColumn(Tipos, "Dias_Mora",
each Duration.Days(Date.From(DateTime.LocalNow()) - [Fecha_Vencimiento]),
Int64.Type),
// Clasificar en tramos de aging
ConTramo = Table.AddColumn(ConDias, "Tramo",
each if [Dias_Mora] <= 0 then "Al día"
else if [Dias_Mora] <= 30 then "0-30 días"
else if [Dias_Mora] <= 60 then "31-60 días"
else if [Dias_Mora] <= 90 then "61-90 días"
else "> 90 días",
type text),
// Total por tramo
Resumen = Table.Group(ConTramo, {"Tramo"}, {
{"Facturas", each Table.RowCount(_), Int64.Type},
{"Saldo_Total",each List.Sum([Saldo]), Currency.Type}
})
in
Resumen
Funciones de fecha más usadas:
Date.From(DateTime.LocalNow()) // fecha de hoy
Duration.Days(fecha1 - fecha2) // diferencia en días
Date.Year([Fecha]) // año
Date.Month([Fecha]) // número de mes (1-12)
Date.MonthName([Fecha]) // nombre del mes ("junio")
Date.QuarterOfYear([Fecha]) // trimestre (1-4)
Date.AddDays([Fecha], 30) // sumar 30 días
Date.StartOfMonth([Fecha]) // primer día del mes
Date.EndOfMonth([Fecha]) // último día del mes
Caso 3: Consolidar archivos de múltiples sucursales
Problema real: cada sucursal envía su reporte de ventas mensual como un Excel separado, todos en la misma carpeta. Hay que consolidarlos en una sola tabla y agregar una columna que identifique de qué sucursal proviene cada fila.
let
// Leer todos los archivos de la carpeta
Carpeta = Folder.Files("C:\Reportes\Sucursales\2026-06"),
// Quedarse solo con los Excel
SoloExcel = Table.SelectRows(Carpeta, each Text.EndsWith([Name], ".xlsx")),
// Función que lee un archivo y agrega el nombre de sucursal
fnLeerSucursal = (ruta as binary, nombre as text) as table =>
let
Libro = Excel.Workbook(ruta, null, true){[Name="Ventas"]}[Data],
ConTipos = Table.TransformColumnTypes(Libro, {
{"Fecha", type date}, {"Producto", type text},
{"Monto", Currency.Type}}),
ConNombre = Table.AddColumn(ConTipos, "Sucursal",
each Text.BeforeDelimiter(nombre, ".xlsx"), type text)
in
ConNombre,
// Aplicar la función a cada fila de la tabla de archivos
TablasPorSucursal = Table.AddColumn(SoloExcel, "Datos",
each fnLeerSucursal([Content], [Name])),
// Apilar todas las tablas en una sola
Consolidado = Table.Combine(TablasPorSucursal[Datos])
in
Consolidado
Con este patrón, cuando llega un nuevo archivo a la carpeta, la consulta lo incorpora automáticamente en la próxima actualización — sin ninguna intervención manual.
Caso 4: Cruzar tablas (el BUSCARV del lenguaje M)
Problema real: la tabla de ventas tiene el ID_Vendedor pero no el nombre ni la zona. Esos datos están en una tabla de vendedores separada. Hay que enriquecer las ventas con esa información.
let
Ventas = Excel.CurrentWorkbook(){[Name="Ventas"]}[Content],
Vendedores= Excel.CurrentWorkbook(){[Name="Vendedores"]}[Content],
// Cruzar por ID_Vendedor (equivalente a BUSCARV / LEFT JOIN)
Cruzado = Table.NestedJoin(
Ventas, {"ID_Vendedor"},
Vendedores, {"ID"},
"DatosVendedor",
JoinKind.LeftOuter),
// Expandir las columnas que necesitamos del cruce
Expandido = Table.ExpandTableColumn(
Cruzado, "DatosVendedor",
{"Nombre", "Zona", "Meta_Mensual"})
in
Expandido
Tipos de join disponibles:
JoinKind |
Qué hace | Equivalente SQL |
|---|---|---|
LeftOuter |
Todo lo de la izquierda + coincidencias | LEFT JOIN |
Inner |
Solo coincidencias en ambas tablas | INNER JOIN |
FullOuter |
Todo de ambas tablas | FULL OUTER JOIN |
RightOuter |
Todo lo de la derecha + coincidencias | RIGHT JOIN |
LeftAnti |
Solo los que NO tienen coincidencia | NOT IN |
Caso 5: Carga segura de datos con errores conocidos
Problema real: el sistema ERP exporta un campo Codigo_Postal que a veces contiene letras (cuando el cliente es extranjero). Al convertirlo a número, Power Query rompe toda la consulta.
let
Origen = Csv.Document(File.Contents("C:\ERP\Pedidos.csv"),
[Delimiter=",", Encoding=65001]),
Tipos = Table.TransformColumnTypes(
Table.PromoteHeaders(Origen), {
{"ID_Pedido", type text},
{"Cliente", type text},
{"Cod_Postal", type text}, // lo dejamos text primero
{"Total", Currency.Type}}),
// Convertir código postal a número SOLO si es posible; si no, dejar null
ConCP = Table.AddColumn(Tipos, "CP_Numerico",
each try Int64.From([Cod_Postal]) otherwise null,
Int64.Type),
// Identificar los registros que fallaron para revisarlos
ConFlag = Table.AddColumn(ConCP, "CP_Extranjero",
each [CP_Numerico] = null and [Cod_Postal] <> null,
type logical)
in
ConFlag
Patrón try … otherwise:
// Forma básica: si falla, devuelve null
try Number.FromText([Columna]) otherwise null
// Inspeccionar el error para loggear o ramificar
let
Intento = try Number.FromText([Valor]),
Resultado = if Intento[HasError]
then "Error: " & Intento[Error][Message]
else Text.From(Intento[Value])
in
Resultado
Usá
try … otherwisecuando el fallo es esperado y controlado (datos externos con formatos inconsistentes). No lo uses para ocultar errores de diseño en tu modelo.
Caso 6: Rellenar jerarquías con Fill Down
Problema real: el reporte de inventario exportado desde el ERP tiene la categoría y subcategoría solo en la primera fila de cada grupo — las filas siguientes del grupo tienen esa celda vacía. Power BI no puede agrupar correctamente.
let
Origen = Excel.Workbook(File.Contents("C:\Inventario\Stock.xlsx"), null, true)
{[Name="Stock"]}[Data],
Tipos = Table.TransformColumnTypes(Origen, {
{"Categoria", type text}, {"Subcategoria", type text},
{"Producto", type text}, {"Stock", Int64.Type}}),
// IMPORTANTE: convertir cadenas vacías a null antes de rellenar
// Si la celda tiene "" en lugar de null, FillDown no funciona
NullsLimpios = Table.ReplaceValue(Tipos, "", null,
Replacer.ReplaceValue, {"Categoria", "Subcategoria"}),
// Propagar el valor hacia abajo dentro de cada grupo
Relleno = Table.FillDown(NullsLimpios, {"Categoria", "Subcategoria"})
in
Relleno
Funciones personalizadas: lógica reutilizable
Cuando aplicás la misma transformación en varias consultas, encapsulala en una función. Así cambiás la lógica en un solo lugar y se actualiza en todos los que la usan.
Caso: varias consultas necesitan normalizar el nombre de una empresa quitando sufijos legales (S.A., Cía. Ltda., S.R.L.) para poder cruzar datos de distintas fuentes.
// Consulta independiente llamada "fnNormalizarEmpresa"
(nombre as text) as text =>
let
Limpio = Text.Trim(Text.Clean(nombre)),
SinSA = Text.Replace(Limpio, " S.A.", ""),
SinCia = Text.Replace(SinSA, " Cía. Ltda.",""),
SinSRL = Text.Replace(SinCia, " S.R.L.", ""),
SinLtda = Text.Replace(SinSRL, " Ltda.", ""),
Final = Text.Proper(Text.Trim(SinLtda))
in
Final
Para usarla desde otra consulta:
Table.AddColumn(Origen, "Nombre_Normalizado",
each fnNormalizarEmpresa([Razon_Social]), type text)
ETL completo: reporte de ventas mensual consolidado
Este caso une todo lo anterior en una sola consulta. El objetivo: consolidar ventas de todas las sucursales, enriquecer con datos de vendedores, calcular indicadores y entregar una tabla lista para el reporte ejecutivo.
let
// ── 1. Cargar ventas de la carpeta de sucursales ──────────────────────────
Archivos = Folder.Files("C:\Ventas\2026-06"),
SoloExcel = Table.SelectRows(Archivos, each Text.EndsWith([Name], ".xlsx")),
fnSucursal = (bin as binary, nombre as text) as table =>
let
Hoja = Excel.Workbook(bin, null, true){[Name="Ventas"]}[Data],
Tipado = Table.TransformColumnTypes(Hoja, {
{"Fecha", type date}, {"ID_Vendedor", type text},
{"ID_Producto", type text}, {"Cantidad", Int64.Type},
{"Precio_Unit", Currency.Type}}),
ConNombre= Table.AddColumn(Tipado, "Sucursal",
each Text.BeforeDelimiter(nombre, ".xlsx"), type text)
in ConNombre,
Ventas = Table.Combine(
Table.AddColumn(SoloExcel, "D", each fnSucursal([Content],[Name]))[D]),
// ── 2. Calcular el monto total por línea ──────────────────────────────────
ConMonto = Table.AddColumn(Ventas, "Monto",
each [Cantidad] * [Precio_Unit], Currency.Type),
// ── 3. Enriquecer con datos del vendedor ──────────────────────────────────
Vendedores = Excel.CurrentWorkbook(){[Name="Vendedores"]}[Content],
Cruzado = Table.NestedJoin(ConMonto, {"ID_Vendedor"},
Vendedores, {"ID"}, "V", JoinKind.LeftOuter),
Expandido = Table.ExpandTableColumn(Cruzado, "V",
{"Nombre_Vendedor", "Zona", "Meta_Mensual"}),
// ── 4. Agregar columnas de período ────────────────────────────────────────
ConPeriodo = Table.AddColumn(Expandido, "Mes",
each Date.MonthName([Fecha]) & " " & Text.From(Date.Year([Fecha])),
type text),
// ── 5. Flag de cumplimiento de meta (requiere agrupar primero) ────────────
PorVendedor = Table.Group(ConPeriodo, {"Nombre_Vendedor", "Zona", "Mes", "Meta_Mensual"},{
{"Ventas_Total", each List.Sum([Monto]), Currency.Type},
{"Transacciones",each Table.RowCount(_), Int64.Type}}),
ConCumplimiento = Table.AddColumn(PorVendedor, "Cumple_Meta",
each [Ventas_Total] >= [Meta_Mensual], type logical),
ConPct = Table.AddColumn(ConCumplimiento, "Pct_Meta",
each Number.Round([Ventas_Total] / [Meta_Mensual] * 100, 1),
type number),
// ── 6. Ordenar por zona y monto descendente ───────────────────────────────
Final = Table.Sort(ConPct, {{"Zona", Order.Ascending},
{"Ventas_Total", Order.Descending}})
in
Final
Operadores y expresiones esenciales
// Concatenar texto
"Zona " & [Zona] & " — " & [Nombre_Vendedor]
// Comparación
[Saldo] > 0 // true / false
[Estado] = "Activo"
[Codigo] <> null
// Lógicos
[Monto] > 1000 and [Estado] = "Aprobado"
[Region] = "Sierra" or [Region] = "Costa"
not [Pagado]
// Coalesce: primer valor no-null
[Telefono_Directo] ?? [Telefono_Movil] ?? "Sin contacto"
// Acceso a posición en lista (base cero)
{"Ene","Feb","Mar"}{1} // → "Feb"
Errores frecuentes en contexto empresarial
| Síntoma | Contexto típico | Solución |
|---|---|---|
Expression.Error: 'text.trim' no reconocido |
Copiar código de internet | M es case-sensitive: Text.Trim |
DataFormat.Error al convertir tipo |
Campo monetario con símbolo $ o . como separador de miles |
Limpiar con Text.Replace antes de Number.From |
| FillDown no rellena nada | Reporte de ERP con celdas «vacías» que son "" |
Table.ReplaceValue(t, "", null, ...) primero |
| Cruce devuelve filas nulas | ID con espacios extra en una de las tablas | Text.Trim en la columna clave antes del join |
| La carpeta trae archivos inesperados | Alguien guardó un .xlsm o un PDF en la carpeta |
Filtrá por extensión con Text.EndsWith([Name], ".xlsx") |
| Consulta lenta con millones de filas | Se trae toda la tabla antes de filtrar | Mové el Table.SelectRows lo más arriba posible en el let |
Column X of table not found |
El origen cambió el nombre de un encabezado | Usá try Table.Column(t, "X") otherwise null |
Descargá la guía completa en PDF
La guía incluye todos los patrones de esta página más:
- Chuleta de funciones de referencia rápida (Text, Date, Number, List, Table)
- Caso práctico de ETL paso a paso con capturas
- Extractores de datos para CSV, Excel, carpetas, SQL y APIs
- Sección de buenas prácticas y errores comunes
Ingresá tu correo en el formulario que está al inicio del artículo y te llega directo a tu casilla.
Buenas prácticas para código M mantenible
- Nombrá los pasos con intención.
VentasAprobadases mejor que"Filas filtradas". - Comentá las decisiones no obvias.
// Brevo exporta "" en vez de null — convertir antes de FillDown. - Definí tipos en el paso 2 o 3. Los errores de tipo son mucho más fáciles de depurar temprano.
- Filtrá y seleccioná columnas lo antes posible. Menos datos en memoria = consultas más rápidas.
- Una función por lógica reutilizable. Si la misma limpieza aparece en dos consultas, extraela.
- Usá
try … otherwisesolo en bordes del sistema. Para datos externos donde el formato no está garantizado.
Artículos relacionados
- Expresiones regulares en Power Query — para limpiezas de texto más complejas.
- Reemplazar múltiples valores en un solo paso —
List.Accumulatepara eliminar pasos repetidos. - Extraer valores de una lista de registros — cómo manejar columnas anidadas.
- DataFormat.Error en Power Query — guía de errores de conversión.
- Query references other queries — cuando una consulta depende de otra.

Deja una respuesta