exec-ddl
CREATE TABLE t1 (k INT, i INT, f FLOAT, s STRING)
----

exec-ddl
CREATE TABLE t2 (k INT, i INT, s STRING)
----

exec-ddl
CREATE TABLE t3 (k INT, i INT, f FLOAT, g GEOMETRY)
----

exec-ddl
CREATE TABLE t4 (k INT, i INT, f FLOAT, j JSONB, a INT[])
----

# Ensure that index candidates are not created for virtual tables.
index-candidates
SELECT *
FROM information_schema.schemata JOIN t1
ON true
WHERE information_schema.schemata.SCHEMA_NAME='public' AND t1.k > 3
----
t1:
 (k)

# Ensure that index candidates are not created for system tables.
index-candidates
SELECT * FROM system.table_statistics WHERE name = 'foo'
----

# Ensure that new indexes do not get recommended if an identical existing
# index exists.

exec-ddl
CREATE INDEX existing_t1_k ON t1(k) STORING (s)
----

exec-ddl
CREATE UNIQUE INDEX existing_t1_i ON t1(i)
----

exec-ddl
CREATE INDEX existing_t2_k ON t2(k)
----

# No recommendations because an identical index exists already.
index-recommendations
SELECT i FROM t1 WHERE i >= 3
----
no index recommendations
--
optimal plan:
scan t1@existing_t1_i
 ├── columns: i:2!null
 ├── constraint: /2: [/3 - ]
 ├── cost: 358.263333
 └── key: (2)

# No recommendations because an index with the same explicit columns exists
# already, and no new columns are being stored.
index-recommendations
SELECT k FROM t1 WHERE k >= 3
----
no index recommendations
--
optimal plan:
scan t1@existing_t1_k
 ├── columns: k:1!null
 ├── constraint: /1/5: [/3 - ]
 └── cost: 364.686667

# There is a replacement recommendation because an index with the same explicit
# columns exists already and new columns are being stored here. We stored the
# existing index's stored columns and any new stored columns.
index-recommendations
SELECT i FROM t1 WHERE k >= 3
----
replacement: CREATE INDEX ON t.public.t1 (k) STORING (i, s); DROP INDEX t.public.t1@existing_t1_k;
--
optimal plan:
project
 ├── columns: i:2
 ├── cost: 378.04
 └── scan t1@_hyp_3
      ├── columns: k:1!null i:2
      ├── constraint: /1/5: [/3 - ]
      ├── cost: 374.686667
      ├── lax-key: (1,2)
      └── fd: (2)~~>(1)

# Replacement recommendations for existing unique indexes must also create
# unique indexes.
index-recommendations
SELECT t1.k FROM t1 JOIN t2 ON t1.k = t2.k WHERE t1.i > 3 AND t2.i > 3
----
replacement: CREATE UNIQUE INDEX ON t.public.t1 (i) STORING (k); DROP INDEX t.public.t1@existing_t1_i;
creation: CREATE INDEX ON t.public.t2 (i) STORING (k);
--
optimal plan:
project
 ├── columns: k:1!null
 ├── cost: 774.796132
 └── inner-join (hash)
      ├── columns: t1.k:1!null t1.i:2!null t2.k:8!null t2.i:9!null
      ├── cost: 763.793689
      ├── fd: (2)-->(1), (1)==(8), (8)==(1)
      ├── scan t2@_hyp_2
      │    ├── columns: t2.k:8 t2.i:9!null
      │    ├── constraint: /9/11: [/4 - ]
      │    └── cost: 371.353333
      ├── scan t1@_hyp_3
      │    ├── columns: t1.k:1 t1.i:2!null
      │    ├── constraint: /2/5: [/4 - ]
      │    ├── cost: 371.476667
      │    ├── lax-key: (1,2)
      │    └── fd: (2)-->(1)
      └── filters
           └── t1.k:1 = t2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

exec-ddl
DROP INDEX t1@existing_t1_k
----

exec-ddl
DROP INDEX t1@existing_t1_i
----

exec-ddl
DROP INDEX t2@existing_t2_k
----

# Below are tests without existing indexes on the tables. Every query has two
# tests, one showing its index candidates and the other showing its final index
# recommendations.

# Basic tests for comparison operator, range, equality, join, order by, and
# group by candidates.

index-candidates
SELECT * FROM t1 WHERE i >= 3
----
t1:
 (i)

index-recommendations
SELECT * FROM t1 WHERE i >= 3
----
creation: CREATE INDEX ON t.public.t1 (i) STORING (k, f, s);
--
optimal plan:
scan t1@_hyp_1
 ├── columns: k:1 i:2!null f:3 s:4
 ├── constraint: /2/5: [/3 - ]
 └── cost: 381.353333


index-candidates
SELECT f, k FROM t1 WHERE f > 2 AND f < 8
----
t1:
 (f)

index-recommendations
SELECT f, k FROM t1 WHERE f > 2 AND f < 8
----
creation: CREATE INDEX ON t.public.t1 (f) STORING (k);
--
optimal plan:
scan t1@_hyp_1
 ├── columns: f:3!null k:1
 ├── constraint: /3/5: [/2.0000000000000004 - /7.999999999999999]
 └── cost: 136.908889

index-candidates
SELECT i FROM t1 WHERE k < 3 AND i > 5
----
t1:
 (i)
 (k)

index-recommendations
SELECT i FROM t1 WHERE k < 3 AND i > 5
----
creation: CREATE INDEX ON t.public.t1 (k) STORING (i);
--
optimal plan:
project
 ├── columns: i:2!null
 ├── cost: 381.181111
 └── select
      ├── columns: k:1!null i:2!null
      ├── cost: 378.05
      ├── scan t1@_hyp_1
      │    ├── columns: k:1!null i:2
      │    ├── constraint: /1/5: (/NULL - /2]
      │    └── cost: 374.686667
      └── filters
           └── i:2 > 5 [outer=(2), constraints=(/2: [/6 - ]; tight)]

index-candidates
SELECT i FROM t1 WHERE k < 3 AND i > 5 OR f < 7
----
t1:
 (f)
 (i)
 (k)

index-recommendations
SELECT i FROM t1 WHERE k < 3 AND i > 5 OR f < 7
----
creation: CREATE INDEX ON t.public.t1 (k) STORING (i, f);
creation: CREATE INDEX ON t.public.t1 (f) STORING (k, i);
--
optimal plan:
project
 ├── columns: i:2
 ├── cost: 822.144374
 └── project
      ├── columns: k:1 i:2 f:3
      ├── cost: 816.716967
      └── distinct-on
           ├── columns: k:1 i:2 f:3 rowid:5!null
           ├── grouping columns: rowid:5!null
           ├── cost: 811.289559
           ├── key: (5)
           ├── fd: (5)-->(1-3)
           ├── union-all
           │    ├── columns: k:1 i:2 f:3 rowid:5!null
           │    ├── left columns: k:8 i:9 f:10 rowid:12
           │    ├── right columns: k:15 i:16 f:17 rowid:19
           │    ├── cost: 772.534445
           │    ├── select
           │    │    ├── columns: k:8!null i:9!null f:10 rowid:12!null
           │    │    ├── cost: 384.716667
           │    │    ├── key: (12)
           │    │    ├── fd: (12)-->(8-10)
           │    │    ├── scan t1@_hyp_1
           │    │    │    ├── columns: k:8!null i:9 f:10 rowid:12!null
           │    │    │    ├── constraint: /8/12: (/NULL - /2]
           │    │    │    ├── cost: 381.353333
           │    │    │    ├── key: (12)
           │    │    │    └── fd: (12)-->(8-10)
           │    │    └── filters
           │    │         └── i:9 > 5 [outer=(9), constraints=(/9: [/6 - ]; tight)]
           │    └── scan t1@_hyp_3
           │         ├── columns: k:15 i:16 f:17!null rowid:19!null
           │         ├── constraint: /17/19: (/NULL - /6.999999999999999]
           │         ├── cost: 381.353333
           │         ├── key: (19)
           │         └── fd: (19)-->(15-17)
           └── aggregations
                ├── const-agg [as=k:1, outer=(1)]
                │    └── k:1
                ├── const-agg [as=i:2, outer=(2)]
                │    └── i:2
                └── const-agg [as=f:3, outer=(3)]
                     └── f:3

index-candidates
SELECT s FROM t1 WHERE s = 'NG'
----
t1:
 (s)

index-recommendations
SELECT s FROM t1 WHERE s = 'NG'
----
creation: CREATE INDEX ON t.public.t1 (s);
--
optimal plan:
scan t1@_hyp_1
 ├── columns: s:4!null
 ├── constraint: /4/5: [/'NG' - /'NG']
 ├── cost: 28.6200001
 └── fd: ()-->(4)

index-candidates
SELECT i FROM t1 WHERE i != 5
----
t1:
 (i)

index-recommendations
SELECT i FROM t1 WHERE i != 5
----
creation: CREATE INDEX ON t.public.t1 (i);
--
optimal plan:
scan t1@_hyp_1
 ├── columns: i:2!null
 ├── constraint: /2/5
 │    ├── (/NULL - /4]
 │    └── [/6 - ]
 └── cost: 375.353333

index-candidates
SELECT s FROM t1 WHERE s IS NOT NULL
----
t1:
 (s)

index-recommendations
SELECT s FROM t1 WHERE s IS NOT NULL
----
creation: CREATE INDEX ON t.public.t1 (s);
--
optimal plan:
scan t1@_hyp_1
 ├── columns: s:4!null
 ├── constraint: /4/5: (/NULL - ]
 └── cost: 1067.42

index-candidates
SELECT t1.k FROM t1 JOIN t2 ON t1.k = t2.i
----
t1:
 (k)
t2:
 (i)

