mirror of
https://github.com/Microsoft/sql-server-samples.git
synced 2025-12-08 14:58:54 +00:00
Merge branch 'master' of https://github.com/Microsoft/sql-server-samples
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
+2
@@ -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;
|
||||
Binary file not shown.
@@ -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")
|
||||
@@ -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
-1
@@ -1,5 +1,5 @@
|
||||
apiVersion: v1
|
||||
data: {sapassword: JyFMb2NrczEyMyc=}
|
||||
data: {sapassword: "<>"}
|
||||
kind: Secret
|
||||
metadata: {name: sql-secrets, namespace: ag1}
|
||||
type: Opaque
|
||||
|
||||
Binary file not shown.
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 |
BIN
Binary file not shown.
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+307
@@ -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, we’ll 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
|
||||
BIN
Binary file not shown.
@@ -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%'
|
||||
|
||||
@@ -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/)
|
||||
|
||||
Reference in New Issue
Block a user