Slow query / Indexes creation (PostgreSQL 9.2)

Posted on

Question :

I have the following Query:

explain analyze
SELECT split_part(full_path, '/', 4)::INT AS account_id,
       split_part(full_path, '/', 6)::INT AS note_id,
       split_part(full_path, '/', 9)::TEXT AS variation,
       st_size,
       segment_index,
       reverse(split_part(reverse(full_path), '/', 1)) as file_name,
       i.st_ino,
       full_path,
       (i.st_size / 1000000::FLOAT)::NUMERIC(5,2) || 'MB' AS size_mb
FROM gorfs.inodes i
JOIN gorfs.inode_segments s
  ON i.st_ino = s.st_ino_target
WHERE
      i.checksum_md5 IS NOT NULL
  AND s.full_path ~ '^/userfiles/account/[0-9]+/[a-z]+/[0-9]+'
  AND i.st_size > 0;
  split_part(s.full_path, '/', 4)::INT IN (

SELECT account.id
        FROM public.ja_clients AS account
        WHERE
        NOT (
                ((account.last_sub_pay > EXTRACT('epoch' FROM (transaction_timestamp() - CAST('4 Months' AS INTERVAL)))) AND (account.price_model > 0)) OR
                (account.regdate > EXTRACT('epoch' FROM (transaction_timestamp() - CAST('3 Month' AS INTERVAL)))) OR
                (((account.price_model = 0) AND (account.jobcredits > 0)) AND (account.last_login > EXTRACT('epoch' FROM (transaction_timestamp() - CAST('4 Month' AS INTERVAL)))))
        ) LIMIT 100
);

The query is taking ages, and I can’t get the problem solved.

These are the index I’ve already created on the inode_segments table:

Indexes:
    "ix_account_id_from_full_path" "btree" (("split_part"("full_path"::"text", '/'::"text", 4)::integer)) WHERE "full_path"::"text" ~ '^/userfiles/account/[0-9]+/[a-z]+/[0-9]+'::"text"
    "ix_inode_segments_ja_files_lookup" "btree" ((
CASE
    WHEN "full_path"::"text" ~ '/[^/]*.[^/]*$'::"text" THEN "upper"("regexp_replace"("full_path"::"text", '.*.'::"text", ''::"text", 'g'::"text"))
    ELSE NULL::"text"
END)) WHERE "gorfs"."is_kaminski_note_path"("full_path"::"text")
    "ix_inode_segments_notes_clientids" "btree" (("split_part"("full_path"::"text", '/'::"text", 4)::integer)) WHERE "gorfs"."is_kaminski_note_path"("full_path"::"text")
    "ix_inode_segments_notes_clientids2" "btree" ("full_path")
    "ix_inode_segments_notes_fileids" "btree" (("split_part"("full_path"::"text", '/'::"text", 8)::integer)) WHERE "gorfs"."is_kaminski_note_path"("full_path"::"text")
    "ix_inode_segments_notes_noteids" "btree" ((NULLIF("split_part"("full_path"::"text", '/'::"text", 6), 'unassigned'::"text")::integer)) WHERE "gorfs"."is_kaminski_note_path"("full_path"::"text")

These are the index I’ve already created on the inodes table:

 Indexes:
    "ix_inodes_checksum_st_size" "btree" ("checksum_md5", "st_size") WHERE "checksum_md5" IS NOT NULL

Question:

What else can I do to improve the Performance of the Query?

UPDATE 1:

Explain analyze: http://explain.depesz.com/s/UBr

The index and function have been created as mentioned on the answer below.

UPDATE 2:

Explain analyze: http://explain.depesz.com/s/LHS

Using the Query provided on the answer below

Answer :

Perhaps this will help.

If you’ll rely on the account_id from full_path often, then you’ll benefit from a function and a functional index for it:

