<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Alessandro Marrella - bigquery</title>
    <link rel="self" type="application/atom+xml" href="https://alessandromarrella.com/tags/bigquery/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://alessandromarrella.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2025-03-19T00:00:00+00:00</updated>
    <id>https://alessandromarrella.com/tags/bigquery/atom.xml</id>
    <entry xml:lang="en">
        <title>Avoiding select * in BigQuery</title>
        <published>2025-03-19T00:00:00+00:00</published>
        <updated>2025-03-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Alessandro Marrella
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://alessandromarrella.com/feed/posts/avoid-select-star/"/>
        <id>https://alessandromarrella.com/feed/posts/avoid-select-star/</id>
        <content type="html" xml:base="https://alessandromarrella.com/feed/posts/avoid-select-star/">&lt;p&gt;A quick workaround to avoid people doing &amp;quot;SELECT *&amp;quot; on your tables (which I consider harmful, see &lt;a href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;3639861&#x2F;why-is-select-considered-harmful&quot;&gt;stackoverflow&lt;&#x2F;a&gt;) in BigQuery is the following:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Create a policy tag in the &lt;a href=&quot;https:&#x2F;&#x2F;console.cloud.google.com&#x2F;bigquery&#x2F;policy-tags&quot;&gt;policy tags page&lt;&#x2F;a&gt;. I create it under a new taxonomy &amp;quot;sql_rules&amp;quot;, and call it &amp;quot;no_select_star&amp;quot;&lt;&#x2F;li&gt;
&lt;li&gt;Once created, in the policy tag taxonomy page I enable the &amp;quot;enforce access control&amp;quot; option. If you need users to bypass this policy (e.g. needs to run bigquery copy operations) you can add the &amp;quot;fine graded reader&amp;quot; role to the principal associated with the policy tag in the same page.&lt;&#x2F;li&gt;
&lt;li&gt;In the table you want to protect, add a dummy column (I call it &amp;quot;do_not_select_star&amp;quot;) and assign it the policy tag&lt;&#x2F;li&gt;
&lt;li&gt;Done! From now on, &lt;code&gt;SELECT *&lt;&#x2F;code&gt; will throw an error.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;For more info see &lt;a href=&quot;https:&#x2F;&#x2F;cloud.google.com&#x2F;bigquery&#x2F;docs&#x2F;column-level-security-intro&quot;&gt;Google Cloud docs&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
    </entry>
    <entry xml:lang="en">
        <title>BigQuery Editions vs On Demand</title>
        <published>2024-08-31T00:00:00+00:00</published>
        <updated>2024-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Alessandro Marrella
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://alessandromarrella.com/feed/posts/bigquery-billing/"/>
        <id>https://alessandromarrella.com/feed/posts/bigquery-billing/</id>
        <content type="html" xml:base="https://alessandromarrella.com/feed/posts/bigquery-billing/">&lt;p&gt;These are some notes on the tradeoffs and best practices between On Demand pricing vs Editions pricing.&lt;&#x2F;p&gt;
&lt;span id=&quot;continue-reading&quot;&gt;&lt;&#x2F;span&gt;&lt;h2 id=&quot;pricing-models&quot;&gt;Pricing models&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#pricing-models&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;BigQuery currently offers two very different pricing models&lt;&#x2F;p&gt;
&lt;h3 id=&quot;editions&quot;&gt;Editions&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#editions&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;With &lt;a href=&quot;https:&#x2F;&#x2F;cloud.google.com&#x2F;bigquery&#x2F;docs&#x2F;editions-intro&quot;&gt;Editions&lt;&#x2F;a&gt; you are charged for &amp;quot;compute time&amp;quot; by slot-hour. A slot is a virtual CPU that BigQuery uses to execute queries.&lt;&#x2F;p&gt;
&lt;p&gt;Within Editions, you can purchase a &amp;quot;committment&amp;quot; for a lower price if you always have stuff running,
with the caveat that you are charged the entire time (it doesn&#x27;t scale up or down at will, and so for a yearly committment for 100 slots you end up paying for 100 * 365 * 24 slot hours, even if you don&#x27;t use them).&lt;&#x2F;p&gt;
&lt;p&gt;Outside the committed capacity, you can use an autoscaling reservation, which sets up the minimum (could be 0) and the maximum slots that BigQuery can use.
Slots are scaled up and down based on compute requirements for the queries.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;on-demand&quot;&gt;On Demand&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#on-demand&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;With &lt;a href=&quot;https:&#x2F;&#x2F;cloud.google.com&#x2F;bigquery&#x2F;pricing#on_demand_pricing&quot;&gt;On Demand&lt;&#x2F;a&gt; you are charged for &amp;quot;bytes processed&amp;quot;.
The compute capacity that GCP gives you is about 2000 slots, but you are not being charged for it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;best-practices-based-on-the-pricing-models&quot;&gt;Best practices based on the pricing models&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#best-practices-based-on-the-pricing-models&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;minimize-bytes-scanned-especially-on-on-demand&quot;&gt;Minimize bytes scanned (especially on on demand)&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#minimize-bytes-scanned-especially-on-on-demand&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;Do not select *, but only select the columns you need.&lt;&#x2F;li&gt;
&lt;li&gt;Partition&#x2F;Cluster the tables so that you only scan the minimum amount of rows needed (though this increases compute on write, so it&#x27;s not always helpful with editions, see note on compute used).&lt;&#x2F;li&gt;
&lt;li&gt;Follow the best practices in the &lt;a href=&quot;https:&#x2F;&#x2F;cloud.google.com&#x2F;bigquery&#x2F;docs&#x2F;best-practices-performance-compute#reduce-data-processed&quot;&gt;bigquery docs (reduce data processed)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;minimize-compute-used-especially-for-editions&quot;&gt;Minimize compute used (especially for editions)&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#minimize-compute-used-especially-for-editions&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;Optimize your joins, see &lt;a href=&quot;https:&#x2F;&#x2F;alessandromarrella.com&#x2F;feed&#x2F;posts&#x2F;semi-hash-join&#x2F;&quot;&gt;my note about semi hash joins&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Pay attention to when clustering and partitioning are more harmful than helpful.
Take a look at the execution plan of the jobs that produce&#x2F;update tables with partitioning and clustering.
If the table is large, a lot of slot time is going to be spent on sorting the data to match the clustering.&lt;&#x2F;li&gt;
&lt;li&gt;Follow the best practices in the &lt;a href=&quot;https:&#x2F;&#x2F;cloud.google.com&#x2F;bigquery&#x2F;docs&#x2F;best-practices-performance-compute#optimize-query-operations&quot;&gt;bigquery docs (optimize query ops)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;right-size-your-slots-autoscaling-editions-only&quot;&gt;Right-size your slots autoscaling (editions only)&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#right-size-your-slots-autoscaling-editions-only&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Pay particular attention to the jobs where you have a &amp;quot;contention&amp;quot; warning, and dig through the steps in the execution plan.&lt;&#x2F;p&gt;
&lt;p&gt;Check that the &amp;quot;Wait ms&amp;quot; stat and compare it with &amp;quot;Read&amp;quot;, &amp;quot;Write&amp;quot; and &amp;quot;Compute&amp;quot;.
&amp;quot;Wait&amp;quot; is time that BigQuery spends waiting for slots to be available.&lt;&#x2F;p&gt;
&lt;p&gt;Given that with autoscaling you are charged by the time allocated, and slots are allocated even when they are not used,
wait time still counts towards the cost.&lt;&#x2F;p&gt;
&lt;p&gt;If wait time is too high, this might mean that you need to either:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;serialize the queries so that they are no longer in contention for slots&lt;&#x2F;li&gt;
&lt;li&gt;increase the max in the autoscaling reservation to reduce contention&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Don&#x27;t increase the autoscaling reservation maximum too much,
as from experience the bigquery autoscaler is very eager to use as many slots as it can
to run the query as quickly as possible, but scaling down takes time (that you are billed for), and the minimum
interval of 1 min adds up quickly if you are using many thousands of slots.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;run-queries-with-the-model-that-s-cheapest-if-possible&quot;&gt;Run queries with the model that&#x27;s cheapest (if possible)&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#run-queries-with-the-model-that-s-cheapest-if-possible&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;This sounds obvious, but Google doesn&#x27;t make it exactly easy. You&#x27;ll need to:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;estimate the price of a query in both modes.&lt;&#x2F;li&gt;
&lt;li&gt;run the query in a dedicated project based on which model you choose (you cannot mix modes within a GCP project)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;For 1 (estimate the price of a query in both modes), I like to use variations of the following query:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT
&lt;&#x2F;span&gt;&lt;span&gt;job_id, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#65737e;&quot;&gt;-- the bigquery job id that identifies a run
&lt;&#x2F;span&gt;&lt;span&gt;query, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#65737e;&quot;&gt;-- the bigquery query text
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;destination_table&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;table_id&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#65737e;&quot;&gt;-- the name of the table (if the query writes to a table)
&lt;&#x2F;span&gt;&lt;span&gt;start_time,
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#96b5b4;&quot;&gt;SUM&lt;&#x2F;span&gt;&lt;span&gt;(total_slot_ms) &#x2F; (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;1000 &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;60 &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;60&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;06 &lt;&#x2F;span&gt;&lt;span&gt;as editions_cost &lt;&#x2F;span&gt;&lt;span style=&quot;color:#65737e;&quot;&gt;-- unit cost (here $0.06&#x2F;slot_hour) could be different based on the edition chosen and gcp discounts
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#96b5b4;&quot;&gt;SUM&lt;&#x2F;span&gt;&lt;span&gt;(total_bytes_billed) &#x2F; POW(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;1024&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;6&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;25 &lt;&#x2F;span&gt;&lt;span&gt;as on_demand_cost &lt;&#x2F;span&gt;&lt;span style=&quot;color:#65737e;&quot;&gt;-- unit cost (here $6.25&#x2F;tb) could be different based on the edition chosen and gcp discounts
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;{your_project}&lt;&#x2F;span&gt;&lt;span&gt;`.`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;{your_region}&lt;&#x2F;span&gt;&lt;span&gt;`.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;INFORMATION_SCHEMA&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;JOBS_BY_PROJECT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#65737e;&quot;&gt;-- or JOBS_BY_ORGANIZATION to see the whole company, but then you&amp;#39;ll need to remove the query field
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;GROUP BY&lt;&#x2F;span&gt;&lt;span&gt; ALL
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that the prices here are estimated, because google bills by slots &amp;quot;assigned&amp;quot; not slots used
so sometimes slot price is always slightly higher than the estimate (it takes time to scale up and down,
and google bills a minimum of 1 minute even if a query runs for 3 seconds).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;For 2 (run the query in a dedicated project), you&#x27;ll need to set up two separate GCP projects,
one with the reservation (or the reservation assigned from another project) and the other without.&lt;&#x2F;p&gt;
</content>
    </entry>
    <entry xml:lang="en">
        <title>BigQuery performance best practice: use semi joins when possible</title>
        <published>2024-06-02T00:00:00+00:00</published>
        <updated>2024-06-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Alessandro Marrella
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://alessandromarrella.com/feed/posts/semi-hash-join/"/>
        <id>https://alessandromarrella.com/feed/posts/semi-hash-join/</id>
        <content type="html" xml:base="https://alessandromarrella.com/feed/posts/semi-hash-join/">&lt;p&gt;SQL is an amazing language, it lets you declaratively say what you want, and the engine figures out for you the best way to return it to you. Or should I say, it figures out the best way to return it to you &lt;em&gt;given the information it has and the capabilities of the engine itself&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;In this post, we’ll discuss a performance optimization technique for BigQuery (also other advanced enough Enteprise Data Warehouses and databases support SEMI JOINS, but I&#x27;ll focus on BigQuery since it&#x27;s the one I use the most these days): using semi joins. &lt;&#x2F;p&gt;
&lt;span id=&quot;continue-reading&quot;&gt;&lt;&#x2F;span&gt;
&lt;blockquote&gt;
&lt;p&gt;A SEMI JOIN returns rows from the first table (left table) where one or more matches are found in the second table (right table), but it does not return rows from the second table. This can significantly improve query performance in certain scenarios.”&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Let&#x27;s look at an example query using their public datasets, the NYC taxi dataset, which contains a log of taxi trips in NYC.&lt;&#x2F;p&gt;
&lt;p&gt;Suppose that we want to know the distinct pickup_location_id values where both yellow and green taxis picked up clients in 2022.&lt;&#x2F;p&gt;
&lt;p&gt;One way to express this query might be the following:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` yellow
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;join &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_green_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` green
&lt;&#x2F;span&gt;&lt;span&gt;  on &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If we look at the performance, on my project at the time of running this query&#x27;s performance is the following (results might change a bit based on your google cloud project, slot availability, time of day, etc.):&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Total elapsed time&lt;&#x2F;strong&gt;: 4min 45sec&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Slot time consumed&lt;&#x2F;strong&gt;: 3h40min (A &lt;a href=&quot;https:&#x2F;&#x2F;cloud.google.com&#x2F;bigquery&#x2F;docs&#x2F;slots&quot;&gt;slot&lt;&#x2F;a&gt; in BigQuery is a unit of computational capacity required to execute SQL queries)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If we look at the execution graph, we see the following

&lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;alessandromarrella.com&amp;#x2F;processed_images&amp;#x2F;bq_inefficient_join_graph.4a6b78f46b7ae611.png&quot; style=&quot;display: block; margin: 0 auto;&quot; alt=&quot;Execution Graph, showing that the join took more than 4 minutes&quot; &#x2F;&gt;
&lt;&#x2F;p&gt;
&lt;p&gt;The join is definitely the step that takes the longest! If we click the join step in the graph, BigQuery really helpfully shows us more information:&lt;&#x2F;p&gt;

&lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;alessandromarrella.com&amp;#x2F;processed_images&amp;#x2F;bq_inefficient_join_detail.87ea64423632d682.png&quot; style=&quot;display: block; margin: 0 auto;&quot; alt=&quot;Join Detail, showing the join stats with a lot more rows produced than consumed&quot; &#x2F;&gt;
&lt;p&gt;The UI shows that the join produced a lot more rows than it consumed, due to how the join was applied. You can also see that it uses a &lt;code&gt;INNER HASH JOIN&lt;&#x2F;code&gt;. &lt;&#x2F;p&gt;
&lt;p&gt;In theory, this is a very efficient kind of join, as it builds a hash table with the join keys of one table (the smaller one), and then probes the other table (the larger one) to find the row keys that have a match in the hash table.&lt;&#x2F;p&gt;
&lt;p&gt;The problem in this case is not much in how the join happens, but in what it produces. As you can peek from the screenshot above, the number of rows produced is 83 million! This is due to the many-to-many relationship that we have in this join, where a &lt;code&gt;pickup_location_id&lt;&#x2F;code&gt; can happen multiple times in either table.&lt;&#x2F;p&gt;
&lt;p&gt;In a sense, BigQuery here does way more than we need to, as it would be enough to find one match in the &amp;quot;green&amp;quot; table to consider the row in yellow valid. In other words, we need a &lt;code&gt;SEMI JOIN&lt;&#x2F;code&gt;. &lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A &lt;code&gt;SEMI JOIN&lt;&#x2F;code&gt; returns rows from the first table (left table) where one or more matches are found in the second table (right table), but it does not return rows from the second table. &lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;How do we rewrite the previous query to make BigQuery use a semi join?&lt;&#x2F;p&gt;
&lt;p&gt;For this specific query we have (at least) three options, which all make use of the &lt;code&gt;SEMI HASH JOIN&lt;&#x2F;code&gt; in the query plan:&lt;&#x2F;p&gt;
&lt;h3 id=&quot;option-1-use-exists&quot;&gt;Option 1: use &lt;code&gt;EXISTS&lt;&#x2F;code&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#option-1-use-exists&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` yellow
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span&gt;EXISTS (
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;1
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_green_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` green
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  )
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;option-2-use-in&quot;&gt;Option 2: use &lt;code&gt;IN&lt;&#x2F;code&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#option-2-use-in&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` yellow
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id &lt;&#x2F;span&gt;&lt;span&gt;IN (
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_green_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` green
&lt;&#x2F;span&gt;&lt;span&gt;  )
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;option-3-use-intersect-distinct&quot;&gt;Option 3: use &lt;code&gt;INTERSECT DISTINCT&lt;&#x2F;code&gt;&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#option-3-use-intersect-distinct&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` yellow
&lt;&#x2F;span&gt;&lt;span&gt;INTERSECT DISTINCT 
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_green_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` green
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;comparison&quot;&gt;Comparison&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#comparison&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The three options all return the same result of the original query, and they all produce a query plan with a semi hash join,
with a much better performance:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Total elapsed time&lt;&#x2F;strong&gt;: 1sec&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Slot time consumed&lt;&#x2F;strong&gt;: 40sec&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;While they generate a similarly shaped plan, and in this case produce the same result, they are not the same from a logical point of view.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;exists&quot;&gt;EXISTS&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#exists&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h4&gt;
&lt;p&gt;Option 1 (&lt;code&gt;EXISTS&lt;&#x2F;code&gt;) is the most flexible, because it lets you write multiple predicates in the WHERE clause. So you can for example  write:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` yellow
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span&gt;EXISTS (
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;1
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_green_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` green
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#65737e;&quot;&gt;-- add another predicate
&lt;&#x2F;span&gt;&lt;span&gt;    AND &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;dropoff_location_id &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;dropoff_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  )
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This would still do a &lt;code&gt;SEMI HASH JOIN&lt;&#x2F;code&gt;, but now we also filter the rows so that the result returns locations where both the pickup and dropoff was the same.&lt;&#x2F;p&gt;
&lt;p&gt;From a performance point of view, even if this is fast, this still scans &lt;code&gt;83,869,625&lt;&#x2F;code&gt; rows in the join phase.&lt;&#x2F;p&gt;
&lt;p&gt;If we want to reduce the number of rows scanned, in this case we can do it with a WITH statement, like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; green AS (
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT&lt;&#x2F;span&gt;&lt;span&gt; pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_green_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;`
&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` yellow
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span&gt;EXISTS (
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;1
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; green
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  )
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This query usually performs a bit faster (for me it&#x27;s around 900ms), uses slightly less slot seconds (for me about 30s), and scans less rows in the join phase (now &lt;code&gt;36,272,535&lt;&#x2F;code&gt;). 
The main difference is that we do a &lt;code&gt;DISTINCT&lt;&#x2F;code&gt; before joining.&lt;&#x2F;p&gt;
&lt;p&gt;To reduce the join to a minimum, we can also do one more step and do a distinct on yellow too, and we get&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; green AS (
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT&lt;&#x2F;span&gt;&lt;span&gt; pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_green_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;`
&lt;&#x2F;span&gt;&lt;span&gt;),
&lt;&#x2F;span&gt;&lt;span&gt;yellow AS (
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` yellow
&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; yellow
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span&gt;EXISTS (
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;1
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; green
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  )
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now the performance is even faster (for me around 800ms), uses even less slot seconds (about 20s), and more deterministically we can say that it only scans &lt;code&gt;16,813&lt;&#x2F;code&gt;in the join phase.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;in&quot;&gt;IN&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#in&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h4&gt;
&lt;p&gt;With &lt;code&gt;IN&lt;&#x2F;code&gt; we are a bit more constrained (unless we do ugly string concatenation things), as we can really only compare one element per statement.&lt;&#x2F;p&gt;
&lt;p&gt;So to express a query where we want &lt;code&gt;pickup_location_id&lt;&#x2F;code&gt; and &lt;code&gt;dropoff_location_id&lt;&#x2F;code&gt; to be the same you&#x27;d have to write:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` yellow
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id &lt;&#x2F;span&gt;&lt;span&gt;IN (
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_green_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` green
&lt;&#x2F;span&gt;&lt;span&gt;  )
&lt;&#x2F;span&gt;&lt;span&gt;  AND &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;dropoff_location_id &lt;&#x2F;span&gt;&lt;span&gt;IN (
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;dropoff_location_id
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_green_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` green
&lt;&#x2F;span&gt;&lt;span&gt;  )
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Since this uses two statements, in the &lt;code&gt;JOIN&lt;&#x2F;code&gt; step in the query it does two joins!&lt;&#x2F;p&gt;

&lt;img src=&quot;https:&amp;#x2F;&amp;#x2F;alessandromarrella.com&amp;#x2F;processed_images&amp;#x2F;bq_in_double_join.2347a8c2f36f6d98.png&quot; style=&quot;display: block; margin: 0 auto;&quot; alt=&quot;Join Detail, showing that it does two joins&quot; &#x2F;&gt;
&lt;p&gt;The &lt;code&gt;IN&lt;&#x2F;code&gt; is more perfomant than the original &lt;code&gt;EXISTS&lt;&#x2F;code&gt; query written above, and is more comparable to the &lt;code&gt;EXISTS&lt;&#x2F;code&gt; query where we use &lt;code&gt;WITH green AS...&lt;&#x2F;code&gt; to do the select distinct. Also in the &lt;code&gt;IN&lt;&#x2F;code&gt; case we scan &lt;code&gt;36,272,535&lt;&#x2F;code&gt;rows (like in the first improved exists). &lt;&#x2F;p&gt;
&lt;p&gt;We can reach a comparable performance to the second improved exists (the one with &lt;code&gt;WITH green AS..., yellow AS...&lt;&#x2F;code&gt;) if we do &lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; yellow AS (
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` yellow
&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; yellow
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;yellow&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;pickup_location_id &lt;&#x2F;span&gt;&lt;span&gt;IN (
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT DISTINCT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;green&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;dropoff_location_id
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM &lt;&#x2F;span&gt;&lt;span&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;bigquery-public-data.new_york_taxi_trips.tlc_green_trips_2022&lt;&#x2F;span&gt;&lt;span&gt;` green
&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Also here, we scan &lt;code&gt;16,819&lt;&#x2F;code&gt; rows in the JOIN.&lt;&#x2F;p&gt;
&lt;p&gt;There is one more caveat with &lt;code&gt;IN&lt;&#x2F;code&gt;, as sometimes &lt;code&gt;IN&lt;&#x2F;code&gt; and &lt;code&gt;EXISTS&lt;&#x2F;code&gt; don&#x27;t always produce the same result! See &lt;a href=&quot;https:&#x2F;&#x2F;alessandromarrella.com&#x2F;feed&#x2F;posts&#x2F;in-vs-exists&#x2F;&quot;&gt;NOT IN and NOT EXISTS don&#x27;t always produce the same result&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;intersect-distinct&quot;&gt;INTERSECT DISTINCT&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#intersect-distinct&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h4&gt;
&lt;p&gt;I&#x27;ll admit it, I crafted the query so that intersect distinct would make the cut as well, as I particularly like it syntax wise.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;INTERSECT DISTINCT&lt;&#x2F;code&gt; in our case produces exactly the same result as &lt;code&gt;IN&lt;&#x2F;code&gt; and &lt;code&gt;EXISTS&lt;&#x2F;code&gt; but has a different limitation: the columns selected need to be the same in both tables AND the result needs to be distinct (there is no such thing as an &lt;code&gt;INTERSECT ALL&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;From a performance profile, this immediately produces the most efficient result, scanning only  &lt;code&gt;16,819&lt;&#x2F;code&gt; rows in the JOIN (which again, is a &lt;code&gt;SEMI HASH JOIN&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;a class=&quot;anchor&quot; aria-hidden=&quot;true&quot; href=&quot;#conclusion&quot; hidden=&quot;&quot;&gt;#&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;In conclusion, we saw that &lt;code&gt;SEMI HASH JOIN&lt;&#x2F;code&gt; can be a powerful optimization, especially when dealing with many-to-many relationships, and can be done in several ways. Each method has its strengths and limitations, but in the end the main optimization is the impact on the query plan of moving from a &lt;code&gt;INNER&lt;&#x2F;code&gt; to a &lt;code&gt;SEMI&lt;&#x2F;code&gt; join.&lt;&#x2F;p&gt;
</content>
    </entry>
    <entry xml:lang="en">
        <title>NOT IN and NOT EXISTS don&#x27;t always produce the same result</title>
        <published>2024-06-01T00:00:00+00:00</published>
        <updated>2024-06-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Alessandro Marrella
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://alessandromarrella.com/feed/posts/in-vs-exists/"/>
        <id>https://alessandromarrella.com/feed/posts/in-vs-exists/</id>
        <content type="html" xml:base="https://alessandromarrella.com/feed/posts/in-vs-exists/">&lt;p&gt;&lt;code&gt;IN&lt;&#x2F;code&gt; and &lt;code&gt;EXISTS&lt;&#x2F;code&gt; often produce the same result, but when negated and dealing with &lt;code&gt;NULL&lt;&#x2F;code&gt; values, they behave differently.&lt;&#x2F;p&gt;
&lt;span id=&quot;continue-reading&quot;&gt;&lt;&#x2F;span&gt;
&lt;p&gt;Let&#x27;s see an example, assume we have these two tables:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;orders&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;order_id&lt;&#x2F;th&gt;&lt;th&gt;customer_id&lt;&#x2F;th&gt;&lt;th&gt;amount&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;100&lt;&#x2F;td&gt;&lt;td&gt;50.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;101&lt;&#x2F;td&gt;&lt;td&gt;75.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;102&lt;&#x2F;td&gt;&lt;td&gt;30.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;NULL&lt;&#x2F;td&gt;&lt;td&gt;20.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;customers&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;customer_id&lt;&#x2F;th&gt;&lt;th&gt;name&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;100&lt;&#x2F;td&gt;&lt;td&gt;Bob&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;101&lt;&#x2F;td&gt;&lt;td&gt;Alice&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;102&lt;&#x2F;td&gt;&lt;td&gt;Martha&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;NULL&lt;&#x2F;td&gt;&lt;td&gt;John&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;em&gt;Ignore the fact that a well formed customers table would need to have the id always specified and ideally as a constraint. This is just an example.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Now, let’s say we want to find orders where the customer_id is not present in the customers table.&lt;&#x2F;p&gt;
&lt;p&gt;Using &lt;code&gt;NOT EXISTS&lt;&#x2F;code&gt; we would write something like:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;*
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; orders o
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; NOT EXISTS (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;1 &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; customers c &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;customer_id &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d08770;&quot;&gt;customer_id&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The output, would probably be what we expect:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;order_id&lt;&#x2F;th&gt;&lt;th&gt;customer_id&lt;&#x2F;th&gt;&lt;th&gt;amount&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;NULL&lt;&#x2F;td&gt;&lt;td&gt;20.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Using &lt;code&gt;NOT IN&lt;&#x2F;code&gt; we would write something like:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;*
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; orders
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; customer_id NOT IN (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; customer_id &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; customers);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The output here will be empty!&lt;&#x2F;p&gt;
&lt;p&gt;When we use &lt;code&gt;NOT IN&lt;&#x2F;code&gt;, SQL checks each value in the orders table against the list of values returned by the subquery. If any value in the subquery result is &lt;code&gt;NULL&lt;&#x2F;code&gt;, the entire &lt;code&gt;NOT IN&lt;&#x2F;code&gt; comparison will result in &lt;code&gt;NULL&lt;&#x2F;code&gt; for each row in the orders table. This is because any comparison with &lt;code&gt;NULL&lt;&#x2F;code&gt; yields &lt;code&gt;NULL&lt;&#x2F;code&gt; and &lt;code&gt;NOT IN&lt;&#x2F;code&gt; needs &lt;strong&gt;all comparisons&lt;&#x2F;strong&gt; to be &lt;code&gt;TRUE&lt;&#x2F;code&gt; for a row to be included in the result.&lt;&#x2F;p&gt;
&lt;p&gt;This is not a problem with &lt;code&gt;NOT EXISTS&lt;&#x2F;code&gt;, because the  &lt;code&gt;NOT EXISTS&lt;&#x2F;code&gt; clause checks for the non-existence of rows that meet the criteria specified in the subquery. It does not perform direct comparisons with &lt;code&gt;NULL&lt;&#x2F;code&gt; in the same way &lt;code&gt;NOT IN&lt;&#x2F;code&gt; does. Instead, it simply checks if there are any rows that match the condition. If no such rows exist, the condition is true.&lt;&#x2F;p&gt;
</content>
    </entry>
</feed>