# SQL Server Stored Procedure

```sql!
-- Create
USE AdventureWorks2017
GO -- begin a new batch
CREATE PROCEDURE sp_product_info
AS -- 關鍵字後面的部分是儲存程序的主體
BEGIN
SELECT *
FROM Production.Product p
ORDER BY p.Name DESC
END
-- Alter
USE AdventureWorks2017
GO -- begin a new batch
ALTER PROCEDURE sp_product_info
AS -- 關鍵字後面的部分是儲存程序的主體
BEGIN
SELECT *
FROM Production.Product p
ORDER BY p.Name ASC
END
-- Delete
DROP PROC sp_product_info
```
```sql!
EXEC sp_product_info
```
# Parameters

```sql!
USE AdventureWorks2017
GO
ALTER PROCEDURE [dbo].[spProductList]
(
@MinListPrice AS INT,
@MaxListPrice AS INT
)
AS
BEGIN
SELECT
Name,
ListPrice
FROM Production.Product
WHERE ListPrice >= @MinListPrice AND ListPrice <= @MaxListPrice
ORDER BY Name
END
-- EXEC
EXEC spProductList 500, 1000
-- Naming the parameters before the value will not affect how the procedure works
EXEC spProductList @MinListPrice = 500, @MaxListPrice = 1000
-- ADD TEXT Parameter
USE AdventureWorks2017
GO
ALTER PROCEDURE [dbo].[spProductList]
(
@MinListPrice AS INT,
@MaxListPrice AS INT,
@ProdName AS VARCHAR(MAX)
)
AS
BEGIN
SELECT
Name,
ListPrice
FROM Production.Product
WHERE
ListPrice >= @MinListPrice AND
ListPrice <= @MaxListPrice AND
Name LIKE '%' + @ProdName + '%'
ORDER BY Name
END
EXEC spProductList @MinListPrice = 500, @MaxListPrice = 1000, @ProdName= 'road'
-- ADD NULL to Default
USE AdventureWorks2017
GO
ALTER PROCEDURE [dbo].[spProductList]
(
@MinListPrice AS INT = NULL,
@MaxListPrice AS INT = NULL,
@ProdName AS VARCHAR(MAX)
)
AS
BEGIN
SELECT
Name,
ListPrice
FROM Production.Product
WHERE
(@MinListPrice IS NULL OR ListPrice >= @MinListPrice) AND
(@MaxListPrice IS NULL OR ListPrice <= @MaxListPrice) AND
Name LIKE '%' + @ProdName + '%'
ORDER BY Name
END
EXEC spProductList @ProdName= 'socks'
```
# Variables
> A variable is an object that can hold a single data value of specific type

使用變數前要先宣告,宣告的位置要放在最前面,after use and go statement
將值存在變數裡面,使用關鍵字:`SET`
```sql!
-- assign specific value to our variables using set statement
USE AdventureWorks2017
GO
DECLARE @Date AS DATETIME
SET @Date = '2013-10-31'
SELECT
TransactionID, TransactionDate, 'Transaction' as [Type]
FROM Production.TransactionHistory
WHERE TransactionDate >= @Date
UNION ALL
SELECT
SalesOrderID, OrderDate, 'SalesOrder' as [Type]
FROM Sales.SalesOrderHeader
WHERE OrderDate >= @Date
-- query result stored in variables
USE AdventureWorks2017
GO
DECLARE @Date AS DATETIME
DECLARE @NumTrend AS INT
DECLARE @NumSales AS INT
SET @Date = '2013-10-31'
SET @NumTrend = (SELECT COUNT(0) FROM Production.TransactionHistory WHERE TransactionDate >= @Date)
SET @NumSales = (SELECT COUNT(0) FROM Sales.SalesOrderHeader WHERE OrderDate >= @Date)
SELECT 'Nums Of Transaction', @NumTrend
UNION
SELECT 'Nums Of Sales', @NumSales
SELECT
TransactionID, TransactionDate, 'Transaction' as [Type]
FROM Production.TransactionHistory
WHERE TransactionDate >= @Date
UNION ALL
SELECT
SalesOrderID, OrderDate, 'SalesOrder' as [Type]
FROM Sales.SalesOrderHeader
WHERE OrderDate >= @Date
-- show the value of variables in messages tab
-- sql server concatenate value must be same data type
PRINT 'Nums Of Transaction = ' + CAST(@NumTrend AS VARCHAR(MAX))
PRINT 'Nums of Sales = ' + CAST(@NumSales AS VARCHAR(MAX))
```
```sql!
-- Refer a record in a set of variables
USE AdventureWorks2017
GO
DECLARE @SalesOrderId AS INT
DECLARE @Name AS VARCHAR(MAX)
DECLARE @OrderDate AS DATETIME
SELECT TOP 1
@SalesOrderId = so.SalesOrderID,
@Name = p.Name,
@OrderDate = so.OrderDate
FROM
Sales.SalesOrderHeader so
INNER JOIN Sales.SalesOrderDetail sod ON so.SalesOrderID = sod.SalesOrderID
INNER JOIN Production.Product p ON sod.ProductID = p.ProductID
WHERE so.OrderDate > '2012-05-01'
ORDER BY so.OrderDate DESC
SELECT @SalesOrderId, @Name, @OrderDate
-- Gather values in variables
USE AdventureWorks2017
GO
DECLARE @ProdList VARCHAR(MAX)
SET @ProdList = ''
SELECT
@ProdList = @ProdList + Name + ', ' + CHAR(10)
FROM Production.Product
WHERE YEAR(SellStartDate) = 2012
PRINT @ProdList
-- rows afftected by last statement
SELECT * FROM Production.Product
SELECT @@ROWCOUNT
```
`@@` 這是 global parameters 的前綴
# Output Parameters And Return Values
```sql!
ALTER PROCEDURE spSellStartInYear (
@Year Int,
@ProductList VARCHAR(MAX) OUTPUT,
@ProductCount Int OUTPUT
)
AS
BEGIN
DECLARE @Products VARCHAR(255)
SET @Products = ''
SELECT
@Products = @Products + Name + ', '
FROM Production.Product
WHERE YEAR(SellStartDate) = @Year
ORDER BY Name ASC
SET @ProductCount = @@ROWCOUNT
SET @ProductList = @Products
END
DECLARE @ProdNames VARCHAR(255)
DECLARE @COUNT INT
EXEC spSellStartInYear
@Year = 2013,
@ProductList= @ProdNames OUTPUT,
@ProductCount= @COUNT OUTPUT
SELECT @COUNT AS [Number of Products], @ProdNames AS [List of Products]
```

# If Statements

```sql!
USE AdventureWorks2017
GO
DECLARE @Accessories INT
DECLARE @Clothing INT
SET @Accessories = (SELECT COUNT(1) FROM Production.ProductSubcategory
WHERE ProductCategoryID = 4)
SET @Clothing = (SELECT COUNT(1) FROM Production.ProductSubcategory
WHERE ProductCategoryID = 2)
IF @Accessories > 7
BEGIN
PRINT 'CAUTION'
PRINT 'THERE are too many accessories'
IF @Clothing > 10
BEGIN
PRINT 'more than 10 clothing items'
END
ELSE
BEGIN
PRINT 'less than 10 clothing items'
END
END
ELSE
BEGIN
PRINT 'Less Than 7 Accessories.'
END
```
```SQL!
USE AdventureWorks2017
GO
Alter PROC spGenderData (
@GetInfo VARCHAR(10)
)
AS
BEGIN
IF @GetInfo = 'All'
BEGIN
(SELECT * FROM Production.Product)
RETURN
END
IF @GetInfo = 'Ball'
BEGIN
(SELECT * FROM Production.Product WHERE Name like ('%' + @GetInfo + '%'))
RETURN
END
SELECT 'Not Ball and All'
END
```
# While Loop

