Странный вопрос, в column_default таблицы information_schema.columns.

Казалось бы вопрос закрыт, но что произойдёт когда мы удалим дефолт с колонки?

Как известно, начиная с 11 версии postgresql, при добавлении новой not null колонки со значением по умолчанию, физически не меняет данные в таблицы. Просто в момент чтения старых данных возвращает указанное значение. Но что если удалить дефолт?

Вот что говорит chatGTP по этому поводу:

ИИ заботливо обращает внимание на нюансы
ИИ заботливо обращает внимание на нюансы

Я тоже удивился тому, что not null колонка может возвращаться null и провёл небольшой эксперимент.

Время экспериментов!

Все примеры содержат ссылку на www.db-fiddle.com, чтобы можно было поэкспериментировать с за запросами самостоятельно.

Для начала проверим, какое значение возвращается для колонки после удаления дефолта.

-- 1. Создание таблицы с одним полем id
CREATE TABLE test_table (
    id BIGINT
);

-- 2. Вставка 1000 записей (id от 1 до 1000)
INSERT INTO test_table (id)
SELECT generate_series(1, 1000);

-- 3. Добавление NOT NULL поля name со значением по умолчанию 'none'
ALTER TABLE test_table
ADD COLUMN name TEXT NOT NULL DEFAULT 'none';

-- 4. Удаление значения по умолчанию у поля name
ALTER TABLE test_table
ALTER COLUMN name DROP DEFAULT;

-- 5. Чтение первых 10 записей
SELECT *
FROM test_table
LIMIT 10;

| id  | name |
| --- | ---- |
| 1   | none |
| 2   | none |
| 3   | none |
| 4   | none |
| 5   | none |
| 6   | none |
| 7   | none |
| 8   | none |
| 9   | none |
| 10  | none |

Возвращается none. Получается СhatGTP оказался не прав. Можно выдыхать not null колонка по прежнему не возвращает null.

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

-- 5. Установка значения по умолчанию 'another' для поля name
ALTER TABLE test_table
ALTER COLUMN name SET DEFAULT 'another';

-- 6. Чтение первых 10 записей
SELECT *
FROM test_table
LIMIT 10;

| id  | name |
| --- | ---- |
| 1   | none |
| 2   | none |
| 3   | none |
| 4   | none |
| 5   | none |
| 6   | none |
| 7   | none |
| 8   | none |
| 9   | none |
| 10  | none |

Не изменилось, none на месте. Получается, что никакие манипуляции с дефолтом не изменяют значения, заданного при создании столбца. Значит дефолтное значение не при чём.

Но и сама таблица не при чём, т.к. не изменяется. Убедиться в этом довольно просто.

-- 1. Создание таблицы с одним полем id
CREATE TABLE test_table (
    id BIGINT
);

-- 2. Вставка 1000 записей (id от 1 до 1000)
INSERT INTO test_table (id)
SELECT generate_series(1, 100000);

-- 3. Измерение размера таблицы ДО добавления колонки
SELECT
    'before_add_column' AS stage,
    pg_size_pretty(pg_total_relation_size('test_table')) AS total_size,
    pg_size_pretty(pg_relation_size('test_table'))       AS heap_size;


-- 4. Добавление колонку с DEFAULT 
ALTER TABLE test_table
ADD COLUMN name TEXT DEFAULT 'none';


-- 5. Измерение размера таблицы ПОСЛЕ добавления колонки
SELECT
    'after_add_column_default' AS stage,
    pg_size_pretty(pg_total_relation_size('test_table')) AS total_size,
    pg_size_pretty(pg_relation_size('test_table'))       AS heap_size;

-- 6. Обновление имени
UPDATE test_table
SET name = 'edit';

-- 8. Вакуум, чтобы изменить размер только обновлённых строк
VACUUM FULL test_table;

-- 8. Измерение размера таблицы ПОСЛЕ UPDATE
SELECT
    'after_update_materialized' AS stage,
    pg_size_pretty(pg_total_relation_size('test_table')) AS total_size,
    pg_size_pretty(pg_relation_size('test_table'))       AS heap_size;


| stage                     | total_size | heap_size |
| ------------------------- | ---------- | --------- |
| before_add_column         | 3568 kB    | 3544 kB   |
| after_add_column_default  | 3576 kB    | 3544 kB   |
| after_update_materialized | 4336 kB    | 4328 kB   |

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

Так где хранится дефолт для созданной колонки?