index-recommendations
SELECT t1.k FROM t1 JOIN t2 ON t1.k = t2.i
----
creation: CREATE INDEX ON t.public.t1 (k);
creation: CREATE INDEX ON t.public.t2 (i);
--
optimal plan:
project
 ├── columns: k:1!null
 ├── cost: 2383.2
 └── inner-join (merge)
      ├── columns: t1.k:1!null t2.i:9!null
      ├── left ordering: +1
      ├── right ordering: +9
      ├── cost: 2285.17
      ├── fd: (1)==(9), (9)==(1)
      ├── scan t1@_hyp_1
      │    ├── columns: t1.k:1
      │    ├── cost: 1088.62
      │    └── ordering: +1
      ├── scan t2@_hyp_1
      │    ├── columns: t2.i:9
      │    ├── cost: 1078.52
      │    └── ordering: +9
      └── filters (true)

index-candidates
SELECT t2.s FROM t1 RIGHT JOIN t2 ON t1.s LIKE t2.s
----
t1:
 (s)
t2:
 (s)

# See function comment in indexrec.FindIndexRecommendationSet for an explanation
# as to why there is no recommendation for an index on s.
index-recommendations
SELECT t2.s FROM t1 RIGHT JOIN t2 ON t1.s LIKE t2.s
----
no index recommendations
--
optimal plan:
project
 ├── columns: s:10
 ├── cost: 15530.6696
 └── left-join (cross)
      ├── columns: t1.s:4 t2.s:10
      ├── cost: 12197.3163
      ├── scan t2
      │    ├── columns: t2.s:10
      │    └── cost: 1078.52
      ├── scan t1
      │    ├── columns: t1.s:4
      │    └── cost: 1088.62
      └── filters
           └── t1.s:4 LIKE t2.s:10 [outer=(4,10), constraints=(/4: (/NULL - ]; /10: (/NULL - ])]

index-candidates
SELECT i FROM t1 ORDER BY i
----
t1:
 (i)

index-recommendations
SELECT i FROM t1 ORDER BY i
----
creation: CREATE INDEX ON t.public.t1 (i);
--
optimal plan:
scan t1@_hyp_1
 ├── columns: i:2
 ├── cost: 1088.62
 └── ordering: +2


index-candidates
SELECT k, i FROM t1 ORDER BY k DESC, i ASC
----
t1:
 (k DESC, i)

index-recommendations
SELECT k, i FROM t1 ORDER BY k DESC, i ASC
----
creation: CREATE INDEX ON t.public.t1 (k DESC, i);
--
optimal plan:
scan t1@_hyp_1
 ├── columns: k:1 i:2
 ├── cost: 1098.72
 └── ordering: -1,+2

# Only one index candidate is created with nested ORDER BY clauses.
index-candidates
SELECT * FROM (
  SELECT k, i FROM t1
  ORDER BY k DESC, i ASC
)
ORDER BY k ASC, i DESC
----
t1:
 (k, i DESC)

index-recommendations
SELECT * FROM (
  SELECT k, i FROM t1
  ORDER BY k DESC, i ASC
)
ORDER BY k ASC, i DESC
----
creation: CREATE INDEX ON t.public.t1 (k, i DESC);
--
optimal plan:
scan t1@_hyp_1
 ├── columns: k:1 i:2
 ├── cost: 1098.72
 └── ordering: +1,-2

# Redundant index candidates are created but only one index is recommended.
index-candidates
SELECT k FROM t1 WHERE k > 3
UNION ALL
SELECT k FROM t1 WHERE k < 10
ORDER BY k DESC
----
t1:
 (k DESC)
 (k)

index-recommendations
SELECT k FROM t1 WHERE k > 3
UNION ALL
SELECT k FROM t1 WHERE k < 10
ORDER BY k DESC
----
creation: CREATE INDEX ON t.public.t1 (k DESC);
--
optimal plan:
union-all
 ├── columns: k:15!null
 ├── left columns: t1.k:1
 ├── right columns: t1.k:8
 ├── cost: 749.393334
 ├── ordering: -15
 ├── scan t1@_hyp_1
 │    ├── columns: t1.k:1!null
 │    ├── constraint: /-1/5: [ - /4]
 │    ├── cost: 371.353333
 │    └── ordering: -1
 └── scan t1@_hyp_1
      ├── columns: t1.k:8!null
      ├── constraint: /-8/12: [/9 - /NULL)
      ├── cost: 371.353333
      └── ordering: -8

index-candidates
SELECT count(*) FROM t1 GROUP BY k
----
t1:
 (k)

index-recommendations
SELECT count(*) FROM t1 GROUP BY k
----
creation: CREATE INDEX ON t.public.t1 (k);
--
optimal plan:
project
 ├── columns: count:8!null
 ├── cost: 1110.67
 └── group-by (streaming)
      ├── columns: k:1 count_rows:8!null
      ├── grouping columns: k:1
      ├── internal-ordering: +1
      ├── cost: 1109.65
      ├── key: (1)
      ├── fd: (1)-->(8)
      ├── scan t1@_hyp_1
      │    ├── columns: k:1
      │    ├── cost: 1088.62
      │    └── ordering: +1
      └── aggregations
           └── count-rows [as=count_rows:8]


index-candidates
SELECT sum(k) FROM t1 GROUP BY i, f, k
----
t1:
 (k, i, f)

index-recommendations
SELECT sum(k) FROM t1 GROUP BY i, f, k
----
creation: CREATE INDEX ON t.public.t1 (k, i, f);
--
optimal plan:
project
 ├── columns: sum:8
 ├── cost: 1168.87
 └── group-by (streaming)
      ├── columns: k:1 i:2 f:3 sum:8
      ├── grouping columns: k:1 i:2 f:3
      ├── internal-ordering: +1,+2,+3
      ├── cost: 1158.85
      ├── key: (1-3)
      ├── fd: (1-3)-->(8)
      ├── scan t1@_hyp_1
      │    ├── columns: k:1 i:2 f:3
      │    ├── cost: 1108.82
      │    └── ordering: +1,+2,+3
      └── aggregations
           └── sum [as=sum:8, outer=(1)]
                └── k:1

# Test joins with more complex predicates. See rule 3 and rule 4 in
# indexrec.FindIndexCandidateSet.

index-candidates
SELECT t1.f, t2.k, t2.i
FROM t1 FULL JOIN t2
ON t2.k IS NULL
AND t1.f::STRING NOT LIKE t2.i::STRING
----
t1:
 (f)
t2:
 (i)
 (k)
 (k, i)

index-recommendations
SELECT t1.f, t2.k, t2.i
FROM t1 FULL JOIN t2
ON t2.k IS NULL
AND t1.f::STRING NOT LIKE t2.i::STRING
----
no index recommendations
--
optimal plan:
full-join (cross)
 ├── columns: f:3 k:8 i:9
 ├── stable
 ├── cost: 12207.4263
 ├── scan t1
 │    ├── columns: f:3
 │    └── cost: 1088.62
 ├── scan t2
 │    ├── columns: t2.k:8 t2.i:9
 │    └── cost: 1088.62
 └── filters
      ├── t2.k:8 IS NULL [outer=(8), constraints=(/8: [/NULL - /NULL]; tight), fd=()-->(8)]
      └── f:3::STRING NOT LIKE t2.i:9::STRING [outer=(3,9), stable]

index-candidates
SELECT t1.k, t1.s, t2.k, t2.i
FROM t1 LEFT JOIN t2
ON t1.k != t2.k
AND t1.s IS NOT NULL
AND t2.i IS NULL
----
t1:
 (k)
 (k, s)
 (s)
t2:
 (i)
 (i, k)
 (k)

index-recommendations
SELECT t1.k, t1.s, t2.k, t2.i
FROM t1 LEFT JOIN t2
ON t1.k != t2.k
AND t1.s IS NOT NULL
AND t2.i IS NULL
----
creation: CREATE INDEX ON t.public.t2 (i) STORING (k);
--
optimal plan:
left-join (cross)
 ├── columns: k:1 s:4 k:8 i:9
 ├── cost: 1240.055
 ├── scan t1
 │    ├── columns: t1.k:1 t1.s:4
 │    └── cost: 1098.72
 ├── scan t2@_hyp_1
 │    ├── columns: t2.k:8 t2.i:9
 │    ├── constraint: /9/11: [/NULL - /NULL]
 │    ├── cost: 28.6200002
 │    └── fd: ()-->(9)
 └── filters
      ├── t1.k:1 != t2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ])]
      └── t1.s:4 IS NOT NULL [outer=(4), constraints=(/4: (/NULL - ]; tight)]

# Test more complex queries. See rule 5 in indexrec.FindIndexCandidateSet. The
# aspects of rule 5 that are demonstrated by each test are highlighted the
# test's comment.

# Multi-column combinations used: EQ.
index-candidates
SELECT k, i FROM t1 WHERE k = 1 AND i = 2
----
t1:
 (i)
 (k)
 (k, i)

index-recommendations
SELECT k, i FROM t1 WHERE k = 1 AND i = 2
----
creation: CREATE INDEX ON t.public.t1 (k, i);
--
optimal plan:
scan t1@_hyp_3
 ├── columns: k:1!null i:2!null
 ├── constraint: /1/2/5: [/1/2 - /1/2]
 ├── cost: 18.9945676
 └── fd: ()-->(1,2)

# Multi-column combinations used: EQ + R.
index-candidates
SELECT * FROM t1 WHERE k = 1 AND f > 0
----
t1:
 (f)
 (k)
 (k, f)

index-recommendations
SELECT * FROM t1 WHERE k = 1 AND f > 0
----
creation: CREATE INDEX ON t.public.t1 (k, f) STORING (i, s);
--
optimal plan:
scan t1@_hyp_3
 ├── columns: k:1!null i:2 f:3!null s:4
 ├── constraint: /1/3/5: [/1/5e-324 - /1]
 ├── cost: 28.1933333
 └── fd: ()-->(1)

