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 1sk2q6-0074b7-0p for pgsql-general@arkaria.postgresql.org; Fri, 30 Aug 2024 14:43:02 +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 1sk2q4-005YxB-79 for pgsql-general@arkaria.postgresql.org; Fri, 30 Aug 2024 14:43:00 +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 1sk2q3-005Ywz-Mp for pgsql-general@lists.postgresql.org; Fri, 30 Aug 2024 14:43:00 +0000 Received: from mail-lf1-x12c.google.com ([2a00:1450:4864:20::12c]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.94.2) (envelope-from ) id 1sk2q0-002GP2-AC for pgsql-general@lists.postgresql.org; Fri, 30 Aug 2024 14:42:59 +0000 Received: by mail-lf1-x12c.google.com with SMTP id 2adb3069b0e04-5343617fdddso3441032e87.0 for ; Fri, 30 Aug 2024 07:42:57 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1725028976; x=1725633776; darn=lists.postgresql.org; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=e3hbXDdx4S9gn2FdKj0bZaWBPDM3jTgcpI9+Rc4UeGU=; b=Yp+htp4Um1FTfwC8SkShsaSm5MhJGTACJRJNxLF5jWrEIzkRG3sDyOsh1SZF5YnIqj 2iPGwBziu03CXcA/oeWBf1vBX1kNaPRtnSw1CiYlOVSJOkPjf1VWTYKdWhJ0E7nwR0Nc pqy0lWR7jePdDdjt/CjgEQiZnboLqk6Ly00XYZ9Ri8Mk2CmVJLz1T8y546as24EAhTUi ZS/WdJ99kfGmDjCSTk6hi8+Jtq99z1Ca9K1X1HrZbgl85ci8cdcPgjq81ZRMn0dev4Az NMAutDKhP0RYxKvANTrelaOOuaL+Du45uezhgJUrH4jE95Aa9wpLs/PNHVMnxlflatLx dJuA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1725028976; x=1725633776; h=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=e3hbXDdx4S9gn2FdKj0bZaWBPDM3jTgcpI9+Rc4UeGU=; b=iJk2CWYru+bXithu9enqcKnF3u6kITZ0BM9x5m0uy87w310vlkNoGNARwz+zPGQtJd zir2c/dcb+omHw1g4RlLGY5/31sm3DrAiymKBT/sUiweK75o7la1tVSiOhPhqaDyA00D Mo5bKneOr+SgDIxkkBvk63EAzGFXtGcka7T0wFGtMXnj83QGaAO1dW95QRagHTjQ4/YW Tq2RK7+iXEShaFb5bkVJHT1bns1cUMCJ9nVH4xHtUu8N3Ju5u5bZi02LEfjd9G8JUV1J AO7f960Jx1TNtE8JCjMztnr61Xf/gsTecXq4fi1e6DPH3wcWr3g+PwaAoun4Z5XNcxsp B9WA== X-Gm-Message-State: AOJu0YyT1CJ4tfVVCOqX3kTAP10lzlCPM9hejUDs69Y9+hEZYwAUQsN+ OlfF7fz9mIcfm3aC19V5dxqFxnVa0gAD2X53HPEEDnw68j9iYyDsrivX9wDuqP++dZciH6IVv7J HXnk57c8shlLTYmTu++OgivYnjQ== X-Google-Smtp-Source: AGHT+IFj4GqzF+rRcC64i/P43cpq9TDCMg1yMEnsI9LqDx+JYFKwylITgm6OOEHuIcZZpZfbds4IrFs0nE/nPdaHjvM= X-Received: by 2002:a05:6512:3e20:b0:529:b718:8d00 with SMTP id 2adb3069b0e04-53546b1957amr2411089e87.8.1725028975549; Fri, 30 Aug 2024 07:42:55 -0700 (PDT) MIME-Version: 1.0 References: In-Reply-To: From: =?UTF-8?Q?Torsten_F=C3=B6rtsch?= Date: Fri, 30 Aug 2024 16:42:43 +0200 Message-ID: Subject: Re: optimizing a join against a windowed function To: James Brown Cc: pgsql-general@lists.postgresql.org, Gabriel Sinkin Content-Type: multipart/alternative; boundary="00000000000050a7bd0620e79b78" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --00000000000050a7bd0620e79b78 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Try perhaps something along these lines: ``` SELECT t.id, i.insight_id FROM taxpayers AS t JOIN LATERAL ( SELECT x.id AS insight_id FROM insights AS x WHERE x.taxpayer_id =3D t.id AND x.year IS NOT NULL ORDER BY year DESC LIMIT 1 ) AS i ON true WHERE t.id IN (?, ?) ``` If you don't have millions of ? in that IN clause, then that might be faster. On Fri, Aug 30, 2024 at 1:36=E2=80=AFPM James Brown = wrote: > Hello: > > I'm attempting to figure out whether an optimizer behavior I'm seeing is = a > PostgreSQL bug or expected behavior. The scenario: > > I have two tables: one named taxpayers which has a goodish number of > columns an an integer PK id, and one named insights, which has > a taxpayer_id foreign key to taxpayers, a year, and (again) a lot of othe= r > columns. There's an index on insights (taxpayer_id, year DESC). I'm > executing the following SQL: > > ``` > SELECT taxpayers.id, insight_id > FROM taxpayers > JOIN ( > WITH ordered_insights AS ( > SELECT taxpayer_id, id, RANK() OVER (PARTITION BY taxpayer_id > ORDER BY year DESC) AS rank > FROM insights > WHERE year IS NOT NULL > ) > SELECT taxpayer_id, id AS insight_id > FROM ordered_insights > WHERE rank =3D 1 > ) latest_insights ON latest_insights.taxpayer_id =3D taxpayers.id > WHERE taxpayers.id IN (?, ?) > ``` > > (this is simplified example; the real code has the subselect in a view so > that it can execute this kind of join from an ORM; it also joins quite a > few tables downstream after this) > > If there's only a single value in the IN clause, the EXPLAIN plan looks > great: > > Nested Loop (cost=3D0.86..53.30 rows=3D1 width=3D16) > -> Index Only Scan using taxpayers_pkey on taxpayers (cost=3D0.43..8.= 45 > rows=3D1 width=3D8) > Index Cond: (id =3D 650974) > -> Subquery Scan on ordered_insights (cost=3D0.43..44.83 rows=3D1 wid= th=3D16) > Filter: (ordered_insights.rank =3D 1) > -> WindowAgg (cost=3D0.43..44.71 rows=3D10 width=3D28) > Run Condition: (rank() OVER (?) <=3D 1) > -> Index Scan using index_insights_on_taxpayer_id_year_des= c > on insights (cost=3D0.43..44.53 rows=3D10 width=3D20) > Index Cond: ((taxpayer_id =3D 650974) AND (year IS NO= T > NULL)) > (9 rows) > > However, if there are multiple rows in the IN clause, the optimizer > decides to execute the subselect against the entire giant table, and it i= s > not great: > > Hash Join (cost=3D2611586.97..2800201.15 rows=3D1 width=3D16) > Hash Cond: (ordered_insights.taxpayer_id =3D taxpayers.id) > -> Subquery Scan on ordered_insights (cost=3D2611570.10..2799818.65 > rows=3D28961 width=3D16) > Filter: (ordered_insights.rank =3D 1) > -> WindowAgg (cost=3D2611570.10..2727415.36 rows=3D5792263 widt= h=3D28) > Run Condition: (rank() OVER (?) <=3D 1) > -> Sort (cost=3D2611570.10..2626050.76 rows=3D5792263 wid= th=3D20) > Sort Key: insights.taxpayer_id, insights.year DESC > -> Seq Scan on insights (cost=3D0.00..1723354.01 > rows=3D5792263 width=3D20) > Filter: (year IS NOT NULL) > -> Hash (cost=3D16.85..16.85 rows=3D2 width=3D8) > -> Index Only Scan using taxpayers_pkey on taxpayers > (cost=3D0.43..16.85 rows=3D2 width=3D8) > Index Cond: (id =3D ANY ('{650974,243848}'::bigint[])) > > If I add in a second repetitive WHERE clause, it goes back to being happy= , > but that feels a bit like a hack: > > # EXPLAIN SELECT taxpayers.id, insight_id > FROM taxpayers > JOIN ( > WITH ordered_insights AS ( > SELECT taxpayer_id, id, RANK() OVER (PARTITION BY taxpayer_id > ORDER BY year DESC) AS rank > FROM insights > WHERE year IS NOT NULL > ) > SELECT taxpayer_id, id AS insight_id > FROM ordered_insights > WHERE rank =3D 1 > ) latest_insights ON latest_insights.taxpayer_id =3D taxpayers.id > WHERE taxpayers.id IN (650974, 243848) AND latest_insights.taxpayer_id IN > (650974, 243848); > QUERY PLAN > > > -------------------------------------------------------------------------= ---------------------------------------------------- > Nested Loop (cost=3D0.86..110.57 rows=3D1 width=3D16) > Join Filter: (taxpayers.id =3D ordered_insights.taxpayer_id) > -> Subquery Scan on ordered_insights (cost=3D0.43..93.69 rows=3D1 wid= th=3D16) > Filter: (ordered_insights.rank =3D 1) > -> WindowAgg (cost=3D0.43..93.42 rows=3D21 width=3D28) > Run Condition: (rank() OVER (?) <=3D 1) > -> Index Scan using index_insights_on_taxpayer_id_year_des= c > on insights (cost=3D0.43..93.06 rows=3D21 width=3D20) > Index Cond: ((taxpayer_id =3D ANY > ('{650974,243848}'::bigint[])) AND (year IS NOT NULL)) > -> Index Only Scan using taxpayers_pkey on taxpayers (cost=3D0.43..16= .85 > rows=3D2 width=3D8) > Index Cond: (id =3D ANY ('{650974,243848}'::bigint[])) > > This feels like a bug to me, but maybe I'm missing something obvious. I > don't really get why the optimizer wouldn't be able to infer the second > condition given that I'm doing a join on a non-nullable integer column (s= o > there's no NaN nonsense to worry about), but maybe I'm missing something > obvious. > > I've reproduced this on PostgreSQL 15.7 and 17beta3. > > Thanks for any insights y'all can provide! > > -- > James Brown > --00000000000050a7bd0620e79b78 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: base64 PGRpdiBkaXI9Imx0ciI+PGRpdiBkaXI9Imx0ciI+VHJ5IHBlcmhhcHMgc29tZXRoaW5nIGFsb25n IHRoZXNlIGxpbmVzOjxkaXY+PGJyPjwvZGl2PjxkaXY+YGBgPC9kaXY+PGRpdj5TRUxFQ1QgPGEg aHJlZj0iaHR0cDovL3QuaWQiPnQuaWQ8L2E+LCBpLmluc2lnaHRfaWQ8YnI+wqAgRlJPTSB0YXhw YXllcnMgQVMgdDxicj7CoCBKT0lOIExBVEVSQUwgKDxicj7CoCDCoCDCoCDCoCDCoCDCoFNFTEVD VCA8YSBocmVmPSJodHRwOi8veC5pZCI+eC5pZDwvYT4gQVMgaW5zaWdodF9pZDxicj7CoCDCoCDC oCDCoCDCoCDCoCDCoEZST00gaW5zaWdodHMgQVMgeDxicj7CoCDCoCDCoCDCoCDCoCDCoCBXSEVS RSB4LnRheHBheWVyX2lkID0gPGEgaHJlZj0iaHR0cDovL3QuaWQiPnQuaWQ8L2E+PGJyPsKgIMKg IMKgIMKgIMKgIMKgIMKgIEFORCB4LnllYXIgSVMgTk9UIE5VTEw8YnI+wqAgwqAgwqAgwqAgwqAg wqAgT1JERVIgQlkgeWVhciBERVNDPGJyPsKgIMKgIMKgIMKgIMKgIMKgIExJTUlUIDE8YnI+wqAg wqAgwqAgwqApIEFTIGkgT04gdHJ1ZTxicj7CoFdIRVJFIDxhIGhyZWY9Imh0dHA6Ly90LmlkIj50 LmlkPC9hPiBJTiAoPywgPyk8YnI+PC9kaXY+PGRpdj5gYGA8L2Rpdj48ZGl2Pjxicj48L2Rpdj48 ZGl2PklmIHlvdSBkb24mIzM5O3QgaGF2ZSBtaWxsaW9ucyBvZiA/IGluIHRoYXQgSU4gY2xhdXNl LCB0aGVuIHRoYXQgbWlnaHQgYmUgZmFzdGVyLjwvZGl2PjwvZGl2PjwvZGl2Pjxicj48ZGl2IGNs YXNzPSJnbWFpbF9xdW90ZSI+PGRpdiBkaXI9Imx0ciIgY2xhc3M9ImdtYWlsX2F0dHIiPk9uIEZy aSwgQXVnIDMwLCAyMDI0IGF0IDE6MzbigK9QTSBKYW1lcyBCcm93biAmbHQ7PGEgaHJlZj0ibWFp bHRvOmphbWVzQGluc3RydW1lbnRsLmNvbSI+amFtZXNAaW5zdHJ1bWVudGwuY29tPC9hPiZndDsg d3JvdGU6PGJyPjwvZGl2PjxibG9ja3F1b3RlIGNsYXNzPSJnbWFpbF9xdW90ZSIgc3R5bGU9Im1h cmdpbjowcHggMHB4IDBweCAwLjhleDtib3JkZXItbGVmdDoxcHggc29saWQgcmdiKDIwNCwyMDQs MjA0KTtwYWRkaW5nLWxlZnQ6MWV4Ij48ZGl2PjxkaXYgZGlyPSJsdHIiPkhlbGxvOjxicj48YnI+ SSYjMzk7bSBhdHRlbXB0aW5nIHRvIGZpZ3VyZSBvdXQgd2hldGhlciBhbiBvcHRpbWl6ZXIgYmVo YXZpb3IgSSYjMzk7bSBzZWVpbmcgaXMgYSBQb3N0Z3JlU1FMwqBidWcgb3IgZXhwZWN0ZWQgYmVo YXZpb3IuIFRoZSBzY2VuYXJpbzo8YnI+PGJyPkkgaGF2ZSB0d28gdGFibGVzOiBvbmUgbmFtZWTC oHRheHBheWVyc8Kgd2hpY2ggaGFzIGEgZ29vZGlzaCBudW1iZXIgb2YgY29sdW1ucyBhbiBhbiBp bnRlZ2VyIFBLwqBpZCwgYW5kIG9uZSBuYW1lZMKgaW5zaWdodHMswqB3aGljaCBoYXMgYcKgdGF4 cGF5ZXJfaWTCoGZvcmVpZ24ga2V5IHRvIHRheHBheWVycywgYcKgeWVhcizCoGFuZCAoYWdhaW4p IGEgbG90IG9mIG90aGVyIGNvbHVtbnMuIFRoZXJlJiMzOTtzIGFuIGluZGV4IG9uwqBpbnNpZ2h0 cyAodGF4cGF5ZXJfaWQsIHllYXIgREVTQykuwqBJJiMzOTttIGV4ZWN1dGluZyB0aGUgZm9sbG93 aW5nIFNRTDo8YnI+PGJyPmBgYDxicj5TRUxFQ1QgPGEgaHJlZj0iaHR0cDovL3RheHBheWVycy5p ZCIgdGFyZ2V0PSJfYmxhbmsiPnRheHBheWVycy5pZDwvYT4sIGluc2lnaHRfaWQ8YnI+wqDCoMKg RlJPTSB0YXhwYXllcnM8YnI+wqDCoMKgSk9JTiAoPGJyPsKgwqDCoMKgwqDCoMKgV0lUSCBvcmRl cmVkX2luc2lnaHRzIEFTICg8YnI+wqDCoMKgwqDCoMKgwqDCoMKgwqDCoFNFTEVDVCB0YXhwYXll cl9pZCwgaWQsIFJBTksoKSBPVkVSIChQQVJUSVRJT04gQlkgdGF4cGF5ZXJfaWQgT1JERVIgQlkg eWVhciBERVNDKSBBUyByYW5rPGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKgwqBGUk9NIGluc2lnaHRz PGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKgwqBXSEVSRSB5ZWFyIElTIE5PVCBOVUxMPGJyPsKgwqDC oMKgwqDCoMKgKTxicj7CoMKgwqDCoMKgwqDCoFNFTEVDVCB0YXhwYXllcl9pZCwgaWQgQVMgaW5z aWdodF9pZDxicj7CoMKgwqDCoMKgwqDCoEZST00gb3JkZXJlZF9pbnNpZ2h0czxicj7CoMKgwqDC oMKgwqDCoFdIRVJFIHJhbmsgPSAxPGJyPsKgwqDCoCkgbGF0ZXN0X2luc2lnaHRzIE9OIGxhdGVz dF9pbnNpZ2h0cy50YXhwYXllcl9pZCA9IDxhIGhyZWY9Imh0dHA6Ly90YXhwYXllcnMuaWQiIHRh cmdldD0iX2JsYW5rIj50YXhwYXllcnMuaWQ8L2E+PGJyPldIRVJFIDxhIGhyZWY9Imh0dHA6Ly90 YXhwYXllcnMuaWQiIHRhcmdldD0iX2JsYW5rIj50YXhwYXllcnMuaWQ8L2E+IElOICg/LCA/KTxi cj5gYGA8YnI+PGJyPih0aGlzIGlzIHNpbXBsaWZpZWQgZXhhbXBsZTsgdGhlIHJlYWwgY29kZSBo YXMgdGhlIHN1YnNlbGVjdCBpbiBhIHZpZXcgc28gdGhhdCBpdCBjYW4gZXhlY3V0ZSB0aGlzIGtp bmQgb2Ygam9pbiBmcm9tIGFuIE9STTsgaXQgYWxzbyBqb2lucyBxdWl0ZSBhIGZldyB0YWJsZXMg ZG93bnN0cmVhbSBhZnRlciB0aGlzKTxicj48YnI+SWYgdGhlcmUmIzM5O3Mgb25seSBhIHNpbmds ZSB2YWx1ZSBpbiB0aGUgSU4gY2xhdXNlLCB0aGUgRVhQTEFJTiBwbGFuIGxvb2tzIGdyZWF0Ojxi cj48YnI+TmVzdGVkIExvb3AgwqAoY29zdD0wLjg2Li41My4zMCByb3dzPTEgd2lkdGg9MTYpPGJy PsKgwqAtJmd0OyDCoEluZGV4IE9ubHkgU2NhbiB1c2luZyB0YXhwYXllcnNfcGtleSBvbiB0YXhw YXllcnMgwqAoY29zdD0wLjQzLi44LjQ1IHJvd3M9MSB3aWR0aD04KTxicj7CoMKgwqDCoMKgwqDC oMKgSW5kZXggQ29uZDogKGlkID0gNjUwOTc0KTxicj7CoMKgLSZndDsgwqBTdWJxdWVyeSBTY2Fu IG9uIG9yZGVyZWRfaW5zaWdodHMgwqAoY29zdD0wLjQzLi40NC44MyByb3dzPTEgd2lkdGg9MTYp PGJyPsKgwqDCoMKgwqDCoMKgwqBGaWx0ZXI6IChvcmRlcmVkX2luc2lnaHRzLnJhbmsgPSAxKTxi cj7CoMKgwqDCoMKgwqDCoMKgLSZndDsgwqBXaW5kb3dBZ2cgwqAoY29zdD0wLjQzLi40NC43MSBy b3dzPTEwIHdpZHRoPTI4KTxicj7CoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgUnVuIENvbmRp dGlvbjogKHJhbmsoKSBPVkVSICg/KSAmbHQ7PSAxKTxicj7CoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgLSZndDsgwqBJbmRleCBTY2FuIHVzaW5nIGluZGV4X2luc2lnaHRzX29uX3RheHBheWVy X2lkX3llYXJfZGVzYyBvbiBpbnNpZ2h0cyDCoChjb3N0PTAuNDMuLjQ0LjUzIHJvd3M9MTAgd2lk dGg9MjApPGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqBJbmRleCBD b25kOiAoKHRheHBheWVyX2lkID0gNjUwOTc0KSBBTkQgKHllYXIgSVMgTk9UIE5VTEwpKTxicj4o OSByb3dzKTxicj48YnI+SG93ZXZlciwgaWYgdGhlcmUgYXJlIG11bHRpcGxlIHJvd3MgaW4gdGhl IElOIGNsYXVzZSwgdGhlIG9wdGltaXplciBkZWNpZGVzIHRvIGV4ZWN1dGUgdGhlIHN1YnNlbGVj dCBhZ2FpbnN0IHRoZSBlbnRpcmUgZ2lhbnQgdGFibGUsIGFuZCBpdCBpcyBub3QgZ3JlYXQ6PGJy Pjxicj5IYXNoIEpvaW4gwqAoY29zdD0yNjExNTg2Ljk3Li4yODAwMjAxLjE1IHJvd3M9MSB3aWR0 aD0xNik8YnI+wqDCoEhhc2ggQ29uZDogKG9yZGVyZWRfaW5zaWdodHMudGF4cGF5ZXJfaWQgPSA8 YSBocmVmPSJodHRwOi8vdGF4cGF5ZXJzLmlkIiB0YXJnZXQ9Il9ibGFuayI+dGF4cGF5ZXJzLmlk PC9hPik8YnI+wqDCoC0mZ3Q7IMKgU3VicXVlcnkgU2NhbiBvbiBvcmRlcmVkX2luc2lnaHRzIMKg KGNvc3Q9MjYxMTU3MC4xMC4uMjc5OTgxOC42NSByb3dzPTI4OTYxIHdpZHRoPTE2KTxicj7CoMKg wqDCoMKgwqDCoMKgRmlsdGVyOiAob3JkZXJlZF9pbnNpZ2h0cy5yYW5rID0gMSk8YnI+wqDCoMKg wqDCoMKgwqDCoC0mZ3Q7IMKgV2luZG93QWdnIMKgKGNvc3Q9MjYxMTU3MC4xMC4uMjcyNzQxNS4z NiByb3dzPTU3OTIyNjMgd2lkdGg9MjgpPGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqBS dW4gQ29uZGl0aW9uOiAocmFuaygpIE9WRVIgKD8pICZsdDs9IDEpPGJyPsKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqAtJmd0OyDCoFNvcnQgwqAoY29zdD0yNjExNTcwLjEwLi4yNjI2MDUwLjc2 IHJvd3M9NTc5MjI2MyB3aWR0aD0yMCk8YnI+wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoFNvcnQgS2V5OiBpbnNpZ2h0cy50YXhwYXllcl9pZCwgaW5zaWdodHMueWVhciBE RVNDPGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAtJmd0OyDCoFNl cSBTY2FuIG9uIGluc2lnaHRzIMKgKGNvc3Q9MC4wMC4uMTcyMzM1NC4wMSByb3dzPTU3OTIyNjMg d2lkdGg9MjApPGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqBGaWx0ZXI6ICh5ZWFyIElTIE5PVCBOVUxMKTxicj7CoMKgLSZndDsgwqBIYXNoIMKg KGNvc3Q9MTYuODUuLjE2Ljg1IHJvd3M9MiB3aWR0aD04KTxicj7CoMKgwqDCoMKgwqDCoMKgLSZn dDsgwqBJbmRleCBPbmx5IFNjYW4gdXNpbmcgdGF4cGF5ZXJzX3BrZXkgb24gdGF4cGF5ZXJzIMKg KGNvc3Q9MC40My4uMTYuODUgcm93cz0yIHdpZHRoPTgpPGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqBJbmRleCBDb25kOiAoaWQgPSBBTlkgKCYjMzk7ezY1MDk3NCwyNDM4NDh9JiMzOTs6 OmJpZ2ludFtdKSk8YnI+PGJyPklmIEkgYWRkIGluIGEgc2Vjb25kIHJlcGV0aXRpdmUgV0hFUkUg Y2xhdXNlLCBpdCBnb2VzIGJhY2sgdG8gYmVpbmcgaGFwcHksIGJ1dCB0aGF0IGZlZWxzIGEgYml0 IGxpa2UgYSBoYWNrOjxicj48YnI+IyBFWFBMQUlOIFNFTEVDVCA8YSBocmVmPSJodHRwOi8vdGF4 cGF5ZXJzLmlkIiB0YXJnZXQ9Il9ibGFuayI+dGF4cGF5ZXJzLmlkPC9hPiwgaW5zaWdodF9pZDxi cj7CoMKgwqBGUk9NIHRheHBheWVyczxicj7CoMKgwqBKT0lOICg8YnI+wqDCoMKgwqDCoMKgwqBX SVRIIG9yZGVyZWRfaW5zaWdodHMgQVMgKDxicj7CoMKgwqDCoMKgwqDCoMKgwqDCoMKgU0VMRUNU IHRheHBheWVyX2lkLCBpZCwgUkFOSygpIE9WRVIgKFBBUlRJVElPTiBCWSB0YXhwYXllcl9pZCBP UkRFUiBCWSB5ZWFyIERFU0MpIEFTIHJhbms8YnI+wqDCoMKgwqDCoMKgwqDCoMKgwqDCoEZST00g aW5zaWdodHM8YnI+wqDCoMKgwqDCoMKgwqDCoMKgwqDCoFdIRVJFIHllYXIgSVMgTk9UIE5VTEw8 YnI+wqDCoMKgwqDCoMKgwqApPGJyPsKgwqDCoMKgwqDCoMKgU0VMRUNUIHRheHBheWVyX2lkLCBp ZCBBUyBpbnNpZ2h0X2lkPGJyPsKgwqDCoMKgwqDCoMKgRlJPTSBvcmRlcmVkX2luc2lnaHRzPGJy PsKgwqDCoMKgwqDCoMKgV0hFUkUgcmFuayA9IDE8YnI+wqDCoMKgKSBsYXRlc3RfaW5zaWdodHMg T04gbGF0ZXN0X2luc2lnaHRzLnRheHBheWVyX2lkID0gPGEgaHJlZj0iaHR0cDovL3RheHBheWVy cy5pZCIgdGFyZ2V0PSJfYmxhbmsiPnRheHBheWVycy5pZDwvYT48YnI+V0hFUkUgPGEgaHJlZj0i aHR0cDovL3RheHBheWVycy5pZCIgdGFyZ2V0PSJfYmxhbmsiPnRheHBheWVycy5pZDwvYT4gSU4g KDY1MDk3NCwgMjQzODQ4KSBBTkQgbGF0ZXN0X2luc2lnaHRzLnRheHBheWVyX2lkIElOICg2NTA5 NzQsIDI0Mzg0OCk7PGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqBRVUVSWSBQTEFOIMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoDxicj4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTxicj5OZXN0ZWQgTG9vcCDC oChjb3N0PTAuODYuLjExMC41NyByb3dzPTEgd2lkdGg9MTYpPGJyPsKgwqBKb2luIEZpbHRlcjog KDxhIGhyZWY9Imh0dHA6Ly90YXhwYXllcnMuaWQiIHRhcmdldD0iX2JsYW5rIj50YXhwYXllcnMu aWQ8L2E+ID0gb3JkZXJlZF9pbnNpZ2h0cy50YXhwYXllcl9pZCk8YnI+wqDCoC0mZ3Q7IMKgU3Vi cXVlcnkgU2NhbiBvbiBvcmRlcmVkX2luc2lnaHRzIMKgKGNvc3Q9MC40My4uOTMuNjkgcm93cz0x IHdpZHRoPTE2KTxicj7CoMKgwqDCoMKgwqDCoMKgRmlsdGVyOiAob3JkZXJlZF9pbnNpZ2h0cy5y YW5rID0gMSk8YnI+wqDCoMKgwqDCoMKgwqDCoC0mZ3Q7IMKgV2luZG93QWdnIMKgKGNvc3Q9MC40 My4uOTMuNDIgcm93cz0yMSB3aWR0aD0yOCk8YnI+wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oFJ1biBDb25kaXRpb246IChyYW5rKCkgT1ZFUiAoPykgJmx0Oz0gMSk8YnI+wqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoC0mZ3Q7IMKgSW5kZXggU2NhbiB1c2luZyBpbmRleF9pbnNpZ2h0c19v bl90YXhwYXllcl9pZF95ZWFyX2Rlc2Mgb24gaW5zaWdodHMgwqAoY29zdD0wLjQzLi45My4wNiBy b3dzPTIxIHdpZHRoPTIwKTxicj7CoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgSW5kZXggQ29uZDogKCh0YXhwYXllcl9pZCA9IEFOWSAoJiMzOTt7NjUwOTc0LDI0Mzg0OH0m IzM5Ozo6YmlnaW50W10pKSBBTkQgKHllYXIgSVMgTk9UIE5VTEwpKTxicj7CoMKgLSZndDsgwqBJ bmRleCBPbmx5IFNjYW4gdXNpbmcgdGF4cGF5ZXJzX3BrZXkgb24gdGF4cGF5ZXJzIMKgKGNvc3Q9 MC40My4uMTYuODUgcm93cz0yIHdpZHRoPTgpPGJyPsKgwqDCoMKgwqDCoMKgwqBJbmRleCBDb25k OiAoaWQgPSBBTlkgKCYjMzk7ezY1MDk3NCwyNDM4NDh9JiMzOTs6OmJpZ2ludFtdKSk8YnI+PGJy PlRoaXMgZmVlbHMgbGlrZSBhIGJ1ZyB0byBtZSwgYnV0IG1heWJlIEkmIzM5O20gbWlzc2luZyBz b21ldGhpbmcgb2J2aW91cy4gSSBkb24mIzM5O3QgcmVhbGx5IGdldCB3aHkgdGhlIG9wdGltaXpl ciB3b3VsZG4mIzM5O3QgYmUgYWJsZSB0byBpbmZlciB0aGUgc2Vjb25kIGNvbmRpdGlvbiBnaXZl biB0aGF0IEkmIzM5O20gZG9pbmcgYSBqb2luIG9uIGEgbm9uLW51bGxhYmxlIGludGVnZXIgY29s dW1uIChzbyB0aGVyZSYjMzk7cyBubyBOYU4gbm9uc2Vuc2UgdG8gd29ycnkgYWJvdXQpLCBidXQg bWF5YmUgSSYjMzk7bSBtaXNzaW5nIHNvbWV0aGluZyBvYnZpb3VzLjxicj48YnI+SSYjMzk7dmUg cmVwcm9kdWNlZCB0aGlzIG9uIFBvc3RncmVTUUwgMTUuNyBhbmQgMTdiZXRhMy48YnI+PGJyPlRo YW5rcyBmb3IgYW55IGluc2lnaHRzIHkmIzM5O2FsbCBjYW4gcHJvdmlkZSE8YnI+PGJyPi0twqA8 YnI+SmFtZXMgQnJvd248YnI+PC9kaXY+PC9kaXY+DQo8L2Jsb2NrcXVvdGU+PC9kaXY+DQo= --00000000000050a7bd0620e79b78--