{"id":1145,"date":"2016-11-27T18:56:30","date_gmt":"2016-11-27T18:56:30","guid":{"rendered":"http:\/\/nenadnoveljic.com\/blog\/?p=1145"},"modified":"2022-09-06T09:08:04","modified_gmt":"2022-09-06T09:08:04","slug":"tables-current-historical-rows","status":"publish","type":"post","link":"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/","title":{"rendered":"Tables Containing &#8220;Current&#8221; and &#8220;Historical&#8221; Rows"},"content":{"rendered":"<p>I&#8217;ve seen quite a few applications with tables containing both &#8220;current&#8221; and &#8220;historical&#8221; rows. In this blog post I&#8217;m going to demonstrate how devastating this pattern to&nbsp;cardinality estimates can be.<\/p>\n<p>To illustrate this point, I&#8217;m going to create a table which stores <em>product_ids<\/em> with <em>prices<\/em>:<\/p>\n<pre><code>create table prices (product_id number, price number, is_current number) ;<\/code><\/pre>\n<p><em>is_current<\/em> is 1 for current prices and 0 for the historical rows. Any given&nbsp;<em>product_id<\/em> can have only one current&nbsp;price. Consequently, the <em>product_id<\/em> is unique for <em>is_current=1<\/em>. Note that this is something that&nbsp;a human&nbsp;would know intuitively, but the optimizer misses this information completely.<\/p>\n<p>Let&#8217;s continue with generating a data set for the test case. First, I&#8217;m going to populate the table with 100000 current prices:<\/p>\n<pre><code>exec dbms_random.seed(1) ;\n\ninsert into prices \n  select rownum,trunc(dbms_random.value(100,10000),2),1 \n    from dual connect by level &lt;=100000 ; <\/code><\/pre>\n<p>Second, I&#8217;m going to insert historical records (is_current=0):<\/p>\n<pre><code>insert into prices \n  select \n    trunc(dbms_random.normal * 50000 ),\n    trunc(dbms_random.value(100,10000),2),0 \n    from dual connect by level &lt;=300000 ;<\/code><\/pre>\n<p>Finally, I&#8217;m going to add a lot of history records for a small number of products. This will lead to several popular values which will&nbsp;pop up&nbsp;in the histogram as the endpoint values:<\/p>\n<pre><code>insert into prices \n  select mod(rownum,10),trunc(dbms_random.value(100,10000),2),0\n    from dual connect by level &lt;= 100000 ; \ncommit ; \n\nbegin\n  dbms_stats.gather_table_stats( \n    user,'PRICES',method_opt=&gt;'for all columns size skewonly'\n  );\nend;\n\n<\/code><\/pre>\n<p>At the end, we&#8217;ve got a table with 500000 rows, of which are 100000 current:<\/p>\n<pre><code>select is_current,count(*) from prices group by is_current ;\nIS_CURRENT\tCOUNT(*)\n1\t        100000\n0\t        400000\n<\/code><\/pre>\n<h1>Self-Join<\/h1>\n<p>To&nbsp;select all the prices of currently active products we have to perform the following self-join:<\/p>\n<pre><code>select count(*) from prices active,prices history \n  where active.is_current = 1 and active.product_id = history.product_id ;\n\nCOUNT(*)\n332901\n<\/code><\/pre>\n<p>As we already know, there can be at most only one active price for any given product (remember, the optimizer doesn&#8217;t have this information!). Therefore, the self-join will be one-to-many. As a consequence, such a self-join can&#8217;t possibly return more rows than the total number of rows in the table. But let&#8217;s check the optimizer&#8217;s estimation:<\/p>\n<pre><code>\n--------------------------------------------------------------------------------------\n| Id  | Operation           | Name   | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |\n--------------------------------------------------------------------------------------\n|   0 | SELECT STATEMENT    |        |     1 |    13 |       |  1053  (38)| 00:00:01 |\n|   1 |  SORT AGGREGATE     |        |     1 |    13 |       |            |          |\n|*  2 |   HASH JOIN         |        |   <span style=\"color: #ff0000;\">205M<\/span>|  2551M|  1984K|  1053  (38)| 00:00:01 |\n|*  3 |    TABLE ACCESS FULL| PRICES |   100K|   781K|       |   164   (2)| 00:00:01 |\n|   4 |    TABLE ACCESS FULL| PRICES |   500K|  2441K|       |   163   (1)| 00:00:01 |\n--------------------------------------------------------------------------------------\n<\/code><\/pre>\n<p>Ouch. The optimizer estimated that the join would return 205 million rows, which is, of course, out of the ballpark.<\/p>\n<p>By running the following query it can be verified that the largest contributor to the join cardinality are the popular values:<\/p>\n<pre><code>select \n(select \n    sum(\n        endpoint_repeat_count*endpoint_repeat_count*num_rows*num_rows\/\n        ucs.sample_size\/ucs.sample_size\n    )  \n    from\n        user_tab_histograms uth\n       ,user_tab_col_statistics ucs\n       ,user_tables ut\n    where\n\t\tuth.table_name   = ucs.table_name\n\t\tand uth.table_name   = ut.table_name\n\t\tand uth.column_name   = ucs.column_name\n\t\tand uth.table_name    = 'PRICES'\n\t\tand uth.column_name   = 'PRODUCT_ID'\n\t\tand (uth.endpoint_repeat_count - ucs.sample_size\/ucs.num_buckets) &gt; 0\n)\n* (\n    select \n(\n    select samples from \n    (\n        select \n          endpoint_value,\n          (endpoint_number - nvl(lag(endpoint_number)over (order by endpoint_number asc) , 0 )) samples \n          from user_tab_histograms \n          where table_name = 'PRICES' and column_name='IS_CURRENT'\n    ) where endpoint_value=1\n) \/ ( select num_rows from user_tables where table_name='PRICES' ) ratio_current\n    from dual ) join_card_populars\n from dual ;\n\n<span style=\"color: #ff0000;\">205599507.076327<\/span>\n<\/code><\/pre>\n<p>The query above assumes that there is a frequency histogram on the column <em>prices<\/em> and a hybrid histogram for the column <em>product_id<\/em>.<\/p>\n<h1>Partitioning<\/h1>\n<p>Luckily, there is a solution to this problem which doesn&#8217;t require any changes&nbsp;to the application code. The secret weapon is partitioning the table on <em>is_current<\/em>:<\/p>\n<pre><code>\ncreate table prices_partitioned\n (product_id number, price number, is_current number)\n partition by list (is_current)\n ( partition current_p values(1), partition history values(0) ) ;\n\ninsert into prices_partitioned select * from prices ;\n\ncommit ;\n\nbegin \n  dbms_stats.gather_table_stats(\n    user,'PRICES_PARTITIONED',method_opt=&gt;'for all columns size skewonly'\n  );\nend ;\n\/  \n<\/code><\/pre>\n<p>Being able to rely on the partition statistics, the optimizer can now accurately estimate the number of &#8220;current&#8221; rows which is driving the join.<\/p>\n<pre><code>\n----------------------------------------------------------------------------------------------------------------------\n| Id  | Operation               | Name               | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     | Pstart| Pstop |\n----------------------------------------------------------------------------------------------------------------------\n|   0 | SELECT STATEMENT        |                    |     1 |    13 |       |   731   (1)| 00:00:01 |       |       |\n|   1 |  SORT AGGREGATE         |                    |     1 |    13 |       |            |          |       |       |\n|*  2 |   HASH JOIN             |                    |   <span style=\"color: #ff0000;\">271K<\/span>|  3442K|  1984K|   731   (1)| 00:00:01 |       |       |\n|   3 |    PARTITION LIST SINGLE|                    | <span style=\"color: #ff0000;\">99991<\/span> |   781K|       |   131   (1)| 00:00:01 |   KEY |   KEY |\n|   4 |     TABLE ACCESS FULL   | PRICES_PARTITIONED | 99991 |   781K|       |   131   (1)| 00:00:01 |     1 |     1 |\n|   5 |    PARTITION LIST ALL   |                    |   500K|  2441K|       |   261   (1)| 00:00:01 |     1 |     2 |\n|   6 |     TABLE ACCESS FULL   | PRICES_PARTITIONED |   500K|  2441K|       |   261   (1)| 00:00:01 |     1 |     2 |\n----------------------------------------------------------------------------------------------------------------------\n\nselect num_rows from user_tab_partitions \n  where table_name='PRICES_PARTITIONED' and partition_name='CURRENT_P';\n\nNUM_ROWS\n<span style=\"color: #ff0000;\">100000\n<\/span><\/code><\/pre>\n<p>As we can see, this solution is very effective, but it&nbsp;comes with&nbsp;a price tag. You would need to license the enterprise edition and the partitioning option.<\/p>\n<h1>Conclusion<\/h1>\n<p>If your data model forsees storing &#8220;current&#8221; and &#8220;historical&#8221; rows in the same table, there is a chance that there will be queries doing self-joins drived by the &#8220;current&#8221; rows. This model can give rise to wrong join cardinality estimates applied on skewed data distributions. The more emphasized the skew is, the larger is the error. Table partitioning turns out to be a good solution in such cases.<\/p>\n<h1>References<\/h1>\n<ul>\n<li>Alberto Dell&#8217;Era: <a href=\"http:\/\/www.adellera.it\/investigations\/join_over_histograms\/JoinOverHistograms.pdf\" target=\"_blank\" rel=\"noopener\">Join Over Histograms<\/a><\/li>\n<li>Mohamed Houri: <a href=\"http:\/\/allthingsoracle.com\/12c-hybrid-histogram\/\" target=\"_blank\" rel=\"noopener\">12c hybrid histogram<\/a><\/li>\n<\/ul>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If your data model forsees storing &#8220;current&#8221; and &#8220;historical&#8221; rows in the same table, there is a chance that there will be queries doing self-joins drived by the &#8220;current&#8221; rows. This model can give rise to wrong join cardinality estimates applied on skewed data distributions. The more emphasized the skew is, the larger is the error. Table partitioning turns out to be a good solution in such cases. <a href=\"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/\" 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":[8,11,57,5],"tags":[],"class_list":["post-1145","post","type-post","status-publish","format-standard","hentry","category-12c","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>Tables Containing &quot;Current&quot; and &quot;Historical&quot; Rows - All-round Database Topics<\/title>\n<meta name=\"description\" content=\"If your data model forsees storing &quot;current&quot; and &quot;historical&quot; rows in the same table, there is a chance that there will be queries doing self-joins drived by the &quot;current&quot; rows. This model can give rise to wrong join cardinality estimates applied on skewed data distributions. The more emphasized the skew is, the larger is the error. Table partitioning turns out to be a good solution in such cases.\" \/>\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\/tables-current-historical-rows\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Tables Containing &quot;Current&quot; and &quot;Historical&quot; Rows - All-round Database Topics\" \/>\n<meta property=\"og:description\" content=\"If your data model forsees storing &quot;current&quot; and &quot;historical&quot; rows in the same table, there is a chance that there will be queries doing self-joins drived by the &quot;current&quot; rows. This model can give rise to wrong join cardinality estimates applied on skewed data distributions. The more emphasized the skew is, the larger is the error. Table partitioning turns out to be a good solution in such cases.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/\" \/>\n<meta property=\"og:site_name\" content=\"All-round Database Topics\" \/>\n<meta property=\"article:published_time\" content=\"2016-11-27T18:56:30+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2022-09-06T09:08:04+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=\"4 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/tables-current-historical-rows\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/tables-current-historical-rows\\\/\"},\"author\":{\"name\":\"Nenad Noveljic\",\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/#\\\/schema\\\/person\\\/51458d9dd86dbbdd19f5add451d44efa\"},\"headline\":\"Tables Containing &#8220;Current&#8221; and &#8220;Historical&#8221; Rows\",\"datePublished\":\"2016-11-27T18:56:30+00:00\",\"dateModified\":\"2022-09-06T09:08:04+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/tables-current-historical-rows\\\/\"},\"wordCount\":500,\"commentCount\":0,\"articleSection\":[\"12c\",\"cost based optimizer\",\"data modelling\",\"Oracle\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/tables-current-historical-rows\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/tables-current-historical-rows\\\/\",\"url\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/tables-current-historical-rows\\\/\",\"name\":\"Tables Containing \\\"Current\\\" and \\\"Historical\\\" Rows - All-round Database Topics\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/#website\"},\"datePublished\":\"2016-11-27T18:56:30+00:00\",\"dateModified\":\"2022-09-06T09:08:04+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/#\\\/schema\\\/person\\\/51458d9dd86dbbdd19f5add451d44efa\"},\"description\":\"If your data model forsees storing \\\"current\\\" and \\\"historical\\\" rows in the same table, there is a chance that there will be queries doing self-joins drived by the \\\"current\\\" rows. This model can give rise to wrong join cardinality estimates applied on skewed data distributions. The more emphasized the skew is, the larger is the error. Table partitioning turns out to be a good solution in such cases.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/tables-current-historical-rows\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/tables-current-historical-rows\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/tables-current-historical-rows\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/nenadnoveljic.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Tables Containing &#8220;Current&#8221; and &#8220;Historical&#8221; Rows\"}]},{\"@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":"Tables Containing \"Current\" and \"Historical\" Rows - All-round Database Topics","description":"If your data model forsees storing \"current\" and \"historical\" rows in the same table, there is a chance that there will be queries doing self-joins drived by the \"current\" rows. This model can give rise to wrong join cardinality estimates applied on skewed data distributions. The more emphasized the skew is, the larger is the error. Table partitioning turns out to be a good solution in such cases.","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\/tables-current-historical-rows\/","og_locale":"en_US","og_type":"article","og_title":"Tables Containing \"Current\" and \"Historical\" Rows - All-round Database Topics","og_description":"If your data model forsees storing \"current\" and \"historical\" rows in the same table, there is a chance that there will be queries doing self-joins drived by the \"current\" rows. This model can give rise to wrong join cardinality estimates applied on skewed data distributions. The more emphasized the skew is, the larger is the error. Table partitioning turns out to be a good solution in such cases.","og_url":"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/","og_site_name":"All-round Database Topics","article_published_time":"2016-11-27T18:56:30+00:00","article_modified_time":"2022-09-06T09:08:04+00:00","author":"Nenad Noveljic","twitter_card":"summary_large_image","twitter_creator":"@NenadNoveljic","twitter_misc":{"Written by":"Nenad Noveljic","Est. reading time":"4 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/#article","isPartOf":{"@id":"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/"},"author":{"name":"Nenad Noveljic","@id":"https:\/\/nenadnoveljic.com\/blog\/#\/schema\/person\/51458d9dd86dbbdd19f5add451d44efa"},"headline":"Tables Containing &#8220;Current&#8221; and &#8220;Historical&#8221; Rows","datePublished":"2016-11-27T18:56:30+00:00","dateModified":"2022-09-06T09:08:04+00:00","mainEntityOfPage":{"@id":"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/"},"wordCount":500,"commentCount":0,"articleSection":["12c","cost based optimizer","data modelling","Oracle"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/","url":"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/","name":"Tables Containing \"Current\" and \"Historical\" Rows - All-round Database Topics","isPartOf":{"@id":"https:\/\/nenadnoveljic.com\/blog\/#website"},"datePublished":"2016-11-27T18:56:30+00:00","dateModified":"2022-09-06T09:08:04+00:00","author":{"@id":"https:\/\/nenadnoveljic.com\/blog\/#\/schema\/person\/51458d9dd86dbbdd19f5add451d44efa"},"description":"If your data model forsees storing \"current\" and \"historical\" rows in the same table, there is a chance that there will be queries doing self-joins drived by the \"current\" rows. This model can give rise to wrong join cardinality estimates applied on skewed data distributions. The more emphasized the skew is, the larger is the error. Table partitioning turns out to be a good solution in such cases.","breadcrumb":{"@id":"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/nenadnoveljic.com\/blog\/tables-current-historical-rows\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/nenadnoveljic.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Tables Containing &#8220;Current&#8221; and &#8220;Historical&#8221; Rows"}]},{"@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\/1145","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=1145"}],"version-history":[{"count":1,"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/posts\/1145\/revisions"}],"predecessor-version":[{"id":4339,"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/posts\/1145\/revisions\/4339"}],"wp:attachment":[{"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/media?parent=1145"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/categories?post=1145"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nenadnoveljic.com\/blog\/wp-json\/wp\/v2\/tags?post=1145"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}