Using Crystal Reports and SQL to Create a Custom Report to Help Close the Care Gaps in Your Practice Steve King North Country HealthCare October 2019 Three Objectives:
1. Learn how to upload a report to Centricity, how to use parameters, and how to use formulas 2. Learn how to use SQL Server Management Studio to explore the data in Centricity 3. Create a report that can close the gaps of care in your practice 1. Mammogram 2. Expand to Diabetes
• Session is for Beginner to Immediate Report creators • Assumes you are a CPS 12.X user • And you use clinical content, not CCC • Bonus Content
SDID is a filed that is used to link DOCUMENT (where all EHR transactions start) to the various medical tables in the EHR (e.g. PROBLEM, ORDERS, OBS, ALLERGY, MEDICATE) Translating a EHR timestamp date to a normal date: DATEADD(ss, DOCUMENT.CLINICALDATE / 1000000, ‘01/01/1960’) SQL Keywords used in this presentation NULL: there is no value. It is neither true or false, doesn’t have a value. To do a comparison, must use
[email protected] Employed at North Country for over 9 years North Country has 14 access points throughout northern Arizona and parts of central Arizona Main campus is in Flagstaff, AZ 55,000 patients a year 160,000 visits 80 providers Tools to Develop Reports For Centricity
Crystal Reports XI
I don’t know how to acquire – your VAR should be able to help
Crystal Report can be integrated differently in Centricity – this session uses Crystal Reports as a stand-alone application Microsoft SQL Server Management Studio
Free with your version of SQL Server Centricity Data Dictionary
Download from aethenahealth web site Crystal Report: Objectives
1. Using Parameters
2. Adding parameters to a SQL query
3. Creating formulas to show parameters in Centricity
4. Toggling the footer Upload a Crystal Report Into Centricity
The Main Report
Parameters in Centricity, Parameters and Formulas in Crystal Reports
A footer that toggles visibility Crystal Report: Parameters
Because these parameters are used to build formulas in Crystal Report, the Crystal Report data type may not be the same if this was a stand-alone report
1. STARTDATE and ENDATE– although they will be a date in Centricity, for the report they are strings 2. FACILITY and PROVIDER – although these will be used in Centricity as number, for the report they are strings 3. HIDE_FOOTER: used as a number in both Crystal Reports and Centricity Crystal Report: Formulas
Formulas are used to
1. Display information
2. Change information
3. Store information
For this report we are only displaying the data in the parameters FACILITY_CRITERIA and PROVIDER_CRITERIA
Used to show the facility and the provider
1. Because this report requires to choose a facility this formula is different than one that allows the user to choose one or more facilities
'Facility: ' + {?FACILITY}
Formulas to show all facilities or selected facilities/providers or selected providers
If {?FACILITY} = 'NULL' THEN If {?Provider} = 'NULL' THEN "Facilities: all" “Providers: all" ELSE ELSE "Facilities: " & {?FACILITY} “Providers: " & {?Provider} DATE_CRITERIA
Because we force the user to enter dates, we know that there will be a start date and end date
"Dates from " & {?STARTDATE} & " to " & {?ENDDATE} Toggle a Report Section in Crystal Reports
In Crystal Reports
Create a parameter named HIDE_FOOTER
Field type: number
In the Report footer enter the text you want to show
In Section Expert make sure Suppress (No Drill-Down is unchecked and enter this formula: {?HIDE_FOTTER}=1
This can be done for any section you want to hide Report Design Mode
Toggle between design mode and report mode Centricity: Parameters
To transfer parameters values from Centricity to Crystal Report, usually use text The are different ways use parameters from Centricity to the SQL query
Select Boxes: ?CONTROLNAME.ITEMDATA.U? or ?CONTROLNAME.ITEMDATA?
“U” shows actual numbers
No “U” shows numbers as quoted string
Dates To And FROM: ?CONTROLNAME.DATE1? or ?CONTROLNAME.DATE2?
Check boxes and radio buttons: ?CONTROLNAME.VALUE? Centricity: Parameters As a Filter in a Query
This will let you filter on None, One, or Multiple Providers AND ( (?PROVIDER.ITEMDATA? IS NOT NULL AND a.DoctorID IN (?PROVIDER.ITEMDATA.U?)) OR (?PROVIDER.ITEMDATA? IS NULL) )
Note the use or non-use of “U”
AND ( ('2972' IS NOT NULL AND a.DoctorID IN (2972)) OR ('2972' IS NULL) ) Centricity and Crystal Reports: Tying the Parameters Together
If {?Provider} = 'NULL' THEN “Providers: all" ELSE “Providers: " & {?Provider} Centricity and Report Dates
If your report is date sensitive, always require dates You can use a default statement ISNULL(
You can use a RAISERROR statement if your user enters a wrong date
SQL keyword that raises an error, causing the query to stop
Centricity has no way to capture the error
You get an ID Dispatch error
Full use of the keyword is outside of this discussion IF @StartDate <= '12/31/2018' RAISERROR ('StartDate is too small', 16, 1) Centricity SQL Preview Button
Lets you see the SQL in the report Don’t Forget to Assign Security!
Right click on the Report Assign by Group, not by User Always uncheck the Delete Column Checking Edit will allow user/group to save criteria report
Carefully think about allowing that – otherwise your report folders will get cluttered. Exploring Data in Your Database OBSTERMs
OBSTERMs (or Observation Terms) are what determines what is shown in the Flowsheet The actual observation lives in the table OBS, or in the view RPTOBS
Key fields are OBSDATE, OBSVALUE, PID, and XID OBSHEAD is a table that contains observation codes and descriptions for OBSTERM
Key fields are HDID, NAME, and DESCRIPTION OBSTERMs to Close a Gap: Mammogram
Three ways to determine if a mammogram was performed:
1. OBSTERMS
2. ICD-10 Codes
3. CPT Codes
What to use depends on your clinical workflow – for this example we’re using OBSTERM How to Find The OBSTERM: Clinical Content
Find forms where the OBSTERM might be used Start a visit with the form End the visit Click “View Clinical Content” How to Find The OBSTERM: Export a Clinical Kit • Administration > System > Export Clinical Kits > Form Component will bring up the Search Screen • Then click Export Clinical Kit, and save it to your computer Clinical Kit: Includes a Lot of Files
• The Microsoft Excel Template file has the OBSTERMS by NAME used in the form • Excel will complain when you open the file – just answer yes • You’ll have to do some detective work to find the possible OBSTERM you’re looking for [This is from the XLT file] /*Function for RUNPROCESS Button Record Mammogram*/ fn fn1151_1503350234_635(){ if (PATIENT._AGEINMONTHS >= 480 and PATIENT._AGEINMONTHS < 600) then fnRecObs_CSF(DOCUMENT.RESULTMAM_1151_1503350234_633,DOCUMENT.DATEMAM_1151_1503350234_634,"MAMMOGRAM","MAMMO DUE",731) else fnRecObs_CSF(DOCUMENT.RESULTMAM_1151_1503350234_633,DOCUMENT.DATEMAM_1151_1503350234_634,"MAMMOGRAM","MAMMO DUE",365) endif View Clinical Content is a lot easier! Finding the OBSTERM HDID
OBSHEAD is a table that stores all the OBSTERMs OBSHEAD.NAME is the field that was returned when you clicked View Clinical Content OBSHEAD.HDID is the field that identifies the OBSTERM in OBS To get OBSHEAD.HDID for MAMMOGRAM run a query on OBSHEAD OR do a Join (Shown on next slide) Verifying Your OBSTERM is Being Used
SET NOCOUNT ON SET NOCOUNT ON OBS is the actual Table; SELECT SELECT RPTOBS is a view COUNT(*) COUNT(*) AS [Count] , oh.Name , oh.Name , oh.Description , oh.Description FROM OBS o FROM RPTOBS o JOIN OBSHEAD oh ON o.HDID = oh.HDID JOIN OBSHEAD oh ON o.HDID = oh.HDID WHERE (oh.Name = 'MAMMOGRAM') WHERE o.HDID = 71 GROUP BY GROUP BY oh.Name oh.Name , oh.Description , oh.Description
That’s a lot of mammograms – Guess we have the correct OBSTERM Verifying Your OBSTERM is Being Used Correctly
SET NOCOUNT ON
DECLARE @StartDate datetime = '08/01/2019' , @EndDate datetime = '08/31/2019'
DECLARE @Lookback date = DATEADD(mm, -14, @EndDate) /* 15 months from end of measurement period. Zero month counts as a month */
SELECT DISTINCT o.OBSVALUE FROM OBS o WHERE (o.HDID = 71) AND (o.OBSDATE >= @Lookback AND o.OBSDATE < DATEADD(d, 1, @EndDate)) Hmm, some of those values don’t look valid Verifying the Correct OBSVALUE
Work with your clinical team Some of the results are obviously not valid:
Declines
Assessment Incomplete After talking to the clinical team, results that are either Normal, Abnormal, or contain the phrase BIRAD are valid mammograms
The OBSVALUE is not case sensitive How to Return the Correct OBSVALUE?
Hold that thought! Will explain when we look at the report query! Developing a Care Gap Report Thinking ahead…
Looking for appointments Looking for patients that meet the denominator for the gaps we are trying to close Numerator include an observation during a measurement period – but do we need to look outside the measurement period? Denominators usually only include medical visits Looking for patients that DON’T meet the numerator for the gaps we are trying to close There could be a lot of appointments, which can impact the performance of Centricity
Limit the time of day when the report is ran?
Limit the data that is being returned? Understanding the Requirements: What is a Medical Visit?
Can use document type (EHR) or CPT code Internally, we decided to use CPT codes
Document types are too “fuzzy”, i.e. what’s an office visit, nursing visit, walk in visit, or urgent care visit?
The table PatientVisitProcs has a column named CPTProcedureCodeQualifierMId. This column should be linked to the MedLists table MedListsId column with a TableName of ProcedureCodeQualifier.
To see the full list use the query "SELECT * FROM MedLists WHERE TableName = 'ProcedureCodeQualifier'"
HCPC - BH HCPC - DEN UDS encounter types! HCPC - EN HCPC - MED HCPC - OP Understanding the Requirements: Defining the Patient’s Last Visit
Going to look for the patient’s last visit, based on an unvoided ticket Visit must be a medical visit How far to look back?
The measure is looking for a year’s worth of data, but our report only looking for small window of time
Lookback period will depend on numerator’s definition Pseudocode For the Query Do housekeeping tasks Create a table to store results of multiple queries Get all patients with upcoming appointments Include demographic information including age and gender Get the patients last medical visits Care Gap 1: Mammogram Determine if patients are in the mammogram denominator Determine if patients are in the mammogram exclusion Determine if patients are in the mammogram numerator Care Gap 2: Diabetes Determine if patients are in the diabetes denominator Determine if patients are in the diabetes numerator Determine if patients are in the diabetes exclusion (we will skip this logic) Return results that include upcoming appointment, facility, provider, and care gaps Understanding the Query: Housekeeping tasks
SET NOCOUNT ON /* Prevents a count of row returning, which if not included will effect Excel reporting */
DECLARE @StartDate datetime = '07/01/2019' , @EndDate datetime = '07/07/2019' Understanding the Query: Store Appointments in a Temporary Table
SQL server has tables, temporary tables, and table variables
Tables: data that lives permanently in the database
CREATE TABLE PatientProfile… Temporary tables: live in the tempdb (temporary) database, are not permanent, have database statistics (topic beyond this presentation)
CREATE TABLE #Results… Table variables: only live the transaction in which they are created
DECLARE @Patients TABLE… Deciding what to use is beyond the scope of this discussion, but I’m going to use a table variable Understanding the Query: Temporary Table to Store Results From Multiple Queries
DECLARE @Patients TABLE ( /* Patient identifiers */ PatientProfileID int /* PM side */ , PID numeric(19) /* EHR side */ /* Basic demographic info to determine denominator */ , Age int , Gender char(1) /* 3 ways to update the last medical visit for the patient: cursor, loop, set based */ , LastVisitCursor date , LastVisitLoop date , LastVisitSet date /* ^ Need the patients last medical visit to determine the numerator */ , MammogramDenom tinyint DEFAULT 0 , MammogramNum tinyint DEFAULT 0 , MammogramExclusion tinyint DEFAULT 0 /* If time permits will also look at Diabetes and A1C If we're out of time the code is commented */ , DiabetesDX tinyint DEFAULT 0 , DiabetesDenomimator tinyint DEFAULT 0 , DiabetesNum tinyint DEFAULT 0 , A1cSDID numeric(19) , A1cObsValue decimal(5,2) NULL ) Understanding the Query: INSERT and SELECT to Get Appointments
Since only inserting selected fields into @Patients need to list what fields we want to insert INSERT INTO @Patients (PatientProfileID, PID, Age, Gender) SELECT DISTINCT a.OwnerID /* For patients, this is PatientProfileID */ , pp.PID /* Get this value from PatientProfile since it doesn't live in table Appointments */ , DATEDIFF (YEAR, pp.birthdate, @EndDate) - CASE WHEN 100 * MONTH(@endDate) + DAY(@endDate) < 100 * MONTH(pp.birthdate) + DAY(pp.birthdate) THEN 1 ELSE 0 END /* Well known solution developed by Steve Kass which accounts for a number of edge cases including leap years. Found on Stackoverflow */ , pp.Sex FROM PatientProfile pp JOIN Appointments a ON pp.PatientProfileID = a.OwnerID WHERE (a.ApptStart >= @StartDate and a.ApptStart < DATEADD(d, 1, @EndDate)) AND (a.ApptKind = 1) AND (ISNULL(a.Canceled,0) = 0) /* Valid appointments only */ AND (a.FacilityId = @FacilityID) /* Limit to a single facility */ Understanding the Query: Getting the Last Visit
Three ways to get the last visit
1. Using a SQL Server cursor – worst practice
2. Using a WHILE loop – okay practice
3. Using a set based query – best practice Understanding the Query: Getting the Last Visit With a Cursor – General Set Up
DECLARE @PID numeric /* value you want to get */ , @PatientProfileID int /* not needed -- just to show that you can return multiple columns for the cursor */ , @LastVisit datetime
DECLARE db_cursor CURSOR FOR SELECT PID, PatientProfileID FROM @Patients
OPEN db_cursor FETCH NEXT FROM db_cursor INTO @PID, @PatientProfileID
WHILE @@FETCH_STATUS = 0 BEGIN /* Do stuff */ FETCH NEXT FROM db_cursor INTO @PID, @PatientPRofileID /* Bring in the next row */ END
CLOSE db_cursor DEALLOCATE db_cursor Understanding the Query: “Do stuff” in the Cursor Loop SET @LastVisit = NULL SELECT @LastVisit = MAX(pv.Visit) /* Max bring back the last visit */ FROM PatientVisit pv JOIN PatientVisitProcs pvp ON pv.PatientVisitID = pvp.PatientVisitID WHERE (ISNULL(pvp.VOided,0) = 0) /* This is the best way to identify unvoided ticket If a ticket has one or more unvoided procedures then it is a valid ticket */ AND (pvp.CPTProcedureCodeQualifierMId = 7343) /* HCPC - Med */ /* This query is using a magic number. If you have to use, create a comment that explains what it is. Your future self will thank you when you do maintenance on the report. */ AND (pv.PatientProfileID = @PatientProfileID) /* The cursor brings values for each row (Patient) in @Patients */ IF @LastVisit IS NOT NULL /* It's possible that a patient won't have a visit, so check for that */ UPDATE @Patients SET LastVisitCursor = @LastVisit WHERE PatientProfileID = @PatientProfileID Understanding the Query: Using a WHILE Loop Pseudocode /* Table variable -- the actual script uses a temp table */ DECLARE @TABLE_NAME TABLE ( ID int IDENTITY /* Unique numeric field in the table – doesn’t have to be an IDENTITY */ , Some fields...); /* variables that store the ID values */ DECLARE @curID int, @nextID int; /* Get the first ID */ SET @nextID = (SELECT MIN(ID) FROM @TABLE_NAME); /* WHILE loop, instead of cursor */ WHILE (1 = 1) BEGIN SET @curID = @nextID; /* Do stuff here */ SET @nextID = NULL; /* not really necessary, but I do it anyway just to be safe */ SET @nextID = (SELECT MIN(ID) FROM @TABLE_NAME WHERE ID > @curID); /* this finds the next ID that is greater than the current id */ IF @nextID IS NULL /* if out of IDs break out of the loop */ BREAK; END Understanding the Query: “Do stuff” in the WHILE Loop
Same code as for a cursor /* Do stuff here */ SET @LastVisit = NULL
SELECT @LastVIsit = MAX(pv.Visit) FROM PatientVisit pv JOIN PatientVisitProcs pvp ON pv.PatientVisitID = pvp.PatientVisitID WHERE (ISNULL(pvp.Voided,0) = 0) AND (pv.PatientPRofileID = @curPatientProfileID) AND (pvp.CPTProcedureCodeQualifierMId = 7343) /* HCPC - Med */
IF @LastVisit IS NOT NULL UPDATE @Patients SET LastVisitLoop = @LastVisit WHERE PatientPRofileID = @curPatientProfileID Understanding the Query: Get Last Visit Using Set Based Logic (JOINs)
SQL is designed around set based, rather than procedural logic UPDATE @Patients SET LastVisitSet = q.LastVisit FROM @Patients p JOIN (SELECT MAX(pv.Visit) AS [LastVisit], pv.PatientProfileID FROM PatientVisit pv JOIN PatientVisitProcs pvp ON pv.PatientVisitID = pvp.PatientVisitID WHERE (ISNULL(pvp.Voided,0) = 0) AND (pvp.CPTProcedureCodeQualifierMId = 7343) /* HCPC - Med */ GROUP BY PatientProfileID) q ON p.PatientProfileID = q.PatientProfileID Verifying the Results for Last Visit
Should be the same – and they are! SELECT PID, LastVisitCursor, LastVisitLoop, LastVisitSet FROM @Patients ORDER BY PID
What’s the NULL doing in there? • Must be a new patient! Care Gap 1: Women 50 – 74 With an Upcoming Appointment That Need a Mammogram
Breast Cancer Screening Ages 50-74 (NQF 2372) Endorser: NQF 2372 / CMS eCQM 125v6 Percentage of women 50-74 years of age who had a mammogram to screen for breast cancer. Numerator: Women with one or more mammograms during the measurement period or the 15 months prior to the measurement period. [Mammogram in the last 27 months] Denominator: Women 51-74 years of age with a visit during the measurement period. [Females Age 52-74 at the end of the measurement period] Qualifying visit (see Technical Specifications) in the measurement period Exclusions: Bilateral mastectomy; 2 unilateral mastectomies; Exclude patients in hospice care Understanding the Query: Mammogram Denominator
UPDATE @Patients SET MammogramDenom = 1 WHERE (Age BETWEEN 52 AND 74) AND (Gender = 'F') AND (LastVisitSet IS NOT NULL) AND (LastVisitSet >= @Lookback AND LastVisitSet < DATEADD(d, 1, @EndDate)) Understanding the Query: Updating the Mammogram Numerator Based on Set
/* Updating numerator based on set */ UPDATE @Patients SET MammogramNum = q.MAMMOGRAM FROM @Patients p JOIN (SELECT 1 AS [Mammogram], PID FROM OBS o WHERE (o.HDID = 71) /* Mammogram */ /* acceptable values */ AND (o.OBSVALUE LIKE 'BiRad%' OR o.OBSVALUE LIKE 'Normal%' OR o.OBSVALUE LIKE 'Benign%') AND (o.OBSDATE >= @Lookback AND o.OBSDATE < DATEADD(d, 1, @EndDate)) ) q ON p.PID = q.PID Understanding the Query: WHERE Clause For Mammogram Numerator Using the Correct OBSVALUE
WHERE (o.HDID = 71) /* Mammogram */ /* acceptable values */ AND (o.OBSVALUE LIKE 'BiRad%' OR o.OBSVALUE LIKE 'Normal%' OR o.OBSVALUE LIKE 'Benign%') Not happy with 'Normal%' but clinical team said it was ok Understanding the Query: Mammogram Exclusions
Do we use OBSTERMS, ICD-10 Codes, or CPT Codes? Won’t look at CPT codes because most FQHC/CHCs don’t perform mastectomies In our clinical content OBSTERMS can be Mastectomies or Past Surgical History Understanding the Query: Mammogram OBSTERM Exclusion
SELECT COUNT(*), oh.Name, oh.DESCRIPTION FROM OBS o JOIN OBSHEAD oh ON o.HDID = oh.HDID WHERE oh.DESCRIPTION LIKE '%Mastec%' GROUP BY oh.Name, oh.DESCRIPTION Doesn’t return a lot of results: Count Name DESCRIPTION 93 BILAT MAST bilateral mastectomy, hx of 88 MASTECTRTHX mastectomy, right breast, hx of 5 MASTLNDRTHX mastectomy with lymph node disection, right breast, hx of 1360 MASTECTOMY mastectomy, hx of 6 MASTLNDLTHX mastectomy with lymph node disection, left breast, hx of 104 MASTECTLTHX mastectomy, left breast, hx of Understanding the Query: ICD-10 Code Mammogram Exclusion
SELECT COUNT(*), md.Code, md.ShortDescription FROM PROBLEM p JOIN MasterDiagnosis md ON p.ICD10MasterDiagnosisId = md.MasterDiagnosisId WHERE (md.Code IN ('Z90.10','Z90.11','Z90.12','Z90.13')) GROUP BY md.Code, md.ShortDescription Doesn’t return a lot of results Count Code ShortDescription 24 Z90.13 Acquired absence of bilateral breasts and nipples 59 Z90.12 Acquired absence of left breast and nipple 14 Z90.11 Acquired absence of right breast and nipple 82 Z90.10 Acquired absence of unspecified breast and nipple Understanding the Query: Final Mammogram Exclusions
Use both!
Discuss with clinical team if there are other ways that a mastectomy is documented in the EHR
Not showing the code except for ICD-10 codes UPDATE @Patients SET MammogramExclusion = q.Exclusion FROM @Patients p JOIN (SELECT 1 as Exclusion, PID FROM PROBLEM prb JOIN MasterDiagnosis md ON prb.ICD10MasterDiagnosisId = md.MasterDiagnosisId WHERE (md.Code IN ('Z90.10','Z90.11','Z90.12')) /* Unilateral, right, left mastectomy */ AND (prb.XID = 1e18) GROUP BY PID HAVING COUNT(*) > 1 /* Right + left mastectomy = full mastectomy, 2 unilateral is assumed to be full mastectomy */ ) q ON p.PID = q.PID Understanding the Query: SELECT Mammogram Gaps in Care Using CASE
To verify out of SSMS :
Return all fields from @Patients so you can verify the data in Centricity Use a CASE statement to determine if there is a gap in care CASE WHEN p.MammogramDenom = 1 AND p.MammogramExclusion = 0 AND p.MammogramNum = 0 THEN 'X' ELSE NULL END AS [MammogramGap] Care Gap 2: Diabetics with Poor A1c Control
Diabetes A1c > 9 or Untested (NQF 0059) Diabetes Endorser: NQF 0059 / CMS eCQM 122v7 Percentage of patients 18-75 years of age with diabetes who had hemoglobin A1c > 9.0% during the measurement period. Numerator: Patients whose most recent HbA1c level (performed during the measurement period or last 12 months) is >9.0% or No A1c in the last 12 months Denominator: Patients 18-75 years of age with diabetes with a visit during the measurement period. Exclusions: Hospice Care overlapping the measurement period Understanding the Query: Finding Diabetics
Going to skip explaining most of the logic, will show a pitfall instead Find all patients with diabetes: UPDATE @Patients SET DiabetesDX = q.Diabetes FROM @Patients p JOIN (SELECT 1 AS [Diabetes], PID FROM PROBLEM prb JOIN MasterDiagnosis md ON prb.ICD10MasterDiagnosisId = md.MasterDiagnosisId WHERE (md.Code LIKE 'E10.%' OR md.Code LIKE 'E11.%') /* Type 1, Type 2 */ AND (prb.XID = 1e18) AND (prb.STOPDATE > GETDATE()) ) q ON p.PID = q.PID Understanding the Query: Updating the Diabetic Denominator
Reset @Lookback to 1 year See if their last visit with within the lookback period AND in the proper age range SET @Lookback = DATEADD(mm, -11, @EndDate)
UPDATE @Patients SET DiabetesDenomimator = 1 FROM @Patients p WHERE (DiabetesDx = 1) AND (p.Age BETWEEN 18 AND 75) AND (p.LastVisitSet >= @Lookback AND p.LastVisitSet < DATEADD(d, 1, @EndDate)) Understanding the Query: Getting the Last A1c Lab Value The WRONG Way (OBSVALUE)
UPDATE @Patients SET A1cOBSValue = q.OBSVALUE FROM @Patients p JOIN ( SELECT MAX(o.SDID) AS [MaxSDID], o.OBSVALUE, o.PID /* MAX(o.SDID) should be the last value */ FROM OBS o WHERE (o.HDID = 28) /* HGBA1C */ AND (o.OBSDATE >= @Lookback AND o.OBSDATE < DATEADD(d, 1, @EndDate)) GROUP BY o.OBSVALUE, o.PID) q ON p.PID = q.PID /* Because a patient can have different OBSVALUE, the MAX(SDID) brings all of those back We want the most recent obsvalue */ Understanding the Query: Getting the Last A1C Lab Value The RIGHT Way [MAX(SDID)]
UPDATE @Patients SET A1cSDID = q.SDID FROM @Patients p JOIN (SELECT MAX(o.SDID) AS [SDID], PID FROM OBS o WHERE (o.HDID = 28) /* HGBA1c */ AND (o.OBSDATE >= @Lookback AND o.OBSDATE < DATEADD(d, 1, @EndDate)) GROUP BY o.PID) q ON p.PID = q.PID /* Look for the last document that had a HGBA1c value */ Understanding the Query: Getting the Last OBSVALUE for the A1c Lab Result
UPDATE @Patients SET A1cObsValue = TRY_PARSE(o.OBSVALUE AS decimal(5,2)) /* TRY_PARSE is only for SQL Server 2012 or greater Possible to have OBSVALUE that aren’t numeric */ FROM @Patients p JOIN OBS o ON p.A1cSDID = o.SDID WHERE (o.HDID = 28) /* HGBA1c */ Understanding the Query: Updating the Diabetic Numerator
UPDATE @Patients SET DiabetesNum = 1 WHERE (A1cObsValue < 9.0) AND (LastVisitSet < @LastVisit) AND (DiabetesDenomimator = 1) Understanding the Query: SELECT Diabetics Gaps in Care Using CASE
CASE WHEN p.DiabetesDenomimator = 1 AND p.DiabetesNum = 0 THEN 'X' ELSE NULL END AS [DiabetesGap] We found patients in the numerator, want patients that AREN’T in then numerator Understanding the Query: Final SELECT for the Report – There’s a Design Problem!
SELECT p.* ,[Appointment Info], [Provider],[Facility] FROM @Patients p JOIN PatientProfile pp ON p.PatientProfileID = pp.PatientProfileID JOIN Appointments a ON pp.PatientProfileID = a.OwnerId /* Oops, have to join to appointments */ JOIN DoctorFacility doc ON a.DoctorId = doc.DoctorFacilityId JOIN DoctorFacility fac ON a.FacilityId = fac.DoctorFacilityId WHERE (a.FacilityID = @FacilityID) /* Oops, have to filter on facility Without this filter, appointments for facilities are returned which is not what we want */ Final SELECT: Fixing the “Oops”
Should have added a field in @Patient that would include AppointmentID Sometimes it takes several tries to get what you want! Questions?
Steve King [email protected] Github: https://github.com/gophersounds/CHUG2019