index-candidates
SELECT * FROM t1 WHERE k = 1 AND f != 0
----
t1:
 (f)
 (k)
 (k, f)

index-recommendations
SELECT * FROM t1 WHERE k = 1 AND f != 0
----
creation: CREATE INDEX ON t.public.t1 (k) STORING (i, f, s);
--
optimal plan:
select
 ├── columns: k:1!null i:2 f:3!null s:4
 ├── cost: 29.0500001
 ├── fd: ()-->(1)
 ├── scan t1@_hyp_1
 │    ├── columns: k:1!null i:2 f:3 s:4
 │    ├── constraint: /1/5: [/1 - /1]
 │    ├── cost: 28.9200001
 │    └── fd: ()-->(1)
 └── filters
      └── f:3 != 0.0 [outer=(3), constraints=(/3: (/NULL - /-5e-324] [/5e-324 - ]; tight)]

# Multi-column combinations used: EQ, EQ + R.
index-candidates
SELECT  k, i, f FROM t1 WHERE k = 1 AND i = 2 AND f > 0
----
t1:
 (f)
 (i)
 (k)
 (k, i)
 (k, i, f)

index-recommendations
SELECT k, i, f FROM t1 WHERE k = 1 AND i = 2 AND f > 0
----
creation: CREATE INDEX ON t.public.t1 (k, i, f);
--
optimal plan:
scan t1@_hyp_5
 ├── columns: k:1!null i:2!null f:3!null
 ├── constraint: /1/2/3/5: [/1/2/5e-324 - /1/2]
 ├── cost: 18.3187027
 └── fd: ()-->(1,2)

index-candidates
SELECT  k, i, s FROM t1 WHERE k = 1 AND i = 2 AND s IS NOT NULL
----
t1:
 (i)
 (k)
 (k, i)
 (k, i, s)
 (s)

index-recommendations
SELECT k, i, s FROM t1 WHERE k = 1 AND i = 2 AND s IS NOT NULL
----
creation: CREATE INDEX ON t.public.t1 (k, i, s);
--
optimal plan:
scan t1@_hyp_5
 ├── columns: k:1!null i:2!null s:4!null
 ├── constraint: /1/2/4/5: (/1/2/NULL - /1/2]
 ├── cost: 18.907147
 └── fd: ()-->(1,2)

# Multi-column combinations used: J + R.
index-candidates
SELECT t1.k, t1.f FROM t1 JOIN t2 ON t1.k != t2.k WHERE t1.f > 0
----
t1:
 (f)
 (k)
 (k, f)
t2:
 (k)

index-recommendations
SELECT t1.k, t1.f FROM t1 JOIN t2 ON t1.k != t2.k WHERE t1.f > 0
----
creation: CREATE INDEX ON t.public.t1 (f) STORING (k);
--
optimal plan:
project
 ├── columns: k:1!null f:3!null
 ├── cost: 5893.93736
 └── inner-join (cross)
      ├── columns: t1.k:1!null f:3!null t2.k:8!null
      ├── cost: 4804.91736
      ├── scan t2
      │    ├── columns: t2.k:8
      │    └── cost: 1078.52
      ├── scan t1@_hyp_1
      │    ├── columns: t1.k:1 f:3!null
      │    ├── constraint: /3/5: [/5e-324 - ]
      │    └── cost: 374.686667
      └── filters
           └── t1.k:1 != t2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ])]

index-candidates
SELECT t1.k, t1.s FROM t1 JOIN t2 ON t1.k = t2.k WHERE t1.s IS NOT NULL
----
t1:
 (k)
 (k, s)
 (s)
t2:
 (k)

index-recommendations
SELECT t1.k, t1.s FROM t1 JOIN t2 ON t1.k = t2.k WHERE t1.s IS NOT NULL
----
creation: CREATE INDEX ON t.public.t1 (s) STORING (k);
--
optimal plan:
project
 ├── columns: k:1!null s:4!null
 ├── cost: 2379.90804
 └── inner-join (hash)
      ├── columns: t1.k:1!null t1.s:4!null t2.k:8!null
      ├── cost: 2282.85814
      ├── fd: (1)==(8), (8)==(1)
      ├── scan t2
      │    ├── columns: t2.k:8
      │    └── cost: 1078.52
      ├── scan t1@_hyp_1
      │    ├── columns: t1.k:1 t1.s:4!null
      │    ├── constraint: /4/5: (/NULL - ]
      │    └── cost: 1077.32
      └── filters
           └── t1.k:1 = t2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)]

# Multi-column combinations used: EQ, EQ + J.
index-candidates
SELECT t1.i, t1.s FROM t1 JOIN t2 ON t1.k != t2.k WHERE t1.i = 2 AND t1.s = 'NG'
----
t1:
 (i)
 (i, s)
 (i, s, k)
 (k)
 (s)
t2:
 (k)

index-recommendations
SELECT t1.i, t1.s FROM t1 JOIN t2 ON t1.k != t2.k WHERE t1.i = 2 AND t1.s = 'NG'
----
creation: CREATE INDEX ON t.public.t1 (i, s) STORING (k);
--
optimal plan:
project
 ├── columns: i:2!null s:4!null
 ├── cost: 1122.17334
 ├── fd: ()-->(2,4)
 └── inner-join (cross)
      ├── columns: t1.k:1!null t1.i:2!null t1.s:4!null t2.k:8!null
      ├── cost: 1119.17772
      ├── fd: ()-->(2,4)
      ├── scan t2
      │    ├── columns: t2.k:8
      │    └── cost: 1078.52
      ├── scan t1@_hyp_4
      │    ├── columns: t1.k:1 t1.i:2!null t1.s:4!null
      │    ├── constraint: /2/4/5: [/2/'NG' - /2/'NG']
      │    ├── cost: 19.0036757
      │    └── fd: ()-->(2,4)
      └── filters
           └── t1.k:1 != t2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ])]

index-candidates
SELECT t1.i, t1.s FROM t1 JOIN t2 ON t1.k = t2.k WHERE t1.i = 2 AND t1.s = 'NG'
----
t1:
 (i)
 (i, s)
 (i, s, k)
 (k)
 (s)
t2:
 (k)

index-recommendations
SELECT t1.i, t1.s FROM t1 JOIN t2 ON t1.k = t2.k WHERE t1.i = 2 AND t1.s = 'NG'
----
creation: CREATE INDEX ON t.public.t1 (i, s) STORING (k);
creation: CREATE INDEX ON t.public.t2 (k);
--
optimal plan:
project
 ├── columns: i:2!null s:4!null
 ├── cost: 59.7384854
 ├── fd: ()-->(2,4)
 └── inner-join (lookup t2@_hyp_1)
      ├── columns: t1.k:1!null t1.i:2!null t1.s:4!null t2.k:8!null
      ├── key columns: [1] = [8]
      ├── cost: 59.6292168
      ├── fd: ()-->(2,4), (1)==(8), (8)==(1)
      ├── scan t1@_hyp_4
      │    ├── columns: t1.k:1 t1.i:2!null t1.s:4!null
      │    ├── constraint: /2/4/5: [/2/'NG' - /2/'NG']
      │    ├── cost: 19.0036757
      │    └── fd: ()-->(2,4)
      └── filters (true)

# Multi-column combinations used: EQ, EQ + R, J + R, EQ + J, EQ + J + R.
index-candidates
SELECT count(*)
FROM t1 LEFT JOIN t2
ON t1.k != t2.k
GROUP BY t2.s, t2.i
UNION ALL
SELECT count(*)
FROM (
  SELECT *
  FROM t1
  WHERE t1.f > t1.i
  AND t1.s = 'NG'
)
----
t1:
 (f)
 (i)
 (k)
 (k, f)
 (k, i)
 (s)
 (s, f)
 (s, i)
 (s, k)
 (s, k, f)
 (s, k, i)
t2:
 (i, s)
 (k)

index-recommendations
SELECT count(*)
FROM t1 LEFT JOIN t2
ON t1.k != t2.k
GROUP BY t2.s, t2.i
UNION ALL
SELECT count(*)
FROM (
  SELECT *
  FROM t1
  WHERE t1.f > t1.i
  AND t1.s = 'NG'
)
----
creation: CREATE INDEX ON t.public.t1 (s, f) STORING (i);
--
optimal plan:
union-all
 ├── columns: count:23!null
 ├── left columns: count_rows:14
 ├── right columns: count_rows:22
 ├── cardinality: [1 - ]
 ├── cost: 25610.0782
 ├── project
 │    ├── columns: count_rows:14!null
 │    ├── cost: 25571.0458
 │    └── group-by (hash)
 │         ├── columns: t2.i:9 t2.s:10 count_rows:14!null
 │         ├── grouping columns: t2.i:9 t2.s:10
 │         ├── cost: 25561.0258
 │         ├── key: (9,10)
 │         ├── fd: (9,10)-->(14)
 │         ├── left-join (cross)
 │         │    ├── columns: t1.k:1 t2.k:8 t2.i:9 t2.s:10
 │         │    ├── cost: 12217.5163
 │         │    ├── scan t1
 │         │    │    ├── columns: t1.k:1
 │         │    │    └── cost: 1088.62
 │         │    ├── scan t2
 │         │    │    ├── columns: t2.k:8 t2.i:9 t2.s:10
 │         │    │    └── cost: 1098.72
 │         │    └── filters
 │         │         └── t1.k:1 != t2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ])]
 │         └── aggregations
 │              └── count-rows [as=count_rows:14]
 └── scalar-group-by
      ├── columns: count_rows:22!null
      ├── cardinality: [1 - 1]
      ├── cost: 29.002367
      ├── key: ()
      ├── fd: ()-->(22)
      ├── select
      │    ├── columns: t1.i:16!null f:17!null t1.s:18!null
      │    ├── cost: 28.9391
      │    ├── fd: ()-->(18)
      │    ├── scan t1@_hyp_5
      │    │    ├── columns: t1.i:16 f:17!null t1.s:18!null
      │    │    ├── constraint: /18/17/19: (/'NG'/NULL - /'NG']
      │    │    ├── cost: 28.8092
      │    │    └── fd: ()-->(18)
      │    └── filters
      │         └── f:17 > t1.i:16 [outer=(16,17), constraints=(/16: (/NULL - ]; /17: (/NULL - ])]
      └── aggregations
           └── count-rows [as=count_rows:22]

