Листинг Тип NULL при обновлении нескольких строк

У меня проблема, когда я пытаюсь обновить многие строки одновременно.

Вот таблица и запрос, которые я использую (упрощенные для лучшего чтения):

Таблица

CREATE TABLE foo
(
    pkid integer,
    x integer,
    y integer
)

запрос

UPDATE foo SET x=t.x, y=t.y FROM
(VALUES (50, 50, 1),
        (100, 120, 2))
AS t(x, y, pkid) WHERE foo.pkid=t.pkid

Этот запрос работает отлично, но когда я пытаюсь выполнить запрос, где все значения x или y равны null, я получаю сообщение об ошибке:

запрос с нулями

UPDATE foo SET x=t.x, y=t.y FROM
(VALUES (null, 20, 1),
        (null, 50, 2))
AS t(x, y, pkid) WHERE foo.pkid=t.pkid

Ошибка

ERROR:  column "x" is of type integer but expression is of type text
LINE 1: UPDATE foo SET x=t.x FROM

Единственный способ исправить это - изменить хотя бы одно из значений (null, 20, 1) - (null:int, 50, 2), но я не могу этого сделать, поскольку у меня есть функция, которая генерирует запрос "обновить несколько строк", и это не делает Не знаю ничего о типах столбцов.

Какое лучшее решение здесь? Есть ли лучший запрос на обновление для нескольких строк? Есть ли какая-либо функция или синтаксис, например AS t(x:gettype(foo.x), y:gettype(foo.y), pkid:gettype(foo.pkid))?

Ответ 1

С автономным выражением VALUES PostgreSQL не знает, какие типы данных должны быть. С помощью простых числовых литералов система с удовольствием принимает соответствующие типы. Но с другим вводом (например, NULL) вам нужно будет явно указать - как вы уже узнали.

Вы можете запросить pg_catalog (быстрый, но специфичный для PostgreSQL) или information_schema (медленный, но стандартный SQL), чтобы узнать и подготовить свой оператор к соответствующим типам.

Или вы можете использовать один из этих простых "трюков":

1. Выберите строку с LIMIT 0, добавьте строки с UNION ALL

UPDATE foo f
SET    x = t.x
     , y = t.y
FROM  (
  (SELECT pkid, x, y FROM foo LIMIT 0) -- parenthesis needed with LIMIT
   UNION ALL
   SELECT 1, 20, NULL
   UNION ALL
   SELECT 2, 50, NULL
   ) t               -- column names and types are already defined
WHERE  f.pkid = t.pkid

Первый подвыбор подзапроса:

(SELECT x, y, pkid  FROM foo LIMIT 0)

получает имена и типы для столбцов, но LIMIT 0 не позволяет добавить фактическую строку. Последующие строки принудительно применяются к теперь четко определенному типу строк - и сразу же проверяются, если они соответствуют типу. Должно быть тонкое дополнительное улучшение по сравнению с вашей оригинальной формой.

Основное ограничение : с отдельными линиями SELECT, Postgres сразу же вводит литералы ввода в "наилучшее усилие". Позже, когда он попытается применить к указанным типам первый SELECT, для некоторых типов может быть слишком поздно, если не существует зарегистрированного присваивания между предполагаемым типом и целевым типом. Пример texttimestamp.

Pro:
- Минимальные накладные расходы.
- Читаемость, простота и быстрота для нескольких строк.
- Вам нужно знать только соответствующие имена столбцов таблицы.

Con:
- Разрешение типа может быть неудачным для некоторых типов.
- UNION ALL SELECT медленнее, чем выражение VALUES для длинных списков строк, как вы нашли в своем тесте.
- Подробный синтаксис для каждой строки.

2. VALUES выражение с типом столбца

...
FROM  (
   VALUES 
     ((SELECT pkid FROM foo LIMIT 0)
    , (SELECT x    FROM foo LIMIT 0)
    , (SELECT y    FROM foo LIMIT 0)) -- Get type for each col individually
   , (1, 20, NULL)
   , (2, 50, NULL)
   ) t (pkid, x, y)  -- columns names not defined yet, only types.
...

Первая строка в выражении VALUES представляет собой строку значений NULL, которая определяет тип для всех последующих строк.

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

Con:
- Подробный синтаксис для нескольких строк
- Менее читаемый (ИМО).

3. VALUES выражение с типом строки

UPDATE foo f
SET x = (t.r).x       -- parenthesis needed to make syntax unambiguous
  , y = (t.r).y
FROM (
   VALUES
      ('(1,20,)'::foo)  -- columns need to be in default order of table
     ,('(2,50,)')       -- nothing after the last comma for NULL
   ) t (r)              -- column name for row type
WHERE  f.pkid = (t.r).pkid

Вы, очевидно, знаете имя таблицы. Если вы также знаете количество столбцов и их порядок, вы можете работать с этим.

Для каждой таблицы в PostgreSQL тип строки регистрируется автоматически. Если вы сопоставляете количество столбцов в своем выражении, вы можете указать тип строки таблицы ('(1,50,)'::foo), тем самым неявно назначая типы столбцов. Не вводите ничего за запятую, чтобы ввести значение NULL. Добавьте запятую для каждого неулокального конечного столбца.
На следующем шаге вы можете получить доступ к отдельным столбцам с продемонстрированным синтаксисом. Подробнее о Выбор поля в руководстве.

Или вы можете добавить строку значений NULL и использовать единый синтаксис для фактических данных:

...
  VALUES
      ((NULL::foo))  -- row of NULL values
    , ('(1,20,)')    -- uniform ROW value syntax for all
    , ('(2,50,)')
...

Добавленная строка исключается предложением WHERE в вашем UPDATE.
Для других целей вы можете исключить добавленную первую строку с OFFSET 1 в подзапрос.

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

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

4. VALUES выражение с разложенным типом строки

Как 3., но с разложенными строками в стандартном синтаксисе:

UPDATE foo f
SET    x = t.x
     , y = t.y
FROM (
   VALUES
      (('(1,20,)'::foo).*)  -- decomposed row of values
    , (2,50,NULL)
   ) t(pkid, x, y)  -- arbitrary column names (I made them match)
WHERE  f.pkid = t.pkid;

Или снова с ведущей строкой значений NULL:

...
   VALUES
      ((NULL::foo).*)  -- row of NULL values
    , (1,20,NULL)      -- uniform syntax for all
    , (2,50,NULL)
...

Плюсы и минусы похожи на 3., но с более распространенным синтаксисом.
И вам нужно указать имена столбцов (если они вам понадобятся).

Последний мой любимый.

Ответ 2

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

SELECT column_name,data_type,udt_name 
FROM information_schema.columns 
WHERE table_name = 'foo';

Из этого udt_name вы получите необходимый бросок, как вы объяснили в последнем абзаце. Кроме того, вы можете сделать это:

UPDATE foo
SET x = t.x
FROM (VALUES(null::int4,756),(null::int4,6300))
AS t(x,pkid)
WHERE foo.pkid = t.pkid;

Ответ 3

Ваш script создаст временную таблицу из foo. Он будет иметь те же типы данных, что и foo. Используйте невозможное условие, чтобы оно было пустым:

select x, y, pkid
into temp t
from foo
where pkid = -1

Сделайте свой script для вставки в него:

insert into t (x, y, pkid) values
(null, 20, 1),
(null, 50, 2)

Теперь обновите его:

update foo 
set x=t.x, y=t.y 
from t
where foo.pkid=t.pkid

Наконец, отбросьте его:

drop table t