В атрибутах столбца: колонка attmissingval таблицы pg_attribute.

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

Давайте, прочитаем это значение:

-- 1. Создание таблицы с одним полем id
CREATE TABLE test_table (
    id BIGINT
);

-- 2. Вставка 1000 записей (id от 1 до 1000)
INSERT INTO test_table (id)
SELECT generate_series(1, 1000);

-- 3. Добавление NOT NULL поля name со значением по умолчанию 'none'
ALTER TABLE test_table
ADD COLUMN name TEXT NOT NULL DEFAULT 'none';

-- 4. Удаление значения по умолчанию у поля name
ALTER TABLE test_table
ALTER COLUMN name DROP DEFAULT;

-- 5. Установка значения по умолчанию 'another' для поля name
ALTER TABLE test_table
ALTER COLUMN name SET DEFAULT 'another';

--6. Получение текущего значения по умолчанию
SELECT column_default
FROM information_schema.columns
WHERE table_name = 'test_table'
  AND column_name = 'name';

--6. Получение значения по умолчанию указанного при создании колонки
SELECT
    a.attname,
    a.attmissingval
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
WHERE c.relname = 'test_table'
  AND a.attname = 'name';

| column_default  |
| --------------- |
| 'another'::text |

| attname | attmissingval |
| ------- | --------------- |
| name    | {none}          |