index-candidates
SELECT count(*)
FROM t1 LEFT JOIN t2
ON t1.k = t2.k
GROUP BY t2.s, t2.i
UNION ALL
SELECT count(*)
FROM (
  SELECT *
  FROM t1
  WHERE t1.f > t1.i
  AND t1.s = 'NG'
)
----
t1:
 (f)
 (i)
 (k)
 (k, f)
 (k, i)
 (s)
 (s, f)
 (s, i)
 (s, k)
 (s, k, f)
 (s, k, i)
t2:
 (i, s)
 (k)

index-recommendations
SELECT count(*)
FROM t1 LEFT JOIN t2
ON t1.k = t2.k
GROUP BY t2.s, t2.i
UNION ALL
SELECT count(*)
FROM (
  SELECT *
  FROM t1
  WHERE t1.f > t1.i
  AND t1.s = 'NG'
)
----
creation: CREATE INDEX ON t.public.t1 (k);
creation: CREATE INDEX ON t.public.t1 (s, f) STORING (i);
creation: CREATE INDEX ON t.public.t2 (k) STORING (i, s);
--
optimal plan:
union-all
 ├── columns: count:23!null
 ├── left columns: count_rows:14
 ├── right columns: count_rows:22
 ├── cardinality: [1 - ]
 ├── cost: 2766.58731
 ├── project
 │    ├── columns: count_rows:14!null
 │    ├── cost: 2727.55537
 │    └── group-by (hash)
 │         ├── columns: t2.i:9 t2.s:10 count_rows:14!null
 │         ├── grouping columns: t2.i:9 t2.s:10
 │         ├── cost: 2717.53581
 │         ├── key: (9,10)
 │         ├── fd: (9,10)-->(14)
 │         ├── left-join (merge)
 │         │    ├── columns: t1.k:1 t2.k:8 t2.i:9 t2.s:10
 │         │    ├── left ordering: +1
 │         │    ├── right ordering: +8
 │         │    ├── cost: 2307.36
 │         │    ├── scan t1@_hyp_4
 │         │    │    ├── columns: t1.k:1
 │         │    │    ├── cost: 1088.62
 │         │    │    └── ordering: +1
 │         │    ├── scan t2@_hyp_2
 │         │    │    ├── columns: t2.k:8 t2.i:9 t2.s:10
 │         │    │    ├── cost: 1098.72
 │         │    │    └── ordering: +8
 │         │    └── filters (true)
 │         └── aggregations
 │              └── count-rows [as=count_rows:14]
 └── scalar-group-by
      ├── columns: count_rows:22!null
      ├── cardinality: [1 - 1]
      ├── cost: 29.002367
      ├── key: ()
      ├── fd: ()-->(22)
      ├── select
      │    ├── columns: t1.i:16!null f:17!null t1.s:18!null
      │    ├── cost: 28.9391
      │    ├── fd: ()-->(18)
      │    ├── scan t1@_hyp_5
      │    │    ├── columns: t1.i:16 f:17!null t1.s:18!null
      │    │    ├── constraint: /18/17/19: (/'NG'/NULL - /'NG']
      │    │    ├── cost: 28.8092
      │    │    └── fd: ()-->(18)
      │    └── filters
      │         └── f:17 > t1.i:16 [outer=(16,17), constraints=(/16: (/NULL - ]; /17: (/NULL - ])]
      └── aggregations
           └── count-rows [as=count_rows:22]

index-candidates
SELECT count(*)
FROM t1 LEFT JOIN t2
ON t1.k = t2.k
GROUP BY t2.s, t2.i
UNION ALL
SELECT count(*)
FROM (
  SELECT *
  FROM t1
  WHERE t1.f != 0
  AND t1.s = 'NG'
)
----
t1:
 (f)
 (k)
 (k, f)
 (s)
 (s, f)
 (s, k)
 (s, k, f)
t2:
 (i, s)
 (k)

index-recommendations
SELECT count(*)
FROM t1 LEFT JOIN t2
ON t1.k = t2.k
GROUP BY t2.s, t2.i
UNION ALL
SELECT count(*)
FROM (
  SELECT *
  FROM t1
  WHERE t1.f != 0
  AND t1.s = 'NG'
)
----
creation: CREATE INDEX ON t.public.t1 (k);
creation: CREATE INDEX ON t.public.t1 (s) STORING (f);
creation: CREATE INDEX ON t.public.t2 (k) STORING (i, s);
--
optimal plan:
union-all
 ├── columns: count:23!null
 ├── left columns: count_rows:14
 ├── right columns: count_rows:22
 ├── cardinality: [1 - ]
 ├── cost: 2766.55828
 ├── project
 │    ├── columns: count_rows:14!null
 │    ├── cost: 2727.55537
 │    └── group-by (hash)
 │         ├── columns: t2.i:9 t2.s:10 count_rows:14!null
 │         ├── grouping columns: t2.i:9 t2.s:10
 │         ├── cost: 2717.53581
 │         ├── key: (9,10)
 │         ├── fd: (9,10)-->(14)
 │         ├── left-join (merge)
 │         │    ├── columns: t1.k:1 t2.k:8 t2.i:9 t2.s:10
 │         │    ├── left ordering: +1
 │         │    ├── right ordering: +8
 │         │    ├── cost: 2307.36
 │         │    ├── scan t1@_hyp_3
 │         │    │    ├── columns: t1.k:1
 │         │    │    ├── cost: 1088.62
 │         │    │    └── ordering: +1
 │         │    ├── scan t2@_hyp_2
 │         │    │    ├── columns: t2.k:8 t2.i:9 t2.s:10
 │         │    │    ├── cost: 1098.72
 │         │    │    └── ordering: +8
 │         │    └── filters (true)
 │         └── aggregations
 │              └── count-rows [as=count_rows:14]
 └── scalar-group-by
      ├── columns: count_rows:22!null
      ├── cardinality: [1 - 1]
      ├── cost: 28.9733334
      ├── key: ()
      ├── fd: ()-->(22)
      ├── select
      │    ├── columns: f:17!null t1.s:18!null
      │    ├── cost: 28.8500001
      │    ├── fd: ()-->(18)
      │    ├── scan t1@_hyp_1
      │    │    ├── columns: f:17 t1.s:18!null
      │    │    ├── constraint: /18/19: [/'NG' - /'NG']
      │    │    ├── cost: 28.7200001
      │    │    └── fd: ()-->(18)
      │    └── filters
      │         └── f:17 != 0.0 [outer=(17), constraints=(/17: (/NULL - /-5e-324] [/5e-324 - ]; tight)]
      └── aggregations
           └── count-rows [as=count_rows:22]

# No rule 5 multi-column index combinations.
index-candidates
SELECT t1.k, t1.i, t2.i
FROM t1 LEFT JOIN t2
ON t1.k = t2.k
WHERE EXISTS (SELECT * FROM t3 WHERE t3.f > t3.k)
ORDER BY t1.k, t2.i, t1.i DESC
----
t1:
 (k)
 (k, i DESC)
t2:
 (i)
 (k)
t3:
 (f)
 (k)

