<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Judy</title>
    <description>The latest articles on DEV Community by Judy (@esproc_spl).</description>
    <link>https://dev.to/esproc_spl</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1191782%2Fdac3272f-c56a-41d9-914e-8f8fba86506b.jpg</url>
      <title>DEV Community: Judy</title>
      <link>https://dev.to/esproc_spl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/esproc_spl"/>
    <language>en</language>
    <item>
      <title>LLM Plans, Compiler Generates – A Deterministic SQL Generator with the Workflow as Its Contract</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Tue, 27 Oct 2026 08:43:28 +0000</pubDate>
      <link>https://dev.to/esproc_spl/llm-plans-compiler-generates-a-deterministic-sql-generator-with-the-workflow-as-its-contract-1fbe</link>
      <guid>https://dev.to/esproc_spl/llm-plans-compiler-generates-a-deterministic-sql-generator-with-the-workflow-as-its-contract-1fbe</guid>
      <description>&lt;p&gt;AI can already produce SQL that runs – but runnable isn’t the same as auditable.&lt;/p&gt;

&lt;p&gt;On complex queries, public evaluations show that even the most advanced models often hover around 60% execution accuracy on benchmarks such as BIRD-bench, far short of what it takes to pass review as is. The typical errors are not in syntax but in more complex logic: window boundaries, partition keys, and aggregation definitions, especially in sessionization, time-series gaps, and dynamic pivots. This kind of SQL needs multiple layers of nested CTEs and cumulative window calculations, with hidden join conditions, so a change in one place has to be checked layer by layer, starting from the innermost, to see whether it affects the outer ones.&lt;/p&gt;

&lt;p&gt;The consequence is a triple cost: it’s hard to review, hard to debug step by step, and hard to migrate across databases. The code does run, but to know whether the logic is right, you have to replay the whole nesting in your head.&lt;/p&gt;

&lt;p&gt;Why having an LLM write SQL directly isn’t deterministic&lt;/p&gt;

&lt;p&gt;The root of the problem isn’t AI capability, but the paradigm. Making a probabilistic model responsible for generating the final SQL directly binds planning and generation together in a single sampling pass, so determinism can’t be guaranteed.&lt;/p&gt;

&lt;p&gt;Here’s an example. A status log table records, in each row, the status (NewStatus) of an ID at a certain time point. The requirement is to retrieve, for each ID, the most recent Closed row before ConfirmationStarted.&lt;/p&gt;

&lt;p&gt;If you ask AI to generate this SQL directly, the typical result is a multi-layer nested query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
                &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ConfirmationStarted'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mytable&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Closed'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t_1&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t_2&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Closed'&lt;/span&gt;
                &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t_3&lt;/span&gt;
        &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t_4&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This SQL does run correctly, but you have to unfold it layer by layer to check whether the logic is right. That’s the cost of having AI produce the final SQL directly: every step is buried inside the nesting, and none of them can be audited on its own.&lt;/p&gt;

&lt;p&gt;To restore determinism, planning needs to be separated from generation.&lt;/p&gt;

&lt;p&gt;Architecture overview: separating planning from generation&lt;/p&gt;

&lt;p&gt;Break down the one-shot Prompt→SQL black box into a Prompt→Workflow→SQL white box, with the Workflow as the contract.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5oyoml4kzw65do2hi8m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5oyoml4kzw65do2hi8m.png" alt="Architecture overview" width="799" height="450"&gt;&lt;/a&gt;&lt;br&gt;
The three components:&lt;/p&gt;

&lt;p&gt;Planner: A person or an LLM produces a set of natural-language steps, breaking a plain-language requirement down into a sequence.&lt;/p&gt;

&lt;p&gt;Workflow: It uses the DSL contract, has a clear structure, decompose the logic into steps, is human-readable, lets you run and preview each step, and is itself executable documentation. The same workflow always produces the same SQL.&lt;/p&gt;

&lt;p&gt;Compiler: A deterministic compiler that translates a workflow into native SQL for the target dialect according to predefined rules. A single workflow can be compiled into native SQL for MySQL, PostgreSQL, Snowflake, or BigQuery.&lt;/p&gt;

&lt;p&gt;The contract constrains both ends. The LLM’s output is bound by DSL, so it never produces SQL directly. The compiler takes the workflow as input and does not guess. A single workflow produces exactly the same SQL, whether it is compiled today or tomorrow. The workflow itself becomes auditable logic documentation. Reviewers no longer have to guess what the AI intended; they simply review the steps.&lt;/p&gt;

&lt;p&gt;SQLazy is the concrete implementation of this architecture: The LLM plans steps, and the compiler generates SQL deterministic.&lt;/p&gt;

&lt;p&gt;Key design elements&lt;/p&gt;

&lt;p&gt;Three key design elements underpin SQLazy’s determinism and auditability.&lt;/p&gt;

&lt;p&gt;Design 1: DSL-based workflow and step-by-step debugging&lt;/p&gt;

&lt;p&gt;The workflow follows the natural order of human reasoning. If an error occurs in step 2, you only need to modify that step. Each step can be executed independently, with its intermediate table available for preview. Boundary errors show up directly in the intermediate results, so there’s no need to work backward from the final output. Take partitioning as an example: run the step, and you immediately see the values in the partition column, so you can tell on the spot whether the data was split as expected.&lt;/p&gt;

&lt;p&gt;Design 2: Deterministic compilation&lt;/p&gt;

&lt;p&gt;The compiler translates by predefined rules, not by probabilistic generation. Each workflow operation – such as sorting, filtering, partitioning and aggregation – maps one-to-one to corresponding SQL constructs – ORDER BY, WHERE, SUM(CASE WHEN) OVER(PARTITION BY) for partitioning, and GROUP BY. There are no hallucinations, and the results are reproducible. A single workflow produces the same result across runs and across machines, meeting the traceability requirement of compliance-regulated scenarios.&lt;/p&gt;

&lt;p&gt;Design 3: Change audit and version control&lt;/p&gt;

&lt;p&gt;A workflow is a plain-text contract, so it fits naturally into Git for version control. When the logic changes, you edit only the corresponding step. And the compiler automatically rebuilds the SQL, keeping the logic and SQL in sync. Every change can be diffed, reviewed, and rolled back. The history remains clear and traceable, and regulatory audits only need to go through the Git commit history.&lt;/p&gt;

&lt;p&gt;Example validation and scope of applicability&lt;/p&gt;

&lt;p&gt;Let’s validate the SQLazy approach using the example above.&lt;/p&gt;

&lt;p&gt;Source data mytable:&lt;/p&gt;

&lt;p&gt;CreatedAt ID NewStatus&lt;/p&gt;

&lt;p&gt;2022-05-25 23:17:44 147 Active&lt;/p&gt;

&lt;p&gt;2022-05-28 05:59:02 147 Closed&lt;/p&gt;

&lt;p&gt;2022-06-18 05:59:01 147 Closed&lt;/p&gt;

&lt;p&gt;2022-06-25 05:59:01 147 Closed&lt;/p&gt;

&lt;p&gt;2022-07-13 00:02:47 147 ConfirmationStarted&lt;/p&gt;

&lt;p&gt;2022-08-25 05:59:01 147 Closed&lt;/p&gt;

&lt;p&gt;2023-04-29 05:59:02 1645 Closed&lt;/p&gt;

&lt;p&gt;2023-05-08 14:53:34 1645 ConfirmationStarted&lt;/p&gt;

&lt;p&gt;The expected result contains only two rows (147→2022-06-25, 1645→2023-04-29). For all other rows, the Closed date is either too early or falls after ConfirmationStarted.&lt;/p&gt;

&lt;p&gt;In SQLazy, the same logic is expressed as an ordered workflow:&lt;/p&gt;

&lt;p&gt;Name Anchor Statement&lt;/p&gt;

&lt;p&gt;t1 mytable sort ID, CreatedAt asc&lt;/p&gt;

&lt;p&gt;t2&lt;/p&gt;

&lt;p&gt;segment condition (NewStatus = "ConfirmationStarted") partition ID as seg&lt;/p&gt;

&lt;p&gt;t3&lt;/p&gt;

&lt;p&gt;filter (NewStatus = "Closed" and seg = 1)&lt;/p&gt;

&lt;p&gt;t4&lt;/p&gt;

&lt;p&gt;summarize max CreatedAt as CreatedAt; group ID&lt;/p&gt;

&lt;p&gt;Run this example onlin: &lt;a href="https://www.sqlazy.com/?36K" rel="noopener noreferrer"&gt;https://www.sqlazy.com/?36K&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 1: sort ID, CreatedAt asc sorts data by ID and CreatedAt to preserve chronological order. The sort operation maps to SQL’s ORDER BY clause.&lt;/p&gt;

&lt;p&gt;Step 2: segment condition (NewStatus = "ConfirmationStarted") partition ID as seg splits the data into segments at each occurrence of ConfirmationStarted. partition ID ensures that each ID is segmented independently. After the operation runs, a new column, seg, appears. Rows where seg=1 identify the target interval, eliminating the need to hand-write SUM(CASE WHEN ...) OVER. To verify the segmentation, simply click t2 to inspect the seg column.&lt;/p&gt;

&lt;p&gt;Step 3: filter (NewStatus = "Closed" and seg = 1) only retains the Closed row within the target interval. filter maps to SQL’s WHERE clause.&lt;/p&gt;

&lt;p&gt;Step 4: summarize max CreatedAt as CreatedAt; group ID groups the data by ID and gets the latest CreatedAt timestamp for each group. The summarize operation maps to SQL’s GROUP BY and aggregate operation.&lt;/p&gt;

&lt;p&gt;Once the workflow is finished, click “Compile” to produce SQL according to predefined rules. Compared with the original SQL, this SQL has a clearer step-by-step structure:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5oysxkwrlsac59vkfjf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5oysxkwrlsac59vkfjf.png" alt="Image descriptionArchitecture overview" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 3: filter (NewStatus = "Closed" and seg = 1) only retains the Closed row within the target interval. filter maps to SQL’s WHERE clause.&lt;/p&gt;

&lt;p&gt;Step 4: summarize max CreatedAt as CreatedAt; group ID groups the data by ID and gets the latest CreatedAt timestamp for each group. The summarize operation maps to SQL’s GROUP BY and aggregate operation.&lt;/p&gt;

&lt;p&gt;Once the workflow is finished, click “Compile” to produce SQL according to predefined rules. Compared with the original SQL, this SQL has a clearer step-by-step structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
                &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ConfirmationStarted'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mytable&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Closed'&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t_3&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same workflow produces exactly the same SQL every time it is compiled. Switching SQL dialects requires no changes to the workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ril6dis64sipvplaagz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ril6dis64sipvplaagz.png" alt=" " width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The scope of applicability should also be stated honestly. For simple CRUD operations, writing SQL directly is more appropriate; introducing a workflow for just a few lines of logic is hardly worth the overhead. Features such as macros and loops are still on the roadmap. SQLazy is built for helping developers design and validate complex analytical logic, which can then be embedded into workflows such as dbt. It is intended to complement these tools, not replace them.&lt;/p&gt;

&lt;p&gt;Open-source vs Commercial components: SQLazy’s syntax and examples are open source, while its compiler and IDE are proprietary, with a free version available. The IDE supports local and private deployment, meeting enterprise requirements for data export and working offline.&lt;/p&gt;

