<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aleksandr Kan</title>
    <description>The latest articles on DEV Community by Aleksandr Kan (@aleks_sqlbrief).</description>
    <link>https://dev.to/aleks_sqlbrief</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4176793%2Ff6cdd850-0ee1-44c4-9d68-c1cc7b22d313.png</url>
      <title>DEV Community: Aleksandr Kan</title>
      <link>https://dev.to/aleks_sqlbrief</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aleks_sqlbrief"/>
    <language>en</language>
    <item>
      <title>SQL beginners: what should happen after your first query?</title>
      <dc:creator>Aleksandr Kan</dc:creator>
      <pubDate>Sat, 17 Oct 2026 09:37:18 +0000</pubDate>
      <link>https://dev.to/aleks_sqlbrief/sql-beginners-what-should-happen-after-your-first-query-37gf</link>
      <guid>https://dev.to/aleks_sqlbrief/sql-beginners-what-should-happen-after-your-first-query-37gf</guid>
      <description>&lt;p&gt;Running a ready-made SQL query is easy. Changing it yourself is where a lesson has to earn its keep.&lt;/p&gt;

&lt;p&gt;I'm building SQLBrief, and this is the bit I want feedback on. The first task gives you a query to run. The next asks you to make a small change, with instructions and hints instead of filling in the answer for you.&lt;/p&gt;

&lt;p&gt;If you're new to SQL, could you try those two tasks and tell me &lt;strong&gt;the first place you weren't sure what to do next&lt;/strong&gt;? Was it the question, the editor, or what to do with the result? One sentence is plenty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sqlbrief.com/en/courses/sql-basics/tasks/sqlf2-001?utm_source=dev&amp;amp;utm_medium=referral&amp;amp;utm_campaign=sqlbrief-launch&amp;amp;utm_content=th_basics_reply_01" rel="noopener noreferrer"&gt;Try the first task&lt;/a&gt;. It runs in your browser, is free, and needs no signup or installation. This link opens the English version; Russian is available too.&lt;/p&gt;

&lt;p&gt;If you teach SQL, I'd also like to hear what you ask learners to change in their first query.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>beginners</category>
    </item>
    <item>
      <title>100% activation, three inactive users: a SQL interview trap</title>
      <dc:creator>Aleksandr Kan</dc:creator>
      <pubDate>Thu, 15 Oct 2026 19:33:43 +0000</pubDate>
      <link>https://dev.to/aleks_sqlbrief/100-activation-three-inactive-users-a-sql-interview-trap-5bgo</link>
      <guid>https://dev.to/aleks_sqlbrief/100-activation-three-inactive-users-a-sql-interview-trap-5bgo</guid>
      <description>&lt;p&gt;Suppose you're analysing a project-management app. People sign up to organise their work, but some leave without creating a project. You want to know how many new users take that first step within seven days of registering.&lt;/p&gt;

&lt;p&gt;In this example, creating at least one project during those seven days counts as &lt;strong&gt;activation&lt;/strong&gt;. The &lt;strong&gt;activation rate&lt;/strong&gt; is the percentage of registered users who do it.&lt;/p&gt;

&lt;p&gt;We'll compare people who signed up in two different weeks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signup week beginning&lt;/th&gt;
&lt;th&gt;People who signed up&lt;/th&gt;
&lt;th&gt;People who created a project within seven days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3 August 2026&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 August 2026&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Among the people who signed up in the first week, three created a project; among those who signed up in the second, four did. The product manager sees the increase and says: "More people created a project. Activation improved."&lt;/p&gt;

&lt;p&gt;The counts did rise. The percentages tell a different story: 3 out of 4 is &lt;strong&gt;75.0%&lt;/strong&gt;, while 4 out of 6 is &lt;strong&gt;66.7%&lt;/strong&gt;. To compare the rates, we need to count everyone who registered in each week, including those who never created a project.&lt;/p&gt;

&lt;p&gt;Now imagine the SQL report returns &lt;strong&gt;100% for both weeks&lt;/strong&gt;. Below, we'll reproduce that result, find the users it left out, and correct the calculation. The example uses PostgreSQL; it assumes you can read a &lt;code&gt;SELECT&lt;/code&gt; with a join and &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data: registrations and recorded actions
&lt;/h2&gt;

