Customer Satisfaction Score (CSAT)

Measures customer satisfaction with specific interactions, products, or services

Overview

Customer Satisfaction Score (CSAT) measures how satisfied customers are with a specific interaction, product, service, or overall experience. Unlike NPS which measures loyalty and likelihood to recommend, CSAT focuses on immediate satisfaction levels using simple rating scales.

CSAT is typically collected through post-interaction surveys asking "How satisfied were you with...?" and provides actionable insights for improving specific aspects of the customer experience in real-time.

Formula:
CSAT = (Number of Satisfied Customers / Total Survey Responses) × 100

5-Point Scale

Very Dissatisfied → Very Satisfied

Most common format

7-Point Scale

1 (Lowest) → 7 (Highest)

More granular responses

10-Point Scale

1 (Poor) → 10 (Excellent)

Maximum differentiation

85-100%

Excellent

75-84%

Good

65-74%

Average

<65%

Needs Improvement

Timing Strategy

CSAT is most effective when measured immediately after specific interactions (support tickets, purchases, onboarding) while the experience is fresh in customers' minds.

Why It Matters

  • Immediate Feedback: Captures real-time satisfaction with specific interactions
  • Process Improvement: Identifies specific areas needing attention
  • Team Performance: Measures effectiveness of support and service teams
  • Customer Retention: High CSAT correlates with lower churn rates
  • Product Development: Guides feature and service improvements
  • Competitive Advantage: Superior satisfaction drives market differentiation

How to Measure It

Calculate CSAT by surveying customers after specific interactions using rating scales, then analyzing satisfaction percentages, trends, and drivers across different touchpoints and segments.

Basic CSAT Calculation

-- Calculate Customer Satisfaction Score across different touchpoints
WITH csat_responses AS (
  SELECT 
    survey_id,
    customer_id,
    survey_date,
    interaction_type,
    csat_score,
    survey_channel,
    customer_segment,
    subscription_tier,
    support_agent_id,
    CASE 
      WHEN csat_score >= 4 THEN 1 ELSE 0  -- 5-point scale: 4-5 = satisfied
    END as is_satisfied,
    CASE 
      WHEN csat_score >= 4 THEN 'Satisfied'
      WHEN csat_score = 3 THEN 'Neutral'
      ELSE 'Dissatisfied'
    END as satisfaction_category,
    DATE_TRUNC('month', survey_date) as survey_month,
    DATE_TRUNC('week', survey_date) as survey_week
  FROM customer_csat_surveys
  WHERE survey_date >= CURRENT_DATE - INTERVAL '12 months'
    AND csat_score IS NOT NULL
    AND csat_score BETWEEN 1 AND 5
),
csat_by_interaction AS (
  SELECT 
    interaction_type,
    survey_month,
    COUNT(*) as total_responses,
    SUM(is_satisfied) as satisfied_responses,
    ROUND(100.0 * SUM(is_satisfied) / COUNT(*), 1) as csat_percentage,
    ROUND(AVG(csat_score), 2) as avg_csat_score,
    ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY csat_score), 1) as median_csat_score,
    COUNT(CASE WHEN satisfaction_category = 'Satisfied' THEN 1 END) as satisfied_count,
    COUNT(CASE WHEN satisfaction_category = 'Neutral' THEN 1 END) as neutral_count,
    COUNT(CASE WHEN satisfaction_category = 'Dissatisfied' THEN 1 END) as dissatisfied_count
  FROM csat_responses
  GROUP BY 1, 2
),
csat_trends AS (
  SELECT 
    interaction_type,
    survey_month,
    total_responses,
    csat_percentage,
    avg_csat_score,
    LAG(csat_percentage) OVER (
      PARTITION BY interaction_type 
      ORDER BY survey_month
    ) as prev_month_csat,
    ROUND(csat_percentage - LAG(csat_percentage) OVER (
      PARTITION BY interaction_type 
      ORDER BY survey_month
    ), 1) as csat_change,
    satisfied_count,
    neutral_count,
    dissatisfied_count
  FROM csat_by_interaction
  WHERE total_responses >= 20  -- Minimum sample size
)
SELECT 
  interaction_type,
  survey_month,
  total_responses,
  csat_percentage,
  avg_csat_score,
  median_csat_score,
  prev_month_csat,
  csat_change,
  satisfied_count,
  neutral_count,
  dissatisfied_count,
  CASE 
    WHEN csat_percentage >= 85 THEN 'Excellent'
    WHEN csat_percentage >= 75 THEN 'Good'
    WHEN csat_percentage >= 65 THEN 'Average'
    ELSE 'Needs Improvement'
  END as csat_rating,
  RANK() OVER (PARTITION BY survey_month ORDER BY csat_percentage DESC) as monthly_rank
