My Personal Guide to the TouchPoint's Church Management Database
I could no longer keep up with all the tables and relationships, so started creating my own documented version of the structure to take at least a small bite out of TouchPoint's extensive SQL database structure.
Ryanne at 2PC Memphis - Thank you for your invaluable contributions, insights, and corrections that have made this documentation more accurate and comprehensive!
The SQL interface provides READ-ONLY access to the database. You cannot INSERT, UPDATE, or DELETE records in TouchPoint tables. All data modifications must be done through:
💡 TIP: While you can't modify TouchPoint tables, you CAN create and use temporary tables (#TempTableName) within your SQL scripts for complex queries, data staging, and intermediate calculations. Temp tables are session-specific and automatically cleaned up.
This guide focuses on SELECT queries for reporting, analysis, and data extraction.
TouchPoint is an enterprise-level church management system with one of the most comprehensive databases in the church software industry. Understanding its scale and architecture is crucial for effective use.
| Metric | Count | Description |
|---|---|---|
| Total Tables | 389 | Complete system coverage from people to analytics |
| Total Columns | 4,451 | Extensive data points for comprehensive tracking |
| Foreign Keys | 460 | Complex relationships ensuring data integrity |
| Lookup Tables | 66 | Business logic and validation rules (lookup schema) |
| Extra Value Tables | 17 | Unlimited custom field capabilities |
| Stored Procedures | 161 | Complex operations and reporting |
Real-world production databases contain massive data volumes (actual counts from a DB. Your counts may vary pending on how you use the database.):
| Table | Record Count | Purpose | Performance Impact |
|---|---|---|---|
TagPerson |
159.3M | Flexible categorization | EXTREME - Always use filters! |
EmailResponses |
4.6M | Email engagement tracking | HIGH |
ActivityLog |
3.8M | Audit trail | HIGH |
Attend |
1.5M | Attendance records | MEDIUM |
Contribution |
691K | Financial giving | MEDIUM |
EmailQueue |
195K | Email messages sent | LOW |
TaskNote |
175K | Tasks and notes | LOW |
People |
51K | Person records | LOW |
Families |
31K | Family units | LOW |
-- Core Entity Relationships
People (PeopleId)
├── FamilyId → Groups family members
├── SpouseId → Direct spouse link
└── OrganizationMembers → Group participation
└── Organizations (OrganizationId)
├── Meetings → Scheduled events
└── Attend → Attendance records
People (PeopleId)
├── Contribution → Financial giving
│ └── ContributionFund → Designated funds
├── TaskNote → Tasks and notes
└── Registration → Event signups
└── RegPeople → Participants
└── RegAnswer → Form responses
TouchPoint databases can contain millions of records. Never run unlimited queries without the TOP clause during exploration or testing.
-- ✅ CORRECT: Always start with TOP
SELECT TOP 100 * FROM People WHERE IsDeceased = 0 AND ArchivedFlag = 0
-- ❌ WRONG: Never run unlimited on large tables
SELECT * FROM TagPerson -- This table has 138M+ records!
-- 🚨 FILTER #1: Active People Only
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
-- 🚨 FILTER #2: Actual Attendance (not absences)
WHERE a.AttendanceFlag = 1 -- 1 = present, 0 = absent
-- 🚨 FILTER #3: Real Donations (exclude non-giving types)
WHERE c.ContributionTypeId NOT IN (6, 7, 8, 99) -- 6=Returned, 7=Reversed, 8=Pledge, 99=Event Fee
AND c.ContributionStatusId = 0 -- Posted contributions only
-- 🚨 FILTER #4: Active Organizations
WHERE o.OrganizationStatusId = 30 -- 30 = Active
-- 🚨 FILTER #5: Current Members Only
WHERE om.InactiveDate IS NULL
-- 🚨 FILTER #6: Completed Meetings
WHERE m.DidNotMeet = 0
-- Always check permissions before communication
WHERE p.DoNotMailFlag = 0 -- Can send mail
WHERE p.DoNotCallFlag = 0 -- Can make calls
WHERE p.DoNotVisitFlag = 0 -- Can visit
WHERE p.ReceiveSMS = 1 -- Can send texts
WHERE p.SendEmailAddress1 = 1 -- Can email primary
WHERE p.SendEmailAddress2 = 1 -- Can email secondary
-- Ensure good data quality
WHERE p.PrimaryBadAddrFlag = 0 -- Valid addresses only
WHERE p.CellPhoneIsValid = 1 -- Valid phone numbers
WHERE p.EmailAddress IS NOT NULL AND p.EmailAddress != ''
These are critical discoveries from months of real-world development. Each one caused real debugging pain. Learn from our mistakes!
Transaction is a SQL reserved word. You MUST use brackets in all queries:
-- ✅ CORRECT
SELECT * FROM [Transaction] WHERE Approved = 1
-- ❌ WRONG - Will cause a syntax error
SELECT * FROM Transaction WHERE Approved = 1
Also critical: The Approved column is a BIT field (0 or 1), NOT a string. Do not compare it to 'Approved' or 'true'.
-- ✅ CORRECT - BIT comparison
WHERE t.Approved = 1
-- ❌ WRONG - Approved is not a string
WHERE t.Approved = 'Approved'
To find abandoned registrations, you must use IS NULL, not = 0:
-- ✅ CORRECT - Catches all abandoned registrations
WHERE rd.Completed IS NULL
-- ❌ WRONG - Will miss records (NULL != 0)
WHERE rd.Completed = 0
-- ❌ WRONG - This is not valid SQL for NULL comparison
WHERE rd.Completed = NULL
The column in the OrganizationStructure view is literally spelled Vistors (missing the 'i'). This is a typo in the actual TouchPoint database schema that cannot be changed:
-- ✅ CORRECT - Uses the actual (misspelled) column name
SELECT Vistors FROM OrganizationStructure
-- ❌ WRONG - This column does not exist
SELECT Visitors FROM OrganizationStructure
The join between TagPerson and Tag uses Id, not a column called TagId:
-- ✅ CORRECT
SELECT * FROM TagPerson tp
JOIN Tag t ON tp.Id = t.Id
-- ❌ WRONG - TagId is NOT the join column
SELECT * FROM TagPerson tp
JOIN Tag t ON tp.TagId = t.Id
This is NOT a foreign key relationship - it's a direct ID match:
-- ✅ CORRECT - Direct match, not FK
SELECT * FROM EmailQueue eq
JOIN EmailQueueTo eqt ON eq.Id = eqt.Id
Using != 99 alone is insufficient. You must exclude ALL non-giving types:
-- ✅ CORRECT - Excludes all non-giving types
WHERE c.ContributionTypeId NOT IN (6, 7, 8, 99)
AND c.ContributionStatusId = 0 -- Posted only
-- ❌ INCOMPLETE - Only excludes event fees, includes returned checks and reversals
WHERE c.ContributionTypeId != 99
-- Types to exclude:
-- 6 = Returned Check (bounced)
-- 7 = Reversed (cancelled)
-- 8 = Pledge (commitment, not actual giving)
-- 99 = Event Registration Fee (payment, not donation)
Use the IncompleteTasks VIEW instead of the raw Task table. The column names differ:
-- ✅ CORRECT - Uses the VIEW with correct column name
SELECT * FROM IncompleteTasks WHERE CreatedOn >= DATEADD(day, -30, GETDATE())
-- ❌ WRONG - CreatedDate is the Task TABLE column, not the VIEW column
SELECT * FROM IncompleteTasks WHERE CreatedDate >= DATEADD(day, -30, GETDATE())
MemberTag (SubGroup) names are stored exactly as entered and are organization-specific. Always match the name precisely as it appears in the database:
-- ✅ BEST PRACTICE - Match exact name from database
WHERE mt.Name = 'Blue Team'
AND mt.OrgId = 123 -- SubGroups are org-specific
The Age column on the People table is automatically maintained by TouchPoint. Use it directly instead of calculating from BDate:
-- ✅ SIMPLER - Use pre-calculated field
SELECT Name, Age FROM People WHERE Age >= 18
-- Also works but unnecessary for filtering
SELECT Name, DATEDIFF(year, BDate, GETDATE()) as CalculatedAge FROM People
Active members have InactiveDate IS NULL. Don't assume it's 0 or empty string:
-- ✅ CORRECT
WHERE om.InactiveDate IS NULL -- Active members only
-- ❌ WRONG
WHERE om.InactiveDate = 0
Always exclude cancelled meetings from attendance queries:
WHERE m.DidNotMeet = 0 -- Meeting actually occurred
The EmailResponses table only has Type = 'o' (opens). There is no click tracking available in the database.
The column is SortOrder, not DisplayOrder. Using the wrong name will cause an error.
It's CheckInActivities (plural, a VIEW), not CheckInActivity (singular). The table version doesn't exist.
Division IDs 285, 170, 172, and 131 (Fitness/Member Center) are excluded from active records billing. This means ~325 gym-only members don't count toward your TouchPoint subscription cost. See the Active Records Billing section for details.
| Purpose | TOP Value | Use Case |
|---|---|---|
| Initial Exploration | TOP 100 |
Testing queries, understanding data structure |
| Sample Review | TOP 500 |
Reviewing data patterns, quality checks |
| Larger Analysis | TOP 1000 |
Statistical sampling, trend analysis |
| Comprehensive Sample | TOP 5000 |
Large samples (use with caution) |
| Full Dataset | No TOP | ONLY for final production reports after testing |
These tables REQUIRE date filters and TOP clauses:
-- TagPerson (138M+ records) - EXTREME CAUTION
SELECT TOP 100 * FROM TagPerson tp
WHERE tp.DateCreated >= DATEADD(day, -30, GETDATE())
-- EmailResponses (3.9M+ records)
SELECT TOP 100 * FROM EmailResponses er
WHERE er.Dt >= DATEADD(week, -1, GETDATE())
-- ActivityLog (3.4M+ records)
SELECT TOP 100 * FROM ActivityLog al
WHERE al.ActivityDate >= DATEADD(day, -7, GETDATE())
-- EngagementScoreTag (1.4M+ records)
SELECT TOP 100 * FROM EngagementScoreTag est
JOIN EngagementScore es ON est.EngagementScoreId = es.Id
WHERE es.WeekDate >= DATEADD(month, -3, GETDATE())
-- EmailQueueTo (3.8M+ records)
SELECT TOP 100 * FROM EmailQueueTo eqt
JOIN EmailQueue eq ON eqt.Id = eq.Id
WHERE eq.Sent >= DATEADD(month, -1, GETDATE())
-- STEP 1: Start with minimal data
SELECT TOP 10 *
FROM YourTable
WHERE DateField >= DATEADD(day, -1, GETDATE())
-- STEP 2: Verify query logic with small sample
SELECT TOP 100
SpecificColumns,
YouNeed
FROM YourTable
WHERE DateField >= DATEADD(week, -1, GETDATE())
AND OtherFilters = 'Applied'
-- STEP 3: Expand sample if needed
SELECT TOP 1000
-- your final column list
FROM YourTable
WHERE -- all your filters
ORDER BY -- your sort
-- STEP 4: Remove TOP only for production reports
-- after confirming performance is acceptable
| Table | Record Count | Required Filters |
|---|---|---|
ActivityLog |
3.7M+ | ALWAYS use date filter |
PeopleSearchTokens |
1.7M+ | Use specific PeopleId |
EngagementScoreTag |
1.5M+ | Date range required |
EngagementScore |
1.4M+ | WeekDate filter mandatory |
Tag |
986K+ | Use specific TagId or Name |
Contribution |
687K+ | Date range required |
BundleDetail |
687K+ | Join through BundleHeader |
-- GOOD: ActivityLog with date filter
SELECT TOP 1000 *
FROM ActivityLog
WHERE ActivityDate >= DATEADD(day, -7, GETDATE())
-- BAD: Never do this!
-- SELECT * FROM ActivityLog
-- GOOD: EngagementScore with filters
SELECT es.*
FROM EngagementScore es
WHERE es.WeekDate >= DATEADD(month, -3, GETDATE())
AND es.PeopleId IN (SELECT PeopleId FROM ...)
-- GOOD: Contribution with date range
SELECT c.*
FROM Contribution c
WHERE c.ContributionDate BETWEEN '2024-01-01' AND '2024-12-31'
AND c.FundId = 1
Database Structure: TouchPoint uses 4 schemas with 389+ tables and 107 views
dbo - Main application tables (322 tables, 82 views)lookup - Reference/lookup tables (66 tables, 1 view)custom - Church-specific customizations (1+ tables, 1 view)export - Integration/export views (23 views for data exchange)-- PEOPLE & IDENTITY (Primary Keys and Relationships)
People -- 50K+ records, PeopleId primary key
PeopleExtra -- 213K+ records, custom fields for people
Families -- Family units, FamilyId primary key
FamilyExtra -- Custom fields for families
Users -- 7K+ records, links PeopleId to system users
-- ORGANIZATIONAL HIERARCHY (3-Level Structure)
Program -- 42 records, top level (e.g., "Adult Ministry")
Division -- 217 records, second level, links to Program via ProgId
Organizations -- 1.5K+ records, actual groups/classes, links to Division
DivOrg -- Links Divisions to Organizations (if org has multiple divisions)
ProgDiv -- Links Programs to Divisions
OrganizationMembers -- People's involvement in organizations
OrganizationExtra -- Custom fields for organizations
-- ATTENDANCE & MEETINGS
Attend -- 2.6M+ records, attendance records (CHECK AttendanceFlag!)
Meetings -- 61K+ records, scheduled gatherings
MeetingExtra -- Custom meeting fields
-- FINANCIAL
Contribution -- 687K+ records, donations (EXCLUDE TypeId=99)
ContributionFund -- Fund designations
Transaction -- Financial transactions
TransactionPeople -- Links transactions to people
-- COMMUNICATION
EmailQueue -- 3.9M+ records, email campaigns
EmailQueueTo -- Recipients for emails
EmailResponses -- Email opens/clicks
SMSList -- Text messages sent
SmsReceived -- Incoming texts
-- TASKS & NOTES
TaskNote -- 174K+ records, ministry tasks and notes
TaskList -- Task categories
TaskStatus -- Task workflow states
-- TAGS & SEGMENTATION
Tag -- Tag definitions
TagPerson -- 138M+ records! ALWAYS use date filters
TagShare -- Shared tag permissions
-- REGISTRATION & EVENTS
Registration -- Event registrations
RegPeople -- People registered for events
RegAnswer -- Form responses
RegSetting -- Registration configuration
-- DATABASE VIEWS (107 total across 4 schemas)
-- Custom Schema (1 view)
custom.MissionTripTotals_Optimized -- Mission trip financials
-- DBO Schema Views (82 views) - Key Examples:
ContributionsView -- Enriched contribution data with donor info
OrganizationStructure -- Complete org hierarchy with programs/divisions
AttendCredits -- Weekly attendance credit calculations
CheckInActivities -- Check-in activity with person names (denormalized)
EmailDelegation -- Email delegation permissions
UserList -- User details with last activity
FailedEmails -- Failed email tracking
MobileAppActions -- Mobile app user actions
RecurringGivingDueForToday -- Today's expected recurring gifts
TasksAssigned -- Task assignments and status
IncompleteTasks -- Open tasks needing completion
-- Export Schema Views (23 views) - For Integration:
export.XpPeople -- People data for export (text values not IDs)
export.XpFamily -- Family data for export
export.XpOrganization -- Organization data for export
export.XpAttend -- Attendance data for export
export.XpContribution -- Contribution data for export
export.XpTag -- Tag assignments for export
-- Lookup Schema Views (1 view):
lookup.PushPayBundleHeaderTypes -- Pushpay integration bundle types
-- T-SQL BUILT-IN FUNCTIONS (No Custom Functions Allowed)
-- String Functions (20+)
CONCAT(), SUBSTRING() -- String concatenation and extraction
LEFT(), RIGHT(), LEN() -- String positioning functions
REPLACE(), STUFF() -- String modification
UPPER(), LOWER(), TRIM() -- Case and whitespace handling
STRING_AGG(), STRING_SPLIT() -- Row aggregation and splitting (SQL 2016+)
FORMAT(), QUOTENAME() -- Formatting and SQL identifiers
-- Date & Time Functions (14+)
GETDATE(), DATEADD() -- Current date and date arithmetic
DATEDIFF(), DATEPART() -- Date differences and components
YEAR(), MONTH(), DAY() -- Date part extraction
EOMONTH(), DATEFROMPARTS() -- End of month and date construction (SQL 2012+)
-- Mathematical Functions (15+)
SUM(), AVG(), COUNT() -- Basic aggregates
MIN(), MAX() -- Range functions
ROUND(), CEILING(), FLOOR() -- Rounding functions
STDEV(), VAR() -- Statistical functions
ABS(), SIGN(), POWER() -- Mathematical operations
-- Window Functions (12+)
ROW_NUMBER(), RANK() -- Ranking functions
DENSE_RANK(), NTILE() -- Distribution functions
LAG(), LEAD() -- Row offset functions
FIRST_VALUE(), LAST_VALUE() -- Window boundary values
PERCENT_RANK(), CUME_DIST() -- Percentile functions
-- Conversion Functions (6+)
CAST(), CONVERT() -- Type conversions
TRY_CAST(), TRY_CONVERT() -- Safe conversions (returns NULL on error)
PARSE(), TRY_PARSE() -- Culture-aware parsing (SQL 2012+)
-- Logical Functions (6+)
CASE WHEN...THEN...END -- Conditional logic
IIF(), CHOOSE() -- Inline conditionals (SQL 2012+)
COALESCE(), ISNULL() -- NULL handling
NULLIF() -- Return NULL if equal
-- TOUCHPOINT USER-DEFINED FUNCTIONS (337+ UDFs)
-- Most Critical UDFs by Category:
-- CONTRIBUTIONS (51 Functions)
Contributions2() -- ⭐ MOST IMPORTANT: Handles spouse, excludes non-tax (7 params)
GetTotalContributions() -- Person's total giving (2 params: PeopleId, optional date)
ContributionAmount() -- Single contribution amount (3 params)
PledgeAmount() -- Pledge total for person/fund (3 params)
-- PEOPLE & PROFILES (42 Functions)
Age() -- Calculate age from BDate (1 param: PeopleId)
GetSpouseId() -- Get spouse PeopleId (1 param: PeopleId)
FamilyGreetingName() -- Format family greeting (1 param: FamilyId)
DaysSinceContact() -- Days since last contact (1 param: PeopleId)
-- ATTENDANCE (31 Functions)
LastAttend() -- Last attendance date (2 params: orgid, peopleid - ORDER MATTERS!)
AttendancePercentage() -- Attendance rate calculation (3 params)
RecentAttendance() -- Recent attendance details (2 params)
AttendanceCredits() -- Weekly attendance credits (2 params)
-- ORGANIZATIONS (29 Functions)
OrgPeopleCurrent() -- Current org members (1 param: OrgId)
-- Returns: PeopleId, AttPct, LastAttended, MemberType, Groups, etc.
OrganizationLeaderName() -- Get leader name (1 param: OrgId)
InSmallGroup() -- Check small group membership (2 params)
-- FORMATTING (8 Functions)
FmtPhone() -- Format phone numbers (1 param: phone)
FmtZip() -- Format zip codes (1 param: zip)
-- Function Types:
-- 179 Scalar Functions (return single value - slower in WHERE clauses)
-- 118 Inline Table-Valued (best performance, optimizer-friendly)
-- 35 Multi-Statement Table-Valued (moderate performance)
-- 5 CLR Functions (compiled .NET functions)
-- ⚠️ IMPORTANT UDF USAGE NOTES:
-- 1. Table-valued functions require FROM: SELECT * FROM dbo.GetTotalContributions(117, NULL)
-- 2. Birth date column is 'BDate' not 'BirthDate'
-- 3. LastAttend takes (orgid, peopleid) NOT (peopleid, orgid)
-- 4. Some functions require SET QUOTED_IDENTIFIER ON
-- 5. Use CROSS APPLY with table-valued functions for better performance
-- JSON Functions (SQL 2016+)
JSON_VALUE(), JSON_QUERY() -- Extract from JSON
ISJSON(), OPENJSON() -- Validate and parse JSON
FOR JSON PATH/AUTO -- Format results as JSON
-- System Functions (10+)
@@ROWCOUNT, @@IDENTITY -- Session values
DB_NAME(), USER_NAME() -- Database context
NEWID(), CHECKSUM() -- GUIDs and checksums
ERROR_MESSAGE() -- Error handling
-- User Authentication Chain
Users.PeopleId → People.PeopleId
Users.UserId → UserRole.UserId → Roles.RoleId
-- Organization Hierarchy
Program.Id → Division.ProgId → Organizations.DivisionId
-- Family Structure
People.FamilyId → Families.FamilyId
Families.HeadOfHouseholdId → People.PeopleId
Families.HeadOfHouseholdSpouseId → People.PeopleId
-- Attendance Chain
People.PeopleId → Attend.PeopleId
Attend.MeetingId → Meetings.MeetingId
Meetings.OrganizationId → Organizations.OrganizationId
-- Email System
EmailQueue.Id = EmailQueueTo.Id (direct match, not FK)
EmailQueueTo.PeopleId → People.PeopleId
EmailResponses.EmailQueueId → EmailQueue.Id
-- Tag System (Special!)
TagPerson.Id = Tag.Id (NOT TagPerson.TagId!)
TagPerson.PeopleId → People.PeopleId
-- Complete hierarchy from Program to Organization
SELECT
p.Name as Program,
d.Name as Division,
o.OrganizationName,
o.OrganizationId
FROM Organizations o
JOIN Division d ON o.DivisionId = d.Id
JOIN Program p ON d.ProgId = p.Id
WHERE o.OrganizationStatusId = 30 -- Active only
-- Link system users to people records
SELECT
u.Username,
p.Name,
p.EmailAddress,
r.RoleName
FROM Users u
JOIN People p ON u.PeopleId = p.PeopleId
LEFT JOIN UserRole ur ON u.UserId = ur.UserId
LEFT JOIN Roles r ON ur.RoleId = r.RoleId
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
-- Get all family members with relationships
-- Note: Families table does not have FamilyName column - use household names instead
SELECT
COALESCE(hoh.Name, '') +
CASE WHEN spouse.Name IS NOT NULL THEN ' & ' + spouse.Name ELSE '' END as HouseholdName,
p.Name,
fp.Description as FamilyPosition,
CASE
WHEN p.PeopleId = f.HeadOfHouseholdId THEN 'Head'
WHEN p.PeopleId = f.HeadOfHouseholdSpouseId THEN 'Spouse'
ELSE 'Member'
END as Role
FROM People p
JOIN Families f ON p.FamilyId = f.FamilyId
LEFT JOIN People hoh ON f.HeadOfHouseholdId = hoh.PeopleId
LEFT JOIN People spouse ON f.HeadOfHouseholdSpouseId = spouse.PeopleId
JOIN lookup.FamilyPosition fp ON p.PositionInFamilyId = fp.Id
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
ORDER BY HouseholdName, p.PositionInFamilyId
-- Attendance with complete organizational context
SELECT
p.Name,
prog.Name as Program,
div.Name as Division,
o.OrganizationName,
m.MeetingDate,
at.Description as AttendType
FROM Attend a
JOIN People p ON a.PeopleId = p.PeopleId
JOIN Meetings m ON a.MeetingId = m.MeetingId
JOIN Organizations o ON m.OrganizationId = o.OrganizationId
JOIN Division div ON o.DivisionId = div.Id
JOIN Program prog ON div.ProgId = prog.Id
JOIN lookup.AttendType at ON a.AttendanceTypeId = at.Id
WHERE a.AttendanceFlag = 1 -- Present only
AND m.DidNotMeet = 0 -- Meeting occurred
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
-- Get people with their custom fields
SELECT
p.Name,
pe.Field as CustomField,
pe.Data as Value,
pe.Type as DataType
FROM People p
LEFT JOIN PeopleExtra pe ON p.PeopleId = pe.PeopleId
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
AND pe.Field IN ('Status', 'LifeStage', 'MembershipDate')
ORDER BY p.Name, pe.Field
SELECT TOP 1000
p.PeopleId,
p.Name,
p.EmailAddress,
p.CellPhone,
ms.Description as MemberStatus,
DATEDIFF(year, p.BDate, GETDATE()) as Age,
c.Description as Campus
FROM People p
JOIN lookup.MemberStatus ms ON p.MemberStatusId = ms.Id
LEFT JOIN lookup.Campus c ON p.CampusId = c.Id
WHERE p.IsDeceased = 0
AND p.ArchivedFlag = 0
AND p.MemberStatusId = 10 -- Members only
ORDER BY p.Name
SELECT TOP 500
p.Name,
o.OrganizationName,
m.MeetingDate,
at.Description as AttendanceType
FROM Attend a
JOIN People p ON a.PeopleId = p.PeopleId
JOIN Organizations o ON a.OrganizationId = o.OrganizationId
JOIN Meetings m ON a.MeetingId = m.MeetingId
JOIN lookup.AttendType at ON a.AttendanceTypeId = at.Id
WHERE a.AttendanceFlag = 1 -- Critical: actual attendance
AND m.MeetingDate >= DATEADD(week, -1, GETDATE())
AND m.DidNotMeet = 0
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
ORDER BY m.MeetingDate DESC, o.OrganizationName, p.Name
SELECT TOP 500
p.Name,
c.ContributionDate,
c.ContributionAmount,
cf.FundName,
c.CheckNo
FROM Contribution c
JOIN People p ON c.PeopleId = p.PeopleId
JOIN ContributionFund cf ON c.FundId = cf.FundId
WHERE c.ContributionTypeId NOT IN (6, 7, 8, 99) -- Exclude returned, reversed, pledges, event fees
AND c.ContributionStatusId = 0 -- Posted only
AND c.ContributionDate >= DATEADD(month, -1, GETDATE())
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
ORDER BY c.ContributionDate DESC, c.ContributionAmount DESC
-- Example: Using temp tables to stage data for complex reporting
-- Temp tables are allowed in TouchPoint SQL and are very useful!
-- Create a temp table with attendance summary
SELECT
p.PeopleId,
p.Name,
COUNT(DISTINCT a.MeetingDate) as AttendanceCount,
MAX(a.MeetingDate) as LastAttended
INTO #AttendanceSummary
FROM People p
JOIN Attend a ON p.PeopleId = a.PeopleId
WHERE a.MeetingDate >= DATEADD(month, -6, GETDATE())
GROUP BY p.PeopleId, p.Name
-- Create another temp table with giving summary
SELECT
p.PeopleId,
SUM(c.ContributionAmount) as TotalGiving,
COUNT(c.ContributionId) as GiftCount
INTO #GivingSummary
FROM People p
JOIN Contribution c ON p.PeopleId = c.PeopleId
WHERE c.ContributionDate >= DATEADD(month, -6, GETDATE())
AND c.ContributionTypeId NOT IN (8, 99)
GROUP BY p.PeopleId
-- Now join the temp tables for final analysis
SELECT
a.Name,
a.AttendanceCount,
a.LastAttended,
ISNULL(g.TotalGiving, 0) as TotalGiving,
ISNULL(g.GiftCount, 0) as GiftCount,
CASE
WHEN a.AttendanceCount > 10 AND g.TotalGiving > 500 THEN 'Highly Engaged'
WHEN a.AttendanceCount > 5 OR g.TotalGiving > 100 THEN 'Engaged'
ELSE 'New/Occasional'
END as EngagementLevel
FROM #AttendanceSummary a
LEFT JOIN #GivingSummary g ON a.PeopleId = g.PeopleId
ORDER BY a.AttendanceCount DESC, g.TotalGiving DESC
-- Temp tables are automatically cleaned up when your session ends
-- Or you can manually drop them:
-- DROP TABLE #AttendanceSummary
-- DROP TABLE #GivingSummary
-- Recent first-time visitors for follow-up
-- Note: AttendType values vary by church. Common values: 60=New Guest, 50=Recent Guest
-- Always check your lookup.AttendType table for actual values
WITH FirstVisit AS (
SELECT
p.PeopleId,
MIN(a.MeetingDate) as FirstVisitDate
FROM Attend a
JOIN People p ON a.PeopleId = p.PeopleId
WHERE a.AttendanceFlag = 1
AND a.AttendanceTypeId IN (50, 60) -- Recent Guest, New Guest (verify your IDs)
GROUP BY p.PeopleId
)
SELECT TOP 100
p.Name,
p.EmailAddress,
p.CellPhone,
fv.FirstVisitDate,
DATEDIFF(day, fv.FirstVisitDate, GETDATE()) as DaysSinceVisit
FROM FirstVisit fv
JOIN People p ON fv.PeopleId = p.PeopleId
WHERE fv.FirstVisitDate >= DATEADD(week, -2, GETDATE())
AND p.DoNotCallFlag = 0
AND p.DoNotVisitFlag = 0
AND p.IsDeceased = 0
AND p.ArchivedFlag = 0
ORDER BY fv.FirstVisitDate DESC
SELECT TOP 100
o.OrganizationName,
o.Location,
leader.Name as Leader,
o.MemberCount,
o.Limit as Capacity,
CASE
WHEN o.Limit > 0 THEN
CAST(o.MemberCount as float) / o.Limit * 100
ELSE 0
END as PercentFull,
o.FirstMeetingDate,
o.LastMeetingDate
FROM Organizations o
LEFT JOIN People leader ON o.LeaderId = leader.PeopleId
WHERE o.OrganizationStatusId = 30 -- Active
ORDER BY o.MemberCount DESC
Purpose: Central repository for all person and family data
Primary Key: PeopleId
Scale: 136 columns with 23 foreign key relationships
Critical Rule: Always filter WHERE IsDeceased = 0 AND ArchivedFlag = 0
| Field | Type | Description | Usage Notes |
|---|---|---|---|
PeopleId |
int | Primary key | Unique person identifier |
FirstName |
nvarchar(25) | First name | Legal first name |
LastName |
nvarchar(100) | Last name | Family name |
NickName |
nvarchar(25) | Preferred name | May override FirstName in displays |
Name |
nvarchar(139) | Full name (computed) | ✅ USE FOR DISPLAY |
Name2 |
nvarchar(139) | Last, First format | ✅ USE FOR SORTING |
AltName |
nvarchar(100) | Alternative name | Used for search optimization |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
BirthDay |
int | Day of birth | 1-31 |
BirthMonth |
int | Month of birth | 1-12 |
BirthYear |
int | Year of birth | 4-digit year |
BDate |
datetime | Full birth date | ⚠️ Includes time, cast to date |
Age |
int | Current age | Computed, updated regularly |
GenderId |
int | Gender | → lookup.Gender (1=Male, 2=Female) |
MaritalStatusId |
int | Marital status | → lookup.MaritalStatus |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
FamilyId |
int | Family grouping | → Families.FamilyId |
SpouseId |
int | Spouse link | → People.PeopleId (direct link) |
PositionInFamilyId |
int | Family position | → lookup.FamilyPosition |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
EmailAddress |
nvarchar(150) | Primary email | Main contact email |
EmailAddress2 |
nvarchar(150) | Secondary email | Alternative email |
SendEmailAddress1 Critical |
bit | Email permission 1 | Check before emailing |
SendEmailAddress2 Critical |
bit | Email permission 2 | Check before emailing |
CellPhone |
nvarchar(20) | Cell phone | Formatted number |
HomePhone |
nvarchar(20) | Home phone | 10 digits |
WorkPhone |
nvarchar(20) | Work phone | Business number |
CellPhoneValidated |
bit | Phone verified | Verification status |
CellPhoneIsValid |
bit | Phone working | Active number check |
ReceiveSMS Critical |
bit | SMS permission | Check before texting |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
PrimaryAddress |
nvarchar(100) | Street address | ✅ Use for mailings |
PrimaryAddress2 |
nvarchar(100) | Apt/Suite | Secondary line |
PrimaryCity |
nvarchar(30) | City | Primary city |
PrimaryState |
nvarchar(20) | State | State abbreviation |
PrimaryZip |
nvarchar(15) | ZIP code | Postal code |
PrimaryCountry |
nvarchar(40) | Country | Country name |
PrimaryBadAddrFlag |
bit | Invalid address | Filter bad addresses |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
MemberStatusId Critical |
int | Member status | → lookup.MemberStatus |
JoinDate |
datetime | Joined church | Membership start |
DropDate |
datetime | Left church | Membership end |
DropCodeId |
int | Reason left | → lookup.DropType |
BaptismDate |
datetime | Baptized date | Baptism completion |
BaptismStatusId |
int | Baptism status | → lookup.BaptismStatus |
DecisionDate |
datetime | Salvation date | Faith decision |
NewMemberClassDate |
datetime | Class completion | New member training |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
DoNotMailFlag Critical |
bit | No postal mail | Filter marketing |
DoNotCallFlag Critical |
bit | No phone calls | Filter campaigns |
DoNotVisitFlag Critical |
bit | No visits | Filter assignments |
DoNotPublishPhones |
bit | Directory privacy | Hide from directory |
ElectronicStatement |
bit | Email statements | Prefers electronic |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
IsDeceased Critical |
bit | Person deceased | ALWAYS filter = 0 |
ArchivedFlag Critical |
bit | Record archived | ALWAYS filter = 0 |
CreatedDate |
datetime | Record created | Creation timestamp |
CreatedBy |
int | Created by | → People.PeopleId |
ModifiedDate |
datetime | Last modified | Update timestamp |
ModifiedBy |
int | Modified by | → People.PeopleId |
CampusId |
int | Campus | → lookup.Campus |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
BlockMobileAccess |
bit | Block mobile app | Security restriction |
BarcodeId |
bigint | Check-in barcode | Unique identifier |
BarcodeExpires |
datetime | Barcode expiration | Security timeout |
PictureId |
int | Profile photo | → Picture.PictureId |
TouchPoint includes 67+ lookup tables that provide business logic, validation rules, and categorization throughout the system.
TouchPoint contains exactly 67 lookup tables in the lookup schema. Each table stores reference data used throughout the system for consistency and data integrity.
| Table Name | Description | Primary Usage |
|---|---|---|
AccountCode |
Account codes for financial tracking | Contribution and transaction categorization |
AddressType |
Types of addresses (Home, Work, etc.) | Address classification for people and organizations |
AttendCredit |
Attendance credit types and rules | Tracking different types of attendance credits |
AttendType |
Attendance classification (Member, Guest, etc.) | Categorizing attendance records |
BackGroundCheckApprovalCodes |
Background check approval statuses | Volunteer screening and approval tracking |
BackgroundCheckLabels |
Labels for background check types | Categorizing different background check requirements |
BadgeColor |
Check-in badge color codes | Visual identification in check-in system |
BaptismStatus |
Baptism status tracking | Recording baptism completion status |
BaptismType |
Types of baptism (Immersion, Sprinkling, etc.) | Classifying baptism methods |
BuildingAccessTypes |
Building access permissions | Facility security and access control |
BundleHeaderTypes |
Contribution bundle types | Categorizing contribution batches |
BundleStatusTypes |
Bundle processing statuses | Tracking contribution batch workflow |
Campus |
Church campus locations | Multi-site church management |
CategoryMobile |
Mobile app categories | Organizing content in mobile applications |
CommunicationTypes |
Communication method types | Tracking preferred communication channels |
ContactPreference |
Contact preference settings | Managing communication preferences |
ContactReason |
Reasons for contact | Categorizing contact interactions |
ContactType |
Types of contact interactions | Classifying touchpoints with members |
ContributionSources |
Sources of contributions | Tracking where donations originate |
ContributionStatus |
Contribution processing statuses | Managing donation workflow |
ContributionType |
Types of contributions | Classifying donations (tax-deductible, fees, etc.) |
Country |
Country codes and names | International address support |
DecisionType |
Types of spiritual decisions | Recording salvation, baptism, membership decisions |
DropType |
Reasons for dropping enrollment | Tracking why people leave programs |
EntryPoint |
Entry points into church | Tracking how people first connect |
EnvelopeOption |
Envelope assignment options | Managing giving envelope distribution |
FamilyMemberType |
Family member classifications | Defining relationships within families |
FamilyPosition |
Position in family hierarchy | Primary Adult, Child, etc. |
FamilyRelationship |
Family relationship types | Defining specific family connections |
GatewayConfigurationTemplate |
Payment gateway templates | Online giving configuration |
GatewayReasonCodes |
Payment gateway response codes | Processing payment results |
Gateways |
Payment gateway providers | Available payment processors |
GatewayServiceType |
Types of gateway services | Different payment processing options |
Gender |
Gender classifications | Male, Female, Unknown |
GradeLevel |
School grade levels | Age-appropriate ministry grouping |
InterestPoint |
Ministry interest areas | Tracking ministry preferences |
JoinType |
How people join church | Baptism, Transfer, Statement, etc. |
LocationMobile |
Mobile app locations | Location-based features in app |
MaritalStatus |
Marital status options | Married, Single, Divorced, Widowed |
Medication |
Common medications list | Medical information for children's ministry |
MeetingType |
Types of meetings | Regular, Special, Make-up, etc. |
MemberLetterStatus |
Member letter tracking | Status of membership transfer letters |
MemberStatus |
Membership statuses | Member, Prospect, Previous Member, etc. |
MemberType |
Organization member types | Leader, Member, Volunteer, Guest |
MobileAgeRestriction |
Age restrictions for mobile features | Child safety in mobile app |
NewMemberClassStatus |
New member class progress | Tracking membership class completion |
OrganizationStatus |
Organization statuses | Active, Inactive, Pending |
OrganizationType |
Types of organizations | Class, Home Group, Ministry Team, etc. |
OrgUse |
Organization usage types | How organizations function in system |
Origin |
Data origin sources | Where records were created from |
PbActionType |
Pushpay action types | Integration with Pushpay giving |
PostalLookup |
ZIP code database | Address validation and standardization |
ProcessStepCompletionType |
Process step completion methods | Tracking how steps are completed |
PushPayBundleHeaderTypes |
Pushpay bundle types (VIEW not table) | Pushpay integration bundle categories |
RegistrationMobile |
Mobile registration options | Mobile app registration settings |
ReservableType |
Types of reservable resources | Room, Equipment, Vehicle categories |
ResidentCode |
Resident status codes | Member, Non-member resident tracking |
ResourceMediaFormat |
Media format types | Audio, Video, Document formats |
ResourceMediaType |
Media content types | Sermon, Music, Teaching categories |
ScheduledGiftType |
Recurring giving schedules | Weekly, Monthly, Bi-weekly options |
ShirtSize |
Apparel sizes | Event registration and volunteer gear |
StateLookup |
US state codes and names | Address standardization |
SurveyType |
Survey categories | Feedback and assessment types |
TaskStatus |
Task workflow statuses | Pending, Active, Complete, Delegated |
TitleCode |
Name titles and prefixes | Mr., Mrs., Dr., Rev., etc. |
VolApplicationStatus |
Volunteer application statuses | Tracking volunteer screening process |
VolunteerCodes |
Volunteer position codes | Categorizing volunteer opportunities |
-- Get all lookup tables with record counts
SELECT
t.TABLE_NAME,
(SELECT COUNT(*)
FROM lookup.[' + t.TABLE_NAME + ']) as RecordCount
FROM INFORMATION_SCHEMA.TABLES t
WHERE t.TABLE_SCHEMA = 'lookup'
ORDER BY t.TABLE_NAME;
-- Query any specific lookup table
SELECT * FROM lookup.MemberStatus ORDER BY Id;
SELECT * FROM lookup.AttendType ORDER BY Id;
SELECT * FROM lookup.ContributionType ORDER BY Id;
⚠️ Important Note: Common lookup values shown below may not match your church's configuration. Always verify the actual values in your database by querying the lookup tables directly. Each church customizes these values for their specific needs.
| Id | Description | Usage |
|---|---|---|
| 10 | Member | Full church members - voting, leadership eligible |
| 20 | Prospect | Potential members - follow-up focus |
| 30 | Not Member | Regular attenders, not members |
| 40 | Previous Member | Former members - different communication |
| 50 | Just Added | New records - need data cleanup |
⚠️ Important: AttendType values are customized per church. Always verify your actual values with SELECT * FROM lookup.AttendType
| Common Id | Typical Description | Usage |
|---|---|---|
| 10 | Leader | Leadership roles |
| 20 | Volunteer | Volunteer workers |
| 30 | Member | Regular members |
| 40 | Visiting Member | Members from other churches |
| 50 | Recent Guest | Return visitors |
| 60 | New Guest |
| Id | Description | Usage |
|---|---|---|
| 10 | Primary Adult | Both spouses/parents in nuclear family (distinguished by HeadOfHouseholdId and HeadOfHouseholdSpouseId in Families table) |
| 20 | Secondary Adult (or Other Adult) | Adults outside nuclear family structure (e.g., grandparent living with family, adult sibling) |
| 30 | Child | Dependent children of the Primary Adults |
| Common Ids | Typical Description | Usage Notes |
|---|---|---|
| 1 | Tax Deductible | Standard charitable contribution |
| 6 | Returned Check | EXCLUDE from reports - failed payment |
| 7 | Reversed | EXCLUDE from reports - reversed transaction |
| 8 | Pledge | EXCLUDE from contribution reports - commitment only |
| 99 | Event Registration Fee | EXCLUDE from giving reports - not a donation |
Note: Your church may have different contribution types. Query SELECT * FROM lookup.ContributionType to see your specific values. Types like Cash, Check, Credit Card are typically NOT in this table but may be tracked elsewhere.
-- Common lookup tables and their purposes
SELECT 'Gender' as LookupTable, Id, Description FROM lookup.Gender
UNION ALL
SELECT 'MaritalStatus', Id, Description FROM lookup.MaritalStatus
UNION ALL
SELECT 'Campus', Id, Description FROM lookup.Campus
UNION ALL
SELECT 'OrganizationStatus', Id, Description FROM lookup.OrganizationStatus
UNION ALL
SELECT 'ContributionType', Id, Description FROM lookup.ContributionType
UNION ALL
SELECT 'BaptismStatus', Id, Description FROM lookup.BaptismStatus
UNION ALL
SELECT 'DecisionType', Id, Description FROM lookup.DecisionType
UNION ALL
SELECT 'DropType', Id, Description FROM lookup.DropType
ORDER BY LookupTable, Id
-- Executive Dashboard Query
WITH Metrics AS (
SELECT
-- People Metrics
(SELECT COUNT(*) FROM People WHERE IsDeceased = 0 AND ArchivedFlag = 0 AND MemberStatusId = 10) as ActiveMembers,
(SELECT COUNT(*) FROM People WHERE IsDeceased = 0 AND ArchivedFlag = 0 AND MemberStatusId = 20) as Prospects,
-- Attendance Metrics (Last Week)
(SELECT COUNT(DISTINCT a.PeopleId)
FROM Attend a
WHERE a.AttendanceFlag = 1
AND a.MeetingDate >= DATEADD(week, -1, GETDATE())) as WeeklyAttendance,
-- Attendance Metrics (Last Month)
(SELECT COUNT(DISTINCT a.PeopleId)
FROM Attend a
WHERE a.AttendanceFlag = 1
AND a.MeetingDate >= DATEADD(month, -1, GETDATE())) as MonthlyAttendance,
-- Financial Metrics
(SELECT SUM(ContributionAmount)
FROM Contribution
WHERE ContributionTypeId NOT IN (6, 7, 8, 99)
AND ContributionStatusId = 0
AND ContributionDate >= DATEADD(month, -1, GETDATE())) as MonthlyGiving,
(SELECT COUNT(DISTINCT PeopleId)
FROM Contribution
WHERE ContributionTypeId NOT IN (6, 7, 8, 99)
AND ContributionStatusId = 0
AND ContributionDate >= DATEADD(month, -1, GETDATE())) as MonthlyGivers,
-- Organization Metrics
(SELECT COUNT(*) FROM Organizations WHERE OrganizationStatusId = 30) as ActiveOrganizations,
-- New People
(SELECT COUNT(*)
FROM People
WHERE CreatedDate >= DATEADD(month, -1, GETDATE())
AND IsDeceased = 0 AND ArchivedFlag = 0) as NewPeopleThisMonth
)
SELECT * FROM Metrics
-- Weekly attendance trends over 12 weeks
WITH WeeklyAttendance AS (
SELECT
DATEPART(year, a.MeetingDate) as Year,
DATEPART(week, a.MeetingDate) as Week,
MIN(CAST(a.MeetingDate as DATE)) as WeekStart,
COUNT(DISTINCT a.PeopleId) as UniqueAttenders,
COUNT(*) as TotalAttendances,
COUNT(DISTINCT a.OrganizationId) as OrgsWithMeetings,
COUNT(DISTINCT CASE WHEN at.Guest = 1 THEN a.PeopleId END) as Guests,
COUNT(DISTINCT CASE WHEN at.Worker = 1 THEN a.PeopleId END) as Workers
FROM Attend a
JOIN lookup.AttendType at ON a.AttendanceTypeId = at.Id
WHERE a.AttendanceFlag = 1
AND a.MeetingDate >= DATEADD(week, -12, GETDATE())
GROUP BY DATEPART(year, a.MeetingDate), DATEPART(week, a.MeetingDate)
)
SELECT
WeekStart,
UniqueAttenders,
Guests,
Workers,
UniqueAttenders - Guests - Workers as Members,
TotalAttendances,
OrgsWithMeetings,
CAST(TotalAttendances as float) / NULLIF(UniqueAttenders, 0) as AvgAttendancesPerPerson
FROM WeeklyAttendance
ORDER BY WeekStart DESC
-- Comprehensive giving analysis
WITH GivingStats AS (
SELECT
p.PeopleId,
p.Name,
COUNT(*) as GiftCount,
SUM(c.ContributionAmount) as TotalGiven,
MIN(c.ContributionDate) as FirstGift,
MAX(c.ContributionDate) as LastGift,
DATEDIFF(day, MAX(c.ContributionDate), GETDATE()) as DaysSinceLastGift
FROM Contribution c
JOIN People p ON c.PeopleId = p.PeopleId
WHERE c.ContributionTypeId NOT IN (6, 7, 8, 99)
AND c.ContributionStatusId = 0
AND c.ContributionDate >= DATEADD(year, -1, GETDATE())
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
GROUP BY p.PeopleId, p.Name
)
SELECT
CASE
WHEN TotalGiven >= 10000 THEN 'Major Donor ($10K+)'
WHEN TotalGiven >= 5000 THEN 'Significant Donor ($5K-$10K)'
WHEN TotalGiven >= 1000 THEN 'Regular Donor ($1K-$5K)'
WHEN TotalGiven >= 100 THEN 'Small Donor ($100-$1K)'
ELSE 'Micro Donor (<$100)'
END as DonorLevel,
COUNT(*) as DonorCount,
SUM(TotalGiven) as TotalFromLevel,
AVG(TotalGiven) as AvgGiftPerDonor,
AVG(GiftCount) as AvgGiftsPerDonor
FROM GivingStats
GROUP BY
CASE
WHEN TotalGiven >= 10000 THEN 'Major Donor ($10K+)'
WHEN TotalGiven >= 5000 THEN 'Significant Donor ($5K-$10K)'
WHEN TotalGiven >= 1000 THEN 'Regular Donor ($1K-$5K)'
WHEN TotalGiven >= 100 THEN 'Small Donor ($100-$1K)'
ELSE 'Micro Donor (<$100)'
END
ORDER BY MIN(TotalGiven) DESC
-- Organization/group health metrics
SELECT TOP 500
o.OrganizationName,
o.Location,
leader.Name as Leader,
o.MemberCount,
o.Limit as Capacity,
-- Capacity Metrics
CASE
WHEN o.Limit > 0 THEN CAST(o.MemberCount as float) / o.Limit * 100
ELSE NULL
END as PercentFull,
-- Meeting Metrics
(SELECT COUNT(*)
FROM Meetings m
WHERE m.OrganizationId = o.OrganizationId
AND m.MeetingDate >= DATEADD(month, -3, GETDATE())
AND m.DidNotMeet = 0) as MeetingsLast3Months,
-- Attendance Metrics
(SELECT AVG(CAST(NumPresent as float))
FROM Meetings m
WHERE m.OrganizationId = o.OrganizationId
AND m.MeetingDate >= DATEADD(month, -3, GETDATE())
AND m.DidNotMeet = 0) as AvgAttendance,
-- Engagement
(SELECT AVG(om.AttendPct)
FROM OrganizationMembers om
WHERE om.OrganizationId = o.OrganizationId
AND om.InactiveDate IS NULL) as AvgMemberEngagement,
-- Growth
(SELECT COUNT(*)
FROM OrganizationMembers om
WHERE om.OrganizationId = o.OrganizationId
AND om.EnrollmentDate >= DATEADD(month, -3, GETDATE())) as NewMembersLast3Months
FROM Organizations o
LEFT JOIN People leader ON o.LeaderId = leader.PeopleId
WHERE o.OrganizationStatusId = 30
ORDER BY o.MemberCount DESC
Problem: Query runs forever or times out
Solutions:
TOP 100 to limit resultsWHERE DateField >= DATEADD(month, -1, GETDATE())Problem: Attendance counts differ from TouchPoint interface
Solution: Verify these filters are applied:
WHERE a.AttendanceFlag = 1 -- Only count actual attendance
AND m.DidNotMeet = 0 -- Exclude cancelled meetings
AND p.IsDeceased = 0 -- Exclude deceased
AND p.ArchivedFlag = 0 -- Exclude archived
Problem: Donation totals are inflated by returned checks, reversed transactions, pledges, or event fees
Solution: Always exclude ALL non-donation types and filter for posted contributions only:
WHERE c.ContributionTypeId NOT IN (6, 7, 8, 99) -- Exclude returned checks, reversed, pledges, event fees
AND c.ContributionStatusId = 0 -- Posted contributions only
Problem: Reports include deceased individuals
Solution: Always include both filters:
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
Problem: Active organizations not appearing
Solution: Use correct status value:
WHERE o.OrganizationStatusId = 30 -- NOT 1!
Problem: Church-specific fields not found
Solution: Query Extra Value tables:
-- Discover available custom fields
SELECT DISTINCT Field, COUNT(*) as Uses
FROM PeopleExtra
GROUP BY Field
ORDER BY COUNT(*) DESC
TouchPoint's subscription is based on the number of "active records" in your database. Understanding how "active" is defined helps you manage your data effectively.
A person is "active" if they meet EITHER of these criteria within the past 365 days:
| Criteria | Definition | Details |
|---|---|---|
| 1. Household Giving | Person belongs to household where at least one member gave | Must be posted (StatusId=0), excludes types 6, 7, 8, 99 |
| 2. Activity/Engagement | Person has 2+ attendance records OR 2+ registrations | One-time guests are NOT counted (requires sustained engagement) |
-- Count current active records
WITH HouseholdsWithGiving AS (
SELECT DISTINCT p.FamilyId
FROM Contribution c WITH (NOLOCK)
JOIN People p WITH (NOLOCK) ON c.PeopleId = p.PeopleId
WHERE c.ContributionDate >= DATEADD(year, -1, GETDATE())
AND c.ContributionStatusId = 0
AND c.ContributionTypeId NOT IN (6, 7, 8, 99)
),
PeopleWithActivity AS (
SELECT a.PeopleId
FROM Attend a WITH (NOLOCK)
JOIN Meetings m WITH (NOLOCK) ON a.MeetingId = m.MeetingId
WHERE m.MeetingDate >= DATEADD(year, -1, GETDATE())
AND a.AttendanceFlag = 1
GROUP BY a.PeopleId
HAVING COUNT(*) >= 2
UNION
SELECT PeopleId
FROM EnrollmentTransaction WITH (NOLOCK)
WHERE TransactionDate >= DATEADD(year, -1, GETDATE())
GROUP BY PeopleId
HAVING COUNT(*) >= 2
)
SELECT
COUNT(DISTINCT p.PeopleId) as ActiveRecords
FROM People p WITH (NOLOCK)
LEFT JOIN HouseholdsWithGiving hwg ON p.FamilyId = hwg.FamilyId
LEFT JOIN PeopleWithActivity pwa ON p.PeopleId = pwa.PeopleId
WHERE p.IsDeceased = 0
AND p.ArchivedFlag = 0
AND (hwg.FamilyId IS NOT NULL OR pwa.PeopleId IS NOT NULL)
SubGroups allow organizations to create smaller groups within an organization (e.g., "Blue Team", "Red Team" within a serving team).
SubGroup names are stored exactly as entered. When querying, match the name as it appears in the database to ensure accurate results.
| Field | Type | Description | Notes |
|---|---|---|---|
Id |
int | SubGroup ID | Primary key |
OrgId |
int | Organization this subgroup belongs to | SubGroups are organization-specific |
Name |
nvarchar | SubGroup name | Match exactly as stored |
| Field | Type | Description | Notes |
|---|---|---|---|
MemberTagId |
int | Links to MemberTags.Id | Foreign key |
PeopleId |
int | Person in subgroup | Links to People.PeopleId |
OrgId |
int | Organization | Links to Organizations.OrganizationId |
IsLeader |
bit | SubGroup leader flag | Separate from OrganizationMembers.MemberTypeId |
Number |
int | Display/sequence order | For ordering within subgroup |
-- List all subgroups and their members for an organization
SELECT
mt.Name as SubGroup,
p.Name as PersonName,
ommt.IsLeader,
om.MemberTypeId
FROM MemberTags mt
JOIN OrgMemMemTags ommt ON mt.Id = ommt.MemberTagId
JOIN People p ON ommt.PeopleId = p.PeopleId
LEFT JOIN OrganizationMembers om
ON ommt.PeopleId = om.PeopleId
AND ommt.OrgId = om.OrganizationId
WHERE mt.OrgId = 123 -- Your organization ID
AND om.InactiveDate IS NULL -- Active members only
ORDER BY mt.Name, p.Name
-- Find people in a specific subgroup
SELECT p.Name, p.EmailAddress
FROM OrgMemMemTags ommt
JOIN MemberTags mt ON ommt.MemberTagId = mt.Id
JOIN People p ON ommt.PeopleId = p.PeopleId
WHERE mt.OrgId = 123
AND mt.Name = 'Blue Team' -- Match name exactly as stored
-- Count members per subgroup across all active orgs
SELECT
o.OrganizationName,
mt.Name as SubGroup,
COUNT(DISTINCT ommt.PeopleId) as MemberCount,
SUM(CASE WHEN ommt.IsLeader = 1 THEN 1 ELSE 0 END) as LeaderCount
FROM MemberTags mt
JOIN OrgMemMemTags ommt ON mt.Id = ommt.MemberTagId
JOIN Organizations o ON mt.OrgId = o.OrganizationId
WHERE o.OrganizationStatusId = 30
GROUP BY o.OrganizationName, mt.Name
ORDER BY o.OrganizationName, mt.Name
IsLeader is separate from the org-level MemberTypeId| Field | Type | Description | Notes |
|---|---|---|---|
PeopleId |
int | Person checked | Links to People.PeopleId |
Created |
datetime | When check was initiated | |
Updated |
datetime | When check was last updated | Use for expiration calculations |
ReportTypeID |
int | Type of check | 3 = training clearance |
ServiceCode |
nvarchar | Service provider code | |
Status |
nvarchar | Current status |
| Field | Type | Description | Notes |
|---|---|---|---|
PeopleId |
int | Volunteer person | Links to People.PeopleId |
ProcessedDate |
datetime | When application was processed | |
StatusId |
int | Application status | Links to lookup.VolApplicationStatus |
Standard |
bit | Standard clearance | |
Children |
bit | Children's ministry clearance | |
Leader |
bit | Leadership clearance |
-- Find volunteers with expiring background checks (3-year validity)
SELECT
p.Name,
p.EmailAddress,
bc.Updated as LastCheckDate,
DATEADD(YEAR, 3, bc.Updated) as ExpirationDate,
DATEDIFF(day, GETDATE(), DATEADD(YEAR, 3, bc.Updated)) as DaysUntilExpiry,
CASE
WHEN DATEADD(YEAR, 3, bc.Updated) <= GETDATE() THEN 'EXPIRED'
WHEN DATEADD(YEAR, 3, bc.Updated) <= DATEADD(day, 90, GETDATE()) THEN 'EXPIRING SOON'
ELSE 'Valid'
END as BGStatus
FROM BackgroundChecks bc
JOIN People p ON bc.PeopleId = p.PeopleId
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
ORDER BY ExpirationDate ASC
-- Volunteers serving with children's ministry
SELECT
p.Name,
v.ProcessedDate,
vs.Description as ApplicationStatus,
v.Children as ChildrenCleared,
v.Leader as LeaderCleared
FROM Volunteer v
JOIN People p ON v.PeopleId = p.PeopleId
LEFT JOIN lookup.VolApplicationStatus vs ON v.StatusId = vs.Id
WHERE v.Children = 1 -- Cleared for children's ministry
ORDER BY v.ProcessedDate DESC
TOP 10 to test logic-- Copy this template for every query
WHERE 1=1
-- People filters
AND p.IsDeceased = 0
AND p.ArchivedFlag = 0
-- Attendance filters
AND a.AttendanceFlag = 1
AND m.DidNotMeet = 0
-- Organization filters
AND o.OrganizationStatusId = 30
AND om.InactiveDate IS NULL
-- Contribution filters
AND c.ContributionTypeId NOT IN (6, 7, 8, 99)
AND c.ContributionStatusId = 0
-- Performance filters
AND DateField >= DATEADD(month, -3, GETDATE())
| Technique | When to Use | Example |
|---|---|---|
| Use TOP | Exploration & testing | SELECT TOP 100 |
| Date filters | Always on large tables | WHERE Date >= DATEADD(month, -1, GETDATE()) |
| Specific IDs | When possible | WHERE PeopleId IN (1,2,3) |
| EXISTS vs JOIN | Checking existence | WHERE EXISTS (SELECT 1...) |
| Column list | Always specify | Avoid SELECT * |
-- Query: Monthly Giving Summary
-- Purpose: Summarize giving by fund for board report
-- Author: [Your Name]
-- Date: [Current Date]
-- Performance: ~2 seconds on production
-- Dependencies: Contribution, ContributionFund, People tables
-- Business Rules:
-- - Excludes returned checks (6), reversed (7), pledges (8), event fees (99)
-- - Only includes posted contributions (StatusId = 0)
-- - Groups by calendar month
SELECT TOP 500
/* Month grouping */
YEAR(c.ContributionDate) as GiftYear,
MONTH(c.ContributionDate) as GiftMonth,
/* Fund analysis */
cf.FundName,
COUNT(DISTINCT c.PeopleId) as UniqueGivers,
SUM(c.ContributionAmount) as TotalAmount
FROM Contribution c
JOIN ContributionFund cf ON c.FundId = cf.FundId
WHERE c.ContributionTypeId NOT IN (6, 7, 8, 99) -- Exclude non-donations
AND c.ContributionStatusId = 0 -- Posted only
AND c.ContributionDate >= '2024-01-01'
GROUP BY
YEAR(c.ContributionDate),
MONTH(c.ContributionDate),
cf.FundId,
cf.FundName
ORDER BY
GiftYear DESC,
GiftMonth DESC,
TotalAmount DESC
People (PeopleId) ←─────────────────────────┐
│ │
├──→ Families (FamilyId) │
│ Groups family members │
│ │
├──→ People (SpouseId) │
│ Direct spouse link │
│ │
├──→ OrganizationMembers ←───────────────┤
│ │ │
│ └──→ Organizations (OrganizationId) │
│ │ │
│ ├──→ Meetings (MeetingId) ←────┤
│ │ │
│ └──→ Attend ←──────────────────┘
│ (PeopleId + MeetingId + OrganizationId)
│
├──→ Contribution (ContributionId)
│ │
│ └──→ ContributionFund (FundId)
│
├──→ Registration (RegistrationId)
│ │
│ └──→ RegPeople (RegPeopleId)
│ │
│ └──→ RegAnswer (RegAnswerId)
│ │
│ └──→ RegQuestion (RegQuestionId)
│
├──→ TaskNote (TaskNoteId)
│ About person tasks/notes
│
├──→ EmailQueueTo (EmailQueueToId)
│ │
│ └──→ EmailResponses (Event tracking)
│
└──→ TagPerson (138M+ records!)
│
└──→ Tag (TagId)
People Table Lookups:
├──→ lookup.MemberStatus (MemberStatusId)
├──→ lookup.Gender (GenderId)
├──→ lookup.MaritalStatus (MaritalStatusId)
├──→ lookup.FamilyPosition (PositionInFamilyId)
├──→ lookup.Campus (CampusId)
├──→ lookup.EntryPoint (EntryPointId)
├──→ lookup.BaptismStatus (BaptismStatusId)
└──→ lookup.DropType (DropCodeId)
Organization Table Lookups:
├──→ lookup.OrganizationStatus (OrganizationStatusId = 30 for Active)
├──→ lookup.OrganizationType (OrganizationTypeId)
├──→ lookup.Campus (CampusId)
└──→ Division → Program (Hierarchy)
Attendance Lookups:
├──→ lookup.AttendType (AttendanceTypeId)
└──→ lookup.MemberType (MemberTypeId)
└──→ lookup.AttendType (AttendanceTypeId)
-- Join lookup TABLES (not views) for human-readable values
-- NOTE: These are TABLES in the lookup schema, not views
SELECT
p.PeopleId,
p.Name2,
p.EmailAddress,
ms.Description as MemberStatus, -- From lookup TABLE
g.Description as Gender, -- From lookup TABLE
c.Description as Campus -- From lookup TABLE
FROM People p
LEFT JOIN lookup.MemberStatus ms ON p.MemberStatusId = ms.Id
LEFT JOIN lookup.Gender g ON p.GenderId = g.Id
LEFT JOIN lookup.Campus c ON p.CampusId = c.Id
WHERE p.IsDeceased = 0
AND p.ArchivedFlag = 0;
-- Use lookup tables for complete organization details
-- NOTE: Organization -> Division -> Program hierarchy
SELECT
o.OrganizationId,
o.OrganizationName,
p.Name as ProgramName,
d.Name as DivisionName,
os.Description as Status, -- From lookup TABLE
ot.Description as OrgType, -- From lookup TABLE
c.Description as Campus, -- From lookup TABLE
mt.Description as LeaderType -- From lookup TABLE
FROM Organizations o
LEFT JOIN Division d ON o.DivisionId = d.Id -- Division first
LEFT JOIN Program p ON d.ProgId = p.Id -- Then Program through Division
LEFT JOIN lookup.OrganizationStatus os ON o.OrganizationStatusId = os.Id
LEFT JOIN lookup.OrganizationType ot ON o.OrganizationTypeId = ot.Id
LEFT JOIN lookup.Campus c ON o.CampusId = c.Id
LEFT JOIN lookup.MemberType mt ON o.LeaderMemberTypeId = mt.Id
WHERE o.OrganizationStatusId = 30; -- Active organizations
-- Volunteer status tracking using lookup tables
-- Note: Volunteer table has StatusId, MVRStatusId, and TrainingStatusId
SELECT
p.Name2 as VolunteerName,
v.ProcessedDate,
vs.Description as ApplicationStatus, -- From lookup TABLE
mvrs.Description as MVRStatus, -- From lookup TABLE
ts.Description as TrainingStatus, -- From lookup TABLE
ms.Description as MemberStatus, -- From lookup TABLE
v.Standard,
v.Children,
v.Leader
FROM Volunteer v
JOIN People p ON v.PeopleId = p.PeopleId
LEFT JOIN lookup.VolApplicationStatus vs ON v.StatusId = vs.Id
LEFT JOIN lookup.VolApplicationStatus mvrs ON v.MVRStatusId = mvrs.Id
LEFT JOIN lookup.VolApplicationStatus ts ON v.TrainingStatusId = ts.Id
LEFT JOIN lookup.MemberStatus ms ON p.MemberStatusId = ms.Id
WHERE v.StatusId > 0
ORDER BY v.ProcessedDate DESC;
TouchPoint uses SQL Server views to provide simplified access to complex data relationships. Views are virtual tables that combine data from multiple tables through joins and can include calculated fields.
Views in TouchPoint are virtual tables that:
Important: Views are different from lookup tables. Views compute data dynamically, while lookup tables store reference data.
-- Discover all available views in your TouchPoint database
SELECT
v.TABLE_SCHEMA,
v.TABLE_NAME,
'VIEW' as OBJECT_TYPE,
COUNT(c.COLUMN_NAME) as COLUMN_COUNT
FROM INFORMATION_SCHEMA.VIEWS v
LEFT JOIN INFORMATION_SCHEMA.COLUMNS c
ON v.TABLE_SCHEMA = c.TABLE_SCHEMA
AND v.TABLE_NAME = c.TABLE_NAME
WHERE v.TABLE_SCHEMA NOT IN ('sys', 'INFORMATION_SCHEMA')
GROUP BY v.TABLE_SCHEMA, v.TABLE_NAME
ORDER BY v.TABLE_NAME;
-- Get detailed column information for a specific view
SELECT
c.COLUMN_NAME,
c.DATA_TYPE,
c.CHARACTER_MAXIMUM_LENGTH,
c.IS_NULLABLE,
c.ORDINAL_POSITION
FROM INFORMATION_SCHEMA.COLUMNS c
WHERE c.TABLE_SCHEMA = 'lookup' -- Change schema name
AND c.TABLE_NAME = 'MemberStatus' -- Change view name
ORDER BY c.ORDINAL_POSITION;
⚠️ IMPORTANT - SET QUOTED_IDENTIFIER Requirement:
Some views (particularly TransactionList, TransactionSummary, OrgSchedules2, InProgressRegistrations, and RegistrationList) require:
SET QUOTED_IDENTIFIER ON;
If you get error "SELECT failed because the following SET options have incorrect settings", add this before your query.
TouchPoint contains 107 database views across 4 schemas: custom (1), dbo (82), export (23), and lookup (1).
The table below lists ALL views with their descriptions. Detailed SQL examples follow in the next section.
| Schema | View Name | Description |
|---|---|---|
| CUSTOM SCHEMA (1 view) | ||
| custom | MissionTripTotals_Optimized |
Optimized mission trip financial summary |
| DBO SCHEMA (82 views) | ||
| dbo | AccessUserInfo |
User access and permission details |
| dbo | ActiveRegistrations |
Currently active event/class registrations |
| dbo | ActivityAll |
All system activity tracking |
| dbo | AddressInfo |
Formatted address information for people |
| dbo | AllLookups |
Consolidated view of all lookup tables |
| dbo | AllStatusFlags |
All status flags across the system |
| dbo | AppRegistrations |
Mobile app registration data |
| dbo | AttendCredits |
Attendance credits by person and week |
| dbo | AttendCredits2 |
Alternative attendance credit calculation |
| dbo | Attributes |
Person and organization attribute definitions |
| dbo | BundleList |
Contribution bundle summaries |
| dbo | ChAiGiftData |
AI-ready gift/contribution data |
| dbo | ChAiIndividualData |
AI-ready individual person data |
| dbo | ChangeLogDetails |
Audit trail change summary |
| dbo | CheckInActivities |
Check-in system activity log (Note: column is 'Activities' plural) |
| dbo | Churches |
Church/campus location information |
| dbo | City |
City list for address standardization |
| dbo | ContributionsBasic |
Simplified contribution data |
| dbo | ContributionsBasicPledges |
Contribution pledges simplified view |
| dbo | ContributionsView |
Full contribution details with donor info |
| dbo | CreditGiver |
Credit card giving information |
| dbo | CustomMenuRoles |
Custom menu items with role assignments |
| dbo | CustomScriptRoles |
Custom scripts with role permissions |
| dbo | DepositDateTotals |
Deposit totals by date |
| dbo | DonorProfileList |
Donor profile summaries |
| dbo | EmailDelegation |
Email sending delegation permissions |
| dbo | FailedEmails |
Failed email send attempts |
| dbo | FailedRecurringGiving |
Failed recurring donation attempts |
| dbo | FamilyFirstTimes |
Family first visit tracking |
| dbo | FirstAttends |
First attendance dates by person/org |
| dbo | FirstName |
First name search optimization view |
| dbo | FirstName2 |
Alternative first name search view |
| dbo | FirstNick |
First name/nickname combination search |
| dbo | FirstPersonSameEmail |
Primary person for duplicate emails |
| dbo | HeadOrSpouseWithEmail |
Household heads/spouses with emails |
| dbo | IncompleteTasks |
Open tasks not yet completed |
| dbo | InProgressRegistrations |
Registrations currently being processed |
| dbo | LastAttends |
Last attendance with contact info |
| dbo | LastName |
Last name search optimization view |
| dbo | ManagedGivingList |
Managed/recurring giving accounts |
| dbo | MasterOrgs |
Master organization hierarchy |
| dbo | MeetingConflicts |
Schedule conflict detection |
| dbo | MemberData |
Member information summary |
| dbo | MinistryInfo |
Ministry participation details |
| dbo | MissionTripTotals |
Mission trip financial summary |
| dbo | MobileAppActions |
Configurable mobile app menu actions |
| dbo | MoveSchedule |
Scheduled organization moves/transfers |
| dbo | Nick |
Nickname search view |
| dbo | OnlineRegQA |
Online registration questions/answers |
| dbo | OrganizationLeaders |
Organization leadership assignments |
| dbo | OrganizationStructure |
Org hierarchy with programs/divisions (Note: 'Vistors' column has typo) |
| dbo | OrgSchedules2 |
Organization meeting schedules |
| dbo | OrgsWithFees |
Organizations requiring fees |
| dbo | OrgsWithoutFees |
Free organizations (no fees) |
| dbo | PaymentProcessDetails |
Payment processing transaction details |
| dbo | PeopleBasicModifed |
Recently modified people records |
| dbo | PickListOrgs |
Organizations for selection lists |
| dbo | PickListOrgs2 |
Alternative org selection list |
| dbo | PrevAddress |
Previous address history |
| dbo | PreviousMemberCounts |
Historical member count tracking |
| dbo | ProspectCounts |
Prospect tracking statistics |
| dbo | RandNumber |
Random number generation utility |
| dbo | RecurringGivingDueForToday |
Today's scheduled recurring gifts (requires SET QUOTED_IDENTIFIER ON) |
| dbo | RegistrationList |
Event registration summaries |
| dbo | RegistrationTransactionSummary |
Registration payment summaries |
| dbo | RegsettingCounts |
Registration settings usage statistics |
| dbo | RegsettingMessages |
Registration confirmation messages |
| dbo | RegsettingOptions |
Registration option configurations |
| dbo | RegsettingUsage |
Registration settings utilization |
| dbo | SearchNoDiacritics |
Search without accent marks |
| dbo | SpouseOrHeadWithEmail |
Primary contacts with emails |
| dbo | Sprocs |
Stored procedure metadata |
| dbo | StatusFlagColumns |
Status flag column definitions |
| dbo | StatusFlagList |
Available status flags |
| dbo | StatusFlagNamesRoles |
Status flags with role permissions |
| dbo | TaskSearch |
Task search optimization view |
| dbo | TransactionBalances |
Outstanding transaction balances (requires SET QUOTED_IDENTIFIER ON) |
| dbo | TransactionList |
Transaction history list |
| dbo | TransactionSummary |
Transaction totals and summaries |
| dbo | Triggers |
Database trigger metadata |
| dbo | UserFeed |
User activity feed/timeline |
| dbo | UserLeaders |
User leadership assignments |
| dbo | UserList |
System user accounts with activity |
| dbo | UserRoles |
User role assignments |
| dbo | VolunteerTimes |
Volunteer service time tracking |
| EXPORT SCHEMA (23 views) | ||
| export | XpAttendance |
Attendance data for export |
| export | XpBackgroundCheck |
Background check data export |
| export | XpContact |
Contact history export |
| export | XpContactee |
Contact recipient export |
| export | XpContactor |
Contact initiator export |
| export | XpContribution |
Contribution data export (denormalized - uses text values not IDs) |
| export | XpDivision |
Division hierarchy export |
| export | XpDivOrg |
Division-organization mapping export |
| export | XpEnrollHistory |
Enrollment history export |
| export | XpFamily |
Family units export (no CreatedDate column) |
| export | XpFamilyExtra |
Family custom fields export |
| export | XpMeeting |
Meeting data export |
| export | XpOrganization |
Organization details export |
| export | XpOrgMember |
Organization membership export |
| export | XpOrgSchedule |
Organization schedule export |
| export | XpPeople |
Complete person data export (78 columns - uses text values not IDs) |
| export | XpPeopleExtra |
Person custom fields export |
| export | XpProgDiv |
Program-division mapping export |
| export | XpProgram |
Program hierarchy export |
| export | XpRelatedFamily |
Related family connections export |
| export | XpSubGroup |
Sub-group membership export |
| LOOKUP SCHEMA (1 view) | ||
| lookup | PushPayBundleHeaderTypes |
PushPay bundle type definitions |
| View Name | Purpose | Key Columns | Example Usage |
|---|---|---|---|
PeopleBasicModifed |
Enhanced people data with computed fields | 21 columns including age calculations | SET QUOTED_IDENTIFIER ON; SELECT * FROM PeopleBasicModifed |
AddressInfo |
Complete address information with formatting | 16 columns: full addresses, city, state, zip | SELECT * FROM AddressInfo WHERE State = 'TN' |
HeadOrSpouseWithEmail |
Family heads/spouses with valid emails | PeopleId with email validation | SELECT * FROM HeadOrSpouseWithEmail |
SearchNoDiacritics |
Search-friendly names without special characters | PeopleId, FirstName, LastName, EmailAddress, phones | SELECT * FROM SearchNoDiacritics WHERE FirstName LIKE '%jose%' OR LastName LIKE '%jose%' |
FirstName, LastName, Nick |
Name parsing helpers | PeopleId, FirstName/LastName/Nick | SELECT * FROM FirstName WHERE FirstName LIKE 'John%' |
| View Name | Purpose | Key Columns | Example Usage |
|---|---|---|---|
ContributionsView |
Simplified contribution data with donor names | FundId, Amount, Name, Fund, Status, Type | SELECT * FROM ContributionsView WHERE Amount > 100 |
TransactionList |
⭐ Complete transaction details (66 columns!) Requires SET QUOTED_IDENTIFIER ON | Id, TransactionDate, First, Last, Amt, AmtPaid, AmtDue | SET QUOTED_IDENTIFIER ON; SELECT * FROM TransactionList WHERE TransactionDate > '2024-01-01' |
TransactionSummary |
Aggregated transaction totals Requires SET QUOTED_IDENTIFIER ON | RegId, PeopleId, OrganizationId, TotalAmt, TotPaid, TotDue, IndAmt, IndPaid | SET QUOTED_IDENTIFIER ON; SELECT * FROM TransactionSummary WHERE TotPaid > 0 |
BundleList |
Contribution bundle management | BundleHeaderId, CreatedDate, Status, TotalCash, TotalChecks | SELECT * FROM BundleList WHERE Status = 'Open' |
ManagedGivingList |
Recurring giving management | 14 columns: recurring schedules | SELECT * FROM ManagedGivingList |
RecurringGivingDueForToday |
Today's expected recurring gifts | PeopleId, Amount, NextDate | SELECT * FROM RecurringGivingDueForToday |
DepositDateTotals |
Daily deposit summaries | Date, Total, Count, FundId | SELECT * FROM DepositDateTotals WHERE DepositDate = CAST(GETDATE() as DATE) |
| View Name | Purpose | Key Columns | Example Usage |
|---|---|---|---|
CheckInActivities |
Real-time check-in tracking (plural, not CheckInActivity) | CheckInTime, PeopleId, Name, Location, Activities, AccessType | SELECT * FROM CheckInActivities WHERE CheckInTime > DATEADD(hour, -3, GETDATE()) |
LastAttends |
Most recent attendance by person | PeopleId, LastAttendedDate, OrganizationId, OrganizationName | SELECT * FROM LastAttends WHERE PeopleId IS NOT NULL |
FirstAttends |
First-time visitor tracking | PeopleId, CreatedDate, FirstAttendedDate | SELECT * FROM FirstAttends WHERE PeopleId IS NOT NULL |
AttendCredits, AttendCredits2 |
Attendance credit calculations | WeekOfYear, AttendanceCredits | SELECT * FROM AttendCredits WHERE PeopleId = 123 |
ActivityAll |
All system activities | Id, ActivityDate, Machine, Name, Type | SELECT * FROM ActivityAll WHERE ActivityDate > DATEADD(day, -7, GETDATE()) |
| View Name | Purpose | Key Columns | Example Usage |
|---|---|---|---|
OrganizationStructure |
⭐ Complete org hierarchy with counts Note: 'Vistors' column is misspelled | Program, Division, Organization, Members, Vistors (typo), Previous, Meetings | SELECT * FROM OrganizationStructure WHERE Members > 10 |
OrganizationLeaders |
Organization leadership roster | OrganizationId, OrganizationName, LeaderName | SELECT * FROM OrganizationLeaders WHERE OrganizationId IS NOT NULL |
OrgSchedules2 |
Organization meeting schedules Requires SET QUOTED_IDENTIFIER ON | OrganizationId, SchedTime, SchedDay (only 3 columns!) | SET QUOTED_IDENTIFIER ON; SELECT * FROM OrgSchedules2 WHERE SchedDay = 1 |
OrgsWithFees, OrgsWithoutFees |
Organizations by fee status | OrganizationId | SELECT * FROM OrgsWithFees |
MasterOrgs |
Parent organization list | OrgId, Name, ChildCount | SELECT * FROM MasterOrgs |
| View Name | Purpose | Key Columns | Example Usage |
|---|---|---|---|
RegistrationList |
⭐ Complete registration data (24 columns) Requires SET QUOTED_IDENTIFIER ON | Id, PeopleId, OrganizationId, UserData columns | SET QUOTED_IDENTIFIER ON; SELECT * FROM RegistrationList |
ActiveRegistrations |
Currently active registrations | RegistrationId | SELECT * FROM ActiveRegistrations |
InProgressRegistrations |
Incomplete registrations Requires SET QUOTED_IDENTIFIER ON | Name, OrganizationName, Stamp, PeopleId, OrganizationId, RegDataId | SET QUOTED_IDENTIFIER ON; SELECT * FROM InProgressRegistrations WHERE Stamp > DATEADD(day, -7, GETDATE()) |
RegistrationTransactionSummary |
Registration financial summary | 23 columns: payment details | SELECT * FROM RegistrationTransactionSummary WHERE TotalPaid > 0 |
RegsettingOptions |
Registration form settings (55 columns!) | InvId, Involvement, plus 53 setting columns | SELECT * FROM RegsettingOptions WHERE InvId = 100 |
| View Name | Purpose | Key Columns | Example Usage |
|---|---|---|---|
IncompleteTasks |
Open task management (19 columns) | Id, OwnerId, WhoId, Description, StatusId, CreatedOn, Due | SELECT * FROM IncompleteTasks WHERE Due < GETDATE() |
TaskSearch |
Comprehensive task search (26 columns) | All task fields for searching | SELECT * FROM TaskSearch WHERE Description LIKE '%follow up%' |
StatusFlagColumns |
All status flags as columns (29 flags) | PeopleId + 29 flag columns | SELECT * FROM StatusFlagColumns WHERE PeopleId = 123 |
AllStatusFlags |
Status flag definitions | FlagId, Name, Description | SELECT * FROM AllStatusFlags |
| View Name | Purpose | Key Columns | Example Usage |
|---|---|---|---|
UserList |
System user accounts | Username, UserId, Name, EmailAddress, LastActivityDate, PeopleId, Roles | SELECT * FROM UserList WHERE LastActivityDate > DATEADD(day, -30, GETDATE()) |
UserRoles |
User role assignments | Name, EmailAddress, Roles (concatenated comma-separated list) | SELECT * FROM UserRoles WHERE Roles LIKE '%Admin%' |
ChangeLogDetails |
Audit trail details | Id, PeopleId, Section, Created, UserPeopleId, Field, Before, After | SELECT * FROM ChangeLogDetails WHERE Created > DATEADD(day, -1, GETDATE()) |
CustomMenuRoles, CustomScriptRoles |
Custom permissions | Role-based access controls | SELECT * FROM CustomScriptRoles |
| View Name | Purpose | Key Columns | Example Usage |
|---|---|---|---|
EmailDelegation |
Email sending permissions | CanEmailId, CanEmailName, OnBehalfOfIds, OnBehalfOfNames | SELECT * FROM EmailDelegation WHERE CanEmailId = 123 |
FailedEmails |
Failed email tracking | time, Id, PeopleId, Fail (only 4 columns!) | SELECT * FROM FailedEmails WHERE time > DATEADD(day, -7, GETDATE()) |
FirstPersonSameEmail |
Duplicate email detection | EmailAddress, PeopleId | SELECT * FROM FirstPersonSameEmail |
| View Name | Purpose | Key Columns | Example Usage |
|---|---|---|---|
MemberData |
Member statistics and metrics | PeopleId, First, Last, Age, Marital, DecisionDt, JoinDt, Decision, Baptism | SELECT * FROM MemberData WHERE JoinDt > '2024-01-01' |
ChAiGiftData, ChAiIndividualData |
Church analytics data | ChAiGiftData: ContributionId, ContributionDate, ContributionAmount, FundName | SELECT * FROM ChAiGiftData WHERE ContributionAmount > 100 |
DonorProfileList |
Donor segmentation | PeopleId, SpouseId, FamilyId, prof (encoded profile data) | SELECT * FROM DonorProfileList WHERE PeopleId IS NOT NULL |
ProspectCounts, PreviousMemberCounts |
Member status counts | StatusId, Count | SELECT * FROM ProspectCounts |
MissionTripTotals |
Mission trip summaries | InvolvementId, Trip, PeopleId, Name, TripCost, Raised, Due | SELECT * FROM MissionTripTotals WHERE Due > 0 |
-- Use OrganizationStructure for hierarchy and member counts
-- NOTE: Column is misspelled as 'Vistors' in the view (missing 'i')
SELECT
Program,
Division,
Organization,
OrgStatus,
Members,
Previous,
Vistors, -- Note: typo in actual view column name
Meetings,
CAST(ISNULL(Vistors * 100.0 / NULLIF(Members, 0), 0) as DECIMAL(10,2)) as VisitorPercent
FROM OrganizationStructure
WHERE ProgId = 1 -- Specific program
AND Members > 0
ORDER BY Program, Division, Organization;
-- Group summary by division
SELECT
Program,
Division,
COUNT(DISTINCT OrgId) as OrgCount,
SUM(Members) as TotalMembers,
SUM(Vistors) as TotalVisitors, -- Note: typo in column name
SUM(Meetings) as TotalMeetings
FROM OrganizationStructure
WHERE Members > 0
GROUP BY ProgId, Program, DivId, Division
ORDER BY Program, Division;
-- Get outstanding balances by person
SELECT
ts.PeopleId,
p.Name2,
ts.OrganizationId,
o.OrganizationName,
ts.TotDue,
ts.TotPaid,
(ts.TotDue - ts.TotPaid) as Balance,
ts.TranDate as LastTransaction
FROM TransactionSummary ts
JOIN People p ON ts.PeopleId = p.PeopleId
JOIN Organizations o ON ts.OrganizationId = o.OrganizationId
WHERE ts.IsLatestTransaction = 1
AND (ts.TotDue - ts.TotPaid) > 0 -- Outstanding balance
ORDER BY Balance DESC;
-- Mission trip financial analysis
-- NOTE: GoerSenderAmounts view doesn't exist - use MissionTripTotals instead
SET QUOTED_IDENTIFIER ON; -- Required for this view
SELECT
m.InvolvementId,
m.Trip,
m.PeopleId,
m.Name,
m.TripCost,
m.Raised,
m.Due,
CAST(ISNULL(m.Raised * 100.0 / NULLIF(m.TripCost, 0), 0) as DECIMAL(10,2)) as PercentRaised
FROM MissionTripTotals m
WHERE m.PeopleId IS NOT NULL -- Exclude undesignated funds
AND m.TripCost > 0
ORDER BY m.Due DESC;
-- First and Last Attendance Analysis
-- Note: Views already include person names and org details
SELECT
p.PeopleId,
p.Name2,
fa.OrganizationName,
fa.FirstAttend,
la.LastAttend,
DATEDIFF(day, fa.FirstAttend, la.LastAttend) as DaysAttending,
DATEDIFF(day, la.LastAttend, GETDATE()) as DaysSinceLastAttend,
la.EmailAddress,
la.CellPhone
FROM People p
LEFT JOIN (
SELECT PeopleId, MIN(FirstAttend) as FirstAttend,
MAX(OrganizationName) as OrganizationName
FROM FirstAttends
GROUP BY PeopleId
) fa ON p.PeopleId = fa.PeopleId
LEFT JOIN (
SELECT PeopleId, MAX(LastAttend) as LastAttend,
MAX(EmailAddress) as EmailAddress,
MAX(CellPhone) as CellPhone
FROM LastAttends
GROUP BY PeopleId
) la ON p.PeopleId = la.PeopleId
WHERE p.IsDeceased = 0
AND p.ArchivedFlag = 0
AND la.LastAttend >= DATEADD(year, -1, GETDATE())
ORDER BY DaysSinceLastAttend;
-- Organization Member Count Comparison
-- Note: PreviousMemberCounts only has OrganizationId and prevcount
SELECT TOP 100
os.Organization,
pmc.prevcount as PreviousCount,
os.Members as CurrentCount,
(os.Members - pmc.prevcount) as Change,
CAST(ISNULL((os.Members - pmc.prevcount) * 100.0 / NULLIF(pmc.prevcount, 0), 0) as DECIMAL(10,2)) as PercentChange
FROM PreviousMemberCounts pmc
JOIN OrganizationStructure os ON pmc.OrganizationId = os.OrgId
WHERE pmc.prevcount > 0
AND os.Members IS NOT NULL
ORDER BY ABS(os.Members - pmc.prevcount) DESC;
-- GOOD: Filtered view query with TOP
SELECT TOP 1000 *
FROM MyComplexView WITH (NOLOCK)
WHERE DateColumn >= DATEADD(month, -1, GETDATE())
ORDER BY DateColumn DESC;
-- BAD: Unfiltered view query (may timeout)
SELECT * FROM MyComplexView;
-- GOOD: Cache lookup values for repeated use
DECLARE @MemberStatusLookup TABLE (
Id INT,
Description NVARCHAR(100)
);
INSERT INTO @MemberStatusLookup
SELECT Id, Description FROM lookup.MemberStatus;
You CANNOT create views in TouchPoint. All SQL access is read-only. However, you can use temporary tables within a session to simulate view functionality.
-- Create a temporary table to simulate a view for active adult members
-- Temp tables exist only for your current session
SELECT
p.PeopleId,
p.Name2 as FullName,
p.EmailAddress,
p.CellPhone,
p.Age,
ms.Description as MemberStatus,
fp.Description as FamilyPosition,
c.Description as Campus,
la.LastAttendDate,
DATEDIFF(day, la.LastAttendDate, GETDATE()) as DaysSinceAttended
INTO #ActiveAdultMembers -- Creates temporary table
FROM People p
LEFT JOIN lookup.MemberStatus ms ON p.MemberStatusId = ms.Id
LEFT JOIN lookup.FamilyPosition fp ON p.PositionInFamilyId = fp.Id
LEFT JOIN lookup.Campus c ON p.CampusId = c.Id
LEFT JOIN (
-- Get last attendance date from Attend table
SELECT PeopleId, MAX(a.MeetingDate) as LastAttendDate
FROM Attend a
JOIN Meetings m ON a.MeetingId = m.MeetingId
WHERE a.AttendanceFlag = 1
GROUP BY PeopleId
) la ON p.PeopleId = la.PeopleId
WHERE p.IsDeceased = 0
AND p.ArchivedFlag = 0
AND p.MemberStatusId = 10 -- Members only
AND p.Age >= 18 -- Adults only
AND p.PositionInFamilyId IN (10, 20); -- Primary/Secondary adults
-- Now use the temp table like a view
SELECT * FROM #ActiveAdultMembers
WHERE DaysSinceAttended <= 30
ORDER BY DaysSinceAttended;
-- Temp table automatically drops when session ends
While you cannot create custom functions in TouchPoint's read-only environment, you can leverage SQL Server's extensive built-in functions and combine them with temporary tables for powerful data analysis.
This comprehensive reference lists all SQL Server built-in functions you can use in TouchPoint's read-only environment.
| Function | Category | Description | Example Usage |
|---|---|---|---|
| 📝 String Functions | |||
CONCAT() |
String | Concatenates two or more strings | CONCAT(FirstName, ' ', LastName) |
SUBSTRING() |
String | Extracts part of a string | SUBSTRING(Phone, 1, 3) |
LEFT() |
String | Returns leftmost characters | LEFT(PostalCode, 5) |
RIGHT() |
String | Returns rightmost characters | RIGHT(SSN, 4) |
LEN() |
String | Returns string length | LEN(EmailAddress) |
CHARINDEX() |
String | Finds position of substring | CHARINDEX('@', Email) |
REPLACE() |
String | Replaces all occurrences | REPLACE(Phone, '-', '') |
UPPER() |
String | Converts to uppercase | UPPER(City) |
LOWER() |
String | Converts to lowercase | LOWER(EmailAddress) |
LTRIM() |
String | Removes leading spaces | LTRIM(Name) |
RTRIM() |
String | Removes trailing spaces | RTRIM(Name) |
TRIM() |
String | Removes leading/trailing spaces (2017+) | TRIM(Name) |
STRING_AGG() |
String | Concatenates rows (2017+) | STRING_AGG(Name, ', ') |
STRING_SPLIT() |
String | Splits delimited string (2016+) | STRING_SPLIT('a,b,c', ',') |
REVERSE() |
String | Reverses string | REVERSE('hello') |
REPLICATE() |
String | Repeats string N times | REPLICATE('*', 10) |
STUFF() |
String | Inserts string into another | STUFF(Phone, 4, 0, '-') |
FORMAT() |
String | Formats values (2012+) | FORMAT(Amount, 'C') |
QUOTENAME() |
String | Adds delimiters for SQL identifiers | QUOTENAME('Table Name') |
PATINDEX() |
String | Pattern position in string | PATINDEX('%[0-9]%', Address) |
| 📅 Date & Time Functions | |||
GETDATE() |
Date | Current date and time | GETDATE() |
GETUTCDATE() |
Date | Current UTC date and time | GETUTCDATE() |
DATEADD() |
Date | Adds interval to date | DATEADD(day, 7, GETDATE()) |
DATEDIFF() |
Date | Difference between dates | DATEDIFF(year, BirthDate, GETDATE()) |
DATEPART() |
Date | Extract date part as integer | DATEPART(month, BirthDate) |
DATENAME() |
Date | Extract date part as string | DATENAME(weekday, BirthDate) |
DAY() |
Date | Extract day of month | DAY(BirthDate) |
MONTH() |
Date | Extract month | MONTH(BirthDate) |
YEAR() |
Date | Extract year | YEAR(BirthDate) |
EOMONTH() |
Date | End of month date (2012+) | EOMONTH(GETDATE()) |
DATEFROMPARTS() |
Date | Create date from parts (2012+) | DATEFROMPARTS(2024, 12, 25) |
ISDATE() |
Date | Check if valid date | ISDATE('2024-01-01') |
SWITCHOFFSET() |
Date | Change timezone offset | SWITCHOFFSET(GETDATE(), '+00:00') |
SYSDATETIME() |
Date | High precision current datetime | SYSDATETIME() |
| 🔢 Mathematical Functions | |||
ABS() |
Math | Absolute value | ABS(-123) |
CEILING() |
Math | Round up to integer | CEILING(123.45) |
FLOOR() |
Math | Round down to integer | FLOOR(123.45) |
ROUND() |
Math | Round to specified decimals | ROUND(123.456, 2) |
POWER() |
Math | Raise to power | POWER(2, 10) |
SQRT() |
Math | Square root | SQRT(16) |
RAND() |
Math | Random number 0-1 | RAND() |
SIGN() |
Math | Returns -1, 0, or 1 | SIGN(Amount) |
EXP() |
Math | Exponential (e^x) | EXP(1) |
LOG() |
Math | Natural logarithm | LOG(10) |
LOG10() |
Math | Base-10 logarithm | LOG10(100) |
PI() |
Math | Returns PI value | PI() |
COS() |
Math | Cosine | COS(0) |
SIN() |
Math | Sine | SIN(PI()/2) |
TAN() |
Math | Tangent | TAN(PI()/4) |
| 📊 Aggregate Functions | |||
COUNT() |
Aggregate | Count rows | COUNT(*) |
COUNT(DISTINCT) |
Aggregate | Count unique values | COUNT(DISTINCT PeopleId) |
SUM() |
Aggregate | Sum values | SUM(Amount) |
AVG() |
Aggregate | Average value | AVG(Amount) |
MIN() |
Aggregate | Minimum value | MIN(ContributionDate) |
MAX() |
Aggregate | Maximum value | MAX(ContributionDate) |
STDEV() |
Aggregate | Standard deviation | STDEV(Amount) |
STDEVP() |
Aggregate | Population standard deviation | STDEVP(Amount) |
VAR() |
Aggregate | Statistical variance | VAR(Amount) |
VARP() |
Aggregate | Population variance | VARP(Amount) |
GROUPING() |
Aggregate | Identifies grouped rows | GROUPING(CampusId) |
| 🪟 Window Functions | |||
ROW_NUMBER() |
Window | Sequential row number | ROW_NUMBER() OVER (ORDER BY Name) |
RANK() |
Window | Rank with gaps | RANK() OVER (ORDER BY Amount DESC) |
DENSE_RANK() |
Window | Rank without gaps | DENSE_RANK() OVER (ORDER BY Score) |
NTILE() |
Window | Divide into N groups | NTILE(4) OVER (ORDER BY Amount) |
LAG() |
Window | Previous row value | LAG(Amount, 1) OVER (ORDER BY Date) |
LEAD() |
Window | Next row value | LEAD(Amount, 1) OVER (ORDER BY Date) |
FIRST_VALUE() |
Window | First value in window | FIRST_VALUE(Name) OVER (PARTITION BY FamilyId) |
LAST_VALUE() |
Window | Last value in window | LAST_VALUE(Date) OVER (PARTITION BY PeopleId) |
PERCENT_RANK() |
Window | Relative rank (0-1) | PERCENT_RANK() OVER (ORDER BY Score) |
CUME_DIST() |
Window | Cumulative distribution | CUME_DIST() OVER (ORDER BY Amount) |
PERCENTILE_CONT() |
Window | Continuous percentile | PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY Amount) |
PERCENTILE_DISC() |
Window | Discrete percentile | PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY Amount) |
| 🔄 Conversion Functions | |||
CAST() |
Conversion | Convert data type | CAST('123' AS INT) |
CONVERT() |
Conversion | Convert with style | CONVERT(VARCHAR, GETDATE(), 101) |
TRY_CAST() |
Conversion | Safe cast (returns NULL) | TRY_CAST('bad' AS INT) |
TRY_CONVERT() |
Conversion | Safe convert | TRY_CONVERT(DATE, '2024-13-45') |
PARSE() |
Conversion | Parse string to type (2012+) | PARSE('€1,234.56' AS MONEY USING 'de-DE') |
TRY_PARSE() |
Conversion | Safe parse (2012+) | TRY_PARSE('bad' AS DATE) |
| 🔀 Logical Functions | |||
IIF() |
Logical | Inline if-then-else (2012+) | IIF(Age >= 18, 'Adult', 'Minor') |
CHOOSE() |
Logical | Select by index (2012+) | CHOOSE(2, 'A', 'B', 'C') |
CASE |
Logical | Conditional logic | CASE WHEN x > 0 THEN 'Positive' END |
COALESCE() |
Logical | First non-null value | COALESCE(Email, Phone, 'No contact') |
ISNULL() |
Logical | Replace NULL | ISNULL(MiddleName, '') |
NULLIF() |
Logical | Return NULL if equal | NULLIF(Division, 0) |
| ⚙️ System Functions | |||
@@ROWCOUNT |
System | Rows affected by last statement | SELECT @@ROWCOUNT |
@@IDENTITY |
System | Last inserted identity | SELECT @@IDENTITY |
@@ERROR |
System | Last error number | SELECT @@ERROR |
@@SERVERNAME |
System | Server name | SELECT @@SERVERNAME |
@@VERSION |
System | SQL Server version | SELECT @@VERSION |
DB_NAME() |
System | Current database name | SELECT DB_NAME() |
USER_NAME() |
System | Current user name | SELECT USER_NAME() |
NEWID() |
System | Generate GUID | SELECT NEWID() |
CHECKSUM() |
System | Calculate checksum | CHECKSUM(Name, Email) |
HASHBYTES() |
System | Generate hash | HASHBYTES('SHA2_256', Password) |
| 📋 JSON Functions (SQL Server 2016+) | |||
ISJSON() |
JSON | Validate JSON | ISJSON(Data) |
JSON_VALUE() |
JSON | Extract scalar value | JSON_VALUE(Data, '$.name') |
JSON_QUERY() |
JSON | Extract object/array | JSON_QUERY(Data, '$.address') |
JSON_MODIFY() |
JSON | Update JSON value | JSON_MODIFY(Data, '$.age', 25) |
OPENJSON() |
JSON | Parse JSON to table | OPENJSON(@json) |
FOR JSON |
JSON | Format results as JSON | SELECT * FOR JSON AUTO |
| 📄 XML Functions | |||
FOR XML |
XML | Format as XML | SELECT * FOR XML PATH |
.value() |
XML | Extract XML value | @xml.value('(/root/item)[1]', 'varchar(100)') |
.query() |
XML | Query XML | @xml.query('/root/items') |
.exist() |
XML | Check XML existence | @xml.exist('/root/item') |
.modify() |
XML | Modify XML | @xml.modify('insert |
| 🔧 Other Useful Functions | |||
ISNUMERIC() |
Validation | Check if numeric | ISNUMERIC('123.45') |
ISDATE() |
Validation | Check if valid date | ISDATE('2024-01-01') |
ERROR_MESSAGE() |
Error | Get error message | ERROR_MESSAGE() |
ERROR_NUMBER() |
Error | Get error number | ERROR_NUMBER() |
ERROR_LINE() |
Error | Get error line | ERROR_LINE() |
SCOPE_IDENTITY() |
Identity | Last identity in scope | SCOPE_IDENTITY() |
IDENT_CURRENT() |
Identity | Current identity for table | IDENT_CURRENT('People') |
-- Common string manipulation functions
SELECT
PeopleId,
Name,
-- Remove extra spaces
LTRIM(RTRIM(Name)) as CleanName,
-- Extract parts (using actual column names)
LEFT(CellPhone, 3) as AreaCode,
RIGHT(ZipCode, 4) as Plus4,
SUBSTRING(EmailAddress, CHARINDEX('@', EmailAddress) + 1, 100) as EmailDomain,
-- Transform case (using actual column names)
UPPER(CityName) as CityUpper,
LOWER(EmailAddress) as EmailLower,
-- Replace and format
REPLACE(HomePhone, '-', '') as PhoneDigitsOnly,
CONCAT(FirstName, ' ', LastName) as FullName,
-- Pattern matching
CASE
WHEN EmailAddress LIKE '%gmail.com' THEN 'Gmail'
WHEN EmailAddress LIKE '%yahoo.com' THEN 'Yahoo'
WHEN EmailAddress LIKE '%outlook.com' THEN 'Outlook'
ELSE 'Other'
END as EmailProvider
FROM People
WHERE IsDeceased = 0;
-- String aggregation (SQL 2017+) - Cannot use OVER clause with STRING_AGG
-- Must use GROUP BY instead
SELECT
FamilyId,
STRING_AGG(Name, ', ') WITHIN GROUP (ORDER BY Name) as FamilyMembers,
COUNT(*) as FamilyMemberCount
FROM People
WHERE FamilyId IS NOT NULL
AND IsDeceased = 0
GROUP BY FamilyId
HAVING COUNT(*) > 1;
-- Date manipulation and calculations
SELECT
PeopleId,
Name,
BDate, -- TouchPoint uses BDate, not BirthDate
-- Age calculations
DATEDIFF(year, BDate, GETDATE()) as AgeInYears,
DATEDIFF(month, BDate, GETDATE()) / 12 as ExactAge,
-- Date parts
YEAR(BDate) as BirthYear,
MONTH(BDate) as BirthMonth,
DATENAME(month, BDate) as BirthMonthName,
DATEPART(week, BDate) as BirthWeek,
DATENAME(weekday, BDate) as BirthDayOfWeek,
-- Date formatting
FORMAT(BDate, 'MMM dd, yyyy') as FormattedBirthDate,
CONVERT(VARCHAR(10), BDate, 101) as USDateFormat,
-- Date calculations
DATEADD(year, 1, BDate) as FirstBirthday,
EOMONTH(BDate) as EndOfBirthMonth,
-- Upcoming birthday
CASE
WHEN DATEPART(dayofyear, BDate) >= DATEPART(dayofyear, GETDATE())
THEN DATEDIFF(day, GETDATE(),
DATEADD(year, YEAR(GETDATE()) - YEAR(BDate), BDate))
ELSE DATEDIFF(day, GETDATE(),
DATEADD(year, YEAR(GETDATE()) - YEAR(BDate) + 1, BDate))
END as DaysUntilBirthday
FROM People
WHERE BDate IS NOT NULL;
-- Mathematical calculations for reporting
SELECT
-- Giving statistics
PeopleId,
COUNT(*) as GiftCount,
SUM(ContributionAmount) as TotalGiving,
AVG(ContributionAmount) as AverageGift,
MIN(ContributionAmount) as SmallestGift,
MAX(ContributionAmount) as LargestGift,
STDEV(ContributionAmount) as GivingStdDev,
-- Rounding and formatting
ROUND(AVG(ContributionAmount), 2) as RoundedAverage,
CEILING(AVG(ContributionAmount)) as RoundUp,
FLOOR(AVG(ContributionAmount)) as RoundDown,
-- Percentage calculations
CAST(SUM(ContributionAmount) * 100.0 /
SUM(SUM(ContributionAmount)) OVER() as DECIMAL(5,2)) as PercentOfTotal,
-- Quartile calculations using NTILE
NTILE(4) OVER (ORDER BY SUM(ContributionAmount)) as GivingQuartile
FROM Contribution
WHERE ContributionDate >= '2024-01-01'
AND ContributionTypeId NOT IN (6, 7, 8, 99)
GROUP BY PeopleId;
-- Type conversion examples
SELECT
-- String to number conversions
CAST('123' as INT) as StringToInt,
CONVERT(DECIMAL(10,2), '123.45') as StringToDecimal,
TRY_CAST('bad data' as INT) as SafeCast, -- Returns NULL instead of error
TRY_CONVERT(DATE, '2024-13-45') as SafeDateConvert,
-- Number to string formatting
CAST(12345.67 as VARCHAR(20)) as NumberToString,
FORMAT(12345.67, 'N2') as FormattedNumber,
FORMAT(12345.67, 'C', 'en-US') as CurrencyFormat,
-- Date conversions
CAST(GETDATE() as DATE) as DateOnly,
CAST(GETDATE() as TIME) as TimeOnly,
CONVERT(VARCHAR(10), GETDATE(), 101) as USDateString,
-- Boolean conversions
CAST(CASE WHEN IsDeceased = 1 THEN 'Yes' ELSE 'No' END as VARCHAR(3)) as DeceasedText
FROM People
WHERE PeopleId = 1;
Window functions are extremely powerful for analytics and reporting. They allow calculations across sets of rows while keeping row-level detail.
-- Ranking donors by giving amount
WITH DonorRanking AS (
SELECT
p.Name,
SUM(c.ContributionAmount) as TotalGiving,
-- Different ranking methods
ROW_NUMBER() OVER (ORDER BY SUM(c.ContributionAmount) DESC) as RowNum,
RANK() OVER (ORDER BY SUM(c.ContributionAmount) DESC) as RankNum,
DENSE_RANK() OVER (ORDER BY SUM(c.ContributionAmount) DESC) as DenseRankNum,
PERCENT_RANK() OVER (ORDER BY SUM(c.ContributionAmount) DESC) as PercentileRank,
-- Grouping rankings
ROW_NUMBER() OVER (
PARTITION BY YEAR(c.ContributionDate)
ORDER BY SUM(c.ContributionAmount) DESC
) as YearlyRank
FROM People p
JOIN Contribution c ON p.PeopleId = c.PeopleId
WHERE c.ContributionDate >= '2024-01-01'
AND c.ContributionTypeId NOT IN (6, 7, 8, 99)
GROUP BY p.PeopleId, p.Name, YEAR(c.ContributionDate)
)
SELECT * FROM DonorRanking
WHERE RowNum <= 100; -- Top 100 donors
-- Running totals and moving averages
WITH AttendanceAnalysis AS (
SELECT
MeetingDate,
OrganizationId,
COUNT(*) as Attendance,
-- Running total
SUM(COUNT(*)) OVER (
PARTITION BY OrganizationId
ORDER BY MeetingDate
) as RunningTotal,
-- Moving average (last 4 weeks)
AVG(COUNT(*)) OVER (
PARTITION BY OrganizationId
ORDER BY MeetingDate
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW
) as MovingAvg4Week,
-- Comparison to average
COUNT(*) - AVG(COUNT(*)) OVER (
PARTITION BY OrganizationId
) as DifferenceFromAvg,
-- Min/Max in window
MIN(COUNT(*)) OVER (
PARTITION BY OrganizationId
ORDER BY MeetingDate
ROWS BETWEEN 11 PRECEDING AND CURRENT ROW
) as Min12Week,
MAX(COUNT(*)) OVER (
PARTITION BY OrganizationId
ORDER BY MeetingDate
ROWS BETWEEN 11 PRECEDING AND CURRENT ROW
) as Max12Week
FROM Attend
WHERE AttendanceFlag = 1
AND MeetingDate >= DATEADD(month, -6, GETDATE())
GROUP BY MeetingDate, OrganizationId
)
SELECT * FROM AttendanceAnalysis
ORDER BY OrganizationId, MeetingDate;
-- Compare values with previous/next rows
WITH GivingTrends AS (
SELECT
PeopleId,
YEAR(ContributionDate) as GivingYear,
SUM(ContributionAmount) as YearlyTotal,
-- Previous year's giving
LAG(SUM(ContributionAmount), 1, 0) OVER (
PARTITION BY PeopleId
ORDER BY YEAR(ContributionDate)
) as PreviousYearTotal,
-- Next year's giving (if exists)
LEAD(SUM(ContributionAmount), 1) OVER (
PARTITION BY PeopleId
ORDER BY YEAR(ContributionDate)
) as NextYearTotal,
-- Year-over-year change
SUM(ContributionAmount) - LAG(SUM(ContributionAmount), 1, 0) OVER (
PARTITION BY PeopleId
ORDER BY YEAR(ContributionDate)
) as YoYChange,
-- Percentage change
CASE
WHEN LAG(SUM(ContributionAmount), 1) OVER (
PARTITION BY PeopleId ORDER BY YEAR(ContributionDate)
) > 0
THEN CAST(
(SUM(ContributionAmount) - LAG(SUM(ContributionAmount), 1) OVER (
PARTITION BY PeopleId ORDER BY YEAR(ContributionDate)
)) * 100.0 / LAG(SUM(ContributionAmount), 1) OVER (
PARTITION BY PeopleId ORDER BY YEAR(ContributionDate)
) as DECIMAL(10,2)
)
ELSE NULL
END as YoYPercentChange
FROM Contribution
WHERE ContributionTypeId NOT IN (6, 7, 8, 99)
GROUP BY PeopleId, YEAR(ContributionDate)
)
SELECT * FROM GivingTrends
WHERE PeopleId IN (SELECT TOP 100 PeopleId FROM People)
ORDER BY PeopleId, GivingYear;
While you can't create custom functions, you can simulate table-valued function behavior using temporary tables and CTEs.
-- Using CROSS APPLY to simulate a function that gets last N attendances
WITH PersonList AS (
SELECT TOP 100 PeopleId, Name
FROM People
WHERE MemberStatusId = 10
)
SELECT
pl.PeopleId,
pl.Name,
la.LastAttendances
FROM PersonList pl
CROSS APPLY (
SELECT STRING_AGG(
CONVERT(VARCHAR(10), MeetingDate, 101), ', '
) WITHIN GROUP (ORDER BY MeetingDate DESC) as LastAttendances
FROM (
SELECT TOP 5 a.MeetingDate
FROM Attend a
WHERE a.PeopleId = pl.PeopleId
AND a.AttendanceFlag = 1
ORDER BY a.MeetingDate DESC
) last5
) la;
-- Alternative using temp table for reusability
CREATE TABLE #LastAttendances (
PeopleId INT,
AttendanceList VARCHAR(MAX)
);
INSERT INTO #LastAttendances
SELECT
p.PeopleId,
STRING_AGG(
CONVERT(VARCHAR(10), a.MeetingDate, 101), ', '
) WITHIN GROUP (ORDER BY a.MeetingDate DESC)
FROM People p
CROSS APPLY (
SELECT TOP 5 MeetingDate
FROM Attend
WHERE PeopleId = p.PeopleId
AND AttendanceFlag = 1
ORDER BY MeetingDate DESC
) a
WHERE p.MemberStatusId = 10
GROUP BY p.PeopleId;
-- Now use like a function result
SELECT
p.Name,
la.AttendanceList
FROM People p
JOIN #LastAttendances la ON p.PeopleId = la.PeopleId;
-- Working with JSON data in Extra Value fields
SELECT
PeopleId,
Field,
Data,
-- Extract JSON values
JSON_VALUE(Data, '$.firstName') as FirstName,
JSON_VALUE(Data, '$.lastName') as LastName,
JSON_VALUE(Data, '$.preferences.emailOptIn') as EmailOptIn,
-- Query JSON arrays
JSON_QUERY(Data, '$.children') as ChildrenArray,
-- Check if valid JSON
ISJSON(Data) as IsValidJson
FROM PeopleExtra
WHERE Field = 'CustomData'
AND ISJSON(Data) = 1;
-- Parse JSON array
SELECT
p.PeopleId,
p.Name,
children.ChildName,
children.ChildAge
FROM People p
CROSS APPLY OPENJSON(
(SELECT Data FROM PeopleExtra
WHERE PeopleId = p.PeopleId AND Field = 'Children')
) WITH (
ChildName NVARCHAR(100) '$.name',
ChildAge INT '$.age'
) as children;
-- Useful system functions for debugging and analysis
SELECT
-- Session information
@@SERVERNAME as ServerName,
DB_NAME() as DatabaseName,
USER_NAME() as CurrentUser,
GETDATE() as CurrentDateTime,
@@VERSION as SQLServerVersion,
-- Query performance helpers
@@ROWCOUNT as LastRowCount,
-- Null handling
ISNULL(NULL, 'Default') as NullReplacement,
COALESCE(NULL, NULL, 'First Non-Null') as FirstNonNull,
NULLIF(0, 0) as ReturnsNull,
-- Type checking
ISNUMERIC('123.45') as IsNumber,
ISDATE('2024-01-01') as IsDate;
-- Calculate custom engagement score using multiple functions
WITH EngagementData AS (
SELECT
p.PeopleId,
p.Name,
-- Attendance score (0-40 points)
CAST(
CASE
WHEN att.AttendanceCount >= 12 THEN 40
WHEN att.AttendanceCount >= 8 THEN 30
WHEN att.AttendanceCount >= 4 THEN 20
WHEN att.AttendanceCount >= 1 THEN 10
ELSE 0
END as INT
) as AttendanceScore,
-- Giving score (0-30 points)
CAST(
CASE
WHEN giv.TotalGiving >= 1000 THEN 30
WHEN giv.TotalGiving >= 500 THEN 20
WHEN giv.TotalGiving >= 100 THEN 10
WHEN giv.TotalGiving > 0 THEN 5
ELSE 0
END as INT
) as GivingScore,
-- Involvement score (0-30 points)
CAST(
CASE
WHEN inv.OrgCount >= 3 THEN 30
WHEN inv.OrgCount = 2 THEN 20
WHEN inv.OrgCount = 1 THEN 10
ELSE 0
END as INT
) as InvolvementScore
FROM People p
LEFT JOIN (
SELECT PeopleId, COUNT(*) as AttendanceCount
FROM Attend
WHERE AttendanceFlag = 1
AND MeetingDate >= DATEADD(month, -3, GETDATE())
GROUP BY PeopleId
) att ON p.PeopleId = att.PeopleId
LEFT JOIN (
SELECT PeopleId, SUM(ContributionAmount) as TotalGiving
FROM Contribution
WHERE ContributionDate >= DATEADD(month, -3, GETDATE())
AND ContributionTypeId NOT IN (6, 7, 8, 99)
GROUP BY PeopleId
) giv ON p.PeopleId = giv.PeopleId
LEFT JOIN (
SELECT PeopleId, COUNT(DISTINCT OrganizationId) as OrgCount
FROM OrganizationMembers
WHERE OrganizationId IN (
SELECT OrganizationId FROM Organizations
WHERE OrganizationStatusId = 30
)
GROUP BY PeopleId
) inv ON p.PeopleId = inv.PeopleId
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
)
SELECT
PeopleId,
Name,
AttendanceScore,
GivingScore,
InvolvementScore,
AttendanceScore + GivingScore + InvolvementScore as TotalScore,
-- Percentile ranking
PERCENT_RANK() OVER (
ORDER BY AttendanceScore + GivingScore + InvolvementScore
) * 100 as PercentileRank,
-- Categorization
CASE
WHEN AttendanceScore + GivingScore + InvolvementScore >= 70 THEN 'Highly Engaged'
WHEN AttendanceScore + GivingScore + InvolvementScore >= 40 THEN 'Engaged'
WHEN AttendanceScore + GivingScore + InvolvementScore >= 20 THEN 'Somewhat Engaged'
WHEN AttendanceScore + GivingScore + InvolvementScore > 0 THEN 'Minimally Engaged'
ELSE 'Not Engaged'
END as EngagementLevel
FROM EngagementData
ORDER BY TotalScore DESC;
-- Comprehensive family analysis using multiple functions
WITH FamilyAnalysis AS (
SELECT
f.FamilyId,
-- Family composition
COUNT(DISTINCT p.PeopleId) as FamilySize,
STRING_AGG(p.Name, ', ') WITHIN GROUP (ORDER BY p.PositionInFamilyId, p.Age DESC) as FamilyMembers,
-- Age statistics
MIN(p.Age) as YoungestAge,
MAX(p.Age) as OldestAge,
AVG(p.Age) as AverageAge,
-- Count by position
SUM(CASE WHEN p.PositionInFamilyId = 10 THEN 1 ELSE 0 END) as Adults,
SUM(CASE WHEN p.PositionInFamilyId = 30 THEN 1 ELSE 0 END) as Children,
-- Contact information
MAX(CASE WHEN p.PositionInFamilyId = 10 THEN p.EmailAddress END) as PrimaryEmail,
MAX(CASE WHEN p.PositionInFamilyId = 10 THEN p.CellPhone END) as PrimaryPhone,
-- Giving (family total)
ISNULL(fam_giving.TotalGiving, 0) as FamilyGiving,
-- Attendance (family average)
ISNULL(fam_attend.AvgAttendance, 0) as FamilyAttendance
FROM Families f
JOIN People p ON f.FamilyId = p.FamilyId
LEFT JOIN (
SELECT
p2.FamilyId,
SUM(c.ContributionAmount) as TotalGiving
FROM Contribution c
JOIN People p2 ON c.PeopleId = p2.PeopleId
WHERE c.ContributionDate >= DATEADD(year, -1, GETDATE())
AND c.ContributionTypeId NOT IN (6, 7, 8, 99)
GROUP BY p2.FamilyId
) fam_giving ON f.FamilyId = fam_giving.FamilyId
LEFT JOIN (
SELECT
p3.FamilyId,
AVG(CAST(person_attend.AttendCount as FLOAT)) as AvgAttendance
FROM People p3
JOIN (
SELECT PeopleId, COUNT(*) as AttendCount
FROM Attend
WHERE AttendanceFlag = 1
AND MeetingDate >= DATEADD(month, -3, GETDATE())
GROUP BY PeopleId
) person_attend ON p3.PeopleId = person_attend.PeopleId
GROUP BY p3.FamilyId
) fam_attend ON f.FamilyId = fam_attend.FamilyId
WHERE p.IsDeceased = 0
GROUP BY f.FamilyId, fam_giving.TotalGiving, fam_attend.AvgAttendance
)
SELECT
*,
-- Classification
CASE
WHEN Adults >= 2 AND Children > 0 THEN 'Traditional Family'
WHEN Adults = 1 AND Children > 0 THEN 'Single Parent'
WHEN Adults >= 2 AND Children = 0 THEN 'Couple'
WHEN Adults = 1 AND Children = 0 THEN 'Single'
ELSE 'Other'
END as FamilyType,
-- Engagement ranking
DENSE_RANK() OVER (ORDER BY FamilyGiving DESC) as GivingRank,
DENSE_RANK() OVER (ORDER BY FamilyAttendance DESC) as AttendanceRank
FROM FamilyAnalysis
WHERE FamilySize > 0
ORDER BY FamilyGiving DESC;
TouchPoint provides 337+ User-Defined Functions that extend SQL Server's capabilities for church-specific operations. While you cannot create new UDFs, you can use these existing functions in your queries.
Important: Your read-only database access means many UDFs may not work due to missing EXECUTE permissions. Functions are marked based on verified testing results:
Alternative: Use the standard SQL queries provided in examples instead of relying on UDFs that may not work.
| Function Name | Type | Description | Read-Only Access | Example Usage |
|---|---|---|---|---|
dbo.Age(PeopleId) |
Scalar | Returns person's current age | ❌ No access | dbo.Age(p.PeopleId) |
dbo.ComputeAge(birthdate) |
Scalar | Calculate age from birthdate | ⚠️ May not work | dbo.ComputeAge('1990-01-15') |
dbo.Birthday(PeopleId) |
Scalar | Returns person's birthday | ✅ Works | dbo.Birthday(123) |
dbo.NextBirthday(PeopleId) |
Scalar | Returns next birthday date | ✅ Works | dbo.NextBirthday(123) |
dbo.SpouseId(PeopleId) |
Scalar | Returns spouse's PeopleId | ❌ No access | dbo.SpouseId(123) |
dbo.HeadOfHouseholdId(FamilyId) |
Scalar | Returns head of household | ⚠️ May not work | dbo.HeadOfHouseholdId(f.FamilyId) |
dbo.FamilyMembers(FamilyId) |
Table-Valued | Returns all family members | 📘 Usually works | SELECT * FROM dbo.FamilyMembers(456) |
dbo.FindPerson(first, last, email) |
Table-Valued | Search for person by name/email | 📘 Usually works | SELECT * FROM dbo.FindPerson('John', 'Smith', NULL) |
dbo.UName(PeopleId) |
Scalar | Returns formatted name | ❌ No access | dbo.UName(123) |
dbo.UName2(PeopleId) |
Scalar | Returns name (Last, First) | ❌ No access | dbo.UName2(123) |
| Function Name | Type | Description | Read-Only Access | Parameters |
|---|---|---|---|---|
dbo.Contributions2 |
Inline Table | ⭐ MOST USED: Returns contributions with spouse handling & exclusions | 📘 Usually works | @fd, @td, @campusid, @pledges, @nontaxded, @includeUnclosed, @fundids |
dbo.ContributionAmount |
Scalar | Total contributions for person in date range | ✅ Works | @pid, @fromdate, @todate, @fundid |
dbo.ContributionCount |
Scalar | Count of contributions | ✅ Works | @pid, @fromdate, @todate |
dbo.GetTotalContributions |
Table-Valued | Detailed contribution totals by person | 📘 Usually works | @fd, @td |
dbo.GetTotalContributions2 |
Table-Valued | Enhanced contribution totals with funds | 📘 Usually works | @fd, @td, @campusid, @nontaxded |
dbo.PledgeAmount |
Scalar | Total pledge amount for person | ✅ Works | @pid, @fundid |
dbo.PledgeBalances |
Table-Valued | Outstanding pledge balances | 📘 Usually works | @fd, @td |
dbo.RecentGiver |
Table-Valued | People who gave recently | 📘 Usually works | @days |
dbo.FirstTimeGivers |
Table-Valued | Identify first-time donors | 📘 Usually works | @days1, @days2 |
dbo.GivingChange |
Table-Valued | Year-over-year giving changes | 📘 Usually works | @fd1, @td1, @fd2, @td2 |
| Function Name | Type | Description | Read-Only Access | Key Parameters |
|---|---|---|---|---|
dbo.LastAttend |
Scalar | Last attendance date for person | ❌ No access | @pid, @orgid |
dbo.AttendedAsOf |
Table-Valued | People who attended by date | 📘 Usually works | @progid, @divid, @orgid, @asof |
dbo.AttendanceCredits |
Table-Valued | Weekly attendance credits | 📘 Works | @orgid, @year |
dbo.RecentAttendance |
Table-Valued | Recent attendance records | 📘 Usually works | @orgid, @days |
dbo.RecentAbsents |
Table-Valued | Recently absent members | 📘 Usually works | @orgid, @divid, @days |
dbo.ConsecutiveAbsents |
Table-Valued | Members absent X weeks in row | 📘 Usually works | @orgid, @weeks |
dbo.AttendMemberTypeAsOf |
Table-Valued | Attendance by member type | 🚫 May not work | @orgid, @asof |
dbo.GetTodaysMeetingId |
Scalar | Today's meeting ID for org | ⚠️ May not work | @orgid, @thisday |
| Function Name | Type | Description | Read-Only Access | Usage |
|---|---|---|---|---|
dbo.OrgMember |
Table-Valued | Current org members | 📘 Usually works | @orgid |
dbo.OrgPeople |
Table-Valued | All people in org (all types) | 📘 Usually works | @orgid |
dbo.OrgPeopleCurrent |
Table-Valued | Current active members only | 📘 Usually works | @orgid |
dbo.OrgFilterGuests |
Table-Valued | Guest/visitor list | 📘 Usually works | @orgid |
dbo.OrganizationMemberCount |
Scalar | Count of current members | ⚠️ May not work | @orgid |
dbo.OrganizationLeaderName |
Scalar | Returns leader's name | ✅ Works | @orgid |
dbo.InSmallGroup |
Scalar | Check if in small group | ✅ Works | @pid |
dbo.RollList |
Table-Valued | Attendance roll sheet | 📘 Usually works | @meetingid, @orgid |
| Function Name | Type | Description | Read-Only Access | Example |
|---|---|---|---|---|
dbo.FmtPhone |
Scalar | Format phone number | ✅ Works | dbo.FmtPhone('1234567890') |
dbo.HomePhone |
Scalar | Get home phone | ❌ No access | dbo.HomePhone(123) |
dbo.IsValidEmail |
Scalar | Validate email format | ⚠️ May not work | dbo.IsValidEmail('test@example.com') |
dbo.UEmail |
Scalar | Get person's email | ❌ No access | dbo.UEmail(123) |
dbo.LastContact |
Scalar | Last contact date | ⚠️ May not work | dbo.LastContact(123) |
dbo.ContactSummary |
Table-Valued | Contact history summary | 📘 Usually works | @pid |
dbo.OptOuts |
Table-Valued | Email opt-out list | 📘 Usually works | @tagid |
| Function Name | Type | Description | Read-Only Access | Usage |
|---|---|---|---|---|
dbo.SplitInts |
Table-Valued | Split comma-separated integers | 📘 Usually works | SELECT * FROM dbo.SplitInts('1,2,3') |
dbo.SplitStrings |
Table-Valued | Split delimited strings | 📘 Usually works | SELECT * FROM dbo.SplitStrings('a;b;c', ';') |
dbo.ParseDate |
Scalar | Parse various date formats | ⚠️ May not work | dbo.ParseDate('01/15/2024') |
dbo.ElapsedTime |
Scalar | Time between dates | ⚠️ May not work | dbo.ElapsedTime(@start, @end) |
dbo.WeekNumber |
Scalar | Week number of date | ⚠️ May not work | dbo.WeekNumber('2024-01-15') |
dbo.SundayForDate |
Scalar | Sunday of week for date | ✅ Works | dbo.SundayForDate(GETDATE()) |
dbo.CompleteZip |
Scalar | Format ZIP code | ⚠️ May not work | dbo.CompleteZip('12345', '6789') |
| Function Name | Type | Description | Read-Only Access | Purpose |
|---|---|---|---|---|
dbo.FindPerson |
Table-Valued | Search for person | 📘 Works | Name/email search |
dbo.FindPersonByEmail |
Table-Valued | Search by email | 📘 Works | Email lookup |
dbo.TaggedPeople |
Table-Valued | People with specific tag | 📘 Usually works | Tag filtering |
dbo.StatusFlags |
Table-Valued | People with status flags | 📘 Usually works | Status filtering |
dbo.PotentialDups |
Table-Valued | Find duplicate records | 📘 Usually works | Data cleanup |
dbo.RegexMatch |
CLR Scalar | Regular expression match | ✅ Works | Pattern matching |
dbo.AllDigits |
CLR Scalar | Extract digits from string | ⚠️ May not work | Phone cleanup |
AddressMatch, Age, AgeInMonths, AllDigits, AllRegexMatchs, AttendDesc, AttendItem,
AvgSunAttendance, BaptismAgeRange, BibleFellowshipClassId, Birthday, CompactAttendHistory,
CompleteZip, ComputeAge, ComputePositionInFamily, ContributionAmount, ContributionAmount2,
ContributionChange, ContributionCount, CoupleFlag, CreateForeignKeys, DayAndTime,
DaysBetween12Attend, DaysSinceAttend, DaysSinceContact, DecToBase, DOB, DollarRange,
DonorTotalGifts, DonorTotalGiftsAttrRange, DonorTotalGiftsSize, DonorTotalMean,
DonorTotalMedian, DonorTotalUnits, DonorTotalUnitsAttrRange, DonorTotalUnitsSize,
DropForeignKeys, ElapsedTime, EnrollmentTransactionId, EntryPointId,
FamilyContributionGreetingName, FamilyGreetingName, FamilyMakeup, FindPerson0,
FindResCode, FirstActivity, FirstMeetingDateLastLear, FirstMondayOfMonth, FmtPhone,
GetAttendedTodaysMeeting, GetAttendType, GetCurrentBundleByType, GetCurrentMissionTripBundle,
GetCurrentOnlineBundle, GetCurrentOnlinePledgeBundle, GetDigits, GetEldestFamilyMember,
GetPeopleIdFromACS, GetPeopleIdFromIndividualNumber, GetScheduleDesc, GetScheduleTime,
GetSecurityCode, GetStreet, GetTodaysMeetingHour, GetTodaysMeetingId, GetWeekDayNameOfDate,
HeadOfHouseholdId, HeadOfHouseHoldSpouseId, HomePhone, InSmallGroup, IpVelocity,
IsSmallGroupLeaderOnly, IsValidEmail, LastActive, LastAttend, LastAttended, LastChanged,
LastContact, LastDrop, LastIdInTrans, LastMemberTypeInTrans, MaxMeetingDate, MaxPastMeeting,
MemberDesc, MemberStatusDescription, MemberTypeAsOf, MemberTypeAtLastDrop, MinMeetingDate,
NextAnniversary, NextBirthday, NextChangeTransactionId, NextChangeTransactionId2,
NextTranChangeDate, OneHeadOfHouseholdIsMember, OrganizationLeaderId, OrganizationLeaderName,
OrganizationMemberCount, OrganizationMemberCount2, OrganizationPrevCount,
OrganizationPrevMemberCount, OrganizationProspectCount, OrgCheckedCount, OrgFee,
OrgFilterCheckedCount, ParentNames, ParentNamesAndCells, ParseDate, PersonAttendCountOrg,
PledgeAmount, PledgeCount, PrimaryAddress, PrimaryAddress2, PrimaryBadAddressFlag,
PrimaryCity, PrimaryCountry, PrimaryResCode, PrimaryState, PrimaryZip, RegexMatch,
RemoveDiacritics, ScheduleId, SchoolGrade, SchoolGradeLevel, SmallGroupLeader, SpaceToNull,
SpouseId, SpouseIdJoint, StartsLower, StatusFlag, StatusFlagsAll, SundayForDate,
SundayForWeek, SundayForWeekNumber, Tool_VarbinaryToVarcharHex, TotalPaid, UEmail,
UName, UName2, UserName, UserPeopleIdFromEmail, UserRoleList, VisitAttendStr,
WasDeaconActive2008, WeekNumber, WidowedDate
AttendanceChange, AttendanceChangeDetail, AttendanceTypeAsOf, AttendCntHistory,
AttendCommitments, AttendDaysAfterNthVisitAsOf, AttendedAsOf, CheckInActivitiesSearch,
ConsecutiveAbsents, ContactSummary, ContactTypeTotals, Contributions2, Contributions2ByPerson,
Contributions3, ContributionsPledge, Contributors, Donors, DonorsWithoutPledges,
DownlineDetails, DownlineLevels, DuplicateMergeInfo, EngagementScoresPerson,
EnrollmentHistory, FamilyGiver, FamilyGiverFunds, FamilyMembers, FirstRecurringGiversByDate,
FirstTimeGivers, FirstTimeGiversByDate, FN_ContributionsByPerson, FN_FirstTimeGivers,
GetContributions, GetContributionsDetails, GetContributionsRange,
GetContributionTotalsBothIfJoint, GetPledgedTotalsBothIfJoint, GetPledgedTotalsBothIfJoint2,
GetTotalContributions, GetTotalContributions2, GetTotalContributions3,
GetTotalContributionsAgeRange, GetTotalContributionsDonor, GetTotalContributionsDonor2,
GetTotalContributionsDonorFund, GetTotalContributionsRange, GetTotalPledgesDonor2,
GetTotalPledgesDonor3, GiftsInKind, GiftSummary, GivingChange, GivingChangeFund,
GivingChangeFundQuarters, GivingChangeQuartersFund, GivingChangeQuartersFund2,
GivingCurrentPercentOfFormer, GuestList, GuestList2, HasIncompleteRegistrations,
InvolvementCurrent, InvolvementPrevious, LastAddrElement, LastAttendOrg, LastFamilyOrgAttends,
LastMeetings, MeetingsDataForDateRange, NonTaxContributions, NotAttendedAsOf, OptOuts,
OrgFilterCurrent, OrgFilterGuests, OrgFilterIds, OrgFilterIds3, OrgFilterInactive,
OrgFilterPending, OrgFilterPeople, OrgFilterPeople3, OrgFilterPrevious, OrgFilterProspects,
OrgMember, OrgMemberInfo, OrgMemberQuestions, OrgMembersAsOfDate, OrgMembersAsOfDate2,
OrgMinistryInfo, OrgPeople, OrgPeopleCurrent, OrgPeopleGuests, OrgPeopleIds,
OrgPeopleInactive, OrgPeoplePending, OrgPeoplePrevious, OrgPeopleProspects, OrgSearch,
OrgVisitorsAsOfDate, PeopleIdsFromOrgSearch, PersonStatusFlags, PledgeBalances,
PledgeFulfillment, PledgeReport, PotentialDups, PotentialSubstitutes, RecentAbsents,
RecentAttendance, RecentAttendInDaysByCount, RecentAttendInDaysByCountDesc, RecentGiver,
RecentGiverFund, RecentGiverFunds, RecentRegistrations, RegisterLinksFromMaster,
RegistrationGradeOptions, Registrations, RegistrationSmallGroups, SchedulerPotentialSubstitutes,
SearchDivisions, SenderGifts, SoftCreditContributions, SplitFundIds, SplitInts,
SplitStrings, StatusFlags, StatusFlagsPerson, StockGifts, StringSplitOrdered, TPStats,
UnitPledgeSummary, VisitNumberSinceDate, VisitsAbsents, VolunteerCalendar
ActiveRecords, ActivityLogSearch, AttendanceCredits, AttendMemberTypeAsOf, CheckinByDate,
CheckinFamilyMembers, CheckinMatch, CheckinMatchBarCodeOnly, ContributionCountTable,
Contributions0, Contributions2SearchIds, ContributionSearch, CsvTable, CurrOrgMembers,
CurrOrgMembers2, DownlineCategories, DownlineSingleTrace, DownlineSummary, FamilyAttendance,
FilterOnlineReg, FilterOrgSearchName, FindPerson, FindPerson2, FindPerson3, FindPerson4,
FindPersonByEmail, FindPersonByExtraValue, FirstLast, GenRanges, GetTodaysMeetingHours,
GetTodaysMeetingHours2, GetTodaysMeetingHours3, MembersAsOf, MembersWhoAttendedOrgs,
MostRecentItems, NormalContributions, OnlineRegMatches, OrgDayStats, OrgFilterPeople2,
OrgMembersGroupFiltered, OrgPeople2, PastAttendanceCredits, PbGetInvolvementJoinedCompleted,
PbGetInvolvementMembersNotInProcess, PeopleWeeklyEngagementScore, Pledges0, PledgesSummary,
RecentAbsents2, RecentAttendMemberType, RecentAttendType, RecentContributionSummaries,
RecentIncompleteRegistrations, RecentIncompleteRegistrations2, RecentNewVisitCount,
RollList, RollListFilteredBySubgroups, RollListHighlight, Split, SplitIntsKey, SundayDates,
TaggedPeople, TransactionSearch, WeeklyAttendsForOrgs
Important: All TouchPoint users have read-only SQL access with the ability to write to temp tables.
This means:
If you get errors like: The EXECUTE permission was denied on the object 'FunctionName'
Solutions:
Contributions2 which typically work with read-only accessSELECT TOP 10
p.PeopleId,
p.Name,
dbo.Age(p.PeopleId) as Age,
dbo.Birthday(p.PeopleId) as Birthday,
dbo.SpouseId(p.PeopleId) as SpouseId,
dbo.UName(dbo.SpouseId(p.PeopleId)) as SpouseName,
dbo.FamilyGreetingName(1, p.PeopleId) as FamilyGreeting -- Takes (type, peopleId)
FROM People p
WHERE p.MemberStatusId = 10 -- Members only
AND p.IsDeceased = 0
ORDER BY p.Name;
-- Use the most important UDF: Contributions2
-- This handles spouse contributions, excludes non-tax items, etc.
-- Actual columns: FamilyId, PeopleId, Date, HeadName, SpouseName, Amount, FundName, etc.
SELECT
c.FamilyId,
c.HeadName,
c.SpouseName,
c.Amount,
c.Date as ContributionDate,
c.FundName,
c.CheckNo,
c.BundleType
FROM dbo.Contributions2(
'2024-01-01', -- @fd (from date)
'2024-12-31', -- @td (to date)
0, -- @campusid (0 = all campuses)
0, -- @pledges (0 = exclude pledges)
0, -- @nontaxded (0 = tax-deductible only)
1, -- @includeUnclosed (1 = include unclosed bundles)
NULL -- @fundids (NULL = all funds, or '1,2,3' for specific)
) c
WHERE c.Amount > 100
ORDER BY c.Amount DESC;
-- Note: LastAttend takes (orgid, peopleid) - parameter order matters!
-- Some UDFs like OrgPeopleCurrent require SET QUOTED_IDENTIFIER ON
SET QUOTED_IDENTIFIER ON;
-- Example 1: Check attendance for a specific organization
DECLARE @orgid INT = 100; -- Replace with your organization ID
SELECT TOP 10
p.Name,
dbo.LastAttend(@orgid, p.PeopleId) as LastAttendDate,
DATEDIFF(day, dbo.LastAttend(@orgid, p.PeopleId), GETDATE()) as DaysSinceAttended
FROM People p
WHERE p.PeopleId IN (
SELECT PeopleId FROM dbo.OrgPeopleCurrent(@orgid)
)
AND dbo.LastAttend(@orgid, p.PeopleId) IS NOT NULL
ORDER BY DaysSinceAttended DESC;
-- Example 2: Check attendance across all organizations (simpler, no variable needed)
SELECT TOP 10
p.Name,
dbo.LastAttend(0, p.PeopleId) as LastAttendDate, -- 0 = all orgs
DATEDIFF(day, dbo.LastAttend(0, p.PeopleId), GETDATE()) as DaysSinceAttended
FROM People p
WHERE dbo.LastAttend(0, p.PeopleId) IS NOT NULL
ORDER BY DaysSinceAttended DESC;
-- Get all current members with contact info
-- Note: OrgPeopleCurrent returns limited columns, JOIN with People for full details
SET QUOTED_IDENTIFIER ON; -- Required for OrgPeopleCurrent
SELECT TOP 20
op.PeopleId,
p.Name2, -- From People table
p.EmailAddress, -- From People table
dbo.FmtPhone(p.CellPhone) as CellPhone, -- FmtPhone takes 1 param
op.MemberType,
op.AttPct as AttendancePercent,
dbo.OrganizationLeaderName(101) as LeaderName
FROM dbo.OrgPeopleCurrent(101) op -- 101 is the OrgId
JOIN People p ON op.PeopleId = p.PeopleId
ORDER BY p.Name2;
-- Working with Age calculation (BDate column in People table)
SELECT
p.PeopleId,
p.Name,
p.BDate as BirthDate,
dbo.Age(p.PeopleId) as CalculatedAge
FROM People p
WHERE p.BDate IS NOT NULL
AND p.IsDeceased = 0
ORDER BY CalculatedAge DESC;
-- Using GetTotalContributions (table-valued function)
-- Note: Returns empty for many people, check contribution records
SELECT *
FROM dbo.GetTotalContributions(16901, NULL) -- PeopleId, optional date
-- Using LastAttend with correct parameters (orgid, peopleid)
SELECT
p.PeopleId,
p.Name,
dbo.LastAttend(0, p.PeopleId) as LastAttendanceDate -- 0 = all orgs
FROM People p
WHERE EXISTS (
SELECT 1 FROM Attend a
WHERE a.PeopleId = p.PeopleId
AND a.AttendanceFlag = 1
)
ORDER BY p.PeopleId;
-- Working with Contributions2 - verified parameters
SELECT TOP 100
FamilyId,
HeadName,
SpouseName,
Date as ContributionDate,
Amount,
FundName,
CheckNo
FROM dbo.Contributions2(
'2020-01-01', -- From date
'2024-12-31', -- To date
0, -- Campus (0=all)
0, -- Include pledges (0=no)
NULL, -- Non-tax deductible (NULL=default)
1, -- Include unclosed bundles
NULL -- Fund IDs (NULL=all)
)
WHERE Amount > 0
ORDER BY Date DESC, Amount DESC;
-- Using OrgPeopleCurrent - returns specific columns
-- Columns: PeopleId, Tab, GroupCode, AttPct, LastAttended, Joined,
-- Dropped, InactiveDate, MemberCode, MemberType, Hidden, Groups, Grade
SELECT
PeopleId,
AttPct as AttendancePercent,
LastAttended,
MemberType
FROM dbo.OrgPeopleCurrent(100) -- Single parameter: OrgId
WHERE AttPct > 50; -- Members with >50% attendance
-- DaysSinceContact function
SELECT TOP 20
p.PeopleId,
p.Name,
dbo.DaysSinceContact(p.PeopleId) as DaysSinceLastContact
FROM People p
WHERE p.MemberStatusId = 10 -- Members only
AND dbo.DaysSinceContact(p.PeopleId) IS NOT NULL
ORDER BY DaysSinceLastContact DESC;
-- ⚠️ PITFALL: Some functions require SET QUOTED_IDENTIFIER ON
SET QUOTED_IDENTIFIER ON;
SELECT * FROM dbo.SomeTableValuedFunction(params);
-- ⚠️ PITFALL: Birth date column is 'BDate' not 'BirthDate'
-- The People table stores birth info in: BDate, BirthYear, BirthMonth, BirthDay
SELECT BDate, BirthYear, BirthMonth, BirthDay FROM People WHERE PeopleId = 117;
-- ✅ CORRECT: Table-valued functions need FROM clause
SELECT * FROM dbo.GetTotalContributions(117, NULL); -- Correct
-- ❌ WRONG: SELECT dbo.GetTotalContributions(117, NULL); -- Error
-- ✅ CORRECT: Parameter order matters - check sys.parameters
-- LastAttend takes (orgid, peopleid) NOT (peopleid, orgid)
SELECT dbo.LastAttend(0, 117); -- Correct: orgid first
-- ❌ WRONG: SELECT dbo.LastAttend(117, 0); -- Wrong order
-- ✅ OPTIMIZATION: Use CROSS APPLY for better performance
SELECT
p.Name,
contrib.*
FROM People p
CROSS APPLY dbo.GetTotalContributions(p.PeopleId, NULL) contrib
WHERE p.FamilyId = 8392;
-- ✅ NULL handling in UDFs
-- Many UDFs return NULL for no data - always handle
SELECT
p.Name,
ISNULL(dbo.DaysSinceContact(p.PeopleId), 9999) as DaysSince
FROM People p;
These table-valued functions often have broader permissions:
-- Contributions2 is a table-valued function that often works when scalar functions don't
SELECT TOP 20
c.FamilyId,
c.HeadName,
c.SpouseName,
c.Date as ContributionDate,
c.Amount,
c.FundName,
c.CheckNo
FROM dbo.Contributions2(
'2024-01-01', -- From date
'2024-12-31', -- To date
0, -- Campus (0=all)
0, -- Pledges (0=exclude)
0, -- Non-tax deductible (0=exclude)
1, -- Include unclosed bundles
NULL -- Fund IDs (NULL=all)
) c
WHERE c.Amount > 0
ORDER BY c.Date DESC;
-- Split function for parsing delimited strings
SELECT value
FROM dbo.Split('Apple,Banana,Cherry', ',');
-- Use Split to parse multi-value extra values
SELECT
p.PeopleId,
p.Name,
s.value as Interest
FROM People p
CROSS APPLY dbo.Split(
(SELECT Data FROM PeopleExtra
WHERE PeopleId = p.PeopleId
AND Field = 'Interests'), ',') s
WHERE p.PeopleId = 12345;
-- Get pledge fulfillment data
SELECT *
FROM dbo.PledgeReport(1, '2024-01-01', '2024-12-31')
WHERE AmountPledged > 0;
-- Get contribution ranges for analysis (8 parameters)
SELECT TOP 20 *
FROM dbo.GetContributionsRange(
'2024-01-01', -- @fd (from date)
'2024-12-31', -- @td (to date)
0, -- @campusid
0, -- @nontaxded
1, -- @includeUnclosed
0, -- @pledge
NULL, -- @fundid
NULL -- @fundids
)
WHERE Amount > 100;
If you don't have EXECUTE permissions on any UDFs, use these standard SQL alternatives:
-- Calculate age using standard SQL instead of dbo.Age()
SELECT TOP 10
p.PeopleId,
p.Name,
p.BDate,
-- Calculate age manually
DATEDIFF(year, p.BDate, GETDATE()) -
CASE
WHEN MONTH(p.BDate) > MONTH(GETDATE()) OR
(MONTH(p.BDate) = MONTH(GETDATE()) AND DAY(p.BDate) > DAY(GETDATE()))
THEN 1
ELSE 0
END as Age,
-- Get spouse info using JOIN instead of dbo.SpouseId()
spouse.PeopleId as SpouseId,
spouse.Name as SpouseName
FROM People p
LEFT JOIN People spouse ON p.SpouseId = spouse.PeopleId
WHERE p.MemberStatusId = 10
AND p.IsDeceased = 0
ORDER BY p.Name;
-- Get contributions using direct table query instead of dbo.Contributions2()
SELECT TOP 20
p.FamilyId,
p.Name as HeadName,
c.ContributionDate,
c.ContributionAmount as Amount,
cf.FundName
FROM Contribution c
JOIN People p ON c.PeopleId = p.PeopleId
JOIN ContributionFund cf ON c.FundId = cf.FundId
WHERE c.ContributionDate BETWEEN '2024-01-01' AND '2024-12-31'
AND c.ContributionTypeId = 1 -- Tax-deductible only
AND c.ContributionStatusId = 0 -- Posted
AND c.ContributionAmount > 100
ORDER BY c.ContributionAmount DESC;
-- Get last attendance date using subquery instead of dbo.LastAttend()
SELECT TOP 10
p.PeopleId,
p.Name,
(SELECT MAX(m.MeetingDate)
FROM Attend a
JOIN Meetings m ON a.MeetingId = m.MeetingId
WHERE a.PeopleId = p.PeopleId
AND a.AttendanceFlag = 1) as LastAttendDate,
DATEDIFF(day,
(SELECT MAX(m.MeetingDate)
FROM Attend a
JOIN Meetings m ON a.MeetingId = m.MeetingId
WHERE a.PeopleId = p.PeopleId
AND a.AttendanceFlag = 1),
GETDATE()) as DaysSinceAttended
FROM People p
WHERE EXISTS (
SELECT 1 FROM Attend a
WHERE a.PeopleId = p.PeopleId
AND a.AttendanceFlag = 1
)
ORDER BY DaysSinceAttended DESC;
-- Format phone numbers using SQL string functions instead of dbo.FmtPhone()
SELECT TOP 10
p.PeopleId,
p.Name,
p.CellPhone,
-- Format phone as (XXX) XXX-XXXX
CASE
WHEN LEN(REPLACE(REPLACE(REPLACE(p.CellPhone, '-', ''), '(', ''), ')', '')) = 10
THEN '(' + SUBSTRING(REPLACE(REPLACE(REPLACE(p.CellPhone, '-', ''), '(', ''), ')', ''), 1, 3) + ') ' +
SUBSTRING(REPLACE(REPLACE(REPLACE(p.CellPhone, '-', ''), '(', ''), ')', ''), 4, 3) + '-' +
SUBSTRING(REPLACE(REPLACE(REPLACE(p.CellPhone, '-', ''), '(', ''), ')', ''), 7, 4)
ELSE p.CellPhone
END as FormattedPhone
FROM People p
WHERE p.CellPhone IS NOT NULL
AND p.CellPhone != '';
Since all users have read-only access with temp table permissions, use temp tables for complex calculations:
-- Create temp table with age calculations (replaces dbo.Age() function)
SELECT
PeopleId,
Name,
BDate,
DATEDIFF(year, BDate, GETDATE()) -
CASE
WHEN MONTH(BDate) > MONTH(GETDATE()) OR
(MONTH(BDate) = MONTH(GETDATE()) AND DAY(BDate) > DAY(GETDATE()))
THEN 1
ELSE 0
END as Age
INTO #PeopleWithAges
FROM People
WHERE BDate IS NOT NULL
AND IsDeceased = 0;
-- Now use the temp table for analysis
SELECT
Age,
COUNT(*) as PeopleCount
FROM #PeopleWithAges
GROUP BY Age
ORDER BY Age;
-- Clean up
DROP TABLE #PeopleWithAges;
-- Create contribution summary temp table (replaces complex UDF logic)
WITH ContributionSummary AS (
SELECT
p.FamilyId,
p.Name as HeadOfHousehold,
c.ContributionDate,
c.ContributionAmount,
cf.FundName,
ROW_NUMBER() OVER (PARTITION BY p.FamilyId ORDER BY c.ContributionDate DESC) as RowNum
FROM Contribution c
JOIN People p ON c.PeopleId = p.PeopleId
JOIN ContributionFund cf ON c.FundId = cf.FundId
WHERE c.ContributionDate >= DATEADD(year, -1, GETDATE())
AND c.ContributionTypeId = 1 -- Tax deductible only
AND c.ContributionStatusId = 0 -- Posted
)
SELECT *
INTO #FamilyGiving
FROM ContributionSummary;
-- Analyze family giving patterns
SELECT
FamilyId,
HeadOfHousehold,
COUNT(*) as GiftCount,
SUM(ContributionAmount) as TotalGiving,
AVG(ContributionAmount) as AverageGift,
MAX(ContributionDate) as LastGiftDate
FROM #FamilyGiving
GROUP BY FamilyId, HeadOfHousehold
HAVING SUM(ContributionAmount) > 1000
ORDER BY TotalGiving DESC;
-- Clean up
DROP TABLE #FamilyGiving;
-- Build attendance summary in temp table
SELECT
a.PeopleId,
p.Name,
MAX(m.MeetingDate) as LastAttendDate,
COUNT(DISTINCT m.MeetingId) as MeetingsAttended,
COUNT(DISTINCT DATEPART(week, m.MeetingDate)) as WeeksAttended
INTO #AttendanceSummary
FROM Attend a
JOIN People p ON a.PeopleId = p.PeopleId
JOIN Meetings m ON a.MeetingId = m.MeetingId
WHERE a.AttendanceFlag = 1
AND m.MeetingDate >= DATEADD(month, -3, GETDATE())
GROUP BY a.PeopleId, p.Name;
-- Analyze attendance patterns
SELECT
*,
DATEDIFF(day, LastAttendDate, GETDATE()) as DaysSinceLastAttend
FROM #AttendanceSummary
ORDER BY DaysSinceLastAttend DESC;
-- Find irregular attenders
SELECT * FROM #AttendanceSummary
WHERE WeeksAttended BETWEEN 2 AND 8 -- Attended 2-8 weeks in last 3 months
ORDER BY LastAttendDate DESC;
-- Clean up
DROP TABLE #AttendanceSummary;
-- Check if a view exists
SELECT COUNT(*) as ViewExists
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA = 'lookup'
AND TABLE_NAME = 'MemberStatus';
-- Get view definition (if permissions allow)
SELECT VIEW_DEFINITION
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA = 'dbo'
AND TABLE_NAME = 'vw_ActiveAdultMembers';
-- List all columns in a view with data types
SELECT
COLUMN_NAME,
DATA_TYPE,
CHARACTER_MAXIMUM_LENGTH,
NUMERIC_PRECISION,
NUMERIC_SCALE,
IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'lookup'
AND TABLE_NAME = 'MemberStatus'
ORDER BY ORDINAL_POSITION;
-- Find views that reference a specific table
SELECT DISTINCT
v.TABLE_SCHEMA,
v.TABLE_NAME as ViewName
FROM INFORMATION_SCHEMA.VIEWS v
WHERE v.VIEW_DEFINITION LIKE '%People%'
AND v.TABLE_SCHEMA NOT IN ('sys', 'INFORMATION_SCHEMA')
ORDER BY v.TABLE_SCHEMA, v.TABLE_NAME;
| Use Case | Views to Use | Example Query Pattern |
|---|---|---|
| Member Reports | lookup.MemberStatus, lookup.Campus, lookup.FamilyPosition | Join People table with status and campus views |
| Organization Lists | lookup.OrganizationStatus, lookup.OrganizationType, lookup.MemberType | Filter by OrganizationStatusId = 30 for active |
| Attendance Analysis | lookup.AttendType, lookup.MemberType | Group by AttendType for categorized counts |
| Giving Reports | lookup.ContributionType, lookup.ContributionFund | Exclude TypeId = 99 (event fees) |
| Volunteer Management | lookup.VolApplicationStatus, lookup.MemberStatus | Track application and approval processes |
| Family Structure | lookup.FamilyPosition, lookup.MaritalStatus | Identify heads of household and relationships |
-- Example 1: Organization analysis with visitor percentage
-- NOTE: Column is misspelled as 'Vistors' in the view
SELECT
Program,
Division,
Organization,
OrgStatus,
Members,
Previous,
Vistors, -- Note: typo in actual view column name
Meetings,
CAST(ISNULL(Vistors * 100.0 / NULLIF(Members, 0), 0) as DECIMAL(10,2)) as VisitorPercent
FROM OrganizationStructure
WHERE ProgId = 1115 -- Adults program (verify your ProgIds!)
AND Members > 0
ORDER BY Program, Division, Organization;
-- Example 2: Program summary statistics
SELECT
Program,
COUNT(DISTINCT OrgId) as TotalOrgs,
SUM(Members) as TotalMembers,
SUM(Vistors) as TotalVisitors,
SUM(Meetings) as TotalMeetings,
CAST(AVG(CAST(Members as FLOAT)) as DECIMAL(10,2)) as AvgMembersPerOrg
FROM OrganizationStructure
WHERE OrgStatus = 'Active'
GROUP BY ProgId, Program
ORDER BY TotalMembers DESC;
-- Example 3: Find high-visitor organizations (growth opportunities)
SELECT TOP 20
Program,
Division,
Organization,
Members,
Vistors,
CAST(Vistors * 100.0 / NULLIF(Members, 0) as DECIMAL(10,2)) as VisitorRate
FROM OrganizationStructure
WHERE OrgStatus = 'Active'
AND Members > 5 -- Exclude very small groups
AND Vistors > 0
ORDER BY VisitorRate DESC;
-- Real-time check-in monitoring (last 3 hours)
SELECT
PeopleId,
Name,
Activities,
AccessType,
CheckInTime,
DATEDIFF(minute, CheckInTime, GETDATE()) as MinutesAgo
FROM CheckInActivities
WHERE CheckInTime >= DATEADD(hour, -3, GETDATE())
ORDER BY CheckInTime DESC;
-- Check-in patterns by hour of day
SELECT
DATEPART(hour, CheckInTime) as Hour,
Activities, -- Note: plural form
COUNT(*) as CheckInCount,
COUNT(DISTINCT PeopleId) as UniquePeople
FROM CheckInActivities
WHERE CheckInTime >= DATEADD(day, -7, GETDATE())
GROUP BY DATEPART(hour, CheckInTime), Activities
ORDER BY Hour, Activities;
-- Member status distribution with percentages
SELECT
ms.Description as Status,
COUNT(*) as Count,
CAST(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER() as DECIMAL(5,2)) as Percentage
FROM People p
JOIN lookup.MemberStatus ms ON p.MemberStatusId = ms.Id
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
GROUP BY ms.Id, ms.Description
ORDER BY ms.Id;
-- Guest retention analysis using AttendType
WITH GuestAttendance AS (
SELECT
a.PeopleId,
MIN(a.MeetingDate) as FirstVisit,
MAX(a.MeetingDate) as LastVisit,
COUNT(*) as VisitCount
FROM Attend a
WHERE a.AttendanceTypeId IN (30, 50, 60) -- Guest types
AND a.AttendanceFlag = 1
AND a.MeetingDate >= DATEADD(month, -6, GETDATE())
GROUP BY a.PeopleId
)
SELECT
CASE
WHEN VisitCount = 1 THEN 'One-time visitor'
WHEN VisitCount BETWEEN 2 AND 3 THEN 'Returning guest'
WHEN VisitCount > 3 THEN 'Regular attender'
END as Category,
COUNT(*) as PeopleCount
FROM GuestAttendance
GROUP BY
CASE
WHEN VisitCount = 1 THEN 'One-time visitor'
WHEN VisitCount BETWEEN 2 AND 3 THEN 'Returning guest'
WHEN VisitCount > 3 THEN 'Regular attender'
END;
-- Mission trip funding status using MissionTripTotals
-- MissionTripTotals table lookup is slow due to how it updates the views
SELECT
Trip,
COUNT(*) as Participants,
SUM(TripCost) as TotalCost,
SUM(Raised) as TotalRaised,
SUM(Due) as TotalDue,
CAST(SUM(Raised) * 100.0 / NULLIF(SUM(TripCost), 0) as DECIMAL(5,2)) as PercentRaised
FROM MissionTripTotals
WHERE Due > 0
GROUP BY Trip
ORDER BY TotalDue DESC;
-- Outstanding balances by organization using TransactionSummary
SELECT
o.OrganizationName,
COUNT(DISTINCT ts.PeopleId) as PeopleWithBalances,
SUM(ts.IndDue - ts.IndAmt) as TotalOutstanding
FROM TransactionSummary ts
JOIN Organizations o ON ts.OrganizationId = o.OrganizationId
WHERE ts.IsLatestTransaction = 1
AND (ts.IndDue - ts.IndAmt) > 0
GROUP BY o.OrganizationId, o.OrganizationName
HAVING SUM(ts.IndDue - ts.IndAmt) > 0
ORDER BY TotalOutstanding DESC;
The following sections provide detailed documentation and SQL examples for the most commonly used views.
| View Name | Purpose | Key Columns |
|---|---|---|
FirstAttends |
First attendance date per person/org | PeopleId, Name2, OrganizationId, OrganizationName, FirstAttend |
LastAttends |
Last attendance with contact info | PeopleId, Name2, OrganizationId, LastAttend, EmailAddress, CellPhone |
AttendCredits |
Attendance credit tracking | OrganizationId, PeopleId, Attended, WeekDate, OtherAttends |
MeetingConflicts |
Scheduling conflicts detection | MeetingId1, MeetingId2, ConflictType, DateTime |
| View Name | Purpose | Key Columns |
|---|---|---|
FirstAttends |
First attendance date per person/org | PeopleId, Name2, OrganizationId, OrganizationName, FirstAttend |
LastAttends |
Last attendance with contact info | PeopleId, Name2, OrganizationId, LastAttend, EmailAddress, CellPhone |
AttendCredits |
Attendance credit tracking | OrganizationId, PeopleId, Attended, WeekDate, OtherAttends |
MeetingConflicts |
Scheduling conflicts detection | MeetingId1, MeetingId2, ConflictType, DateTime |
-- Find people who haven't attended in 90+ days
SELECT
la.Name2,
la.OrganizationName,
la.LastAttend,
la.EmailAddress,
la.CellPhone,
DATEDIFF(day, la.LastAttend, GETDATE()) as DaysSinceAttended
FROM LastAttends la
WHERE la.LastAttend < DATEADD(day, -90, GETDATE())
AND la.HasEmail = 1 -- Has email for follow-up
ORDER BY la.LastAttend DESC;
-- Track first-time visitors by organization
SELECT
fa.OrganizationName,
COUNT(*) as FirstTimeVisitors,
MIN(fa.FirstAttend) as EarliestVisit,
MAX(fa.FirstAttend) as LatestVisit
FROM FirstAttends fa
WHERE fa.FirstAttend >= DATEADD(month, -1, GETDATE())
GROUP BY fa.OrganizationId, fa.OrganizationName
ORDER BY FirstTimeVisitors DESC;
-- Attendance credits by week
-- NOTE: Attended is a BIT column, use CASE for aggregation
SELECT
ac.WeekDate,
COUNT(DISTINCT ac.PeopleId) as UniqueAttendees,
SUM(CASE WHEN ac.Attended = 1 THEN 1 ELSE 0 END) as TotalCredits,
CAST(SUM(CASE WHEN ac.Attended = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as DECIMAL(5,2)) as AttendanceRate
FROM AttendCredits ac
WHERE ac.WeekDate >= DATEADD(week, -4, GETDATE())
GROUP BY ac.WeekDate
ORDER BY ac.WeekDate DESC;
| View Name | Purpose | Key Columns |
|---|---|---|
TransactionBalances |
Outstanding balances Requires: SET QUOTED_IDENTIFIER ON |
BalancesId, Payment, TotDue, TotalPayment, Deposit, People |
MissionTripTotals |
Mission trip fundraising summary | Trip, PeopleId, Name, TripCost, Raised, Due |
ContributionsBasic |
Simplified contribution view | ContributionId, PeopleId, Date, Amount, Fund |
RecurringGivingDueForToday |
Today's expected recurring gifts (pre-filtered) | PeopleId, Name2, Amt |
DepositDateTotals |
Deposit totals by date | DepositDate, TotalAmount, Count, BundleHeaderId |
DonorProfileList |
Donor profiles summary | PeopleId, LastGift, TotalGiven, GiftCount |
ManagedGivingList |
Managed giving accounts | PeopleId, ManagedGiverId, StartDate, NextDate |
-- Mission trip funding status
SELECT
Trip,
COUNT(DISTINCT PeopleId) as Participants,
SUM(TripCost) as TotalCost,
SUM(Raised) as TotalRaised,
SUM(Due) as TotalDue,
CAST(SUM(Raised) * 100.0 / NULLIF(SUM(TripCost), 0) as DECIMAL(5,2)) as PercentRaised
FROM MissionTripTotals
GROUP BY Trip
HAVING SUM(Due) > 0
ORDER BY SUM(Due) DESC;
-- Outstanding balances summary (requires SET QUOTED_IDENTIFIER ON)
SET QUOTED_IDENTIFIER ON;
SELECT
COUNT(*) as TransactionsWithBalance,
SUM(TotDue - TotalPayment) as TotalOutstanding,
AVG(TotDue - TotalPayment) as AvgBalance
FROM TransactionBalances
WHERE TotDue > TotalPayment;
-- Recurring giving analysis
-- NOTE: View is already filtered for "today", columns are PeopleId, Name2, Amt
SELECT
COUNT(*) as ScheduledGiftsToday,
SUM(Amt) as ExpectedTotalToday,
MIN(Amt) as SmallestGift,
MAX(Amt) as LargestGift
FROM RecurringGivingDueForToday;
| View Name | Purpose | Key Columns |
|---|---|---|
MemberData |
Core member information | PeopleId, First, Last, Age, Marital, DecisionDt, JoinDt, Baptism |
PeopleBasicModifed |
People with modification tracking | PeopleId, Name, Modified, ModifiedBy, Created |
HeadOrSpouseWithEmail |
Household heads/spouses with email | PeopleId (filtered view) |
SpouseOrHeadWithEmail |
Alternate household email view | PeopleId (filtered view) |
AddressInfo |
Complete address information | PeopleId, AddressLineOne, City, StateCode, ZipCode |
ProspectCounts |
Organization prospect counts | OrganizationId, ProspectCount |
PreviousMemberCounts |
Historical member counts | OrganizationId, prevcount |
-- Member analysis with decisions
SELECT
md.First + ' ' + md.Last as FullName,
md.Age,
md.Marital,
md.Decision,
md.Baptism,
CASE
WHEN md.JoinDt IS NOT NULL THEN DATEDIFF(year, md.JoinDt, GETDATE())
ELSE NULL
END as YearsMember
FROM MemberData md
WHERE md.JoinDt IS NOT NULL
ORDER BY md.JoinDt DESC;
-- Find households for communication
SELECT
p.Name2,
p.EmailAddress
FROM People p
WHERE p.PeopleId IN (SELECT PeopleId FROM HeadOrSpouseWithEmail)
AND p.EmailAddress IS NOT NULL;
-- Compare current vs previous member counts
-- NOTE: Percentage can exceed 999%, so using DECIMAL(10,2)
SELECT TOP 100
os.Organization,
os.Members as CurrentMembers,
ISNULL(pmc.prevcount, 0) as PreviousMembers,
os.Members - ISNULL(pmc.prevcount, 0) as Change,
CASE
WHEN pmc.prevcount > 0 THEN
CAST((os.Members - pmc.prevcount) * 100.0 / pmc.prevcount as DECIMAL(10,2))
WHEN os.Members > 0 AND pmc.prevcount IS NULL THEN 100.00
ELSE 0
END as PercentChange
FROM OrganizationStructure os
LEFT JOIN PreviousMemberCounts pmc ON os.OrgId = pmc.OrganizationId
WHERE os.OrgStatus = 'Active'
AND os.Members IS NOT NULL
ORDER BY ABS(os.Members - ISNULL(pmc.prevcount, 0)) DESC;
| View Name | Purpose | Key Columns |
|---|---|---|
FailedEmails |
Email delivery failures | time (lowercase), Id, PeopleId, Fail |
EmailDelegation |
Who can send on behalf of others | CanEmailId, CanEmailName2, OnBehalfOfIds, OnBehalfOfNames |
FirstPersonSameEmail |
Primary person per email address | PeopleId (deduped by email) |
-- Recent email failures for follow-up
-- Columns: time (lowercase), Id, PeopleId, Fail
SELECT TOP 100
fe.Id as EmailQueueId,
p.Name2,
p.EmailAddress,
fe.time as FailureTime,
fe.Fail as FailureReason,
DATEDIFF(hour, fe.time, GETDATE()) as HoursAgo
FROM FailedEmails fe
JOIN People p ON fe.PeopleId = p.PeopleId
WHERE fe.time >= DATEADD(day, -7, GETDATE())
ORDER BY fe.time DESC;
-- Email delegation permissions
-- Shows who can send emails on behalf of other accounts
SELECT
ed.CanEmailName2 as PersonWhoCanEmail,
ed.OnBehalfOfNames as CanEmailOnBehalfOf,
LEN(ed.OnBehalfOfIds) - LEN(REPLACE(ed.OnBehalfOfIds, ',', '')) + 1 as NumberOfDelegations
FROM EmailDelegation ed
WHERE ed.OnBehalfOfIds IS NOT NULL
ORDER BY ed.CanEmailName2;
| View Name | Purpose | Key Columns |
|---|---|---|
ActiveRegistrations |
Currently active registrations | OrganizationId |
InProgressRegistrations |
Incomplete registrations Requires: SET QUOTED_IDENTIFIER ON |
RegDataId, Name, PeopleId, OrganizationId, OrganizationName, Stamp |
RegistrationList |
Complete registration details (24 columns) | RegId, Name, Email, Phone, Completed, OrganizationName |
AppRegistrations |
Mobile app registrations | RegId, PeopleId, Device, AppVersion, Platform |
OnlineRegQA |
Registration questions/answers | RegId, Question, Answer, QuestionOrder |
-- Find abandoned registrations for follow-up
-- NOTE: Requires SET QUOTED_IDENTIFIER ON
SET QUOTED_IDENTIFIER ON;
SELECT TOP 100
ipr.RegDataId,
ipr.Name,
p.EmailAddress,
ipr.OrganizationName,
ipr.Stamp as StartedTime,
DATEDIFF(day, ipr.Stamp, GETDATE()) as DaysAgo
FROM InProgressRegistrations ipr
LEFT JOIN People p ON ipr.PeopleId = p.PeopleId
WHERE ipr.Stamp < DATEADD(day, -3, GETDATE())
ORDER BY ipr.Stamp DESC;
-- Active registrations by organization
SELECT
o.OrganizationName,
COUNT(*) as ActiveRegCount
FROM ActiveRegistrations ar
JOIN Organizations o ON ar.OrganizationId = o.OrganizationId
GROUP BY o.OrganizationId, o.OrganizationName
ORDER BY ActiveRegCount DESC;
| View Name | Purpose | Key Columns |
|---|---|---|
UserList |
System users with details | UserId, Username, PeopleId, LastActivityDate, EmailAddress |
UserRoles |
User role assignments | UserId, RoleId, RoleName |
UserLeaders |
User leadership assignments | UserId, OrganizationId, LeaderType |
StatusFlagList |
All status flags | FlagId, Name, Description, FlagType |
TaskSearch |
Task management search (26 columns) | TaskId, Description, Owner, Due, Status, Priority |
IncompleteTasks |
Open tasks not yet completed (19 columns) | Id, Description, OwnerId, WhoId (About person), Due, StatusId |
ChangeLogDetails |
Audit trail summary view (5 columns) | Id, PeopleId, Section, Created, FamilyId |
-- Overdue tasks report
SELECT
it.Id as TaskId,
it.Description,
owner.Name2 as Owner,
about.Name2 as AboutPerson,
it.Due,
DATEDIFF(day, it.Due, GETDATE()) as DaysOverdue
FROM IncompleteTasks it
LEFT JOIN People owner ON it.OwnerId = owner.PeopleId
LEFT JOIN People about ON it.WhoId = about.PeopleId
WHERE it.Due < GETDATE()
ORDER BY it.Due;
-- User activity analysis
SELECT
ul.Username,
p.Name2,
ul.LastActivityDate,
DATEDIFF(day, ul.LastActivityDate, GETDATE()) as DaysInactive
FROM UserList ul
LEFT JOIN People p ON ul.PeopleId = p.PeopleId
WHERE ul.LastActivityDate < DATEADD(day, -30, GETDATE())
ORDER BY ul.LastActivityDate;
-- Recent changes from ChangeLog (actual audit trail)
SELECT TOP 100
cl.Created,
u.Name2 as ChangedBy,
p.Name2 as PersonChanged,
cl.Field,
cl.Before,
cl.After
FROM ChangeLog cl
LEFT JOIN People p ON cl.PeopleId = p.PeopleId
LEFT JOIN People u ON cl.UserPeopleId = u.PeopleId
WHERE cl.Created >= DATEADD(day, -7, GETDATE())
ORDER BY cl.Created DESC;
The export schema contains views optimized for data export and integration with external systems:
| View Name | Purpose | Column Count |
|---|---|---|
export.XpPeople |
Complete person export | 78 columns - ALL person data |
export.XpFamily |
Family units with relationships | 15 columns |
export.XpOrganization |
Organization complete details | 24 columns |
export.XpContribution |
Contribution records for export | 13 columns |
export.XpAttendance |
Attendance history export | 7 columns |
export.XpMeeting |
Meeting details export | 14 columns |
export.XpEnrollHistory |
Enrollment history tracking | 8 columns |
-- Export active members with all details
SELECT *
FROM export.XpPeople
WHERE MemberStatus = 'Member' -- Text value, not ID
AND DeceasedDate IS NULL; -- Not deceased
-- Export family units with size calculation
SELECT TOP 1000
f.*,
(SELECT COUNT(*) FROM export.XpPeople p WHERE p.FamilyId = f.FamilyId) as FamilySize
FROM export.XpFamily f
WHERE f.HeadOfHouseholdId IS NOT NULL;
-- Export contribution data for year
SELECT TOP 1000
c.*,
p.FirstName,
p.LastName
FROM export.XpContribution c
JOIN export.XpPeople p ON c.PeopleId = p.PeopleId
WHERE c.Date >= '2024-01-01'
ORDER BY c.Date;
| View Name | Purpose | Notes |
|---|---|---|
AllLookups |
All lookup table values | Consolidated reference data |
BundleList |
Contribution bundle management | 15 columns for reconciliation |
City |
Cities with state/zip | Geographic reference |
OrgsWithFees |
Organizations charging fees | Financial tracking |
VolunteerTimes |
Volunteer scheduling | Time slot management |
MoveSchedule |
Scheduled moves between orgs | 10 columns for automation |
MinistryInfo |
Ministry participation summary | Cross-ministry involvement |
-- Discover all views in your database
SELECT
v.TABLE_SCHEMA,
v.TABLE_NAME,
COUNT(c.COLUMN_NAME) as ColumnCount
FROM INFORMATION_SCHEMA.VIEWS v
LEFT JOIN INFORMATION_SCHEMA.COLUMNS c
ON v.TABLE_SCHEMA = c.TABLE_SCHEMA
AND v.TABLE_NAME = c.TABLE_NAME
WHERE v.TABLE_SCHEMA NOT IN ('sys', 'INFORMATION_SCHEMA')
GROUP BY v.TABLE_SCHEMA, v.TABLE_NAME
ORDER BY v.TABLE_SCHEMA, v.TABLE_NAME;
-- Analyze view usage (requires appropriate permissions)
SELECT
OBJECT_NAME(v.object_id) as ViewName,
s.last_user_seek,
s.last_user_scan,
s.user_seeks + s.user_scans as TotalReads
FROM sys.views v
LEFT JOIN sys.dm_db_index_usage_stats s
ON v.object_id = s.object_id
WHERE s.database_id = DB_ID()
ORDER BY TotalReads DESC;
| Field | Type | Description | Critical Notes |
|---|---|---|---|
Id |
int | Transaction ID | Primary key |
OrgId |
int | Organization | → Organizations.OrganizationId |
PeopleId |
int | Person | → People.PeopleId |
Amt |
decimal | Amount | Transaction amount |
TransactionDate |
datetime | Date | When processed |
PaymentType |
nvarchar | Payment method | Credit, Check, Cash, etc. |
Approved |
bit | Status | 1 = Approved |
| Purpose | Key Fields | Links To |
|---|---|---|
| Registration payments | TransactionId, RegPersonId, Amt | Transaction, RecReg, People |
-- Monthly giving summary by fund
SELECT
YEAR(c.ContributionDate) as Year,
MONTH(c.ContributionDate) as Month,
cf.FundName,
COUNT(DISTINCT c.PeopleId) as Givers,
COUNT(*) as Gifts,
SUM(c.ContributionAmount) as TotalAmount,
AVG(c.ContributionAmount) as AvgGift
FROM Contribution c
JOIN ContributionFund cf ON c.FundId = cf.FundId
WHERE c.ContributionDate >= DATEADD(month, -12, GETDATE())
AND c.ContributionStatusId = 0 -- Posted
GROUP BY YEAR(c.ContributionDate), MONTH(c.ContributionDate), cf.FundName
ORDER BY Year DESC, Month DESC, TotalAmount DESC
-- Registration POTENTIAL revenue (based on enrollment and fee structure)
-- Shows what should be collected based on current enrollments
SELECT
o.OrganizationName,
o.RegFeePerPerson as FeePerPerson,
o.RegDepositAmount as DepositRequired,
COUNT(om.PeopleId) as CurrentEnrollment,
o.RegFeePerPerson * COUNT(om.PeopleId) as TotalPotentialRevenue,
CASE
WHEN o.RegistrationTypeId = 6 THEN 'Mission Trip'
WHEN o.RegistrationTypeId = 8 THEN 'Event'
ELSE 'Other'
END as RegistrationType
FROM Organizations o
JOIN OrganizationMembers om ON o.OrganizationId = om.OrganizationId
WHERE o.RegFeePerPerson > 0
AND om.MemberTypeId NOT IN (310, 320) -- Exclude leaders/assistants
AND om.InactiveDate IS NULL -- Active members only
GROUP BY o.OrganizationId, o.OrganizationName, o.RegFeePerPerson,
o.RegDepositAmount, o.RegistrationTypeId
ORDER BY TotalPotentialRevenue DESC
-- BEST: Actual payments received using TransactionSummary
-- This is what my payment management systems use and you need to be adding CSH CHK to the description prefix
-- Shows REAL money collected, not just online transactions.
SELECT
o.OrganizationName,
COUNT(DISTINCT ts.PeopleId) as PayingParticipants,
SUM(CASE WHEN t.amt > 0 THEN t.amt ELSE 0 END) as TotalPaid,
COUNT(DISTINCT t.Id) as NumTransactions,
MIN(t.TransactionDate) as FirstPayment,
MAX(t.TransactionDate) as LastPayment,
-- Payment type breakdown
SUM(CASE WHEN t.Message LIKE 'CHK%' THEN t.amt ELSE 0 END) as CheckPayments,
SUM(CASE WHEN t.Message LIKE 'CSH%' THEN t.amt ELSE 0 END) as CashPayments,
SUM(CASE WHEN t.Message LIKE 'Response%' THEN t.amt ELSE 0 END) as CreditCardPayments
FROM TransactionSummary ts
JOIN Organizations o ON ts.OrganizationId = o.OrganizationId
LEFT JOIN [Transaction] t ON ts.RegId = t.OriginalId
AND t.amt > 0
AND t.Approved = 1
WHERE (ts.TotDue > 0 OR ts.TotPaid > 0) -- Has financial activity
AND t.TransactionDate >= DATEADD(year, -1, GETDATE())
GROUP BY o.OrganizationId, o.OrganizationName
HAVING SUM(CASE WHEN t.amt > 0 THEN t.amt ELSE 0 END) > 0
ORDER BY TotalPaid DESC
TouchPoint has two different registration data systems. Older registrations store answers as XML blobs in RegistrationData. Newer registrations use structured tables (RegQuestion/RegAnswer). You may need to query both to get complete data.
| Table | Records | Purpose | System |
|---|---|---|---|
Registration |
Varies | Registration transaction (parent record) | New system |
RegPeople |
Varies | Individual registrants within a registration | New system |
RegQuestion |
Varies | Custom questions defined for an event | New system |
RegAnswer |
Varies | Answers to custom questions | New system |
RegistrationData |
179K+ | Legacy XML blob with all form responses | Legacy (XML) |
RecReg |
Varies | Medical/emergency info per person (emcontact, doctor, insurance, allergies) | Both systems |
PeopleAuthorizedCheckOut |
Varies | People authorized to pick up a child | Both systems |
PersonMedication |
Varies | Approved medications per person | New system (→ lookup.Medication) |
Flow: Registration → RegPeople → RegAnswer ← RegQuestion
This is the current system. Questions are defined in RegQuestion, answers stored in RegAnswer, linked through RegPeople.
-- Registration answers from new structured tables
SELECT
rp.PeopleId,
p.Name,
rq.Label as Question,
rq.[Order] as QuestionOrder,
ra.AnswerValue as Answer
FROM Registration r WITH (NOLOCK)
JOIN RegPeople rp WITH (NOLOCK) ON r.RegistrationId = rp.RegistrationId
LEFT JOIN RegAnswer ra WITH (NOLOCK) ON rp.RegPeopleId = ra.RegPeopleId
LEFT JOIN RegQuestion rq WITH (NOLOCK) ON ra.RegQuestionId = rq.RegQuestionId
JOIN People p ON rp.PeopleId = p.PeopleId
WHERE r.OrganizationId = 123 -- Your org
AND rq.Label IS NOT NULL
ORDER BY p.Name, rq.[Order]
Older registrations store all form data as XML in RegistrationData.Data (NVARCHAR(MAX)). The XML contains <OnlineRegPersonModel> blocks with nested <ExtraQuestion>, <Text>, and <YesNoQuestion> elements. You must parse this XML to extract answers.
Key fields:
RegistrationData.completed = 1 — Completed registrationsRegistrationData.completed IS NULL or = 0 — Abandoned/incompleteRegistrationData.Data — The XML blob containing all answersRegistrationData.Stamp — When registration was submitted-- Legacy: Event registration summary from RegistrationData
SELECT
o.OrganizationName,
COUNT(DISTINCT rd.PeopleId) as Registrants,
SUM(CASE WHEN rd.completed = 1 THEN 1 ELSE 0 END) as Completed,
SUM(CASE WHEN rd.completed IS NULL OR rd.completed = 0 THEN 1 ELSE 0 END) as Abandoned,
MIN(rd.Stamp) as FirstRegistration,
MAX(rd.Stamp) as LastRegistration
FROM RegistrationData rd
JOIN Organizations o ON rd.OrganizationId = o.OrganizationId
WHERE rd.Stamp >= DATEADD(month, -6, GETDATE())
GROUP BY o.OrganizationName
ORDER BY Registrants DESC
-- Abandoned registrations (started but never completed)
SELECT
o.OrganizationName,
p.Name,
p.EmailAddress,
rd.Stamp as StartedDate,
DATEDIFF(day, rd.Stamp, GETDATE()) as DaysAgo
FROM RegistrationData rd
JOIN Organizations o ON rd.OrganizationId = o.OrganizationId
JOIN People p ON rd.PeopleId = p.PeopleId
WHERE (rd.completed IS NULL OR rd.completed = 0) -- Never finished
AND rd.Stamp >= DATEADD(month, -3, GETDATE())
AND o.OrganizationStatusId = 30
ORDER BY rd.Stamp DESC
The RecReg table stores per-person medical and emergency contact information. This data is shared across both registration systems and is commonly used for children's ministry and event safety forms.
-- Medical/emergency data for org members
SELECT
p.Name, p.Age,
rr.emcontact as EmergencyContact,
rr.emphone as EmergencyPhone,
rr.doctor, rr.docphone as DoctorPhone,
rr.insurance, rr.policy as PolicyNumber,
rr.MedicalDescription as Allergies,
rr.MedAllergy as HasAllergy,
ISNULL(p.CustodyIssue, 0) as CustodyIssue
FROM OrganizationMembers om
JOIN People p ON om.PeopleId = p.PeopleId
LEFT JOIN RecReg rr ON rr.PeopleId = p.PeopleId
WHERE om.OrganizationId = 123
AND om.InactiveDate IS NULL
ORDER BY p.Name2
-- Authorized pickup people (children's ministry)
SELECT p.Name as Child, auth.Name2 as AuthorizedPerson
FROM PeopleAuthorizedCheckOut ac
JOIN People p ON p.PeopleId = ac.PeopleId
JOIN People auth ON auth.PeopleId = ac.AuthorizedPeopleId
WHERE ac.PeopleId IN (SELECT PeopleId FROM OrganizationMembers WHERE OrganizationId = 123)
-- Approved medications (new system with lookup table)
SELECT p.Name, md.Description as Medication
FROM PersonMedication pm
JOIN People p ON p.PeopleId = pm.PeopleId
JOIN lookup.Medication md ON md.Id = pm.MedicationId
WHERE pm.PeopleId IN (SELECT PeopleId FROM OrganizationMembers WHERE OrganizationId = 123)
ORDER BY p.Name, md.Description
| Field | Type | Description | Usage Notes |
|---|---|---|---|
RegistrationId |
int | Primary key | Registration ID |
OrganizationId |
int | Event/Org | → Organizations |
PeopleId |
int | Registrant | Person who registered |
CreatedDate |
datetime | Started | Registration begun |
CompletedDate |
datetime | Completed | Registration finished |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
RegPeopleId |
int | Primary key | Person in registration |
RegistrationId |
int | Registration | → Registration |
PeopleId |
int | Person | → People |
FirstName |
nvarchar | First name | As entered |
LastName |
nvarchar | Last name | As entered |
Email |
nvarchar | Contact email | |
Fee |
money | Fee amount | Cost for this person |
-- Event registration details
SELECT TOP 500
o.OrganizationName as Event,
r.RegistrationId,
registrant.Name as RegisteredBy,
r.CreatedDate as RegistrationStarted,
r.CompletedDate as RegistrationCompleted,
rp.FirstName + ' ' + rp.LastName as Participant,
rp.Email,
rp.Fee,
CASE
WHEN rp.CompletedDate IS NOT NULL THEN 'Complete'
ELSE 'Incomplete'
END as Status
FROM Registration r
JOIN Organizations o ON r.OrganizationId = o.OrganizationId
JOIN People registrant ON r.PeopleId = registrant.PeopleId
JOIN RegPeople rp ON r.RegistrationId = rp.RegistrationId
WHERE o.RegistrationTypeId > 0 -- Has registration
AND r.CreatedDate >= DATEADD(month, -3, GETDATE())
ORDER BY r.CreatedDate DESC
-- Custom form responses
SELECT TOP 1000
o.OrganizationName,
p.Name as Participant,
rq.Label as Question,
rq.[Order] as QuestionOrder,
ra.AnswerValue as Answer,
CASE
WHEN rq.IsRequired = 1 THEN 'Required'
ELSE 'Optional'
END as QuestionType
FROM RegAnswer ra
JOIN RegPeople rp ON ra.RegPeopleId = rp.RegPeopleId
JOIN People p ON rp.PeopleId = p.PeopleId
JOIN RegQuestion rq ON ra.RegQuestionId = rq.RegQuestionId
JOIN Registration r ON rp.RegistrationId = r.RegistrationId
JOIN Organizations o ON r.OrganizationId = o.OrganizationId
WHERE r.CompletedDate >= DATEADD(month, -1, GETDATE())
ORDER BY o.OrganizationName, p.Name, rq.[Order]
| Table | Purpose | Key Fields |
|---|---|---|
Resource |
Resource definitions | ResourceId, Name, Type, Capacity |
Reservable |
Reservation settings | ResourceId, StartTime, EndTime |
Calendar |
Scheduled events | Id, ResourceId, StartTime, EndTime |
-- Room utilization analysis
SELECT
r.Name as ResourceName,
r.Type,
r.Capacity,
COUNT(c.Id) as TotalReservations,
SUM(DATEDIFF(hour, c.StartTime, c.EndTime)) as TotalHours,
AVG(DATEDIFF(hour, c.StartTime, c.EndTime)) as AvgHours
FROM Resource r
LEFT JOIN Calendar c ON r.ResourceId = c.ResourceId
WHERE c.StartTime >= DATEADD(month, -3, GETDATE())
GROUP BY r.Name, r.Type, r.Capacity
ORDER BY TotalReservations DESC
Scale: TaskNote table contains 174K+ records for ministry follow-up and task tracking
| Column | Type | Description | Notes |
|---|---|---|---|
Id |
int | Primary key | Unique task identifier |
PeopleId |
int | Task about person | → People.PeopleId |
OwnerId |
int | Task owner | → People.PeopleId |
DelegatedId |
int | Delegated to | → People.PeopleId |
TaskListId |
int | Task category | → TaskList.Id |
CreatedOn |
datetime | Created date | Task creation timestamp |
Due |
datetime | Due date | When task should be done |
CompletedOn |
datetime | Completion date | NULL if not complete |
Notes |
nvarchar(max) | Task description | Detailed notes |
Priority |
int | Priority level | 1=High, 2=Medium, 3=Low |
-- Recently created open tasks (more useful than future due dates)
-- ⚠️ NOTE: Most TaskNote records have due dates in the past (162K+ overdue vs 1 future)
-- This query shows recently CREATED tasks regardless of due date
SELECT TOP 500
owner.Name as Owner,
about.Name as AboutPerson,
tn.Notes,
tn.DueDate,
tn.CreatedDate,
CASE
WHEN tn.DueDate > GETDATE() THEN DATEDIFF(day, GETDATE(), tn.DueDate)
ELSE -DATEDIFF(day, tn.DueDate, GETDATE())
END as DaysFromDue,
ts.Description as TaskStatus
FROM TaskNote tn
JOIN People owner ON tn.OwnerId = owner.PeopleId
JOIN People about ON tn.AboutPersonId = about.PeopleId
LEFT JOIN lookup.TaskStatus ts ON tn.StatusId = ts.Id
WHERE tn.CompletedDate IS NULL
AND tn.CreatedDate >= DATEADD(month, -3, GETDATE()) -- Created in last 3 months
ORDER BY tn.CreatedDate DESC
-- Recently overdue tasks (practical date range)
-- Filters to tasks due within last 2 years to avoid ancient tasks
SELECT TOP 100
owner.Name as Owner,
about.Name as AboutPerson,
tn.Notes,
tn.DueDate,
DATEDIFF(day, tn.DueDate, GETDATE()) as DaysOverdue
FROM TaskNote tn
JOIN People owner ON tn.OwnerId = owner.PeopleId
JOIN People about ON tn.AboutPersonId = about.PeopleId
WHERE tn.CompletedDate IS NULL
AND tn.DueDate < GETDATE()
AND tn.DueDate > DATEADD(year, -2, GETDATE()) -- Due within last 2 years
ORDER BY tn.DueDate DESC
-- Task completion metrics
SELECT
YEAR(CreatedDate) as Year,
MONTH(CreatedDate) as Month,
COUNT(*) as TasksCreated,
SUM(CASE WHEN CompletedDate IS NOT NULL THEN 1 ELSE 0 END) as TasksCompleted,
AVG(DATEDIFF(day, CreatedDate, CompletedDate)) as AvgDaysToComplete
FROM TaskNote
WHERE CreatedDate >= DATEADD(year, -1, GETDATE())
GROUP BY YEAR(CreatedDate), MONTH(CreatedDate)
ORDER BY Year DESC, Month DESC
-- ENHANCED: Top task creators/owners/completers (from TaskNote Activity Dashboard)
-- Shows who is most active in the task system
SELECT TOP 20
p.Name,
COUNT(DISTINCT CASE WHEN tn.OwnerId = p.PeopleId THEN tn.TaskNoteId END) AS TasksCreated,
COUNT(DISTINCT CASE WHEN tn.AssigneeId = p.PeopleId THEN tn.TaskNoteId END) AS TasksAssigned,
COUNT(DISTINCT CASE WHEN tn.CompletedBy = p.PeopleId THEN tn.TaskNoteId END) AS TasksCompleted,
COUNT(DISTINCT CASE WHEN tn.OwnerId = p.PeopleId AND tn.IsNote = 1 THEN tn.TaskNoteId END) AS NotesCreated
FROM People p
JOIN TaskNote tn ON p.PeopleId IN (tn.OwnerId, tn.AssigneeId, tn.CompletedBy)
WHERE tn.CreatedDate >= DATEADD(month, -3, GETDATE())
GROUP BY p.PeopleId, p.Name
ORDER BY (COUNT(DISTINCT CASE WHEN tn.OwnerId = p.PeopleId THEN tn.TaskNoteId END) +
COUNT(DISTINCT CASE WHEN tn.AssigneeId = p.PeopleId THEN tn.TaskNoteId END) +
COUNT(DISTINCT CASE WHEN tn.CompletedBy = p.PeopleId THEN tn.TaskNoteId END)) DESC
-- Overdue tasks by assignee with workload analysis
SELECT
p.Name AS Assignee,
COUNT(*) AS OverdueTasks,
MIN(tn.DueDate) AS OldestDueDate,
DATEDIFF(day, MIN(tn.DueDate), GETDATE()) AS MaxDaysOverdue,
AVG(DATEDIFF(day, tn.DueDate, GETDATE())) AS AvgDaysOverdue
FROM TaskNote tn
JOIN People p ON p.PeopleId = tn.AssigneeId
WHERE tn.DueDate IS NOT NULL
AND tn.DueDate < GETDATE()
AND tn.StatusId NOT IN (1, 5) -- Not completed or archived
AND tn.IsNote = 0 -- Tasks only, not notes
AND tn.DueDate > DATEADD(year, -2, GETDATE()) -- Practical date range
GROUP BY p.PeopleId, p.Name
HAVING COUNT(*) > 0
ORDER BY COUNT(*) DESC
-- Task/Note activity trend by month
WITH MonthlyActivity AS (
SELECT
FORMAT(CreatedDate, 'yyyy-MM') AS MonthYear,
COUNT(CASE WHEN IsNote = 0 THEN 1 END) AS Tasks,
COUNT(CASE WHEN IsNote = 1 THEN 1 END) AS Notes,
COUNT(CASE WHEN CompletedDate IS NOT NULL AND IsNote = 0 THEN 1 END) AS TasksCompleted
FROM TaskNote
WHERE CreatedDate >= DATEADD(month, -12, GETDATE())
GROUP BY FORMAT(CreatedDate, 'yyyy-MM')
)
SELECT
MonthYear,
Tasks,
Notes,
TasksCompleted,
CASE
WHEN Tasks > 0 THEN CAST(TasksCompleted AS float) / Tasks * 100
ELSE 0
END AS CompletionRate
FROM MonthlyActivity
ORDER BY MonthYear DESC
-- Recent notes about people
SELECT TOP 500
about.Name as AboutPerson,
creator.Name as CreatedBy,
tn.CreatedDate,
tn.Instructions as NoteContent,
tn.Notes as AdditionalInfo
FROM TaskNote tn
JOIN People about ON tn.AboutPersonId = about.PeopleId
JOIN People creator ON tn.CreatedBy = creator.PeopleId
WHERE tn.IsNote = 1 -- Notes only
AND tn.CreatedDate >= DATEADD(month, -1, GETDATE())
ORDER BY tn.CreatedDate DESC
Critical Relationship: Program (top) → Division (middle) → Organizations (bottom)
This three-tier hierarchy organizes all groups, classes, and ministries
| Column | Type | Description | Example |
|---|---|---|---|
Id |
int | Primary key | 101 |
Name |
nvarchar(50) | Program name | 'Adult Ministry' |
SortOrder |
int | Display order | 1, 2, 3... |
IsActive |
bit | Active status | 1=active |
| Column | Type | Description | Example |
|---|---|---|---|
Id |
int | Primary key | 201 |
Name |
nvarchar(50) | Division name | 'Small Groups' |
ProgId |
int | Parent program | → Program.Id |
SortOrder |
int | Display order | 1, 2, 3... |
-- Complete organizational structure
SELECT
p.Name as Program,
d.Name as Division,
o.OrganizationName,
COUNT(om.PeopleId) as MemberCount
FROM Program p
JOIN Division d ON p.Id = d.ProgId
JOIN Organizations o ON d.Id = o.DivisionId
LEFT JOIN OrganizationMembers om ON o.OrganizationId = om.OrganizationId
AND om.InactiveDate IS NULL
WHERE o.OrganizationStatusId = 30
GROUP BY p.Id, p.Name, d.Id, d.Name, o.OrganizationId, o.OrganizationName
ORDER BY p.SortOrder, d.SortOrder, o.OrganizationName
-- Programs with most participation
SELECT
p.Name as Program,
COUNT(DISTINCT d.Id) as Divisions,
COUNT(DISTINCT o.OrganizationId) as Organizations,
COUNT(DISTINCT om.PeopleId) as TotalPeople
FROM Program p
LEFT JOIN Division d ON p.Id = d.ProgId
LEFT JOIN Organizations o ON d.Id = o.DivisionId
AND o.OrganizationStatusId = 30
LEFT JOIN OrganizationMembers om ON o.OrganizationId = om.OrganizationId
AND om.InactiveDate IS NULL
GROUP BY p.Id, p.Name
ORDER BY COUNT(DISTINCT om.PeopleId) DESC
TouchPoint uses dual relationship tracking:
-- Complete family units with all members
-- Note: Both spouses are Primary Adults (10), distinguished by HeadOfHouseholdId/HeadOfHouseholdSpouseId
WITH FamilyStructure AS (
SELECT TOP 500
f.FamilyId,
hoh.Name as HeadOfHousehold,
spouse.Name as Spouse,
STRING_AGG(CASE WHEN p.PositionInFamilyId = 30 THEN p.Name END, ', ') as Children,
STRING_AGG(CASE WHEN p.PositionInFamilyId = 20 THEN p.Name END, ', ') as OtherAdults,
COUNT(DISTINCT p.PeopleId) as FamilySize,
COUNT(DISTINCT CASE WHEN p.PositionInFamilyId = 30 THEN p.PeopleId END) as NumChildren
FROM Families f
LEFT JOIN People hoh ON f.HeadOfHouseholdId = hoh.PeopleId
AND hoh.IsDeceased = 0
LEFT JOIN People spouse ON f.HeadOfHouseholdSpouseId = spouse.PeopleId
AND spouse.IsDeceased = 0
LEFT JOIN People p ON f.FamilyId = p.FamilyId
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
GROUP BY f.FamilyId, hoh.Name, spouse.Name
)
SELECT
FamilyId,
HeadOfHousehold,
Spouse,
Children,
OtherAdults,
FamilySize,
NumChildren
FROM FamilyStructure
WHERE FamilySize > 1 -- Multi-person families only
ORDER BY FamilySize DESC, NumChildren DESC
-- Direct spouse relationships (most reliable)
SELECT
p1.PeopleId as Person1Id,
p1.Name as Person1,
p2.PeopleId as Person2Id,
p2.Name as Person2,
p1.WeddingDate,
DATEDIFF(year, p1.WeddingDate, GETDATE()) as YearsMarried
FROM People p1
JOIN People p2 ON p1.SpouseId = p2.PeopleId
WHERE p1.IsDeceased = 0 AND p1.ArchivedFlag = 0
AND p2.IsDeceased = 0 AND p2.ArchivedFlag = 0
AND p1.PeopleId < p2.PeopleId -- Avoid duplicates
ORDER BY p1.Name
⚠️ Important for Email Communications: When emailing "parents," TouchPoint targets Primary Adults (PositionInFamilyId = 10). Since BOTH spouses are Primary Adults, both will receive emails. If someone is incorrectly marked as Secondary Adult (20) instead of Primary Adult, they will NOT receive parent communications. Secondary Adult should only be used for non-nuclear family members like grandparents living with the family.
-- Find all children with their parents
-- IMPORTANT: Both parents are Primary Adults (10), distinguished by HeadOfHouseholdId/HeadOfHouseholdSpouseId
-- Secondary Adult (20) is for non-nuclear family members (grandparents, adult siblings living with family)
SELECT TOP 500
child.Name as ChildName,
child.Age,
primary_adult.Name as Parent1,
secondary_adult.Name as Parent2,
f.FamilyId
FROM People child
JOIN Families f ON child.FamilyId = f.FamilyId
LEFT JOIN People primary_adult ON primary_adult.PeopleId = f.HeadOfHouseholdId
AND primary_adult.IsDeceased = 0
LEFT JOIN People secondary_adult ON secondary_adult.PeopleId = f.HeadOfHouseholdSpouseId
AND secondary_adult.IsDeceased = 0
WHERE child.PositionInFamilyId = 30 -- Child
AND child.Age < 18
AND child.IsDeceased = 0 AND child.ArchivedFlag = 0
ORDER BY child.Age DESC, child.Name
TouchPoint tracks detailed communication preferences. Violating these preferences can result in complaints and legal issues.
-- People who CAN receive emails
SELECT TOP 1000
p.PeopleId,
p.Name,
p.EmailAddress,
p.EmailAddress2,
CASE
WHEN p.SendEmailAddress1 = 1 AND p.EmailAddress IS NOT NULL THEN p.EmailAddress
WHEN p.SendEmailAddress2 = 1 AND p.EmailAddress2 IS NOT NULL THEN p.EmailAddress2
ELSE NULL
END as PreferredEmail
FROM People p
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
AND p.DoNotMailFlag = 0 -- General mail permission
AND (
(p.SendEmailAddress1 = 1 AND p.EmailAddress IS NOT NULL AND p.EmailAddress != '')
OR
(p.SendEmailAddress2 = 1 AND p.EmailAddress2 IS NOT NULL AND p.EmailAddress2 != '')
)
AND p.PrimaryBadAddrFlag = 0 -- Good address on file
ORDER BY p.Name
-- People who CAN receive text messages
SELECT TOP 1000
p.PeopleId,
p.Name,
p.CellPhone,
p.CellPhoneValidated,
p.CellPhoneIsValid
FROM People p
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
AND p.DoNotCallFlag = 0 -- Can call/text
AND p.ReceiveSMS = 1 -- SMS specifically allowed
AND p.CellPhoneIsValid = 1 -- Valid phone number
AND p.CellPhone IS NOT NULL AND p.CellPhone != ''
ORDER BY p.Name
-- People who CAN receive postal mail
SELECT TOP 1000
p.PeopleId,
p.Name,
p.PrimaryAddress,
p.PrimaryAddress2,
p.PrimaryCity,
p.PrimaryState,
p.PrimaryZip
FROM People p
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
AND p.DoNotMailFlag = 0 -- Mail allowed
AND p.PrimaryBadAddrFlag = 0 -- Valid address
AND p.PrimaryAddress IS NOT NULL AND p.PrimaryAddress != ''
ORDER BY p.PrimaryZip, p.Name
-- People who CAN be visited
SELECT TOP 1000
p.PeopleId,
p.Name,
p.PrimaryAddress,
p.PrimaryCity,
p.CellPhone,
rc.Description as ResidentCode
FROM People p
LEFT JOIN lookup.ResidentCode rc ON p.ResCodeId = rc.Id
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
AND p.DoNotVisitFlag = 0 -- Visits allowed
AND p.PrimaryBadAddrFlag = 0 -- Valid address
AND p.ResCodeId IN (10, 20) -- Metro or Marginal (close enough to visit)
ORDER BY p.PrimaryCity, p.Name
| Id | Status | Description | Typical Use |
|---|---|---|---|
| 10 | Member | Full church member | Voting, leadership eligible |
| 20 | Prospect | Potential member | Follow-up, engagement tracking |
| 30 | Not Member | Regular attender | Non-member participant |
| 40 | Previous Member | Former member | Different communication approach |
| 50 | Just Added | New record | Needs data validation |
-- Complete membership journey analysis
SELECT TOP 500
p.PeopleId,
p.Name,
ms.Description as CurrentStatus,
-- Key Dates
p.CreatedDate as FirstRecorded,
p.DecisionDate,
p.BaptismDate,
p.NewMemberClassDate,
p.JoinDate,
p.DropDate,
-- Status Details
dt.Description as DecisionType,
bs.Description as BaptismStatus,
nmcs.Description as NewMemberClassStatus,
drop_type.Description as DropReason,
-- Journey Metrics
DATEDIFF(day, p.CreatedDate, p.JoinDate) as DaysToMembership,
DATEDIFF(day, p.DecisionDate, p.BaptismDate) as DaysToBaptism,
CASE
WHEN p.DropDate IS NOT NULL THEN 'Inactive'
WHEN p.JoinDate IS NOT NULL THEN 'Member'
WHEN p.BaptismDate IS NOT NULL THEN 'Baptized'
WHEN p.DecisionDate IS NOT NULL THEN 'Decided'
ELSE 'Exploring'
END as JourneyStage
FROM People p
LEFT JOIN lookup.MemberStatus ms ON p.MemberStatusId = ms.Id
LEFT JOIN lookup.DecisionType dt ON p.DecisionTypeId = dt.Id
LEFT JOIN lookup.BaptismStatus bs ON p.BaptismStatusId = bs.Id
LEFT JOIN lookup.NewMemberClassStatus nmcs ON p.NewMemberClassStatusId = nmcs.Id
LEFT JOIN lookup.DropType drop_type ON p.DropCodeId = drop_type.Id
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
ORDER BY p.JoinDate DESC
-- Track new member pipeline
SELECT TOP 500
CASE
WHEN p.JoinDate IS NOT NULL THEN '5. Member'
WHEN p.NewMemberClassDate IS NOT NULL THEN '4. Class Complete'
WHEN p.BaptismDate IS NOT NULL THEN '3. Baptized'
WHEN p.DecisionDate IS NOT NULL THEN '2. Decision Made'
WHEN p.MemberStatusId = 20 THEN '1. Prospect'
ELSE '0. Visitor'
END as Stage,
COUNT(*) as PeopleCount,
AVG(DATEDIFF(day, p.CreatedDate, GETDATE())) as AvgDaysInStage
FROM People p
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
AND p.CreatedDate >= DATEADD(year, -1, GETDATE())
AND p.MemberStatusId IN (20, 50) -- Prospects and Just Added
GROUP BY
CASE
WHEN p.JoinDate IS NOT NULL THEN '5. Member'
WHEN p.NewMemberClassDate IS NOT NULL THEN '4. Class Complete'
WHEN p.BaptismDate IS NOT NULL THEN '3. Baptized'
WHEN p.DecisionDate IS NOT NULL THEN '2. Decision Made'
WHEN p.MemberStatusId = 20 THEN '1. Prospect'
ELSE '0. Visitor'
END
ORDER BY Stage
Purpose: Groups, classes, ministries, events (called "Involvements" in UI)
Primary Key: OrganizationId
Scale: 188 columns with 16 foreign key relationships
Hierarchy: Program → Division → Organization
Note: Organizations are called "Involvements" in the TouchPoint user interface
| Field | Type | Description | Usage Notes |
|---|---|---|---|
OrganizationId |
int | Primary key | Unique identifier |
OrganizationName |
nvarchar(100) | Display name | Public name |
Description |
ntext | Detailed description | Full text description |
OrganizationStatusId Critical |
int | Status | 30 = Active (not 1!) |
OrganizationTypeId |
int | Type | → lookup.OrganizationType |
DivisionId |
int | Division | → Division.Id |
CampusId |
int | Campus | → lookup.Campus |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
LeaderId |
int | Primary leader | → People.PeopleId |
LeaderName |
nvarchar(50) | Leader name | Computed field |
MainLeaderId |
int | Main leader | If different from Leader |
LeaderMemberTypeId |
int | Leader role | → lookup.MemberType |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
MemberCount |
int | Current members | Auto-updated |
PrevMemberCount |
int | Previous count | Historical tracking |
ProspectCount |
int | Prospects | Potential members |
Limit |
int | Max capacity | Room/resource limit |
ClassFilled |
bit | At capacity | Registration closed |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
RegistrationTypeId |
int | Registration type | 0=none, others=form types |
RegStart |
datetime | Registration opens | Start accepting signups |
RegEnd |
datetime | Registration closes | Stop accepting signups |
RegLimit |
int | Registration limit | Max registrations |
RegistrationClosed |
bit | Manually closed | Override dates |
RegFeePerPerson |
money | Fee amount | Cost per person |
RegFeeType |
int | Fee type | → lookup.ContributionType |
RegDepositAmount |
money | Deposit required | Upfront payment |
RegFundId |
int | Payment fund | → ContributionFund |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
CanSelfCheckin |
bit | Allow self check-in | Kiosk enabled |
NumCheckInLabels |
int | Labels per person | Print count |
NumWorkerCheckInLabels |
int | Worker labels | Volunteer labels |
ShowOnlyRegisteredAtCheckIn |
bit | Restrict check-in | Members only |
AllowNonCampusCheckIn |
bit | Cross-campus | Other locations OK |
SuspendCheckin |
bit | Disable check-in | Temporary hold |
EarlyCheckin |
int | Minutes early | Before meeting time |
LateCheckin |
int | Minutes late | After meeting time |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
AllowMobileView |
bit | Show in app | Mobile visibility |
ShowInSites |
bit | Show in sites | Web visibility |
RecommendInMobile |
bit | Recommend | Featured in app |
CategoryMobileId |
int | Mobile category | App organization |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
IsMissionTrip |
bit | Mission trip flag | Special handling |
TripFundingPagesEnable |
bit | Funding pages | Fundraising enabled |
TripFundingPagesPublic |
bit | Public funding | External visibility |
TripFundingPagesShowAmounts |
bit | Show amounts | Display progress |
Purpose: Links people to organizations with specific roles
Composite Key: OrganizationId + PeopleId
Critical Rule: Use InactiveDate IS NULL for current members
| Field | Type | Description | Usage Notes |
|---|---|---|---|
OrganizationId |
int | Organization | → Organizations |
PeopleId |
int | Person | → People |
MemberTypeId |
int | Role in org | → lookup.MemberType |
EnrollmentDate |
datetime | Joined date | When enrolled |
InactiveDate |
datetime | Left date | NULL = active |
LastAttended |
datetime | Last attendance | Most recent |
AttendPct |
decimal | Attendance % | Engagement metric |
Pending |
bit | Pending approval | Awaiting confirmation |
Hidden |
bit | Hide from lists | Privacy setting |
-- Complete organization roster with roles
SELECT TOP 500
o.OrganizationName,
p.Name,
mt.Description as Role,
at.Description as RoleCategory,
om.EnrollmentDate,
om.LastAttended,
om.AttendPct,
CASE
WHEN at.Worker = 1 THEN 'Worker'
WHEN at.Guest = 1 THEN 'Guest'
ELSE 'Member'
END as PersonType
FROM OrganizationMembers om
JOIN People p ON om.PeopleId = p.PeopleId
JOIN Organizations o ON om.OrganizationId = o.OrganizationId
JOIN lookup.MemberType mt ON om.MemberTypeId = mt.Id
JOIN lookup.AttendType at ON mt.AttendanceTypeId = at.Id
WHERE om.InactiveDate IS NULL -- Current members only
AND o.OrganizationStatusId = 30 -- Active orgs
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
ORDER BY o.OrganizationName, at.Worker DESC, p.Name
-- Find all leaders across organizations
SELECT TOP 500
p.Name as LeaderName,
STRING_AGG(o.OrganizationName, ', ') as Organizations,
COUNT(*) as LeadershipRoles
FROM OrganizationMembers om
JOIN People p ON om.PeopleId = p.PeopleId
JOIN Organizations o ON om.OrganizationId = o.OrganizationId
JOIN lookup.MemberType mt ON om.MemberTypeId = mt.Id
WHERE om.InactiveDate IS NULL
AND mt.AttendanceTypeId = 10 -- Leaders (not volunteers)
AND o.OrganizationStatusId = 30
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
GROUP BY p.PeopleId, p.Name
HAVING COUNT(*) > 1 -- Leading multiple orgs
ORDER BY COUNT(*) DESC
-- Complete organizational hierarchy
SELECT TOP 500
Program, -- Top level
Division, -- Second level
Organization, -- Third level (actual group)
OrgStatus,
Members,
Previous,
Vistors,
Meetings,
ProgId, -- Program ID for joins
DivId, -- Division ID for joins
OrgId -- Organization ID for joins
FROM OrganizationStructure
WHERE OrgStatus = 'Active'
ORDER BY Program, Division, Organization
-- Program-level statistics
SELECT TOP 500
Program,
COUNT(DISTINCT DivId) as Divisions,
COUNT(DISTINCT OrgId) as Organizations,
SUM(Members) as TotalMembers,
SUM(Vistors) as TotalVisitors,
SUM(Meetings) as TotalMeetings
FROM OrganizationStructure
WHERE OrgStatus = 'Active'
GROUP BY Program, ProgId
ORDER BY SUM(Members) DESC
-- Division-level breakdown
SELECT TOP 500
Program,
Division,
COUNT(OrgId) as OrgCount,
SUM(Members) as TotalMembers,
AVG(Members) as AvgOrgSize,
MAX(Members) as LargestOrg,
SUM(CASE WHEN Members >= Limit AND Limit > 0 THEN 1 ELSE 0 END) as AtCapacity
FROM OrganizationStructure os
JOIN Organizations o ON os.OrgId = o.OrganizationId
WHERE os.OrgStatus = 'Active'
GROUP BY Program, Division, DivId
ORDER BY Program, Division
AttendanceFlag = 1 means the person attended
AttendanceFlag = 0 means the person was absent
ALWAYS filter WHERE AttendanceFlag = 1 for attendance counts
| Field | Type | Description | Usage Notes |
|---|---|---|---|
AttendId |
bigint | Primary key | Unique record ID |
PeopleId |
int | Person | → People |
MeetingId |
int | Meeting | → Meetings |
OrganizationId |
int | Organization | → Organizations |
MeetingDate |
datetime | Meeting date/time | When it occurred |
AttendanceFlag CRITICAL |
bit | Present/Absent | 1=present, 0=absent |
AttendanceTypeId |
int | Attendance type | → lookup.AttendType |
MemberTypeId |
int | Detailed role | → lookup.MemberType |
NoShow |
bit | No-show flag | Registered but absent |
SubGroupName |
nvarchar | Sub-group | Group within org |
Commitment |
int | RSVP status | 99/NULL=uncommitted, 0=regrets, 1=attending |
-- Attendance records with full details
SELECT TOP 500
p.Name,
o.OrganizationName,
m.MeetingDate,
m.Location,
at.Description as AttendanceType,
mt.Description as SpecificRole,
a.SubGroupName,
CASE a.Commitment
WHEN 1 THEN 'Attending'
WHEN 0 THEN 'Regrets'
WHEN 2 THEN 'Find Sub'
WHEN 3 THEN 'Sub Found'
ELSE 'Uncommitted'
END as CommitmentStatus
FROM Attend a
JOIN People p ON a.PeopleId = p.PeopleId
JOIN Organizations o ON a.OrganizationId = o.OrganizationId
JOIN Meetings m ON a.MeetingId = m.MeetingId
JOIN lookup.AttendType at ON a.AttendanceTypeId = at.Id
JOIN lookup.MemberType mt ON a.MemberTypeId = mt.Id
WHERE a.AttendanceFlag = 1 -- CRITICAL: Only actual attendance
AND m.MeetingDate >= DATEADD(month, -1, GETDATE())
AND m.DidNotMeet = 0 -- Meeting actually happened
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
ORDER BY m.MeetingDate DESC, o.OrganizationName, p.Name
-- Individual attendance patterns
WITH AttendanceHistory AS (
SELECT
p.PeopleId,
p.Name,
COUNT(*) as TimesAttended,
COUNT(DISTINCT a.OrganizationId) as DifferentOrgs,
COUNT(DISTINCT CAST(a.MeetingDate as DATE)) as UniqueDays,
MIN(a.MeetingDate) as FirstAttended,
MAX(a.MeetingDate) as LastAttended
FROM Attend a
JOIN People p ON a.PeopleId = p.PeopleId
WHERE a.AttendanceFlag = 1
AND a.MeetingDate >= DATEADD(month, -6, GETDATE())
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
GROUP BY p.PeopleId, p.Name
)
SELECT
Name,
TimesAttended,
DifferentOrgs,
UniqueDays,
DATEDIFF(day, FirstAttended, LastAttended) as DaySpan,
CAST(TimesAttended as float) / NULLIF(DATEDIFF(week, FirstAttended, LastAttended), 0) as AvgPerWeek
FROM AttendanceHistory
WHERE TimesAttended > 1
ORDER BY TimesAttended DESC
| Field | Type | Description | Usage Notes |
|---|---|---|---|
MeetingId |
int | Primary key | Unique meeting ID |
OrganizationId |
int | Organization | → Organizations |
MeetingDate |
datetime | Start time | Meeting begins |
MeetingEnd |
datetime | End time | Meeting ends |
Location |
nvarchar | Location | Where held |
NumPresent |
int | Total attendance | Recorded count |
HeadCount |
int | Manual count | Override count |
NumMembers |
int | Members present | Regular members |
NumNewVisit |
int | First-time visitors | New guests |
NumRepeatVst |
int | Repeat visitors | Returning guests |
DidNotMeet Important |
bit | Cancelled | 1=cancelled, 0=held |
-- Upcoming meetings schedule
SELECT
o.OrganizationName,
m.MeetingDate,
m.MeetingEnd,
m.Location,
DATEDIFF(minute, m.MeetingDate, m.MeetingEnd) as DurationMinutes,
p.Name as Leader
FROM Meetings m
JOIN Organizations o ON m.OrganizationId = o.OrganizationId
LEFT JOIN People p ON o.LeaderId = p.PeopleId
WHERE m.MeetingDate >= GETDATE()
AND m.MeetingDate <= DATEADD(week, 2, GETDATE())
AND m.DidNotMeet = 0 -- Not cancelled
AND o.OrganizationStatusId = 30 -- Active orgs
ORDER BY m.MeetingDate
-- Meeting attendance analytics
SELECT
o.OrganizationName,
COUNT(*) as MeetingsHeld,
AVG(m.NumPresent) as AvgAttendance,
MAX(m.NumPresent) as MaxAttendance,
MIN(m.NumPresent) as MinAttendance,
SUM(m.NumNewVisit) as TotalNewVisitors,
SUM(m.NumRepeatVst) as TotalRepeatVisitors,
AVG(CAST(m.NumPresent as float) / NULLIF(o.MemberCount, 0)) * 100 as AvgAttendancePct
FROM Meetings m
JOIN Organizations o ON m.OrganizationId = o.OrganizationId
WHERE m.MeetingDate >= DATEADD(month, -3, GETDATE())
AND m.DidNotMeet = 0 -- Actually met
AND o.OrganizationStatusId = 30
GROUP BY o.OrganizationId, o.OrganizationName, o.MemberCount
HAVING COUNT(*) > 3 -- At least 3 meetings
ORDER BY AVG(m.NumPresent) DESC
ContributionTypeId = 99 represents EVENT REGISTRATION FEES
ContributionTypeId = 8 represents PLEDGES (not actual contributions)
PledgeFlag = 1 also indicates pledges
ALWAYS exclude these from giving reports: WHERE ContributionTypeId NOT IN (8, 99) AND (PledgeFlag IS NULL OR PledgeFlag = 0)
💡 TIP: Consider using the Contributions2 function instead of the Contribution table directly. It automatically excludes non-contributions and pledges (when configured), and makes counting spouses who give together easier to report on.
| Field | Type | Description | Usage Notes |
|---|---|---|---|
ContributionId |
int | Primary key | Unique gift ID |
PeopleId |
int | Giver | → People |
ContributionAmount |
money | Gift amount | Dollar value |
ContributionDate |
datetime | Gift date | When received |
FundId |
int | Fund designation | → ContributionFund |
ContributionTypeId CRITICAL |
int | Gift type | 99 = event fee! |
ContributionStatusId |
int | Processing status | → lookup.ContributionStatus |
CheckNo |
nvarchar | Check number | Payment reference |
BundleHeaderId |
int | Batch ID | → BundleHeader |
-- Individual giving summary
SELECT
g.Name,
COUNT(*) AS GiftCount,
SUM(g.ContributionAmount) AS TotalGiven,
AVG(g.ContributionAmount) AS AvgGift,
MIN(g.ContributionDate) AS FirstGift,
MAX(g.ContributionDate) AS LastGift,
STRING_AGG(CAST(df.FundName AS nvarchar(max)), ',') AS Funds
FROM (
-- All gift rows for normal counts/sums
SELECT
p.PeopleId,
p.Name,
c.ContributionAmount,
c.ContributionDate,
cf.FundName
FROM Contribution c
JOIN People p ON c.PeopleId = p.PeopleId
JOIN ContributionFund cf ON c.FundId = cf.FundId
WHERE c.ContributionTypeId NOT IN (6, 7, 8, 99)
AND c.ContributionStatusId = 0
AND c.ContributionDate >= DATEADD(year, -1, GETDATE())
AND p.IsDeceased = 0
AND p.ArchivedFlag = 0
) AS g
JOIN (
-- Distinct funds per person
SELECT DISTINCT
p.PeopleId,
cf.FundName
FROM Contribution c
JOIN People p ON c.PeopleId = p.PeopleId
JOIN ContributionFund cf ON c.FundId = cf.FundId
WHERE c.ContributionTypeId NOT IN (6, 7, 8, 99)
AND c.ContributionStatusId = 0
AND c.ContributionDate >= DATEADD(year, -1, GETDATE())
AND p.IsDeceased = 0
AND p.ArchivedFlag = 0
) AS df
ON g.PeopleId = df.PeopleId
GROUP BY g.PeopleId, g.Name
HAVING SUM(g.ContributionAmount) > 0
ORDER BY SUM(g.ContributionAmount) DESC;
-- Giving by fund
SELECT
cf.FundName,
COUNT(DISTINCT c.PeopleId) as Givers,
COUNT(*) as Gifts,
SUM(c.ContributionAmount) as TotalAmount,
AVG(c.ContributionAmount) as AvgGift,
MAX(c.ContributionAmount) as LargestGift
FROM Contribution c
JOIN ContributionFund cf ON c.FundId = cf.FundId
WHERE c.ContributionTypeId NOT IN (6, 7, 8, 99)
AND c.ContributionStatusId = 0
AND c.ContributionDate >= DATEADD(month, -3, GETDATE())
GROUP BY cf.FundId, cf.FundName
ORDER BY SUM(c.ContributionAmount) DESC
-- Monthly giving trends
SELECT
YEAR(c.ContributionDate) as Year,
MONTH(c.ContributionDate) as Month,
DATENAME(month, c.ContributionDate) as MonthName,
COUNT(DISTINCT c.PeopleId) as UniqueGivers,
COUNT(*) as TotalGifts,
SUM(c.ContributionAmount) as TotalAmount,
AVG(c.ContributionAmount) as AvgGift
FROM Contribution c
WHERE c.ContributionTypeId NOT IN (6, 7, 8, 99)
AND c.ContributionStatusId = 0
AND c.ContributionDate >= DATEADD(year, -2, GETDATE())
GROUP BY YEAR(c.ContributionDate), MONTH(c.ContributionDate), DATENAME(month, c.ContributionDate)
ORDER BY Year DESC, Month DESC
TouchPoint's check-in system provides secure child and event check-in with barcode scanning, security labels, and real-time tracking.
| Field | Type | Description | Usage Notes |
|---|---|---|---|
Id |
int | Primary key | Activity record |
PeopleId |
int | Person | → People |
OrganizationId |
int | Organization | → Organizations |
CheckInTime |
datetime | Check-in time | When checked in |
CheckInBy |
int | Checked in by | → People (worker) |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
Id |
int | Primary key | Time record |
PeopleId |
int | Person | → People |
OrganizationId |
int | Organization | → Organizations |
MeetingId |
int | Meeting | → Meetings |
CheckInTime |
datetime | Time stamp | Exact check-in |
-- Recent check-in activity
SELECT TOP 100
cia.Name as PersonName,
o.OrganizationName,
cia.CheckInTime,
cia.AccessType,
cia.Location,
DATEDIFF(minute, cia.ActivityTime, cia.CheckInTime) as MinutesEarlyLate
FROM CheckInActivities cia
JOIN Organizations o ON cia.OrganizationId = o.OrganizationId
WHERE cia.CheckInTime >= DATEADD(day, -7, GETDATE())
ORDER BY cia.CheckInTime DESC
-- Check-in statistics by organization
SELECT TOP 500
o.OrganizationName,
COUNT(DISTINCT cia.PeopleId) as UniqueCheckIns,
COUNT(*) as TotalCheckIns,
MIN(cia.CheckInTime) as EarliestCheckIn,
MAX(cia.CheckInTime) as LatestCheckIn
FROM CheckInActivities cia
JOIN Organizations o ON cia.OrganizationId = o.OrganizationId
WHERE cia.CheckInTime >= DATEADD(month, -1, GETDATE())
GROUP BY o.OrganizationId, o.OrganizationName
ORDER BY COUNT(*) DESC
Email tables contain millions of records. Always use date filters and TOP clauses:
| Field | Type | Description | Usage Notes |
|---|---|---|---|
Id |
int | Primary key | Email campaign ID |
Subject |
nvarchar(100) | Email subject | Subject line |
Body |
ntext | Email content | HTML/text body |
FromAddress |
nvarchar(50) | From email | Sender address |
QueuedBy |
int | Queued by | → People |
SendAt |
datetime | Send time | Scheduled/sent |
-- Email campaign performance (with date filter for performance)
SELECT TOP 100
eq.Id,
eq.Subject,
eq.SendWhen,
sender.Name as SentBy,
COUNT(DISTINCT eqt.PeopleId) as Recipients,
COUNT(DISTINCT CASE WHEN er.Type = 'o' THEN er.PeopleId END) as UniqueOpens,
COUNT(DISTINCT CASE WHEN eqt.Bounced = 1 THEN eqt.PeopleId END) as Bounces,
CAST(COUNT(DISTINCT CASE WHEN er.Type = 'o' THEN er.PeopleId END) as float) /
NULLIF(COUNT(DISTINCT eqt.PeopleId), 0) * 100 as OpenRate
FROM EmailQueue eq
JOIN People sender ON eq.QueuedBy = sender.PeopleId
LEFT JOIN EmailQueueTo eqt ON eq.Id = eqt.Id
LEFT JOIN EmailResponses er ON eq.Id = er.EmailQueueId
WHERE eq.SendWhen >= DATEADD(month, -1, GETDATE()) -- REQUIRED for performance
GROUP BY eq.Id, eq.Subject, eq.SendWhen, sender.Name
ORDER BY eq.SendWhen DESC
-- Individual email engagement (limited timeframe for performance)
SELECT TOP 500
p.Name,
p.EmailAddress,
COUNT(DISTINCT eq.Id) as EmailsReceived,
COUNT(DISTINCT CASE WHEN er.Type = 'o' THEN eq.Id END) as EmailsOpened,
MAX(CASE WHEN er.Type = 'o' THEN er.Dt END) as LastOpened,
CAST(CASE
WHEN COUNT(DISTINCT eq.Id) > 0
THEN COUNT(DISTINCT CASE WHEN er.Type = 'o' THEN eq.Id END) * 100.0 / COUNT(DISTINCT eq.Id)
ELSE 0
END AS DECIMAL(5,2)) as OpenRate
FROM People p
JOIN EmailQueueTo eqt ON p.PeopleId = eqt.PeopleId
JOIN EmailQueue eq ON eqt.Id = eq.Id
LEFT JOIN EmailResponses er ON eq.Id = er.EmailQueueId AND p.PeopleId = er.PeopleId
WHERE eq.SendWhen >= DATEADD(month, -3, GETDATE()) -- REQUIRED
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
GROUP BY p.PeopleId, p.Name, p.EmailAddress
HAVING COUNT(DISTINCT eq.Id) > 0
ORDER BY COUNT(DISTINCT CASE WHEN er.Type = 'o' THEN eq.Id END) DESC
⚠️ Performance Note: EngagementScoreTag has 1.4M+ records. Always filter by WeekDate.
| Field | Type | Description | Usage Notes |
|---|---|---|---|
Id |
int | Primary key | Score record |
PeopleId |
int | Person | → People |
WeekDate |
datetime | Week | Week of scoring |
TotalScore |
int | Total points | Week's total |
-- Top engaged members (recent)
SELECT TOP 100
p.Name,
SUM(es.TotalScore) as TotalEngagement,
AVG(es.TotalScore) as AvgWeeklyScore,
COUNT(DISTINCT es.WeekDate) as ActiveWeeks,
STRING_AGG(sfl.Name, ', ') as Activities
FROM EngagementScore es
JOIN People p ON es.PeopleId = p.PeopleId
LEFT JOIN EngagementScoreTag est ON es.Id = est.EngagementScoreId
LEFT JOIN StatusFlagList sfl ON est.StatusFlagId = sfl.Flag
WHERE es.WeekDate >= DATEADD(month, -3, GETDATE()) -- REQUIRED
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
GROUP BY p.PeopleId, p.Name
HAVING SUM(es.TotalScore) > 0
-- Weekly engagement trends
SELECT
es.WeekDate,
COUNT(DISTINCT es.PeopleId) as EngagedPeople,
SUM(es.TotalScore) as TotalPoints,
AVG(es.TotalScore) as AvgScore,
MAX(es.TotalScore) as MaxScore
FROM EngagementScore es
WHERE es.WeekDate >= DATEADD(month, -6, GETDATE())
GROUP BY es.WeekDate
ORDER BY es.WeekDate DESC
TagPerson table contains 138 MILLION+ records!
NEVER query without:
| Field | Type | Description | Usage Notes |
|---|---|---|---|
Id |
int | Primary key | Tag ID |
Name |
nvarchar(100) | Tag name | Display name |
TypeId |
int | Tag type | → TagType |
Owner |
int | Tag owner | → People |
-- SAFE: Query specific tag with date filter
SELECT TOP 100
t.Name as TagName,
p.Name as PersonName,
tp.DateCreated
FROM TagPerson tp
JOIN Tag t ON tp.Id = t.Id
JOIN People p ON tp.PeopleId = p.PeopleId
WHERE t.Name = 'Volunteer' -- Specific tag
AND tp.DateCreated >= DATEADD(month, -1, GETDATE()) -- REQUIRED
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
ORDER BY tp.DateCreated DESC
-- NEVER DO THIS:
-- SELECT * FROM TagPerson -- 138M+ records!
-- Tag usage summary (aggregated, not detail)
SELECT TOP 50
t.Name as TagName,
COUNT(*) as PeopleTagged,
MIN(tp.DateCreated) as FirstUsed,
MAX(tp.DateCreated) as LastUsed
FROM Tag t
JOIN TagPerson tp ON t.Id = tp.Id
WHERE tp.DateCreated >= DATEADD(month, -6, GETDATE()) -- REQUIRED
GROUP BY t.Id, t.Name
HAVING COUNT(*) > 10
ORDER BY COUNT(*) DESC
TouchPoint provides 17 Extra Value tables allowing churches to add unlimited custom fields without modifying the database schema.
| Table | Data Type | Usage |
|---|---|---|
PeopleExtra |
Mixed | Person-specific custom fields (most common) |
ExtraValueBit |
Boolean | Yes/No checkbox fields |
ExtraValueDate |
DateTime | Date picker fields |
ExtraValueInt |
Integer | Whole number fields |
ExtraValueText |
Text | Free text fields |
ExtraValueCode |
Code | Dropdown/lookup fields |
OrgExtra |
Mixed | Organization-specific custom fields |
MeetingExtra |
Mixed | Meeting-specific custom fields |
ContributionExtra |
Mixed | Contribution custom fields |
FamilyExtra |
Mixed | Family-level custom fields |
ContactExtra |
Mixed | Contact-specific fields |
TaskExtra |
Mixed | Task custom fields |
VolunteerExtra |
Mixed | Volunteer-specific fields |
MemberExtra |
Mixed | Membership custom fields |
ResourceExtra |
Mixed | Resource tracking fields |
EventExtra |
Mixed | Event-specific fields |
AdminExtra |
Mixed | Administrative custom fields |
-- Find all custom fields in use
SELECT
'PeopleExtra' as TableType,
Field,
COUNT(*) as RecordCount,
COUNT(DISTINCT PeopleId) as PeopleWithValue,
MIN(TransactionTime) as FirstUsed,
MAX(TransactionTime) as LastUsed
FROM PeopleExtra
GROUP BY Field
HAVING COUNT(*) > 10 -- Only show fields with significant usage
ORDER BY COUNT(*) DESC
-- Find custom fields for a specific person
SELECT
p.Name,
'PeopleExtra' as Source,
pev.Field,
pev.Data as Value,
pev.TransactionTime as LastUpdated
FROM People p
JOIN PeopleExtra pev ON p.PeopleId = pev.PeopleId
WHERE p.PeopleId = 12345 -- Specific person
ORDER BY pev.Field
-- Example: Church-specific membership covenant field
SELECT TOP 500
p.Name,
p.MemberStatusId,
pev.Data as MembershipCovenantDate
FROM People p
LEFT JOIN PeopleExtra pev ON p.PeopleId = pev.PeopleId
AND pev.Field = 'MembershipCovenant'
WHERE p.MemberStatusId = 10 -- Members only
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
ORDER BY p.Name
-- Example: Multiple custom fields
SELECT
p.Name,
MAX(CASE WHEN pev.Field = 'BackgroundCheck' THEN pev.Data END) as BackgroundCheckDate,
MAX(CASE WHEN pev.Field = 'SafetyTraining' THEN pev.Data END) as SafetyTrainingDate,
MAX(CASE WHEN pev.Field = 'VolunteerAgreement' THEN pev.Data END) as VolunteerAgreement
FROM People p
LEFT JOIN PeopleExtra pev ON p.PeopleId = pev.PeopleId
WHERE pev.Field IN ('BackgroundCheck', 'SafetyTraining', 'VolunteerAgreement')
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
GROUP BY p.PeopleId, p.Name
HAVING MAX(CASE WHEN pev.Field = 'BackgroundCheck' THEN pev.Data END) IS NOT NULL
-- Find custom fields on organizations
SELECT
o.OrganizationName,
oev.Field,
oev.Data,
oev.StrValue,
oev.DateValue,
oev.IntValue,
oev.BitValue,
oev.TransactionTime
FROM Organizations o
JOIN OrganizationExtra oev ON o.OrganizationId = oev.OrganizationId
WHERE o.OrganizationStatusId = 30
ORDER BY o.OrganizationName, oev.Field
-- Get all custom fields for a person with proper type handling
SELECT
Field,
CASE Type
WHEN 'Text' THEN Data
WHEN 'Date' THEN CONVERT(nvarchar, Date, 101)
WHEN 'Int' THEN CAST(IntValue as nvarchar)
WHEN 'Bit' THEN CASE BitValue WHEN 1 THEN 'Yes' ELSE 'No' END
END as Value
FROM PeopleExtra
WHERE PeopleId = @PeopleId
ORDER BY Field
| Table | Records | Purpose | Key Fields |
|---|---|---|---|
SMSGroups |
Varies | Text groups | Id, Name, Description |
SMSGroupMembers |
Varies | Group membership | GroupId, PeopleId |
SMSItems |
332K+ | Individual messages | Id, PeopleId, Message, Sent |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
ID |
int | Primary key | Message batch ID |
SenderID |
int | Sender | → People.PeopleId |
Message |
ntext | SMS content | Message text |
SendAt |
datetime | Send timestamp | When sent |
SentSMS |
int | Count sent | Number of recipients |
Sent |
bit | Sent flag | Successfully sent |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
Id |
int | Primary key | Received message ID |
FromPeopleId |
int | Sender | → People.PeopleId |
FromNumber |
nvarchar(15) | Sender phone | Phone number |
Body |
ntext | Message content | Text received |
DateReceived |
datetime | Received timestamp | When received |
-- SMS engagement analytics
WITH SMSMetrics AS (
SELECT
-- Sent messages
(SELECT COUNT(*) FROM SMSList WHERE Sent = 1 AND SendAt >= DATEADD(month, -1, GETDATE())) as MessagesSent,
(SELECT SUM(SentSMS) FROM SMSList WHERE Sent = 1 AND SendAt >= DATEADD(month, -1, GETDATE())) as TotalRecipients,
-- Received messages
(SELECT COUNT(*) FROM SmsReceived WHERE DateReceived >= DATEADD(month, -1, GETDATE())) as MessagesReceived,
(SELECT COUNT(DISTINCT FromPeopleId) FROM SmsReceived WHERE DateReceived >= DATEADD(month, -1, GETDATE()) AND FromPeopleId IS NOT NULL) as UniqueResponders
)
SELECT
MessagesSent,
TotalRecipients,
MessagesReceived,
UniqueResponders,
CASE
WHEN TotalRecipients > 0
THEN CAST(UniqueResponders as float) / TotalRecipients * 100
ELSE 0
END as ResponseRate
FROM SMSMetrics
-- SMS conversation history for a person
SELECT TOP 100
COALESCE(p.Name, 'Unknown') as Person,
CASE
WHEN sl.ID IS NOT NULL THEN 'Sent'
ELSE 'Received'
END as Direction,
COALESCE(sl.Message, sr.Body) as Message,
COALESCE(sl.SendAt, sr.DateReceived) as MessageTime
FROM (
SELECT SenderID as PeopleId, Message, SendAt, NULL as DateReceived
FROM SMSList
WHERE Sent = 1 AND SendAt >= DATEADD(month, -6, GETDATE())
UNION ALL
SELECT FromPeopleId, Body, NULL, DateReceived
FROM SmsReceived
WHERE DateReceived >= DATEADD(month, -6, GETDATE())
) messages
LEFT JOIN People p ON messages.PeopleId = p.PeopleId
LEFT JOIN SMSList sl ON messages.SendAt IS NOT NULL
LEFT JOIN SmsReceived sr ON messages.DateReceived IS NOT NULL
WHERE p.PeopleId = 12345 -- Specific person
ORDER BY MessageTime DESC
| Table | Purpose | Key Fields |
|---|---|---|
lookup.CategoryMobile |
Mobile app categories | Id, Code, Description, SortOrder |
LocationMobile |
Mobile location types | Id, Description, Address |
RegistrationMobile |
Mobile registration types | Id, Type, Settings |
AnswersMobileSettings |
Mobile form answers | Id, QuestionId, Answer |
QuestionsMobileSettings |
Mobile form questions | Id, Question, Type |
| View | Purpose | Key Fields |
|---|---|---|
MobileAppActions |
Configurable mobile app menu actions (13 columns) | id, type, title, loginType, enabled, order |
-- Organizations visible in mobile app
SELECT TOP 500
o.OrganizationName,
cm.Description as MobileCategory,
o.AllowMobileView,
o.RecommendInMobile,
o.ShowInSites,
o.ShowMobileRegStart,
o.ShowMobileRegEnd,
o.ShowMobileButtonText
FROM Organizations o
LEFT JOIN lookup.CategoryMobile cm ON o.CategoryMobileId = cm.Id
WHERE o.AllowMobileView = 1
AND o.OrganizationStatusId = 30
ORDER BY cm.SortOrder, o.OrganizationName
-- Active mobile app actions
SELECT
id,
type,
title,
loginType,
enabled,
[order]
FROM MobileAppActions
WHERE enabled = 1
ORDER BY [order]
| Field | Type | Description | Usage Notes |
|---|---|---|---|
PictureId |
int | Primary key | Picture ID |
ThumbUrl |
nvarchar | Thumbnail URL | Small preview |
SmallUrl |
nvarchar | Small size URL | Mobile size |
MediumUrl |
nvarchar | Medium size URL | Standard display |
LargeUrl |
nvarchar | Large size URL | Full resolution |
Note: When a picture is uploaded, it takes up to 15 minutes for ThumbUrl, SmallUrl, and MediumUrl to be generated
-- People with pictures
SELECT TOP 1000
p.Name,
pic.PictureId,
pic.ThumbUrl,
pic.MediumUrl,
pic.LargeUrl,
CASE
WHEN pic.ThumbUrl IS NOT NULL THEN 'Processed'
ELSE 'Processing'
END as Status
FROM People p
JOIN Picture pic ON p.PictureId = pic.PictureId
WHERE p.IsDeceased = 0 AND p.ArchivedFlag = 0
ORDER BY p.Name
| Field | Type | Description | Usage Notes |
|---|---|---|---|
UserId |
int | Primary key | User account ID |
PeopleId |
int | Person link | → People.PeopleId |
Username |
nvarchar | Login name | Unique username |
Name |
nvarchar | Display name | User's full name |
EmailAddress |
nvarchar | Login email | Authentication email |
IsApproved |
bit | Account active | Can login |
LastLoginDate |
datetime | Last login | Most recent access |
MustChangePassword |
bit | Password reset | Force change |
MFAEnabled |
bit | 2FA enabled | Multi-factor auth |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
RoleId |
int | Primary key | Role ID |
RoleName |
nvarchar | Role name | Admin, Finance, etc. |
-- Active users with roles
SELECT TOP 500
u.UserId,
u.Username,
u.Name,
u.EmailAddress,
u.LastLoginDate,
DATEDIFF(day, u.LastLoginDate, GETDATE()) as DaysSinceLogin,
u.MFAEnabled,
STRING_AGG(r.RoleName, ', ') as Roles
FROM Users u
LEFT JOIN UserRole ur ON u.UserId = ur.UserId
LEFT JOIN Roles r ON ur.RoleId = r.RoleId
WHERE u.IsApproved = 1
GROUP BY u.UserId, u.Username, u.Name, u.EmailAddress, u.LastLoginDate, u.MFAEnabled
ORDER BY u.LastLoginDate DESC
-- Users who haven't logged in recently
SELECT TOP 100
u.Name,
u.Username,
u.LastLoginDate,
DATEDIFF(day, u.LastLoginDate, GETDATE()) as DaysInactive
FROM Users u
WHERE u.IsApproved = 1
AND u.LastLoginDate < DATEADD(month, -3, GETDATE())
ORDER BY u.LastLoginDate
| Purpose | Key Fields | Critical for |
|---|---|---|
| Volunteer screening | PeopleId, Status, DateCompleted, ExpirationDate | Children's ministry, youth work |
-- Background check status for volunteers
SELECT
p.Name,
bc.Status,
bc.DateCompleted,
bc.ExpirationDate,
DATEDIFF(day, GETDATE(), bc.ExpirationDate) as DaysUntilExpiry,
STRING_AGG(o.OrganizationName, ', ') as VolunteerRoles
FROM BackgroundChecks bc
JOIN People p ON bc.PeopleId = p.PeopleId
LEFT JOIN OrganizationMembers om ON p.PeopleId = om.PeopleId
LEFT JOIN Organizations o ON om.OrganizationId = o.OrganizationId
WHERE om.MemberTypeId IN (140, 150, 160, 710) -- Leader/volunteer types
AND om.InactiveDate IS NULL
GROUP BY p.Name, bc.Status, bc.DateCompleted, bc.ExpirationDate
ORDER BY bc.ExpirationDate
⚠️ Performance Warning: ActivityLog contains 3.4M+ records. Always use date filters.
| Field | Type | Description | Usage Notes |
|---|---|---|---|
Id |
bigint | Primary key | Activity ID |
ActivityDate |
datetime | When occurred | Timestamp |
UserId |
int | User | → Users.UserId |
ActivityType |
nvarchar(50) | Activity type | Login, Update, etc. |
Machine |
nvarchar(50) | Computer/device | Client info |
Activity |
ntext | Activity details | Full description |
-- Recent user activity (MUST have date filter)
SELECT TOP 100
al.ActivityDate,
u.Name as UserName, -- Changed from 'User' to avoid reserved keyword
al.Activity,
al.Machine,
al.PageUrl
FROM ActivityLog al
LEFT JOIN Users u ON al.UserId = u.UserId
WHERE al.ActivityDate >= DATEADD(day, -7, GETDATE()) -- REQUIRED
ORDER BY al.ActivityDate DESC
-- Activity summary
SELECT
al.Activity,
COUNT(*) as ActivityCount,
COUNT(DISTINCT al.UserId) as UniqueUsers,
MIN(al.ActivityDate) as FirstOccurrence,
MAX(al.ActivityDate) as LastOccurrence
FROM ActivityLog al
WHERE al.ActivityDate >= DATEADD(month, -1, GETDATE()) -- REQUIRED
GROUP BY al.Activity
ORDER BY COUNT(*) DESC
| Field | Type | Description | Usage Notes |
|---|---|---|---|
Id |
int | Primary key | Change ID |
PeopleId |
int | Person changed | → People.PeopleId (NULL for non-person changes) |
FamilyId |
int | Family changed | → Family.FamilyId (NULL for non-family changes) |
UserPeopleId |
int | Who made the change | → People.PeopleId |
Created |
datetime | When changed | Timestamp of change |
Field |
nvarchar | Field changed | Column name that was modified |
Data |
nvarchar | Additional data | Extra context about the change |
Before |
nvarchar | Old value | Previous data |
After |
nvarchar | New value | Updated data |
-- Recent changes to People records
SELECT TOP 100
cl.Created,
p.Name as ChangedBy,
cl.Field,
cl.Before,
cl.After
FROM ChangeLog cl
JOIN People p ON cl.UserPeopleId = p.PeopleId
WHERE cl.PeopleId IS NOT NULL -- Changes to people records
AND cl.Created >= DATEADD(day, -7, GETDATE())
ORDER BY cl.Created DESC
-- Track specific person's history
SELECT
cl.Created,
modifier.Name as ModifiedBy,
cl.Field,
cl.Before,
cl.After
FROM ChangeLog cl
LEFT JOIN People modifier ON cl.UserPeopleId = modifier.PeopleId
WHERE cl.PeopleId = 12345 -- Specific PeopleId
ORDER BY cl.Created DESC
| Field | Type | Description | Usage Notes |
|---|---|---|---|
BundleHeaderId |
int | Primary key | Batch ID |
ContributionDate |
datetime | Contribution date | When received |
DepositDate |
datetime | Bank deposit | When deposited |
TotalCash |
money | Cash total | Cash in batch |
TotalChecks |
money | Check total | Checks in batch |
TotalEnvelopes |
int | Envelope count | Number processed |
| Field | Type | Description | Usage Notes |
|---|---|---|---|
BundleDetailId |
int | Primary key | Detail ID |
BundleHeaderId |
int | Batch | → BundleHeader |
ContributionId |
int | Contribution | → Contribution |
CreatedDate |
datetime | Entry date | When entered |
-- Recent batch summary
SELECT TOP 500
bh.BundleHeaderId,
bh.ContributionDate,
bh.DepositDate,
bh.TotalCash + bh.TotalChecks as TotalAmount,
bh.TotalEnvelopes,
COUNT(bd.BundleDetailId) as ContributionCount
FROM BundleHeader bh
LEFT JOIN BundleDetail bd ON bh.BundleHeaderId = bd.BundleHeaderId
WHERE bh.ContributionDate >= DATEADD(month, -1, GETDATE())
GROUP BY bh.BundleHeaderId, bh.ContributionDate, bh.DepositDate,
bh.TotalCash, bh.TotalChecks, bh.TotalEnvelopes
ORDER BY bh.ContributionDate DESC
Transaction is a SQL Server reserved word. You MUST wrap it in brackets in every query or you will get a syntax error. This is the #1 most common mistake with this table.
-- ✅ CORRECT
SELECT * FROM [Transaction] WHERE OrgId = 123
-- ❌ WRONG - syntax error
SELECT * FROM Transaction WHERE OrgId = 123
The Approved column is a BIT (boolean), not a string. Use 0 or 1:
-- ✅ CORRECT
SELECT * FROM [Transaction] WHERE Approved = 1
-- ❌ WRONG - Approved is not a string
SELECT * FROM [Transaction] WHERE Approved = 'Approved'
| Field | Type | Description | Usage Notes |
|---|---|---|---|
Id |
int | Primary key | Transaction ID |
OrgId |
int | Organization ID | Links to Organizations.OrganizationId |
PeopleId |
int | Person making transaction | Links to People.PeopleId |
TransactionDate |
datetime | Date | When occurred |
Amt |
decimal(11,2) | Transaction amount | Total amount |
amtdue |
money | Amount due | Outstanding balance |
PaymentType |
nvarchar(20) | Payment method | Credit, Check, Cash, etc. |
Approved |
bit | Approval status | BIT field! Use 0 or 1, NOT string |
Message |
nvarchar | Transaction notes | Transaction details/description |
Description |
nvarchar | Description | Transaction details |
Important: EnrollmentTransaction is a junction table linking registrations to transactions. Do NOT query it directly for payment status - always JOIN to the [Transaction] table for actual transaction details.
| Field | Type | Description | Usage Notes |
|---|---|---|---|
TransactionId |
int | Links to [Transaction].Id | Foreign key to Transaction table |
TranId |
int | Primary key for OrganizationMembers link | Referenced by OrganizationMembers.TranId |
RegPersonId |
int | Registration person ID | Links to registration record |
Amt |
decimal | Amount for this person | Individual amount (may differ from total) |
TransactionDate |
datetime | When transaction occurred | Used for date range filtering |
-- Correct way to get transaction details with enrollment info
SELECT
p.Name,
o.OrganizationName,
t.Amt,
t.TransactionDate,
t.Approved,
t.PaymentType,
et.Amt as IndividualAmt
FROM [Transaction] t
JOIN EnrollmentTransaction et ON t.Id = et.TransactionId
JOIN People p ON t.PeopleId = p.PeopleId
JOIN Organizations o ON t.OrgId = o.OrganizationId
WHERE t.Approved = 1
AND t.TransactionDate >= DATEADD(month, -3, GETDATE())
ORDER BY t.TransactionDate DESC
-- People with outstanding balances
SELECT TOP 500
p.Name,
SUM(t.amtdue) as TotalDue,
COUNT(*) as OpenTransactions,
MIN(t.TransactionDate) as OldestTransaction,
DATEDIFF(day, MIN(t.TransactionDate), GETDATE()) as DaysOutstanding
FROM TransactionPeople tp
JOIN People p ON tp.PeopleId = p.PeopleId
JOIN [Transaction] t ON tp.Id = t.Id
WHERE t.amtdue > 0
AND p.IsDeceased = 0 AND p.ArchivedFlag = 0
GROUP BY p.PeopleId, p.Name
HAVING SUM(t.amtdue) > 0
ORDER BY SUM(t.amtdue) DESC
This comprehensive guide covers TouchPoint's extensive database architecture, from basic queries to advanced analytics. Remember these key principles:
This guide is maintained by the TouchPoint community. If you discover new patterns, corrections, or useful queries, please contribute back to help other churches maximize their use of TouchPoint.
Contact: For questions or contributions, please open an issue on GitHub or contact the community forum.