Summarizing Subscriber Activity

Here’s a scalable method I’ve used to aggregate subscriber-level engagement data. I’ve used the results of this data to populate high/low engagement audiences. I will typically drop this in as soon as I can — just to get the data accumulating. It’s intended to run daily.

Automation

Subscriber_Activity_Summary

Step 1 – SQL Query: Subscriber_Activity_Summary_Status

Step 2 – SQL Query: Subscriber_Activity_Summary_Sends

Step 3 – SQL Query: Subscriber_Activity_Summary_Opens

Step 4 – SQL Query: Subscriber_Activity_Summary_Clicks

Step 5 – SQL Query: Subscriber_Activity_Summary_MostRecentInteraction

Data Extensions

Subscriber_Activity_Summary

Name Data Type Len PK Reqโ€™d Default
SubscriberId Number 0 true true
EmailAddress EmailAddress 254 false false
SubscriberKey Text 254 false false
Status Text 15 false false
SendCount Number false false 0
LastSend Date false false
OpenCount Number false false 0
LastOpen Date false false
ClickCount Number false false 0
LastClick Date false false
mostRecentInteraction Date false false
InsertedDate Date false false GetDate()

Queries

Subscriber_Activity_Summary_Status

NOTE: Add the ent. prefix on _Subscribers if you’re in a child BU.

select
  s.subscriberid
, s.subscriberkey
, s.emailAddress
, s.status
from _Subscribers s
/* name: Subscriber_Activity_Summary_Status */
/* target: Subscriber_Activity_Summary */
/* action: update */

Subscriber_Activity_Summary_Sends

select
  x.subscriberid
, x.subscriberkey
, isnull(sas.SendCount,0) + x.SendCount SendCount
, x.LastSend
from (

    select
      y.subscriberid
    , y.subscriberkey
    , count(*) SendCount
    , max(y.eventDate) LastSend
    from (
        select
          a.subscriberid
        , a.subscriberkey
        , a.eventDate
        from _Sent a
        where a.EventDate >= convert(date, getDate()-1)
        and a.EventDate < convert(date, getDate())
    ) y
    group by
    y.subscriberid
    , y.subscriberkey

) x
left join Subscriber_Activity_Summary sas on sas.subscriberid = x.subscriberid
where
sas.subscriberid is null
or x.lastSend > isnull(sas.lastSend,'1980-01-01')
/* name: Subscriber_Activity_Summary_Sends */
/* target: Subscriber_Activity_Summary */
/* action: update */

Subscriber_Activity_Summary_Opens

select
x.subscriberid
, x.subscriberkey
, isnull(sas.OpenCount,0) + x.OpenCount OpenCount
, x.LastOpen
from (

    select
      y.subscriberid
    , y.subscriberkey
    , count(*) OpenCount
    , max(y.eventDate) LastOpen
    from (
        select
          a.subscriberid
        , a.subscriberkey
        , a.eventDate
        from _Open a
        where a.EventDate >= convert(date, getDate()-1)
        and a.EventDate < convert(date, getDate())
    ) y
    group by
    y.subscriberid
    , y.subscriberkey

) x
left join Subscriber_Activity_Summary sas on sas.subscriberid = x.subscriberid
where
sas.subscriberid is null
or x.lastOpen > isnull(sas.lastOpen,'1980-01-01')
/* name: Subscriber_Activity_Summary_Opens */
/* target: Subscriber_Activity_Summary */
/* action: update */

Subscriber_Activity_Summary_Clicks

select
x.subscriberid
, x.subscriberkey
, isnull(sas.ClickCount,0) + x.ClickCount ClickCount
, x.LastClick
from (

    select
      y.subscriberid
    , y.subscriberkey
    , count(*) ClickCount
    , max(y.eventDate) LastClick
    from (
        select
          a.subscriberid
        , a.subscriberkey
        , a.eventDate
        from _Click a
        where a.EventDate >= convert(date, getDate()-1)
        and a.EventDate < convert(date, getDate())
    ) y
    group by
    y.subscriberid
    , y.subscriberkey

) x
left join Subscriber_Activity_Summary sas on sas.subscriberid = x.subscriberid
where
sas.subscriberid is null
or x.lastClick > isnull(sas.lastClick,'1980-01-01')
/* name: Subscriber_Activity_Summary_Clicks */
/* target: Subscriber_Activity_Summary */
/* action: update */

Subscriber_Activity_Summary_MostRecentInteraction

select
  a.subscriberid
, a.subscriberkey
, case
      when a.lastOpen > coalesce(a.lastClick, convert(datetime, '1980-01-01')) then a.lastOpen
      else a.lastClick
  end mostRecentInteraction
