Setup ๐Ÿ›  Rails 8 App โ€“ Partย 2: Command line, VS Code, RuboCop, Server, Action text, Image processing

In the first part of this guide, we covered setting up a Rails 8 app with essential configurations. In this follow-up, weโ€™ll go over optimizing command-line usage, setting up VS Code for development, running migrations, styling the app, and enabling Action Text.


1. Optimizing Command-Line Usage with Aliases

One of the best ways to speed up development is to create shortcuts for frequently used commands. You can do this by adding aliases to your shell configuration.

Steps to Add an Alias:

  1. Open your shell configuration file:
    vim ~/.zshrc
  2. Search for the alias section:
    <esc> / alias <enter>
  3. Add your alias:
    alias gs="git status"
  4. Save and exit:
    <esc> :wq
  5. Reload your configuration:
    source ~/.zshrc
  6. Use your new alias:
    gs

This method saves time by allowing you to run frequently used commands more quickly.


2. Using Terminal Efficiently in VS Code

By default, VS Code uses `Ctrl + “ to toggle the terminal, which may not be intuitive. You can change this shortcut:

  1. Open VS Code.
  2. Go to Settings โ†’ Keyboard Shortcuts.
  3. Search for Toggle Terminal.
  4. Click Edit and change it to Ctrl + Opt + T for easier access.

3. Setting Up RuboCop in VS Code

RuboCop ensures your Ruby code follows best practices. Hereโ€™s how to set it up:

Checking RuboCop from the Terminal:

rubocop .

VS Code Setup:

  1. Open Command Palette (Cmd + Shift + P) and search for “Lint by RuboCop”.
  2. Go to Extensions Tab and install “VS Code RuboCop”.
  3. In VS Code Settings, search for “Rubocop” and check Ruby -> Rubocop -> Execute Path.
  4. Find the RuboCop installation path: whereis rubocop Example output: ~/.local/share/mise/installs/ruby/3.4.1/bin/rubocop/
  5. Update the Execute Path in VS Code to: ~/.local/share/mise/installs/ruby/3.4.1/bin/
  6. If RuboCop still returns an empty output, check .rubocop.yml in your project: ~/rails/design_studio/.rubocop.yml
  7. If the issue persists, ensure the gem is installed: gem install rubocop
  8. Restart VS Code from the Rails project root: code .

For more details, check the official documentation: RuboCop Usage


4. Running Migrations and Starting the Server

Running Migrations:

rails db:migrate -t

You can check the file: db/schema.rb You can see the active storage tables for attachments and other info.

Starting the Rails Server:

bin/dev

Access your application at: http://localhost:3000/

Check the product routes:


5. Adding Basic Styles

To quickly improve the appearance of your site, add this to application.html.erb:

<%= stylesheet_link_tag "https://cdn.simplecss.org/simple.css" %>

Alternatively, use Tailwind CSS for a more modern approach.

An example of convention over configurations in view file in Rails:

<div id="products">
  <%= render brand.products %>
</div>

Rails will look into the views/products/ folder and fetch right partial file that match.


6. Debugging with Rails Console

If an error occurs, you can inspect variables and data in the Rails console:

rails console


7. Installing Action Text for Rich Text Editing

Action Text is not installed by default but can be added easily:

rails action_text:install

This command:

  • Installs JavaScript dependencies
  • Creates necessary stylesheets
  • Generates database migrations for rich text

Running Migrations:

rails db:migrate -t

If you encounter an error about missing gems:

Could not find gem 'image_processing (~> 1.2)'

Run:

bundle install
rails db:migrate -t

Updating the Product Model:

Add this to app/models/product.rb:

has_rich_text :description

Updating the Form View:

In app/views/products/_form.html.erb, replace the description input with:

<%= form.rich_text_area :description %>

Now, visiting the new product page should display a rich text editor.


8. Solving Image Processing Errors

If you see an error like:

LoadError: Could not open library 'vips.42'

Install libvips to resolve it:

brew install vips


Conclusion

In this second part, we covered:

  • Optimizing terminal usage with aliases.
  • Configuring VS Code for efficient development.
  • Running migrations and starting the Rails server.
  • Enhancing the UI with styles.
  • Debugging using Rails console.
  • Installing and configuring Action Text for rich text support.

Stay tuned for more Rails 8 improvements!

Part 3: https://railsdrop.com/2025/03/25/setup-rails-8-app-git-setup-gitignore-part-3/

To be continued… ๐Ÿš€

Setup ๐Ÿ›  Rails 8 App – Part 1: Setup All Necessary Configurations | Ruby | Rails setup | Kamal | Rails Generations

Ruby on Rails 8 introduces several improvements that make development easier, more secure, and more maintainable. In this guide, we’ll walk through setting up a new Rails 8 application while noting the significant configurations and features that come out of the box.

1. Check Your Ruby and Rails Versions

If not installed Ruby 3.4 and Rails 8.0 please check: https://railsdrop.com/2025/02/11/installing-and-setup-ruby-3-rails-8-vscode-ide-on-macos-in-2025/

Before starting, ensure that you have the correct versions of Ruby and Rails installed:

$ ruby -v
ruby 3.4.1

$ rails -v
Rails 8.0.1

If you donโ€™t have these versions installed, update them using your package manager or version manager (like rbenv or RVM).

2. Create a New Rails 8 Application

Run the following command to create a new Rails app:

$ rails new design_studio

Noteworthy Files and Directories Created

Here are some interesting files and directories that are generated with a new Rails 8 app:

 create  .ruby-version
 create  bin/brakeman
 create  bin/rubocop
 create  bin/docker-entrypoint
 create  .rubocop.yml
 create  .github/workflows
 create  .github/workflows/ci.yml
 create  config/cable.yml
 create  config/storage.yml
 create  config/initializers/content_security_policy.rb
 create  config/initializers/filter_parameter_logging.rb
 create  config/initializers/new_framework_defaults_8_0.rb

Key Takeaways:

  • Security & Code Quality Tools: Brakeman (security scanner) and RuboCop (code style linter) are included by default.
  • Docker Support: The presence of bin/docker-entrypoint suggests better built-in support for containerized deployment.
  • GitHub Actions Workflow: The .github/workflows/ci.yml file provides default CI configurations.
  • Enhanced Security: The content_security_policy.rb initializer helps enforce a strict security policy.
  • New Rails Defaults: The new_framework_defaults_8_0.rb initializer helps manage breaking changes in Rails 8.

Rails automatically creates the following during the creation of the rails new app.

a. Configuring Import Maps and Installing Turbo & Stimulus

Rails 8 still defaults to Import Maps for JavaScript package management, avoiding the need for Node.js and Webpack:

$ rails turbo:install stimulus:install

This creates the following files:

create    config/importmap.rb
create    app/javascript/controllers
create    app/javascript/controllers/index.js
create    app/javascript/controllers/hello_controller.js
append    config/importmap.rb

Key Takeaways:

  • Import Maps: Defined in config/importmap.rb, allowing dependency management without npm.
  • Hotwired Support: Turbo and Stimulus are automatically configured for modern front-end development.
  • Generated Controllers: Stimulus controllers are pre-configured inside app/javascript/controllers/.

b. Deploying with Kamal

Kamal simplifies deployment with Docker and Kubernetes. Rails 8 includes built-in support:

$ bundle binstubs kamal
$ bundle exec kamal init

This results in:

Created .kamal/secrets file
Created sample hooks in .kamal/hooks

Key Takeaways:

  • Automated Deployment Setup: Kamal provides easy-to-use deployment scripts.
  • Secret Management: The .kamal/secrets file ensures secure handling of credentials.
  • Deployment Hooks: Custom hooks allow pre- and post-deployment scripts for automation.

c. Setting Up Caching and Queues with Solid Cache, Queue, and Cable

NOTE: Rails automatically creates this for you while creating the rails app.

Rails 8 includes Solid Cache, Solid Queue, and Solid Cable for enhanced performance and scalability:

$ rails solid_cache:install solid_queue:install solid_cable:install

This creates:

create  config/cache.yml
create  db/cache_schema.rb
create  config/queue.yml

Key Takeaways:

  • Caching Support: config/cache.yml manages application-wide caching.
  • Database-Powered Queue System: Solid Queue simplifies background job management without requiring external dependencies like Sidekiq.
  • Real-Time WebSockets: Solid Cable offers Action Cable improvements for real-time features.

3. Rails 8 Migration Enhancements

Rails 8 provides new shortcuts and syntax improvements for database migrations:

NOT NULL Constraints with ! Shortcut

You can impose NOT NULL constraints directly from the command line using !:

# Example for not null constraints: 
โžœ rails generate model User name:string!

Type Modifiers in Migrations

Rails 8 allows passing commonly used type modifiers directly via the command line. These modifiers are enclosed in curly braces {} after the field type.

# Example for model generation: 
โžœ rails generate model Product name:string description:text
# Example for passing modifiers: 
โžœ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}

Generating a Scaffold for the Product Model

Let’s generate a complete scaffold for our Product model:

โœ— rails generate scaffold product title:string! description:text category:string color:string 'size:string{10}' 'mrp:decimal{7,2}' 'discount:decimal{7,2}' 'rating:decimal{1,1}'
โžœ  design_studio git:(main) โœ— rails -v
Rails 8.0.1
โžœ  design_studio git:(main) โœ— ruby -v
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [arm64-darwin24]
โžœ  design_studio git:(main) โœ— rails generate scaffold product title:string! description:text category:string color:string 'size:string{10}' 'mrp:decimal{7,2}' 'discount:decimal{7,2}' 'rating:decimal{1,1}'

Using the Rails Resource Generator

The rails g resource command is a powerful way to generate models, controllers, migrations, and routes all in one go. This is particularly useful when setting up RESTful resources in a Rails application.

Basic Syntax

โžœ rails g resource product

This command creates the necessary files for a new resource, including:

  • A model (app/models/product.rb)
  • A migration file (db/migrate/)
  • A controller (app/controllers/product_controller.rb)
  • Routes in config/routes.rb
  • A test file (test/controllers/product_controller_test.rb or spec/)

Example Usage

To generate a Post resource with attributes:

โžœ rails g resource Product title:string! description:text brand:references

This will:

  1. Create a Product model with title and description attributes.
  2. Add a brand_id foreign key as a reference.
  3. Apply a NOT NULL constraint on title (! shortcut).
  4. Generate a corresponding migration file.
  5. Set up routes automatically (resources :products).

Running the Migration

After generating a resource, apply the migration to update the database:

โžœ rails db:migrate


Difference Between resource and scaffold

Rails provides both rails g resource and rails g scaffold, but they serve different purposes:

Featurerails g resourcerails g scaffold
Generates a Modelโœ…โœ…
Generates a Migrationโœ…โœ…
Generates a Controllerโœ… (empty actions)โœ… (full CRUD actions)
Generates Views (HTML/ERB)โŒโœ… (index, show, new, edit, etc.)
Generates Routesโœ…โœ…
Generates Helper FilesโŒโœ…
Generates Testsโœ…โœ…
  • rails g resource is minimalโ€”it generates only essential files without view templates. It’s useful when you want more control over how your views and controller actions are built.
  • rails g scaffold is more opinionated and generates full CRUD functionality with prebuilt forms and views, making it ideal for rapid prototyping.

If you need full CRUD functionality quickly, use scaffold. If you prefer a leaner setup with manual control over implementation, use resource.

Conclusion

Rails 8 significantly enhances the development experience with built-in security tools, CI/CD workflows, streamlined deployment via Kamal, and modern front-end support with Turbo & Stimulus. It also improves caching, background jobs, and real-time features with Solid tools.

These improvements make Rails 8 a robust framework for modern web applications, reducing the need for additional dependencies while keeping development efficient and secure.

Enjoy Rails! ๐Ÿš€

Writing Perfect Active Record ๐Ÿ—’๏ธ Queries in Ruby on Rails 8

Active Record (AR) is the heart of Ruby on Rails when it comes to database interactions. Writing efficient and readable queries is crucial for application performance and maintainability. This guide will help you master Active Record queries with real-world examples and best practices.


Setting Up a Sample Database

To demonstrate complex Active Record queries, letโ€™s create a Rails app with a sample database structure containing multiple tables.

Generate Models & Migrations

rails new MyApp --database=postgresql
cd MyApp
rails g model User name:string email:string
rails g model Post title:string body:text user:references
rails g model Comment body:text user:references post:references
rails g model Category name:string
rails g model PostCategory post:references category:references
rails g model Like user:references comment:references
rails db:migrate

Database Schema Overview

  • users: Stores user information.
  • posts: Stores blog posts written by users.
  • comments: Stores comments on posts, linked to users and posts.
  • categories: Stores post categories.
  • post_categories: Join table for posts and categories.
  • likes: Stores likes on comments by users.

Basic Active Record Queries

1. Fetching All Records

User.all  # Returns all users (Avoid using it directly on large datasets as it loads everything into memory)

โš ๏ธ User.all can lead to performance issues if the table contains a large number of records. Instead, prefer pagination (User.limit(100).offset(0)) or batch processing (User.find_each).

2. Finding a Specific Record

User.find(1)  # Finds a user by ID
User.find_by(email: 'john@example.com')  # Finds by attribute

3. Filtering with where vs having

Post.where(user_id: 2)  # Fetch all posts by user with ID 2

Difference between where and having:

  • where is used for filtering records before grouping.
  • having is used for filtering after group operations.

Example:

Post.group(:user_id).having('COUNT(id) > ?', 5)  # Users with more than 5 posts

4. Ordering Results

User.order(:name)  # Order users alphabetically
Post.order(created_at: :desc)  # Order posts by newest first

5. Limiting Results

Post.limit(5)  # Get the first 5 posts

6. Selecting Specific Columns

User.select(:id, :name)  # Only fetch ID and name

7. Fetching Users with a Specific Email Domain

User.where("email LIKE ?", "%@gmail.com")

8. Fetching the Most Recent Posts

Post.order(created_at: :desc).limit(5)

9. Using pluck for Efficient Data Retrieval

User.pluck(:email)  # Fetch only emails as an array

10. Checking if a Record Exists Efficiently

User.exists?(email: 'john@example.com')

11. Including Associations (eager loading to avoid N+1 queries)

Post.includes(:comments).where(comments: { body: 'Great post!' })


Advanced Queries with Joins

1. Joining Tables (INNER JOIN)

Post.joins(:user).where(users: { name: 'John' })

2. Self Join Example

A self-join is useful when dealing with hierarchical relationships, such as an employee-manager structure.

Model Setup

class Employee < ApplicationRecord
  belongs_to :manager, class_name: 'Employee', optional: true
  has_many :subordinates, class_name: 'Employee', foreign_key: 'manager_id'
end

Sample Data

idnamemanager_id
1AliceNULL
2Bob1
3Carol1
4Dave2

Query: Find Employees Who Report to Alice

Employee.joins(:manager).where(managers_employees: { name: 'Alice' })

Result:

idnamemanager_id
2Bob1
3Carol1

This query fetches employees who report to Alice (i.e., those where manager_id = 1).

3. Fetching Users with No Posts (LEFT JOIN with NULL check)

User.left_outer_joins(:posts).where(posts: { id: nil })

4. Counting Posts Per User

User.joins(:posts).group('users.id').count

Complex Queries in Active Record

1. Fetching Posts with the Most Comments

Post.joins(:comments)
    .group('posts.id')
    .order('COUNT(comments.id) DESC')
    .limit(1)

2. Fetching Posts with More than 5 Comments

Post.joins(:comments)
    .group(:id)
    .having('COUNT(comments.id) > ?', 5)

3. Finding Users Who Liked the Most Comments

User.joins(comments: :likes)
    .group('users.id')
    .select('users.id, users.name, COUNT(likes.id) AS likes_count')
    .order('likes_count DESC')
    .limit(1)

4. Fetching Posts Belonging to Multiple Categories

Post.joins(:categories).group('posts.id').having('COUNT(categories.id) > ?', 1)

5. Fetching the Last Comment of Each Post

Comment.select('DISTINCT ON (post_id) *').order('post_id, created_at DESC')

6. Fetching Users Who Havenโ€™t Commented on a Specific Post

User.where.not(id: Comment.where(post_id: 10).select(:user_id))

7. Fetching Users Who Have Commented on Every Post

User.joins(:comments).group(:id).having('COUNT(DISTINCT comments.post_id) = ?', Post.count)

8. Finding Posts With No Comments

Post.left_outer_joins(:comments).where(comments: { id: nil })

9. Fetching the User Who Created the Most Posts

User.joins(:posts)
    .group('users.id')
    .select('users.id, users.name, COUNT(posts.id) AS post_count')
    .order('post_count DESC')
    .limit(1)

10. Fetching the Most Liked Comment

Comment.joins(:likes)
    .group('comments.id')
    .order('COUNT(likes.id) DESC')
    .limit(1)

11. Fetching Comments with More than 3 Likes and Their Associated Posts

Comment.joins(:likes, :post)
    .group('comments.id', 'posts.id')
    .having('COUNT(likes.id) > ?', 3)

12. Finding Users Who Havenโ€™t Liked Any Comments

User.left_outer_joins(:likes).where(likes: { id: nil })

13. Fetching Users, Their Posts, and the Count of Comments on Each Post

User.joins(posts: :comments)
    .group('users.id', 'posts.id')
    .select('users.id, users.name, posts.id AS post_id, COUNT(comments.id) AS comment_count')
    .order('comment_count DESC')

Importance of inverse_of in Model Associations

What is inverse_of?

The inverse_of option in Active Record associations helps Rails correctly link objects in memory, avoiding unnecessary database queries and ensuring bidirectional association consistency.

Example Usage

class User < ApplicationRecord
  has_many :posts, inverse_of: :user
end

class Post < ApplicationRecord
  belongs_to :user, inverse_of: :posts
end

Why Use inverse_of?

  • Performance Optimization: Prevents extra queries by using already loaded objects.
  • Ensures Data Consistency: Updates associations without additional database fetches.
  • Enables Nested Attributes: Helps when using accepts_nested_attributes_for.

Example:

user = User.new(name: 'Alice')
post = user.posts.build(title: 'First Post')
post.user == user  # True without needing an additional query

Best Practices to use in Rails Projects

1. Using Scopes for Readability

class Post < ApplicationRecord
  scope :recent, -> { order(created_at: :desc) }
end

Post.recent.limit(10)  # Fetch recent posts

2. Using find_each for Large Datasets

User.find_each(batch_size: 100) do |user|
  puts user.email
end

3. Avoiding SELECT * for Performance

User.select(:id, :name).load

4. Avoiding N+1 Queries with includes

Post.includes(:comments).each do |post|
  puts post.comments.count
end


Conclusion

Mastering Active Record queries is essential for writing performant and maintainable Rails applications. By using joins, scopes, batch processing, and eager loading, you can write clean and efficient queries that scale well.

Do you have any favorite Active Record query tricks? Share them in the comments!

Mastering ๐–กŽ Ruby: Exploring Syntax Sugar, Operators, and Useful Methods

Ruby is known for its elegant and expressive syntax, making it one of the most enjoyable programming languages to work with. In this blog post, we will explore some interesting aspects of Ruby, including syntax sugar, operator methods, useful array operations, and handy built-in methods.


Ruby Syntax Sugar

Ruby provides several shorthand notations that enhance code readability and reduce verbosity. Let’s explore some useful syntax sugar techniques.

Shorthand for Array and Symbol Arrays

Instead of manually writing arrays of strings or symbols, Ruby provides %w and %i shortcuts.

words = %w[one two three]   # => ["one", "two", "three"]
symbols = %i[one two three] # => [:one, :two, :three]

These notations also support interpolation:

prefix = 'item'
words = %W[#{prefix}_one #{prefix}_two #{prefix}_three]  # => ["item_one", "item_two", "item_three"]
symbols = %I[#{prefix}_one #{prefix}_two #{prefix}_three] # => [:item_one, :item_two, :item_three]

Shorthand for Mapping and Selecting Elements

Instead of using map with blocks, we can use &: shorthand.

strings = %w[one two three]
upcased = strings.map(&:upcase) # => ["ONE", "TWO", "THREE"]

This is equivalent to:

strings.map { |str| str.upcase }


Operator Method Calls

Many mathematical and array operations in Ruby are actually method calls, thanks to Ruby’s message-passing model.

puts 1.+(2)  # => 3 (Same as 1 + 2)

Array Indexing and Append Operations

words = %w[zero one two three four]
puts words.[](2)   # => "two" (Equivalent to words[2])
words.<<('five')   # => ["zero", "one", "two", "three", "four", "five"] (Equivalent to words << "five")

Avoiding Dot Notation with Operators

# Bad practice
num = 10
puts num.+(5) # Avoid this syntax

# Good practice
puts num + 5  # Preferred

Using a linter like rubocop can help enforce best practices in Ruby code.


none? Method in Ruby Arrays

The none? method checks if none of the elements in an array satisfy a given condition.

numbers = [1, 2, 3, 4, 5]
puts numbers.none? { |n| n > 10 }  # => true (None are greater than 10)
puts numbers.none?(&:odd?)          # => false (Some numbers are odd)

This method is useful for quickly asserting that an array does not contain specific values.


Exponent Operator (**) and XOR Operator (^)

Exponentiation

Ruby uses ** for exponentiation instead of ^ (which performs a bitwise XOR operation).

puts 2 ** 3  # => 8 (2 raised to the power of 3)
puts 10 ** 0.5 # => 3.162277660168379 (Square root of 10)

XOR Operator

In Ruby, ^ is used for bitwise XOR operations, which differ from exponentiation.

puts 5 ^ 3  # => 6 (Binary: 101 ^ 011 = 110)
puts 10 ^ 7 # => 13 (Binary: 1010 ^ 0111 = 1101)

This is useful in scenarios involving bitwise manipulations, such as cryptography or performance optimizations.


Safe Navigation Operator (&.)

Ruby provides the safe navigation operator (&.) to avoid NilClass errors when calling methods on potentially nil objects.

user = nil
puts user&.name  # => nil (Avoids NoMethodError)

This is helpful when dealing with uncertain data, such as API responses or optional attributes.


Useful String Methods: chop and find_all

chop: Removing the Last Character

The chop method removes the last character from a string, unlike chomp, which removes only the newline character.

str = "Hello!"
puts str.chop  # => "Hello"

find_all: Filtering Collections

The find_all method (alias of select) filters elements that match a condition.

numbers = [1, 2, 3, 4, 5]
even_numbers = numbers.find_all(&:even?) # => [2, 4]

This is particularly useful for data processing tasks.


=~ Operator in Ruby

The =~ operator is used to match a string against a regular expression and returns the index of the first match or nil if no match is found.

puts "hello" =~ /e/  # => 1 (Index of 'e' in "hello")
puts "hello" =~ /z/  # => nil (No match found)

This operator is useful for quick pattern matching in strings.


.squish Method in Ruby

The .squish method is part of ActiveSupport (Rails) and removes leading, trailing, and extra spaces within a string.

require 'active_support/all'

text = "   Hello   World!  "
puts text.squish  # => "Hello World!"

This is particularly useful for cleaning up user input or text from external sources.


Conclusion

Ruby’s syntax sugar, operator methods, and built-in methods make code more readable, expressive, and powerful. By leveraging these features effectively, you can write clean, efficient, and maintainable Ruby code.

Enjoy Ruby! ๐Ÿš€

Rails ๐Ÿ›ค DSLs Explained: How Ruby Makes Configuration Elegant

Ruby on Rails is known for its developer-friendly syntax and expressive code structure. One of the key reasons behind this elegance is its use of Domain-Specific Languages (DSLs). DSLs make Rails configurations, routes, and testing more intuitive by allowing developers to write code that reads like natural language.

In this blog post, we’ll explore what DSLs are, how Rails implements them, and why they make development in Rails both powerful and enjoyable.


What is a DSL?

A Domain-Specific Language (DSL) is a specialized language designed to solve problems in a specific domain. Unlike general-purpose languages (like Ruby or Java), a DSL provides a more concise and readable syntax for a particular task.

Two types of DSLs exist:

  • Internal DSLs: Written using an existing programming languageโ€™s syntax (e.g., Rails DSLs in Ruby).
  • External DSLs: Separate from the host language and require a custom parser (e.g., SQL, Regular Expressions).

Rails uses Internal DSLs to simplify web development. Letโ€™s explore some core DSLs in Rails and how they work under the hood.


1. Routes in Rails: A Classic Example of DSL

In config/routes.rb, Rails provides a DSL to define application routes in a clear and structured way.

Example:

Rails.application.routes.draw do
  resources :users do
    resources :posts
  end

  get '/about', to: 'pages#about'
  root 'home#index'
end

How Does This Work?

  • resources :users automatically generates RESTful routes for UsersController.
  • get '/about', to: 'pages#about' maps a GET request to the about action in PagesController.
  • root 'home#index' sets the default landing page.

Why Use a DSL for Routes?

  • Concise & Readable: Avoids manually defining each route.
  • Expressive Syntax: Reads like a structured list of instructions.
  • Reduces Boilerplate Code: Automates RESTful route creation.

Under the hood, Rails uses metaprogramming to convert this DSL into actual Ruby methods that map HTTP requests to controllers.


2. Configuration DSL in Rails: config/environments/development.rb

Rails also provides a DSL for application configuration using Rails.application.configure.

Example:

Rails.application.configure do
  config.cache_classes = false
  config.eager_load = false
  config.consider_all_requests_local = true
end

What is config Here?

  • config is an instance of Rails::Application::Configuration, a special Ruby object that stores settings.
  • The configure block modifies application settings dynamically using method calls.

Why a DSL for Configuration?

  • Expressiveness: Instead of setting key-value pairs in a hash, we use method calls (config.cache_classes = false).
  • Customization: Each environment (development, test, production) has its own configuration file.
  • Readability: Makes it easy to understand and modify settings.

3. RSpecโ€™s describe Method: A DSL for Testing

RSpec, the popular testing framework for Ruby, provides a DSL for writing tests.

Example:

describe User do
  it "has a valid factory" do
    user = FactoryBot.create(:user)
    expect(user).to be_valid
  end
end

How Does This Work?

  • describe User do ... end defines a test suite for the User model.
  • it "has a valid factory" do ... end describes an individual test case.
  • expect(user).to be_valid checks if the user instance is valid.

Under the hood, describe is a method that creates a structured test suite dynamically.

Why Use a DSL for Testing?

  • Improves Readability: Tests read like English sentences.
  • Encapsulates Test Logic: Eliminates boilerplate setup code.
  • Encourages Behavior-Driven Development (BDD).

4. Defining Methods Dynamically: ActiveSupport::Concern

Rails extends DSL capabilities with ActiveSupport::Concern, which allows modular mixins in models and controllers.

Example:

module Trackable
  extend ActiveSupport::Concern

  included do
    before_save :track_changes
  end

  private
  def track_changes
    puts "Tracking changes!"
  end
end

class User < ApplicationRecord
  include Trackable
end

How This Works:

  • included do ... end executes code when the module is included in a class.
  • before_save :track_changes hooks into the Rails lifecycle to run before saving a record.

Why a DSL for Mixins?

  • Encapsulation: Keeps related logic together.
  • Reusability: Can be included in multiple models.
  • Cleaner Code: Removes redundant callbacks in models.

Conclusion: Why Rails Embraces DSLs

DSLs in Rails make the framework expressive, flexible, and developer-friendly. They provide:

โœ… Concise syntax (reducing boilerplate code). โœ… Readability (code reads like natural language). โœ… Powerful abstractions (simplifying complex tasks). โœ… Customization (tailoring behavior dynamically).

By leveraging DSLs, Rails makes web development intuitive, allowing developers to focus on building great applications rather than writing repetitive code.

So next time you’re defining routes, configuring settings, or writing tests in Railsโ€”remember, you’re using DSLs that make your life easier!

Enjoy Ruby ๐Ÿš€

Exploring Rails 8: Powerful ๐Ÿ’ช Features, Deployment & Real-Time Updates

Introduction

Rails 8.x has arrived, bringing exciting new features and enhancements to improve productivity, performance, and ease of development. From built-in authentication to real-time WebSocket updates, this latest version of Rails continues its commitment to being a powerful and developer-friendly framework.

Let’s dive into some of the most significant features and improvements introduced in Rails 8.


Rails 8 Features & Enhancements

1. Modern JavaScript with Importmaps & Hotwire

Rails 8 eliminates the need for Webpack and Node.js, allowing developers to manage JavaScript dependencies more efficiently. Importmaps simplify dependency management by fetching JavaScript packages directly and caching them locally, removing runtime dependencies.

Key Benefits:

  • Faster page loads and reduced complexity
  • No need for Node.js or Webpack
  • Dependencies are cached locally and loaded efficiently

Example: Pinning a Package

bin/importmap pin local-time

This command fetches the package from npm and stores it locally for future use.

Hotwire Integration

Hotwire enables dynamic page updates without requiring heavy JavaScript frameworks. Rails 8 fully integrates Turbo and Stimulus, making frontend interactivity more seamless.

Importing Dependencies in application.js:
import "trix";

With this setup, developers can create reactive UI elements with minimal JavaScript.


2. Real-Time WebSockets with Action Cable & Turbo Streams

Rails 8 enhances real-time functionality with Action Cable and Turbo Streams, allowing WebSocket-based updates across multiple pages without additional JavaScript libraries.

Setting Up Turbo Streams in Views:

<%= turbo_stream_from @object %>

This creates a WebSocket channel tied to the object.

Broadcasting Updates from Models:

broadcast_to :object, render(partial: "objects/object", locals: { object: self })

Any changes to the object will be instantly reflected across all connected clients.

Why This Matters:

  • No need for third-party WebSocket npm packages
  • Real-time updates are built into Rails
  • Simplifies building interactive applications

3. Rich Text with ActionText

Rails 8 continues to support ActionText, making it easy to handle rich text content within models and views.

Model Level Implementation:

has_rich_text :body

This enables rich text storage and formatting for the body attribute of a model.

View Implementation:

<%= form.rich_text_area :body %>

This adds a full-featured WYSIWYG text editor to the form, allowing users to create and edit rich text content seamlessly.

Displaying Updated Timestamps:

<%= time_tag post.updated_at %>

This helper formats timestamps cleanly, improving date and time representation in views.


4. Deployment with Kamal โ€“ Simpler & Faster

Rails 8 introduces Kamal, a modern deployment tool that simplifies remote deployment by leveraging Docker containers.

Deployment Steps:

  1. Setup Remote Serverkamal setup
    • Installs Docker (if missing) and configures the server.
  2. Deploy the Applicationkamal deploy
    • Builds and ships a Docker container using Railsโ€™ default Dockerfile.

File Uploads with Active Storage

By default, Kamal stores uploaded files in Docker volumes, but this can be customized based on specific deployment needs.


5. Built-in Authentication โ€“ No Devise Needed

Rails 8 introduces native authentication, reducing reliance on third-party gems like Devise. This built-in system manages password encryption, user sessions, and password resets while keeping signup flows flexible.

Generating Authentication:

rails g authentication
rails db:migrate

Creating a User for Testing:

User.create(email: "user@example.com", password: "securepass")

Managing Authentication:

  • Uses bcrypt for password encryption
  • Provides a pre-built sessions_controller for handling authentication
  • Allows remote database changes via: kamal console

6. Turning a Rails App into a PWA

Rails 8 makes it incredibly simple to transform any app into a Progressive Web App (PWA), enabling offline support and installability.

Steps to Enable PWA:

  1. Modify application.html.erb: <%= tag.link pwa_manifest_path %>
  2. Ensure manifest and service-worker routes are enabled.
  3. Verify PWA files: pwa/manifest.json.erb and pwa/service-worker.js.
  4. Deploy and restart the application to see the Install button in the browser.

Final Thoughts

Rails 8 is packed with developer-friendly features that improve security, real-time updates, and deployment workflows. With Hotwire, Kamal, and native authentication, itโ€™s clear that Rails is evolving to reduce dependencies while enhancing performance.

Are you excited about Rails 8? Let me know your thoughts and experiences in the comments below!

Installing โš™๏ธ and Setting Up ๐Ÿ”ง Ruby 3.4, Rails 8.0 and IDE on macOS in 2025

Ruby on Rails is a powerful framework for building web applications. If you’re setting up your development environment on macOS in 2025, this guide will walk you through installing Ruby 3.4, Rails 8, and a best IDE for development.

1. Installing Ruby and Rails

“While macOS comes with Ruby pre-installed, it’s often outdated and can’t be upgraded easily. Using a version manager like Mise allows you to install the latest Ruby version, switch between versions, and upgrade as needed.” – Rails guides

Install Dependencies

Run the following command to install essential dependencies (takes time):

brew install openssl@3 libyaml gmp rust

โ€ฆ..
==> Installing rust dependency: libssh2, readline, sqlite, python@3.13, pkgconf
==> Installing rust

zsh completions have been installed to:
/opt/homebrew/share/zsh/site-functions
==> Summary
๐Ÿบ /opt/homebrew/Cellar/rust/1.84.1: 3,566 files, 321.3MB
==> Running brew cleanup rustโ€ฆ
==> openssl@3
A CA file has been bootstrapped using certificates from the system
keychain. To add additional certificates, place .pem files in
/opt/homebrew/etc/openssl@3/certs

and run
/opt/homebrew/opt/openssl@3/bin/c_rehash
==> rust
zsh completions have been installed to:
/opt/homebrew/share/zsh/site-functions

Install Mise Version Manager

curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc
source ~/.zshrc

Install Ruby and Rails

mise use -g ruby@3
mise ruby@3.4.1 โœ“ installed
mise ~/.config/mise/config.toml tools: ruby@3.4.1

ruby --version   # output Ruby 3.4.1

gem install rails

# reload terminal and check
rails --version  # output Rails 8.0.1

For additional guidance, refer to these resources:


2. Installing an IDE for Ruby on Rails Development

Choosing the right Integrated Development Environment (IDE) is crucial for productivity. Here are some popular options:

RubyMine

  • Feature-rich and specifically designed for Ruby on Rails.
  • Includes debugging tools, database integration, and smart code assistance.
  • Paid software that can be resource-intensive.

Sublime Text

  • Lightweight and highly customizable.
  • Requires plugins for additional functionality.

Visual Studio Code (VS Code) (Recommended)

  • Free and open-source.
  • Excellent plugin support.

Install VS Code

Follow the official installation guide.

Enable GitHub Copilot for AI-assisted coding:

  1. Open VS Code.
  2. Sign in with your GitHub account.
  3. Enable Copilot from the extensions panel.

To use VS Code from the terminal, ensure code is added to your $PATH:

  1. Open Command Palette (Cmd+Shift+P).
  2. Search for Shell Command: Install 'code' command in PATH.
  3. Restart your terminal and try: code .

3. Your 15 Essential VS Code Extensions for Ruby on Rails

To enhance your development workflow, install the following VS Code extensions:

  1. GitHub Copilot – AI-assisted coding (already installed).
  2. vscode-icons – Better file and folder icons.
  3. Tabnine AI – AI autocompletion for JavaScript and other languages.
  4. Ruby & Ruby LSP – Language support and linting.
  5. ERB Formatter/Beautify – Formats .erb files (requires htmlbeautifier gem): gem install htmlbeautifier
  6. ERB Helper Tags – Autocomplete for ERB tags.
  7. GitLens – Advanced Git integration.
  8. Ruby Solargraph – Provides code completion and inline documentation (requires solargraph gem): gem install solargraph
  9. Rails DB Schema – Auto-completion for Rails database schema.
  10. ruby-rubocop – Ruby linting and auto-formatting (requires rubocop gem): gem install rubocop
  11. endwise – Auto-adds end keyword in Ruby.
  12. Output Colorizer – Enhances syntax highlighting in log files.
  13. Auto Rename Tag – Automatically renames paired HTML/Ruby tags.
  14. Highlight Matching Tag – Highlights matching tags for better visibility.
  15. Bracket Pair Colorizer 2 – Improved bracket highlighting.

Conclusion

By following this guide, you’ve successfully set up a robust Ruby on Rails development environment on macOS. With Mise for version management, Rails installed, and VS Code configured with essential extensions, you’re ready to start building Ruby on Rails applications.

Part 2: https://railsdrop.com/2025/03/22/setup-rails-8-app-rubocop-actiontext-image-processing-part-2

Happy Rails setup! ๐Ÿš€

The Evolution of Asset ๐Ÿ“‘ Management in Web and Ruby on Rails

Understanding Middleware in Rails

When a client request comes into a Rails application, it doesn’t always go directly to the MVC (Model-View-Controller) layer. Instead, it might first pass through middleware, which handles tasks such as authentication, logging, and static asset management.

Rails uses middleware like ActionDispatch::Static to efficiently serve static assets before they even reach the main application.

ActionDispatch::Static Documentation

“This middleware serves static files from disk, if available. If no file is found, it hands off to the main app.”

Where Are Static Files Stored?

Rails stores static assets in the public/ directory, and ActionDispatch::Static ensures these are served efficiently without hitting the Rails stack.

Core Components of Ruby on Rails – A reminder

To understand asset management evolution, let’s quickly revisit Rails’ core components:

  • ActiveRecord: Object-relational mapping (ORM) system for database interactions.
  • Action Pack: Handles the controller and view layers.
  • Active Support: A collection of utility classes and standard library extensions.
  • Action Mailer: A framework for designing email services.

The Role of Browsers in Asset Management

Web browsers cache static assets to improve performance. The caching strategy varies based on asset types:

  • Images: Rarely change, so they are aggressively cached.
  • JavaScript and CSS files: Frequently updated, requiring cache-busting mechanisms.

The Era of Sprockets

Historically, Rails used Sprockets as its default asset pipeline. Sprockets provided:

  • Conversion of CoffeeScript to JavaScript and SCSS to CSS.
  • Minification and bundling of assets into fewer files.
  • Digest-based caching to ensure updated assets were fetched when changed.

The Rise of JavaScript & The Shift Towards Webpack

The release of ES6 (2015-2016) was a turning point for JavaScript, fueling the rise of Single Page Applications (SPAs). This marked a shift from traditional asset management:

  • Sprockets was effective but became complex and difficult to configure for modern JS frameworks.
  • Projects started including package.json at the root, indicating JavaScript dependency management.
  • Webpack emerged as the go-to tool for handling JavaScript, offering features like tree-shaking, hot module replacement, and modern JavaScript syntax support.

The Landscape in 2024: A More Simplified Approach

Recent advancements in web technology have drastically simplified asset management:

  1. ES6 Native Support in All Major Browsers
    • No need for transpilation of modern JavaScript.
  2. CSS Advancements
    • Features like variables and nesting eliminate the need for preprocessors like SASS.
  3. HTTP/2 and Multiplexing
    • Enables parallel loading of multiple assets over a single connection, reducing dependency on bundling strategies.

Enter Propshaft: The Modern Asset Pipeline

Propshaft is the new asset management solution introduced in Rails, replacing Sprockets for simpler and faster asset handling. Key benefits include:

  • Digest-based file stamping for effective cache busting.
  • Direct and predictable mapping of assets without complex processing.
  • Better integration with HTTP/2 for efficient asset delivery.

Rails 8 Precompile Uses Propshaft

What is Precompile? A Reminder

Precompilation hashes all file names and places them in the public/ folder, making them accessible to the public.

Propshaft improves upon this by creating a manifest file that maps the original filename as a key and the hashed filename as a value. This significantly enhances the developer experience in Rails.

Propshaft ultimately moves asset management in Rails to the next level, making it more efficient and streamlined.

The Future of Asset Management in Rails

With advancements like native ES6 support and CSS improvements, Rails continues evolving to embrace simpler, more efficient asset management strategies. Propshaft, combined with modern browser capabilities, makes asset handling seamless and more performance-oriented.

As the web progresses, we can expect further simplifications in asset pipelines, making Rails applications faster and easier to maintain.

Stay tuned for more innovations in the Rails ecosystem!

Happy Rails Coding! ๐Ÿš€

Learn SQL: Day 5 โ€“ Subqueries, EXISTS, IN, NOT EXISTS, ANY, and ALL

Today we move from basic filtering and aggregation into query composition.

This is an important topic for senior Rails interviews because many real production queries can be expressed in several ways:

JOIN
IN
EXISTS
Subquery
ActiveRecord association queries

A senior engineer should know not only how to write them, but also:

Which query expresses the business requirement most clearly?

Does the query preserve duplicates?

Does NULL affect the result?

Does PostgreSQL need to calculate one value or evaluate rows repeatedly?

What SQL is ActiveRecord generating?

We’ll build on the users and orders tables from Day 4.


Today’s Goals

By the end of Day 5, you should understand:

  • What a subquery is
  • Scalar subqueries
  • Multi-row subqueries
  • Subqueries in WHERE
  • Subqueries in FROM
  • Correlated subqueries
  • IN
  • EXISTS
  • NOT EXISTS
  • NOT IN and the NULL trap
  • ANY
  • ALL
  • JOIN vs IN vs EXISTS
  • ActiveRecord equivalents
  • Common mistakes
  • Senior interview questions

Part 1 โ€“ Prepare the Practice Data

We’ll use the same domain from Day 4, but add a few more rows to make today’s queries more interesting.

First, inspect your current data:

SELECT * FROM users ORDER BY id;
SELECT * FROM orders ORDER BY id;

If you want to recreate everything from scratch, run:

DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS users;

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    city VARCHAR(100)
);

CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    amount NUMERIC(10,2) NOT NULL,
    status VARCHAR(20) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),

    CONSTRAINT fk_orders_user
        FOREIGN KEY (user_id)
        REFERENCES users(id)
);

Insert users:

INSERT INTO users(name, city)
VALUES
('John', 'New York'),
('Mary', 'Chicago'),
('Bob', 'Chicago'),
('Alice', 'Boston'),
('David', 'Mumbai'),
('Sara', 'Boston');

Insert orders:

INSERT INTO orders(user_id, amount, status)
VALUES
(1, 100, 'completed'),
(1, 250, 'completed'),
(1, 75, 'pending'),
(2, 500, 'completed'),
(2, 300, 'completed'),
(3, 200, 'pending'),
(4, 800, 'completed'),
(4, 150, 'cancelled'),
(6, 1000, 'completed'),
(6, 1200, 'completed');

Now our data looks conceptually like this:

UserOrders
John100, 250, 75
Mary500, 300
Bob200
Alice800, 150
DavidNo orders
Sara1000, 1200

This dataset is intentionally designed so we can practice:

  • users with orders
  • users without orders
  • users above average spending
  • users with expensive orders
  • correlated subqueries
  • EXISTS and NOT EXISTS

Part 2 โ€“ What Is a Subquery?

A subquery is a query nested inside another SQL statement.

Example:

SELECT *
FROM orders
WHERE amount > (
SELECT AVG(amount)
FROM orders
);

The inner query is:

SELECT AVG(amount)
FROM orders;

The outer query is:

SELECT *
FROM orders
WHERE amount > (...);

Conceptually:

Inner query
    โ†“
Calculate average order amount
    โ†“
Return the result
    โ†“
Outer query
    โ†“
Find orders greater than that value


Let’s run the inner query separately first:

SELECT AVG(amount)
FROM orders;

Total amount:

4575

Number of orders:

10

Average:

457.5

Now the outer query becomes conceptually:

SELECT *
FROM orders
WHERE amount > 457.5;

Result:

500
800
1000
1200

Rails Equivalent

Order.where(
"amount > (?)",
Order.select("AVG(amount)")
)

However, in Rails you may also see:

average = Order.average(:amount)
Order.where("amount > ?", average)

These are not exactly the same approach.

The first can produce one SQL statement containing a subquery.

The second executes:

Query 1 โ†’ Calculate average

Query 2 โ†’ Find orders above average

That distinction can matter when data changes between queries and when minimizing database round trips.


Part 3 โ€“ Scalar Subqueries

A scalar subquery returns:

One row
One column

Therefore, it produces a single value.

Example:

SELECT AVG(amount)
FROM orders;

Result:

457.5

We can use that result with operators such as:

=
>
<
>=
<=
<>

Example:

SELECT
id,
user_id,
amount
FROM orders
WHERE amount > (
SELECT AVG(amount)
FROM orders
);

What Happens if the Subquery Returns Multiple Rows?

Try:

SELECT *
FROM orders
WHERE amount = (
SELECT amount
FROM orders
);

The inner query returns many rows.

PostgreSQL will raise an error similar to:

more than one row returned by a subquery used as an expression

Why?

Because:

amount = ???

expects one value.

But the subquery returned:

100
250
75
500
300
...

PostgreSQL cannot compare one amount against multiple scalar values using =.

This leads us to IN.


Part 4 โ€“ IN with a Subquery

Suppose the requirement is:

Find users who have placed at least one order.

We can write:

SELECT *
FROM users
WHERE id IN (
SELECT user_id
FROM orders
);

Run the subquery separately:

SELECT user_id
FROM orders;

Result:

1
1
1
2
2
3
4
4
6
6

Conceptually:

SELECT *
FROM users
WHERE id IN (1, 1, 1, 2, 2, 3, 4, 4, 6, 6);

Result:

John
Mary
Bob
Alice
Sara

David is excluded because he has no orders.

Rails Equivalent

A good ActiveRecord version is:

User.where(
id: Order.select(:user_id)
)

Conceptually, Rails can generate:

SELECT *
FROM users
WHERE id IN (
SELECT user_id
FROM orders
);

Notice the difference between:

Order.select(:user_id)

and:

Order.pluck(:user_id)

This is important.

Using select

User.where(id: Order.select(:user_id))

can remain a single SQL statement with a subquery.

Using pluck

User.where(id: Order.pluck(:user_id))

executes the inner query immediately.

Conceptually:

Query 1
SELECT user_id FROM orders;

Then Rails constructs another query:

Query 2
SELECT *
FROM users
WHERE id IN (1, 1, 1, 2, 2, 3, ...);

For a large dataset, that can be undesirable.

Senior-Level Insight

When building SQL subqueries in ActiveRecord, don’t automatically reach for pluck.

Ask:

Do I want Ruby to materialize these IDs?

Or:

Can PostgreSQL keep the work inside one SQL statement?


Part 5 โ€“ NOT IN

Suppose the requirement is:

Find users who have never placed an order.

You might write:

SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM orders
);

Result:

David

With our current schema, this works because:

orders.user_id BIGINT NOT NULL

Therefore, the subquery cannot return NULL.

But NOT IN has a famous SQL trap.


Part 6 โ€“ The NOT IN + NULL Trap

Let’s create a small demonstration table.

DROP TABLE IF EXISTS order_users_demo;
CREATE TABLE order_users_demo (
user_id BIGINT
);

Insert:

INSERT INTO order_users_demo(user_id)
VALUES
(1),
(2),
(NULL);

Now run:

SELECT *
FROM users
WHERE id NOT IN (
SELECT user_id
FROM order_users_demo
);

You might expect:

Bob
Alice
David
Sara

But you get:

0 rows

Why?

Because SQL uses three-valued logic:

TRUE
FALSE
UNKNOWN

Conceptually:

id NOT IN (1, 2, NULL)

behaves like:

id <> 1
AND id <> 2
AND id <> NULL

But:

id <> NULL

is not TRUE.

It is:

UNKNOWN

And:

TRUE AND TRUE AND UNKNOWN

results in:

UNKNOWN

WHERE only keeps rows where the condition evaluates to TRUE.

This is one of the most important SQL interview traps to remember.


Part 7 โ€“ EXISTS

Now let’s solve:

Find users who have at least one order.

Using EXISTS:

SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);

Result:

John
Mary
Bob
Alice
Sara

David is excluded.

How Does EXISTS Work?

Look carefully:

SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);

The inner query references:

u.id

But u was defined in the outer query.

Therefore, this is a:

Correlated Subquery

Conceptually PostgreSQL evaluates:

John
โ†“
Does an order exist with user_id = John's id?
โ†“
Yes
โ†“
Keep John

Then:

Mary
โ†“
Does an order exist?
โ†“
Yes
โ†“
Keep Mary

Then:

David
โ†“
Does an order exist?
โ†“
No
โ†“
Remove David

Important: this is a useful conceptual model, but it does not mean PostgreSQL must literally execute the inner query once per outer row. The optimizer can transform correlated EXISTS queries into efficient semi-join plans.

We’ll inspect that later using:

EXPLAIN ANALYZE

Why SELECT 1?

You commonly see:

EXISTS (
SELECT 1
FROM orders
...
)

Why 1?

Because EXISTS doesn’t care what columns are returned.

It only asks:

Does at least one matching row exist?

These are semantically equivalent:

EXISTS (
SELECT 1
FROM orders
WHERE ...
)
EXISTS (
SELECT *
FROM orders
WHERE ...
)
EXISTS (
SELECT amount
FROM orders
WHERE ...
)

SELECT 1 communicates intent clearly.


Part 8 โ€“ NOT EXISTS

Requirement:

Find users who have never placed an order.

SELECT *
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);

Result:

David

This is called an anti-join pattern.

Conceptually:

For each user:
Does a matching order exist?
YES โ†’ reject
NO โ†’ keep

Compare With LEFT JOIN

We learned this yesterday:

SELECT u.*
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
WHERE o.id IS NULL;

And today:

SELECT u.*
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);

Both express:

Find users without orders.

The PostgreSQL optimizer may produce similar execution strategies.

However, NOT EXISTS often expresses the business requirement more directly:

Keep the user if no matching order exists.

Rails Equivalent: where.missing

Rails provides a very readable API:

User.where.missing(:orders)

Conceptually, Rails generates a LEFT OUTER JOIN with an IS NULL condition.

Another option is to build a NOT EXISTS query using Arel, but for standard Rails association queries, where.missing is usually clearer.

Rails Equivalent: where.associated

Find users who have orders:

User.where.associated(:orders)

Depending on Rails version and query construction, this uses an association join and filters out missing related rows.

You may also write:

User.joins(:orders).distinct

Remember why distinct can be needed:

John has 3 orders
JOIN result:
John
John
John

EXISTS does not duplicate John because it tests existence rather than returning matching order rows.

This is a major conceptual difference.

Part 9 โ€“ IN vs EXISTS

Let’s compare them.

IN

SELECT *
FROM users
WHERE id IN (
SELECT user_id
FROM orders
);

Conceptually:

Is this user’s ID present in the set of order user IDs?

EXISTS

SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);

Conceptually:

Does at least one matching order exist for this user?

Which Is Faster?

A common junior-level answer is:

EXISTS is always faster.

That’s incorrect.

Modern PostgreSQL can rewrite IN and EXISTS into similar plans, such as semi-joins.

Performance depends on:

  • table sizes
  • indexes
  • statistics
  • data distribution
  • selectivity
  • query structure
  • PostgreSQL planner decisions

The correct senior-level approach is:

Choose the query that expresses the requirement clearly, then inspect the execution plan when performance matters.

Later we’ll compare:

EXPLAIN ANALYZE
SELECT ...
WHERE id IN (...);

with:

EXPLAIN ANALYZE
SELECT ...
WHERE EXISTS (...);

Part 10 โ€“ Correlated Subqueries

We’ve already seen one:

SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);

The inner query depends on the outer query.

Let’s look at another important example.

Requirement:

Find orders whose amount is greater than the average order amount for that particular user.

This is different from:

Find orders above the global average.

Global average:

SELECT *
FROM orders
WHERE amount > (
SELECT AVG(amount)
FROM orders
);

Per-user average:

SELECT
o.id,
o.user_id,
o.amount
FROM orders o
WHERE o.amount > (
SELECT AVG(o2.amount)
FROM orders o2
WHERE o2.user_id = o.user_id
);

Notice:

o2.user_id = o.user_id

The inner query references the outer row.

Let’s manually reason through John.

John’s orders:

100
250
75

Average:

141.67

Which John’s orders are above John’s average?

250

Mary:

500
300

Average:

400

Above average:

500

Alice:

800
150

Average:

475

Above average:

800

Sara:

1000
1200

Average:

1100

Above average:

1200

Bob has only one order:

200

Average:

200

Condition:

200 > 200

False.

So Bob has no matching result.

Rails Equivalent

A direct SQL fragment is often the clearest ActiveRecord solution:

Order.where(<<~SQL)
orders.amount > (
SELECT AVG(o2.amount)
FROM orders o2
WHERE o2.user_id = orders.user_id
)
SQL

Senior Rails Insight:

Not every query should be forced into a chain of ActiveRecord methods.

For complex database logic, readable SQL inside ActiveRecord can be better than complicated Arel code.

The important things are:

  • parameterize external input
  • keep the SQL understandable
  • test it
  • inspect its execution plan when needed

Part 11 โ€“ Subqueries in FROM

A subquery can also act like a temporary result set.

Requirement:

Calculate each user’s total spending, then return only users whose total spending exceeds 500.

First calculate totals:

SELECT
user_id,
SUM(amount) AS total_spent
FROM orders
GROUP BY user_id;

Now use that result as a derived table:

SELECT *
FROM (
SELECT
user_id,
SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
) user_totals
WHERE total_spent > 500;

Important:

PostgreSQL requires an alias for the derived table:

user_totals

Conceptually:

orders
โ†“
GROUP BY user_id
โ†“
temporary result set
user_id | total_spent
โ†“
filter temporary result
โ†“
total_spent > 500

Of course, for this particular query, HAVING is simpler:

SELECT
user_id,
SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 500;

So why learn subqueries in FROM?

Because derived tables become useful when:

  • aggregating in multiple stages
  • joining against aggregated results
  • ranking data
  • reporting queries
  • building complex analytical queries

Part 12 โ€“ ANY

ANY compares a value against values returned by a subquery.

Example:

SELECT *
FROM orders
WHERE amount > ANY (
SELECT amount
FROM orders
WHERE user_id = 1
);

John’s order amounts:

100
250
75

The condition is:

amount > ANY (100, 250, 75)

This means:

The amount must be greater than at least one value.

Effectively:

amount > 75

because being greater than the smallest value is enough to satisfy the condition.

Therefore:

> ANY

can often be thought of as:

Greater than at least one value

Part 13 โ€“ ALL

Now:

SELECT *
FROM orders
WHERE amount > ALL (
SELECT amount
FROM orders
WHERE user_id = 1
);

John’s amounts:

100
250
75

Condition:

amount > ALL (100, 250, 75)

The amount must be greater than every value.

Effectively:

amount > 250

Therefore:

> ALL

means:

Greater than every value returned by the subquery.

Important ANY / ALL Mental Model

Given:

10
20
30

Then:

value > ANY (10, 20, 30)

means:

value > at least one of them

Equivalent threshold:

value > 10

But:

value > ALL (10, 20, 30)

means:

value > every one of them

Equivalent threshold:

value > 30

Be careful: this shortcut depends on the comparison operator. For example, < ANY and < ALL have different effective thresholds.


Part 14 โ€“ ANY with ActiveRecord Arrays

You may occasionally see PostgreSQL queries like:

SELECT *
FROM users
WHERE id = ANY(ARRAY[1, 2, 3]);

However, normal Rails code would usually use:

User.where(id: [1, 2, 3])

which generates an IN condition.

Don’t use PostgreSQL-specific syntax unless it provides a real advantage.


Part 15 โ€“ JOIN vs IN vs EXISTS

Requirement:

Find users who have completed orders.

JOIN

SELECT DISTINCT u.*
FROM users u
JOIN orders o
ON o.user_id = u.id
WHERE o.status = 'completed';

Potential issue:

The join produces one row per matching order.

Therefore, duplicates may occur.

We use:

DISTINCT

IN

SELECT *
FROM users
WHERE id IN (
SELECT user_id
FROM orders
WHERE status = 'completed'
);

No duplicate users in the outer result.

EXISTS

SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
AND o.status = 'completed'
);

Also no duplicate users.

How Should You Choose?

Use JOIN when:

You need columns from both tables.
You need to aggregate related rows.
You intentionally need matching rows.

Use EXISTS when:

You're asking whether a related row exists.
You don't need columns from the related table.
You want existence semantics without row multiplication.

Use IN when:

You're checking membership in a set of values.
The query reads naturally as "value belongs to this result set."

Do not choose solely based on old rules such as:

EXISTS is always faster than IN.

PostgreSQL’s optimizer is smarter than that.


Part 16 โ€“ Practical PostgreSQL Exercises

Let’s practice one query at a time.

Exercise 1

Find all orders above the global average order amount.

SELECT *
FROM orders
WHERE amount > (
SELECT AVG(amount)
FROM orders
);

Rails:

Order.where(
"amount > (?)",
Order.select("AVG(amount)")
)

Exercise 2

Find users who have orders.

SQL using IN:

SELECT *
FROM users
WHERE id IN (
SELECT user_id
FROM orders
);

Rails:

User.where(id: Order.select(:user_id))

Exercise 3

Find users who have orders using EXISTS.

SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);

Rails association-oriented alternative:

User.where.associated(:orders)

or:

User.joins(:orders).distinct

Exercise 4

Find users without orders.

SELECT *
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);

Rails:

User.where.missing(:orders)

Exercise 5

Find users who have at least one completed order greater than 400.

SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
AND o.status = 'completed'
AND o.amount > 400
);

Try writing the ActiveRecord version yourself before looking below.

One option:

User
.joins(:orders)
.where(orders: { status: "completed" })
.where("orders.amount > ?", 400)
.distinct

Exercise 6

Find orders above the average order amount for that order’s user.

SELECT *
FROM orders o
WHERE o.amount > (
SELECT AVG(o2.amount)
FROM orders o2
WHERE o2.user_id = o.user_id
);

This is today’s most important correlated subquery exercise.

Run it and manually verify every returned row.


Part 17 โ€“ Common Mistakes

Mistake 1: Using = with a Multi-Row Subquery

Wrong:

WHERE id = (
SELECT user_id
FROM orders
)

If multiple rows are returned, PostgreSQL raises an error.

Use:

IN

or:

EXISTS

depending on the requirement.

Mistake 2: Using NOT IN Without Considering NULL

Potentially dangerous:

WHERE id NOT IN (
SELECT user_id
FROM some_table
)

If the subquery can return NULL, the result may surprise you.

Safer existence-oriented query:

WHERE NOT EXISTS (...)

Mistake 3: Using pluck When You Want a SQL Subquery

Potentially inefficient:

User.where(id: Order.pluck(:user_id))

Better:

User.where(id: Order.select(:user_id))

when you want PostgreSQL to handle the operation as a subquery.

Mistake 4: Using JOIN + DISTINCT for Every Existence Check

User.joins(:orders).distinct

works.

But if your requirement is simply:

Does a matching row exist?

EXISTS more directly expresses the requirement.

Mistake 5: Assuming a Correlated Subquery Always Executes Once Per Row

Conceptually, we reason about it that way.

Physically, PostgreSQL may optimize it into:

  • Semi Join
  • Anti Join
  • Hash Join
  • Nested Loop
  • other execution strategies

Always distinguish:

SQL semantics

from:

physical execution plan

This distinction is very important for senior-level interviews.


Part 18 โ€“ Senior Interview Questions

Try answering these without looking back.

Q1

What is the difference between a normal subquery and a correlated subquery?

Q2

What happens if a scalar subquery returns multiple rows?

Q3

What’s the difference between:

Order.select(:user_id)

and:

Order.pluck(:user_id)

when used to build another query?

Q4

Why can NOT IN return zero rows when the subquery contains NULL?

Q5

What’s the difference between:

JOIN

and:

EXISTS

when one user has many matching orders?

Q6

Is EXISTS always faster than IN in PostgreSQL?

Q7

What is a semi-join?

Q8

What is an anti-join?

Q9

When would you use a subquery in the FROM clause instead of HAVING?

Q10

What is the difference between:

> ANY

and:

> ALL

Part 19 โ€“ Today’s Interview Challenge

Do not run this immediately.

First predict the result.

SELECT
u.id,
u.name
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
AND o.amount > (
SELECT AVG(o2.amount)
FROM orders o2
WHERE o2.user_id = u.id
)
);

Questions:

  1. What does the innermost query calculate?
  2. Is the innermost query correlated?
  3. What does the middle EXISTS query check?
  4. Which users will be returned?
  5. Will a user with exactly one order be returned?
  6. Why does this query not need DISTINCT?

Try to manually execute it for:

John
Mary
Bob
Alice
David
Sara

If you can reason through this query confidently, your understanding of SQL has moved beyond basic CRUD querying.


Homework

Use the users and orders tables.

Write both SQL and ActiveRecord for each exercise.

  1. Find users who have at least one completed order.
  2. Find users who have no completed orders.
  3. Find orders greater than the global average order amount.
  4. Find orders greater than the average order amount for their respective user.
  5. Find users whose total order amount is greater than the average total spending across all users who have orders.
  6. Find users who have an order greater than every order placed by John. Use ALL.
  7. Find users who have an order greater than at least one order placed by Sara. Use ANY.
  8. Rewrite “users without orders” using:
    • LEFT JOIN
    • NOT EXISTS
    • NOT IN
    Then explain the NULL behavior of each approach.
  9. Write a query using a subquery in FROM to calculate user totals, then join the derived table with users to display:
user name
total spent
  1. Use EXPLAIN ANALYZE to compare:
IN

versus:

EXISTS

for finding users with orders.

Don’t worry if you can’t interpret the complete execution plan yet. Save the output – we’ll learn how to read it systematically.


Day 6 Preview

On Day 6, we’ll cover Indexes and EXPLAIN ANALYZE.

This is one of the most important transitions in the course because we’ll move from:

“Can I write the correct query?”

to:

“Can I explain why this query is fast or slow?”

We’ll cover:

  • How PostgreSQL stores tables and indexes conceptually
  • B-tree indexes
  • Single-column indexes
  • Composite indexes
  • Index selectivity
  • Sequential Scan
  • Index Scan
  • Bitmap Index Scan
  • EXPLAIN
  • EXPLAIN ANALYZE
  • Why PostgreSQL sometimes ignores an index
  • Indexes for foreign keys
  • Rails migrations for indexes
  • Query optimization interview questions

For a senior Rails interview, Day 6 is one of the highest-value lessons in the entire course.

Day 6: https://railsdrop.com/learn-sql-day-6-part-14-interview-questions/

Happy Learning! ๐Ÿš€


Learn SQL: Day 3 โ€“ JOINs (One of the Most Important SQL Topic)

If I had to choose one SQL topic that appears most frequently in Senior Developer interviews, it would be:

JOINs

Most Rails developers know:

User.joins(:orders)

But many cannot explain:

  • What SQL Rails generates
  • How PostgreSQL executes it
  • Why duplicates occur
  • When to use joins
  • When to use includes
  • When JOINs become slow

A senior engineer should be comfortable with all of these.

Today’s Goals

By the end of Day 3, you’ll understand:

  • INNER JOIN
  • LEFT JOIN
  • RIGHT JOIN
  • FULL OUTER JOIN
  • CROSS JOIN
  • Self JOIN
  • How Rails translates associations into JOINs
  • N+1 query problem
  • joins vs includes
  • Interview questions

Step 1: Create Fresh Tables

Let’s create a simple system.

Users

DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS users;

CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  name VARCHAR(100)
);

Orders

CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT NOT NULL,
  amount NUMERIC(10,2),

  CONSTRAINT fk_orders_user
  FOREIGN KEY (user_id)
  REFERENCES users(id)
);

Insert Sample Data

Users:

INSERT INTO users(name)
VALUES
('John'),
('Mary'),
('Bob'),
('Alice');


Orders:

INSERT INTO orders(user_id, amount)
VALUES
(1,100),
(1,200),
(2,300),
(2,400),
(2,500);


Current data:

users

idname
1John
2Mary
3Bob
4Alice

orders

iduser_idamount
11100
21200
32300
42400
52500

Notice:

Bob has no orders
Alice has no orders

This becomes important.

What is a JOIN?

A JOIN combines rows from multiple tables.

Think:

users + orders = business information


The database uses a common column:

users.id = orders.user_id


1. INNER JOIN

Most common JOIN.

Returns only matching rows.

Query

SELECT
users.id,
users.name,
orders.amount
FROM users
INNER JOIN orders
ON users.id = orders.user_id;


Result:

nameamount
John100
John200
Mary300
Mary400
Mary500

Notice:

Bob disappeared
Alice disappeared

Why?

Because they have no matching order.

Visual

users         orders

John      <->   100
John      <->   200
Mary      <->   300
Mary      <->   400
Mary      <->   500

Bob       X
Alice     X

Only matches survive.

Rails Equivalent

User.joins(:orders)

Generated SQL:

SELECT users.*
FROM users
INNER JOIN orders
ON orders.user_id = users.id;

Interview Question

What type of JOIN does Rails joins use?

Answer:

INNER JOIN

Many candidates miss this.

2. LEFT JOIN

Returns:

All rows from LEFT table
+
matching rows from RIGHT table

Query

SELECT
users.name,
orders.amount
FROM users
LEFT JOIN orders
ON users.id = orders.user_id;

Result:

nameamount
John100
John200
Mary300
Mary400
Mary500
BobNULL
AliceNULL

Notice:

Bob exists
Alice exists

Even without orders.

Visual

LEFT TABLE = users
Keep everything
John -> order
Mary -> order
Bob -> NULL
Alice -> NULL

Rails Equivalent

User.left_joins(:orders)

Generated SQL:

LEFT OUTER JOIN

Practical Example

Find users without orders.

SELECT users.*
FROM users
LEFT JOIN orders
ON users.id = orders.user_id
WHERE orders.id IS NULL;

Result:

Bob
Alice

Rails:

User.left_joins(:orders)
.where(orders: { id: nil })

Common Interview Question

Find customers who never placed an order.

Expected answer:

LEFT JOIN
+
IS NULL

3. RIGHT JOIN

Opposite of LEFT JOIN.

Keep all rows from right table.

SELECT *
FROM users
RIGHT JOIN orders
ON users.id = orders.user_id;

In real-world Rails projects:

Rarely used

Most engineers rewrite it as LEFT JOIN.

4. FULL OUTER JOIN

Keep everything.

SELECT *
FROM users
FULL OUTER JOIN orders
ON users.id = orders.user_id;

Returns:

All users
+
All orders

matched where possible.

Used occasionally for:

  • reporting
  • analytics
  • reconciliation

Rare in Rails applications.

5. CROSS JOIN

Creates every possible combination.

Example:

CREATE TABLE colors (
color VARCHAR(20)
);
INSERT INTO colors
VALUES ('Red'),('Blue');

Sizes:

CREATE TABLE sizes (
size VARCHAR(20)
);
INSERT INTO sizes
VALUES ('S'),('M');

Query:

SELECT *
FROM colors
CROSS JOIN sizes;

Result:

Red S
Red M
Blue S
Blue M

Every row paired with every row.

Formula:

RowsA ร— RowsB

Interview Question:

10 rows ร— 100 rows

How many rows?

Answer:

1000

6. Self JOIN

A table joins itself.

Very common interview topic.

Create employees:

CREATE TABLE employees (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100),
manager_id BIGINT
);

Insert:

INSERT INTO employees
(name, manager_id)
VALUES
('CEO', NULL),
('Manager1',1),
('Manager2',1),
('Developer1',2),
('Developer2',2);

Query:

SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.id;

Result:

employeemanager
CEONULL
Manager1CEO
Developer1Manager1

Rails

class Employee < ApplicationRecord
belongs_to :manager,
class_name: "Employee",
optional: true
has_many :subordinates,
class_name: "Employee",
foreign_key: :manager_id
end

Why Duplicates Occur

Look at:

SELECT *
FROM users
INNER JOIN orders
ON users.id = orders.user_id;

Mary has:

3 orders

Therefore:

Mary appears 3 times

JOINs multiply rows.

This is one of the most misunderstood SQL concepts.

DISTINCT After JOIN

Sometimes we want unique users.

SELECT DISTINCT users.*
FROM users
JOIN orders
ON users.id = orders.user_id;

Rails

User.joins(:orders).distinct

The N+1 Query Problem

Every Rails interview asks this.

Suppose:

users = User.all
users.each do |user|
puts user.orders.count
end

Queries:

SELECT * FROM users;

Then:

SELECT * FROM orders WHERE user_id=1;
SELECT * FROM orders WHERE user_id=2;
SELECT * FROM orders WHERE user_id=3;
...

100 users:

101 queries

Called:

N+1 problem

Fix Using includes

User.includes(:orders)

Rails loads:

SELECT * FROM users;

and

SELECT * FROM orders
WHERE user_id IN (...);

Only 2 queries.

joins vs includes

This is a favorite interview question.

joins

Used for filtering.

User.joins(:orders)

SQL:

INNER JOIN

Purpose:

Filter data

includes

Used for eager loading.

User.includes(:orders)

Purpose:

Avoid N+1

Example

Find users with orders.

User.joins(:orders)

Display users and orders.

User.includes(:orders)

Interview Question

Which is better?

joins

or

includes

Answer:

Depends on the problem.

Different purposes.

Real Interview Queries

Find users with orders

SELECT DISTINCT users.*
FROM users
JOIN orders
ON users.id = orders.user_id;

Rails:

User.joins(:orders).distinct

Find users without orders

SELECT users.*
FROM users
LEFT JOIN orders
ON users.id = orders.user_id
WHERE orders.id IS NULL;

Rails:

User.left_joins(:orders)
.where(orders: { id: nil })

Find total orders per user

SELECT
users.name,
COUNT(orders.id)
FROM users
LEFT JOIN orders
ON users.id = orders.user_id
GROUP BY users.name;

We’ll study GROUP BY in Day 4.

Senior-Level Insights

1. Most Rails JOINs are INNER JOINs

joins

means:

INNER JOIN

2. LEFT JOIN + IS NULL

is the standard pattern for:

Find missing records

Examples:

  • users without orders
  • customers without subscriptions
  • products without inventory

3. JOINs Multiply Rows

One user:

3 orders

becomes:

3 result rows

Always remember this.

4. Understand the Generated SQL

When writing:

User.joins(:orders)

you should mentally see:

INNER JOIN orders
ON orders.user_id = users.id

immediately.


Practical Exercises

Exercise 1

Find all users with orders.

Exercise 2

Find all users without orders.

Exercise 3

Add 3 more orders for John.

Run:

INNER JOIN

Observe duplicates.

Exercise 4

Use:

DISTINCT

to remove duplicates.

Exercise 5

Create:

categories
products

and practice:

INNER JOIN
LEFT JOIN

Homework

Create the following schema:

authors
books
publishers

Relationships:

Author has many Books
Publisher has many Books
Book belongs to Author
Book belongs to Publisher

Insert sample data.

Write queries:

  1. Books with author names
  2. Books with publisher names
  3. Authors without books
  4. Publishers without books
  5. Unique authors who have books

Write both:

  • SQL
  • ActiveRecord

Qn) I cannot see any difference in output between left join and full outer join with our example. can u create a new data to see me correct difference between them?

Check for Answer : https://railsdrop.com/learn-sql-day3-difference-between-left-join-and-full-outer-join-output/

When to use FULL OUTER JOIN?

Check the page: https://railsdrop.com/learn-sql-day-3-when-to-use-full-outer-join/


Quick Quiz

Given these tables:

A

id
1
2
3

B

id
2
3
4

Without running SQL, what rows do you expect from:

  1. INNER JOIN
  2. LEFT JOIN (A LEFT JOIN B)
  3. RIGHT JOIN (A RIGHT JOIN B)
  4. FULL OUTER JOIN

Try answering these on paper first. If you can do that confidently, you’ve truly understood how the different joins work.


Day 4 Preview

Tomorrow we’ll cover one of the highest-frequency SQL interview topics:

GROUP BY & Aggregations

Including:

  • COUNT
  • SUM
  • AVG
  • MIN
  • MAX
  • GROUP BY
  • HAVING
  • Aggregate queries in Rails
  • Real interview problems such as:
    • Top customers
    • Revenue calculations
    • Most purchased products
    • Reporting queries

This is where SQL starts becoming analytical rather than just relational.

Day 4: https://railsdrop.com/2024/02/15/learn-sql-day-4-group-by-aggregate-functions/

Happy Learning!