How do I retrieve data using a SELECT statement?

The SELECT statement is the most fundamental SQL command — it lets you read (query) data from one or more tables in a relational database. Here’s a comprehensive guide to using it effectively.

1. The Basic Syntax

SELECT column1, column2, ...
FROM table_name;
  • SELECT — tells the database what columns you want.
  • FROM — tells it where (which table) to look.

Example: Get all names and emails from a users table

SELECT name, email
FROM users;

2. Selecting All Columns with *

Use the asterisk (*) to retrieve every column:

SELECT *
FROM users;

Tip: Avoid SELECT * in production code. It’s slower, returns unnecessary data, and breaks if the schema changes. Always list the columns you actually need.

3. Filtering Rows with WHERE

The WHERE clause restricts which rows are returned.

SELECT name, email
FROM users
WHERE age > 18;

Common WHERE operators

Operator Meaning Example
= Equal status = 'ACTIVE'
<> or != Not equal country <> 'US'
<, >, <=, >= Comparison age >= 21
BETWEEN Within a range age BETWEEN 18 AND 65
IN Matches any value in a list country IN ('US', 'UK', 'DE')
LIKE Pattern match name LIKE 'A%' (starts with “A”)
IS NULL Missing value deleted_at IS NULL

Combining conditions with AND / OR

SELECT id, name
FROM users
WHERE age >= 18 AND country = 'US';

4. Sorting Results with ORDER BY

SELECT name, age
FROM users
ORDER BY age DESC;
  • ASC — ascending (default)
  • DESC — descending

You can sort by multiple columns:

SELECT name, country, age
FROM users
ORDER BY country ASC, age DESC;

5. Limiting Results

Return only the first N rows (syntax varies by database):

-- PostgreSQL, MySQL, SQLite
SELECT name FROM users LIMIT 10;

-- SQL Server
SELECT TOP 10 name FROM users;

-- Oracle / Standard SQL
SELECT name FROM users FETCH FIRST 10 ROWS ONLY;

6. Removing Duplicates with DISTINCT

SELECT DISTINCT country
FROM users;

Returns each unique country only once.

7. Renaming Columns with Aliases (AS)

SELECT
    name       AS full_name,
    age * 12   AS age_in_months
FROM users;

Aliases make output more readable and are useful for computed columns.

8. Aggregating Data

Aggregate functions summarize multiple rows into one:

Function Purpose
COUNT() Number of rows
SUM() Total of a numeric col
AVG() Average
MIN() Smallest value
MAX() Largest value
SELECT COUNT(*) AS total_users,
       AVG(age) AS average_age
FROM users;

Grouping with GROUP BY

SELECT country, COUNT(*) AS user_count
FROM users
GROUP BY country
ORDER BY user_count DESC;

Filtering groups with HAVING

WHERE filters rows before grouping; HAVING filters after.

SELECT country, COUNT(*) AS user_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

9. Joining Multiple Tables

Retrieve data spread across related tables using JOIN.

SELECT u.name, o.total, o.created_at
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.total > 100
ORDER BY o.created_at DESC;

Common join types:

  • INNER JOIN — only matching rows in both tables (default)
  • LEFT JOIN — all rows from the left table + matches from the right
  • RIGHT JOIN — the opposite of LEFT
  • FULL OUTER JOIN — all rows from both sides

10. The Logical Order of a SELECT

Even though you write clauses in this order:

SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT ...

The database executes them in this order:

  1. FROM (and JOIN)
  2. WHERE
  3. GROUP BY
  4. HAVING
  5. SELECT
  6. ORDER BY
  7. LIMIT

Understanding this order helps explain why you can’t use a SELECT alias in WHERE, but you can in ORDER BY.

11. A Complete Realistic Example

SELECT
    u.country,
    COUNT(o.id)       AS order_count,
    SUM(o.total)      AS total_revenue,
    AVG(o.total)      AS avg_order_value
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.status = 'COMPLETED'
  AND o.created_at >= '2025-01-01'
GROUP BY u.country
HAVING SUM(o.total) > 10000
ORDER BY total_revenue DESC
LIMIT 20;

“Show me the top 20 countries by revenue in 2025, but only those exceeding $10,000 in completed orders.”

12. Using SELECT in Java (Spring Data JPA)