from Subscriber_Activity_Summary a
where a.lastOpen is not null
or a.lastClick is not null
/* name: Subscriber_Activity_Summary_MostRecentInteraction */
/* target: Subscriber_Activity_Summary */
/* action: update */

Journey Email and Job Queries

Journey Emails

This comes up a lot — trouble joining the _Journey and _JourneyActivity data views. The issue is that the VersionID is a UniqueIdentifier data type behind the scenes. The workaround is to cast/convert it to a varchar(36):

Journey Jobs

I like to maintain a list of Journey _Job records beyond the 6 month lookback threshold for the data views. You can apply the same VersionID join trick on this one also:

Determining Business Unit Subscriber Status

Business unit subscriber status (or business unit membership) has been one of those frustrating and elusive things in SFMC for a long time. Since the platform only stores a business unit-level status for a subscriber if they’ve been sent an email from the child business unit, the various sources of this status in the platform are not accurate.

It wasn’t until Salesforce introduced the _BusinessUnitUnsubscribes System Data View in 2019 that there was, in my opinion, an accurate source of truth for this data.

Here are my go-to queries to arrive at this data set. I’d suggest scheduling a daily Automation with these queries in the parent business unit.

Queries

All Subscribers in all business units

This query creates a row for every subscriber in every business unit. Since subscribers exist at the enterprise level, they could potentially have a send (or unsubscribe) in any child business unit.

You’ll need to update this query to add your own MIDs and business unit names.

Unsubscribes per business unit

This one sets the status for the subscriber if an explicit unsubscribe in a business unit has been recorded.

You’ll need to update the CASE statement for your own set of MIDs and business unit names.
Unsubscribes are the only deliverability status recorded at the business unit level.

Caveats

Due to the dependencies on the System Data Views, if you have more than a few million subscribers, you’ll be hard-pressed to keep these queries running under the 30-minute timeout window. In other words, this solution doesn’t scale very well if you have a large subscriber base.

All of this assumes that your business units are configured to limit unsubscribes to the business unit level (i.e. The “Subscribers will be unsubscribed from this BU only” option is selected in Setup > Business units > Unsubscribes settings).

Uses

The business unit subscriber status data is helpful in a few areas in SFMC.

Data Integrations

If Marketing Cloud is not your database of record for deliverability and status by business unit is a key data point, this can be a good source to extract and load into an external data warehouse.

Segmentation

You can use this data as a source for suppressing opt-outs so your pre-send counts more accurately reflect actual send counts.

Reporting

If you have any reporting needs relating to business unit membership and cross-over you can utilize this data source. I don’t recommend treating SFMC like a reporting platform, but the flexibility of Query Activities and Data Extensions can’t be denied.

Further reading

Journey Logging using Update Contact

One of the appeals of Journey Builder is that it’s visual — you can see subscribers flow through the branches and channels.

What if you need more information about those decision splits and messages across channels? Your options are limited to what’s in the built-in tracking pages. The _Journey and _JourneyBuilderActivity data views offer some insights on email-related activities, but there’s not much beyond that.

The Update Contact activity in Journey Builder might just be the go-to solution. You can supplement the base reporting using Update Contact activities to log activity by subscriber as they traverse your journey.

NOTE: Once you start writing rows to Data Extensions using Update Contact you might not realize that it doesn’t respect the primary keys of your Data Extension. Update Contact will update any row that has a Subscriber Key match, even if the primary key is Subscriber Key plus something else.ย  There’s hope, however — even with this limitation of the Update Contact activity.

Here’s a simple solution utilizes an Automation to sweep events to a centralized Journey logging Data Extension.
Simply create these two Data Extensions, add one in Contact Builder and then create a Query in an Automation that runs every hour.

Journey_Builder_Status Data Extension

The Update Contact activity writes to this Data Extension.

  • SubscriberKey, Text (254), Primary Key
  • Journey, Text (100), nullable
  • Event, Text (100), nullable
  • EventDate, Date, nullable, defaulted to the current date

It will need to be mapped Subscriber Key to Contact Key in Contact Builder — one-to-one.

Journey_Builder_Logย Data Extension

This is the final destination of the Journey log data.

  • SubscriberKey, Text (254), Primary Key
  • EventDate, Date, Primary Key
  • Journey, Text (100), nullable
  • Event, Text (100), nullable

Journey_Builder_Logย Query Activity

This sweeps rows from Journey_Builder_Status to Journey_Builder_Log.

Journey_Builder_Logย Automation

Runs hourly, ends never.

  • Step 1, Query Activity: Journey_Builder_Log

Once that’s done, drag as many Update Contact Activities that you need to your Journey canvas for recording events. In those activities, specify these field values: Event, EventDate and Journey.

This works for tracking entry and exit, depending on where you place the Update Contact activities.