Usuwanie kolumn PostgreSQL 9.6 i skutki uboczne funkcji SQL za pomocą CTE

15

Gdybym miał tabelę z 3 kolumnami - powiedzmy A, B i D - i musiałbym wprowadzić nową - powiedzmy C, aby zastąpić obecną pozycję D. Użyłbym następującej metody:

  1. Wprowadź 2 nowe kolumny jako C i D2.
  2. Skopiuj zawartość D do D2.
  3. Usuń D.
  4. Zmień nazwę D2 na D.

Nowe zamówienie będzie A, B, C i D.

Myślałem, że jest to uzasadniona praktyka, ponieważ (jak dotąd) nie powodowała żadnych problemów.

Jednak dzisiaj natknąłem się na problem, gdy funkcja wykonująca instrukcję w tej samej tabeli zwróciła następujący błąd:

table row type and query-specified row type do not match

I następujący szczegół:

Query provides a value for a dropped column at ordinal position 13

Próbowałem zrestartować PostgreSQL, VACUUM FULLa na końcu usunąć i ponownie utworzyć funkcję, jak to sugerowano tutaj i tutaj, ale te rozwiązania nie działały (poza tym, że próbują rozwiązać sytuację, w której zmieniono tabelę systemową).

Mając luksus pracy z bardzo małą bazą danych, wyeksportowałem ją, usunąłem, a następnie ponownie zaimportowałem, co rozwiązało problem z moją funkcją.


Wiedziałem, że nie należy mieszać się z naturalną kolejnością kolumn , modyfikując tabele systemowe (brudząc sobie ręce pg_attributeitp.), Jak pokazano tutaj:

Czy można zmienić naturalną kolejność kolumn w Postgres?

Sądząc po błędzie rzuconym przez moją funkcję, teraz zdaję sobie sprawę, że zmiana kolejności kolumn za pomocą mojej metody jest również nie-nie. Czy ktoś może świecić światłem, dlaczego to, co robię, jest również złe?


Wersja Postgres to 9.6.0.

Oto funkcja:

CREATE OR REPLACE FUNCTION "public"."__post_users" ("facebookid" text, "useremail" text, "username" text) RETURNS TABLE (authentication_code text, id integer, key text, stripe_id text) AS '

-- First, select the user:
WITH select_user AS
(SELECT
users.id
FROM
users
WHERE
useremail = users.email),

-- Second, update the user (if user exists):
update_user AS
(UPDATE
users
SET
authentication_code = GEN_RANDOM_UUID(),
authentication_date = current_timestamp,
facebook_id = facebookid
WHERE EXISTS (SELECT * FROM select_user)
AND
useremail = users.email
RETURNING
users.authentication_code,
users.id,
users.key,
users.stripe_id),

-- Third, insert the user (if user does not exist):
insert_user AS
(INSERT INTO
users (authentication_code, authentication_date, email, key, name, facebook_id)
SELECT
GEN_RANDOM_UUID(),
current_timestamp,
useremail,
GEN_RANDOM_UUID(),
COALESCE(username, SUBSTRING(useremail FROM ''([^@]+)'')),
facebookid
WHERE NOT EXISTS (SELECT * FROM select_user)
RETURNING
users.authentication_code,
users.id,
users.key,
users.stripe_id)

-- Finally, select the authentication code, ID, key and Stripe ID:
SELECT
*
FROM
update_user
UNION ALL
SELECT
*
FROM
insert_user' LANGUAGE "sql" COST 100 ROWS 1
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER

Przeprowadziłem zmianę nazwy / kolejność na obu kolumnach facebook_idi stripe_id(przed nimi została dodana nowa kolumna, co jest przyczyną zmiany nazwy, ale zapytanie to nie dotyczy).

Posiadanie kolumn w określonej kolejności nie leży w interesie porządku. Jednak powodem zadawania tego pytania nie jest obawa, że ​​prosta zmiana nazwy i usunięcie kolumny może wywołać prawdziwe problemy dla kogoś, kto używa funkcji w trybie produkcyjnym (jak to się stało).

Andy
źródło
Spójrz na Jak zmienić pozycję kolumny w tabeli bazy danych PostgreSQL? na przepełnieniu stosu, które mogą być pomocne, chociaż najczęściej sugerują, aby NIE zmieniać kolejności kolumn.
joanolo,

Odpowiedzi:

16