If you are using Spring Data JPA, you typically don’t execute raw SQL. Instead:

Derived query methods

List<User> findByAgeGreaterThan(int age);
List<User> findByCountryAndStatusOrderByCreatedAtDesc(String country, Status status);

JPQL with @Query

@Query("SELECT u FROM User u WHERE u.age > :minAge")
List<User> findAdults(@Param("minAge") int minAge);

Native SQL with @Query(nativeQuery = true)

@Query(value = "SELECT * FROM users WHERE age > :minAge", nativeQuery = true)
List<User> findAdultsNative(@Param("minAge") int minAge);

Projections (return only certain fields)

public interface UserSummary {
    String getName();
    String getEmail();
}

@Query("SELECT u.name AS name, u.email AS email FROM User u WHERE u.age > :minAge")
List<UserSummary> findAdultSummaries(@Param("minAge") int minAge);

13. Best Practices

  • List columns explicitly instead of SELECT *.
  • Use parameter binding (? placeholders or :named params) — never concatenate user input into SQL (prevents SQL injection).
  • Filter early with WHERE to reduce the working set.
  • Index columns used in WHERE, JOIN, and ORDER BY.
  • Use LIMIT when you only need a few rows.
  • Read the execution plan (EXPLAIN / EXPLAIN ANALYZE) when queries are slow.

Summary

Clause Purpose
SELECT Which columns to return
FROM Which table(s) to read from
JOIN Combine rows from related tables
WHERE Filter individual rows
GROUP BY Group rows sharing a value
HAVING Filter groups
ORDER BY Sort the results
LIMIT Restrict how many rows are returned

How do I insert data into a SQL table?

To insert data into a SQL table, you use the INSERT INTO statement. Here’s a breakdown of the syntax, common patterns, and how it fits into a Spring Data JPA project.

1. Basic Syntax

INSERT INTO table_name (column1, column2, column3)
VALUES (value1, value2, value3);

Example — inserting a user into a users table:

INSERT INTO users (name, email, age)
VALUES ('Alice', '[email protected]', 25);

2. Inserting Without Specifying Columns

If you provide values for every column in the exact order they’re defined in the table, you can omit the column list:

INSERT INTO users
VALUES (1, 'Alice', '[email protected]', 25);

Not recommended — if the schema changes, your query breaks. Always list columns explicitly.

3. Inserting Multiple Rows at Once

Most databases (MySQL, PostgreSQL, SQL Server) support multi-row inserts:

INSERT INTO users (name, email, age)
VALUES
  ('Alice', '[email protected]', 25),
  ('Bob',   '[email protected]',   30),
  ('Carol', '[email protected]', 22);

This is much faster than running many separate INSERT statements.

4. Inserting from Another Table

You can copy rows from one table into another using INSERT INTO ... SELECT:

INSERT INTO archived_users (name, email, age)
SELECT name, email, age
FROM users
WHERE age < 18;

5. Handling Auto-Generated Columns

If a column is auto-generated (e.g., id BIGINT AUTO_INCREMENT PRIMARY KEY), simply omit it — the database will generate the value:

INSERT INTO users (name, email, age)
VALUES ('Dave', '[email protected]', 40);

6. Handling Duplicates

Different databases offer different ways to handle conflicts:

PostgreSQL — ON CONFLICT:

INSERT INTO users (email, name)
VALUES ('[email protected]', 'Alice')
ON CONFLICT (email) DO NOTHING;

MySQL — ON DUPLICATE KEY UPDATE:

INSERT INTO users (email, name)
VALUES ('[email protected]', 'Alice')
ON DUPLICATE KEY UPDATE name = VALUES(name);

7. In a Spring Data JPA Project

In a project that uses Spring Data JPA, you usually don’t write INSERT statements directly. Instead:

Option A — Use the repository (recommended)

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private int age;
    // getters/setters or Lombok @Data
}
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

    public User createUser(String name, String email, int age) {
        User user = new User();
        user.setName(name);
        user.setEmail(email);
        user.setAge(age);
        return userRepository.save(user); // Generates the INSERT for you
    }
}

Option B — Native SQL with @Query

public interface UserRepository extends JpaRepository<User, Long> {

