PostgreSQL Antipatterns: harmful JOINs and OR

Beware of operations, buffers that may cause issues...
Let's consider some universal approaches to optimizing queries in PostgreSQL using a small example query. Whether to use them or not is your choice, but it's worth knowing about them.

In some future versions of PG, the situation may change with the 'smarter' query planner, but for 9.4/9.6 it looks roughly the same as the examples here.

I'll take a quite real query:

SELECT
  TRUE
FROM
  "Document" d
INNER JOIN
  "DocumentExtension" doc_ex
    USING("@Document")
INNER JOIN
  "DocumentType" t_doc ON
    t_doc."@DocumentType" = d."DocumentType"
WHERE
  (d."Face3" = 19091 or d."Employee" = 19091) AND
  d."$Draft" IS NULL AND
  d."Deleted" IS NOT TRUE AND
  doc_ex."State"[1] IS TRUE AND
  t_doc."DocumentType" = 'WorkPlan'
LIMIT 1;

on table and field namesYou can have different opinions about 'Russian' names of fields and tables, but it's a matter of taste. Since we don't have foreign developers in 'Tensor', and PostgreSQL allows us to use names even in hieroglyphs, as long as they are enclosed in quotes , we prefer naming objects clearly and understandably to avoid misunderstandings.Let's look at the resulting plan:
144ms and almost 53K buffers
PostgreSQL Antipatterns: harmful JOINs and OR
[view on explain.tensor.ru]

— meaning more than 400MB of data! And we will be lucky if all of them are in the cache by the time of our query; otherwise, it will take significantly longer to read from the disk. The algorithm is the most important!

To optimize any query, you first need to understand what it is supposed to do.

We'll leave the development of the database structure out of this article for now, and agree that we can relatively 'cheaply'
rewrite the query and/or apply some necessary indexes So, the query:.

— checks for the existence of at least one document
— in the state we need and of a specific type
— where the author or executor is the employee we need
JOIN + LIMIT 1

Developers often find it easier to write a query that first joins a large number of tables and then retrieves a single record from that multitude. But easier for the developer does not mean more efficient for the database.

In our case, there were only 3 tables — and what an effect...
Let's start by eliminating the join with the 'DocumentType' table, and at the same time let the database know that

the record type is unique (we know this, but the planner is not aware yet): (we know this, but the planner is still unaware):

WITH T AS (
  SELECT
    "@DocumentType"
  FROM
    "DocumentType"
  WHERE
    "DocumentType" = 'WorkPlan'
  LIMIT 1
)
...
WHERE
  d."DocumentType" = (TABLE T)
...

Yes, if the table/CTE consists of a single field with a single record, it can be written in PG like this instead of

d."DocumentType" = (SELECT "@DocumentType" FROM T LIMIT 1)

Lazy calculations in PostgreSQL queries

BitmapOr vs UNION

In some cases, Bitmap Heap Scan can be very costly for us — for instance, in our situation, where a significant number of records match the required condition. We obtained this due to the OR condition transformed into BitmapOr-operation in the plan.
Let's return to the original task — we need to find a record that corresponds to any of the conditions — that is, there's no need to search through all 59K records for both conditions. There's a way to process one condition, and only move to the second when nothing was found for the first. We will use such a construct:

(
  SELECT
    ...
  LIMIT 1
)
UNION ALL
(
  SELECT
    ...
  LIMIT 1
)
LIMIT 1

"Outer" LIMIT 1 guarantees that the search will stop upon finding the first record. If it is found in the first block, the execution of the second will not occur (never executed in the plan).

Hiding complex conditions under CASE

There is a very inconvenient point in the original query — checking the status in the related table "DocumentExtension". Regardless of the truth of the other conditions in the expression (for instance, d."Deleted" IS NOT TRUE), this join is always executed and "costs resources". Whether more or less will be spent depends on the size of this table.
However, we can modify the query so that the search for the related record occurs only when it is genuinely necessary:

SELECT
  ...
FROM
  "Document" d
WHERE
  ... /*index cond*/ AND
  CASE
    WHEN "$Draft" IS NULL AND "Deleted" IS NOT TRUE THEN (
      SELECT
        "Status"[1] IS TRUE
      FROM
        "DocumentExtension"
      WHERE
        "@Document" = d."@Document"
    )
  END

Since we don’t need any of the fields from the related table for the result , we have the opportunity to turn the JOIN into a subquery condition.We will leave the indexed fields outside the CASE, moving the simple conditions into the WHEN block — and now the "heavy" query only executes when moving into THEN.
My surname is "Total"

We collect the resulting query with all the mechanics described above:

We are assembling the resulting query with all the mechanics described above:

WITH T AS (
  SELECT
    "@DocumentType"
  FROM
    "DocumentType"
  WHERE
    "DocumentType" = 'WorkPlan'
)
  (
    SELECT
      TRUE
    FROM
      "Document" d
    WHERE
      ("Person3", "DocumentType") = (19091, (TABLE T)) AND
      CASE
        WHEN "$Draft" IS NULL AND "Deleted" IS NOT TRUE THEN (
          SELECT
            "Status"[1] IS TRUE
          FROM
            "DocumentExtension"
          WHERE
            "@Document" = d."@Document"
        )
      END
    LIMIT 1
  )
UNION ALL
  (
    SELECT
      TRUE
    FROM
      "Document" d
    WHERE
      ("DocumentType", "Employee") = ((TABLE T), 19091) AND
      CASE
        WHEN "$Draft" IS NULL AND "Deleted" IS NOT TRUE THEN (
          SELECT
            "Status"[1] IS TRUE
          FROM
            "DocumentExtension"
          WHERE
            "@Document" = d."@Document"
        )
      END
    LIMIT 1
  )
LIMIT 1;

Fitting [to] indexes

A keen eye noticed that the indexed conditions in the UNION sub-blocks vary slightly — this is because we already have suitable indexes on the table. If they did not exist, it would be worth creating them: Document(Person3, DocumentType) and Document(DocumentType, Employee).
on the order of fields in ROW conditionsFrom the planner's perspective, of course, one could also write (A, B) = (constA, constB), and (B, A) = (constB, constA). But when written in the order of fields in the index, such a query is just easier to debug later.
What's in the plan?
PostgreSQL Antipatterns: harmful JOINs and OR
[view on explain.tensor.ru]

Unfortunately, we were unlucky, and nothing was found in the first UNION block, so the second one went for execution nonetheless. But even then — just 0.037ms and 11 buffers!
We accelerated the query and reduced the "data pumping" in memory by several thousand times, using quite simple techniques — a decent result with a little copying/pasting. 🙂

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster