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.96) (envelope-from ) id 1wRtd7-002mtK-0Z for pgsql-hackers@arkaria.postgresql.org; Tue, 26 May 2026 15:23:41 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wRtd4-004jRG-33 for pgsql-hackers@arkaria.postgresql.org; Tue, 26 May 2026 15:23:39 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wRtd4-004jR7-1e for pgsql-hackers@lists.postgresql.org; Tue, 26 May 2026 15:23:39 +0000 Received: from mail-wr1-x431.google.com ([2a00:1450:4864:20::431]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wRtd2-00000000s3x-3vn5 for pgsql-hackers@postgresql.org; Tue, 26 May 2026 15:23:38 +0000 Received: by mail-wr1-x431.google.com with SMTP id ffacd0b85a97d-45e9f4a3510so3036201f8f.1 for ; Tue, 26 May 2026 08:23:35 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cybertec.at; s=google; t=1779809013; x=1780413813; darn=postgresql.org; h=mime-version:user-agent:content-transfer-encoding:references :in-reply-to:date:to:from:subject:message-id:from:to:cc:subject:date :message-id:reply-to; bh=daWksvyyPfFie9b0FmyvqFPN5ZseNW5zV1e9lZKspkE=; b=PSeOZehxI7XSi8Db1HMCGLCkPUWj1KisSg4Ox9wLsaEjMcswUEubmgRWOVGMxXIEsy cTecMalXhvGpD+wcG7Cx2hSy3QceWUN8G8u62VK+gIWMNuzrmUQ/q/C8laK7CS6pGLM3 4juA9DI8Sdl7tfk64fT6WNdRH/E9rAvKqSSJ50QvtPX7/za0+byL9jNImQLNa7W6c9kE oDYqGbas1KnlGyGjshwrHKc10IxqKv5Y/Jkkzz7b0jSdGdw9SGaxGJarCXgBvMWT/dch exr5zTaIZQYZLGhHQg4Qt0yCne+aO2zvlybpFaLmMOMuYtwtdq+Yt5SL8ua0XZSGThVR 2WlQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1779809013; x=1780413813; h=mime-version:user-agent:content-transfer-encoding:references :in-reply-to:date:to:from:subject:message-id:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=daWksvyyPfFie9b0FmyvqFPN5ZseNW5zV1e9lZKspkE=; b=GftfjP83pQU2wzGdBNsC45DvNkfTFnGGsTxXX1qzvBtq6fiwG0mWzszl2b0FhdZquF UhMmh+JEVKTKlxBt2wsqHjpqzWXvKvqfVy5Okm1+RvluZ+wJtunsZ40vb2W6A9iniPx8 3m/fx10+FYoWZVXGjDeR7fKQkiQHiaYmW6aKxavj3eceCyKgPth9gQtPPZ/D7r6zLoIF hzUvFlSF4/1TA63P3HHvrUE+2Fynm9cc2+wSuZrOq5A440LDd+9mnGeyeQo344gOHbeM oweVkMpjYqoqcMhaVImdO4vpWcOuAaxBHKjV6sl/aL5x0KdiIhMJh1oK/AfXbf4gPoQV jeOg== X-Forwarded-Encrypted: i=1; AFNElJ8Ffa1MLBK4ELr+75dj5bh3/g7Nn729wttRMH5dQqA8w9S2T/Pjvf2omUuG7ijlUdKq8GpIE/+s606Jpn9y@postgresql.org X-Gm-Message-State: AOJu0YySkD4KdJ80q6KXJQLMsU7mKC4ThqIywI1A3/t1qIKPRTUQzqUt Cz4ErlFwDsFUrEZc0zx+dIK+j+xcVkW8n7/3raarQNrMSb6zlmyTMGz1t8NGvt4EpxQ= X-Gm-Gg: Acq92OGcA9dDhg3C3yD7zafnbw7Km+J5F/PvAQJ0NpyVLINnHlHK6oADVMLfet9Z4aj U8bZVgkUc++yLy/ZqIg8T/lYXNpG/8j5bgqRutUcUdcSt6uYe/kLTancPN0EiZfdP4fWnCGMfo8 RlSVCfLrhQyeHluIaGperC9Zy/rL8UGZmBNMe7roHBArt0JzJGAqjoZbzTYnNzNDpzBM9M+iVat pLQpQMIw9WBIeWAw+tOrVZdGy3fMp/mVIEYxw68iEDqh1sK1zNN5d1xDraMqJw8cQbAXIBYlsTD n6Ba7tdcKqWTgvwzko/dUBQDu3ubSM5smXGSRqvYrWag3P5nVuO9cZLUT0B295CPtoCTvvE9ahr yw+NsUWoGlbe1z6As6GITZcOR/66kcxPABMKAlhtHM31PPtgLRaHyW8CIaJfWmchLFGtyJIYQbb WqGypKykr1AB64EV2eJ5FNi0wON/axsy67Iajwe9AV2iS2/Cctaj0QKaQ6m7B4xyQf X-Received: by 2002:a05:6000:4a1e:b0:44a:fa76:5193 with SMTP id ffacd0b85a97d-45eb36881a0mr29663308f8f.12.1779809012881; Tue, 26 May 2026 08:23:32 -0700 (PDT) Received: from laurenz.albe-K4N0CV00F97414D ([2001:871:260:cf92:cffb:5f45:6c92:e602]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-45eb6d48e23sm35116257f8f.20.2026.05.26.08.23.32 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 26 May 2026 08:23:32 -0700 (PDT) Message-ID: <1514ac90bc374f750df34fe465caadd77121b067.camel@cybertec.at> Subject: Re: Adding a stored generated column without long-lived locks From: Laurenz Albe To: Alberto Piai , pgsql-hackers@postgresql.org Date: Tue, 26 May 2026 17:23:31 +0200 In-Reply-To: References: Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable User-Agent: Evolution 3.58.3 (3.58.3-1.fc43) MIME-Version: 1.0 List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On Thu, 2026-05-14 at 15:46 -0700, Alberto Piai wrote: > > > On Tue Mar 17, 2026 at 5:31 PM +07, Alberto Piai wrote: > > >=20 > > > > I recently needed to add a stored generated column to a table of > > > > nontrivial size, and realized that currently there is no way to do > > > > that without rewriting the table under an AccessExclusiveLock. >=20 > The attached v4 is a rebase against current master I understand the need that the patch fulfills, and I agree that it would be= a nice feature. I have a few thoughts about this that don't concern the implementation: 1) The SQL standard knows ALTER TABLE ... ADD ... GENERATED ALWAYS AS (...) and ALTER TABLE ... ALTER ... DROP EXPRESSION, but there is no provision for ALTER TABLE ... ADD GENERATED ALWAYS AS (...). So this patch adds non-standard syntax that may one day conflict with a new version of the standard. I think we can still do it, and the proposed syntax looks right, but I thought I should mention it. 2) We currently have ALTER TABLE ... ALTER ... SET EXPRESSION AS (...) to change the generation expression of a column. This command always rewrites the table, according to the documentation. I think that if the present patch adds support to skip rewriting the tab= le when a generation expression is added and the expression matches a check constraint, changing the generation expression should also be possible without a rewrite. If not, I would consider that a violation of the principle of least astonishment. Would it be difficult to extend the patch to support that? 3) We already have a couple of tricks to avoid blocking for a long time: - ALTER TABLE ... ALTER ... SET NOT NULL can skip the table scan if ther= e is a check constraint that makes sure that the column is NOT NULL - ALTER TABLE ... ATTACH PARTITION can skip the scan of the new partitio= n if there is a check constraint matching the partition constraint It would be great to document these little tricks in the documentation, probably on the ALTER TABLE page. This is not necessarily the job of this patch, but it would also not be off-topic for the patch. Comments on the patch: ---------------------- The patch applies and builds cleanly and passes the regression tests. Missing parts: - There is no documentation. At least ALTER TABLE needs a description of t= he new syntax, and would ideally mention the trick with the check constraint= . - There should be support for command line completion for the new syntax. Bugs: - The patch doesn't test if the column is an identity column: CREATE TABLE tab (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY); INSERT INTO tab VALUES (DEFAULT); ALTER TABLE tab ALTER id ADD GENERATED ALWAYS AS (1) STORED; The ALTER TABLE should fail, but doesn't. - Strange behavior with sequences owned by the column: CREATE TABLE tab (id bigserial); INSERT INTO tab VALUES (DEFAULT); ALTER TABLE tab ALTER id ADD GENERATED ALWAYS AS (1) STORED; \ds tab_id_seq List of sequences Schema | Name | Type | Owner =20 --------+------------+----------+---------- public | tab_id_seq | sequence | postgres (1 row) I think that any sequence owned by the column should be dropped. Alternatively, you could throw an error. - Incorrect handling of NULL values: CREATE TABLE tab (col1 integer, col2 integer); INSERT INTO tab VALUES (2, NULL); -- works, because NULL results from the check are accepted ALTER TABLE tab ADD CHECK (col2 =3D col1); SELECT pg_relation_filenode('tab'); pg_relation_filenode=20 ---------------------- 19920 (1 row) ALTER TABLE tab ALTER col2 ADD GENERATED ALWAYS AS (col1) STORED; SELECT pg_relation_filenode('tab'); pg_relation_filenode=20 ---------------------- 19920 (1 row) TABLE tab; col1 | col2=20 ------+------ 2 | =E2=88=85 (1 row) I am not sure what the correct approach would be. The simple approach wo= uld be to only skip the rewrite if the column has a NOT NULL constraint or an eq= uivalent check constraint, but perhaps you can think of a way to do better. Comments on the code: > --- a/src/backend/commands/tablecmds.c > +++ b/src/backend/commands/tablecmds.c > @@ -5093,6 +5102,13 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableC= md *cmd, > ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, contex= t); > pass =3D AT_PASS_SET_EXPRESSION; > break; > + case AT_AddGeneratedAsExprStored: You should add a comment, same as for the other branches. > @@ -6695,6 +6717,8 @@ alter_table_type_to_string(AlterTableType cmdtype) > return "ALTER COLUMN ... SET NOT NULL"; > case AT_SetExpression: > return "ALTER COLUMN ... SET EXPRESSION"; > + case AT_AddGeneratedAsExprStored: > + return "ALTER COLUMN ... ADD GENERATED ALWAYS AS (...) STORED= "; Keep it short, like "ALTER COLUMN ... ADD GENERATED". > --- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c > +++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c > @@ -129,6 +129,9 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS) > case AT_SetNotNull: > strtype =3D "SET NOT NULL"; > break; > + case AT_AddGeneratedAsExprStored: > + strtype =3D "ADD GENERATED ALWAYS AS (...) STORED"; > + break; I suggest "ALTER COLUMN ADD GENERATED ALWAYS AS", but I won't insist. Yours, Laurenz Albe