    @Modifying
    @Transactional
    @Query(value = "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)",
           nativeQuery = true)
    void insertUser(@Param("name") String name,
                    @Param("email") String email,
                    @Param("age") int age);
}

8. Best Practices

  • Always list columns explicitly — makes queries resilient to schema changes.
  • Use parameterized queries / prepared statements to prevent SQL injection.
  • Batch inserts when loading large amounts of data.
  • Wrap multiple inserts in a transaction for atomicity.
  • Don’t insert into auto-generated columns manually unless you have a reason.

Summary

Task Statement
Insert one row INSERT INTO t (cols) VALUES (...);
Insert multiple rows INSERT INTO t (cols) VALUES (...), (...);
Copy from another table INSERT INTO t (cols) SELECT ... FROM other;
Handle duplicates ON CONFLICT (Postgres) / ON DUPLICATE KEY (MySQL)

How do I choose the correct SQL data type for a column?

Choosing the right data type is one of the most important decisions when designing a database schema. It affects storage size, performance, data integrity, and future maintainability. Here’s a practical guide to help you decide.


1. Start With the Nature of the Data

Ask yourself: What kind of value will this column hold?

Data Nature Typical SQL Types
Whole numbers SMALLINT, INT, BIGINT
Decimal / monetary DECIMAL(p,s) / NUMERIC(p,s)
Approximate numbers REAL, DOUBLE PRECISION, FLOAT
Short text VARCHAR(n), CHAR(n)
Long text TEXT, CLOB
Dates & times DATE, TIME, TIMESTAMP, TIMESTAMP WITH TIME ZONE
Boolean flags BOOLEAN (or BIT / TINYINT in some DBs)
Binary data BLOB, BYTEA, VARBINARY
Identifiers (UUIDs) UUID (Postgres), CHAR(36), BINARY(16)
Structured JSON JSON, JSONB (Postgres)

2. Match the Range and Precision

Pick the smallest type that safely fits your data — but don’t over-optimize prematurely.

Integers

  • SMALLINT → -32,768 to 32,767 (age, small counters)
  • INT → ~±2.1 billion (most IDs, counts)
  • BIGINT → for very large IDs or high-volume tables

Decimals

  • Use DECIMAL(p, s) for money and anything requiring exact arithmetic.
price DECIMAL(10, 2)  -- up to 99,999,999.99
  • Avoid FLOAT/DOUBLE for financial data — rounding errors will bite you.

Strings

  • Use VARCHAR(n) when you know a reasonable maximum length.
  • Use TEXT for free-form or unbounded text (descriptions, comments).
  • Use CHAR(n) only for truly fixed-length values (e.g., ISO country codes CHAR(2)).

3. Prefer Semantic Types Over Generic Ones

If your database offers a specialized type, use it — it enforces integrity and enables optimizations.

  • DATE instead of VARCHAR for dates
  • BOOLEAN instead of CHAR(1) with 'Y'/'N'
  • UUID instead of VARCHAR(36)
  • INET / CIDR for IP addresses (Postgres)
  • JSONB instead of TEXT for JSON payloads (Postgres)

4. Consider Time Zones for Timestamps

  • TIMESTAMP → stores no time zone; ambiguous across regions.
  • TIMESTAMP WITH TIME ZONE (TIMESTAMPTZ in Postgres) → recommended for anything user-facing or distributed.
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()

5. Think About NULL vs NOT NULL and Defaults

The type alone isn’t enough — pair it with proper constraints:

email       VARCHAR(255) NOT NULL,
status      VARCHAR(20)  NOT NULL DEFAULT 'ACTIVE',
deleted_at  TIMESTAMP WITH TIME ZONE NULL

6. Watch Out for Common Pitfalls

Anti-pattern Better choice
FLOAT for money DECIMAL(p, s)
VARCHAR for dates DATE / TIMESTAMP
TEXT for everything VARCHAR(n) with a sane limit
CHAR(1) 'Y'/'N' BOOLEAN
INT for phone numbers VARCHAR(20) (leading zeros, +, formatting)
VARCHAR(255) reflexively Choose a length that reflects the domain

7. Align With Your Application Layer

Keep the SQL type consistent with the Java field type:

