MVCC-7. Автоочистка

    Напомню, что мы начали с вопросов, связанных с изоляцией, сделали отступление про организацию данных на низком уровне, подробно поговорили о версиях строк и о том, как из версий получаются снимки данных.

    Затем мы рассмотрели внутристраничную очистку (и HOT-обновления), обычную очистку, ну а сегодня посмотрим на автоматическую очистку.

    Автоочистка (autovacuum)


    Мы уже говорили о том, что обычная очистка в нормальных условиях (когда никто не удерживает надолго горизонт транзакций) должна справляться со своей работой. Вопрос в том, как часто ее вызывать.

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

    Если очищать таблицу слишком часто, то вместо полезной работы сервер будет постоянно заниматься обслуживанием — тоже нехорошо.

    Заметим, что запуск обычной очистки по расписанию никак не решает проблему, потому что нагрузка может изменяться со временем. Если таблица стала обновляться активней, то и очищать ее надо чаще.

    Автоматическая очистка — как раз тот самый механизм, который позволяет запускать очистку в зависимости от активности изменений в таблицах.

    При включенной автоочистке (конфигурационный параметр autovacuum) в системе всегда присутствует процесс autovacuum launcher, который планирует работу, а реальной очисткой занимаются рабочие процессы autovacuum worker, несколько экземпляров которых могут работать параллельно.

    Процесс autovacuum launcher составляет список баз данных, в которых есть какая-либо активность. Активность определяется по статистике, а чтобы она собиралась, должен быть установлен параметр track_counts. Никогда не выключайте autovacuum и track_counts, иначе автоочистка не будет работать.

    Раз в autovacuum_naptime процесс autovacuum launcher запускает (с помощью процесса postmaster) рабочий процесс для каждой БД из списка. Иными словами, если в базе данных есть какая-то активность, то рабочие процессы будет приходить в нее с интервалом autovacuum_naptime. Для этого, если имеется несколько активных БД (N штук), то рабочие процессы запускаются в N раз чаще, чем autovacuum_naptime. Но при этом общее количество одновременно работающих рабочих процессов ограничено параметром autovacuum_max_workers.

    Запустившись, рабочий процесс подключается к указанной ему базе данных и начинает с того, что строит список:

    • всех таблиц, материализованных представлений и toast-таблиц, требующих очистки,
    • всех таблиц и материализованных представлений, требующих анализа (toast-таблицы не анализируются, потому что обращение к ним всегда происходит по индексу).

    Дальше рабочий процесс по очереди очищает и/или анализирует отобранные объекты и по окончании очистки завершается.

    Если процесс не успел выполнить всю намеченную работу за autovacuum_naptime, процесс autovacuum launcher пошлет в ту же базу данных еще один рабочий процесс, и они будут работать вместе. «Вместе» просто означает, что второй процесс построит свой список таблиц и пойдет по нему. Таким образом, параллельно будут обрабатываться разные таблицы, но на уровне одной таблицы параллелизма нет — если один из рабочих процессов уже занимается таблицей, другой пропустит ее и пойдет дальше.

    Обсуждение необходимости параллельной обработки идет уже давно, но патч пока не принят.

    Теперь разберемся детальнее, что значит «требует очистки» и «требует анализа».

    Какие таблицы требуют очистки


    Считается, что очистка необходима, если число «мертвых», то есть неактуальных, версий строк превышает установленное пороговое значение. Число мертвых версий постоянно собирается коллектором статистики и хранится в таблице pg_stat_all_tables. А порог задается двумя параметрами:

    • autovacuum_vacuum_threshold определяет абсолютное значение (в штуках),
    • autovacuum_vacuum_scale_factor определяет долю строк в таблице.

    Итоговая формула такая: очистка требуется, если pg_stat_all_tables.n_dead_tup >= autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * pg_class.reltupes.

    Настройки по умолчанию устанавливают autovacuum_vacuum_threshold = 50 и
    autovacuum_vacuum_scale_factor = 0.2. Главный параметр здесь, конечно, autovacuum_vacuum_scale_factor — именно он важен для больших таблиц (а именно с ними связаны возможные проблемы). Значение 20% представляется сильно завышенным, скорее всего его потребуется существенно уменьшить.

    Оптимальные значения параметров могут отличаться для разных таблиц в зависимости от их размера и характера изменений. Имеет смысл установить в целом адекватные значения, и — при необходимости — настроить специальным образом параметры на уровне некоторых таблиц с помощью параметров хранения:

    • autovacuum_vacuum_threshold и toast.autovacuum_vacuum_threshold,
    • autovacuum_vacuum_scale_factor и toast.autovacuum_vacuum_scale_factor.

    Чтобы не запутаться, это стоит делать только для небольшого числа таблиц, которые выделяются среди прочих объемом или интенсивностью изменений, и только в том случае, когда глобально установленные значения не подходят.

    Кроме того, автоочистку можно отключать на уровне таблиц (хотя сложно придумать причину, по которой это было бы необходимо):

    • autovacuum_enabled и toast.autovacuum_enabled.

    Например, в прошлый раз мы создавали таблицу vac с отключенной автоочисткой, чтобы — для целей демонстрации — управлять очисткой вручную. Параметр хранения можно изменить следующим образом:

    => ALTER TABLE vac SET (autovacuum_enabled = off);
    

    Чтобы формализовать все сказанное выше, создадим представление, показывающее, какие таблицы в данный момент нуждаются в очистке. Оно будет использовать функцию, возвращающую текущее значение параметра с учетом того, что оно может быть переопределено на уровне таблицы:

    => CREATE FUNCTION get_value(param text, reloptions text[], relkind "char")
    RETURNS float
    AS $$
      SELECT coalesce(
        -- если параметр хранения задан, то берем его
        (SELECT option_value
         FROM   pg_options_to_table(reloptions)
         WHERE  option_name = CASE
                  -- для toast-таблиц имя параметра отличается
                  WHEN relkind = 't' THEN 'toast.' ELSE ''
                END || param
        ),
        -- иначе берем значение конфигурационного параметра
        current_setting(param)
      )::float;
    $$ LANGUAGE sql;
    

    А вот и представление:

    => CREATE VIEW need_vacuum AS
      SELECT st.schemaname || '.' || st.relname tablename,
             st.n_dead_tup dead_tup,
             get_value('autovacuum_vacuum_threshold', c.reloptions, c.relkind) +
             get_value('autovacuum_vacuum_scale_factor', c.reloptions, c.relkind) * c.reltuples
             max_dead_tup,
             st.last_autovacuum
      FROM   pg_stat_all_tables st,
             pg_class c
      WHERE  c.oid = st.relid
      AND    c.relkind IN ('r','m','t');
    

    Какие таблицы требуют анализа


    С автоанализом дело обстоит примерно так же. Считается, что анализа требуют ту таблицы, у которых число измененных (с момента прошлого анализа) версий строк превышает пороговое значение, заданное двумя аналогичными параметрами: pg_stat_all_tables.n_mod_since_analyze >= autovacuum_analyze_threshold + autovacuum_analyze_scale_factor * pg_class.reltupes.

    Умолчательные настройки автоанализа немного отличаются: autovacuum_analyze_threshold = 50 и autovacuum_analyze_scale_factor = 0.1. Их также можно определить на уровне параметров хранения отдельных таблиц:

    • autovacuum_analyze_threshold
    • autovacuum_analyze_scale_factor

    Поскольку toast-таблицы не анализируются, соответствующих параметров для них нет.

    Создадим представление и для анализа:

    => CREATE VIEW need_analyze AS
      SELECT st.schemaname || '.' || st.relname tablename,
             st.n_mod_since_analyze mod_tup,
             get_value('autovacuum_analyze_threshold', c.reloptions, c.relkind) +
             get_value('autovacuum_analyze_scale_factor', c.reloptions, c.relkind) * c.reltuples
             max_mod_tup,
             st.last_autoanalyze
      FROM   pg_stat_all_tables st,
             pg_class c
      WHERE  c.oid = st.relid
      AND    c.relkind IN ('r','m');
    

    Пример


    Для экспериментов установим такие значение параметров:

    => ALTER SYSTEM SET autovacuum_naptime = ‘1s’; -- чтобы долго не ждать
    => ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.03;  -- 3%
    => ALTER SYSTEM SET autovacuum_vacuum_threshold = 0;
    => ALTER SYSTEM SET autovacuum_analyze_scale_factor = 0.02; -- 2%
    => ALTER SYSTEM SET autovacuum_analyze_threshold = 0;
    
    => SELECT pg_reload_conf();
    
     pg_reload_conf
    ----------------
     t
    (1 row)
    

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

    => CREATE TABLE autovac(
      id serial,
      s char(100)
    ) WITH (autovacuum_enabled = off);
    => INSERT INTO autovac SELECT g.id,'A' FROM generate_series(1,1000) g(id);
    

    Вот что покажет наше представление для очистки:

    => SELECT * FROM need_vacuum WHERE tablename = 'public.autovac';
    
       tablename    | dead_tup | max_dead_tup | last_autovacuum 
    ----------------+----------+--------------+-----------------
     public.autovac |        0 |            0 | 
    (1 row)
    

    Тут есть два момента, на которые стоит обратить внимание. Во-первых, max_dead_tup = 0, хотя 3% от 1000 строк составляет 30 строк. Дело в том, что у нас еще нет статистики по таблице, поскольку INSERT сам по себе ее не обновляет. Пока наша таблица не будет проанализирована, нули так и останутся, поскольку pg_class.reltuples = 0. Однако заглянем во второе представление для анализа:

    => SELECT * FROM need_analyze WHERE tablename = 'public.autovac';
    
       tablename    | mod_tup | max_mod_tup | last_autoanalyze 
    ----------------+---------+-------------+------------------
     public.autovac |    1000 |           0 | 
    (1 row)
    

    Поскольку в таблице изменилось (добавилось) 1000 строк, и это больше нуля, должен сработать автоанализ. Проверим это:

    => ALTER TABLE autovac SET (autovacuum_enabled = on);
    

    После небольшой паузы видим, что таблица проанализирована и вместо нулей в max_mod_tup мы видим корректные 20 строк:

    => SELECT * FROM need_analyze WHERE tablename = 'public.autovac';
    
       tablename    | mod_tup | max_mod_tup |       last_autoanalyze        
    ----------------+---------+-------------+-------------------------------
     public.autovac |       0 |          20 | 2019-05-21 11:59:48.465987+03
    (1 row)
    

    => SELECT reltuples, relpages FROM pg_class WHERE relname = 'autovac';
    
     reltuples | relpages 
    -----------+----------
          1000 |       17
    (1 row)
    

    Вернемся к автоочистке:

    => SELECT * FROM need_vacuum WHERE tablename = 'public.autovac';
    
       tablename    | dead_tup | max_dead_tup | last_autovacuum
    ----------------+----------+--------------+-----------------
     public.autovac |        0 |           30 |
    (1 row)
    

    Max_dead_tup, как мы видим, уже исправился. Второй момент, на который надо обратить внимание — dead_tup = 0. Статистика показывает, что в таблице нет мертвых версий строк… и это правда. Очищать в нашей таблице пока нечего. Так и любая таблица, использующаяся только в режиме добавления данных (append-only), не будет очищаться и, стало быть, для нее не будет обновляться карта видимости. А это делает невозможным использование исключительно индексного сканирования (index-only scan).

    (В следующий раз мы увидим, что очистка рано или поздно придет и в append-only-таблицу, но происходить это будет очень редко.)

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

    Теперь снова отключим автоочистку и обновим 31 строку — на один больше, чем пороговое значение.

    => ALTER TABLE autovac SET (autovacuum_enabled = off);
    => UPDATE autovac SET s = 'B' WHERE id <= 31;
    => SELECT * FROM need_vacuum WHERE tablename = 'public.autovac';
    
       tablename    | dead_tup | max_dead_tup | last_autovacuum 
    ----------------+----------+--------------+-----------------
     public.autovac |       31 |           30 | 
    (1 row)
    

    Теперь условие срабатывания автоочистки выполняется. Включим автоочистку и после непродолжительной паузы увидим, что таблица обработана:

    => ALTER TABLE autovac SET (autovacuum_enabled = on);
    => SELECT * FROM need_vacuum WHERE tablename = 'public.autovac';
    
       tablename    | dead_tup | max_dead_tup |        last_autovacuum        
    ----------------+----------+--------------+-------------------------------
     public.autovac |        0 |           30 | 2019-05-21 11:59:52.554571+03
    (1 row)
    

    Регулирование нагрузки


    Очистка не блокирует другие процессы, поскольку работает постранично, но тем не менее создает нагрузку на систему и может оказывать заметное влияние на производительность.

    Регулирование для обычной очистки


    Чтобы иметь возможность управлять интенсивностью очистки и, следовательно, ее влиянием на систему, процесс чередует работу и ожидание. Очистка выполняет примерно vacuum_cost_limit условных единиц работы, а затем засыпает на vacuum_cost_delay мс.

    Настройки по умолчанию устанавливают vacuum_cost_limit = 200, vacuum_cost_delay = 0. Последний нолик фактически означает, что (обычная) очистка не засыпает, так что конкретное значение vacuum_cost_limit не играет никакой роли. Это сделано из соображения, что если уж администратору пришлось запускать VACUUM вручную, то он, вероятно, хочет выполнить очистку как можно быстрее.

    Тем не менее, если все-таки установить время сна, то указанный в vacuum_cost_limit объем работы будет складываться из стоимостей работы со страницами в буферном кэше. Каждое обращение к странице оценивается следующим образом:

    • если страница нашлась в буферном кэше, то vacuum_cost_page_hit = 1;
    • если не нашлась, то vacuum_cost_page_miss = 10;
    • если не нашлась, да еще пришлось вытеснять из буфера грязную страницу, то vacuum_cost_page_dirty = 20.

    То есть с настройками vacuum_cost_limit по умолчанию, за один присест могут быть обработаны 200 страниц из кэша, или 20 страниц с диска, или 10 страниц с вытеснением. Понятно, что это довольно условные цифры, но подбирать их точнее нет смысла.

    Регулирование для автоочистки


    Регулирование нагрузки при автоматической очистке работает так же, как и для обычной. Но чтобы очистка, запускаемая вручную, и автоочистка могли работать с разной интенсивностью, для автоочистки сделаны собственные параметры: autovacuum_vacuum_cost_limit и autovacuum_vacuum_cost_delay. Если эти параметры принимают значение -1, то используется значение из vacuum_cost_limit и/или vacuum_cost_delay.

    По умолчанию autovacuum_vacuum_cost_limit = -1 (то есть используется значение vacuum_cost_limit = 200) и autovacuum_vacuum_cost_delay = 20ms. На современной аппаратуре с этими цифрами автоочистка будет работать очень и очень медленно.

    В версии 12 значение autovacuum_vacuum_cost_delay будет уменьшено до 2ms, что можно считать более подходящим первым приближением.

    Кроме того следует учитывать, что предел, устанавливаемый этими параметрами, общий для всех рабочих процессов. Иными словами, при изменении числа одновременно работающих рабочих процессов общая нагрузка будет оставаться постоянной. Поэтому, если стоит задача увеличить производительность автоочистки, то при добавлении рабочих процессов стоит увеличить и autovacuum_vacuum_cost_limit.

    Использование памяти и мониторинг


    В прошлый раз мы рассматривали, как очистка используется оперативную память размером maintenance_work_mem для хранения идентификаторов версий строк, подлежащих очистке.

    Автоочистка поступает абсолютно так же. Но одновременно работающих процессов может быть много, если установить autovacuum_max_workers в большое значение. К тому же вся память выделяется сразу и полностью, а не по необходимости. Поэтому для рабочего процесса автоочистки можно установить собственное ограничение с помощью параметра autovacuum_work_mem. По умолчанию этот параметр равен -1, то есть не используется.

    Как уже говорилось, очистка может работать и с минимальным объемом памяти. Но если на таблице созданы индексы, то небольшое значение maintenance_work_mem может привести к повторным сканированиям индексов. То же самое справедливо и для автоочистки. В идеале следует подобрать такое минимальное значение autovacuum_work_mem, при котором повторные сканирования не происходят.

    Мы видели, что для мониторинга очистки можно использовать параметр VERBOSE (но его нельзя указать для автоочистки) или представление pg_stat_progress_vacuum (но оно показывает только текущую информацию). Поэтому основной способ мониторинга автоочистки — параметр log_autovacuum_min_duration, который выводит информацию в журнал сообщений сервера. По умолчанию он выключен (установлен в -1). Есть резон включить этот параметр (при значении 0 будет выводиться информация о всех запусках автоочистки) и наблюдать за цифрами.

    Вот как выглядит выводимая информация:

    => ALTER SYSTEM SET log_autovacuum_min_duration = 0;
    => SELECT pg_reload_conf();
    
     pg_reload_conf 
    ----------------
     t
    (1 row)
    

    => UPDATE autovac SET s = 'C' WHERE id <= 31;
    

    student$ tail -n 7 /var/log/postgresql/postgresql-11-main.log
    
    2019-05-21 11:59:55.675 MSK [9737] LOG:  automatic vacuum of table "test.public.autovac": index scans: 0
    	pages: 0 removed, 18 remain, 0 skipped due to pins, 0 skipped frozen
    	tuples: 31 removed, 1000 remain, 0 are dead but not yet removable, oldest xmin: 4040
    	buffer usage: 78 hits, 0 misses, 0 dirtied
    	avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
    	system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
    2019-05-21 11:59:55.676 MSK [9737] LOG:  automatic analyze of table "test.public.autovac" system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
    

    Все необходимые сведения здесь присутствуют.

    Напомним, что зачастую следует не увеличивать размер памяти, а уменьшать порог срабатывания очистки, чтобы за один раз обрабатывалось меньше данных.

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

    Продолжение.
    Postgres Professional
    131,42
    Разработчик СУБД Postgres Pro
    Поделиться публикацией

    Похожие публикации

    Комментарии 10

      +1
      Егор, спасибо за статью и отдельное спасибо за темп 1 статья в неделю. Такой темп непросто выдерживать, поэтому вдвойне благодарен. Традиционные вопросы.

      Процесс autovacuum launcher составляет список баз данных, в которых есть какая-либо активность. Активность определяется по статистике, а чтобы она собиралась, должен быть установлен параметр track_counts. Никогда не выключайте autovacuum и track_counts, иначе автоочистка не будет работать.


      Получается, есть еще какой-то фоновый сборщик статистики? Потому что автовакуум сам по себе собирает статистику.
      Вероятно не получится так, что на основе своей же статистики он решает когда запускать сбор статистики.
        +1

        Спасибо, Владимир.


        Получается, есть еще какой-то фоновый сборщик статистики?

        Да. Поленился я начать с самого начала, поэтому обошел это вниманием…
        Есть такой процесс — stats collector. Он накапливает статистику, которую постоянно получает от всех процессов, агрегирует ее и показывает в представлениях pg_stats_*. Это не та статистика, что использует планировщик и которая собирается командой ANAYZE. Подробно все это рассматривается в курсе DBA1, тема Мониторинг.

        +1
        Число мертвых версий постоянно собирается коллектором статистики


        Анализ таблиц, который осуществляет AUTOVACUUM и коллектор статистики — это разные процессы?
          +1

          Да, совершенно разные. См. выше.

          +1
          С автоанализом дело обстоит примерно так же. Считается, что анализа требуют ту таблицы, у которых число измененных (с момента прошлого анализа) версий строк превышает пороговое значение


          А где хранится информация о том, когда именно была изменена строка?
          * Запоминается дата последнего анализа
          * Как именно выбрираются строки, которые изменились после? Вероятно, запоминается xmax (или горизонт событий), а не сама дата. И мониторится
          разница (дельта) между горизонтом событий и последней транзакцией, которая относится к таблице. Верно?
            +1
            А где хранится информация о том, когда именно была изменена строка?

            А такая информация и не хранится. Нет, ну по xmax можно что-то погадать, но это все не нужно.


            Просто каждая транзакция в конце работы отчитывается коллектору статистики: вставила, мол, столько-то строк в такой-то таблице, да столько-то удалила, да еще столько-то изменила. А коллектор эти цифры агрегирует и складывает (см. pg_stat_all_tables).


            Поэтому мы там имеем, в числе прочего, примерное общее число измененных строк (поле n_mod_since_analyze). А когда приходит анализ, он это поле обнуляет.


            Как именно выбрираются строки, которые изменились после?

            Никак. Их постфактум не вычислить. Да и не нужно.

            +1
            Очистка не блокирует другие процессы, поскольку работает постранично


            Но она работает с буфферным кешем? То есть закрепляет страницы буфферного кеша + ставит физическую блокировку, чтобы
            работать со страницей эксклюзивно. То есть получается, что блокировка все таки есть, если я все правильно понял.
              +1

              Все верно, блокирует. Но по одной страничке за раз и ненадолго. И этим не особо отличается от любого другого процесса, который что-то меняет в таблице.


              Я тут имел в виду, что таблица не блокируется, скажем, целиком на все время очистки.

              +1
              Запустившись, рабочий процесс подключается к указанной ему базе данных и начинает с того, что строит список:
              всех таблиц, материализованных представлений и toast-таблиц, требующих очистки,


              И снова вопрос про TOAST, немного не в тему статьи
              * Пусть в таблице есть 2 поля — JSONB и TEXT.
              * Пусть есть строки, в которых содержатся достаточно крупные значения в этих полях, чтобы работал TOAST
              * берем одну из строк
              * меняем один символ в JSONB, например в поле number_of_posts было значение 1, а стало — 2.
              * Создается ли новая версия TOAST только для JSONB? То есть TOAST это таблица, где данные хранятся в виде ключ-значение?
              И каждой строке основной таблицы соответствует 2 строки TOAST таблицы (на каждое из полей)?
                +1
                Создается ли новая версия TOAST только для JSONB?

                Да.


                То есть TOAST это таблица, где данные хранятся в виде ключ-значение?

                Ммм?


                И каждой строке основной таблицы соответствует 2 строки TOAST таблицы (на каждое из полей)?

                Не обязательно. В toast-таблице будет минимум одна строка на каждый "длинный" атрибут, который не сжался и попал в тостер.


                Вообще, по задумке, эти статьи должны дать не просто какие-то факты, но и показать, как эти факты можно проверить. Для этого я и привожу кучу примеров, где, что и как посмотреть. Вот возьмем и проверим.


                =# CREATE TABLE ttt(a text, b text);

                Пусть два текстовых поля будут для простоты, JSONB ничем не отличается. Чтобы не воевать со сжатием, поставим стратегию external:


                =# ALTER TABLE ttt ALTER COLUMN a SET STORAGE EXTERNAL;
                =# ALTER TABLE ttt ALTER COLUMN b SET STORAGE EXTERNAL;

                Вставляем что-нибудь длинное. 3000 байт будут порезаны на два кусочка.


                =# INSERT INTO ttt VALUES (repeat('a',3000), repeat('b',3000));

                Находим toast-таблицу:


                =# SELECT relnamespace::regnamespace, relname
                FROM pg_class WHERE oid = (
                  SELECT reltoastrelid FROM pg_class WHERE relname = 'ttt'
                );

                 relnamespace |    relname     
                --------------+----------------
                 pg_toast     | pg_toast_49334
                (1 row)

                Вот что мы там видим (сами данные не показываю):


                =# SELECT chunk_id, chunk_seq FROM pg_toast.pg_toast_49334;

                 chunk_id | chunk_seq 
                ----------+-----------
                    49340 |         0
                    49340 |         1
                    49341 |         0
                    49341 |         1
                (4 rows)

                Тут у нас два фрагмента (chunk_id = 49340 и 49341), в каждом — два кусочка (chunk_seq).
                Теперь обновим одно поле:


                =# UPDATE ttt SET a = a || 'a';
                =# SELECT chunk_id, chunk_seq FROM pg_toast.pg_toast_49334;

                 chunk_id | chunk_seq 
                ----------+-----------
                    49341 |         0
                    49341 |         1
                    49342 |         0
                    49342 |         1
                (4 rows)

                Фрагмент 49341 (который, очевидно, соответствует полю b) остался, а вместо 49340 (поле a) имеем 49342.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое