Back
Dec 10, 2024 · 12 min read

SQL guide

The most-used queries in real work settings: Docker setup, sales reports, customer analysis, inventory, data cleaning and a syntax reference. SQL Server + AdventureWorks.

SQLDataSQL ServerReferencia

00 · Work environment setup

Spin up SQL Server with Docker and load AdventureWorks on Linux.

Step 0 — Install Docker

Ubuntu / Debian:

sudo apt update
sudo apt install -y docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker

Arch Linux:

sudo pacman -S docker
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker

Verify with `docker ps`. If there are no containers it shows an empty table — that's correct.

Step 1 — Install and start SQL Server

A single command downloads SQL Server and leaves it running:

docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Admin1234!" \
  -p 1433:1433 --name sqlserver \
  -d mcr.microsoft.com/mssql/server:2019-latest

The first time it pulls the image (1–2 min); after that it's instant. To stop or restart:

docker stop sqlserver    # stop
docker start sqlserver   # start again

Step 2 — Download AdventureWorks

wget https://github.com/Microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorks2019.bak

Step 3 — Copy the .bak into the container

docker cp AdventureWorks2019.bak sqlserver:/var/opt/mssql/data/

Step 4 — Restore the database

Use mssql-tools18 (not mssql-tools — that path doesn't exist on recent SQL Server 2019 images):

docker exec -it sqlserver /opt/mssql-tools18/bin/sqlcmd \
  -S localhost -U SA -P "Admin1234!" -No \
  -Q "RESTORE DATABASE AdventureWorks2019 FROM DISK='/var/opt/mssql/data/AdventureWorks2019.bak'
  WITH MOVE 'AdventureWorks2019' TO '/var/opt/mssql/data/AdventureWorks2019.mdf',
  MOVE 'AdventureWorks2019_log' TO '/var/opt/mssql/data/AdventureWorks2019_log.ldf'"

Step 5 — Connect to the database

Interactive mode (each block ends with GO):

docker exec -it sqlserver /opt/mssql-tools18/bin/sqlcmd \
  -S localhost -U SA -P "Admin1234!" -No \
  -d AdventureWorks2019

Direct mode with -Q (runs and exits, handy for one-off queries):

docker exec -it sqlserver /opt/mssql-tools18/bin/sqlcmd \
  -S localhost -U SA -P "Admin1234!" -No \
  -d AdventureWorks2019 \
  -Q "SELECT TOP 5 Name FROM Production.Product"

Common errors

ErrorCauseFix
mssql-tools/bin/sqlcmd: no such fileOld mssql-tools pathUse mssql-tools18
gzip: stdin: not in gzip formatWrong download URLDownload from GitHub releases
Could not resolve hostNo access to that URLFind the correct URL in the official repo
Query doesn't run (interactive)Missing GO at the endType GO on a new line and Enter
-No flagUntrusted SSL certificateNormal in development, -No ignores it
Verify everything works: run SELECT TOP 3 Name, ListPrice FROM Production.Product. If you see rows with product names, the environment is ready.

01 · Explore the database

The first thing to do at a new job: understand the database structure.

List all tables

SELECT SCHEMA_NAME(schema_id) AS schema_name, name AS table_name
FROM sys.tables
ORDER BY schema_name, table_name

View a table's columns

SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'YourTableName'
ORDER BY ORDINAL_POSITION

Row count per table

SELECT t.TABLE_NAME, p.rows AS RowCount
FROM INFORMATION_SCHEMA.TABLES t
JOIN sys.tables st ON st.name = t.TABLE_NAME
JOIN sys.partitions p ON p.object_id = st.object_id
WHERE t.TABLE_TYPE = 'BASE TABLE' AND p.index_id IN (0,1)
ORDER BY p.rows DESC

02 · Sales reports

The most requested query in any company with commercial data.

Sales by category

SELECT c.Name AS Category,
       SUM(od.LineTotal) AS TotalSales,
       COUNT(DISTINCT oh.SalesOrderID) AS OrderCount
FROM Sales.SalesOrderDetail od
JOIN Production.Product p ON od.ProductID = p.ProductID
JOIN Production.ProductSubcategory s ON p.ProductSubcategoryID = s.ProductSubcategoryID
JOIN Production.ProductCategory c ON s.ProductCategoryID = c.ProductCategoryID
JOIN Sales.SalesOrderHeader oh ON od.SalesOrderID = oh.SalesOrderID
GROUP BY c.Name
ORDER BY TotalSales DESC

Sales by month and year

SELECT YEAR(OrderDate) AS Year,
       MONTH(OrderDate) AS Month,
       SUM(TotalDue) AS TotalSales
FROM Sales.SalesOrderHeader
GROUP BY YEAR(OrderDate), MONTH(OrderDate)
ORDER BY Year, Month

Top 10 best-selling products

SELECT TOP 10
    p.Name AS Product,
    SUM(od.OrderQty) AS UnitsSold,
    SUM(od.LineTotal) AS TotalSales
FROM Sales.SalesOrderDetail od
JOIN Production.Product p ON od.ProductID = p.ProductID
GROUP BY p.Name
ORDER BY UnitsSold DESC

Sales by salesperson

SELECT
    p.FirstName + ' ' + p.LastName AS Salesperson,
    COUNT(oh.SalesOrderID) AS OrderCount,
    SUM(oh.TotalDue) AS TotalSales
FROM Sales.SalesOrderHeader oh
JOIN Sales.SalesPerson sp ON oh.SalesPersonID = sp.BusinessEntityID
JOIN Person.Person p ON sp.BusinessEntityID = p.BusinessEntityID
GROUP BY p.FirstName, p.LastName
ORDER BY TotalSales DESC

03 · Customer analysis

Understand who buys, how much they spend and how often.

Top customers by total spend

SELECT TOP 10
    p.FirstName + ' ' + p.LastName AS Customer,
    COUNT(oh.SalesOrderID) AS OrderCount,
    SUM(oh.TotalDue) AS TotalSpent
FROM Sales.SalesOrderHeader oh
JOIN Sales.Customer c ON oh.CustomerID = c.CustomerID
JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
GROUP BY p.FirstName, p.LastName
ORDER BY TotalSpent DESC

Customers who haven't purchased in the last year

SELECT p.FirstName + ' ' + p.LastName AS Customer,
       MAX(oh.OrderDate) AS LastPurchase
FROM Sales.Customer c
JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
LEFT JOIN Sales.SalesOrderHeader oh ON c.CustomerID = oh.CustomerID
GROUP BY p.FirstName, p.LastName
HAVING MAX(oh.OrderDate) < DATEADD(YEAR, -1, GETDATE())
    OR MAX(oh.OrderDate) IS NULL
ORDER BY LastPurchase

04 · Products and inventory

Track stock and spot products with no movement.

Products with no sales

SELECT p.Name, p.ListPrice
FROM Production.Product p
LEFT JOIN Sales.SalesOrderDetail od ON p.ProductID = od.ProductID
WHERE od.ProductID IS NULL
ORDER BY p.Name

Stock below minimum

SELECT p.Name,
       pi.Quantity AS CurrentStock,
       p.ReorderPoint AS MinStock
FROM Production.Product p
JOIN Production.ProductInventory pi ON p.ProductID = pi.ProductID
WHERE pi.Quantity < p.ReorderPoint
ORDER BY pi.Quantity ASC

Most expensive product per category

SELECT c.Name AS Category,
       p.Name AS Product,
       p.ListPrice
FROM Production.Product p
JOIN Production.ProductSubcategory s ON p.ProductSubcategoryID = s.ProductSubcategoryID
JOIN Production.ProductCategory c ON s.ProductCategoryID = c.ProductCategoryID
WHERE p.ListPrice = (
    SELECT MAX(p2.ListPrice)
    FROM Production.Product p2
    JOIN Production.ProductSubcategory s2 ON p2.ProductSubcategoryID = s2.ProductSubcategoryID
    WHERE s2.ProductCategoryID = c.ProductCategoryID
)
ORDER BY c.Name

05 · Data cleaning and quality

You'll find dirty data at any company. These queries detect it.

Duplicate rows

SELECT EmailAddress, COUNT(*) AS Duplicates
FROM Person.EmailAddress
GROUP BY EmailAddress
HAVING COUNT(*) > 1
ORDER BY Duplicates DESC

Null values per column

SELECT
    COUNT(*) AS TotalRows,
    SUM(CASE WHEN Color IS NULL THEN 1 ELSE 0 END) AS Color_Nulls,
    SUM(CASE WHEN Size IS NULL THEN 1 ELSE 0 END) AS Size_Nulls,
    SUM(CASE WHEN Weight IS NULL THEN 1 ELSE 0 END) AS Weight_Nulls
FROM Production.Product

06 · Quick syntax reference

ClauseWhat it doesExample
SELECT TOP NLimit rows returnedSELECT TOP 10 *
WHEREFilter rowsWHERE Color = 'Red'
AND / ORCombine conditionsWHERE Price > 100 AND Color = 'Red'
BETWEENRange of valuesWHERE Price BETWEEN 100 AND 500
INList of valuesWHERE Color IN ('Red','Blue')
LIKEPartial matchWHERE Name LIKE '%Bike%'
IS NULLDetect nullsWHERE Color IS NULL
ORDER BY DESCSort descendingORDER BY Price DESC
GROUP BYGroup for aggregationGROUP BY Color
HAVINGFilter groupsHAVING COUNT(*) > 5
INNER JOINOnly matching rowsJOIN Table b ON a.id = b.id
LEFT JOINAll rows from the left tableLEFT JOIN Table b ON a.id = b.id
CASE WHENConditional logicCASE WHEN x > 0 THEN 'YES' ELSE 'NO' END
DATEADDAdd / subtract datesDATEADD(MONTH, -1, GETDATE())
DATEDIFFDate differenceDATEDIFF(DAY, StartDate, EndDate)

07 · Aggregate functions

FunctionWhat it does
COUNT(*)Counts all rows
COUNT(column)Counts non-null values
SUM(column)Sums the values
AVG(column)Average of the values
MIN(column)Minimum value
MAX(column)Maximum value
ROUND(n, decimals)Rounds a number
CAST(x AS type)Converts data type
Back