&lt;p&gt;AI writes the logic. A compiler writes the SQL.&lt;/p&gt;

&lt;p&gt;Online trial: &lt;a href="https://sqlazy.com/" rel="noopener noreferrer"&gt;sqlazy.com&lt;/a&gt; (Free to use, no sign-up required. Playground runs locally in your browser).&lt;/p&gt;

&lt;p&gt;Open-source examples: &lt;a href="https://github.com/SPLWare/SQLazy/tree/master/examples" rel="noopener noreferrer"&gt;github.com/SPLWare/SQLazy/examples&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>llm</category>
      <category>sql</category>
    </item>
    <item>
      <title>Convert Time Columns into Rows and Pivot into a New Cross Table</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:22:57 +0000</pubDate>
      <link>https://dev.to/esproc_spl/convert-time-columns-into-rows-and-pivot-into-a-new-cross-table-3im2</link>
      <guid>https://dev.to/esproc_spl/convert-time-columns-into-rows-and-pivot-into-a-new-cross-table-3im2</guid>
      <description>&lt;h2&gt;
  
  
  Problem Description
&lt;/h2&gt;

&lt;p&gt;The table tb can be seen as a cross table: the combination of the ID and PartNum fields forms the row header, the three field names EnteredOn, PickedTime and DeliveredTime form the column header, and the cells hold time points. Neither the content nor the number of the row headers is fixed. Now the table has to be turned around: the original cells (the time points) become the row header of the new table, the original row header (the combination of PartNum and ID) becomes the column header of the new table, and the original three column headers are replaced by the strings ENTERED, PICKED and DELIVERED, which are filled into the cells.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdfplbpeisdpbysopumb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdfplbpeisdpbysopumb.png" alt="Source Data" width="799" height="236"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Expected Result&lt;/strong&gt;&lt;br&gt;
The column header is the combination of PartNum and ID, the row header is the time point, and the cells hold the action names:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhfh80jmdhtllj9z8sfe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhfh80jmdhtllj9z8sfe.png" alt="action names" width="800" height="489"&gt;&lt;/a&gt;&lt;br&gt;
The first row 2024-03-28 08:59:13.727 falls in the column 50A_100 with the value ENTERED, which means it is the entered time of the record with ID=100 and PartNum=50A. Further down the same column the rows PICKED and DELIVERED appear, which are the picked time and the delivered time of that record. In other words, the three time points that used to lie side by side in one row are now stacked vertically in one column.&lt;/p&gt;

&lt;p&gt;The records with ID=171 and ID=211 have only an entered time, while the picked and delivered times are empty, so the columns 50A_171 and 70B_211 each contain a single ENTERED and no PICKED or DELIVERED. Empty values have to be filtered out first, otherwise the new table would contain two meaningless time point rows.&lt;/p&gt;

&lt;p&gt;The number of columns of the new table equals the number of "row header combinations" in the source data (4 in this example), and the number of rows equals the total number of non-empty values in the three time columns (6 plus 2, that is 8, in this example). Neither direction is fixed, which is why this problem cannot be solved by hard-coding the column names.&lt;/p&gt;

&lt;p&gt;SQLazy Step-by-Step Implementation&lt;br&gt;
The essence of this problem is pivoting twice. The first pivot turns the three time columns into rows, so that every time point occupies a row of its own and carries the action it belongs to; the second pivot turns the rows back into columns, using the combination of PartNum and ID as the new column names. Both pivots use the pivot action: the first one uses its inverse mode (column-to-row) and the second one uses the default mode (row-to-column). Two small things happen in between: filtering out the empty time points, and replacing the action field names with the short names the problem asks for.&lt;/p&gt;

