Correctly re-use hash tables in buildSubPlanHash().

Commit 356687bd8 omitted to remove leftover code for destroying
a hashed subplan's hash tables, with the result that the tables
were always rebuilt not reused; this leads to severe memory
leakage if a hashed subplan is re-executed enough times.
Moreover, the code for reusing the hashnulls table had a typo
that would have made it do the wrong thing if it were reached.

Looking at the code coverage report shows severe under-coverage
of the potential callers of ResetTupleHashTable, so add some test
cases that exercise them.

Andreas Karlsson and Tom Lane, per reports from Ranier Vilela
and Justin Pryzby.

Backpatch to v11, as the faulty commit was.

Discussion: https://postgr.es/m/edb62547-c453-c35b-3ed6-a069e4d6b937@proxel.se
Discussion: https://postgr.es/m/CAEudQAo=DCebm1RXtig9OH+QivpS97sMkikt0A9qHmMUs+g6ZA@mail.gmail.com
Discussion: https://postgr.es/m/20200210032547.GA1412@telsasoft.com
This commit is contained in:
Tom Lane 2020-02-29 13:48:09 -05:00
parent 6afc8aefd3
commit 58c47ccfff
3 changed files with 191 additions and 3 deletions

View File

@ -495,8 +495,6 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
* need to store subplan output rows that contain NULL.
*/
MemoryContextReset(node->hashtablecxt);
node->hashtable = NULL;
node->hashnulls = NULL;
node->havehashrows = false;
node->havenullrows = false;
@ -533,7 +531,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
}
if (node->hashnulls)
ResetTupleHashTable(node->hashtable);
ResetTupleHashTable(node->hashnulls);
else
node->hashnulls = BuildTupleHashTableExt(node->parent,
node->descRight,
@ -549,6 +547,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
node->hashtempcxt,
false);
}
else
node->hashnulls = NULL;
/*
* We are probably in a short-lived expression-evaluation context. Switch

View File

@ -873,6 +873,129 @@ explain (verbose, costs off)
One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1)
(8 rows)
--
-- Test rescan of a hashed subplan (the use of random() is to prevent the
-- sub-select from being pulled up, which would result in not hashing)
--
explain (verbose, costs off)
select sum(ss.tst::int) from
onek o cross join lateral (
select i.ten in (select f1 from int4_tbl where f1 <= o.hundred) as tst,
random() as r
from onek i where i.unique1 = o.unique1 ) ss
where o.ten = 0;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate
Output: sum((((hashed SubPlan 1)))::integer)
-> Nested Loop
Output: ((hashed SubPlan 1))
-> Seq Scan on public.onek o
Output: o.unique1, o.unique2, o.two, o.four, o.ten, o.twenty, o.hundred, o.thousand, o.twothousand, o.fivethous, o.tenthous, o.odd, o.even, o.stringu1, o.stringu2, o.string4
Filter: (o.ten = 0)
-> Index Scan using onek_unique1 on public.onek i
Output: (hashed SubPlan 1), random()
Index Cond: (i.unique1 = o.unique1)
SubPlan 1
-> Seq Scan on public.int4_tbl
Output: int4_tbl.f1
Filter: (int4_tbl.f1 <= $0)
(14 rows)
select sum(ss.tst::int) from
onek o cross join lateral (
select i.ten in (select f1 from int4_tbl where f1 <= o.hundred) as tst,
random() as r
from onek i where i.unique1 = o.unique1 ) ss
where o.ten = 0;
sum
-----
100
(1 row)
--
-- Test rescan of a SetOp node
--
explain (costs off)
select count(*) from
onek o cross join lateral (
select * from onek i1 where i1.unique1 = o.unique1
except
select * from onek i2 where i2.unique1 = o.unique2
) ss
where o.ten = 1;
QUERY PLAN
------------------------------------------------------------------------------
Aggregate
-> Nested Loop
-> Seq Scan on onek o
Filter: (ten = 1)
-> Subquery Scan on ss
-> HashSetOp Except
-> Append
-> Subquery Scan on "*SELECT* 1"
-> Index Scan using onek_unique1 on onek i1
Index Cond: (unique1 = o.unique1)
-> Subquery Scan on "*SELECT* 2"
-> Index Scan using onek_unique1 on onek i2
Index Cond: (unique1 = o.unique2)
(13 rows)
select count(*) from
onek o cross join lateral (
select * from onek i1 where i1.unique1 = o.unique1
except
select * from onek i2 where i2.unique1 = o.unique2
) ss
where o.ten = 1;
count
-------
100
(1 row)
--
-- Test rescan of a RecursiveUnion node
--
explain (costs off)
select sum(o.four), sum(ss.a) from
onek o cross join lateral (
with recursive x(a) as
(select o.four as a
union
select a + 1 from x
where a < 10)
select * from x
) ss
where o.ten = 1;
QUERY PLAN
---------------------------------------------------
Aggregate
-> Nested Loop
-> Seq Scan on onek o
Filter: (ten = 1)
-> CTE Scan on x
CTE x
-> Recursive Union
-> Result
-> WorkTable Scan on x x_1
Filter: (a < 10)
(10 rows)
select sum(o.four), sum(ss.a) from
onek o cross join lateral (
with recursive x(a) as
(select o.four as a
union
select a + 1 from x
where a < 10)
select * from x
) ss
where o.ten = 1;
sum | sum
------+------
1700 | 5350
(1 row)
--
-- Check we don't misoptimize a NOT IN where the subquery returns no rows.
--

View File

@ -493,6 +493,71 @@ explain (verbose, costs off)
select x, x from
(select (select random() where y=y) as x from (values(1),(2)) v(y)) ss;
--
-- Test rescan of a hashed subplan (the use of random() is to prevent the
-- sub-select from being pulled up, which would result in not hashing)
--
explain (verbose, costs off)
select sum(ss.tst::int) from
onek o cross join lateral (
select i.ten in (select f1 from int4_tbl where f1 <= o.hundred) as tst,
random() as r
from onek i where i.unique1 = o.unique1 ) ss
where o.ten = 0;
select sum(ss.tst::int) from
onek o cross join lateral (
select i.ten in (select f1 from int4_tbl where f1 <= o.hundred) as tst,
random() as r
from onek i where i.unique1 = o.unique1 ) ss
where o.ten = 0;
--
-- Test rescan of a SetOp node
--
explain (costs off)
select count(*) from
onek o cross join lateral (
select * from onek i1 where i1.unique1 = o.unique1
except
select * from onek i2 where i2.unique1 = o.unique2
) ss
where o.ten = 1;
select count(*) from
onek o cross join lateral (
select * from onek i1 where i1.unique1 = o.unique1
except
select * from onek i2 where i2.unique1 = o.unique2
) ss
where o.ten = 1;
--
-- Test rescan of a RecursiveUnion node
--
explain (costs off)
select sum(o.four), sum(ss.a) from
onek o cross join lateral (
with recursive x(a) as
(select o.four as a
union
select a + 1 from x
where a < 10)
select * from x
) ss
where o.ten = 1;
select sum(o.four), sum(ss.a) from
onek o cross join lateral (
with recursive x(a) as
(select o.four as a
union
select a + 1 from x
where a < 10)
select * from x
) ss
where o.ten = 1;
--
-- Check we don't misoptimize a NOT IN where the subquery returns no rows.
--