Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1tjnb8-00DWOp-Ny for pgsql-general@arkaria.postgresql.org; Sun, 16 Feb 2025 22:58:50 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.94.2) (envelope-from ) id 1tjnb5-00BLVq-VE for pgsql-general@arkaria.postgresql.org; Sun, 16 Feb 2025 22:58:47 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1tjnb5-00BLVh-Id for pgsql-general@lists.postgresql.org; Sun, 16 Feb 2025 22:58:47 +0000 Received: from mail-il1-x143.google.com ([2607:f8b0:4864:20::143]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.96) (envelope-from ) id 1tjnb2-001EW0-2J for pgsql-general@lists.postgresql.org; Sun, 16 Feb 2025 22:58:46 +0000 Received: by mail-il1-x143.google.com with SMTP id e9e14a558f8ab-3d03ac846a7so13473425ab.2 for ; Sun, 16 Feb 2025 14:58:44 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1739746723; x=1740351523; darn=lists.postgresql.org; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=V0pM660yAfmFI8sR6jVTAjrPGrV0wt74+3HPvKqYFNA=; b=Zj23g785Eg1U3Lg1rjSHjGlAiMgqZViI0jJYwFd3rw/zfQYQH2lqHRxUJfesiJreEo ebJrqLfGfA4M77v+NBS/6L6uy4L1k8Q2MVvismbDkXcydx+qJEko7ymymcahxMCusejF /O94aVzcqwxDUB82w2wThJOCrM+BdobRRUzv8JuRUgRcLn1ERwDU0n4BYOfv2vooLUx2 6AJDMkkpsSA4tu5se3zYC7VcpwLnEZrsn8CZQ1tKUfPfjf4rrFP3QvCZdRABCh0RLjnD RyLbFOH5QiRpwOLP5JeJ5n0/3wMfDciC7WKcp8cZIC52r5hVoRmCahI+Er/KU7Y0EvsV sDkg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1739746723; x=1740351523; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=V0pM660yAfmFI8sR6jVTAjrPGrV0wt74+3HPvKqYFNA=; b=VDc0kYtaJUXtl1T/GJWjlxN4tFcyLZcoYB2MSBTVwQuz9J0dblsNgEMVHDOyirkZoM RMEQs2CAVCk5SNoVUo6kg31ha7Wu4UqWm9Tvye9pfskiUZNKHLCxdgTlS2Q8xflmoePF ebBULnQvx3lPDtbtWk+dp92TpG65ZGZVz+Dkbj4h0UrC9zmfiyN2zzTHsOQA7ZSXwF4c rT2YsPqR+e4r8BgsmKBixBQJi+ZzP6KrNIPq4vVSYnOD+rj+c3BkuMd84MUMqAqiplG9 We4BIW1jQOUQmYw+ldOnlF/ZlnEm42vm8+x6QzUsbxRp6bPreV1L/LNEoqJvTlsiB08/ R1uw== X-Forwarded-Encrypted: i=1; AJvYcCV+mlCPTz2SCjN9XDc4GoeXmwJCwMcZ6VD/YdoNSLhYPNNLcSB9c7Pp0Eut7AqWur5y7dMMp/8RiLGhJv0I@lists.postgresql.org X-Gm-Message-State: AOJu0Ywkm6IhBZrRphxO/ZjjkFa7tLzc6wUKa8yLmZNJr2qqoZN1hLx6 S2OAn7yCQKSB2E66urMCiD0O7E5gGOcI6rz0mFtUiyyxJ4hfaIkfv1y9TiJtUWE/yM19SXUg09d ZBPaaUzOC+/NaB5QkApCWDDFtcDI= X-Gm-Gg: ASbGncsAyiYOCDEp8mFHHDyDKK3UzJGpI86W8bBMGXudrybI/Zy3VrunBWzEXMenKuj yZzyUdnBfmABhedhyRuzXmwJ+c27rHsN1+tak+QDVoOMc+ESiQ2l7MwHc9t5lnu/xdQIW9kGAvg == X-Google-Smtp-Source: AGHT+IEUV9hIZwiNVADQptQ3RMZXjJZwoItAwInrdHsj5M2yeebAbGLRLSrL5A7yxOMKT7cuP5tBTS0GBodyPoODFMI= X-Received: by 2002:a05:6e02:2411:b0:3d2:1206:cabe with SMTP id e9e14a558f8ab-3d28095152emr71281275ab.22.1739746722567; Sun, 16 Feb 2025 14:58:42 -0800 (PST) MIME-Version: 1.0 References: <8ac6a1dd-db82-4ea7-8186-0c354d79049f@aklaver.com> <9fa75099-1ee9-43de-ac6e-945841b184eb@aklaver.com> <22406cdf-83f5-4b98-b07e-7827afe97f93@aklaver.com> <94e1205e-8ddf-403b-8477-02d7939f2905@aklaver.com> In-Reply-To: From: Marcelo Fernandes Date: Mon, 17 Feb 2025 11:58:31 +1300 X-Gm-Features: AWEUYZlvJCZP5Lma8ABR7uIUrKS9LHuK8FChEPGZE1aynclULGatuxK2YX360A0 Message-ID: Subject: Re: Best Approach for Swapping a Table with its Copy To: Greg Sabino Mullane Cc: Adrian Klaver , Dominique Devienne , pgsql-general@lists.postgresql.org Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On Sat, Feb 15, 2025 at 4:12=E2=80=AFAM Greg Sabino Mullane wrote: > The pg_repack link posted earlier has the details on how it is done. But = messing with system catalogs like this is highly discouraged, for good reas= ons. Still, if you need to go that route, test heavily and post the solutio= ns here for feedback. I'm trying to digest what pg_repack does by reproducing the same behaviour using SQL. I have come up with the script below to test the whole scenario,= but I have two major problems: - The foreign keys are not being updated to point to the new table. - pg_repack seems to update relcache entries. I am not sure how to do that = with SQL. See: https://github.com/marcelofern/pg_repack/blob/9f36c65bd57ca1b228025687843= 758556b56df8e/lib/repack.c#L1373-L1387 And for reference, here is the script I have used so far. Keen for any suggestions on how to swap the foreign keys so that they can point towards = the new table. ```sql -- Create the original table that will be later swapped by its copy. DROP TABLE IF EXISTS original CASCADE; CREATE TABLE original ( id SERIAL PRIMARY KEY, name VARCHAR(5000) NOT NULL, -- necessary for the TOAST table. value INTEGER NOT NULL ); -- Insert 10_000 rows into it. INSERT INTO original (name, value) SELECT 'item_' || generate_series(1, 10000) AS name, (generate_series(1, 10000) % 10000) + 1 AS value; -- Create the copy table, this table will be swapped for the original table -- later DROP TABLE IF EXISTS copy; CREATE TABLE copy ( id SERIAL PRIMARY KEY, name VARCHAR(5000) NOT NULL, value INTEGER NOT NULL ); -- Pull all the data from the original table into the copy table. INSERT INTO copy SELECT id, name, value FROM ONLY original; -- Create a table with a foreign key to the original table to verify if the -- swap addresses the foreign key table. DROP TABLE IF EXISTS table_with_fk; CREATE TABLE table_with_fk ( id SERIAL PRIMARY KEY, original_id INTEGER NOT NULL, CONSTRAINT fk_original FOREIGN KEY (original_id) REFERENCES original(id) ON DELETE CASCADE ON UPDATE CASCADE ); -- Insert 10_000 rows into it. INSERT INTO table_with_fk (original_id) SELECT generate_series(1, 10000); -- Analogously, create a table with a NOT VALID foreign key. DROP TABLE IF EXISTS table_with_not_valid_fk; CREATE TABLE table_with_not_valid_fk ( id SERIAL PRIMARY KEY, original_id INTEGER NOT NULL, CONSTRAINT not_valid_fk_original FOREIGN KEY (original_id) REFERENCES original(id) ON DELETE CASCADE ON UPDATE CASCADE NOT VALID ); -- Insert 10_000 rows INSERT INTO table_with_not_valid_fk (original_id) SELECT generate_series(1, 10000); -- All tables must have 10_000 rows in them. SELECT count(*) FROM original; SELECT count(*) FROM copy; SELECT count(*) FROM table_with_fk; SELECT count(*) FROM table_with_not_valid_fk; -- See relation info for the tables and their TOASTs. SELECT X.relname, X.reltablespace, X.oid, X.reltoastrelid, X.relowner, X.relkind, X.relfrozenxid, X.relminmxid, X.relpages, X.reltuples, X.relallvisible, X.relfilenode, TOAST_X.indexrelid as toast_indexrelid FROM pg_catalog.pg_class X LEFT JOIN pg_catalog.pg_index TOAST_X ON X.reltoastrelid =3D TOAST_X.indrelid AND TOAST_X.indisvalid WHERE X.oid IN (('original')::regclass, ('copy')::regclass) ORDER BY X.relname; -- -[ RECORD 1 ]----+--------- -- relname | copy -- reltablespace | 0 -- oid | 22522 -- reltoastrelid | 22526 -- relowner | 10 -- relkind | r -- relfrozenxid | 2068 -- relminmxid | 1 -- relpages | 64 -- reltuples | 10000 -- relallvisible | 64 -- relfilenode | 22522 -- toast_indexrelid | 22527 -- -[ RECORD 2 ]----+--------- -- relname | original -- reltablespace | 0 -- oid | 22513 -- reltoastrelid | 22517 -- relowner | 10 -- relkind | r -- relfrozenxid | 2065 -- relminmxid | 1 -- relpages | 64 -- reltuples | 10000 -- relallvisible | 64 -- relfilenode | 22513 -- toast_indexrelid | 22518 -- Take note of the dependencies for the toast table to compare later. SELECT d1.objid AS original_objid, d2.objid AS copy_objid FROM pg_depend d1, pg_depend d2 WHERE d1.objid =3D ('original'::regclass) AND d2.objid =3D ('copy'::regclas= s); -- -[ RECORD 1 ]--+------ -- original_objid | 22513 -- copy_objid | 22522 -- Start table swap inside a transaction. BEGIN; LOCK TABLE original, copy IN ACCESS EXCLUSIVE MODE; SELECT * FROM pg_class WHERE relname in ('original', 'copy') FOR UPDATE; WITH swapped AS ( SELECT c1.oid AS original_oid, c2.oid AS copy_oid, c1.relfilenode AS original_filenode, c2.relfilenode AS copy_filenod= e, c1.reltablespace AS original_tablespace, c2.reltablespace AS copy_tablespace, c1.reltoastrelid AS original_toast, c2.reltoastrelid AS copy_toast, c1.relfrozenxid AS original_frozenxid, c2.relfrozenxid AS copy_frozenxid, c1.relminmxid as original_relminmxid, c2.relminmxid AS copy_relminm= xid, c1.relpages AS original_pages, c2.relpages AS copy_pages, c1.reltuples AS original_tuples, c2.reltuples AS copy_tuples, c1.relallvisible AS original_allvisible, c2.relallvisible AS copy_allvisible FROM pg_class c1, pg_class c2 WHERE c1.relname =3D 'original' AND c2.relname =3D 'copy' ) UPDATE pg_class SET relfilenode =3D CASE WHEN oid =3D (SELECT original_oid FROM swapped) THEN (SELECT copy_filenode FROM swapped) WHEN oid =3D (SELECT copy_oid FROM swapped) THEN (SELECT original_filenode FROM swapped) END, reltablespace =3D CASE WHEN oid =3D (SELECT original_oid FROM swapped) THEN (SELECT copy_tablespace FROM swapped) WHEN oid =3D (SELECT copy_oid FROM swapped) THEN (SELECT original_tablespace FROM swapped) END, reltoastrelid =3D CASE WHEN oid =3D (SELECT original_oid FROM swapped) THEN (SELECT copy_toast FROM swapped) WHEN oid =3D (SELECT copy_oid FROM swapped) THEN (SELECT original_toast FROM swapped) END, relfrozenxid =3D CASE WHEN oid =3D (SELECT original_oid FROM swapped) THEN (SELECT copy_frozenxid FROM swapped) WHEN oid =3D (SELECT copy_oid FROM swapped) THEN (SELECT original_frozenxid FROM swapped) END, relminmxid =3D CASE WHEN oid =3D (SELECT original_oid FROM swapped) THEN (SELECT copy_relminmxid FROM swapped) WHEN oid =3D (SELECT copy_oid FROM swapped) THEN (SELECT original_relminmxid FROM swapped) END, relpages =3D CASE WHEN oid =3D (SELECT original_oid FROM swapped) THEN (SELECT copy_pages FROM swapped) WHEN oid =3D (SELECT copy_oid FROM swapped) THEN (SELECT original_pages FROM swapped) END, reltuples =3D CASE WHEN oid =3D (SELECT original_oid FROM swapped) THEN (SELECT copy_tuples FROM swapped) WHEN oid =3D (SELECT copy_oid FROM swapped) THEN (SELECT original_tuples FROM swapped) END, relallvisible =3D CASE WHEN oid =3D (SELECT original_oid FROM swapped) THEN (SELECT copy_allvisible FROM swapped) WHEN oid =3D (SELECT copy_oid FROM swapped) THEN (SELECT original_allvisible FROM swapped) END WHERE oid IN (SELECT original_oid FROM swapped UNION SELECT copy_oid FROM swapped); -- See that relevant fields have been swapped SELECT X.relname, X.reltablespace, X.oid, X.reltoastrelid, X.relowner, X.relkind, X.relfrozenxid, X.relminmxid, X.relpages, X.reltuples, X.relallvisible, X.relfilenode, TOAST_X.indexrelid as toast_indexrelid FROM pg_catalog.pg_class X LEFT JOIN pg_catalog.pg_index TOAST_X ON X.reltoastrelid =3D TOAST_X.indrelid AND TOAST_X.indisvalid WHERE X.oid IN (('original')::regclass, ('copy')::regclass) ORDER BY X.relname; -- -[ RECORD 1 ]----+--------- -- relname | copy -- reltablespace | 0 -- oid | 22522 -- reltoastrelid | 22517 -- relowner | 10 -- relkind | r -- relfrozenxid | 2065 -- relminmxid | 1 -- relpages | 64 -- reltuples | 10000 -- relallvisible | 64 -- relfilenode | 22513 -- toast_indexrelid | 22518 -- -[ RECORD 2 ]----+--------- -- relname | original -- reltablespace | 0 -- oid | 22513 -- reltoastrelid | 22526 -- relowner | 10 -- relkind | r -- relfrozenxid | 2068 -- relminmxid | 1 -- relpages | 64 -- reltuples | 10000 -- relallvisible | 64 -- relfilenode | 22522 -- toast_indexrelid | 22527 -- Lock the pg_depend rows that correspond to 'original' and 'copy' SELECT * FROM pg_depend WHERE objid IN (('original')::regclass, ('copy')::regclass) FOR UPDATE; -- Swap the objid values for the two dependencies WITH swapped_dep AS ( SELECT d1.objid AS original_objid, d2.objid AS copy_objid FROM pg_depend d1, pg_depend d2 WHERE d1.objid =3D ('original'::regclass) AND d2.objid =3D ('copy'::regclass) ) -- TODO: this update is not working, maybe it needs to be deleted and then -- inserted again? A delete-followed-by create is what pg_repack seems to d= o. UPDATE pg_depend SET objid =3D CASE WHEN objid =3D (SELECT original_objid FROM swapped_dep) THEN (SELECT copy_objid FROM swapped_dep) WHEN objid =3D (SELECT copy_objid FROM swapped_dep) THEN (SELECT original_objid FROM swapped_dep) END WHERE objid IN (SELECT original_objid FROM swapped_dep UNION SELECT copy_objid FROM swapped_dep); -- Verify the dependencies have been swapped. SELECT d1.objid AS original_objid, d2.objid AS copy_objid FROM pg_depend d1, pg_depend d2 WHERE d1.objid =3D ('original'::regclass) AND d2.objid =3D ('copy'::regclas= s); ---[ RECORD 1 ]--+------ --original_objid | 22513 --copy_objid | 22522 -- Renames! ALTER TABLE original RENAME TO temp_original; ALTER TABLE copy RENAME TO original; ALTER TABLE temp_original RENAME TO copy; DROP TABLE copy CASCADE; -- Insert a couple of rows in the new "original" to verify it works INSERT INTO original (id, name, value) values (10001, 'my_new_row', 10); SELECT * from original order by id DESC; -- TODO (minor): Index names for pks and its seq have not been renamed. -- TODO (major): The FKs on the related tables weren't updated to use the n= ew -- table -- \d table_with_fk -- Table "public.table_with_fk" -- Column | Type | Collation | Nullable | Default -- -------------+---------+-----------+----------+-------------------------= ------------------ -- id | integer | | not null | nextval('table_with_fk_id_seq'::regclass) -- original_id | integer | | not null | -- Indexes: -- "table_with_fk_pkey" PRIMARY KEY, btree (id) --=20 -- \d table_with_not_valid_fk -- Table "public.table_with_not_valid_fk" -- Column | Type | Collation | Nullable | De= fault -- -------------+---------+-----------+----------+-------------------------= ---------------------------- -- id | integer | | not null | nextval('table_with_not_valid_fk_id_seq'::regclass) -- original_id | integer | | not null | -- Indexes: -- "table_with_not_valid_fk_pkey" PRIMARY KEY, btree (id) -- Roll this back so that your postgres db doesn't get potentially messed u= p. ROLLBACK; ```