This guide provides simple, ready-to-use SQL queries for Planning Center Services data. Each query is designed to answer common ministry questions without requiring deep SQL knowledge.
Query Customization Required
These example queries demonstrate common patterns but may require adjustments to match your specific database schema and field names. Test thoroughly in your environment before use.
IMPORTANT: All tables in the Planning Center Services module live in the planning_center schema. Always prefix table names with planning_center. in your SQL.✅ CORRECT: SELECT * FROM planning_center.services_plans
❌ INCORRECT: SELECT * FROM services_plans
-- List everyone scheduled for this Sunday's servicesSELECT p.full_name, pp.team_position_name as role, t.name as team, pl.title as service, pl.short_dates as date, pp.statusFROM planning_center.services_plan_people ppJOIN planning_center.services_people p ON pp.person_id = p.person_idJOIN planning_center.services_teams t ON pp.team_id = t.team_idJOIN planning_center.services_plans pl ON pp.plan_id = pl.plan_idWHERE pl.sort_date >= CURRENT_DATE AND pl.sort_date < CURRENT_DATE + INTERVAL '7 days'ORDER BY pl.sort_date, t.name, pp.team_position_name;
-- See confirmed vs unconfirmed for this weekSELECT t.name as team, COUNT(*) as total_scheduled, COUNT(CASE WHEN pp.status = 'C' THEN 1 END) as confirmed, COUNT(CASE WHEN pp.status = 'U' THEN 1 END) as unconfirmed, COUNT(CASE WHEN pp.status = 'D' THEN 1 END) as declinedFROM planning_center.services_plan_people ppJOIN planning_center.services_teams t ON pp.team_id = t.team_idJOIN planning_center.services_plans pl ON pp.plan_id = pl.plan_idWHERE pl.sort_date >= CURRENT_DATE AND pl.sort_date < CURRENT_DATE + INTERVAL '7 days'GROUP BY t.nameORDER BY total_scheduled DESC;
-- Next 4 weeks of servicesSELECT st.name as service_type, p.title, p.series_title, p.short_dates as dates, p.plan_people_count as volunteers, p.needed_positions_count as open_positions, p.total_length / 60 as duration_minutesFROM planning_center.services_plans pJOIN planning_center.services_service_types st ON p.service_type_id = st.service_type_idWHERE p.sort_date >= CURRENT_DATE AND p.sort_date <= CURRENT_DATE + INTERVAL '28 days'ORDER BY p.sort_date;
-- Top 20 most frequently used songsSELECT s.title, s.author, s.ccli_number, COUNT(DISTINCT i.plan_id) as times_used, STRING_AGG(DISTINCT a.chord_chart_key, ', ') as keys_used, MAX(pl.sort_date) as last_usedFROM planning_center.services_songs sJOIN planning_center.services_items i ON s.song_id = i.song_idJOIN planning_center.services_plans pl ON i.plan_id = pl.plan_idLEFT JOIN planning_center.services_arrangements a ON i.arrangement_id = a.arrangement_idWHERE pl.sort_date >= CURRENT_DATE - INTERVAL '3 months' AND i.item_type = 'song'GROUP BY s.song_id, s.title, s.author, s.ccli_numberORDER BY times_used DESCLIMIT 20;
-- Find songs by theme/tagSELECT title, author, themes, last_scheduled_at, CASE WHEN hidden = true THEN 'Hidden' ELSE 'Active' END as statusFROM planning_center.services_songsWHERE LOWER(themes) LIKE '%worship%' OR LOWER(themes) LIKE '%communion%'ORDER BY last_scheduled_at DESC NULLS LAST;
-- Most common keys for frequently used songsSELECT s.title, a.chord_chart_key as song_key, COUNT(*) as times_in_keyFROM planning_center.services_items iJOIN planning_center.services_songs s ON i.song_id = s.song_idJOIN planning_center.services_arrangements a ON i.arrangement_id = a.arrangement_idJOIN planning_center.services_plans p ON i.plan_id = p.plan_idWHERE p.sort_date >= CURRENT_DATE - INTERVAL '6 months' AND a.chord_chart_key IS NOT NULLGROUP BY s.title, a.chord_chart_keyORDER BY s.title, times_in_key DESC;
-- How many times each person served this monthSELECT p.full_name, t.name as team, COUNT(DISTINCT pl.plan_id) as times_scheduled, COUNT(CASE WHEN pp.status = 'C' THEN 1 END) as times_confirmed, COUNT(CASE WHEN pp.status = 'D' THEN 1 END) as times_declinedFROM planning_center.services_people pJOIN planning_center.services_plan_people pp ON p.person_id = pp.person_idJOIN planning_center.services_teams t ON pp.team_id = t.team_idJOIN planning_center.services_plans pl ON pp.plan_id = pl.plan_idWHERE pl.sort_date >= DATE_TRUNC('month', CURRENT_DATE) AND pl.sort_date < DATE_TRUNC('month', CURRENT_DATE) + INTERVAL '1 month'GROUP BY p.full_name, t.nameHAVING COUNT(DISTINCT pl.plan_id) > 0ORDER BY times_scheduled DESC;
-- People with upcoming blockout datesSELECT p.full_name, b.reason, bd.date as unavailable_dateFROM planning_center.services_blockouts bJOIN planning_center.services_blockout_dates bd ON b.blockout_id = bd.blockout_idJOIN planning_center.services_people p ON b.person_id = p.person_idWHERE bd.date >= CURRENT_DATE AND bd.date <= CURRENT_DATE + INTERVAL '30 days'ORDER BY bd.date, p.full_name;
-- Teams and their available positionsSELECT t.name as team, tp.name as position, COUNT(DISTINCT pa.person_id) as available_peopleFROM planning_center.services_teams tJOIN planning_center.services_team_positions tp ON t.team_id = tp.team_idJOIN planning_center.services_person_team_position_assignments pa ON tp.team_position_id = pa.team_position_idJOIN planning_center.services_people p ON pa.person_id = p.person_idWHERE p.archived = false AND t.archived_at IS NULLGROUP BY t.name, tp.nameORDER BY t.name, tp.name;
-- What's in this Sunday's service?SELECT pl.title as service, i.sequence as order_num, i.item_type, i.title as item, s.title as song_title, i.length / 60 as minutes, i.service_positionFROM planning_center.services_items iJOIN planning_center.services_plans pl ON i.plan_id = pl.plan_idLEFT JOIN planning_center.services_songs s ON i.song_id = s.song_idWHERE pl.sort_date >= CURRENT_DATE AND pl.sort_date < CURRENT_DATE + INTERVAL '7 days'ORDER BY pl.sort_date, i.sequence;
-- Average service length by typeSELECT st.name as service_type, COUNT(p.plan_id) as total_services, AVG(p.total_length) / 60 as avg_length_minutes, MIN(p.total_length) / 60 as shortest_minutes, MAX(p.total_length) / 60 as longest_minutesFROM planning_center.services_plans pJOIN planning_center.services_service_types st ON p.service_type_id = st.service_type_idWHERE p.sort_date >= CURRENT_DATE - INTERVAL '3 months' AND p.total_length > 0GROUP BY st.nameORDER BY avg_length_minutes DESC;
-- Unfilled positions for upcoming servicesSELECT pl.title as service, pl.short_dates as date, t.name as team, np.team_position_name as position, np.quantity as spots_neededFROM planning_center.services_needed_positions npJOIN planning_center.services_plans pl ON np.plan_id = pl.plan_idJOIN planning_center.services_teams t ON np.team_id = t.team_idWHERE pl.sort_date >= CURRENT_DATE AND pl.sort_date <= CURRENT_DATE + INTERVAL '14 days'ORDER BY pl.sort_date, t.name;
-- Count of people who have served in last 3 monthsSELECT COUNT(DISTINCT pp.person_id) as active_volunteersFROM planning_center.services_plan_people ppJOIN planning_center.services_plans p ON pp.plan_id = p.plan_idWHERE p.sort_date >= CURRENT_DATE - INTERVAL '3 months' AND pp.status IN ('C', 'U'); -- Confirmed or Unconfirmed
-- Total songs and arrangementsSELECT COUNT(DISTINCT s.song_id) as total_songs, COUNT(DISTINCT a.arrangement_id) as total_arrangements, COUNT(DISTINCT CASE WHEN s.hidden = false THEN s.song_id END) as active_songsFROM planning_center.services_songs sLEFT JOIN planning_center.services_arrangements a ON s.song_id = a.song_id;
-- Active service types and their frequencySELECT name, frequency, CASE WHEN archived_at IS NULL THEN 'Active' ELSE 'Archived' END as statusFROM planning_center.services_service_typesORDER BY sequence;
-- Recently added chord charts and lead sheetsSELECT s.title as song, a.filename, a.file_size / 1024 as size_kb, a.created_at as added_dateFROM planning_center.services_attachments aJOIN planning_center.services_songs s ON a.attachable_id = s.song_idWHERE a.attachable_type = 'Song' AND (LOWER(a.filename) LIKE '%.pdf' OR LOWER(a.content_type) LIKE '%pdf%')ORDER BY a.created_at DESCLIMIT 20;