<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Ingeniería De Datos el josiete.com</title><link>https://www.josiete.com/tags/ingenier%C3%ADa-de-datos/</link><description>Contenido reciente en Ingeniería De Datos el josiete.com</description><generator>Hugo</generator><language>es-es</language><lastBuildDate>Fri, 25 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.josiete.com/tags/ingenier%C3%ADa-de-datos/index.xml" rel="self" type="application/rss+xml"/><item><title>PIVOT en SQL Server: de lecturas de sensores a una tabla por instante</title><link>https://www.josiete.com/posts/pivot-sql-server/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://www.josiete.com/posts/pivot-sql-server/</guid><description>&lt;p&gt;&lt;img src="https://www.josiete.com/images/sql-server-pivot.webp" alt="Lecturas de temperatura, humedad y presión se reorganizan desde filas por métrica hacia columnas"&gt;&lt;/p&gt;&#10;&lt;p&gt;Imagina que recibes lecturas de sensores cada cinco minutos. Cada mensaje contiene un instante, el sensor que lo envió, el nombre de una métrica y su valor. Guardar los mensajes tal como llegan es cómodo: si mañana aparece una métrica nueva, basta con recibir más filas. Pero el informe que consume esos datos pide otra cosa: &lt;strong&gt;una fila por instante y sensor&lt;/strong&gt;, con temperatura, humedad y presión en columnas separadas.&lt;/p&gt;&#10;&lt;p&gt;Ese paso de filas a columnas es &lt;em&gt;pivotar&lt;/em&gt; los datos. Es habitual en ingeniería de datos y en series temporales: el formato largo facilita la ingesta de eventos y el formato ancho sirve para ciertos informes, exportaciones, modelos y consumidores que esperan un conjunto fijo de variables por observación.&lt;/p&gt;&#10;&lt;h2 id="de-lecturas-a-una-tabla-de-ejemplo"&gt;De lecturas a una tabla de ejemplo&lt;/h2&gt;&#10;&lt;p&gt;En el formato largo, cada fila representa una lectura. Usaremos &lt;code&gt;observed_at&lt;/code&gt; para el timestamp, &lt;code&gt;sensor_id&lt;/code&gt; para el dispositivo, &lt;code&gt;metric&lt;/code&gt; para el tipo de medida y &lt;code&gt;metric_value&lt;/code&gt; para el valor. En este ejemplo interpretamos los instantes como UTC y los valores como grados Celsius, porcentaje de humedad y hectopascales, según la métrica. El tipo &lt;code&gt;DATETIME2&lt;/code&gt; por sí solo no registra la zona horaria.&lt;/p&gt;&#10;&lt;p&gt;Puedes reproducir el ejemplo en SQL Server con estas doce filas:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; sensor_readings (&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; observed_at DATETIME2(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sensor_id VARCHAR(&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; metric VARCHAR(&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; metric_value DECIMAL(&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;INSERT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INTO&lt;/span&gt; sensor_readings&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (observed_at, sensor_id, metric, metric_value)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;VALUES&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:00:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-01&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;temperature&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:00:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-01&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;temperature&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:00:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-01&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;humidity&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;45&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:00:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-01&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;pressure&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1012&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:00:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-02&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;temperature&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:00:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-02&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;humidity&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:00:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-02&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;pressure&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1011&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:05:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-01&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;temperature&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;21&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:05:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-01&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;humidity&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;44&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:05:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-01&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;pressure&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1012&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:05:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-02&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;temperature&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#39;2026-09-25T10:05:00&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;S-02&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;pressure&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1011&lt;/span&gt;.&lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;);&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;La tabla no impone una clave única sobre instante, sensor y métrica: queremos conservar dos temperaturas para &lt;code&gt;S-01&lt;/code&gt; a las 10:00 y decidir explícitamente qué hacer con ellas. En un sistema real también habría que distinguir una segunda medición de un mensaje retransmitido.&lt;/p&gt;&#10;&lt;p&gt;Veamos primero los datos tal como están almacenados:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; observed_at, sensor_id, metric, metric_value&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; sensor_readings&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ORDER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; observed_at, sensor_id, metric, metric_value;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Encontrarás dos filas &lt;code&gt;temperature&lt;/code&gt; para &lt;code&gt;S-01&lt;/code&gt; a las 10:00, y ninguna fila &lt;code&gt;humidity&lt;/code&gt; para &lt;code&gt;S-02&lt;/code&gt; a las 10:05. Esa ausencia no equivale a una lectura de humedad igual a cero.&lt;/p&gt;&#10;&lt;h2 id="el-resultado-que-necesitamos"&gt;El resultado que necesitamos&lt;/h2&gt;&#10;&lt;p&gt;Antes de escribir la consulta, hay que fijar su &lt;strong&gt;granularidad&lt;/strong&gt;: cada fila del resultado representará una combinación de &lt;code&gt;observed_at&lt;/code&gt; y &lt;code&gt;sensor_id&lt;/code&gt;. Queremos obtener esto (valores mostrados con dos decimales):&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;observed_at&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;sensor_id&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: right"&gt;temperature&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: right"&gt;humidity&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: right"&gt;pressure&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2026-09-25 10:00:00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;S-01&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;21.00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;45.00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;1012.00&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2026-09-25 10:00:00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;S-02&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;19.00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;50.00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;1011.00&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2026-09-25 10:05:00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;S-01&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;21.50&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;44.00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;1012.50&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2026-09-25 10:05:00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;S-02&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;19.50&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;NULL&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;1011.50&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;El &lt;code&gt;21.00&lt;/code&gt; procede de la media de &lt;code&gt;20.00&lt;/code&gt; y &lt;code&gt;22.00&lt;/code&gt;. El &lt;code&gt;NULL&lt;/code&gt; indica que no hay humedad registrada para esa combinación de instante y sensor. El motor puede mostrar más decimales en los resultados de &lt;code&gt;AVG&lt;/code&gt;; la tabla anterior solo facilita la lectura.&lt;/p&gt;&#10;&lt;h2 id="resolverlo-con-pivot-en-sql-server"&gt;Resolverlo con PIVOT en SQL Server&lt;/h2&gt;&#10;&lt;p&gt;SQL Server ofrece el operador &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/from-using-pivot-and-unpivot?view=sql-server-ver17"&gt;&lt;code&gt;PIVOT&lt;/code&gt;&lt;/a&gt; para convertir valores de una columna en columnas del resultado mientras agrega las lecturas correspondientes:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; observed_at,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sensor_id,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [temperature],&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [humidity],&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [pressure]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; (&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; observed_at, sensor_id, metric, metric_value&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; sensor_readings&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; readings&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PIVOT (&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;AVG&lt;/span&gt;(metric_value)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;FOR&lt;/span&gt; metric &lt;span style="color:#66d9ef"&gt;IN&lt;/span&gt; ([temperature], [humidity], [pressure])&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; wide_readings&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ORDER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; observed_at, sensor_id;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Se lee de dentro hacia fuera:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;La subconsulta &lt;code&gt;readings&lt;/code&gt; entrega &lt;strong&gt;solo&lt;/strong&gt; las dos dimensiones, la métrica y su valor.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;FOR metric&lt;/code&gt; indica que los valores de &lt;code&gt;metric&lt;/code&gt; se convertirán en encabezados.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;IN (...)&lt;/code&gt; enumera las columnas que queremos crear. Los corchetes son la forma de delimitar identificadores en T-SQL; la lista no se descubre automáticamente.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;AVG(metric_value)&lt;/code&gt; decide qué valor colocar en cada celda si llegan varias lecturas para la misma métrica, instante y sensor.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;wide_readings&lt;/code&gt; es el alias obligatorio de la tabla resultante; el &lt;code&gt;ORDER BY&lt;/code&gt; presenta las filas en orden temporal.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;La subconsulta también protege la granularidad. &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/from-transact-sql?view=sql-server-ver17#pivot-clause"&gt;SQL Server agrupa por las columnas de entrada que no son ni la columna pivotada ni la agregada&lt;/a&gt;. Si pasáramos además un identificador de evento o una fecha de ingesta, podríamos obtener varias filas para el mismo instante y sensor. Si, por el contrario, quitáramos &lt;code&gt;sensor_id&lt;/code&gt;, mezclaríamos las lecturas de ambos sensores.&lt;/p&gt;&#10;&lt;p&gt;La agregación no es un adorno sintáctico: el motor necesita una regla para reducir varias filas a una celda. Aquí elegimos la media para mostrarlo, pero esa decisión depende del significado de las mediciones. Si las dos temperaturas fueran retransmisiones del mismo evento, habría que deduplicar antes; si interesa la lectura más reciente, necesitaríamos una marca temporal o un identificador adicional y una regla para escogerla. En ninguno de esos casos &lt;code&gt;AVG&lt;/code&gt; sería automáticamente la operación adecuada.&lt;/p&gt;&#10;&lt;h2 id="la-misma-transformación-con-case-y-group-by"&gt;La misma transformación con CASE y GROUP BY&lt;/h2&gt;&#10;&lt;p&gt;Podemos expresar la misma idea con agregación condicional, sin &lt;code&gt;PIVOT&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; observed_at,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sensor_id,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;AVG&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;CASE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;WHEN&lt;/span&gt; metric &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;temperature&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;THEN&lt;/span&gt; metric_value &lt;span style="color:#66d9ef"&gt;END&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; temperature,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;AVG&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;CASE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;WHEN&lt;/span&gt; metric &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;humidity&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;THEN&lt;/span&gt; metric_value &lt;span style="color:#66d9ef"&gt;END&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; humidity,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;AVG&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;CASE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;WHEN&lt;/span&gt; metric &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;pressure&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;THEN&lt;/span&gt; metric_value &lt;span style="color:#66d9ef"&gt;END&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; pressure&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; sensor_readings&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;GROUP&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; observed_at, sensor_id&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ORDER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; observed_at, sensor_id;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;GROUP BY&lt;/code&gt; forma una fila por instante y sensor. Cada &lt;code&gt;CASE&lt;/code&gt; deja pasar solo los valores de una métrica y devuelve &lt;code&gt;NULL&lt;/code&gt; para las demás; &lt;code&gt;AVG&lt;/code&gt; ignora esos &lt;code&gt;NULL&lt;/code&gt;. Para &lt;code&gt;S-01&lt;/code&gt; a las 10:00 calcula la media de las dos temperaturas. Para la humedad ausente de &lt;code&gt;S-02&lt;/code&gt; a las 10:05 no encuentra ningún valor y devuelve &lt;code&gt;NULL&lt;/code&gt;. Es una alternativa útil cuando queremos ver la lógica de cada columna de forma explícita o escribir SQL portable.&lt;/p&gt;&#10;&lt;h2 id="y-en-mysql"&gt;¿Y en MySQL?&lt;/h2&gt;&#10;&lt;p&gt;MySQL no incorpora el operador &lt;code&gt;PIVOT&lt;/code&gt; de T-SQL. Podemos crear la misma tabla cambiando el tipo temporal y reutilizando el &lt;code&gt;INSERT&lt;/code&gt; anterior; el formato de fecha con &lt;code&gt;T&lt;/code&gt; también es válido para MySQL:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; sensor_readings (&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; observed_at DATETIME &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sensor_id VARCHAR(&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; metric VARCHAR(&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; metric_value DECIMAL(&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;La consulta es la agregación condicional que acabamos de ver:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; observed_at,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sensor_id,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;AVG&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;CASE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;WHEN&lt;/span&gt; metric &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;temperature&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;THEN&lt;/span&gt; metric_value &lt;span style="color:#66d9ef"&gt;END&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; temperature,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;AVG&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;CASE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;WHEN&lt;/span&gt; metric &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;humidity&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;THEN&lt;/span&gt; metric_value &lt;span style="color:#66d9ef"&gt;END&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; humidity,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;AVG&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;CASE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;WHEN&lt;/span&gt; metric &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;pressure&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;THEN&lt;/span&gt; metric_value &lt;span style="color:#66d9ef"&gt;END&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; pressure&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; sensor_readings&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;GROUP&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; observed_at, sensor_id&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ORDER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; observed_at, sensor_id;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;En MySQL, &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/flow-control-functions.html"&gt;&lt;code&gt;CASE WHEN&lt;/code&gt; sin &lt;code&gt;ELSE&lt;/code&gt; devuelve &lt;code&gt;NULL&lt;/code&gt; cuando no se cumple la condición&lt;/a&gt;, y &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/aggregate-functions.html"&gt;&lt;code&gt;AVG&lt;/code&gt; ignora los valores nulos&lt;/a&gt;. Por eso produce el mismo resultado lógico. La sintaxis concreta de las fechas y el tipo de columna cambian entre motores, pero la idea de agrupar dimensiones y calcular cada métrica por separado se mantiene.&lt;/p&gt;&#10;&lt;h2 id="cuando-las-métricas-cambian"&gt;Cuando las métricas cambian&lt;/h2&gt;&#10;&lt;p&gt;Este es un &lt;strong&gt;pivot estático&lt;/strong&gt;: hemos escrito &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;humidity&lt;/code&gt; y &lt;code&gt;pressure&lt;/code&gt; en la consulta. Si llega &lt;code&gt;battery_level&lt;/code&gt;, no aparecerá como columna hasta que cambiemos el SQL. Lo mismo sucede con la solución mediante &lt;code&gt;CASE&lt;/code&gt;: cada columna nueva requiere una expresión nueva.&lt;/p&gt;&#10;&lt;p&gt;Cuando las métricas no se conocen de antemano, se puede construir la lista de columnas y la consulta mediante SQL dinámico: &lt;em&gt;dynamic pivot&lt;/em&gt;. En SQL Server eso exige tratar con cuidado los identificadores y los valores de entrada. Merece un artículo aparte. Además, un esquema de salida que cambia en cada ejecución puede complicar a los consumidores; a veces es preferible conservar el formato largo.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;UNPIVOT&lt;/code&gt; es la operación relacionada que lleva columnas de vuelta a filas. No deshace exactamente este ejemplo: el promedio &lt;code&gt;21.00&lt;/code&gt; ya no conserva por separado las lecturas &lt;code&gt;20.00&lt;/code&gt; y &lt;code&gt;22.00&lt;/code&gt;, y &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/queries/from-using-pivot-and-unpivot?view=sql-server-ver17#unpivot-example"&gt;los &lt;code&gt;NULL&lt;/code&gt; pueden desaparecer al despivotar&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="en-la-base-de-datos-o-fuera-de-ella"&gt;¿En la base de datos o fuera de ella?&lt;/h2&gt;&#10;&lt;p&gt;Un pipeline puede recibir telemetría en formato largo, almacenarla así y entregar una vista ancha a un informe o a un consumidor concreto. Si los datos ya están en SQL Server o MySQL, resolver esa transformación en el motor puede evitar extraer muchas filas, transferirlas, reconstruir la tabla en Python y volver a escribirla. Conocer bien SQL permite tomar esa decisión, en vez de recurrir automáticamente a un script.&lt;/p&gt;&#10;&lt;p&gt;Eso no significa que el SQL sea siempre más rápido. Importan el volumen, los índices, el plan de consulta, la memoria disponible, la arquitectura del pipeline y la complejidad de la transformación. Para una regla de negocio difícil de expresar o para datos que ya se procesan fuera de la base, otra herramienta puede encajar mejor. Lo esencial es decidir primero &lt;strong&gt;qué representa una fila y cómo se resuelven las lecturas repetidas&lt;/strong&gt;; después viene la elección del operador.&lt;/p&gt;&#10;</description></item></channel></rss>