Skip to content

[feature](fe) Support ORDER BY and LIMIT clauses for UPDATE and DELETE commands#61681

Open
morrySnow wants to merge 1 commit intoapache:masterfrom
morrySnow:delete-update-limit
Open

[feature](fe) Support ORDER BY and LIMIT clauses for UPDATE and DELETE commands#61681
morrySnow wants to merge 1 commit intoapache:masterfrom
morrySnow:delete-update-limit

Conversation

@morrySnow
Copy link
Contributor

What problem does this PR solve?

Issue Number: close #xxx

Problem Summary: MySQL supports ORDER BY and LIMIT clauses in single-table
UPDATE and DELETE statements, but Doris does not. This PR adds support for
these clauses so users can write statements like:

DELETE FROM table1 ORDER BY c1 ASC NULLS FIRST LIMIT 10, 3;
UPDATE table1 SET c1 = 10 ORDER BY c2 LIMIT 100, 20;

The implementation reuses the existing queryOrganization grammar rule and
withQueryOrganization builder method. For DELETE, when ORDER BY or LIMIT is
present, the statement is routed through DeleteFromUsingCommand (INSERT INTO
SELECT path) which naturally supports sort/limit semantics. For UPDATE, the
queryOrganization is applied to the query plan after withFilter.

Release note

Support ORDER BY and LIMIT clauses in UPDATE and DELETE statements, consistent
with MySQL single-table UPDATE/DELETE syntax.

Check List (For Author)

  • Test: Regression test / Unit Test
    • Parser syntax tests in NereidsParserTest
    • Plan tree structure tests in LogicalPlanBuilderTest
    • DELETE regression tests in test_delete_order_by_limit.groovy
    • UPDATE regression tests in test_update_order_by_limit.groovy
  • Behavior changed: No
  • Does this need documentation: No

@hello-stephen
Copy link
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morrySnow
Copy link
Contributor Author

run buildall

@doris-robot
Copy link

TPC-H: Total hot run time: 26742 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 9fbf062aaef62c7b81f72077c27b4354c39afa6e, data reload: false

------ Round 1 ----------------------------------
orders	Doris	NULL	NULL	0	0	0	NULL	0	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	17657	4639	4341	4341
q2	q3	10629	782	524	524
q4	4671	346	247	247
q5	7558	1220	1021	1021
q6	179	181	149	149
q7	783	843	675	675
q8	9657	1487	1322	1322
q9	5376	4772	4703	4703
q10	6338	1925	1642	1642
q11	492	258	246	246
q12	747	582	462	462
q13	18069	2689	1949	1949
q14	224	244	217	217
q15	q16	752	745	685	685
q17	732	826	492	492
q18	5850	5464	5190	5190
q19	1361	990	616	616
q20	540	504	388	388
q21	4499	1857	1594	1594
q22	452	349	279	279
Total cold run time: 96566 ms
Total hot run time: 26742 ms

----- Round 2, with runtime_filter_mode=off -----
orders	Doris	NULL	NULL	150000000	42	6422171781	NULL	22778155	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	4873	4684	4597	4597
q2	q3	3887	4357	3860	3860
q4	896	1253	822	822
q5	4052	4430	4371	4371
q6	185	182	141	141
q7	1772	1656	1524	1524
q8	2521	2727	2638	2638
q9	7650	7338	7324	7324
q10	3881	4007	3629	3629
q11	517	431	426	426
q12	483	616	474	474
q13	2562	2878	2319	2319
q14	299	320	289	289
q15	q16	729	787	728	728
q17	1195	1461	1469	1461
q18	7324	6891	6565	6565
q19	881	864	908	864
q20	2145	2180	1999	1999
q21	3981	3499	3361	3361
q22	445	425	384	384
Total cold run time: 50278 ms
Total hot run time: 47776 ms

@doris-robot
Copy link

TPC-DS: Total hot run time: 169630 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 9fbf062aaef62c7b81f72077c27b4354c39afa6e, data reload: false

