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 1sjzwa-006Wva-BK for pgsql-general@arkaria.postgresql.org; Fri, 30 Aug 2024 11:37:32 +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 1sjzwY-002XJS-FJ for pgsql-general@arkaria.postgresql.org; Fri, 30 Aug 2024 11:37:31 +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 1sjrhh-00CTBl-0K for pgsql-general@lists.postgresql.org; Fri, 30 Aug 2024 02:49:37 +0000 Received: from mail-ua1-x930.google.com ([2607:f8b0:4864:20::930]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.94.2) (envelope-from ) id 1sjrhZ-0026sj-Ly for pgsql-general@lists.postgresql.org; Fri, 30 Aug 2024 02:49:35 +0000 Received: by mail-ua1-x930.google.com with SMTP id a1e0cc1a2514c-846bc787856so82040241.0 for ; Thu, 29 Aug 2024 19:49:28 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=instrumentl.com; s=google; t=1724986168; x=1725590968; darn=lists.postgresql.org; h=cc:to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=RSNx35Hju1oMgrd2x1GBQV1K20SSIPv4G3aUAVFTXKs=; b=E/ywDL2i4f9BWdqdoULh36SeIjlZS6uBvfjlWZW1xtsF/PrWJw622Q+1vzYCu1jUqG s8W5r1HIiilGZTvtCkenJCcUuzvaOWi4YP6wCHI25qIaGeJ6+QB6zmfU6N7uV6IDhtnI othtrxsd/ZtUQ7XLzguHtEF6ocjDz29h+3PNUWliu5hCTwxeGQCEJsHGjiX1xS4BLobX +Lcw+OYagbu75aESMvnKaAkkGpI97Nw7BWaHTDJvwuDFqsYX4Hqmg+cq7icznk3dlD8+ zWIAs6zxrFqVRcwW0kYQAbAOYEUvYCwgE2O6qJPpZdfUWl+5DfkFiWlENn1S4kFsYYj4 ADFA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1724986168; x=1725590968; h=cc:to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=RSNx35Hju1oMgrd2x1GBQV1K20SSIPv4G3aUAVFTXKs=; b=Na0ToxbqOFROoChcTWDf6bbC8WnDaJo8/bTVFVewXfnm/FkyE+D3MlGog57Dty/jRb bZuR+RiRAD0l4iAq6rYc9ks+zkcpfVy8jWhy6RC2D4kZFF457QjNdANeYDOOK/tJ0wb/ f21Ed2UJWlq3TpT/xPbH4zEQuqg2yt4F+34xAsT0klQw8DZp+ktVtHKLr8jKGFJ+1Lod haEW/xqM0Rok8OCq+XbzkTmezsDYHBJmNZ6cpHrFp4TekRvXCKHTmKpONAwcgX4p5igk TJCCHeY/v/NRhdakK8dKAlBx51D9NaF2hfv/TKESLZU12ObMOQGMddsN8p2237jhWBVc JNKw== X-Gm-Message-State: AOJu0YyH3+LqfyuVE5/HwmGqDpRnBG5cfw8msfA9BWap7Rc0GT3j6CN4 R7tyT/c2j4QmZULxOEt5MYXz0pRblMlM6KZZXKWCkbbvptpTUlbSVgjDoH5M8tl4koE2yWaRDuH H9lXeIofODTb7lvX/lZQAZFHKMlTOe5u24fMraLblg3Lnze// X-Google-Smtp-Source: AGHT+IHwcsVrpgRruHTrw41naZzE9NIb7HcGS9E1s2Poi8R/HbjaNHsn+B3K19kMcvsUn2PEp57sap2PkdrfLQNJyCc= X-Received: by 2002:a05:6102:3714:b0:49a:1dd2:3cc8 with SMTP id ada2fe7eead31-49a5b19dd7emr5815082137.24.1724986167787; Thu, 29 Aug 2024 19:49:27 -0700 (PDT) Received: from 1064022179695 named unknown by gmailapi.google.com with HTTPREST; Thu, 29 Aug 2024 19:49:27 -0700 Received: from 1064022179695 named unknown by gmailapi.google.com with HTTPREST; Thu, 29 Aug 2024 19:49:24 -0700 MIME-Version: 1.0 (Mimestream 1.3.8) From: James Brown Date: Thu, 29 Aug 2024 19:49:27 -0700 Message-ID: Subject: optimizing a join against a windowed function To: pgsql-general@lists.postgresql.org Cc: Gabriel Sinkin Content-Type: multipart/alternative; boundary="000000000000c621360620dda304" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --000000000000c621360620dda304 Content-Type: text/plain; charset="UTF-8" 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 other 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 = 1 ) latest_insights ON latest_insights.taxpayer_id = 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=0.86..53.30 rows=1 width=16) -> Index Only Scan using taxpayers_pkey on taxpayers (cost=0.43..8.45 rows=1 width=8) Index Cond: (id = 650974) -> Subquery Scan on ordered_insights (cost=0.43..44.83 rows=1 width=16) Filter: (ordered_insights.rank = 1) -> WindowAgg (cost=0.43..44.71 rows=10 width=28) Run Condition: (rank() OVER (?) <= 1) -> Index Scan using index_insights_on_taxpayer_id_year_desc on insights (cost=0.43..44.53 rows=10 width=20) Index Cond: ((taxpayer_id = 650974) AND (year IS NOT 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 is not great: Hash Join (cost=2611586.97..2800201.15 rows=1 width=16) Hash Cond: (ordered_insights.taxpayer_id = taxpayers.id) -> Subquery Scan on ordered_insights (cost=2611570.10..2799818.65 rows=28961 width=16) Filter: (ordered_insights.rank = 1) -> WindowAgg (cost=2611570.10..2727415.36 rows=5792263 width=28) Run Condition: (rank() OVER (?) <= 1) -> Sort (cost=2611570.10..2626050.76 rows=5792263 width=20) Sort Key: insights.taxpayer_id, insights.year DESC -> Seq Scan on insights (cost=0.00..1723354.01 rows=5792263 width=20) Filter: (year IS NOT NULL) -> Hash (cost=16.85..16.85 rows=2 width=8) -> Index Only Scan using taxpayers_pkey on taxpayers (cost=0.43..16.85 rows=2 width=8) Index Cond: (id = 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 = 1 ) latest_insights ON latest_insights.taxpayer_id = taxpayers.id WHERE taxpayers.id IN (650974, 243848) AND latest_insights.taxpayer_id IN (650974, 243848); QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------- Nested Loop (cost=0.86..110.57 rows=1 width=16) Join Filter: (taxpayers.id = ordered_insights.taxpayer_id) -> Subquery Scan on ordered_insights (cost=0.43..93.69 rows=1 width=16) Filter: (ordered_insights.rank = 1) -> WindowAgg (cost=0.43..93.42 rows=21 width=28) Run Condition: (rank() OVER (?) <= 1) -> Index Scan using index_insights_on_taxpayer_id_year_desc on insights (cost=0.43..93.06 rows=21 width=20) Index Cond: ((taxpayer_id = ANY ('{650974,243848}'::bigint[])) AND (year IS NOT NULL)) -> Index Only Scan using taxpayers_pkey on taxpayers (cost=0.43..16.85 rows=2 width=8) Index Cond: (id = 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 (so 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 --000000000000c621360620dda304 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: base64 PGh0bWw+PGJvZHk+PGRpdiBkaXI9Imx0ciI+SGVsbG86PGJyPjxicj5JJiMzOTttIGF0dGVtcHRp bmcgdG8gZmlndXJlIG91dCB3aGV0aGVyIGFuIG9wdGltaXplciBiZWhhdmlvciBJJiMzOTttIHNl ZWluZyBpcyBhIFBvc3RncmVTUUzCoGJ1ZyBvciBleHBlY3RlZCBiZWhhdmlvci4gVGhlIHNjZW5h cmlvOjxicj48YnI+SSBoYXZlIHR3byB0YWJsZXM6IG9uZSBuYW1lZMKgdGF4cGF5ZXJzwqB3aGlj aCBoYXMgYSBnb29kaXNoIG51bWJlciBvZiBjb2x1bW5zIGFuIGFuIGludGVnZXIgUEvCoGlkLCBh bmQgb25lIG5hbWVkwqBpbnNpZ2h0cyzCoHdoaWNoIGhhcyBhwqB0YXhwYXllcl9pZMKgZm9yZWln biBrZXkgdG8gdGF4cGF5ZXJzLCBhwqB5ZWFyLMKgYW5kIChhZ2FpbikgYSBsb3Qgb2Ygb3RoZXIg Y29sdW1ucy4gVGhlcmUmIzM5O3MgYW4gaW5kZXggb27CoGluc2lnaHRzICh0YXhwYXllcl9pZCwg eWVhciBERVNDKS7CoEkmIzM5O20gZXhlY3V0aW5nIHRoZSBmb2xsb3dpbmcgU1FMOjxicj48YnI+ YGBgPGJyPlNFTEVDVCA8YSBocmVmPSJodHRwOi8vdGF4cGF5ZXJzLmlkIj50YXhwYXllcnMuaWQ8 L2E+LCBpbnNpZ2h0X2lkPGJyPsKgwqDCoEZST00gdGF4cGF5ZXJzPGJyPsKgwqDCoEpPSU4gKDxi cj7CoMKgwqDCoMKgwqDCoFdJVEggb3JkZXJlZF9pbnNpZ2h0cyBBUyAoPGJyPsKgwqDCoMKgwqDC oMKgwqDCoMKgwqBTRUxFQ1QgdGF4cGF5ZXJfaWQsIGlkLCBSQU5LKCkgT1ZFUiAoUEFSVElUSU9O IEJZIHRheHBheWVyX2lkIE9SREVSIEJZIHllYXIgREVTQykgQVMgcmFuazxicj7CoMKgwqDCoMKg wqDCoMKgwqDCoMKgRlJPTSBpbnNpZ2h0czxicj7CoMKgwqDCoMKgwqDCoMKgwqDCoMKgV0hFUkUg eWVhciBJUyBOT1QgTlVMTDxicj7CoMKgwqDCoMKgwqDCoCk8YnI+wqDCoMKgwqDCoMKgwqBTRUxF Q1QgdGF4cGF5ZXJfaWQsIGlkIEFTIGluc2lnaHRfaWQ8YnI+wqDCoMKgwqDCoMKgwqBGUk9NIG9y ZGVyZWRfaW5zaWdodHM8YnI+wqDCoMKgwqDCoMKgwqBXSEVSRSByYW5rID0gMTxicj7CoMKgwqAp IGxhdGVzdF9pbnNpZ2h0cyBPTiBsYXRlc3RfaW5zaWdodHMudGF4cGF5ZXJfaWQgPSA8YSBocmVm PSJodHRwOi8vdGF4cGF5ZXJzLmlkIj50YXhwYXllcnMuaWQ8L2E+PGJyPldIRVJFIDxhIGhyZWY9 Imh0dHA6Ly90YXhwYXllcnMuaWQiPnRheHBheWVycy5pZDwvYT4gSU4gKD8sID8pPGJyPmBgYDxi cj48YnI+KHRoaXMgaXMgc2ltcGxpZmllZCBleGFtcGxlOyB0aGUgcmVhbCBjb2RlIGhhcyB0aGUg c3Vic2VsZWN0IGluIGEgdmlldyBzbyB0aGF0IGl0IGNhbiBleGVjdXRlIHRoaXMga2luZCBvZiBq b2luIGZyb20gYW4gT1JNOyBpdCBhbHNvIGpvaW5zIHF1aXRlIGEgZmV3IHRhYmxlcyBkb3duc3Ry ZWFtIGFmdGVyIHRoaXMpPGJyPjxicj5JZiB0aGVyZSYjMzk7cyBvbmx5IGEgc2luZ2xlIHZhbHVl IGluIHRoZSBJTiBjbGF1c2UsIHRoZSBFWFBMQUlOIHBsYW4gbG9va3MgZ3JlYXQ6PGJyPjxicj5O ZXN0ZWQgTG9vcCDCoChjb3N0PTAuODYuLjUzLjMwIHJvd3M9MSB3aWR0aD0xNik8YnI+wqDCoC0m Z3Q7IMKgSW5kZXggT25seSBTY2FuIHVzaW5nIHRheHBheWVyc19wa2V5IG9uIHRheHBheWVycyDC oChjb3N0PTAuNDMuLjguNDUgcm93cz0xIHdpZHRoPTgpPGJyPsKgwqDCoMKgwqDCoMKgwqBJbmRl eCBDb25kOiAoaWQgPSA2NTA5NzQpPGJyPsKgwqAtJmd0OyDCoFN1YnF1ZXJ5IFNjYW4gb24gb3Jk ZXJlZF9pbnNpZ2h0cyDCoChjb3N0PTAuNDMuLjQ0LjgzIHJvd3M9MSB3aWR0aD0xNik8YnI+wqDC oMKgwqDCoMKgwqDCoEZpbHRlcjogKG9yZGVyZWRfaW5zaWdodHMucmFuayA9IDEpPGJyPsKgwqDC oMKgwqDCoMKgwqAtJmd0OyDCoFdpbmRvd0FnZyDCoChjb3N0PTAuNDMuLjQ0LjcxIHJvd3M9MTAg d2lkdGg9MjgpPGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqBSdW4gQ29uZGl0aW9uOiAo cmFuaygpIE9WRVIgKD8pICZsdDs9IDEpPGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqAt Jmd0OyDCoEluZGV4IFNjYW4gdXNpbmcgaW5kZXhfaW5zaWdodHNfb25fdGF4cGF5ZXJfaWRfeWVh cl9kZXNjIG9uIGluc2lnaHRzIMKgKGNvc3Q9MC40My4uNDQuNTMgcm93cz0xMCB3aWR0aD0yMCk8 YnI+wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoEluZGV4IENvbmQ6ICgo dGF4cGF5ZXJfaWQgPSA2NTA5NzQpIEFORCAoeWVhciBJUyBOT1QgTlVMTCkpPGJyPig5IHJvd3Mp PGJyPjxicj5Ib3dldmVyLCBpZiB0aGVyZSBhcmUgbXVsdGlwbGUgcm93cyBpbiB0aGUgSU4gY2xh dXNlLCB0aGUgb3B0aW1pemVyIGRlY2lkZXMgdG8gZXhlY3V0ZSB0aGUgc3Vic2VsZWN0IGFnYWlu c3QgdGhlIGVudGlyZSBnaWFudCB0YWJsZSwgYW5kIGl0IGlzIG5vdCBncmVhdDo8YnI+PGJyPkhh c2ggSm9pbiDCoChjb3N0PTI2MTE1ODYuOTcuLjI4MDAyMDEuMTUgcm93cz0xIHdpZHRoPTE2KTxi cj7CoMKgSGFzaCBDb25kOiAob3JkZXJlZF9pbnNpZ2h0cy50YXhwYXllcl9pZCA9IDxhIGhyZWY9 Imh0dHA6Ly90YXhwYXllcnMuaWQiPnRheHBheWVycy5pZDwvYT4pPGJyPsKgwqAtJmd0OyDCoFN1 YnF1ZXJ5IFNjYW4gb24gb3JkZXJlZF9pbnNpZ2h0cyDCoChjb3N0PTI2MTE1NzAuMTAuLjI3OTk4 MTguNjUgcm93cz0yODk2MSB3aWR0aD0xNik8YnI+wqDCoMKgwqDCoMKgwqDCoEZpbHRlcjogKG9y ZGVyZWRfaW5zaWdodHMucmFuayA9IDEpPGJyPsKgwqDCoMKgwqDCoMKgwqAtJmd0OyDCoFdpbmRv d0FnZyDCoChjb3N0PTI2MTE1NzAuMTAuLjI3Mjc0MTUuMzYgcm93cz01NzkyMjYzIHdpZHRoPTI4 KTxicj7CoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgUnVuIENvbmRpdGlvbjogKHJhbmsoKSBP VkVSICg/KSAmbHQ7PSAxKTxicj7CoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgLSZndDsgwqBT b3J0IMKgKGNvc3Q9MjYxMTU3MC4xMC4uMjYyNjA1MC43NiByb3dzPTU3OTIyNjMgd2lkdGg9MjAp PGJyPsKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqBTb3J0IEtleTogaW5z aWdodHMudGF4cGF5ZXJfaWQsIGluc2lnaHRzLnllYXIgREVTQzxicj7CoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgLSZndDsgwqBTZXEgU2NhbiBvbiBpbnNpZ2h0cyDCoChj b3N0PTAuMDAuLjE3MjMzNTQuMDEgcm93cz01NzkyMjYzIHdpZHRoPTIwKTxicj7CoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgRmlsdGVyOiAoeWVhciBJ UyBOT1QgTlVMTCk8YnI+wqDCoC0mZ3Q7IMKgSGFzaCDCoChjb3N0PTE2Ljg1Li4xNi44NSByb3dz PTIgd2lkdGg9OCk8YnI+wqDCoMKgwqDCoMKgwqDCoC0mZ3Q7IMKgSW5kZXggT25seSBTY2FuIHVz aW5nIHRheHBheWVyc19wa2V5IG9uIHRheHBheWVycyDCoChjb3N0PTAuNDMuLjE2Ljg1IHJvd3M9 MiB3aWR0aD04KTxicj7CoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgSW5kZXggQ29uZDogKGlk ID0gQU5ZICgmIzM5O3s2NTA5NzQsMjQzODQ4fSYjMzk7OjpiaWdpbnRbXSkpPGJyPjxicj5JZiBJ IGFkZCBpbiBhIHNlY29uZCByZXBldGl0aXZlIFdIRVJFIGNsYXVzZSwgaXQgZ29lcyBiYWNrIHRv IGJlaW5nIGhhcHB5LCBidXQgdGhhdCBmZWVscyBhIGJpdCBsaWtlIGEgaGFjazo8YnI+PGJyPiMg RVhQTEFJTiBTRUxFQ1QgPGEgaHJlZj0iaHR0cDovL3RheHBheWVycy5pZCI+dGF4cGF5ZXJzLmlk PC9hPiwgaW5zaWdodF9pZDxicj7CoMKgwqBGUk9NIHRheHBheWVyczxicj7CoMKgwqBKT0lOICg8 YnI+wqDCoMKgwqDCoMKgwqBXSVRIIG9yZGVyZWRfaW5zaWdodHMgQVMgKDxicj7CoMKgwqDCoMKg wqDCoMKgwqDCoMKgU0VMRUNUIHRheHBheWVyX2lkLCBpZCwgUkFOSygpIE9WRVIgKFBBUlRJVElP TiBCWSB0YXhwYXllcl9pZCBPUkRFUiBCWSB5ZWFyIERFU0MpIEFTIHJhbms8YnI+wqDCoMKgwqDC oMKgwqDCoMKgwqDCoEZST00gaW5zaWdodHM8YnI+wqDCoMKgwqDCoMKgwqDCoMKgwqDCoFdIRVJF IHllYXIgSVMgTk9UIE5VTEw8YnI+wqDCoMKgwqDCoMKgwqApPGJyPsKgwqDCoMKgwqDCoMKgU0VM RUNUIHRheHBheWVyX2lkLCBpZCBBUyBpbnNpZ2h0X2lkPGJyPsKgwqDCoMKgwqDCoMKgRlJPTSBv cmRlcmVkX2luc2lnaHRzPGJyPsKgwqDCoMKgwqDCoMKgV0hFUkUgcmFuayA9IDE8YnI+wqDCoMKg KSBsYXRlc3RfaW5zaWdodHMgT04gbGF0ZXN0X2luc2lnaHRzLnRheHBheWVyX2lkID0gPGEgaHJl Zj0iaHR0cDovL3RheHBheWVycy5pZCI+dGF4cGF5ZXJzLmlkPC9hPjxicj5XSEVSRSA8YSBocmVm PSJodHRwOi8vdGF4cGF5ZXJzLmlkIj50YXhwYXllcnMuaWQ8L2E+IElOICg2NTA5NzQsIDI0Mzg0 OCkgQU5EIGxhdGVzdF9pbnNpZ2h0cy50YXhwYXllcl9pZCBJTiAoNjUwOTc0LCAyNDM4NDgpOzxi cj7CoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg UVVFUlkgUExBTiDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDC oMKgwqDCoMKgwqA8YnI+LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS08YnI+TmVzdGVkIExvb3AgwqAoY29zdD0wLjg2Li4x MTAuNTcgcm93cz0xIHdpZHRoPTE2KTxicj7CoMKgSm9pbiBGaWx0ZXI6ICg8YSBocmVmPSJodHRw Oi8vdGF4cGF5ZXJzLmlkIj50YXhwYXllcnMuaWQ8L2E+ID0gb3JkZXJlZF9pbnNpZ2h0cy50YXhw YXllcl9pZCk8YnI+wqDCoC0mZ3Q7IMKgU3VicXVlcnkgU2NhbiBvbiBvcmRlcmVkX2luc2lnaHRz IMKgKGNvc3Q9MC40My4uOTMuNjkgcm93cz0xIHdpZHRoPTE2KTxicj7CoMKgwqDCoMKgwqDCoMKg RmlsdGVyOiAob3JkZXJlZF9pbnNpZ2h0cy5yYW5rID0gMSk8YnI+wqDCoMKgwqDCoMKgwqDCoC0m Z3Q7IMKgV2luZG93QWdnIMKgKGNvc3Q9MC40My4uOTMuNDIgcm93cz0yMSB3aWR0aD0yOCk8YnI+ wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoFJ1biBDb25kaXRpb246IChyYW5rKCkgT1ZFUiAo PykgJmx0Oz0gMSk8YnI+wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoC0mZ3Q7IMKgSW5kZXgg U2NhbiB1c2luZyBpbmRleF9pbnNpZ2h0c19vbl90YXhwYXllcl9pZF95ZWFyX2Rlc2Mgb24gaW5z aWdodHMgwqAoY29zdD0wLjQzLi45My4wNiByb3dzPTIxIHdpZHRoPTIwKTxicj7CoMKgwqDCoMKg wqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgwqDCoMKgSW5kZXggQ29uZDogKCh0YXhwYXllcl9pZCA9 IEFOWSAoJiMzOTt7NjUwOTc0LDI0Mzg0OH0mIzM5Ozo6YmlnaW50W10pKSBBTkQgKHllYXIgSVMg Tk9UIE5VTEwpKTxicj7CoMKgLSZndDsgwqBJbmRleCBPbmx5IFNjYW4gdXNpbmcgdGF4cGF5ZXJz X3BrZXkgb24gdGF4cGF5ZXJzIMKgKGNvc3Q9MC40My4uMTYuODUgcm93cz0yIHdpZHRoPTgpPGJy PsKgwqDCoMKgwqDCoMKgwqBJbmRleCBDb25kOiAoaWQgPSBBTlkgKCYjMzk7ezY1MDk3NCwyNDM4 NDh9JiMzOTs6OmJpZ2ludFtdKSk8YnI+PGJyPlRoaXMgZmVlbHMgbGlrZSBhIGJ1ZyB0byBtZSwg YnV0IG1heWJlIEkmIzM5O20gbWlzc2luZyBzb21ldGhpbmcgb2J2aW91cy4gSSBkb24mIzM5O3Qg cmVhbGx5IGdldCB3aHkgdGhlIG9wdGltaXplciB3b3VsZG4mIzM5O3QgYmUgYWJsZSB0byBpbmZl ciB0aGUgc2Vjb25kIGNvbmRpdGlvbiBnaXZlbiB0aGF0IEkmIzM5O20gZG9pbmcgYSBqb2luIG9u IGEgbm9uLW51bGxhYmxlIGludGVnZXIgY29sdW1uIChzbyB0aGVyZSYjMzk7cyBubyBOYU4gbm9u c2Vuc2UgdG8gd29ycnkgYWJvdXQpLCBidXQgbWF5YmUgSSYjMzk7bSBtaXNzaW5nIHNvbWV0aGlu ZyBvYnZpb3VzLjxicj48YnI+SSYjMzk7dmUgcmVwcm9kdWNlZCB0aGlzIG9uIFBvc3RncmVTUUwg MTUuNyBhbmQgMTdiZXRhMy48YnI+PGJyPlRoYW5rcyBmb3IgYW55IGluc2lnaHRzIHkmIzM5O2Fs bCBjYW4gcHJvdmlkZSE8YnI+PGJyPi0twqA8YnI+SmFtZXMgQnJvd248YnI+PC9kaXY+PC9ib2R5 PjwvaHRtbD4NCg== --000000000000c621360620dda304--