Respondes preguntas de negocio con GROUP BY, descubres que el archivo escribe 100 nombres de cliente donde hay 18 clientes y que no se arregla solo, y lo enriqueces con un JOIN contra dos catálogos que tampoco están limpios.
El notebook de esta clase: https://gaa1104.vercel.app/notebooks/clase-03.ipynb
Ing. en Administración · mención Finanzas · Prof. Tomás Fernández
La pregunta es de magnitud comparada entre categorías (venta por segmento). Horizontales porque las etiquetas son nombres largos, y ordenadas porque el orden alfabético no dice nada.
La forma la decide el trabajo que hace el dato —magnitud, identidad, polaridad, cambio en el tiempo, distribución—, no el gusto. Regla del curso: un solo eje.
Lo que quedó planteado la clase anterior (se proyecta la pregunta, NO la respuesta):
«Si el segmento está escrito de 23 formas, ¿de cuántas formas estará escrito el nombre del cliente? ¿Y el RUT?»
Tres preguntas, en silencio, 90 segundos (espaciado 1–3–7–21). Nadie habla todavía:
WHERE segmento = 'Minería' y salieron CLP 980.768.001. Con las cinco escrituras salieron 1.360.516.886. ¿Por qué la primera consulta no dio error?GROUP BY, funciona y lo entregas. ¿En qué nivel del contrato estás y qué tienes que hacer antes de entregarlo?Par (2 min) → plenaria (3 min).
La pregunta colgada NO se responde acá con el número: se responde a las 21:00, corriendo la consulta. Hoy la apertura sólo deja el pronóstico anotado: pedir en voz alta «¿cuántas formas creen que hay del nombre del cliente? escríbanlo en el cuaderno».
HAVING · 22 minLa analogía, dicha una sola vez: tomas las 6.000 filas, las repartes en cajones —un cajón por segmento— y pegas un resumen en la tapa de cada cajón. Eso es GROUP BY. Lo que va en la tapa lo eliges tú: SUM, COUNT, AVG, MIN, MAX.
HAVING · 22 minCREATE OR REPLACE TABLE ventas AS SELECT * FROM read_csv_auto( 'datos/finanzas/ventas_extracto.csv'); SELECT segmento, COUNT(*) AS lineas, ROUND(SUM(cantidad * precio_unitario)) AS venta_clp FROM ventas WHERE moneda = 'CLP' GROUP BY segmento ORDER BY venta_clp DESC;
HAVING · 22 min| segmento | lineas | venta_clp |
|---|---|---|
| Construcción | 977 | 1.782.114.743 |
| Retail | 988 | 1.762.573.723 |
| Industrial | 1.008 | 1.451.896.518 |
| Minería | 664 | 980.768.001 |
| INDUSTRIAL | 284 | 530.292.913 |
| RETAIL | 270 | 417.771.014 |
| CONSTRUCCIÓN | 217 | 398.467.114 |
| Público | 265 | 395.441.830 |
HAVING · 22 minPregunta a la sala: «¿Cuántos segmentos comerciales tiene esta empresa?»
«ElGROUP BYno se rompió. Hizo exactamente lo que le pediste: juntar lo que está escrito igual.INDUSTRIALno está escrito igual queIndustrial. Para SQL son dos cajones. Para el negocio son el mismo cliente pagando la misma factura.»
Primer auxilio, y su límite:
HAVING · 22 minSELECT upper(strip_accents(trim(segmento))) AS seg, COUNT(*) AS lineas, ROUND(SUM(cantidad * precio_unitario)) AS venta_clp FROM ventas WHERE moneda = 'CLP' GROUP BY 1 ORDER BY venta_clp DESC;
HAVING · 22 min| seg | lineas | venta_clp |
|---|---|---|
| CONSTRUCCION | 1.464 | 2.599.966.121 |
| RETAIL | 1.400 | 2.439.292.126 |
| INDUSTRIAL | 1.454 | 2.197.864.560 |
| MINERIA | 911 | 1.360.516.886 |
| PUBLICO | 376 | 531.366.093 |
HAVING · 22 minCinco filas. Y ahí está otra vez el 1.360.516.886 de la clase pasada — el número correcto de Minería, ahora sin escribir las cinco variantes a mano.
«Tres funciones de texto —mayúsculas, sin tilde, sin espacios sobrantes— y los 23 cajones se volvieron 5. Guarden esa idea: **normalizar es agrupar por lo que el dato significa, no por cómo se escribió.**»
HAVING · 22 minY ahora se mira. Las dos consultas ya dieron el número: el gráfico no lo reemplaza, lo hace evidente. Es el primer gráfico del semestre y trae escrita la regla que se usa hasta el examen — la forma la decide el trabajo que hace el dato. Acá el trabajo es comparar magnitudes entre categorías con nombres largos, así que son barras horizontales ordenadas por valor, nunca alfabéticas: el orden alfabético no dice nada.
HAVING · 22 minimport matplotlib.pyplot as plt # La paleta del curso, y no cambia en todo el semestre: gris para todo, # UN color para lo único que se quiere mirar. GRIS, TINTA, ALERTA = "#b8b8b8", "#102a38", "#8c2b20" sucio = bd.sql(""" SELECT segmento AS etiqueta, upper(strip_accents(trim(segmento))) AS clave, SUM(cantidad * precio_unitario) / 1e6 AS venta_mm FROM ventas WHERE moneda = 'CLP' GROUP BY 1, 2 ORDER BY venta_mm""") limpio = bd.sql(""" SELECT upper(strip_accents(trim(segmento))) AS etiqueta, upper(strip_accents(trim(segmento))) AS clave, SUM(cantidad * precio_unitario) / 1e6 AS venta_mm FROM ventas WHERE moneda = 'CLP' GROUP BY 1, 2 ORDER BY venta_mm""") fig, ejes = plt.subplots(1, 2, figsize=(12, 6.4), sharex=True) for eje, d, rotulo, grosor in ((ejes[0], sucio, "Como lo escribió el ERP", 0.72), (ejes[1], limpio, "Como se llama el negocio", 0.45)): eje.barh(range(len(d)), d.venta_mm, height=grosor, color=[ALERTA if c == "MINERIA" else GRIS for c in d.clave]) eje.set_yticks(range(len(d))) eje.set_yticklabels([f"«{e}»" for e in d.etiqueta], fontsize=8.5) eje.set_title(f"{rotulo}: {len(d)} barras", fontsize=12, color=TINTA, loc="left") eje.set_xlabel("venta en millones de CLP", fontsize=9) for lado in ("top", "right", "left"): eje.spines[lado].set_visible(False) eje.tick_params(length=0) for y, v in enumerate(limpio.venta_mm): ejes[1].text(v + 30, y, f"{v:,.0f}".replace(",", "."), va="center", fontsize=9, color=TINTA) fig.suptitle("Los 23 cajones de la izquierda son 5 segmentos:\n" "en color, las cinco escrituras de Minería que a la derecha son una sola barra", fontsize=13, color=TINTA, x=0.02, ha="left") fig.tight_layout() plt.show()
HAVING · 22 minQué se mira en el resultado, en este orden y sin apurarse:
HAVING · 22 minMinería, MINERIA, MINERÍA, minería y una que además trae un espacio al final—; a la derecha son una sola, y mide exactamente lo que miden las cinco juntas.«Construcción ». Un carácter invisible partió un segmento en dos.HAVING · 22 minTicket promedio por canal — y la primera lección de grano del semestre:
HAVING · 22 minSELECT COUNT(*) AS lineas, COUNT(DISTINCT numero_factura) AS facturas, ROUND(COUNT(*)::DOUBLE / COUNT(DISTINCT numero_factura), 3) AS lineas_por_factura FROM ventas; -- 6000 | 2951 | 2.033
HAVING · 22 minDetenerse aquí. «6.000 líneas, 2.951 facturas. 2,03 líneas por factura. Anoten ese número: explica un crimen que vamos a resolver en la clase 09 y que cuesta 9,7 puntos de margen.»
HAVING · 22 minWITH factura AS ( SELECT numero_factura, ANY_VALUE(canal) AS canal, SUM(cantidad * precio_unitario) AS bruto FROM ventas WHERE moneda = 'CLP' GROUP BY numero_factura ) SELECT canal, COUNT(*) AS facturas, ROUND(AVG(bruto)) AS ticket_promedio_clp FROM factura GROUP BY canal ORDER BY ticket_promedio_clp DESC;
HAVING · 22 min| canal | facturas | ticket_promedio_clp |
|---|---|---|
| Distribuidor | 609 | 3.562.330 |
| Directo | 1.369 | 3.327.875 |
| E-commerce | 86 | 3.137.598 |
| Licitación | 694 | 3.074.717 |
HAVING · 22 min«Fíjense en lo que hicimos: primero agrupamos por factura, y después promediamos. Si promedian directo sobre las líneas, están promediando pedazos de factura y el "ticket" no es el ticket. Se llama grano, es la palabra más importante del semestre y vuelve en la clase 09.»
HAVING: filtrar después de agregar. WHERE filtra filas antes de repartirlas en cajones; HAVING filtra cajones ya armados.
HAVING · 22 minSELECT upper(strip_accents(trim(region_cliente))) AS region, COUNT(*) AS lineas, ROUND(SUM(cantidad * precio_unitario)) AS venta_clp FROM ventas WHERE moneda = 'CLP' GROUP BY 1 HAVING COUNT(*) > 500 ORDER BY venta_clp DESC; -- METROPOLITANA 1446 | 2.290.179.576 -- VALPARAISO 742 | 1.306.465.271 -- ANTOFAGASTA 658 | 985.557.458
HAVING · 22 minError clásico, provocado a propósito: escribir WHERE COUNT(*) > 500. Leer el mensaje de DuckDB en pantalla y traducirlo al español con ellos. El error es material didáctico, no un accidente.
Y la primera pasada al margen —la mitad de la pregunta del gerente:
HAVING · 22 minSELECT linea_negocio, ROUND(100.0 * (SUM(cantidad * precio_unitario) - SUM(cantidad * costo_unitario) - SUM(descuento_factura) - SUM(flete_despacho)) / SUM(cantidad * precio_unitario), 2) AS margen_aparente_pct FROM ventas GROUP BY linea_negocio ORDER BY margen_aparente_pct DESC;
HAVING · 22 min| linea_negocio | margen_aparente_pct |
|---|---|
| Herramientas | 21,76 |
| Seguridad | 20,79 |
| Eléctrico | 8,66 |
| Fijaciones | −13,22 |
«Según esto, cada peso que vendemos en Fijaciones nos hace perder trece centavos. ¿Qué haría un gerente con esta lámina?»
HAVING · 22 min«Esta consulta corrió sin error. Sumó cuatro columnas que existen. Y es la lámina con la que se cierran negocios rentables. Lo llamo el margen aparente y lo vamos a desarmar en la clase 09. Hoy sólo quiero que sepan que existe.»
Hilo 3 — salir de Excel: «Esto en Excel era una tabla dinámica. Y la tabla dinámica te habría dado las mismas 23 filas, con la misma cara de seguridad. El problema no es la lentitud de Excel: es que ninguna de las dos herramientas sabe que INDUSTRIAL e Industrial son lo mismo.»
Aquí se responde la pregunta colgada, corriéndola con ellos. Antes de correr, cobrar el pronóstico que anotaron a las 20:30.
SELECT COUNT(*) AS lineas, COUNT(DISTINCT cliente) AS formas_cliente, COUNT(DISTINCT rut_cliente) AS formas_rut, COUNT(DISTINCT segmento) AS formas_segmento, COUNT(DISTINCT region_cliente) AS formas_region FROM ventas; -- 6000 | 100 | 74 | 23 | 62
«Cien. Y hay dieciocho clientes. El RUT, que es el número que el Estado usa para identificar a una empresa, viene escrito de 74 formas para 18 RUT. Eso es lo que hay adentro del archivo con que se arma el reporte de ventas.»
Ahora el mismo problema donde duele, cliente por cliente:
SELECT cliente, COUNT(*) AS lineas FROM ventas WHERE upper(strip_accents(cliente)) LIKE '%LOS ANDES%' GROUP BY cliente ORDER BY lineas DESC;
| cliente | lineas |
|---|---|
Ferretería Los Andes Ltda. | 235 |
FERRETERÍA LOS ANDES LTDA. | 69 |
Ferretería Los Andes Ltda. (espacio delante) | 59 |
Ferretería Los Andes LTDA | 58 |
Ferretería Los Andes Ltda.. (punto de más) | 35 |
Un cliente. Cinco filas en el reporte. Ninguna con el total.
Y el intento de arreglarlo sólo con texto, corrido en vivo:
SELECT COUNT(DISTINCT cliente) AS crudo, COUNT(DISTINCT upper(trim(cliente))) AS mas_caja_y_bordes, COUNT(DISTINCT upper(regexp_replace( trim(cliente), ' +', ' ', 'g'))) AS mas_espacios_internos, COUNT(DISTINCT upper(replace(replace(replace( rut_cliente,'.',''),'-',''),' ',''))) AS por_rut_normalizado FROM ventas; -- 100 | 64 | 46 | 18
La cascada, escrita en la pizarra: 100 → 64 → 46 → … y ahí se atasca.
«Mayúsculas y espacios te llevan de 100 a 46. De 46 a 18 no llegas limpiando texto: llegas adivinando siSAes lo mismo queS.A., si el punto de más es un error de dedo, si "Ferretería" con y sin tilde es la misma empresa. Cada adivinanza es una apuesta. Y hay una columna en el archivo que no requiere adivinar ninguna: el RUT. Normalizado, cierra en 18 de una sola pasada.»
SELECT COUNT(DISTINCT rut_cliente) AS formas_crudas, COUNT(DISTINCT upper(replace(replace(replace( rut_cliente,'.',''),'-',''),' ',''))) AS ruts_reales FROM ventas; -- 74 | 18
Regla que se copia: para agrupar personas o empresas, agrupa por la llave, muestra el nombre. El nombre es para leer; el RUT es para contar.
Los cinco clientes más grandes, ya bien contados:
SELECT upper(replace(replace(replace( rut_cliente,'.',''),'-',''),' ','')) AS rut_norm, ANY_VALUE(cliente) AS un_nombre_cualquiera, COUNT(DISTINCT cliente) AS formas_de_escritura, ROUND(SUM(cantidad * precio_unitario) / 1e6, 1) AS venta_MM_clp FROM ventas WHERE moneda = 'CLP' GROUP BY 1 ORDER BY venta_MM_clp DESC LIMIT 5;
| rut_norm | un_nombre_cualquiera | formas | venta_MM_clp |
|---|---|---|---|
| 764128859 | Constructora Vilches S.A. | 6 | 1.340,6 |
| 772039141 | Ferretería Los Andes LTDA | 6 | 1.012,4 |
| 96874230K | Minera Altos del Loa SpA | 5 | 985,6 |
| 781156602 | COMERCIAL ÑUBLE SPA | 5 | 710,9 |
| 763304828 | CONSTRUCTORA AYSÉN LTDA. | 6 | 683,9 |
«Ninguno de estos cinco clientes aparecía completo en el reporte anterior. El más grande de la cartera estaba partido en seis pedazos.»
Pararse, estirar, subir la luz un segundo. Clase nocturna: el cuerpo también aprende.
Éste es el bloque que convierte dos sesiones en una clase. Se dicta de pie, sin proyectar SQL.
«Entonces el archivo no se puede arreglar solo. Mayúsculas y espacios te dejan en 46 y ahí se atasca: para llegar a 18 tienes que salir del archivo. ¿Y si le pegamos el maestro de clientes?»
Recoger tres respuestas de la sala en 90 segundos, sin corregir ninguna. Después:
«Es exactamente lo que haría cualquiera, y es lo que vamos a hacer en los próximos cuarenta minutos. Les adelanto una sola cosa, y quiero que la anoten antes de que la veamos: entran 6.000 líneas y salen 6.195. El resto de la clase es averiguar de dónde salieron las 195 que nadie vendió.»
JOIN · 16 minProyectar el JSON en un editor de texto, no en el navegador. Que vean la forma.
{ "rut": "76.412.885-9", "razon_social": "Constructora Vilches S.A.",
"segmento": "Construcción", "region": "Metropolitana",
"limite_credito_clp": 236000000 }
JOIN · 16 minJOIN · 16 minLeerlo con SQL, sin abrir nada más:
JOIN · 16 minCREATE OR REPLACE VIEW maestro AS SELECT * FROM ( SELECT unnest(clientes, recursive := true) FROM read_json_auto( 'datos/finanzas/catalogo_clientes.json')); SELECT COUNT(*) AS entradas, COUNT(DISTINCT rut) AS ruts FROM maestro; -- 18 | 17
JOIN · 16 minDetenerse. «Dieciocho entradas. Diecisiete RUT. Antes de unir nada: ahí ya hay un problema, y está del lado del maestro, no del nuestro.»
El JOIN en una frase: pegar a cada línea de venta la información del cliente que la generó, usando el RUT como puente.
Primero, la versión ingenua —la que casi todo el mundo escribe:
JOIN · 16 minSELECT COUNT(*) AS lineas_pegadas FROM ventas v JOIN maestro m ON v.rut_cliente = m.rut; -- 3641
«3.641 de 6.000. El 60,7 %. Y la consulta corrió sin error, sin advertencia y en medio segundo. Si mandas ese reporte, le acabas de decir a Finanzas que la empresa vendió el 39 % menos de lo que vendió.»
JOIN · 16 minPreguntar: «¿por qué no pegó?» Van a llegar solos: el RUT viene escrito de 74 formas y el maestro lo escribe de una.
La cura, con la misma normalización de recién aplicada a los dos lados:
JOIN · 16 minCREATE OR REPLACE VIEW ventas_n AS SELECT *, upper(replace(replace(replace( rut_cliente,'.',''),'-',''),' ','')) AS rut_norm FROM ventas; CREATE OR REPLACE VIEW maestro_n AS SELECT *, upper(replace(replace( rut,'.',''),'-','')) AS rut_norm FROM maestro; SELECT COUNT(*) FROM ventas_n JOIN maestro_n USING (rut_norm); -- 6195
JOIN · 16 minEscribir el número en la pizarra, grande:
Entraron 6.000 líneas. Salieron 6.195.
«ElJOINinventó 195 líneas de venta. No están en el archivo de ventas, no las facturó nadie, y aparecerían en el reporte con su venta, su costo y su margen. UnJOINque cambia el conteo de filas sin que nadie lo note es la forma más común de mentir con datos sin querer.»
JOIN · 16 minLa consulta de control que hay que correr SIEMPRE después de un JOIN —se copia, se pega en el cuaderno, se usa el resto del semestre:
JOIN · 16 minSELECT (SELECT COUNT(*) FROM ventas_n) AS lineas_origen, (SELECT COUNT(*) FROM ventas v JOIN maestro m ON v.rut_cliente = m.rut) AS join_sin_normalizar, (SELECT COUNT(*) FROM ventas_n JOIN maestro_n USING (rut_norm)) AS join_normalizado, (SELECT COUNT(*) FROM ventas_n LEFT JOIN maestro_n USING (rut_norm) WHERE maestro_n.rut IS NULL) AS lineas_sin_maestro, (SELECT COUNT(*) FROM ventas_n JOIN maestro_n USING (rut_norm) WHERE maestro_n.region IS NULL) AS lineas_sin_region; -- 6000 | 3641 | 6195 | 99 | 774
JOIN · 16 minLEFT vs INNER, en lámina y en una sola idea:
| Qué hace con lo que no calza | Cuándo se usa | |
|---|---|---|
INNER JOIN | lo borra en silencio | cuando ya verificaste que no falta nada |
LEFT JOIN | lo conserva con NULL — lo VES | siempre, para diagnosticar |
JOIN · 16 min«Regla del curso: para diagnosticar,LEFT. Siempre. ElINNERes una decisión que se toma después de mirar, no antes.»
Hilo 3 — salir de Excel: «Esto en Excel era un BUSCARV. Y hay una diferencia que hoy importa: BUSCARV devuelve la primera coincidencia y sigue. Con el cliente duplicado del maestro habrían obtenido 6.000 filas —el conteo cuadra, se ve perfecto— y a la mitad de las líneas de Punta Verde les habría pegado el segmento equivocado, sin duplicar nada y sin avisar nunca. El JOIN al menos te muestra el problema cambiando el conteo. Un error visible es infinitamente mejor que uno invisible.»
Cada trampa se corre en vivo, se mide y se le pone nombre. Es el corazón de la clase y no se salta: es el corazón de RA2.
Ya está medida arriba. Nombre: fallo de llave. Se ve porque el conteo baja.
SELECT rut, COUNT(*) AS entradas, string_agg(segmento, ' | ') AS segmentos FROM maestro GROUP BY rut HAVING COUNT(*) > 1; -- 96.701.443-5 | 2 | Minería | Industrial
SELECT (SELECT COUNT(*) FROM ventas_n WHERE rut_norm = '967014435') AS lineas_reales, (SELECT COUNT(*) FROM ventas_n JOIN maestro_n USING (rut_norm) WHERE rut_norm = '967014435') AS lineas_tras_join; -- 294 | 588
«Minera Punta Verde tiene 294 líneas de venta. Después del JOIN tiene 588. El cliente factura el doble sin haber vendido una unidad más, porque alguien la cargó dos veces en el maestro: una como Minería y otra como Industrial. Ésas son las 195 líneas inventadas (294 duplicadas menos 99 que se pierden, y las 99 vienen ahora).»
Nombre: fan-out. Se ve porque el conteo sube.
Ésta no se proyecta primero: la corren ellos. Son 3 minutos y salen del tiempo de la Trampa 4, que es la más corta.
«Antes de que yo lo proyecte: corran ustedes elLEFT JOINcontra el maestro conWHERE maestro_n.rut IS NULLy díganme cuántas líneas les da. La vistaventas_ny la vistamaestro_nya están creadas: es una consulta de cuatro líneas.»
SELECT ANY_VALUE(cliente) AS cliente, COUNT(*) AS lineas, COUNT(DISTINCT numero_factura) AS facturas, ROUND(SUM(cantidad * precio_unitario)) AS venta_nominal_clp FROM ventas_n LEFT JOIN maestro_n USING (rut_norm) WHERE maestro_n.rut IS NULL GROUP BY rut_norm; -- Salmonera Chiloé SpA | 99 | 49 | 85.343.323
«Salmonera Chiloé SpA. 99 líneas, 49 facturas, 85,3 millones. No está en el maestro de clientes. ElINNER JOINla borra y nadie se entera. ElLEFT JOINla deja con segmento nulo y aparece en el reporte como "(en blanco)". Ninguna de las dos cosas es aceptable.»
SELECT maestro_n.razon_social, COUNT(*) AS lineas FROM ventas_n JOIN maestro_n USING (rut_norm) WHERE maestro_n.region IS NULL GROUP BY 1 ORDER BY 2 DESC; -- Comercial Ñuble SpA | 415 -- Constructora Aysén Ltda. | 359
«774 líneas. Cuando Finanzas abra la cartera por región, esas 774 líneas se caen del gráfico sin avisar. El total del gráfico no va a cuadrar con el total del estado de resultados, y nadie va a saber por qué.»
Hilo 2: «Este maestro es exactamente lo que exporta un ERP chileno. La entrada duplicada de Punta Verde no es un invento mío: es lo que pasa cuando un cliente cambia de clasificación y alguien crea una ficha nueva en vez de editar la vieja. Van a ver esto en su primera semana de trabajo.»
Este catálogo es más chico y es más grave: define cuánto crédito puede dar la empresa.
CREATE OR REPLACE VIEW condiciones AS SELECT * FROM ( SELECT unnest(condiciones, recursive := true) FROM read_json_auto( 'datos/finanzas/catalogo_condiciones_pago.json')); SELECT * FROM condiciones ORDER BY dias;
| dias | nombre | vigente | costo_financiero_anual_pct |
|---|---|---|---|
| 0 | Contado | true | 0,0 |
| 30 | Crédito 30 | true | 9,6 |
| 45 | Crédito 45 | false | 10,2 |
| 60 | Crédito 60 | true | 10,8 |
| 90 | Crédito 90 | true | 12,4 |
Ahora, contra lo que el ERP realmente emitió:
SELECT v.condicion_pago_dias AS dias, COUNT(*) AS lineas, ANY_VALUE(c.nombre) AS nombre_en_catalogo, ANY_VALUE(c.vigente) AS vigente FROM ventas v LEFT JOIN condiciones c ON v.condicion_pago_dias = c.dias GROUP BY v.condicion_pago_dias ORDER BY dias;
| dias | lineas | nombre_en_catalogo | vigente |
|---|---|---|---|
| 0 | 598 | Contado | true |
| 30 | 1.961 | Crédito 30 | true |
| 45 | 233 | Crédito 45 | false |
| 60 | 1.828 | Crédito 60 | true |
| 90 | 1.245 | Crédito 90 | true |
| 120 | 135 | (null) | (null) |
368 líneas fuera del catálogo vigente. Y son dos fallas distintas —esto es lo que hay que dejar clavado:
vigente: false. El JOIN pega perfecto y no dice nada. Sólo la ves si miras la columna vigente. Es un problema de gobierno del catálogo: existió, se retiró, y el ERP la sigue emitiendo.INNER JOIN la borra; el LEFT JOIN la deja con nombre nulo. Es un problema de cobertura del catálogo.«La primera es peor, porque no se cae. Un INNER JOIN te avisa cuando algo falta. Nada te avisa cuando algo calza pero ya no vale.»
Y la pregunta de negocio, que es de Finanzas y no de sintaxis:
SELECT condicion_pago_dias AS dias, COUNT(*) AS lineas, ROUND(SUM(cantidad * precio_unitario)) AS venta_clp FROM ventas WHERE condicion_pago_dias IN (45, 120) AND moneda = 'CLP' GROUP BY 1 ORDER BY 1; -- 45 | 218 | 277.352.802 -- 120 | 128 | 260.974.516
«538 millones de pesos de crédito que Finanzas nunca autorizó, dados a 45 y 120 días. Y el catálogo trae el costo_financiero_anual_pct justamente para poder preguntar: ¿cuánto nos cuesta el crédito que nadie aprobó? → eso se le pregunta a Tesorería y al Gerente de Administración y Finanzas.»
Los dos roles en la misma lámina. Las dudas concretas que salen del dato de hoy:
KAM: «77.203.914-1factura comoFerretería Los Andes Ltda.y también comoFerretería Los Andes LTDA. Mismo RUT, dos escrituras. ¿Es el mismo cliente? Y el RUT96.701.443-5está cargado dos veces: ¿es Minería o es Industrial?»
Administración de Ventas: «¿Por qué Salmonera Chiloé no está en el maestro, si nos compra hace dos años y le facturamos 85 millones?»
| Duda que salió del dato de hoy | El rol que tiene la respuesta |
|---|---|
| ¿Este cliente y este otro son el mismo, o son dos razones sociales? | Ejecutivo comercial (KAM) de la cuenta |
| ¿Punta Verde es Minería o Industrial? | Ejecutivo comercial (KAM) de la cuenta |
| ¿Por qué Salmonera Chiloé no está en el maestro? | Administración de Ventas, dueña del maestro (y Cuentas por Cobrar, que le cobra todos los meses: sabe que existe) |
| ¿Comercial Ñuble no tiene región o nadie la llenó? | Administración de Ventas |
«Cuatro preguntas. Ninguna se responde con SQL. Y las cuatro tienen que estar contestadas antes de que ese reporte salga de tu computador. Ninguna toma más de un correo.»
Y la regla del curso, que ya está en la pizarra desde la clase 02, se repite en voz alta:
Una consulta SQL sin una conversación previa es una opinión con formato de tabla.
Síntesis, construida con ellos, escrita en la pizarra con sus palabras:
GROUP BY en una frase que entienda tu jefa?» → parte la tabla en cajones y resume cada cajón.JOIN?» → el conteo de filas: antes y después.Ticket TC03 (3 min, se proyecta): a) ¿Por qué pegar el maestro NO arregla las 100 formas de escribir 18 clientes? Una frase. b) Uniste por rut_cliente = rut sin normalizar y pegaron 3.641 de 6.000 líneas; el GROUP BY por cliente sigue dando 100 grupos. Escribe el arreglo que va en la fuente y el que va en el maestro, en ese orden. c) 1–5 y qué te falta.
Anki: se importa hoy el mazo S1–S2 (mostrar el import en 1 minuto, no más).
Pregunta que queda planteada (se escribe en la pizarra y no se responde hoy):
→ El RUT sin normalizar pega 3.641 de 6.000 líneas. ¿De dónde saco lo que falta, y con qué fuente lo valido?
Puente a la clase 04, corto:
«La próxima salimos del edificio: CMF, Banco Central, SII, INE, Bolsa de Santiago. Y van a aprender a validar un RUT antes de ir a buscarlo afuera, con una regla aritmética que se explica en dos frases.»
Tres minutos. No suma nota. Sirve para armar la apertura de la próxima clase.
El notebook de esta clase:
https://gaa1104.vercel.app/notebooks/clase-03.ipynb