query5	4342	641	514	514
query6	354	226	206	206
query7	4216	462	260	260
query8	360	256	233	233
query9	8747	2735	2712	2712
query10	536	390	365	365
query11	6951	5086	4894	4894
query12	190	134	120	120
query13	1303	486	351	351
query14	5726	3789	3463	3463
query14_1	2869	2865	2847	2847
query15	206	195	178	178
query16	993	466	445	445
query17	941	728	649	649
query18	2451	465	363	363
query19	223	214	187	187
query20	141	127	127	127
query21	217	136	113	113
query22	13127	14134	15170	14134
query23	16613	16276	16250	16250
query23_1	16379	16192	15941	15941
query24	7152	1639	1249	1249
query24_1	1239	1258	1240	1240
query25	593	519	413	413
query26	1243	260	146	146
query27	2788	481	298	298
query28	4462	1852	1835	1835
query29	843	565	491	491
query30	298	228	197	197
query31	1030	945	859	859
query32	80	69	71	69
query33	528	341	284	284
query34	888	880	527	527
query35	632	680	601	601
query36	1051	1114	988	988
query37	128	98	81	81
query38	2960	2965	2887	2887
query39	854	830	831	830
query39_1	794	791	798	791
query40	232	152	135	135
query41	63	62	59	59
query42	259	254	258	254
query43	233	244	216	216
query44	
query45	204	187	181	181
query46	889	981	619	619
query47	2107	2556	2098	2098
query48	307	311	232	232
query49	651	462	386	386
query50	691	285	217	217
query51	4106	4009	3997	3997
query52	266	265	257	257
query53	290	341	285	285
query54	303	267	276	267
query55	93	88	85	85
query56	340	319	324	319
query57	1937	1607	1562	1562
query58	285	276	277	276
query59	2775	2979	2751	2751
query60	344	345	327	327
query61	158	152	144	144
query62	625	594	524	524
query63	314	274	280	274
query64	5069	1277	1009	1009
query65	
query66	1457	448	353	353
query67	24194	24238	24319	24238
query68	
query69	407	313	295	295
query70	965	955	969	955
query71	339	300	315	300
query72	2791	2625	2434	2434
query73	545	560	316	316
query74	9581	9582	9415	9415
query75	2885	2754	2492	2492
query76	2308	1038	688	688
query77	371	425	306	306
query78	11005	11179	10482	10482
query79	2628	770	590	590
query80	1725	643	565	565
query81	549	265	223	223
query82	991	152	113	113
query83	342	264	248	248
query84	297	119	101	101
query85	902	504	468	468
query86	426	319	297	297
query87	3141	3129	2982	2982
query88	3599	2687	2661	2661
query89	436	369	345	345
query90	2013	176	170	170
query91	173	163	136	136
query92	75	79	72	72
query93	1157	823	504	504
query94	634	323	298	298
query95	583	402	324	324
query96	675	511	225	225
query97	2446	2470	2424	2424
query98	240	220	219	219
query99	1001	993	934	934
Total cold run time: 252069 ms
Total hot run time: 169630 ms

@hello-stephen
Copy link
Contributor

FE UT Coverage Report

Increment line coverage 66.67% (4/6) 🎉
Increment coverage report
Complete coverage report

@morrySnow
Copy link
Contributor Author

/review

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR adds ORDER BY and LIMIT clause support for single-table UPDATE and DELETE statements, consistent with MySQL syntax. The implementation reuses existing queryOrganization grammar and withQueryOrganization builder method.

Critical Checkpoint Conclusions

1. Goal accomplishment: The grammar and builder changes correctly parse ORDER BY/LIMIT for both UPDATE and DELETE, producing the expected logical plan tree (Sort -> Limit above Filter). The approach of routing DELETE with ORDER BY/LIMIT through DeleteFromUsingCommand (INSERT INTO SELECT path) is sound since the simple DeleteFromCommand path only supports predicate-based deletion.

2. Modification clarity: The change is focused and minimal — 2 grammar lines, ~10 lines of builder logic, plus tests.

3. Concurrency: No new concurrency concerns. The changes are in the parser/plan-building phase (single-threaded per query).

4. Lifecycle management: No new lifecycle concerns.

5. Configuration: No new configuration items.

6. Incompatible changes: The grammar is purely additive (new optional clauses). Existing SQL continues to work unchanged because queryOrganization can match empty (both sortClause? and limitClause? are optional).

7. Parallel code paths: The visitUpdate and visitDelete paths are both correctly updated. The withQueryOrganization call is a no-op when neither ORDER BY nor LIMIT is present.

8. Test coverage: Good positive test coverage for both UPDATE and DELETE with ORDER BY, LIMIT, OFFSET, WHERE combinations. Unit tests verify plan tree structure. However, negative test cases are completely missing (see inline comments).

9. Observability: No new observability concerns.

10. Data correctness: The plan tree structure is correct — Sort/Limit operate on the filtered result set before the Project layer adds DELETE_SIGN or UPDATE assignments. The INSERT INTO SELECT execution path handles atomicity.

Issues Found

  1. [Medium] Misleading error message on non-UNIQUE KEY tables: When DELETE FROM dup_table WHERE c1=1 LIMIT 5 is executed on a DUPLICATE KEY table, the error message says "delete command on with using clause only supports unique key model" even though the user never used a USING clause. This is because the ORDER BY/LIMIT path forces routing through DeleteFromUsingCommand which has this error message. The error message should be updated or a separate check should be added before routing. Note: plain DELETE FROM dup_table WHERE c1=1 (without LIMIT) works fine on DUP tables through DeleteHandler.

  2. [Medium] Missing negative test cases: No test { sql ...; exception ... } blocks in either regression test file. Should test at minimum: ORDER BY on non-existent column, LIMIT on non-UNIQUE KEY table (for DELETE), and LIMIT 0 edge case.

  3. [Low] Missing UPDATE with LIMIT-only test: The DELETE regression test has a LIMIT-only (no ORDER BY) case, but the UPDATE test does not.