Как видно, attmissingval задан и равен изначальном значению по умолчанию none, в то время как текущее значение по умолчанию another.

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


  1. Akina
    04.03.2026 20:13

    Вот что говорит chatGTP по этому поводу

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

    А если мы поле, которое не имело ограничения NOT NULL, угостим таким ограничением, то все существующие записи будут проверены на соответствие ограничению. И при обнаружении не соответствующих ограничению записей в изменении структуры нам будет отказано.

    Так что "колонка NOT NULL может вернуть NULL" - это галлюцинация ИИ, в принципе невозможная на практике. Можно было даже не экспериментировать..

    А вот раскопки с метаданными - это по делу и интересно.


    1. Gromilo Автор
      04.03.2026 20:13

      А вот значение поля в конкретной записи - это уже данные. И хранится это значение в теле таблицы

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

      Теоретически, мы можем поменятьattmissingval и у миллиона строк поменяется значение за O(1) . На практике - я не смог этого сделать :)


      1. Akina
        04.03.2026 20:13

        Не всегда.

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

        Для новой колонки со значением по умолчанию, "значения поля в конкретной записи" определяется как раз таки метаданными.

        Именно. Метаданные отвечают на вопрос "какое значение присвоить, если создаётся новая запись, а значение для поля не указано". тогда как данные отвечают на вопрос "какое значение хранится в данном поле у данной конкретной уже существующей записи".

        Теоретически, мы можем поменятьattmissingval и у миллиона строк поменяется значение за O(1)

        Нет, нет и ещё раз нет! Значение НЕ поменяется. Потому что и запись, и соответственно значение поля, УЖЕ существуют и УЖЕ хранятся в теле данных таблицы. И им ну совершенно сиренево, что там происходит в метаданных.

        Весь вопрос был в том, что это за метаданные, т.к. точно не DEFAULT.

        Почему я и сказал, что раскопки в метаданных - это интересно.


        1. Gromilo Автор
          04.03.2026 20:13

          Нет, нет и ещё раз нет! Значение НЕ поменяется. Потому что и запись, и соответственно значение поля, УЖЕ существуют и УЖЕ хранятся в теле данных таблицы. 

          Либо я чего-то не понимаю, либо это не так.

          Смотри, я создал колонку со значением по умолчанию. И размер таблицы не изменился. Как значение может храниться в теле таблицы, если размер таблицы не изменился?
          before_add_column | 3568 kB
          after_add_column_default | 3576 kB

          Далее идём в доку:

          attmissingval anyarray

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


          До 11 версии при создании колонки с дефолтом строки в таблице физически обновлялись и всё было так как ты говоришь. А сейчас это поведение имитируется с помощью attmissingval.


          1. Akina
            04.03.2026 20:13

            Блин. Поля НЕ БЫЛО! Ты его только что создал. Если не было поля - не было и значения в этом поле. Вот объясни мне, дураку, как нечто не существующее может ПОМЕНЯТЬСЯ? Поменяться может только существующее значение - некое конкретное или признак отсутствия ака NULL. Было старое значение, стало новое - это называется поменяться. А у тебя значение ПРИСВОЕНО. Что называется, "с нуля". Раньше не было, теперь есть.

            И это косвенно подтверждается тем, что значение, записанное в attmissingval, не изменяется при изменении DEFAULT. Потому что либо запись существовала на момент создания поля, и тогда ему было присвоено начальное DEFAULT, но оно не записано в тело записи, и его надо брать из attmissingval, либо запись не существовала и была создана позднее - тогда в тело данных записи сразу было записано значение этого поля, взятое из column_default. И для записи, которая существовала, при любом её изменении будет записано новое состояние этой записи, на сей раз уже с внесённым туда полем и его значением, взятым из attmissingval. И уже по телу записи невозможно определить, была ли запись на момент создания поля, или была вставлена позже.


            1. Gromilo Автор
              04.03.2026 20:13

              Postgresql имитирует наличие значения, подставляя его при чтении из метаданных.

              Когда писал про изменение значения в миллионах строк, я рассуждал с точки зрения потребителя. Читаю таблицу, получаю одно значение. Меняю метаданные, читаю таблицу ещё раз, получают другое значение. В самой таблице, физически, конечно, ничего не меняется.

              Могу переформулировать: теоретически, мы можем поменятьattmissingval и это будет выглядеть, как-будто, у миллиона строк поменяется значение за O(1) . На практике - я не смог обновить attmissingval.

              Давай лучше выясним, с чем мы согласны :)

              Согласен ли ты, что при создании новой not null колонки со значением по умолчанию, значения в строках таблицы не изменяются. А при чтении строк созданных до добавления колонки, значение для колонки возвращается из attmissingval?


              1. Akina
                04.03.2026 20:13

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

                Да, согласен. Изменение метаданных не затрагивает область данных. Я вроде об этом говорил.

                А при чтении строк созданных до добавления колонки, значение для колонки возвращается из attmissingval?

                И с этим согласен. И об этом я тоже говорил.

                я рассуждал с точки зрения потребителя

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

                Читаю таблицу, получаю одно значение. Меняю метаданные, читаю таблицу ещё раз, получают другое значение.

                Вот опять. Читаешь таблицу, и НЕ получаешь поля, о котором говоришь, ибо оно не создано, и его в структуре нет. Меняешь метаданные, создавая поле, читаешь таблицу, и теперь получаешь запись другой структуры, со свежесозданным полем. Это не ДРУГОЕ значение. Потому что твоего "одного значения" - не было, а получение этого "одного значения" - галлюцинация из-за неаккуратной формулировки.

                На практике - я не смог обновить attmissingval.

                Конечно, это же запрещено. Нет, можно остановить Постгресс, влезть грязными лапами через какой-нить бин-редактор в файл метаданных, поправить значение, снова запустить Постгресс и посмотреть, скушает ли он это изменение и покажет ли теперь изменённое значение. Я, правда, сомневаюсь, и сильно надеюсь, что Постгресс по каким-нибудь внутренним контрольным суммам сумеет обнаружить стороннее изменение и завопит о разрушении данных, но надо проверять.


                1. Gromilo Автор
                  04.03.2026 20:13

                  Рад, что в целом мы согласны.

                  Я говорил про сценарий: создали колонку, прочитали, поменяли attmissingval, прочитали другое значение. Ты же говоришь про: прочитали, создали, прочитали. Я же правильно понял?

                  Хз, почему ты так решил, я изначально говорил "мы можем поменятьattmissingval ", а значит колонка уже должна существовать.

                  Конечно, это же запрещено.

                  И правильно, а всё-равно было бы интересно попробовать :)


  1. arch1lochus
    04.03.2026 20:13

    constraint проверяется только при INSERT/UPDATE а не при изменении metadata

    Уж очень явная галлюцинация. Единожды испытав боль от добавления нового constraint в существующую таблицу, ошибиться в этом уже нереально, как мне кажется.
    Но что-то мы отстаем от повестки сообщества, там ИИ-агенты уже сами себя и всё вокруг пишут без участия программиста, а мы про какие-то галлюцинации :)


    1. Akina
      04.03.2026 20:13

      ИИ ж не думает, ему тупо нечем. Ему никто не сказал, а сам он не в жисть не догадается, что NOT NULL в описании поля - это просто синтаксический сахар для CONSTRAINT constraint_name CHECK (column_name IS NOT NULL). Вот и галлюцинирует.