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.
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
| Error | Cause | Fix |
|---|---|---|
| mssql-tools/bin/sqlcmd: no such file | Old mssql-tools path | Use mssql-tools18 |
| gzip: stdin: not in gzip format | Wrong download URL | Download from GitHub releases |
| Could not resolve host | No access to that URL | Find the correct URL in the official repo |
| Query doesn't run (interactive) | Missing GO at the end | Type GO on a new line and Enter |
| -No flag | Untrusted SSL certificate | Normal in development, -No ignores it |
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
| Clause | What it does | Example |
|---|---|---|
| SELECT TOP N | Limit rows returned | SELECT TOP 10 * |
| WHERE | Filter rows | WHERE Color = 'Red' |
| AND / OR | Combine conditions | WHERE Price > 100 AND Color = 'Red' |
| BETWEEN | Range of values | WHERE Price BETWEEN 100 AND 500 |
| IN | List of values | WHERE Color IN ('Red','Blue') |
| LIKE | Partial match | WHERE Name LIKE '%Bike%' |
| IS NULL | Detect nulls | WHERE Color IS NULL |
| ORDER BY DESC | Sort descending | ORDER BY Price DESC |
| GROUP BY | Group for aggregation | GROUP BY Color |
| HAVING | Filter groups | HAVING COUNT(*) > 5 |
| INNER JOIN | Only matching rows | JOIN Table b ON a.id = b.id |
| LEFT JOIN | All rows from the left table | LEFT JOIN Table b ON a.id = b.id |
| CASE WHEN | Conditional logic | CASE WHEN x > 0 THEN 'YES' ELSE 'NO' END |
| DATEADD | Add / subtract dates | DATEADD(MONTH, -1, GETDATE()) |
| DATEDIFF | Date difference | DATEDIFF(DAY, StartDate, EndDate) |
07 · Aggregate functions
| Function | What 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 |