From 08301313b288a35af18bc00b65175e9ca94dd8da Mon Sep 17 00:00:00 2001 From: Rebecca Taft Date: Thu, 5 Nov 2020 17:30:58 -0600 Subject: [PATCH] opt: increase cost for table descriptor fetch during virtual scan This commit bumps the cost of each virtual scan to 25*randIOCostFactor from its previous value of 10*randIOCostFactor. This new value threads the needle so that a lookup join will still be chosen if the predicate is very selective, but the plan for the PGJDBC query identified in #55140 no longer includes lookup joins. Fixes #55140 Release note (performance improvement): Adjusted the cost model in the optimizer so that the optimizer is less likely to plan a lookup join into a virtual table. Performing a lookup join into a virtual table is expensive, so this change will generally result in better performance for queries involving joins with virtual tables. --- pkg/sql/opt/exec/execbuilder/testdata/join | 88 +++++++++++-------- pkg/sql/opt/exec/execbuilder/testdata/virtual | 19 ++-- pkg/sql/opt/xform/coster.go | 2 +- pkg/sql/opt/xform/testdata/coster/join | 20 ++--- .../opt/xform/testdata/coster/virtual-scan | 4 +- pkg/sql/opt/xform/testdata/external/pgjdbc | 41 +++++---- 6 files changed, 101 insertions(+), 73 deletions(-) diff --git a/pkg/sql/opt/exec/execbuilder/testdata/join b/pkg/sql/opt/exec/execbuilder/testdata/join index f08b0015001e..03fb4c8665d1 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/join +++ b/pkg/sql/opt/exec/execbuilder/testdata/join @@ -431,23 +431,23 @@ vectorized: false │ render 12: relname │ └── • hash join (inner) - │ columns: (attrelid, attname, attnum, oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, attrelid, attname, attnum, oid, nspname, generate_series, oid, relname, relnamespace, oid, nspname, objid, refobjid, oid, relname, relkind) + │ columns: (attrelid, attname, attnum, attrelid, attname, attnum, oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, oid, nspname, generate_series, oid, relname, relnamespace, oid, nspname, objid, refobjid, oid, relname, relkind) │ estimated row count: 110908 (missing stats) │ equality: (oid) = (objid) │ ├── • hash join (inner) - │ │ columns: (attrelid, attname, attnum, oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, attrelid, attname, attnum, oid, nspname, generate_series, oid, relname, relnamespace, oid, nspname) + │ │ columns: (attrelid, attname, attnum, attrelid, attname, attnum, oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, oid, nspname, generate_series, oid, relname, relnamespace, oid, nspname) │ │ estimated row count: 114302 (missing stats) │ │ equality: (relnamespace) = (oid) │ │ │ ├── • hash join (inner) - │ │ │ columns: (attrelid, attname, attnum, oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, attrelid, attname, attnum, oid, nspname, generate_series, oid, relname, relnamespace) + │ │ │ columns: (attrelid, attname, attnum, attrelid, attname, attnum, oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, oid, nspname, generate_series, oid, relname, relnamespace) │ │ │ estimated row count: 11557 (missing stats) │ │ │ equality: (attrelid) = (oid) │ │ │ pred: attnum = confkey[generate_series] │ │ │ │ │ ├── • hash join (inner) - │ │ │ │ columns: (attrelid, attname, attnum, oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, attrelid, attname, attnum, oid, nspname, generate_series) + │ │ │ │ columns: (attrelid, attname, attnum, attrelid, attname, attnum, oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, oid, nspname, generate_series) │ │ │ │ estimated row count: 3502 (missing stats) │ │ │ │ equality: (attrelid) = (confrelid) │ │ │ │ @@ -457,47 +457,59 @@ vectorized: false │ │ │ │ table: pg_attribute@primary │ │ │ │ │ │ │ └── • cross join (inner) - │ │ │ │ columns: (oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, attrelid, attname, attnum, oid, nspname, generate_series) + │ │ │ │ columns: (attrelid, attname, attnum, oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, oid, nspname, generate_series) │ │ │ │ estimated row count: 354 (missing stats) │ │ │ │ pred: attnum = conkey[generate_series] │ │ │ │ - │ │ │ ├── • hash join (inner) - │ │ │ │ │ columns: (oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, attrelid, attname, attnum, oid, nspname) + │ │ │ ├── • merge join (inner) + │ │ │ │ │ columns: (attrelid, attname, attnum, oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, oid, nspname) │ │ │ │ │ estimated row count: 107 (missing stats) - │ │ │ │ │ equality: (relnamespace) = (oid) + │ │ │ │ │ equality: (attrelid) = (oid) + │ │ │ │ │ merge ordering: +"(attrelid=oid)" │ │ │ │ │ - │ │ │ │ ├── • virtual table lookup join (inner) - │ │ │ │ │ │ columns: (oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, attrelid, attname, attnum) - │ │ │ │ │ │ estimated row count: 105 (missing stats) - │ │ │ │ │ │ table: pg_attribute@pg_attribute_attrelid_idx - │ │ │ │ │ │ equality: (oid) = (attrelid) - │ │ │ │ │ │ - │ │ │ │ │ └── • virtual table lookup join (inner) - │ │ │ │ │ │ columns: (oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey) - │ │ │ │ │ │ estimated row count: 10 (missing stats) - │ │ │ │ │ │ table: pg_constraint@pg_constraint_conrelid_idx - │ │ │ │ │ │ equality: (oid) = (conrelid) - │ │ │ │ │ │ pred: contype = 'f' - │ │ │ │ │ │ - │ │ │ │ │ └── • filter - │ │ │ │ │ │ columns: (oid, relname, relnamespace) - │ │ │ │ │ │ estimated row count: 10 (missing stats) - │ │ │ │ │ │ filter: relname = 'orders' - │ │ │ │ │ │ - │ │ │ │ │ └── • virtual table - │ │ │ │ │ columns: (oid, relname, relnamespace) - │ │ │ │ │ estimated row count: 1000 (missing stats) - │ │ │ │ │ table: pg_class@primary + │ │ │ │ ├── • virtual table + │ │ │ │ │ columns: (attrelid, attname, attnum) + │ │ │ │ │ ordering: +attrelid + │ │ │ │ │ estimated row count: 1000 (missing stats) + │ │ │ │ │ table: pg_attribute@pg_attribute_attrelid_idx │ │ │ │ │ - │ │ │ │ └── • filter - │ │ │ │ │ columns: (oid, nspname) - │ │ │ │ │ estimated row count: 10 (missing stats) - │ │ │ │ │ filter: nspname = 'public' + │ │ │ │ └── • sort + │ │ │ │ │ columns: (oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, oid, nspname) + │ │ │ │ │ ordering: +oid + │ │ │ │ │ estimated row count: 11 (missing stats) + │ │ │ │ │ order: +oid │ │ │ │ │ - │ │ │ │ └── • virtual table - │ │ │ │ columns: (oid, nspname) - │ │ │ │ estimated row count: 1000 (missing stats) - │ │ │ │ table: pg_namespace@primary + │ │ │ │ └── • hash join (inner) + │ │ │ │ │ columns: (oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey, oid, nspname) + │ │ │ │ │ estimated row count: 11 (missing stats) + │ │ │ │ │ equality: (relnamespace) = (oid) + │ │ │ │ │ + │ │ │ │ ├── • virtual table lookup join (inner) + │ │ │ │ │ │ columns: (oid, relname, relnamespace, oid, conname, contype, condeferrable, condeferred, conrelid, confrelid, confupdtype, confdeltype, conkey, confkey) + │ │ │ │ │ │ estimated row count: 10 (missing stats) + │ │ │ │ │ │ table: pg_constraint@pg_constraint_conrelid_idx + │ │ │ │ │ │ equality: (oid) = (conrelid) + │ │ │ │ │ │ pred: contype = 'f' + │ │ │ │ │ │ + │ │ │ │ │ └── • filter + │ │ │ │ │ │ columns: (oid, relname, relnamespace) + │ │ │ │ │ │ estimated row count: 10 (missing stats) + │ │ │ │ │ │ filter: relname = 'orders' + │ │ │ │ │ │ + │ │ │ │ │ └── • virtual table + │ │ │ │ │ columns: (oid, relname, relnamespace) + │ │ │ │ │ estimated row count: 1000 (missing stats) + │ │ │ │ │ table: pg_class@primary + │ │ │ │ │ + │ │ │ │ └── • filter + │ │ │ │ │ columns: (oid, nspname) + │ │ │ │ │ estimated row count: 10 (missing stats) + │ │ │ │ │ filter: nspname = 'public' + │ │ │ │ │ + │ │ │ │ └── • virtual table + │ │ │ │ columns: (oid, nspname) + │ │ │ │ estimated row count: 1000 (missing stats) + │ │ │ │ table: pg_namespace@primary │ │ │ │ │ │ │ └── • project set │ │ │ │ columns: (generate_series) diff --git a/pkg/sql/opt/exec/execbuilder/testdata/virtual b/pkg/sql/opt/exec/execbuilder/testdata/virtual index 0b7f7eea9643..add2abdb8c7b 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/virtual +++ b/pkg/sql/opt/exec/execbuilder/testdata/virtual @@ -164,20 +164,25 @@ vectorized: false │ └── • render │ - └── • virtual table lookup join - │ table: pg_class@pg_class_oid_idx - │ equality: (confrelid) = (oid) + └── • merge join + │ equality: (oid) = (confrelid) + │ + ├── • virtual table + │ table: pg_class@pg_class_oid_idx │ └── • virtual table lookup join │ table: pg_class@pg_class_oid_idx │ equality: (conrelid) = (oid) │ pred: oid = 'b'::REGCLASS │ - └── • filter - │ filter: (conrelid = 'b'::REGCLASS) AND (contype = 'f') + └── • sort + │ order: +confrelid │ - └── • virtual table - table: pg_constraint@primary + └── • filter + │ filter: (conrelid = 'b'::REGCLASS) AND (contype = 'f') + │ + └── • virtual table + table: pg_constraint@primary # Test that limits are respected. query T diff --git a/pkg/sql/opt/xform/coster.go b/pkg/sql/opt/xform/coster.go index 16ba3065c571..23f2698e331c 100644 --- a/pkg/sql/opt/xform/coster.go +++ b/pkg/sql/opt/xform/coster.go @@ -110,7 +110,7 @@ const ( // virtualScanTableDescriptorFetchCost is the cost to retrieve the table // descriptors when performing a virtual table scan. - virtualScanTableDescriptorFetchCost = 10 * randIOCostFactor + virtualScanTableDescriptorFetchCost = 25 * randIOCostFactor // Input rows to a join are processed in batches of this size. // See joinreader.go. diff --git a/pkg/sql/opt/xform/testdata/coster/join b/pkg/sql/opt/xform/testdata/coster/join index 66dbe512fae4..2f50c4fb5b37 100644 --- a/pkg/sql/opt/xform/testdata/coster/join +++ b/pkg/sql/opt/xform/testdata/coster/join @@ -857,32 +857,32 @@ WHERE project ├── columns: attname:3!null atttypid:4!null typbasetype:50 typtype:32 ├── stats: [rows=198] - ├── cost: 2724.37877 + ├── cost: 2844.37877 └── inner-join (merge) ├── columns: attname:3!null atttypid:4!null oid:26!null typtype:32 typbasetype:50 ├── left ordering: +26 ├── right ordering: +4 ├── stats: [rows=198, distinct(4)=17.2927193, null(4)=0, distinct(26)=17.2927193, null(26)=0] - ├── cost: 2722.38877 + ├── cost: 2842.38877 ├── fd: (4)==(26), (26)==(4) ├── scan pg_type@secondary [as=t] │ ├── columns: oid:26!null typtype:32 typbasetype:50 │ ├── stats: [rows=1000, distinct(26)=100, null(26)=0] - │ ├── cost: 1394.02 + │ ├── cost: 1454.02 │ └── ordering: +26 ├── sort │ ├── columns: attname:3!null atttypid:4 │ ├── stats: [rows=20, distinct(3)=2, null(3)=0, distinct(4)=18.2927193, null(4)=0.2] - │ ├── cost: 1316.17877 + │ ├── cost: 1376.17877 │ ├── ordering: +4 │ └── select │ ├── columns: attname:3!null atttypid:4 │ ├── stats: [rows=20, distinct(3)=2, null(3)=0, distinct(4)=18.2927193, null(4)=0.2] - │ ├── cost: 1314.04 + │ ├── cost: 1374.04 │ ├── scan pg_attribute [as=a] │ │ ├── columns: attname:3 atttypid:4 │ │ ├── stats: [rows=1000, distinct(3)=100, null(3)=10, distinct(4)=100, null(4)=10] - │ │ └── cost: 1304.02 + │ │ └── cost: 1364.02 │ └── filters │ └── attname:3 IN ('descriptor_id', 'descriptor_name') [outer=(3), constraints=(/3: [/'descriptor_id' - /'descriptor_id'] [/'descriptor_name' - /'descriptor_name']; tight)] └── filters (true) @@ -905,23 +905,23 @@ WHERE project ├── columns: attname:3!null atttypid:4!null typbasetype:50 typtype:32 ├── stats: [rows=99] - ├── cost: 2148.69 + ├── cost: 2808.69 ├── fd: ()-->(3) └── inner-join (lookup pg_type@secondary [as=t]) ├── columns: attname:3!null atttypid:4!null oid:26!null typtype:32 typbasetype:50 ├── key columns: [4] = [26] ├── stats: [rows=99, distinct(4)=8.5617925, null(4)=0, distinct(26)=8.5617925, null(26)=0] - ├── cost: 2147.69 + ├── cost: 2807.69 ├── fd: ()-->(3), (4)==(26), (26)==(4) ├── select │ ├── columns: attname:3!null atttypid:4 │ ├── stats: [rows=10, distinct(3)=1, null(3)=0, distinct(4)=9.5617925, null(4)=0.1] - │ ├── cost: 1314.04 + │ ├── cost: 1374.04 │ ├── fd: ()-->(3) │ ├── scan pg_attribute [as=a] │ │ ├── columns: attname:3 atttypid:4 │ │ ├── stats: [rows=1000, distinct(3)=100, null(3)=10, distinct(4)=100, null(4)=10] - │ │ └── cost: 1304.02 + │ │ └── cost: 1364.02 │ └── filters │ └── attname:3 = 'descriptor_id' [outer=(3), constraints=(/3: [/'descriptor_id' - /'descriptor_id']; tight), fd=()-->(3)] └── filters (true) diff --git a/pkg/sql/opt/xform/testdata/coster/virtual-scan b/pkg/sql/opt/xform/testdata/coster/virtual-scan index b13ea424d1c5..0fe6bebaa68b 100644 --- a/pkg/sql/opt/xform/testdata/coster/virtual-scan +++ b/pkg/sql/opt/xform/testdata/coster/virtual-scan @@ -4,11 +4,11 @@ SELECT * FROM information_schema.schemata WHERE SCHEMA_NAME='public' select ├── columns: catalog_name:2!null schema_name:3!null default_character_set_name:4 sql_path:5 crdb_is_user_defined:6 ├── stats: [rows=10, distinct(3)=1, null(3)=0] - ├── cost: 1164.04 + ├── cost: 1224.04 ├── fd: ()-->(3) ├── scan schemata │ ├── columns: catalog_name:2!null schema_name:3!null default_character_set_name:4 sql_path:5 crdb_is_user_defined:6 │ ├── stats: [rows=1000, distinct(2)=100, null(2)=0, distinct(3)=100, null(3)=0] - │ └── cost: 1154.02 + │ └── cost: 1214.02 └── filters └── schema_name:3 = 'public' [outer=(3), constraints=(/3: [/'public' - /'public']; tight), fd=()-->(3)] diff --git a/pkg/sql/opt/xform/testdata/external/pgjdbc b/pkg/sql/opt/xform/testdata/external/pgjdbc index 1873e6f2786d..2272f5089f2b 100644 --- a/pkg/sql/opt/xform/testdata/external/pgjdbc +++ b/pkg/sql/opt/xform/testdata/external/pgjdbc @@ -204,32 +204,43 @@ sort │ │ │ │ │ │ │ │ └── columns: objoid:98 classoid:99 objsubid:100 description:101 │ │ │ │ │ │ │ └── filters │ │ │ │ │ │ │ └── objsubid:100 > 0 [outer=(100), constraints=(/100: [/1 - ]; tight)] - │ │ │ │ │ │ ├── inner-join (lookup pg_attribute@secondary [as=a]) + │ │ │ │ │ │ ├── inner-join (hash) │ │ │ │ │ │ │ ├── columns: n.oid:2!null n.nspname:3!null c.oid:7!null c.relname:8!null c.relnamespace:9!null c.relkind:24!null attrelid:36!null attname:37 atttypid:38 attlen:40 attnum:41!null atttypmod:44 a.attnotnull:48 attisdropped:52!null - │ │ │ │ │ │ │ ├── key columns: [7] = [36] │ │ │ │ │ │ │ ├── fd: ()-->(3,52), (2)==(9), (9)==(2), (7)==(36), (36)==(7) - │ │ │ │ │ │ │ ├── inner-join (hash) - │ │ │ │ │ │ │ │ ├── columns: n.oid:2!null n.nspname:3!null c.oid:7!null c.relname:8!null c.relnamespace:9!null c.relkind:24!null - │ │ │ │ │ │ │ │ ├── fd: ()-->(3), (2)==(9), (9)==(2) + │ │ │ │ │ │ │ ├── inner-join (merge) + │ │ │ │ │ │ │ │ ├── columns: c.oid:7!null c.relname:8!null c.relnamespace:9 c.relkind:24!null attrelid:36!null attname:37 atttypid:38 attlen:40 attnum:41!null atttypmod:44 a.attnotnull:48 attisdropped:52!null + │ │ │ │ │ │ │ │ ├── left ordering: +7 + │ │ │ │ │ │ │ │ ├── right ordering: +36 + │ │ │ │ │ │ │ │ ├── fd: ()-->(52), (7)==(36), (36)==(7) │ │ │ │ │ │ │ │ ├── select │ │ │ │ │ │ │ │ │ ├── columns: c.oid:7!null c.relname:8!null c.relnamespace:9 c.relkind:24!null - │ │ │ │ │ │ │ │ │ ├── scan pg_class [as=c] - │ │ │ │ │ │ │ │ │ │ └── columns: c.oid:7!null c.relname:8!null c.relnamespace:9 c.relkind:24 + │ │ │ │ │ │ │ │ │ ├── ordering: +7 + │ │ │ │ │ │ │ │ │ ├── scan pg_class@secondary [as=c] + │ │ │ │ │ │ │ │ │ │ ├── columns: c.oid:7!null c.relname:8!null c.relnamespace:9 c.relkind:24 + │ │ │ │ │ │ │ │ │ │ └── ordering: +7 │ │ │ │ │ │ │ │ │ └── filters │ │ │ │ │ │ │ │ │ ├── c.relkind:24 IN ('f', 'm', 'p', 'r', 'v') [outer=(24), constraints=(/24: [/'f' - /'f'] [/'m' - /'m'] [/'p' - /'p'] [/'r' - /'r'] [/'v' - /'v']; tight)] │ │ │ │ │ │ │ │ │ └── c.relname:8 LIKE '%' [outer=(8), constraints=(/8: (/NULL - ])] │ │ │ │ │ │ │ │ ├── select - │ │ │ │ │ │ │ │ │ ├── columns: n.oid:2 n.nspname:3!null - │ │ │ │ │ │ │ │ │ ├── fd: ()-->(3) - │ │ │ │ │ │ │ │ │ ├── scan pg_namespace [as=n] - │ │ │ │ │ │ │ │ │ │ └── columns: n.oid:2 n.nspname:3!null + │ │ │ │ │ │ │ │ │ ├── columns: attrelid:36!null attname:37 atttypid:38 attlen:40 attnum:41!null atttypmod:44 a.attnotnull:48 attisdropped:52!null + │ │ │ │ │ │ │ │ │ ├── fd: ()-->(52) + │ │ │ │ │ │ │ │ │ ├── ordering: +36 opt(52) [actual: +36] + │ │ │ │ │ │ │ │ │ ├── scan pg_attribute@secondary [as=a] + │ │ │ │ │ │ │ │ │ │ ├── columns: attrelid:36!null attname:37 atttypid:38 attlen:40 attnum:41 atttypmod:44 a.attnotnull:48 attisdropped:52 + │ │ │ │ │ │ │ │ │ │ └── ordering: +36 opt(52) [actual: +36] │ │ │ │ │ │ │ │ │ └── filters - │ │ │ │ │ │ │ │ │ └── n.nspname:3 LIKE 'public' [outer=(3), constraints=(/3: [/'public' - /'public']; tight), fd=()-->(3)] + │ │ │ │ │ │ │ │ │ ├── attnum:41 > 0 [outer=(41), constraints=(/41: [/1 - ]; tight)] + │ │ │ │ │ │ │ │ │ └── NOT attisdropped:52 [outer=(52), constraints=(/52: [/false - /false]; tight), fd=()-->(52)] + │ │ │ │ │ │ │ │ └── filters (true) + │ │ │ │ │ │ │ ├── select + │ │ │ │ │ │ │ │ ├── columns: n.oid:2 n.nspname:3!null + │ │ │ │ │ │ │ │ ├── fd: ()-->(3) + │ │ │ │ │ │ │ │ ├── scan pg_namespace [as=n] + │ │ │ │ │ │ │ │ │ └── columns: n.oid:2 n.nspname:3!null │ │ │ │ │ │ │ │ └── filters - │ │ │ │ │ │ │ │ └── c.relnamespace:9 = n.oid:2 [outer=(2,9), constraints=(/2: (/NULL - ]; /9: (/NULL - ]), fd=(2)==(9), (9)==(2)] + │ │ │ │ │ │ │ │ └── n.nspname:3 LIKE 'public' [outer=(3), constraints=(/3: [/'public' - /'public']; tight), fd=()-->(3)] │ │ │ │ │ │ │ └── filters - │ │ │ │ │ │ │ ├── attnum:41 > 0 [outer=(41), constraints=(/41: [/1 - ]; tight)] - │ │ │ │ │ │ │ └── NOT attisdropped:52 [outer=(52), constraints=(/52: [/false - /false]; tight), fd=()-->(52)] + │ │ │ │ │ │ │ └── c.relnamespace:9 = n.oid:2 [outer=(2,9), constraints=(/2: (/NULL - ]; /9: (/NULL - ]), fd=(2)==(9), (9)==(2)] │ │ │ │ │ │ └── filters │ │ │ │ │ │ ├── c.oid:7 = objoid:98 [outer=(7,98), constraints=(/7: (/NULL - ]; /98: (/NULL - ]), fd=(7)==(98), (98)==(7)] │ │ │ │ │ │ └── attnum:41 = objsubid:100 [outer=(41,100), constraints=(/41: (/NULL - ]; /100: (/NULL - ]), fd=(41)==(100), (100)==(41)]