# Temp Table
```sql!
-- method 1
SELECT
Name, SellStartDate
INTO #temp_table
FROM [Production].[Product]
WHERE Name LIKE '%THIN%'
SELECT * FROM #temp_table
-- method 2 create a table explicitly
IF OBJECT_ID('tempdb..#temp_table2') IS NOT NULL
DROP TABLE #temp_table2
GO
CREATE TABLE #temp_table2
(
Name VARCHAR(MAX),
SellStartDate DATETIME
)
INSERT INTO #temp_table2
SELECT
Name, SellStartDate
FROM [Production].[Product]
WHERE Name LIKE '%THIN%'
SELECT * FROM #temp_table2
```
temp tables will be stored as part of the system database folder

## Cursor
A Cursor enables traversal over the rows of our results set. It allows to process individual row return by a query.
```sql!
DECLARE @ProductCategoryID INT
DECLARE @ProdCatName VARCHAR(MAX)
DECLARE ProdCatCursor CURSOR FOR
SELECT Name, ProductCategoryID FROM Production.ProductCategory
Open ProdCatCursor
FETCH NEXT FROM ProdCatCursor INTO @ProdCatName, @ProductCategoryID
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT 'Subcategories in Category ' + @ProdCatName
SELECT Name FROM Production.ProductCategory WHERE ProductCategoryID = @ProductCategoryID
FETCH NEXT FROM ProdCatCursor INTO @ProdCatName, @ProductCategoryID
END
CLOSE ProdCatCursor
DEALLOCATE ProdCatCursor
```
# User Defined Functions


```sql!
USE AdventureWorks2017
GO
ALTER FUNCTION fnLengthyDate
(
@EntireDate AS DATETIME
)
RETURNS VARCHAR(MAX)
AS
BEGIN
RETURN DATENAME(DW, @EntireDate) + ' ' + DATENAME(D, @EntireDate) +
CASE
WHEN DAY(@EntireDate) IN (1, 11, 21, 31) THEN 'st'
WHEN DAY(@EntireDate) IN (2, 11, 21, 31) THEN 'nd'
WHEN DAY(@EntireDate) IN (3, 13, 23) THEN 'rd'
ELSE 'th'
END + ' '
+ DATENAME(M, @EntireDate) + ' ' + DATENAME(YY, @EntireDate)
END
```
```sql!
SELECT
[dbo].[fnLengthyDate](SellStartDate)
FROM Production.Product
```
```sql!
USE AdventureWorks2017
GO
CREATE FUNCTION fnFirstWordProdName
(
@ProdName AS VARCHAR(MAX)
)
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE @BlankPosition AS INT
DECLARE @OutCome AS VARCHAR(MAX)
SET @BlankPosition = CHARINDEX(' ', @ProdName)
IF @BlankPosition = 0
SET @OutCome = @ProdName
ELSE
SET @OutCome = LEFT(@ProdName, @BlankPosition - 1)
RETURN @OutCome
END
```
```sql!
SELECT
[dbo].[fnFirstWordProdName](Name),
Name
FROM Production.Product
```
# Table Variables

```SQL!
USE AdventureWorks2017
DECLARE @TempProducts TABLE
(
Name VARCHAR(MAX),
SellStartDate DATETIME
)
INSERT INTO @TempProducts
SELECT
Name, SellStartDate
FROM Production.Product
WHERE SellStartDate < '2012-01-01'
SELECT * FROM @TempProducts
```
## Temp tables VS Table Variables
差異:存續時間
**temporary tables will exist as long as the session that created it remains open**
```sql!
-- 需要在最前面去 drop 不然會出問題
IF OBJECT_ID('tempdb..#TempName') IS NOT NULL
DROP TABLE #TempName;
GO
CREATE TABLE #TempName
(
Name VARCHAR(MAX),
SellStartDate DATETIME
)
INSERT INTO #TempName
SELECT Name, SellStartDate FROM [Production].[Product]
WHERE Name LIKE '%Thin%'
SELECT * FROM #TempName
```

