
{"id":5173,"date":"2024-12-04T00:56:12","date_gmt":"2024-12-04T05:56:12","guid":{"rendered":"https:\/\/ikriv.com\/blog\/?p=5173"},"modified":"2024-12-04T00:59:17","modified_gmt":"2024-12-04T05:59:17","slug":"join-on-more-than-just-ids","status":"publish","type":"post","link":"https:\/\/ikriv.com\/blog\/?p=5173","title":{"rendered":"JOIN on more than just IDs"},"content":{"rendered":"<p>When we write a JOIN statement in SQL, it usually goes like this<\/p>\n<p><code>SELECT {columns} FROM parent LEFT JOIN child ON child.parent_id = parent.id<\/code><\/p>\n<p>Does it ever make sense for the <code>ON<\/code> condition to be more elaborate? The answer is a definite <b>YES<\/b> as demonstrated by this example.<\/p>\n<p>If we have a parent &rarr; child relationship, and we want to left-join parents to children with specific criteria, this criteria should<br \/>\ngo to the <code>ON<\/code> condition of the join. For example, if we have brands with promotions, and we want to fetch brands alongside promotions that start soon, we should put &#8220;starts soon&#8221; into the ON condition like this:<\/p>\n<pre>\r\nSELECT b.name, p.description, p.start_date, p.end_date\r\nFROM brands b LEFT JOIN promotions p \r\nON b.id = p.brand_id <font color=\"red\">AND p.start_date BETWEEN DATE'2024-12-05' AND DATE'2024-12-31'<\/font>\r\n<\/pre>\n<p>NOTE: this is <b>NOT<\/b> the same as putting the condition in the <code>WHERE<\/code> clause.<\/p>\n<p>Code on Github: <a href=\"https:\/\/github.com\/ikriv-samples\/join\">https:\/\/github.com\/ikriv-samples\/join<\/a><\/p>\n<h2>Creating the Database<\/h2>\n<p>To create the database <\/p>\n<ol>\n<li>Clone the repo: <code>git clone https:\/\/github.com\/ikriv-samples\/join.git<\/code><\/li>\n<li>Start mysql container: <code>.\/run-container.sh<\/code>.<br \/>\nNOTE: you should have Docker installed on your system.<\/li>\n<li>Fill the database: <code>.\/sql.sh create_db.sql<\/code><\/li>\n<\/ol>\n<p>This will create a simple database with two tables: brands and promotions.<\/p>\n<pre>\r\nmysql> select * from brands;\r\n+----+-------------------------+\r\n| id | name                    |\r\n+----+-------------------------+\r\n|  1 | ACME                    |\r\n|  2 | Road Runner Auto Repair |\r\n|  3 | Bugs Bunny Foods        |\r\n|  4 | Daffy Duck Outfitters   |\r\n+----+-------------------------+\r\n<\/pre>\n<pre>\r\nmysql> select * from promotions;\r\n+----+----------+---------------------------+------------+------------+\r\n| id | brand_id | description               | start_date | end_date   |\r\n+----+----------+---------------------------+------------+------------+\r\n|  1 |        1 | 50% off all items         | 2024-12-01 | 2024-12-31 |\r\n|  2 |        1 | Buy one bomb get one free | 2024-12-05 | 2024-12-25 |\r\n|  3 |        3 | 50 carrots for $3         | 2024-10-05 | 2024-10-07 |\r\n|  4 |        3 | 100 carrots for $5        | 2024-12-10 | 2024-12-15 |\r\n+----+----------+---------------------------+------------+------------+\r\n<\/pre>\n<h2>Fetching brands alongside upcoming promotions<\/h2>\n<p>We need to fetch brands alongside upcoming promotions, i.e. promotions that start between 2024-12-05 and 2024-12-31. If a brand has no upcoming promotions, we still want to see it, but with NULLs in the promotion data columns, following the LEFT JOIN semantics.<\/p>\n<p>The SQL query to do so is<\/p>\n<pre>\r\nSELECT b.name, p.description, p.start_date, p.end_date\r\nFROM brands b\r\nLEFT JOIN promotions p \r\nON b.id = p.brand_id AND p.start_date BETWEEN DATE'2024-12-05' AND DATE'2024-12-31'\r\n<\/pre>\n<p>Note how the promotion condition is included as part of the ON criteria. <\/p>\n<p><code>.\/sql.sh upcoming_promotions.sql<\/code><\/p>\n<pre>\r\n+-------------------------+---------------------------+------------+------------+\r\n| name                    | description               | start_date | end_date   |\r\n+-------------------------+---------------------------+------------+------------+\r\n| ACME                    | Buy one bomb get one free | 2024-12-05 | 2024-12-25 |\r\n| Road Runner Auto Repair | NULL                      | NULL       | NULL       |\r\n| Bugs Bunny Foods        | 100 carrots for $5        | 2024-12-10 | 2024-12-15 |\r\n| Daffy Duck Outfitters   | NULL                      | NULL       | NULL       |\r\n+-------------------------+---------------------------+------------+------------+\r\n<\/pre>\n<h2>Compare to other usages of JOIN<\/h2>\n<p>If we want to see all promotions, we execute a traditional JOIN with id comparison:<\/p>\n<pre>\r\nSELECT b.name, p.description, p.start_date, p.end_date\r\nFROM brands b\r\nLEFT JOIN promotions p\r\nON b.id = p.brand_id\r\n<\/pre>\n<p><code>.\/sql.sh all_promotions.sql<\/code><\/p>\n<pre>\r\n+-------------------------+---------------------------+------------+------------+\r\n| name                    | description               | start_date | end_date   |\r\n+-------------------------+---------------------------+------------+------------+\r\n| ACME                    | Buy one bomb get one free | 2024-12-05 | 2024-12-25 |\r\n| ACME                    | 50% off all items         | 2024-12-01 | 2024-12-31 |\r\n| Road Runner Auto Repair | NULL                      | NULL       | NULL       |\r\n| Bugs Bunny Foods        | 100 carrots for $5        | 2024-12-10 | 2024-12-15 |\r\n| Bugs Bunny Foods        | 50 carrots for $3         | 2024-10-05 | 2024-10-07 |\r\n| Daffy Duck Outfitters   | NULL                      | NULL       | NULL       |\r\n+-------------------------+---------------------------+------------+------------+\r\n<\/pre>\n<p>Beware of applying the promotion criteria AFTER the join. This will select only brands with upcoming promotions and leave out all other brands.<\/p>\n<pre>\r\nFROM brands b\r\nLEFT JOIN promotions\r\nON b.id = p.brand_id\r\nWHERE p.start_date BETWEEN DATE'2024-12-05' AND DATE'2024-12-31' -- WRONG!\r\n<\/pre>\n<p><code>.\/sql.sh wrong_promotions.sql<\/code><\/p>\n<pre>\r\n+------------------+---------------------------+------------+------------+\r\n| name             | description               | start_date | end_date   |\r\n+------------------+---------------------------+------------+------------+\r\n| ACME             | Buy one bomb get one free | 2024-12-05 | 2024-12-25 |\r\n| Bugs Bunny Foods | 100 carrots for $5        | 2024-12-10 | 2024-12-15 |\r\n+------------------+---------------------------+------------+------------+\r\n<\/pre>\n<p>Only the brands with upcoming promotions are retrieved, and all other brands are hidden.<\/p>\n<h2>Conclusion<\/h2>\n<p>With <code>LEFT JOIN<\/code>, including a criteria in the <code>ON<\/code> clause has different effect from including the same criteria in the <code>WHERE<\/code> clause. <code>WHERE<\/code> clause filters out the parent records that don&#8217;t have any children that match the criteria, effectively turning <code>LEFT JOIN<\/code> into an <code>INNER JOIN<\/code>. To maintain the <code>LEFT JOIN<\/code> semantics, any criteria pertaining to the child table should be placed in the <code>ON<\/code> clause.<\/p>\n<p>Many thanks to <a href=\"https:\/\/www.linkedin.com\/in\/arkadi-klepatch-8544a455\/\">Arkadi Klepatch<\/a> for giving me the idea of this post.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>When we write a JOIN statement in SQL, it usually goes like this SELECT {columns} FROM parent LEFT JOIN child ON child.parent_id = parent.id Does it ever make sense for <a href=\"https:\/\/ikriv.com\/blog\/?p=5173\" class=\"more-link\">[&hellip;]<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"Layout":"","footnotes":""},"categories":[14],"tags":[],"class_list":["entry","author-ikriv","post-5173","post","type-post","status-publish","format-standard","category-db"],"_links":{"self":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/5173","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5173"}],"version-history":[{"count":11,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/5173\/revisions"}],"predecessor-version":[{"id":5184,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/5173\/revisions\/5184"}],"wp:attachment":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5173"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5173"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5173"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}