This commit is contained in:
Jovan Popovic
2018-10-04 09:52:27 +02:00
50 changed files with 71599 additions and 13 deletions
+4
View File
@@ -23,3 +23,7 @@ Samples that help with the management of SQL Server and Azure SQL Database.
__[tutorials](tutorials/)__
Samples showing how to connect to SQL databases using various programming languages, including Python, C#, Java, Ruby, Node.js, and PHP.
__[containers](containers/)__
Samples showing various SQL Server in container scenarios.
+3
View File
@@ -0,0 +1,3 @@
__[replication](replication/)__
Example of SQL Server Replication in containers. This demo uses docker-compose to start two SQL Server containers; one that acts as the publisher and distributor, and the other as the subscriber in a push snapshot configuration.
+38
View File
@@ -0,0 +1,38 @@
## SQL Server Replication with Containers
This demo uses docker-compose to start two SQL Server containers; one that acts as the publisher and distributor, and the other as the subscriber in a push snapshot configuration.
### How to Use
1. Run the following command in this directory:
```
docker-compose up
```
note: this will take approx. 2 min.
In your terminal, you should see something like this
```
db1 | Job 'DB1-Sales-SnapshotRepl-1' started successfully.
db1 | Creating Snapshot...
db1 | Job 'db1-Sales-SnapshotRepl-DB2-1' started successfully.
```
2. Connect to the subscriber listening on localhost,2600 and see that the Sales Database has a Customer table with data in it.
note: credentials are listed in the **docker-compose.yml**
3. when you are done, clean up by running the following command
```
docker-compose down
```
### How it Works
1. Both SQL Server containers start with the environment variables specified in the docker-compose file. In this example, **db1** is the publisher/distributor and **db2** is the subscriber.
2. *db1/db-init.sh* and *db2/db-init.sh* waits for SQL Server to start up and run the *db-init.sql* scripts
3. *db1/db-init.sql* creates a *Sales* Database with *Customer* table and sample data, and proceeds by setting up snapshot replication.
4. *db2/db-init.sql* creates a *Sales* Database.
5. db1 starts replication jobs to push the snapshot to db2
@@ -0,0 +1,6 @@
FROM mcr.microsoft.com/mssql/server:vNext-CTP2.0-ubuntu
COPY . /
RUN chmod +x /db-init.sh
CMD /bin/bash ./entrypoint.sh
@@ -0,0 +1,11 @@
#wait for the SQL Server to come up
sleep 25s
mkdir /var/opt/mssql/ReplData/
chown mssql /var/opt/mssql/ReplData/
chgrp mssql /var/opt/mssql/ReplData/
echo "running set up script"
#run the setup script to create the DB and the schema in the DB
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P MssqlPass123 -d master -i db-init.sql
@@ -0,0 +1,167 @@
CREATE DATABASE Sales
GO
USE [SALES]
GO
CREATE TABLE CUSTOMER([CustomerID] [int] NOT NULL, [SalesAmount] [decimal] NOT NULL)
GO
INSERT INTO CUSTOMER (CustomerID, SalesAmount) VALUES (1,100),(2,200),(3,300)
DECLARE @distributor AS sysname
DECLARE @distributorlogin AS sysname
DECLARE @distributorpassword AS sysname
-- Specify the Distributor name. Use 'hostname' command on in terminal to find the hostname
SET @distributor = @@SERVERNAME--in this example, it will be the name of the publisher
SET @distributorlogin = N'sa'
SET @distributorpassword = N'MssqlPass123'
-- Specify the distribution database.
use master
exec sp_adddistributor @distributor = @distributor -- this should be the hostname
-- Log into Distributor and create Distribution Database. In this example, our Publisher and Distributor is on the same host
exec sp_adddistributiondb @database = N'distribution', @log_file_size = 2, @deletebatchsize_xact = 5000, @deletebatchsize_cmd = 2000, @security_mode = 0, @login = @distributorlogin, @password = @distributorpassword
GO
DECLARE @snapshotdirectory AS nvarchar(500)
SET @snapshotdirectory = N'/var/opt/mssql/ReplData/'
-- Log into Distributor and create Distribution Database. In this example, our Publisher and Distributor is on the same host
use [distribution]
if (not exists (select * from sysobjects where name = 'UIProperties' and type = 'U '))
create table UIProperties(id int)
if (exists (select * from ::fn_listextendedproperty('SnapshotFolder', 'user', 'dbo', 'table', 'UIProperties', null, null)))
EXEC sp_updateextendedproperty N'SnapshotFolder', @snapshotdirectory, 'user', dbo, 'table', 'UIProperties'
else
EXEC sp_addextendedproperty N'SnapshotFolder', @snapshotdirectory, 'user', dbo, 'table', 'UIProperties'
GO
DECLARE @publisher AS sysname
DECLARE @distributorlogin AS sysname
DECLARE @distributorpassword AS sysname
-- Specify the Distributor name. Use 'hostname' command on in terminal to find the hostname
SET @publisher = @@SERVERNAME
SET @distributorlogin = N'sa'
SET @distributorpassword = N'MssqlPass123'
-- Specify the distribution database.
-- Adding the distribution publishers
exec sp_adddistpublisher @publisher = @publisher, @distribution_db = N'distribution', @security_mode = 0, @login = @distributorlogin, @password = @distributorpassword, @working_directory = N'/var/opt/mssql/ReplData', @trusted = N'false', @thirdparty_flag = 0, @publisher_type = N'MSSQLSERVER'
GO
DECLARE @replicationdb AS sysname
DECLARE @publisherlogin AS sysname
DECLARE @publisherpassword AS sysname
SET @replicationdb = N'Sales'
SET @publisherlogin = N'sa'
SET @publisherpassword = N'MssqlPass123'
use [Sales]
exec sp_replicationdboption @dbname = N'Sales', @optname = N'publish', @value = N'true'
-- Addi the snapshot publication
exec sp_addpublication
@publication = N'SnapshotRepl',
@description = N'Snapshot publication of database ''Sales'' from Publisher ''<PUBLISHER HOSTNAME>''.',
@retention = 0,
@allow_push = N'true',
@repl_freq = N'snapshot',
@status = N'active',
@independent_agent = N'true'
exec sp_addpublication_snapshot @publication = N'SnapshotRepl',
@frequency_type = 128,
@frequency_interval = 8,
@frequency_relative_interval = 1,
@frequency_recurrence_factor = 0,
@frequency_subday = 4,
@frequency_subday_interval = 2,
@active_start_time_of_day = 0,
@active_end_time_of_day = 235959,
@active_start_date = 0,
@active_end_date = 0,
@publisher_security_mode = 0,
@publisher_login = @publisherlogin,
@publisher_password = @publisherpassword
use [Sales]
exec sp_addarticle
@publication = N'SnapshotRepl',
@article = N'customer',
@source_owner = N'dbo',
@source_object = N'customer',
@type = N'logbased',
@description = null,
@creation_script = null,
@pre_creation_cmd = N'drop',
@schema_option = 0x000000000803509D,
@identityrangemanagementoption = N'manual',
@destination_table = N'customer',
@destination_owner = N'dbo',
@vertical_partition = N'false'
DECLARE @subscriber AS sysname
DECLARE @subscriber_db AS sysname
DECLARE @subscriberLogin AS sysname
DECLARE @subscriberPassword AS sysname
SET @subscriber = N'db2' -- for example, MSSQLSERVER
SET @subscriber_db = N'Sales'
SET @subscriberLogin = N'sa'
SET @subscriberPassword = N'MssqlPass123'
use [Sales]
exec sp_addsubscription
@publication = N'SnapshotRepl',
@subscriber = @subscriber,
@destination_db = @subscriber_db,
@subscription_type = N'Push',
@sync_type = N'automatic',
@article = N'all',
@update_mode = N'read only',
@subscriber_type = 0
exec sp_addpushsubscription_agent
@publication = N'SnapshotRepl',
@subscriber = @subscriber,
@subscriber_db = @subscriber_db,
@subscriber_security_mode = 0,
@subscriber_login = @subscriberLogin,
@subscriber_password = @subscriberPassword,
@frequency_type = 128,
@frequency_interval = 8,
@frequency_relative_interval = 0,
@frequency_recurrence_factor = 0,
@frequency_subday = 4,
@frequency_subday_interval = 2,
@active_start_time_of_day = 0,
@active_end_time_of_day = 0,
@active_start_date = 0,
@active_end_date = 19950101
GO
exec sp_startpublication_snapshot
@publication = N'SnapshotRepl',
@publisher = NULL
GO
PRINT 'Creating Snapshot...'
WAITFOR DELAY '00:00:17'
DECLARE @jobname NVARCHAR(max)
--use the following query to query for the jobname of replication job
select @jobname=s.name
from msdb.dbo.sysjobs s inner join msdb.dbo.syscategories c on s.category_id = c.category_id
where c.name in ('REPL-Distribution')
--select @jobname
exec msdb.dbo.sp_start_job @jobname
-- SELECT name, date_modified FROM msdb.dbo.sysjobs order by date_modified desc
@@ -0,0 +1,5 @@
#start SQL Server, start the script to create/setup the DB
#You need a non-terminating process to keep the container alive.
#In a series of commands separated by single ampersands the commands to the left of the right-most ampersand are run in the background.
#So - if you are executing a series of commands simultaneously using single ampersands, the command at the right-most position needs to be non-terminating
/db-init.sh & /opt/mssql/bin/sqlservr
@@ -0,0 +1,6 @@
FROM mcr.microsoft.com/mssql/server:vNext-CTP2.0-ubuntu
COPY . /
RUN chmod +x /db-init.sh
CMD /bin/bash ./entrypoint.sh
@@ -0,0 +1,6 @@
#wait for the SQL Server to come up
sleep 20s
echo "running set up script"
#run the setup script to create the DB and the schema in the DB
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P MssqlPass123 -d master -i db-init.sql
@@ -0,0 +1,2 @@
CREATE DATABASE Sales
GO
@@ -0,0 +1,5 @@
#start SQL Server, start the script to create/setup the DB
#You need a non-terminating process to keep the container alive.
#In a series of commands separated by single ampersands the commands to the left of the right-most ampersand are run in the background.
#So - if you are executing a series of commands simultaneously using single ampersands, the command at the right-most position needs to be non-terminating
/db-init.sh & /opt/mssql/bin/sqlservr
@@ -0,0 +1,23 @@
version: "3"
services:
db1:
build: ./db1
environment:
SA_PASSWORD: "MssqlPass123"
ACCEPT_EULA: "Y"
MSSQL_AGENT_ENABLED: "true"
ports:
- "2500:1433"
container_name: db1
hostname: db1
db2:
build: ./db2
environment:
SA_PASSWORD: "MssqlPass123"
ACCEPT_EULA: "Y"
MSSQL_AGENT_ENABLED: "true"
ports:
- "2600:1433"
container_name: db2
hostname: db2
@@ -818,6 +818,7 @@ CREATE TABLE [cso].[FactOnlineSales] WITH (DISTRIBUTION = HASH([ProductKey
CREATE TABLE [cso].[FactSales] WITH (DISTRIBUTION = HASH([ProductKey] ) ) AS SELECT * FROM [asb].[FactSales] OPTION (LABEL = 'CTAS : Load [cso].[FactSales] ');
CREATE TABLE [cso].[FactSalesQuota] WITH (DISTRIBUTION = HASH([ProductKey] ) ) AS SELECT * FROM [asb].[FactSalesQuota] OPTION (LABEL = 'CTAS : Load [cso].[FactSalesQuota] ');
CREATE TABLE [cso].[FactStrategyPlan] WITH (DISTRIBUTION = HASH([EntityKey]) ) AS SELECT * FROM [asb].[FactStrategyPlan] OPTION (LABEL = 'CTAS : Load [cso].[FactStrategyPlan] ');
CREATE TABLE [cso].[FactExchangeRate] WITH (DISTRIBUTION = HASH([ExchangeRateKey]) ) AS SELECT * FROM [asb].[ExchangeRateKey] OPTION (LABEL = 'CTAS : Load [cso].[FactExchangeRate] ');
-- Track the load progress
@@ -864,6 +865,7 @@ ALTER INDEX ALL ON [cso].[FactInventory] REBUILD;
ALTER INDEX ALL ON [cso].[FactOnlineSales] REBUILD;
ALTER INDEX ALL ON [cso].[FactSales] REBUILD;
ALTER INDEX ALL ON [cso].[FactSalesQuota] REBUILD;
ALTER INDEX ALL ON [cso].[FactExchangeRate] REBUILD;
-- Optimize statistics
@@ -0,0 +1,33 @@
CREATE TABLE [Application].[Logs](
[Message] NVARCHAR(4000) NOT NULL,
[Level] VARCHAR(16) NOT NULL,
[EventTime] DATETIME2 (7) NOT NULL,
[LogEvent] NVARCHAR(max) NULL,
INDEX CCX_Application_Logs CLUSTERED COLUMNSTORE
)
GO
GO
EXECUTE sp_addextendedproperty @name = N'Description', @value = 'CLUSTERED COLUMNSTORE INDEX that compress application log.', @level0type = N'SCHEMA', @level0name = N'Application', @level1type = N'TABLE', @level1name = N'Logs', @level2type = N'INDEX', @level2name = N'CCX_Application_Logs';
GO
EXECUTE sp_addextendedproperty @name = N'Description', @value = N'Application logs that are stored in database', @level0type = N'SCHEMA', @level0name = N'Application', @level1type = N'TABLE', @level1name = N'Logs';
GO
EXECUTE sp_addextendedproperty @name = N'Description', @value = 'Logged message', @level0type = N'SCHEMA', @level0name = N'Application', @level1type = N'TABLE', @level1name = N'Logs', @level2type = N'COLUMN', @level2name = N'Message';
GO
EXECUTE sp_addextendedproperty @name = N'Description', @value = 'Severity of the log entry', @level0type = N'SCHEMA', @level0name = N'Application', @level1type = N'TABLE', @level1name = N'Logs', @level2type = N'COLUMN', @level2name = N'Level';
GO
EXECUTE sp_addextendedproperty @name = N'Description', @value = 'Time when the record is logged', @level0type = N'SCHEMA', @level0name = N'Application', @level1type = N'TABLE', @level1name = N'Logs', @level2type = N'COLUMN', @level2name = 'EventTime';
GO
EXECUTE sp_addextendedproperty @name = N'Description', @value = 'Details about the logged event', @level0type = N'SCHEMA', @level0name = N'Application', @level1type = N'TABLE', @level1name = N'Logs', @level2type = N'COLUMN', @level2name = N'LogEvent';
@@ -309,6 +309,7 @@
<AnsiNulls>On</AnsiNulls>
<QuotedIdentifier>On</QuotedIdentifier>
</Build>
<Build Include="Application\Tables\Logs.sql" />
<Build Include="Sales\Tables\SpecialDeals.sql">
<AnsiNulls>On</AnsiNulls>
<QuotedIdentifier>On</QuotedIdentifier>
@@ -0,0 +1,8 @@
-- CTAS statement to create Trip table with hashed distribution on DateID column
CREATE TABLE dbo.TripHashed
WITH
(
DISTRIBUTION = Hash(DateID),
CLUSTERED COLUMNSTORE INDEX
)
AS SELECT * FROM dbo.Trip;
@@ -0,0 +1,79 @@
-- CTAS (Create Table as Select) Creates tables in the SQL DW from external tables
CREATE TABLE [dbo].[Date]
WITH
( DISTRIBUTION = ROUND_ROBIN
, CLUSTERED COLUMNSTORE INDEX
)
AS
SELECT *
FROM [ext].[Date]
OPTION (LABEL = 'CTAS : Load [dbo].[Date]')
;
CREATE TABLE [dbo].[Geography]
WITH
( DISTRIBUTION = ROUND_ROBIN
, CLUSTERED COLUMNSTORE INDEX
)
AS
SELECT *
FROM [ext].[Geography]
OPTION (LABEL = 'CTAS : Load [dbo].[Geography]')
;
CREATE TABLE [dbo].[HackneyLicense]
WITH
( DISTRIBUTION = ROUND_ROBIN
, CLUSTERED COLUMNSTORE INDEX
)
AS
SELECT *
FROM [ext].[HackneyLicense]
OPTION (LABEL = 'CTAS : Load [dbo].[HackneyLicense]')
;
CREATE TABLE [dbo].[Medallion]
WITH
( DISTRIBUTION = ROUND_ROBIN
, CLUSTERED COLUMNSTORE INDEX
)
AS
SELECT *
FROM [ext].[Medallion]
OPTION (LABEL = 'CTAS : Load [dbo].[Medallion]')
;
CREATE TABLE [dbo].[Time]
WITH
( DISTRIBUTION = ROUND_ROBIN
, CLUSTERED COLUMNSTORE INDEX
)
AS
SELECT *
FROM [ext].[Time]
OPTION (LABEL = 'CTAS : Load [dbo].[Time]')
;
CREATE TABLE [dbo].[Weather]
WITH
( DISTRIBUTION = ROUND_ROBIN
, CLUSTERED COLUMNSTORE INDEX
)
AS
SELECT *
FROM [ext].[Weather]
OPTION (LABEL = 'CTAS : Load [dbo].[Weather]')
;
CREATE TABLE [dbo].[Trip]
WITH
( DISTRIBUTION = ROUND_ROBIN
, CLUSTERED COLUMNSTORE INDEX
)
AS
SELECT *
FROM [ext].[Trip]
OPTION (LABEL = 'CTAS : Load [dbo].[Trip]')
;
@@ -0,0 +1 @@
This folder contains the PDF and SQL files in order to complete the SQL Data Warehouse Free Trial lab.
@@ -0,0 +1,2 @@
-- Adds user XLRCUser to the xlargerc resource class role
EXEC sp_addrolemember 'xlargerc', 'XLRCUser'
@@ -0,0 +1,9 @@
-- Define an external data source
-- For accessing non-public external data sources, make sure to setup credentials
-- Read more here: https://azure.microsoft.com/en-us/documentation/articles/sql-data-warehouse-get-started-load-with-polybase/#step-2-create-an-external-table-for-the-sample-data
CREATE EXTERNAL DATA SOURCE NYTPublic
WITH
(
TYPE = Hadoop
, LOCATION = 'wasbs://2013@nytpublic.blob.core.windows.net/'
);
@@ -0,0 +1,22 @@
-- Defines external file format for the NYT data in Azure Blob Storage
CREATE EXTERNAL FILE FORMAT uncompressedcsv
WITH
( FORMAT_TYPE = DELIMITEDTEXT
, FORMAT_OPTIONS ( FIELD_TERMINATOR = ','
, STRING_DELIMITER = ''
, DATE_FORMAT = ''
, USE_TYPE_DEFAULT = False
)
);
CREATE EXTERNAL FILE FORMAT compressedcsv
WITH
( FORMAT_TYPE = DELIMITEDTEXT
, FORMAT_OPTIONS ( FIELD_TERMINATOR = '|'
, STRING_DELIMITER = ''
, DATE_FORMAT = ''
, USE_TYPE_DEFAULT = False
)
, DATA_COMPRESSION = 'org.apache.hadoop.io.compress.GzipCodec'
);
@@ -0,0 +1,3 @@
-- Creates a schema for the external data
CREATE SCHEMA ext;
GO
@@ -0,0 +1,170 @@
-- Creates external tables
CREATE EXTERNAL TABLE [ext].[Date]
(
[DateID] int NOT NULL,
[Date] datetime NULL,
[DateBKey] char(10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DayOfMonth] varchar(2) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DaySuffix] varchar(4) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DayName] varchar(9) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DayOfWeek] char(1) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DayOfWeekInMonth] varchar(2) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DayOfWeekInYear] varchar(2) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DayOfQuarter] varchar(3) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DayOfYear] varchar(3) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[WeekOfMonth] varchar(1) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[WeekOfQuarter] varchar(2) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[WeekOfYear] varchar(2) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Month] varchar(2) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[MonthName] varchar(9) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[MonthOfQuarter] varchar(2) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Quarter] char(1) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[QuarterName] varchar(9) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Year] char(4) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[YearName] char(7) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[MonthYear] char(10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[MMYYYY] char(6) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[FirstDayOfMonth] date NULL,
[LastDayOfMonth] date NULL,
[FirstDayOfQuarter] date NULL,
[LastDayOfQuarter] date NULL,
[FirstDayOfYear] date NULL,
[LastDayOfYear] date NULL,
[IsHolidayUSA] bit NULL,
[IsWeekday] bit NULL,
[HolidayUSA] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
)
WITH
(
LOCATION = 'Date'
, DATA_SOURCE = NYTPublic
, FILE_FORMAT = uncompressedcsv
, REJECT_TYPE = value
, REJECT_VALUE = 0
)
CREATE EXTERNAL TABLE [ext].[Geography]
(
[GeographyID] int NOT NULL,
[ZipCodeBKey] varchar(10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[County] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[City] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[State] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Country] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[ZipCode] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
)
WITH
(
LOCATION = 'Geography'
, DATA_SOURCE = NYTPublic
, FILE_FORMAT = uncompressedcsv
, REJECT_TYPE = value
, REJECT_VALUE = 0
)
;
CREATE EXTERNAL TABLE [ext].[HackneyLicense]
(
[HackneyLicenseID] int NOT NULL,
[HackneyLicenseBKey] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[HackneyLicenseCode] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
)
WITH
(
LOCATION = 'HackneyLicense'
, DATA_SOURCE = NYTPublic
, FILE_FORMAT = uncompressedcsv
, REJECT_TYPE = value
, REJECT_VALUE = 0
)
;
CREATE EXTERNAL TABLE [ext].[Medallion]
(
[MedallionID] int NOT NULL,
[MedallionBKey] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[MedallionCode] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
)
WITH
(
LOCATION = 'Medallion'
, DATA_SOURCE = NYTPublic
, FILE_FORMAT = uncompressedcsv
, REJECT_TYPE = value
, REJECT_VALUE = 0
)
;
CREATE EXTERNAL TABLE [ext].[Time]
(
[TimeID] int NOT NULL,
[TimeBKey] varchar(8) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[HourNumber] tinyint NOT NULL,
[MinuteNumber] tinyint NOT NULL,
[SecondNumber] tinyint NOT NULL,
[TimeInSecond] int NOT NULL,
[HourlyBucket] varchar(15) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[DayTimeBucketGroupKey] int NOT NULL,
[DayTimeBucket] varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL
)
WITH
(
LOCATION = 'Time'
, DATA_SOURCE = NYTPublic
, FILE_FORMAT = uncompressedcsv
, REJECT_TYPE = value
, REJECT_VALUE = 0
)
;
CREATE EXTERNAL TABLE [ext].[Trip]
(
[DateID] int NOT NULL,
[MedallionID] int NOT NULL,
[HackneyLicenseID] int NOT NULL,
[PickupTimeID] int NOT NULL,
[DropoffTimeID] int NOT NULL,
[PickupGeographyID] int NULL,
[DropoffGeographyID] int NULL,
[PickupLatitude] float NULL,
[PickupLongitude] float NULL,
[PickupLatLong] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DropoffLatitude] float NULL,
[DropoffLongitude] float NULL,
[DropoffLatLong] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[PassengerCount] int NULL,
[TripDurationSeconds] int NULL,
[TripDistanceMiles] float NULL,
[PaymentType] varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[FareAmount] money NULL,
[SurchargeAmount] money NULL,
[TaxAmount] money NULL,
[TipAmount] money NULL,
[TollsAmount] money NULL,
[TotalAmount] money NULL
)
WITH
(
LOCATION = 'Trip2013'
, DATA_SOURCE = NYTPublic
, FILE_FORMAT = compressedcsv
, REJECT_TYPE = value
, REJECT_VALUE = 0
)
;
CREATE EXTERNAL TABLE [ext].[Weather]
(
[DateID] int NOT NULL,
[GeographyID] int NOT NULL,
[PrecipitationInches] float NOT NULL,
[AvgTemperatureFahrenheit] float NOT NULL
)
WITH
(
LOCATION = 'Weather2013'
, DATA_SOURCE = NYTPublic
, FILE_FORMAT = uncompressedcsv
, REJECT_TYPE = value
, REJECT_VALUE = 0
)
;
@@ -0,0 +1,3 @@
-- Connect to master database and create a login
CREATE LOGIN XLRCLogin WITH PASSWORD = ' a123reallySTRONGpassword!';
CREATE USER XLRCUser FOR LOGIN XLRCLogin;
@@ -0,0 +1,3 @@
-- Creates statistics on the Date and Trip DateID columns to check join performance improvements
CREATE STATISTICS [dbo.Date DateID stats] ON dbo.Date (DateID);
CREATE STATISTICS [dbo.Trip DateID stats] ON dbo.Trip (DateID);
@@ -0,0 +1,2 @@
-- Connect to SQL DW database and create a database user
CREATE USER XLRCUser FOR LOGIN XLRCLogin;
@@ -0,0 +1,2 @@
-- Grants control of DW db to new user
GRANT CONTROL ON DATABASE::NYT to XLRCUser;
@@ -0,0 +1,29 @@
-- Join Date table and Trip table
SELECT TOP (1000000) dt.[DayOfWeek]
,tr.[MedallionID]
,tr.[HackneyLicenseID]
,tr.[PickupTimeID]
,tr.[DropoffTimeID]
,tr.[PickupGeographyID]
,tr.[DropoffGeographyID]
,tr.[PickupLatitude]
,tr.[PickupLongitude]
,tr.[PickupLatLong]
,tr.[DropoffLatitude]
,tr.[DropoffLongitude]
,tr.[DropoffLatLong]
,tr.[PassengerCount]
,tr.[TripDurationSeconds]
,tr.[TripDistanceMiles]
,tr.[PaymentType]
,tr.[FareAmount]
,tr.[SurchargeAmount]
,tr.[TaxAmount]
,tr.[TipAmount]
,tr.[TollsAmount]
,tr.[TotalAmount]
FROM [dbo].[Trip] as tr
join
dbo.[Date] as dt
on tr.DateID = dt.DateID
@@ -0,0 +1 @@
SELECT TOP(1000000) * FROM dbo.[Trip]
@@ -0,0 +1,2 @@
-- Shows rows and space used per distribution
DBCC PDW_SHOWSPACEUSED ("dbo.Trip")
+2 -2
View File
@@ -1,4 +1,4 @@
#EPM Framework 4 Release Notes
# EPM Framework 4 Release Notes
Over 4.12.1 below, EPM Framework 4.12.2 includes the following updates (as per received feedback): Fixed collation issues on views and reports
@@ -58,4 +58,4 @@ These scripts have some fixes for the Microsoft provided policies, and include e
- Are DB Compatibility levels same as engine version?
- Is Tempdb number of files appropriate? Regarding number of schedulers and if is multiple of 4?
- Do TempDB data file sizes match?
- Is MaxDOP setting at the recommended value?
- Is MaxDOP setting at the recommended value?
@@ -1,4 +1,8 @@
apiVersion: v1
kind: Namespace
metadata: {name: ag1}
---
apiVersion: v1
kind: ServiceAccount
metadata: {name: mssql-operator, namespace: ag1}
---
@@ -1,5 +1,5 @@
apiVersion: v1
data: {sapassword: JyFMb2NrczEyMyc=}
data: {sapassword: "<>"}
kind: Secret
metadata: {name: sql-secrets, namespace: ag1}
type: Opaque
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
**List of data sets**
| Data Set Name | Link to the Full Data Set | Full Data Set Size (MB) | Link to Report |
| ---:| ---: | ---: | ---: |
| CDNOW_master.csv | [link](https://github.com/ZhouFang928/sql-server-samples/blob/master/samples/features/r-services/Retail%20Precision%20Marketing/Data/CDNOW_master.csv) | 1.55MB | N/A|
**Description of data sets**
* The CDNOW data contains the entire purchase history up to the end of June 1998 of the cohort of 23,570 individuals who made their first-ever purchase at CDNOW in the first quarter of 1997. This CDNOW dataset was first used by Fader and Hardie (2001). Each record in this file, 69,659 in total, comprises four fields: the customer's ID, the date of the transaction, the number of CDs purchased, and the dollar value of the transaction.
Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

@@ -0,0 +1,237 @@
#############################################################
# Title : CRM Demo in-memory
# Author: Microsoft
# Date: Dec, 2015
#############################################################
# Install package
install.packages("rmarkdown")
install.packages("fpc")
# Set directory
wd <- getwd()
data.path <- file.path(wd, "Data", "CDNOW_master.csv")
# Connect to SQL database using ODBC and read data from SQL via Open Source R
library(RODBC)
getSqlTypeInfo()
# Connect from local PC
channel <- odbcDriverConnect("driver={SQL Server Native Client 11.0};
server=tcp:sqlserver2012-81yms1ai.cloudapp.net,57500;
database=RREDemoSql;
uid=******;
pwd=******;")
df <- sqlFetch(channel, 'CDNOW')
df <- sqlQuery(channel, paste("select * from dbo.CDNOW"))
df$Date<-as.Date(df$Date)
str(df)
head(df)
# Remove the rows with the duplicated IDs to see how many customers in total
uid <- df[!duplicated(df[,"ID"]), ]
dim(uid)
# Step 1: RFM analysis
# Call RFM source code
source(wd, "R", "RFM_Analysis_R_Source_Codes_V1.3.R")
# Set the startDate and endDate, we will only analysis the records in this date range
startDate <- as.Date("19970101","%Y%m%d")
endDate <- as.Date("19980701","%Y%m%d")
# Calculate RFM value
df <- getDataFrame(df, startDate, endDate, tIDColName="ID", tDateColName="Date", tAmountColName="Amount")
head(df)
# Obtain independent RFM score
df1 <-getIndependentScore(df)
head(df1)
# Draw the histograms in the R, F, and M dimensions
drawHistograms(df1)
S500 <- df1[df1$Total_Score > 500, ]
dim(S500)
S400 <- df1[df1$Total_Score > 400, ]
dim(S400)
# Obtain RFM score with breaks
# Take a look at the distribution of R, F, M
par(mfrow = c(1,3))
hist(df$Recency)
hist(df$Frequency)
hist(df$Monetary)
# Set the Recency ranges as 0-120 days, 120-240 days, 240-450 days, 450-500 days, and more than 500 days.
r <- c(120, 240, 450, 500)
# Set the Frequency ranges as 0-2 times, 2-5 times, 5-8 times, 8-10 times, and more than 10 times.
f <- c(2, 5, 8, 10)
# Set the Monetary ranges as 0-10 dollars, 10-20 dollars, and so on.
m <-c(10,20,30,100)
# Calculate RFM score with breaks
df2 <- getScoreWithBreaks(df, r, f, m)
drawHistograms(df2)
S500 <- df2[df2$Total_Score > 500, ]
dim(S500)
S400 <- df2[df2$Total_Score > 400, ]
dim(S400)
target <- df2[df2$Total_Score >= 441,]
dim(target)
# Obtain RFM scores with quantiles as breaks
r <-c(cutpoint(df$Recency))
f <-c(cutpoint(df$Frequency))
m <-c(cutpoint(df$Monetary))
df3 <- getScoreWithBreaks(df, r, f, m)
str(df3)
head(df3)
tail(df3)
RFM_Result <- subset(df3,
select=c("ID", "Recency", "Frequency", "Monetary",
"R_Score", "F_Score", "M_Score", "Total_Score"))
colnames(RFM_Result) <- c("ID", "R", "F", "M", "R_Score", "F_Score", "M_Score", "Total_Score")
head(RFM_Result)
time <- system.time({
sqlSave(channel,
RFM_Result,
rownames=FALSE,
append=FALSE,
varTypes=list(numeric="float",
integer="int"))
})
sqlUpdate(channel, df)
odbcClose(channel)
# Clustering using RFM
library(fpc)
library(cluster)
# Kmeans clustering with number of cluster equal to 8
cl.fit1 <- kmeans(RFM_Result[, 2:8],
centers=8,
iter.max=10,
nstart=1)
cl.fit2 <- kmeans(RFM_Result[, 2:8],
centers=8,
iter.max=20,
nstart=200)
summary(cl.fit1)
cluster<-cl.fit1$cluster
centers<-cl.fit1$centers
size<-cl.fit1$size
plot(RFM_Result[, 2:4], col=cl.fit1$cluster)
title(main="K-means",line=3)
# Classification using RFM
# Create IsVIP variable
IsVIP <- ifelse(RFM_Result[,'Total_Score'] >= 441, 1, 0)
Cluster <- cl.fit1$cluster
RFMVIPCluster <- cbind(RFM_Result, IsVIP, Cluster)
# Create training/testing data set
RD <- sample(1:10, dim(RFMVIPCluster)[1], replace=TRUE)
str(RD)
table(RD)
RFMVIPCluster$RD <- RD;
urv <- factor(ifelse(RD <= 8,'TRAIN','TEST'))
TrainTest <- cbind(RFMVIPCluster, urv)
Train <- TrainTest[which(TrainTest$urv == "TRAIN"), ]
Test <- TrainTest[which(TrainTest$urv == "TEST"), ]
# Logistic model
# Build our Logistic Regression Model with IsVIP as response
r1 <- glm(IsVIP~R+F+M, data=Train, family = binomial)
summary(r1)
p1 <- predict.glm(r1, data=Test, type="response")
head(p1)
tail(p1)
# Decision tree
# Grow tree
fit <- rpart(Cluster~R+F+M,
method="class",
data=Train)
# Display the results
printcp(fit)
# Visualize cross-validation results
plotcp(fit)
# Detailed summary of splits
summary(fit)
# Plot tree
library(rpart)
plot(fit, uniform=TRUE, main="Classification Tree for CDNOW")
text(fit, use.n=TRUE, all=TRUE, cex=.8)
# Prune the tree
pfit <- prune(fit, cp=fit$cptable[which.min(fit$cptable[,"xerror"]), "CP"])
# Plot the pruned tree
plot(pfit, uniform=TRUE,
main="Pruned Classification Tree for CDNOW")
text(pfit, use.n=TRUE, all=TRUE, cex=.8)
@@ -0,0 +1,208 @@
################################################################
# Title: CRM Demo in-SQL
# Author: Microsoft
# Date: Dec, 2015
#################################################################
# Specify connection string and compute context
connectionString <- "Driver=SQL Server;
Server=tcp:192.168.176.130,1433;
Database=sqlr;
Uid=******;
Pwd=******"
RFMData <- RxSqlServerData(connectionString=connectionString,
table="RFM_Result")
cc <- RxInSqlServer(connectionString=connectionString,
autoCleanup=FALSE,
consoleOutput=TRUE)
rxSetComputeContext(cc)
rxGetInfo(RFMData, getVarInfo=T, numRows=3)
# Step 1: RFM analysis
# Visualize the RFM values
rxHistogram(~R, data=RFMData, xNumTicks=20)
rxHistogram(~F, data=RFMData, rowSelection=F < 30, xNumTicks=20)
rxHistogram(~M, data=RFMData, rowSelection=M < 200, xNumTicks=20)
# Count frequency of each RFMscore Level
tmp <- rxCube(~F(Toltal_Score), data=RFMData)
results <- rxResultsDF(tmp)
results <- results[results$Counts != 0, ]
results[order(results$Counts, decreasing=TRUE), ]
# Step 2: K-means Clustering
KmeansData <- RxSqlServerData(connectionString=connectionString,
table = "Kmeans_Result")
md.km <- rxKmeans(formula=~R+F+M+R_Score+F_Score+M_Score,
data=RFMData,
outFile=KmeansData,
numClusters=8,
algorithm="lloyd",
writeModelVars=TRUE,
overwrite=TRUE)
rxGetInfo(KmeansData, getVarInfo=TRUE, numRows=10)
centers <- round(md.km$centers, digits=2)
size <- md.km$size
centers.txt <- file.path(output.path, "centers.txt")
write.table(centers, file=centers.txt, sep=" ")
size.txt <- file.path(output.path, "size.txt")
write.table(size, file=size.txt, sep=" ")
# Connect to SQL database via odbcConnect
library(RODBC)
channel<-odbcDriverConnect(connection=connectionString)
# Read Kmeans_Result from SQL via OSR
Kmeans.df <- sqlQuery(channel, paste("select * from dbo.Kmeans_Result"))
head(Kmeans.df)
plot(Kmeans.df[, 2:4], col=Kmeans.df$X_rxCluster)
title(main="RFM-based K-means on CDNOW Data", line=3)
# Step 3: Prediction-logistic and decision tree
# Create IsVIP variable
RFMVIPData <- RxSqlServerData(connectionString=connectionString,
table="RFMVIP")
rxDataStep(inData=RFMData,
outFile=RFMVIPData,
transforms=list(IsVIP=ifelse(Toltal_Score >= 441, 1, 0)),
overwrite=TRUE,
reportProgress=1)
rxGetInfo(RFMVIPData, getVarInfo=T, numRows=3)
rxGetInfo(RFMVIPRDData, getVarInfo=T, numRows=3)
RFMVIPRDData <- RxSqlServerData(connectionString=connectionString,
table="RFMVIPRD")
RFMVIPTrainTestData <- RxSqlServerData(connectionString = connectionString,
table="RFMVIPTrainTest")
rxDataStep(inData=RFMVIPRDData,
outFile=RFMVIPTrainTestData,
transforms=list(urv=factor(ifelse(RD <= 8,'TRAIN','TEST'))),
overwrite=T)
rxGetInfo(RFMVIPTrainTestData, T, numRows=3)
## Split data into training/testing data set
TrainData <- RxSqlServerData(connectionString=connectionString,
table = "RFMVIPTrainTest.urv.TRAIN")
TestData <- RxSqlServerData(connectionString=connectionString,
table="RFMVIPTrainTest.urv.TEST")
rxSplit(RFMVIPTrainTestData, outFilesBase=RFMVIPTrainTestData,
splitByFactor='urv', overwrite=T, reportProgress=1)
## Built our Logistic Regression Model with IsVIP as response
r1<- rxLogit(IsVIP~R+F+M,
data =RFMVIPData,
variableSelection = rxStepControl(method="stepwise",
scope = ~ R+F+M))
summary(r1)
## r1:stepwise selection shows that Monetary, Recency are significant.
## Build our Logistic Regression Model
r2 <- rxLogit(IsVIP~R+F,
data=RFMVIPData, covCoef=TRUE)
summary(r2)
## Predict our Logistic Model on our test Dataset
LogisticPred.xdf <- file.path(output.path,"LogisticPred.xdf")
p2 <- rxPredict(r2, data=test.xdf, outData=LogisticPred.xdf,
writeModelVars=TRUE, extraVarsToWrite="ID",
computeResiduals=TRUE, computeStdErr=TRUE,
interval="confidence", predVarNames='LogitPredict',
overwrite=TRUE)
rxGetInfo(LogisticPred.xdf, getVarInfo=T, numRows=10)
LogisticPredInfoTop10 <- rxGetInfo(LogisticPred.xdf, getVarInfo=T, numRows=10)
LogisticPredInfoTop10.txt <- file.path(output.path, "LogisticPredInfoTop10.txt")
write.table(LogisticPredInfoTop10$data, file=LogisticPredInfoTop10.txt, sep=" ")
## Draw a ROC curve
rxRocCurve(actualVarName='IsVIP',predVarNames='LogitPredict',data=LogisticPred.xdf)
## Build a Decision Tree with Cluster as response
d1 <- rxDTree(Cluster~R+F+M, data=TrainData, blocksPerRead=5)
d1 <- rxDTree(Cluster~R_Score+F_Score+M_Score, data=TrainData, blocksPerRead=5)
d1
d1Cp<- rxDTreeBestCp(d1)
d1 <- prune.rxDTree(d1, cp=d1Cp)
d2 <- rxDTree(Cluster~R+F+M, data=TrainData, pruneCp="auto")
d2 <- rxDTree(Cluster~R_Score+F_Score+M_Score, data=TrainData, pruneCp="auto")
d2
# View Decision Tree
# View 1
library(RevoTreeView)
plot(createTreeView(d1))
plot(createTreeView(d2))
# View 2
library(rpart)
plot(rxAddInheritance(d1))
text(rxAddInheritance(d1))
title(main="RFM-based Decision Tree on CDNOW Data",line=3)
plot(rxAddInheritance(d2))
text(rxAddInheritance(d2))
title(main="RFM-based Decision Tree on CDNOW Data",line=3)
# Prediction
DTreePred.xdf<-file.path(output.path,"DTreePred.xdf")
rxPredict(d1, data=test.xdf, outData=DTreePred.xdf,
writeModelVars=TRUE, extraVarsToWrite="ID",
computeResiduals=T, overwrite=TRUE)
rxGetInfo(DTreePred.xdf, getVarInfo=T, numRows=3)
DTreePredInfoTop10 <- rxGetInfo(DTreePred.xdf, getVarInfo=T, numRows=10)
DTreePredInfoTop10.txt <- file.path(output.path, "DTreePredInfoTop10.txt")
write.table(DTreePredInfoTop10$data, file=DTreePredInfoTop10.txt, sep=" ")
DTreePred <- rxXdfToDataFrame(file=DTreePred.xdf)
sqlSave(channel, DTreePred, rownames=FALSE, append=FALSE,
varTypes=list(numeric="float",
integer="int",
Date="date"))
odbcClose(channel)
@@ -0,0 +1,307 @@
###############################################################################
#Description: A set of R functions to implement the Independent RFM scoring and the RFM scoring with input breaks.
#Author: Jack Han http://www.DataApple.net email: jackhan2008 # qq.com
#Version: 1.3
#Date: 23 Dec 2013
#Usage: Read the article "RFM Customer Analysis with R Language" http://www.dataapple.net/?p=84
################################################################################
################################################################################
# Function
# getDataFrame(df,startDate,endDate,tIDColName="ID",tDateColName="Date",tAmountColName="Amount")
#
# Description
# Process the input data frame of transcation records so that the data frame can be ready for RFM scoring.
# A.Remove the duplicate records with the same customer ID
# B.Find the most recent date for each ID and calculate the days to the endDate, to get the Recency data
# C.Calculate the quantity of translations of a customer, to get the Frequency data
# D.Sum the amount of money a customer spent and divide it by Frequency, to get the average amount per transaction, that is the Monetary data.
#
# Arguments
# df - A data frame of transcation records with customer ID, dates, and the amount of money of each transation
# startDate - the start date of transcation, the records that happened after the start date will be kepted
# endDate - the end date of transcation, the records that happed after the end date will be removed. It works with the start date to set a time scope
# tIDColName - the column name which contains customer IDs in the input data frame
# tDateColName - the column name which contains transcation dates in the input data frame
# tAmountColName - the column name which contains the amount of money of each transcation in the input data frame
#
# Return Value
# Returns a new data frame with three new columns of "Recency","Frequency", and "Monetary". The number in "Recency" is the quantity of days from the # #most recent transcation of a customer to the endDate; The number in the "Frequency" is the quantity of transcations of a customer during the period from # #startDate to endDate; the number in the "Monetary" is the average amount of money per transcation of a customer during that period.
#
#################################################################################
getDataFrame <- function(df,startDate,endDate,tIDColName="ID",tDateColName="Date",tAmountColName="Amount"){
#order the dataframe by date descendingly
df <- df[order(df[,tDateColName],decreasing = TRUE),]
#remove the record before the start data and after the end Date
df <- df[df[,tDateColName]>= startDate,]
df <- df[df[,tDateColName]<= endDate,]
#remove the rows with the duplicated IDs, and assign the df to a new df.
newdf <- df[!duplicated(df[,tIDColName]),]
# caculate the Recency(days) to the endDate, the smaller days value means more recent
Recency<-as.numeric(difftime(endDate,newdf[,tDateColName],units="days"))
# add the Days column to the newdf data frame
newdf <-cbind(newdf,Recency)
#order the dataframe by ID to fit the return order of table() and tapply()
newdf <- newdf[order(newdf[,tIDColName]),]
# caculate the frequency
fre <- as.data.frame(table(df[,tIDColName]))
Frequency <- fre[,2]
newdf <- cbind(newdf,Frequency)
#caculate the Money per deal
m <- as.data.frame(tapply(df[,tAmountColName],df[,tIDColName],sum))
Monetary <- m[,1]/Frequency
newdf <- cbind(newdf,Monetary)
return(newdf)
} # end of function getDataFrame
################################################################################
# Function
# getIndependentScore(df,r=5,f=5,m=5)
#
# Description
# Scoring the Recency, Frequency, and Monetary in r, f, and m in aliquots independently
#
# Arguments
# df - A data frame returned by the function of getDataFrame
# r - The highest point of Recency
# f - The highest point of Frequency
# m - The highest point of Monetary
#
# Return Value
# Returns a new data frame with four new columns of "R_Score","F_Score","M_Score", and "Total_Score".
#################################################################################
getIndependentScore <- function(df,r=5,f=5,m=5) {
if (r<=0 || f<=0 || m<=0) return
#order and the score
df <- df[order(df$Recency,-df$Frequency,-df$Monetary),]
R_Score <- scoring(df,"Recency",r)
df <- cbind(df, R_Score)
df <- df[order(-df$Frequency,df$Recency,-df$Monetary),]
F_Score <- scoring(df,"Frequency",f)
df <- cbind(df, F_Score)
df <- df[order(-df$Monetary,df$Recency,-df$Frequency),]
M_Score <- scoring(df,"Monetary",m)
df <- cbind(df, M_Score)
#order the dataframe by R_Score, F_Score, and M_Score desc
df <- df[order(-df$R_Score,-df$F_Score,-df$M_Score),]
# caculate the total score
Total_Score <- c(100*df$R_Score + 10*df$F_Score+df$M_Score)
df <- cbind(df,Total_Score)
return (df)
} # end of function getIndependentScore
################################################################################
# Function
# scoring(df,column,r=5)
#
# Description
# A function to be invoked by the getIndepandentScore function
#######################################
scoring <- function (df,column,r=5){
#get the length of rows of df
len <- dim(df)[1]
score <- rep(0,times=len)
# get the quantity of rows per 1/r e.g. 1/5
nr <- round(len / r)
if (nr > 0){
# seperate the rows by r aliquots
rStart <-0
rEnd <- 0
for (i in 1:r){
#set the start row number and end row number
rStart = rEnd+1
#skip one "i" if the rStart is already in the i+1 or i+2 or ...scope.
if (rStart> i*nr) next
if (i == r){
if(rStart<=len ) rEnd <- len else next
}else{
rEnd <- i*nr
}
# set the Recency score
score[rStart:rEnd]<- r-i+1
# make sure the customer who have the same recency have the same score
s <- rEnd+1
if(i<r & s <= len){
for(u in s: len){
if(df[rEnd,column]==df[u,column]){
score[u]<- r-i+1
rEnd <- u
}else{
break;
}
}
}
}
}
return(score)
} #end of function Scoring
################################################################################
# Function
# getScoreWithBreaks(df,r,f,m)
#
# Description
# Scoring the Recency, Frequency, and Monetary in r, f, and m which are vector object containing a series of breaks
#
# Arguments
# df - A data frame returned by the function of getDataFrame
# r - A vector of Recency breaks
# f - A vector of Frequency breaks
# m - A vector of Monetary breaks
#
# Return Value
# Returns a new data frame with four new columns of "R_Score","F_Score","M_Score", and "Total_Score".
#
#################################################################################
cutpoint <- function(vec){
temp <- as.vector(quantile(vec,probs = c(0,0.2,0.4,0.6,0.8,1.0)))
temp[2:5]
}
getScoreWithBreaks <- function(df,r,f,m) {
## scoring the Recency
len = length(r)
R_Score <- c(rep(1,length(df[,1])))
df <- cbind(df,R_Score)
for(i in 1:len){
if(i == 1){
p1=0
}else{
p1=r[i-1]
}
p2=r[i]
if(dim(df[p1<df$Recency & df$Recency<=p2,])[1]>0) df[p1<df$Recency & df$Recency<=p2,]$R_Score = len - i+ 2
}
## scoring the Frequency
len = length(f)
F_Score <- c(rep(1,length(df[,1])))
df <- cbind(df,F_Score)
for(i in 1:len){
if(i == 1){
p1=0
}else{
p1=f[i-1]
}
p2=f[i]
if(dim(df[p1<df$Frequency & df$Frequency<=p2,])[1]>0) df[p1<df$Frequency & df$Frequency<=p2,]$F_Score = i
}
if(dim(df[f[len]<df$Frequency,])[1]>0) df[f[len]<df$Frequency,]$F_Score = len+1
## scoring the Monetary
len = length(m)
M_Score <- c(rep(1,length(df[,1])))
df <- cbind(df,M_Score)
for(i in 1:len){
if(i == 1){
p1=0
}else{
p1=m[i-1]
}
p2=m[i]
if(dim(df[p1<df$Monetary & df$Monetary<=p2,])[1]>0) df[p1<df$Monetary & df$Monetary<=p2,]$M_Score = i
}
if(dim(df[m[len]<df$Monetary,])[1]>0) df[m[len]<df$Monetary,]$M_Score = len+1
#order the dataframe by R_Score, F_Score, and M_Score desc
df <- df[order(-df$R_Score,-df$F_Score,-df$M_Score),]
# caculate the total score
Total_Score <- c(100*df$R_Score + 10*df$F_Score+df$M_Score)
df <- cbind(df,Total_Score)
return(df)
} # end of function of getScoreWithBreaks
################################################################################
# Function
# drawHistograms(df,r,f,m)
#
# Description
# Draw the histograms in the R, F, and M dimensions so that we can see the quantity of customers in each RFM block.
#
# Arguments
# df - A data frame returned by the function of getIndependent or getScoreWithBreaks
# r - The highest point of Recency
# f - The highest point of Frequency
# m - The highest point of Monetary
#
# Return Value
# No return value.
#
#################################################################################
drawHistograms <- function(df,r=5,f=5,m=5){
#set the layout plot window
par(mfrow = c(f,r))
names <-rep("",times=m)
for(i in 1:m) names[i]<-paste("M",i)
for (i in 1:f){
for (j in 1:r){
c <- rep(0,times=m)
for(k in 1:m){
tmpdf <-df[df$R_Score==j & df$F_Score==i & df$M_Score==k,]
c[k]<- dim(tmpdf)[1]
}
if (i==1 & j==1)
barplot(c,col="lightblue",names.arg=names)
else
barplot(c,col="lightblue")
if (j==1) title(ylab=paste("F",i))
if (i==1) title(main=paste("R",j))
}
}
par(mfrow = c(1,1))
} # end of drawHistograms function
@@ -0,0 +1,54 @@
Data Driven Precision Marketing with SQL Server R Service
As a data professional, do you wonder how you can leverage data science for creating new value in your organization? In this sample, learn how you can leverage your familiar knowledge on working with databases, and learn how you can get started with doing data science with databases.
----------
**Example**
In retail, tons of data are generated every data, which imply rich information about the whole market.
To provide market intelligence, the CRM analysis is commonly used to understand customer segmentation, predict customer behavior and better target potential buyers via right recommendation. With growing size of data and higher request on timeliness, it becomes a bit more challenging to do precision marketing on big data in a much more time-efficient way.
In this demo, we address the issue with Microsoft R Server's parallel computing algorithms and build an end-to-end operationalized analytical system using SQL Server R and Power BI.
Using a concrete example of customer relationship management for retail, well share how you can jumpstart by
- Running R scripts using SQL Server as the compute context
- Operationalize your R scripts using stored procedures.
The insights delivered by these models are visualized using a Power BI dashboard.
----------
**Pre-requirements**
You have to do the following set-up before playing with this demo.
- Install SQL Server 2016 or create a SQL Server 2016 Enterprise VM on Azure with Standalone R Server and R Services installed/configured.
- Install R IDE: R Tools for Visual Studio or R Studio.
- Install PowerBI Desktop.
- Validate the successful installation.
----------
**Files**
This sample consists of the following directory structure.
- **Data** - This folder contains the CD sales data CDNOW.
- **R** - This folder contains the R code that you can run in any R IDE.
- **SQL Server** - This folder contains the sql files that you can run to create T-SQL stored procedures (with R code embeded) and try out this precision marketing example.
- **PowerBI** - This folder contains a sample PowerBI report.
To jumpstart, run the T-SQL files (crm_demo.sql)
**Note**
This is a demo built on SQL 2016 RC1 in Dec 2015. To try out it, please modify it to fit the new version of SQL Server R Services.
@@ -0,0 +1,429 @@
--use the database RREDemoSql
use sqlr;
go
drop procedure if exists get_CDNOW_RFM
go
--create stored procedure to get RFM
create proc get_CDNOW_RFM (@start datetime = '1900-1-1', @end datetime = '3000-1-1', @now datetime = null)
as
begin
if @now is null
set @now = getdate()
select
ID, DATEDIFF(d,R,@now) as R ,F,M
from
(select
ID, MAX([Date]) as R, COUNT(Volume) as F, round(avg(Amount),2) as M
from
[dbo].[CDNOW]
where
[Date] BETWEEN @start AND @end
group by ID ) as rfm_tmp
order by cast (ID as int)
end
go
--execute the stored procedure to obtain CDNOWRFM table
exec dbo.get_CDNOW_RFM @start='1997-1-1',@end='1998-7-1',@now='1998-7-1'
go
drop procedure if exists BreakScoreRFM
go
--create stored procedure to break RFM score
create proc BreakScoreRFM (@start datetime = '1900-1-1', @end datetime = '3000-1-1', @now datetime = null,
@r_cut varchar(254) = null, @f_cut varchar(254) = null, @m_cut varchar(254) = null)
as
begin
if @now is null
set @now = getdate()
declare @r_cut1 float = 100, @r_cut2 float = 200, @r_cut3 float = 300, @r_cut4 float = 400
declare @f_cut1 float = 100, @f_cut2 float = 200, @f_cut3 float = 300, @f_cut4 float = 400
declare @m_cut1 float = 100, @m_cut2 float = 200, @m_cut3 float = 300, @m_cut4 float = 400
declare @idx int = 0, @len int = 0
--get cut parameter, should add more code to check cut parameter.
if(@r_cut is not null) -- and @r_cut follow the syntax
begin
set @idx = CHARINDEX('-',@r_cut)
set @len = len(@r_cut)
set @r_cut1 = substring(@r_cut,1,@idx-1)
set @r_cut=substring(@r_cut,@idx+1,@len-@idx)
set @idx = CHARINDEX('-',@r_cut)
set @len = len(@r_cut)
set @r_cut2 = substring(@r_cut,1,@idx-1)
set @r_cut=substring(@r_cut,@idx+1,@len-@idx)
set @idx = CHARINDEX('-',@r_cut)
set @len = len(@r_cut)
set @r_cut3 = substring(@r_cut,1,@idx-1)
set @r_cut4=substring(@r_cut,@idx+1,@len-@idx)
end
if(@f_cut is not null) -- and @f_cut follow the syntax
begin
set @idx = CHARINDEX('-',@f_cut)
set @len = len(@f_cut)
set @f_cut1 = substring(@f_cut,1,@idx-1)
set @f_cut=substring(@f_cut,@idx+1,@len-@idx)
set @idx = CHARINDEX('-',@f_cut)
set @len = len(@f_cut)
set @f_cut2 = substring(@f_cut,1,@idx-1)
set @f_cut=substring(@f_cut,@idx+1,@len-@idx)
set @idx = CHARINDEX('-',@f_cut)
set @len = len(@f_cut)
set @f_cut3 = substring(@f_cut,1,@idx-1)
set @f_cut4=substring(@f_cut,@idx+1,@len-@idx)
end
if(@m_cut is not null) -- and @m_cut follow the syntax
begin
set @idx = CHARINDEX('-',@m_cut)
set @len = len(@m_cut)
set @m_cut1 = substring(@m_cut,1,@idx-1)
set @m_cut=substring(@m_cut,@idx+1,@len-@idx)
set @idx = CHARINDEX('-',@m_cut)
set @len = len(@m_cut)
set @m_cut2 = substring(@m_cut,1,@idx-1)
set @m_cut=substring(@m_cut,@idx+1,@len-@idx)
set @idx = CHARINDEX('-',@m_cut)
set @len = len(@m_cut)
set @m_cut3 = substring(@m_cut,1,@idx-1)
set @m_cut4=substring(@m_cut,@idx+1,@len-@idx)
end
--drop exists tmp tables.
if exists (select 1 from sys.tables where [object_id] =object_id('RFM_Score') and type= 'U')
begin
truncate table RFM_Score
drop table RFM_Score
end
if exists (select 1 from sys.tables where [object_id] =object_id('RFM') and type= 'U')
begin
truncate table RFM
drop table RFM
end
-- get RFM table from initial data.
select
ID, DATEDIFF(d,R,@now) as R, F, M into RFM
from
(select
ID, max([Date]) as R, count(Volume) as F, round(avg(Amount),2) as M
from
CDNOW
where
[Date] between @start and @end
group by ID ) as rfm_tmp
order by cast (ID as int)
-- record R_Score at temp table '#R'
select
ID,
case when R <= @r_cut1 then 5
when R > @r_cut1 and R <= @r_cut2 then 4
when R > @r_cut2 and R <= @r_cut3 then 3
when R > @r_cut3 and R <= @r_cut4 then 2
when R > @r_cut4 then 1
else 0
end as R_Score
into #R
from RFM
-- score F
select
ID,
case when F >= @f_cut4 then 5
when F > @f_cut3 and F <= @f_cut4 then 4
when F > @f_cut2 and F <= @f_cut3 then 3
when F > @f_cut3 and F <= @f_cut4 then 2
when F < @f_cut4 then 1
else 0
end as F_Score
into #F
from RFM
-- score M
select
ID,
case when M >= @m_cut4 then 5
when M > @m_cut3 and M <= @m_cut4 then 4
when M > @m_cut2 and M <= @m_cut3 then 3
when M > @m_cut3 and M <= @m_cut4 then 2
when M < @m_cut4 then 1
else 0
end as M_Score
into #M
from RFM
--union all
select #R.ID, R_Score, F_Score, M_Score, R_Score*100 + F_Score*10 + M_Score as Toltal_Score
into RFM_Score
from #R, #F, #M
where #R.ID = #F.ID and #R.ID = #M.ID
select * from RFM_Score order by Toltal_Score desc,ID
end
go
--execute the stored procedure to obtain RFM_Score table
exec dbo.BreakScoreRFM @start ='1997-1-1', @end = '1998-7-1' ,@now = '1998-7-1', @r_cut ='142-433-486-513', @f_cut = '1-1-2-4', @m_cut ='14.37-20.25-29.37-44.29'
go
--combine RFM and RFM_Score
drop table RFM_Result;
select a.*, b.R_Score, b.F_Score, b.M_Score, b.Toltal_Score
into RFM_Result
from
[dbo].[RFM] a left outer join
[dbo].[RFM_Score] b on
a.[ID] = b.[ID];
select top 10 * from RFM_Result;
--create stored procedure to visualize RFM
drop procedure if exists visualizeRFM;
go
create procedure visualizeRFM
as
begin
execute sp_execute_external_script
@language = N'R'
, @script = N'
drawHistograms <- function(df,r=5,f=5,m=5){
#set the layout plot window
par(mfrow = c(f,r))
names <-rep("",times=m)
for(i in 1:m) names[i]<-paste("M",i)
for (i in 1:f){
for (j in 1:r){
c <- rep(0,times=m)
for(k in 1:m){
tmpdf <-df[df$R_Score==j & df$F_Score==i & df$M_Score==k,]
c[k]<- dim(tmpdf)[1]
}
if (i==1 & j==1)
barplot(c,col="lightblue",names.arg=names)
else
barplot(c,col="lightblue")
if (j==1) title(ylab=paste("F",i))
if (i==1) title(main=paste("R",j))
}
}
par(mfrow = c(1,1))
}
RFMhist<-drawHistograms(RFM_Result[,1:4])
ff= tempfile()
png(filename=ff, width=620, height=240)
print(RFMhist)
dev.off()
OutputDataSet <- data.frame(data=readBin(file(ff, "rb"), what=raw(), n=1e6));
'
, @input_data_1 = N'select "R", "F", "M" from RFM_Result'
, @input_data_1_name = N'RFM_Result'
with result sets ((plot varbinary(max)));
end;
go
grant execute on visualizeRFM to rdemo;
go
--clustering based on RFM
drop table if exists Kmeans_Result;
drop table if exists CDNOW_rx_models;
go
create table CDNOW_rx_models(
model_name varchar(30) not null default('default model') primary key,
model varbinary(max) not null
);
go
create table Kmeans_Result (
"X_rxCluster" int null
, "R" int null, "F" int null, "M" float null
, "R_Score" int null, "F_Score" int null, "M_Score" int null
);
go
--create stored procedure to do clustering
drop procedure if exists generate_CDNOW_rx_Kmeans;
go
create procedure generate_CDNOW_rx_Kmeans
as
begin
execute sp_execute_external_script
@language = N'R'
, @script = N'
require("RevoScaleR");
CDNOWKmeans <- rxKmeans(formula=~R+F+M+R_Score+F_Score+M_Score,
data=RFM_Result,
#outFile=Kmeans_Result,
numClusters=8,
algorithm="lloyd",
writeModelVars=TRUE,
overwrite=TRUE)
rxKmeans_model <- data.frame(payload=as.raw(serialize(CDNOWKmeans, connection=NULL)));
'
, @input_data_1 = N'select * from RFM_Result'
, @input_data_1_name = N'RFM_Result'
, @output_data_1_name = N'rxKmeans_model'
with result sets ((model varbinary(max)));
end;
go
--how to write Kmeans_Result back to database?[To Be Modified]
insert into CDNOW_rx_models (model)
exec generate_CDNOW_rx_Kmeans;
update CDNOW_rx_models set model_name = 'rxKmeans' where model_name = 'default model';
select * from CDNOW_rx_models;
go
--create stored procedure to build logistic regression model
drop procedure if exists generate_CDNOW_rx_Logit;
go
create procedure generate_CDNOW_rx_Logit
as
begin
execute sp_execute_external_script
@language = N'R'
, @script = N'
require("RevoScaleR");
CDNOWLogit <- rxLogit(IsVIP~R+F+M,
data=RFMVIPCluster,
variableSelection=rxStepControl(method="stepwise",
scope=~R+F+M))
summary(CDNOWLogit)
rxLogit_model <- data.frame(payload = as.raw(serialize(CDNOWLogit, connection=NULL)));
'
, @input_data_1 = N'select * from RFMVIPCluster'
, @input_data_1_name = N'RFMVIPCluster'
, @output_data_1_name = N'rxLogit_model'
with result sets ((model varbinary(max)));
end;
go
insert into CDNOW_rx_models (model)
exec generate_CDNOW_rx_Logit;
update CDNOW_rx_models set model_name = 'rxLogit' where model_name = 'default model';
select * from CDNOW_rx_models;
go
--create stored procedure to build decision tree model
drop procedure if exists generate_CDNOW_rx_Dtree;
go
create procedure generate_CDNOW_rx_Dtree
as
begin
execute sp_execute_external_script
@language = N'R'
, @script = N'
require("RevoScaleR");
CDNOWDtree <- rxDTree(Cluster~R+F+M, data=RFMVIPCluster, pruneCp="auto")
rxDtree_model <- data.frame(payload=as.raw(serialize(CDNOWDtree, connection=NULL)));
'
, @input_data_1 = N'select * from RFMVIPCluster'
, @input_data_1_name = N'RFMVIPCluster'
, @output_data_1_name = N'rxDtree_model'
with result sets ((model varbinary(max)));
end;
go
insert into CDNOW_rx_models (model)
exec generate_CDNOW_rx_Dtree;
update CDNOW_rx_models set model_name = 'rxDtree' where model_name = 'default model';
select * from CDNOW_rx_models;
go
--create stored procedure to predict whether the customer is VIP or not
drop procedure if exists predict_CDNOW_IsVIP;
go
create procedure predict_CDNOW_IsVIP (@model varchar(100))
as
begin
declare @rx_model varbinary(max) = (select model from CDNOW_rx_models where model_name = @model);
-- Predict based on the specified model:
exec sp_execute_external_script
@language = N'R'
, @script = N'
require("RevoScaleR");
CDNOWmodel <- unserialize(rx_model);
CDNOWpred <- rxPredict(CDNOWmodel, data=RFMVIPCluster, writeModelVars = TRUE);
OutputDataSet <- cbind(RFMVIPCluster[,1], CDNOWpred$IsVIP, round(CDNOWpred$IsVIP_Pred,2));
colnames(OutputDataSet) <- c("ID", "IsVIP.Actual", "IsVIP.Expected");
OutputDataSet <- as.data.frame(OutputDataSet);
'
, @input_data_1 = N'
select * from RFMVIPCluster'
, @input_data_1_name = N'RFMVIPCluster'
, @params = N'@rx_model varbinary(max)'
, @rx_model = @rx_model
with result sets ( ("ID" int, "IsVIP.Actual" int, "IsVIP.Expected" float)
);
end;
go
--execute the stored procedure to obtain the prediction on IsVIP
exec predict_CDNOW_IsVIP 'rxLogit';
go
--create stored procedure to predict which cluster the customer belongs to
drop procedure if exists predict_CDNOW_Cluster;
go
create procedure predict_CDNOW_Cluster (@model varchar(100))
as
begin
declare @rx_model varbinary(max) = (select model from CDNOW_rx_models where model_name = @model);
-- Predict based on the specified model:
exec sp_execute_external_script
@language = N'R'
, @script = N'
require("RevoScaleR");
CDNOWmodel <- unserialize(rx_model);
CDNOWpred <- rxPredict(CDNOWmodel,
data=RFMVIPCluster,
predVarNames=c("prob1", "prob2", "prob3", "prob4", "prob5", "prob6", "prob7", "prob8"),
writeModelVars=TRUE, extraVarsToWrite="ID",
computeResiduals=T, overwrite=TRUE);
OutputDataSet <- round(cbind(RFMVIPCluster[,1], RFMVIPCluster[,10],
CDNOWpred$prob1,CDNOWpred$prob2,CDNOWpred$prob3,CDNOWpred$prob4,
CDNOWpred$prob5,CDNOWpred$prob6,CDNOWpred$prob7,CDNOWpred$prob8),2);
colnames(OutputDataSet) <- c("ID", "Cluster.Actual", "Cluster1.Prob","Cluster2.Prob","Cluster3.Prob","Cluster4.Prob","Cluster5.Prob","Cluster6.Prob","Cluster7.Prob","Cluster8.Prob");
OutputDataSet<-as.data.frame(OutputDataSet);
'
, @input_data_1 = N'
select * from RFMVIPCluster'
, @input_data_1_name = N'RFMVIPCluster'
, @params = N'@rx_model varbinary(max)'
, @rx_model = @rx_model
with result sets ( ("ID" int, "Cluster.Actual" int, "Cluster1.Prob" float,"Cluster2.Prob" float,"Cluster3.Prob" float,"Cluster4.Prob" float,"Cluster5.Prob" float,"Cluster6.Prob" float,"Cluster7.Prob" float,"Cluster8.Prob" float)
);
end;
go
--execute the stored procedure to obtain the prediction on Cluster
exec predict_CDNOW_Cluster 'rxDtree';
go
@@ -0,0 +1,28 @@
use sqlr;
go
drop table if exists CDNOW;
go
-- create the fraud table to hold invoice data:
create table CDNOW(
[ID] int not null,
[Date] date not null,
[Volume] int not null,
[Amount] float not null);
go
-- Modify path to the data file: "CDNOW_master.csv"
bulk insert CDNOW
from 'C:\sqlr\mydemos\CRM\CDNOW_master.csv'
with(
fieldterminator = ',',
firstrow = 2);
go
--create clustered columnstore index cs_CDNOW on CDNOW;
--go
grant select on CDNOW to rdemo;
go
@@ -1,34 +1,34 @@
-- List Data Sync user tables
select * from sys.tables as st join sys.schemas as ss on ss.schema_id = st.schema_id
where ss.name = 'DataSync' and st.name like '%_dss%' and st.name like '%<tablename>%'
where ss.name = 'DataSync' and st.name like '%_dss%' and st.name like '%<tablename>_dss%'
-- Generate the script to drop Data Sync tables
select 'Drop table [DataSync].['+ st.name+ '];' from sys.tables as st join sys.schemas as ss on ss.schema_id = st.schema_id
where ss.name = 'DataSync' and st.name like '%_dss%' and st.name like '%<tablename>%'
where ss.name = 'DataSync' and st.name like '%_dss%' and st.name like '%<tablename>_dss%'
-- List Data Sync stored procedures
select * from sys.procedures as sp join sys.schemas as ss on ss.schema_id = sp.schema_id
where ss.name = 'DataSync' and sp.name like '%_dss_%' and sp.name like '%<tablename>%'
where ss.name = 'DataSync' and sp.name like '%_dss_%' and sp.name like '%<tablename>_dss%'
--- Generate the script to drop Data Sync stored procedures
select 'Drop procedure [DataSync].['+ sp.name+ '];' from sys.procedures as sp join sys.schemas as ss on ss.schema_id = sp.schema_id
where ss.name = 'DataSync' and sp.name like '%_dss_%' and sp.name like '%<tablename>%'
where ss.name = 'DataSync' and sp.name like '%_dss_%' and sp.name like '%<tablename>_dss%'
-- List Data Sync triggers
select * from sys.triggers as st
where st.name like '%_dss%' and st.name like '%trigger' and st.name like '%<tablename>%'
where st.name like '%_dss%' and st.name like '%trigger' and st.name like '%<tablename>_dss%'
-- Generate the script to drop Data Sync triggers
select 'Drop trigger ['+st.name+']' from sys.triggers as st
where st.name like '%_dss%' and st.name like '%trigger' and st.name like '%<tablename>%'
where st.name like '%_dss%' and st.name like '%trigger' and st.name like '%<tablename>_dss%'
-- List Data Sync UDTs
select * from sys.types as st join
sys.schemas as ss on st.schema_id = ss.schema_id
where ss.name = 'DataSync' and st.name like '%_dss_%' and st.name like '%<tablename>%'
where ss.name = 'DataSync' and st.name like '%_dss_%' and st.name like '%<tablename>_dss%'
-- Generate the script to drop Data Sync UDTs
select 'Drop Type [DataSync].['+ st.name+ '];'
from sys.types as st join
sys.schemas as ss on st.schema_id = ss.schema_id
where ss.name = 'DataSync' and st.name like '%_dss_%' and st.name like '%<tablename>%'
where ss.name = 'DataSync' and st.name like '%_dss_%' and st.name like '%<tablename>_dss%'
+2 -2
View File
@@ -50,7 +50,7 @@ To run this sample, you need the following prerequisites.
## Sample details
Please visit the [C# on Windows tutorial](https://www.microsoft.com/en-us/sql-server/developer-get-started/csharp-windows) to run through the sample in full with more detail.
Please visit the [C# on Windows tutorial](https://www.microsoft.com/en-us/sql-server/developer-get-started/csharp/win/) to run through the sample in full with more detail.
<a name=disclaimers></a>
@@ -62,4 +62,4 @@ The scripts and this guide are provided as samples. They are not part of any Azu
## Related Links
For more information, see these articles:
* To see more getting started tutorials, visit our [tutorials page](https://www.microsoft.com/en-us/sql-server/developer-get-started/)
* To see more getting started tutorials, visit our [tutorials page](https://www.microsoft.com/en-us/sql-server/developer-get-started/)