要馬寫 drop 要馬關掉 session
對於 Table Variables 而言,變數存續在代碼運行時。It will be created and dropped instantaneously.
## Table Variables 的缺點
暫存表不用一定要宣告表後再寫入資料,可以透過 select into ie create an insert record into a table in one single step.
```sql!
IF OBJECT_ID('tempdb..#TempName') IS NOT NULL
DROP TABLE #TempName;
GO
SELECT Name, SellStartDate INTO #TempName
FROM [Production].[Product]
WHERE Name LIKE '%Ball%'
SELECT * FROM #TempName
```
因為 sql server 中的變數需要被明確的宣告

# Table Values Functions

使用 user defined function,schema 是很重要的
```sql!
USE AdventureWorks2017
GO
CREATE FUNCTION SellStartDateInYear
(
@SellStartDateYear INT
) RETURNS TABLE
AS
RETURN
SELECT
*
FROM Production.Product
WHERE YEAR(SellStartDate) = @SellStartDateYear
SELECT * FROM [dbo].[SellStartDateInYear](2011)
```
## Multiple Statement Table Value Functions
```sql!
USE AdventureWorks2017
GO
CREATE FUNCTION dbo.ProductNameFilter
(
@Name VARCHAR(255)
) RETURNS @t TABLE (ProductID VARCHAR(MAX), Name VARCHAR(255), ProductNumber VARCHAR(30))
AS
BEGIN
INSERT INTO @t
SELECT
ProductID, Name, ProductNumber
FROM Production.Product WHERE Name Like '%' + @Name + '%'
RETURN
END
SELECT * FROM dbo.ProductNameFilter('BE')
```
# CTE
create a temporary set of records which we can use right away for another select statement

# Dynamic SQL


Def: Dynamic sql is where you can take any valid SQL statement, and change it to a string of text, then run that string of text like it was the original SQL statement.
```sql!
-- method 1
EXEC ('SELECT * FROM Production.Product')
-- method 2
-- Must be UniCode String
EXEC sp_executesql N'SELECT * FROM Production.Product'
```
sp_executesql 接受 unicode string 將他變成有效的 SQL
法一:
1. string 未必要是 unicode
2. no query plan
法二:
1. more efficient: query plan is generated when you utilize system store procedure
2. parameters and extra arguments
Note:
query plan will be stored in a cache when you pass in whatever `SELECT` statements to sp
```sql!
DECLARE @TableName NVARCHAR(128)
DECLARE @SQLStrung NVARCHAR(MAX)
SET @TableName = N'Production.ProductCategory'
SET @SQLStrung = 'SELECT * FROM ' + @TableName
EXEC sp_executesql @SQLStrung
```