Java/Kotlin Recommended SQL
Long / Int BIGINT / INT
BigDecimal DECIMAL(p, s)
String VARCHAR(n) or TEXT
LocalDate DATE
LocalDateTime TIMESTAMP
OffsetDateTime / Instant TIMESTAMP WITH TIME ZONE
UUID UUID (or BINARY(16))
Boolean BOOLEAN
enum VARCHAR (with @Enumerated(EnumType.STRING))

8. A Quick Decision Checklist

Before finalizing a column type, ask:

  1. What values will it hold, and what’s the realistic range?
  2. Exact or approximate arithmetic required?
  3. Fixed or variable length?
  4. Time-zone aware or not?
  5. Does the DB offer a native type (JSON, UUID, INET, etc.)?
  6. Does it match the application layer type cleanly?
  7. Is the column indexed / searched / joined on? Smaller types = faster indexes.
  8. Will it be NULLable? What’s the default?

TL;DR

Pick the most specific, smallest, semantically correct type that fits the data — and align it with both your business rules and your ORM mappings.

How do I create my first database and table?

Now that you have a database server installed, let’s walk through creating your first database and table. I’ll show you how to do this both directly with SQL (recommended for learning) and briefly mention the programmatic approach.

Part 1: Create Your First Database

Step 1: Connect to Your Database Server

Open a terminal and connect using the CLI:

MySQL:

mysql -u root -p

PostgreSQL:

psql -U postgres

Enter the password you set during installation.

Step 2: Create the Database

Once connected, run:

CREATE DATABASE bookstore;

You should see a confirmation like Query OK, 1 row affected.

Step 3: Verify It Was Created

SHOW DATABASES;   -- MySQL
-- \l            -- PostgreSQL

You should see bookstore in the list.

Step 4: Switch to Your New Database

Before creating tables, tell the server which database to work with:

USE bookstore;   -- MySQL
-- \c bookstore  -- PostgreSQL

Part 2: Create Your First Table

A table is where actual data lives. It has columns (fields) and rows (records).

Step 1: Design Your Table

Let’s create a simple book table. Before writing SQL, think about:

  • What data do you want to store? (title, author, price, etc.)
  • What type is each field? (text, number, date)
  • Which field uniquely identifies a row? (the primary key)

Step 2: Write the CREATE TABLE Statement

CREATE TABLE book (
    id             BIGINT       NOT NULL AUTO_INCREMENT,
    isbn           VARCHAR(50)  NOT NULL,
    title          VARCHAR(100) NOT NULL,
    author         VARCHAR(100) NOT NULL,
    published_year INT,
    price          DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
    PRIMARY KEY (id)
);

Understanding Each Part

Element Meaning
id Column name
BIGINT Data type — a large integer
AUTO_INCREMENT Database auto-generates the next number (MySQL syntax)
NOT NULL This field is required
VARCHAR(100) Variable-length text, up to 100 characters
DECIMAL(10, 2) Number with 10 digits total, 2 after the decimal point
DEFAULT 0.00 Default value if none is provided
PRIMARY KEY (id) Marks id as the unique identifier for each row

PostgreSQL note: Replace BIGINT ... AUTO_INCREMENT with BIGSERIAL or BIGINT GENERATED ALWAYS AS IDENTITY.

Step 3: Verify the Table Was Created

SHOW TABLES;              -- MySQL
DESCRIBE book;            -- MySQL — shows column details

-- \dt                    -- PostgreSQL — list tables
-- \d book                -- PostgreSQL — describe a table

Part 3: Add Some Data

Insert Rows

INSERT INTO book (isbn, title, author, published_year, price)
VALUES
    ('978-0134685991', 'Effective Java',        'Joshua Bloch',    2018, 45.00),
    ('978-0132350884', 'Clean Code',            'Robert C. Martin', 2008, 39.99),
    ('978-0596009205', 'Head First Design Patterns', 'Eric Freeman', 2004, 49.95);

Query Your Data

SELECT * FROM book;

You should see all three rows with their auto-generated id values.

Part 4: Common Beginner Operations

Filter with WHERE

SELECT title, price FROM book WHERE price < 45.00;

Sort with ORDER BY

SELECT title, published_year FROM book ORDER BY published_year DESC;

Update a Row

UPDATE book SET price = 42.00 WHERE isbn = '978-0134685991';

Delete a Row

DELETE FROM book WHERE id = 3;

Part 5: Doing This from Java (Optional)