&lt;p&gt;[&lt;a href="https://www.sqlazy.com/?2Yx" rel="noopener noreferrer"&gt;Click to run this example online&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;The steps are explained below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd7f4l5ryqoip54bpywl6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd7f4l5ryqoip54bpywl6.png" alt="he steps are explained below." width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 1: Turn the three time columns into rows&lt;/p&gt;

&lt;p&gt;pivot group ID, PartNum; name action_type; value t_stamp; inverse&lt;/p&gt;

&lt;p&gt;This is the first key action of the problem. The pivot action turns rows into columns by default, and adding the inverse parameter switches it to column-to-row: the two grouping columns ID and PartNum stay unchanged, the other three time columns are expanded into rows, the original column names EnteredOn, PickedTime and DeliveredTime become the values of the new column action_type, and the original column values (the time points) become the values of the new column t_stamp.&lt;/p&gt;

&lt;p&gt;After this step one wide record becomes three long records: the record with ID=100 turns into three rows carrying the three action names EnteredOn, PickedTime and DeliveredTime together with their time points. How many row header combinations and how many column headers there are does not affect this action, so an unfixed number of row headers is not a problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkbkxzbtdqt9taqb65v5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkbkxzbtdqt9taqb65v5t.png" alt="unfixed number" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2: Filter out the empty time points&lt;br&gt;
filter (t_stamp notnull)&lt;br&gt;
In the source data the records with ID=171 and ID=211 have only an entered time, while the picked and delivered times are empty. Column-to-row also expands the empty values into rows, and those rows are meaningless, so they are filtered out first. The test for an empty value uses the not-null function, and the whole condition is written inside the parentheses of the unfixed number.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fak8maixijkib50j0ae7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fak8maixijkib50j0ae7n.png" alt="unfixed number" width="799" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 3: Replace the action names with the short names the problem asks for&lt;br&gt;
compute if(action_type = “EnteredOn” then “ENTERED”, action_type = “PickedTime” then “PICKED”, action_type = “DeliveredTime” then “DELIVERED”) as status&lt;br&gt;
The problem requires the cells to hold ENTERED, PICKED and DELIVERED, while column-to-row produces the original column names, so a computed column with the condition function performs the mapping: when the action name equals EnteredOn it becomes ENTERED, when it equals PickedTime it becomes PICKED, and when it equals DeliveredTime it becomes DELIVERED. The result goes into the new column status. The condition function can test several conditions in turn and return the matching value, which suits this kind of mapping with several equality branches.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fucgvye6778l9t6zdfohp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fucgvye6778l9t6zdfohp.png" alt="equality branches" width="799" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 4: Build the column names of the new table&lt;br&gt;
compute concat(PartNum, “_”, ID) as col_name&lt;br&gt;
The column header of the new table is the combination of PartNum and ID joined by an underscore, such as 50A_100. The concat function joins the two fields and the underscore in between into one string, which is written into the new column col_name. The column names are not known in advance but computed from the data, which is the other reason this problem cannot hard-code them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0bcu42bbmdlmlz7vvcu2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0bcu42bbmdlmlz7vvcu2.png" alt="cannot hard-code them" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 5: Keep only the three columns that are needed&lt;br&gt;
derive t_stamp, col_name, status&lt;br&gt;
The previous steps leave intermediate columns such as ID, PartNum and action_type. The second pivot needs only the time point, the column name and the action name, so the derive action picks those three out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhluh1qdhg0phbivbj978.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhluh1qdhg0phbivbj978.png" alt="action name" width="800" height="348"&gt;&lt;/a&gt;&lt;br&gt;
Step 6: Turn the rows into columns&lt;br&gt;
pivot group t_stamp; name col_name; value status&lt;br&gt;
The second pivot does not write a calculation method and uses the default row-to-column mode: group by t_stamp, let every value of col_name become the name of a new column, and fill the values of status into the matching cells. One row and one column occur only once in the data (a time point belongs to the action of a single record), so no aggregation algorithm has to be specified and the default of taking the first record in the group is enough.&lt;br&gt;
No inverse parameter is written here, which is the opposite of step 1: step 1 turns columns into rows and this step turns rows into columns, and the two pivots together turn the whole table around.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fv8f7wrbow092k2gg1q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fv8f7wrbow092k2gg1q.png" alt="two pivotsn" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Generated SQL&lt;br&gt;
After confirming the logic of the above 6 steps, the SQLazy compiler automatically generates native SQL (MySQL syntax used here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WITH t1 AS (
    SELECT ID, PartNum, 'EnteredOn' AS action_type, EnteredOn AS t_stamp
    FROM tb
    UNION ALL
    SELECT ID, PartNum, 'PickedTime' AS action_type, PickedTime AS t_stamp
    FROM tb
    UNION ALL
    SELECT ID, PartNum, 'DeliveredTime' AS action_type, DeliveredTime AS t_stamp
    FROM tb
  ),
  t2 AS (
    SELECT ID, PartNum, EnteredOn, PickedTime, DeliveredTime
    FROM t1
    WHERE t_stamp IS NOT NULL
  ),
  t4 AS (
    SELECT ID, PartNum, action_type, t_stamp
      , CASE
        WHEN action_type = 'EnteredOn' THEN 'ENTERED'
        WHEN action_type = 'PickedTime' THEN 'PICKED'
        WHEN action_type = 'DeliveredTime' THEN 'DELIVERED'
        ELSE NULL
      END AS status
      , concat_ws('', PartNum, '_', ID) AS col_name
    FROM t2
  ),
  t5 AS (
    SELECT t_stamp, col_name, status
      , FIRST_VALUE(status) OVER (PARTITION BY t_stamp, col_name ORDER BY NULL) AS FIRST_2
    FROM t4
  )
SELECT t_stamp
  , MIN(CASE
    WHEN col_name = '50A_100' THEN FIRST_2
  END) AS `50A_100`
  , MIN(CASE
    WHEN col_name = '50A_171' THEN FIRST_2
  END) AS `50A_171`
  , MIN(CASE
    WHEN col_name = '60B_125' THEN FIRST_2
  END) AS `60B_125`
  , MIN(CASE
    WHEN col_name = '70B_211' THEN FIRST_2
  END) AS `70B_211`
FROM t5
GROUP BY t_stamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLazy lets you describe logic in business language instead of writing nested SQL queries. Each of the two pivots in this problem is expressed by a single pivot statement: the first uses the inverse mode to expand the three time columns into rows and the second uses the default mode to turn the rows back into columns, so the two steps "columns to rows, then rows to columns" are obvious at a glance and there is no need to hand-write UNION ALL and hard-coded CASE branches in SQL.&lt;/p&gt;

&lt;p&gt;The two modes of the pivot action are the key to this problem. One and the same action switches direction depending on whether the inverse parameter is present, and both directions share the same wording of grouping column, name column and value column, so the business statement "which field becomes the new column name and which field becomes the new value" can be copied straight into the parameters without first working out whether SQL needs CASE or UNION.&lt;/p&gt;

&lt;p&gt;When the new column names come from the data itself (PartNum joined with ID), SQL can only hard-code them in the statement, and the statement has to be edited whenever the data changes. SQLazy treats building the column name as an ordinary computed column, so the names are generated from the data and neither a change in the number of row headers nor in the number of column headers requires a code change.&lt;/p&gt;

&lt;p&gt;Step-by-step calculation lets every step be inspected on its own: after step 1 you can see the expanded long table, after step 2 you can see that the empty values are gone, and after step 4 you can see the finished column names, so when something goes wrong there is no need to run the whole statement and then guess which part is at fault.&lt;/p&gt;

&lt;p&gt;Official Links&lt;br&gt;
SQLazy Online Experience: &lt;a href="https://sqlazy.com/" rel="noopener noreferrer"&gt;sqlazy.com&lt;/a&gt; (free, no registration required)&lt;/p&gt;

&lt;p&gt;SQLazy Repository: &lt;a href="https://github.com/SPLWare/SQLazy" rel="noopener noreferrer"&gt;github.com/SPLWare/SQLazy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>devops</category>
      <category>sql</category>
    </item>
    <item>
      <title>SQLazy: Find Strings with 5 or More Consecutive Alphabetical Letters</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Tue, 20 Oct 2026 04:03:36 +0000</pubDate>
      <link>https://dev.to/esproc_spl/sqlazy-find-strings-with-5-or-more-consecutive-alphabetical-letters-1aic</link>
      <guid>https://dev.to/esproc_spl/sqlazy-find-strings-with-5-or-more-consecutive-alphabetical-letters-1aic</guid>
      <description>&lt;h2&gt;
  
  
  Problem Description
&lt;/h2&gt;

&lt;p&gt;Table table_name has a single string field VALUE (max length 50). Find strings that contain at least 5 consecutive letters in ascending alphabetical order. That is: for each string, extract all letters, check them in original order, find the longest consecutive ascending subsequence (each letter &amp;gt;= previous one), and keep rows where the length &amp;gt;= 5. Non-letter characters must be excluded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9989rm76bzsrr9eerbcn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9989rm76bzsrr9eerbcn.png" alt="Source Data" width="800" height="270"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Expected Result&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F068shoxs29wagewhfgyg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F068shoxs29wagewhfgyg.png" alt="Expected Result" width="799" height="118"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two qualifying strings as examples:&lt;/p&gt;

&lt;p&gt;ABCDTest: the letters are A, B, C, D, T, e, s, t. The first four A-B-C-D are consecutively ascending. Then T (ASCII 84) is still greater than D, so the ascending segment extends to A-B-C-D-T (5 letters). But the next letter e (ASCII 101) is greater than T (84), so the ascent continues. The entire letter sequence A-B-C-D-T-e-s-t is ascending, with a max run length of 8. In fact &amp;gt;= 5 is sufficient to qualify.&lt;/p&gt;

&lt;p&gt;Testuvwxyz: the letters are T, e, s, t, u, v, w, x, y, z. Among them, u-v-w-x-y-z has 6 consecutive ascending letters, length &amp;gt;= 5, qualifying.&lt;/p&gt;
&lt;h2&gt;
  
  
  SQLazy Step-by-step Implementation
&lt;/h2&gt;

&lt;p&gt;Core idea: first expand by string length into one row per character, filter out non-letter characters, then use segment's not_up condition to detect where the ascending order breaks (start a new group when the current character is less than the previous one), then aggregate by group size, and finally filter strings whose max run &amp;gt;= 5. This pattern of character expansion, segmentation, and aggregation filtering is a typical SQLazy paradigm for string sequence problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzxh6ordvohz63lbx0dr2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzxh6ordvohz63lbx0dr2.png" alt="SQLazy Step-by-step Implementation" width="800" height="343"&gt;&lt;/a&gt;&lt;br&gt;
【&lt;a href="https://www.sqlazy.com/?5bc" rel="noopener noreferrer"&gt;Online operation case&lt;/a&gt;】&lt;/p&gt;

&lt;p&gt;The steps are explained below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Expand rows by max string length&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;expand 50 as Position&lt;/p&gt;

&lt;p&gt;Expand each string into 50 rows, with Position ranging 1~50. This is a preprocessing step for strings, enabling subsequent per-character computation. Since the maximum string length is 50, expand 50 covers all character positions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw9qma8dotc7rx6sa2s6d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw9qma8dotc7rx6sa2s6d.png" alt="expand 50 as Position" width="800" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Extract character at each position&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;compute mid(VALUE, Position, 1) as Ch&lt;/p&gt;

&lt;p&gt;Use the mid function to extract 1 character at Position from VALUE, named Ch. For positions beyond the actual string length, mid returns empty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fszmsx5zowjz2sp8lda42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fszmsx5zowjz2sp8lda42.png" alt="compute mid" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Filter out non-letter characters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;filter (Chisalpha)&lt;/p&gt;

&lt;p&gt;Use the isalpha function to keep only letters (A-Z, a-z), removing empty characters, digits, and symbols.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkl1zsdzara2jdshwpxii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkl1zsdzara2jdshwpxii.png" alt="filter" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Detect ascending breaks (core)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;segment Ch; not_up; partition VALUE; as GrpNo&lt;/p&gt;

&lt;p&gt;This is the most critical step. The segment statement partitions by VALUE and checks the Ch sequence within each partition: not_up specifies the non-ascending condition - when the current character (ASCII value) is less than or equal to the previous one, a segment break is triggered. Each time the ascending order breaks, a new group starts, generating group number GrpNo. Consecutively ascending letters are assigned to the same group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Count characters per group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;summarize Ch count as GrpSize; group VALUE, GrpNo&lt;/p&gt;

&lt;p&gt;Group by VALUE and GrpNo, count the number of letters in each group as GrpSize.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69cczx5leg28lk74tcdx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69cczx5leg28lk74tcdx.png" alt="Count characters" width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0iioblajn49mp9td1a1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0iioblajn49mp9td1a1o.png" alt="summarize" width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Get max group length per string&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;summarize GrpSize max as MaxRun; group VALUE&lt;/p&gt;

&lt;p&gt;Group by VALUE, take the maximum GrpSize to get the longest consecutive ascending segment length MaxRun for each string.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzhcuxw79wjrz0pz5lvhv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzhcuxw79wjrz0pz5lvhv.png" alt="summarize GrpSize" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7: Filter strings with length &amp;gt;= 5&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;filter (MaxRun &amp;gt;= 5)&lt;/p&gt;

&lt;p&gt;Keep rows where MaxRun &amp;gt;= 5.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrs22dg6bfhrlzr9hscp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrs22dg6bfhrlzr9hscp.png" alt="filter" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8: Keep final result columns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generated SQL&lt;br&gt;
After confirming the above steps, the SQLazy compiler automatically generates native SQL (Oracle syntax):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;TEMP_TABLE__2&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;col_1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;col_1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;TEMP_TABLE__2&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;col_1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col_1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;TEMP_TABLE__2&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUBSTR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;Ch&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;t3&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Ch&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;REGEXP_LIKE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^[A-Za-z]+$'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;t5&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GrpNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;GrpSize&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;Ch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;col__5&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt;
                &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;GrpNo&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;col__5&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t4&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GrpNo&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;t6&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GrpSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;MaxRun&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t5&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t6&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;MaxRun&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLazy lets you describe logic in business language instead of writing nested queries in SQL syntax. In this example of finding consecutive alphabetical letters, SQLazy uses 8 steps to clearly describe the entire flow from character expansion to segment aggregation. The segment not_up condition directly expresses the semantics of starting a new group when alphabetical order breaks, simplifying what would otherwise require recursive CTE + LAG + SUM window functions into a single readable declaration. The expand row expansion combined with per-character computation makes string processing as intuitive as table row operations; the step-by-step computation model ensures every step from character extraction to grouping and filtering is traceable and debuggable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Official Links&lt;/strong&gt;&lt;br&gt;
SQLazy Online Experience: &lt;a href="https://sqlazy.com/" rel="noopener noreferrer"&gt;sqlazy.com&lt;/a&gt; (free, no registration required)&lt;br&gt;
SQLazy Repository: &lt;a href="https://github.com/SPLWare/SQLazy" rel="noopener noreferrer"&gt;github.com/SPLWare/SQLazy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>database</category>
      <category>programming</category>
      <category>sql</category>
    </item>
    <item>
      <title>Flag Office Visits Within 6 Months After Surgery</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Thu, 15 Oct 2026 08:11:17 +0000</pubDate>
      <link>https://dev.to/esproc_spl/flag-office-visits-within-6-months-after-surgery-hgg</link>
      <guid>https://dev.to/esproc_spl/flag-office-visits-within-6-months-after-surgery-hgg</guid>
      <description>&lt;h2&gt;
  
  
  Problem Description
&lt;/h2&gt;

&lt;p&gt;The table tb_healthy records the medical history of patients; each patient has several records, where Visit_code = Surgery means an operation and Visit_code = Office means a consultation. Some patients have only operations, some have only consultations. Two questions have to be answered: whether the patient ever had a consultation, and whether the patient had a consultation within 6 months after an operation. The first goes into Office_Visit and the second into SX_past_6mo, with 1 for yes and 0 for no.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source Data&lt;/strong&gt;&lt;br&gt;
tb_healthy table:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgq4d1c3kyjltciz25d75.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgq4d1c3kyjltciz25d75.png" alt=" Source Data" width="800" height="412"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Expected Result&lt;/strong&gt;&lt;br&gt;
Office_Visit means the patient has consultation records, SX_past_6mo means the patient had a consultation within 6 months after an operation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzisbkza9wej9ht0dveo7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzisbkza9wej9ht0dveo7.png" alt="operation" width="800" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A30 had an operation on 2004-05-15 and a consultation on 2005-02-05, more than 8 months apart, beyond the 6-month window, so Office_Visit is 1 while SX_past_6mo is 0; this shows that "having a consultation" and "having a consultation within 6 months after an operation" are two different things.&lt;/p&gt;

&lt;p&gt;C01 has two operation records and two consultation records, and only the operation of 2001-05-15 pairs with the consultation of 2001-11-01 inside the 6-month window, so SX_past_6mo is 1; C02 has only operations and C03 has only consultations, so both flags are 0 for them.&lt;/p&gt;
&lt;h2&gt;
  
  
  SQLazy Step-by-Step Implementation
&lt;/h2&gt;

&lt;p&gt;This problem has to answer two questions at once, so it suits calculating the two things separately and merging them at the end. First convert the string dates into real dates, then filter out one copy of the operation records and one copy of the consultation records. "Has the patient ever had a consultation" comes from a summarize that groups by patient and takes the maximum. "Has the patient had a consultation within 6 months after an operation" needs the join action to attach consultation records to operation records, and the attaching condition is "the consultation date is not earlier than the operation date and not later than the operation date plus 6 months", which is a cross-table interval condition and exactly what the non-equi condition parameter of the join action is for. Summarizing the paired records gives the flag, and patients that never paired up are filled with 0 when the results are merged at the end.&lt;/p&gt;

&lt;p&gt;[&lt;a href="https://www.sqlazy.com/?4Pt" rel="noopener noreferrer"&gt;Click to run this example online&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;The steps are explained below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiapzhqnommm5kdshfo9o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiapzhqnommm5kdshfo9o.png" alt="steps" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 1: Convert the string dates into date values&lt;/p&gt;

&lt;p&gt;compute date(Visit_Date; format "M/d/yyyy") as dt&lt;/p&gt;

&lt;p&gt;Visit_Date is a string such as "5/15/2004" and cannot be compared directly. Use the date function with the format "M/d/yyyy" to parse it into a date value and write it into the new column dt. This step is the prerequisite for every date comparison that follows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe3gtn9lxbhjsopm837ri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe3gtn9lxbhjsopm837ri.png" alt="Visit_Date " width="799" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2: Filter one copy of the operation records and one of the consultation records&lt;br&gt;
filter Visit_code=“Surgery”&lt;br&gt;
filter Visit_code=“Office”&lt;br&gt;
The two copies are used by different calculations later: the operation records serve as the base table of the join and the consultation records as the table being attached. The comparison uses the symbol “=”, not “==” as in SQL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9cvqwqa0pngk9yck0g6y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9cvqwqa0pngk9yck0g6y.png" alt="in SQL" width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 3: Answer “has the patient ever had a consultation” first&lt;br&gt;
summarize max if(Visit_code=“Office” then 1 else 0) as Office_Visit; group Id&lt;br&gt;
The summarize action groups by Id and takes the maximum of the condition if(Visit_code=“Office” then 1 else 0) inside each group: a group with at least one consultation record yields 1, a group without any yields 0. The aggregation max is written before the aggregated item, and the result is named Office_Visit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fds6dfep02gtg7as0d643.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fds6dfep02gtg7as0d643.png" alt="Office_Visit" width="800" height="262"&gt;&lt;/a&gt;&lt;br&gt;
Step 4: Attach the consultations within 6 months to the operations&lt;br&gt;
join Id; with t_off; Id; take dt as off_dt; inner; condition ((dt&amp;gt;=dt@) and (dt&amp;lt;=(dt@ elapse 6 month)))&lt;br&gt;
This is the core step of the problem. The join action takes the operation table t_sx as the base table, joins the consultation table t_off on Id, and attaches the consultation date dt under the new name off_dt.&lt;br&gt;
The condition is a cross-table interval test written in the condition parameter: dt@ with the @ suffix is the date of the base table (the operation), and dt without @ is the date of the joined table (the consultation), so the whole condition reads “the consultation date is not earlier than the operation date and not later than the operation date plus 6 months”. Moving 6 months forward uses the elapse function, written as (dt@ elapse 6 month).&lt;br&gt;
Note: to reference a joined-table field in the condition, use its original name dt in that table, not the new name off_dt; the inner join drops the operation records that do not fit the window, so what remains are the paired records.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhhhpfu76uop4ediidqo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhhhpfu76uop4ediidqo.png" alt="paired records" width="800" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 5: Summarize the paired records into a flag&lt;br&gt;
summarize max 1 as SX_past_6mo; group Id&lt;br&gt;
Group by Id and take the maximum of the constant 1 inside each group. A patient that paired up at least once has at least one row and yields 1; a patient that never paired up does not appear in this table at all, which is exactly why 0 has to be filled in later. The aggregated item here is the constant 1 rather than a column, because the only information needed is whether a pair exists, not any particular value.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fduw4tgg1qjzh7pqlj329.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fduw4tgg1qjzh7pqlj329.png" alt="particular value" width="800" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 6: Merge the two results&lt;br&gt;
join Id; with t_sx_yes; Id; take SX_past_6mo&lt;br&gt;
Using the result of step 3 as the base table, attach the flag from step 5 with a left join. The left join keeps the patients that never paired up in the result, with SX_past_6mo left empty. No join type is written here because the default is a left join.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa81hri8b4l642l1ncpy4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa81hri8b4l642l1ncpy4.png" alt="left join" width="800" height="260"&gt;&lt;/a&gt;&lt;br&gt;
Step 7: Replace the empty values with 0&lt;br&gt;
compute nvl(SX_past_6mo,0) as SX_past&lt;br&gt;
Use nvl to replace the empty values with 0 and name the column SX_past, so every flag in the output is either 0 or 1. The last step uses the derive action to pick the three columns that are needed: the patient Id, whether there was a consultation Office_Visit, and whether there was a consultation within 6 months after an operation SX_past.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzbh5mk407lka79fhaki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzbh5mk407lka79fhaki.png" alt="operation SX_past" width="799" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last step picks the columns to output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated SQL&lt;/strong&gt;&lt;br&gt;
After confirming the logic of the above 7 steps, the SQLazy compiler automatically generates native SQL (Oracle syntax used here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Visit_Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Visit_code&lt;/span&gt;
      &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Visit_Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'M/d/yyyy'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tb_healthy&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;t_results&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;
      &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;Visit_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Office'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;Office_Visit&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;t_off&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Visit_Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Visit_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Visit_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Office'&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;t_sx_yes&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;SX_past_6mo&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t_sx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_sx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Visit_Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_sx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Visit_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_sx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;off_dt&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;
        &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;t_off&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t_sx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t_off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t_off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;t_sx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t_off&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;ADDMONTHS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t_sx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t_sx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Visit_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Surgery'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t_2&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;t_merged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Office_Visit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_sx_yes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SX_past_6mo&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t_results&lt;/span&gt;
      &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;t_sx_yes&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t_sx_yes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;t_final&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Office_Visit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SX_past_6mo&lt;/span&gt;
      &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SX_past_6mo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;SX_past&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t_merged&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Office_Visit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SX_past&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t_final&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLazy lets you describe logic in business language instead of writing nested SQL queries. This problem has to answer both "has the patient ever had a consultation" and "was there a consultation within 6 months after an operation", and SQLazy splits them into two independent groups of steps that are worked out separately and merged at the end. The non-equi condition parameter of the join action is the key: "the consultation date falls within 6 months after the operation date" is a cross-table interval test, and SQLazy tells the two tables apart with the @ suffix so the interval condition can be written straight into the join statement instead of building a Cartesian product and filtering afterwards. The summarize action keeps "is there any" questions short, since a group maximum yields 0 or 1 without filtering records first and then testing for existence. Every step is an intermediate result table you can inspect on its own, so when something goes wrong it is immediately clear which step is at fault.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Official Links&lt;/strong&gt;&lt;br&gt;
SQLazy Online Experience: &lt;a href="https://sqlazy.com/" rel="noopener noreferrer"&gt;sqlazy.com&lt;/a&gt; (free, no registration required)&lt;/p&gt;

&lt;p&gt;SQLazy Repository:  &lt;a href="https://github.com/SPLWare/SQLazy" rel="noopener noreferrer"&gt;github.com/SPLWare/SQLazy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>programming</category>
      <category>sql</category>
    </item>
    <item>
      <title>Start and End Dates of the Longest Rising Period</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Thu, 08 Oct 2026 07:21:04 +0000</pubDate>
      <link>https://dev.to/esproc_spl/start-and-end-dates-of-the-longest-rising-period-3m6j</link>
      <guid>https://dev.to/esproc_spl/start-and-end-dates-of-the-longest-rising-period-3m6j</guid>
      <description>&lt;h2&gt;
  
  
  Problem Description
&lt;/h2&gt;

&lt;p&gt;The stock table records the daily closing prices of many stocks, with the fields CODE (stock code), DT (trading date) and CL (closing price). For a given target stock, find the period during which it rose for the largest number of consecutive days, and give the start date and end date of that period.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source Data
&lt;/h2&gt;

&lt;p&gt;stock table (only the records of the target stock CODE = 100046 are listed, in ascending DT order):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0r9a42wns0il425wp5rh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0r9a42wns0il425wp5rh.png" alt="Source Data" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Expected Result&lt;br&gt;
NoRisingDays is the interval number:&lt;br&gt;
On 01-02 the closing price rose from 3.89 to 3.93, a continuous rise; on 01-05 it fell from 3.93 to 3.78, which breaks the rise. The interval number starts at 1 and increments at each break, so a new interval begins there.&lt;/p&gt;

&lt;p&gt;Interval 2 covers 2009-01-07 to 2009-01-12, 4 trading days in total. Its starting point 01-07 closed at 3.77, below the 4.04 of 01-06, and then 4.02, 4.42 and 4.86 climbed steadily, without a single down day in between.&lt;/p&gt;

&lt;p&gt;Interval 3 covers 2009-01-13 to 2009-01-16, also 4 trading days. Its starting point 01-13 closed at 4.44, below the 4.86 of 01-12, and then 4.88, 4.98 and 4.99 rose consecutively, tying with interval 2 for the longest run.&lt;/p&gt;

&lt;p&gt;SQLazy Step-by-Step Implementation&lt;br&gt;
Core idea: A continuous rise is simply a run of days that does not fall. Sort the data by date first, then use the segment action to watch the closing price: as soon as the closing price drops, the previous rise has ended, the group number increments and a new interval starts. Every record then carries the number of the rising interval it belongs to. Next, group by interval number and count the rows; that count is the number of consecutive rising days in the interval. Finally pick the interval with the largest count and take its first date and last date, which is the answer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.sqlazy.com/?4OY" rel="noopener noreferrer"&gt;[Click to run this example online]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The steps are explained below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9l2dhmfl8mi81kn805pk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9l2dhmfl8mi81kn805pk.png" alt="Step-by-Step" width="799" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 1: Take the target stock&lt;/p&gt;

&lt;p&gt;filter CODE = 100046&lt;/p&gt;

&lt;p&gt;Filter the stock table for the records whose CODE equals 100046; the rest of the calculation works on this stock only. The comparison uses the symbol "=", not "==" as in SQL.&lt;/p&gt;

&lt;p&gt;Step 2: Sort by date in ascending order&lt;/p&gt;

&lt;p&gt;sort DT asc&lt;/p&gt;

&lt;p&gt;Sort the filtered records by trading date DT in ascending order. Segmentation walks through the records in order; if the order is wrong, the intervals it produces are meaningless.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqnjyohz2uss72aoj0apu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqnjyohz2uss72aoj0apu.png" alt="sort DT asc" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 3: Start a new interval when the closing price drops&lt;br&gt;
segment CL down as NoRisingDays&lt;br&gt;
This is the core step of the problem. The segment action uses the fixed condition “down” on the closing price CL: when a day closes lower than the previous day, the group number increments; when it does not (a rise or a flat day), the group number stays the same. The generated group numbers are written into the new column NoRisingDays. Records in the same interval share the same group number, and a batch of records with the same group number is exactly one continuous rising run.&lt;br&gt;
There is no need to write a cross-row comparison such as CL &amp;lt; CL[-1], and no need to worry about the first record having no previous day. The “down” condition itself expresses the business meaning of “a drop starts a new interval”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20d98zo1qhqpxqqojeiv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20d98zo1qhqpxqqojeiv.png" alt="new interval" width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 4: Count how many days each interval has&lt;br&gt;
compute count DT as ContinuousDays; partition NoRisingDays&lt;br&gt;
The compute action counts the records within each NoRisingDays partition: the aggregation count comes before the aggregated item DT, and the result goes into the new column ContinuousDays. Every row in a partition is filled with the total number of days of that interval, so after this step each record carries “how many days my interval rose”. Because partition has already grouped the rows by interval number, taking the maximum of ContinuousDays afterwards is equivalent to “find the longest rising interval”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuobzhshm8cr2p6zmn8wi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuobzhshm8cr2p6zmn8wi.png" alt="interval" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 5: Pick out the longest interval&lt;br&gt;
rank ContinuousDays max&lt;br&gt;
The rank action takes the records whose ContinuousDays is the maximum. There may be more than one such record, in which case all tied records are returned, so every interval that ties for the longest is kept and none is lost to a tie. This step only filters records and does not change their order.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzx96quv529x68kl706nv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzx96quv529x68kl706nv.png" alt=" order." width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 6: Take the start and end dates of each interval&lt;br&gt;
summarize min DT as start_date, max DT as end_date; group NoRisingDays&lt;br&gt;
The summarize action groups by NoRisingDays, names the minimum DT of each group start_date and the maximum DT end_date. Since the previous step has already limited the records to the longest interval (or the intervals tied for longest), what is computed here is exactly the start date and end date of the longest rising period. The aggregation algorithm of summarize comes before the aggregated item, written as min DT and max DT.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspi1jlqf01s6djs9oho1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspi1jlqf01s6djs9oho1.png" alt=" before the aggregated" width="799" height="242"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Generated SQL
&lt;/h2&gt;

&lt;p&gt;After confirming the logic of the above 6 steps, SQLazy compiler automatically generates native SQL (MySQL syntax used here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CL&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;CODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100046&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;t3&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CL&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;CL&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;col__1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;DT&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DT&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;NoRisingDays&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;DT&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DT&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;col__1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;sub__2&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;t4&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NoRisingDays&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;NoRisingDays&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ContinuousDays&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t3&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;sub__6&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sub__5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ContinuousDays&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;col_4&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t4&lt;/span&gt; &lt;span class="n"&gt;sub__5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NoRisingDays&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ContinuousDays&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sub__6&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;col_4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLazy lets you describe logic in business language instead of writing nested SQL queries. For this "longest rising period" problem the core is a single sentence: start a new interval when the closing price drops. SQLazy expresses "a drop starts a new interval" directly with segment CL down, whereas hand-written SQL has to build a LAG comparison for the up/down flag first, then accumulate the interval number with SUM, nesting two window functions and handling the boundaries and NULL values by itself.&lt;/p&gt;

&lt;p&gt;SQLazy is valuable because it works step by step. The whole calculation is split into six steps: fetch, sort, segment, count, take the longest and summarize. Each step is an intermediate result table you can inspect directly: after step 3 you can see the interval numbers, after step 4 you can see how many days each interval has, and which interval is the longest is obvious. There is no need to run the whole SQL query and then guess whether something went wrong in the middle.&lt;/p&gt;

&lt;p&gt;SQLazy also writes the natural business order of "count the days of each interval first, then pick the largest, then take the dates" directly as the code order: compute counts within a partition, rank takes the records with the maximum value, and summarize takes the minimum and maximum dates within a group. In the aggregation actions the aggregation algorithm is written before the aggregated item (count DT, min DT, max DT), so reading the code matches the way the business describes it.&lt;/p&gt;

&lt;p&gt;Partitioning and segmentation make problems of the "measure each interval, then pick one" kind solvable without a self-join: partition puts the records of one interval into one group, and counting within a group and taking its first and last dates are ready-made actions. SQLazy compiles this logic into SQL that the target database can run, so the same code needs no rewrite when you switch databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official Links
&lt;/h2&gt;

&lt;p&gt;SQLazy Online Experience: &lt;a href="https://sqlazy.com/" rel="noopener noreferrer"&gt;sqlazy.com &lt;/a&gt;(free, no registration required)&lt;/p&gt;

&lt;p&gt;SQLazy Repository: &lt;a href="https://github.com/SPLWare/SQLazy" rel="noopener noreferrer"&gt;github.com/SPLWare/SQLazy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>computerscience</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>Your AI SQL Tool Doesn’t Have an “Undo Logic” Button – That’s the Real Problem</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Tue, 29 Sep 2026 07:19:07 +0000</pubDate>
      <link>https://dev.to/esproc_spl/your-ai-sql-tool-doesnt-have-an-undo-logic-button-thats-the-real-problem-4i6f</link>
      <guid>https://dev.to/esproc_spl/your-ai-sql-tool-doesnt-have-an-undo-logic-button-thats-the-real-problem-4i6f</guid>
      <description>&lt;p&gt;Have you ever had this experience: revising the prompt for an AI SQL tool through five or six versions, clicking Undo and rewriting countless times, and the result that finally comes out is still wrong.&lt;/p&gt;

&lt;p&gt;You can undo what you typed, or delete code you don’t like. But you can’t revert the logic that AI quietly drifted into – and that’ s the most insidious trap in AI-written SQL.&lt;/p&gt;

&lt;p&gt;Take a user behavior analysis task as an example: resetting the session number whenever the time gap exceeds 1 hour. The requirement can be stated in a single sentence, while the AI-generated SQL is always syntactically perfect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WITH lagged AS (
  SELECT *, LAG(dt) OVER (PARTITION BY account_number ORDER BY dt) AS prev_dt
  FROM numEvents
), grouped AS (
  SELECT *,
    SUM(CASE WHEN TIMESTAMPDIFF(SECOND, prev_dt, dt) &amp;gt; 3600 THEN 1 ELSE 0 END)
    OVER (PARTITION BY account_number ORDER BY dt) AS grp
  FROM lagged
)
SELECT *, ROW_NUMBER() OVER (PARTITION BY account_number, grp ORDER BY dt) AS seq
FROM grouped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three levels of nesting, syntactically flawless. But what if the result is wrong? You don’t know what the bug is – is it an edge case handling error in LAG, a reversed condition in the cumulative sum, or wrong row-number partitioning? Want to fix it? All you can do is revise the prompt and regenerate the entire query – which amounts to rewriting the whole thing from scratch. Your Undo button can delete the entire block of code, but it can’t revert the drift in the logic.&lt;/p&gt;

&lt;p&gt;In SQLazy, the same logic is divided into three clear, neat steps:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fejgp69ddfah33o1qf9io.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fejgp69ddfah33o1qf9io.png" alt="neat steps" width="800" height="219"&gt;&lt;/a&gt;&lt;br&gt;
Each step corresponds to a specific business action. If the time interval is wrong, fix the interval; if the partition is wrong, fix the partition. The error is pinpointed precisely and fixed step by step. You don’t need to rewrite the whole thing from scratch, because each step of the logic comes with a built-in “revert node”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmoedlm2k19cvaytu7x7d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmoedlm2k19cvaytu7x7d.png" alt="revert node" width="800" height="428"&gt;&lt;/a&gt;&lt;br&gt;
In this process, AI is only responsible for translating logic at each step – turning natural-language input into SQLazy syntax. You can tell at a glance whether the translation is correct. Once confirmed, the compiler turns it into deterministic SQL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm0pigtpwamkiqrsgm9it.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm0pigtpwamkiqrsgm9it.png" alt="deterministic SQL" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI writes the logic. A compiler writes the SQL.&lt;/p&gt;

&lt;p&gt;You see every step of the logic, so you can fix every drift precisely.&lt;/p&gt;

&lt;p&gt;Our open-source example repository collects complete solutions for these time-window and session-analysis scenarios, covering a dozen common business requirements.&lt;/p&gt;

&lt;p&gt;GitHub repository: &lt;a href="https://github.com/SPLWare/SQLazy/tree/master/examples/time-window-analytics" rel="noopener noreferrer"&gt;https://github.com/SPLWare/SQLazy/tree/master/examples/time-window-analytics&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re also fed up with the “revise prompt → hope it works → scrap it and start over” cycle, try SQLazy’s online Playground. No sign-up, no download – everything runs locally in your browser, and the data never leaves you device.&lt;/p&gt;

&lt;p&gt;Try SQLazsy: sqlazy.com&lt;/p&gt;

&lt;p&gt;A truly efficient tool doesn’t mean repeated undos and rewrites – it lets you see the logic from the start, so you take fewer wrong turns.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>devops</category>
      <category>sql</category>
    </item>
    <item>
      <title>SQLazy is live on Product Hunt today~~</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Tue, 22 Sep 2026 05:51:28 +0000</pubDate>
      <link>https://dev.to/esproc_spl/sqlazy-is-live-on-product-hunt-today-2id0</link>
      <guid>https://dev.to/esproc_spl/sqlazy-is-live-on-product-hunt-today-2id0</guid>
      <description>&lt;p&gt;If you've ever wrestled with SQL, this one's for you.&lt;/p&gt;

&lt;p&gt;AI can write runnable SQL statements, but it is often unreliable. SQLazyturns SQL development into a step-by-step, verifiable, and auditable workflow, with a compiler to ensure that the final output is correct. How SQLazy works? SQLazy does not generate a huge blob of SQL in one go. Instead, it turns the SQL development into a step-by-step, traceable workflow:&lt;br&gt;
Use semi-natural language syntax to describe what each step does.&lt;/p&gt;

&lt;p&gt;Verify the logic at each step – Intermediate results are inspectable.&lt;/p&gt;

&lt;p&gt;Let the compiler generate the final SQL.&lt;/p&gt;

&lt;p&gt;The final SQL is generated by the compiler, not the LLM. This means:&lt;/p&gt;

&lt;p&gt;Zero SQL errors from AI hallucination, with 100% correct results.&lt;/p&gt;

&lt;p&gt;The logic is fully auditable.&lt;/p&gt;

&lt;p&gt;The output is production-ready.&lt;/p&gt;

&lt;p&gt;Would love your support — every upvote and comment helps:&lt;br&gt;
&lt;a href="https://www.producthunt.com/products/sqlazy?utm_source=other&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;https://www.producthunt.com/products/sqlazy?utm_source=other&amp;amp;utm_medium=social&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank you very much.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Stopped Trusting AI-Generated SQL – Here’s the Approach I Trust Instead</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Fri, 18 Sep 2026 07:28:33 +0000</pubDate>
      <link>https://dev.to/esproc_spl/i-stopped-trusting-ai-generated-sql-heres-the-approach-i-trust-instead-5gc7</link>
      <guid>https://dev.to/esproc_spl/i-stopped-trusting-ai-generated-sql-heres-the-approach-i-trust-instead-5gc7</guid>
      <description>&lt;h2&gt;
  
  
  Approach I Trust Instead
&lt;/h2&gt;

&lt;p&gt;Why I no longer trust AI-generated SQL: not because it fails to run, but because it runs too smoothly. It passes syntax checks. It runs. But it can be riddled with errors in places nobody’s watching.&lt;/p&gt;

&lt;p&gt;Have you ever had AI-generated SQL that passed syntax checks, ran without errors – but silently got a row wrong?&lt;/p&gt;

&lt;h2&gt;
  
  
  Runs ≠ Trustworthy
&lt;/h2&gt;

&lt;p&gt;Here’s a seemingly simple requirement: In a status history table, the NewStatus field records the status of each ID. Each ID has a ConfirmationStarted row and multiple Closed rows. We need to retrieve the most recent Closed row before the ConfirmationStarted row.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fglypg95aw9gkpu3r83k3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fglypg95aw9gkpu3r83k3.png" alt=" " width="799" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The expected result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7xq72l1430jolirrruj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7xq72l1430jolirrruj.png" alt="The expected result" width="798" height="117"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Take ID=147 as an example. It has three Closed rows before ConfirmationStarted; and the last one – dated 06-25 – is “the most recent”. The Closed row dated 08-25 doesn’t count because it comes after ConfirmationsStarted.&lt;/p&gt;

&lt;p&gt;The expected output contains only two rows: "147→2022-06-25"、"1645→2023-04-29". The other Closed rows are either too early or come after ConfirmationStarted – none of them count.&lt;/p&gt;

&lt;p&gt;You throw the requirement at an AI, and seconds later, get a beautiful block of SQL – CTEs stacked with window functions. Even the code review turns up nothing wrong. It’s not until two weeks after going live that you discover ID 147 was matched to 05-28. One row is wrong, yet the SQL runs fine.&lt;/p&gt;

&lt;p&gt;The bug is hard to spot at first glance: the segmentation logic is correct, but the final aggregation uses MIN instead of MAX. As a result, the query returns the earliest Closed row before ConfirmationStarted instead of the one closest to it, quietly shifting the result by one row for boundary cases.&lt;/p&gt;

&lt;p&gt;Here’s the buggy AI-generated SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WITH t2 AS (
        SELECT CreatedAt, ID, NewStatus
            , 1 + SUM(CASE WHEN NewStatus='ConfirmationStarted' THEN 1 ELSE 0 END)
                OVER (PARTITION BY ID ORDER BY ID ASC, CreatedAt ASC ROWS UNBOUNDED PRECEDING) AS seg
        FROM mytable
    )
SELECT ID, MIN(CreatedAt) AS CreatedAt -- This is one of the several errors – the correct function should be MAX, to get the most recent row
FROM (
    SELECT CreatedAt, ID, NewStatus, seg
    FROM t2
    WHERE NewStatus='Closed' AND seg=1
) t_3
GROUP BY ID
ORDER BY ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most dangerous part of having AI generate the final SQL: AI can rewrite a block of code, but catching a subtle one-row logical error is a different story.&lt;/p&gt;

&lt;h2&gt;
  
  
  We asked AI to do exactly what it shouldn’t
&lt;/h2&gt;

&lt;p&gt;AI is good at breaking down the plain-language requirements into steps, but it isn’t good at giving a 100% guarantee for every boundary condition in the final SQL. It’s a probabilistic model, not a compiler.&lt;/p&gt;

&lt;p&gt;Public benchmarks show that even leading large language models often achieve only around 60% execution accuracy on complex queries when translating natural language directly into executable SQL. That’s nowhere near reliable enough for direct sign-off – on average, one attempt in every three or four can be wrong.&lt;/p&gt;

&lt;p&gt;The old paradigm is “prompt → AI → non-deterministic final SQL”. It’s a black-box delivery model: where the only option is to have AI re-guess the whole block from scratch.&lt;/p&gt;

&lt;p&gt;What we need is a new paradigm: prompt → AI → standardized steps → compiler → deterministic SQL, with every step independently verifiable.&lt;/p&gt;

&lt;p&gt;“It runs” doesn’t count. “You’d sign off on it” does.&lt;/p&gt;

&lt;p&gt;SQLazy is that confidence to sign off. It is the compiler for standardized steps – and the tool that actually turns the new paradigm into reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  In SQLazy: This takes just 4 steps
&lt;/h2&gt;

&lt;p&gt;The same “most recent Closed row” task takes just 4 steps in SQLazy. The key is that you can click any step and inspect the intermediate result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdewscwfyc7xp6je5cun.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdewscwfyc7xp6je5cun.png" alt="just 4 steps" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1zwaxjwqmm83l16nbc6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1zwaxjwqmm83l16nbc6.png" alt="just 4 steps" width="768" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 1: sort ID, CreatedAt asc – puts the rows in the correct chronological order first.&lt;/p&gt;

&lt;p&gt;Step 2: segment condition (NewStatus ="ConfirmationStarted") partition ID as seg – segments rows by ConfirmationStarted; partition ID ensures that each ID is segmented independently. The segmentation column is seg, where seg=1 is the target segment. There’s no need to hand-write SUM(CASE WHEN ...) OVER.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh0g9vkj2fmxg5t4rs27v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh0g9vkj2fmxg5t4rs27v.png" alt="segment condition" width="768" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 3: filter (NewStatus ="Closed"and seg = 1) – keeps only the Closed rows in the target segment.&lt;/p&gt;

&lt;p&gt;Step 4: summarize CreatedAt max as CreatedAt; group ID – takes the row with the largest timestamp for each ID: that’s the most recent one.&lt;/p&gt;

&lt;p&gt;To check whether the segmentation is correct, click t2 and check the seg column. Fix whichever step is wrong – if the error is in step 2, you only fix that step. There’s no need to start over from scratch. Whether you compile for MySQL or Snowflake, the same input always produces the same output. It doesn’t invent fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WITH t2 AS (
        SELECT CreatedAt, ID, NewStatus
            , 1 + SUM(CASE
                WHEN (NewStatus = 'ConfirmationStarted') THEN 1
                ELSE 0
            END) OVER (PARTITION BY ID ORDER BY ID ASC, CreatedAt ASC ROWS UNBOUNDED PRECEDING) AS seg
        FROM mytable
    )
SELECT ID, MAX(CreatedAt) AS CreatedAt
FROM (
    SELECT CreatedAt, ID, NewStatus, seg
    FROM t2
    WHERE (NewStatus = 'Closed'
        AND seg = 1)
) t_3
GROUP BY ID
ORDER BY ID

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This SQL isn’t guessed by AI. It’s what the compiler produces by translating the four steps according to fixed rules. It’s guaranteed correct. No need to review it.&lt;/p&gt;

&lt;p&gt;Of course, there’s no need to use SQLazy for simple CRUD queries. It’s intended for complex logic that takes 30+ lines of SQL, such as logic involving time sequence, segmentation, and relative positions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paste your most suspicious AI-generated SQL here
&lt;/h2&gt;

&lt;p&gt;What’s the most typical kind of hallucination? Fabricating fields or tables that don’t exist, applying a filter condition to the wrong column, or getting the join key or aggregation logic wrong. The SQL still runs, but the result is already off. It’s the kind of thing we’ve all seen before.&lt;/p&gt;

&lt;p&gt;Stop cycling through “rewrite prompt → rerun → try your luck”. Just hand the part of your business logic you trust least to SQLazy, and let it break the logic down into a step-by-step workflow that can be validated stepwise. Then share your AI hallucination cases in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI provides the logic; the compiler makes it real – with zero hallucinations.
&lt;/h2&gt;

</description>
    </item>
    <item>
      <title>Is SQLazy Something Missing from Your dbt Workflow?</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Tue, 15 Sep 2026 07:55:11 +0000</pubDate>
      <link>https://dev.to/esproc_spl/is-sqlazy-something-missing-from-your-dbt-workflow-4k47</link>
      <guid>https://dev.to/esproc_spl/is-sqlazy-something-missing-from-your-dbt-workflow-4k47</guid>
      <description>&lt;h2&gt;
  
  
  dbt is good – but it has blind spots
&lt;/h2&gt;

&lt;p&gt;If you’re working in analytics engineering, dbt is probably already part of your workflow. Hand it the T in ELT, and SQL becomes testable, documentable, collaborative code. For simple transformations such as JOIN, CASE and GROUP BY, it handles them with ease.&lt;/p&gt;

&lt;p&gt;But once the analytical logic gets more complex – for example, when it involves multiple levels of window operations, conditional segmentation, sessionization, cumulative-value/running-total resets, and the like – the SQL in a dbt model balloons. Pile up enough nesting, and the code turns into a wall: no one can read it, no one dares to touch it, and no one knows whether a change will actually fix things or make them worse. Three months later, the requirement changes. You open that model and stare blankly at five CTEs and three window functions –change it, and you risk breaking something; leave it alone, and no one may notice when the data goes wrong. In the end, you either work around it, or simply drop the requirement.&lt;/p&gt;

&lt;p&gt;dbt solves the “pipeline” problem in SQL engineering, but it doesn’t address the “design process” for complex analytical logic. What’s needed is a way to work out and validate that complex logic outside dbt first, and then embed the clean SQL into the model.&lt;/p&gt;

&lt;p&gt;A real-world sessionization example&lt;/p&gt;

&lt;p&gt;Business logic: There is a user behavior event table, where session IDs needs to rest based on a time interval – start a new session whenever there has been no activity for more than one hour. This is the foundation of user behavior analysis – session duration, conversion rate, and retention funnels all depend on it. In the dbt community, almost every analytics engineer has encountered this kind of sessionization or event-based query.&lt;/p&gt;

&lt;p&gt;Implementing this logic in SQL usually takes three layers of nested CTEs: use LAG to retrieve the previous timestamp, CASE WHEN to run a cumulative check for session breaks, and finally ROW_NUMBER to generate sequence numbers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;lagged&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;account_number&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;prev_time&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;event_tb&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPDIFF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SECOND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;account_number&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;grp&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;lagged&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;account_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grp&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With so many levels of nesting, any change requires validation layer by layer, from innermost CTE outward. The SQL runs – but no one wants to maintain it. Then, three months later, new logic comes along, for example, sessions that cross midnight now count as new. You have to start by understanding the code from the innermost LAG, verify the logic layer by layer to make sure everything still holds. The whole query amounts to a rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLazy’s approach: Design in steps, compile and embed
&lt;/h2&gt;

&lt;p&gt;SQLazy decomposes complex analytical logic into clear, step-by-step operations, validates them one by one, then compiles them into SQL and embeds it in dbt.&lt;/p&gt;

&lt;p&gt;Back to the sessionization example. Here’s how SQLazy implements it, step by step:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcysodb91o7q7xj5skdmx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcysodb91o7q7xj5skdmx.png" alt="step by step" width="800" height="171"&gt;&lt;/a&gt;&lt;br&gt;
Run the code online: &lt;a href="https://www.sqlazy.com/?4M4" rel="noopener noreferrer"&gt;https://www.sqlazy.com/?4M4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three steps, in exactly the same logical order as humans would think through them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Sort by user and timestamp, ensuring that events are handled in chronological order.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;sort account_number asc dt asc&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi8u683gh4xy7kaoa979.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi8u683gh4xy7kaoa979.png" alt="sort account_number asc dt asc" width="768" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2: Segment by time interval and assign session ID.&lt;/p&gt;

&lt;p&gt;segment condition ((dt[-1] elapse 3600 second)&amp;lt;= dt) partition account_number as grp&lt;/p&gt;

&lt;p&gt;This is the critical step. segment handles grouping – it traverses through each user’s data, and when the interval between the current event and the previous one exceeds one hour, start a new group. dt[-1] elapse 3600 second means “add 3,600 seconds to the previous row’s timestamp”. If this value is less than or equal to the current row’s timestamp, the interval is within one hour, and the row stays in the current group; otherwise, a new group starts. account_number ensures that each user’s data is segmented independently.&lt;/p&gt;

&lt;p&gt;Once this step is executed, the intermediate table gets a new column, grp – where the same number represents the same session. You can immediately see whether the segmentation is correct.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7khg71gqpkv7bphjren.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7khg71gqpkv7bphjren.png" alt="segmentation" width="768" height="554"&gt;&lt;/a&gt;&lt;br&gt;
Step 3: Generate incrementing sequence nubers within each session.&lt;/p&gt;

&lt;p&gt;compute # as seq partition account_number grp&lt;/p&gt;

&lt;p&gt;compute is used to create a computed column; # is the row number, generating incrementing sequence numbers starting from 1 within each account_number + grp combination. partition specifies the partition or grouping dimension, ensuring the sequence numbers are numbered independently within each session.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvuzzuqifubxtpj5zwuu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvuzzuqifubxtpj5zwuu.png" alt="within each session" width="768" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can run each step separately and preview the intermediate result. To change the interval threshold, you only need to modify the one-line step T2 while keeping the other steps unchanged. Once T2 is executed, you can immediately see how grp changes and whether the segmentation is correct. There’s no need to mentally unravel the nested logic or reason about whether “changing one layer breaks the one above it”.&lt;/p&gt;

&lt;p&gt;Once the workflow (SQLazy steps) is designed and its logic is validated, click “Compile”. SQLazy’s compiler deterministically converts the workflow into native SQL for the target database – MySQL, PostgreSQL, Snowflake, or BigQuery – just switch the database option. Then paste the compiled SQL into a dbt model’s .sql file, and let dbt handle materialization, testing, documentation, and lineage.&lt;/p&gt;

&lt;p&gt;SQLazy doesn’t replace dbt – it fills the gap that dbt doesn’t cover: designing and validating complex analytical logic.&lt;/p&gt;

&lt;p&gt;The SQLazy workflow is a living document. When requirements change three months later, you open the workflow, understand the logic from each step, and modify the relevant steps. The compiler guarantees correct SQL – not generated through AI guesswork, but produced through deterministic compilation: the same workflow always generates the same SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SQLazy over writing SQL by hand
&lt;/h2&gt;

&lt;p&gt;Some users might think: I’ll just hand-write it. It’s only a little more complicated, so what.&lt;/p&gt;

&lt;p&gt;The problem is that in SQL, the cost of becoming “a little more complicated” doesn’t scale linearly. Typically, going from two levels of nested window functions to four increases the maintenance burden by an order of magnitude. And analytical requirements tend to get more complex over time.&lt;/p&gt;

&lt;p&gt;Once you embed SQLazy into your dbt workflow, it brings the following changes:&lt;/p&gt;

&lt;p&gt;Step-level debugging. Inspect every intermediate table just as you would when debugging code – no need to add a debug field in dbt and repeatedly run dbt run. Say step 2 segments data wrong – fix it on the spot, and every subsequent step recalculates automatically.&lt;/p&gt;

&lt;p&gt;Logic as documentation. The workflow itself is a readable description of the logic. A new team member can open it and understand what the analysis does within minutes – no need to dig through dbt’s docs or ask the person who wrote the code.&lt;/p&gt;

&lt;p&gt;Cross-dialect portability. The same workflow can be compiled into MySQL, PostgreSQL, Snowflake, and BigQuery dialects at will. When a dbt project is migrated from PostgreSQL to Snowflake, there’s no need to rewrite the analytical logic – the compiler adapts to the target database automatically.&lt;/p&gt;

&lt;p&gt;LLM-assisted design. Converting plain-language step descriptions into a standardized workflow lowers the barrier to designing complex logic. AI only translates; it does not make decisions. The final SQL is generated deterministically by the compiler, with zero hallucinations.&lt;/p&gt;

&lt;p&gt;Of course, SQLazy has its limits. You don’t need it for simple CRUD queries – using it for SQL queries that can be written in just a few lines is overkill. Where it really shines is exactly where dbt users feel the most pain: analytical logic complex enough to need step-by-step decomposition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;dbt controls the transformation pipeline, while SQLazy designs the analytical logic. The two aren’t a replacement for each other – they are complementary. dbt handles materializing, testing, documenting, and tracking lineage for the analytical output, while SQLazy helps you design and validate complex analytical logic before turning it into SQL.&lt;/p&gt;

&lt;p&gt;If there’s a chunk of analytical SQL running over 30 lines in your dbt model, try decomposing it into steps in SQLazy first.&lt;/p&gt;

&lt;p&gt;Try SQLazy online: &lt;a href="https://sqlazy.com/" rel="noopener noreferrer"&gt;sqlazy.com&lt;/a&gt; (Free to use, signup not required)&lt;/p&gt;

&lt;p&gt;Installer download link: &lt;a href="https://www.raqsoft.com.cn/download-NaturalSPL" rel="noopener noreferrer"&gt;https://www.raqsoft.com.cn/download-NaturalSPL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project repository: &lt;a href="https://github.com/SPLWare/SQLazy" rel="noopener noreferrer"&gt;github.com/SPLWare/SQLazy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Case collection: &lt;a href="https://github.com/SPLWare/sqlazy/tree/master/examples" rel="noopener noreferrer"&gt;github.com/SPLWare/sqlazy/tree/master/examples&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>dataengineering</category>
      <category>sql</category>
    </item>
    <item>
      <title>SQLazy: Convert Swipe Records into Single-Row Sessions by Pairing Order</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Tue, 08 Sep 2026 09:13:37 +0000</pubDate>
      <link>https://dev.to/esproc_spl/sqlazy-convert-swipe-records-into-single-row-sessions-by-pairing-order-1iel</link>
      <guid>https://dev.to/esproc_spl/sqlazy-convert-swipe-records-into-single-row-sessions-by-pairing-order-1iel</guid>
      <description>&lt;h2&gt;
  
  
  Problem Description
&lt;/h2&gt;

&lt;p&gt;Table userBuilding stores swipe logs for personnel entering and exiting buildings, with one record per timestamp and fields username, building, action (IN/OUT), and timestamp. Normally, records for the same person in the same building appear in pairs, IN followed by OUT. In practice, the data is messy: unpaired records and consecutive actions in the same direction occur. The task is to turn each pair of records for each person and each building into one row by pivoting rows to columns; unpaired records become separate rows, with NULL for the missing side, i.e., convert the vertical log into horizontal sessions in pairing order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsaa6d7oyg430rxookat.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsaa6d7oyg430rxookat.png" alt="Source Data" width="800" height="746"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Expected Result&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujh4ihyq1wkimprvrlxa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujh4ihyq1wkimprvrlxa.png" alt="Expected Result" width="800" height="714"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Take user-3/building-1 as an example. The raw sequence is OUT, IN, IN, IN, OUT, OUT, OUT, which is split into 6 segments by the pairing rules: the first OUT stands alone, the next two INs each stand alone, the fourth IN pairs with the first OUT into one row, and the last two OUTs each stand alone. Consecutive actions in the same direction are never forced into a pair, ensuring correct session boundaries. In the 13-row result, this user accounts for 6 rows, which illustrates the logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  SQLazy Step-by-Step Implementation
&lt;/h2&gt;

&lt;p&gt;Core idea: First sort by username, building, and timestamp to arrange the log of the same person in the same building in time order; then use a conditional segment to detect session boundaries - start a new group when the previous record is OUT or the current record is IN, so each group contains at most one IN and at most one OUT; finally group by username, building, and seg, and use conditional max aggregation to collapse the IN time and OUT time within each group into one row, with NULL for unpaired sides.&lt;/p&gt;

&lt;p&gt;[Click to run this example online]&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm1jbhpnzfandz2d0fc52.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm1jbhpnzfandz2d0fc52.png" alt="example" width="800" height="289"&gt;&lt;/a&gt;&lt;br&gt;
The steps are explained below.&lt;/p&gt;

&lt;p&gt;Step 1: Sort by person, building, and time&lt;/p&gt;

&lt;p&gt;sort username, building, timestamp asc&lt;/p&gt;

&lt;p&gt;Sort records of the same person in the same building by timestamp in ascending order to ensure subsequent pairing decisions follow time order. Using username and building as leading sort keys keeps the order within each partition consistent with the partition keys. Sorting is a prerequisite for the subsequent segment and summarize steps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk4wmw4y44149uk9exu2y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk4wmw4y44149uk9exu2y.png" alt="summarize steps" width="800" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2: Segment by pairing semantics to generate seg (core)&lt;/p&gt;

&lt;p&gt;segment condition ((action[-1] = "OUT")or (action[-1] = "IN" and action = "IN")) partition username, building as seg&lt;/p&gt;

&lt;p&gt;The most critical step is to express session boundaries with a conditional segment: start a new group when the previous record is OUT, or when the previous record is IN and the current is also IN. A previous OUT means the previous session is closed and a new one should start; consecutive INs mean multiple swipes for entry, and each additional IN starts a new group to avoid squeezing multiple INs into one session. partition username, building keeps different persons and buildings independent, each numbered with its own seg. The condition action[-1] is SQLazy's relative position syntax, equivalent to LAG(action,1), without manually writing window functions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkwgyzabj8a2u5t3cv5td.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkwgyzabj8a2u5t3cv5td.png" alt="window functions" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 3: Group by person, building, and segment number, then conditionally aggregate rows to columns&lt;/p&gt;

&lt;p&gt;summarize condition (action = "IN") max timestamp as 'IN', condition (action = "OUT") max timestamp as 'OUT'; group username, building, seg&lt;/p&gt;

&lt;p&gt;Group by username, building, and seg; each group contains at most one entry and one exit. Use conditional aggregation: when action = "IN", take max(timestamp) as the IN column, and when action = "OUT", take max(timestamp) as the OUT column. max and first are equivalent here because there is at most one record of each type per group; using conditional max naturally yields NULL on the other side for unpaired groups. Note that in the latest syntax the aggregation function (max) comes before the aggregated expression (timestamp), and grouping keys are specified via "group username, building, seg".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpudohdszlijui1rdeoz0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpudohdszlijui1rdeoz0.png" alt="building, seg" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 4: Clean up the helper column&lt;/p&gt;

&lt;p&gt;derive delete seg&lt;/p&gt;

&lt;p&gt;Remove the auxiliary column seg produced by segment, keeping only the four columns username, building, IN, and OUT for the final result. The table is cleaner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated SQL&lt;/strong&gt;&lt;br&gt;
After confirming the four steps above, the SQLazy compiler automatically generates native SQL (Oracle syntax here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'OUT'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"OUT"&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'IN'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"IN"&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
            &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col__2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'OUT'&lt;/span&gt;
                &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'IN'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;col__2&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;sub__3&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t_4&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLazy lets you describe logic in business language instead of writing nested queries in SQL syntax. In this example, a single conditional segment expresses the business rule clearly: segment condition ((action[-1] = "OUT")or (action[-1] = "IN" and action = "IN"))partition username, building, i.e., "start a new session when the previous segment is closed or a consecutive swipe-in occurs". Writing this in SQL manually requires LAG to fetch the previous row, SUM OVER to accumulate segment numbers, two layers of subqueries to wrap window columns, and finally conditional aggregation MAX(CASE...) to pivot rows to columns, plus handling partition and ordering consistency. SQLazy compresses all of this into four steps - sort, segment, conditional summarize, and cleanup - each verifiable independently; relative positions and partitions are compiled into window functions, and conditional summarize handles NULL automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Official Links&lt;/strong&gt;&lt;br&gt;
SQLazy Online Experience: &lt;a href="https://sqlazy.com/" rel="noopener noreferrer"&gt;sqlazy.com&lt;/a&gt; (free, no registration required)&lt;/p&gt;

&lt;p&gt;SQLazy Repository: &lt;a href="https://github.com/SPLWare/SQLazy" rel="noopener noreferrer"&gt;github.com/SPLWare/SQLazy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>SQLazy: Get the Latest Closed Before ConfirmationStarted</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Fri, 04 Sep 2026 08:04:22 +0000</pubDate>
      <link>https://dev.to/esproc_spl/sqlazy-get-the-latest-closed-before-confirmationstarted-ha5</link>
      <guid>https://dev.to/esproc_spl/sqlazy-get-the-latest-closed-before-confirmationstarted-ha5</guid>
      <description>&lt;h2&gt;
  
  
  Problem Description
&lt;/h2&gt;

&lt;p&gt;Database table mytable stores the status NewStatus of multiple IDs at different timestamps CreatedAt. Each ID has exactly one ConfirmationStarted and one or more Closed statuses. The task is: within each ID, among all the Closed records before ConfirmationStarted, find the one closest to ConfirmationStarted, and take the record's ID and time fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ox3fysr0e540jehiump.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ox3fysr0e540jehiump.png" alt="Source Data" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected Result&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0sfh7c6q2cgbpk93lcc7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0sfh7c6q2cgbpk93lcc7.png" alt="Expected Result" width="800" height="123"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Take ID=147 as an example:&lt;/p&gt;

&lt;p&gt;ConfirmationStarted occurs on 2022-07-13; before it, the three Closed records happen on 05-28, 06-18 and 06-25, and the one closest to it is 2022-06-25 05:59:01, which is exactly the time in the expected result.&lt;/p&gt;

&lt;p&gt;For ID=1645, ConfirmationStarted occurs on 2023-05-08 14:53:34, with only one Closed (2023-04-29 05:59:02) before it, so the result takes that one.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLazy Step-by-Step Implementation
&lt;/h2&gt;

&lt;p&gt;Core idea: After sorting each ID's records by time, use segment to cut segments wherever ConfirmationStarted appears; records before the first ConfirmationStarted naturally fall into seg=1. Then filter out the records with seg=1 and status Closed, and finally summarize by ID taking the maximum CreatedAt, which is the Closed closest to ConfirmationStarted.&lt;/p&gt;

&lt;p&gt;[&lt;a href="https://www.sqlazy.com/?36K" rel="noopener noreferrer"&gt;Click to run this example online&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;The steps are explained below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftr0g1osm4e0jm7rbsalq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftr0g1osm4e0jm7rbsalq.png" alt="explained" width="799" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 1: Sort by ID and time in ascending order&lt;/p&gt;

&lt;p&gt;sort ID, CreatedAt asc&lt;/p&gt;

&lt;p&gt;Ensures the records within each ID are arranged in time order, providing the basis for the subsequent segmentation and for taking the "latest".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3zvcbt6s1f77ddf31nk7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3zvcbt6s1f77ddf31nk7.png" alt="CreatedAt asc" width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2: Start a new segment when ConfirmationStarted is encountered&lt;/p&gt;

&lt;p&gt;segment condition (NewStatus = “ConfirmationStarted”) partition ID as seg&lt;br&gt;
This is the core step. segment with partition ID segments independently within each ID; the segment condition specifies that whenever a record whose NewStatus is ConfirmationStarted is encountered, a new segment is opened and numbered as seg. In this way, all records before the first ConfirmationStarted fall into seg=1, and the seg of ConfirmationStarted itself and the records after it increases in turn. A single statement cuts out the range “before the target status”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwxx04455rdpriinevo2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwxx04455rdpriinevo2.png" alt="target status" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Step 3: Filter out the target records&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
filter (NewStatus = “Closed” and seg = 1)&lt;br&gt;
Keep only the records with seg=1 (before the first ConfirmationStarted) and status Closed; these are all the Closed records of each ID before ConfirmationStarted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5n5wxvlspr4ojxoj662.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5n5wxvlspr4ojxoj662.png" alt="records" width="800" height="285"&gt;&lt;/a&gt;&lt;br&gt;
*&lt;em&gt;Step 4: Summarize by ID to take the latest Closed time&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
summarize CreatedAt max as CreatedAt; group ID&lt;br&gt;
Take the maximum CreatedAt within each ID. Since the records were sorted by time in ascending order earlier, the maximum is exactly the Closed closest to ConfirmationStarted. summarize directly describes the aggregation with the business semantics of “group by ID and take the maximum CreatedAt”, without manually writing window functions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fetx928zio661dly0eip5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fetx928zio661dly0eip5.png" alt="window functions" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated SQL&lt;/strong&gt;&lt;br&gt;
After confirming the above 4-step logic, the SQLazy compiler automatically generates native SQL (Oracle syntax here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
                &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ConfirmationStarted'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mytable&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NewStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NewStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Closed'&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t_3&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLazy lets you describe logic in business language instead of writing nested SQL queries. For this kind of problem of cutting segments by events and then taking records from a specified segment, the key is to mark the event stream with segment labels: segment's conditional segmentation directly describes the business semantics with"cut a segment when ConfirmationStarted is encountered", and partition makes the segmentation run independently within each ID. The step-by-step computation of segment first, then filter, then summarize lets every step's intermediate result be verified independently; summarize completes the aggregation with a plain statement like "group by ID and take the maximum time", and the compiler automatically generates runnable SQL.&lt;/p&gt;

&lt;p&gt;Official Links&lt;br&gt;
SQLazy Online Experience: &lt;a href="https://sqlazy.com/" rel="noopener noreferrer"&gt;sqlazy.com &lt;/a&gt;(free, no registration required)&lt;/p&gt;

&lt;p&gt;SQLazy Repository:&lt;a href="https://github.com/SPLWare/SQLazy" rel="noopener noreferrer"&gt; github.com/SPLWare/SQLazy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>No More Rewriting the Same Logic for Every Database</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:16:31 +0000</pubDate>
      <link>https://dev.to/esproc_spl/no-more-rewriting-the-same-logic-for-every-database-59b9</link>
      <guid>https://dev.to/esproc_spl/no-more-rewriting-the-same-logic-for-every-database-59b9</guid>
      <description>&lt;p&gt;First, let me ask you: how many databases does your team use?&lt;/p&gt;

&lt;p&gt;MySQL for OLTP, PostgreSQL for OLAP, Snowflake for the data warehouse, and occasionally, running a report on some legacy Oracle system – that’s practically standard for every data team today.&lt;/p&gt;

&lt;p&gt;Now comes the question: How many times do you need to implement the same business logic?&lt;/p&gt;

&lt;h2&gt;
  
  
  The “dialect tax” every database imposes
&lt;/h2&gt;

&lt;p&gt;SQL has a subtle concept called “dialect.” Standard SQL is one thing, but every database has its own “accent”:&lt;/p&gt;

&lt;p&gt;MySQL has LIMIT. Oracle has ROWNUM. SQL Server has TOP.&lt;/p&gt;

&lt;p&gt;Date functions: MySQL has DATE_ADD. PostgreSQL has INTERVAL. Oracle has ADD_MONTHS.&lt;/p&gt;

&lt;p&gt;String concatenation: MySQL has CONCAT. SQL Server has +. PostgreSQL has ||.&lt;/p&gt;

&lt;p&gt;Window function support, recursive CTE syntax, and GROUP BY strictness — every database has its own way of doing things.&lt;/p&gt;

&lt;p&gt;You spend a whole afternoon getting a complex analytical SQL query working on MySQL. It runs beautifully. Then comes the requirement: run the same logic on Snowflake. You open the editor, copy the SQL over, hit run — and it throws an error.&lt;/p&gt;

&lt;p&gt;Not because the logic is wrong, but because you are using the wrong dialect.&lt;/p&gt;

&lt;p&gt;So, you start fixing it: replace LIMIT with QUALIFY, rewrite every date function, change the string concatenation syntax… Eventually, it works. But what happens when the requirements change again? Rewrite everything all over again? And what if the next project uses a different database?&lt;/p&gt;

&lt;p&gt;You spend more time translating between dialects than thinking through the logic.&lt;/p&gt;

&lt;p&gt;This isn’t an isolated case. According to a Stack Overflow survey, developers spend a significant amount of their coding time dealing with environment setup and compatibility issues. And SQL dialect differences are one of the most subtle yet costly forms of “compatibility tax”.&lt;/p&gt;

&lt;p&gt;Worse still, the cost compounds. Every new database you support means one more SQL code version to maintain. Three databases = three SQL code versions. The change of one business rule means three places to update. Miss one update = production data don’t match = a midnight page to fix bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can’t we “write once, run everywhere”?
&lt;/h2&gt;

&lt;p&gt;You might think: “Then why not just use standard SQL?”&lt;/p&gt;

&lt;p&gt;In theory, yes. But in practice, standard SQL cannot cover real-world business requirements. Dialect differences in window function support, the many different approaches to date and time handling, and different implementations of string operations – these are either undefined or loosely defined in the standard SQL, leaving each database with its own implementation.&lt;/p&gt;

&lt;p&gt;Think about it differently: you don’t need “one SQL running everywhere”; you need “one logic generating SQL everywhere.”&lt;/p&gt;

&lt;p&gt;These are two very different concepts.&lt;/p&gt;

&lt;p&gt;“One SQL running everywhere” means sending the same SQL text to every database – and this approach simply doesn’t work.&lt;/p&gt;

&lt;p&gt;“One logic generating SQL for every database” means writing the logic once and letting a tool compile it into each database’s dialect – and this is a path that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLazy’s approach: Write logic once, adapt to every dialect automatically
&lt;/h2&gt;

&lt;p&gt;SQLazy takes exactly the second approach.&lt;/p&gt;

&lt;p&gt;You express your logic as a workflow – a readable, step-by-step sequence of operations. The compiler then translates it into native SQL for the target database.&lt;/p&gt;

&lt;p&gt;For example, the logic of “calculating the longest streak of consecutive up days of a stock” can be expressed as the following workflow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F919irmc3k90ck9r1dx2l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F919irmc3k90ck9r1dx2l.png" alt="example" width="800" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This workflow is database-independent. It describes the logic, not SQL syntax.&lt;/p&gt;

&lt;p&gt;Then you tell the compiler the target database – MySQL, PostgreSQL, Oracle, Snowflake, or BigQuery, and it automatically generates native SQL for that database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faq0crar04o0omgz9sl05.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faq0crar04o0omgz9sl05.png" alt="database" width="800" height="383"&gt;&lt;/a&gt;&lt;br&gt;
With the same workflow, simply switch the database option to generate SQL in a different dialect. You don’t need to rewrite anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkgpjbupbwxfjd12v8ksw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkgpjbupbwxfjd12v8ksw.png" alt="different dialect" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4inc07vll47uhx15bmoy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4inc07vll47uhx15bmoy.png" alt="uploads" width="800" height="199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workflow stays the same because the logic stays the same. What changes is only the “accent” output by the compiler.&lt;/p&gt;

&lt;p&gt;What does this mean?&lt;/p&gt;

&lt;p&gt;First, you only need to maintain one version of the logic. When business requirements change, only update the workflow. The compiler regenerates native SQL for every database – no more “updating MySQL but forgetting Oracle”.&lt;/p&gt;

&lt;p&gt;Second, it’s easier for new developers to get started. They don’t need to learn database dialect differences – they only need to understand the workflow, while the compiler handles the dialect translation. The workflow is for humans to read; SQL is for databases to execute.&lt;/p&gt;

&lt;p&gt;Third, migration costs are nearly zero. Moving from MySQL to PostgreSQL? Just switch the database option, recompile, and the compiler automatically adapts to every SQL dialect. No need to modify code line by line, and no more dialect pitfalls.&lt;/p&gt;

&lt;p&gt;Fourth, auditing becomes simpler. You audit the workflow – a readable representation of the logic, instead of hundreds of lines of SQL code versions scattered across different databases.&lt;/p&gt;

&lt;p&gt;SQL dialect differences are not a minor inconvenience. They are the hidden tax data teams pay every day. Every additional database means more maintenance costs and a higher risk of errors.&lt;/p&gt;

&lt;p&gt;SQLazy’s solution is simple: let compilers, instead of humans, translate dialects.&lt;/p&gt;

&lt;p&gt;Your job is to write clear logic, and leave the rest to the compiler. You can also use an LLM to help write the logic. Natural language descriptions can then be standardized into SQLazy statements.&lt;/p&gt;

&lt;p&gt;AI writes the logic. A compiler writes the SQL.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>devops</category>
      <category>sql</category>
      <category>sqlazy</category>
    </item>
  </channel>
</rss>