FROM csat_trends
ORDER BY survey_month DESC, csat_percentage DESC;

CSAT Driver Analysis

-- Analyze factors that drive customer satisfaction scores
WITH interaction_details AS (
  SELECT 
    cr.survey_id,
    cr.customer_id,
    cr.csat_score,
    cr.satisfaction_category,
    cr.interaction_type,
    cr.support_agent_id,
    cr.survey_date,
    -- Support ticket details
    st.ticket_id,
    st.issue_category,
    st.priority,
    st.resolution_time_hours,
    st.first_response_time_minutes,
    st.escalation_count,
    -- Customer context
    c.subscription_tier,
    c.account_age_months,
    c.industry,
    c.company_size,
    -- Agent performance
    sa.experience_level,
    sa.department
  FROM csat_responses cr
  LEFT JOIN support_tickets st ON cr.survey_id = st.survey_id
  LEFT JOIN customers c ON cr.customer_id = c.customer_id
  LEFT JOIN support_agents sa ON cr.support_agent_id = sa.agent_id
  WHERE cr.interaction_type = 'support_ticket'
),
satisfaction_correlations AS (
  SELECT 
    'Resolution Time' as factor,
    CORR(csat_score, -resolution_time_hours) as correlation,  -- Negative because faster = better
    ROUND(AVG(CASE WHEN satisfaction_category = 'Satisfied' THEN resolution_time_hours END), 1) as satisfied_avg,
    ROUND(AVG(CASE WHEN satisfaction_category = 'Dissatisfied' THEN resolution_time_hours END), 1) as dissatisfied_avg
  FROM interaction_details
  WHERE resolution_time_hours IS NOT NULL
  
  UNION ALL
  
  SELECT 
    'First Response Time' as factor,
    CORR(csat_score, -first_response_time_minutes) as correlation,
    ROUND(AVG(CASE WHEN satisfaction_category = 'Satisfied' THEN first_response_time_minutes END), 1) as satisfied_avg,
    ROUND(AVG(CASE WHEN satisfaction_category = 'Dissatisfied' THEN first_response_time_minutes END), 1) as dissatisfied_avg
  FROM interaction_details
  WHERE first_response_time_minutes IS NOT NULL
  
  UNION ALL
  
  SELECT 
    'Account Age (Months)' as factor,
    CORR(csat_score, account_age_months) as correlation,
    ROUND(AVG(CASE WHEN satisfaction_category = 'Satisfied' THEN account_age_months END), 1) as satisfied_avg,
    ROUND(AVG(CASE WHEN satisfaction_category = 'Dissatisfied' THEN account_age_months END), 1) as dissatisfied_avg
  FROM interaction_details
  WHERE account_age_months IS NOT NULL
),
categorical_analysis AS (
  SELECT 
    issue_category,
    COUNT(*) as total_tickets,
    ROUND(100.0 * SUM(CASE WHEN satisfaction_category = 'Satisfied' THEN 1 ELSE 0 END) / COUNT(*), 1) as satisfaction_rate,
    ROUND(AVG(csat_score), 2) as avg_csat_score,
    ROUND(AVG(resolution_time_hours), 1) as avg_resolution_time
  FROM interaction_details
  WHERE issue_category IS NOT NULL
  GROUP BY issue_category
  HAVING COUNT(*) >= 10
  
  UNION ALL
  
  SELECT 
    CONCAT('Priority: ', priority) as issue_category,
    COUNT(*) as total_tickets,
    ROUND(100.0 * SUM(CASE WHEN satisfaction_category = 'Satisfied' THEN 1 ELSE 0 END) / COUNT(*), 1) as satisfaction_rate,
    ROUND(AVG(csat_score), 2) as avg_csat_score,
    ROUND(AVG(resolution_time_hours), 1) as avg_resolution_time
  FROM interaction_details
  WHERE priority IS NOT NULL
  GROUP BY priority
  HAVING COUNT(*) >= 10
  
  UNION ALL
  
  SELECT 
    CONCAT('Agent Level: ', experience_level) as issue_category,
    COUNT(*) as total_tickets,
    ROUND(100.0 * SUM(CASE WHEN satisfaction_category = 'Satisfied' THEN 1 ELSE 0 END) / COUNT(*), 1) as satisfaction_rate,
    ROUND(AVG(csat_score), 2) as avg_csat_score,
    ROUND(AVG(resolution_time_hours), 1) as avg_resolution_time
  FROM interaction_details
  WHERE experience_level IS NOT NULL
  GROUP BY experience_level
  HAVING COUNT(*) >= 10
)
SELECT 
  factor,
  ROUND(correlation, 3) as correlation_coefficient,
  satisfied_avg,
  dissatisfied_avg,
  ROUND(satisfied_avg - dissatisfied_avg, 1) as difference,
  CASE 
    WHEN ABS(correlation) >= 0.3 THEN 'Strong'
    WHEN ABS(correlation) >= 0.1 THEN 'Moderate'
    ELSE 'Weak'
  END as correlation_strength