&lt;p&gt;We need two tables. &lt;code&gt;interview_users&lt;/code&gt; lists every registered user once. Its &lt;code&gt;signup_week&lt;/code&gt; column stores the Monday of the week when that person registered. This lets us compare people by when they joined, regardless of when they later created a project.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;interview_events&lt;/code&gt; records actions reported by the app: &lt;code&gt;project_created&lt;/code&gt; for creating a project and &lt;code&gt;page_view&lt;/code&gt; for viewing a page. The same user can have several rows. A delivery retry can also record the same action again, under a new &lt;code&gt;receipt_id&lt;/code&gt;, so counting rows does not tell us how many people acted.&lt;/p&gt;

&lt;p&gt;The data is invented for this exercise. Each person has been observed for a full seven days after signup, and their event rows have already been limited to that period. We can therefore concentrate on counting users. There are no timestamp columns in this simplified dataset; these queries cannot apply the seven-day limit to an unfiltered event log.&lt;/p&gt;

&lt;p&gt;Run this setup once in an empty PostgreSQL database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;interview_users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;signup_week&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;interview_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;receipt_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;interview_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;interview_users&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'U01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-03'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="s1"&gt;'U02'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-03'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'U03'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-03'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="s1"&gt;'U04'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-03'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'U05'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-10'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="s1"&gt;'U06'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-10'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'U07'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-10'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="s1"&gt;'U08'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-10'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'U09'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-10'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="s1"&gt;'U10'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-10'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;interview_events&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U02'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U03'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U05'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U05'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U05'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U06'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U07'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U08'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U04'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'page_view'&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U09'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'page_view'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'U05'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'page_view'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read a few rows before writing the report. U01 has two &lt;code&gt;project_created&lt;/code&gt; records, but is still one person. U05 has three such records and one page view. U04 and U09 have only page views, so neither activated. U10 appears in the users table but has no event rows at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the query arrives at 100%
&lt;/h2&gt;

&lt;p&gt;This first attempt starts from the users table and uses a &lt;code&gt;LEFT JOIN&lt;/code&gt; to attach their events. It then selects project-creation events in &lt;code&gt;WHERE&lt;/code&gt; and counts distinct users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signup_week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;registered_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;activated_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;activation_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;interview_users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;interview_events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'project_created'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signup_week&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signup_week&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;signup_week&lt;/th&gt;
&lt;th&gt;registered_users&lt;/th&gt;
&lt;th&gt;activated_users&lt;/th&gt;
&lt;th&gt;activation_pct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-03&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;100.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-10&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;100.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compare the &lt;code&gt;registered_users&lt;/code&gt; column with the registration totals at the start: it says 3 instead of 4, and 4 instead of 6. The percentages are wrong because the denominator is already missing people.&lt;/p&gt;

&lt;p&gt;To see where they disappeared, follow U04, U09 and U10 through the join and the filter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User&lt;/th&gt;
&lt;th&gt;Row after LEFT JOIN&lt;/th&gt;
&lt;th&gt;Row after the project filter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;U04&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;page_view&lt;/code&gt; event&lt;/td&gt;
&lt;td&gt;Removed: &lt;code&gt;page_view&lt;/code&gt; does not equal &lt;code&gt;project_created&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U09&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;page_view&lt;/code&gt; event&lt;/td&gt;
&lt;td&gt;Removed for the same reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U10&lt;/td&gt;
&lt;td&gt;NULL in the event columns&lt;/td&gt;
&lt;td&gt;Removed: comparing NULL with &lt;code&gt;project_created&lt;/code&gt; does not return true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The join itself kept all three. For U10, it supplied NULL event columns because it found no matching event. The later &lt;code&gt;WHERE&lt;/code&gt; condition kept only rows where &lt;code&gt;e.event_name = 'project_created'&lt;/code&gt; was true. U04 and U09 failed that comparison; U10's comparison was unknown because the event name was NULL. All three were removed before the counts were calculated.&lt;/p&gt;

&lt;p&gt;That leaves only people who created a project. Both counts now describe the same people, so dividing one by the other gives 100%.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COUNT(DISTINCT ...)&lt;/code&gt; does prevent several event rows from counting a person twice. It cannot restore someone removed by &lt;code&gt;WHERE&lt;/code&gt;. PostgreSQL's &lt;a href="https://www.postgresql.org/docs/18/queries-table-expressions.html#QUERIES-JOIN" rel="noopener noreferrer"&gt;join documentation&lt;/a&gt; shows why placing an event restriction in &lt;code&gt;ON&lt;/code&gt; and placing it in &lt;code&gt;WHERE&lt;/code&gt; produce different results with a left join.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why allowing NULLs only fixes one of the three users
&lt;/h2&gt;

