<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>SQL Server on josiete.com</title><link>https://www.josiete.com/en/tags/sql-server/</link><description>Recent content in SQL Server on josiete.com</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 25 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.josiete.com/en/tags/sql-server/index.xml" rel="self" type="application/rss+xml"/><item><title>SQL Server PIVOT: turning sensor readings into one row per instant</title><link>https://www.josiete.com/en/posts/pivot-sql-server/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://www.josiete.com/en/posts/pivot-sql-server/</guid><description>&lt;p&gt;&lt;img src="https://www.josiete.com/images/sql-server-pivot.webp" alt="Temperature, humidity, and pressure readings move from one row per metric into separate columns"&gt;&lt;/p&gt;&#10;&lt;p&gt;Imagine receiving sensor readings every five minutes. Each message contains a time, a sensor ID, a metric name, and a value. Storing messages as they arrive is convenient: if a new metric appears tomorrow, it simply adds more rows. But a report consuming the data wants something else: &lt;strong&gt;one row per time and sensor&lt;/strong&gt;, with temperature, humidity, and pressure in separate columns.&lt;/p&gt;&#10;&lt;p&gt;Turning those rows into columns is &lt;em&gt;pivoting&lt;/em&gt; the data. It comes up often in data engineering and time series work: long format is convenient for ingesting events, while wide format suits certain reports, exports, models, and consumers that expect a fixed set of variables per observation.&lt;/p&gt;&#10;&lt;h2 id="from-readings-to-a-sample-table"&gt;From readings to a sample table&lt;/h2&gt;&#10;&lt;p&gt;In long format, each row is a reading. We will use &lt;code&gt;observed_at&lt;/code&gt; for the timestamp, &lt;code&gt;sensor_id&lt;/code&gt; for the device, &lt;code&gt;metric&lt;/code&gt; for the measurement type, and &lt;code&gt;metric_value&lt;/code&gt; for the value. In this example, we interpret the times as UTC and the values as degrees Celsius, percent humidity, and hectopascals, depending on the metric. The &lt;code&gt;DATETIME2&lt;/code&gt; type does not itself record a time zone.&lt;/p&gt;&#10;&lt;p&gt;You can reproduce the example in SQL Server with these twelve rows:&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;The table does not enforce a unique key across time, sensor, and metric: we want to keep two temperatures for &lt;code&gt;S-01&lt;/code&gt; at 10:00 and explicitly decide what to do with them. A production system would also need to distinguish a second measurement from a retransmitted message.&lt;/p&gt;&#10;&lt;p&gt;First, inspect the data as stored:&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;You will find two &lt;code&gt;temperature&lt;/code&gt; rows for &lt;code&gt;S-01&lt;/code&gt; at 10:00 and no &lt;code&gt;humidity&lt;/code&gt; row for &lt;code&gt;S-02&lt;/code&gt; at 10:05. A missing reading is not the same thing as a humidity reading of zero.&lt;/p&gt;&#10;&lt;h2 id="the-result-we-want"&gt;The result we want&lt;/h2&gt;&#10;&lt;p&gt;Before writing the query, we need to fix its &lt;strong&gt;grain&lt;/strong&gt;: each output row will represent one &lt;code&gt;observed_at&lt;/code&gt; and &lt;code&gt;sensor_id&lt;/code&gt; pair. We want this result (values shown to two decimal places):&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;The &lt;code&gt;21.00&lt;/code&gt; is the average of &lt;code&gt;20.00&lt;/code&gt; and &lt;code&gt;22.00&lt;/code&gt;. The &lt;code&gt;NULL&lt;/code&gt; means that no humidity reading exists for that time and sensor. The database may display more decimal places for an &lt;code&gt;AVG&lt;/code&gt; result; the table above is formatted for readability.&lt;/p&gt;&#10;&lt;h2 id="solving-it-with-pivot-in-sql-server"&gt;Solving it with PIVOT in SQL Server&lt;/h2&gt;&#10;&lt;p&gt;SQL Server provides the &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; operator&lt;/a&gt; to turn values from one column into result columns while aggregating the corresponding readings:&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;Read it from the inside out:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;The &lt;code&gt;readings&lt;/code&gt; subquery supplies &lt;strong&gt;only&lt;/strong&gt; the two dimensions, the metric, and its value.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;FOR metric&lt;/code&gt; says that values of &lt;code&gt;metric&lt;/code&gt; will become column headings.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;IN (...)&lt;/code&gt; lists the columns to create. Square brackets delimit identifiers in T-SQL; the list is not discovered automatically.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;AVG(metric_value)&lt;/code&gt; decides what to put in a cell when multiple readings exist for the same metric, time, and sensor.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;wide_readings&lt;/code&gt; is the required alias for the resulting table; &lt;code&gt;ORDER BY&lt;/code&gt; displays the rows in time order.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The subquery also protects the grain. &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 groups by input columns other than the pivot column and the aggregated value&lt;/a&gt;. Passing an event ID or ingestion time through as well could produce multiple rows for the same time and sensor. Dropping &lt;code&gt;sensor_id&lt;/code&gt;, on the other hand, would mix readings from both devices.&lt;/p&gt;&#10;&lt;p&gt;Aggregation is not just a syntax requirement: the engine needs a rule to reduce several rows to one cell. We chose the average to make this visible, but the correct rule depends on what the readings mean. If the two temperatures were retransmissions of the same event, we should deduplicate first; if we wanted the latest reading, we would need another timestamp or identifier and a rule for choosing it. &lt;code&gt;AVG&lt;/code&gt; is not automatically right in either case.&lt;/p&gt;&#10;&lt;h2 id="the-same-transformation-with-case-and-group-by"&gt;The same transformation with CASE and GROUP BY&lt;/h2&gt;&#10;&lt;p&gt;We can express the same idea through conditional aggregation, without &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; creates one row per time and sensor. Each &lt;code&gt;CASE&lt;/code&gt; passes through values for one metric and returns &lt;code&gt;NULL&lt;/code&gt; for the others; &lt;code&gt;AVG&lt;/code&gt; ignores those &lt;code&gt;NULL&lt;/code&gt;s. For &lt;code&gt;S-01&lt;/code&gt; at 10:00 it averages the two temperatures. For &lt;code&gt;S-02&lt;/code&gt; at 10:05 it finds no humidity value and returns &lt;code&gt;NULL&lt;/code&gt;. This version is useful when we want to see each column&amp;rsquo;s logic explicitly or write more portable SQL.&lt;/p&gt;&#10;&lt;h2 id="what-about-mysql"&gt;What about MySQL?&lt;/h2&gt;&#10;&lt;p&gt;MySQL does not provide SQL Server&amp;rsquo;s &lt;code&gt;PIVOT&lt;/code&gt; operator. We can create the same table by changing the temporal type and reuse the previous &lt;code&gt;INSERT&lt;/code&gt;; the date format with a &lt;code&gt;T&lt;/code&gt; is valid in MySQL too:&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;The query is the conditional aggregation we just used:&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;In 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; without &lt;code&gt;ELSE&lt;/code&gt; returns &lt;code&gt;NULL&lt;/code&gt; when the condition is false&lt;/a&gt;, and &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/aggregate-functions.html"&gt;&lt;code&gt;AVG&lt;/code&gt; ignores null values&lt;/a&gt;. It therefore produces the same logical result. Date syntax and column types differ between engines, but the idea of grouping the dimensions and calculating each metric separately is the same.&lt;/p&gt;&#10;&lt;h2 id="when-the-metrics-change"&gt;When the metrics change&lt;/h2&gt;&#10;&lt;p&gt;This is a &lt;strong&gt;static pivot&lt;/strong&gt;: we wrote &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;humidity&lt;/code&gt;, and &lt;code&gt;pressure&lt;/code&gt; into the query. If &lt;code&gt;battery_level&lt;/code&gt; arrives, it will not appear as a column until we change the SQL. The &lt;code&gt;CASE&lt;/code&gt; solution has the same constraint: a new output column needs a new expression.&lt;/p&gt;&#10;&lt;p&gt;When the metrics are not known in advance, we can build the column list and query using dynamic SQL: a &lt;em&gt;dynamic pivot&lt;/em&gt;. In SQL Server that means handling identifiers and input values carefully. It deserves its own article. A result schema that changes from one run to the next can also complicate downstream consumers; sometimes keeping long format is the better choice.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;UNPIVOT&lt;/code&gt; is a related operator that turns columns back into rows. It cannot fully undo this example: the &lt;code&gt;21.00&lt;/code&gt; average no longer contains the separate &lt;code&gt;20.00&lt;/code&gt; and &lt;code&gt;22.00&lt;/code&gt; readings, and &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;null values can disappear when unpivoting&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="in-the-database-or-outside-it"&gt;In the database or outside it?&lt;/h2&gt;&#10;&lt;p&gt;A pipeline may receive telemetry in long format, store it that way, and deliver a wide view to a report or a particular consumer. If the data is already in SQL Server or MySQL, doing the transformation in the database can avoid extracting many rows, transferring them, reshaping them in Python, and writing them back. Knowing SQL well lets us make that choice instead of automatically reaching for a script.&lt;/p&gt;&#10;&lt;p&gt;That does not mean SQL will always be faster. Data volume, indexes, the query plan, available memory, pipeline architecture, and transformation complexity all matter. Another tool may fit better for a hard-to-express business rule or for data already being processed outside the database. The important first step is to decide &lt;strong&gt;what one row represents and how repeated readings should be resolved&lt;/strong&gt;; the choice of operator comes after that.&lt;/p&gt;&#10;</description></item></channel></rss>