CREATE OR REPLACE FUNCTION gorfs.f_get_account_from_full_path(p_full_path text) RETURNS int AS $body$
SELECT (regexp_matches($1, '^/userfiles/account/([0-9]+)/[a-z]+/[0-9]+'))[1]::int
$body$ LANGUAGE SQL IMMUTABLE SECURITY DEFINER RETURNS NULL ON NULL INPUT;

CREATE INDEX ON gorfs.inode_segments (gorfs.f_get_account_from_full_path(full_path));

Ensure gorfs.inodes has an index (or key much better if applicable) on st_ino!

You run the function split_part several times for each row, this is likely taking a significant toll. I’ve replaced it with string_to_array, and then fetch the individual pieces as needed. I also didn’t understand what you intended to obtain for the field field_name using reverse? The query below returns the last element for it.

Your query returns many million rows. Even if PostgreSQL processes the query reasonably quickly, your client application (especially if you use PgAdminIII) will struggle allocating enough memory and receive and format the results, and probably be what takes the most time. So you may want to create a temporary table with the results, and then query against the temporary table:

CREATE TEMP TABLE myresults AS
WITH
  accounts AS (
    SELECT id
    FROM public.ja_clients
    WHERE NOT (
               (last_sub_pay > EXTRACT('epoch' FROM now() - '4 Months'::INTERVAL) AND price_model > 0) OR
               regdate > EXTRACT('epoch' FROM now() - '3 Month'::INTERVAL) OR
               (price_model = 0 AND jobcredits > 0 AND last_login > EXTRACT('epoch' FROM now() - '4 Month'::INTERVAL))
              )
    ORDER BY 1 LIMIT 100 -- first 100 accounts for testing purposes; comment out this line once the query is proven performant enough
    ) 
SELECT r.parts[4]::INT AS account_id, r.parts[6]::INT AS note_id, r.parts[9] AS variation,
       st_size, segment_index, r.parts[array_upper(r.parts, 1)] AS file_name, st_ino, full_path, size_mb
FROM (
  SELECT string_to_array(full_path, '/') AS parts, st_size, segment_index, i.st_ino, full_path,
         (i.st_size / 1000000::FLOAT)::NUMERIC(5,2) || 'MB' AS size_mb
  FROM gorfs.inode_segments s
  JOIN gorfs.inodes i ON (i.st_ino = s.st_ino_target)
  WHERE gorfs.f_get_account_from_full_path(s.full_path) IN (SELECT * FROM accounts)
    AND i.checksum_md5 IS NOT NULL
    AND i.st_size > 0
  ) r;

SELECT *
FROM myresults
LIMIT 100;

Your strategy for getting information from full_path can be useful for a one-off, but for ongoing queries to it, especially over millions of records and expecting quick results, it is far from optimal.

Considering the sheer size of your tables, you’ll probably benefit from datawarehousing. If your tables are constantly updated, you’ll need a trigger to keep your datawarehouse updated.

The following is not exactly datawarehousing, but it follows the principle of creating the most lightweight extract of the data you’ll need for future queries:

CREATE TABLE gorfs.inodes_segments_dw(
  st_ino_target "gorfs"."ino_t" NOT NULL,
  segment_index "gorfs"."pathname_component" NOT NULL,
  account_id int NULL,
  note_id int NULL,
  variation text NULL,
  file_name text NULL
);

INSERT INTO gorefs.inodes_segments_dw
SELECT st_ino_target, segment_index, parts[4]::INT AS account_id, parts[6]::INT AS note_id, parts[9] AS variation,
       parts[array_upper(parts, 1)] AS file_name
FROM (
  SELECT *, string_to_array(full_path, '/') AS parts
  FROM gorfs.inode_segments
  ) s;

CREATE INDEX ON gorfs.inodes_segments_dw (account_id);

Then you can run the full query again against it rather than the full table with calculated fields:

CREATE TEMP TABLE myresults AS
WITH
  accounts AS (
    SELECT id
    FROM public.ja_clients
    WHERE NOT (
               (last_sub_pay > EXTRACT('epoch' FROM now() - '4 Months'::INTERVAL) AND price_model > 0) OR
               regdate > EXTRACT('epoch' FROM now() - '3 Month'::INTERVAL) OR
               (price_model = 0 AND jobcredits > 0 AND last_login > EXTRACT('epoch' FROM now() - '4 Month'::INTERVAL))
              )
    ORDER BY 1 LIMIT 100 -- first 100 accounts for testing purposes; comment out this line once the query is proven performant enough
    ) 
SELECT s.account_id, s.note_id, s.variation, s.st_size, s.segment_index, s.file_name, s.st_ino, is.full_path, s.size_mb
FROM (
  SELECT s.account_id, s.note_id, s.variation, i.st_size, s.segment_index, s.file_name, i.st_ino,
         (i.st_size / 1000000::FLOAT)::NUMERIC(5,2) || 'MB' AS size_mb
  FROM accounts a
  JOIN gorfs.inodes_segments_dw s ON (s.account_id = a.account_id)
  JOIN gorfs.inodes i ON (i.st_ino = s.st_ino_target AND i.checksum_md5 IS NOT NULL AND i.st_size > 0)
  ) s
JOIN gorefs.inodes_segments is ON ((is.st_ino, is.segment_index) = (s.st_ino, s.segment_index));

Another advisable strategy would be separating this query in parts, using temporary tables, step by step. This is not necessarily faster, but convenient for finding problems with the selection logic, or using a strategy that best suits our own knowledge of the data:

CREATE TEMP TABLE accounts (account_id int NOT NULL PRIMARY KEY);
INSERT INTO accounts
SELECT id
FROM public.ja_clients
WHERE NOT (
           (last_sub_pay > EXTRACT('epoch' FROM now() - '4 Months'::INTERVAL) AND price_model > 0) OR
           regdate > EXTRACT('epoch' FROM now() - '3 Month'::INTERVAL) OR
           (price_model = 0 AND jobcredits > 0 AND last_login > EXTRACT('epoch' FROM now() - '4 Month'::INTERVAL))
          );

CREATE TEMP TABLE inodes (st_ino "gorfs"."ino_t" NOT NULL PRIMARY KEY, st_size "gorfs"."off_t", size_mb text);
INSERT INTO inodes
SELECT st_ino, st_size, (st_size / 1000000::FLOAT)::NUMERIC(5,2) || 'MB' AS size_mb
FROM gorfs.inodes
WHERE checksum_md5 IS NOT NULL AND st_size > 0;

CREATE TEMP TABLE segments (
  st_ino_target "gorfs"."ino_t" NOT NULL,
  segment_index "gorfs"."pathname_component" NOT NULL,
  account_id int NOT NULL
);

INSERT INTO segments
SELECT *
FROM (
  SELECT st_ino_target, segment_index, (regexp_matches($1, '^/userfiles/account/([0-9]+)/[a-z]+/[0-9]+'))[1]::int AS account_id
  FROM gorfs.inode_segments
  )
WHERE account_id IS NOT NULL

CREATE INDEX ON segments (account_id);

CREATE TEMP TABLE results AS
SELECT account_id, parts[6]::INT AS note_id, parts[9] AS variation,
       st_size, segment_index, parts[array_upper(parts, 1)] AS file_name, st_ino, full_path, size_mb
FROM (
  SELECT sall.*, string_to_array(full_path, '/') AS parts, full_path
  FROM (
    SELECT i.*, s.segment_index
    FROM accounts a
    JOIN segments s ON (s.account_id = a.account_id)
    JOIN inodes i ON (i.st_ino = s.st_ino_target)
    JOIN (s.st_ino_target = i.st_ino)
    ) sall    
  JOIN gorfs.inode_segments s ON ((s.st_ino_target, s.segment_index) = (sall.st_ino_target, sall.segment_index))
  ) s;

SELECT *
FROM results
LIMIT 100;

Leave a Reply

Your email address will not be published. Required fields are marked *