&lt;p&gt;Once you notice U10's NULL event columns, it is tempting to add &lt;code&gt;OR e.user_id IS NULL&lt;/code&gt;. That should allow users with no events to remain. Rather than calculate a percentage yet, list the users this proposed filter would keep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;interview_users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;interview_events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'project_created'&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;U10 returns, but U04 and U09 are still missing. Their page views matched the join, so their event columns contain values, including a non-NULL &lt;code&gt;e.user_id&lt;/code&gt;. Neither part of the &lt;code&gt;OR&lt;/code&gt; condition is true for them.&lt;/p&gt;

&lt;p&gt;The query returns eight IDs: U01, U02, U03, U05, U06, U07, U08 and U10. A left join supplies NULL columns when it finds no match; it does not create a replacement NULL row after &lt;code&gt;WHERE&lt;/code&gt; rejects an existing match.&lt;/p&gt;

&lt;p&gt;This distinction matters when you test the fix. A person with no events and a person with only page views are both inactive, but this filter treats them differently. The denominator needs to include both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the project condition in the join
&lt;/h2&gt;

&lt;p&gt;We want to keep every registered user and attach an event only if it records a project creation. Put both requirements for an event match in &lt;code&gt;ON&lt;/code&gt;: its user ID must match, and its name must be &lt;code&gt;project_created&lt;/code&gt;. Remove the event condition from &lt;code&gt;WHERE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signup_week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;registered_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;activated_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;activation_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;interview_users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;interview_events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
 &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'project_created'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signup_week&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signup_week&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page views belonging to U04 and U09 no longer match this join condition. Those users now remain with NULL event columns, just like U10. Their IDs in &lt;code&gt;u.user_id&lt;/code&gt; are still present and contribute to the registration count. The NULLs in &lt;code&gt;e.user_id&lt;/code&gt; do not contribute to the activation count.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;signup_week&lt;/th&gt;
&lt;th&gt;registered_users&lt;/th&gt;
&lt;th&gt;activated_users&lt;/th&gt;
&lt;th&gt;activation_pct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-03&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;75.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-10&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;66.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The registration counts are back to 4 and 6, and the rates agree with the calculation by hand. We still need &lt;code&gt;DISTINCT&lt;/code&gt;: U01 and U05 each have several project-creation records. Moving the condition into &lt;code&gt;ON&lt;/code&gt; preserves inactive users, but does not remove repeated matches for active ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  An alternative: calculate each user's activation first
&lt;/h2&gt;

&lt;p&gt;Another way to make the calculation easier to inspect is to build a list with exactly one row per user. Give each person a 1 if any of their events records a project creation, or a 0 otherwise. Then count the people in each signup week and add their 1s.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;WITH per_user AS (...)&lt;/code&gt; part of this query names that intermediate result &lt;code&gt;per_user&lt;/code&gt;. The final &lt;code&gt;SELECT&lt;/code&gt; reads it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;per_user&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signup_week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'project_created'&lt;/span&gt;
      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;activated&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;interview_users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
  &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;interview_events&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signup_week&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;signup_week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;registered_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;activated_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;activation_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;per_user&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;signup_week&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;signup_week&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;per_user&lt;/code&gt;, &lt;code&gt;CASE&lt;/code&gt; produces 1 for a project event and 0 otherwise. &lt;code&gt;MAX&lt;/code&gt; then chooses the largest value for each user. U05 has several 1s, but their maximum is still 1. U04 and U09 have only 0s. U10's NULL event name also takes the &lt;code&gt;ELSE 0&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;per_user&lt;/code&gt; has one row per registered user, the outer &lt;code&gt;COUNT(*)&lt;/code&gt; counts people. &lt;code&gt;SUM(activated)&lt;/code&gt; counts those whose flag is 1. The result is again 75.0% and 66.7%.&lt;/p&gt;

&lt;p&gt;Both solutions multiply by &lt;code&gt;100.0&lt;/code&gt; before dividing so that PostgreSQL uses decimal arithmetic. Neither has been benchmarked here; the reason to show both is that one fixes the join directly, while the other makes each person's contribution explicit.&lt;/p&gt;

&lt;p&gt;We can now answer the manager: "More people created a project among those who signed up in the week of 10 August: four instead of three. But more people also signed up, so the share who created a project fell from 75.0% to 66.7%."&lt;/p&gt;

&lt;p&gt;These are descriptive results for ten users. They do not establish a lasting trend, or tell us why the percentages differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the fix by changing the data
&lt;/h2&gt;