index-recommendations
SELECT t1.k, t1.i, t2.i
FROM t1 LEFT JOIN t2
ON t1.k = t2.k
WHERE EXISTS (SELECT k, i FROM t3 WHERE t3.f > t3.k)
ORDER BY t1.k, t2.i, t1.i DESC
----
creation: CREATE INDEX ON t.public.t1 (k, i DESC);
creation: CREATE INDEX ON t.public.t2 (k) STORING (i);
creation: CREATE INDEX ON t.public.t3 (f) STORING (k);
--
optimal plan:
sort (segmented)
 ├── columns: k:1 i:2 i:9
 ├── cost: 2432.08948
 ├── ordering: +1,+9,-2
 └── project
      ├── columns: t1.k:1 t1.i:2 t2.i:9
      ├── cost: 2279.40815
      ├── ordering: +1
      └── left-join (merge)
           ├── columns: t1.k:1 t1.i:2 t2.k:8 t2.i:9
           ├── left ordering: +1
           ├── right ordering: +8
           ├── cost: 2268.08095
           ├── ordering: +1
           ├── select
           │    ├── columns: t1.k:1 t1.i:2
           │    ├── cost: 1130.09355
           │    ├── ordering: +1
           │    ├── scan t1@_hyp_1
           │    │    ├── columns: t1.k:1 t1.i:2
           │    │    ├── cost: 1098.72
           │    │    └── ordering: +1
           │    └── filters
           │         └── coalesce [subquery]
           │              ├── subquery
           │              │    └── project
           │              │         ├── columns: column22:22!null
           │              │         ├── cardinality: [0 - 1]
           │              │         ├── cost: 21.3435466
           │              │         ├── key: ()
           │              │         ├── fd: ()-->(22)
           │              │         ├── limit
           │              │         │    ├── columns: t3.k:14!null t3.f:16!null
           │              │         │    ├── cardinality: [0 - 1]
           │              │         │    ├── cost: 21.3135466
           │              │         │    ├── key: ()
           │              │         │    ├── fd: ()-->(14,16)
           │              │         │    ├── select
           │              │         │    │    ├── columns: t3.k:14!null t3.f:16!null
           │              │         │    │    ├── cost: 21.2935466
           │              │         │    │    ├── limit hint: 1.00
           │              │         │    │    ├── scan t3@_hyp_1
           │              │         │    │    │    ├── columns: t3.k:14 t3.f:16!null
           │              │         │    │    │    ├── constraint: /16/18: (/NULL - ]
           │              │         │    │    │    ├── cost: 21.2332132
           │              │         │    │    │    └── limit hint: 3.00
           │              │         │    │    └── filters
           │              │         │    │         └── t3.f:16 > t3.k:14 [outer=(14,16), constraints=(/14: (/NULL - ]; /16: (/NULL - ])]
           │              │         │    └── 1
           │              │         └── projections
           │              │              └── true [as=column22:22]
           │              └── false
           ├── select
           │    ├── columns: t2.k:8 t2.i:9
           │    ├── cost: 1119.99355
           │    ├── ordering: +8
           │    ├── scan t2@_hyp_2
           │    │    ├── columns: t2.k:8 t2.i:9
           │    │    ├── cost: 1088.62
           │    │    └── ordering: +8
           │    └── filters
           │         └── coalesce [subquery]
           │              ├── subquery
           │              │    └── project
           │              │         ├── columns: column22:22!null
           │              │         ├── cardinality: [0 - 1]
           │              │         ├── cost: 21.3435466
           │              │         ├── key: ()
           │              │         ├── fd: ()-->(22)
           │              │         ├── limit
           │              │         │    ├── columns: t3.k:14!null t3.f:16!null
           │              │         │    ├── cardinality: [0 - 1]
           │              │         │    ├── cost: 21.3135466
           │              │         │    ├── key: ()
           │              │         │    ├── fd: ()-->(14,16)
           │              │         │    ├── select
           │              │         │    │    ├── columns: t3.k:14!null t3.f:16!null
           │              │         │    │    ├── cost: 21.2935466
           │              │         │    │    ├── limit hint: 1.00
           │              │         │    │    ├── scan t3@_hyp_1
           │              │         │    │    │    ├── columns: t3.k:14 t3.f:16!null
           │              │         │    │    │    ├── constraint: /16/18: (/NULL - ]
           │              │         │    │    │    ├── cost: 21.2332132
           │              │         │    │    │    └── limit hint: 3.00
           │              │         │    │    └── filters
           │              │         │    │         └── t3.f:16 > t3.k:14 [outer=(14,16), constraints=(/14: (/NULL - ]; /16: (/NULL - ])]
           │              │         │    └── 1
           │              │         └── projections
           │              │              └── true [as=column22:22]
           │              └── false
           └── filters (true)

# Tests for set operation indexes. See rule 6 in indexrec.FindIndexCandidateSet.

index-candidates
SELECT k FROM t1 UNION SELECT i FROM t1
----
t1:
 (i)
 (k)

index-recommendations
SELECT k FROM t1 UNION SELECT i FROM t1
----
creation: CREATE INDEX ON t.public.t1 (k);
creation: CREATE INDEX ON t.public.t1 (i);
--
optimal plan:
union
 ├── columns: k:15
 ├── left columns: t1.k:1
 ├── right columns: i:9
 ├── internal-ordering: +15
 ├── cost: 2179.26
 ├── key: (15)
 ├── scan t1@_hyp_1
 │    ├── columns: t1.k:1
 │    ├── cost: 1088.62
 │    └── ordering: +1
 └── scan t1@_hyp_2
      ├── columns: i:9
      ├── cost: 1088.62
      └── ordering: +9

index-candidates
SELECT k FROM t1 INTERSECT SELECT i FROM t1
----
t1:
 (i)
 (k)

index-recommendations
SELECT k FROM t1 INTERSECT SELECT i FROM t1
----
creation: CREATE INDEX ON t.public.t1 (k);
creation: CREATE INDEX ON t.public.t1 (i);
--
optimal plan:
intersect
 ├── columns: k:1
 ├── left columns: k:1
 ├── right columns: i:9
 ├── internal-ordering: +1
 ├── cost: 2178.26
 ├── key: (1)
 ├── scan t1@_hyp_1
 │    ├── columns: k:1
 │    ├── cost: 1088.62
 │    └── ordering: +1
 └── scan t1@_hyp_2
      ├── columns: i:9
      ├── cost: 1088.62
      └── ordering: +9

index-candidates
SELECT k FROM t1 INTERSECT ALL SELECT i FROM t1
----
t1:
 (i)
 (k)

index-recommendations
SELECT k FROM t1 INTERSECT ALL SELECT i FROM t1
----
creation: CREATE INDEX ON t.public.t1 (k);
creation: CREATE INDEX ON t.public.t1 (i);
--
optimal plan:
intersect-all
 ├── columns: k:1
 ├── left columns: k:1
 ├── right columns: i:9
 ├── internal-ordering: +1
 ├── cost: 2187.26
 ├── scan t1@_hyp_1
 │    ├── columns: k:1
 │    ├── cost: 1088.62
 │    └── ordering: +1
 └── scan t1@_hyp_2
      ├── columns: i:9
      ├── cost: 1088.62
      └── ordering: +9

index-candidates
SELECT k FROM t1 EXCEPT SELECT i FROM t1
----
t1:
 (i)
 (k)

index-recommendations
SELECT k FROM t1 EXCEPT SELECT i FROM t1
----
creation: CREATE INDEX ON t.public.t1 (k);
creation: CREATE INDEX ON t.public.t1 (i);
--
optimal plan:
except
 ├── columns: k:1
 ├── left columns: k:1
 ├── right columns: i:9
 ├── internal-ordering: +1
 ├── cost: 2178.26
 ├── key: (1)
 ├── scan t1@_hyp_1
 │    ├── columns: k:1
 │    ├── cost: 1088.62
 │    └── ordering: +1
 └── scan t1@_hyp_2
      ├── columns: i:9
      ├── cost: 1088.62
      └── ordering: +9

index-candidates
SELECT k FROM t1 EXCEPT ALL SELECT i FROM t1
----
t1:
 (i)
 (k)

index-recommendations
SELECT k FROM t1 EXCEPT ALL SELECT i FROM t1
----
creation: CREATE INDEX ON t.public.t1 (k);
creation: CREATE INDEX ON t.public.t1 (i);
--
optimal plan:
except-all
 ├── columns: k:1
 ├── left columns: k:1
 ├── right columns: i:9
 ├── internal-ordering: +1
 ├── cost: 2187.26
 ├── scan t1@_hyp_1
 │    ├── columns: k:1
 │    ├── cost: 1088.62
 │    └── ordering: +1
 └── scan t1@_hyp_2
      ├── columns: i:9
      ├── cost: 1088.62
      └── ordering: +9

index-candidates
SELECT k, f FROM t1 UNION SELECT i, f FROM t1
----
t1:
 (i, f)
 (k, f)

index-recommendations
SELECT k, f FROM t1 UNION SELECT i, f FROM t1
----
creation: CREATE INDEX ON t.public.t1 (k, f);
creation: CREATE INDEX ON t.public.t1 (i, f);
--
optimal plan:
union
 ├── columns: k:15 f:16
 ├── left columns: t1.k:1 t1.f:3
 ├── right columns: i:9 t1.f:10
 ├── internal-ordering: +15,+16
 ├── cost: 2217.46
 ├── key: (15,16)
 ├── scan t1@_hyp_1
 │    ├── columns: t1.k:1 t1.f:3
 │    ├── cost: 1098.72
 │    └── ordering: +1,+3
 └── scan t1@_hyp_2
      ├── columns: i:9 t1.f:10
      ├── cost: 1098.72
      └── ordering: +9,+10

# Tests for inverted index recommendations. See rule 7 of
# indexrec.FindIndexCandidateSet.

index-candidates
SELECT k, f FROM t4 WHERE j->'a' = '1'
----
t4:
 (j)

index-recommendations
SELECT k, f FROM t4 WHERE j->'a' = '1'
----
creation: CREATE INVERTED INDEX ON t.public.t4 (j);
--
optimal plan:
project
 ├── columns: k:1 f:3
 ├── immutable
 ├── cost: 811.393333
 └── index-join t4
      ├── columns: k:1 f:3 j:4
      ├── immutable
      ├── cost: 810.262222
      └── scan t4@_hyp_1,inverted
           ├── columns: rowid:6!null
           ├── inverted constraint: /9/6
           │    └── spans: ["a"/1, "a"/1]
           ├── cost: 132.464444
           └── key: (6)

index-candidates
SELECT k, f FROM t4 WHERE j @> '{"foo": "1"}'
----
t4:
 (j)

index-recommendations
SELECT k, f FROM t4 WHERE j @> '{"foo": "1"}'
----
creation: CREATE INVERTED INDEX ON t.public.t4 (j);
--
optimal plan:
project
 ├── columns: k:1 f:3
 ├── immutable
 ├── cost: 809.048889
 └── index-join t4
      ├── columns: k:1 f:3 j:4!null
      ├── immutable
      ├── cost: 807.928889
      └── scan t4@_hyp_1,inverted
           ├── columns: rowid:6!null
           ├── inverted constraint: /9/6
           │    └── spans: ["foo"/"1", "foo"/"1"]
           ├── cost: 132.464444
           └── key: (6)

index-candidates
SELECT k, f FROM t4 WHERE j <@ '{"foo": "1"}'
----
t4:
 (j)

