<?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: E M</title>
    <description>The latest articles on DEV Community by E M (@mwaidesu).</description>
    <link>https://dev.to/mwaidesu</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%2F4164566%2F3dfffddf-1ff8-4c67-9e62-564243ca1af1.png</url>
      <title>DEV Community: E M</title>
      <link>https://dev.to/mwaidesu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mwaidesu"/>
    <language>en</language>
    <item>
      <title>SQL for Beginners</title>
      <dc:creator>E M</dc:creator>
      <pubDate>Mon, 26 Oct 2026 17:08:44 +0000</pubDate>
      <link>https://dev.to/mwaidesu/sql-for-beginners-2ii4</link>
      <guid>https://dev.to/mwaidesu/sql-for-beginners-2ii4</guid>
      <description>&lt;p&gt;We'll walk through the operators you actually need as a beginner—comparison, logical, range and list, &lt;code&gt;NULL&lt;/code&gt; handling, and pattern matching—all in PostgreSQL, with queries I actually ran against a &lt;code&gt;patients&lt;/code&gt; table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison Operators
&lt;/h2&gt;

&lt;p&gt;Comparison operators are the ones you'll use every single day. They return a boolean for every row, and &lt;code&gt;WHERE&lt;/code&gt; keeps only the rows where the result is &lt;code&gt;true&lt;/code&gt;. The core set is &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt; (or &lt;code&gt;!=&lt;/code&gt;), &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;=&lt;/code&gt;, and &lt;code&gt;&amp;gt;=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's a query for a patients table:&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;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patient_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bill_amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bill_amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple enough. The trouble starts when you combine conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logical Operators: AND &amp;amp; OR
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AND&lt;/code&gt; requires both sides to be true. &lt;code&gt;OR&lt;/code&gt; requires at least one. &lt;code&gt;NOT&lt;/code&gt; negates a condition. But they don't have equal priority—&lt;strong&gt;&lt;code&gt;AND&lt;/code&gt; is evaluated before &lt;code&gt;OR&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I wrote this query to find male patients over 40, or anyone with a huge bill:&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;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;gender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Male'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;bill_amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;150000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works but only by luck. The database parsed it as &lt;code&gt;(gender = 'Male' AND age &amp;gt; 40) OR (bill_amount &amp;gt; 150000)&lt;/code&gt;, which is what I intended. But if I'd added another &lt;code&gt;AND&lt;/code&gt; later, the grouping could have shifted silently.&lt;/p&gt;

&lt;p&gt;The fix is boring and essential: &lt;strong&gt;use parentheses&lt;/strong&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="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Male'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bill_amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;150000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the logic is unmistakable to the next person who reads it—including future you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Range and List Filtering: BETWEEN and IN
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BETWEEN&lt;/code&gt; is shorthand for a range check, and in PostgreSQL it's &lt;strong&gt;inclusive on both ends&lt;/strong&gt;. &lt;code&gt;a BETWEEN x AND y&lt;/code&gt; is exactly &lt;code&gt;a &amp;gt;= x AND a &amp;lt;= y&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I used it to find patients with bills in a middle band:&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;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bill_amount&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a patient's bill is exactly &lt;code&gt;50000&lt;/code&gt;, they appear in the results. Same for exactly &lt;code&gt;10000&lt;/code&gt;. This tripped me up at first—I assumed &lt;code&gt;BETWEEN&lt;/code&gt; might be exclusive on the upper end. It isn't. I verified with a row where &lt;code&gt;bill_amount = 50000&lt;/code&gt;, and it came back.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IN&lt;/code&gt; is the cleaner way to check against a list:&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;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Nairobi'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mombasa'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Kisumu'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logically, &lt;code&gt;IN (a, b, c)&lt;/code&gt; is the same as &lt;code&gt;= a OR = b OR = c&lt;/code&gt;. For short lists, readability is the main win. For very long lists, consider &lt;code&gt;VALUES&lt;/code&gt; + &lt;code&gt;JOIN&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  NULL
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NULL&lt;/code&gt; means "unknown," not "empty." So &lt;code&gt;column = NULL&lt;/code&gt; doesn't return true—it returns &lt;code&gt;NULL&lt;/code&gt; (which &lt;code&gt;WHERE&lt;/code&gt; treats as not-true), and you get zero rows.&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="c1"&gt;-- WRONG: returns nothing&lt;/span&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;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bill_amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- RIGHT: use IS NULL&lt;/span&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;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bill_amount&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to treat &lt;code&gt;NULL&lt;/code&gt; as a comparable value (for example, checking whether two columns differ even when one is &lt;code&gt;NULL&lt;/code&gt;), PostgreSQL offers &lt;code&gt;IS DISTINCT FROM&lt;/code&gt; and &lt;code&gt;IS NOT DISTINCT FROM&lt;/code&gt;. For everyday filtering, &lt;code&gt;IS NULL&lt;/code&gt; and &lt;code&gt;IS NOT NULL&lt;/code&gt; are all you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  LIKE vs. ILIKE: Case Sensitivity Matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LIKE&lt;/code&gt; does pattern matching with two wildcards: &lt;code&gt;%&lt;/code&gt; matches any number of characters, &lt;code&gt;_&lt;/code&gt; matches exactly one.&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;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%@gmail.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This finds addresses ending in &lt;code&gt;@gmail.com&lt;/code&gt;. It will &lt;strong&gt;not&lt;/strong&gt; match &lt;code&gt;@Gmail.com&lt;/code&gt; or &lt;code&gt;@GMAIL.COM&lt;/code&gt;—&lt;code&gt;LIKE&lt;/code&gt; is case-sensitive.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ILIKE&lt;/code&gt; is the case-insensitive version, and it's a PostgreSQL extension:&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;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'%@gmail.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;@gmail.com&lt;/code&gt;, &lt;code&gt;@Gmail.com&lt;/code&gt;, and &lt;code&gt;@GMAIL.COM&lt;/code&gt; all match.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real Business Query
&lt;/h2&gt;