&lt;p&gt;Getting the expected percentages on the original rows is a useful start. We can also check whether the queries respond correctly when we add a repeated event, an inactive user, or a week with no activations.&lt;/p&gt;

&lt;p&gt;Run the additions below in order, once each, after the original setup. After each addition, rerun either corrected query. The deliberately broken query near the start is not the one to reuse here.&lt;/p&gt;

&lt;p&gt;First, add another project-creation record for U05, who already has several:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;interview_events&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'U05'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'project_created'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;U05 should still count as one activated person, so the rates should remain 75.0% and 66.7%. Next, add U11 to the registrations for the week of 10 August, without adding any events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;interview_users&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'U11'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-08-10'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are now seven registered users in that week, and still four who created a project. Its rate should become 4 / 7, rounded to &lt;strong&gt;57.1%&lt;/strong&gt;. Unlike another event for U05, this addition changes the number of people we are measuring.&lt;/p&gt;

&lt;p&gt;Finally, add U12 as the only registered user in the week of 17 August. Again, add no events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;interview_users&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'U12'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-08-17'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The report should gain a row for 17 August: one registered user, zero activated users and &lt;strong&gt;0.0%&lt;/strong&gt;. A query that starts from project events alone would miss this week entirely.&lt;/p&gt;

&lt;p&gt;This is different from a week when nobody registered. With no users, there is no group to aggregate in these tables and no activation percentage to calculate. Zero out of one is 0%; zero out of zero is undefined.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;What must happen&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Another project record for U05&lt;/td&gt;
&lt;td&gt;Registered and activated user counts stay the same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U11 signs up in the week of 10 August, with no events&lt;/td&gt;
&lt;td&gt;Registered users: 6 to 7; activated users: still 4; rate: 57.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U12 is the only signup in the week of 17 August, with no events&lt;/td&gt;
&lt;td&gt;A new row: 1 registered, 0 activated, 0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The examples were checked in PGlite, a PostgreSQL runtime. Both corrected queries produced the expected results on the original data and after each addition. The broken &lt;code&gt;WHERE&lt;/code&gt; query and the &lt;code&gt;OR IS NULL&lt;/code&gt; patch were also run to verify the missing users shown above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the example assumes about real event data
&lt;/h2&gt;

&lt;p&gt;We started with events already selected from each person's first seven days after signup. In a real app, you would need signup and event timestamps to make that selection. You would also need to decide what "seven days" means: exactly 168 hours after registration, or a calendar-based period with an agreed timezone.&lt;/p&gt;

&lt;p&gt;Make sure each person has had the whole period to act. Someone who registered an hour ago cannot yet be compared fairly with someone observed for a full week.&lt;/p&gt;

&lt;p&gt;Finally, check whether event collection is complete. In this exercise, the absence of a project event means no activation was recorded. In a real system, an absent event could also mean a tracking failure or delayed delivery. Correct SQL cannot distinguish those causes from these two tables alone.&lt;/p&gt;

&lt;p&gt;The useful habit to take from this example is to inspect who gets counted before checking the percentage. Include someone who acted, someone who did something else, and someone who did nothing. Then verify that repeated events do not become extra people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the same case in the browser
&lt;/h2&gt;

&lt;p&gt;I build SQLBrief, where this case is available as a &lt;a href="https://sqlbrief.com/en/briefs/sql-interview-activation?utm_source=dev&amp;amp;utm_medium=referral&amp;amp;utm_campaign=sqlbrief-launch" rel="noopener noreferrer"&gt;free browser exercise in English&lt;/a&gt;. The two tables are already loaded. You choose who to count, write the SQL and answer the product manager, then open a review of the result. A Russian version is available too. No signup or database installation is needed.&lt;/p&gt;

&lt;p&gt;You can use an optional 20-minute timer or work without one. Since this article gives the solution, use the exercise to rehearse the explanation, not to measure how you would do on an unfamiliar interview question. The browser starts with the original ten users; U11, U12 and the extra event above are experiments for your own database.&lt;/p&gt;

&lt;p&gt;If you try it, tell me where you got stuck or which part of the explanation was hard to follow. You can comment here or use the feedback link in the exercise.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>analytics</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your first SQL query can work before you understand it</title>
      <dc:creator>Aleksandr Kan</dc:creator>
      <pubDate>Sun, 11 Oct 2026 10:08:04 +0000</pubDate>
      <link>https://dev.to/aleks_sqlbrief/your-first-sql-query-can-work-before-you-understand-it-4lfj</link>
      <guid>https://dev.to/aleks_sqlbrief/your-first-sql-query-can-work-before-you-understand-it-4lfj</guid>
      <description>&lt;p&gt;A working example gives you something to investigate. An empty editor gives you a blinking cursor.&lt;/p&gt;

