v5-0004-jsonb-optimize-multi-subscript-casts-via-extract-.patch

application/octet-stream

Filename: v5-0004-jsonb-optimize-multi-subscript-casts-via-extract-.patch
Type: application/octet-stream
Part: 4
Message: Re: Extract numeric filed in JSONB more effectively

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v5-0004
Subject: jsonb: optimize multi-subscript casts via extract-path rewrite
File+
src/backend/utils/adt/jsonb.c 101 15
src/test/regress/expected/jsonb.out 157 0
src/test/regress/sql/jsonb.sql 43 0
From 1c5bc428388e6a4f97c1e684b7705b98e5d0baf9 Mon Sep 17 00:00:00 2001
From: Haibo Yan <haibo.yan@apple.com>
Date: Sun, 26 Apr 2026 18:49:33 -0700
Subject: [PATCH v5 4/5] jsonb: optimize multi-subscript casts via extract-path
 rewrite

Extend the existing support-function rewrite to multi-subscript jsonb
subscripting chains, lowering them to the extract-path typed extractor
family already introduced by the series.

Only chains whose integer subscripts are all constants are eligible for
the rewrite; non-constant integer subscripts (which would require a
runtime CoerceViaIO conversion) cause the entire chain to be left in
its original form.

This keeps single-subscript behavior unchanged and supports casts to
numeric, bool, int2, int4, int8, float4, and float8.
---
 src/backend/utils/adt/jsonb.c       | 116 +++++++++++++++++---
 src/test/regress/expected/jsonb.out | 157 ++++++++++++++++++++++++++++
 src/test/regress/sql/jsonb.sql      |  43 ++++++++
 3 files changed, 301 insertions(+), 15 deletions(-)

diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 7eba437b8a4..d5c0871a213 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -1835,6 +1835,7 @@ cannotCastJsonbValue(enum jbvType type, const char *sqltype, Node *escontext)
  *   - jsonb_object_field(j, 'key')  /  j -> 'key'  /  j['key']
  *   - jsonb_array_element(j, idx)   /  j -> idx    /  j[idx]
  *   - jsonb_extract_path(j, ...)    /  j #> '{a,b}'
+ *   - multi-subscript chains         j['a']['b'], j['a'][0], etc.
  */
 Datum
 jsonb_cast_support(PG_FUNCTION_ARGS)