FROM satisfaction_correlations
WHERE correlation IS NOT NULL
ORDER BY ABS(correlation) DESC;

Agent Performance Analysis

-- Analyze CSAT performance by support agents and teams
WITH agent_performance AS (
  SELECT 
    sa.agent_id,
    sa.name as agent_name,
    sa.department,
    sa.experience_level,
    DATE_TRUNC('month', cr.survey_date) as performance_month,
    COUNT(*) as total_surveys,
    SUM(cr.is_satisfied) as satisfied_responses,
    ROUND(100.0 * SUM(cr.is_satisfied) / COUNT(*), 1) as agent_csat_percentage,
    ROUND(AVG(cr.csat_score), 2) as avg_csat_score,
    -- Operational metrics
    ROUND(AVG(st.resolution_time_hours), 1) as avg_resolution_time,
    ROUND(AVG(st.first_response_time_minutes), 1) as avg_first_response_time,
    COUNT(CASE WHEN st.escalation_count > 0 THEN 1 END) as escalated_tickets,
    ROUND(100.0 * COUNT(CASE WHEN st.escalation_count > 0 THEN 1 END) / COUNT(*), 1) as escalation_rate
  FROM csat_responses cr
  JOIN support_agents sa ON cr.support_agent_id = sa.agent_id
  LEFT JOIN support_tickets st ON cr.survey_id = st.survey_id
  WHERE cr.interaction_type = 'support_ticket'
    AND cr.survey_date >= CURRENT_DATE - INTERVAL '6 months'
  GROUP BY 1, 2, 3, 4, 5
  HAVING COUNT(*) >= 10  -- Minimum surveys for statistical relevance
),
department_benchmarks AS (
  SELECT 
    department,
    performance_month,
    COUNT(*) as agents_count,
    ROUND(AVG(agent_csat_percentage), 1) as dept_avg_csat,
    ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY agent_csat_percentage), 1) as dept_median_csat,
    ROUND(STDDEV(agent_csat_percentage), 1) as dept_csat_stddev,
    MAX(agent_csat_percentage) as dept_max_csat,
    MIN(agent_csat_percentage) as dept_min_csat
  FROM agent_performance
  GROUP BY 1, 2
),
agent_rankings AS (
  SELECT 
    ap.*,
    db.dept_avg_csat,
    db.dept_median_csat,
    ROUND(ap.agent_csat_percentage - db.dept_avg_csat, 1) as csat_vs_dept_avg,
    CASE 
      WHEN ap.agent_csat_percentage >= db.dept_avg_csat + db.dept_csat_stddev THEN 'Top Performer'
      WHEN ap.agent_csat_percentage <= db.dept_avg_csat - db.dept_csat_stddev THEN 'Needs Improvement'
      ELSE 'Average Performer'
    END as performance_tier,
    RANK() OVER (PARTITION BY ap.department, ap.performance_month ORDER BY ap.agent_csat_percentage DESC) as dept_rank,
    RANK() OVER (PARTITION BY ap.performance_month ORDER BY ap.agent_csat_percentage DESC) as company_rank
  FROM agent_performance ap
  JOIN department_benchmarks db ON ap.department = db.department AND ap.performance_month = db.performance_month
),
improvement_opportunities AS (
  SELECT 
    agent_id,
    agent_name,
    department,
    performance_month,
    agent_csat_percentage,
    avg_resolution_time,
    avg_first_response_time,
    escalation_rate,
    CASE 
      WHEN avg_resolution_time > (SELECT PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY avg_resolution_time) 
                                  FROM agent_performance 
                                  WHERE performance_month = ar.performance_month) THEN 'Slow Resolution'
      WHEN avg_first_response_time > (SELECT PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY avg_first_response_time) 
                                      FROM agent_performance 
                                      WHERE performance_month = ar.performance_month) THEN 'Slow Response'
      WHEN escalation_rate > (SELECT PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY escalation_rate) 
                              FROM agent_performance 
                              WHERE performance_month = ar.performance_month) THEN 'High Escalations'
      ELSE 'Process Optimization'
    END as primary_improvement_area
  FROM agent_rankings ar
  WHERE performance_tier = 'Needs Improvement'
)
SELECT 
  ar.performance_month,
  ar.agent_name,
  ar.department,
  ar.experience_level,
  ar.total_surveys,
  ar.agent_csat_percentage,
  ar.avg_csat_score,
  ar.dept_avg_csat,
  ar.csat_vs_dept_avg,
  ar.performance_tier,
  ar.dept_rank,
  ar.avg_resolution_time,
  ar.escalation_rate,
  io.primary_improvement_area