&lt;p&gt;If you are starting SQL from zero, try this small experiment. You will predict a result, change one part of a query, and check what changed. No joins, database administration or interview puzzles yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with four accounts
&lt;/h2&gt;

&lt;p&gt;Imagine a shop has this &lt;code&gt;users&lt;/code&gt; table. This is a deliberately tiny teaching example, not customer data.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;country_code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u001&lt;/td&gt;
&lt;td&gt;US&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u002&lt;/td&gt;
&lt;td&gt;GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u003&lt;/td&gt;
&lt;td&gt;US&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u004&lt;/td&gt;
&lt;td&gt;CA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One row represents one account. Two accounts can have the same country code; that does not make them the same account.&lt;/p&gt;

&lt;p&gt;Read this query aloud as “show me all columns from users”:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SELECT&lt;/code&gt; says which columns to show. The asterisk means all of them. &lt;code&gt;FROM&lt;/code&gt; names the table to read. This query reads data; it does not change the table.&lt;/p&gt;

&lt;p&gt;Before moving on, name the two column headings you expect. Then predict the number of rows. You should expect two columns and four rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change one thing
&lt;/h2&gt;

&lt;p&gt;Replace the asterisk with a column name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;country_code&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Will this give you three rows or four?&lt;/p&gt;

&lt;p&gt;There are three countries, but still four accounts. The result contains &lt;code&gt;US&lt;/code&gt; twice, plus &lt;code&gt;GB&lt;/code&gt; and &lt;code&gt;CA&lt;/code&gt;. Choosing a column changes what you can see about each row. It does not automatically combine rows that look alike.&lt;/p&gt;

&lt;p&gt;This is a useful early distinction: the number of columns and the number of rows answer different questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now ask for different values
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;country_code&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you get three rows: one for each country code in this example. &lt;code&gt;DISTINCT&lt;/code&gt; removes duplicate combinations of the selected values. With a single selected column, that means duplicate country codes.&lt;/p&gt;

&lt;p&gt;The result order is not guaranteed in any of these queries. When order matters, you request it explicitly with &lt;code&gt;ORDER BY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is a trap worth trying before you scroll further. Predict this result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country_code&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns four rows again. Each pair is different because every account has a different &lt;code&gt;user_id&lt;/code&gt;. &lt;code&gt;DISTINCT&lt;/code&gt; applies to the whole selected combination, not just the country column beside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the example yours
&lt;/h2&gt;

&lt;p&gt;Change the country for &lt;code&gt;u004&lt;/code&gt; from &lt;code&gt;CA&lt;/code&gt; to &lt;code&gt;US&lt;/code&gt; on paper. Predict the row counts for the last three queries before running anything: four, two, four.&lt;/p&gt;

&lt;p&gt;If you can explain those numbers, you have done more than copy SQL syntax. You have connected a query to the meaning of its result. If your prediction was wrong, change only one part and inspect it again. The small table makes the reason visible.&lt;/p&gt;

&lt;p&gt;For a runnable version of this toy table, paste this setup and one of the queries into a SQLite session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;country_code&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'u001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'u002'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'GB'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'u003'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'u004'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CA'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, SQLBrief opens with a prepared database and query in the browser. It uses a larger shop dataset, so its row counts differ from this four-row example. The first step is still to inspect what one row represents and where the result headings come from. Later exercises move through filtering, grouping, joins and reports.&lt;/p&gt;

&lt;p&gt;SQLBrief is our product. Its &lt;a href="https://sqlbrief.com/en/courses/sql-basics?utm_source=dev&amp;amp;utm_medium=referral&amp;amp;utm_campaign=sql-basics-launch" rel="noopener noreferrer"&gt;SQL Basics course&lt;/a&gt; currently has 125 tasks, is free, and works without an account or installation. English and Russian versions are available.&lt;/p&gt;

&lt;p&gt;If you try the first lesson, tell us where you started guessing. A task number and the sentence that confused you would give us something concrete to fix.&lt;/p&gt;




&lt;p&gt;This article was prepared with an AI agent for SQLBrief. The toy SQL examples were executed locally, and the course details were checked against the live product.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
