<?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: Carlos Saldaña</title>
    <description>The latest articles on DEV Community by Carlos Saldaña (@csalda3a).</description>
    <link>https://dev.to/csalda3a</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F492159%2F100251ec-0f45-4f66-af7a-1d84c679344b.jpg</url>
      <title>DEV Community: Carlos Saldaña</title>
      <link>https://dev.to/csalda3a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/csalda3a"/>
    <language>en</language>
    <item>
      <title>Debugging PostgreSQL Performance</title>
      <dc:creator>Carlos Saldaña</dc:creator>
      <pubDate>Tue, 02 Jun 2026 15:11:22 +0000</pubDate>
      <link>https://dev.to/csalda3a/debugging-postgresql-performance-4nlh</link>
      <guid>https://dev.to/csalda3a/debugging-postgresql-performance-4nlh</guid>
      <description>&lt;p&gt;Let's chat about performance, optimization, and debugging a PostgreSQL database. Can you read &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; output fluently? Do you know how to find which tenant is causing a slow query, how to use &lt;code&gt;pg_stat_statements&lt;/code&gt;, how to spot lock contention? If you have a hard time answering those, don't worry. Today we're going to cover all of them and practice how to get better at it.&lt;/p&gt;

&lt;p&gt;The goal here: you should be able to look &lt;a href="https://dev.tourl"&gt;&lt;/a&gt;at a slow API endpoint and trace it down to the exact query, the exact tenant, the exact missing index, in under 10 minutes. That's the bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  EXPLAIN ANALYZE: read the plan, not the vibes
&lt;/h2&gt;

&lt;p&gt;Let's start with &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; because everything else builds on it. &lt;/p&gt;

&lt;p&gt;So why does this exist?&lt;/p&gt;

&lt;p&gt;The query planner makes a cost-based guess about the best way to execute your SQL query. &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; actually runs the query and shows you whether that guess was correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always use this form:&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;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ANALYZE&lt;/strong&gt; → Runs the query and reports what actually happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BUFFERS&lt;/strong&gt; → Shows cache hits vs. disk reads. Without it, performance analysis is incomplete.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;code&gt;ANALYZE&lt;/code&gt; executes the query for real, including writes. Use &lt;code&gt;BEGIN&lt;/code&gt; and &lt;code&gt;ROLLBACK&lt;/code&gt; to test without committing changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The single most important skill: estimated vs actual&lt;/strong&gt;&lt;br&gt;
Every node prints two row counts. When &lt;code&gt;rows&lt;/code&gt; (estimate) and &lt;code&gt;actual rows&lt;/code&gt; diverge by more than ~10x, the planner is working from bad information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to read the tree&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The plan is a tree. Read the most-indented node first. That's where execution starts. Costs and times are cumulative: a parent node's time includes its children.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;actual time=X..Y&lt;/code&gt; → X is time to first row, Y is time to last row, per loop. If &lt;code&gt;loops=5000&lt;/code&gt;, the real total for that node is &lt;code&gt;Y × 5000&lt;/code&gt;. This is how a "0.05 ms" node secretly costs 4 seconds.&lt;/p&gt;

&lt;p&gt;The best resource if you're starting out is &lt;a href="https://explain.depesz.com/" rel="noopener noreferrer"&gt;explain.depesz.com&lt;/a&gt;. You can paste any EXPLAIN output and it visualizes the slow nodes. Use it on your own queries until you can predict what it'll say before you paste.&lt;/p&gt;
&lt;h2&gt;
  
  
  Index Design
&lt;/h2&gt;

&lt;p&gt;I'm not going to go deep into index design here. Instead, read &lt;a href="https://use-the-index-luke.com/" rel="noopener noreferrer"&gt;Use The Index, Luke!&lt;/a&gt; by Markus Winand.&lt;/p&gt;

&lt;p&gt;It's free, easy to read, and probably the best resource available on SQL indexes. Many senior engineers use indexes every day, but haven't taken the time to read it from start to finish.&lt;/p&gt;

&lt;p&gt;If you read one resource on indexing, make it this one.&lt;/p&gt;

&lt;p&gt;If you've ever thought indexes were some kind of database magic, you're not alone. Most of the confusion disappears once you realize they're just a clever way of keeping data sorted.&lt;/p&gt;

&lt;p&gt;The key idea is that a B-tree index is a sorted copy of one or more columns, plus pointers to the corresponding table rows.&lt;/p&gt;

