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 1vEAut-006hFz-Pv for pgsql-general@arkaria.postgresql.org; Wed, 29 Oct 2025 18:29:03 +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 1vEAus-002tOJ-Q4 for pgsql-general@arkaria.postgresql.org; Wed, 29 Oct 2025 18:29:01 +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.94.2) (envelope-from ) id 1vEAus-002tOB-9s for pgsql-general@lists.postgresql.org; Wed, 29 Oct 2025 18:29:01 +0000 Received: from mail-ed1-x530.google.com ([2a00:1450:4864:20::530]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.96) (envelope-from ) id 1vEAuk-004QiE-31 for pgsql-general@lists.postgresql.org; Wed, 29 Oct 2025 18:29:00 +0000 Received: by mail-ed1-x530.google.com with SMTP id 4fb4d7f45d1cf-63c0c9a408aso156370a12.3 for ; Wed, 29 Oct 2025 11:28:54 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1761762532; x=1762367332; darn=lists.postgresql.org; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=ivNAG1kA+XA3ekrvgrv65nVYESe0UW28CPQ+QLteoKs=; b=Bj+Mj2xgEYO/54nz9IARzoPoOryDH5WiovgsulByxdPrJRJpgymUOQwA+o9PA94x7x weXT5y/bBXkS8734cQ9KU3QGx5pXDHaFddYbZB3GcVnohKX8iuYqdgn7m3NlXkdtLcCF liZ7k3lsVLwo39LvtMfNa/RcSCs5az1C9cCwVbotDx3CqzsT0Xx9k6qpWmQ6AjjMMexr H0BNYL8hjZTUrEhC/ZnyT1Pp5mX4C+FcbMmtsV9HjJvZd9G1E7L5qdV4iXa0bQAuEcjP mey/MOhFTohgDwEjd7GwB3fxN8eXI1b5aOBVP5+p9AVoKZIKXLJqLcL5ZAF7aWi0P20V wT6g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1761762532; x=1762367332; h=to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=ivNAG1kA+XA3ekrvgrv65nVYESe0UW28CPQ+QLteoKs=; b=o3L+ejsftloG4xmi3SRh/wJfKRkmVglUN2x3mFOa9LQFh2WI5ReNe5VVwxjJoiQJAz +tDkf9CcDsCtra5uUtp4Xd9v1U0xemsFAJrC8KblxOOm2cnAMjJcCCsjR/MAHPx5QSBZ FlGvHpeeL1IZKkS9ixT3EneAwKkrZN61IPmFbfjAUvzbQLPuvnrsngfJHUm2fNkXGnKF XmVWjqQxg6CYRWPk9e50jDk5kT1A+NM3i6NSv87V9TsBKLmuD2M9EMKIY1MHOiwwq7ua xiArTEB9gj5sem3p8lccDLDTOXu6sVfKvKuAEDHHWVwKENofCDLMyBsTSBBldKUxkB8J skog== X-Gm-Message-State: AOJu0Yzq/NjcDtQY/7yqPCzHcWibB6KZRjaz3h3o5MB1Z4izcbbi5TFZ z6fx0VgHuLmbH9OTMGdB6ZLb1Ypuroxgb8gdkvA+ZVSEiEC0xzmNpIUnCsRRPZpzWtsuFv1TUdS Kvu3VHDR40y0r5UvFy/UyfaCCYsFysaXoNH9wkqo= X-Gm-Gg: ASbGncuR1t1QrmEn5ik4lyyEU4JMJWwEggFQFFbP8uSkRlVGKNZFFZAj3rVsjAEpBeF 3pbP8d3CvHSw3LRkN65ekBoIQSg6+ZTcTHB0qzHaIsI1c/SR8kPgUw7n0hBfAfmWeWzBZNrYWQU ukSYeNNCmCr9BqTx0Cf4Hkmy/pA6N9+qVlDpl6mkijwYVCaR0Y8LrDoZufXdkwvGJIaR+sqO8D9 1KNwAK2oMUc2HS2BqizWfS8bSJg2KMLA1w3jc0tvDDJo1RLZ0Fk1surp5uBP3V8yP8FNGGW X-Google-Smtp-Source: AGHT+IGziZ0bYg+IzqjWNsTyAzBcapAiij27m+6Kyj1b3tvIuWiUxp2TfwsYnDr1c39SxUQwdbzg0UbwbxA0midI3Ls= X-Received: by 2002:a05:6402:42c1:b0:63c:103b:e1cf with SMTP id 4fb4d7f45d1cf-6406179551amr218217a12.0.1761762531883; Wed, 29 Oct 2025 11:28:51 -0700 (PDT) MIME-Version: 1.0 From: Jacob Jackson Date: Wed, 29 Oct 2025 14:28:40 -0400 X-Gm-Features: AWmQ_bmagMcGigs2N7qjmsPtAZJPHSYe5HD4Jqenh5af1tndbWtKKUMHCTK9tqQ Message-ID: Subject: Postgres memoizing inner loop of join when outer loop join key is guaranteed unique? To: pgsql-general@lists.postgresql.org Content-Type: multipart/alternative; boundary="000000000000e43cde0642504db4" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --000000000000e43cde0642504db4 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Hello. I was looking at some query plans recently and noticed something that didn't make sense. I have a query that joins a table of questions with results for each question (using a table with a composite primary key of question id and a user id), filtered by user id. The question IDs and the combined question-userIds are guaranteed unique due to being primary keys, and yet Postgres still memoizes the inner loop results. Any ideas why? Is this just a failure of the query planner (I would be happy to explore creating a PR), did I not properly guarantee uniqueness, or is there another reason for memoization? The memoized version is consistently slightly slower in my testing, despite the calculated cost being lower. Here are the query plans: enable_memoize=3Don: EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM questions LEFT JOIN "QuestionUserStatus" ON questions.id =3D "QuestionUserStatus".question WHER= E "QuestionUserStatus".user =3D 0; Nested Loop (cost=3D0.71..514.09 rows=3D277 width=3D1381) (actual time=3D0.021..0.520 rows=3D231 loops=3D1) =E2=94=9C Buffers: shared hit=3D859 =E2=94=9C Index Scan using "QuestionUserStatus_user_question_pk" on "QuestionUserStatus" (cost=3D0.42..178.88 rows=3D277 width=3D18) (actual time=3D0.014..0.114 rows=3D231 loops=3D1) =E2=94=82 =E2=94=9C Index Cond: ("user" =3D '0'::bigint) =E2=94=82 =E2=94=94 Buffers: shared hit=3D166 =E2=94=94 Memoize (cost=3D0.29..1.25 rows=3D1 width=3D1363) (actual time= =3D0.001..0.001 rows=3D1 loops=3D231) =E2=94=9C Cache Key: "QuestionUserStatus".question =E2=94=9C Cache Mode: logical =E2=94=9C Hits: 0 Misses: 231 Evictions: 0 Overflows: 0 Memory Usage:= 320kB =E2=94=9C Buffers: shared hit=3D693 =E2=94=94 Index Scan using questions_pkey on questions (cost=3D0.28..1.2= 4 rows=3D1 width=3D1363) (actual time=3D0.001..0.001 rows=3D1 loops=3D231) =E2=94=9C Index Cond: (id =3D "QuestionUserStatus".question) =E2=94=94 Buffers: shared hit=3D693 Planning: =E2=94=94 Buffers: shared hit=3D6 Planning Time: 0.183 ms Execution Time: 0.548 ms enable_memoize=3Doff: Nested Loop (cost=3D0.70..521.98 rows=3D277 width=3D1381) (actual time=3D0.018..0.421 rows=3D231 loops=3D1) =E2=94=9C Buffers: shared hit=3D859 =E2=94=9C Index Scan using "QuestionUserStatus_user_question_pk" on "QuestionUserStatus" (cost=3D0.42..178.88 rows=3D277 width=3D18) (actual time=3D0.014..0.099 rows=3D231 loops=3D1) =E2=94=82 =E2=94=9C Index Cond: ("user" =3D '3477145805513'::bigint) =E2=94=82 =E2=94=94 Buffers: shared hit=3D166 =E2=94=94 Index Scan using questions_pkey on questions (cost=3D0.28..1.24 = rows=3D1 width=3D1363) (actual time=3D0.001..0.001 rows=3D1 loops=3D231) =E2=94=9C Index Cond: (id =3D "QuestionUserStatus".question) =E2=94=94 Buffers: shared hit=3D693 Planning: =E2=94=94 Buffers: shared hit=3D6 Planning Time: 0.197 ms Execution Time: 0.444 ms Thanks for the help, Jacob --000000000000e43cde0642504db4 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Hello. I was looking at some query=C2=A0plans recently and= noticed something that didn't make sense. I have a query that joins a = table of questions with results for each question (using a table with a com= posite primary key of question id and a user id), filtered by user id. The = question IDs and the combined question-userIds are guaranteed unique due to= being primary keys, and yet Postgres still memoizes the inner loop results= . Any ideas why? Is this just a failure of the query planner (I would be ha= ppy to explore creating a PR), did I not properly=C2=A0guarantee uniqueness= , or is there another reason for memoization? The memoized=C2=A0version is = consistently slightly slower in my testing, despite the calculated=C2=A0cos= t being lower. Here are the query plans:

enable_memoize=3Don:
EXPLAIN (ANALYZE, BUFFERS) SEL= ECT * FROM questions LEFT JOIN "QuestionUserStatus" ON questions.id =3D "QuestionUserStatus".q= uestion WHERE "QuestionUserStatus".user =3D 0;
<= font face=3D"monospace">
Nest= ed Loop =C2=A0(cost=3D0.71..514.09 rows=3D277 width=3D1381) (actual time=3D= 0.021..0.520 rows=3D231 loops=3D1)
=E2=94=9C Buffers: shared hit=3D859=E2=94=9C Index Scan using "QuestionUserStatus_user_question_pk"= ; on "QuestionUserStatus" =C2=A0(cost=3D0.42..178.88 rows=3D277 w= idth=3D18) (actual time=3D0.014..0.114 rows=3D231 loops=3D1)
=E2=94=82 = =E2=94=9C Index Cond: ("user" =3D '0'::bigint)
=E2=94= =82 =E2=94=94 Buffers: shared hit=3D166
=E2=94=94 Memoize =C2=A0(cost=3D= 0.29..1.25 rows=3D1 width=3D1363) (actual time=3D0.001..0.001 rows=3D1 loop= s=3D231)
=C2=A0 =E2=94=9C Cache Key: "QuestionUserStatus".ques= tion
=C2=A0 =E2=94=9C Cache Mode: logical
=C2=A0 =E2=94=9C Hits: 0 = =C2=A0Misses: 231 =C2=A0Evictions: 0 =C2=A0Overflows: 0 =C2=A0Memory Usage:= 320kB
=C2=A0 =E2=94=9C Buffers: shared hit=3D693
=C2=A0 =E2=94=94 In= dex Scan using questions_pkey on questions =C2=A0(cost=3D0.28..1.24 rows=3D= 1 width=3D1363) (actual time=3D0.001..0.001 rows=3D1 loops=3D231)
=C2=A0= =C2=A0 =E2=94=9C Index Cond: (id =3D "QuestionUserStatus".questi= on)
=C2=A0 =C2=A0 =E2=94=94 Buffers: shared hit=3D693
Planning:
= =E2=94=94 Buffers: shared hit=3D6
Planning Time: 0.183 ms
Execution T= ime: 0.548 ms

enable_memoize=3Doff:

Nested Loop= =C2=A0(cost=3D0.70..521.98 rows=3D277 width=3D1381) (actual time=3D0.018..= 0.421 rows=3D231 loops=3D1)
=E2=94=9C Buffers: shared hit=3D859
=E2= =94=9C Index Scan using "QuestionUserStatus_user_question_pk" on = "QuestionUserStatus" =C2=A0(cost=3D0.42..178.88 rows=3D277 width= =3D18) (actual time=3D0.014..0.099 rows=3D231 loops=3D1)
=E2=94=82 =E2= =94=9C Index Cond: ("user" =3D '3477145805513'::bigint)=E2=94=82 =E2=94=94 Buffers: shared hit=3D166
=E2=94=94 Index Scan usi= ng questions_pkey on questions =C2=A0(cost=3D0.28..1.24 rows=3D1 width=3D13= 63) (actual time=3D0.001..0.001 rows=3D1 loops=3D231)
=C2=A0 =E2=94=9C I= ndex Cond: (id =3D "QuestionUserStatus".question)
=C2=A0 =E2= =94=94 Buffers: shared hit=3D693
Planning:
=E2=94=94 Buffers: shared = hit=3D6
Planning Time: 0.197 ms
Execution Time: 0.444 ms
=

Thanks for the help,
Jacob
--000000000000e43cde0642504db4--