index-recommendations
SELECT k, f FROM t4 WHERE j <@ '{"foo": "1"}'
----
creation: CREATE INVERTED INDEX ON t.public.t4 (j);
--
optimal plan:
project
 ├── columns: k:1 f:3
 ├── immutable
 ├── cost: 820.998889
 └── select
      ├── columns: k:1 f:3 j:4
      ├── immutable
      ├── cost: 817.645556
      ├── index-join t4
      │    ├── columns: k:1 f:3 j:4
      │    ├── cost: 816.504444
      │    └── inverted-filter
      │         ├── columns: rowid:6!null
      │         ├── inverted expression: /9
      │         │    ├── tight: false, unique: false
      │         │    └── union spans
      │         │         ├── [{}, {}]
      │         │         └── ["foo"/"1", "foo"/"1"]
      │         ├── cost: 138.706667
      │         ├── key: (6)
      │         └── scan t4@_hyp_1,inverted
      │              ├── columns: rowid:6!null j_inverted_key:9!null
      │              ├── inverted constraint: /9/6
      │              │    └── spans
      │              │         ├── [{}, {}]
      │              │         └── ["foo"/"1", "foo"/"1"]
      │              └── cost: 137.575556
      └── filters
           └── j:4 <@ '{"foo": "1"}' [outer=(4), immutable]

index-candidates
SELECT j FROM t4 WHERE j @> '{"foo": "1", "bar": "2"}'
----
t4:
 (j)

index-recommendations
SELECT j FROM t4 WHERE j @> '{"foo": "1", "bar": "2"}'
----
creation: CREATE INVERTED INDEX ON t.public.t4 (j);
--
optimal plan:
inner-join (lookup t4)
 ├── columns: j:4!null
 ├── key columns: [6] = [6]
 ├── lookup columns are key
 ├── immutable
 ├── cost: 245.980741
 ├── inner-join (zigzag t4@_hyp_1,inverted t4@_hyp_1,inverted)
 │    ├── columns: rowid:6!null
 │    ├── eq columns: [6] = [6]
 │    ├── left fixed columns: [9] = ['\x37626172000112320001']
 │    ├── right fixed columns: [9] = ['\x37666f6f000112310001']
 │    ├── cost: 167.155802
 │    └── filters (true)
 └── filters (true)

index-candidates
SELECT k, f FROM t4 WHERE j <@ '{"foo": "1"}' AND k = 1
----
t4:
 (j)
 (k)
 (k, j)

index-recommendations
SELECT k, f FROM t4 WHERE j <@ '{"foo": "1"}' AND k = 1
----
creation: CREATE INVERTED INDEX ON t.public.t4 (k, j);
--
optimal plan:
project
 ├── columns: k:1!null f:3
 ├── immutable
 ├── cost: 26.1100001
 ├── fd: ()-->(1)
 └── select
      ├── columns: k:1!null f:3 j:4
      ├── immutable
      ├── cost: 26.0566667
      ├── fd: ()-->(1)
      ├── index-join t4
      │    ├── columns: k:1 f:3 j:4
      │    ├── cost: 26.0155556
      │    └── inverted-filter
      │         ├── columns: rowid:6!null
      │         ├── inverted expression: /10
      │         │    ├── tight: false, unique: false
      │         │    └── union spans
      │         │         ├── [{}, {}]
      │         │         └── ["foo"/"1", "foo"/"1"]
      │         ├── cost: 19.2177778
      │         ├── key: (6)
      │         └── scan t4@_hyp_3,inverted
      │              ├── columns: rowid:6!null j_inverted_key:10!null
      │              ├── constraint: /1: [/1 - /1]
      │              ├── inverted constraint: /10/6
      │              │    └── spans
      │              │         ├── [{}, {}]
      │              │         └── ["foo"/"1", "foo"/"1"]
      │              └── cost: 19.1866667
      └── filters
           └── j:4 <@ '{"foo": "1"}' [outer=(4), immutable]

index-candidates
SELECT k, f FROM t4 WHERE j <@ '{"foo": "1"}' AND k = 1 AND i = 2
----
t4:
 (i)
 (i, j)
 (j)
 (k)
 (k, i)
 (k, i, j)
 (k, j)

index-recommendations
SELECT k, f FROM t4 WHERE j <@ '{"foo": "1"}' AND k = 1 AND i = 2
----
creation: CREATE INVERTED INDEX ON t.public.t4 (k, i, j);
--
optimal plan:
project
 ├── columns: k:1!null f:3
 ├── immutable
 ├── cost: 18.8406727
 ├── fd: ()-->(1)
 └── select
      ├── columns: k:1!null i:2!null f:3 j:4
      ├── immutable
      ├── cost: 18.8176366
      ├── fd: ()-->(1,2)
      ├── index-join t4
      │    ├── columns: k:1 i:2 f:3 j:4
      │    ├── cost: 18.7866246
      │    └── inverted-filter
      │         ├── columns: rowid:6!null
      │         ├── inverted expression: /12
      │         │    ├── tight: false, unique: false
      │         │    └── union spans
      │         │         ├── [{}, {}]
      │         │         └── ["foo"/"1", "foo"/"1"]
      │         ├── cost: 18.1482853
      │         ├── key: (6)
      │         └── scan t4@_hyp_7,inverted
      │              ├── columns: rowid:6!null j_inverted_key:12!null
      │              ├── constraint: /1/2: [/1/2 - /1/2]
      │              ├── inverted constraint: /12/6
      │              │    └── spans
      │              │         ├── [{}, {}]
      │              │         └── ["foo"/"1", "foo"/"1"]
      │              └── cost: 18.1272733
      └── filters
           └── j:4 <@ '{"foo": "1"}' [outer=(4), immutable]

index-candidates
SELECT k, f FROM t4 WHERE j IS NULL
----
t4:
 (j)

index-recommendations
SELECT k, f FROM t4 WHERE j IS NULL
----
no index recommendations
--
optimal plan:
project
 ├── columns: k:1 f:3
 ├── immutable
 ├── cost: 1129.07
 └── select
      ├── columns: k:1 f:3 j:4
      ├── immutable
      ├── cost: 1128.95
      ├── fd: ()-->(4)
      ├── scan t4
      │    ├── columns: k:1 f:3 j:4
      │    └── cost: 1118.92
      └── filters
           └── j:4 IS NULL [outer=(4), immutable, constraints=(/4: [/NULL - /NULL]; tight), fd=()-->(4)]

index-candidates
SELECT k, f FROM t4 WHERE a IS NULL
----
t4:
 (a)

index-recommendations
SELECT k, f FROM t4 WHERE a IS NULL
----
creation: CREATE INDEX ON t.public.t4 (a) STORING (k, f);
--
optimal plan:
project
 ├── columns: k:1 f:3
 ├── cost: 29.0400002
 └── scan t4@_hyp_1
      ├── columns: k:1 f:3 a:5
      ├── constraint: /5/6: [/NULL - /NULL]
      ├── cost: 28.9200002
      └── fd: ()-->(5)

index-candidates
SELECT k, f FROM t4 WHERE a @> ARRAY[1]
----
t4:
 (a)

# This test generates no index recommendations because the index recommended
# doesn't yet support recommending inverted indexes on types that are also
# forward indexable, such as trigrams and arrays.
# See #91777.

index-recommendations
SELECT k, f FROM t4 WHERE a @> ARRAY[1]
----
no index recommendations
--
optimal plan:
project
 ├── columns: k:1 f:3
 ├── immutable
 ├── cost: 1130.07
 └── select
      ├── columns: k:1 f:3 a:5!null
      ├── immutable
      ├── cost: 1128.95
      ├── scan t4
      │    ├── columns: k:1 f:3 a:5
      │    └── cost: 1118.92
      └── filters
           └── a:5 @> ARRAY[1] [outer=(5), immutable, constraints=(/5: (/NULL - ])]

index-candidates
SELECT k, f FROM t4 WHERE a = ARRAY[1]
----
t4:
 (a)

index-recommendations
SELECT k, f FROM t4 WHERE a = ARRAY[1]
----
creation: CREATE INDEX ON t.public.t4 (a) STORING (k, f);
--
optimal plan:
project
 ├── columns: k:1 f:3
 ├── cost: 29.0400001
 └── scan t4@_hyp_1
      ├── columns: k:1 f:3 a:5!null
      ├── constraint: /5/6: [/ARRAY[1] - /ARRAY[1]]
      ├── cost: 28.9200001
      └── fd: ()-->(5)

index-candidates
SELECT k, f FROM t4 WHERE a @> ARRAY[1] AND i = 1
----
t4:
 (a)
 (i)

index-recommendations
SELECT k, f FROM t4 WHERE a @> ARRAY[1] AND i = 1
----
creation: CREATE INDEX ON t.public.t4 (i) STORING (k, f, a);
--
optimal plan:
project
 ├── columns: k:1 f:3
 ├── immutable
 ├── cost: 29.1810001
 └── select
      ├── columns: k:1 i:2!null f:3 a:5!null
      ├── immutable
      ├── cost: 29.1500001
      ├── fd: ()-->(2)
      ├── scan t4@_hyp_2
      │    ├── columns: k:1 i:2!null f:3 a:5
      │    ├── constraint: /2/6: [/1 - /1]
      │    ├── cost: 29.0200001
      │    └── fd: ()-->(2)
      └── filters
           └── a:5 @> ARRAY[1] [outer=(5), immutable, constraints=(/5: (/NULL - ])]

index-candidates
SELECT k, f FROM t4 WHERE a @> ARRAY[1] AND f > 3
----
t4:
 (a)
 (f)

index-recommendations
SELECT k, f FROM t4 WHERE a @> ARRAY[1] AND f > 3
----
creation: CREATE INDEX ON t.public.t4 (f) STORING (k, a);
--
optimal plan:
project
 ├── columns: k:1 f:3!null
 ├── immutable
 ├── cost: 385.103333
 └── select
      ├── columns: k:1 f:3!null a:5!null
      ├── immutable
      ├── cost: 384.716667
      ├── scan t4@_hyp_2
      │    ├── columns: k:1 f:3!null a:5
      │    ├── constraint: /3/6: [/3.0000000000000004 - ]
      │    └── cost: 381.353333
      └── filters
           └── a:5 @> ARRAY[1] [outer=(5), immutable, constraints=(/5: (/NULL - ])]

