There is no advantages of indexes for your data structure/query. Just try to imagine how indexes could be used here. I have no luck.
My suggestion is to convert domains/suffixes to arrays like
alter table "companyDomain" add column adomain text[];
update "companyDomain" set adomain = string_to_array(domain, '.');
create index idx_adom on "companyDomain" using gin (adomain array_ops);
alter table "publicSuffix" add column asuffix text[];
update "publicSuffix" set asuffix = string_to_array(ltrim(suffix, '.'), '.');
create index idx_asuffix on "publicSuffix" using gin (asuffix array_ops);
Lets compare those queries:
ostgres=# explain (analyze, verbose, buffers)
SELECT DISTINCT ON ("companyDomain".id)
"companyDomain".domain,
"publicSuffix".suffix
FROM
"companyDomain"
INNER JOIN "publicSuffix" ON REVERSE("companyDomain".domain) LIKE REVERSE("publicSuffix".suffix) || '%'
ORDER BY "companyDomain".id, LENGTH("publicSuffix".suffix) DESC;
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ QUERY PLAN │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Unique (cost=185738.35..185940.72 rows=908 width=31) (actual time=2364.720..2364.890 rows=908 loops=1) │
│ Output: "companyDomain".domain, "publicSuffix".suffix, "companyDomain".id, (length(("publicSuffix".suffix)::text)) │
│ Buffers: shared hit=306 │
│ -> Sort (cost=185738.35..185839.53 rows=40474 width=31) (actual time=2364.719..2364.764 rows=1006 loops=1) │
│ Output: "companyDomain".domain, "publicSuffix".suffix, "companyDomain".id, (length(("publicSuffix".suffix)::text)) │
│ Sort Key: "companyDomain".id, (length(("publicSuffix".suffix)::text)) DESC │
│ Sort Method: quicksort Memory: 103kB │
│ Buffers: shared hit=306 │
│ -> Nested Loop (cost=0.00..182641.13 rows=40474 width=31) (actual time=22.735..2364.484 rows=1006 loops=1) │
│ Output: "companyDomain".domain, "publicSuffix".suffix, "companyDomain".id, length(("publicSuffix".suffix)::text) │
│ Join Filter: (reverse(("companyDomain".domain)::text) ~~ (reverse(("publicSuffix".suffix)::text) || '%'::text)) │
│ Rows Removed by Join Filter: 8093814 │
│ Buffers: shared hit=306 │
│ -> Seq Scan on public."publicSuffix" (cost=0.00..377.15 rows=8915 width=12) (actual time=0.081..0.794 rows=8915 loops=1) │
│ Output: "publicSuffix".id, "publicSuffix".suffix, "publicSuffix".created_at, "publicSuffix".asuffix │
│ Buffers: shared hit=288 │
│ -> Materialize (cost=0.00..31.62 rows=908 width=15) (actual time=0.001..0.036 rows=908 loops=8915) │
│ Output: "companyDomain".domain, "companyDomain".id │
│ Buffers: shared hit=18 │
│ -> Seq Scan on public."companyDomain" (cost=0.00..27.08 rows=908 width=15) (actual time=11.576..11.799 rows=908 loops=1) │
│ Output: "companyDomain".domain, "companyDomain".id │
│ Buffers: shared hit=18 │
│ Planning Time: 0.167 ms │
│ JIT: │
│ Functions: 9 │
│ Options: Inlining false, Optimization false, Expressions true, Deforming true │
│ Timing: Generation 1.956 ms, Inlining 0.000 ms, Optimization 0.507 ms, Emission 10.878 ms, Total 13.341 ms │
│ Execution Time: 2366.971 ms │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
The bottleneck here, as I understand, Rows Removed by Join Filter: 8093814
It seems that PostgreSQL builds the cartesian join of tables and then filters it using ON condition:
select count(*) from "companyDomain", "publicSuffix";
---
8094820
To workaround try to use array operator:
postgres=# explain (analyze, verbose, buffers)
SELECT DISTINCT ON ("companyDomain".id)
"companyDomain".domain,
"publicSuffix".suffix
FROM
"companyDomain"
INNER JOIN "publicSuffix" ON adomain @> asuffix
ORDER BY "companyDomain".id, LENGTH("publicSuffix".suffix) DESC;
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ QUERY PLAN │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Unique (cost=8310.60..8512.97 rows=908 width=31) (actual time=180.149..180.335 rows=908 loops=1) │
│ Output: "companyDomain".domain, "publicSuffix".suffix, "companyDomain".id, (length(("publicSuffix".suffix)::text)) │
│ Buffers: shared hit=48986 │
│ -> Sort (cost=8310.60..8411.78 rows=40474 width=31) (actual time=180.148..180.200 rows=1239 loops=1) │
│ Output: "companyDomain".domain, "publicSuffix".suffix, "companyDomain".id, (length(("publicSuffix".suffix)::text)) │
│ Sort Key: "companyDomain".id, (length(("publicSuffix".suffix)::text)) DESC │
│ Sort Method: quicksort Memory: 145kB │
│ Buffers: shared hit=48986 │
│ -> Nested Loop (cost=0.59..5213.39 rows=40474 width=31) (actual time=0.190..179.693 rows=1239 loops=1) │
│ Output: "companyDomain".domain, "publicSuffix".suffix, "companyDomain".id, length(("publicSuffix".suffix)::text) │
│ Buffers: shared hit=48986 │
│ -> Seq Scan on public."companyDomain" (cost=0.00..27.08 rows=908 width=57) (actual time=0.015..0.098 rows=908 loops=1) │
│ Output: "companyDomain".id, "companyDomain".domain, "companyDomain".created_at, "companyDomain".adomain │
│ Buffers: shared hit=18 │
│ -> Bitmap Heap Scan on public."publicSuffix" (cost=0.59..5.15 rows=45 width=54) (actual time=0.052..0.197 rows=1 loops=908) │
│ Output: "publicSuffix".id, "publicSuffix".suffix, "publicSuffix".created_at, "publicSuffix".asuffix │
│ Recheck Cond: ("companyDomain".adomain @> "publicSuffix".asuffix) │
│ Rows Removed by Index Recheck: 572 │
│ Heap Blocks: exact=41510 │
│ Buffers: shared hit=48968 │
│ -> Bitmap Index Scan on idx_asuffix (cost=0.00..0.58 rows=45 width=0) (actual time=0.039..0.039 rows=573 loops=908) │
│ Index Cond: ("publicSuffix".asuffix <@ "companyDomain".adomain) │
│ Buffers: shared hit=7458 │
│ Planning Time: 0.189 ms │
│ Execution Time: 180.434 ms │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
It is probably not too accurate (for example aaa.bbb is equal to bbb.aaa here) but you can fix it in WHERE clause. In any case it will be faster.
And for now the old domain and suffix columns are redundant because you can to restore them from adomain/asuffix using array_to_string(anyarray, text [, text]) function.
As an alternative, to avoid changes in tables structure, you can to create functional indexes on string_to_array() and then using it in the filters/joins.