AMPScript and SFMC SQL Guide
Adjust Technical Level
Select your expertise level to customize content
AMPScript and SFMC SQL represent the primary programmatic interfaces within Salesforce Marketing Cloud for content personalization and data manipulation. AMPScript functions as a server-side scripting language that executes at send-time or page load to dynamically generate personalized content based on subscriber attributes and business logic. SFMC SQL provides a specialized implementation of SQL that enables complex data querying, transformation, and relationship management across data extensions within the platform's data model.
Understanding AMPScript
Technical Overview
What AMPScript Does for Marketers
AMPScript is the "secret ingredient" that transforms standard emails and landing pages into personalized, dynamic experiences for each recipient.
How AMPScript Helps Your Marketing:
- Personalization Beyond "Dear [Name]": Create truly personalized experiences based on any data you have about your subscribers
- Dynamic Content: Show different content to different people based on their preferences, behavior, or demographic information
- Real-time Content: Include content that's current when the email is opened (like countdown timers, latest products, or weather forecasts)
- Complex Logic: Apply sophisticated "if-then" logic to deliver the right message to each person
- Data Integration: Pull in information from different data sources to create complete, relevant messages
- Interactive Experiences: Create more engaging landing pages and microsites that respond to user input
Common Business Use Cases:
- Product Recommendations: Displaying items based on past purchases or browsing behavior
- Localized Content: Showing nearest store locations or events based on the recipient's address
- Segmented Messaging: Varying content sections based on customer segments (loyalty tier, account status, etc.)
- Personalized Offers: Generating unique coupon codes or special offers for each recipient
- Approval Workflows: Creating review and approval processes for email content
- Preference Management: Building custom preference centers that update subscriber data
Benefits for Marketing Teams:
- Higher Engagement: More relevant content typically leads to better open and click rates
- Improved Conversion: Personalized content and offers generally convert better
- Operational Efficiency: Create once, personalize many times instead of building multiple versions
- Enhanced Testing: Test different content variations more efficiently
- Better Data Utilization: Make practical use of the customer data you're collecting
- Reduced Errors: Automate content updates rather than manual copy/paste processes
Business Perspective
AMPScript Technical Fundamentals
AMPScript is a proprietary scripting language developed by Salesforce for Marketing Cloud. It's a server-side language that processes at send time (for emails) or page load (for landing pages and CloudPages).
Key Technical Characteristics:
- Execution Context: Server-side processing that occurs before content delivery to recipient
- Language Structure: Procedural language with elements of markup languages
- Case Sensitivity: Case-insensitive for functions and keywords
- Delimiter Style: Uses
%%[
and]%%
for code blocks - Variable Declaration: Uses
VAR
orSET
keywords with@
prefix for variables - Function Calling: Function calls using parentheses and comma-separated parameters
- Data Types: Weakly typed with automatic type conversion
- Inline Usage: Can be embedded directly within HTML using
%%=Function()=%%
syntax
Execution Flow:
- Marketing Cloud retrieves the message template
- The system identifies AMPScript blocks and inline functions
- Variables are initialized and populated with context data (subscriber attributes, data extension fields)
- AMPScript code executes sequentially, processing logic and function calls
- Output is generated by replacing AMPScript code with its computed results
- The final rendered content is delivered to the recipient
Integration Points:
- Data Extensions: Direct access to Marketing Cloud data through
LookupRows
and related functions - Personalization Strings: Can reference personalization strings like
%%FirstName%%
- Content Areas: Can dynamically include or process Content Areas
- External Systems: API interaction capabilities through
HTTPGet
,HTTPPost
, etc. - System Context: Access to system variables like
JobID
,_MessageContext
, etc.
Technical Limitations:
- Processing Time: Maximum execution time limits (particularly for API calls)
- Debugging: Limited native debugging capabilities
- Error Handling: Basic error handling through
TryCatch
functions - Script Size: Practical limits on script complexity and size
- Libraries: No native support for external libraries or modules
AMPScript Syntax and Basics
- Basic Syntax
- Variables
- Conditional Logic
- Loops
- String Functions
- Data Functions
%%[ /* This is a block of AMPScript code */
/* Variable declaration and assignment */
VAR @firstName, @lastName, @fullName
SET @firstName = "John"
SET @lastName = "Doe"
SET @fullName = CONCAT(@firstName, " ", @lastName)
/* Output a value inline */
]%%
Hello, %%=v(@fullName)=%%!
%%[
/* Conditional logic */
IF NOT EMPTY(@firstName) THEN
SET @greeting = CONCAT("Welcome back, ", @firstName, "!")
ELSE
SET @greeting = "Welcome, new friend!"
ENDIF
]%%
%%=v(@greeting)=%%
/* This demonstrates inline function usage */
Today's date is: %%=FormatDate(Now(), "MMMM d, yyyy")=%%
%%[
/* Variable declarations */
VAR @simpleVar /* Declaration only */
VAR @firstName, @age, @isActive /* Multiple declaration */
SET @firstName = "Jane" /* Assignment after declaration */
/* Declaration and assignment in one line */
SET @age = 32
SET @isActive = true
/* Variables are not strongly typed */
SET @age = "thirty-two" /* No error, just converts to string */
/* System/context variables don't need declaration */
SET @jobId = JobID
SET @emailAddr = emailaddr /* Subscriber attribute */
/* Variables from Data Extensions */
SET @product = [ProductName] /* Field from sending DE */
]%%
Using variables in content:
• Name: %%=v(@firstName)=%%
• Age: %%=v(@age)=%%
• Account status: %%=IIF(@isActive, "Active", "Inactive")=%%
%%[
VAR @temperature, @weatherMsg
SET @temperature = 75
/* Basic IF-THEN-ELSE */
IF @temperature > 80 THEN
SET @weatherMsg = "It's hot today!"
ELSEIF @temperature > 60 THEN
SET @weatherMsg = "It's a pleasant day."
ELSE
SET @weatherMsg = "It's chilly out there."
ENDIF
/* Nested conditionals */
VAR @timeOfDay, @greeting
SET @timeOfDay = "morning"
IF @timeOfDay == "morning" THEN
IF @temperature < 50 THEN
SET @greeting = "Bundle up this morning!"
ELSE
SET @greeting = "Good morning!"
ENDIF
ELSEIF @timeOfDay == "evening" THEN
SET @greeting = "Good evening!"
ELSE
SET @greeting = "Hello there!"
ENDIF
/* Using AND, OR, NOT operators */
VAR @isWeekend, @isHoliday, @storeMsg
SET @isWeekend = true
SET @isHoliday = false
IF @isWeekend OR @isHoliday THEN
SET @storeMsg = "We're open from 10 AM to 4 PM today."
ELSEIF NOT @isWeekend AND NOT @isHoliday THEN
SET @storeMsg = "We're open from 9 AM to 9 PM today."
ENDIF
]%%
%%=v(@greeting)=%% %%=v(@weatherMsg)=%%
Store hours: %%=v(@storeMsg)=%%
%%[
/* Basic counting loop */
VAR @counter, @loopOutput
SET @loopOutput = ""
FOR @counter = 1 TO 5 DO
SET @loopOutput = CONCAT(@loopOutput, "Count: ", @counter, "<br>")
NEXT @counter
/* Loop with step value */
VAR @evenNumbers
SET @evenNumbers = ""
FOR @counter = 2 TO 10 STEP 2 DO
SET @evenNumbers = CONCAT(@evenNumbers, @counter, " ")
NEXT @counter
/* Loop through data set rows */
VAR @rows, @rowCount, @row, @productList
SET @rows = LookupRows("ProductCatalog", "Category", "Featured")
SET @rowCount = RowCount(@rows)
SET @productList = ""
IF @rowCount > 0 THEN
FOR @counter = 1 TO @rowCount DO
SET @row = Row(@rows, @counter)
SET @productName = Field(@row, "ProductName")
SET @productPrice = Field(@row, "Price")
SET @productList = CONCAT(@productList,
@productName, " - $", @productPrice, "<br>")
NEXT @counter
ENDIF
]%%
<h3>Loop Output Example</h3>
%%=v(@loopOutput)=%%
<h3>Even Numbers</h3>
%%=v(@evenNumbers)=%%
<h3>Featured Products</h3>
%%=v(@productList)=%%
%%[
/* Basic string operations */
VAR @message, @formattedMsg, @length
SET @message = "welcome to marketing cloud"
/* String case manipulation */
SET @upperCase = Upper(@message)
SET @lowerCase = Lower(@message)
SET @properCase = ProperCase(@message)
/* String measurement */
SET @length = Length(@message)
SET @wordCount = Length(Replace(@message, " ", "@@")) - Length(@message) + 1
/* String position and extraction */
SET @cloudPos = IndexOf(@message, "cloud")
SET @marketingSubstr = Substring(@message, 12, 9) /* "marketing" */
/* String manipulation */
SET @trimmedText = Trim(" Hello World! ")
SET @replacedText = Replace(@message, "marketing cloud", "SFMC")
/* String concatenation */
SET @firstName = "John"
SET @lastName = "Doe"
SET @fullName = CONCAT(@firstName, " ", @lastName)
/* Complex formatting */
SET @formattedPhone = Format("9175551234", "Phone")
SET @formattedDate = FormatDate(Now(), "MMMM d, yyyy")
]%%
<h3>String Function Examples</h3>
<ul>
<li>Original: %%=v(@message)=%%</li>
<li>Uppercase: %%=v(@upperCase)=%%</li>
<li>Proper Case: %%=v(@properCase)=%%</li>
<li>Length: %%=v(@length)=%% characters</li>
<li>Position of "cloud": %%=v(@cloudPos)=%%</li>
<li>Substring: %%=v(@marketingSubstr)=%%</li>
<li>Replace: %%=v(@replacedText)=%%</li>
<li>Formatted Phone: %%=v(@formattedPhone)=%%</li>
<li>Today's Date: %%=v(@formattedDate)=%%</li>
</ul>
%%[
/* Lookup a single row from a Data Extension */
VAR @customerRow, @customerStatus
SET @customerRow = Lookup("Customers", "Status", "CustomerID", "67890")
SET @customerStatus = @customerRow
/* LookupRows for multiple rows */
VAR @productRows, @rowCount, @displayOutput
SET @productRows = LookupRows("Products", "Category", "Electronics")
SET @rowCount = RowCount(@productRows)
SET @displayOutput = ""
/* Loop through results */
IF @rowCount > 0 THEN
FOR @i = 1 TO @rowCount DO
SET @row = Row(@productRows, @i)
SET @name = Field(@row, "ProductName")
SET @price = Field(@row, "Price")
SET @displayOutput = CONCAT(@displayOutput,
"<li>", @name, " - $", @price, "</li>")
NEXT @i
ELSE
SET @displayOutput = "<li>No products found</li>"
ENDIF
/* DataExtensionRowCount function */
SET @totalCustomers = DataExtensionRowCount("Customers")
/* Lookup with multiple criteria using LookupOrderedRows */
SET @topProds = LookupOrderedRows("Products", 3, "Price DESC",
"Category", "Electronics",
"InStock", "true")
]%%
<h3>Customer Information</h3>
<p>Status: %%=v(@customerStatus)=%%</p>
<h3>Electronics Products</h3>
<ul>
%%=v(@displayOutput)=%%
</ul>
<p>Total customers: %%=v(@totalCustomers)=%%</p>
Common AMPScript Use Cases
- Dynamic Content
- Content Regionalization
- Product Recommendations
- Coupon Generation
- Form Handling
%%[
/* Basic personalization with fallback */
VAR @firstName
SET @firstName = AttributeValue("FirstName")
IF EMPTY(@firstName) THEN
SET @firstName = "Valued Customer"
ENDIF
/* Content based on customer segment */
VAR @segment, @offerHeadline, @offerImage, @offerText
SET @segment = AttributeValue("CustomerSegment")
IF @segment == "New Customer" THEN
SET @offerHeadline = "Welcome Offer Just For You"
SET @offerImage = "http://example.com/images/new_customer_offer.jpg"
SET @offerText = "As a new customer, enjoy 15% off your first purchase."
ELSEIF @segment == "Loyal Customer" THEN
SET @offerHeadline = "Thanks for Your Loyalty"
SET @offerImage = "http://example.com/images/loyal_customer_offer.jpg"
SET @offerText = "As a thank you for your loyalty, enjoy 20% off your next purchase."
ELSE
SET @offerHeadline = "Special Offer Inside"
SET @offerImage = "http://example.com/images/standard_offer.jpg"
SET @offerText = "Enjoy 10% off your next purchase with us."
ENDIF
]%%
<h1>Hello, %%=v(@firstName)=%%!</h1>
<div class="offer-block">
<h2>%%=v(@offerHeadline)=%%</h2>
<img src="%%=v(@offerImage)=%%" alt="Special offer">
<p>%%=v(@offerText)=%%</p>
</div>
%%[
/* Determine content based on recipient's country */
VAR @country, @language, @currencySymbol, @storeLocatorURL
SET @country = AttributeValue("Country")
/* Set defaults */
SET @language = "English"
SET @currencySymbol = "$"
SET @storeLocatorURL = "https://example.com/stores/us"
/* Regionalize based on country */
IF @country == "France" THEN
SET @language = "French"
SET @currencySymbol = "€"
SET @storeLocatorURL = "https://example.com/stores/fr"
ELSEIF @country == "Germany" THEN
SET @language = "German"
SET @currencySymbol = "€"
SET @storeLocatorURL = "https://example.com/stores/de"
ELSEIF @country == "Japan" THEN
SET @language = "Japanese"
SET @currencySymbol = "¥"
SET @storeLocatorURL = "https://example.com/stores/jp"
ENDIF
/* Get localized content from ContentArea */
IF @language == "French" THEN
SET @welcomeMessage = ContentAreaByName("my contents\French Welcome")
ELSEIF @language == "German" THEN
SET @welcomeMessage = ContentAreaByName("my contents\German Welcome")
ELSEIF @language == "Japanese" THEN
SET @welcomeMessage = ContentAreaByName("my contents\Japanese Welcome")
ELSE
SET @welcomeMessage = ContentAreaByName("my contents\English Welcome")
ENDIF
/* Get nearest store information */
VAR @storeDE, @nearestStore, @storeAddress, @storePhone
SET @storeDE = LookupRows("StoreLocations", "Country", @country)
IF RowCount(@storeDE) > 0 THEN
SET @nearestStore = Row(@storeDE, 1)
SET @storeAddress = Field(@nearestStore, "Address")
SET @storePhone = Field(@nearestStore, "Phone")
ENDIF
]%%
<!-- Localized welcome message -->
%%=v(@welcomeMessage)=%%
<!-- Product with localized pricing -->
<div class="product">
<h3>Premium Subscription</h3>
<p class="price">%%=v(@currencySymbol)=%% 99.99/month</p>
<a href="%%=v(@storeLocatorURL)=%%">Find a Store Near You</a>
</div>
<!-- Nearest store information if available -->
%%[ IF NOT EMPTY(@storeAddress) THEN ]%%
<div class="store-info">
<h4>Your Nearest Store:</h4>
<p>%%=v(@storeAddress)=%%</p>
<p>Phone: %%=v(@storePhone)=%%</p>
</div>
%%[ ENDIF ]%%
%%[
/* Get customer information */
VAR @customerId, @lastCategory
SET @customerId = AttributeValue("CustomerId")
SET @lastCategory = AttributeValue("LastPurchaseCategory")
/* Default category if none found */
IF EMPTY(@lastCategory) THEN
SET @lastCategory = "Bestsellers"
ENDIF
/* Look up recommended products based on last purchase category */
VAR @recommendedProducts, @productCount, @currentProduct
SET @recommendedProducts = LookupOrderedRows("ProductCatalog", 3, "PopularityScore DESC",
"Category", @lastCategory,
"InStock", "true")
SET @productCount = RowCount(@recommendedProducts)
/* Start building HTML output */
SET @productHTML = ""
/* Build product recommendation HTML */
IF @productCount > 0 THEN
SET @productHTML = '<table width="100%" border="0" cellspacing="0" cellpadding="0"><tr>'
FOR @i = 1 TO @productCount DO
SET @currentProduct = Row(@recommendedProducts, @i)
SET @productName = Field(@currentProduct, "ProductName")
SET @productImage = Field(@currentProduct, "ImageURL")
SET @productPrice = Field(@currentProduct, "Price")
SET @productURL = Field(@currentProduct, "ProductURL")
/* Add product cell to HTML */
SET @productHTML = CONCAT(@productHTML, '<td width="33%" align="center" valign="top">',
'<img src="', @productImage, '" width="150" alt="', @productName, '"><br>',
'<strong>', @productName, '</strong><br>',
'$', @productPrice, '<br>',
'<a href="', @productURL, '">View Details</a>',
'</td>')
NEXT @i
SET @productHTML = CONCAT(@productHTML, '</tr></table>')
ELSE
SET @productHTML = '<p>No recommendations available at this time.</p>'
ENDIF
/* Add recently viewed items if available */
VAR @recentlyViewed, @recentCount
SET @recentlyViewed = LookupRows("CustomerProductViews", "CustomerId", @customerId)
SET @recentCount = RowCount(@recentlyViewed)
SET @recentHTML = ""
IF @recentCount > 0 THEN
SET @recentHTML = '<h3>Recently Viewed Items</h3><ul>'
FOR @j = 1 TO @recentCount DO
IF @j <= 3 THEN /* Limit to 3 items */
SET @recentItem = Row(@recentlyViewed, @j)
SET @itemName = Field(@recentItem, "ProductName")
SET @itemURL = Field(@recentItem, "ProductURL")
SET @recentHTML = CONCAT(@recentHTML, '<li><a href="', @itemURL, '">', @itemName, '</a></li>')
ENDIF
NEXT @j
SET @recentHTML = CONCAT(@recentHTML, '</ul>')
ENDIF
]%%
<h2>Recommended for You</h2>
<p>Based on your interest in %%=v(@lastCategory)=%%</p>
%%=v(@productHTML)=%%
%%=v(@recentHTML)=%%
%%[
/* Get subscriber information */
VAR @subscriberId, @email, @firstName, @loyaltyTier
SET @subscriberId = _subscriberkey
SET @email = emailaddr
SET @firstName = AttributeValue("FirstName")
SET @loyaltyTier = AttributeValue("LoyaltyTier")
/* Define discount based on loyalty tier */
VAR @discountPercent, @expirationDays
IF @loyaltyTier == "Platinum" THEN
SET @discountPercent = "25"
SET @expirationDays = 30
ELSEIF @loyaltyTier == "Gold" THEN
SET @discountPercent = "20"
SET @expirationDays = 21
ELSEIF @loyaltyTier == "Silver" THEN
SET @discountPercent = "15"
SET @expirationDays = 14
ELSE
SET @discountPercent = "10"
SET @expirationDays = 7
ENDIF
/* Generate coupon code */
VAR @randomString, @couponCode, @fullCode
SET @randomString = CONCAT(SUBSTRING(@subscriberId, 1, 4), SUBSTRING(@email, 1, 3))
SET @randomString = Replace(@randomString, "@", "A")
SET @randomString = Replace(@randomString, ".", "X")
SET @couponCode = CONCAT("SAVE", @discountPercent, Upper(@randomString))
/* Calculate expiration date */
VAR @expirationDate, @expirationFormatted
SET @expirationDate = DateAdd(Now(), @expirationDays, "D")
SET @expirationFormatted = FormatDate(@expirationDate, "MM/dd/yyyy")
/* Store coupon in Data Extension for validation */
VAR @insertResult
SET @insertResult = InsertData("CouponCodes",
"SubscriberKey", @subscriberId,
"Email", @email,
"CouponCode", @couponCode,
"DiscountPercent", @discountPercent,
"IssueDate", Now(),
"ExpirationDate", @expirationDate,
"IsRedeemed", "false")
/* Set appropriate images based on discount */
VAR @couponImage
IF @discountPercent >= 20 THEN
SET @couponImage = "http://example.com/images/premium_coupon.jpg"
ELSE
SET @couponImage = "http://example.com/images/standard_coupon.jpg"
ENDIF
]%%
<div class="coupon-container" style="border: 2px dashed #333; padding: 20px; text-align: center; margin: 20px auto; max-width: 500px;">
<img src="%%=v(@couponImage)=%%" alt="Special Offer" style="max-width: 100%;">
<h2>EXCLUSIVE OFFER FOR %%=v(ProperCase(@firstName))=%%</h2>
<p style="font-size: 24px; font-weight: bold;">SAVE %%=v(@discountPercent)=%%% ON YOUR NEXT PURCHASE</p>
<div style="background-color: #f5f5f5; padding: 10px; margin: 15px 0; font-family: monospace; font-size: 18px; letter-spacing: 2px;">
%%=v(@couponCode)=%%
</div>
<p>Use this code online or in-store.</p>
<p style="font-size: 12px;">Expires: %%=v(@expirationFormatted)=%%</p>
<p style="font-size: 10px;">Limit one per customer. Cannot be combined with other offers.</p>
</div>
%%[
/* This example is for a CloudPage with a preference form */
/* Check if form was submitted */
VAR @formSubmitted, @email, @updateResult, @message
SET @formSubmitted = RequestParameter("submitted")
IF @formSubmitted == "true" THEN
/* Get form values */
SET @email = RequestParameter("email")
SET @firstName = RequestParameter("firstName")
SET @lastName = RequestParameter("lastName")
SET @preferenceEmail = RequestParameter("prefEmail")
SET @preferenceSMS = RequestParameter("prefSMS")
SET @preferenceDirectMail = RequestParameter("prefDirectMail")
/* Default values for checkboxes that might not be submitted */
IF EMPTY(@preferenceEmail) THEN
SET @preferenceEmail = "false"
ENDIF
IF EMPTY(@preferenceSMS) THEN
SET @preferenceSMS = "false"
ENDIF
IF EMPTY(@preferenceDirectMail) THEN
SET @preferenceDirectMail = "false"
ENDIF
/* Validate email */
IF NOT EMPTY(@email) AND IndexOf(@email, "@") > 0 THEN
/* Look up subscriber */
VAR @subscriber
SET @subscriber = LookupRows("Subscribers", "EmailAddress", @email)
IF RowCount(@subscriber) > 0 THEN
/* Update existing subscriber */
SET @subscriberRow = Row(@subscriber, 1)
SET @subscriberId = Field(@subscriberRow, "SubscriberKey")
/* Update subscriber preferences */
SET @updateResult = UpdateData("Subscribers",
1, "SubscriberKey", @subscriberId,
"FirstName", @firstName,
"LastName", @lastName,
"EmailPreference", @preferenceEmail,
"SMSPreference", @preferenceSMS,
"DirectMailPreference", @preferenceDirectMail,
"LastModified", Now())
IF @updateResult == 1 THEN
SET @message = "Your preferences have been updated successfully."
ELSE
SET @message = "There was an error updating your preferences. Please try again."
ENDIF
ELSE
/* Create new subscriber */
SET @subscriberId = GUID()
SET @createResult = InsertData("Subscribers",
"SubscriberKey", @subscriberId,
"EmailAddress", @email,
"FirstName", @firstName,
"LastName", @lastName,
"EmailPreference", @preferenceEmail,
"SMSPreference", @preferenceSMS,
"DirectMailPreference", @preferenceDirectMail,
"DateCreated", Now(),
"LastModified", Now())
IF @createResult == 1 THEN
SET @message = "Thank you for submitting your preferences."
ELSE
SET @message = "There was an error saving your preferences. Please try again."
ENDIF
ENDIF
ELSE
SET @message = "Please enter a valid email address."
ENDIF
ENDIF
]%%
<!DOCTYPE html>
<html>
<head>
<title>Communication Preferences</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.form-container { max-width: 600px; margin: 0 auto; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], input[type="email"] { width: 100%; padding: 8px; }
.message { padding: 10px; margin-bottom: 20px; }
.success { background-color: #dff0d8; border: 1px solid #d6e9c6; }
.error { background-color: #f2dede; border: 1px solid #ebccd1; }
</style>
</head>
<body>
<div class="form-container">
<h1>Communication Preferences</h1>
%%[ IF NOT EMPTY(@message) THEN ]%%
<div class="message %%[ IF @updateResult == 1 OR @createResult == 1 THEN ]%%success%%[ ELSE ]%%error%%[ ENDIF ]%%">
%%=v(@message)=%%
</div>
%%[ ENDIF ]%%
<form method="post">
<input type="hidden" name="submitted" value="true">
<div class="form-group">
<label for="email">Email Address *</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" id="firstName" name="firstName">
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" id="lastName" name="lastName">
</div>
<h3>Preferences</h3>
<div class="form-group">
<label>
<input type="checkbox" name="prefEmail" value="true">
I would like to receive email communications
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="prefSMS" value="true">
I would like to receive SMS/text messages
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="prefDirectMail" value="true">
I would like to receive direct mail
</label>
</div>
<div class="form-group">
<button type="submit">Update Preferences</button>
</div>
</form>
</div>
</body>
</html>
Understanding SFMC SQL
Technical Overview
What SFMC SQL Does for Marketers
SFMC SQL is the engine that powers data transformation and segmentation within Salesforce Marketing Cloud, helping marketers organize, filter, and prepare their customer data for targeted campaigns.
How SFMC SQL Helps Your Marketing:
- Advanced Segmentation: Create precisely targeted audiences based on complex criteria and behaviors
- Data Consolidation: Combine information from multiple sources into unified customer views
- Automated Data Management: Schedule regular data updates, cleanups, and transformations
- Campaign Preparation: Prepare and refine data for use in email campaigns and customer journeys
- Performance Analysis: Analyze campaign results and create custom reporting data sets
- Data Quality Control: Clean, standardize, and deduplicate customer data
Common Business Use Cases:
- Audience Selection: Creating targeted segments like "customers who purchased in the last 30 days but not in the last 7 days"
- Data Enrichment: Adding calculated fields like customer lifetime value or loyalty duration
- RFM Analysis: Categorizing customers by Recency, Frequency, and Monetary value
- Campaign Performance: Analyzing engagement metrics across different customer segments
- Data Cleansing: Standardizing formats and removing duplicate records
- Journey Preparation: Creating entry lists for customer journeys in Journey Builder
Benefits for Marketing Teams:
- Greater Precision: More accurate targeting based on multiple data points
- Increased Efficiency: Automation of repetitive data tasks
- Better Insights: Custom reporting tailored to your specific business needs
- Improved Personalization: More sophisticated data for dynamic content
- Enhanced Data Governance: Better control and standardization of marketing data
- Reduced Manual Effort: Less spreadsheet manipulation and manual list building
Business Perspective
SFMC SQL Technical Characteristics
SQL in Salesforce Marketing Cloud is a specialized implementation of Transact-SQL (T-SQL) used primarily within Query Activities in Automation Studio. It provides data manipulation and extraction capabilities focused on the platform's Data Extension model.
Key Technical Characteristics:
- SQL Variant: Based on Microsoft T-SQL with Marketing Cloud-specific extensions and limitations
- Execution Context: Primarily within Automation Studio's Query Activities
- Data Model Focus: Operates on Data Extensions as primary data structures
- Query Types Supported: SELECT, INSERT, UPDATE, DELETE operations
- Join Capabilities: Supports various join types (INNER, LEFT, RIGHT, FULL) between Data Extensions
- Aggregate Functions: Standard SQL aggregates like COUNT, SUM, AVG, MIN, MAX
- Execution Scheduling: Runs through scheduled or triggered Automation Studio activities
- Result Handling: Results typically output to target Data Extensions
Technical Limitations:
- Timeout Constraints: 30-minute maximum execution time for queries
- Row Limitations: Practical limits on the number of rows processed efficiently
- Subquery Support: Limited compared to standard SQL implementations
- Function Support: Subset of T-SQL functions plus Marketing Cloud-specific functions
- Transaction Control: Limited transaction management capabilities
- Direct API Access: No direct SQL API access; must execute through platform interfaces
System Integration:
- Data Sources: Primarily Data Extensions, with limited access to system data views
- Automation Integration: Core component of data processing within Automation Studio
- Journey Builder Integration: Can be used to prepare data for journey entry
- Scheduled Processing: Key role in recurring data processing operations
- ETL Workflows: Often used as part of extract-transform-load processes
Performance Considerations:
- Query Optimization: Critical for large datasets to avoid timeouts
- Indexing Strategy: Proper indexes on Data Extensions significantly impact performance
- Execution Windows: Best practices include running resource-intensive queries during off-peak hours
- Resource Contention: Queries compete with other Marketing Cloud processes for resources
- Data Volume Management: Strategies needed for handling large datasets (batching, filtering)
SFMC SQL Basics
- Basic SELECT
- Joins
- Aggregates & Grouping
- Data Manipulation
- Date Functions
- String Functions
-- Basic SELECT statement from a Data Extension
SELECT
SubscriberKey,
EmailAddress,
FirstName,
LastName,
Status
FROM Subscribers
-- Using aliases for readability
SELECT
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName
FROM Subscribers as s
-- Selecting specific records with WHERE
SELECT
SubscriberKey,
EmailAddress,
FirstName,
LastName
FROM Subscribers
WHERE Status = 'Active'
AND DateJoined > '2023-01-01'
-- Limiting results (Note: TOP is used instead of LIMIT in SFMC SQL)
SELECT TOP 100
SubscriberKey,
EmailAddress
FROM Subscribers
ORDER BY DateJoined DESC
-- Using DISTINCT to remove duplicates
SELECT DISTINCT
EmailDomain
FROM Subscribers
WHERE EmailDomain IS NOT NULL
-- Inner join example (only matching records)
SELECT
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName,
o.OrderID,
o.OrderDate,
o.OrderTotal
FROM Subscribers s
INNER JOIN Orders o ON s.SubscriberKey = o.CustomerID
-- Left join example (all subscribers, even those without orders)
SELECT
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName,
o.OrderID,
o.OrderDate,
o.OrderTotal
FROM Subscribers s
LEFT JOIN Orders o ON s.SubscriberKey = o.CustomerID
-- Multiple joins
SELECT
s.SubscriberKey,
s.EmailAddress,
o.OrderID,
o.OrderDate,
p.ProductName,
p.ProductPrice,
oi.Quantity
FROM Subscribers s
INNER JOIN Orders o ON s.SubscriberKey = o.CustomerID
INNER JOIN OrderItems oi ON o.OrderID = oi.OrderID
INNER JOIN Products p ON oi.ProductID = p.ProductID
-- Using WHERE with joins
SELECT
s.SubscriberKey,
s.EmailAddress,
o.OrderID,
o.OrderTotal
FROM Subscribers s
INNER JOIN Orders o ON s.SubscriberKey = o.CustomerID
WHERE o.OrderDate >= DATEADD(day, -30, GETDATE())
AND o.OrderStatus = 'Completed'
-- Count of subscribers by status
SELECT
Status,
COUNT(*) as StatusCount
FROM Subscribers
GROUP BY Status
-- Multiple aggregations
SELECT
DATEPART(year, OrderDate) as OrderYear,
DATEPART(month, OrderDate) as OrderMonth,
COUNT(DISTINCT CustomerID) as UniqueCustomers,
COUNT(*) as OrderCount,
SUM(OrderTotal) as TotalRevenue,
AVG(OrderTotal) as AverageOrderValue,
MAX(OrderTotal) as LargestOrder
FROM Orders
GROUP BY
DATEPART(year, OrderDate),
DATEPART(month, OrderDate)
ORDER BY
OrderYear DESC,
OrderMonth DESC
-- Using HAVING to filter grouped results
SELECT
ProductCategory,
COUNT(*) as ProductCount,
AVG(ProductPrice) as AveragePrice
FROM Products
GROUP BY ProductCategory
HAVING COUNT(*) > 5
AND AVG(ProductPrice) > 50
-- Grouping with joins
SELECT
s.Country,
s.State,
COUNT(DISTINCT s.SubscriberKey) as Subscribers,
COUNT(o.OrderID) as Orders,
SUM(o.OrderTotal) as Revenue
FROM Subscribers s
LEFT JOIN Orders o ON s.SubscriberKey = o.CustomerID
GROUP BY
s.Country,
s.State
ORDER BY
Revenue DESC
-- INSERT example
INSERT INTO TargetSegment (
SubscriberKey,
EmailAddress,
SegmentName,
DateAdded
)
SELECT
s.SubscriberKey,
s.EmailAddress,
'Summer Sale 2023',
GETDATE()
FROM Subscribers s
INNER JOIN Orders o ON s.SubscriberKey = o.CustomerID
WHERE
s.Status = 'Active'
AND o.OrderDate >= DATEADD(month, -3, GETDATE())
AND o.OrderTotal > 100
-- UPDATE example
UPDATE SubscriberPreferences
SET
MarketingOptIn = 1,
LastUpdated = GETDATE()
FROM SubscriberPreferences p
INNER JOIN FormSubmissions f ON p.SubscriberKey = f.SubscriberKey
WHERE
f.FormName = 'Newsletter Signup'
AND f.SubmissionDate >= DATEADD(day, -7, GETDATE())
-- DELETE example
DELETE FROM TargetSegment
WHERE DateAdded < DATEADD(month, -1, GETDATE())
AND SegmentName = 'Flash Sale April 2023'
-- UPSERT pattern (not native MERGE syntax)
SELECT
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName,
'Active' as Status,
GETDATE() as LastUpdated
INTO #TempContacts -- Create temp table
FROM NewContacts s
WHERE s.EmailAddress IS NOT NULL
-- Then insert only new records
INSERT INTO AllContacts (
SubscriberKey,
EmailAddress,
FirstName,
LastName,
Status,
LastUpdated
)
SELECT
t.SubscriberKey,
t.EmailAddress,
t.FirstName,
t.LastName,
t.Status,
t.LastUpdated
FROM #TempContacts t
LEFT JOIN AllContacts a ON t.SubscriberKey = a.SubscriberKey
WHERE a.SubscriberKey IS NULL
-- Then update existing records
UPDATE AllContacts
SET
FirstName = t.FirstName,
LastName = t.LastName,
Status = t.Status,
LastUpdated = t.LastUpdated
FROM AllContacts a
INNER JOIN #TempContacts t ON a.SubscriberKey = t.SubscriberKey
-- Current date/time
SELECT
GETDATE() as CurrentDateTime,
CONVERT(date, GETDATE()) as CurrentDate
-- Date arithmetic
SELECT
SubscriberKey,
EmailAddress,
DateJoined,
DATEDIFF(day, DateJoined, GETDATE()) as DaysSinceJoined,
DATEADD(year, 1, DateJoined) as MembershipRenewalDate
FROM Subscribers
-- Date parts
SELECT
OrderID,
OrderDate,
DATEPART(year, OrderDate) as OrderYear,
DATEPART(month, OrderDate) as OrderMonth,
DATEPART(day, OrderDate) as OrderDay,
DATEPART(hour, OrderDate) as OrderHour,
DATENAME(weekday, OrderDate) as OrderDayName,
DATENAME(month, OrderDate) as OrderMonthName
FROM Orders
-- Date filtering examples
SELECT
SubscriberKey,
EmailAddress,
LastOrderDate
FROM Subscribers
WHERE
-- Subscribers with orders in the last 30 days
LastOrderDate >= DATEADD(day, -30, GETDATE())
-- Subscribers with orders this month
OR (
DATEPART(year, LastOrderDate) = DATEPART(year, GETDATE())
AND DATEPART(month, LastOrderDate) = DATEPART(month, GETDATE())
)
-- Subscribers who joined last year
OR (
DATEPART(year, DateJoined) = DATEPART(year, DATEADD(year, -1, GETDATE()))
)
-- Date formatting
SELECT
OrderID,
OrderDate,
CONVERT(varchar, OrderDate, 101) as FormattedDate_MM_DD_YYYY,
CONVERT(varchar, OrderDate, 103) as FormattedDate_DD_MM_YYYY,
CONVERT(varchar, OrderDate, 120) as FormattedDate_YYYY_MM_DD,
CONVERT(varchar, OrderDate, 107) as FormattedDate_MonDDYYYY
FROM Orders
-- Basic string functions
SELECT
EmailAddress,
UPPER(FirstName) as UpperFirstName,
LOWER(LastName) as LowerLastName,
LEN(EmailAddress) as EmailLength,
LEFT(PostalCode, 5) as ZipCode,
RIGHT(PhoneNumber, 4) as Last4Digits,
LTRIM(RTRIM(Comments)) as TrimmedComments
FROM Subscribers
-- String concatenation
SELECT
SubscriberKey,
FirstName,
LastName,
CONCAT(FirstName, ' ', LastName) as FullName,
FirstName + ' ' + LastName as FullNameAlt,
CONCAT('Dear ', FirstName, ', thank you for your order.') as PersonalizedGreeting
FROM Subscribers
-- Substring
SELECT
EmailAddress,
CHARINDEX('@', EmailAddress) as AtPosition,
SUBSTRING(EmailAddress, 1, CHARINDEX('@', EmailAddress) - 1) as Username,
SUBSTRING(EmailAddress, CHARINDEX('@', EmailAddress) + 1, LEN(EmailAddress)) as Domain
FROM Subscribers
-- Replace
SELECT
PhoneNumber,
REPLACE(PhoneNumber, '-', '') as CleanPhone,
REPLACE(REPLACE(REPLACE(PhoneNumber, '(', ''), ')', ''), ' ', '') as VeryCleanPhone
FROM Subscribers
-- Pattern matching with LIKE
SELECT
SubscriberKey,
EmailAddress,
FirstName,
LastName
FROM Subscribers
WHERE
-- Emails from Gmail
EmailAddress LIKE '%@gmail.com'
-- Names starting with 'J'
OR FirstName LIKE 'J%'
-- Contains 'smith' anywhere in last name (case insensitive)
OR LastName LIKE '%smith%'
Common SFMC SQL Use Cases
- Customer Segmentation
- RFM Analysis
- Email Engagement
- Campaign Analytics
- Data Cleansing
-- Active customers who have purchased in the last 90 days
INSERT INTO RecentCustomerSegment (
SubscriberKey,
EmailAddress,
FirstName,
LastName,
LastOrderDate,
LastOrderAmount,
SegmentAddedDate
)
SELECT
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName,
MAX(o.OrderDate) as LastOrderDate,
MAX(CASE WHEN o.OrderDate = MAX(o.OrderDate) OVER (PARTITION BY o.CustomerID) THEN o.OrderTotal ELSE NULL END) as LastOrderAmount,
GETDATE() as SegmentAddedDate
FROM Subscribers s
INNER JOIN Orders o ON s.SubscriberKey = o.CustomerID
WHERE
s.Status = 'Active'
AND o.OrderDate >= DATEADD(day, -90, GETDATE())
AND o.OrderStatus = 'Completed'
GROUP BY
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName
-- Lapsed customers (previously purchased but not in last 180 days)
INSERT INTO LapsedCustomerSegment (
SubscriberKey,
EmailAddress,
FirstName,
LastName,
DaysSinceLastOrder,
LifetimeOrders,
LifetimeValue
)
SELECT
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName,
DATEDIFF(day, MAX(o.OrderDate), GETDATE()) as DaysSinceLastOrder,
COUNT(o.OrderID) as LifetimeOrders,
SUM(o.OrderTotal) as LifetimeValue
FROM Subscribers s
INNER JOIN Orders o ON s.SubscriberKey = o.CustomerID
WHERE
s.Status = 'Active'
AND o.OrderStatus = 'Completed'
AND NOT EXISTS (
SELECT 1 FROM Orders o2
WHERE o2.CustomerID = s.SubscriberKey
AND o2.OrderDate >= DATEADD(day, -180, GETDATE())
AND o2.OrderStatus = 'Completed'
)
GROUP BY
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName
HAVING
MAX(o.OrderDate) < DATEADD(day, -180, GETDATE())
AND MAX(o.OrderDate) >= DATEADD(day, -365, GETDATE())
-- High-value customers (top 10% by spend)
WITH CustomerSpend AS (
SELECT
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName,
SUM(o.OrderTotal) as TotalSpend,
COUNT(o.OrderID) as OrderCount,
NTILE(10) OVER (ORDER BY SUM(o.OrderTotal) DESC) as SpendPercentile
FROM Subscribers s
INNER JOIN Orders o ON s.SubscriberKey = o.CustomerID
WHERE
s.Status = 'Active'
AND o.OrderDate >= DATEADD(month, -12, GETDATE())
AND o.OrderStatus = 'Completed'
GROUP BY
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName
)
SELECT
SubscriberKey,
EmailAddress,
FirstName,
LastName,
TotalSpend,
OrderCount,
'VIP' as CustomerTier
INTO HighValueSegment
FROM CustomerSpend
WHERE SpendPercentile = 1 -- Top 10%
-- RFM (Recency, Frequency, Monetary) Analysis
-- First, calculate the raw RFM metrics for each customer
WITH CustomerRFM AS (
SELECT
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName,
DATEDIFF(day, MAX(o.OrderDate), GETDATE()) as Recency,
COUNT(o.OrderID) as Frequency,
SUM(o.OrderTotal) as Monetary,
-- Calculate quintiles for each metric (1 is best, 5 is worst)
NTILE(5) OVER (ORDER BY DATEDIFF(day, MAX(o.OrderDate), GETDATE()) ASC) as R_Score,
NTILE(5) OVER (ORDER BY COUNT(o.OrderID) DESC) as F_Score,
NTILE(5) OVER (ORDER BY SUM(o.OrderTotal) DESC) as M_Score
FROM Subscribers s
INNER JOIN Orders o ON s.SubscriberKey = o.CustomerID
WHERE
s.Status = 'Active'
AND o.OrderDate >= DATEADD(year, -2, GETDATE())
AND o.OrderStatus = 'Completed'
GROUP BY
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName
)
-- Then, calculate the combined RFM score and segment
SELECT
SubscriberKey,
EmailAddress,
FirstName,
LastName,
Recency,
Frequency,
Monetary,
R_Score,
F_Score,
M_Score,
-- Combined RFM Score (100-555)
CONCAT(R_Score, F_Score, M_Score) as RFM_Score,
-- RFM Segments
CASE
WHEN (R_Score = 1 AND F_Score = 1 AND M_Score = 1)
OR (R_Score = 2 AND F_Score = 1 AND M_Score = 1)
OR (R_Score = 1 AND F_Score = 2 AND M_Score = 1)
OR (R_Score = 1 AND F_Score = 1 AND M_Score = 2)
THEN 'Champions'
WHEN (R_Score <= 2 AND F_Score <= 3 AND M_Score <= 3)
THEN 'Loyal Customers'
WHEN (R_Score = 3 AND F_Score <= 3 AND M_Score <= 3)
THEN 'Potential Loyalists'
WHEN (R_Score = 4 AND F_Score = 1 AND M_Score = 1)
OR (R_Score = 5 AND F_Score = 1 AND M_Score = 1)
THEN 'New Customers'
WHEN (R_Score = 5 AND F_Score <= 2 AND M_Score <= 2)
THEN 'Promising'
WHEN (R_Score = 3 AND F_Score >= 3)
OR (R_Score = 4 AND F_Score >= 3)
OR (R_Score = 5 AND F_Score >= 3)
THEN 'Needs Attention'
WHEN (R_Score <= 2 AND F_Score >= 4 AND M_Score >= 4)
THEN 'At Risk'
WHEN (R_Score = 5 AND F_Score = 2 AND M_Score <= 2)
OR (R_Score = 5 AND F_Score = 1 AND M_Score >= 3)
THEN 'Can't Lose Them'
WHEN (R_Score >= 4 AND F_Score >= 4 AND M_Score >= 4)
THEN 'Hibernating'
WHEN (R_Score = 5 AND F_Score >= 3 AND M_Score >= 3)
THEN 'Lost'
ELSE 'Other'
END as CustomerSegment,
GETDATE() as AnalysisDate
INTO RFMAnalysis
FROM CustomerRFM
-- Create an engagement scoring model based on email interactions
WITH EmailEngagement AS (
SELECT
s.SubscriberKey,
s.EmailAddress,
COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.JobID END) as EmailsSent,
COUNT(DISTINCT CASE WHEN e.EventType = 'Open' THEN e.JobID END) as EmailsOpened,
COUNT(DISTINCT CASE WHEN e.EventType = 'Click' THEN e.JobID END) as EmailsClicked,
-- Calculate engagement rates
CASE
WHEN COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.JobID END) = 0 THEN 0
ELSE CAST(COUNT(DISTINCT CASE WHEN e.EventType = 'Open' THEN e.JobID END) as float) /
COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.JobID END)
END as OpenRate,
CASE
WHEN COUNT(DISTINCT CASE WHEN e.EventType = 'Open' THEN e.JobID END) = 0 THEN 0
ELSE CAST(COUNT(DISTINCT CASE WHEN e.EventType = 'Click' THEN e.JobID END) as float) /
COUNT(DISTINCT CASE WHEN e.EventType = 'Open' THEN e.JobID END)
END as ClickToOpenRate,
-- Calculate recency of engagement
MIN(CASE WHEN e.EventType IN ('Open', 'Click') THEN DATEDIFF(day, e.EventDate, GETDATE()) ELSE NULL END) as DaysSinceLastEngagement,
-- Count interactions in last 30/90 days
COUNT(DISTINCT CASE WHEN e.EventType IN ('Open', 'Click') AND e.EventDate >= DATEADD(day, -30, GETDATE()) THEN e.JobID END) as EngagementsLast30Days,
COUNT(DISTINCT CASE WHEN e.EventType IN ('Open', 'Click') AND e.EventDate >= DATEADD(day, -90, GETDATE()) THEN e.JobID END) as EngagementsLast90Days
FROM Subscribers s
LEFT JOIN EmailEvents e ON s.SubscriberKey = e.SubscriberKey
WHERE
s.Status = 'Active'
AND e.EventDate >= DATEADD(day, -180, GETDATE())
GROUP BY
s.SubscriberKey,
s.EmailAddress
)
-- Now calculate engagement scores and segments
SELECT
ee.SubscriberKey,
ee.EmailAddress,
s.FirstName,
s.LastName,
ee.EmailsSent,
ee.EmailsOpened,
ee.EmailsClicked,
ee.OpenRate,
ee.ClickToOpenRate,
ee.DaysSinceLastEngagement,
ee.EngagementsLast30Days,
ee.EngagementsLast90Days,
-- Calculate engagement score (0-100)
CASE
WHEN ee.EmailsSent = 0 THEN 0
ELSE
-- Up to 40 points for open rate
CEILING(ee.OpenRate * 40) +
-- Up to 40 points for click-to-open rate
CEILING(ee.ClickToOpenRate * 40) +
-- Up to 20 points for recency (20 if engaged in last 7 days, sliding scale down to 0 for 60+ days)
CASE
WHEN ee.DaysSinceLastEngagement IS NULL THEN 0
WHEN ee.DaysSinceLastEngagement <= 7 THEN 20
WHEN ee.DaysSinceLastEngagement <= 14 THEN 15
WHEN ee.DaysSinceLastEngagement <= 30 THEN 10
WHEN ee.DaysSinceLastEngagement <= 60 THEN 5
ELSE 0
END
END as EngagementScore,
-- Segment based on engagement score
CASE
WHEN ee.EmailsSent = 0 THEN 'No Emails'
WHEN ee.EmailsSent > 0 AND (ee.EmailsOpened = 0 AND ee.EmailsClicked = 0) THEN 'Never Engaged'
ELSE
CASE
WHEN CEILING(ee.OpenRate * 40) + CEILING(ee.ClickToOpenRate * 40) +
CASE
WHEN ee.DaysSinceLastEngagement IS NULL THEN 0
WHEN ee.DaysSinceLastEngagement <= 7 THEN 20
WHEN ee.DaysSinceLastEngagement <= 14 THEN 15
WHEN ee.DaysSinceLastEngagement <= 30 THEN 10
WHEN ee.DaysSinceLastEngagement <= 60 THEN 5
ELSE 0
END >= 75 THEN 'Highly Engaged'
WHEN CEILING(ee.OpenRate * 40) + CEILING(ee.ClickToOpenRate * 40) +
CASE
WHEN ee.DaysSinceLastEngagement IS NULL THEN 0
WHEN ee.DaysSinceLastEngagement <= 7 THEN 20
WHEN ee.DaysSinceLastEngagement <= 14 THEN 15
WHEN ee.DaysSinceLastEngagement <= 30 THEN 10
WHEN ee.DaysSinceLastEngagement <= 60 THEN 5
ELSE 0
END >= 40 THEN 'Moderately Engaged'
ELSE 'Minimally Engaged'
END
END as EngagementSegment
INTO EmailEngagementScoring
FROM EmailEngagement ee
INNER JOIN Subscribers s ON ee.SubscriberKey = s.SubscriberKey
-- Find subscribers who haven't engaged in 90+ days for re-engagement campaign
SELECT
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName,
MAX(CASE WHEN e.EventType IN ('Open', 'Click') THEN e.EventDate ELSE NULL END) as LastEngagementDate,
DATEDIFF(day, MAX(CASE WHEN e.EventType IN ('Open', 'Click') THEN e.EventDate ELSE NULL END), GETDATE()) as DaysSinceEngagement,
COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.JobID END) as EmailsSent
INTO ReengagementCandidates
FROM Subscribers s
LEFT JOIN EmailEvents e ON s.SubscriberKey = e.SubscriberKey
WHERE
s.Status = 'Active'
GROUP BY
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName
HAVING
(MAX(CASE WHEN e.EventType IN ('Open', 'Click') THEN e.EventDate ELSE NULL END) IS NULL OR
MAX(CASE WHEN e.EventType IN ('Open', 'Click') THEN e.EventDate ELSE NULL END) < DATEADD(day, -90, GETDATE()))
AND COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.JobID END) >= 5
-- Email campaign performance summary
SELECT
c.JobID,
c.CampaignName,
c.Subject,
c.SendDate,
c.SenderProfile,
COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.SubscriberKey END) as TotalSent,
COUNT(DISTINCT CASE WHEN e.EventType = 'Bounce' THEN e.SubscriberKey END) as TotalBounces,
COUNT(DISTINCT CASE WHEN e.EventType = 'Open' THEN e.SubscriberKey END) as UniqueOpens,
COUNT(DISTINCT CASE WHEN e.EventType = 'Click' THEN e.SubscriberKey END) as UniqueClicks,
COUNT(DISTINCT CASE WHEN e.EventType = 'Unsubscribe' THEN e.SubscriberKey END) as Unsubscribes,
-- Calculate rates
CASE
WHEN COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.SubscriberKey END) = 0 THEN 0
ELSE CAST(COUNT(DISTINCT CASE WHEN e.EventType = 'Bounce' THEN e.SubscriberKey END) as float) /
COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.SubscriberKey END) * 100
END as BounceRate,
CASE
WHEN COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.SubscriberKey END) -
COUNT(DISTINCT CASE WHEN e.EventType = 'Bounce' THEN e.SubscriberKey END) = 0 THEN 0
ELSE CAST(COUNT(DISTINCT CASE WHEN e.EventType = 'Open' THEN e.SubscriberKey END) as float) /
(COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.SubscriberKey END) -
COUNT(DISTINCT CASE WHEN e.EventType = 'Bounce' THEN e.SubscriberKey END)) * 100
END as OpenRate,
CASE
WHEN COUNT(DISTINCT CASE WHEN e.EventType = 'Open' THEN e.SubscriberKey END) = 0 THEN 0
ELSE CAST(COUNT(DISTINCT CASE WHEN e.EventType = 'Click' THEN e.SubscriberKey END) as float) /
COUNT(DISTINCT CASE WHEN e.EventType = 'Open' THEN e.SubscriberKey END) * 100
END as ClickToOpenRate,
CASE
WHEN COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.SubscriberKey END) -
COUNT(DISTINCT CASE WHEN e.EventType = 'Bounce' THEN e.SubscriberKey END) = 0 THEN 0
ELSE CAST(COUNT(DISTINCT CASE WHEN e.EventType = 'Unsubscribe' THEN e.SubscriberKey END) as float) /
(COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.SubscriberKey END) -
COUNT(DISTINCT CASE WHEN e.EventType = 'Bounce' THEN e.SubscriberKey END)) * 100
END as UnsubscribeRate
INTO CampaignPerformanceSummary
FROM Campaigns c
LEFT JOIN EmailEvents e ON c.JobID = e.JobID
WHERE
c.SendDate >= DATEADD(month, -3, GETDATE())
GROUP BY
c.JobID,
c.CampaignName,
c.Subject,
c.SendDate,
c.SenderProfile
ORDER BY
c.SendDate DESC
-- Click-level analysis for URL performance
SELECT
c.JobID,
c.CampaignName,
c.SendDate,
e.URL,
e.LinkName,
COUNT(*) as ClickCount,
COUNT(DISTINCT e.SubscriberKey) as UniqueClickers,
-- Calculate click rate for this URL
CASE
WHEN (SELECT COUNT(DISTINCT SubscriberKey) FROM EmailEvents
WHERE JobID = c.JobID AND EventType = 'Open') = 0 THEN 0
ELSE CAST(COUNT(DISTINCT e.SubscriberKey) as float) /
(SELECT COUNT(DISTINCT SubscriberKey) FROM EmailEvents
WHERE JobID = c.JobID AND EventType = 'Open') * 100
END as ClickRateOfOpeners
INTO URLPerformanceAnalysis
FROM Campaigns c
INNER JOIN EmailEvents e ON c.JobID = e.JobID
WHERE
e.EventType = 'Click'
AND c.SendDate >= DATEADD(month, -3, GETDATE())
GROUP BY
c.JobID,
c.CampaignName,
c.SendDate,
e.URL,
e.LinkName
ORDER BY
c.SendDate DESC,
ClickCount DESC
-- Conversion tracking (assuming order data is connected to email clicks)
SELECT
c.JobID,
c.CampaignName,
c.Subject,
c.SendDate,
COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.SubscriberKey END) as TotalSent,
COUNT(DISTINCT CASE WHEN e.EventType = 'Open' THEN e.SubscriberKey END) as UniqueOpens,
COUNT(DISTINCT CASE WHEN e.EventType = 'Click' THEN e.SubscriberKey END) as UniqueClicks,
COUNT(DISTINCT o.OrderID) as Conversions,
SUM(o.OrderTotal) as Revenue,
-- Calculate conversion rates and ROI
CASE
WHEN COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.SubscriberKey END) = 0 THEN 0
ELSE CAST(COUNT(DISTINCT o.OrderID) as float) /
COUNT(DISTINCT CASE WHEN e.EventType = 'Sent' THEN e.SubscriberKey END) * 100
END as ConversionRate,
CASE
WHEN c.CampaignCost = 0 THEN 0
ELSE (SUM(o.OrderTotal) - c.CampaignCost) / c.CampaignCost * 100
END as ROI
INTO CampaignConversionAnalysis
FROM Campaigns c
LEFT JOIN EmailEvents e ON c.JobID = e.JobID
LEFT JOIN Orders o ON e.SubscriberKey = o.CustomerID
AND o.OrderDate BETWEEN c.SendDate AND DATEADD(day, 7, c.SendDate)
AND o.UTMSource = 'email'
AND o.UTMMedium = 'email'
AND o.UTMCampaign = c.CampaignName
WHERE
c.SendDate >= DATEADD(month, -3, GETDATE())
GROUP BY
c.JobID,
c.CampaignName,
c.Subject,
c.SendDate,
c.CampaignCost
ORDER BY
c.SendDate DESC
-- Standardize and clean contact data
SELECT
s.SubscriberKey,
s.EmailAddress,
-- Standardize name casing
CASE
WHEN s.FirstName IS NULL THEN NULL
ELSE UPPER(LEFT(LTRIM(s.FirstName), 1)) +
LOWER(SUBSTRING(LTRIM(s.FirstName), 2, LEN(LTRIM(s.FirstName))))
END as FirstName,
CASE
WHEN s.LastName IS NULL THEN NULL
ELSE UPPER(LEFT(LTRIM(s.LastName), 1)) +
LOWER(SUBSTRING(LTRIM(s.LastName), 2, LEN(LTRIM(s.LastName))))
END as LastName,
-- Standardize phone numbers
CASE
WHEN s.PhoneNumber IS NULL THEN NULL
ELSE REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
s.PhoneNumber, ' ', ''), '-', ''), '(', ''), ')', ''), '.', '')
END as CleanPhoneNumber,
-- Format as (XXX) XXX-XXXX if 10 digits
CASE
WHEN s.PhoneNumber IS NULL THEN NULL
WHEN LEN(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
s.PhoneNumber, ' ', ''), '-', ''), '(', ''), ')', ''), '.', '')) = 10
THEN '(' + SUBSTRING(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
s.PhoneNumber, ' ', ''), '-', ''), '(', ''), ')', ''), '.', ''), 1, 3) + ') ' +
SUBSTRING(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
s.PhoneNumber, ' ', ''), '-', ''), '(', ''), ')', ''), '.', ''), 4, 3) + '-' +
SUBSTRING(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
s.PhoneNumber, ' ', ''), '-', ''), '(', ''), ')', ''), '.', ''), 7, 4)
ELSE REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
s.PhoneNumber, ' ', ''), '-', ''), '(', ''), ')', ''), '.', '')
END as FormattedPhoneNumber,
-- Extract email domain
CASE
WHEN s.EmailAddress IS NULL THEN NULL
WHEN CHARINDEX('@', s.EmailAddress) = 0 THEN NULL
ELSE LOWER(SUBSTRING(s.EmailAddress, CHARINDEX('@', s.EmailAddress) + 1,
LEN(s.EmailAddress) - CHARINDEX('@', s.EmailAddress)))
END as EmailDomain,
-- Standardize address fields
UPPER(s.State) as State,
-- Standardize postal code format
CASE
WHEN s.PostalCode IS NULL THEN NULL
WHEN LEN(REPLACE(s.PostalCode, ' ', '')) = 5 THEN REPLACE(s.PostalCode, ' ', '')
WHEN LEN(REPLACE(s.PostalCode, ' ', '')) = 9 AND CHARINDEX('-', s.PostalCode) = 6
THEN REPLACE(s.PostalCode, ' ', '')
WHEN LEN(REPLACE(s.PostalCode, ' ', '')) = 9 AND CHARINDEX('-', s.PostalCode) = 0
THEN SUBSTRING(REPLACE(s.PostalCode, ' ', ''), 1, 5) + '-' +
SUBSTRING(REPLACE(s.PostalCode, ' ', ''), 6, 4)
ELSE REPLACE(s.PostalCode, ' ', '')
END as FormattedPostalCode
INTO CleanedSubscribers
FROM Subscribers s
-- Find and mark potential duplicate subscribers
WITH EmailGroups AS (
SELECT
LOWER(EmailAddress) as LowercaseEmail,
COUNT(*) as EmailCount
FROM Subscribers
GROUP BY LOWER(EmailAddress)
HAVING COUNT(*) > 1
),
RankedDuplicates AS (
SELECT
s.SubscriberKey,
s.EmailAddress,
s.FirstName,
s.LastName,
s.Status,
s.DateCreated,
s.LastModifiedDate,
ROW_NUMBER() OVER (PARTITION BY LOWER(s.EmailAddress)
ORDER BY
CASE WHEN s.Status = 'Active' THEN 0 ELSE 1 END,
s.LastModifiedDate DESC,
s.DateCreated DESC) as RowRank
FROM Subscribers s
INNER JOIN EmailGroups eg ON LOWER(s.EmailAddress) = eg.LowercaseEmail
)
SELECT
SubscriberKey,
EmailAddress,
FirstName,
LastName,
Status,
DateCreated,
LastModifiedDate,
CASE
WHEN RowRank = 1 THEN 'Primary'
ELSE 'Duplicate'
END as DuplicateStatus
INTO DuplicateSubscribers
FROM RankedDuplicates
ORDER BY LOWER(EmailAddress), RowRank
Summary
AMPScript and SFMC SQL are powerful tools that work together to make your email marketing more personalized, relevant, and data-driven:
- AMPScript is like a "personalization engine" that transforms standard emails into tailored experiences for each recipient. It works at the moment an email is sent or a page is loaded, inserting the right content for each person based on what you know about them. Think of it as a sophisticated mail merge on steroids, capable of showing different products, offers, or messages depending on customer attributes and behaviors.
- SFMC SQL is your "data organizer" that helps you make sense of customer information before you even send emails. It lets you filter, combine, and analyze your data to create precisely targeted audience segments, clean up messy information, calculate new insights, and prepare data for your campaigns.
When used together, these tools create a powerful combination: SQL helps you identify exactly who should receive what type of message, while AMPScript ensures that when those messages arrive, they contain content perfectly tailored to each recipient. This combination leads to more effective email marketing with higher engagement, better conversion rates, and stronger customer relationships built on relevance and value.
Additional Resources
- AMPScript Guide - Official Salesforce documentation
- AMPScript.com - Community-driven AMPScript resource
- SFMC SQL Query Activity - Official query activity documentation
- Marketing Cloud SQL Guide - Practical guide to SQL in Marketing Cloud
- AMPScript and SFMC SQL Slack Community - Connect with other Marketing Cloud developers