因為他嘗試將 SELECT TOP 轉成 int
```sql!
DECLARE @Number INT
DECLARE @SQLString NVARCHAR(MAX)
DECLARE @NumberString NVARCHAR(4)
SET @Number = 3
SET @NumberString = CAST(@Number AS NVARCHAR(4))
SET @SQLString = N'SELECT TOP ' + @NumberString + ' * FROM Production.Product'
EXEC sp_executesql @SQLString
```
```sql!
-- single parameter
CREATE PROC spTableVariable
(
@TName VARCHAR(128)
)
AS
BEGIN
DECLARE @SQLStrung NVARCHAR(MAX)
SET @SQLStrung = N'SELECT * FROM ' + @TName
EXEC sp_executesql @SQLStrung
END
EXEC spTableVariable 'Production.ProductCategory'
```
```sql!
-- multiple parameters
ALTER PROC spTableVariable
(
@TName VARCHAR(128),
@Number INT
)
AS
BEGIN
DECLARE @SQLStrung NVARCHAR(MAX)
DECLARE @NumberStr NVARCHAR(6)
SET @NumberStr = CAST(@Number AS NVARCHAR(6))
SET @SQLStrung = N'SELECT TOP ' + @NumberStr + ' * FROM ' + @TName
EXEC sp_executesql @SQLStrung
END
EXEC spTableVariable 'Production.Product', 20
```
## Parameters of sp_executesql
參數化 actual sql string
```sql!
EXEC sp_executesql
N'SELECT * FROM Person.AddressType t
INNER JOIN Person.Address a ON t.AddressTypeID = a.AddressID
WHERE t.AddressTypeID > @AdressTypeID
AND t.Name LIKE @FirstLetter + ''%'' ',
N'@AdressTypeID INT, @FirstLetter VARCHAR(3)',
@AdressTypeID = 2, @FirstLetter = 'A'
```
## SQL Injection
[The Curse and Blessings of Dynamic SQL
](https://www.sommarskog.se/dynamic_sql.html)
# Transaction


Naming Transaction 的好處是 easily for read
```sql!
BEGIN TRAN UpdateAddressLine2 -- BEGIN TRANSACTION
UPDATE Person.Address
SET ModifiedDate = GETDATE(), AddressLine2 = 'Karchanaburi'
WHERE AddressID = 20
--ROLLBACK TRAN UpdateAddressLine2
COMMIT TRAN UpdateAddressLine2 -- COMMIT, or COMMIT TRANSACTION
```
## EX - IF Statements to commit or rollback
```sql!
IF OBJECT_ID(N'dbo.books', N'U') IS NOT NULL
DROP TABLE dbo.books;
GO
CREATE TABLE dbo.books
(
id INT IDENTITY(1, 1),
book_name VARCHAR(256),
sold_copies INT NOT NULL
)
INSERT INTO dbo.books (book_name, sold_copies) VALUES
('book1', 10),
('book2', 20),
('book3', 30),
('book4', 40),
('book5', 50)
SELECT * FROM dbo.books;
```
SQL Server 中如果變量已經被聲明,可以在 `SELECT` 語句中直接為變量賦值。
優點:一次 DB 訪問,查詢與賦值同時做,不需要先執行 `SET` 語句來初始化變量
```sql!
-- Conditionally committing or rolling back
DECLARE @BookNameCount INT
BEGIN TRANSACTION AddBook
INSERT INTO dbo.books(book_name, sold_copies)
VALUES('book_6', 60)
SELECT @BookNameCount = COUNT(0) FROM dbo.books WHERE book_name = 'book_6'
IF (@BookNameCount > 1)
BEGIN
ROLLBACK TRANSACTION AddBook
PRINT 'book_6 already exists'
END
ELSE
BEGIN
COMMIT TRANSACTION AddBook
END
```
## Error Handling
通常會使用 Error Handling 而非 IF ELSE 來決定 COMMIT or ROLL BACK
在 BEGIN & COMMIT 中間出事,會自動做整組 Transaction 的 ROLL BACK。
```sql!
-- default rollback
BEGIN TRANSACTION AddBook
INSERT INTO dbo.books(book_name, sold_copies)
VALUES('book_8', 80)
UPDATE dbo.books
SET sold_copies = 'Apple'
WHERE book_name = 'book_8'
COMMIT TRANSACTION AddBook
```
## Try and Catch Block
```sql!
BEGIN TRY
BEGIN TRANSACTION AddBook
INSERT INTO dbo.books(book_name, sold_copies)
VALUES('book_8', 80)
UPDATE dbo.books
SET sold_copies = 'Apple'
WHERE book_name = 'book_8'
COMMIT TRANSACTION AddBook
END TRY
BEGIN CATCH
ROLLBACK TRAN ADDBook
PRINT 'ADDING NEW BOOK FAILED'
END CATCH
```
## Nested Transaction
在SQL Server中,`@@TRANCOUNT` 是一個系統函數,用於返回當前連接上已執行的BEGIN TRANSACTION 語句的數目。這個數字反映了嵌套事務的層次
1. 每次執行 BEGIN TRANSACTION 語句時,`@@TRANCOUNT`會增加 1
2. **每次執行 COMMIT TRANSACTION 或 COMMIT WORK 語句時,`@@TRANCOUNT` 會減少 1**
3. 執行 ROLLBACK TRANSACTION 語句會將 `@@TRANCOUNT` 減少到 0
```sql!
BEGIN TRANSACTION TRN1
PRINT @@TRANCOUNT
BEGIN TRANSACTION TRN2
PRINT @@TRANCOUNT
COMMIT TRANSACTION TRN2
PRINT @@TRANCOUNT
COMMIT TRANSACTION TRN1
```


不只是 rollback inner nested transaction,當執行 rollback, everything is rolled back.
所以他才會說沒辦法 COMMIT 因為已經沒有 TRANSACTION 是處於 OPEN 的。
看起來像是 saved the transaction in the inner transaction. 但這邊的 ROLL BACK 不只 ROLL BACK OUTER TRANSACTION。他會將所有包在 OUTER TRANSACTION 裡面的東西通通 ROLL BACK

## SAFE POINTS

# Trigger
Trigger 可以將它視為另一種類型的 sp, you can attach to different events that occur in DB

# DML Trigger
Data Manipulation Language Trigger

WHERE AddressID = 20
INSERT INTO Person.Address(AddressLine1, City, StateProvinceID, PostalCode)
VALUES('岡山', '高雄', 79, 820)
DELETE FROM Person.Address
WHERE AddressLine1 = '岡山'
```
> instead of trigger replaces the original event that could have occurred
```sql
USE AdventureWorks2017
GO
CREATE TRIGGER trgAddressInserted
ON Person.Address
INSTEAD OF INSERT
AS
BEGIN
RAISERROR('Cannot insert a new address', 16, 1)
END
```
## Inserted & Deleted Tables
1. Inserted Tables: for insert and update events. inserted table shows us any records that have been inserted into a table in an insert event.
2. Deleted Tables: for delete and update events
```sql!
Alter TRIGGER Person.trgAddressInserted
ON Person.Address
AFTER INSERT
AS
BEGIN
SELECT * FROM inserted
END
```
# DDL Trigger
Data Definition Language Trigger

資料庫級別的觸發:anything that modifies the object in the DB

確認作用範圍:single DB or to the entire server
Database scope trigger

Server scope trigger

## EX : Create New DDL trigger is scope to a database
Create Trigger
```sql!
USE AdventureWorks2017
GO
CREATE TRIGGER trgNewTablesNotAllowed
-- our trigger will responsd to events in the db that the trigger is being created
ON DATABASE
FOR CREATE_TABLE -- Google for DDL events
AS
BEGIN
PRINT 'Please do not create new tables'
ROLLBACK
END
```
> A database scoped trigger only affects events in the database that our trigger is attached to.
Alter Trigger
```sql!
Alter TRIGGER trgNewTablesNotAllowed
-- our trigger will responsd to events in the db that the trigger is being created
ON DATABASE
FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE
AS
BEGIN
PRINT 'Please do not create new tables, or make chages to existing tables'
ROLLBACK
END
```
Delete Trigger
```sql!
USE AdventureWorks2017
GO
DROP TRIGGER trgNewTablesNotAllowed ON DATABASE
```
## Trigger Disabling and Enabling
```sql!
USE AdventureWorks2017
GO
DISABLE TRIGGER trgNewTablesNotAllowed ON DATABASE
ENABLE TRIGGER trgNewTablesNotAllowed ON DATABASE
-- 同時操作多個 triggers
DISABLE TRIGGER ALL ON DATABASE
ENABLE TRIGGER ALL ON DATABASE
```
## Server-scoped trigger creation
將 DATABASE 改成 ALL SERVER
```sql!
USE AdventureWorks2017
GO
CREATE TRIGGER trgNewTablesNotAllowed
ON ALL SERVER
FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE
AS
BEGIN
PRINT 'Please do not create new tables, or make chages to existing tables'
ROLLBACK
END
-- Enable Trigger
ENABLE TRIGGER ALL ON DATABASE
-- Drop Trigger
DROP TRIGGER trgNewTablesNotAllowed ON ALL SERVER
```
## Modify the execution order of triggers
Server Scopes Triggers 會比 Database triggers 優先觸發執行
如果 create 多個 triggers 在同個層級,就可以決定他們的優先順序
```sql!
USE AdventureWorks2017
GO
CREATE TRIGGER trgTrigger1 ON DATABASE
FOR CREATE_TABLE AS
PRINT 'Trigger 1'
GO
CREATE TRIGGER trgTrigger2 ON DATABASE
FOR CREATE_TABLE AS
PRINT 'Trigger 2'
GO
```
