Hi hackers,

Was tinkering with semijoins in the planner and was surprised
by some cost estimates produced. Richard's work here
(db2d99323f1) fixed much but not all of what I was seeing.

Right now when the planner decides to unique-ify the RHS,
it reads every righthand row, including rows whose join key
is NULL. Those rows cannot contribute to the result. They get
sorted or hashed, deduplicated, and then discarded at the join.

The attached patch adds a pass, add_semijoin_not_null_quals(),
which runs before set_base_rel_sizes(), that walks
root->join_info_list, and, for each semijoin whose
righthand keys can be unique-ified, pushes an IS NOT NULL
restriction down onto the key.

Consider:

CREATE TABLE rhs_250000_90 AS
SELECT CASE
  WHEN i % 100 < 90
  THEN NULL ELSE i % 20000
  END AS k,
  (i % 500) AS k2
FROM generate_series(1, 250000) i;

CREATE TABLE probe AS
  SELECT i AS id, (i % 500) AS id2
  FROM generate_series(1, 2000) i;
ANALYZE rhs_250000_90, probe;

SELECT count(*)
FROM probe p
WHERE p.id IN (SELECT k FROM rhs_250000_90);

Master:

Aggregate
  ->  Hash Join
        Hash Cond: (rhs_250000_90.k = p.id)
        ->  HashAggregate
              Group Key: rhs_250000_90.k
              ->  Seq Scan on rhs_250000_90
        ->  Hash
              ->  Seq Scan on probe p

Patched:

Aggregate
  ->  Hash Right Semi Join
        Hash Cond: (rhs_250000_90.k = p.id)
        ->  Seq Scan on rhs_250000_90
              Filter: (k IS NOT NULL)
        ->  Hash
              ->  Seq Scan on probe p

Serial plans, work_mem = 64MB, best of 15/7/5 runs at 250k/1M/4M,
probe side fixed at 2000 rows. Times in ms:

shape       rhs rows   NULLs   master   patched   speedup
---------------------------------------------------------
IN            250,000     0%     30.7      29.3     1.05x
IN            250,000    50%     54.8      27.2     2.01x
IN            250,000    90%     48.9      20.7     2.36x
IN          1,000,000     0%    232.8     230.6     1.01x
IN          1,000,000    50%    218.1     168.6     1.29x
IN          1,000,000    90%    203.3      94.7     2.15x
IN          4,000,000     0%    951.7     944.9     1.01x
IN          4,000,000    50%    937.5     676.4     1.39x
IN          4,000,000    90%    860.6     397.8     2.16x

IN 2-col      250,000     0%     71.6      72.4     0.99x
IN 2-col      250,000    50%     66.9      31.8     2.10x
IN 2-col      250,000    90%     63.9      22.2     2.88x
IN 2-col    1,000,000     0%    262.8     263.3     1.00x
IN 2-col    1,000,000    50%    262.2     138.4     1.89x
IN 2-col    1,000,000    90%    261.0      90.4     2.89x
IN 2-col    4,000,000     0%   1230.3    1200.9     1.02x
IN 2-col    4,000,000    50%   1168.2     548.4     2.13x
IN 2-col    4,000,000    90%   1129.1     388.3     2.91x

The cells that gain ~2x are the cells where the plan flips to a plain semi join.

The pass skips a handful of cases: NOT NULL columns known to the catalog,
entries with unknown nullability, and columns whose statistics indicate we
would gain no benefit (selectivity >= 1.0 -> no NULLs). Additionally, it 
prevents
double-counting selectivity when an existing strict qual already rejects.
The key also has to resolve to a single base relation, which handles outer 
joins for
free: pull_varnos() folds in outer-join relids, so an outer-join-nullable key
comes back with more than one id and is skipped.

A couple things worth looking at:
One of the self-join-elimination tests reorders a join. And in the lateral test,
the filter that lands on t3 is ((t1.a + a) IS NOT NULL). t1.a is an
outer reference here. Is this acceptable?  pull_varnos() reports t3 alone.

Thoughts?

-Will

Attachment: v1-0001-Skipping-NULL-keys-when-uniqueifying-a-semijoin-s.patch
Description: v1-0001-Skipping-NULL-keys-when-uniqueifying-a-semijoin-s.patch

Reply via email to