@@ -1885,35 +1886,120 @@ jsonb_cast_support(PG_FUNCTION_ARGS)
 		else if (IsA(arg, SubscriptingRef))
 		{
 			SubscriptingRef *sbsref = (SubscriptingRef *) arg;
-			Node	   *subscript;
-			Oid			subscript_type;
+			int			nsubscripts;
 
 			/*
-			 * Handle single-subscript jsonb access with no slice and no
-			 * assignment.  Text subscripts map to object-field extraction;
-			 * int4 subscripts map to array-element extraction.
+			 * Handle jsonb subscript access with no slice and no assignment.
+			 * Single subscripts map to object-field or array-element
+			 * extraction; multi-subscript chains lower to the extract-path
+			 * family.
 			 */
 			if (sbsref->refcontainertype != JSONBOID)
 				PG_RETURN_POINTER(NULL);
-			if (list_length(sbsref->refupperindexpr) != 1)
-				PG_RETURN_POINTER(NULL);
 			if (sbsref->reflowerindexpr != NIL)
 				PG_RETURN_POINTER(NULL);
 			if (sbsref->refassgnexpr != NULL)
 				PG_RETURN_POINTER(NULL);
 
-			subscript = (Node *) linitial(sbsref->refupperindexpr);
-			subscript_type = exprType(subscript);
+			nsubscripts = list_length(sbsref->refupperindexpr);
 
-			if (subscript_type == TEXTOID)
+			if (nsubscripts == 1)
 			{
-				inner_funcid = F_JSONB_OBJECT_FIELD;
-				inner_args = list_make2(sbsref->refexpr, subscript);
+				/*
+				 * Single subscript: text maps to object-field, int4 maps to
+				 * array-element.
+				 */
+				Node	   *subscript;
+				Oid			subscript_type;
+
+				subscript = (Node *) linitial(sbsref->refupperindexpr);
+				subscript_type = exprType(subscript);
+
+				if (subscript_type == TEXTOID)
+				{
+					inner_funcid = F_JSONB_OBJECT_FIELD;
+					inner_args = list_make2(sbsref->refexpr, subscript);
+				}
+				else if (subscript_type == INT4OID)
+				{
+					inner_funcid = F_JSONB_ARRAY_ELEMENT;
+					inner_args = list_make2(sbsref->refexpr, subscript);
+				}
+				else
+					PG_RETURN_POINTER(NULL);
 			}
-			else if (subscript_type == INT4OID)
+			else if (nsubscripts >= 2)
 			{
-				inner_funcid = F_JSONB_ARRAY_ELEMENT;
-				inner_args = list_make2(sbsref->refexpr, subscript);
+				/*
+				 * Multi-subscript chain: build a text[] path and lower to
+				 * the extract-path family.  Each subscript must be text or
+				 * a constant int4; non-constant integer subscripts cause
+				 * the entire chain to be left unoptimized.
+				 */
+				List	   *path_elems = NIL;
+				ListCell   *lc;
+				ArrayExpr  *aexpr;
+
+				foreach(lc, sbsref->refupperindexpr)
+				{
+					Node	   *subscript = (Node *) lfirst(lc);
+					Oid			subscript_type = exprType(subscript);
+
+					if (subscript_type == TEXTOID)
+					{
+						path_elems = lappend(path_elems, subscript);
+					}
+					else if (subscript_type == INT4OID)
+					{
+						Const	   *con;
+
+						/*
+						 * Only constant integer subscripts can be safely
+						 * converted to text at plan time.  Non-constant
+						 * ones would require a runtime CoerceViaIO node;
+						 * decline the rewrite for the entire chain.
+						 */
+						if (!IsA(subscript, Const))
+							PG_RETURN_POINTER(NULL);
+
+						con = (Const *) subscript;
+
+						if (con->constisnull)
+						{
+							path_elems = lappend(path_elems,
+												 makeNullConst(TEXTOID,
+															   -1,
+															   InvalidOid));
+						}
+						else
+						{
+							char	   *str;
+
+							str = DatumGetCString(
+								DirectFunctionCall1(int4out,
+													con->constvalue));
+							path_elems = lappend(path_elems,
+								makeConst(TEXTOID, -1, InvalidOid, -1,
+										  CStringGetTextDatum(str),
+										  false, false));
+						}
+					}
+					else
+						PG_RETURN_POINTER(NULL);
+				}
+
+				aexpr = makeNode(ArrayExpr);
+				aexpr->array_typeid = TEXTARRAYOID;
+				aexpr->array_collid = InvalidOid;
+				aexpr->element_typeid = TEXTOID;
+				aexpr->elements = path_elems;
+				aexpr->multidims = false;
+				aexpr->list_start = -1;
+				aexpr->list_end = -1;
+				aexpr->location = -1;
+
+				inner_funcid = F_JSONB_EXTRACT_PATH;
+				inner_args = list_make2(sbsref->refexpr, aexpr);
 			}
 			else
 				PG_RETURN_POINTER(NULL);
diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out
index b96c095525c..3198442352c 100644
--- a/src/test/regress/expected/jsonb.out
+++ b/src/test/regress/expected/jsonb.out
@@ -1474,6 +1474,163 @@ SELECT jsonb_extract_path_float4('{"a":3.14}'::jsonb, ARRAY['a']);
                       3.14
 (1 row)
 
+-- Optimized typed extraction: multi-subscript chains
+-- The planner lowers j['a']['b'], j['a'][0], etc. to the extract-path typed
+-- extractor family, reusing the same functions as the #> operator path.
+-- Section M1: planner rewrite verification for multi-subscript chains
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field6']['f1'])::int4 FROM test_jsonb WHERE json_type = 'object';
+                                   QUERY PLAN                                    
+---------------------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+   Output: jsonb_extract_path_int4(test_json, ARRAY['field6'::text, 'f1'::text])
+   Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field5'][0])::int4 FROM test_jsonb WHERE json_type = 'object';
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+   Output: jsonb_extract_path_int4(test_json, ARRAY['field5'::text, '0'::text])
+   Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_arr[6]['k'])::int4 FROM test_jsonb_arr;
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb_arr
+   Output: jsonb_extract_path_int4(test_arr, ARRAY['6'::text, 'k'::text])
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field6']['f1'])::float8 FROM test_jsonb WHERE json_type = 'object';
+                                    QUERY PLAN                                     
+-----------------------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+   Output: jsonb_extract_path_float8(test_json, ARRAY['field6'::text, 'f1'::text])
+   Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field6']['f1'])::numeric FROM test_jsonb WHERE json_type = 'object';
+                                     QUERY PLAN                                     
+------------------------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+   Output: jsonb_extract_path_numeric(test_json, ARRAY['field6'::text, 'f1'::text])
+   Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+-- Verify single subscript still uses the existing object-field family, not extract-path
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field4'])::int4 FROM test_jsonb WHERE json_type = 'object';
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+   Output: jsonb_object_field_int4(test_json, 'field4'::text)
+   Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+-- Section M1b: multi-subscript int2/float4
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field6']['f1'])::int2 FROM test_jsonb WHERE json_type = 'object';
+                                   QUERY PLAN                                    
+---------------------------------------------------------------------------------
+ Seq Scan on pg_temp.test_jsonb
+   Output: jsonb_extract_path_int2(test_json, ARRAY['field6'::text, 'f1'::text])
+   Filter: (test_jsonb.json_type = 'object'::text)
+(3 rows)
+
+SELECT (('{"a":{"b":7}}'::jsonb)['a']['b'])::int2;
+ int2 
+------
+    7
+(1 row)
+
+-- Section M2: execution through multi-subscript rewrite
+SELECT (('{"a":{"b":42}}'::jsonb)['a']['b'])::int4;
+ int4 
+------
+   42
+(1 row)
+
+SELECT (('{"a":[10,20,30]}'::jsonb)['a'][1])::int4;
+ int4 
+------
+   20
+(1 row)
+
+SELECT (('[{"a":true}]'::jsonb)[0]['a'])::bool;
+ bool 
+------
+ t
+(1 row)
+
+SELECT (('[[1,2],[3,4]]'::jsonb)[0][1])::int4;
+ int4 
+------
+    2
+(1 row)
+
+SELECT (('{"a":[{"b":3.14}]}'::jsonb)['a'][0]['b'])::float8;
+ float8 
+--------
+   3.14
+(1 row)
+
+SELECT (('{"a":{"b":42}}'::jsonb)['a']['b'])::numeric;
+ numeric 
+---------
+      42
+(1 row)
+
+SELECT (('{"a":{"b":42}}'::jsonb)['a']['b'])::int8;
+ int8 
+------
+   42
+(1 row)
+
+-- negative index in nested chain
+SELECT (('{"a":[10,20,30]}'::jsonb)['a'][-1])::int4;
+ int4 
+------
+   30
+(1 row)
+
+-- Section M3: NULL semantics for multi-subscript chains
+SELECT (('{"a":1}'::jsonb)['x']['b'])::int4;  -- missing intermediate key
+ int4 
+------
+     
+(1 row)
+
+SELECT (('{"a":{"c":1}}'::jsonb)['a']['b'])::int4;  -- missing final key
+ int4 
+------
+     
+(1 row)
+
+SELECT (('{"a":{"b":null}}'::jsonb)['a']['b'])::int4;  -- JSON null leaf
+ int4 
+------
+     
+(1 row)
+
+SELECT (('{"a":[1]}'::jsonb)['a'][5])::int4;  -- out-of-range nested index
+ int4 
+------
+     
+(1 row)
+
+-- Section M4: type-mismatch errors for multi-subscript chains
+SELECT (('{"a":{"b":"hello"}}'::jsonb)['a']['b'])::int4;  -- string to int4
+ERROR:  cannot cast jsonb string to type integer
+SELECT (('{"a":{"b":[1,2]}}'::jsonb)['a']['b'])::int4;  -- container to int4
+ERROR:  cannot cast jsonb array to type integer
+-- Section M5: non-constant int4 subscript declines rewrite
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT (('{"a":[[10,20],[30,40]]}'::jsonb)['a'][i][0])::int4 FROM generate_series(0,1) AS i;
+                                  QUERY PLAN                                  
+------------------------------------------------------------------------------
+ Function Scan on pg_catalog.generate_series i
+   Output: (('{"a": [[10, 20], [30, 40]]}'::jsonb)['a'::text][i][0])::integer
+   Function Call: generate_series(0, 1)
+(3 rows)
+
 SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'scalar';
  ?column? 
 ----------
diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql
index 11191fd0f82..ed70c4d29ee 100644
--- a/src/test/regress/sql/jsonb.sql
+++ b/src/test/regress/sql/jsonb.sql
@@ -419,6 +419,49 @@ SELECT jsonb_extract_path_int2('{"a":{"b":7}}'::jsonb, ARRAY['a','b']);
 SELECT jsonb_extract_path_float4('{"a":3.14}'::jsonb, ARRAY['a']);
 
 
+-- Optimized typed extraction: multi-subscript chains
+-- The planner lowers j['a']['b'], j['a'][0], etc. to the extract-path typed
+-- extractor family, reusing the same functions as the #> operator path.
+
+-- Section M1: planner rewrite verification for multi-subscript chains
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field6']['f1'])::int4 FROM test_jsonb WHERE json_type = 'object';
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field5'][0])::int4 FROM test_jsonb WHERE json_type = 'object';
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_arr[6]['k'])::int4 FROM test_jsonb_arr;
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field6']['f1'])::float8 FROM test_jsonb WHERE json_type = 'object';
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field6']['f1'])::numeric FROM test_jsonb WHERE json_type = 'object';
+
+-- Verify single subscript still uses the existing object-field family, not extract-path
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field4'])::int4 FROM test_jsonb WHERE json_type = 'object';
+
+
+-- Section M1b: multi-subscript int2/float4
+EXPLAIN (VERBOSE, COSTS OFF) SELECT (test_json['field6']['f1'])::int2 FROM test_jsonb WHERE json_type = 'object';
+SELECT (('{"a":{"b":7}}'::jsonb)['a']['b'])::int2;
+-- Section M2: execution through multi-subscript rewrite
+SELECT (('{"a":{"b":42}}'::jsonb)['a']['b'])::int4;
+SELECT (('{"a":[10,20,30]}'::jsonb)['a'][1])::int4;
+SELECT (('[{"a":true}]'::jsonb)[0]['a'])::bool;
+SELECT (('[[1,2],[3,4]]'::jsonb)[0][1])::int4;
+SELECT (('{"a":[{"b":3.14}]}'::jsonb)['a'][0]['b'])::float8;
+SELECT (('{"a":{"b":42}}'::jsonb)['a']['b'])::numeric;
+SELECT (('{"a":{"b":42}}'::jsonb)['a']['b'])::int8;
+-- negative index in nested chain
+SELECT (('{"a":[10,20,30]}'::jsonb)['a'][-1])::int4;
+
+-- Section M3: NULL semantics for multi-subscript chains
+SELECT (('{"a":1}'::jsonb)['x']['b'])::int4;  -- missing intermediate key
+SELECT (('{"a":{"c":1}}'::jsonb)['a']['b'])::int4;  -- missing final key
+SELECT (('{"a":{"b":null}}'::jsonb)['a']['b'])::int4;  -- JSON null leaf
+SELECT (('{"a":[1]}'::jsonb)['a'][5])::int4;  -- out-of-range nested index
+
+-- Section M4: type-mismatch errors for multi-subscript chains
+SELECT (('{"a":{"b":"hello"}}'::jsonb)['a']['b'])::int4;  -- string to int4
+SELECT (('{"a":{"b":[1,2]}}'::jsonb)['a']['b'])::int4;  -- container to int4
+
+-- Section M5: non-constant int4 subscript declines rewrite
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT (('{"a":[[10,20],[30,40]]}'::jsonb)['a'][i][0])::int4 FROM generate_series(0,1) AS i;
+
 SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'scalar';
 SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'array';
 SELECT test_json -> 'x' FROM test_jsonb WHERE json_type = 'object';
-- 
2.52.0