query = withRelations(query, ((FromRelationsContext) ctx.fromClause()).relations().relation());
}
query = withFilter(query, Optional.ofNullable(ctx.whereClause()));
query = withQueryOrganization(query, ctx.queryOrganization());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When hasQueryOrganization is true (e.g., DELETE FROM dup_table LIMIT 5), this routes to DeleteFromUsingCommand which rejects non-UNIQUE KEY tables with the error "delete command on with using clause only supports unique key model". This is misleading because the user did not use a USING clause.

Plain DELETE FROM dup_table WHERE c1=1 (no LIMIT) works fine on DUP/AGG tables through DeleteHandler.process(), but adding LIMIT changes the error behavior.

Consider either:

  1. Updating DeleteFromUsingCommand.checkTargetTable() to provide a clearer error message when ORDER BY/LIMIT is used (e.g., "DELETE with ORDER BY/LIMIT only supports unique key model"), or
  2. Adding a dedicated check here before routing to DeleteFromUsingCommand that gives a clear error for non-UNIQUE KEY tables.

// skip 1 (id=10), delete 2 (id=9, id=8)
sql "DELETE FROM test_delete_obl ORDER BY c1 ASC LIMIT 2 OFFSET 1;"
order_qt_delete_limit_offset_syntax """SELECT * FROM test_delete_obl ORDER BY id;"""
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing negative test cases. Per review standards, regression tests should include test { sql ...; exception ... } blocks for error scenarios. Recommended additions:

  1. DELETE with ORDER BY/LIMIT on a DUPLICATE KEY table — should produce a clear error
  2. DELETE with ORDER BY referencing a non-existent column
  3. DELETE with LIMIT 0 — verify behavior (no-op or error?)

// update both c1 and c2 for the 3 rows with largest c1
sql "UPDATE test_update_obl SET c1 = 999, c2 = 'top3' ORDER BY c1 DESC LIMIT 3;"
order_qt_update_multi_set """SELECT * FROM test_update_obl ORDER BY id;"""
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing negative test cases and a LIMIT-only (no ORDER BY) case.

Recommended additions:

  1. test { sql "UPDATE non_unique_table SET c1=1 ORDER BY c2 LIMIT 5"; exception "..." } — UPDATE on non-UNIQUE KEY table
  2. test { sql "UPDATE test_update_obl SET c1=1 ORDER BY nonexistent LIMIT 5"; exception "..." } — non-existent ORDER BY column
  3. UPDATE test_update_obl SET c1=1 LIMIT 5 — LIMIT without ORDER BY (positive case, for parity with DELETE test)

@github-actions
Copy link
Contributor

PR approved by anyone and no changes requested.

…E commands

### What problem does this PR solve?

Issue Number: close #xxx

Problem Summary: MySQL supports ORDER BY and LIMIT clauses in single-table
UPDATE and DELETE statements, but Doris does not. This PR adds support for
these clauses so users can write statements like:

  DELETE FROM table1 ORDER BY c1 ASC NULLS FIRST LIMIT 10, 3;
  UPDATE table1 SET c1 = 10 ORDER BY c2 LIMIT 100, 20;

The implementation reuses the existing queryOrganization grammar rule and
withQueryOrganization builder method. For DELETE, when ORDER BY or LIMIT is
present, the statement is routed through DeleteFromUsingCommand (INSERT INTO
SELECT path) which naturally supports sort/limit semantics. For UPDATE, the
queryOrganization is applied to the query plan after withFilter.

### Release note

Support ORDER BY and LIMIT clauses in UPDATE and DELETE statements, consistent
with MySQL single-table UPDATE/DELETE syntax.

### Check List (For Author)

- Test: Regression test / Unit Test
    - Parser syntax tests in NereidsParserTest
    - Plan tree structure tests in LogicalPlanBuilderTest
    - DELETE regression tests in test_delete_order_by_limit.groovy
    - UPDATE regression tests in test_update_order_by_limit.groovy
- Behavior changed: No
- Does this need documentation: No

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@morrySnow morrySnow force-pushed the delete-update-limit branch from d901526 to 5d7fb19 Compare March 25, 2026 07:27
@morrySnow
Copy link
Contributor Author

run buildall

@doris-robot
Copy link

TPC-H: Total hot run time: 26487 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 5d7fb193c4e08ecfef80b919a00d085442696dd6, data reload: false

