{"id":2503,"date":"2025-05-07T22:12:50","date_gmt":"2025-05-07T20:12:50","guid":{"rendered":"https:\/\/blog.sqlora.com\/en\/?p=2503"},"modified":"2025-09-18T15:10:53","modified_gmt":"2025-09-18T13:10:53","slug":"merge-and-dml-returning-clause-in-oracle-23ai","status":"publish","type":"post","link":"https:\/\/blog.sqlora.com\/en\/merge-and-dml-returning-clause-in-oracle-23ai\/","title":{"rendered":"MERGE and DML RETURNING clause in Oracle 23ai"},"content":{"rendered":"\n<p>For a long time, we all &#8220;knew&#8221; that <strong>MERGE <\/strong>did not support a <strong>RETURNING <\/strong>clause. You can ask ChatGPT, Gemini, or Google, and the answer would still be the same: Nope, no luck! LLMs respond with confidence, and Google&#8217;s top results show various workarounds for the problem. I swear I saw the lifting of this restriction in the New Features Guide back when the version was still called 23c :-).   Since I\u2019m currently preparing a conference talk where this is relevant, I naturally wanted to check whether my memory was just playing tricks on me \u2014 or if it\u2019s actually true.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">TL;DR:<\/h2>\n\n\n\n<p>Yes, Oracle 23ai now supports <strong>MERGE &#8230; RETURNING<\/strong>. ChatGPT and Gemini still deny it, even inventing fake error messages. But the Oracle 23ai documentation quietly confirms it \u2014 and real tests prove it works, including <strong>OLD<\/strong> and <strong>NEW <\/strong>value support.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A Word of Caution<\/h2>\n\n\n\n<p>In today\u2019s fast-paced world, everyone wants to save time and optimize effort. As an Oracle developer, why dig through AskTom, Oracle Docs, or StackOverflow when ChatGPT gives you a confident answer, complete with examples?<\/p>\n\n\n\n<p>Having used LLMs a lot in recent weeks and months, I\u2019m impressed by their progress. But despite improvements, hallucinations are still very much a thing \u2014 and some are tricky to spot. You need to know your stuff.<\/p>\n\n\n\n<p>Both ChatGPT and Google Gemini insist that <strong>MERGE <\/strong>with <strong>RETURNING <\/strong>is impossible. ChatGPT even invents a fictitious error message \u2014 <strong>ORA-38911<\/strong> \u2014 which <em>does<\/em> exist, but has nothing to do with <strong>MERGE <\/strong>or <strong>RETURNING<\/strong>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/GPT_returning.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"1000\" height=\"159\" src=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/GPT_returning.jpg\" alt=\"\" class=\"wp-image-2506\" srcset=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/GPT_returning.jpg 1000w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/GPT_returning-300x48.jpg 300w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/GPT_returning-768x122.jpg 768w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/GPT_returning-624x99.jpg 624w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/a><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/ora-38911.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"1000\" height=\"168\" src=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/ora-38911.jpg\" alt=\"\" class=\"wp-image-2512\" srcset=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/ora-38911.jpg 1000w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/ora-38911-300x50.jpg 300w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/ora-38911-768x129.jpg 768w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/ora-38911-624x105.jpg 624w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/a><\/figure>\n\n\n\n<p><strong><em>Figure 1: ChatGPT with fictitious error message<\/em><\/strong><\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/gemini_returning.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"1000\" height=\"204\" src=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/gemini_returning.jpg\" alt=\"\" class=\"wp-image-2508\" srcset=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/gemini_returning.jpg 1000w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/gemini_returning-300x61.jpg 300w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/gemini_returning-768x157.jpg 768w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/gemini_returning-624x127.jpg 624w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/a><\/figure>\n\n\n\n<p><strong><em>Figure 2: Also Google&#8217;s LLM doesn&#8217;t know about new feature<\/em><\/strong><\/p>\n\n\n\n<p>I\u2019m not saying don\u2019t use LLMs \u2014 just <em>always<\/em> double-check the answers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A Hard-to-Find Feature<\/h2>\n\n\n\n<p>Next stop: Oracle documentation. And yes, you really have to look twice. In the New Features Guide, the section is called <strong>SQL UPDATE RETURN clause enhancements<\/strong>, and <strong>MERGE <\/strong>is just mentioned casually \u2014 as if it had always supported <strong>RETURNING<\/strong>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/Merge_returning.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"1000\" height=\"129\" src=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/Merge_returning.jpg\" alt=\"\" class=\"wp-image-2516\" srcset=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/Merge_returning.jpg 1000w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/Merge_returning-300x39.jpg 300w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/Merge_returning-768x99.jpg 768w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2025\/05\/Merge_returning-624x80.jpg 624w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/a><\/figure>\n\n\n\n<p><strong><em>Figure 3: MERGE casually dropped in under \u201cnew\/old values\u201d enhancements<\/em><\/strong><\/p>\n\n\n\n<p>Looking at the <a class=\"\" href=\"https:\/\/docs.oracle.com\/en\/database\/oracle\/oracle-database\/23\/sqlrf\/MERGE.html\">MERGE documentation page<\/a>, you\u2019ll find the <strong>RETURNING <\/strong>clause shown in the syntax railroad diagram \u2014 but not a single word about it in the actual text!<\/p>\n\n\n\n<p>Curious, I wondered whether I had missed this feature already being added in Oracle 21c? A quick check confirms: nope. Back then, <strong>RETURNING <\/strong>was only documented for <a href=\"https:\/\/docs.oracle.com\/en\/database\/oracle\/oracle-database\/21\/lnpls\/RETURNING-INTO-clause.html\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">INSERT, UPDATE AND DELETE<\/a>. Digging deeper into the 23ai docs, the same section from &#8220;Database PL\/SQL Language Reference&#8221; <a href=\"https:\/\/docs.oracle.com\/en\/database\/oracle\/oracle-database\/23\/lnpls\/RETURNING-INTO-clause.html\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">documents the usage with MERGE<\/a>!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Test<\/h2>\n\n\n\n<p>Let\u2019s try it out to be sure. Listing 1 shows that the <strong>RETURNING <\/strong>clause works with <strong>MERGE<\/strong>, and that the 23ai enhancements like <strong>OLD <\/strong>and <strong>NEW <\/strong>values are fully functional.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; highlight: [19]; title: ; notranslate\" title=\"\">\nSQL&gt; SELECT * FROM emp_sal_increase\n\n    DEPTNO INCREASE_PCT\n---------- ------------\n        20           20\n1 row selected.\n\nSQL&gt; DECLARE \nTYPE t_sal_tab IS TABLE OF emp.sal%TYPE;\nTYPE t_empno_tab IS TABLE OF emp.empno%TYPE;\nl_empno t_empno_tab;\nl_salo t_sal_tab;\nl_saln t_sal_tab;\nBEGIN \n    MERGE INTO emp t\n    USING  emp_sal_increase  q\n    ON    (t.deptno = q.deptno)\n    WHEN MATCHED THEN UPDATE SET t.sal=t.sal*(1+q.increase_pct\/100)\n    RETURNING empno, OLD sal, NEW sal BULK COLLECT INTO l_empno, l_salo, l_saln;\n   \n    FOR i IN l_salo.first .. l_salo.last LOOP\n      DBMS_OUTPUT.put_line(&#039;EMPNO=&#039; || l_empno(i)|| &#039;, SAL changed from &#039;||l_salo(i) ||&#039; to &#039; ||l_saln(i));\n    END LOOP;\nEND;\n\nPL\/SQL procedure successfully completed.\n\nEMPNO=7369, SAL changed from 800 to 960\nEMPNO=7566, SAL changed from 2975 to 3570\nEMPNO=7788, SAL changed from 3000 to 3600\nEMPNO=7876, SAL changed from 1100 to 1320\nEMPNO=7902, SAL changed from 3000 to 3600\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 1: Returning clause works for MERGE in 23ai<\/em><\/strong><\/p>\n\n\n\n<p>Listing 1 shows <strong>BULK COLLECT<\/strong> for multiple rows, but it works with scalar values too \u2014 even aggregate functions, as shown in Listing 2.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; highlight: [9]; title: ; notranslate\" title=\"\">\nSQL&gt; DECLARE \nv_old_avg NUMBER;\nv_new_avg NUMBER;\nBEGIN \n    MERGE INTO emp t\n    USING  emp_sal_increase  q\n    ON    (t.deptno = q.deptno)\n    WHEN MATCHED THEN UPDATE SET t.sal=t.sal*(1+q.increase_pct\/100)\n    RETURNING AVG(old sal), AVG(new sal)  INTO v_old_avg, v_new_avg;\n    \n    DBMS_OUTPUT.put_line(&#039;Average SAL changed from &#039;||v_old_avg ||&#039; to &#039; ||v_new_avg);\n    \nEND;\nPL\/SQL procedure successfully completed.\n\nAverage SAL changed from 2175 to 2610\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 2: Returning with scalar values<\/em><\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>So yes, <strong>MERGE &#8230; RETURNING<\/strong> now works in Oracle 23ai \u2014 including with <strong>OLD <\/strong>and <strong>NEW <\/strong>values. It\u2019s real, it\u2019s documented (somewhere), and can be quite useful.<\/p>\n\n\n\n<p><strong>But Oracle, if you&#8217;re listening: could we maybe highlight this a bit more clearly in the documentation?<\/strong><br>This was a long-awaited feature \u2014 many developers have been hoping for it for years. No need to hide it in a syntax diagram. Give it the spotlight it deserves!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">UPDATE <\/h2>\n\n\n\n<p>Please check <a href=\"https:\/\/connor-mcdonald.com\/2025\/09\/18\/merge-in-23ai-so-much-more-than-returning\/\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">Connor McDonald&#8217;s post<\/a> about <strong>MERGE &#8230; RETURNING<\/strong> clause in 23ai and using it to get separated <strong><em>rows inserted<\/em><\/strong> and <em><strong>rows updated<\/strong><\/em> counts. Additionally, he suggests using <strong>MERGE<\/strong> instead of <strong>INSERT AS SELECT<\/strong> when a <strong>RETURNING<\/strong> clause is needed.<\/p>\n\n\n<script async src=\"\/\/pagead2.googlesyndication.com\/pagead\/js\/adsbygoogle.js?client=ca-pub-2925154690547867\" crossorigin=\"anonymous\"><\/script><ins class=\"adsbygoogle\" style=\"display:block; text-align:center;\" data-ad-client=\"ca-pub-2925154690547867\" \ndata-ad-slot=\"2727403138\" \ndata-ad-layout=\"in-article\"\ndata-ad-format=\"fluid\"><\/ins>\n<script> \n(adsbygoogle = window.adsbygoogle || []).push({}); \n<\/script>\n\n\n<div class=\"crp-list-container\"><h3 class=\"crp-list-title\">Related Posts<\/h3><ul class=\"crp-list\"><li class=\"crp-list-item crp-list-item-image-none\"><div class=\"crp-list-item-title\"><a href=\"https:\/\/blog.sqlora.com\/en\/ora-30926-in-oracle-23ai-whats-changed\/\" target=\"_blank\">ORA-30926 in Oracle 23ai: What\u2019s Changed?<\/a><\/div><\/li><\/ul><\/div>","protected":false},"excerpt":{"rendered":"<p>For a long time, we all &#8220;knew&#8221; that MERGE did not support a RETURNING clause. You can ask ChatGPT, Gemini, or Google, and the answer would still be the same: Nope, no luck! LLMs respond with confidence, and Google&#8217;s top results show various workarounds for the problem. I swear I saw the lifting of this [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":2528,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[143,1,46,2,3],"tags":[6,51],"class_list":["post-2503","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-23ai","category-general","category-merge","category-oracle","category-sql","tag-merge","tag-sql"],"_links":{"self":[{"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts\/2503","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/comments?post=2503"}],"version-history":[{"count":24,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts\/2503\/revisions"}],"predecessor-version":[{"id":2555,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts\/2503\/revisions\/2555"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/media\/2528"}],"wp:attachment":[{"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/media?parent=2503"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/categories?post=2503"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/tags?post=2503"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}