FROM agent_rankings ar
LEFT JOIN improvement_opportunities io ON ar.agent_id = io.agent_id AND ar.performance_month = io.performance_month
WHERE ar.performance_month >= DATE_TRUNC('month', CURRENT_DATE - INTERVAL '3 months')
ORDER BY ar.performance_month DESC, ar.agent_csat_percentage DESC;

Survey Best Practices

Keep CSAT surveys short and specific, use consistent scales across touchpoints, send surveys immediately after interactions, and ensure high response rates through multiple channels and follow-ups.

Best Practices

1. Survey Design

  • Use consistent rating scales across all touchpoints
  • Keep surveys short with 1-3 questions maximum
  • Include open-ended follow-up questions for context
  • Make surveys mobile-friendly and accessible

2. Timing & Frequency

  • Send surveys immediately after interactions
  • Avoid survey fatigue with appropriate frequency limits
  • Use triggered surveys based on specific events
  • Consider customer journey stage when surveying

3. Response Collection

  • Multiple Channels: Email, in-app, SMS, phone
  • Response Incentives: Offer value for participation
  • Follow-up Strategy: Gentle reminders for non-responders
  • Representative Sampling: Ensure all segments are covered

4. Analysis & Action

  • Segment results by customer type and interaction
  • Identify specific drivers of satisfaction/dissatisfaction
  • Track trends over time to measure improvements
  • Close the loop with customers who provide feedback

5. Organizational Integration

  • Share results with relevant teams immediately
  • Connect CSAT to employee performance metrics
  • Use insights for process improvement initiatives
  • Establish CSAT targets and improvement goals