Prawdopodobny błąd w 9.6 i 9.6.1

Dla mnie to wygląda jak błąd ...

Nie wiem, dlaczego tak się dzieje, ale mogę potwierdzić, że tak się dzieje. Jest to najprostsza znaleziona konfiguracja, która odtwarza problem (w wersji 9.6.0 i 9.6.1).

CREATE TABLE users
(
    id SERIAL PRIMARY KEY,
    email TEXT NOT NULL,
    column_that_we_will_drop TEXT
) ;

-- Function that uses the previous table, and that has a CTE
CREATE OR REPLACE FUNCTION __post_users
    (_useremail text) 
RETURNS integer AS
$$
-- Need a CTE to produce the error. A 'constant' one suffices.
WITH something_even_if_useless(a) AS
(
    VALUES (1)
)
UPDATE
    users
SET
    id = id
WHERE 
    -- The CTE needs to be referenced, if the next
    -- condition were not in place, the problem is not reproduced
    EXISTS (SELECT * FROM something_even_if_useless)
    AND email = _useremail
RETURNING
    id
$$
LANGUAGE "sql" ;

Po tej konfiguracji następna instrukcja po prostu działa

SELECT * FROM __post_users('[email protected]');

W tym momencie KROKUJEMY jedną kolumnę:

ALTER TABLE users 
    DROP COLUMN column_that_we_will_drop ;

Ta zmiana powoduje, że następna instrukcja generuje błąd

SELECT * FROM __post_users('[email protected]');

który jest taki sam jak wspomniany przez @Andy:

ERROR: table row type and query-specified row type do not match
SQL state: 42804
Detail: Query provides a value for a dropped column at ordinal position 3.
Context: SQL function "__post_users" statement 1
    SELECT * FROM __post_users('[email protected]');

Upuszczenie i ponowne odtworzenie funkcji NIE rozwiązuje problemu.
VACUUM FULL (tabela lub cała baza danych) nie rozwiązuje problemu.


Raport o błędzie został przekazany na odpowiednią listę mailingową PostgreSQL i otrzymaliśmy bardzo szybką odpowiedź :

Nie mogę tego odtworzyć w HEAD lub końcówce gałęzi 9.6. Myślę, że został już naprawiony przez tę łatkę, która pojawiła się trochę po 9.6.1:

https://git.postgresql.org/gitweb/?p=postgresql.git&a=commitdiff&h=f4d865f22

Ale dziękuję za raport!

pozdrowienia, Tom Lane


Wersja 9.6.2

W dniu 2017-03-06 mogę potwierdzić, że nie mogę odtworzyć tego zachowania w wersji 9.6.2. Oznacza to, że błąd został poprawiony w tym wydaniu.

AKTUALIZACJA

Na komentarz @Jana: „Mogę potwierdzić, że błąd jest obecny w wersji 9.6.1 i został naprawiony w wersji 9.6.2. Poprawka jest również wymieniona na stronie internetowej wydania postgres : Poprawka fałszywa„ zapytanie zawiera wartość błędów w usuniętej kolumnie ”podczas INSERT lub UPDATE w tabeli z upuszczoną kolumną „


joanolo
źródło
4
Używam 9.6.0 i @joanolo jest poprawny, mogę odtworzyć błąd za pomocą jego metody. Jeśli Tom nie może go odtworzyć, prawdopodobnie był to izolowany błąd w tej konkretnej wersji (i zakładam, że 9.6.1?). Jak wspomniano w odpowiedzi, problem pojawia się po upuszczeniu kolumny w tabeli i użyciu funkcji z CTE wpływającej na tabelę. Problemem jest więc nie tylko zmiana kolejności i właśnie do tego dążyłem z moim pytaniem. Zmienię tytuł, aby to odzwierciedlić.
Andy,
Mogę potwierdzić, że błąd jest obecny w 9.6.1 i został naprawiony w 9.6.2. Poprawka jest również wymieniona na stronie z wydaniem Postgres : Napraw fałszywe zapytanie „podaje wartość dla pomijanej kolumny” podczas INSERT lub UPDATE w tabeli z usuniętą kolumną
Jana
0

Wiem, że pewnie już to słyszałeś, ale to okropny pomysł.

  • Logiczne porządkowanie wpływa tylko na takie rzeczy SELECT *
  • Efekt logicznego uporządkowania jest ograniczony do wyglądu.

