Оптимизация запроса, который сравнивает таблицу с миллионами строк - PullRequest
0 голосов
/ 07 октября 2019

Я мог бы использовать некоторую помощь в оптимизации запроса, который сравнивает строки в одной таблице с миллионами записей. Вот определение таблицы:

CREATE TABLE IF NOT EXISTS data.row_check (
    id         uuid NOT NULL DEFAULT NULL,
    version    int8 NOT NULL DEFAULT NULL,
    row_hash   int8 NOT NULL DEFAULT NULL,
    table_name text NOT NULL DEFAULT NULL,

CONSTRAINT row_check_pkey
    PRIMARY KEY (id, version)
);

Я переделываю наш push-код, и у меня есть испытательный стенд с миллионами записей в 20 таблицах. Я запускаю свои тесты, получаю количество строк и могу определить, когда некоторые из моих вставок изменились. Следующим шагом является проверка каждой строки, а затем сравнение строк на предмет различий между версиями моего кода. Примерно так:

-- Run my test of "version 0" of the push code, the base code I'm refactoring.  
-- Insert the ID and checksum for each pushed row.
INSERT INTO row_check (id,version,row_hash,table_name)
            SELECT id, 0, hashtext(record_changes_log::text),'record_changes_log' 
            FROM record_changes_log

            ON CONFLICT ON CONSTRAINT row_check_pkey DO UPDATE SET
                row_hash   = EXCLUDED.row_hash,
                table_name = EXCLUDED.table_name;

truncate table record_changes_log;

-- Run my test of "version 1" of the push code, the new code I'm validating.
-- Insert the ID and checksum for each pushed row.

INSERT INTO row_check (id,version,row_hash,table_name)
            SELECT id, 1, hashtext(record_changes_log::text),'record_changes_log' 
            FROM record_changes_log

            ON CONFLICT ON CONSTRAINT row_check_pkey DO UPDATE SET
                row_hash   = EXCLUDED.row_hash,
                table_name = EXCLUDED.table_name;

Это получает две строки в row_check для каждой строки в record_changes_log или любой другой таблицы, которую я проверяю. Для двух запусков record_changes_log я получаю более 8.6M строк в row_check. Они выглядят так:

id                                      version row_hash    table_name
e6218751-ab78-4942-9734-f017839703f6    0   -142492569  record_changes_log
6c0a4111-2f52-4b8b-bfb6-e608087ea9c1    0   -1917959999 record_changes_log
7fac6424-9469-4d98-b887-cd04fee5377d    0   -323725113  record_changes_log
1935590c-8d22-4baf-85ba-00b563022983    0   -1428730186 record_changes_log
2e5488b6-5b97-4755-8a46-6a46317c1ae2    0   -1631086027 record_changes_log
7a645ffd-31c5-4000-ab66-a565e6dad7e0    0   1857654119  record_changes_log

Я попросил вчера помочь с запросом сравнения, и это привело к следующему:

 select v0.table_name,
        v0.id,
        v0.row_hash as v0,
        v1.row_hash as v1   

   from row_check v0 
   join row_check v1 on v0.id = v1.id  and
        v0.version = 0 and
        v1.version  = 1 and
        v0.row_hash <> v1.row_hash

Это работает, нотеперь я надеюсь немного его оптимизировать. В качестве эксперимента я сгруппировал данные по версии и затем построил индекс BRIN, например:

drop index if exists row_check_version_btree;
create index row_check_version_btree
          on row_check
        using btree(version);

cluster row_check using row_check_version_btree;    
drop index row_check_version_btree; -- Eh? I want to see how the BRIN performs.

drop index if exists row_check_version_brin;
create index row_check_version_brin
          on row_check
        using brin(row_hash);

vacuum analyze row_check;       

Я выполнил запрос через объяснение и получил:

Merge Join  (cost=1.12..559750.04 rows=4437567 width=51) (actual time=1511.987..14884.045 rows=10 loops=1)
  Output: v0.table_name, v0.id, v0.row_hash, v1.row_hash
  Inner Unique: true
  Merge Cond: (v0.id = v1.id)
  Join Filter: (v0.row_hash <> v1.row_hash)
  Rows Removed by Join Filter: 4318290
  Buffers: shared hit=8679005 read=42511
  ->  Index Scan using row_check_pkey on ascendco.row_check v0  (cost=0.56..239156.79 rows=4252416 width=43) (actual time=0.032..5548.180 rows=4318300 loops=1)
        Output: v0.id, v0.version, v0.row_hash, v0.table_name
        Index Cond: (v0.version = 0)
        Buffers: shared hit=4360752
  ->  Index Scan using row_check_pkey on ascendco.row_check v1  (cost=0.56..240475.33 rows=4384270 width=24) (actual time=0.031..6070.790 rows=4318300 loops=1)
        Output: v1.id, v1.version, v1.row_hash, v1.table_name
        Index Cond: (v1.version = 1)
        Buffers: shared hit=4318253 read=42511
Planning Time: 1.073 ms
Execution Time: 14884.121 ms

. ..что я действительно не понял правильную идею ... поэтому я снова запустил ее в JSON и передал результаты в этот замечательный визуализатор плана:

http://tatiyants.com/pev/#/plans

query plan node map

Советы верны, оценка верхнего узла неверна. Результат - 10 строк, оценка - около 443 757 строк.

Я надеюсь узнать больше об оптимизации такого рода вещей, и этот запрос кажется хорошей возможностью. У меня много идей о том, что может помочь:

- CREATE STATISTICS?
- Переработать запрос, чтобы переместить сравнение куда?
- Использовать лучший индекс? Я попробовал индекс GIN и прямое B-дерево в версии, но ни один из них не был лучше.
- Переработать формат row_check, чтобы переместить два хэша в одну строку вместо разделения их на две строки, сравнить на insert /обновить, пометить несоответствия и добавить частичный индекс для несоответствующих значений.

Конечно, забавно даже пытаться индексировать что-то, где есть только два значения (0 и 1 в случае выше)вот и все. На самом деле, есть ли какая-нибудь хитрая уловка для логических выражений? Я всегда буду сравнивать две версии, поэтому «старая» и «новая», которые я могу выразить, однако, делает жизнь лучше. Я понимаю, что Postgres имеет внутренние индексы только во время поиска / слияния (?) И что у него нет индекса типа растрового изображения. Будет ли какой-то INTERSECT, который может помочь? Я не знаю, как в Postgres реализованы математические операторы набора для внутренних операций.

Спасибо за любые предложения о том, как переосмыслить эти данные или запрос, чтобы сделать их более быстрыми для сравнений, включающих миллионы или десятки миллионов строк.

1 Ответ

1 голос
/ 07 октября 2019

Я собираюсь добавить ответ на свой вопрос, но все еще интересуюсь тем, что кто-то еще может сказать. В процессе написания моего первоначального вопроса я понял, что, возможно, потребуется редизайн. Это зависит от моего плана сравнивать только две версии одновременно. Это хорошее решение, но есть и другие случаи, когда оно не работает. В любом случае, вот немного другой дизайн таблицы, который складывает два результата в одну строку:

DROP TABLE IF EXISTS data.row_compare;
CREATE TABLE IF NOT EXISTS data.row_compare (
    id           uuid NOT NULL DEFAULT NULL,
    hash_1       int8,    -- Want NULL to defer calculating hash comparison until after both hashes are entered.
    hash_2       int8,    -- Ditto
    hashes_match boolean, -- Likewise 
    table_name   text NOT NULL DEFAULT NULL,

CONSTRAINT row_compare_pkey
    PRIMARY KEY (id)
);

Следующий индекс выражений должен быть очень маленьким, поскольку у меня не должно быть несовпадающих записей:

CREATE INDEX row_compare_fail ON row_compare (hashes_match)
    WHERE hashes_match = false;

Триггер, приведенный ниже, выполняет вычисление столбца, если оба значения: hash_1 и hash_2:

-- Run this as a BEFORE INSERT or UPDATE ROW trigger.
CREATE OR REPLACE FUNCTION data.on_upsert_row_compare()
  RETURNS trigger AS 

$BODY$
BEGIN

    IF  NEW.hash_1 = NULL OR 
        NEW.hash_2 = NULL THEN
        RETURN NEW; -- Don't do the comparison, hash_1 hasn't been populated yet.

    ELSE-- Do the comparison. The point of this is to avoid constantly thrashing the expression index.
       NEW.hashes_match := NEW.hash_1 = NEW.hash_2;
      RETURN NEW;     -- important!
   END IF;
END;

$BODY$
LANGUAGE plpgsql;

Теперь добавляются 4,3 млн строк вместо 8,6 млн строк:

-- Add the first set of results and build out the row_compare records.
INSERT INTO row_compare (id,hash_1,table_name)
            SELECT id, hashtext(record_changes_log::text),'record_changes_log'
            FROM record_changes_log

            ON CONFLICT ON CONSTRAINT row_compare_pkey DO UPDATE SET
                hash_1   = EXCLUDED.hash_1,
                table_name = EXCLUDED.table_name;

-- I'll truncate the record_changes_log and push my sample data again here.

-- Add the second set of results and update the row compare records.
-- This time, the hash is going into the hash_2 field for comparison
INSERT INTO row_compare (id,hash_2,table_name)
            SELECT id, hashtext(record_changes_log::text),'record_changes_log'
            FROM record_changes_log

            ON CONFLICT ON CONSTRAINT row_compare_pkey DO UPDATE SET
                hash_2   = EXCLUDED.hash_2,
                table_name = EXCLUDED.table_name;

И теперь результаты легко найти:

select * from row_compare where hashes_match = false;

Это изменяет время запроса с 17 секунд до 24 миллисекунд.

...