&lt;p&gt;The fact that it's sorted is what gives it its power. PostgreSQL can quickly find the rows it needs instead of reading the entire table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule that matters most: leftmost prefix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An index on &lt;code&gt;(tenant_id, created_at, status)&lt;/code&gt; is sorted by &lt;code&gt;tenant_id&lt;/code&gt;, then within&lt;br&gt;
each tenant by &lt;code&gt;created_at&lt;/code&gt;, then by &lt;code&gt;status&lt;/code&gt;. So it can serve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;✅ WHERE tenant_id = ?
✅ WHERE tenant_id = ? AND created_at &amp;gt; ?
✅ WHERE tenant_id = ? AND created_at &amp;gt; ? AND status = ?
✅ ORDER BY created_at      (within a fixed tenant_id)
❌ WHERE created_at &amp;gt; ?     (skips the leading column, can't seek)
❌ WHERE status = ?         (skips two leading columns)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can only use a contiguous prefix, starting from the left. This is why &lt;strong&gt;column&lt;br&gt;
order in a composite index is a design decision&lt;/strong&gt;, not an afterthought.&lt;/p&gt;
&lt;h3&gt;
  
  
  Things that quietly break index usage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Using a function on the indexed column&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WHERE lower(email) = ?&lt;/code&gt; can't use a regular index on &lt;code&gt;email&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fix: create an expression index:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CREATE INDEX ON users (lower(email));&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Modifying the column in the filter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WHERE created_at + interval '1 day' &amp;gt; now()&lt;/code&gt; makes it harder to use the index.&lt;/p&gt;

&lt;p&gt;Instead, apply the calculation to the constant side of the comparison.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Leading wildcards&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LIKE '%foo'&lt;/code&gt; can't efficiently use a B-tree index.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LIKE 'foo%'&lt;/code&gt; can.&lt;/p&gt;

&lt;p&gt;For substring searches, use a trigram index (&lt;code&gt;pg_trgm&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Type mismatches&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Implicit casts on the column side can prevent index usage. Make sure the types match.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Low-selectivity columns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An index on a boolean column or a status column with only a few possible values is often not useful by itself. PostgreSQL may choose a sequential scan instead. Consider a composite index or a partial index.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Index features worth knowing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Covering indexes (&lt;code&gt;INCLUDE&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add extra columns to the index so PostgreSQL can answer the query without reading the table. This can enable an &lt;strong&gt;Index Only Scan&lt;/strong&gt;, which is often faster.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Partial indexes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Index only the rows you actually query. The index is smaller and cheaper to maintain. Often faster, too.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;B-tree is the one you'll use 95% of the time.&lt;/strong&gt; Learn it first. The others exist for specialized data types and query patterns.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  The trade-off: indexes aren't free
&lt;/h3&gt;

&lt;p&gt;Indexes make reads faster, but they make writes more expensive.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; must update the table and all of its indexes. More indexes mean more disk, more memory, more work on every write.&lt;/p&gt;

&lt;p&gt;Regularly check for unused indexes (&lt;code&gt;pg_stat_user_indexes.idx_scan = 0&lt;/code&gt;) and remove them when appropriate.&lt;/p&gt;

&lt;p&gt;The goal isn't to have more indexes. The goal is to have the fewest indexes needed to support your important queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;pg_stat_statements&lt;/code&gt;: where the server actually spends its life
&lt;/h2&gt;

&lt;p&gt;When someone tells me "the database is slow," this is usually the first place I look. Most performance problems aren't caused by one terrible query. They're caused by a handful of queries quietly consuming time all day long.&lt;/p&gt;

&lt;p&gt;A production application can execute thousands of different queries. You can't run &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; on all of them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pg_stat_statements&lt;/code&gt; solves this problem by collecting execution statistics for every normalized query. Queries that differ only by parameter values are grouped together, making it easy to see where the database is spending its time.&lt;/p&gt;

&lt;p&gt;To enable it, add the following to &lt;code&gt;postgresql.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;shared_preload_libraries&lt;/span&gt; = &lt;span class="s1"&gt;'pg_stat_statements'&lt;/span&gt;
&lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;.&lt;span class="n"&gt;track&lt;/span&gt; = &lt;span class="n"&gt;all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart PostgreSQL, then create the 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;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Learn these columns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;calls&lt;/code&gt; → How many times the query ran.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;total_exec_time&lt;/code&gt; → Total time spent executing the query.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mean_exec_time&lt;/code&gt; → Average execution time per call.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rows&lt;/code&gt; → Total rows returned or affected.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shared_blks_hit&lt;/code&gt; vs &lt;code&gt;shared_blks_read&lt;/code&gt; → Cache hits vs disk reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most useful question you can ask in production is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where is my database spending most of its time?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This query answers 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;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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="mi"&gt;80&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;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;calls&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="n"&gt;total_exec_time&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;total_ms&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="n"&gt;mean_exec_time&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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mean_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;rows&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="n"&gt;shared_blks_hit&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="k"&gt;nullif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shared_blks_hit&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;shared_blks_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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;hit_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&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;total_exec_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which queries are globally expensive,&lt;/li&gt;
&lt;li&gt;slowest average queries,&lt;/li&gt;
&lt;li&gt;most frequently executed queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is often where real tuning starts in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sort by total time, not average time&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many engineers focus on &lt;code&gt;mean_exec_time&lt;/code&gt;, but &lt;code&gt;total_exec_time&lt;/code&gt; is usually the more important metric.&lt;/p&gt;

&lt;p&gt;A query that takes 3 ms and runs 2 million times per hour can consume far more resources than a report that takes 2 seconds and runs twice a day.&lt;/p&gt;

&lt;p&gt;Think about it this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sort by &lt;strong&gt;total execution time&lt;/strong&gt; to find where the server is spending most of its effort.&lt;/li&gt;
&lt;li&gt;Sort by &lt;strong&gt;average execution time&lt;/strong&gt; to find individually slow queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both views are useful, but &lt;code&gt;total_exec_time&lt;/code&gt; is where you'll usually find the biggest wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resetting the stats&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you want to start a fresh measurement window:&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;pg_stat_statements_reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the "Top 20 by total execution time" report every week is one of the simplest ways to catch performance problems before they become outages.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;auto_explain&lt;/code&gt;: catch the slow query you can't reproduce
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pg_stat_statements&lt;/code&gt; tells you &lt;strong&gt;which queries are slow overall&lt;/strong&gt;. What it can't tell you is &lt;em&gt;why&lt;/em&gt; a query was slow at 2 AM during an incident.&lt;/p&gt;

&lt;p&gt;Execution plans can change as data grows, statistics become outdated, or different parameter values are used. A query that's normally fast can suddenly take a very different path.&lt;/p&gt;

&lt;p&gt;That's where &lt;code&gt;auto_explain&lt;/code&gt; helps.&lt;/p&gt;

&lt;p&gt;It automatically logs the full execution plan for queries that exceed a time threshold, capturing the plan that was actually used when the slowdown happened.&lt;/p&gt;

&lt;p&gt;Configure it in &lt;code&gt;postgresql.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;session_preload_libraries&lt;/span&gt; = &lt;span class="s1"&gt;'auto_explain'&lt;/span&gt;

&lt;span class="n"&gt;auto_explain&lt;/span&gt;.&lt;span class="n"&gt;log_min_duration&lt;/span&gt; = &lt;span class="s1"&gt;'500ms'&lt;/span&gt;
&lt;span class="n"&gt;auto_explain&lt;/span&gt;.&lt;span class="n"&gt;log_analyze&lt;/span&gt; = &lt;span class="n"&gt;on&lt;/span&gt;
&lt;span class="n"&gt;auto_explain&lt;/span&gt;.&lt;span class="n"&gt;log_buffers&lt;/span&gt; = &lt;span class="n"&gt;on&lt;/span&gt;
&lt;span class="n"&gt;auto_explain&lt;/span&gt;.&lt;span class="n"&gt;log_nested_statements&lt;/span&gt; = &lt;span class="n"&gt;on&lt;/span&gt;
&lt;span class="n"&gt;auto_explain&lt;/span&gt;.&lt;span class="n"&gt;log_format&lt;/span&gt; = &lt;span class="s1"&gt;'text'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use it carefully in production&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;log_analyze = on&lt;/code&gt; measures actual execution times, which adds overhead to every logged query.&lt;/p&gt;

&lt;p&gt;A safe approach is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with a relatively high threshold (for example, 500 ms or 1 second).&lt;/li&gt;
&lt;li&gt;Leave &lt;code&gt;log_analyze&lt;/code&gt; off initially.&lt;/li&gt;
&lt;li&gt;Enable more detailed logging only when investigating a performance issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why it matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think of the tools as solving different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pg_stat_statements&lt;/code&gt; answers: &lt;strong&gt;"Which queries are costing us the most?"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auto_explain&lt;/code&gt; answers: &lt;strong&gt;"What plan did PostgreSQL actually use when this query became slow?"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together they form one of the most valuable performance debugging tool sets in PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pg_stat_statements&lt;/code&gt; helps you find the problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;auto_explain&lt;/code&gt; helps you understand it.&lt;/p&gt;

&lt;p&gt;When you have both, performance investigations become much less about guessing and much more about reading the evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;pg_locks&lt;/code&gt;: who is blocking whom
&lt;/h2&gt;

&lt;p&gt;When a query suddenly stops making progress, the problem is often not the query itself. It's waiting for a lock.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pg_locks&lt;/code&gt; shows one row for every lock currently held or being waited on by PostgreSQL sessions.&lt;/p&gt;

&lt;p&gt;The most important column is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;granted = true&lt;/code&gt; → the lock has been acquired.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;granted = false&lt;/code&gt; → the session is waiting for a lock held by someone else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Joining it with &lt;code&gt;pg_stat_activity&lt;/code&gt; gives you the lock plus the SQL behind 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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;locktype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;granted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&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;query&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_locks&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;granted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A waiting query is often a symptom of lock contention somewhere else in the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deadlocks: not a database failure, a concurrency bug
&lt;/h3&gt;

&lt;p&gt;A deadlock happens when two transactions are waiting on each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Transaction A holds lock 1 and wants lock 2.&lt;/li&gt;
&lt;li&gt;Transaction B holds lock 2 and wants lock 1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither transaction can continue, creating a cycle.&lt;/p&gt;

&lt;p&gt;PostgreSQL detects this situation automatically (after &lt;code&gt;deadlock_timeout&lt;/code&gt;, which defaults to 1 second) and resolves it by aborting one of the transactions with a &lt;strong&gt;deadlock detected&lt;/strong&gt; error.&lt;/p&gt;

&lt;p&gt;Your application should be prepared to catch this error and retry the transaction.&lt;/p&gt;

&lt;p&gt;One lesson that's easy to forget: a deadlock isn't a database failure. It's a concurrency bug. PostgreSQL is doing exactly what it should, so the system can move forward.&lt;/p&gt;

&lt;p&gt;For lock contention specifically, the canonical resource is the Postgres wiki page "Lock Monitoring." Search for it, bookmark it; the query at the bottom that shows blocking chains is one you'll run many times in your career.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use it with &lt;code&gt;pg_stat_activity&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;On its own, &lt;code&gt;pg_locks&lt;/code&gt; is hard to read: raw &lt;code&gt;pid&lt;/code&gt;s and lock modes, no context.&lt;/p&gt;

&lt;p&gt;That's why the query above joins it with &lt;code&gt;pg_stat_activity&lt;/code&gt;. Together they show you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which queries are currently waiting.&lt;/li&gt;
&lt;li&gt;Which sessions are blocking them.&lt;/li&gt;
&lt;li&gt;How long they've been waiting.&lt;/li&gt;
&lt;li&gt;The SQL being executed by both sides.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When debugging database issues, think of these views as a pair:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pg_stat_activity&lt;/code&gt; → What is everyone doing?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg_locks&lt;/code&gt; → Who is blocking whom?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together they help you diagnose lock contention, blocked queries, and deadlocks before they turn into production incidents.&lt;/p&gt;

&lt;p&gt;Once you've worked through all of this, the natural next step is &lt;strong&gt;MVCC and VACUUM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's where you'll learn why &lt;code&gt;idle in transaction&lt;/code&gt; causes bloat, why dead rows affect query performance, and why a perfectly reasonable index can still be slow.&lt;/p&gt;

&lt;p&gt;In my experience, MVCC is the point where PostgreSQL starts to make sense as a system instead of a collection of features. Many performance problems that look mysterious become obvious once you understand how PostgreSQL stores and cleans up row versions.&lt;/p&gt;

</description>
      <category>database</category>
      <category>performance</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
    <item>
      <title>A prompt is not a conversation. It's a component contract.</title>
      <dc:creator>Carlos Saldaña</dc:creator>
      <pubDate>Mon, 25 May 2026 21:48:25 +0000</pubDate>
      <link>https://dev.to/csalda3a/a-prompt-is-not-a-conversation-its-a-component-contract-4jk8</link>
      <guid>https://dev.to/csalda3a/a-prompt-is-not-a-conversation-its-a-component-contract-4jk8</guid>
      <description>&lt;p&gt;Most of us use LLMs by trial and error. This post gives you a structure: the building blocks of an LLM, and a reusable template for writing production prompts.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is an LLM?
&lt;/h3&gt;

&lt;p&gt;Foundation models are very large models pretrained on internet-data; that's what builds Generative AI. With a foundation model, you can adapt one pretrained model to many tasks.&lt;/p&gt;

&lt;p&gt;A Large Language Model (LLM) is a foundation model for text, and at its core, the same FM can be used for many tasks: summarisation, classification, translation, code generation.&lt;/p&gt;

&lt;p&gt;So what an LLM does is predict the next word (next token) in a sequence. At each step it checks the surrounding context of what it has seen so far and then produces a probability distribution over the possible next tokens. Running this in a loop produces fluent, coherent new content. So basically, an LLM gets text and returns text. The input is called the prompt and the output is called completion.&lt;/p&gt;

&lt;h3&gt;
  
  
  What goes into a prompt
&lt;/h3&gt;

&lt;p&gt;A prompt is any input given to a generative model to produce a desired output. Prompt engineering is the practice of designing and refining those prompts to get the best possible results from the model. Refining a prompt means experimenting with the factors that influence the model's output. A vague prompt can lead to many reasonable responses; every constraint you add reduces the number of possible responses.&lt;/p&gt;

&lt;p&gt;A well-structured prompt is usually assembled from four parts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Building block&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Role&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instruction&lt;/td&gt;
&lt;td&gt;The task you want performed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;Relevant background that frames the situation for the model.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input data&lt;/td&gt;
&lt;td&gt;The specific content the task should operate on.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output indicator&lt;/td&gt;
&lt;td&gt;A description (or example) of the form the response should take.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The instruction and context here are two of the template slots, &lt;strong&gt;Task&lt;/strong&gt; and &lt;strong&gt;Context&lt;/strong&gt;, we'll pull together at the end.&lt;/p&gt;

&lt;p&gt;And best practice for writing prompts can be organised into four dimensions. Strong prompts attend to all four.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Dimension&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Practice&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clarity&lt;/td&gt;
&lt;td&gt;Use simple, direct language. Avoid ambiguous or overly complex terminology so the prompt is easily understood.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;Provide relevant background and specific details to guide the model's understanding of the situation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precision&lt;/td&gt;
&lt;td&gt;Clearly state the type of response you want, and use examples to illustrate the expected output.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role-play / Persona&lt;/td&gt;
&lt;td&gt;Write the prompt from the perspective of a specific character or expert, with enough detail for the model to assume that role effectively.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Think of a prompt like a search beam. Vague = wide beam, the model lands somewhere in a large valid region. Each constraint narrows the beam. Specificity isn't politeness toward the model; it's aiming.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who reads the output: code or a person
&lt;/h3&gt;

&lt;p&gt;The output is the more interesting part for us. An LLM's output has two possible audiences, a parser and a person, and you write a contract for each. Even when a person reads the output, "looks fine" isn't the same as "matches what I needed." When an LLM's output is read by &lt;em&gt;code&lt;/em&gt; instead of &lt;em&gt;eyes&lt;/em&gt;, the output is an API response and the prompt is its schema. A human forgives a messy answer; &lt;code&gt;json.loads()&lt;/code&gt; doesn't. It either succeeds or throws. Without an explicit spec, the &lt;em&gt;model&lt;/em&gt; decides format, length, tone, and depth, and it picks something plausible but generic. Controlling output here means moving that decision from the model to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two kinds of control:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Format control:&lt;/strong&gt; the &lt;em&gt;shape&lt;/em&gt; of the output (JSON keys and types; or prose vs. bullets vs. table, headings, sections).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior control:&lt;/strong&gt; what the model may and may not &lt;em&gt;do&lt;/em&gt; (e.g. "valid Terraform only, no comments"; or "concise executive tone, no technical jargon").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Each audience fails differently, and one of them fails silently:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Parser&lt;/strong&gt; (output read by code)&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Person&lt;/strong&gt; (output read by a human)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What breaks&lt;/td&gt;
&lt;td&gt;Markdown fences, chatty preamble, invented/renamed keys, numbers as strings&lt;/td&gt;
&lt;td&gt;Too long, wrong tone, missing a section, pitched at the wrong level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How it breaks&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Loud:&lt;/strong&gt; &lt;code&gt;json.loads()&lt;/code&gt; throws, the pipeline stops; you notice immediately&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Quiet:&lt;/strong&gt; output looks fluent and complete; the gap only shows on a second read, and nobody flags it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A loud failure is annoying; a quiet failure is more dangerous because a subtly off-target answer can go unnoticed.&lt;/p&gt;

&lt;p&gt;There are some mitigations you can apply for better results:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For the parser:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Specify the exact schema.&lt;/li&gt;
&lt;li&gt;Forbid the noise.&lt;/li&gt;
&lt;li&gt;Give one example of the exact shape.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;temperature: 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use native &lt;strong&gt;structured outputs / tool-calling&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;For the person:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;State the structure explicitly.&lt;/li&gt;
&lt;li&gt;Give a length budget.&lt;/li&gt;
&lt;li&gt;Name the audience.&lt;/li&gt;
&lt;li&gt;List the required elements.&lt;/li&gt;
&lt;li&gt;For a recurring format, show one example of the desired output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the point here is that prompt-only format control is a request; decode-time constraints are a guarantee. Treat the model's output as an API response, and the prompt as its schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constraints&lt;/strong&gt; and &lt;strong&gt;Output&lt;/strong&gt; are template slots too: the rules that prune behavior, and the exact shape you contract for.&lt;/p&gt;

&lt;h3&gt;
  
  
  The system and user split
&lt;/h3&gt;

&lt;p&gt;We already saw the role as one of the four dimensions of an effective prompt. It's the part lots of people forget about.&lt;/p&gt;

&lt;p&gt;A prompt can include three types of messages: system, user, and assistant.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System message:&lt;/strong&gt; defines the model's behavior, rules, and overall role.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User message:&lt;/strong&gt; contains the request or input for the current task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assistant message:&lt;/strong&gt; previous responses from the model, used as conversational context or examples.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "type" is usually set through the &lt;code&gt;role&lt;/code&gt; field of each message.&lt;/p&gt;

&lt;p&gt;The system message is the component's configuration, while the user message is the input for a specific call.&lt;/p&gt;

&lt;p&gt;The system prompt defines persistent behavior and rules. The user message provides the variable data for that particular request. The system sets how the component behaves in general; the user message tells it what to do right now.&lt;/p&gt;

&lt;p&gt;Why do roles work? It's not magic.&lt;/p&gt;

&lt;p&gt;When you say, "Act as a senior security engineer," the model shifts its output toward patterns statistically associated with that kind of writing in its training data.&lt;/p&gt;

&lt;p&gt;Likewise, "Explain this to a junior developer" pushes the model toward simpler, more educational, and more heavily explained responses.&lt;/p&gt;

&lt;p&gt;A role doesn't give the model a real personality. It changes the probability distribution of the kind of text the model is likely to generate. This is the &lt;strong&gt;Role&lt;/strong&gt; slot, the first line of the template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the system/user split matters in production:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Caching.&lt;/strong&gt; The system message is usually stable and reused across requests, which makes it ideal for prompt caching. Keep the system prompt consistent, and place most of the changing data in the user message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testability.&lt;/strong&gt; The system prompt is one of the highest-leverage parts of an AI application. Treat it like code: version it, compare changes, and test it carefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security.&lt;/strong&gt; Trusted instructions should live in the system message. Untrusted content (user input, retrieved documents, tool outputs) should stay in the user message. When untrusted text lands somewhere the model treats as instructions, you get &lt;strong&gt;prompt injection&lt;/strong&gt;. Clean separation is the first line of defense.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Showing the model examples
&lt;/h3&gt;

&lt;p&gt;One good prompt gets you far. A few techniques get you further. The most useful one: showing the model examples.&lt;/p&gt;

&lt;p&gt;In few-shot prompting, the prompt includes a few worked demonstrations. The model uses in-context learning to infer the pattern and apply it to the new input.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;few-shot examples are unit tests that double as a spec.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it works&lt;/strong&gt;: the model is a pattern continuator. A few input→output pairs establish a strong, low-ambiguity pattern, and the model continues it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use which:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Zero-shot:&lt;/strong&gt; simple, common tasks the model has clearly seen many times.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  {
    "role": "user",
    "content": "Summarize this article in 3 bullet points."
  }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;One-shot:&lt;/strong&gt; when you mainly need to pin the &lt;em&gt;format&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  {
    "role": "user",
    "content": "Convert countries to JSON.\n\nExample:\nFrance -&amp;gt; {\"country\": \"France\"}\n\nNow convert:\nBrazil"
  }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Few-shot:&lt;/strong&gt; classification, structured output, code style, anything with a specific schema or edge cases the model wouldn't guess.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those worked demonstrations are the &lt;strong&gt;Examples&lt;/strong&gt; slot.&lt;/p&gt;

&lt;p&gt;In the example below, the last message is the new input; the model produces the &lt;code&gt;assistant&lt;/code&gt; turn.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
   {"role": "user",      "content": "Today the weather is fantastic"},
   {"role": "assistant", "content": "positive"},
   {"role": "user",      "content": "I don't like your attitude"},
   {"role": "assistant", "content": "negative"},
   {"role": "user",      "content": "That shot selection was awful"},
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tokens are what you pay for
&lt;/h3&gt;

&lt;p&gt;LLMs are not free, and the token is the meter. For an LLM, it's the unit of both cost and latency. For most of us the mental model is familiar: tokens are network payload, and each LLM call is a billable, latency-bearing API request.&lt;/p&gt;

&lt;p&gt;Output tokens dominate latency. The model generates text one token at a time, and each new token depends on all the previous ones, so generation has to happen sequentially.&lt;/p&gt;

&lt;p&gt;Input works differently: the prompt is processed in a single parallel "prefill" pass, which is relatively fast (even though you still pay for those tokens).&lt;/p&gt;

&lt;p&gt;Four levers do most of the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compress the input&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of sending entire documents, summarize them first or extract only the relevant parts. Most prompts ship context the model never reads.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Limit and structure the output&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;max_tokens&lt;/code&gt;, prefer structured formats like JSON or arrays instead of long prose, and ask for concise summaries such as "keep under 120 tokens."&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use the right model for the task&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Small models are faster and cheaper for simple work like classification, routing, or extraction. Save the stronger models for tasks that actually require reasoning or synthesis.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use caching&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are two different kinds:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt/prefix caching&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reuses stable prompt sections like system prompts, examples, or reference documents. Since the provider caches this server-side, you avoid recomputing the expensive input processing step.&lt;/p&gt;

&lt;p&gt;Practical implication: put stable content first and variable content last to maximize cache hits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Response/semantic caching&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your own infrastructure stores previous answers and reuses them when the same or a very similar request appears again. This caches outputs, not prompts.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A Reusable Prompt Template
&lt;/h3&gt;

&lt;p&gt;All that we've seen here gives us the structure to build a template to use when working on a production prompt.&lt;/p&gt;

&lt;p&gt;The template &lt;strong&gt;R-T-C-C-E-O&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;[ROLE]        Who the model acts as. Sets the output distribution.
[TASK]        The one thing to do, stated unambiguously.
[CONTEXT]     Inputs, background, data; clearly delimited from instructions.
[CONSTRAINTS] Rules that prune the space. Each maps to a real failure mode.
[EXAMPLES]    1–3 representative input→output pairs (for structured/edge-case tasks).
[OUTPUT]      The exact shape of the response. Schema + example if machine-consumed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every prompt needs all six, but every prompt should be a &lt;em&gt;deliberate&lt;/em&gt; subset, not an accident.&lt;/p&gt;

&lt;p&gt;Finally, let's make the production checklist, a pre-flight pass before a prompt ships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role&lt;/strong&gt; set and matched to the task?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task:&lt;/strong&gt; could a competent stranger misread it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraints:&lt;/strong&gt; does each map to a failure mode you've actually seen? Drop the decorative ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output contract:&lt;/strong&gt; explicit; schema + example if consumed by code?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format guarantee:&lt;/strong&gt; decode-time constraint (structured output / tool call), not just a worded request?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples:&lt;/strong&gt; present for classification/structured work; diverse, consistent, minimal?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sampling:&lt;/strong&gt; &lt;code&gt;temperature&lt;/code&gt; matched to the task?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token budget:&lt;/strong&gt; &lt;code&gt;max_tokens&lt;/code&gt; capped; output format compact?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; right-sized, not just "the strong one"?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache-friendliness:&lt;/strong&gt; stable content first, variable content last?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Injection safety:&lt;/strong&gt; instructions in the system message, data in the user message?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned &amp;amp; tested:&lt;/strong&gt; in source control with a few regression cases?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we covered six slots: Role, Task, Context, Constraints, Examples, Output, plus a pre-flight checklist. Run every production prompt through both, and you've turned a hopeful request into a tested component. &lt;strong&gt;A prompt is not a conversation. It's a component contract.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>promptengineering</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Protocol Behind Python's Containers</title>
      <dc:creator>Carlos Saldaña</dc:creator>
      <pubDate>Tue, 19 May 2026 14:35:05 +0000</pubDate>
      <link>https://dev.to/csalda3a/the-protocol-behind-pythons-containers-10km</link>
      <guid>https://dev.to/csalda3a/the-protocol-behind-pythons-containers-10km</guid>
      <description>&lt;p&gt;Lists, tuples, dicts, and sets look like four different things. Underneath, they share the same idea: they all implement a small set of protocols, and once you see that, the rest stops feeling arbitrary. &lt;/p&gt;

&lt;h3&gt;
  
  
  What is a protocol?
&lt;/h3&gt;

&lt;p&gt;A protocol in Python is an informal contract defined by behavior, not by declaration. Anything that behaves like a sequence is a sequence, as far as Python cares. This style of typing has a name, it’s called structural typing, or as most of us know it, duck typing.("if it walks like a duck...").&lt;/p&gt;

&lt;p&gt;Think of this like the USB port. A USB port doesn't ask "are you a keyboard?" or "are you a hard drive?" It exposes a physical shape and a data protocol. Anything that fits the shape and speaks the protocol works.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Iterators and Iterables&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If you only learn one thing about Python containers, learn the difference between an iterable and an iterator.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;iterable&lt;/strong&gt; is an object that we can iterate over, it's an object that can produce an iterator. To make this happen, it implements &lt;code&gt;__iter__()&lt;/code&gt;, which returns a fresh iterator each time it's called.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;iterator&lt;/strong&gt; is an object that &lt;em&gt;produces values one at a time&lt;/em&gt;. It implements &lt;code&gt;__next__()&lt;/code&gt; (and, by convention, &lt;code&gt;__iter__()&lt;/code&gt; returning &lt;code&gt;self&lt;/code&gt;). It raises &lt;code&gt;StopIteration&lt;/code&gt; when there are no more values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are a few &lt;strong&gt;built-in iterables&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List&lt;/li&gt;
&lt;li&gt;Tuples&lt;/li&gt;
&lt;li&gt;Strings&lt;/li&gt;
&lt;li&gt;Dictionaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s try an example to understand why a list is iterable but not an iterator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;nums&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="mi"&gt;2&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="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;list&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt;
&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;for&lt;/code&gt; loop is doing this exact dance. &lt;code&gt;for i in nums:&lt;/code&gt; it’s roughly doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_it&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;StopIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the whole &lt;strong&gt;protocol&lt;/strong&gt;. Once you understand this, things like &lt;code&gt;for&lt;/code&gt; loops, generators, &lt;code&gt;zip&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, comprehensions, unpacking, and &lt;code&gt;*args&lt;/code&gt; all start to work the same way in your mind.&lt;/p&gt;

&lt;p&gt;I'm more visual, so here's a sequence diagram showing the interaction over time between two objects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5jbmzqnfmjaqja488fm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5jbmzqnfmjaqja488fm.png" alt="A for loop" width="800" height="743"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Built-in Containers
&lt;/h3&gt;

&lt;p&gt;This is the good part. Most of us learn Python's containers at the surface: lists are mutable and ordered, tuples are immutable and ordered, dicts are key-value pairs, sets are unique and unordered. That's correct but also useless most of the time. It tells you nothing about performance. Nothing about why your set of dicts blew up with a TypeError. Nothing about why your queue is suddenly O(n²). &lt;/p&gt;

&lt;p&gt;Here's what to focus on instead. Every container in Python is three things at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A data structure: how it's stored in memory and how fast operations are.&lt;/li&gt;
&lt;li&gt;A set of protocols: the special methods it supports.&lt;/li&gt;
&lt;li&gt;A semantic choice: the meaning you communicate to other developers by using it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  List: the dynamic array
&lt;/h4&gt;

&lt;p&gt;A python list is a dynamic array (called a vector or &lt;code&gt;ArrayList&lt;/code&gt; in other languages).&lt;/p&gt;

&lt;p&gt;Under the hood, it’s a block of pointers to python objects, with extra space preallocated at the end for appending new items.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────┬────┬────┬────┬────┬────┬────┬────┐
│ p0 │ p1 │ p2 │ p3 │ ▒▒ │ ▒▒ │ ▒▒ │ ▒▒ │   length=4, capacity=8
└────┴────┴────┴────┴────┴────┴────┴────┘
  ↑    ↑    ↑    ↑
  obj  obj  obj  obj

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one detail is what determines the entire complexity table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lst[i]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lst.append(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O(1) amortized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lst.pop()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lst.pop(0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lst.insert(0, x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x in lst&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;len(lst)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Protocol:&lt;/strong&gt; &lt;code&gt;MutableSequence&lt;/code&gt; (which gives you &lt;code&gt;Sequence&lt;/code&gt; + &lt;code&gt;Iterable&lt;/code&gt; + &lt;code&gt;Container&lt;/code&gt; + &lt;code&gt;Sized&lt;/code&gt; + &lt;code&gt;Reversible&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic intent:&lt;/strong&gt; An ordered, mutable collection of items that I expect to iterate over, append to, and access by index.&lt;/p&gt;

&lt;p&gt;A common mistake is to use a &lt;code&gt;list&lt;/code&gt; as a FIFO queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is pretty expensive, the &lt;code&gt;pop(0)&lt;/code&gt; shifts every remaining element one position to the left, and remember this operation is &lt;code&gt;O(n)&lt;/code&gt;. If this runs inside a loop it can accidentally become &lt;code&gt;O(n²)&lt;/code&gt; .&lt;/p&gt;

&lt;h4&gt;
  
  
  Tuple: the record
&lt;/h4&gt;

&lt;p&gt;We think of tuples as immutable lists, but that idea can mislead. &lt;/p&gt;

&lt;p&gt;A tuple is better understood as a record: a fixed-size structure where each position can hold different types of data.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tuples represent structured data.&lt;/li&gt;
&lt;li&gt;Lists represent collections of items.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# List
&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;

&lt;span class="c1"&gt;# Tuple
&lt;/span&gt;&lt;span class="n"&gt;db_row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immutability is a result of this distinction, not the point. &lt;/p&gt;

&lt;p&gt;And because tuples are immutable, they gain a few important properties that lists don’t have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hashable&lt;/li&gt;
&lt;li&gt;Memory efficiency&lt;/li&gt;
&lt;li&gt;Read-only contract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From a protocol perspective, tuples behave like read-only sequences and can also be hashable.&lt;/p&gt;

&lt;p&gt;Semantically, a tuple means: “This is a single thing identified by its contents.”&lt;/p&gt;

&lt;p&gt;This matters a lot in a production application, tuples are commonly used for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compound dictionary keys&lt;/li&gt;
&lt;li&gt;Deduplication&lt;/li&gt;
&lt;li&gt;Cache and memoization keys&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Dict: the hash table
&lt;/h4&gt;

&lt;p&gt;The king. If lists are the most common container in Python, dictionaries are probably the most important. A big part of Python's internals is built on top of dictionaries. &lt;/p&gt;

&lt;p&gt;A dict is a hash table with open addressing. The keys you insert get hashed, and the hash points to a slot.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Average&lt;/th&gt;
&lt;th&gt;Worst&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;d[k]&lt;/code&gt;, &lt;code&gt;d[k] = v&lt;/code&gt;, &lt;code&gt;del d[k]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(n) (hash collisions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;k in d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iteration&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;From a protocol perspective, a &lt;code&gt;dict&lt;/code&gt; implements &lt;code&gt;MutableMapping&lt;/code&gt;, not &lt;code&gt;Sequence&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There are two things you must remember about dictionaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keys must be hashable&lt;/li&gt;
&lt;li&gt;Insertion order is preserved&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Set: the dict without values
&lt;/h4&gt;

&lt;p&gt;A &lt;code&gt;set&lt;/code&gt; is almost the same thing as a dictionary, except it only cares about keys.&lt;/p&gt;

&lt;p&gt;It uses the same hash-table implementation underneath, which means it has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;O(1) average-time lookups, inserts, and removals&lt;/li&gt;
&lt;li&gt;elements must be hashable&lt;/li&gt;
&lt;li&gt;same "no duplicates" guarantee, duplicates are naturally eliminated
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&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;if&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# the antipattern
&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;So in practice it's just a collection of unique keys. From a protocol perspective, a &lt;code&gt;set&lt;/code&gt; implements &lt;code&gt;MutableSet&lt;/code&gt;, which gives you built-in set operations like: &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;^&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Semantically, a set means: “I only care about uniqueness and fast membership checks.”&lt;/p&gt;

&lt;h4&gt;
  
  
  Conclusion
&lt;/h4&gt;

&lt;p&gt;These are some of the most commonly used containers in Python in our applications, and it's important to understand the trade-offs when deciding which one to use. When you reach for a container, try to ask these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What's the access pattern?&lt;/li&gt;
&lt;li&gt;Will it grow?&lt;/li&gt;
&lt;li&gt;Does identity-by-contents matter?&lt;/li&gt;
&lt;li&gt;What does the choice say to the reader?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the common bad choices we covered and what you should reach for instead:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You wrote&lt;/th&gt;
&lt;th&gt;You probably wanted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;list.pop(0)&lt;/code&gt; in a loop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;collections.deque&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;if x in big_list&lt;/code&gt; repeatedly&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;set&lt;/code&gt; (convert once)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;f"{a}:{b}"&lt;/code&gt; as a dict key&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(a, b)&lt;/code&gt; tuple key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List of &lt;code&gt;(key, value)&lt;/code&gt; pairs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dict&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dict[str, None]&lt;/code&gt; for membership&lt;/td&gt;
&lt;td&gt;&lt;code&gt;set&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tuple with named indexes everywhere&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;namedtuple&lt;/code&gt; or &lt;code&gt;dataclass&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you remember one thing: the container you pick is a contract, not a convenience.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datastructures</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