Jeśli więc nie ma znaczenia, że ​​cię to nie zniechęca i przyznamy, że gramy w Photoshopie ze strukturą wierszy i obsesją na punkcie wyświetlania, wyjaśnijmy jeszcze kilka rzeczy.

  • Kolejność logiczna nie istnieje w PostgreSQL
  • Fizyczne zamawianie przynosi realne korzyści (pakowanie stolików)
  • PostgreSQL nie daje żadnej kontroli nad porządkiem fizycznym po CREATE TABLEżadnym z nich (choć byłby to o wiele wyższy priorytet)

PostgreSQL jest więc złą warstwą wyświetlania. Poza tym, chociaż ALTERdziała dobrze, nie powinieneś temperować smoka. Zarówno ALTER TABLE, jak i sekcja CAVEAT wspominają o tym,

Niektóre polecenia DDL, obecnie tylko TRUNCATEi formy przepisywania tabel ALTER TABLE, nie są bezpieczne dla MVCC. Oznacza to, że po zatwierdzeniu obcięcia lub przepisania tabela będzie wyglądać pusto dla równoczesnych transakcji, jeśli używają migawki wykonanej przed zatwierdzeniem polecenia DDL. Będzie to dotyczyło tylko transakcji, która nie uzyskała dostępu do danej tabeli przed uruchomieniem polecenia DDL - każda transakcja, która to zrobiła, zawierałaby co najmniej blokadę tabeli ACCESS SHARE, która blokowałaby polecenie DDL do czasu zakończenia tej transakcji. Dlatego te polecenia nie spowodują żadnej widocznej niespójności w treści tabeli dla kolejnych zapytań w tabeli docelowej, ale mogą powodować widoczną niespójność między zawartością tabeli docelowej a innymi tabelami w bazie danych.

A jeśli to wszystko nie wystarczy, a ty nadal chcesz udawać, że to dobry pomysł, a raczej okropny pomysł. Następnie spróbuj tego

  1. Zrzuć stół za pomocą pg_dump -t
  2. Zmień kolejność kolumn ręcznie w zrzucie.
  3. Załaduj rzeczy do tabeli TEMP.
  4. BEGIN transakcja
  5. DROP stary stół całkowicie,
  6. RENAME tabela temp do tabeli prod.
  7. COMMIT

Jeśli wszystko to wydaje się nadmierne, pamiętaj, że aktualizacja wierszy w bazie danych wymaga przepisania wierszy (zakładając, że nieone TOAST . Musisz przeanalizować dane i odbudować schemat tabeli, ale tak czy inaczej musisz przepisać wiersz. Gdybym musiał wykonać to zadanie, tak bym to zrobił.

Ale to wszystko mówi ogólnie. Nikt nie odtworzył twoich wyników.

Dałeś przypadek testowy, którego nie możemy uruchomić

ERROR:  column users.authentication_code does not exist
LINE 24: users.authentication_code,

I nie podałeś nam dokładnie tej wersji.

Evan Carroll
źródło
Dziękuję za dokładne wyjaśnienie. Zredagowałem pytanie, aby uwzględnić konkretną wersję.
Andy,
4
Błąd jest odtwarzalny w wersji 9.6.0
ypercubeᵀᴹ
0

Rozwiązałem ten problem, tworząc kopię zapasową i przywracając bazę danych.

Kroki dla Heroku

  • Włącz tryb konserwacji: heroku maintenance:on
  • Zapasowa baza danych: heroku pg:backups:capture
  • Przywróć bazę danych: heroku pg:backups:restore
  • Uruchom ponownie aplikację: heroku restart
  • Wyłącz tryb konserwacji: heroku maintenance:off
ma11hew28
źródło
0

Na ten błąd też wpadłem. Dla tych, którzy nie chcą w pełni tworzyć kopii zapasowych / przywracać swoją bazę danych. Wiedz, że po prostu kopiowanie tabeli działa. Nie ma jednak „magicznego” sposobu na skopiowanie tabeli. Zrobiłem to za pomocą:

SELECT * INTO mytable_copy FROM mytable;
ALTER TABLE mytable RENAME TO mytable_backup; -- just in case. you never know
ALTER TABLE mytable_copy RENAME TO mytable;

Po tym nadal będziesz musiał ręcznie ponownie utworzyć indeksy, klucze obce i wartości domyślne. Ponowne odtworzenie tabeli sprawiło, że błąd zniknął.

Thibauld
źródło