Since your project uses Java, you can also create databases and tables programmatically through JDBC. The general pattern is:

  1. Get a Connection via DriverManager.getConnection(url, user, password)
  2. Create a Statement or PreparedStatement
  3. Execute your CREATE DATABASE / CREATE TABLE SQL
  4. Use try-with-resources so connections close automatically

However, in a modern Spring Data JPA project, you usually don’t create tables manually with JDBC. Instead:

  • Hibernate/JPA can auto-generate tables from your @Entity classes (via spring.jpa.hibernate.ddl-auto)
  • Flyway or Liquibase manage schema migrations with versioned SQL scripts

But learning the raw SQL first (as shown above) gives you the foundation to understand what these tools do under the hood.

Recommended Next Steps

  1. Create the bookstore database and book table using the SQL above
  2. Insert 5–10 sample rows
  3. Practice SELECT with different WHERE conditions
  4. Learn about relationships — create a second table (e.g., author) and link it with a foreign key
  5. Explore JOINs to combine data from multiple tables

How do I install and set up a database for learning SQL?

Getting a local database up and running is the first step toward learning SQL hands-on. Here’s a practical, beginner-friendly guide.

Step 1: Choose a Database System

For learning SQL, I recommend one of these free options:

Database Best For
SQLite Absolute beginners, no setup
MySQL Web development, widely used
PostgreSQL Modern SQL features, professional use
H2 Java developers, embedded testing

Step 2: Install the Database

Option A: MySQL (Recommended for Beginners)

Windows / macOS:
1. Download the MySQL Community Server from dev.mysql.com/downloads
2. Run the installer and choose “Developer Default”
3. During setup, set a root password (remember it!)
4. Accept default port 3306

macOS (via Homebrew):

brew install mysql
brew services start mysql
mysql_secure_installation

Linux (Ubuntu/Debian):

sudo apt update
sudo apt install mysql-server
sudo systemctl start mysql
sudo mysql_secure_installation

Option B: PostgreSQL

Windows / macOS: Download from postgresql.org/download

macOS:

brew install postgresql@16
brew services start postgresql@16

Linux:

sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql

Option C: Docker (Cleanest Setup)

If you have Docker, this is the fastest way:

# MySQL
docker run --name mysql-learn -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql:8

# PostgreSQL
docker run --name pg-learn -e POSTGRES_PASSWORD=root -p 5432:5432 -d postgres:16

Step 3: Install a Database Client

You’ll want a GUI to browse tables and run queries visually:

  • DataGrip (JetBrains, paid but excellent) — or use the Database tool window built into IntelliJ IDEA Ultimate
  • DBeaver (free, universal) — dbeaver.io
  • MySQL Workbench (free, MySQL-specific)
  • pgAdmin (free, PostgreSQL-specific)

Step 4: Create Your First Database

Connect to your server via CLI or GUI and run:

CREATE DATABASE learning_sql;
USE learning_sql;  -- MySQL syntax
-- \c learning_sql  -- PostgreSQL syntax

Step 5: Load Sample Data

Learning SQL is much easier with realistic data. Try one of these well-known sample databases:

Import via CLI, for example:

mysql -u root -p learning_sql < sakila-schema.sql
mysql -u root -p learning_sql < sakila-data.sql

Step 6: Practice SQL Basics

Start with these fundamentals in order:

  1. SELECT, FROM, WHERE — retrieving data
  2. ORDER BY, LIMIT — sorting and paging
  3. JOIN — combining tables (INNER, LEFT, RIGHT)
  4. GROUP BY, aggregate functions (COUNT, SUM, AVG)
  5. Subqueries and CTEs (WITH clauses)
  6. INSERT, UPDATE, DELETE — modifying data
  7. CREATE TABLE, constraints, indexes — schema design

Great Practice Resources

Step 7: Verify Your Setup

Run a quick sanity check in your SQL client:

SELECT VERSION();
SHOW DATABASES;   -- MySQL
-- \l              -- PostgreSQL

If you see version info and your learning_sql database listed, you’re all set!

Quick Recommendation

If you want the fastest path with minimal friction:

  1. Install MySQL (or run it via Docker)
  2. DBeaver as your client
  3. Import the Sakila sample database
  4. Work through SQLZoo tutorials alongside it