------ Round 1 ----------------------------------
orders	Doris	NULL	NULL	0	0	0	NULL	0	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	17602	4431	4324	4324
q2	q3	10642	783	541	541
q4	4685	349	250	250
q5	7551	1205	1011	1011
q6	181	181	156	156
q7	813	858	687	687
q8	9310	1472	1343	1343
q9	4865	4697	4706	4697
q10	6249	1884	1654	1654
q11	464	263	248	248
q12	716	588	468	468
q13	18049	2684	1936	1936
q14	223	235	214	214
q15	q16	712	750	661	661
q17	735	838	448	448
q18	5898	5284	5194	5194
q19	1250	978	619	619
q20	546	486	375	375
q21	4722	1848	1413	1413
q22	339	294	248	248
Total cold run time: 95552 ms
Total hot run time: 26487 ms

----- Round 2, with runtime_filter_mode=off -----
orders	Doris	NULL	NULL	150000000	42	6422171781	NULL	22778155	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	4874	4663	4576	4576
q2	q3	3886	4341	3819	3819
q4	861	1213	785	785
q5	4112	4397	4332	4332
q6	191	181	142	142
q7	1785	1648	1527	1527
q8	2504	2754	2558	2558
q9	7813	7411	7435	7411
q10	3760	3956	3622	3622
q11	516	439	430	430
q12	483	602	444	444
q13	2409	2865	2028	2028
q14	284	295	269	269
q15	q16	711	754	723	723
q17	1172	1401	1447	1401
q18	7308	6838	6556	6556
q19	903	933	906	906
q20	2080	2210	2028	2028
q21	3925	3474	3347	3347
q22	477	423	387	387
Total cold run time: 50054 ms
Total hot run time: 47291 ms

@doris-robot
Copy link

TPC-DS: Total hot run time: 168904 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 5d7fb193c4e08ecfef80b919a00d085442696dd6, data reload: false

query5	4341	646	522	522
query6	350	239	212	212
query7	4235	476	278	278
query8	349	254	234	234
query9	8737	2734	2691	2691
query10	528	403	346	346
query11	7024	5088	4870	4870
query12	211	133	141	133
query13	1253	443	335	335
query14	5761	3621	3398	3398
query14_1	2786	2813	2771	2771
query15	198	193	175	175
query16	977	439	458	439
query17	864	696	608	608
query18	2465	434	335	335
query19	212	211	180	180
query20	136	129	123	123
query21	212	132	109	109
query22	13323	14204	14822	14204
query23	16628	16109	16188	16109
query23_1	16121	15696	15666	15666
query24	7240	1624	1180	1180
query24_1	1218	1220	1210	1210
query25	541	468	414	414
query26	1242	256	144	144
query27	2804	478	306	306
query28	4478	1823	1836	1823
query29	848	557	478	478
query30	292	229	188	188
query31	1013	943	867	867
query32	85	71	82	71
query33	515	337	282	282
query34	887	887	523	523
query35	622	680	598	598
query36	1076	1140	945	945
query37	135	90	83	83
query38	2943	2947	2856	2856
query39	842	824	805	805
query39_1	792	798	781	781
query40	229	157	137	137
query41	62	60	58	58
query42	262	258	268	258
query43	235	245	219	219
query44	
query45	196	188	184	184
query46	881	1012	605	605
query47	2114	2139	2052	2052
query48	310	319	221	221
query49	646	467	378	378
query50	690	288	209	209
query51	4056	4036	3958	3958
query52	263	265	252	252
query53	287	339	284	284
query54	315	281	259	259
query55	94	82	84	82
query56	323	323	315	315
query57	1915	1759	1837	1759
query58	287	270	276	270
query59	2777	2968	2756	2756
query60	344	344	338	338
query61	155	164	149	149
query62	637	582	528	528
query63	318	290	276	276
query64	4990	1286	1025	1025
query65	
query66	1463	458	361	361
query67	24275	24291	24186	24186
query68	
query69	401	310	286	286
query70	979	960	964	960
query71	352	306	313	306
query72	3011	2919	2663	2663
query73	539	546	319	319
query74	9643	9536	9361	9361
query75	2887	2793	2532	2532
query76	2279	1026	663	663
query77	362	381	312	312
query78	10966	11127	10439	10439
query79	1107	764	571	571
query80	808	644	546	546
query81	500	262	227	227
query82	1357	151	118	118
query83	341	259	248	248
query84	296	118	96	96
query85	967	500	452	452
query86	419	302	277	277
query87	3184	3111	3046	3046
query88	3522	2630	2644	2630
query89	436	374	339	339
query90	1794	185	176	176
query91	180	159	140	140
query92	83	76	76	76
query93	903	857	495	495
query94	486	323	295	295
query95	588	404	323	323
query96	651	516	225	225
query97	2452	2492	2408	2408
query98	251	228	227	227
query99	1039	987	901	901
Total cold run time: 249571 ms
Total hot run time: 168904 ms

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants