{"id":4337,"date":"2022-09-07T14:31:23","date_gmt":"2022-09-07T14:31:23","guid":{"rendered":"https:\/\/nenadnoveljic.com\/blog\/?p=4337"},"modified":"2022-09-07T14:31:25","modified_gmt":"2022-09-07T14:31:25","slug":"suboptimal-semi-join-and-anti-join-cardinality-estimates","status":"publish","type":"post","link":"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/","title":{"rendered":"A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates"},"content":{"rendered":"<h1>Problem definition<\/h1>\n<p>This blog post is a demonstration of how a simple change in a data model can massively improve a join cardinality estimate.<\/p>\n<p>The original execution plan consists of ~250 lines, but I reduced the minimum test case for reproducing the problem to only two tables. The first table T_SUBSET_OF_ACCOUNTS contains a subset of accounts:<\/p>\n<pre><code>create table T_SUBSET_OF_ACCOUNTS ( id number ) ;<\/code><\/pre>\n<p>The accounts can be either active or closed. The table, though, doesn&#8217;t contain that information. Instead, additional information about closed accounts is stored in the separate table T_SUBSET_OF_ACCOUNTS:<\/p>\n<pre><code>create table T_CLOSED_ACCOUNTS ( id number ) ;<\/code><\/pre>\n<p>Consequently, a semi-join is required to retrieve only closed accounts:<\/p>\n<pre><code>select * from T_SUBSET_OF_ACCOUNTS a \n  where <span style=\"color:blue\">exists<\/span> ( select 1 from T_CLOSED_ACCOUNTS c where a.id = c.id );<\/code><\/pre>\n<p>Similarly, active accounts are retrieved with an anti-join:<\/p>\n<pre><code>select * from T_SUBSET_OF_ACCOUNTS a \n  where <span style=\"color:blue\">not exists<\/span> ( select 1 from T_CLOSED_ACCOUNTS c where a.id = c.id ) ;<\/code><\/pre>\n<p>A problem arises because of the specific combination of two dataset properties.<\/p>\n<p>One is that the majority of the accounts are still active. This means that they don&#8217;t have a matching row in the table T_CLOSED_ACCOUNTS. In other words, a semi-join between both tables returns only few rows.<\/p>\n<p>Another is that the accounts have being permanently opened and closed. As a consequence, the range of ID values in T_SUBSET_OF_ACCOUNTS is contained in T_CLOSED_ACCOUNTS. Because of the overlapping ranges, optimizer thinks that the join will return many rows.<\/p>\n<p>Simply put, the overlapping ranges of the join columns lead to high join cardinality estimates, but since both tables contain only few matches, the semi-join cardinality is being massively overestimated. By the same token, the anti-join cardinality is being massively underestimated.<\/p>\n<h1>Dataset<\/h1>\n<p>It&#8217;s fairly easy to simulate the production dataset.<\/p>\n<p>The following statement inserts even numbers between 400 and 800 into T_SUBSET_OF_ACCOUNTS:<\/p>\n<pre><code>insert into T_SUBSET_OF_ACCOUNTS select level + 400 from dual \n  where <span style=\"color:blue\">mod(level, 2) = 0<\/span> connect by level &lt;= 400 ;\ncommit ;\n\nselect min(id), max(id), count(*) from T_SUBSET_OF_ACCOUNTS ;\n\n   MIN(ID)    MAX(ID)   COUNT(*)\n---------- ---------- ----------\n       402        800        200\n\nexec dbms_stats.gather_table_stats(null, 'T_SUBSET_OF_ACCOUNTS');<\/code><\/pre>\n<p>The following statement inserts odd numbers between 1 and 8000 into T_CLOSED_ACCOUNTS:<\/p>\n<pre><code>insert into T_CLOSED_ACCOUNTS select level from dual \n  where <span style=\"color:blue\">mod(level, 2) = 1<\/span> connect by level &lt;= 8000 ;\n\ncommit ;\n\nselect min(id), max(id), count(*) from T_CLOSED_ACCOUNTS ;\n\n   MIN(ID)    MAX(ID)   COUNT(*)\n---------- ---------- ----------\n         1       7999       4000\n\nexec dbms_stats.gather_table_stats(null, 'T_CLOSED_ACCOUNTS');<\/code><\/pre>\n<p>Since T_SUBSET_OF_ACCOUNTS stores only even and T_CLOSED_ACCOUNTS only odd numbers, there aren&#8217;t any matching rows. Simply put, I&#8217;m simulating the condition that the T_SUBSET_OF_ACCOUNTS table contains only active accounts.<\/p>\n<h1>Semi-join<\/h1>\n<p>But since the ID value ranges fully overlap, optimizer thinks that join will return many rows. The result is disastrous &#8211; the cardinality is overestimated by orders of magnitude:<\/p>\n<pre><code>select \/*+ gather_plan_statistics *\/ * from T_SUBSET_OF_ACCOUNTS a \n  where exists ( select 1 from T_CLOSED_ACCOUNTS c where a.id = c.id ) ;\n\nselect * from table(dbms_xplan.display_cursor(NULL,NULL, 'LAST, ALLSTATS')) ;\n\n--------------------------------------------------------------------------------------------------------------------------------\n| Id  | Operation          | Name                 | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |\n--------------------------------------------------------------------------------------------------------------------------------\n|   0 | SELECT STATEMENT   |                      |      1 |        |      0 |00:00:00.01 |      21 |       |       |          |\n|*  1 |  HASH JOIN <span style=\"color:blue\">SEMI<\/span>    |                      |      1 |    <span style=\"color:red\">200<\/span> |      0 |00:00:00.01 |      21 |  2078K|  2078K| 1526K (0)|\n|   2 |   TABLE ACCESS FULL| T_SUBSET_OF_ACCOUNTS |      1 |    200 |    200 |00:00:00.01 |       6 |       |       |          |\n|   3 |   TABLE ACCESS FULL| T_CLOSED_ACCOUNTS    |      1 |   4000 |   4000 |00:00:00.01 |      15 |       |       |          |\n--------------------------------------------------------------------------------------------------------------------------------\n\nPredicate Information (identified by operation id):\n---------------------------------------------------\n\n   1 - access(\"A\".\"ID\"=\"C\".\"ID\")<\/code><\/pre>\n<h1>Anti-join<\/h1>\n<p>For the same reason, optimizer massively underestimates the anti-join cardinality:<\/p>\n<pre><code>select \/*+ gather_plan_statistics *\/ * from T_SUBSET_OF_ACCOUNTS a \n  where not exists ( select 1 from T_CLOSED_ACCOUNTS c where a.id = c.id ) ;\n\nselect * from table(dbms_xplan.display_cursor(NULL,NULL, 'LAST, ALLSTATS')) ;\n\n--------------------------------------------------------------------------------------------------------------------------------\n| Id  | Operation          | Name                 | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |\n--------------------------------------------------------------------------------------------------------------------------------\n|   0 | SELECT STATEMENT   |                      |      1 |        |    200 |00:00:00.01 |      21 |       |       |          |\n|*  1 |  HASH JOIN <span style=\"color:blue\">ANTI<\/span>    |                      |      1 |      <span style=\"color:red\">2<\/span> |    200 |00:00:00.01 |      21 |  2546K|  2546K| 1502K (0)|\n|   2 |   TABLE ACCESS FULL| T_SUBSET_OF_ACCOUNTS |      1 |    200 |    200 |00:00:00.01 |       6 |       |       |          |\n|   3 |   TABLE ACCESS FULL| T_CLOSED_ACCOUNTS    |      1 |   4000 |   4000 |00:00:00.01 |      15 |       |       |          |\n--------------------------------------------------------------------------------------------------------------------------------\n\nPredicate Information (identified by operation id):\n---------------------------------------------------\n\n   1 - access(\"A\".\"ID\"=\"C\".\"ID\")<\/code><\/pre>\n<p>The NOT EXISTS subquery above selects only active accounts. That was exactly the problem in the production query when I first spotted the issue. The active accounts were used as the input to other operations in the execution plan, and their cardinality misestimate resulted in a bad plan.<\/p>\n<h1>Outer join to anti-join transformation<\/h1>\n<p>By the way, the application query didn&#8217;t use exactly NOT EXISTS for producing the anti-join. It was written as an outer join instead:<\/p>\n<pre><code>select a.* from T_SUBSET_OF_ACCOUNTS a \n  <span style=\"color:blue\">left outer join T_CLOSED_ACCOUNTS c<\/span> on a.id = c.id \n  <span style=\"color:blue\">where c.id is null<\/span> ;<\/code><\/pre>\n<p>Optimizer, however, produced the identical execution plan because it transformed the outer join to the anti-join:<\/p>\n<pre><code>select * from table(dbms_xplan.display_cursor(NULL,NULL, 'LAST, ALLSTATS')) ;\n\n--------------------------------------------------------------------------------------------------------------------------------\n| Id  | Operation          | Name                 | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |\n--------------------------------------------------------------------------------------------------------------------------------\n|   0 | SELECT STATEMENT   |                      |      1 |        |    200 |00:00:00.01 |      21 |       |       |          |\n|*  1 |  <span style=\"color:blue\">HASH JOIN ANTI<\/span>    |                      |      1 |      <span style=\"color:red\">2<\/span> |    200 |00:00:00.01 |      21 |  2546K|  2546K| 1486K (0)|\n|   2 |   TABLE ACCESS FULL| T_SUBSET_OF_ACCOUNTS |      1 |    200 |    200 |00:00:00.01 |       6 |       |       |          |\n|   3 |   TABLE ACCESS FULL| T_CLOSED_ACCOUNTS    |      1 |   4000 |   4000 |00:00:00.01 |      15 |       |       |          |\n--------------------------------------------------------------------------------------------------------------------------------\n\nPredicate Information (identified by operation id):\n---------------------------------------------------\n\n   1 - access(\"A\".\"ID\"=\"C\".\"ID\")<\/code><\/pre>\n<p>This transformation can be disabled by setting the undocumented parameter &#8220;_optimizer_outer_to_anti_enabled&#8221; to false:<\/p>\n<pre><code>alter session set \"_optimizer_outer_to_anti_enabled\" = false ;<\/code><\/pre>\n<pre><code>---------------------------------------------------------------------------------------------------------------------------------\n| Id  | Operation           | Name                 | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |\n---------------------------------------------------------------------------------------------------------------------------------\n|   0 | SELECT STATEMENT    |                      |      1 |        |    200 |00:00:00.01 |      21 |       |       |          |\n|*  1 |  FILTER             |                      |      1 |        |    200 |00:00:00.01 |      21 |       |       |          |\n|*  2 |   HASH JOIN <span style=\"color:blue\">OUTER<\/span>   |                      |      1 |      <span style=\"color:red\">2<\/span> |    200 |00:00:00.01 |      21 |  2546K|  2546K| 1478K (0)|\n|   3 |    TABLE ACCESS FULL| T_SUBSET_OF_ACCOUNTS |      1 |    200 |    200 |00:00:00.01 |       6 |       |       |          |\n|   4 |    TABLE ACCESS FULL| T_CLOSED_ACCOUNTS    |      1 |   4000 |   4000 |00:00:00.01 |      15 |       |       |          |\n---------------------------------------------------------------------------------------------------------------------------------\n\nPredicate Information (identified by operation id):\n---------------------------------------------------\n\n   1 - filter(\"C\".\"ID\" IS NULL)\n   2 - access(\"A\".\"ID\"=\"C\".\"ID\")<\/code><\/pre>\n<p>As we can see, the outer to anti-join transformation isn&#8217;t performed after disabling it with the hidden parameter. The cardinality estimate is, though, equally bad for the new plan.<\/p>\n<h1>Solution<\/h1>\n<p>In such cases, you can introduce the STATE column into the base table T_SUBSET_OF_ACCOUNTS and use it as a filter, instead of filtering data with semi-joins and anti-joins. Not only will the STATE column eliminate the join, but it&#8217;ll also yield a much better cardinality estimate:<\/p>\n<pre><code>alter table T_SUBSET_OF_ACCOUNTS add ( state number ) ;\n\nupdate T_SUBSET_OF_ACCOUNTS set state = 1 ;\n\ncommit ;\n\nexec dbms_stats.gather_table_stats(null, 'T_SUBSET_OF_ACCOUNTS');\n\nselect \/*+ gather_plan_statistics *\/ * from T_SUBSET_OF_ACCOUNTS \n  where state = 1 ;\n\nselect * from table(dbms_xplan.display_cursor(NULL,NULL, 'LAST, ALLSTATS')) ;\n\nThe cardinality estimate is accurate:\n\n----------------------------------------------------------------------------------------------------\n| Id  | Operation         | Name                 | Starts | E-Rows | A-Rows |   A-Time   | Buffers |\n----------------------------------------------------------------------------------------------------\n|   0 | SELECT STATEMENT  |                      |      1 |        |    200 |00:00:00.01 |       8 |\n|*  1 |  TABLE ACCESS FULL| T_SUBSET_OF_ACCOUNTS |      1 |    <span style=\"color:green\">200<\/span> |    200 |00:00:00.01 |       8 |\n----------------------------------------------------------------------------------------------------\n\nPredicate Information (identified by operation id):\n---------------------------------------------------\n\n   1 - filter(\"STATE\"=1)<\/code><\/pre>\n<h1>Summary<\/h1>\n<p>In summary, an application stores a subset of the accounts in a table. The accounts can be either active or closed. Unfortunately, this table doesn&#8217;t have a column to indicate the account state. Instead, it stores the information about closed accounts in a separate table. Therefore, it requires a semi-join to retrieve closed accounts and an anti-join to query the active accounts. Since the join values in both tables overlap, optimizer comes up with a large semi-join cardinality estimate. But since the most of the accounts are still active, and therefore not contained in the closed accounts table, the query, in reality, returns only few rows. In other words, optimizer overestimates the semi-join and underestimates the anti-join cardinality. To avoid these misestimates &#8211; and eliminate joins altogether &#8211; a STATE column can be introduced to indicate whether the account is active or closed.<\/p>\n<p>Further, we can even generalize this problem and the solution. When a semi-join returns few rows but the values of the join column overlap, the semi-join cardinality estimate will be massively overestimated. The anti-join cardinality will be underestimated in a such case. The problem can be solved by introducing a new column which indicates whether the value is contained in another table.<\/p>\n<p>This small tweak on data model can drastically improve join cardinality estimate.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Analysis of dataset properties leading to a bad performance problem with a simple fix. <a href=\"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/\" class=\"more-link\">Continue Reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[11,57,5],"tags":[],"class_list":["post-4337","post","type-post","status-publish","format-standard","hentry","category-cost-based-optimizer","category-data-modelling","category-oracle"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.4 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates - All-round Database Topics<\/title>\n<meta name=\"description\" content=\"Analysis of dataset properties leading to a bad performance problem with a simple fix.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates - All-round Database Topics\" \/>\n<meta property=\"og:description\" content=\"Analysis of dataset properties leading to a bad performance problem with a simple fix.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/\" \/>\n<meta property=\"og:site_name\" content=\"All-round Database Topics\" \/>\n<meta property=\"article:published_time\" content=\"2022-09-07T14:31:23+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2022-09-07T14:31:25+00:00\" \/>\n<meta name=\"author\" content=\"Nenad Noveljic\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@NenadNoveljic\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Nenad Noveljic\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"7 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/suboptimal-semi-join-and-anti-join-cardinality-estimates\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/suboptimal-semi-join-and-anti-join-cardinality-estimates\\\/\"},\"author\":{\"name\":\"Nenad Noveljic\",\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/#\\\/schema\\\/person\\\/51458d9dd86dbbdd19f5add451d44efa\"},\"headline\":\"A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates\",\"datePublished\":\"2022-09-07T14:31:23+00:00\",\"dateModified\":\"2022-09-07T14:31:25+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/suboptimal-semi-join-and-anti-join-cardinality-estimates\\\/\"},\"wordCount\":780,\"commentCount\":0,\"articleSection\":[\"cost based optimizer\",\"data modelling\",\"Oracle\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/suboptimal-semi-join-and-anti-join-cardinality-estimates\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/suboptimal-semi-join-and-anti-join-cardinality-estimates\\\/\",\"url\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/suboptimal-semi-join-and-anti-join-cardinality-estimates\\\/\",\"name\":\"A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates - All-round Database Topics\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/#website\"},\"datePublished\":\"2022-09-07T14:31:23+00:00\",\"dateModified\":\"2022-09-07T14:31:25+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/#\\\/schema\\\/person\\\/51458d9dd86dbbdd19f5add451d44efa\"},\"description\":\"Analysis of dataset properties leading to a bad performance problem with a simple fix.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/suboptimal-semi-join-and-anti-join-cardinality-estimates\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/suboptimal-semi-join-and-anti-join-cardinality-estimates\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/suboptimal-semi-join-and-anti-join-cardinality-estimates\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/\",\"name\":\"All-round Database Topics\",\"description\":\"Nenad Noveljic\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/#\\\/schema\\\/person\\\/51458d9dd86dbbdd19f5add451d44efa\",\"name\":\"Nenad Noveljic\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a97b796613ea48ec8a7b79c8ffe1c685dcffc920c68121f6238d5caab5070670?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a97b796613ea48ec8a7b79c8ffe1c685dcffc920c68121f6238d5caab5070670?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a97b796613ea48ec8a7b79c8ffe1c685dcffc920c68121f6238d5caab5070670?s=96&d=mm&r=g\",\"caption\":\"Nenad Noveljic\"},\"sameAs\":[\"nenad-noveljic-9b746a6\",\"https:\\\/\\\/x.com\\\/NenadNoveljic\"],\"url\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/author\\\/nenad\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates - All-round Database Topics","description":"Analysis of dataset properties leading to a bad performance problem with a simple fix.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/","og_locale":"en_US","og_type":"article","og_title":"A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates - All-round Database Topics","og_description":"Analysis of dataset properties leading to a bad performance problem with a simple fix.","og_url":"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/","og_site_name":"All-round Database Topics","article_published_time":"2022-09-07T14:31:23+00:00","article_modified_time":"2022-09-07T14:31:25+00:00","author":"Nenad Noveljic","twitter_card":"summary_large_image","twitter_creator":"@NenadNoveljic","twitter_misc":{"Written by":"Nenad Noveljic","Est. reading time":"7 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/#article","isPartOf":{"@id":"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/"},"author":{"name":"Nenad Noveljic","@id":"https:\/\/nenadnoveljic.com\/blog\/#\/schema\/person\/51458d9dd86dbbdd19f5add451d44efa"},"headline":"A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates","datePublished":"2022-09-07T14:31:23+00:00","dateModified":"2022-09-07T14:31:25+00:00","mainEntityOfPage":{"@id":"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/"},"wordCount":780,"commentCount":0,"articleSection":["cost based optimizer","data modelling","Oracle"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/","url":"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/","name":"A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates - All-round Database Topics","isPartOf":{"@id":"https:\/\/nenadnoveljic.com\/blog\/#website"},"datePublished":"2022-09-07T14:31:23+00:00","dateModified":"2022-09-07T14:31:25+00:00","author":{"@id":"https:\/\/nenadnoveljic.com\/blog\/#\/schema\/person\/51458d9dd86dbbdd19f5add451d44efa"},"description":"Analysis of dataset properties leading to a bad performance problem with a simple fix.","breadcrumb":{"@id":"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/nenadnoveljic.com\/blog\/suboptimal-semi-join-and-anti-join-cardinality-estimates\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/nenadnoveljic.com\/blog\/"},{"@type":"ListItem","position":2,"name":"A Data Model Fix for Suboptimal Semi-Join and Anti-Join Cardinality Estimates"}]},{"@type":"WebSite","@id":"https:\/\/nenadnoveljic.com\/blog\/#website","url":"https:\/\/nenadnoveljic.com\/blog\/","name":"All-round Database Topics","description":"Nenad Noveljic","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/nenadnoveljic.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/nenadnoveljic.com\/blog\/#\/schema\/person\/51458d9dd86dbbdd19f5add451d44efa","name":"Nenad Noveljic","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/a97b796613ea48ec8a7b79c8ffe1c685dcffc920c68121f6238d5caab5070670?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/a97b796613ea48ec8a7b79c8ffe1c685dcffc920c68121f6238d5caab5070670?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/a97b796613ea48ec8a7b79c8ffe1c685dcffc920c68121f6238d5caab5070670?s=96&d=mm&r=g","caption":"Nenad Noveljic"},"sameAs":["nenad-noveljic-9b746a6","https:\/\/x.com\/NenadNoveljic"],"url":"https:\/\/nenadnoveljic.com\/blog\/author\/nenad\/"}]}},"_links":{"self":[{"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/posts\/4337","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/comments?post=4337"}],"version-history":[{"count":1,"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/posts\/4337\/revisions"}],"predecessor-version":[{"id":4344,"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/posts\/4337\/revisions\/4344"}],"wp:attachment":[{"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/media?parent=4337"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/categories?post=4337"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/tags?post=4337"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}