# Ensure that index recommendations are not made if there is an existing
# inverted, partial, or expression index that can be used to create an
# equivalent or better plan.

# 1. Inverted index case:

exec-ddl
CREATE INVERTED INDEX inverted ON t4 (k, j)
----

index-recommendations
SELECT k, j FROM t4 WHERE k = 1 AND j->'a' = '1'
----
no index recommendations
--
optimal plan:
index-join t4
 ├── columns: k:1!null j:4
 ├── immutable
 ├── cost: 25.9622223
 ├── fd: ()-->(1)
 └── scan t4@inverted,inverted
      ├── columns: rowid:6!null
      ├── constraint: /1: [/1 - /1]
      ├── inverted constraint: /9/6
      │    └── spans: ["a"/1, "a"/1]
      ├── cost: 19.1755556
      └── key: (6)

exec-ddl
DROP INDEX t4@inverted
----

# 2. Partial index case:

# Test queries that should use existing partial indexes, partial unique indexes,
# and partial inverted indexes.

exec-ddl
CREATE INDEX partial_idx ON t4 (i) WHERE k > 1;
----

index-recommendations
SELECT i FROM t4 WHERE i = 1 AND k > 1;
----
no index recommendations
--
optimal plan:
project
 ├── columns: i:2!null
 ├── cost: 27.84
 ├── fd: ()-->(2)
 └── scan t4@partial_idx,partial
      ├── columns: i:2!null rowid:6!null
      ├── constraint: /2/6: [/1 - /1]
      ├── cost: 27.7266667
      ├── key: (6)
      └── fd: ()-->(2)

exec-ddl
CREATE UNIQUE INDEX partial_unique_idx ON t4 (f) WHERE i > 1;
----

index-recommendations
SELECT f FROM t4 WHERE f > 5 AND i > 1
----
no index recommendations
--
optimal plan:
project
 ├── columns: f:3!null
 ├── cost: 344.706667
 └── scan t4@partial_unique_idx,partial
      ├── columns: f:3!null rowid:6!null
      ├── constraint: /3: [/5.000000000000001 - ]
      ├── cost: 341.575556
      ├── key: (6)
      └── fd: (6)-->(3), (3)-->(6)

exec-ddl
CREATE INVERTED INDEX partial_inverted_idx ON t4 (j) WHERE i = 1;
----

index-recommendations
SELECT j FROM t4 WHERE j->'a' = '1' AND i = 1
----
no index recommendations
--
optimal plan:
project
 ├── columns: j:4
 ├── immutable
 ├── cost: 25.9822223
 └── index-join t4
      ├── columns: i:2!null j:4
      ├── immutable
      ├── cost: 25.9511112
      ├── fd: ()-->(2)
      └── scan t4@partial_inverted_idx,inverted,partial
           ├── columns: rowid:6!null
           ├── inverted constraint: /10/6
           │    └── spans: ["a"/1, "a"/1]
           ├── cost: 19.1644445
           └── key: (6)

# Ensure that we don't recommend replacing partial indexes, partial unique
# indexes, or partial inverted indexes.

index-recommendations
SELECT i FROM t4 WHERE i > 5
----
creation: CREATE INDEX ON t.public.t4 (i);
--
optimal plan:
scan t4@_hyp_4
 ├── columns: i:2!null
 ├── constraint: /2/6: [/6 - ]
 └── cost: 381.353333

index-recommendations
SELECT i, f FROM t4 WHERE f > 5
----
creation: CREATE INDEX ON t.public.t4 (f) STORING (i);
--
optimal plan:
scan t4@_hyp_4
 ├── columns: i:2 f:3!null
 ├── constraint: /3/6: [/5.000000000000001 - ]
 └── cost: 384.686667

index-recommendations
SELECT j FROM t4 WHERE j->'a' = '1'
----
creation: CREATE INVERTED INDEX ON t.public.t4 (j);
--
optimal plan:
index-join t4
 ├── columns: j:4
 ├── immutable
 ├── cost: 808.04
 └── scan t4@_hyp_4,inverted
      ├── columns: rowid:6!null
      ├── inverted constraint: /11/6
      │    └── spans: ["a"/1, "a"/1]
      ├── cost: 132.464444
      └── key: (6)

exec-ddl
DROP INDEX t4@partial_idx
----

exec-ddl
DROP INDEX t4@partial_unique_idx
----

exec-ddl
DROP INDEX t4@partial_inverted_idx
----

# 3. Expression index case:

exec-ddl
CREATE INDEX expr ON t1 (k, lower(s))
----

index-recommendations
SELECT k FROM t1 WHERE lower(s) = 'cockroach' AND k = 1
----
no index recommendations
--
optimal plan:
project
 ├── columns: k:1!null
 ├── immutable
 ├── cost: 19.0296847
 ├── fd: ()-->(1)
 └── scan t1@expr
      ├── columns: k:1!null rowid:5!null
      ├── constraint: /1/8/5: [/1/'cockroach' - /1/'cockroach']
      ├── cost: 18.9763514
      ├── key: (5)
      └── fd: ()-->(1)

exec-ddl
DROP INDEX t1@expr
----

# Regression test for #83965.
exec-ddl
CREATE TABLE t83965a (c INT);
----

exec-ddl
CREATE TABLE t83965b (c INT);
----

exec-ddl
CREATE TABLE t83965c (c INT);
----

exec-ddl
CREATE TABLE t83965d (c INT);
----

exec-ddl
CREATE TABLE t83965e (c VARBIT(1)[]);
----

exec-ddl
CREATE TABLE t83965f (c INT);
----

index-recommendations
SELECT bool_and(t83965e.c < t83965e.c) FROM t83965f, t83965a, t83965c, t83965b, t83965d, t83965e
----
no index recommendations
--
optimal plan:
scalar-group-by
 ├── columns: bool_and:26
 ├── cardinality: [1 - 1]
 ├── cost: 4.00225225e+16
 ├── key: ()
 ├── fd: ()-->(26)
 ├── project
 │    ├── columns: column25:25
 │    ├── cost: 3.00225225e+16
 │    ├── inner-join (cross)
 │    │    ├── columns: t83965e.c:21
 │    │    ├── cost: 1.00225225e+16
 │    │    ├── inner-join (cross)
 │    │    │    ├── columns: t83965e.c:21
 │    │    │    ├── cost: 1.00225225e+13
 │    │    │    ├── inner-join (cross)
 │    │    │    │    ├── columns: t83965e.c:21
 │    │    │    │    ├── cost: 1.00225268e+10
 │    │    │    │    ├── inner-join (cross)
 │    │    │    │    │    ├── columns: t83965e.c:21
 │    │    │    │    │    ├── cost: 10025702.6
 │    │    │    │    │    ├── inner-join (cross)
 │    │    │    │    │    │    ├── columns: t83965e.c:21
 │    │    │    │    │    │    ├── cost: 12136.7063
 │    │    │    │    │    │    ├── scan t83965d
 │    │    │    │    │    │    │    └── cost: 1048.22
 │    │    │    │    │    │    ├── scan t83965e
 │    │    │    │    │    │    │    ├── columns: t83965e.c:21
 │    │    │    │    │    │    │    └── cost: 1058.32
 │    │    │    │    │    │    └── filters (true)
 │    │    │    │    │    ├── scan t83965b
 │    │    │    │    │    │    └── cost: 1048.22
 │    │    │    │    │    └── filters (true)
 │    │    │    │    ├── scan t83965c
 │    │    │    │    │    └── cost: 1048.22
 │    │    │    │    └── filters (true)
 │    │    │    ├── scan t83965a
 │    │    │    │    └── cost: 1048.22
 │    │    │    └── filters (true)
 │    │    ├── scan t83965f
 │    │    │    └── cost: 1048.22
 │    │    └── filters (true)
 │    └── projections
 │         └── (t83965e.c:21 IS NOT DISTINCT FROM CAST(NULL AS VARBIT(1)[])) AND CAST(NULL AS BOOL) [as=column25:25, outer=(21)]
 └── aggregations
      └── bool-and [as=bool_and:26, outer=(25)]
           └── column25:25

exec-ddl
CREATE TABLE t5 (
  k INT PRIMARY KEY,
  v INT,
  i INT,
  j INT
)
----

exec-ddl
CREATE INDEX idx_1 ON t5(v)
----

exec-ddl
CREATE INDEX idx_2 ON t5(v) STORING (i)
----

# The index recommendation picks the best existing index candidate to drop. In
# this case, idx_2 is closer to this new index recommendation than idx_1 as it
# stores i.
index-recommendations
SELECT i, j FROM t5 WHERE v > 1
----
replacement: CREATE INDEX ON t.public.t5 (v) STORING (i, j); DROP INDEX t.public.t5@idx_2;
--
optimal plan:
project
 ├── columns: i:3 j:4
 ├── cost: 378.04
 └── scan t5@_hyp_3
      ├── columns: v:2!null i:3 j:4
      ├── constraint: /2/1: [/2 - ]
      └── cost: 374.686667

exec-ddl
DROP INDEX idx_2
----

exec-ddl
CREATE INDEX idx_3 ON t5(v, i)
----

# The index recommendation still picks idx_1 to drop because idx_3 does not
# store the same explicit columns as wanted.
index-recommendations
SELECT i, j FROM t5 WHERE v > 1
----
replacement: CREATE INDEX ON t.public.t5 (v) STORING (i, j); DROP INDEX t.public.t5@idx_1;
--
optimal plan:
project
 ├── columns: i:3 j:4
 ├── cost: 378.04
 └── scan t5@_hyp_3
      ├── columns: v:2!null i:3 j:4
      ├── constraint: /2/1: [/2 - ]
      └── cost: 374.686667

