Trae + SQLazy: A Practical Guide to Writing Complex SQL
1. Introduction
AI-assisted data development is reshaping the way we write traditional SQL. Current mainstream approaches can be broadly divided into two categories: one is the end-to-end mode that directly generates raw SQL; the other is the layered mode that generates structured intermediate scripts which are then compiled. The former is easy to get started with but difficult to master for complex business scenarios; the latter adds a layer of abstraction but provides engineering-grade guarantees of auditability, debuggability, and reproducibility.
The combined approach adopted in this document is: Trae (ByteDance's AI programming tool) serves as the"brain,"responsible for understanding requirements, clarifying ambiguities, and outputting SQLazy step-by-step scripts (.nspl); the SQLazy dedicated IDE serves as the"execution layer," responsible for syntax validation, step-by-step debugging, and cross-database compilation. Together, they form a closed loop of AI planning + human review + deterministic engine execution.
We selected four real-world cases, from easy to hard, covering typical scenarios such as statistical aggregation, multi-table merging, cross-subgroup filling, and amount splitting. The complete process from requirement input to validation passing is demonstrated, with emphasis on documenting the errors encountered and the reasoning behind corrections.
2. Tool Collaboration Mechanism
2.1 Trae's Role and Capabilities
Trae plays three key roles in this workflow:
1. Automatic Loading of Project Knowledge Base
A global knowledge base specification file (see Section 3.1) is deployed in the project, centrally declaring:
- Output format specifications for SQLazy scripts (three-column tab-separated, single function per step)
- Hard constraints (reserved word handling, cross-step reference rules, etc.)
- Loading paths for function documentation and feature documentation
When the user enters /sqlazy-plan followed by business requirements in the chat box, Trae automatically inherits all the above rules without needing to restate them.
2. Structured Four-Step Output
Trae is constrained to output solutions following these four steps rather than giving conclusions directly:
1. Capability Review: List the functions and features involved in the task
2. Requirement Decomposition: Break down business requirements into ordered data processing steps
3. Feature Matching: Match each step with appropriate SQLazy features
4. Code Implementation: Output the final .nspl step-by-step script
This enforced output flow ensures the auditability of the solution—the rationale for each step choice is clearly visible.
3. Proactive Requirement Clarification
Facing complex requirements, Trae proactively raises key questions, such as: How should the date range be defined? How should null values be handled? Is the grouping key unique? This prevents the AI from making assumptions on its own.
2.2 SQLazy's Core Value
SQLazy provides a dedicated IDE for writing and executing .nspl scripts. Its core value is reflected in three aspects:
1. Clear Step-by-Step Semantics, Low Audit Barrier
Taking "longest consecutive rising days of a stock" as an example, the .nspl script requires only 5 steps: filter → sort → segment → summarize count → summarize max. Reading the entire script is like reading a business operation checklist, rather than parsing complex nested SQL. Each line is one step; the output of the previous step is the input of the next, with logic fully unfolded.
2. Step-by-Step Execution, Quick Problem Localization
In the IDE, you can run each step incrementally and inspect intermediate results in real time. Once a step's output doesn't meet expectations, you can immediately pinpoint the specific logic error without having to dig through dozens of lines of nested SQL.
3. One Script, Multi-Database Compilation
After validation passes, a one-click compile generates standard SQL for mainstream databases such as MySQL, PostgreSQL, Oracle, etc., without rewriting for each database separately.
3. Workflow
3.1 Environment Setup
The project adopts the following standard directory structure:
project_root/
├── plan.md # Global specification: format conventions, loading paths
├── sqlazy-plan.md # Command entry: /sqlazy-plan trigger, inherits all rules from plan.md
├── nspl/ # Delivery directory: .nspl scripts stored here
├── function/ # Function reference documentation (auto-loaded)
└── action/ # Action reference documentation (auto-loaded)
After creating a new project in Trae, copy the sqlazy-plan.md, plan.md files, and the function and action directories from the SQLazy installation directory's LLM folder to the project root.
3.2 How to Trigger
Use the /sqlazy-plan command in the Trae chat box to trigger the task, followed by a complete business requirement description. It is recommended to specify the involved tables, fields, join relationships, grouping dimensions, time range, and output requirements in one go.
3.3 Validation and Correction
This is the most critical step in the entire chain:
Construct a small amount of representative test data and manually calculate the expected results
Run the script step by step in the SQLazy IDE, comparing intermediate results with expected values
When issues are found, directly modify the script or feed back to Trae for regeneration
After validation passes, compile to generate the target database's SQL
4. Case Studies
The following four cases, from easy to hard, fully document the problem-solving process.
Case 1: Longest Consecutive Rising Days for a Stock
Requirement:
/sqlazy-plan In the stock price table stock, which contains three columns—CODE (integer),DT (date), and CL (closing price)—calculate the maximum number of consecutive days that the stock with code 100046 has been rising (i.e., each day's closing price is higher than the previous day's closing price).
Analysis and Implementation:
This is the simplest type of statistical requirement. Trae outputs the following script according to the four-step process:
VariableName |
Anchor |
Statement |
t1 |
stock |
filter CODE = 100046 |
t2 |
sort DT |
|
t3 |
segment CL; not_up; as SegGroup |
|
t4 |
summarize CL count as cnt; group SegGroup |
|
t5 |
compute cnt - 1, as up_days |
|
t6 |
summarize up_days max as max_up_days |
Simple statistical requirement, generated correctly by AI in one attempt. Run and validate
directly in the SQLazy IDE, then compile to generate the corresponding database SQL.
Case 2: Merging Multiple Tables by ID into a Single Row
Requirement:
/sqlazy-plan There are four data tables T1, T2, T3, and T4 with similar structures. Each table has two fields: the first field is an ID (named id, id2, id3, and id4 respectively), and the second field is named colA, colB, colC, and colD respectively. The goal is to merge these four tables by their ID values into a single result table with 9 columns: the first column, ID_main, stores the ID value, and the remaining 8 columns are the two columns from each of the four tables (i.e., all fields from T1, T2, T3, and T4). Each distinct ID appears as exactly one row in the merged table. If an ID is missing from any of the original tables, the corresponding columns from that table are set to NULL.
Analysis and Implementation:
The core challenge of this requirement is that the four tables have different ID field names (id, id2, id3, id4), and a method is needed to unify them while ensuring all IDs are preserved without loss.
Trae designed a dual-track strategy of "full join + ID coalescing," outputting an 8-step script:
VariableName |
Anchor |
Statement |
t1 |
T1 |
derive id as ID_main, id, colA |
t2 |
join ID_main; with T2; id2; take id2, colB; full |
|
t3 |
compute ifn(ID_main, id2), as ID_main |
|
t4 |
join ID_main; with T3; id3; take id3, colC; full |
|
t5 |
compute ifn(ID_main, id3), as ID_main |
|
t6 |
join ID_main; with T4; id4; take id4, colD; full |
|
t7 |
compute ifn(ID_main, id4), as ID_main |
|
t8 |
derive ID_main, id, colA, id2, colB, id3, colC, id4, colD |
Same as Case 1, correct on first attempt.
Case 3: Cross-Group Sequential Field Value Filling
Requirement:
/sqlazy-plan Given a data table lines where the first two fields Group1 and Group2 are grouping columns, the third field LineID is a unique row identifier, and the fourth field TargetField is a numeric target column. After sorting by Group1, Group2, and LineID, within each distinct Group1, every Group2 has the same number of records; only the last Group2 (in sort order) has non-null values in TargetField, while all other Group2 groups contain nulls. The requirement is to copy the TargetField values from that last Group2 group to the other groups within the same Group1, in the same sequential row order (i.e., matching positions by sorted order). The final output should be a table containing only the columns Group1, Group2, LineID, and TargetField, sorted in ascending order by the first three columns.
Analysis and Implementation (corrected after one iteration):
The core difficulty of this requirement is that LineID is unique across all rows and cannot be directly used as a cross-group join key. Trae's first version incorrectly used Group1 + LineID as the join key, causing mapping failures. Below is the complete iterative correction process.
First Version (Incorrect)
Trae's first attempt used Group1 + LineID as the join key to fill the TargetField from the last subgroup back to the whole table:
VariableName |
Anchor |
Statement |
t1 |
sort Group1, Group2, LineID |
|
t2 |
rank Group2; max; partition Group1 |
|
t3 |
derive Group1, LineID, TargetField as filled_TargetField |
|
t4 |
join Group1, LineID; with t3; Group1, LineID; take filled_TargetField |
|
t5 |
derive Group1, Group2, LineID, filled_TargetField as TargetField |
Problem: LineID is unique across every row (e.g., 101, 105, 201, 205, 301, 305).
Different Group2 groups share no LineID values. Therefore, when using Group1 + LineID
as the join key, only the last subgroup's own rows can match; all other subgroups' rows
fail to match, TargetField remains NULL, and the fill logic completely fails.
Corrected Version: Using Row Number as Mapping Bridge
Instead of using LineID for joining, the "row number" (positional sequence 1, 2, 3, ... obtained by ranking within each Group2 by LineID) serves as the mapping bridge. Since every Group2 within the same Group1 has the same row count, the "Nth row" naturally corresponds across different Group2 groups.
VariableName |
Anchor |
Statement |
t1 |
lines |
sort Group1, Group2, LineID |
t2 |
rank as seq; partition Group1, Group2 |
|
t3 |
rank Group2; max; partition Group1 |
|
t4 |
t2 |
join Group1, seq; with t3; Group1, seq; take TargetField as TF_src |
t5 |
compute ifn(TargetField, TF_src) as TargetField |
|
t6 |
derive Group1, Group2, LineID, TargetField |
|
t7 |
sort Group1, Group2, LineID |
Validation Example:
Assume the data is as follows (all LineID values are unique):
Group1 |
Group2 |
LineID |
TargetField |
A |
G2-1 |
101 |
NULL |
A |
G2-1 |
105 |
NULL |
A |
G2-2 |
201 |
NULL |
A |
G2-2 |
205 |
NULL |
A |
G2-3 |
301 |
100 |
A |
G2-3 |
305 |
200 |
After t2 ranking, seq is obtained: G2-1(101→1, 105→2), G2-2(201→1, 205→2), G2-3(301→1, 305→2). t3 takes the two records of G2-3. t4 creates a mapping table (A,1→100), (A,2→200). After t5 backfill, the 1st row of every subgroup gets 100, and the 2nd row gets 200.
When LineID is unique across the entire table, it cannot be directly used as a cross-subgroup
join key. You must first rank within each subgroup to obtain a row number, and use the row number
as the mapping bridge. The rank ... max filter can directly select the last subgroup without
requiring an additional summarize + backfill step.
Case 4: Invoice Amount Split by Accounts with Total Conservation
Requirement:
/sqlazy-plan For the invoice table i (containing fields invoiceid, amount, projectid) and the project table p (containing fields id, projectid, accountcode), join them on projectid. For the joined result, add a new allocation field splitamount to implement a splitting logic that distributes the amount evenly by the number of accounts under each project, while ensuring the total sum remains conserved. Within each group, sort by accountcode in ascending order. For the 2nd through the Nth account, splitamount is calculated as amount / total_number_of_accounts, rounded to 2 decimal places. The 1st account bears the rounding remainder; its splitamount is set to the original invoice amount minus the sum of the splitamount values of all other accounts, so that the total exactly matches the original amount.
Analysis and Implementation (finalized after two iterations):
This is the most complex of the four cases. Although Trae's first version correctly identified the partitioned aggregation approach, there were issues with syntax details and step organization, requiring two iterations to finalize.
First Iteration: Partitioned Aggregation (partially corrected)
VariableName |
Anchor |
Statement |
t1 |
i |
join projectid; with p; projectid; take accountcode; inner |
t2 |
sort projectid, accountcode |
|
t3 |
rank accountcode; as rank; partition projectid |
|
t4 |
compute *, count; as account_cnt; partition projectid |
|
t5 |
compute round(amount/account_cnt, 2) as temp_split; temp_split, sum as 'sum_temp_split'; partition projectid |
|
t6 |
compute if(rank=1 then if(account_cnt>=1 then amount else amount-t5.'sum_temp_split'+temp_split) else temp_split) as splitamount |
Two issues were found after running:
Issue 1: Line 4 used * as the count formula, and the SQLazy parser reported 'logic error near [*]'.
The current version does not support * as a count formula.
Issue 2: User feedback: 'Complete the calculation directly in one step based on whether rank=1,
without intermediate temporary columns' — hoping to merge the base allocation calculation and
final conditional calculation into a single step.
Second Iteration: Final Version
Two separate corrections were applied:
- Changed * to the specific field projectid (counting projectid within a partition is equivalent to counting rows)
- Restructured the approach: first derive projectid and accountcode from table p separately, then join with table i. This avoids the single-letter table name issue and makes the join explicit.
VariableName |
Anchor |
Statement |
t0 |
p |
derive projectid, accountcode |
t1 |
i |
join projectid; with t0; projectid; take accountcode |
t2 |
sort invoiceid, accountcode |
|
t3 |
rank as seq; partition invoiceid |
|
t4 |
compute count, as N; partition invoiceid |
|
t5 |
compute round(amount / N, 2) as even_split |
|
t6 |
compute if(seq=1 then amount - even_split*(N-1) else even_split) as splitamount |
|
t7 |
derive invoiceid, amount, projectid, accountcode, splitamount |
|
t8 |
sort invoiceid, accountcode |
Validation Example:
projectid=1, amount=100.00, 3 accounts (accountcode=1, 2, 3)
VariableName |
Result |
t1 |
3 rows, each with invoiceid, amount=100.00, projectid=1, accountcode |
t2 |
Sort: accountcode=1, 2, 3 |
t3 |
rank: 1, 2, 3 |
t4 |
N = 3 |
t5 |
even_split = [33.33, 33.33, 33.33] |
t6 |
splitamount = [33.34, 33.33, 33.33], total = 100.00 ✓ |
The split logic is correct: the first account bears the rounding difference of 0.01,
and the total is conserved. Three key lessons from this case: ① * is not supported in the current
SQLazy environment; specific field names must be used; ② in compute statements, aggregate
parameters and cross-row parameters are mutually exclusive and cannot be used simultaneously;
③ reserved words (such as sum) used as names must be enclosed in single quotes.
5. Conclusion
The core value of the Trae + SQLazy combination does not lie in "letting AI automatically write SQL," but rather in constraining the AI's uncertainty within an auditable, debuggable intermediate layer, with a deterministic engine completing the final execution.
In this workflow, the three parties have clear division of labor:
- Trae is responsible for understanding requirements, clarifying ambiguities, and generating the structured initial NSPL — this is what AI does best: structuring fuzzy problems
- SQLazy IDE is responsible for syntax validation, step-by-step debugging, and cross-database compilation — this is the reliable execution of a deterministic engine
- Human is responsible for verifying business definitions, validating test results, and correcting logic deviations — this is irreplaceable business judgment
Reviewing the four cases: from the simple statistics that passed on first attempt, to the multi-table merge generated correctly, to the cross-group filling that required correcting the join key, to the invoice split finalized after two iterations—each step confirms the feasibility of the path of "AI assistance + human review + small-sample validation." In practice, this process can be standardized to make AI a true productivity amplifier rather than a source of risk.
SPL Official Website 👉 https://www.esproc.com
SPL Feedback and Help 👉 https://www.reddit.com/r/esProcSPL
SPL Learning Material 👉 https://c.esproc.com
SPL Source Code and Package 👉 https://github.com/SPLWare/esProc
Discord 👉 https://discord.gg/sxd59A8F2W
Youtube 👉 https://www.youtube.com/@esProc_SPL