&lt;p&gt;A clinic wants to text young adult men who don't live in Nairobi, have no insurance, and have a recorded phone number:&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="c1"&gt;-- Which male patients aged 18-35, NOT living in Nairobi,&lt;/span&gt;
&lt;span class="c1"&gt;-- with no insurance and a recorded phone number, should be contacted?&lt;/span&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;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;gender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Male'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Nairobi'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;insurance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'No'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="k"&gt;IS&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;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each line uses a different operator, and together they express a rule that would be painful to describe in plain English. &lt;/p&gt;

&lt;h2&gt;
  
  
  Operator Precedence
&lt;/h2&gt;

&lt;p&gt;When an expression mixes operators, SQL follows a fixed evaluation order. Simplified, from highest to lowest: &lt;strong&gt;arithmetic → comparison → &lt;code&gt;NOT&lt;/code&gt; → &lt;code&gt;AND&lt;/code&gt; → &lt;code&gt;OR&lt;/code&gt;&lt;/strong&gt;. When unsure, just add parentheses—it's the safest and most readable approach.&lt;/p&gt;

&lt;p&gt;If you take one thing from this post, make it this: &lt;strong&gt;never write &lt;code&gt;= NULL&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It doesn't show errors or warnings.&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="c1"&gt;-- This will never match anything&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bill_amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Always use this instead&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bill_amount&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same principle applies to &lt;code&gt;&amp;lt;&amp;gt; NULL&lt;/code&gt; and &lt;code&gt;NOT IN&lt;/code&gt; with &lt;code&gt;NULL&lt;/code&gt; in the list—&lt;code&gt;NULL&lt;/code&gt; propagates through comparisons and can surprise you. When in doubt, handle &lt;code&gt;NULL&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;p&gt;Operators are the grammar of SQL filtering. The small rules that aren't obvious from the syntax are what trip beginners up: &lt;code&gt;AND&lt;/code&gt; before &lt;code&gt;OR&lt;/code&gt;, &lt;code&gt;BETWEEN&lt;/code&gt; is inclusive, &lt;code&gt;NULL&lt;/code&gt; isn't equal to anything, and &lt;code&gt;LIKE&lt;/code&gt; cares about case.&lt;/p&gt;

&lt;p&gt;Don't try to memorize every operator at once. Start with &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;AND&lt;/code&gt;, and &lt;code&gt;OR&lt;/code&gt;, then move on to &lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;BETWEEN&lt;/code&gt;, and &lt;code&gt;LIKE&lt;/code&gt;. The rest can wait until you actually need them.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>datascience</category>
      <category>database</category>
    </item>
  </channel>
</rss>