# The following tests check for not visible indexes. Note that for alter index
# visibility, the output, optimal plan, for index recommendations uses the
# hypothetical indexes created instead of using the visible version of the
# original indexes. This is not ideal, but we will accept this behaviour as of
# now since it is not user-facing.

exec-ddl
CREATE TABLE t_notvisible (
  k INT PRIMARY KEY,
  v INT,
  i INT,
  p INT,
  INDEX idx_i_visible(i) VISIBLE,
  INDEX idx_v_invisible(v) NOT VISIBLE,
  INDEX idx_p_halfvisible(p) VISIBILITY 0.5
)
----

# If the index is visible and stores the same column, no recommendation.
index-recommendations
SELECT i FROM t_notvisible WHERE i > 1
----
no index recommendations
--
optimal plan:
scan t_notvisible@idx_i_visible
 ├── columns: i:3!null
 ├── constraint: /3/1: [/2 - ]
 └── cost: 361.353333

# If the index is not visible and stores the same column, recommend alter index.
# visible.
index-recommendations
SELECT v FROM t_notvisible WHERE v > 1
----
alteration: ALTER INDEX t.public.t_notvisible@idx_v_invisible VISIBLE;
--
optimal plan:
scan t_notvisible@_hyp_4
 ├── columns: v:2!null
 ├── constraint: /2/1: [/2 - ]
 └── cost: 368.02

# Same with partially visible index.
index-recommendations
SELECT p FROM t_notvisible WHERE p > 1
----
alteration: ALTER INDEX t.public.t_notvisible@idx_p_halfvisible VISIBLE;
--
optimal plan:
scan t_notvisible@_hyp_4
 ├── columns: p:4!null
 ├── constraint: /4/1: [/2 - ]
 └── cost: 368.02

# If the index is not visible and need to store more columns, still recommend
# create index. We do not want to recommend dropping any invisible index.
index-recommendations
SELECT i FROM t_notvisible WHERE v > 1
----
creation: CREATE INDEX ON t.public.t_notvisible (v) STORING (i);
--
optimal plan:
project
 ├── columns: i:3
 ├── cost: 374.706667
 └── scan t_notvisible@_hyp_4
      ├── columns: v:2!null i:3
      ├── constraint: /2/1: [/2 - ]
      └── cost: 371.353333

# Same with partially visible index.
index-recommendations
SELECT i FROM t_notvisible WHERE p > 1
----
creation: CREATE INDEX ON t.public.t_notvisible (p) STORING (i);
--
optimal plan:
project
 ├── columns: i:3
 ├── cost: 374.706667
 └── scan t_notvisible@_hyp_4
      ├── columns: i:3 p:4!null
      ├── constraint: /4/1: [/2 - ]
      └── cost: 371.353333

# If there exists both not visible and visible indexes storing the same explicit
# column, the index recommendation should not recommend alter index just because
# not visible index is in the front.
exec-ddl
CREATE INDEX idx_v_visible ON t_notvisible(v) VISIBLE
----

index-recommendations
SELECT v FROM t_notvisible WHERE v > 1
----
no index recommendations
--
optimal plan:
scan t_notvisible@idx_v_visible
 ├── columns: v:2!null
 ├── constraint: /2/1: [/2 - ]
 └── cost: 361.353333

# Same with partially visible index.
exec-ddl
CREATE INDEX idx_p_visible ON t_notvisible(p) VISIBLE
----

index-recommendations
SELECT p FROM t_notvisible WHERE p > 1
----
no index recommendations
--
optimal plan:
scan t_notvisible@idx_p_visible
 ├── columns: p:4!null
 ├── constraint: /4/1: [/2 - ]
 └── cost: 361.353333

# If there exists both visible and invisible index, the index recommendation
# should not recommend dropping the not visible index even if it stores the same
# key column.
index-recommendations
SELECT i FROM t_notvisible WHERE v > 1
----
replacement: CREATE INDEX ON t.public.t_notvisible (v) STORING (i); DROP INDEX t.public.t_notvisible@idx_v_visible;
--
optimal plan:
project
 ├── columns: i:3
 ├── cost: 374.706667
 └── scan t_notvisible@_hyp_6
      ├── columns: v:2!null i:3
      ├── constraint: /2/1: [/2 - ]
      └── cost: 371.353333

# Same with partially visible index.
index-recommendations
SELECT i FROM t_notvisible WHERE p > 1
----
replacement: CREATE INDEX ON t.public.t_notvisible (p) STORING (i); DROP INDEX t.public.t_notvisible@idx_p_visible;
--
optimal plan:
project
 ├── columns: i:3
 ├── cost: 374.706667
 └── scan t_notvisible@_hyp_6
      ├── columns: i:3 p:4!null
      ├── constraint: /4/1: [/2 - ]
      └── cost: 371.353333

# Regression test for #108490. Alter the correct index when there are multiple
# invisible indexes.
exec-ddl
CREATE TABLE t108490 (
  k INT PRIMARY KEY,
  v INT,
  i INT,
  j INT,
  INDEX idx_v_invisible(v) NOT VISIBLE,
  INDEX idx_j_invisible(j) NOT VISIBLE,
  INDEX idx_i_j_invisible(i, j) NOT VISIBLE
)
----

index-recommendations
SELECT j FROM t108490 WHERE j > 1
----
alteration: ALTER INDEX t.public.t108490@idx_j_invisible VISIBLE;
--
optimal plan:
scan t108490@_hyp_4
 ├── columns: j:4!null
 ├── constraint: /4/1: [/2 - ]
 └── cost: 368.02

# We can use idx_i_j_invisible for this query, even though the hypothetical
# index would be (i) STORING j.
index-recommendations
SELECT j FROM t108490 WHERE i > 1
----
alteration: ALTER INDEX t.public.t108490@idx_i_j_invisible VISIBLE;
--
optimal plan:
project
 ├── columns: j:4
 ├── cost: 374.706667
 └── scan t108490@_hyp_4
      ├── columns: i:3!null j:4
      ├── constraint: /3/1: [/2 - ]
      └── cost: 371.353333

# We can't use idx_v_invisible for this query, since it doesn't include j.
index-recommendations
SELECT j FROM t108490 WHERE v > 1
----
creation: CREATE INDEX ON t.public.t108490 (v) STORING (j);
--
optimal plan:
project
 ├── columns: j:4
 ├── cost: 374.706667
 └── scan t108490@_hyp_4
      ├── columns: v:2!null j:4
      ├── constraint: /2/1: [/2 - ]
      └── cost: 371.353333

# Regression test for #109974. Do not panic by trying to find the inverted
# source column for a non-inverted index column.
exec-ddl
CREATE TABLE t109974a (
  g1 GEOGRAPHY
)
----

exec-ddl
CREATE TABLE t109974b (
  k2 INT PRIMARY KEY,
  g2 GEOGRAPHY,
  INVERTED INDEX (k2, g2) NOT VISIBLE
)
----

index-recommendations
DELETE FROM t109974b USING t109974a WHERE st_intersects(g2, g1);
----
creation: CREATE INVERTED INDEX ON t.public.t109974b (g2);
--
optimal plan:
delete t109974b
 ├── columns: <none>
 ├── fetch columns: k2:6 g2:7
 ├── passthrough columns g1:11 rowid:12 t109974a.crdb_internal_mvcc_timestamp:13 t109974a.tableoid:14
 ├── cardinality: [0 - 0]
 ├── volatile, mutations
 ├── cost: 112692.926
 └── distinct-on
      ├── columns: k2:6!null g2:7!null g1:11!null rowid:12!null t109974a.crdb_internal_mvcc_timestamp:13 t109974a.tableoid:14
      ├── grouping columns: k2:6!null
      ├── immutable
      ├── cost: 112692.916
      ├── key: (6)
      ├── fd: (6)-->(7,11-14), (12)-->(11,13,14)
      ├── inner-join (lookup t109974b)
      │    ├── columns: k2:6!null g2:7!null g1:11!null rowid:12!null t109974a.crdb_internal_mvcc_timestamp:13 t109974a.tableoid:14
      │    ├── key columns: [23] = [6]
      │    ├── lookup columns are key
      │    ├── immutable
      │    ├── cost: 111996.67
      │    ├── key: (6,12)
      │    ├── fd: (6)-->(7), (12)-->(11,13,14)
      │    ├── inner-join (inverted t109974b@_hyp_2,inverted)
      │    │    ├── columns: g1:11 rowid:12!null t109974a.crdb_internal_mvcc_timestamp:13 t109974a.tableoid:14 k2:23!null
      │    │    ├── inverted-expr
      │    │    │    └── st_intersects(g1:11, g2:24)
      │    │    ├── cost: 41492.64
      │    │    ├── key: (12,23)
      │    │    ├── fd: (12)-->(11,13,14)
      │    │    ├── scan t109974a
      │    │    │    ├── columns: g1:11 rowid:12!null t109974a.crdb_internal_mvcc_timestamp:13 t109974a.tableoid:14
      │    │    │    ├── cost: 1088.62
      │    │    │    ├── key: (12)
      │    │    │    └── fd: (12)-->(11,13,14)
      │    │    └── filters (true)
      │    └── filters
      │         └── st_intersects(g2:7, g1:11) [outer=(7,11), immutable, constraints=(/7: (/NULL - ]; /11: (/NULL - ])]
      └── aggregations
           ├── first-agg [as=g2:7, outer=(7)]
           │    └── g2:7
           ├── first-agg [as=g1:11, outer=(11)]
           │    └── g1:11
           ├── first-agg [as=rowid:12, outer=(12)]
           │    └── rowid:12
           ├── first-agg [as=t109974a.crdb_internal_mvcc_timestamp:13, outer=(13)]
           │    └── t109974a.crdb_internal_mvcc_timestamp:13
           └── first-agg [as=t109974a.tableoid:14, outer=(14)]
                └── t109974a.tableoid:14
